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

Denial of Service from Underflow in _total_supply() due to Unchecked _unlocked_shares() – ScrvusdOracleV2

Summary

ScrvusdOracleV2 Contract contains an arithmetic underflow issue when calculating unlocked shares. Specifically, if the _unlocked_shares() function returns a value greater than p.total_supply, the subsequent subtraction in _total_supply produces a negative result and triggers a revert in Vyper. This revert halts normal price update operations, potentially causing disruptions for integrators and rendering parts of the protocol inoperable. The problem arises due to the absence of a constraint ensuring that unlocked shares cannot surpass the total supply, especially under scenarios where large amounts of shares become unlocked over extended periods.

Vulnerability Details

This underflow issue arises in the ScrvusdOracleV2 Contract when _unlocked_shares() returns a value exceeding p.total_supply. Subtracting that value within _total_supply leads to a negative result, which Vyper handles by reverting the transaction. Under normal conditions, the protocol relies on consistent price updates to function properly. However, if an attacker or unusual input parameters inflate balance_of_self beyond the total supply—especially when a large amount of time has passed—subsequent price update calls can revert, creating a denial-of-service scenario.

By triggering repeated reverts, an attacker could disrupt critical functions related to price determination. This effectively breaks the protocol’s guarantee of reliable pricing, preventing integrators and other smart contracts from obtaining the correct asset valuation. The logic for calculating unlocked shares lacks a safeguard ensuring the unlocked portion cannot exceed the total supply, allowing a malicious or extreme set of parameters to propagate into _total_supply and cause the underflow-based revert.

Impact

This arithmetic underflow issue prevents normal execution of price updates whenever _unlocked_shares() returns a value greater than p.total_supply, causing a revert in Vyper. As a result, the affected contract becomes unable to provide accurate pricing data, disrupting dependent systems and integrators. While no direct token loss occurs immediately, the revert effectively halts a critical function of the protocol and can lead to broader availability issues. If exploits or extreme conditions repeatedly trigger this scenario, the resulting disruption can cause significant damage by blocking essential liquidity operations and preventing users from interacting with the protocol as intended.

Likelihood Explanation

The underflow condition depends on specific parameters being set such that the unlocked shares exceed the total supply. While this scenario may not arise under normal operations, especially if the system’s parameters are carefully controlled and updated regularly, it remains plausible that a large accumulation of shares or unexpected time gaps could trigger it. If these parameters are externally influenced or if the vault operates under high volatility or extended periods without updates, the chance of encountering this issue increases. Thus, while not guaranteed to happen in a typical setup, it is sufficiently possible to warrant caution and corrective measures.

Proof of Concept

An underflow arises when the _unlocked_shares() function returns a value greater than p.total_supply. Consequently, subtracting this value in _total_supply yields a negative result, causing a revert in Vyper. Although the transaction fails, this issue can halt normal execution and potentially leave the protocol in an inoperable state along that execution path.

Code Analysis

Below is a simplified, annotated snippet illustrating where the issue occurs:

def _unlocked_shares(
full_profit_unlock_date: uint256,
profit_unlocking_rate: uint256,
last_profit_update: uint256,
balance_of_self: uint256,
ts: uint256
) -> uint256:
unlocked_shares: uint256 = 0
# Calculates the number of unlocked shares accrued over time,
# based on the unlocking rate.
# unlocked_shares can potentially exceed total_supply
return unlocked_shares
def _total_supply(p: PriceParams, ts: uint256) -> uint256:
# (!) Subtraction without checking that unlocked_shares <= 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,
)

( ! ) If unlocked_shares exceeds p.total_supply, an underflow occurs, causing the execution to revert in Vyper.

Explanation

The _unlocked_shares function calculates unlocked shares based on factors such as elapsed time and profit_unlocking_rate. If the resulting value surpasses p.total_supply, the subtraction in _total_supply triggers an underflow and reverts the transaction. This revert disrupts price updates and impacts the function’s availability, hindering normal protocol operations.

Vulnerable Scenario

The system receives parameters that boost the vault’s balance_of_self above its total supply. In addition, the elapsed time (ts - last_profit_update) can be large enough for _unlocked_shares to grow significantly. When _total_supply is invoked, _unlocked_shares surpasses p.total_supply, the subtraction yields a negative result, and the contract reverts, blocking price updates and halting normal protocol operations.

Test and Result

This test confirms that if the unlocked shares (_unlocked_shares) exceed the vault's total_supply, the contract reverts due to arithmetic underflow. In the test, balance_of_self was set greater than total_supply while configuring a past full_profit_unlock_date, causing _unlocked_shares to be higher than total_supply. The resulting underflow triggers a revert, which the test successfully detects. The final output indicates 1 passed, confirming that the contract properly reverts under these conditions.

  • Add the following test to tests/scrvusd/oracle/unitary/test_v2.py

def test_unlocked_shares_exceed_total_supply_underflow(soracle, verifier):
"""
Validates that if the amount of unlocked shares (_unlocked_shares)
exceeds p.total_supply, the transaction reverts due to underflow.
"""
current_ts = boa.env.evm.patch.timestamp
# Configure a past full_profit_unlock_date and set balance_of_self higher than total_supply,
# ensuring unlocked_shares == balance_of_self is greater than total_supply and triggers an underflow.
price_params = [
1000, # total_debt
1000, # total_idle
1000, # total_supply
current_ts - 1, # full_profit_unlock_date (earlier than current time)
100, # profit_unlocking_rate (arbitrary)
current_ts - 500,# last_profit_update
2000, # balance_of_self exceeds total_supply
]
# Expect the transaction to revert due to underflow
with boa.env.prank(verifier):
with boa.reverts():
soracle.update_price(
price_params,
current_ts,
soracle.last_block_number() + 1
)
============== test session starts ===============
tests\scrvusd\oracle\unitary\test_v2.py . [100%]
=============== 1 passed in 0.26s ================

Confirmation

Review of the logic confirms that without an explicit cap on the unlocked share count relative to total_supply, an underflow inevitably occurs if certain parameters and extended time periods are met. This issue is genuine and causes transaction failures in the contract whenever the described condition materializes.

Tools Used

Manual Code Review
The contract code was meticulously analyzed line by line to detect logical oversights and confirm the underflow condition through targeted tests.

Recommendations

Implement a safeguard preventing _unlocked_shares from exceeding total_supply. For instance:

def _unlocked_shares(
full_profit_unlock_date: uint256,
profit_unlocking_rate: uint256,
last_profit_update: uint256,
balance_of_self: uint256,
ts: uint256
) -> uint256:
# ...
if full_profit_unlock_date > ts:
unlocked_shares = profit_unlocking_rate * (ts - last_profit_update) // MAX_BPS_EXTENDED
elif full_profit_unlock_date != 0:
unlocked_shares = balance_of_self
+ # Restrict unlocked_shares if it surpasses total_supply
+ if unlocked_shares > p.total_supply:
+ unlocked_shares = p.total_supply
return unlocked_shares

This ensures no arithmetic underflow when subtracting unlocked shares from p.total_supply.

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-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.