DeFiLayer 1Layer 2
14,723 OP
View results
Submission Details
Severity: high
Invalid

Vyper Contract Critical Logic Flaw: Overextended Loop Leading to Price Distortion

Summary

In the _obtain_price_params(parameters_ts: uint256) -> PriceParams function, the code uses for _: uint256 in range(number_of_periods, bound=MAX_V2_DURATION). However, in Vyper, range(a, b) iterates from a through b-1, resulting in far more iterations than intended. For instance, if only number_of_periods times are expected (e.g., 2 iterations), it will actually run until MAX_V2_DURATION - 1. This dramatically skews the contract’s price computation logic and can produce extreme pricing outcomes, posing significant risks to any mechanism relying on these values (e.g., swaps, liquidations, or lending collateral calculations).

Vulnerability Details

Source code reference

number_of_periods: uint256 = min(
(parameters_ts - params.last_profit_update) // period,
self.max_v2_duration,
)
# ...
# First, assume gains over a certain period and increase total_idle
gain: uint256 = (
params.balance_of_self * (params.total_idle + params.total_debt) // params.total_supply
)
params.total_idle += gain * number_of_periods
# Attempts multiple iterations, simulating multiple periods
for _: uint256 in range(number_of_periods, bound=MAX_V2_DURATION):
new_balance_of_self: uint256 = (
params.balance_of_self
* (params.total_supply - params.balance_of_self)
// params.total_supply
)
params.total_supply -= (
params.balance_of_self * params.balance_of_self // params.total_supply
)
params.balance_of_self = new_balance_of_self
  1. Root Cause

    • Vyper’s range(a, b) behaves differently from Python—it iterates from a up to b-1. While the author intended to loop number_of_periods times, it actually loops (MAX_V2_DURATION - number_of_periods) times.

    • As a result, params.total_supply and params.balance_of_self are recalculated excessively, leading to extreme deviations in price computations.

  2. Symptoms

    • When number_of_periods is small but MAX_V2_DURATION is large, the loop runs far more times than expected, significantly amplifying or shrinking the computed price parameters.

    • The final price can approach near-zero or skyrocket to an extremely high value, diverging drastically from the real market value.

  3. Scope of Impact

    • The _obtain_price_params() function is called by _raw_price() and price_v2(). Any feature relying on these outputs (e.g., StableSwap, liquidations, collateral calculations) could be operating on grossly inaccurate prices.

Proof of Concept

Step 1: Alice Initiates moveLiquidity()

  1. Alice notices a prompt

    • A specific pool (e.g., BootstrapPool) has reached its target reserves, triggering a call to moveLiquidity() to migrate leftover liquidity to another pool (e.g., Fraxswap).

    • The moveLiquidity() function internally calls something like price = getPrice() to determine how many tokens to inject.

  2. Alice constructs a transaction

    • tx_Alice: moveLiquidity()

    • This transaction will ultimately rely on price = getPrice().

  3. Alice broadcasts the transaction

    • The transaction is pending in the mempool, waiting to be mined.

Step 2: Bob Exploits the Flaw in a Front-Run

  1. Bob monitors pending transactions

    • Bob runs a node or service to watch for upcoming contract calls in the mempool.

    • He observes Alice’s pending moveLiquidity() call, noticing it will trigger getPrice().

  2. Bob identifies the vulnerability

    • Bob knows that in Vyper, for i in range(a, b) loops from a through b-1, while the developer mistakenly assumed it only runs a times.

    • If number_of_periods is 2, 3, or 5, etc. (much smaller than MAX_V2_DURATION), the actual loop count skyrockets, significantly distorting the calculation.

  3. Bob creates a preemptive transaction

    • Through a function like updateParameters(), update_price(), or some call path involving _obtain_price_params(), Bob forces the contract to set number_of_periods = 2.

    • Result: During execution, the contract will iterate for i in range(2, bound=MAX_V2_DURATION) excessively, causing params.total_supply and params.balance_of_self to deviate severely—price may crash or spike.

    • Example: By tweaking parameters_ts, Bob ensures (parameters_ts - last_profit_update) // period = 2, triggering the erroneous loop.

  4. Bob front-runs Alice

    • He sets a higher gas price (or tip) so miners/validators execute his transaction tx_Bob before Alice’s tx_Alice in the same block.

Step 3: Alice’s Transaction Now Uses the Distorted Price

  1. Block ordering

    • Bob’s transaction tx_Bob runs first, corrupting the contract’s internal price parameters.

    • Next, Alice’s tx_Alice executes.

  2. Alice calls moveLiquidity()

    • moveLiquidity() calls getPrice(), which is now based on heavily distorted data.

    • This could result in an inflated or deflated token valuation.

  3. Outcome

    • If the price is artificially high, Alice might inject far fewer tokens into Fraxswap.

    • If the price is artificially low, the contract might overcommit tokens or perform other incorrect operations.

    • Either way, Alice executes her transaction at a wildly off-market price; Bob can then profit from subsequent trades or liquidation events exploiting the warped price data.

Impact

  1. Price Distortion

    Excessive loops cause extreme values in params.total_supply and params.balance_of_self, leading to excessively high or low price outputs.

  2. Potential Economic Losses

    • If attackers exploit the extreme prices for swaps, lending, or collateral calculations, protocol funds can be siphoned, or malicious arbitrage can occur.

    • Downstream contracts using this price (liquidation, oracles, or stable pools) risk erroneous triggers or actions.

  3. Ease of Exploitation

    • Whenever _price_v2() or related calls are made with number_of_periods > 0, the “over-looping” can be forced to achieve harmful pricing.

Tools Used

Manual Review

Recommendations

Fix the loop syntax:

Replace

for _: uint256 in range(number_of_periods, bound=MAX_V2_DURATION):
...

with

for i in range(number_of_periods):
...

to ensure it only runs number_of_periods times. This prevents additional unintended iterations.

Updates

Lead Judging Commences

0xnevi Lead Judge
3 months ago
0xnevi Lead Judge 2 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement
Assigned finding tags:

[invalid] finding-incorrect-loop-bound

Invalid, `bound` here has a different meaning from Python's `range(a, b)`. It is a bound of maximum iterations, meaning the loop will only go to the bounded `MAX_V2_DURATION` when `number_of_periods >= MAX_V2_DURATION`

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.