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

Potential front-run attack

Summary

The ScrvusdVerifierV1 and ScrvusdVerifierV2 contracts are designed to verify and update the scrvUSD price and profit_max_unlock_time parameters using state proofs. However, a vulnerability exists due to the lack of strict validation on the block_number parameter in the update_price and update_profit_max_unlock_time functions. This allows an attacker to front-run legitimate calls by submitting a newer block_number through ScrvusdVerifierV2, causing subsequent legitimate calls to fail. This issue can disrupt the normal operation of the system and potentially be exploited for malicious purposes.

Vulnerability Details

In ScrvusdVerifierV1 , anyone can call verifyScrvusdByBlockHash and verifyScrvusdByStateRoot :

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);
}
/// @param _block_number Number of the block to use state root hash
/// @param _proof_rlp The state proof of the parameters
function verifyScrvusdByStateRoot(
uint256 _block_number,
bytes memory _proof_rlp
) external returns (uint256) {
bytes32 state_root = IBlockHashOracle(BLOCK_HASH_ORACLE).get_state_root(_block_number);
uint256[PARAM_CNT] memory params = _extractParametersFromProof(state_root, _proof_rlp);
// Use last_profit_update as the timestamp surrogate
return _updatePrice(params, params[5], _block_number);
}

These two functions will call update_price :

function _updatePrice(
uint256[PARAM_CNT] memory params,
uint256 ts,
uint256 number
) internal returns (uint256) {
return IScrvusdOracle(SCRVUSD_ORACLE).update_price(params, ts, number);
}

In ScrvusdOracle , the update_price function will check the last_block_number :

def update_price(
_parameters: uint256[ALL_PARAM_CNT], _ts: uint256, _block_number: uint256
) -> uint256:
"""
@notice Update price using `_parameters`
@param _parameters Parameters of Yearn Vault to calculate scrvUSD price
@param _ts Timestamp at which these parameters are true
@param _block_number Block number of parameters to linearize updates
@return Absolute relative price change of final price with 10^18 precision
"""
access_control._check_role(PRICE_PARAMETERS_VERIFIER, msg.sender)
# Allowing same block updates for fixing bad blockhash provided (if possible)
assert self.last_block_number <= _block_number, "Outdated"
self.last_block_number = _block_number

In ScrvusdVerifierV2 , anyone can call verifyPeriodByBlockHash and verifyPeriodByStateRoot :

function verifyPeriodByBlockHash(
bytes memory _block_header_rlp,
bytes memory _proof_rlp
) external returns (bool) {
Verifier.BlockHeader memory block_header = Verifier.parseBlockHeader(_block_header_rlp);
require(block_header.hash != bytes32(0), "Invalid blockhash");
require(
block_header.hash == IBlockHashOracle(ScrvusdVerifierV1.BLOCK_HASH_ORACLE).get_block_hash(block_header.number),
"Blockhash mismatch"
);
uint256 period = _extractPeriodFromProof(block_header.stateRootHash, _proof_rlp);
return IScrvusdOracleV2(SCRVUSD_ORACLE).update_profit_max_unlock_time(period, block_header.number);
}
/// @param _block_number Number of the block to use state root hash
/// @param _proof_rlp The state proof of the period
function verifyPeriodByStateRoot(
uint256 _block_number,
bytes memory _proof_rlp
) external returns (bool) {
bytes32 state_root = IBlockHashOracle(ScrvusdVerifierV1.BLOCK_HASH_ORACLE).get_state_root(_block_number);
uint256 period = _extractPeriodFromProof(state_root, _proof_rlp);
return IScrvusdOracleV2(SCRVUSD_ORACLE).update_profit_max_unlock_time(period, _block_number);
}

which will call update_profit_max_unlock_time :

return IScrvusdOracleV2(SCRVUSD_ORACLE).update_profit_max_unlock_time(period, _block_number);

In ScrvusdOracle , the update_profit_max_unlock_time function will check the same last_block_number :

@external
def update_profit_max_unlock_time(_profit_max_unlock_time: uint256, _block_number: uint256) -> bool:
"""
@notice Update price using `_parameters`
@param _profit_max_unlock_time New `profit_max_unlock_time` value
@param _block_number Block number of parameters to linearize updates
@return Boolean whether value changed
"""
access_control._check_role(UNLOCK_TIME_VERIFIER, msg.sender)
# Allowing same block updates for fixing bad blockhash provided (if possible)
assert self.last_block_number <= _block_number, "Outdated"
self.last_block_number = _block_number

The vulnerability arises because both update_price and update_profit_max_unlock_time functions validate the block_number against the same last_block_number. If a user submits a transaction with an older block_number, an attacker can monitor the transaction and front-run it by submitting a newer block_number through ScrvusdVerifierV2. This causes the legitimate transaction to fail due to the block_number being outdated.

Attack Scenario

  1. A user calls ScrvusdVerifierV1's verifyScrvusdByBlockHash or verifyScrvusdByStateRoot function, submitting an older block_number.

  2. An attacker observes the transaction and quickly calls ScrvusdVerifierV2's verifyPeriodByBlockHash or verifyPeriodByStateRoot function, submitting a newer block_number.

  3. The attacker's transaction is executed first, updating the last_block_number to the newer value.

  4. The user's transaction is executed afterward but fails because the submitted block_number is now considered outdated.

Impact

  • Legitimate transactions may fail, preventing the timely update of scrvUSD prices or profit_max_unlock_time.

  • Attackers can exploit this vulnerability to disrupt the system's normal operation or manipulate parameters for their benefit.

  • The system's reliability and trustworthiness may be compromised.

The impact is Medium, the likelihood is Medium, so the severity is Medium.

Tools Used

Manual Review

Recommendations

Do not use the same last_block_number for different function.

Updates

Lead Judging Commences

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