Summary
ScrvusdOracleV2's price calculation mechanism is significantly impacted by changes to self.profit_max_unlock_time
. The system lacks proper smoothing when this parameter is modified by the verifier, leading to sudden price movements.
Vulnerability Details
The price calculation in ScrvusdOracleV2 heavily relies on the _obtain_price_params()
function, where self.profit_max_unlock_time
plays a crucial role in determining price parameters:
@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)
self.max_v2_duration,
)
gain: uint256 = (
params.balance_of_self * (params.total_idle + params.total_debt)
)
params.total_idle += gain * number_of_periods
# functions are reduced from `VaultV3._process_report()` given assumptions with constant gain
for _: uint256 in range(number_of_periods, bound=MAX_V2_DURATION):
new_balance_of_self: uint256 = (
params.balance_of_self
* (params.total_supply - params.balance_of_self)
)
params.total_supply -= (
params.balance_of_self * params.balance_of_self
)
params.balance_of_self = new_balance_of_self
if params.full_profit_unlock_date > params.last_profit_update:
# copy from `VaultV3._process_report()`
params.profit_unlocking_rate = params.balance_of_self * MAX_BPS_EXTENDED
params.full_profit_unlock_date - params.last_profit_update
)
else:
params.profit_unlocking_rate = 0
params.full_profit_unlock_date += number_of_periods * period
params.last_profit_update += number_of_periods * period
return params
The problem lies in the update_profit_max_unlock_time()
function, which allows immediate parameter changes without any transition mechanism:
@external
def update_profit_max_unlock_time(_profit_max_unlock_time: uint256, _block_number: uint256) -> bool:
"""
@notice Update price using `_parameters`
@param _profit_max_unlock_time New `profit_max_unlock_time` value
@param _block_number Block number of parameters to linearize updates
@return Boolean whether value changed
"""
access_control._check_role(UNLOCK_TIME_VERIFIER, msg.sender)
assert self.last_block_number <= _block_number, "Outdated"
self.last_block_number = _block_number
prev_value: uint256 = self.profit_max_unlock_time
self.profit_max_unlock_time = _profit_max_unlock_time
return prev_value != _profit_max_unlock_time
Impact
Abrupt changes to self.profit_max_unlock_time
can cause immediate price shifts, bypassing the intended price smoothing mechanism. This could lead to market instability and potential exploitation opportunities.
Tools Used
Manual Code Review
Recommendations
Implement a proper transition mechanism by updating the last prices when self.profit_max_unlock_time
changes:
@external
def update_profit_max_unlock_time(_profit_max_unlock_time: uint256, _block_number: uint256) -> bool:
"""
@notice Update price using `_parameters`
@param _profit_max_unlock_time New `profit_max_unlock_time` value
@param _block_number Block number of parameters to linearize updates
@return Boolean whether value changed
"""
access_control._check_role(UNLOCK_TIME_VERIFIER, msg.sender)
assert self.last_block_number <= _block_number, "Outdated"
self.last_block_number = _block_number
+ self.last_prices = [self._price_v0(), self._price_v1(), self._price_v2()]
+ self.last_update = block.timestamp
prev_value: uint256 = self.profit_max_unlock_time
self.profit_max_unlock_time = _profit_max_unlock_time
return prev_value != _profit_max_unlock_time