Summary
The initial values of price_params
in ScrvusdOracleV2
constructor are hardcoded to total_idle=1
and total_supply=1
. This can cause wrong price calculation in price_v2()
and raw_price()
functions. Wich can lead to opportunity to trade tokens at significantly lower price than expected.
Vulnerability Details
Hardcoded initial values of price_params
in ScrvusdOracleV2
constructor can cause wrong price calculation in price_v2()
and raw_price()
functions.
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
self.profit_max_unlock_time = 7 * 86400
self.price_params = PriceParams(
total_debt=0,
@> total_idle=1,
@> total_supply=1,
full_profit_unlock_date=0,
profit_unlocking_rate=0,
last_profit_update=0,
balance_of_self=0,
)
If the oracle smart contract is deployed much later than the scrvUSD contract is operational, this will result in a temporary discrepancy in the price reported by the oracle. The price_v2()
function will return the initial _initial_price
value, while the raw_price()
function will return 1*10**18
. This discrepancy will be resolved after the first price update, but until then, the price reported by the oracle will be incorrect. What much worse, if the max_price_increment
is updated before the first price update, the discrepancy will affect the price_v2()
.
Impact
Such discrepancy can lead to an opportunity to trade tokens at significantly lower price than expected. The more time passes between the scrvUSD contract deployment and the oracle contract deployment, the more significant gains can be made by exploiting this vulnerability.
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", 4*10**18)
return contract
def test_initial_price_at_later_oracle_deploy(soracle, verifier, admin):
print("\n| where | price_v2() | raw_price() | block_number")
price_v2, raw_price = soracle.price_v2(), soracle.raw_price()
print(" on init ", price_v2, raw_price, boa.env.evm.patch.block_number)
assert price_v2 == 4*10**18
assert raw_price == 1*10**18
with boa.env.prank(admin):
soracle.set_max_price_increment(10**18)
boa.env.time_travel(seconds=12)
price_v2, raw_price = soracle.price_v2(), soracle.raw_price()
print(" max_price_increment updated", price_v2, raw_price, boa.env.evm.patch.block_number)
assert price_v2 == 1*10**18
assert raw_price == 1*10**18
ts = boa.env.evm.patch.timestamp
price_params = [
0,
40000000000000000000000000,
10000000000000000000000000,
ts + 500000,
5831137848451547566180476730,
ts,
3000000000000000000000,
]
with boa.env.prank(verifier):
soracle.update_price(
price_params,
ts,
boa.env.evm.patch.block_number,
)
price_v2, raw_price = soracle.price_v2(), soracle.raw_price()
print(" after update_price ", price_v2, raw_price, boa.env.evm.patch.block_number)
assert price_v2 == 1*10**18
assert raw_price == 4*10**18
boa.env.time_travel(seconds=12)
price_v2, raw_price = soracle.price_v2(), soracle.raw_price()
print(" wait one block ", price_v2, raw_price, boa.env.evm.patch.block_number)
assert price_v2 > 4*10**18
assert raw_price > 4*10**18
Recommendations
Use _initial_price
as value for initial price_params
in ScrvusdOracleV2
constructor.
@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
# initial raw_price is 1
self.profit_max_unlock_time = 7 * 86400 # Week by default
self.price_params = PriceParams(
total_debt=0,
- total_idle=1,
- total_supply=1,
+ total_idle=_initial_price,
+ total_supply=10**18,
full_profit_unlock_date=0,
profit_unlocking_rate=0,
last_profit_update=0,
balance_of_self=0,
)