Low severity
_initial_price lacks zero check
update_price lacks zero check for _total_assets
update_profit_max_unlock_time lacks zero check for _profit_max_unlock_time
[L-1] _initial_price
on ScrvusdOracleV2
constructor lacks zero check
Vulnerability Details
The _initial_price
parameter in the ScrvusdOracleV2
constructor is not checked for zero value. This can lead unoperational oracle contract, as the any following price updates will not be able to change the initial price value.
contracts/scrvusd/oracles/ScrvusdOracleV2.vy
@deploy
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
The calculation made in the _smoothed_price
will always return 0 if last_price
is 0.
@view
def _smoothed_price(last_price: uint256, raw_price: uint256) -> uint256:
@> max_change: uint256 = (
self.max_price_increment * (block.timestamp - self.last_update) * last_price // 10**18
)
@> if unsafe_sub(raw_price + max_change, last_price) > 2 * max_change:
@> return last_price + max_change if raw_price > last_price else last_price - max_change
return raw_price
PoC
Put the following code in a file tests/scrvusd/oracle/unitary/test_v2.py
@pytest.fixture(scope="module")
def soracle(admin):
with boa.env.prank(admin):
contract = boa.load("contracts/scrvusd/oracles/ScrvusdOracleV2.vy", 0)
return contract
def test_zero_initial_price(soracle, verifier):
assert soracle.price_v2() == 0
ts = boa.env.evm.patch.timestamp
price_params = [
0,
2 * 10**18,
1 * 10**18,
ts + 100000,
10**20,
ts,
10**14,
]
with boa.env.prank(verifier):
soracle.update_price(
price_params,
ts,
boa.env.evm.patch.block_number,
)
boa.env.time_travel(seconds=12)
assert soracle.price_v2() == 0
Recommendations
Check the _initial_price
parameter for zero value in the ScrvusdOracleV2
constructor.
@deploy
def __init__(_initial_price: uint256):
"""
@param _initial_price Initial price of asset per share (10**18)
"""
+ assert _initial_price > 0, "Initial price must be greater than zero"
self.last_prices = [_initial_price, _initial_price, _initial_price]
self.last_update = block.timestamp
[L-2] update_price
lacks zero check for _total_assets
Vulnerability Details
The update_price
function allows to update the price parameters with _total_assets
equal to zero. The subsequent update_price
call will fail due to the division by zero.
contracts/scrvusd/oracles/ScrvusdOracleV2.vy
@external
def update_price(
_parameters: uint256[ALL_PARAM_CNT], _ts: uint256, _block_number: uint256
) -> uint256:
.
.
.
@> 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 // current_price
return (current_price - new_price) * 10**18 // current_price
PoC
Put the following code in a file tests/scrvusd/oracle/unitary/test_v2.py
def test_update_price_zero_total_assets(soracle, verifier):
ts = boa.env.evm.patch.timestamp
price_params = [
0,
0,
10**18,
ts + 100000,
10**20,
ts,
10**14,
]
with boa.env.prank(verifier):
soracle.update_price(
price_params,
ts,
boa.env.evm.patch.block_number,
)
assert soracle.price_v2() == 0
assert soracle.raw_price() == 0
boa.env.time_travel(seconds=12)
price_params[1] = 10**18
with boa.env.prank(verifier):
with boa.reverts():
soracle.update_price(
price_params,
ts+12,
boa.env.evm.patch.block_number,
)
Recommendations
Check the _total_assets
parameter for zero value in the update_price
function.
@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)
# Allowing same block updates for fixing bad blockhash provided (if possible)
assert self.last_block_number <= _block_number, "Outdated"
self.last_block_number = _block_number
+ assert _parameters[1] + _parameters[2] > 0, "Total assets must be greater than zero"
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],
)
[L-3] update_profit_max_unlock_time
lacks zero check for _profit_max_unlock_time
Vulnerability Details
The update_profit_max_unlock_time
function allows to update the _profit_max_unlock_time
parameter with zero value. This will lead to the division by zero at any price retrieval function call.
contracts/scrvusd/oracles/ScrvusdOracleV2.vy
@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) // period,
self.max_v2_duration,
)
PoC
Put the following code in a file tests/scrvusd/oracle/unitary/test_v2.py
def test_update_zero_profit_max_unlock_time(soracle, verifier):
with boa.env.prank(verifier):
soracle.update_profit_max_unlock_time(
0,
10,
)
assert soracle.price_v2() != 0
Recommendations
Check the _profit_max_unlock_time
parameter for zero value in the update_profit_max_unlock_time
function.
@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)
# Allowing same block updates for fixing bad blockhash provided (if possible)
assert self.last_block_number <= _block_number, "Outdated"
self.last_block_number =
+ assert _profit_max_unlock_time > 0, "Profit max unlock time must be greater than zero"
prev_value: uint256 = self.profit_max_unlock_time
self.profit_max_unlock_time = _profit_max_unlock_time
return prev_value != _profit_max_unlock_time