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

Precision Truncation in _unlocked_shares Could Result to Loss

Summary

The _unlocked_shares function computes unlocked shares using integer division, which truncates the fractional component. Over multiple periods, this truncation accumulates, leading to a systematic underestimation of unlocked shares.

As a result, the computed total supply is higher than it should be, causing the oracle’s raw_price to be undervalued. This mispricing can be exploited by an attacker to redeem scrvUSD for more underlying assets than deserved, draining value from liquidity providers.

Vulnerability Details

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

@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

The function calculates unlocked shares as follows:

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
return unlocked_shares

Integer division in Vyper truncates any fractional part. The formula:

unlocked_shares = profit_unlocking_rate * delta // MAX_BPS_EXTENDED

where delta = ts - last_profit_update, always rounds down.

Over successive periods, the slight underestimation of unlocked shares accumulates. This leads to a higher computed “active” total supply when the unlocked shares are subtracted:

_total_supply(p, ts) = p.total_supply - _unlocked_shares(...)

A higher total supply in the denominator of the raw_price calculation:

raw_price = total_assets * 10**18 // _total_supply(p, ts)

results in an undervalued price.

The design assumption that truncation does not lead to approximation losses is violated.

Impact

Over time, the systematic undervaluation leads to irreversible losses for liquidity providers, as funds are gradually drained from the pool. An attacker can time redemptions when the truncation error is significant, thereby redeeming scrvUSD for more underlying assets than justified by the actual value.

Tools Used

Manual Review

Recommendations

Modify the unlocked shares calculation to round to the nearest integer rather than always rounding down.

unlocked_shares = (profit_unlocking_rate * delta + MAX_BPS_EXTENDED // 2) // MAX_BPS_EXTENDED
Updates

Lead Judging Commences

0xnevi Lead Judge 3 months ago
Submission Judgement Published
Invalidated
Reason: Lack of quality
Assigned finding tags:

[invalid] finding-precision-loss

All values will be scaled to a combined of 36 decimals before division (be it price-related values or totalSupply). Considering the 18 decimals of all values, no realistic values were presented in any duplicates to proof a substantial impact on precision loss.

Support

FAQs

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