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

Role Assignment Oversight in Oracle Initialization Blocking Verifier Operations

Summary

In ScrvusdOracleV2 The constructor (__init__) fails to assign the PRICE_PARAMETERS_VERIFIER role to ScrvusdVerifierV1 and the UNLOCK_TIME_VERIFIER role to ScrvusdVerifierV2, which are essential for invoking update_price and update_profit_max_unlock_time, respectively. rendering the oracle’s update mechanism inoperable halting its purpose of providing real-time scrvUSD price feeds for stableswap-ng pools until roles are manually assigned post-deployment

Vulnerability Details

https://github.com/CodeHawks-Contests/2025-03-curve/blob/198820f0c30d5080f75073243677ff716429dbfd/contracts/scrvusd/oracles/ScrvusdOracleV2.vy#L76

https://github.com/CodeHawks-Contests/2025-03-curve/blob/198820f0c30d5080f75073243677ff716429dbfd/contracts/scrvusd/oracles/ScrvusdOracleV2.vy#L295

https://github.com/CodeHawks-Contests/2025-03-curve/blob/198820f0c30d5080f75073243677ff716429dbfd/contracts/scrvusd/oracles/ScrvusdOracleV2.vy#L334

The __init__ function initializes the access_control module and sets role administrators (PRICE_PARAMETERS_VERIFIER and UNLOCK_TIME_VERIFIER under DEFAULT_ADMIN_ROLE) but omits calls to grantRole for the respective verifiers (ScrvusdVerifierV1 and ScrvusdVerifierV2). The snekmate.auth.access_control module, assumed to follow an OpenZeppelin-like role-based access control (RBAC) model, defaults to denying permissions unless explicitly granted. Consequently, the verifiers lack the necessary roles to interact with the oracle’s update functions, which enforce strict role checks via access_control._check_role.

@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,
full_profit_unlock_date=0,
profit_unlocking_rate=0,
last_profit_update=0,
balance_of_self=0,
)
# 2 * 10 ** 12 is equivalent to
# 1) 0.02 bps per second or 0.24 bps per block on Ethereum
# 2) linearly approximated to max 63% APY
self.max_price_increment = 2 * 10**12
self.max_v2_duration = 4 * 6 # half a year
access_control.__init__()
access_control._set_role_admin(PRICE_PARAMETERS_VERIFIER, access_control.DEFAULT_ADMIN_ROLE)
access_control._set_role_admin(UNLOCK_TIME_VERIFIER, access_control.DEFAULT_ADMIN_ROLE)
# Missing grantRole for verifiers
@external
def update_price(_parameters: uint256[ALL_PARAM_CNT], _ts: uint256, _block_number: uint256) -> uint256:
access_control._check_role(PRICE_PARAMETERS_VERIFIER, msg.sender)
# ...
@external
def update_profit_max_unlock_time(_profit_max_unlock_time: uint256, _block_number: uint256) -> bool:
access_control._check_role(UNLOCK_TIME_VERIFIER, msg.sender)
# ...

The vulnerability manifests immediately upon deployment when ScrvusdVerifierV1 attempts to call update_price or ScrvusdVerifierV2 attempts to call update_profit_max_unlock_time. The access_control._check_role function reverts with an "access denied" error (e.g., "missing role") because the verifiers’ addresses lack the required permissions in the role mapping.

Impact

ScrvusdVerifierV1’s verifyScrvusdByBlockHash and verifyScrvusdByStateRoot functions, which invoke update_price, fail due to the missing PRICE_PARAMETERS_VERIFIER role. Similarly, ScrvusdVerifierV2’s verifyPeriodByBlockHash and verifyPeriodByStateRoot, which call update_profit_max_unlock_time, fail without the UNLOCK_TIME_VERIFIER role. These reverts halt the flow of Ethereum-derived scrvUSD vault parameters into the oracle.

Without verifier updates, the oracle remains frozen at its initial state (total_supply = 1, total_idle = 1, etc.), unable to reflect current vault parameters or adjust profit_max_unlock_time. Price queries (price_v0, price_v1, price_v2) return static, outdated values, misrepresenting scrvUSD’s actual price per share.

  • The oracle’s inability to update breaks its integration with stableswap-ng pools (e.g., USDC/scrvUSD, FRAX/scrvUSD), rendering these pools non-operational on secondary chains. This directly contradicts the project’s goal of enabling cross-chain scrvUSD markets with dynamic pricing

  • The system Rely on manual post-deployment role assignment which is likely to introduce unnecessary operational overhead and risks human error or delay

Tools Used

Recommendations

  • Modify the ScrvusdOracleV2 constructor to accept verifier addresses and grant roles at deployment

@deploy
def __init__(_initial_price: uint256, _price_verifier: address, _unlock_verifier: address):
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,
)
self.max_price_increment = 2 * 10**12
self.max_v2_duration = 4 * 6
access_control.__init__()
access_control._set_role_admin(PRICE_PARAMETERS_VERIFIER, access_control.DEFAULT_ADMIN_ROLE)
access_control._set_role_admin(UNLOCK_TIME_VERIFIER, access_control.DEFAULT_ADMIN_ROLE)
+ access_control.grantRole(PRICE_PARAMETERS_VERIFIER, _price_verifier)
+ access_control.grantRole(UNLOCK_TIME_VERIFIER, _unlock_verifier)
Updates

Lead Judging Commences

0xnevi Lead Judge
6 months ago
0xnevi Lead Judge 5 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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