Platform: Code4rena
Start Date: 16/09/2021
Pot Size: $200,000 SUSHI
Total HM: 26
Participants: 16
Period: 14 days
Judge: alcueca
Total Solo HM: 13
Id: 29
League: ETH
Rank: 5/16
Findings: 4
Award: $12,511.56
🌟 Selected for report: 7
🚀 Solo Findings: 1
🌟 Selected for report: GreyArt
705.9844 SUSHI - $8,824.81
GreyArt
A number of functions suffer from the erroneous conversion of Balancer V1's implementation.
_compute()
(equivalent to Balancer's [bpow()](https://github.com/balancer-labs/balancer-core/blob/master/contracts/BNum.sol#L108-L126)
)
if (remain == 0) output = wholePow;
when a return statement should be used instead._computeSingleOutGivenPoolIn()
(equivalent to Balancer's [_calcSingleOutGivenPoolIn()](https://github.com/balancer-labs/balancer-core/blob/master/contracts/BMath.sol#L195-L224)
)
tokenOutRatio
should be calculated with _compute()
instead of _pow()
zaz
should be calculated with _mul()
instead of the native *
_pow()
(equivalent to Balancer's [bpowi()](https://github.com/balancer-labs/balancer-core/blob/master/contracts/BNum.sol#L89-L103)
)
{}
for the for loop causes a different interpretation_mul
should be used instead of the native *
The fixed implementation is provided below.
function _computeSingleOutGivenPoolIn( uint256 tokenOutBalance, uint256 tokenOutWeight, uint256 _totalSupply, uint256 _totalWeight, uint256 toBurn, uint256 _swapFee ) internal pure returns (uint256 amountOut) { uint256 normalizedWeight = _div(tokenOutWeight, _totalWeight); uint256 newPoolSupply = _totalSupply - toBurn; uint256 poolRatio = _div(newPoolSupply, _totalSupply); uint256 tokenOutRatio = _compute(poolRatio, _div(BASE, normalizedWeight)); uint256 newBalanceOut = _mul(tokenOutRatio, tokenOutBalance); uint256 tokenAmountOutBeforeSwapFee = tokenOutBalance - newBalanceOut; uint256 zaz = _mul(BASE - normalizedWeight, _swapFee); amountOut = _mul(tokenAmountOutBeforeSwapFee, (BASE - zaz)); } function _compute(uint256 base, uint256 exp) internal pure returns (uint256 output) { require(MIN_POW_BASE <= base && base <= MAX_POW_BASE, "INVALID_BASE"); uint256 whole = (exp / BASE) * BASE; uint256 remain = exp - whole; uint256 wholePow = _pow(base, whole / BASE); if (remain == 0) return wholePow; uint256 partialResult = _powApprox(base, remain, POW_PRECISION); output = _mul(wholePow, partialResult); } function _pow(uint256 a, uint256 n) internal pure returns (uint256 output) { output = n % 2 != 0 ? a : BASE; for (n /= 2; n != 0; n /= 2) { a = _mul(a, a); if (n % 2 != 0) output = _mul(output, a); } }
95.3079 SUSHI - $1,191.35
GreyArt
A mint fee is applied whenever unbalanced liquidity is added, because it is akin to swapping the excess token amount for the other token.
However, the current implementation distributes the minted fee to the minter as well (when he should be excluded). It therefore acts as a rebate of sorts.
As a result, it makes adding and removing liquidity as opposed to swapping directly (negligibly) more desirable. An example is given below using the Constant Product Pool to illustrate this point. The Hybrid pool exhibits similar behaviour.
138573488720892 / 1e18
LP tokens.The mint fee should be distributed to existing LPs first, by incrementing _reserve0
and _reserve1
with the fee amounts. The rest of the calculations follow after.
ConstantProductPool
(uint256 fee0, uint256 fee1) = _nonOptimalMintFee(amount0, amount1, _reserve0, _reserve1); // increment reserve amounts with fees _reserve0 += uint112(fee0); _reserve1 += uint112(fee1); unchecked { _totalSupply += _mintFee(_reserve0, _reserve1, _totalSupply); } uint256 computed = TridentMath.sqrt(balance0 * balance1); ... kLast = computed;
HybridPool
(uint256 fee0, uint256 fee1) = _nonOptimalMintFee(amount0, amount1, _reserve0, _reserve1); // increment reserve amounts with fees _reserve0 += uint112(fee0); _reserve1 += uint112(fee1); uint256 newLiq = _computeLiquidity(balance0, balance1); ...
🌟 Selected for report: GreyArt
70.5984 SUSHI - $882.48
GreyArt
The tridentSwapCallback
is only executed if context
passed into the flash swap function is non-empty. This would cause users to be unable to pay the required input amount to the pool should they not pass any context data.
ITridentCallee(msg.sender).tridentSwapCallback(data);
regardless of data.length
in _processSwap()
.
🌟 Selected for report: GreyArt
32.2581 SUSHI - $403.23
GreyArt
As inspired from Uniswap V3, consider having a special method (Eg. initPool()
) that seeds initial liquidity and unlocks the pool. The main benefits are:
mint()
more gas efficient.require(_reserve0 > 0, "POOL_UNINITIALIZED");
are made redundant (in fact, they are)._mintFee()
can have the if (_kLast != 0)
condition removedAn example for how a sample of the initPool()
implementation for ConstantProductPool is illustrated below.
// TODO: Remove unlocked = 1; in constructor method function initPool(bytes calldata data) external override returns (uint256 liquidity) { require(unlocked == 0, "POOL_ALREADY_INITIALIZED"); address recipient = abi.decode(data, (address)); (uint256 balance0, uint256 balance1) = _balance(); // amount0 and amount1 are balance0 and balance1 respectively require(balance0 > 0 && balance1 > 0, "INVALID_AMOUNTS"); address migrator = IMasterDeployer(masterDeployer).migrator(); if (msg.sender == migrator) { liquidity = IMigrator(migrator).desiredLiquidity(); require(liquidity != 0 && liquidity != type(uint256).max, "BAD_DESIRED_LIQUIDITY"); } else { require(migrator == address(0), "ONLY_MIGRATOR"); liquidity = computed - MINIMUM_LIQUIDITY; } _mint(address(0), MINIMUM_LIQUIDITY); unlocked = 1; }
🌟 Selected for report: GreyArt
32.2581 SUSHI - $403.23
GreyArt
Gas optimisation. Casting something of uint256 to uint32 has the same effect as mod since it will wrap around when it overflows. You need to ensure that unchecked math is used otherwise it will revert.
Line 294 of ConstantProductPool.sol
function _update( uint256 balance0, uint256 balance1, uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast ) internal { require(balance0 <= type(uint112).max && balance1 <= type(uint112).max, "OVERFLOW"); if (blockTimestampLast == 0) { // @dev TWAP support is disabled for gas efficiency. reserve0 = uint112(balance0); reserve1 = uint112(balance1); } else { unchecked { // changes starts here uint32 blockTimestamp = uint32(block.timestamp); } // changes end here ... } emit Sync(balance0, balance1); }
🌟 Selected for report: GreyArt
32.2581 SUSHI - $403.23
GreyArt
Since MAX_WEIGHT = MAX_TOTAL_WEIGHT = BASE * 50
, and overflow is prevented, only the total max weight needs to be checked.
Alternatively, totalWeight += _weights[i];
can be unchecked, because the maximum value of totalWeight
would be MAX_WEIGHT * MAX_TOKENS = 10**18 * 50 * 8 < type(uint136).max
.
Either remove MAX_WEIGHT
and the _weights[i] <= MAX_WEIGHT
check, or have totalWeight += _weights[i];
be unchecked.
🌟 Selected for report: GreyArt
32.2581 SUSHI - $403.23
GreyArt
Negligible / no impact, minor gas optimisation.
transferOwner()
is called, pendingOwner
is set.pendingOwner
can be claimed, direct transferOwner()
is called but pendingOwner
is not reset.transferOwner()
is called, set pendingOwner
to address(1).claimOwner()
is called, set pendingOwner
to address(1).