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

Division by Zero in ScrvusdOracleV2's _raw_price() Function

Summary

The ScrvusdOracleV2 contract contains a critical vulnerability where the _raw_price() function can revert due to division by zero. This occurs when the effective total supply (calculated as total_supply - _unlocked_shares) becomes zero. The vulnerability can be triggered in multiple ways, including direct parameter manipulation, specific timing of updates, or as a result of normal system operations under certain conditions. This vulnerability could lead to a full denial of service for the price oracle, potentially impacting the entire scrvUSD ecosystem.

Vulnerability Details

In the _raw_price() function, the price calculation performs an integer division without validating that the divisor is non-zero:

return self._total_assets(parameters) * 10**18 // self._total_supply(parameters, ts)

The _total_supply() function calculates the effective total supply by subtracting unlocked shares from the total supply:

return p.total_supply - self._unlocked_shares(
p.full_profit_unlock_date,
p.profit_unlocking_rate,
p.last_profit_update,
p.balance_of_self,
ts, # block.timestamp
)

If _unlocked_shares equals or exceeds total_supply, the function will attempt to divide by zero, causing the transaction to revert.

Triggers

The vulnerability can be triggered in the following scenarios:

  1. Direct Parameter Manipulation: If an entity with the PRICE_PARAMETERS_VERIFIER role sets total_supply to zero.

  2. Full Share Unlocking: When the vault has fully unlocked all shares (full_profit_unlock_date is in the past) and the balance_of_self equals the total_supply.

  3. Parameter Calculation Issues: Specific combinations of profit_unlocking_rate, last_profit_update, and time differences can lead to unlocked shares equaling or exceeding the total supply.

  4. Smart Contract State Evolution: Natural system operation could lead to edge cases where the effective total supply becomes zero.

We tried a PoC (below) which confirmed that the vulnerability exists.

Here are the key test cases that demonstrated the issue:

  1. Setting total_supply directly to zero caused a revert.

  2. Setting total_supply equal to balance_of_self with full_profit_unlock_date in the past caused a revert due to effective total supply becoming zero.

  3. Setting a very high profit_unlocking_rate with a long period since last_profit_update caused a revert due to calculation issues in total supply computation.

import boa
import time
from eth.rlp.accounts import Account
import rlp
from eth_utils import keccak
from tests.shared.verifier import get_block_and_proofs
def test_division_by_zero_issue():
"""
Test to verify if a division by zero can occur in the _raw_price() function
of the ScrvusdOracleV2 contract when total_supply becomes zero.
"""
# Set a base timestamp for initialization
base_timestamp = int(time.time())
boa.env.evm.patch.timestamp = base_timestamp
# Setup admin account
admin = boa.env.generate_address()
boa.env.set_balance(admin, 10**18)
# Deploy blockhash oracle mock
with boa.env.prank(admin):
block_hash_oracle = boa.load("tests/shared/contracts/BlockHashOracleMock.vy")
# Deploy oracle contract
with boa.env.prank(admin):
oracle = boa.load("contracts/scrvusd/oracles/ScrvusdOracleV2.vy", 10**18)
# Grant roles
oracle.grantRole(oracle.PRICE_PARAMETERS_VERIFIER(), admin, sender=admin)
# Test Case 1: Normal case with non-zero total_supply
print("\n=== Test Case 1: Normal case with non-zero total_supply ===")
# Setup initial parameters with normal values
total_debt = 1000 * 10**18
total_idle = 500 * 10**18
total_supply = 1450 * 10**18
full_profit_unlock_date = base_timestamp + 86400 * 7 # 1 week from now
profit_unlocking_rate = 10 * 10**18
last_profit_update = base_timestamp - 86400 # 1 day ago
balance_of_self = 50 * 10**18
# Create parameters array
normal_parameters = [
total_debt,
total_idle,
total_supply,
full_profit_unlock_date,
profit_unlocking_rate,
last_profit_update,
balance_of_self
]
# Initialize the oracle
block_header, _ = get_block_and_proofs([])
block_hash_oracle._set_block_hash(100, block_header.hash, sender=admin)
block_hash_oracle._set_state_root(100, block_header.state_root, sender=admin)
# Update price with normal parameters
try:
oracle.update_price(normal_parameters, base_timestamp, 100, sender=admin)
price = oracle.price_v0()
print(f"Price with normal parameters: {price}")
print("Test Case 1: Success - No division by zero with normal parameters")
except Exception as e:
print(f"Test Case 1: Failed - Exception occurred: {e}")
# Test Case 2: Edge case with zero total_supply
print("\n=== Test Case 2: Edge case with zero total_supply ===")
# Setup parameters with zero total_supply
zero_supply_parameters = [
total_debt,
total_idle,
0, # Zero total_supply
full_profit_unlock_date,
profit_unlocking_rate,
last_profit_update,
balance_of_self
]
# Try to update price with zero total_supply
try:
oracle.update_price(zero_supply_parameters, base_timestamp, 101, sender=admin)
price = oracle.price_v0()
print(f"Price with zero total_supply: {price}")
print("Test Case 2: Failed - No division by zero occurred with zero total_supply")
except Exception as e:
print(f"Test Case 2: Success - Division by zero caught: {e}")
# Test Case 3: Edge case where _unlocked_shares equals total_supply
print("\n=== Test Case 3: Edge case where _unlocked_shares equals total_supply ===")
# Setup parameters where unlocked_shares could equal total_supply
# This happens when:
# 1. full_profit_unlock_date <= current timestamp (all shares unlocked)
# 2. balance_of_self equals total_supply
equal_parameters = [
total_debt,
total_idle,
balance_of_self, # total_supply equals balance_of_self
base_timestamp - 1, # full_profit_unlock_date in the past
profit_unlocking_rate,
last_profit_update,
balance_of_self
]
# Try to update price with parameters that could lead to zero effective total_supply
try:
oracle.update_price(equal_parameters, base_timestamp, 102, sender=admin)
price = oracle.price_v0()
print(f"Price with potentially zero effective total_supply: {price}")
print("Test Case 3: Failed - No division by zero occurred when _unlocked_shares equals total_supply")
except Exception as e:
print(f"Test Case 3: Success - Division by zero caught: {e}")
# Test Case 4: Edge case where _unlocked_shares could exceed total_supply
print("\n=== Test Case 4: Edge case where _unlocked_shares could exceed total_supply ===")
# Setup parameters where unlocked_shares could exceed total_supply
# This could happen with certain combinations of parameters
# Set a very high profit_unlocking_rate and a long time since last_profit_update
high_rate_parameters = [
total_debt,
total_idle,
balance_of_self, # Small total_supply
base_timestamp + 86400, # full_profit_unlock_date in the future
10**30, # Very high profit_unlocking_rate
base_timestamp - 86400 * 30, # last_profit_update 30 days ago
balance_of_self
]
# Try to update price with parameters that could lead to _unlocked_shares > total_supply
try:
oracle.update_price(high_rate_parameters, base_timestamp, 103, sender=admin)
price = oracle.price_v0()
print(f"Price with potentially excessive _unlocked_shares: {price}")
print("Test Case 4: Failed - No division by zero occurred when _unlocked_shares could exceed total_supply")
except Exception as e:
print(f"Test Case 4: Success - Division by zero or other exception caught: {e}")
# Summary
print("\n=== Summary ===")
print("The division by zero issue in _raw_price() function:")
print("1. Normal case: Works as expected with non-zero total_supply")
print("2. Zero total_supply: Directly setting total_supply to zero")
print("3. Effective zero: When _unlocked_shares equals total_supply")
print("4. Underflow: When _unlocked_shares could exceed total_supply")
if __name__ == "__main__":
test_division_by_zero_issue()

Impact

The division by zero vulnerability has several significant impacts:

  1. Complete Oracle Failure: When triggered, the oracle becomes completely non-functional, unable to provide price data to dependent contracts.

  2. System-Wide Disruption: As the price oracle is a critical component of the scrvUSD ecosystem, its failure could disrupt:

    • Stableswap pools using the oracle for price adjustments

    • Liquidation mechanisms dependent on accurate pricing

    • User transactions requiring price data

  3. Financial Risks:

    • Potential de-pegging of scrvUSD due to price feed disruptions

    • Inability to execute timely liquidations of undercollateralized positions

    • Arbitrage opportunities arising from price discrepancies

    • Potential loss of funds for users unable to exit positions

  4. Exploitability: While exploitation requires certain privileges or specific conditions, the impact could be substantial, especially if timed during market volatility or system stress.

Tools Used

The vulnerability was verified using:

  1. Custom Python Test Scripts: To simulate various parameter configurations and edge cases.

  2. Boa Framework: For interacting with the contract in a test environment.

  3. Manual Code Review: To identify potential division by zero paths in the contract logic.

Recommendations

Short-term Fixes

  1. Input Validation: Add explicit checks to prevent division by zero:

def _raw_price(self, ts: uint256, parameters_ts: uint256) -> uint256:
parameters: PriceParams = self._obtain_price_params(parameters_ts)
effective_total_supply: uint256 = self._total_supply(parameters, ts)
assert effective_total_supply > 0, "Total supply must be greater than zero"
return self._total_assets(parameters) * 10**18 // effective_total_supply
  1. Parameter Validation: Add checks in the update_price function to validate inputs:

assert _parameters[2] > 0, "Total supply must be greater than zero"
  1. Bound Checking: Ensure unlocked shares never exceed total supply:

def _unlocked_shares(...):
# Existing calculation
unlocked_shares: uint256 = ...
# Add maximum bound
return min(unlocked_shares, p.total_supply)

Long-term Improvements

  1. Fallback Mechanism: Implement a fallback price mechanism that can be used when the primary calculation fails.

  2. Circuit Breaker: Add a circuit breaker pattern to allow for emergency intervention without complete system failure.

  3. Enhanced Access Controls: Implement timelock delays and/or multi-signature requirements for sensitive parameter updates.

  4. Event Monitoring: Add events for significant parameter changes and near-zero values to facilitate monitoring.

  5. Formal Verification: Consider formal verification of critical math operations to ensure they cannot result in division by zero under any circumstances.

Updates

Lead Judging Commences

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

[invalid] finding-division-by-zero

Note that `total_supply` and `profit_unlocking_rate` is initially set to 1 and 0 respectively when the `ScrvusdOracleV2.vy` is deployed 1. `total_supply` and `profit_unlocking_rate` is part of the price param updates within `update_price`, which must have gone through verification via the OOS `StateProofVerifier` contract, so there is no evidence that a 0 supply is allowed either via a 0 supply update or an extremely high `profit_unlocking_rate`. 2. Since price is retrieved via values retrived from the V3Vault, if there is no supply, there is arguably no price to be posted. As such, reverting is arguably the correct choice since a 0 price value is not expected from scrvUSD, which is a stable coin.

Support

FAQs

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