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

Insecure Timestamp Surrogate

Summary

Using last_profit_update as a timestamp surrogate introduce avoidable inaccuracies.

https://github.com/CodeHawks-Contests/2025-03-curve/blob/198820f0c30d5080f75073243677ff716429dbfd/contracts/scrvusd/verifiers/ScrvusdVerifierV1.sol#L71-L80

Vulnerability Detail

The vulnerability occur in ScrvusdVerifierV1.sol within the verifyScrvusdByStateRoot function:

function verifyScrvusdByStateRoot(uint256 \_block\_number, bytes memory \_proof\_rlp) external returns (uint256) {
bytes32 state\_root = IBlockHashOracle(BLOCK\_HASH\_ORACLE).get\_state\_root(\_block\_number);
uint256\[PARAM\_CNT] memory params = \_extractParametersFromProof(state\_root, \_proof\_rlp);
// Use last\_profit\_update (params\[5]) as the timestamp surrogate
return \_updatePrice(params, params\[5], \_block\_number);
}

2. The params[5] correspond to the last_profit_update value extracted from the scrvUSD vault storage. This value represent the timestamp when the vault profit was last updated.

3. The _updatePrice function pass params[5] as ts (timestamp) to the oracle update_price:

function \_updatePrice() internal returns (uint256) {
return IScrvusdOracle(SCRVUSD\_ORACLE).update\_price(params, ts, number);
}

4. The oracle use ts in _raw_price to compute the scrvUSD price:

@view
def \_raw\_price(ts: uint256, parameters\_ts: uint256) -> uint256:
parameters: PriceParams = self.\_obtain\_price\_params(parameters\_ts)
return self.\_total\_assets(parameters) \* 10\*\*18 // self.\_total\_supply(parameters, ts)

there's a mismatched timestamps because the last_profit_update from the vault’s storage is not the actual block timestamp. It reflect the last time the vault profit was processed which lag behind the true block timestamp for example:

- If the vault hasn’t processed profits for days, the last_profit_update will be stale.

- The block actual timestamp when parameters are valid will differ significantly.

example scenario

- If last_profit_update is artificially old (1 week ago), the oracle will compute prices as if 1 week has passed, even if the actual block is recent.

- This create a price lags allowing arbitrageurs to exploit the pool by minting/redeeming scrvUSD at incorrect rates.

- The oracle then incorrectly assume time has not progressed since last_profit_update leading to:

Under/Over-Unlocked Shares: Miscalculate unlocked shares, skewing total_supply and raw_price.

Proof of Concept (PoC

Conditions:

last_profit_update = T (1 week ago).

Current block timestamp = T + 1 week.

Execution:

The verifier pass ts = T to the oracle.

The oracle compute _total_supply using ts = T leading to incorrect unlocked shares and price.

Mismatched Timestamps

The oracle uses a stale ts (T) instead of the actual block timestamp (T + 1 week).

Incorrect Price

The reported price lags behind the true market value enabling arbitrage.

Impact

- Actor exploit the price lag to mint/redeem scrvUSD at incorrect rates draining liquidity from stableswap-ng pools.

- Liquidity pool suffer losses as arbitrageurs profit from the mispricing.

Recommendation

1. Use the block actual timestamp instead of last_profit_update.

function verifyScrvusdByStateRoot(...) {
// Add: Fetch block timestamp from BlockHashOracle
uint256 block\_timestamp = IBlockHashOracle(BLOCK\_HASH\_ORACLE).get\_block\_timestamp(\_block\_number);
return \_updatePrice(params, block\_timestamp, \_block\_number);
}

2. Validate Timestamp Freshness:

Ensure the timestamp is recent (1 hour of the current block):

require(block.timestamp - block_timestamp <= 3600, "Stale timestamp");
Updates

Lead Judging Commences

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

Support

FAQs

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