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: 4/16
Findings: 8
Award: $13,774.94
🌟 Selected for report: 6
🚀 Solo Findings: 0
190.6158 SUSHI - $2,382.70
0xsanson
The flashswap
function in IndexPool.sol doesn't fulfill its function. Indeed it should transfer tokens to the users before they need to pay back, but the transfer happens at the end:
... ITridentCallee(msg.sender).tridentSwapCallback(context); // @dev Check Trident router has sent `amountIn` for skim into pool. unchecked { // @dev This is safe from under/overflow - only logged amounts handled. require(_balance(tokenIn) >= amountIn + inRecord.reserve, "NOT_RECEIVED"); inRecord.reserve += uint120(amountIn); outRecord.reserve -= uint120(amountOut); } _transfer(tokenOut, amountOut, recipient, unwrapBento); ...
Move _transfer
before the callback.
#0 - alcueca
2021-10-30T04:39:33Z
Duplicate of #26
128.6657 SUSHI - $1,608.32
0xsanson
In IndexPool.sol, the function _pow
is called during the computation of the output amount when swapping.
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 = a * a; if (n % 2 != 0) output = output * a; }
It's my understanding that it should calculate the exponentiation _pow(a,n) = a**n
. However a simple test shows it doesn't work: _pow(3,5) = 3
, _pow(3,4) = 10**18
.
Consider documenting the function (and write some tests to check it works). If it needs to compute the exponentiation, one correct way would be (check wiki for alternatives):
function _pow(uint256 a, uint256 n) internal pure returns (uint256 output) { if (n == 0) { output = 1; return output; } uint256 y = 1; while (n > 1) { if (n % 2 == 0) { a = a*a; n = n/2; } else { y = a*y; a = a*a; n = (n-1)/2; } } output = a*y; }
#0 - alcueca
2021-10-27T04:14:16Z
Duplicate with #27
0xsanson
In HybridPool's _updateReserves()
function, the reserves are calculated as:
(uint256 _reserve0, uint256 _reserve1) = _balance();
This is incorrect, since reserves are a bento-share quantity, while _balance()
outputs a bento-amount quantity. This basically inflates artificially the reserves causing accounting issues.
The correct version should be uint256 _reserve0 = __balance(token0);
(the double-underscore balance returns a share quantity).
#0 - maxsam4
2021-10-20T12:07:57Z
95.3079 SUSHI - $1,191.35
0xsanson
IndexPool doesn't collect fees for barFeeTo
. Since this Pool contains also a method updateBarFee()
, probably this is an unintended behavior.
Also without a fee, liquidity providers would probably ditch ConstantProductPool in favor of IndexPool (using the same two tokens with equal weights), since they get all the rewards. This would constitute an issue for the ecosystem.
Add a way to send barFees to barFeeTo, same as the other pools.
95.3079 SUSHI - $1,191.35
0xsanson
In HybridPool's flashSwap
function there's a transfer to barFeeTo
_transfer(tokenIn, fee, barFeeTo, false);
Here fee = (amountIn * swapFee) / MAX_FEE
is the total swap fee. However it should transfer out only a fraction of it (barFee/MAX_FEE) otherwise liquidity providers wouldn't get any reward.
Calculate the appropriate fee to send to the barFeeTo
.
#0 - maxsam4
2021-10-06T08:03:02Z
Yep, we've revamped fee handling since.
#1 - maxsam4
2021-10-06T08:07:35Z
No user funds at risk, just potential fee might go to sushi rather than LPs (which can be sent back). So, I'd suggest med risk.
#2 - maxsam4
2021-10-20T12:09:40Z
0xsanson
In HybridPool.sol, functions _computeLiquidityFromAdjustedBalances
, _getY
and _getYD
may finish before approximation converge, since it's limited by MAX_LOOP_LIMIT
iterations.
In this situation the final estimated value will still be treated as correct, even though it could be relatively inaccurate.
Consider reverting the transactions if this doesn't occur. See https://blog.openzeppelin.com/saddle-contracts-audit/ issue [M03], with their relative fix.
#0 - maxsam4
2021-10-19T11:54:10Z
🌟 Selected for report: nikitastupin
0xsanson
The TridentERC20 token uses EIP-712 for the permit
function. This is correctly implemented but DOMAIN_SEPARATOR
is assumed constant, false when block.chainid
changes (for example after an hard fork).
This assumption can cause replay attacks on an eventual fork of the chain.
Consider implementing a variable DOMAIN_SEPARATOR
. For a possible implementation see @openzeppelin/contracts/utils/cryptography/draft-EIP712.sol
#0 - sarangparikh22
2021-10-05T08:29:12Z
Duplicate of #11
🌟 Selected for report: 0xsanson
70.5984 SUSHI - $882.48
0xsanson
In IndexPool.sol, the internal pure function _powApprox
is called during the computation of the output amount when swapping. It contains a for-loop that exits when a term reaches a certain precision.
for (uint256 i = 1; term >= precision; i++) { ... }
The issue is that there's no evident way that the iteration converges. At least it's possible that the number of necessary cycles is high enough to consume all transaction's gas. Also its function isn't really clear. If it wants to calculate an approximated exp, it actually doesn't work for small numbers (tested on ~10**18), so also a check on the input parameters may be needed.
This function needs some specific tests to show that it works without problems for all possible realistic inputs. Specifically check if a MAX_ITERATIONS parameter is needed. Also consider adding in the function's natSpec a link to the mathematical proof on how the formula works.
#0 - maxsam4
2021-10-06T08:41:41Z
The function has been borrowed from https://github.com/balancer-labs/balancer-core/blob/f4ed5d65362a8d6cec21662fb6eae233b0babc1f/contracts/BNum.sol#L128 where it is well tested.
We'll add more tests for it but there's no apparent vulnerability here.
#1 - alcueca
2021-10-25T05:18:26Z
Downgraded to severity 1 on grounds of lack of documentation with implied risks.
🌟 Selected for report: 0xsanson
70.5984 SUSHI - $882.48
0xsanson
Using BentoBox conventions, shares and amounts are different (they are a base and an elastic quantity respectively).
In HybridPool, balances are defined as amounts. This is respected correctly in all code but in one calculation in the burn
function (L129) and one in burnSingle
(L161,L170):
balance0 -= _toShare(token0, amount0); // idem 1
The _toShare
conversion should be wrong, and can cause serious accounting issues.
Replace with balance0 -= amount0;
#0 - maxsam4
2021-10-06T08:28:23Z
Those variables are not used anywhere after they are updated. therefore, this is a no-op. Non-critical issue IMO.
#1 - alcueca
2021-10-25T06:13:06Z
-No assets at risk, but function incorrect as to spec. Sev 1.
🌟 Selected for report: 0xsanson
70.5984 SUSHI - $882.48
0xsanson
In HybridPool's _computeLiquidityFromAdjustedBalances
there's the following operation:
D = (((N_A * s) / A_PRECISION + 2 * dP) * D) / ((N_A / A_PRECISION - 1) * D + 3 * dP);
The division by A_PRECISION
in N_A / A_PRECISION
can be moved after a multiplication to lose less precision. See the implementation by Curve.
Consider changing the order of the operations like in the link provided.
🌟 Selected for report: 0xsanson
70.5984 SUSHI - $882.48
0xsanson
The _getY
and _getYD
functions in HybridPool contain the following:
c = (c * D) / ((N_A * 2) / A_PRECISION);
Consider rewriting it like this, since performing the multiplications before the division lowers the risk of losing precision.
c = c * D * A_PRECISION / (N_A * 2);
There shouldn't be any overflow since Curve does it this way and it hasn't encountered any problem yet.
#0 - maxsam4
2021-10-05T13:06:29Z
14.5161 SUSHI - $181.45
0xsanson
In ConstantProductPool's _update
, if (blockTimestampLast == 0)
can be rewritten to if (_blockTimestampLast == 0)
since the variable in memory is equal to the storage so far. This saves an SLOAD.
#0 - sarangparikh22
2021-10-05T08:51:17Z
Duplicate of #16