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

Profit unlocking Mismatch in ScrvUSD Oracle leading to price inflation

Summary

Having a mismatch in precision constants between the Ethereum vault and the Oracle. The Ethereum vault uses MAX_BPS = 1e18 for calculations, while the Oracle uses MAX_BPS_EXTENDED = 1e12 -> causes the Oracle to overestimate the number of unlocked shares, underrepresenting the effective total supply of scrvUSD

-> result: The price per share reported by the Oracle becomes inflated

Vulnerability Details

Inconsistent precision constants used in the calculation of unlocked_shares:

The mismatch:

  • Root Cause: MAX_BPS_EXTENDED = 1e12 in the Oracle versus MAX_BPS = 1e18 on Ethereum

Code Location: ScrvusdOracleV2.vy, _obtain_price_params() function:

Uses MAX_BPS_EXTENDED = 1e12 (12 decimal precision):

MAX_BPS_EXTENDED: constant(uint256) = 1_000_000_000_000 // # 1e12
unlocked_shares = profit_unlocking_rate * (ts - last_profit_update) // MAX_BPS_EXTENDED

Example:

Ethereum Vault:

  • Uses MAX_BPS = 1e18 (18 decimal precision).

  • Calculates profit_unlocking_rate as:

    solidity

    profit_unlocking_rate = (locked_shares * MAX_BPS) / profit_max_unlock_time
  • Example:

    • locked_shares = 1e18 (1 token with 18 decimals).

    • profit_max_unlock_time = 604800 (7 days in seconds).

    • profit_unlocking_rate = (1e18 * 1e18) / 604800 ≈ 1.653e15.

Oracle Contract (ScrvusdOracleV2.vy):

  • Uses MAX_BPS_EXTENDED = 1e12 (12 decimal precision).

  • Calculates unlocked_shares as:

    vyper

    unlocked_shares = profit_unlocking_rate * (ts - last_profit_update) // MAX_BPS_EXTENDED
  • Using the example:

    • ts - last_profit_update = 604800 (full unlock period).

    • unlocked_shares = (1.653e15 * 604800) // 1e12 ≈ 1e21.

  • Correct calculation (if aligned with MAX_BPS = 1e18):

    • unlocked_shares = (1.653e15 * 604800) // 1e18 ≈ 1e18

-> Result: The Oracle overestimates unlocked_shares by a factor of 1e6 (since 1e18 / 1e12 = 1e6).

link:

Effect: unlocked_shares is 1e6 times larger, understating total_supply_effective and inflating the price.

@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, # block.timestamp
)
@view
def _raw_price(ts: uint256, parameters_ts: uint256) -> uint256:
"""
@notice Price replication from scrvUSD vault
"""
parameters: PriceParams = self._obtain_price_params(parameters_ts)
return self._total_assets(parameters) * 10**18 // self._total_supply(parameters, ts)

Link:

Impact

Overstated unlocked_shares reduces the effective total supply.

Price Inflation: The Oracle reports an artificially high price for scrvUSD due to the understated effective total supply.

Tools Used

Recommendations

Align Constants: Set MAX_BPS_EXTENDED = 1e18 in the Oracle to match Ethereum.

Or alternative: Scale profit_unlocking_rate down by 1e6 when fetched, but aligning constants is cleaner.

Updates

Lead Judging Commences

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

Support

FAQs

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