Sushi Miso contest - cmichel's results

Part of the Sushi’s product ecosystem in advancing seamless token and auction launchpad for projects.

General Information

Platform: Code4rena

Start Date: 09/09/2021

Pot Size: $100,000 SUSHI

Total HM: 4

Participants: 11

Period: 7 days

Judge: ghoulsol

Total Solo HM: 3

Id: 28

League: ETH

Sushi

Findings Distribution

Researcher Performance

Rank: 1/11

Findings: 4

Award: $24,749.25

🌟 Selected for report: 9

πŸš€ Solo Findings: 1

Findings Information

🌟 Selected for report: WatchPug

Also found by: 0xRajeev, cmichel, tensors

Labels

bug
duplicate
3 (High Risk)

Awards

272.6038 SUSHI - $2,734.22

External Links

Handle

cmichel

Vulnerability details

The PostAuctionLauncher.finalize function takes the raised payment token amounts and uses previously provided auction token amounts to provide liquidity to a Sushiswap pool after an auction has successfully been finalized.

It provides this liquidity at a pre-defined rate and does not take into account the actual reserves ratio of a pool that might already exist. It assumes that it is the first to provide liquidity but this does not have to be the case.

An attacker that has a tiny amount of auction tokens can steal large quantities of the tokens used for providing liquidity.

The reason is that in Uniswap V2 / Sushiswap you receive liquidity tokens that correspond to the worst (minimum) ratio that was provided:

// https://github.com/sushiswap/sushiswap/blob/canary/contracts/uniswapv2/UniswapV2Pair.sol#L136
// amount0, amount1 are the tokens provided for LPing
liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1);
// writing it in math terms and factoring out _totalSupply to make it easier to read
// liquidityToMint = _totalSupply * Math.min(amount0 / _reserve0, amount1 / _reserve1)

POC

Let's say there are two tokens, the auction token A and the payment token B. For simplicity, assume that the auction has decided that they are worth equal amounts, i.e., tokenPrice = 1.0, 1.0 A = 1.0 B. We also use a liquidityPercent = 100%. (The attack works with any tokenPrice and any liquidityPercent as the pool's current reserve ratio can be chosen by the attacker and is independent of any of these auction / PostAuctionLauncher values.)

Let's say the PostAuctionLauncher will provide 10.0A and 10.0B to the pool.

  • Attacker acquires a tiny amount of A and a large amount of B. They could have participated in the auction themselves if market.finalize() has already been called.
  • Attacker observes the PostAuctionLauncher.finalize() transaction in the mempool
  • They frontrun it and create the Sushiswap A <> B pool and provide initial liquidity of 0.0001A <> 10.0B. They receive the initial LP token supply.
  • PostAuctionLauncher.finalize() provides 10.0 A <> 10.0B liquidity which is very different from the current reserves ratio and leads to receiving a horrible rate. They will receive liquidity = _totalSupply * Math.min(amount0 / _reserve0, amount1 / _reserve1) = _totalSupply * Math.min(10.0A/0.0001A=100_000, 10.0B/10.0B=1) = 1*_totalSupply.
  • Attacker and PostAuctionLauncher now each own 50% of the total LP supply.
  • Attacker redeems all their LP tokens and receives their fair share (50%) of the pool reserves 5.00005A <> 10.0B.

The attacker's profit is ~5.0A. (By further increasing the initial B liquidity, the attacker can steal almost the entire A amount that was provided by PostAuctionLauncher.)

Impact

An attacker can steal the auction tokens provided by the PostAuctionLauncher to the liquidity pool. They can even do this risk-free and atomically in a flash-bundle by sandwiching the PostAuctionLauncher.finalize.

Note that this attack is symmetric in the tokens, i.e., if the attacker would rather steal the payment tokens, they can provide liquidity with a large number of auction tokens and a small number of payment tokens

Note that rebalancing the low-liquidity pool for a better rate to provide liquidity at doesn't work. Furthermore, as stated this attack can be done atomically and at the time of transaction submission, the pool might not even exist yet.

It seems like it would be enough to have a standard min. LP return "slippage" check (require(liquidity >= minLpReturn)) in finalize(uint256 minLpReturn) or checking that the contract owns X% of the pool LP total supply after LP provision. (Could even just use the SushiswapRouter with its in-built min return check instead of directly minting on the pool contract.) The function must then be callable with special roles only.

This will prevent the attack as the transaction will revert when receiving bad rates. If the attacker creates the pool and provides liquidity non-atomically, it'll just get arbed to the auction token price and liquidity can then be provided at the expected rate again.

#0 - Clearwood

2021-09-16T04:35:44Z

Duplicate of #14

Findings Information

🌟 Selected for report: cmichel

Labels

bug
3 (High Risk)
disagree with severity

Awards

1495.7685 SUSHI - $15,002.56

External Links

Handle

cmichel

Vulnerability details

When minting / transferring / burning tokens, the SushiToken._beforeTokenTransfer function is called and supposed to correctly shift the voting power due to the increase/decrease in tokens for the from and two accounts. However, it does not correctly do that, it tries to shift the votes from the from account, instead of the _delegates[from] account. This can lead to transfers reverting.

POC

Imagine the following transactions on the SushiToken contract. We'll illustrate the corresponding _moveDelegates calls and written checkpoints for each.

  • mint(A, 1000) = transfer(0, A, 1000) => _moveDelegates(0, delegates[A]=0) => no checkpoints are written to anyone because delegatees are still zero
  • A delegates to A' => _moveDelegates(0, A') => writeCheckpoint(A', 1000)
  • B delegates to B' => no checkpoints are written as B has a zero balance
  • transfer(A, B, 1000) => _moveDelegates(A, delegates[B] = B') => underflows when subtracting amount=1000 from A's non-existent checkpoint (defaults to 0 votes)

It should subtract from A's delegatee A''s checkpoint instead.

Impact

Users that delegated votes will be unable to transfer any of their tokens.

In SushiToken._beforeTokenTransfer, change the _moveDelegates call to be from _delegates[from] instead:

function _beforeTokenTransfer(address from, address to, uint256 amount) internal override { 
    _moveDelegates(_delegates[from], _delegates[to], amount);
    super._beforeTokenTransfer(from, to, amount);
}

This is also how the original code from Compound does it.

#0 - maxsam4

2021-09-16T04:42:19Z

This is a known issue in Sushi token but was kept unchanged in MISO for "preservation of history :)". That was not necessarily a wise choice lol. I think 1 severity should be fine for this as this was an intentional thing. The delegate feature is not supposed to be used in these tokens. We might create a new token type with this bug fixed.

#1 - ghoul-sol

2021-10-05T17:58:42Z

We have crazy wallets on the blockchain that will call every possible function available to them and that's why I'm keeping this as is. Even intentional, the issue stands so the warden should get credit for it.

Findings Information

🌟 Selected for report: cmichel

Labels

bug
1 (Low Risk)
sponsor acknowledged
disagree with severity

Awards

149.5769 SUSHI - $1,500.26

External Links

Handle

cmichel

Vulnerability details

The index of the item in the userItem.lockToItems[_tokenAddress] array is tracked in lockedItem[_id].userIndex (see lockTokens):

uint256 userIndex = userItem.lockToItems[_tokenAddress].length - 1;
lockedItem[_id].userIndex = userIndex;

However, when withdrawing tokens this link breaks, as the withdrawn item is removed (swapped with the last element in this array):

if(userItem.amount == 0) {
    uint256[] storage userItems = users[msg.sender].lockToItems[_tokenAddress];
    // @audit swaps last element (which stores item ID) with element at _index
    userItems[_index] = userItems[userItems.length -1];
    userItems.pop();
    // @audit swapping needs to change lockedItem[ userItems[_index] ].userIndex = index;
}

Impact

After withdrawing tokens, the swapped item's lockedItem[_id].userIndex is not the last element anymore, but takes the old item's _index now. The link is broken and getLockedItemAtId returns wrong data which, when used to get the index for withdrawTokens, will then refer to the wrong token and revert.

After deleting the element in withdrawTokens, update the remaining swapped element's userIndex:

if(userItem.amount == 0) {
    uint256[] storage userItems = users[msg.sender].lockToItems[_tokenAddress];
    userItems[_index] = userItems[userItems.length -1];
    userItems.pop();
    
    // ADD THIS
    // userItems[_index] refers to the previously last item's ID
    lockedItem[ userItems[_index] ].userIndex = index;
}

#0 - maxsam4

2021-09-16T05:18:58Z

This functionality is never used onchain so the risk is only applicable to UIs.

ps. we're no longer using this contract.

#1 - ghoul-sol

2021-10-05T18:35:33Z

no funds at risk, issues with UI, it's a low risk

Findings Information

🌟 Selected for report: cmichel

Also found by: 0xRajeev, tensors

Labels

bug
1 (Low Risk)
sponsor acknowledged

Awards

40.3858 SUSHI - $405.07

External Links

Handle

cmichel

Vulnerability details

The address.transfer function is used to send ETH to an account. It is restricted to a low amount of GAS and might fail if GAS costs change in the future or if a smart contract's fallback function handler implements anything non-trivial.

Occurences:

  • MISOFarmFactory.deployFarm
  • MISOLauncher.deployLauncher
  • MISOMarket.deployMarket
  • MISOTokenFactory.deployToken
  • ListFactory.deployPointList
  • MISOAccessFactory.deployAccessControl
  • CrowdSale.commitEth
  • DutchAuction.commitEth
  • HyperbolicAuction.commitEth
  • MISORecipe0*.prepareMiso
  • SafeTransfer._tokenPayment
  • WETH9.withdraw

Consider using the lower-level .call{value: value} instead and check it's success return value.

Findings Information

🌟 Selected for report: cmichel

Also found by: itsmeSTYJ, leastwood

Labels

bug
1 (Low Risk)
sponsor confirmed

Awards

40.3858 SUSHI - $405.07

External Links

Handle

cmichel

Vulnerability details

The DutchAuction._currentPrice() is computed by multiplying with priceDrop(). However, priceDrop() already performs a division and the final current price, therefore, loses precision.

Note that priceDrop() could even return 0 for ultra-low prices or very long auctions.

Imagine the actual payment per auction token price is 10^-12 => startPrice and endPrice are set with 18 decimals as ~10^6, but for auctions over a year (31,536,000 seconds > 10^6) it'll then return 0.

Impact

Precision can be lost leading to less accurate token auction results or even completely breaking the auction if the price is very low and the auctions are very long.

Perform all multiplications before divisions:

uint256 priceDiff = block.timestamp.sub(uint256(marketInfo.startTime)).mul(
    uint256(_marketPrice.startPrice.sub(_marketPrice.minimumPrice))
  ) / uint256(_marketInfo.endTime.sub(_marketInfo.startTime));

#0 - Clearwood

2021-10-05T18:09:35Z

Great work here πŸ‘

#1 - Clearwood

2021-10-05T18:12:40Z

Findings Information

🌟 Selected for report: cmichel

Labels

bug
sponsor disputed
1 (Low Risk)

Awards

149.5769 SUSHI - $1,500.26

External Links

Handle

cmichel

Vulnerability details

Although the contract is supposed to accept LP tokens only, it does not restrict the tokens used and it could be that a fee-on-transfer token is used as the pool.lpToken.

Impact

This has led to many attacks, see here for write-ups. The gist is that the "pool" token balance in the contract can be reduced to a few wei by continuously depositing/withdrawing tokens due to accounting errors with not using pre-post transfer balances to get the actual amount. (Attacker deposits 1000, is credited 1000, but pool actually only receives 900. Attacker can withdraw 1000, pool balance diff is -100.) Then pool.accRewardsPerShare = pool.accRewardsPerShare.add(rewardsAccum.mul(1e12).div(lpSupply)); is a really high value and it's multiplied by the wrong user.amount.

Document that pool.lpToken should never be a fee-on-transfer token.

Use pre-and-post balances to compute the actual deposited amount.

#0 - Clearwood

2021-09-21T19:04:03Z

Considering that lpTokens should be Sushi LP tokens in most cases, this is an edge case A comment might be helpful.

#1 - ghoul-sol

2021-10-05T23:58:46Z

I think low risk is fair

Findings Information

🌟 Selected for report: cmichel

Labels

bug
1 (Low Risk)

Awards

149.5769 SUSHI - $1,500.26

External Links

Handle

cmichel

Vulnerability details

Some tokens (like USDT) don't correctly implement the EIP20 standard and their transfer/transferFrom function return void instead of a success boolean. Calling these functions with the correct EIP20 function signatures will always revert.

The MISORecipe03.createLiquidityFarm uses a standard ERC20(rewardToken).transferFrom(msg.sender, self, tokensToFarm) call for the rewardToken which could be a non-compliant token like USDT

Impact

Tokens that don't correctly implement the latest EIP20 spec, like USDT, will be unusable in this recipe as they revert the transaction because of the missing return value.

We recommend using OpenZeppelin’s SafeERC20 versions with the safeTransfer and safeTransferFrom functions that handle the return value check as well as non-standard-compliant tokens.

Findings Information

🌟 Selected for report: cmichel

Labels

bug
1 (Low Risk)
disagree with severity
sponsor confirmed

Awards

149.5769 SUSHI - $1,500.26

External Links

Handle

cmichel

Vulnerability details

Some tokens don't correctly implement the EIP20 standard and their approve function returns void instead of a success boolean. Calling these functions with the correct EIP20 function signatures will always revert.

Calls to .approve with user-defined tokens are made in:

  • MISOLauncher.createLauncher
  • MISOMarket.createMarket

Impact

Tokens that don't correctly implement the latest EIP20 spec, like USDT, will be unusable in the mentioned contracts as they revert the transaction because of the missing return value.

We recommend using OpenZeppelin’s SafeERC20 versions with the safeApprove function that handle the return value check as well as non-standard-compliant tokens.

#0 - Clearwood

2021-09-21T18:59:37Z

Would be useful in generalization but does not provide the risk for loss of funds, just limits the usability of the contracts

#1 - Clearwood

2021-10-05T18:27:19Z

#2 - ghoul-sol

2021-10-06T00:00:06Z

I think this is worth low risk.

Findings Information

🌟 Selected for report: cmichel

Also found by: leastwood

Labels

bug
G (Gas Optimization)
sponsor confirmed

Awards

6.2284 SUSHI - $62.47

External Links

Handle

cmichel

Vulnerability details

The DutchAuction.clearingPrice function can save gas by caching the computed prices instead of recomputing it.

Cache the values:

function clearingPrice() public view returns (uint256) {
    /// @dev If auction successful, return tokenPrice
    uint256 _tokenPrice = tokenPrice();
    uint256 _currentPrice = priceFunction();
    return _tokenPrice > _currentPrice ? _tokenPrice : _currentPrice;
}

#0 - Clearwood

2021-10-05T19:11:53Z

Findings Information

🌟 Selected for report: cmichel

Labels

bug
G (Gas Optimization)
sponsor acknowledged

Awards

13.8408 SUSHI - $138.82

External Links

Handle

cmichel

Vulnerability details

The SushiToken.delegateBySig function can save gas by not taking the unnecessary nonce parameter. It currently validates that the provided nonce == sigNonces[signatory] but this is unnecessary as the nonce is already part of the signed message digest.

A shorter function payload leads to gas improvements.

Remove the nonce parameter from the function, and use the sigNonces[signatory] when creating the message digest.

bytes32 structHash = keccak256(
    abi.encode(
        DELEGATION_TYPEHASH,
        delegatee,
        // @audit make sure to increment here when removing the require check below
        sigNonces[signatory]++,
        nonce,
        expiry
    )
);
AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax Β© 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter