GithubHelp home page GithubHelp logo

2021-04-marginswap-findings's Introduction

marginswap-results

2021-04-marginswap-findings's People

Contributors

code423n4 avatar sockdrawermoney avatar c4-staff avatar joshuashort avatar

Stargazers

ze avatar

Watchers

Gabriel Pickard avatar James Cloos avatar Ashok avatar  avatar

2021-04-marginswap-findings's Issues

[INFO] Misleading revert messages

Vulnerability details

This is FYI, not a real issue as you have expressed your interest in minor improvement suggestions (not security or gas related):

There are misleading revert messages, for example: "Calling contract not authorized to deposit" on functions like registerUnwind or registerCloseAccount.

Eth address

0x523B5b2Cc58A818667C22c862930B141f85d49DD

Handle

paulius.eth

Email address

[email protected]

Isolated margin contracts declare but do not set the value of liquidationThresholdPercent

Email address

[email protected]

Handle

paulius.eth

Eth address

0x523B5b2Cc58A818667C22c862930B141f85d49DD

Vulnerability details

CrossMarginTrading sets value of liquidationThresholdPercent in the constructor:
liquidationThresholdPercent = 110;
Isolated margin contracts declare but do not set the value of liquidationThresholdPercent.

Recommended mitigation steps

Set the initial value for the liquidationThresholdPercent in Isolated margin contracts.

Impact

This makes function belowMaintenanceThreshold to always return true unless a value is set via function setLiquidationThresholdPercent. Comments indicate that the value should also be set to 110:

// The following should hold:
// holdings / loan >= 1.1
// => holdings >= loan * 1.1

Missing checks if pairs equal tokens

Email address

[email protected]

Handle

@cmichelio

Eth address

0x6823636c2462cfdcD8d33fE53fBCD0EdbE2752ad

Vulnerability details

The UniswapStyleLib.getAmountsOut, PriceAware.setLiquidationPath (and others) don't check that path.length + 1 == tokens.length which should always hold true.
Also, it does not check that the tokens actually match the pair.

Impact

It's easy to set faulty liquidation paths which then end up reverting the liquidation transactions.

Recommended mitigation steps

Add the missing checks.

Missing `fromToken != toToken` check in `MarginRouter.crossSwapExactTokensForTokens`/`MarginRouter.crossSwapTokensForExactTokens`

Email address

[email protected]

Handle

@cmichelio

Eth address

0x6823636c2462cfdcD8d33fE53fBCD0EdbE2752ad

Vulnerability details

Attacker calls MarginRouter.crossSwapExactTokensForTokens with a fake pair and the same token[0] == tokne[1].
crossSwapExactTokensForTokens(1000 WETH, 0, [ATTACKER_CONTRACT], [WETH, WETH]). When the amounts are computed by the amounts = UniswapStyleLib.getAmountsOut(amountIn - fees, pairs, tokens); call, the attacker contract returns fake reserves that yield 0 output
When _swapExactT4T is called, the funds are sent to the fake contract and doing nothing passes all checks in _swap call that follows because the startingBalance is stored after the initial Fund withdraw to the pair.

function _swapExactT4T() {
  // withdraw happens here
    Fund(fund()).withdraw(tokens[0], pairs[0], amounts[0]);
    _swap(amounts, pairs, tokens, fund());
}

function _swap() {
  uint256 startingBalance = IERC20(outToken).balanceOf(_to);
  uint256 endingBalance = IERC20(outToken).balanceOf(_to);
  // passes as startingBalance == endingBalance + 0
  require(
      endingBalance >= startingBalance + amounts[amounts.length - 1],
      "Defective AMM route; balances don't match"
  );
}

Impact

The full impact is not yet known as registerTrade could still fail when subtracting the inAmount and adding 0 outAmount.
At least, this attack is similar to a withdrawal which is supposed to only occur after a certain coolingOffPeriod has passed, but this time-lock is circumvented with this attack.

Recommended mitigation steps

Move the fund withdrawal to the first pair after the startingBalance assignment.
Check fromToken != toToken as cyclical trades (arbitrages) are likely not what margin traders are after.
Consider if the same check is required for registerTradeAndBorrow / adjustAmounts functions.

lastUpdatedDay not initialized

Email address

[email protected]

Handle

gpersoon

Eth address

gpersoon.eth

Vulnerability details

The variable lastUpdatedDay in IncentiveDistribution.sol is not (properly) initialized.
This means the function updateDayTotals will end up in a very large loop which will lead to an out of gas error.
Even if the loop would end, the variable currentDailyDistribution would be updated very often.
Thus updateDayTotals cannot be performed

Impact

The entire IncentiveDistribution does not work.
If the loop would stop, the variable currentDailyDistribution is not accurate, resulting in a far lower incentive distribution than expected.

Recommended mitigation steps

Initialize lastUpdatedDay with something like block.timestamp / (1 days)

Proof of concept

uint256 lastUpdatedDay; # ==> lastUpdatedDay = 0

#When the function updateDayTotals is called:
uint256 public nowDay = block.timestamp / (1 days); #==> ~ 18721
uint256 dayDiff = nowDay - lastUpdatedDay; #==> 18721-0 = 18721

for (uint256 i = 0; i < dayDiff; i++) { # very long loop (18721)
currentDailyDistribution = ....
}
#will result in an out of gas error

No function for TOKEN_ADMIN in RoleAware.sol

Email address

[email protected]

Handle

gpersoon

Eth address

gpersoon.eth

Vulnerability details

This is a minor suggestion.

RoleAware.sol contains functions for most of the constants. However the one exception is TOKEN_ADMIN.

Impact

The code is slightly longer than necessary.

Recommended mitigation steps

Remove the constant TOKEN_ADMIN, or provide a comment why it isn't used.

Re-entrancy bug in `MarginRouter.crossSwapTokensForExactTokens` allows inflating balance

Email address

[email protected]

Handle

@cmichelio

Eth address

0x6823636c2462cfdcD8d33fE53fBCD0EdbE2752ad

Vulnerability details

One can call the MarginRouter.crossSwapExactTokensForTokens function first with a fake contract disguised as a token pair:
crossSwapExactTokensForTokens(0.0001 WETH, 0, [ATTACKER_CONTRACT], [WETH, WBTC]). When the amounts are computed by the amounts = UniswapStyleLib.getAmountsOut(amountIn - fees, pairs, tokens); call, the attacker contract returns fake reserves that yield 1 WBTC for the tiny input.
The resulting amount is credited through registerTrade.
Afterwards, _swapExactT4T([0.0001 WETH, 1 WBTC], 0, [ATTACKER_CONTRACT], [WETH, WBTC]) is called with the fake pair and token amounts.
At some point _swap is called, the starting balance is stored in startingBalance, and the attacker contract call allows a re-entrancy:

pair.swap(0.0001 WETH, 1 WBTC, FUND, new bytes(0)); // can re-enter here

From the ATTACKER_CONTRACT we re-enter the MarginRouter.crossSwapExactTokensForTokens(30 WETH, 0, WETH_WBTC_PAIR, [WETH, WBTC]) function with the actual WETH <> WBTC pair contract.
All checks pass, the FUND receives the actual amount, the outer _swap continues execution after the re-entrancy and the endingBalance >= startingBalance + amounts[amounts.length - 1] check passes as well because the inner swap successfully deposited these funds.
We end up doing 1 real trade but being credited twice the output amount.

Impact

This allows someone to be credited multiples of the actual swap result. This can be repeated many times and finally, all tokens can be stolen.

Recommended mitigation steps

Add re-entrancy guards (from OpenZeppelin) to all external functions of MarginRouter.

There might be several attack vectors of this function as the attacker controls many parameters.
The idea of first doing an estimation with UniswapStyleLib.getAmountsOut(amountIn - fees, pairs, tokens) and updating the user with these estimated amounts, before doing the actual trade, feels quite vulnerable to me.
Consider removing the estimation and only doing the actual trade first, then calling registerTrade with the actual trade amounts returned.

[INFO] setUpdateMaxPegAmount and setUpdateMinPegAmount do not check boundaries

Email address

[email protected]

Handle

paulius.eth

Eth address

0x523B5b2Cc58A818667C22c862930B141f85d49DD

Vulnerability details

This is FYI, not a real issue as you have expressed your interest in minor improvement suggestions (not security or gas related):

setUpdateMaxPegAmount should check that amount >= UPDATE_MIN_PEG_AMOUNT and setUpdateMinPegAmount should check that amount <= UPDATE_MAX_PEG_AMOUNT. But only owner can change these values so no real issue.

Rewards cannot be withdrawn

Email address

[email protected]

Handle

@cmichelio

Eth address

0x6823636c2462cfdcD8d33fE53fBCD0EdbE2752ad

Vulnerability details

The rewards for a recipient in IncentiveDistribution.sol are stored in the storage mapping indexed by recipient accruedReward[recipient] and the recipient is the actual margin trader account, see updateAccruedReward.

These rewards are supposed to be withdrawn through the withdrawReward function but msg.sender is used here instead of a recipient (withdrawer) parameter.
However, msg.sender is enforced to be the incentive reporter and can therefore not be the margin trader.

Impact

Nobody can withdraw the rewards.

Recommended mitigation steps

Remove the isIncentiveReporter(msg.sender) check from withdrawReward function.

Function parameter named timestamp

Email address

[email protected]

Handle

gpersoon

Eth address

gpersoon.eth

Vulnerability details

This is a minor suggestion.

The function viewCumulativeYieldFP in HourlyBondSubscrptionLending.sol has a parameter named timestamp.

Impact

As there is also an inbuilt variable block.timestamp this could be confusing.

Recommended mitigation steps

Rename the parameter timestamp to a slightly different name.

[INFO] Useless overflow comments

Email address

[email protected]

Handle

paulius.eth

Eth address

0x523B5b2Cc58A818667C22c862930B141f85d49DD

Vulnerability details

This is FYI, not a real issue as you have expressed your interest in minor improvement suggestions (not security or gas related):

I think this comment is useless when Solidity 0.8 is used (protection from overflow by default):

// no overflow because depositAmount >= extinguishableDebt
uint256 addedHolding = depositAmount - extinguishableDebt;

there are more such comments, for example:
/// won't overflow
borrowAmount = inAmount - sellAmount;

Different solidity version in UniswapStyleLib.sol

Email address

[email protected]

Handle

gpersoon

Eth address

gpersoon.eth

Vulnerability details

The solidity version in UniswapStyleLib.sol (>=0.5.0) is different than the solidity version in the other contracts (e.g. ^0.8.0)
Also math actions are present in the functions getAmountOut and getAmountIn that could easily lead to an underflow or division by 0; (note safemath is not used).
Note: In solidity 0.8.0 safemath like protections are default.

Impact

The impact is low because UniswapStyleLib is a library and the solidity version of the contract that uses the library is used (e.g. ^0.8.0), which has safemath like protections.
It is cleaner to have the same solidity version everywhere.

Proof of concept

getAmountIn(3,1,1000) would give division by 0
getAmountIn(1,1,1) will underflow denominator

Recommended mitigation steps

Use the same solidity version everywhere

Wrong liquidation logic

Eth address

0x6823636c2462cfdcD8d33fE53fBCD0EdbE2752ad

Vulnerability details

The belowMaintenanceThreshold function decides if a trader can be liquidated:

function belowMaintenanceThreshold(CrossMarginAccount storage account)
    internal
    returns (bool)
{
    uint256 loan = loanInPeg(account, true);
    uint256 holdings = holdingsInPeg(account, true);
    // The following should hold:
    // holdings / loan >= 1.1
    // =>
    return 100 * holdings >= liquidationThresholdPercent * loan;
}

The inequality in the last equation is wrong because it says the higher the holdings (margin + loan) compared to the loan, the higher the chance of being liquidated.
The inverse equality was probably intended return 100 * holdings <= liquidationThresholdPercent * loan;.

Impact

Users that shouldn't be liquidated can be liquidated, and users that should be liquidated cannot get liquidated.

Recommended mitigation steps

Fix the equation.

Email address

[email protected]

Handle

@cmichelio

runtime > 1 hours error message discrepancy

Email address

[email protected]

Handle

paulius.eth

Eth address

0x523B5b2Cc58A818667C22c862930B141f85d49DD

Vulnerability details

Here, the revert message says that the value needs to be at least 1 hour, however, the code allows value only above the 1 hour (> instead of >=):
require(runtime > 1 hours, "Min runtime needs to be at least 1 hour");

Impact

no impact on security, just a discrepancy between the check and message.

Recommended mitigation steps

Replace > with >= or update the error message to reflect that.

Natspec comments not used in a consistent way

Email address

[email protected]

Handle

gpersoon

Eth address

gpersoon.eth

Vulnerability details

This is a minor suggestion.

The comments do not comply perfectly to the natspec specification.
Too many or too few /'s

Proof of concept

Here are a few examples:
MarginRouter.sol: // @dev internal helper swapping ...
MarginRouter.sol: //// @dev external function ...
MarginRouter.sol: /// about a trade

There should be exactly three /'s before an @.. keyword
If there is no @.. keyword then there should be two /'s

Tools used

grep " // @" *
grep "//// @" *
grep "/// [^@]" *

Recommended mitigation steps

Check and update the comments to comply with the natspec comment
Note in the latest solidity version you can also use
@Custom:...
everywhere within the source

Impact

The documentation generated using the natspec lines might not be accurate.

PriceAware uses prices from getAmountsOut

Vulnerability details

getPriceFromAMM relies on values returned from getAmountsOut which can be manipulated (e.g. with the large capital or the help of flash loans). The impact is reduced with UPDATE_MIN_PEG_AMOUNT and UPDATE_MAX_PEG_AMOUNT, however, it is not entirely eliminated.

Impact

Email address

[email protected]

Handle

paulius.eth

Eth address

0x523B5b2Cc58A818667C22c862930B141f85d49DD

Recommended mitigation steps

Uniswap v2 recommends using their TWAP oracle: https://uniswap.org/docs/v2/core-concepts/oracles/

function crossWithdrawETH does not emit withdraw event

Email address

[email protected]

Handle

paulius.eth

Eth address

0x523B5b2Cc58A818667C22c862930B141f85d49DD

Vulnerability details

contract MarginRouter, function crossWithdrawETH does not emit withdraw event:
emit CrossWithdraw(msg.sender, WETH, withdrawAmount);

Impact

no impact on security.

Recommended mitigation steps

emit the expected event

An erroneous constructor's argument could block the withdrawReward

Email address

[email protected]

Handle

s1m0

Eth address

0x9b3E9e3E4a174d59279FC7cd268e035992412384

Vulnerability details

The constructor of IncentiveDistribution https://github.com/code-423n4/marginswap/blob/main/contracts/IncentiveDistribution.sol#L32
take as argument the address of MFI token but it doesn't check that is != address(0).
Not worth an issue alone but IncentiveDistribution imports IERC20.sol and it never use it.

Impact

In case the address(0) is passed as arguement the withdrawReward woul fail https://github.com/code-423n4/marginswap/blob/main/contracts/IncentiveDistribution.sol#L261 and due to the fact that
MFI is immutable the only solution would be to redeploy the contract meanwhile losing trust from the users.

Proof of concept

Deploy IncentiveDistribution with 0 as _MFI argument and then call withdrawReward.

Tools used

Manual analysis

Recommended mitigation steps

Check _MFI != address(0)

No entry checks in crossSwap[Exact]TokensFor[Exact]Tokens

Email address

[email protected]

Handle

gpersoon

Eth address

gpersoon.eth

Vulnerability details

The functions crossSwapTokensForExactTokens and crossSwapExactTokensForTokens of MarginRouter.sol do not check who is calling the function.
They also do not check the contents of pairs and tokens
They also do not check if the size of pairs and tokens is the same

registerTradeAndBorrow within registerTrade does seem to do an entry check
(require(isMarginTrader(msg.sender)...)
however as this is an external function msg.sender is the address of MarginRouter.sol, which will verify ok.

Impact

Calling these functions allow the caller to trade on behalf of marginswap, which could result in losing funds.
It's possible to construct all parameters to circumvent the checks.
Also the "pairs" can be fully specified; they are contract addresses that are called from getAmountsIn / getAmountsOut and from pair.swap.
This way you can call arbitrary (self constructed) code, which can reentrantly call the marginswap code.

Proof of concept

Based on source code review.
A real attack requires the deployed code to be able to construct the right values.

Tools used

remix

Recommended mitigation steps

Limit who can call the functions
Perhaps whitelist contents of pairs and tokens
Check the size of pairs and tokens is the same

Multisig wallets can't be used for liquidate

Email address

[email protected]

Handle

gpersoon

Eth address

gpersoon.eth

Vulnerability details

The function liquidate, which is defined in both
CrossMarginLiquidation.sol and IsolatedMarginLiquidation.sol, includes the modifier noIntermediary.
This modifier prevents the use of Multisig wallets.

Impact

If the maintainer happens to use a multisig wallet he might not experience any issues until he tries to call the function liquidate. At that moment he can't successfully call the function.

Recommended mitigation steps

Verify if the prevention to use multisig wallets is intentional. In that case add a comment to the liquidate functions.
If it is not intentional update the code so multisigs wallets can be supported.

Users are credited more tokens when paying back debt with `registerTradeAndBorrow`

Email address

[email protected]

Handle

@cmichelio

Eth address

0x6823636c2462cfdcD8d33fE53fBCD0EdbE2752ad

Vulnerability details

The registerTradeAndBorrow is called with the results of a trade (inAmount, outAmount). It first tries to pay back any debt with the outAmount.
However, the full outAmount is credited to the user again as a deposit in the adjustAmounts(account, tokenFrom, tokenTo, sellAmount, outAmount); call.

Impact

As the user pays back their debt and is credited the same amount again, they are essentially credited twice the outAmount, making a profit of one outAmount.
This can be withdrawn and the process can be repeated until the funds are empty.

Recommended mitigation steps

In the adjustAmounts call, it should only credit outAmount - extinguishableDebt as a deposit like in registerDeposit.
The registerDeposit function correctly handles this case.

Todo's left in code

Email address

[email protected]

Handle

gpersoon

Eth address

gpersoon.eth

Vulnerability details

This is a minor suggestion.

Several TODO's are left in the code:
IsolatedMarginAccounts.sol: // TODO check if underflow?
IsolatedMarginAccounts.sol: // TODO TELL LENDING
IsolatedMarginLiquidation.sol: // TODO pay off / extinguish that loan
Lending.sol:// TODO activate bonds for lending
Lending.sol:// TODO disburse token if isolated bond issuer
MarginRouter.sol: // TODO minimum trade?

Impact

TODO usually mean something still have to be checked of done. This could lead to vulnerabilities if not verified.

Tools used

grep

Recommended mitigation steps

Check the TODO's and fix if necessary. Remove them afterwards

[INFO] Code duplication in viewCurrentMaintenanceStaker

Email address

[email protected]

Handle

paulius.eth

Eth address

0x523B5b2Cc58A818667C22c862930B141f85d49DD

Vulnerability details

This is FYI, not a real issue as you have expressed your interest in minor improvement suggestions (not security or gas related):

This code has too much duplication:
if (maintenanceStakePerBlock > currentStake) {
// skip
staker = nextMaintenanceStaker[staker];
currentStake = getMaintenanceStakerStake(staker);
} else {
startBlock += currentStake / maintenanceStakePerBlock;
staker = nextMaintenanceStaker[staker];
currentStake = getMaintenanceStakerStake(staker);
}
and can be refactored to:

if (maintenanceStakePerBlock <= currentStake) {
startBlock += currentStake / maintenanceStakePerBlock;
}
staker = nextMaintenanceStaker[staker];
currentStake = getMaintenanceStakerStake(staker);

Several function have no entry check

Email address

[email protected]

Handle

gpersoon

Eth address

gpersoon.eth

Vulnerability details

The following functions have no entry check or a trivial entry check:
withdrawHourlyBond Lending.sol
closeHourlyBondAccount Lending.sol
haircut Lending.sol
addDelegate(own adress...) Admin.sol
removeDelegate(own adress...) Admin.sol
depositStake Admin.sol
disburseLiqStakeAttacks CrossMarginLiquidation.sol
disburseLiqStakeAttacks IsolatedMarginLiquidation.sol
getCurrentPriceInPeg PriceAware.sol

Impact

By manipulating the input values (for example extremely large values)
you might be able to disturb the internal administration of the contract, thus perhaps locking function or giving wrong rates.

note: function haircut is trivial so hardly any risk

Recommended mitigation steps

Check the functions to see if they are completely risk free and add entry checks if they are not.
Add a comment to notify the function is meant to be called by everyone.

Proof of concept

Based on source code review.
A real attack requires the deployed code to be able to construct the right values.

The bug submissions are accessible

Email address

[email protected]

Handle

gpersoon

Eth address

0x8e2A89fF2F45ed7f8C8506f846200D671e2f176f

Vulnerability details

The github token can be retrieved from the https://c4-marginswap.netlify.app/ website

Proof of concept

In the source of the website (createIssue.js) you can see github is directly accessed from the website.
With Fiddler you can search for GITHUB and see:
REACT_APP_MAILGUN_DOMAIN:"mg.code423n4.com",
REACT_APP_MAILGUN_KEY:"a9763c0878cd90413bf11615456692e7-b6d086a8-be1c8bad",
REACT_APP_GITHUB_TOKEN:"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"

With Fiddler you can search for code-423n4 and see:
"owner":"code-423n4",
"repo":"marginswap-results"

With the GITHUB token you can retrieve the issues:
curl -H "Authorization: token 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" https://api.github.com/repos/code-423n4/marginswap-results/issues

This shows:
"body": "# Email address\n\[email protected]\n\n\n# Handle\n\nadamavenir\n\n\n# Eth address\n\n123123123\n\n\n# Vulnerability details\n\nSome details:\n\n\ndetails(schemtails)\n\n\n\n# Impact\n\nBrace for it!\n\n\n# Proof of concept\n\n- proof\n- of \n- concept\n\n\n# Tools used\n\nI used no tools except this form and my BARE HANDS!\n\n\n# Recommended mitigation steps\n\nI would recommend not doing this bug.\n\n",

Impact

With the token you can access the submissions of others and share in their prices.

Tools used

Fiddler (https://www.telerik.com/)
And the developer console of Chrome to look at the source.

Recommended mitigation steps

Either open source the bug submission enterly
Or split the bug submission application in two parts, where only a backend has the Github keys and does the creating of the issues.

There are tools that promise to do this, i haven't looked into them:
https://fire.fundersclub.com/
https://zapier.com/apps/github/integrations/gmail/10314/create-github-issues-from-new-emails-on-gmail-business-gmail-accounts-only
https://flow.microsoft.com/en-us/galleries/public/templates/6b590f10bc9011e6b2e2c98b01575bae/send-an-email-to-create-github-issues/

diffMaxMinRuntime gets default value of 0

Email address

[email protected]

Handle

paulius.eth

Eth address

0x523B5b2Cc58A818667C22c862930B141f85d49DD

Vulnerability details

uint256 public diffMaxMinRuntime;
This variable is never set nor updated so it gets a default value of 0.

Impact

diffMaxMinRuntime with 0 value is making the calculations that use it either always return 0 (when multiplying) or fail (when dividing) when calculating bucket indexes or sizes.

Recommended mitigation steps

Set the appropriate value for diffMaxMinRuntime and update it whenever min or max runtime variables change.

Re-entrancy bug in `MarginRouter.crossSwapExactTokensForTokens` allows inflating balance

Email address

[email protected]

Handle

@cmichelio

Eth address

0x6823636c2462cfdcD8d33fE53fBCD0EdbE2752ad

Vulnerability details

One can call the MarginRouter.crossSwapExactTokensForTokens function first with a fake contract disguised as a token pair:
crossSwapExactTokensForTokens(0.0001 WETH, 0, [ATTACKER_CONTRACT], [WETH, WBTC]). When the amounts are computed by the amounts = UniswapStyleLib.getAmountsOut(amountIn - fees, pairs, tokens); call, the attacker contract returns fake reserves that yield 1 WBTC for the tiny input.
The resulting amount is credited through registerTrade.
Afterwards, _swapExactT4T([0.0001 WETH, 1 WBTC], 0, [ATTACKER_CONTRACT], [WETH, WBTC]) is called with the fake pair and token amounts.
At some point _swap is called, the starting balance is stored in startingBalance, and the attacker contract call allows a re-entrancy:

pair.swap(0.0001 WETH, 1 WBTC, FUND, new bytes(0)); // can re-enter here

From the ATTACKER_CONTRACT we re-enter the MarginRouter.crossSwapExactTokensForTokens(30 WETH, 0, WETH_WBTC_PAIR, [WETH, WBTC]) function with the actual WETH <> WBTC pair contract.
All checks pass, the FUND receives the actual amount, the outer _swap continues execution after the re-entrancy and the endingBalance >= startingBalance + amounts[amounts.length - 1] check passes as well because the inner swap successfully deposited these funds.
We end up doing 1 real trade but being credited twice the output amount.

Impact

This allows someone to be credited multiples of the actual swap result. This can be repeated many times and finally, all tokens can be stolen.

[INFO] TODOs

Email address

[email protected]

Handle

paulius.eth

Eth address

0x523B5b2Cc58A818667C22c862930B141f85d49DD

Vulnerability details

This is FYI, not a real issue as you have expressed your interest in minor improvement suggestions (not security or gas related):

There are 6 TODOs left. It makes it confusing to audit such code.

`account.holdsToken` is never set

Email address

[email protected]

Handle

@cmichelio

Eth address

0x6823636c2462cfdcD8d33fE53fBCD0EdbE2752ad

Vulnerability details

The addHolding function does not update the account.holdsToken map.

function addHolding(
    CrossMarginAccount storage account,
    address token,
    uint256 depositAmount
) internal {
    if (!hasHoldingToken(account, token)) {
        // SHOULD SET account.holdsToken here
        account.holdingTokens.push(token);
    }

    account.holdings[token] += depositAmount;
}

This leads to a critical vulnerability where deposits of the same token keep being pushed to the account.holdingTokens array but the sum is correctly updated in account.holdings[token].

However, because of the duplicate token in the holdingTokens array the same token is counted several times in the getHoldingAmounts function:

function getHoldingAmounts(address trader)
    external
    view
    override
    returns (
        address[] memory holdingTokens,
        uint256[] memory holdingAmounts
    )
{
    CrossMarginAccount storage account = marginAccounts[trader];
    holdingTokens = account.holdingTokens;

    holdingAmounts = new uint256[](account.holdingTokens.length);
    for (uint256 idx = 0; holdingTokens.length > idx; idx++) {
        address tokenAddress = holdingTokens[idx];
        // RETURNS SUM OF THE BALANCE FOR EACH TOKEN ENTRY
        holdingAmounts[idx] = account.holdings[tokenAddress];
    }
}

The MarginRouter.crossCloseAccount function uses these wrong amounts to withdraw all tokens:

function crossCloseAccount() external {
    (address[] memory holdingTokens, uint256[] memory holdingAmounts) =
        IMarginTrading(marginTrading()).getHoldingAmounts(msg.sender);

    // requires all debts paid off
    IMarginTrading(marginTrading()).registerLiquidation(msg.sender);

    for (uint256 i; holdingTokens.length > i; i++) {
        Fund(fund()).withdraw(
            holdingTokens[i],
            msg.sender,
            holdingAmounts[i]
        );
    }
}

Impact

An attacker can just deposit the same token X times which increases their balance by X times the actual value.
This inflated balance can then be withdrawn to steal all tokens.

Recommended mitigation steps

Correctly set the account.holdsToken map in addHolding.

`getReserves` does not check if tokens match

Email address

[email protected]

Handle

@cmichelio

Eth address

0x6823636c2462cfdcD8d33fE53fBCD0EdbE2752ad

Vulnerability details

The UniswapStyleLib.getReserves function does not check if the tokens are the pair's underlying tokens.
It blindly assumes that the tokens are in the wrong order if the first one does not match but they could also be completely different tokens.

Impact

It could be the case that output amounts are computed for completely different tokens because a wrong pair was provided.

isStakePenalizer differtent than other functions in RoleAware.sol

Email address

[email protected]

Handle

gpersoon

Eth address

gpersoon.eth

Vulnerability details

This is a minor suggestion.

The function isStakePenalizer in RoleAware.sol uses roles.getRole...
However all other function use roleCache...

It's not clear why this difference exists.

Impact

If roleCache could also be used a tiny amount of gas could be safed.

Proof of concept

Recommended mitigation steps

Check if isStakePenalizer can use roleCache, in that case update the source.
Otherwise provide a comment why roles.getRole is neccesary

[INFO] All caps indicates that the value should be constant

Email address

[email protected]

Handle

paulius.eth

Eth address

0x523B5b2Cc58A818667C22c862930B141f85d49DD

Vulnerability details

This is FYI, not a real issue as you have expressed your interest in minor improvement suggestions (not security or gas related):

All caps indicates that the value should be constant:
uint256 public MAINTAINER_CUT_PERCENT = 5;
However, it can be changed with function setMaintainerCutPercent. Then, this comment may become innacurate: // 5% of value borrowed Same with UPDATE_RATE_PERMIL, UPDATE_MAX_PEG_AMOUNT, UPDATE_MIN_PEG_AMOUNT.

Impact

Recommended mitigation steps

Price feed can be manipulated

Vulnerability details

Anyone can trigger an update to the price feed by calling PriceAware.getCurrentPriceInPeg(token, inAmount, forceCurBlock=true).
If the update window has passed, the price will be computed by simulating a Uniswap-like trade with the amounts.
This simulation uses the reserves of the Uniswap pairs which can be changed drastically using flash loans to yield almost arbitrary output amounts, and thus prices.

Impact

Wrong prices break the core functionality of the contracts such as borrowing on margin, liquidations, etc.

Recommended mitigation steps

Do not use the Uniswap spot price as the real price.
Uniswaps itself warns against this and instead recommends implementing a TWAP price oracle using the price*CumulativeLast variables.

Email address

[email protected]

Handle

@cmichelio

Eth address

0x6823636c2462cfdcD8d33fE53fBCD0EdbE2752ad

This is an example finding

Email address

[email protected]

Handle

adamavenir

Eth address

123123123

Vulnerability details

Some details:

details(schemtails)

Impact

Brace for it!

Proof of concept

  • proof
  • of
  • concept

Tools used

I used no tools except this form and my BARE HANDS!

Recommended mitigation steps

I would recommend not doing this bug.

Owner can initialize an already initialized tranche

Email address

[email protected]

Handle

s1m0

Eth address

0x9b3E9e3E4a174d59279FC7cd268e035992412384

Vulnerability details

The owner can initialize an already initialized tranche by calling setTranche https://github.com/code-423n4/marginswap/blob/main/contracts/IncentiveDistribution.sol#L78 with 0 as share argument and then calling initTranche https://github.com/code-423n4/marginswap/blob/main/contracts/IncentiveDistribution.sol#L101 bypassing the check require(tm.rewardShare == 0, "Tranche already initialized");

Recommended mitigation steps

Check share != 0 for setTrancheShare and initTranche

Impact

The state of the system would become not correct by inflating the allTranches variable and it would raise the gas cost for calling withdrawReward

Tools used

Manual analysis

Proof of concept

Assuming the 1 tranche is initialized.

  • call setTrancheShare(1, 0)
  • call initTranche(1, n)

[INFO] Optimize the inheritance tree

Email address

[email protected]

Handle

paulius.eth

Eth address

0x523B5b2Cc58A818667C22c862930B141f85d49DD

Vulnerability details

This is FYI, not a real issue as you have expressed your interest in minor improvement suggestions (not security or gas related):

Optimize the inheritance tree. For example:
contract Lending is
BaseLending,
HourlyBondSubscriptionLending,
BondLending,
...

abstract contract BondLending is BaseLending

abstract contract HourlyBondSubscriptionLending is BaseLending
so Lending already inherits BaseLending from BondLending and HourlyBondSubscriptionLending.

function initTranche should check that the share parameter is > 0

Impact

only admin can call this so highly unlikely to happen yet it would be better if code prevents that.

Recommended mitigation steps

require share to be greater than 0.

Tools used

Email address

[email protected]

Handle

paulius.eth

Eth address

0x523B5b2Cc58A818667C22c862930B141f85d49DD

Vulnerability details

function initTranche should check that the "share" parameter is > 0, otherwise, it may be possible to initialize the same tranche again.

sortTokens can be simplified

Vulnerability details

this is a minor suggestion:

The function sortTokens UniswapStyleLib.sol returns 2 values, but only the first return value is used:
MarginRouter.sol: (address token0, ) = UniswapStyleLib.sortTokens...
UniswapStyleLib.sol: (address token0, ) = sortTokens..
In both cases the used return value is compared to the first parameter of the function call.
Conclusion: the function is only used to determine the smaller of the two tokens, not really to sort tokens.

Handle

gpersoon

Email address

[email protected]

Eth address

gpersoon.eth

Impact

The code is somewhat more difficult to read and a bit longer than neccesary.

Recommended mitigation steps

simplify the code:
function ASmallerThanB(address tokenA, address tokenB)
internal
pure
returns (bool)
{
require(tokenA != tokenB, "Identical address!");
require(tokenA != address(0), "Zero address!");
require(tokenB != address(0), "Zero address!");
return tokenA < tokenB;
}

[INFO] Variable is declared and initialized with different values

Email address

[email protected]

Handle

paulius.eth

Eth address

0x523B5b2Cc58A818667C22c862930B141f85d49DD

Vulnerability details

This is FYI, not a real issue as you have expressed your interest in minor improvement suggestions (not security or gas related):

It is strange to see a variable assigned a value in the declaration but immediadetely overriden in the constructor:
uint256 public maintenanceStakePerBlock = 10 ether;
constructor(
...
maintenanceStakePerBlock = 1 ether;

[INFO] Consistent function names

Email address

[email protected]

Handle

paulius.eth

Eth address

0x523B5b2Cc58A818667C22c862930B141f85d49DD

Vulnerability details

This is FYI, not a real issue as you have expressed your interest in minor improvement suggestions (not security or gas related):

In contract IsolatedMarginTrading the function to set leverage is named "setLeveragePercent" and in CrossMarginTrading function that does the same is named "setLeverage". It would be better to unify them and give the same names to make it more consistent.

No default `liquidationThresholdPercent`

Email address

[email protected]

Handle

@cmichelio

Eth address

0x6823636c2462cfdcD8d33fE53fBCD0EdbE2752ad

Vulnerability details

The IsolatedMarginTrading contract does not define a default liquidationThresholdPercent which means it is set to 0.

The belowMaintenanceThreshold function uses this value and anyone could be liquidated due to 100 * holdings >= liquidationThresholdPercent * loan = 0 being always true.

Impact

Anyone can be liquidated immediately.
If the faulty belowMaintenanceThreshold function is fixed (see other issue), then nobody could be liquidated which is bad as well.

Recommended mitigation steps

Set a default liquidation threshold like in CrossMarginTrading contracts.

Unlocked Pragma

Email address

[email protected]

Handle

@cmichelio

Eth address

0x6823636c2462cfdcD8d33fE53fBCD0EdbE2752ad

Vulnerability details

Every Solidity file specifies in the header a version number of the format pragma solidity ^0.8.0. The caret (^) before the version number implies an unlocked pragma, meaning that the compiler will use the specified version or above.

It’s usually a good idea to pin a specific version to know what compiler bug fixes and optimizations were enabled at the time of compiling the contract.

Impact

Recommended mitigation steps

Pin the compiler versions.

function buyBond charges msg.sender twice

Email address

[email protected]

Handle

paulius.eth

Eth address

0x523B5b2Cc58A818667C22c862930B141f85d49DD

Vulnerability details

function buyBond transfers amount from msg.sender twice:
Fund(fund()).depositFor(msg.sender, issuer, amount);
...
collectToken(issuer, msg.sender, amount);

Impact

This makes the msg.sender pay twice for the same bond.

Recommended mitigation steps

Charge poor man only once.

Naming convention for internal functions not used consistently

Email address

[email protected]

Handle

gpersoon

Eth address

gpersoon.eth

Vulnerability details

This is a minor suggestion.

Most internal function names start with an underscore (_)
However quite a lot of internal function names don't follow this convention.

Proof of concept

One example is: updateHourlyBondAmount in HourlyBondSubscriptionLending.sol
Also all the functions in RoleAware.sol don't comply to the standard.

Impact

The code is more difficult to read if a naming convention is not used consistently.

Recommended mitigation steps

Add an underscore (_) prefix to all internal functions.

setLeveragePercent should check that new _leveragePercent >= 100

Email address

[email protected]

Handle

paulius.eth

Eth address

0x523B5b2Cc58A818667C22c862930B141f85d49DD

Vulnerability details

function setLeveragePercent should check that the _leveragePercent >= 100 so that this calculation will not fail later:
(leveragePercent - 100)

Impact

This variable can only be set by admin so as long as he sets the appropriate value it should be fine.

Recommended mitigation steps

It is always nice to enforce such things via code. Code is law they say.

maintainer can be pushed out

Email address

[email protected]

Handle

gpersoon

Eth address

gpersoon.eth

Vulnerability details

The function liquidate (in both CrossMarginLiquidation.sol and IsolatedMarginLiquidation.sol) can be called by everyone.
If an attacker calls this repeatedly then the maintainer will be punished and eventually be reported as maintainerIsFailing
And then the attacker can take the payouts

Proof of concept

When a non authorized address repeatedly calls liquidate then the following happens:
isAuthorized = false
which means maintenanceFailures[currentMaintainer] increases
after sufficient calls it will be higher than the threshold and then
maintainerIsFailing() will be true
This results in canTakeNow being true
which finally means the following will be executed:
Fund(fund()).withdraw(PriceAware.peg, msg.sender, maintainerCut);

Impact

An attacker can push out a maintainer and take over the liquidation revenues

Tools used

remix

Recommended mitigation steps

put authorization on who can call the liquidate function
review the maintainer punishment scheme

Liquidations can be sandwich attacked

Email address

[email protected]

Handle

@cmichelio

Eth address

0x6823636c2462cfdcD8d33fE53fBCD0EdbE2752ad

Vulnerability details

The liquidation functions liquidateToPeg/liquidateFromPeg uses a minReturn value of zero which allows infinite slippage.
An attacker can frontrun a liquidation trade by buying up the same asset, driving the price higher and resulting in the liquidator receiving fewer tokens. The attacker then backruns the trade by selling the tokens received by their first trade again for a profit. (sandwich attack)

Impact

Liquidators earn less profit

Recommended mitigation steps

Let liquidators define minReturn amounts.

Role 9 in Roles.sol

Email address

[email protected]

Handle

gpersoon

Eth address

gpersoon.eth

Vulnerability details

This is a minor suggestion.

Roles.sol contains the following:
roles[msg.sender][9] = true;
It's not clear what the number 9 means.
In RoleAware.sol there is a constant with the value 9:
uint256 constant TOKEN_ACTIVATOR = 9;

Impact

The code is more difficult to read without an explanation for the number 9.
In case the code would be refactored in the future and the constants in RoleAware.sol are renumbered, the value in Roles.sol would no longer correspond to the right value.

Recommended mitigation steps

Move the constants from Roles.sol to RoleAware.sol and replace 9 with the appropriate constant.

Events not indexed

Email address

[email protected]

Handle

@cmichelio

Eth address

0x6823636c2462cfdcD8d33fE53fBCD0EdbE2752ad

Vulnerability details

The CrossDeposit, CrossTrade, CrossWithdraw, CrossBorrow, CrossOvercollateralizedBorrow events in MarginRouter are not indexed.

Impact

Off-chain scripts cannot efficiently filter these events.

Recommended mitigation steps

Add an index on important arguments like trader.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.