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

Incorrect Timestamp Handling in Price Update Function Leads to Zero Unlocked Shares Consideration in Price Valuation

Summary

The calculation of price_v0 contains a timestamp handling flaw in the update_price() function that affects unlocked shares evaluation. When self.price_params_ts is set to last_profit_update, any subsequent calculations using this timestamp incorrectly determine that no shares are unlocked, even when full_profit_unlock_date > ts. Furthermore, the unlocked shares calculation only proceeds when full_profit_unlock_date != 0. The combination of these conditions creates scenarios where unlocked shares consistently evaluate to zero, compromising the accuracy of price valuations in the oracle system.

Vulnerability Details

This originates from the logic flow surrounding the calculation of unlocked_shares during price_vo computation. Below is the sequence leading to the vulnerability:

When calculating price_vo, the system calls self_raw_price() with self.price_params_ts and self.price_params_last_profit_update as parameters.

def _price_v0() -> uint256:
return self._smoothed_price(
self.last_prices[0],
>> self._raw_price(self.price_params_ts, self.price_params.last_profit_update),
)

Inside self_raw_price(), the function obtain_price_params() is called using self.price_params_last_profit_update as its argument.

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)

Within obtain_price_params(), the condition:

if params.last_profit_update + period >= parameters_ts

evaluates to True.

This causes an early return of params, bypassing further logic that might adjust or update parameters.

The returned params are used to calculate total_asset() and total_supply().
total_supply() calls unlocked_shares()

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
)

In update_price(), self.price_params_ts is explicitly set to last_profit_update as it is mentioned in a comment in ScrvusdVerfierV1 contract

function verifyScrvusdByStateRoot(
uint256 _block_number,
bytes memory _proof_rlp
) external returns (uint256) {
bytes32 state_root = IBlockHashOracle(BLOCK_HASH_ORACLE).get_state_root(_block_number);
uint256[PARAM_CNT] memory params = _extractParametersFromProof(state_root, _proof_rlp);
>> // Use last_profit_update as the timestamp surrogate
return _updatePrice(params, params[5], _block_number);
}

In unlocked_shares(), the ts argument is equal to last_profit_update. Therefore _unlocked_shares() will be zero since the ts - last_profit_update is zero

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 unlocked shares are only considered if:

if full_profit_unlock_date != 0

This means:

  • If full_profit_unlock_date > ts (even if not zero), unlocked shares will still be zero.

Impact

This vulnerability leads to unlocked shares being incorrectly calculated as zero under the described conditions. The key consequences are with unlocked shares ignored or set to zero, total_supply() underestimates the circulating supply, leading to inflated or inaccurate price_vo values.

Tools Used

Manual Review

Recommendation

Ensure that the comparison:

if full_profit_unlock_date > ts

does not prematurely zero out unlocked shares unless this behavior is explicitly desired

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.