QuickSwap and StellaSwap contest - imare's results

A concentrated liquidity DEX with dynamic fees.

General Information

Platform: Code4rena

Start Date: 26/09/2022

Pot Size: $50,000 USDC

Total HM: 13

Participants: 113

Period: 5 days

Judge: 0xean

Total Solo HM: 6

Id: 166

League: ETH

QuickSwap and StellaSwap

Findings Distribution

Researcher Performance

Rank: 9/113

Findings: 2

Award: $910.46

🌟 Selected for report: 0

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: rbserver

Also found by: Lambda, imare

Labels

bug
duplicate
2 (Med Risk)
sponsor confirmed

Awards

878.9235 USDC - $878.92

External Links

Lines of code

https://github.com/code-423n4/2022-09-quickswap/blob/15ea643c85ed936a92d2676a7aabf739b210af39/src/core/contracts/AlgebraPool.sol#L451-L455

Vulnerability details

AlgebraPool mint function has the ability to return excess funds to the sender. But there are two cases where the user can lose all their supplied tokens.

Impact

If the user provides/transfers more tokens than needed inside algebraMintCallback(uint256 amount0Owed, uint256 amount1Owed, bytes calldata data) callback function it will lose all tokens for the following two cases :

  • if amount0Owed is equal to zero will lose all token0 or,
  • if amount1Owed is equal to zero will lose all token1

The lost tokens are not accounted for any user positions or balance.

The case when this happen is specified by the AlgebraPool::_getAmountsForLiquidity function :

  • amount0Owed=0 when : If current tick is greater than the provided top one then only the token1 has to be provided
  • amount1Owed=0 when : If current tick is less than the provided bottom one then only the token0 has to be provided

Proof of Concept

Next example of a user losing all token1 tokens. As previously explaind the same can happen to token0 tokens.

In the case amount1Owed is equal to zero when we get called in callback:

// inside the sending contract function algebraMintCallback( uint256 amount0Owed, uint256 amount1Owed, bytes calldata data ) external { if (amount0Owed > 0) { TestERC20(pool.token0()).transfer(address(pool), amount0Owed + 11); // send also this token we should get the surplus returned to the sender TestERC20(pool.token1()).transfer(address(pool), amount1Owed + 3000); } }

When the transaction finishes the pool has correctly returned 11 token0 but no token1 has been returned. So the user lose all 3000 token1.

The 3000 token are later accounted for liquidity by the protocol with the call balanceToken1() in this case.. or balanceToken0 in the other case. But for this user the minting has gone pretty wrong.

Inside AlgebraPool::mint function instead of:

{ if (amount0 > 0) receivedAmount0 = balanceToken0(); if (amount1 > 0) receivedAmount1 = balanceToken1(); IAlgebraMintCallback(msg.sender).algebraMintCallback(amount0, amount1, data); if (amount0 > 0) require((receivedAmount0 = balanceToken0() - receivedAmount0) > 0, 'IIAM'); if (amount1 > 0) require((receivedAmount1 = balanceToken1() - receivedAmount1) > 0, 'IIAM'); }

try to always calculate receivedAmount{0,1} and then check if we need the amountX to be greater than zero, like this:

{ receivedAmount0 = balanceToken0(); receivedAmount1 = balanceToken1(); IAlgebraMintCallback(msg.sender).algebraMintCallback(amount0, amount1, data); receivedAmount0 = balanceToken0() - receivedAmount0; receivedAmount1 = balanceToken1() - receivedAmount1; if (amount0 > 0) require(receivedAmount0 > 0, 'IIAM'); if (amount1 > 0) require(receivedAmount1 > 0, 'IIAM'); }

Now surplus tokens are correctly returned to the sender.

#0 - vladyan18

2022-10-03T15:40:51Z

Thank you! The tokens will indeed not be returned. However, we need to take into account that the required amount is passed in the callback. So this situation can arise only if the contract called in the callback is implemented incorrectly.

#1 - 0xean

2022-10-06T19:21:43Z

going to lump this is as a duplicate of #255 - same root cause.

[GAS-01] Redundant zero initalization

[GAS-02] Use prefix not postfix in loops

[GAS-03] Cache array length before loop

[GAS-04] Removing unused return values

*https://github.com/code-423n4/2022-09-quickswap/blob/15ea643c85ed936a92d2676a7aabf739b210af39/src/core/contracts/AlgebraPool.sol#L84-L90

[GAS-07] Adhere to tight variable packing pattern

From :

struct Position { uint128 liquidity; // The amount of liquidity concentrated in the range uint32 lastLiquidityAddTimestamp; // Timestamp of last adding of liquidity uint256 innerFeeGrowth0Token; // The last updated fee growth per unit of liquidity uint256 innerFeeGrowth1Token; uint128 fees0; // The amount of token0 owed to a LP uint128 fees1; // The amount of token1 owed to a LP }

to

struct Position { uint256 innerFeeGrowth0Token; // The last updated fee growth per unit of liquidity uint256 innerFeeGrowth1Token; uint128 liquidity; // The amount of liquidity concentrated in the range uint128 fees0; // The amount of token0 owed to a LP uint128 fees1; // The amount of token1 owed to a LP uint32 lastLiquidityAddTimestamp; // Timestamp of last adding of liquidity }

From:

struct Cumulatives { int56 tickCumulative; uint160 outerSecondPerLiquidity; uint32 outerSecondsSpent; }

to:

struct Cumulatives { uint160 outerSecondPerLiquidity; int56 tickCumulative; uint32 outerSecondsSpent; }

From:

struct SwapCalculationCache { uint256 communityFee; // The community fee of the selling token, uint256 to minimize casts uint128 volumePerLiquidityInBlock; int56 tickCumulative; // The global tickCumulative at the moment uint160 secondsPerLiquidityCumulative; // The global secondPerLiquidity at the moment bool computedLatestTimepoint; // if we have already fetched _tickCumulative_ and _secondPerLiquidity_ from the DataOperator int256 amountRequiredInitial; // The initial value of the exact input\output amount int256 amountCalculated; // The additive amount of total output\input calculated trough the swap uint256 totalFeeGrowth; // The initial totalFeeGrowth + the fee growth during a swap uint256 totalFeeGrowthB; IAlgebraVirtualPool.Status incentiveStatus; // If there is an active incentive at the moment bool exactInput; // Whether the exact input or output is specified uint16 fee; // The current dynamic fee int24 startTick; // The tick at the start of a swap uint16 timepointIndex; // The index of last written timepoint }

to

struct SwapCalculationCache { uint256 communityFee; // The community fee of the selling token, uint256 to minimize casts int256 amountRequiredInitial; // The initial value of the exact input\output amount int256 amountCalculated; // The additive amount of total output\input calculated trough the swap uint256 totalFeeGrowth; // The initial totalFeeGrowth + the fee growth during a swap uint256 totalFeeGrowthB; uint160 secondsPerLiquidityCumulative; // The global secondPerLiquidity at the moment uint128 volumePerLiquidityInBlock; int56 tickCumulative; // The global tickCumulative at the moment int24 startTick; // The tick at the start of a swap uint16 fee; // The current dynamic fee uint16 timepointIndex; // The index of last written timepoint IAlgebraVirtualPool.Status incentiveStatus; // If there is an active incentive at the moment bool computedLatestTimepoint; // if we have already fetched _tickCumulative_ and _secondPerLiquidity_ from the DataOperator bool exactInput; // Whether the exact input or output is specified }

From

struct PriceMovementCache { uint160 stepSqrtPrice; // The Q64.96 sqrt of the price at the start of the step int24 nextTick; // The tick till the current step goes bool initialized; // True if the _nextTick is initialized uint160 nextTickPrice; // The Q64.96 sqrt of the price calculated from the _nextTick uint256 input; // The additive amount of tokens that have been provided uint256 output; // The additive amount of token that have been withdrawn uint256 feeAmount; // The total amount of fee earned within a current step }

to

struct PriceMovementCache { uint256 input; // The additive amount of tokens that have been provided uint256 output; // The additive amount of token that have been withdrawn uint256 feeAmount; // The total amount of fee earned within a current step uint160 stepSqrtPrice; // The Q64.96 sqrt of the price at the start of the step uint160 nextTickPrice; // The Q64.96 sqrt of the price calculated from the _nextTick int24 nextTick; // The tick till the current step goes bool initialized; // True if the _nextTick is initialized }

From

struct Configuration { uint16 alpha1; // max value of the first sigmoid uint16 alpha2; // max value of the second sigmoid uint32 beta1; // shift along the x-axis for the first sigmoid uint32 beta2; // shift along the x-axis for the second sigmoid uint16 gamma1; // horizontal stretch factor for the first sigmoid uint16 gamma2; // horizontal stretch factor for the second sigmoid uint32 volumeBeta; // shift along the x-axis for the outer volume-sigmoid uint16 volumeGamma; // horizontal stretch factor the outer volume-sigmoid uint16 baseFee; // minimum possible fee }

to

struct Configuration { uint32 beta1; // shift along the x-axis for the first sigmoid uint32 beta2; // shift along the x-axis for the second sigmoid uint32 volumeBeta; // shift along the x-axis for the outer volume-sigmoid uint16 alpha1; // max value of the first sigmoid uint16 alpha2; // max value of the second sigmoid uint16 gamma1; // horizontal stretch factor for the first sigmoid uint16 gamma2; // horizontal stretch factor for the second sigmoid uint16 volumeGamma; // horizontal stretch factor the outer volume-sigmoid uint16 baseFee; // minimum possible fee }

From

struct Timepoint { bool initialized; // whether or not the timepoint is initialized uint32 blockTimestamp; // the block timestamp of the timepoint int56 tickCumulative; // the tick accumulator, i.e. tick * time elapsed since the pool was first initialized uint160 secondsPerLiquidityCumulative; // the seconds per liquidity since the pool was first initialized uint88 volatilityCumulative; // the volatility accumulator; overflow after ~34800 years is desired :) int24 averageTick; // average tick at this blockTimestamp uint144 volumePerLiquidityCumulative; // the gmean(volumes)/liquidity accumulator }

to

struct Timepoint { uint160 secondsPerLiquidityCumulative; // the seconds per liquidity since the pool was first initialized uint144 volumePerLiquidityCumulative; // the gmean(volumes)/liquidity accumulator uint88 volatilityCumulative; // the volatility accumulator; overflow after ~34800 years is desired :) int56 tickCumulative; // the tick accumulator, i.e. tick * time elapsed since the pool was first initialized uint32 blockTimestamp; // the block timestamp of the timepoint int24 averageTick; // average tick at this blockTimestamp bool initialized; // whether or not the timepoint is initialized }

From

struct Tick { uint128 liquidityTotal; // the total position liquidity that references this tick int128 liquidityDelta; // amount of net liquidity added (subtracted) when tick is crossed left-right (right-left), // fee growth per unit of liquidity on the _other_ side of this tick (relative to the current tick) // only has relative meaning, not absolute — the value depends on when the tick is initialized uint256 outerFeeGrowth0Token; uint256 outerFeeGrowth1Token; int56 outerTickCumulative; // the cumulative tick value on the other side of the tick uint160 outerSecondsPerLiquidity; // the seconds per unit of liquidity on the _other_ side of current tick, (relative meaning) uint32 outerSecondsSpent; // the seconds spent on the other side of the current tick, only has relative meaning bool initialized; // these 8 bits are set to prevent fresh sstores when crossing newly initialized ticks }

to

struct Tick { // fee growth per unit of liquidity on the _other_ side of this tick (relative to the current tick) // only has relative meaning, not absolute — the value depends on when the tick is initialized uint256 outerFeeGrowth0Token; uint256 outerFeeGrowth1Token; uint160 outerSecondsPerLiquidity; // the seconds per unit of liquidity on the _other_ side of current tick, (relative meaning) uint128 liquidityTotal; // the total position liquidity that references this tick int128 liquidityDelta; // amount of net liquidity added (subtracted) when tick is crossed left-right (right-left), int56 outerTickCumulative; // the cumulative tick value on the other side of the tick uint32 outerSecondsSpent; // the seconds spent on the other side of the current tick, only has relative meaning bool initialized; // these 8 bits are set to prevent fresh sstores when crossing newly initialized ticks }
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