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

Lack of Oracle Return Value Validation

Summary

Both the ScrvusdVerifierV1.sol and ScrvusdVerifierV2.sol contracts make external calls to oracle contracts but fail to validate the return values from these calls, potentially masking oracle failures and leading to inconsistent protocol states. This is particularly concerning when examining the oracle contract (ScrvusdOracleV2.vy), which returns meaningful values like price changes and update success indicators.

Vulnerability Details

Severity: Medium

Files Affected:

  • contracts/scrvusd/verifiers/ScrvusdVerifierV1.sol

  • contracts/scrvusd/verifiers/ScrvusdVerifierV2.sol

Functions Affected:

  • _updatePrice() in ScrvusdVerifierV1

  • verifyPeriodByBlockHash() in ScrvusdVerifierV2

  • verifyPeriodByStateRoot() in ScrvusdVerifierV2

In ScrvusdVerifierV1.sol, the _updatePrice function returns the result from the oracle call without any validation:

// ScrvusdVerifierV1.sol - Line 95
function _updatePrice(
uint256[PARAM_CNT] memory params,
uint256 ts,
uint256 number
) internal returns (uint256) {
return IScrvusdOracle(SCRVUSD_ORACLE).update_price(params, ts, number);
// No validation of return value
}

Looking at the oracle contract, we can see that this return value is significant - it represents the relative price change:

# From ScrvusdOracleV2.vy
@external
def update_price(
_parameters: uint256[ALL_PARAM_CNT], _ts: uint256, _block_number: uint256
) -> uint256:
# ...
# Returns the absolute relative price change
if new_price > current_price:
return (new_price - current_price) * 10**18 // current_price
return (current_price - new_price) * 10**18 // current_price

Similarly, in ScrvusdVerifierV2.sol, both verification functions return the boolean result from the oracle calls without validation:

// ScrvusdVerifierV2.sol - Line 33
return IScrvusdOracleV2(SCRVUSD_ORACLE).update_profit_max_unlock_time(period, block_header.number);
// ScrvusdVerifierV2.sol - Line 44
return IScrvusdOracleV2(SCRVUSD_ORACLE).update_profit_max_unlock_time(period, _block_number);

The oracle's corresponding function returns a boolean indicating whether the update was successful:

# From ScrvusdOracleV2.vy
@external
def update_profit_max_unlock_time(_profit_max_unlock_time: uint256, _block_number: uint256) -> bool:
# ...
prev_value: uint256 = self.profit_max_unlock_time
self.profit_max_unlock_time = _profit_max_unlock_time
return prev_value != _profit_max_unlock_time

If these oracle calls fail silently (return 0 or false without reverting), the verifier contracts will propagate these failures without any indication of the issue. This creates a potential for silent failures where price or period updates appear to succeed but actually fail.

Impact

The lack of return value validation could lead to several issues:

  1. Silent Failures: If the oracle operations fail but return a value rather than reverting, callers of these functions might assume the operations succeeded when they actually failed.

  2. Inconsistent State: Silent failures could lead to inconsistent states between the verifier contracts and the oracles they interact with, potentially causing incorrect financial calculations.

  3. Price Change Monitoring Issues: The update_price function returns the relative price change, which could be important for monitoring and alerting systems. Without validation, significant price changes might not trigger appropriate alerts.

  4. Missed Period Updates: If update_profit_max_unlock_time returns false (indicating no change), this could be a meaningful business logic signal that's being ignored.

  5. Protocol Instability: Over time, repeated silent failures could lead to significant divergence between expected and actual protocol states, potentially affecting protocol stability and the accuracy of price data.

Tools Used

  • Manual code review

Recommendations

  1. Validate Oracle Return Values:

    • For ScrvusdVerifierV1, check the returned price change and potentially log or act on significant changes:

function _updatePrice(
uint256[PARAM_CNT] memory params,
uint256 ts,
uint256 number
) internal returns (uint256) {
uint256 priceChange = IScrvusdOracle(SCRVUSD_ORACLE).update_price(params, ts, number);
// Log significant price changes (e.g., more than 1%)
if (priceChange > 10**16) { // 1% = 10**16 with 10**18 precision
emit SignificantPriceChange(number, ts, priceChange);
}
return priceChange;
}
event SignificantPriceChange(uint256 blockNumber, uint256 timestamp, uint256 priceChange);
  • For ScrvusdVerifierV2, explicitly check the boolean return value:

function verifyPeriodByBlockHash(
bytes memory _block_header_rlp,
bytes memory _proof_rlp
) external returns (bool) {
// Existing implementation
bool success = IScrvusdOracleV2(SCRVUSD_ORACLE).update_profit_max_unlock_time(period, block_header.number);
// Log the update attempt and result
emit PeriodUpdateAttempted(block_header.number, period, success);
return success;
}
event PeriodUpdateAttempted(uint256 blockNumber, uint256 period, bool success);
  1. Consider Reverting on Failed Updates:

    • For critical updates, consider reverting the transaction if the update doesn't produce the expected result:

function verifyPeriodByStateRoot(
uint256 _block_number,
bytes memory _proof_rlp
) external returns (bool) {
// Existing implementation
bool success = IScrvusdOracleV2(SCRVUSD_ORACLE).update_profit_max_unlock_time(period, _block_number);
// Revert if update failed (didn't change the value)
require(success, "Period update didn't change the value");
return success;
}
  1. Add Comprehensive Event Emission for Oracle Interactions:

    • Emit detailed events for all oracle interactions to facilitate monitoring and debugging:

// In ScrvusdVerifierV1
event PriceUpdated(uint256 blockNumber, uint256 timestamp, uint256 priceChange);
function _updatePrice(
uint256[PARAM_CNT] memory params,
uint256 ts,
uint256 number
) internal returns (uint256) {
uint256 priceChange = IScrvusdOracle(SCRVUSD_ORACLE).update_price(params, ts, number);
emit PriceUpdated(number, ts, priceChange);
return priceChange;
}
// In ScrvusdVerifierV2
event PeriodUpdated(uint256 blockNumber, uint256 period, bool success);
function verifyPeriodByBlockHash(
bytes memory _block_header_rlp,
bytes memory _proof_rlp
) external returns (bool) {
// Existing implementation
bool success = IScrvusdOracleV2(SCRVUSD_ORACLE).update_profit_max_unlock_time(period, block_header.number);
emit PeriodUpdated(block_header.number, period, success);
return success;
}
  1. Implement Oracle Interaction Circuit Breakers:

    • Add circuit breaker logic to prevent continued updates if the oracle is consistently returning unexpected values:

uint256 public consecutiveFailedUpdates;
uint256 public constant MAX_FAILED_UPDATES = 3;
function verifyScrvusdByBlockHash(
bytes memory _block_header_rlp,
bytes memory _proof_rlp
) external returns (uint256) {
// Existing implementation
uint256 priceChange = _updatePrice(params, block_header.timestamp, block_header.number);
// Circuit breaker logic for suspicious price changes
if (priceChange > 10**17) { // 10% with 10**18 precision
consecutiveFailedUpdates++;
if (consecutiveFailedUpdates >= MAX_FAILED_UPDATES) {
// Trigger circuit breaker
paused = true;
emit CircuitBreakerTriggered(block_header.number, priceChange);
}
} else {
consecutiveFailedUpdates = 0;
}
return priceChange;
}

By implementing these recommendations, particularly the explicit validation and logging of oracle return values, the contracts would be more robust against silent failures and provide better visibility into oracle interaction issues.

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.