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

Incorrect Timestamp Handling in State Root Verification

Summary

State root verification uses an outdated timestamp from the vault instead of the block’s actual timestamp.

Vulnerability Details

File: ScrvusdVerifierV1.sol

In verifyScrvusdByStateRoot, params[5] (vault’s last_profit_update) is used as _ts, which may not reflect the block’s true timestamp.

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 as the timestamp surrogate
@> return _updatePrice(params, params[5], _block_number);
}

The verifyScrvusdByStateRoot method assumes the vault’s last_profit_update (a storage variable in the scrvUSD contract) matches the block’s actual timestamp. However:

  1. last_profit_update is updated only when the vault processes profits, which may not align with the block’s timestamp.

  2. If the vault has not processed profits recently, last_profit_update becomes stale, leading to incorrect price extrapolation.

Example Scenario

  1. Block N has a timestamp of 1700000000.

  2. The vault’s last_profit_update is 1699990000 (1 hour old).

  3. The oracle uses 1699990000 instead of 1700000000 to compute price_v1/price_v2.

  4. The price is underestimated by 1 hour of yield, creating a risk-free arbitrage opportunity.

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

Impact

Price extrapolation uses stale timestamps, creating arbitrage opportunities due to inaccurate rates.

Tools Used

Manual review.

Recommendations

Fetch the block’s timestamp from the state root proof instead of relying on last_profit_update

// ✅ Fixed code (pseudo-implementation)
function verifyScrvusdByStateRoot(
uint256 _block_number,
bytes memory _block_header_rlp, // Add block header RLP
bytes memory _proof_rlp
) external returns (uint256) {
// Parse block header to get timestamp
Verifier.BlockHeader memory block_header = Verifier.parseBlockHeader(_block_header_rlp);
uint256 block_timestamp = block_header.timestamp;
// Use block_timestamp instead of params[5]
return _updatePrice(params, block_timestamp, _block_number);
}
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

Support

FAQs

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