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).
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.
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.
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.
moveLiquidity()
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.
Alice constructs a transaction
tx_Alice
: moveLiquidity()
This transaction will ultimately rely on price = getPrice()
.
Alice broadcasts the transaction
The transaction is pending in the mempool, waiting to be mined.
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()
.
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.
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.
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.
Block ordering
Bob’s transaction tx_Bob
runs first, corrupting the contract’s internal price parameters.
Next, Alice’s tx_Alice
executes.
Alice calls moveLiquidity()
moveLiquidity()
calls getPrice()
, which is now based on heavily distorted data.
This could result in an inflated or deflated token valuation.
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.
Price Distortion
Excessive loops cause extreme values in params.total_supply
and params.balance_of_self
, leading to excessively high or low price outputs.
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.
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.
Manual Review
Fix the loop syntax:
Replace
with
to ensure it only runs number_of_periods
times. This prevents additional unintended iterations.
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`
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.