Summary
A critical vulnerability exists in ScrvusdOracleV2.vy
where the update_price()
function relies on the current profit_max_unlock_time
value without validating its consistency with other parameters. This creates a window of opportunity for price manipulation.
Vulnerability Details
The vulnerability stems from the update_price()
function's implementation in ScrvusdOracleV2.vy
. This function calculates prices using the current profit_max_unlock_time
value without ensuring its synchronization with other updated parameters.
Key problematic areas:
In update_price()
:
@external
def update_price(
_parameters: uint256[ALL_PARAM_CNT], _ts: uint256, _block_number: uint256
) -> uint256:
"""
@notice Update price using `_parameters`
@param _parameters Parameters of Yearn Vault to calculate scrvUSD price
@param _ts Timestamp at which these parameters are true
@param _block_number Block number of parameters to linearize updates
@return Absolute relative price change of final price with 10^18 precision
"""
access_control._check_role(PRICE_PARAMETERS_VERIFIER, msg.sender)
assert self.last_block_number <= _block_number, "Outdated"
self.last_block_number = _block_number
310-> self.last_prices = [self._price_v0(), self._price_v1(), self._price_v2()]
self.last_update = block.timestamp
ts: uint256 = self.price_params_ts
current_price: uint256 = self._raw_price(ts, ts)
self.price_params = PriceParams(
total_debt=_parameters[0],
total_idle=_parameters[1],
total_supply=_parameters[2],
full_profit_unlock_date=_parameters[3],
profit_unlocking_rate=_parameters[4],
last_profit_update=_parameters[5],
balance_of_self=_parameters[6],
)
self.price_params_ts = _ts
new_price: uint256 = self._raw_price(_ts, _ts)
log PriceUpdate(new_price, _ts, _block_number)
if new_price > current_price:
return (new_price - current_price) * 10**18
return (current_price - new_price) * 10**18
So currently update_price()
function does not update self.profit_max_unlock_time
to valid one.
But in _obtain_price_params()
function _price_v0(), _price_v1(), _price_v2()
functions use self.profit_max_unlock_time
to calculate price.
The issue propagates through _obtain_price_params()
:
@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
245-> 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
@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
The vulnerability is exploitable because any user can trigger price updates through ScrvusdVerifierV1.sol
's verification functions (verifyScrvusdByBlockHash()
and verifyScrvusdByStateRoot()
) with a valid signature, while the profit_max_unlock_time
remains unsynchronized.
Impact
Potential price manipulation of the oracle
Incorrect price calculations affecting dependent protocols
Possible financial losses for systems relying on this oracle
Tools Used
Manual Code Review
Vyper Contract Analysis
Recommendations
Implement atomic updates ensuring profit_max_unlock_time
is synchronized with other parameter updates
Add validation checks for parameter consistency before price calculations
Consider implementing a mechanism to verify the validity of profit_max_unlock_time
during price updates