Canto Liquidity Mining Protocol - 3docSec's results

Execution layer for original work.

General Information

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

Canto

Findings Distribution

Researcher Performance

Rank: 11/62

Findings: 2

Award: $364.87

QA:
grade-b

🌟 Selected for report: 0

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: Banditx0x

Also found by: 0xDING99YA, 0xWaitress, 0xpiken, 3docSec, Banditx0x, adriro, emerald7017, maanas, twicek

Labels

bug
3 (High Risk)
satisfactory
sufficient quality report
duplicate-114

Awards

359.9307 USDC - $359.93

External Links

Lines of code

https://github.com/code-423n4/2023-10-canto/blob/37a1d64cf3a10bf37cbc287a22e8991f04298fa0/canto_ambient/contracts/mixins/TradeMatcher.sol#L442 https://github.com/code-423n4/2023-10-canto/blob/37a1d64cf3a10bf37cbc287a22e8991f04298fa0/canto_ambient/contracts/mixins/LiquidityMining.sol#L34 https://github.com/code-423n4/2023-10-canto/blob/37a1d64cf3a10bf37cbc287a22e8991f04298fa0/canto_ambient/contracts/mixins/LiquidityMining.sol#L94

Vulnerability details

Whenever a swap operations moves the nominal exchange price of the pool enough to cross a tick, LiquidityMining.sol keeps track of the event by storing a new TickTracking struct in its stored arrays:

        // TradeMatcher.sol:441 "sweepSwapLiq" function
        if (preTradeTick != postTradeTick) {
            crossTicks(pool.hash_, preTradeTick, postTradeTick);
        }

        // ----

        // LiquidityMining.sol:24
        function crossTicks(
            bytes32 poolIdx,
            int24 exitTick,
            int24 entryTick
        ) internal {
            uint256 numElementsExit = tickTracking_[poolIdx][exitTick].length;
            tickTracking_[poolIdx][exitTick][numElementsExit - 1]
                .exitTimestamp = uint32(block.timestamp);
            StorageLayout.TickTracking memory tickTrackingData = StorageLayout
                .TickTracking(uint32(block.timestamp), 0);
            tickTracking_[poolIdx][entryTick].push(tickTrackingData);
        }

These arrays of TickTracking structs are then looped over in the accrueConcentratedPositionTimeWeightedLiquidity, which is called by any liquidity event of a user's position (deposit or withdraw liquidity; claim rewards).

It is possible that a large number of swap operations create long lists of TickTracking objects stored, and it is also possible that enough of them are stored for a accrueConcentratedPositionTimeWeightedLiquidity call to exceed the maximum gas available in a single Canto block, making operations involving this call (including the MINT_RANGE and more importantly the BURN_RANGE user command that lets users withdraw their funds from the pool) inevitably fail.

To elaborate on the likelihood of this condition to happen, a single loop over a TickTracking object costs about 650 in gas and a block on the CANTO network is limited to 30M gas, so about 45k swaps are sufficient to freeze assets this way.

These swaps can happen during normal operations, because they don't need to happen in the same block or be triggered by the same actor to stack up, so an excessive volume can happen eventually without anybody having to intentionally orchestrate a costly attack.

Impact

The worst case impact is that users are unable to withdraw their liquidity from a heavily-traded position, because the withdraw operation would exceed the maximum gas allowed on a CANTO block.

Proof of Concept

The following test case shows how after 45k swaps, the gas of a deposit exceeds 30M gas. Deposit is shown for simplicity but withdrawal would fail the same way because it's the shared accrual logic that is at fault. The full runnable code in Foundry, including setup and helper methods is available here.

    function testImpossibleMint() public {
        address user1 = address(uint160(uint256(keccak256("user1"))));
        provideLiquidity(user1, 100e18);

        uint256 currentTs = block.timestamp;
        vm.pauseGasMetering();

        for(uint i = 0; i < 23_000; i++) {
            currentTs = currentTs + 1 minutes;
            vm.warp(currentTs);
            swapUsdcForCnote();

            currentTs = currentTs + 1 minutes;
            vm.warp(currentTs);
            swapCnoteForUsdc();
        }

        vm.resumeGasMetering();

        // 
        /*
          The below fails for out of gas, exceeding the 30M gas it was given:
        |   ├─ [29531439] CrocSwapDex::userCmd(2, 0x...)
        │   ├─ [29526362] WarmPath::userCmd(0x...) [delegatecall]
        │   │   └─ ← "EvmError: OutOfGas"
        */
        provideLiquidity(user1, 1);
    }

Tools Used

Code review, Foundry

Right now, tick snapshots are taken at a potentially infinite granularity, because a group of TickTracking objects can very well start and end at the same block and have no use because rewards are calculated on the gap between their start and end.

The most immediate way to mitigate this issue is to aggregate the data on time windows less granular than this, but still more granular than weeks (i.e. with hourly granularity).

Assessed type

Loop

#0 - c4-pre-sort

2023-10-08T03:17:07Z

141345 marked the issue as primary issue

#1 - 141345

2023-10-08T03:23:36Z

the magnitude of 45k swap seems quite large for this DoS to happen. However, if it is intended behavior like https://github.com/code-423n4/2023-10-canto-findings/issues/114 indicates, still seems possible.

#2 - c4-pre-sort

2023-10-08T04:09:45Z

141345 marked the issue as duplicate of #114

#3 - c4-pre-sort

2023-10-09T16:41:03Z

141345 marked the issue as sufficient quality report

#4 - c4-judge

2023-10-18T19:29:02Z

dmvt marked the issue as satisfactory

Awards

4.9369 USDC - $4.94

Labels

bug
grade-b
QA (Quality Assurance)
sufficient quality report
Q-18

External Links

[L-01] LiquidityMiningPath.protocolCmd should be payable The function LiquidityMiningPath.protocolCmd is called by CrocSwapDex.protocolCmd which is payable. Consider making it payable too, especially because this way it would be possible to enable and fund rewards with one protocolCmd call.

[N-01] Consider removing the hardhat console import from production code LevelBook.sol imports the hardhat console. Consider removing this import before production deployment.

#0 - 141345

2023-10-09T04:21:20Z

#1 - c4-pre-sort

2023-10-09T17:21:55Z

141345 marked the issue as sufficient quality report

#2 - c4-judge

2023-10-18T22:50:34Z

dmvt marked the issue as grade-b

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