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

State Root Temporal Consistency Violation

Summary

The Curve Storage Proofs protocol contains a vulnerability where parameters extracted from proofs are not verified to come from the same temporal state. The verification process extracts multiple parameters from different storage slots but fails to validate that these parameters represent a consistent state point in time. This creates an opportunity for manipulated proofs that combine parameters from different temporal states, potentially leading to incorrect price calculations, economic imbalances, and exploitation opportunities across the protocol's cross-chain deployments.

Vulnerability Details

The vulnerability manifests in the parameter extraction process in the verifier contracts:

  1. Independent Parameter Extraction Without Temporal Validation: The _extractParametersFromProof function in ScrvusdVerifierV1.sol extracts parameters from different storage slots but has no mechanism to ensure they represent a consistent state point:

// ScrvusdVerifierV1.sol:83-105
function _extractParametersFromProof(
bytes32 stateRoot,
bytes memory proofRlp
) internal view returns (uint256[PARAM_CNT] memory) {
RLPReader.RLPItem[] memory proofs = proofRlp.toRlpItem().toList();
require(proofs.length == PROOF_CNT, "Invalid number of proofs");
// Extract account proof
Verifier.Account memory account = Verifier.extractAccountFromProof(
SCRVUSD_HASH,
stateRoot,
proofs[0].toList()
);
require(account.exists, "scrvUSD account does not exist");
// Extract slot values without temporal consistency validation
uint256[PARAM_CNT] memory params;
for (uint256 i = 1; i < PROOF_CNT; i++) {
Verifier.SlotValue memory slot = Verifier.extractSlotValueFromProof(
keccak256(abi.encode(PARAM_SLOTS[i])),
account.storageRoot,
proofs[i].toList()
);
// Slots might not exist, but typically we just read them.
params[i - 1] = slot.value;
}
// No validation of temporal relationships between parameters
return params;
}
  1. Critical Time-Dependent Parameters: Several extracted parameters have time-based relationships that should be consistent:

// From ScrvusdVerifierV1.sol:31-40
uint256[PROOF_CNT] internal PARAM_SLOTS = [
uint256(0), // filler for account proof
uint256(21), // total_debt
uint256(22), // total_idle
uint256(20), // totalSupply
uint256(38), // full_profit_unlock_date <-- Time-dependent
uint256(39), // profit_unlocking_rate
uint256(40), // last_profit_update <-- Time-dependent
uint256(keccak256(abi.encode(18, SCRVUSD))) // balanceOf(self)
];
  1. Oracle Assumes Temporal Consistency: The oracle contract uses these parameters assuming they represent a consistent state:

# ScrvusdOracleV2.vy:304-311
self.price_params = PriceParams(
total_debt=_parameters[0],
total_idle=_parameters[1],
total_supply=_parameters[2],
full_profit_unlock_date=_parameters[3],
profit_unlocking_rate=_parameters[4],
last_profit_update=_parameters[5],
balance_of_self=_parameters[6],
)

Exploitation Scenario:
An attacker with the ability to construct manipulated proofs could:

  1. Create a proof that includes total_debt and total_idle from one state point (T1)

  2. Include total_supply and balance_of_self from a different state point (T2)

  3. Mix time-dependent parameters (full_profit_unlock_date and last_profit_update) from inconsistent states

  4. Submit this proof to the verifier

  5. The oracle would accept and use these temporally inconsistent parameters for price calculations

Root Cause:
The fundamental issue is the lack of validation to ensure that parameters extracted from different storage slots represent a consistent temporal state. The protocol relies on the assumption that proofs are constructed honestly, without enforcing temporal consistency through validation.

Impact

Economic Impact:
Temporal inconsistency in parameters can lead to:

  1. Incorrect Price Calculations: The raw price calculation depends on multiple parameters being temporally consistent:

# ScrvusdOracleV2.vy:284-285
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)
  1. Economic Imbalances: Manipulated prices could create arbitrage opportunities:

    • If total_assets is from a later state (higher) while total_supply is from an earlier state (lower), the price would be artificially inflated

    • Conversely, using an earlier total_assets with a later total_supply would deflate the price

  2. Unlocking Calculation Errors: Time-dependent parameters affect profit unlocking:

# ScrvusdOracleV2.vy:191-216
def _unlocked_shares(
full_profit_unlock_date: uint256,
profit_unlocking_rate: uint256,
last_profit_update: uint256,
balance_of_self: uint256,
ts: uint256,
) -> uint256:
# Logic that assumes temporal consistency between parameters

In a protocol with $10M TVL, even modest manipulations could lead to:

  • Price deviations of 0.1-1% (representing 100K in value)

  • Incorrect profit unlocking timing affecting yield calculations

  • Potential for repeated exploitation as new proofs are generated

Technical Impact:

  • Breaks fundamental assumptions about state consistency

  • Undermines the reliability of cross-chain state verification

  • Creates unpredictable interactions between temporally inconsistent parameters

User Impact:

  • Users receive incorrect price information

  • Traders could face unfair or manipulated market conditions

  • Liquidity providers might suffer from value extraction

This vulnerability is classified as MEDIUM severity because:

  1. It requires sophisticated proof manipulation capabilities

  2. The impact depends on the specific parameters manipulated

  3. It breaks fundamental assumptions about state consistency

  4. It creates opportunities for economic exploitation

Tools Used

  • Manual code review focusing on parameter extraction logic

  • Temporal consistency analysis across state parameters

  • State parameter relationship modeling

  • Economic impact calculation for inconsistent parameter states

  • Proof manipulation simulation for mixed-state scenarios

Recommendations

Immediate Mitigations:

  1. Add temporal consistency validation to the parameter extraction process:

function _extractParametersFromProof(
bytes32 stateRoot,
bytes memory proofRlp
) internal view returns (uint256[PARAM_CNT] memory) {
// Existing extraction code
// Add temporal consistency validation
uint256[PARAM_CNT] memory params = // existing extraction logic
// Validate temporal consistency between time-dependent parameters
if (params[3] > 0) { // full_profit_unlock_date
require(params[3] > params[5], "Unlock date before last update");
// Validate profit_unlocking_rate consistency
uint256 expectedUnlockPeriod = params[3] - params[5]; // unlock_date - last_update
require(expectedUnlockPeriod > 0, "Invalid unlock period");
// Optional: validate rate is reasonable given unlock period and balance
uint256 expectedRate = params[6] * 1_000_000_000_000 / expectedUnlockPeriod;
uint256 tolerance = expectedRate / 100; // 1% tolerance
require(
params[4] >= expectedRate - tolerance &&
params[4] <= expectedRate + tolerance,
"Inconsistent unlocking rate"
);
} else {
// If no unlock date, rate should be zero
require(params[4] == 0, "Non-zero rate with no unlock date");
}
return params;
}
  1. Add timestamps to proof verification for freshness validation:

function verifyScrvusdByBlockHash(
bytes memory _block_header_rlp,
bytes memory _proof_rlp,
uint256 _maxAge // Add max age parameter
) external returns (uint256) {
Verifier.BlockHeader memory block_header = Verifier.parseBlockHeader(_block_header_rlp);
// Existing validation
// Add freshness check
require(block.timestamp - block_header.timestamp <= _maxAge, "Proof too old");
// Continue with verification
}

Long-term Fixes:

  1. Redesign the proof format to include explicit timestamps for each parameter:

struct TimestampedParameter {
uint256 value;
uint256 timestamp;
}
struct TimestampedProof {
bytes32 stateRoot;
uint256 blockTimestamp;
TimestampedParameter[] parameters;
}
  1. Implement a full state snapshot approach:

    • Take all parameters from a single, atomic state snapshot

    • Include cryptographic verification that all parameters come from the same block

    • Validate the entire state snapshot as a unit rather than individual parameters

  2. Add a parameter consistency validation layer:

    • Develop a mathematical model of expected relationships between parameters

    • Validate that extracted parameters conform to these relationship constraints

    • Reject proofs where parameters violate these relationships

By implementing these measures, the protocol can ensure that parameters extracted from proofs represent a consistent temporal state, preventing manipulation and ensuring accurate price calculations.

Updates

Lead Judging Commences

0xnevi Lead Judge 7 months ago
Submission Judgement Published
Invalidated
Reason: Out of scope
Assigned finding tags:

[invalid] finding-missing-proof-content-validation

- See [here]([https://github.com/CodeHawks-Contests/2025-03-curve?tab=readme-ov-file#blockhash-oracle)](https://github.com/CodeHawks-Contests/2025-03-curve?tab=readme-ov-file#blockhash-oracle) on how it is used to verify storage variable - All state roots and proofs must be verified by the OOS `StateProofVerifier` inherited as `Verifier` (where the price values and params are extracted), so there is no proof that manipulating timestamp/inputs can affect a price update - It is assumed that the OOS prover will provide accurate data and the OOS verifier will verify the prices/max unlock time to be within an appropriate bound/values - There is a account existance check in L96 of `ScrvusdVerifierV1.sol`, in which the params for price updates are extracted from

Support

FAQs

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