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

Unnecessary execuation logic related with querying price_v2 before calling update_price

Summary

Before updating the price, the result for querying price_v2 is 10^18, which works well, but the function _obtain_price_params executing unnecessary logic.

Vulnerability Details

When calling price_v2, the functions will call as below

price_v2 => _price_v2 => _raw_price => _obtain_price_params

Should notice _raw_price take the current block. timestamp as the default params, so _obtain_price_params will get the the current block. timestamp.

@view
def _price_v2() -> uint256:
return self._smoothed_price(
self.last_prices[2], self._raw_price(block.timestamp, block.timestamp)
)

https://github.com/CodeHawks-Contests/2025-03-curve/blob/198820f0c30d5080f75073243677ff716429dbfd/contracts/scrvusd/oracles/ScrvusdOracleV2.vy#L182C1-L186C6

@view
def _raw_price(ts: uint256, parameters_ts: uint256) -> uint256:
"""
@notice Price replication from scrvUSD vault
"""
parameters: PriceParams = self._obtain_price_params(parameters_ts)

https://github.com/CodeHawks-Contests/2025-03-curve/blob/198820f0c30d5080f75073243677ff716429dbfd/contracts/scrvusd/oracles/ScrvusdOracleV2.vy#L285C1-L291C89

For function _obtain_price_params , params.last_profit_update + period It will also equal the period as the initial value as last_profit_update = 0. so its value (7 * 86400) is forever less than current block.timestamp. So the following logic will be executed, and to update full_profit_unlock_date , last_profit_update , These new values have no meaning when calculating the raw price.

@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
# Calling price_v2 before updating the price will skip the below logic and continue to execute
the following logic, Although the following logic is no need to execute
if params.last_profit_update + period >= parameters_ts:
return params
......
params.full_profit_unlock_date += number_of_periods * period
params.last_profit_update += number_of_periods * period

Impact

Executing much unnecessary logic and no need to update params.full_profit_unlock_date, params.last_profit_update

Tools Used

pytest

pytest -s tests/scrvusd/oracle/stateful/test_prices.py

Add the below test functions in this file https://github.com/CodeHawks-Contests/2025-03-curve/blob/198820f0c30d5080f75073243677ff716429dbfd/tests/scrvusd/oracle/stateful/test_prices.py#L193

# make the price_params as public for the test work
def show_initial_vaule(self):
print("original price_params",self.soracle.price_params())
print("price v2, the calculated price_params",self.soracle.obtain_price_params_2())
print("price_v2 :",self.soracle.price_v2())
def test_initial_value_price_2(crvusd, scrvusd, admin, soracle, soracle_price_slots, verifier):
machine = SoracleTestStateMachine(
# ScrvusdStateMachine
crvusd=crvusd,
scrvusd=scrvusd,
admin=admin,
# SoracleStateMachine
soracle=soracle,
verifier=verifier,
soracle_slots=soracle_price_slots,
)
# show initial values
machine.show_initial_vaule()

To get the calculated price params, add the below temp function in ScrvusdOracleV2.vy.

# for test
@view
@external
def obtain_price_params_2() -> PriceParams:
parameters_ts : uint256 = block.timestamp
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

Test results

tests/scrvusd/oracle/stateful/test_prices.py
original price_params
PriceParams(total_debt=0, total_idle=1, total_supply=1, full_profit_unlock_date=0, profit_unlocking_rate=0, last_profit_update=0, balance_of_self=0)
price v2, the calculated price_params PriceParams(total_debt=0, total_idle=1, total_supply=1, full_profit_unlock_date=14515200, profit_unlocking_rate=0, last_profit_update=14515200, balance_of_self=0)
price_v2 : 1000000000000000000

For the test results full_profit_unlock_date,last_profit_update changed, the new values have no meaning; others remain the same.

Recommendations

Add params.last_profit_update ==0, just return the initial price param instead of executing the following logic

@view
def _obtain_price_params(parameters_ts: uint256) -> PriceParams:
......
+++if params.last_profit_update ==0 or params.last_profit_update + period >= parameters_ts:
---if params.last_profit_update + period >= parameters_ts:
return params
......
params.full_profit_unlock_date += number_of_periods * period
params.last_profit_update += number_of_periods * period
Updates

Lead Judging Commences

0xnevi Lead Judge 3 months ago
Submission Judgement Published
Validated
Assigned finding tags:

finding-last_profit_update-used-instead-timestamp

- Sponsor Comments - State root oracles usually do not provide block.timestamp, so it's simply not available. That is why last_profit_update is intended. - In `update_price`, this value must be a future block, meaning this update is a state checked and allowed by the OOS verifier contracts. The impact is also increasingly limited given price is smoothen and any updates via the block hash `verifyScrvusdByBlockHash` can also update the prices appropriately, meaning the price will likely stay within safe arbitrage range aligning with protocol logic

Appeal created

bauchibred Auditor
3 months ago
0xnevi Lead Judge
2 months ago
0xnevi Lead Judge about 2 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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