The implementation of accrueConcentratedPositionTimeWeightedLiquidity() incurs in complex and unbounded computations that could lead to significant gast costs and a potential denial of service.
The liquidity mining program in the Ambient DEX will track each concentrated liquidity position using the accrueConcentratedPositionTimeWeightedLiquidity():
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 <= 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 < block.timestamp && tickTrackingIndex < 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 < 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 < nextWeek) {
106: // Tick was active before next week, need to add the liquidity
107: if (tickTracking.enterTimestamp < 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 < block.timestamp ? nextWeek : block.timestamp);
117: } else {
118: // Tick is no longer active
119: if (tickTracking.exitTimestamp < 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 <= upperTick - 10; ++i) {
140: uint32 numTickTracking = uint32(tickTracking_[poolIdx][i].length);
141: if (numTickTracking > 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:
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:
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.
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.
DoS
The text was updated successfully, but these errors were encountered:
All reactions