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

Loop in _obtain_price_params Violates "No Approximation Losses" Guarantee

Summary

The _obtain_price_params function is intended to simulate vault reward periods without incurring approximation losses, relying on a cap defined by the admin-controlled max_v2_duration.

However, the loop in this function is incorrectly bounded using a constant MAX_V2_DURATION, causing it to run extra iterations beyond the intended cap. This over-simulation can inflate the computed raw price, which may be exploited in stableswap pools.

Vulnerability Details

The oracle is designed to replicate scrvUSD’s growth with high precision.

@view
def _obtain_price_params(parameters_ts: uint256) -> PriceParams:
"""
@notice Obtain Price parameters true or assumed to be true at `parameters_ts`.
Assumes constant gain(in crvUSD rewards) through distribution periods.
@param parameters_ts Timestamp to obtain parameters for
@return Assumed `PriceParams`
"""
params: PriceParams = self.price_params
period: uint256 = self.profit_max_unlock_time
if params.last_profit_update + period >= parameters_ts:
return params
number_of_periods: uint256 = min(
(parameters_ts - params.last_profit_update) // period,
self.max_v2_duration,
)
# locked shares at moment params.last_profit_update
gain: uint256 = (
params.balance_of_self * (params.total_idle + params.total_debt) // params.total_supply
)
params.total_idle += gain * number_of_periods
# functions are reduced from `VaultV3._process_report()` given assumptions with constant gain
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
if params.full_profit_unlock_date > params.last_profit_update:
# copy from `VaultV3._process_report()`
params.profit_unlocking_rate = params.balance_of_self * MAX_BPS_EXTENDED // (
params.full_profit_unlock_date - params.last_profit_update
)
else:
params.profit_unlocking_rate = 0
params.full_profit_unlock_date += number_of_periods * period
params.last_profit_update += number_of_periods * period
return params

The function calculates:

number_of_periods: uint256 = min(
(parameters_ts - params.last_profit_update) // period,
self.max_v2_duration
)

which should cap the number of simulated reward periods to self.max_v2_duration. However, the simulation loop is written as:

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

This loop iterates from number_of_periods up to the constant MAX_V2_DURATION of 192, regardless of the actual cap defined by self.max_v2_duration.

For instance, if number_of_periods is 24 due to the configured cap, the loop executes an additional 168 iterations. These extra iterations simulate reward accrual for periods that never occurred, leading to an overestimation of rewards and an inflated raw price.

https://github.com/CodeHawks-Contests/2025-03-curve?tab=readme-ov-file#solution

This project contains a solution that fetches scrvUSD vault parameters from Ethereum, and provides them on other chains, with the goal of being able to compute the growth rate in a safe (non-manipulable) and precise (no losses due to approximation) way. Furthermore, this oracle can allow creating stableswap-ng pools for other assets like USDC/scrvUSD, FRAX/scrvUSD, etc.

Impact

The excessive simulation overestimates the rewards, resulting in an inflated raw price. An inflated price can be exploited by arbitrageurs to manipulate stableswap pool balances, potentially draining funds.

This vulnerability arises from a fundamental coding mistake that violates the project's "no approximation losses" guarantee, independent of the trust model for verifier roles.

Tools Used

Manual Review

Recommendations

Modify the loop to iterate exactly number_of_periods times, ensuring that the simulation does not exceed the configured max_v2_duration.

number_of_periods: uint256 = min((parameters_ts - params.last_profit_update) // period, self.max_v2_duration)
for _ in range(number_of_periods):
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
Updates

Lead Judging Commences

0xnevi Lead Judge 3 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.