Summary
In ScrvusdOracleV2.vy:: update_price function the external calls for prices execute first before updating the contract's state for the price leads to price manipulation.
Vulnerability Details
In update_price function the self.last_prices was executed before self.last_update, meanwhile an MeV bot or an authorized user with PRICE_PARAMETERS_VERIFIER role can manipulate the price sources to their advantages by changing the price through the change of the state of Oracle before self.last_update is called. Since the state update (last_update) happens after the external prices is fetched the contract just records block.timestamp after the prices are already retrieved.
def __init__(_initial_price: uint256):
"""
@param _initial_price Initial price of asset per share (10**18)
"""
self.last_prices = [_initial_price, _initial_price, _initial_price]
self.last_update = block.timestamp
@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
self.last_prices = [self._price_v0(), self._price_v1(), self._price_v2()]
self.last_update = block.timestamp
Impact
Price can be exploited for personal gain and contract can operate with incorrect price
Tools Used
Manual Code Review
Recommendations
Update state first before fetching the external prices.
def __init__(_initial_price: uint256):
"""
@param _initial_price Initial price of asset per share (10**18)
"""
self.last_update = block.timestamp
self.last_prices = [_initial_price, _initial_price, _initial_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
self.last_update = block.timestamp
self.last_prices = [self._price_v0(), self._price_v1(), self._price_v2()]