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

ERC4626 Invariant Violation in Total Assets Calculation

Summary

The oracle’s _total_assets function calculates total assets as total_idle + total_debt, omitting locked assets represented by balance_of_self.

Since ERC4626 requires that all assets under management—including locked shares—be reflected in the total assets, the oracle underreports its value. This miscalculation can lead to an undervalued raw_price, allowing attackers to mint scrvUSD at a discount and drain collateral from stableswap pools.

Vulnerability Details

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

The Vyper function is defined as:

@view
def _total_assets(p: PriceParams) -> uint256:
"""
@notice Total amount of assets that are in the vault and in the strategies.
"""
return p.total_idle + p.total_debt

Here, total_idle and total_debt are summed, while balance_of_self (which represents locked shares that still back the vault) is omitted.

Under ERC4626 standards, the totalAssets() function must account for all assets held by the vault. Even if some shares are locked (i.e., pending profit unlocking), their underlying assets still contribute to the vault’s total value.
https://eips.ethereum.org/EIPS/eip-4626#totalassets

By excluding locked shares, the computed total assets are lower than the actual assets backing the vault. When used in the price calculation:

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

the undervalued numerator results in an artificially low price per share.

This mispricing creates an exploitable differences between the real value and the oracle-reported price.

Impact

Attackers can exploit the undervalued price to mint scrvUSD at a discount, then redeem it for a larger proportion of the underlying assets, profiting at the expense of liquidity providers.

Tools Used

Manual Review

Recommendations

Modify the _total_assets function to account for locked assets. For example, if locked shares are represented by balance_of_self and a fraction of these remain locked (i.e., not yet unlocked), update the calculation to:

@view
def _total_assets(p: PriceParams) -> uint256:
return p.total_idle + p.total_debt + (p.balance_of_self - self._unlocked_shares(
p.full_profit_unlock_date,
p.profit_unlocking_rate,
p.last_profit_update,
p.balance_of_self,
block.timestamp
))
Updates

Lead Judging Commences

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

Support

FAQs

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