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

Price Underestimation Due to Incorrect Unlocked Shares Calculation in ScrvusdOracleV2 Contract

Summary

In the _obtain_price_params function, when params.last_profit_update + period < parameters_ts, it indicates that the period has matured. In this case, both params.last_profit_update and params.full_profit_unlock_date are increased by number_of_periods * period. However, in the _unlocked_shares function, if full_profit_unlock_date > ts, the calculated unlocked_shares becomes ts minus last_profit_update (which includes the added periods), without including the already matured balance_of_self. This causes unlocked_shares to be underestimated, which subsequently affects the calculation of the raw price.

Vulnerability Details

Consider the following scenario:

  1. In the _obtain_price_params function, when last_profit_update + period < parameters_ts (indicating the period has matured), the function updates the time parameters

  2. Both updated last_profit_update and full_profit_unlock_date are increased by number_of_periods * period

  3. In the _unlocked_shares function, these updated time parameters are used to determine the unlocking state and calculate unlocked shares

  4. If the updated full_profit_unlock_date > ts, the function calculates partial unlocked shares instead of returning the already matured balance_of_self. This leads to underestimation of unlocked shares.

  5. Price calculation impact: When unlocked_shares is underestimated, total_supply - unlocked_shares is overestimated. This results in an underestimated calculated price:

    price = total_assets/ (total_supply - unlocked_shares)

The issue arises from the interaction between these functions of _obtain_price_params and _unlocked_shares:

contracts/scrvusd/oracles/ScrvusdOracleV2.vy:_obtain_price_params#L279-L280

@view
def _obtain_price_params(parameters_ts: uint256) -> PriceParams:
"""
@notice Obtain Price parameters true or assumed to be true at `parameters_ts`.
Assumes constant gain(in crvUSD rewards) through distribution periods.
@param parameters_ts Timestamp to obtain parameters for
@return Assumed `PriceParams`
"""
params: PriceParams = self.price_params
period: uint256 = self.profit_max_unlock_time
if params.last_profit_update + period >= parameters_ts:
return params
number_of_periods: uint256 = min(
(parameters_ts - params.last_profit_update) // period,
self.max_v2_duration,
)
...
# audit add number_of_periods * period if params.last_profit_update + period < parameters_ts
params.full_profit_unlock_date += number_of_periods * period
params.last_profit_update += number_of_periods * period
return params

contracts/scrvusd/oracles/ScrvusdOracleV2.vy:_unlocked_shares#L205-L207

@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 # audit underestimated calculation of unlocked shares
elif full_profit_unlock_date != 0:
# All shares have been unlocked
unlocked_shares = balance_of_self
return unlocked_shares

contracts/scrvusd/oracles/ScrvusdOracleV2.vy:_total_supply#L219

@view
def _total_supply(p: PriceParams, ts: uint256) -> uint256:
# Need to account for the shares issued to the vault that have unlocked.
# audit total_supply is overestimated by underestimated _unlocked_shares
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
)

contracts/scrvusd/oracles/ScrvusdOracleV2.vy:_raw_price#L291

@view
def _raw_price(ts: uint256, parameters_ts: uint256) -> uint256:
"""
@notice Price replication from scrvUSD vault
"""
parameters: PriceParams = self._obtain_price_params(parameters_ts)
# audit price is underestimated by overestimated total_supply
return self._total_assets(parameters) * 10**18 // self._total_supply(parameters, ts)

Impact

Price Underestimation:

  • price = total_assets/ (total_supply - unlocked_shares)

  • Underestimation of unlocked shares leads to understated price calculations

  • Inaccurate prices may lead to incorrect market decisions

Tools Used

  • Manual code review

Recommendations

It is recommended that when params.last_profit_update + period < parameters_ts in the _obtain_price_params function, it indicates that the period has matured. In this case, the _unlocked_shares function should return shares that include balance_of_self.

Updates

Lead Judging Commences

0xnevi Lead Judge
2 months ago
0xnevi Lead Judge about 2 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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