Lucene search

K
code423n4Code4renaCODE423N4:2023-10-CANTO-FINDINGS-ISSUES-235
HistoryOct 06, 2023 - 12:00 a.m.

Potential denial of service due to out of bound gas usage

2023-10-0600:00:00
Code4rena
github.com
3
denial of service
out of bound gas
liquiditymining

AI Score

6.8

Confidence

High

Lines of code

Vulnerability details

Summary

The implementation of accrueConcentratedPositionTimeWeightedLiquidity() incurs in complex and unbounded computations that could lead to significant gast costs and a potential denial of service.

Impact

The liquidity mining program in the Ambient DEX will track each concentrated liquidity position using the accrueConcentratedPositionTimeWeightedLiquidity():

<https://github.com/code-423n4/2023-10-canto/blob/main/canto_ambient/contracts/mixins/LiquidityMining.sol#L69-L154&gt;

069:     function accrueConcentratedPositionTimeWeightedLiquidity(
070:         address payable owner,
071:         bytes32 poolIdx,
072:         int24 lowerTick,
073:         int24 upperTick
074:     ) internal {
075:         RangePosition72 storage pos = lookupPosition(
076:             owner,
077:             poolIdx,
078:             lowerTick,
079:             upperTick
080:         );
081:         bytes32 posKey = encodePosKey(owner, poolIdx, lowerTick, upperTick);
082:         uint32 lastAccrued = timeWeightedWeeklyPositionConcLiquidityLastSet_[
083:             poolIdx
084:         ][posKey];
085:         // Only set time on first call
086:         if (lastAccrued != 0) {
087:             uint256 liquidity = pos.liquidity_;
088:             for (int24 i = lowerTick + 10; i &lt;= upperTick - 10; ++i) {
089:                 uint32 tickTrackingIndex = tickTrackingIndexAccruedUpTo_[poolIdx][posKey][i];
090:                 uint32 origIndex = tickTrackingIndex;
091:                 uint32 numTickTracking = uint32(tickTracking_[poolIdx][i].length);
092:                 uint32 time = lastAccrued;
093:                 // Loop through all in-range time spans for the tick or up to the current time (if it is still in range)
094:                 while (time &lt; block.timestamp && tickTrackingIndex &lt; numTickTracking) {
095:                     TickTracking memory tickTracking = tickTracking_[poolIdx][i][tickTrackingIndex];
096:                     uint32 currWeek = uint32((time / WEEK) * WEEK);
097:                     uint32 nextWeek = uint32(((time + WEEK) / WEEK) * WEEK);
098:                     uint32 dt = uint32(
099:                         nextWeek &lt; block.timestamp
100:                             ? nextWeek - time
101:                             : block.timestamp - time
102:                     );
103:                     uint32 tickActiveStart; // Timestamp to use for the liquidity addition
104:                     uint32 tickActiveEnd;
105:                     if (tickTracking.enterTimestamp &lt; nextWeek) {
106:                         // Tick was active before next week, need to add the liquidity
107:                         if (tickTracking.enterTimestamp &lt; time) {
108:                             // Tick was already active when last claim happened, only accrue from last claim timestamp
109:                             tickActiveStart = time;
110:                         } else {
111:                             // Tick has become active this week
112:                             tickActiveStart = tickTracking.enterTimestamp;
113:                         }
114:                         if (tickTracking.exitTimestamp == 0) {
115:                             // Tick still active, do not increase index because we need to continue from here
116:                             tickActiveEnd = uint32(nextWeek &lt; block.timestamp ? nextWeek : block.timestamp);
117:                         } else {
118:                             // Tick is no longer active
119:                             if (tickTracking.exitTimestamp &lt; nextWeek) {
120:                                 // Exit was in this week, continue with next tick
121:                                 tickActiveEnd = tickTracking.exitTimestamp;
122:                                 tickTrackingIndex++;
123:                                 dt = tickActiveEnd - tickActiveStart;
124:                             } else {
125:                                 // Exit was in next week, we need to consider the current tick there (i.e. not increase the index)
126:                                 tickActiveEnd = nextWeek;
127:                             }
128:                         }
129:                         timeWeightedWeeklyPositionInRangeConcLiquidity_[poolIdx][posKey][currWeek][i] +=
130:                             (tickActiveEnd - tickActiveStart) * liquidity;
131:                     }
132:                     time += dt;
133:                 }
134:                 if (tickTrackingIndex != origIndex) {
135:                     tickTrackingIndexAccruedUpTo_[poolIdx][posKey][i] = tickTrackingIndex;
136:                 }
137:             }
138:         } else {
139:             for (int24 i = lowerTick + 10; i &lt;= upperTick - 10; ++i) {
140:                 uint32 numTickTracking = uint32(tickTracking_[poolIdx][i].length);
141:                 if (numTickTracking &gt; 0) {
142:                     if (tickTracking_[poolIdx][i][numTickTracking - 1].exitTimestamp == 0) {
143:                         // Tick currently active
144:                         tickTrackingIndexAccruedUpTo_[poolIdx][posKey][i] = numTickTracking - 1;
145:                     } else {
146:                         tickTrackingIndexAccruedUpTo_[poolIdx][posKey][i] = numTickTracking;
147:                     }
148:                 }
149:             }
150:         }
151:         timeWeightedWeeklyPositionConcLiquidityLastSet_[poolIdx][
152:             posKey
153:         ] = uint32(block.timestamp);
154:     }

The algorithm is quite dense and difficult to read at first, but it can be simplified (at least for the current context) as:

  • For each tick between lowTick + 10 and upperTick - 10:
    • For each segment of time between last accrued and now:
      • Add liquidity to the current week proportional to the span of time determined by the segment

Here we refer to segment of time as any period that is bounded between the start of the week, the end of the week, the tick becoming active or the tick becoming inactive. For example, a segment could be defined by the start of week (in a tick that became active before the start of the week) and the exit timestamp of the tick (because the tick exited before the end of the week).

Note, then, that the implemented logic executes two nested loops:

  1. The outer loop ranges for each tick in the position (minus 20, technically, to account for the Β±10 padding). A position that provides liquidity in a wide range will need to iterate each value of a potential big list.
  2. The inner loop goes through segmentations of week periods and/or tick history. For an active pool, with lots of tick crossing, this can have a devastating effect in size of the tick history. Imagine a very active pool being heavily arbitraged, that jumps between close (but different) ticks. Each jump will create a new history entry in the corresponding key of the tickTracking_ mapping. Every entry in this list will need to be iterated and accounted for any potential liquidity (these jumps between near ticks will likely be included in the same position).

A position that has a wide tick range, or belongs to a pool that is subject to lots of trading activity and a long list of tick tracking history, or simply isn’t updated frequently and accumulates pending changes (remember the accrual happens when the position is minted, burned or the user claims the rewards) may result in significant computational requirements, leading to unbounded gas costs and a potential denial of service. The owner of the position may not be able to exit it or claim the associated rewards, leading to loss of funds.

Recommendation

It is difficult to provide a recommendation with the given architecture. Clearly, the data structures chosen to implement the solution require an algorithm that goes through ticks and segmentations of time.

A potential solution, that could require substantial design changes, would be to mimic the traditional staking rewards algorithm used in different protocols. The following article introduces the algorithm for Uniswap V3 (same style as concentrated liquidity in Ambient). An implementation is provided in the following repository.

Assessed type

DoS


The text was updated successfully, but these errors were encountered:

All reactions

AI Score

6.8

Confidence

High