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

Low severity lacks of zero checks

Low severity

  1. _initial_price lacks zero check

  2. update_price lacks zero check for _total_assets

  3. 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:
# here max_change is 0 if last_price is 0
@> max_change: uint256 = (
self.max_price_increment * (block.timestamp - self.last_update) * last_price // 10**18
)
# always True if last_price is 0, because max_change is 0
@> if unsafe_sub(raw_price + max_change, last_price) > 2 * max_change:
# and here the return value is 0, if last_price is 0, because max_change is also 0
@> 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, # total_debt
2 * 10**18, # total_idle
1 * 10**18, # totalSupply
ts + 100000, # full_profit_unlock_date
10**20, # profit_unlocking_rate
ts, # last_profit_update
10**14, # balanceOf(self)
]
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 # still zero

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:
.
.
.
# if total_assets is set to zero, the _raw_price() will return 0
@> current_price: uint256 = self._raw_price(ts, ts)
self.price_params = PriceParams(
total_debt=_parameters[0],
# can be zero
@> total_idle=_parameters[1],
# can be zero
@> 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:
# so here will be division by zero
@> 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, # total_debt
0, # total_idle
10**18, # totalSupply
ts + 100000, # full_profit_unlock_date
10**20, # profit_unlocking_rate
ts, # last_profit_update
10**14, # balanceOf(self)
]
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)
# fixing the total_assets
price_params[1] = 10**18
with boa.env.prank(verifier):
# will revert, because of division by zero
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
# here can be zero
@> period: uint256 = self.profit_max_unlock_time
if params.last_profit_update + period >= parameters_ts:
return params
number_of_periods: uint256 = min(
# here will be division by zero
@> (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, # new value
10,
)
# will revert, because of division by zero
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
Updates

Lead Judging Commences

0xnevi Lead Judge
6 months ago
0xnevi Lead Judge 5 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.