Contrast to how timestamps are managed in the original scrusd VaultV3
implementation where we have the last_profit_update
timestamp to set to the current block timestamp when processing profit reports, ensuring proper time-based calculations. In ScrvusdOracleV2
, this timestamp is incorrectly advanced by a fixed period increment instead of being updated to the current timestamp which should instead be only done to full_profit_unlock_date
.
This discrepancy leads to inaccurate price calculations, potential underflow errors, and ultimately a complete breakdown of the price update mechanism.
First note how during profit updates in VaultV3::_process_report()
we correctly set the last_profit_update
to the current block.timestamp
:
from https://etherscan.io/address/0x0655977FEb2f289A4aB78af67BAB0d17aAb84367#code#L1302
As seen, the Yearn Vault implementation correctly sets last_profit_update
to the current block.timestamp
whenever profit is processed. This anchors the unlocking rate calculation to the precise moment when the profit was reported and the new unlocking rate is calculated.
However, in the ScrvusdOracleV2
contract, the implementation incorrectly advances by the number of periods before full unlock:
Take a look at ScrvusdOracleV2#obtain_price_params()
So instead of setting last_profit_update
to the current timestamp, it's incremented by number_of_periods * period
. Which should correctly only be done to full_profit_unlock_date
. This creates a significant deviation from the original implementation and leads to severe issues in timestamp handling.
The proper implementation should be:
Subtly, the price_v0()
function relies on last_profit_update
for historical price calculations. With incorrect timestamp values on the last_profit_update
, this trusted historical price becomes completely unreliable.
Now, share unlocking calculations depend on the time difference between the current timestamp and last_profit_update
. When last_profit_update
is artificially advanced beyond the current time, the calculation (ts - last_profit_update)
in _unlocked_shares()
will underflow even when the if full_profit_unlock_date > ts: condition is satisfied, causing an underflow, which blocks the updating of prices from the revert.
Since during price updates via update_price()
, we query _raw_price()
, which in turn calls _total_supply()
, and then _unlocked_shares()
, the timestamp underflow in _unlocked_shares()
prevents any price updates from succeeding, effectively breaking the entire oracle mechanism as this would mean we would permanently have stale prices on that destination chain.
Key to note that the above is in the case where the e if full_profit_unlock_date > ts: condition is satisfied, in the case it's not satisfied and we don't revert for updates, the price returned would always be inaccurate, because to calculate the raw price we need to obtain the parameters that would give us the total supply and assets:
https://github.com/CodeHawks-Contests/2025-03-curve/blob/198820f0c30d5080f75073243677ff716429dbfd/contracts/scrvusd/oracles/ScrvusdOracleV2.vy#L285-L292
But since our last profit update has been heavily inflated we always get in this block that just returns the param without updating it:
https://github.com/CodeHawks-Contests/2025-03-curve/blob/198820f0c30d5080f75073243677ff716429dbfd/contracts/scrvusd/oracles/ScrvusdOracleV2.vy#L237-L283
Since we have an inflated total supply the real price would be deflated cause the calculation is self._total_assets(parameters) * 10**18 // self._total_supply(parameters, ts)
further on breaking the consistency with the real price on mainnet.
Manual review
Modify the _obtain_price_params()
function to correctly update last_profit_update
to the current timestamp:
Which is exactly how it's done in the yearn vault.
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.