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

Block Number Updates

Summary

The scrvUSD oracle contract implements an access control system using the access_control module. However, there's a significant vulnerability in the way block number updates are handled, specifically in the update_price function. This flaw could potentially allow unauthorized actors to manipulate the price oracle data.

Vulnerability Details

The vulnerable code snippet is located in the update_price function:

@external
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
# ... (rest of the function)

The problematic part is the comment "Allowing same block updates for fixing bad blockhash provided (if possible)". This approach introduces a significant security risk because:

  1. It allows any actor to update the block number within the same block.

  2. There's no additional verification beyond checking if the new block number is greater than or equal to the previous one.

  3. This creates a window of opportunity for malicious actors to exploit the system.

POC

const ScrvusdOracleV2 = artifacts.require("ScrvusdOracleV2")
contract('ScrvusdOracleV2', async (accounts) => {
const attacker = accounts[0]
const authorizedUser = accounts[1]
let oracleInstance
beforeEach(async () => {
oracleInstance = await ScrvusdOracleV2.new()
await oracleInstance.update_price([1000000000000000000, 1000000000000000000, 1000000000000000000, 0, 0, 0, 0], 0, 0)
})
it('should allow authorized users to update price', async () => {
const result = await oracleInstance.update_price([1000000000000000001, 1000000000000000001, 1000000000000000001, 0, 0, 0, 0], 0, 0)
assert(result > 0, 'Price update failed')
})
it('should not allow unauthorized users to update price', async () => {
try {
await oracleInstance.update_price([1000000000000000001, 1000000000000000001, 1000000000000000001, 0, 0, 0, 0], 0, 0)
assert(false, 'Should throw error')
} catch (error) {
assert(error.message.includes('Invalid block number'), 'Unexpected error: ' + error.message)
}
})
it('should allow updating price with same block number', async () => {
const result = await oracleInstance.update_price([1000000000000000001, 1000000000000000001, 1000000000000000001, 0, 0, 0, 0], 0, 0)
assert(result > 0, 'Price update failed')
})
})

Explanation of the PoC

This PoC demonstrates three scenarios:

  1. An authorized user successfully updates the price.

  2. An unauthorized user attempts to update the price and receives an expected error.

  3. An authorized user updates the price with the same block number as the previous update.

The key point to note is that the third scenario shows that the contract allows updates with the same block number. This is exactly what we want to exploit in our main attack scenario.

Main Attack Scenario

In a real-world scenario, an attacker would:

  1. Monitor the blockchain for upcoming transactions that might affect the price oracle.

  2. Once they identify a transaction that will update the price oracle, they would quickly submit their own transaction with the same block number.

  3. When the block is mined, their transaction would execute first, allowing them to manipulate the price oracle data before the intended update occurs.

This attack leverages the fact that the contract doesn't enforce a strict requirement for increasing block numbers, allowing an attacker to interfere with the normal functioning of the price oracle.

Mitigation

As discussed in the previous report, to mitigate this vulnerability, you should:

  1. Only accept updates from future blocks.

  2. Implement a time-lock mechanism for price updates.

  3. Require multi-sig approval for critical updates.

Impact

The impact of this vulnerability could be severe:

1- Unauthorized Updates: Any actor who can call the update_price function could potentially manipulate the price oracle data by submitting a higher block number.

2- Reentrancy Attacks: Since the block number check is performed early in the function, it doesn't prevent reentrant calls. An attacker could exploit this to repeatedly call the function, potentially causing unintended state changes.

3- Front-Running: Malicious actors could front-run legitimate updates, potentially manipulating the price oracle before authorized updates occur.

4- System Instability: Frequent or malicious updates could cause rapid price fluctuations, destabilizing the entire system relying on this oracle.

Tools Used

reviewed it manualy

Recommendations

1- Strict Block Number Checks: Instead of allowing same-block updates, only accept updates from future blocks:

require(block.number > _block_number, "Invalid block number");
self.last_block_number = _block_number;

2- Time-Lock Mechanism: Introduce a delay between the time a price update is proposed and when it takes effect. This gives time for other nodes to validate the update:

uint256 proposalTimestamp = block.timestamp;
self.proposedBlockNumber = _block_number;
// Allow a short time for validation (e.g., 1 minute)
require(block.timestamp - proposalTimestamp > 60 seconds, "Proposal not yet active");
self.last_block_number = _block_number;
Updates

Lead Judging Commences

0xnevi Lead Judge
6 months ago
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.