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

Centralization Risk in Oracle Parameter Updates

1. Summary

  • Severity: Medium

  • Category: Access Control / Centralization Risk

  • Impact: Single admin can manipulate price movement constraints

  • Likelihood: High (requires only admin key compromise)


2. Affected Code

@external
def set_max_price_increment(_max_price_increment: uint256):
access_control._check_role(access_control.DEFAULT_ADMIN_ROLE, msg.sender)
assert 10**8 <= _max_price_increment and _max_price_increment <= 10**18
self.max_price_increment = _max_price_increment
log SetMaxPriceIncrement(_max_price_increment)
  • Contract: ScrvusdOracleV2.vy

  • Functions: set_max_price_increment, set_max_v2_duration


3. Vulnerability Details

Root Cause

Critical oracle parameters can be modified immediately by a single admin without any timelock or multi-signature requirement. While bounds are implemented, the admin still has significant control over price movement limitations.

Attack Scenario

  1. Attacker compromises admin key

  2. Sets max_price_increment to maximum allowed value (10^18)

  3. This allows sudden price movements up to 100% per second

  4. Malicious actor exploits the increased price volatility in StableSwap pools

  5. Results in potential drain of liquidity pools due to arbitrage opportunities


4. Proof of Concept (PoC)

import pytest
import boa
from eth_utils import encode_hex
from eth_abi import encode
MAX_PRICE_INCREMENT = 2 * 10**12 # Default safe value
ATTACK_PRICE_INCREMENT = 10**18 # Maximum allowed value
INITIAL_PRICE = 10**18 # 1.0 in 18 decimals
@pytest.fixture(scope="module")
def setup_oracle(admin, anne):
with boa.env.prank(admin):
oracle = boa.load("contracts/scrvusd/oracles/ScrvusdOracleV2.vy", INITIAL_PRICE)
return oracle, admin, anne
def test_admin_can_manipulate_price_bounds(setup_oracle):
oracle, admin, _ = setup_oracle
# Step 1: Verify initial safe configuration
assert oracle.max_price_increment() == MAX_PRICE_INCREMENT
initial_time = boa.env.vm.patch.timestamp
# Calculate initial price movement limits
initial_max_change = calculate_max_price_change(
oracle,
INITIAL_PRICE,
MAX_PRICE_INCREMENT,
initial_time + 3600 # 1 hour later
)
# Step 2: Admin (attacker) changes max_price_increment to maximum
with boa.env.prank(admin):
oracle.set_max_price_increment(ATTACK_PRICE_INCREMENT)
# Step 3: Calculate new maximum price change possible
attack_max_change = calculate_max_price_change(
oracle,
INITIAL_PRICE,
ATTACK_PRICE_INCREMENT,
initial_time + 3600 # 1 hour later
)
# Step 4: Verify the attack impact
print(f"Initial max price change (1h): {initial_max_change / 10**16:.2f}%")
print(f"Attack max price change (1h): {attack_max_change / 10**16:.2f}%")
# The attack allows much larger price movements
assert attack_max_change > initial_max_change * 100
# Step 5: Demonstrate impact on price updates
ts = initial_time + 3600
params = [0, 10**18, 10**18, ts + 86400, 0, ts, 0] # Example parameters
with boa.env.prank(admin):
# Grant price verifier role to admin for testing
oracle.grantRole(
encode_hex(oracle.PRICE_PARAMETERS_VERIFIER()),
admin
)
# Update price with manipulated bounds
oracle.update_price(params, ts, boa.env.vm.patch.block_number)
# Verify new price can move drastically
new_price = oracle.price_v0()
assert abs(new_price - INITIAL_PRICE) > initial_max_change * 10
def calculate_max_price_change(oracle, price, increment, future_ts):
"""Helper to calculate maximum price change possible"""
current_ts = boa.env.vm.patch.timestamp
time_delta = future_ts - current_ts
max_change = (increment * time_delta * price) // 10**18
return max_change
def test_non_admin_cannot_change_parameters(setup_oracle):
oracle, _, anne = setup_oracle
# Attempt parameter change as non-admin
with boa.env.prank(anne):
with boa.reverts(): # Should revert
oracle.set_max_price_increment(ATTACK_PRICE_INCREMENT)
# Verify parameters unchanged
assert oracle.max_price_increment() == MAX_PRICE_INCREMENT

5. Recommended Fix

Proposed Solution

Implement a timelock mechanism for parameter updates:

PARAMETER_TIMELOCK: constant(uint256) = 86400 # 24 hours
struct PendingUpdate:
value: uint256
timestamp: uint256
pending_max_price_increment: PendingUpdate
@external
def propose_max_price_increment(_max_price_increment: uint256):
access_control._check_role(access_control.DEFAULT_ADMIN_ROLE, msg.sender)
assert 10**8 <= _max_price_increment and _max_price_increment <= 10**18
self.pending_max_price_increment = PendingUpdate({
value: _max_price_increment,
timestamp: block.timestamp
})
@external
def apply_max_price_increment():
assert block.timestamp >= self.pending_max_price_increment.timestamp + PARAMETER_TIMELOCK
assert self.pending_max_price_increment.value != 0
self.max_price_increment = self.pending_max_price_increment.value
self.pending_max_price_increment = PendingUpdate({value: 0, timestamp: 0})
log SetMaxPriceIncrement(self.max_price_increment)

Alternative Mitigation Strategies

  1. Implement multi-signature requirement for parameter changes

  2. Add emergency pause functionality

  3. Further restrict parameter bounds based on pool economics

  4. Implement gradual parameter updates over time


6. Severity Justification

  • Impact: Medium - While bounds exist, parameter manipulation could still significantly impact pool operations

  • Likelihood: High - Single point of failure through admin key compromise

The issue is rated as Medium severity because:

  1. Parameter bounds provide some protection

  2. Changes are logged and transparent

  3. Existing tests validate role-based access control

  4. Documentation acknowledges DAO control

Updates

Lead Judging Commences

0xnevi Lead Judge 3 months ago
Submission Judgement Published
Invalidated
Reason: Out of scope
Assigned finding tags:

[invalid] finding-centralization-risk

- Per [codehawks documentation](https://docs.codehawks.com/hawks-auditors/how-to-determine-a-finding-validity#findings-that-may-be-invalid) - Parameter change is executed via the Dao per docs > Also, it is worth noting that the oracle is controlled by a DAO and its parameters can be changed by a vote.

Support

FAQs

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