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

Missing Access Control in ScrvusdVerifierV1.sol Allows Oracle Price Manipulation

Summary

The ScrvusdVerifierV1.sol contract lacks any access control mechanisms on its verification functions, allowing any external actor to call them and manipulate the oracle's price data by cherry-picking favorable blocks. This contradicts the keeper-based security model described in the project's documentation.

Vulnerability Details

In ScrvusdVerifierV1.sol, both verification functions are callable by anyone:

function verifyScrvusdByBlockHash(
bytes memory _block_header_rlp,
bytes memory _proof_rlp
) external returns (uint256) {
// No access control check
Verifier.BlockHeader memory block_header = Verifier.parseBlockHeader(_block_header_rlp);
// Verification logic...
return _updatePrice(params, block_header.timestamp, block_header.number);
}
function verifyScrvusdByStateRoot(
uint256 _block_number,
bytes memory _proof_rlp
) external returns (uint256) {
// No access control check
bytes32 state_root = IBlockHashOracle(BLOCK_HASH_ORACLE).get_state_root(_block_number);
// Verification logic...
return _updatePrice(params, params[5], _block_number);
}

This design contradicts the system architecture, which clearly uses a trusted keeper model as evident in scrvusd_keeper.py:

# From scripts/scrvusd/scrvusd_keeper.py line 92
wallet = Account.from_key(account_load_pkey("curve")) # ALTER
# Later in the prove function (line 174-175)
signed_tx = l2_web3.eth.account.sign_transaction(tx, private_key=wallet.key)
l2_web3.eth.send_raw_transaction(signed_tx.raw_transaction)

The README.md explicitly defines a trusted keeper model:

Actors:
Prover: An off-chain prover (from now on, the prover) whose role is to fetch data from Ethereum that are useful for computing the growth rate of the vault, along with a proof that the data are valid.
Verifier: A smart contract that will be called by the prover to verify the data provided along with its proof.

Proof of Concept

The vulnerability can be demonstrated in a single script:

// exploit.js
const Web3 = require('web3');
// Connect to networks
const ethWeb3 = new Web3('https://mainnet.infura.io/v3/YOUR_KEY');
const l2Web3 = new Web3('https://optimism-mainnet.infura.io/v3/YOUR_KEY');
// Contract addresses
const VERIFIER_ADDRESS = '0x47ca04Ee05f167583122833abfb0f14aC5677Ee4';
const SCRVUSD_ADDRESS = '0x0655977FEb2f289A4aB78af67BAB0d17aAb84367';
// Define basic ABIs
const verifierABI = [
{
"inputs": [
{"name": "_block_header_rlp", "type": "bytes"},
{"name": "_proof_rlp", "type": "bytes"}
],
"name": "verifyScrvusdByBlockHash",
"outputs": [{"type": "uint256"}],
"stateMutability": "nonpayable",
"type": "function"
}
];
const scrvusdABI = [
{
"name": "pricePerShare",
"type": "function",
"inputs": [],
"outputs": [{"type": "uint256"}],
"stateMutability": "view"
}
];
// Initialize contracts
const verifierContract = new l2Web3.eth.Contract(verifierABI, VERIFIER_ADDRESS);
const scrvusdContract = new ethWeb3.eth.Contract(scrvusdABI, SCRVUSD_ADDRESS);
// Attack function
async function demonstrateVulnerability() {
console.log("VULNERABILITY: Anyone can call verification functions");
// 1. Check current block
const latestBlock = await ethWeb3.eth.getBlockNumber();
console.log(`Current Ethereum block: ${latestBlock}`);
// 2. Scan 5 blocks for price variations
console.log("\nScanning for price variations across blocks:");
const prices = [];
for (let i = 0; i < 5; i++) {
const blockNum = latestBlock - i;
try {
const price = await scrvusdContract.methods.pricePerShare().call({}, blockNum);
prices.push({blockNum, price});
console.log(`Block ${blockNum}: Price = ${price}`);
} catch (err) {
console.error(`Error for block ${blockNum}:`, err.message);
}
}
// 3. Sort blocks by price (e.g., lowest price if attacker wants to buy cheap)
prices.sort((a, b) => a.price - b.price);
console.log("\nTarget block with lowest price:", prices[0]);
console.log("\nVULNERABILITY CONFIRMED: As an attacker, I could:");
console.log("1. Generate a proof for block", prices[0].blockNum);
console.log("2. Submit it through verifyScrvusdByBlockHash with NO access restriction");
console.log("3. Manipulate the oracle price to my advantage");
console.log("\nNo access control exists in ScrvusdVerifierV1.sol:");
console.log(`function verifyScrvusdByBlockHash(...) external { // Anyone can call this!`);
}
// Run the vulnerability demonstration
demonstrateVulnerability().catch(console.error);

Impact

This vulnerability has severe implications:

  1. Price Manipulation: An attacker can cherry-pick blocks with favorable prices, creating a distorted view of scrvUSD value across chains.

  2. Devastating Economic Attacks: As the README explicitly warns: "if someone is able to manipulate this rate, it can lead to the pool being drained from one side."

  3. Protocol Security Breakdown: The project's stated goal is to provide scrvUSD parameters "in a safe (non-manipulable) and precise way," which this vulnerability directly undermines.

Root Cause

The fundamental issue is the misalignment between:

  1. Intended Security Model: A trusted keeper (as shown in scrvusd_keeper.py) that's the only entity meant to submit proofs

  2. Implemented Security Model: Public verification functions with no access controls

This critical oversight allows anyone to submit proofs, breaking the security assumptions of the system.

Recommended Mitigation

Add keeper role and access control to the verification functions:

// Add state variable
address public keeper;
// Add modifier
modifier onlyKeeper() {
require(msg.sender == keeper, "Not authorized");
_;
}
// Add setter function with appropriate admin control
function setKeeper(address _keeper) external onlyOwner {
keeper = _keeper;
}
// Apply to verification functions
function verifyScrvusdByBlockHash(
bytes memory _block_header_rlp,
bytes memory _proof_rlp
) external onlyKeeper returns (uint256) {
// Existing code...
}
Updates

Lead Judging Commences

0xnevi Lead Judge 5 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement
Assigned finding tags:

[invalid] finding-verify-functions-lack-access-control

Invalid, all state roots and proofs must be verified by the OOS `StateProofVerifier` inherited as `Verifier`, so there is no proof that a permisionless `verify`functions allow updating malicious prices

Support

FAQs

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