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
Rank: 17/113
Findings: 4
Award: $396.25
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: 0xSmartContract
Also found by: 0xDecorativePineapple, Jeiwan, berndartmueller, brgltd, kaden, rbserver
https://github.com/code-423n4/2022-09-quickswap/blob/main/src/core/contracts/AlgebraPool.sol#L589-L623 https://github.com/code-423n4/2022-09-quickswap/blob/main/src/core/contracts/libraries/TransferHelper.sol#L16-L23
The current transfers will not check if the to
address is for an existing token contract. This can cause loss of funds if an user attempts to make a swap for a tokens added to a pool and destructed later.
swap()
for TokenB and TokenA, which will call safeTransfer()
function swap( address recipient, bool zeroToOne, int256 amountRequired, uint160 limitSqrtPrice, bytes calldata data ) external override returns (int256 amount0, int256 amount1) { uint160 currentPrice; int24 currentTick; uint128 currentLiquidity; uint256 communityFee; ... _swapCallback(amount0, amount1, data); // callback to get tokens from the caller require(balance0Before.add(uint256(amount0)) <= balanceToken0(), 'IIA');
function safeTransfer( address token, address to, uint256 value ) internal { (bool success, bytes memory data) = token.call(abi.encodeWithSelector(IERC20Minimal.transfer.selector, to, value)); require(success && (data.length == 0 || abi.decode(data, (bool))), 'TF'); }
Manual review
Add a check for contract existence in TransferHelper.safeTransfer()
.
#0 - debych
2022-10-04T13:09:09Z
duplicate of #267
#1 - 0xean
2022-10-05T01:40:32Z
dupe of #86
🌟 Selected for report: 0xSmartContract
Also found by: 0xDecorativePineapple, 0xNazgul, 0xmatt, Jeiwan, Trust, berndartmueller, brgltd, catchup, ch13fd357r0y3r, cryptonue, ladboy233, minhtrng, neko_nyaa, rbserver, rvierdiiev, s3cunda
35.4829 USDC - $35.48
An attacker can frontrun the initialize()
function in AlgebraPool.sol
to set an unexpected price and can cause loss of funds for the initial LP deposit.
function initialize(uint160 initialPrice) external override { require(globalState.price == 0, 'AI'); // getTickAtSqrtRatio checks validity of initialPrice inside int24 tick = TickMath.getTickAtSqrtRatio(initialPrice); uint32 timestamp = _blockTimestamp(); IDataStorageOperator(dataStorageOperator).initialize(timestamp, tick); globalState.price = initialPrice; globalState.unlocked = true; globalState.tick = tick; emit Initialize(initialPrice, tick); }
AlgebraPool.initialize()
function and set a incorrect price for the pool.Ensure initialize()
has access control - can only be called by the owner or by trusted addresses - e.g. by quickswap frontends or verified external addresses.
Another alternative would be to run initialize in the constructor since it should only be initialized once.
#0 - sameepsi
2022-10-04T07:03:19Z
duplicate of #84
🌟 Selected for report: 0xNazgul
Also found by: 0x1f8b, 0x52, 0xDecorativePineapple, 0xSmartContract, 0xmatt, Aeros, Aymen0909, Bnke0x0, Chom, CodingNameKiki, Deivitto, DimitarDimitrov, IllIllI, JC, Jeiwan, Lambda, Matin, Migue, Mukund, Ocean_Sky, Olivierdem, RaymondFam, RockingMiles, Rolezn, Ruhum, Satyam_Sharma, Shinchan, Tomo, Trabajo_de_mates, V_B, Waze, __141345__, a12jmx, ajtra, asutorufos, aysha, brgltd, bulej93, carrotsmuggler, catchup, cccz, chrisdior4, cryptonue, cryptphi, d3e4, defsec, delfin454000, durianSausage, erictee, fatherOfBlocks, gogo, kaden, karanctf, ladboy233, lukris02, mahdikarimi, martin, mics, natzuu, oyc_109, p_crypt0, pedr02b2, rbserver, reassor, rotcivegaf, rvierdiiev, sikorico, slowmoses, sorrynotsorry, tnevler, trustindistrust
89.3284 USDC - $89.33
Lack of two-step procedure for critical operations leaves them error-prone. Consider adding two step procedure on the critical functions.
function setOwner(address _owner) external override onlyOwner {
https://github.com/code-423n4/2022-09-quickswap/blob/main/src/core/contracts/AlgebraFactory.sol#L77
function setFarmingAddress(address _farmingAddress) external override onlyOwner {
https://github.com/code-423n4/2022-09-quickswap/blob/main/src/core/contracts/AlgebraFactory.sol#L84
function setVaultAddress(address _vaultAddress) external override onlyOwner {
https://github.com/code-423n4/2022-09-quickswap/blob/main/src/core/contracts/AlgebraFactory.sol#L91
function setCommunityFee(uint8 communityFee0, uint8 communityFee1) external override lock onlyFactoryOwner {
https://github.com/code-423n4/2022-09-quickswap/blob/main/src/core/contracts/AlgebraPool.sol#L952
function setIncentive(address virtualPoolAddress) external override {
https://github.com/code-423n4/2022-09-quickswap/blob/main/src/core/contracts/AlgebraPool.sol#L959
function setLiquidityCooldown(uint32 newLiquidityCooldown) external override onlyFactoryOwner {
https://github.com/code-423n4/2022-09-quickswap/blob/main/src/core/contracts/AlgebraPool.sol#L967
assert(false);
Assert should be avoided in production code. As described on the solidity docs. "The assert function creates an error of type Panic(uint256). … Properly functioning code should never create a Panic, not even on invalid external input. If this happens, then there is a bug in your contract which you should fix." Even if the code on line 190 is expected to be unreacheable, consider using a require statement instead of assert.
Missing checks for zero-addresses may lead to infunctional protocol, if the variable addresses are updated incorrectly.
constructor(address _poolDeployer, address _vaultAddress) {
https://github.com/code-423n4/2022-09-quickswap/blob/main/src/core/contracts/AlgebraFactory.sol#L50
function setOwner(address _owner) external override onlyOwner {
https://github.com/code-423n4/2022-09-quickswap/blob/main/src/core/contracts/AlgebraFactory.sol#L77
function setFarmingAddress(address _farmingAddress) external override onlyOwner {
https://github.com/code-423n4/2022-09-quickswap/blob/main/src/core/contracts/AlgebraFactory.sol#L84
function setVaultAddress(address _vaultAddress) external override onlyOwner {
https://github.com/code-423n4/2022-09-quickswap/blob/main/src/core/contracts/AlgebraFactory.sol#L91
function setIncentive(address virtualPoolAddress) external override {
https://github.com/code-423n4/2022-09-quickswap/blob/main/src/core/contracts/AlgebraPool.sol#L959
The arguments liquidity
and volumePerLiquidityInBlock
in the function _writeTimepoint()
in the contract AlgebraPool.sol
are showdowed by state variables with the same name from the contract PoolState.sol
. Considering renaming these function arguments to avoid shadowing.
function _writeTimepoint( uint16 timepointIndex, uint32 blockTimestamp, int24 tick, uint128 liquidity, uint128 volumePerLiquidityInBlock ) private returns (uint16 newTimepointIndex) { return IDataStorageOperator(dataStorageOperator).write(timepointIndex, blockTimestamp, tick, liquidity, volumePerLiquidityInBlock); }
Some functions return named variables, others return explicit values.
Following function return explicit value
function balanceToken0() private view returns (uint256) {
https://github.com/code-423n4/2022-09-quickswap/blob/main/src/core/contracts/AlgebraPool.sol#L70
Following function return named variables
function collect( address recipient, int24 bottomTick, int24 topTick, uint128 amount0Requested, uint128 amount1Requested ) external override lock returns (uint128 amount0, uint128 amount1) {
Consider adopting a consistent approach to return values throughout the codebase by removing all named return variables, explicitly declaring them as local variables, and adding the necessary return statements where appropriate. This would improve both the explicitness and readability of the code, and it may also help reduce regressions during future code refactors.
The solidity documentation recommends a maximum of 120 characters.
Consider adding a limit of 120 characters or less to prevent large lines.
(uint128 currentLiquidity, uint32 lastLiquidityAddTimestamp) = (_position.liquidity, _position.lastLiquidityAddTimestamp);
https://github.com/code-423n4/2022-09-quickswap/blob/main/src/core/contracts/AlgebraPool.sol#L221
(, int256 amount0Int, int256 amount1Int) = _updatePositionTicksAndFees(recipient, bottomTick, topTick, int256(liquidityActual).toInt128());
https://github.com/code-423n4/2022-09-quickswap/blob/main/src/core/contracts/AlgebraPool.sol#L472
Consider modifying the order of functions in AlgebraPool.sol
. The solidty documentation recommends this order:
constructor receive function (if exists) fallback function (if exists) external public internal private
The snippet bellow shows private above external
function balanceToken0() private view returns (uint256) { return IERC20Minimal(token0).balanceOf(address(this)); } function balanceToken1() private view returns (uint256) { return IERC20Minimal(token1).balanceOf(address(this)); } /// @inheritdoc IAlgebraPoolState function timepoints(uint256 index) external view override returns ( bool initialized, uint32 blockTimestamp, int56 tickCumulative, uint160 secondsPerLiquidityCumulative, uint88 volatilityCumulative, int24 averageTick, uint144 volumePerLiquidityCumulative ) { return IDataStorageOperator(dataStorageOperator).timepoints(index); }
https://github.com/code-423n4/2022-09-quickswap/blob/main/src/core/contracts/AlgebraPool.sol#L70-L94
{ (int256 amount0Int, int256 amount1Int, ) = _getAmountsForLiquidity( bottomTick, topTick, int256(liquidityDesired).toInt128(), globalState.tick, globalState.price ); amount0 = uint256(amount0Int); amount1 = uint256(amount1Int); }
{ 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'); }
Some downcasting oprations are using the SafeCast library contract
Following examples are using SafeCast.
(, int256 amount0Int, int256 amount1Int) = _updatePositionTicksAndFees(recipient, bottomTick, topTick, int256(liquidityActual).toInt128());
https://github.com/code-423n4/2022-09-quickswap/blob/main/src/core/contracts/AlgebraPool.sol#L472
-int256(amount).toInt128()
https://github.com/code-423n4/2022-09-quickswap/blob/main/src/core/contracts/AlgebraPool.sol#L522
Following examples are not using SafeCast.
fees0 = uint128(FullMath.mulDiv(innerFeeGrowth0Token - _innerFeeGrowth0Token, currentLiquidity, Constants.Q128));
https://github.com/code-423n4/2022-09-quickswap/blob/main/src/core/contracts/AlgebraPool.sol#L247
liquidityActual = uint128(FullMath.mulDiv(uint256(liquidityActual), receivedAmount0, amount0));
https://github.com/code-423n4/2022-09-quickswap/blob/main/src/core/contracts/AlgebraPool.sol#L460
Not using SafeCast for all downcasting operations can cause silent overflows. Normalizing SafeCast if preferred.
All the contracts in scope are using 0.7.6.
Use a solidity version of at least 0.8.0 to get overflow protection without SafeMath (Manual downcasting still needs overflow checks even with 0.8).
Use a solidity version of at least 0.8.2 to get compiler automatic inlining.
Use a solidity version of at least 0.8.3 to get better struct packing and cheaper multiple storage reads.
Use a solidity version of at least 0.8.4 to get custom errors, which are cheaper at deployment than revert()/require() strings.
Consider document all functions to improve documentation.
function getNewPrice(
res += (xLowestDegree * g) / 5040 + (xLowestDegree * x) / (40320);
🌟 Selected for report: IllIllI
Also found by: 0x1f8b, 0x5rings, 0xNazgul, 0xRoxas, 0xSmartContract, 0xbepresent, 0xmatt, Aeros, Amithuddar, Awesome, Aymen0909, B2, Bnke0x0, ChristianKuri, CodingNameKiki, Deivitto, Diraco, Fitraldys, HardlyCodeMan, JC, Mukund, Noah3o6, Olivierdem, RaymondFam, ReyAdmirado, RockingMiles, Rolezn, Ruhum, Saintcode_, Shinchan, SnowMan, TomJ, Tomio, Tomo, V_B, Waze, __141345__, ajtra, asutorufos, aysha, beardofginger, bobirichman, brgltd, bulej93, c3phas, ch0bu, cryptonue, defsec, delfin454000, dharma09, durianSausage, emrekocak, erictee, fatherOfBlocks, francoHacker, gianganhnguyen, gogo, imare, kaden, karanctf, ladboy233, lukris02, m_Rassska, martin, medikko, mics, natzuu, oyc_109, peiw, rbserver, ret2basic, rotcivegaf, saian, shark, slowmoses, tnevler, trustindistrust, zeesaw, zishansami
24.3033 USDC - $24.30
The following changes would optimize gas for the loop highlighted bellow.
File: src/core/contracts/libraries/DataStorage.sol 307: for (uint256 i = 0; i < secondsAgos.length; i++) {
Replace > 0
with != 0
for unsigned integers.
There are 30 instances of this issue.
File: src/core/contracts/AlgebraPool.sol 224: require(currentLiquidity > 0, 'NP'); // Do not recalculate the empty ranges 228: if (_liquidityCooldown > 0) { 237: liquidityNext > 0 ? (liquidityDelta > 0 ? _blockTimestamp() : lastLiquidityAddTimestamp) : 0 434: require(liquidityDesired > 0, 'IL'); 451: if (amount0 > 0) receivedAmount0 = balanceToken0(); 452: if (amount1 > 0) receivedAmount1 = balanceToken1(); 454: if (amount0 > 0) require((receivedAmount0 = balanceToken0() - receivedAmount0) > 0, 'IIAM'); 455: if (amount1 > 0) require((receivedAmount1 = balanceToken1() - receivedAmount1) > 0, 'IIAM'); 469: require(liquidityActual > 0, 'IIL2'); 505: if (amount0 > 0) TransferHelper.safeTransfer(token0, recipient, amount0); 506: if (amount1 > 0) TransferHelper.safeTransfer(token1, recipient, amount1); 617: if (communityFee > 0) { 641: require((amountRequired = int256(balanceToken0().sub(balance0Before))) > 0, 'IIA'); 645: require((amountRequired = int256(balanceToken1().sub(balance1Before))) > 0, 'IIA'); 667: if (communityFee > 0) { 734: (cache.amountRequiredInitial, cache.exactInput) = (amountRequired, amountRequired > 0); 808: if (cache.communityFee > 0) { 814: if (currentLiquidity > 0) cache.totalFeeGrowth += FullMath.mulDiv(step.feeAmount, Constants.Q128, currentLiquidity); 898: require(_liquidity > 0, 'L'); 904: if (amount0 > 0) { 911: if (amount1 > 0) { 924: if (paid0 > 0) { 927: if (_communityFeeToken0 > 0) { 938: if (paid1 > 0) { 941: if (_communityFeeToken1 > 0) {
File: src/core/contracts/DataStorageOperator.sol 138: if (volume >= 2**192) volumeShifted = (type(uint256).max) / (liquidity > 0 ? liquidity : 1); 139: else volumeShifted = (volume << 64) / (liquidity > 0 ? liquidity : 1);
File: src/core/contracts/libraries/DataStorage.sol 80: last.secondsPerLiquidityCumulative += ((uint160(delta) << 128) / (liquidity > 0 ? liquidity : 1)); // just timedelta if liquidity == 0
File: src/core/contracts/libraries/PriceMovementMath.sol 52: require(price > 0); 53: require(liquidity > 0);
Note that the project would need to update to solidity v0.8.4 to be able to use custom errors.
There are 34 instances of this issue.
File: src/core/contracts/AlgebraFactory.sol 109: require(uint256(alpha1) + uint256(alpha2) + uint256(baseFee) <= type(uint16).max, 'Max fee exceeded'); 110: require(gamma1 != 0 && gamma2 != 0 && volumeGamma != 0, 'Gammas must be > 0');
File: src/core/contracts/AlgebraPool.sol 60: require(topTick < TickMath.MAX_TICK + 1, 'TUM'); 61: require(topTick > bottomTick, 'TLU'); 62: require(bottomTick > TickMath.MIN_TICK - 1, 'TLM'); 194: require(globalState.price == 0, 'AI'); 224: require(currentLiquidity > 0, 'NP'); // Do not recalculate the empty ranges 434: require(liquidityDesired > 0, 'IL'); 454: if (amount0 > 0) require((receivedAmount0 = balanceToken0() - receivedAmount0) > 0, 'IIAM'); 455: if (amount1 > 0) require((receivedAmount1 = balanceToken1() - receivedAmount1) > 0, 'IIAM'); 469: require(liquidityActual > 0, 'IIL2'); 474: require((amount0 = uint256(amount0Int)) <= receivedAmount0, 'IIAM2'); 475: require((amount1 = uint256(amount1Int)) <= receivedAmount1, 'IIAM2'); 608: require(balance0Before.add(uint256(amount0)) <= balanceToken0(), 'IIA'); 614: require(balance1Before.add(uint256(amount1)) <= balanceToken1(), 'IIA'); 636: require(globalState.unlocked, 'LOK'); 641: require((amountRequired = int256(balanceToken0().sub(balance0Before))) > 0, 'IIA'); 645: require((amountRequired = int256(balanceToken1().sub(balance1Before))) > 0, 'IIA'); 731: require(unlocked, 'LOK'); 733: require(amountRequired != 0, 'AS'); 739: require(limitSqrtPrice < currentPrice && limitSqrtPrice > TickMath.MIN_SQRT_RATIO, 'SPL'); 743: require(limitSqrtPrice > currentPrice && limitSqrtPrice < TickMath.MAX_SQRT_RATIO, 'SPL'); 898: require(_liquidity > 0, 'L'); 921: require(balance0Before.add(fee0) <= paid0, 'F0'); 935: require(balance1Before.add(fee1) <= paid1, 'F1');
File: src/core/contracts/DataStorageOperator.sol 27: require(msg.sender == pool, 'only pool can call this'); 45: require(uint256(_feeConfig.alpha1) + uint256(_feeConfig.alpha2) + uint256(_feeConfig.baseFee) <= type(uint16).max, 'Max fee exceeded'); 46: require(_feeConfig.gamma1 != 0 && _feeConfig.gamma2 != 0 && _feeConfig.volumeGamma != 0, 'Gammas must be > 0');
File: src/core/contracts/base/PoolState.sol 41: require(globalState.unlocked, 'LOK');
File: src/core/contracts/libraries/DataStorage.sol 238: require(lteConsideringOverflow(self[oldestIndex].blockTimestamp, target, time), 'OLD');
File: src/core/contracts/libraries/PriceMovementMath.sol 70: require((product = amount * price) / amount == price); // if the product overflows, we know the denominator underflows 71: require(liquidityShifted > product); // in addition, we must check that the denominator does not underflow
File: src/core/contracts/libraries/TickManager.sol 96: require(liquidityTotalAfter < Constants.MAX_LIQUIDITY_PER_TICK + 1, 'LO');
File: src/core/contracts/libraries/TickTable.sol 15: require(tick % Constants.TICK_SPACING == 0, 'tick is not spaced'); // ensure that the tick is spaced
There are 2 instances of this issue.
File: src/core/contracts/libraries/AdaptiveFee.sol 88: res += (xLowestDegree * gHighestDegree) / 2; 96: res += (xLowestDegree * gHighestDegree) / 24;
The values can still be inspected on the source code if necessary
There's 1 instance of this issue.
File: src/core/contracts/libraries/DataStorage.sol 12: uint32 public constant WINDOW = 1 days;
There are 2 instances of this issue.
File: src/core/contracts/AlgebraPool.sol 257: _position.fees0 += fees0; 258: _position.fees1 += fees1;