Platform: Code4rena
Start Date: 03/10/2023
Pot Size: $24,500 USDC
Total HM: 6
Participants: 62
Period: 3 days
Judge: LSDan
Total Solo HM: 3
Id: 288
League: ETH
Rank: 1/62
Findings: 2
Award: $3,294.78
🌟 Selected for report: 1
🚀 Solo Findings: 1
🌟 Selected for report: Banditx0x
Also found by: 0xDING99YA, 0xWaitress, 0xpiken, 3docSec, Banditx0x, adriro, emerald7017, maanas, twicek
359.9307 USDC - $359.93
If a user provides liquidity on ticks which are entered and exited a large number of times, the gas required to call the accrueConcentratedPositionTimeWeightedLiquidity
can exceed the block gas limit.
The accrueConcentratedPositionTimeWeightedLiquidity
function loops over the unbounded TickTracking array inside tickTracking_
mapping.
// Loop through all in-range time spans for the tick or up to the current time (if it is still in range) while (time < block.timestamp && tickTrackingIndex < numTickTracking) { TickTracking memory tickTracking = tickTracking_[poolIdx][i][tickTrackingIndex];
The TickTracking array is pushed with a new value everytime the corresponding tick is entered.
Hence a possible combination of too many tick movements and user's not collecting rewards/updating position for a long time can result in the amount of gas required to iterate the loop exceeding the block gas limit.
With 4000 cross tick movements across two ticks before the user claim rewards, the gas used to call the claimConcentratedRewards
function is 34M which is above the block gas limit of 30M.
To execute the demo the following changes has to be made to the current repo:
diff --git a/canto_ambient/contracts/libraries/ProtocolCmd.sol b/canto_ambient/contracts/libraries/ProtocolCmd.sol index 21a408d..7641c69 100644 --- a/canto_ambient/contracts/libraries/ProtocolCmd.sol +++ b/canto_ambient/contracts/libraries/ProtocolCmd.sol @@ -119,4 +119,5 @@ library UserCmd { //////////////////////////////////////////////////////////////////////////// uint8 constant CLAIM_CONC_REWARDS_CODE = 101; uint8 constant CLAIM_AMB_REWARDS_CODE = 102; + uint8 constant CROSS_TICKS = 103; }
diff --git a/canto_ambient/contracts/callpaths/LiquidityMiningPath.sol b/canto_ambient/contracts/callpaths/LiquidityMiningPath.sol index e6c63f7..bb511c9 100644 --- a/canto_ambient/contracts/callpaths/LiquidityMiningPath.sol +++ b/canto_ambient/contracts/callpaths/LiquidityMiningPath.sol @@ -46,11 +46,15 @@ contract LiquidityMiningPath is LiquidityMining { claimConcentratedRewards(poolHash, lowerTick, upperTick, weeksToClaim); } else if (code == UserCmd.CLAIM_AMB_REWARDS_CODE) { claimAmbientRewards(poolHash, weeksToClaim); + } else if (code == UserCmd.CROSS_TICKS){ + crossTicks(poolHash,lowerTick,upperTick); } else { revert("Invalid user command"); } } + + function claimConcentratedRewards(bytes32 poolIdx, int24 lowerTick, int24 upperTick, uint32[] memory weeksToClaim) public payable
diff --git a/canto_ambient/test_canto/TestLiquidityMining.js b/canto_ambient/test_canto/TestLiquidityMining.js index bd21a32..0e9c0eb 100644 --- a/canto_ambient/test_canto/TestLiquidityMining.js +++ b/canto_ambient/test_canto/TestLiquidityMining.js @@ -31,6 +31,8 @@ function toSqrtPrice(price) { chai.use(solidity); describe("Liquidity Mining Tests", function () { + + this.timeout(3000000); it("deploy contracts and init pool", async function () { const [owner] = await ethers.getSigners(); @@ -225,23 +227,23 @@ describe("Liquidity Mining Tests", function () { //////////////////////////////////////////////// // SAMPLE SWAP TEST (swaps 2 USDC for cNOTE) //////////////////////////////////////////////// - swapTx = await dex.swap( - cNOTE.address, // base - USDC.address, // quote - 36000, // poolIdx - false, // isBuy - false, // inBaseQty - BigNumber.from("2000000"), // qty - 0, // tip - BigNumber.from("16602069666338596454400000"), // limit price - BigNumber.from("1900000000000000000"), // min out - 0 // reserveFlag (to use surplus or not) - ); - - await swapTx.wait(); - expect(await USDC.balanceOf(owner.address)).to.equal( - BigNumber.from("999898351768") - ); + // swapTx = await dex.swap( + // cNOTE.address, // base + // USDC.address, // quote + // 36000, // poolIdx + // false, // isBuy + // false, // inBaseQty + // BigNumber.from("2000000"), // qty + // 0, // tip + // BigNumber.from("16602069666338596454400000"), // limit price + // BigNumber.from("1900000000000000000"), // min out + // 0 // reserveFlag (to use surplus or not) + // ); + + // await swapTx.wait(); + // expect(await USDC.balanceOf(owner.address)).to.equal( + // BigNumber.from("999898351768") + // ); ////////////////////////////////////////////////// // SET LIQUIDITY MINING REWARDS FOR CONCENTRATED LIQUIDITY @@ -270,7 +272,46 @@ describe("Liquidity Mining Tests", function () { tx = await dex.protocolCmd(8, setRewards, true); await tx.wait(); - await time.increase(604800 * 5); // fast forward 1000 seconds so that rewards accrue + // make the ticks exit and reenter + for(let i=1;i<=4000;i++){ + + let tickMover = abi.encode( + ["uint8", "bytes32", "int24", "int24", "uint32[]"], + [ + 103, + keccak256(poolHash), + currentTick , + currentTick +1, + [ + Math.floor(timestampBefore / 604800) * 604800 + 604800, + Math.floor(timestampBefore / 604800) * 604800 + 604800 * 2, + ], + ] + ); + tx = await dex.userCmd(8, tickMover); + await tx.wait(); + + await time.increase(100); + tickMover = abi.encode( + ["uint8", "bytes32", "int24", "int24", "uint32[]"], + [ + 103, + keccak256(poolHash), + currentTick +1, + currentTick , + [ + Math.floor(timestampBefore / 604800) * 604800 + 604800, + Math.floor(timestampBefore / 604800) * 604800 + 604800 * 2, + ], + ] + ); + + tx = await dex.userCmd(8, tickMover); + await tx.wait(); + + await time.increase(100); + } + await time.increase(604800 * 5); ////////////////////////////////////////////////// // CLAIM REWARDS ACCRUED FROM CONCENTRATED REWARDS @@ -294,7 +335,9 @@ describe("Liquidity Mining Tests", function () { ] ); tx = await dex.userCmd(8, claim); - await tx.wait(); + const receipt = await tx.wait(); + console.log(receipt.gasUsed); + expect(receipt.gasUsed > 30000000).to.be.eq(true); // get eth balanace of dex after claim const dexBalAfter = await ethers.provider.getBalance(dex.address);
Hardhat
If possible find an alternative method to calculate the user's concentrated liquidity or warn the user's about this issue.
Loop
#0 - c4-pre-sort
2023-10-08T12:50:52Z
141345 marked the issue as duplicate of #114
#1 - c4-pre-sort
2023-10-09T16:42:23Z
141345 marked the issue as sufficient quality report
#2 - c4-judge
2023-10-18T19:27:45Z
dmvt changed the severity to 3 (High Risk)
#3 - c4-judge
2023-10-18T19:28:55Z
dmvt marked the issue as satisfactory
🌟 Selected for report: maanas
2934.8503 USDC - $2,934.85
https://github.com/code-423n4/2023-10-canto/blob/40edbe0c9558b478c84336aaad9b9626e5d99f34/canto_ambient/contracts/callpaths/LiquidityMiningPath.sol#L65 https://github.com/code-423n4/2023-10-canto/blob/40edbe0c9558b478c84336aaad9b9626e5d99f34/canto_ambient/contracts/callpaths/LiquidityMiningPath.sol#L74
The owner of liquidity mining sidecar can pull the native coins that are stored in the CrocSwapDex to reward the users.
The setConcRewards
and setAmbRewards
functions doesn't check if the quoted amount of rewards are actually sent by the caller. This allows the owner to specify any total amount of native coin which available in the CrocSwapDex from which the funds will be used when distributing the rewards.
function setConcRewards(bytes32 poolIdx, uint32 weekFrom, uint32 weekTo, uint64 weeklyReward) public payable { // require(msg.sender == governance_, "Only callable by governance"); require(weekFrom % WEEK == 0 && weekTo % WEEK == 0, "Invalid weeks"); while (weekFrom <= weekTo) { concRewardPerWeek_[poolIdx][weekFrom] = weeklyReward; weekFrom += uint32(WEEK); } }
function setAmbRewards(bytes32 poolIdx, uint32 weekFrom, uint32 weekTo, uint64 weeklyReward) public payable { // require(msg.sender == governance_, "Only callable by governance"); require(weekFrom % WEEK == 0 && weekTo % WEEK == 0, "Invalid weeks"); while (weekFrom <= weekTo) { ambRewardPerWeek_[poolIdx][weekFrom] = weeklyReward; weekFrom += uint32(WEEK); } }
According to Ambient Docs they allow for deposits in native tokens.
diff --git a/canto_ambient/test_canto/TestLiquidityMining.js b/canto_ambient/test_canto/TestLiquidityMining.js index bd21a32..b917308 100644 --- a/canto_ambient/test_canto/TestLiquidityMining.js +++ b/canto_ambient/test_canto/TestLiquidityMining.js @@ -7,6 +7,7 @@ const { time } = require("@nomicfoundation/hardhat-network-helpers"); var keccak256 = require("@ethersproject/keccak256").keccak256; const chai = require("chai"); +const { network, ethers } = require("hardhat"); const abi = new AbiCoder(); const BOOT_PROXY_IDX = 0; @@ -218,7 +219,6 @@ describe("Liquidity Mining Tests", function () { ); tx = await dex.userCmd(2, mintConcentratedLiqCmd, { gasLimit: 6000000, - value: ethers.utils.parseUnits("10", "ether"), }); await tx.wait(); @@ -243,6 +243,17 @@ describe("Liquidity Mining Tests", function () { BigNumber.from("999898351768") ); + let dexBal = await ethers.provider.getBalance(dex.address); + expect(dexBal.eq(0)).to.be.eq(true); + + // dex gains native token from other methods + await network.provider.send("hardhat_setBalance", [ + dex.address, + ethers.utils.parseEther("2").toHexString(), + ]); + dexBal = await ethers.provider.getBalance(dex.address); + expect(dexBal.eq(ethers.utils.parseEther("2"))).to.be.eq(true); + ////////////////////////////////////////////////// // SET LIQUIDITY MINING REWARDS FOR CONCENTRATED LIQUIDITY //////////////////////////////////////////////////
Hardhat
Add a msg.value check in the rewards function to see that the total value is passed when call the functions.
diff --git a/canto_ambient/contracts/callpaths/LiquidityMiningPath.sol b/canto_ambient/contracts/callpaths/LiquidityMiningPath.sol index e6c63f7..44dd338 100644 --- a/canto_ambient/contracts/callpaths/LiquidityMiningPath.sol +++ b/canto_ambient/contracts/callpaths/LiquidityMiningPath.sol @@ -65,6 +65,7 @@ contract LiquidityMiningPath is LiquidityMining { function setConcRewards(bytes32 poolIdx, uint32 weekFrom, uint32 weekTo, uint64 weeklyReward) public payable { // require(msg.sender == governance_, "Only callable by governance"); require(weekFrom % WEEK == 0 && weekTo % WEEK == 0, "Invalid weeks"); + require((1 +(weekTo - weekFrom) / WEEK) * weeklyReward == msg.value); while (weekFrom <= weekTo) { concRewardPerWeek_[poolIdx][weekFrom] = weeklyReward; weekFrom += uint32(WEEK);
Rug-Pull
#0 - c4-pre-sort
2023-10-07T14:19:31Z
141345 marked the issue as duplicate of #163
#1 - 141345
2023-10-07T14:20:12Z
not quite the same as https://github.com/code-423n4/2023-10-canto-findings/issues/163
this one focus on rug pull, rather than reward balance
#2 - c4-pre-sort
2023-10-09T16:57:59Z
141345 marked the issue as sufficient quality report
#3 - c4-judge
2023-10-18T21:16:57Z
dmvt marked the issue as not a duplicate
#4 - c4-judge
2023-10-18T21:17:04Z
dmvt marked the issue as primary issue
#5 - c4-judge
2023-10-18T21:17:09Z
dmvt marked the issue as selected for report
#6 - c4-sponsor
2023-11-03T17:56:58Z
OpenCoreCH (sponsor) acknowledged
#7 - OpenCoreCH
2023-11-03T17:57:39Z
Rewards will be set and sent in the same Canto governance proposal