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 10 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
10 months ago
0xnevi Lead Judge
10 months ago
0xnevi Lead Judge 9 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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

Give us feedback!