Platform: Code4rena
Start Date: 30/09/2021
Pot Size: $100,000 SUSHI
Total HM: 24
Participants: 7
Period: 7 days
Judge: alcueca
Total Solo HM: 16
Id: 35
League: ETH
Rank: 2/7
Findings: 10
Award: $22,095.11
🌟 Selected for report: 9
🚀 Solo Findings: 4
169.9968 SUSHI - $2,124.96
broccoli
In the burn
function of ConcentratedLiquidityPool
, when calling _updatePosition
, the amount of liquidity to burn is explicitly converted from uint128
to int128
, which could result in a positive integer if amount
is larger than (1 << 127)
and less than (1 << 128)
(i.e., the MSB of amount
is set). As a result, the pool's liquidity decreases, but the attacker's liquidity at a specific location increases. The incorrect liquidity
could affect all trades in the pool afterward.
500000
. Consider an attacker calling the burn
function with lower = upper = 50000
, and amount = (1 << 128) - 100000
.amount0
and amount1
to be withdrawn from the pool will both be 0
. The pool's liquidity
is decreased by amount
(and could underflow to a large number)._updatePosition
function is called, the liquidity to update is calculated by -int128(amount) = -int128((1 << 128) - 100000) = -(-100000) = 100000
, which causes the attacker's liquidity at positions[attacker][lower][lower]
to be increased by 100000
.Referenced code: ConcentratedLiquidityPool.sol#L252
In the burn
function, check the amount
exceeds type(uint128).max
or not. If so, then revert the transaction. Or consider using the SafeCast
library from OpenZeppelin instead.
#0 - sarangparikh22
2021-10-11T13:50:24Z
Duplicate of #50
49.5711 SUSHI - $619.64
broccoli
In the subscribe
function of ConcentratedLiquidityPoolManager
, the incentive
to subscribed is determined as follows:
Incentive memory incentive = incentives[pool][positionId];
However, positionId
should be incentiveId
, a counter that increases by one whenever a new incentive is added to the pool. The usage of positionId
could cause the wrong incentive to be used, and in general, the incentive is not found, and the transaction reverts (the condition block.timestamp < incentive.endTime
is not met). The getReward
and claimReward
functions have the bug of misusing positionId
as the index of incentives.
Referenced code: ConcentratedLiquidityPoolManager.sol#L68 ConcentratedLiquidityPoolManager.sol#L87 ConcentratedLiquidityPoolManager.sol#L105
Change positionId
to incentiveId
in the referenced lines of code.
#0 - sarangparikh22
2021-10-12T09:44:56Z
Duplicate of #39. Disagree with severity, it should be 2.
#1 - alcueca
2021-11-12T10:44:22Z
I'm choosing this one as the main description, since it mentions the three lines in which the error can be found.
49.5711 SUSHI - $619.64
broccoli
The reclaimIncentive
function of ConcentratedLiquidityPoolManager
does not update the rewardsUnclaimed
variable after some rewards are reclaimed. Thus, an attacker could add an incentive with a corresponding token, such as DAI, and reclaim the incentive multiple times to drain all the DAI within the manager contract.
Referenced code: ConcentratedLiquidityPoolManager.sol#L59-L60
Subtract incentive.rewardsUnclaimed
by amount
before the _transfer
(which prevents reentrancy attacks).
#0 - sarangparikh22
2021-10-11T13:54:41Z
Duplicate of #37
🌟 Selected for report: broccoli
377.7706 SUSHI - $4,722.13
broccoli
Similar to a previous finding in the IndexPool
contract, the mint
function of ConcentratedLiquidityPool
allows integer overflows when checking the balance is larger or equal to the received amount of token plus the reserve. As a result, the attacker could get a large amount of liquidity but only provide a small number of tokens to the pool, effectively stealing other LPs' funds when burning his liquidity.
Notice that this bug is independent of another bug of incorrect casting uint256
type to uint128
in the _getAmountsForLiquidity
function. Even if the previously mentioned bug does not exist, the attacker could still steal the funds in the pool by exploiting this bug.
500000
, an attacker calls the mint
function with the following parameters:mintParams.lower = 100000 mintParams.upper = 500000 mintParams.amount1Desired = (1 << 128) - 47541305835 # a carefully chosen number mintParams.amount0Desired = 0
_liquidity = mintParams.amount1Desired * (1 << 96) // (priceUpper - priceLower) = 4731732988155153573010127839
token0
and token1
that the attacker has to pay isamount0Actual = 0 amount1Actual = uint128(DyDxMath.getDy(_liquidity, priceLower, priceUpper, true)) = uint128(_liquidity * (priceUpper - priceLower) // (1 << 96)) # round up = uint128(340282366920938463463374607384226905622) = 340282366920938463463374607384226905622 = (1 << 128) - 47541305834
reserve1
is greater than 47541305834
, the addition amount1Actual + reserve1
overflows to a small number, causing the attacker to pass the balance check.Referenced code: ConcentratedLiquidityPool.sol#L204 ConcentratedLiquidityPool.sol#L209
Consider removing the unchecked
statement to check for integer overflow or casting both amount1Actual
and reserve1
to type uint256
before adding them and comparing to the _balance(token)
.
#0 - sarangparikh22
2021-10-28T21:26:06Z
The example is wrong, you can't add use upper tick as odd, correct the example and resubmit please.
🌟 Selected for report: broccoli
377.7706 SUSHI - $4,722.13
broccoli
The _getAmountsForLiquidity
function of ConcentratedLiquidityPool
explicitly converts the result of DyDxMath.getDy
and DyDxMath.getDx
from type uint256
to type uint128
. The explicit casting without checking whether the integer exceeds the maximum number (i.e., type(uint128).max
) could cause incorrect results being used. Specifically, an attacker could exploit this bug to mint a large amount of liquidity but only pay a little of token0
or token1
to the pool and effectively steal other's funds when burning his liquidity.
500000
, an attacker calls the mint
function with the following parameters:mintParams.lower = 100000 mintParams.upper = 500000 mintParams.amount1Desired = (1 << 128) + 71914955423 # a carefully chosen number mintParams.amount0Desired = 0
_liquidity = mintParams.amount1Desired * (1 << 96) // (priceUpper - priceLower) = 4731732988155153573010127840
token0
and token1
that the attacker has to pay isamount0Actual = 0 amount1Actual = uint128(DyDxMath.getDy(_liquidity, priceLower, priceUpper, true)) = uint128(_liquidity * (priceUpper - priceLower) // (1 << 96)) # round up = uint128(340282366920938463463374607456141861046) # exceed the max = 24373649590 # truncated
24373649590
of token1
to get 4731732988155153573010127840
of the liquidity, which he could burn to get more token1
. As a result, the attacker is stealing the funds from the pool and could potentially drain it.Referenced code: ConcentratedLiquidityPool.sol#L480 concentratedPool/DyDxMath.sol#L15 concentratedPool/DyDxMath.sol#L30
Check whether the result of DyDxMath.getDy
or DyDxMath.getDx
exceeds type(uint128).max
or not. If so, then revert the transaction. Or consider using the SafeCast
library from OpenZeppelin instead.
#0 - sarangparikh22
2021-10-28T21:27:11Z
The example is wrong, you can't add use upper tick as odd, correct the example and resubmit please.
#1 - alcueca
2021-11-12T11:06:48Z
@sarangparikh22, could you confirm whether the casting to uint128 is known to be safe? Are you unconvinced of the issue?
#2 - sarangparikh22
2021-11-16T23:19:23Z
@alcueca I can confirm casting to uint128 is not safe, and will lead to overflow. However, the example mentioned is wrong.
#3 - alcueca
2021-11-18T06:53:42Z
Understood. I will uphold the severity 3 because the overflow happens in a critical function for the management of funds and an incorrect execution will likely lead to loss of funds.
🌟 Selected for report: broccoli
377.7706 SUSHI - $4,722.13
broccoli
The function RangeFeeGrowth
ConcentratedLiquidityPool.sol#L601-L633 would revert the transaction in some cases.
When a pool cross a tick, it only updates either feeGrowthOutside0
or feeGrowthOutside1
. Ticks.sol#L23-L53
RangeFeeGrowth
calculates the fee as follow:
feeGrowthInside0 = _feeGrowthGlobal0 - feeGrowthBelow0 - feeGrowthAbove0; feeGrowthInside1 = _feeGrowthGlobal1 - feeGrowthBelow1 - feeGrowthAbove1;
feeGrowthBelow + feeGrowthAbove
is not necessary smaller than _feeGrowthGlobal
. Please see POC
.
Users can not provide liquidity or burn liquidity. Fund will get stocked in the contract. I consider this is a high-risk issue.
# This is the wrapper. # def add_liquidity(pool, amount, lower, upper) # def swap(pool, buy, amount) add_liquidity(pool, deposit_amount, -800, 500) add_liquidity(pool, deposit_amount, 400, 700) # We cross the tick here to trigger the bug. swap(pool, False, deposit_amount) # Only tick 700's feeGrowthOutside1 is updated swap(pool, True, deposit_amount) # Only tick 500's feeGrowthOutside0 is updated # current tick at -800 # this would revert # feeGrowthBelow1 = feeGrowthGlobal1 # feeGrowthGlobal1 - feeGrowthBelow1 - feeGrowthAbove1 would revert # user would not be able to mint/withdraw/cross this tick. The pool is broken add_liquidity(pool, deposit_amount, 400, 700)
Hardhat
It's either modify the tick's algo or RangeFeeGrowth
. The quick-fix I come up with is to deal with the fee in RangeFeeGrowth
. However, I recommend the team to go through tick's logic again.
#0 - sarangparikh22
2021-10-13T23:40:02Z
The example is wrong, you can't add use upper tick as odd, correct the example and resubmit please.
#1 - alcueca
2021-11-12T13:44:49Z
@sarangparikh22, is the example invalid, or the whole issue? Is this something that you would consider fixing?
#2 - sarangparikh22
2021-11-17T23:09:11Z
@alcueca The example is invalid, but the issue is valid, the fix is to swap the condition of feeGrowthGlobal
68.8487 SUSHI - $860.61
broccoli
ConcentratedLiquidityPool.sol#L263-L266
The dev mistakenly write:
reserve0 -= uint128(amount0fees); reserve1 -= uint128(amount1fees);
It should be
reserve0 -= uint128(amount0fees); reserve1 -= uint128(amount1fees);
Other users can't mint lp unless they first send the extra tokens to match the reserve. I consider this is a high-risk issue.
None
None
I believe it's a mistake.
reserve0 -= uint128(amount0fees); reserve1 -= uint128(amount1fees);
#0 - sarangparikh22
2021-10-11T14:07:00Z
The solution advised is same as the bug. It should be:
unchecked { amount0 += amount0fees; amount1 += amount1fees; } unchecked { reserve0 -= uint128(amount0); reserve1 -= uint128(amount1); }
However, I'm still approving this issue, as the title reflects the correct issue.
Duplicate of #51
14.8713 SUSHI - $185.89
broccoli
The claimReward
function of ConcentratedLiquidityPoolManager
calculates the secondsUnclaimed
variable using a formula with an unclear intention:
uint256 secondsUnclaimed = (maxTime - incentive.startTime) << (128 - incentive.secondsClaimed);
This formula causes an integer underflow error when incentive.secondsClaimed
is greater than 128, which generally happens in most cases since incentive.secondsClaimed
is scaled by a factor of 1 << 128
. The integer underflow reverts the transaction and prevents anyone from claiming the reward.
Referenced code: ConcentratedLiquidityPoolManager.sol#L93 ConcentratedLiquidityPoolManager.sol#L110
Fix the calculation of secondsUnclaimed
.
#0 - sarangparikh22
2021-10-12T09:47:09Z
Duplicate of #41. Disagree with severity, should be 2.
#1 - alcueca
2021-11-12T10:37:59Z
Assets are not at risk of being lost, but functionality is impacted. Severity 2.
🌟 Selected for report: broccoli
113.3312 SUSHI - $1,416.64
broccoli
There could be an integer underflow error when the reward of an incentive is claimed, forcing users to wait for a sufficient period or reduce their liquidity to claim the rewards.
The unclaimed reward that a user could claim is proportional to the secondsInside
, which is, in fact, proportional to the position's liquidity. It is possible that the liquidity is too large and causes secondsInside
to be larger than secondsUnclaimed
. As a result, the rewards that the user wants to claim exceed the incentive.rewardsUnclaimed
and causes an integer underflow error, which prevents him from getting the rewards.
Referenced code: ConcentratedLiquidityPoolManager.sol#L94-L95
Check whether the rewards
exceeds the incentive.rewardsUnclaimed
. If so, then send only incentive.rewardsUnclaimed
amount of rewards to the user.
#0 - sarangparikh22
2021-10-13T20:43:37Z
The problem seems very unlikely to happen, would be great to see a POC.
16.9997 SUSHI - $212.50
broccoli
Unlocked/floating pragmas are used in the contracts (i.e., pragma solidity >=0.8.0;
). Locking the pragma ensures that the contracts are not accidentally deployed using an outdated compiler version with unfixed bugs.
Referenced code:
Please use grep -R "pragma" .
to find the unlocked pragmas.
Lock pragmas to a specific Solidity version. Consider the compiler bugs in the following links and ensure that they do not affect the contracts. It is also recommended to use the latest version of Solidity when writing and deploying contracts (see Solidity docs).
Related links: Solidity repo - known bugs Solidity repo - bugs by version
#0 - sarangparikh22
2021-10-11T13:04:37Z
Duplicate of #3
#1 - alcueca
2021-11-12T10:53:28Z
Severity 1 is correct
🌟 Selected for report: broccoli
37.7771 SUSHI - $472.21
broccoli
In the burn
and swap
functions of ConcentratedLiquidityPool
, the lower tick is not explicitly checked to be less than the upper tick. Besides, the ticks are not checked to be at least the minimum tick and at most the maximum tick.
Referenced code: Ticks.sol#L68-L70
Add sanity checks on the lower and upper ticks in critical functions (see the referenced line of code, for example).
#0 - sarangparikh22
2021-10-13T22:25:59Z
This doesn't seem reachable, can you give a POC?
#1 - alcueca
2021-11-13T06:38:17Z
@sarangparikh22, I don't understand what it is that you mean by not reachable, could you please elaborate?
#2 - sarangparikh22
2021-11-16T21:38:17Z
If we provide an invalid tick condition, such as lower is higher than upper, it would fail at the FullMath check, as it would overflow and we would get a transaction revert. However, it is better to fail early with a proper message. What I meant by unreachable is, I am unable to find a condition where this condition holds true and we are able to swap or burn with wrong tick specs. We'll make the changes to fail early by checking ticks, and will mark this issue as valid, since it makes sense to just check it early, even though it would fail. @alcueca
🌟 Selected for report: broccoli
37.7771 SUSHI - $472.21
broccoli
The _updatePosition
function of ConcentratedLiquidityPool
uses the <
operator to ensure a user's liquidity does not exceed the maximum. However, we should use the <=
operator instead.
Referenced code: ConcentratedLiquidityPool.sol#L549
Change <
to <=
in the referenced line of code.
🌟 Selected for report: broccoli
37.7771 SUSHI - $472.21
broccoli
In the swap
function of ConcentratedLiquidityPool
, the following line of code seems to handle the timestamp overflow problem happening in year 2106:
uint256 diff = timestamp - uint256(lastObservation); /// @dev Underflow in 2106.
However, the subtraction is not within an unchecked
statement. Thus an integer underflow still occurs when the block.timestamp
overflows the maximum of uint256
.
Referenced code: ConcentratedLiquidityPool.sol#L314
Add an unchecked
statement around the referenced line of code.
🌟 Selected for report: broccoli
37.7771 SUSHI - $472.21
broccoli
_price
in the constructorConcentratedLiquidityPool.sol#L111-L137
The pool can be deployed even it's given an invalid initial price.
None
None
Do the sanity check in the constructor.
#0 - sarangparikh22
2021-10-13T20:23:05Z
There is no way to actually sanity check the price at the time of deployment,whatever price is set, the market would make sure it gets balanced. If you have an attack vector from this, please let us know.
#1 - alcueca
2021-11-12T13:41:51Z
Awaiting for warden to provide an example of an invalid initial price
#2 - sarangparikh22
2021-11-16T22:53:08Z
@alcueca as described earlier, there is no way to control the inital price of the pool, however, we are adding checks to make sure that the initial price is in the specific range according to the specs. if (TickMath.MIN_SQRT_RATIO > _price || _price >= TickMath.MAX_SQRT_RATIO) revert BadPrice();
. Not sure, if warden meant this, but would like to give benefit of doubt and would accept this as a valid finding.