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

Integer overflow in `_unlocked_shares` leads to price manipulation

Summary

An unchecked multiplication of profit_unlocking_rate by the time delta can overflow the uint256 type and produce an incorrect computation of unlockedshares. This incorrect computation biases the _total_supply, which ultimately warps the price output in _raw_price. While exploitation is contingent upon a compromised PRICE_PARAMETERS_VERIFIER role to employ an excessively large profit_unlocking_rate, the absence of input validation enhances the risk at the expense of the integrity of the oracle.

Vulnerability Details

Here's the problematic snippet:

@view
def _unlocked_shares(
full_profit_unlock_date: uint256,
profit_unlocking_rate: uint256,
last_profit_update: uint256,
balance_of_self: uint256,
ts: uint256,
) -> uint256:
"""
Returns the amount of shares that have been unlocked.
To avoid sudden price_per_share spikes, profits can be processed
through an unlocking period. The mechanism involves shares to be
minted to the vault which are unlocked gradually over time. Shares
that have been locked are gradually unlocked over profit_max_unlock_time.
"""
unlocked_shares: uint256 = 0
if full_profit_unlock_date > ts:
# If we have not fully unlocked, we need to calculate how much has been.
unlocked_shares = profit_unlocking_rate * (ts - last_profit_update) // MAX_BPS_EXTENDED
elif full_profit_unlock_date != 0:
# All shares have been unlocked
unlocked_shares = balance_of_self
return unlocked_shares

Link for this: https://github.com/CodeHawks-Contests/2025-03-curve/blob/main/contracts/scrvusd/oracles/ScrvusdOracleV2.vy#L190-L213

The critical line is the following:

unlocked_shares = profit_unlocking_rate * (ts - last_profit_update) // MAX_BPS_EXTENDED

The problem with it is that multiplication profit_unlocking_rate * (ts - last_profit_update) uses uint256 variables. In Vyper 0.4.0, if the product exceeds 2**256 - 1 it wraps around because of the absence of overflow checks. This produces an incorrect unlocked_shares value after division by MAX_BPS_EXTENDED

This overflow affects the total supply calculation in the _total_supply function as shown here:

@view
def _total_supply(p: PriceParams, ts: uint256) -> uint256:
# Need to account for the shares issued to the vault that have unlocked.
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 the calculated unlocked_shares is greater than p.total_supply due to the overflow the subtraction will underflow and an artificially large total_supply value will be achieved

Impact

This vulnerability has severe consequences as an underflow in _total_supply results in division errors which disrupts oracle functionality. Furthermore a criminal with the PRICE_PARAMETERS_VERIFIER role could intentionally set a very large profit_unlocking_rate to cause the price oracle to report artificially low prices.

Tools Used

Manual review

Recommendations

Even though to exploit this vulnerability an attacker has to have privileged role I would fix this anyway as we have seen on real world scenarios where privileged role users exploited a vulnerability.

In order to fix this I would cap the profit_unlocking_rate value, like so:

@external
def update_price(
_parameters: uint256[ALL_PARAM_CNT], _ts: uint256, _block_number: uint256
) -> uint256:
# ... existing code ...
# Add a check for reasonable profit_unlocking_rate
assert _parameters[4] <= MAX_BPS_EXTENDED * 10**18, "Profit unlocking rate too high"
# ... rest of the function ...

I would then add a safe multiplication logic to prevent overflow before division

Updates

Lead Judging Commences

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

Support

FAQs

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