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

Oracle Price Manipulation via Smoothing Acceleration Enables Cross-Chain Arbitrage Due to Proof Replay Attacks

Description

A critical vulnerability has been identified in the verifyScrvusdByBlockHash function of the ScrvusdVerifierV1 contract. The function lacks a mechanism to prevent the resubmission of previously processed proofs, allowing for a proof replay attack.

The verifyScrvusdByBlockHash function is designed to verify scrvUSD vault parameters from Ethereum and update the oracle on target chains. However, it only enforces that block numbers are processed in non-decreasing order (self.last_block_number <= _block_number) but does not prevent processing the same block multiple times.

function verifyScrvusdByBlockHash(
bytes memory _block_header_rlp,
bytes memory _proof_rlp
) external returns (uint256) {
Verifier.BlockHeader memory block_header = Verifier.parseBlockHeader(_block_header_rlp);
require(block_header.hash != bytes32(0), "Invalid blockhash");
require(
block_header.hash == IBlockHashOracle(BLOCK_HASH_ORACLE).get_block_hash(block_header.number),
"Blockhash mismatch"
);
uint256[PARAM_CNT] memory params = _extractParametersFromProof(block_header.stateRootHash, _proof_rlp);
return _updatePrice(params, block_header.timestamp, block_header.number);
}

The vulnerability exists because:

  1. The function does not track which specific block hashes have already been processed

  2. It allows reprocessing the same block through _block_number == last_block_number

  3. No nonce or other replay protection mechanism is implemented

Attack Vectors

Smoothing Acceleration Attack

An attacker identifies a significant gap between the current smoothed price and raw price. By repeatedly submitting the same valid proof transaction in consecutive blocks, they manipulate the time-dependent smoothing function. Each resubmission preserves the identical raw price target while resetting self.last_update to the current timestamp. This forces recalculation of max_change based on new block times, applying multiple adjustment steps toward the target price. The cumulative effect artificially expedites price movement, circumventing the protocol's rate-limiting mechanism designed to keep price changes below the arbitrage threshold of 0.5bps per block.

Flash Loan Arbitrage

When scrvUSD price movements exceed the stableswap pool fee threshold (~1bps), arbitrage opportunities emerge. Attackers can detect when smoothed price is approaching but still significantly divergent from raw price, then execute proof replays to accelerate convergence past the arbitrage threshold. By deploying flash loans, they exploit the price discrepancy between the oracle-influenced stableswap pool price and the actual market price of scrvUSD on other venues. This extracts risk-free profit proportional to the pool's liquidity depth at the expense of liquidity providers.

Timed Sandwich Attack

The vulnerability enables precisely timed market manipulation through sandwich attacks. Attackers monitor pending vault parameter proof submissions, and when significant parameter changes appear, they front-run with trades in anticipation of price direction, inject repeated proof submissions of current parameters to accelerate price movement in their favor, then back-run with offsetting trades after price adjustment. This sophisticated attack extracts value from normal market operations without contributing any useful economic activity to the protocol.

Cross-Chain Price Desynchronization

By selectively targeting specific chains with the replay attack, attackers can create artificial price discrepancies between different blockchain implementations of the oracle. Accelerating price movement on Chain A through replay attacks while leaving Chain B to adjust at the normal rate creates exploitable arbitrage opportunities across chains. This undermines the core goal of maintaining consistent scrvUSD pricing across all supported networks and fragments liquidity across the protocol's cross-chain ecosystem.

Impact

When the oracle processes an update, the _smoothed_price function implements a rate-limiting mechanism to prevent abrupt price changes. This security measure works by calculating a maximum allowable price movement (max_change) per update, which is proportional to the time elapsed since the last update, the current price, and a configurable parameter called max_price_increment:

max_change: uint256 = (
self.max_price_increment * (block.timestamp - self.last_update) * last_price // 10**18
)

The vulnerability arises from how this time-dependent formula behaves when processing identical updates repeatedly:

  1. During normal operation, when a new legitimate proof is submitted with updated parameters:

    • The raw price reflects new vault parameters

    • self.last_update is set to current block.timestamp

    • The smoothed price moves toward raw price, limited by max_change

  2. When exploiting the replay vulnerability by submitting an identical proof:

    • The raw price remains unchanged (using same parameters)

    • However, self.last_update is reset to the current timestamp

    • A new max_change value is calculated based on time elapsed

    • The smoothed price can move further toward the same raw price target

The critical technical issue is that each resubmission creates a new discrete step in the smoothing algorithm, effectively partitioning what should be a continuous time-based approach into smaller increments. This allows an attacker to manipulate the rate of price convergence.

For example, if the target raw price is 10% higher than the current smoothed price, and the maximum allowed change is 1% per hour:

  • Normal: Price would take 10 hours to fully converge

  • Exploited: Attacker submits the same proof every 10 minutes, reaching full convergence in ~1.7 hours

This attack directly undermines the protocol's carefully calibrated risk parameters designed to ensure that price movements remain below the arbitrage threshold for AMM pools (typically 1bps). By bypassing this rate-limiting, attackers can create profitable arbitrage opportunities against pools using the oracle.

verifyScrvusdByStateRoot shares the same vulnerability as verifyScrvusdByBlockHash - it lacks protection against proof replays, allowing the same proof to be submitted multiple times.

This function is particularly interesting because it uses params[5] (last_profit_update) as the timestamp, meaning that replaying the same proof would always use the same timestamp value. However, the smoothing acceleration attack remains possible because self.last_update in the oracle would still be updated to the current block time when the proof is processed.

Remediation Steps

  1. Track Processed Blocks: Implement a mapping of already processed block hashes:

    mapping(bytes32 => bool) public processedBlockHashes;
  2. Enforce Strict Ordering: Modify the requirement to strictly enforce increasing block numbers:

    require(self.last_block_number < _block_number, "Block already processed");
  3. Add Replay Protection:

    require(!processedBlockHashes[block_header.hash], "Block already processed");
    processedBlockHashes[block_header.hash] = true;
Updates

Lead Judging Commences

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

[invalid] finding-block-number-no-input-check

- Anything related to the output by the `BLOCK_HASH_ORACLE` is OOS per \[docs here]\(<https://github.com/CodeHawks-Contests/2025-03-curve?tab=readme-ov-file#blockhash-oracle>). - The PoC utilizes a mock `BLOCK_HASH_ORACLE`which is not representative of the one used by the protocol - Even when block hash returned is incorrect, the assumption is already explicitly made known in the docs, and the contract allows a subsequent update within the same block to update and correct prices - All state roots and proofs must be verified by the OOS `StateProofVerifier` inherited as `Verifier`, so there is no proof that manipulating block timestamp/block number/inputs can affect a price update - There seems to be a lot of confusion on the block hash check. The block hash check is a unique identifier of a block and has nothing to do with the state root. All value verifications is performed by the OOS Verifier contract as mentioned above

Support

FAQs

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