Platform: Code4rena
Start Date: 09/01/2024
Pot Size: $100,000 USDC
Total HM: 13
Participants: 28
Period: 28 days
Judge: 0xsomeone
Total Solo HM: 8
Id: 319
League: ETH
Rank: 17/28
Findings: 1
Award: $819.61
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: aariiif
Also found by: 0xepley, LinKenji, Sathish9098, ZanyBonzy, catellatech, fouzantanveer, hassanshakeel13, hunter_w3b, invitedtea, yongskiws
819.6092 USDC - $819.61
Scope and Architecture Overview
Ecosystem analysis: A study of the starknet's ecosystem using its documentation as well as learning the cairo's language syntax to provide a base for understanding of the protocol's implementation.
Documentation Dive: A comprehensive analysis of provided docs was conducted to understand protocol functionality, key points were noted, ambiguities were discussed with the dev, and possible risk areas were mapped.
Code Inspection: Manual review of each contract within defined sections was conducted, testing function behavior against expectations, and working out potential attack vectors. Vulnerabilities related to dependencies and inheritances, were assessed. Comparisons with similar protocols (including older commits) was also performed to identify recurring issues and evaluate fix effectiveness.
Report Compilation: Identified issues were generated into a comprehensive audit report.
For the audit scope, the contracts are sectioned into three different sections.
These are the modules that perform the most important functionalities of the protocol. They're mostly isolated from direct external interactions from users for security reasons.
Yang deposit and withdrawal - The yang for a debt position can be increased or decreased upon calling the deposit
and withdraw
functions. These calls are made by the users via the Abbot, unless the shrine has been killed in which yang can no longer be deposited, but withdrawn via the Caretaker contract instead.
Yang seizure - During liquidations and trove shutdowns, yangs can be seized by calling the seize
fucntion to withdraw a specific amount of the yang and transfer it to the liquidator.
Debt redistribution - Upon liquidation of an unhealthy trove, the redistribute
function is called to redistribute the trove's yand and debt proportionally to its collateral consumption.
Yin forging and melting - Through the forge
and melt
functions, yin is minted and burned for the borrower. Minting the yin increases the user's debt position, burning decreases the debt position.
Yin injection and ejection - Yin can be forged or melted to a user without actually increasing the user's debt position.
+---------------------------------+ | Shrine - CLoC1313 | +---------------------------------+ | - admin | | - name | | - symbol | | - access_control | | - troves | | - yin | | - yang_total | | - initial_yang_amts | | - yangs_count | | - yang_ids | | - deposits | | - total_troves_debt | | - total_yin | | - budget | | - yang_prices | | - yin_spot_price | | - minimum_trove_value | | - debt_ceiling | | - multiplier | | - rates_latest_era | | - rates_intervals | | - yang_rates | | - yang_suspension | | - thresholds | | - redistributions_count | | - trove_redistribution_id | | - is_exceptional_redistribution | | - yang_redistributions | | - yang_to_yang_redistribution | | - is_live | | - yin_name | | - yin_symbol | | - yin_decimals | | - yin_allowances | +---------------------------------+ | + constructor() | | + add_yang() | | + set_threshold() | | + suspend_yang() | | + unsuspend_yang() | | + update_rates() | | + advance() | | + set_multiplier() | | + set_minimum_trove_value() | | + set_debt_ceiling() | | + adjust_budget() | | + update_yin_spot_price() | | + kill() | | + deposit() | | + withdraw() | | + forge() | | + melt() | | + seize() | | + redistribute() | | + inject() | | + eject() | | + get_yin() | | + get_total_yin() | | + get_yin_spot_price() | | + get_yang_total() | | + get_initial_yang_amt() | | + get_yangs_count() | | + get_deposit() | | + get_budget() | | + get_yang_price() | | + get_yang_rate() | | + get_current_rate_era() | | + get_minimum_trove_value() | | + get_debt_ceiling() | | + get_multiplier() | | + get_yang_suspension_status() | | + get_yang_threshold() | | + get_shrine_health() | | + get_redistributions_count() | | + get_trove_redistribution_id() | | + get_redistribution_for_yang() | | + is_recovery_mode() | | + get_live(): bool | +---------------------------------+ | ERC20 Yin | +---------------------------------+ | + name() | | + symbol() | | + decimals() | | + total_supply() | | + balance_of | | + allowance | | + transfer | | + transfer_from | | + approve | | + supports_interface() | +---------------------------------+
Entering and exiting a gate - Through the enter
and exit
functions, made from the sentinel, transfers a stipulated amount of assets from the user returning the corresponding amount of yang and vice versa.
+-----------------------------+ | Gate - CLoC120 | +-----------------------------+ | - shrine: IShrineDispatcher | | - asset: IERC20Dispatcher | | - sentinel: ContractAddress | +-----------------------------+ ^ | +-----------------------------+ | Storage | +-----------------------------+ | - shrine: IShrineDispatcher | | - asset: IERC20Dispatcher | | - sentinel: ContractAddress | +-----------------------------+ ^ | +-----------------------------+ | Event | +-----------------------------+ | - user: ContractAddress | | - trove_id: u64 | | - asset_amt: u128 | | - yang_amt: Wad | +-----------------------------+ | + Enter | | + Exit | +-----------------------------+ ^ | +-----------------------------+ | IGateImpl | +-----------------------------+ | + get_shrine | | + get_sentinel | | + get_asset | | + get_total_assets | | + get_total_yang | | + get_asset_amt_per_yang | | + convert_to_yang | | + convert_to_assets | | + enter | | + exit | +-----------------------------+ ^ | +-----------------------------+ | GateHelpers | +-----------------------------+ | + assert_sentinel | | + get_total_yang_helper | | + convert_to_assets_helper | | + convert_to_yang_helper | +-----------------------------+ ^ | +-----------------------------+ | gate | +-----------------------------+ | # get_total_assets_helper | +-----------------------------+
Yang addition, suspension and unsuspension - Through the add_yang
function, admins can add new yangs to function as collateral in the protocol. As, yangs cannot be removed, there's the suspend_yang
and unsuspend_yang
pause and unpause protocols interactions with the yang. Important to note that there's a time period for which a yang can be suspended before which unsuspension becomes impossible.
Killing a gate - The kill_gate
function freezes a Gate, preventing new deposits but allowing withdrawals. This ensures users can exit when needed while stopping further investments.
+--------------------------+ | Sentinel - CLoC173 | +--------------------------+ | -access_control | | -yang_to_gate | | -yang_addresses | | -yang_asset_max | | -yang_is_live | | -shrine | +--------------------------+ ^ | +--------------------------+ | ISentinelImpl | +--------------------------+ | +get_gate_address | | +get_gate_live | | +get_yang_addresses | | +get_yang | | +get_yang_asset_max | | +get_asset_amt_per_yang | | +convert_to_yang | | +convert_to_assets | | +add_yang | | +set_yang_asset_max | | +enter | | +exit | | +kill_gate | | +suspend_yang | | +unsuspend_yang | +--------------------------+ ^ | +--------------------------+ | SentinelHelpers | +--------------------------+ | #assert_can_enter | +--------------------------+
These contracts perform various management functions of the protocol. Balancing budgets, updating interest multipliers, fetching and updating yang prices from oracles and so on. These modules of the most part free of human interactions to perform their functionalities.
Shrine shutdown - By calling the shut
function, admins disable all user functions on the shrine and kill it. The function also transfers a portion of the collateral to the contract.
Collateral release and reclaim - After the shutdown, users can exchange their yin for a percentage of the collateral assets in the Caretaker. The amount of assets a yin holder is entitled to is proportional to the remaining amount of yin that is reclaimable. They do this by calling the reclaim
function. The release
function functions like this, but allows trove owners instead to withdraw collateral from their trove.
+----------------------------+ | Caretaker - CLoC193 | |----------------------------| | - access_control | | - reentrancy_guard | | - abbot | | - equalizer | | - sentinel | | - shrine | | - reclaimable_yin | |----------------------------| | + preview_release() | | + preview_reclaim() | | + shut() | | + release() | | + reclaim() | +----------------------------+
Multiplier update - The update_multiplier
function updates the interest rate multiplier based on the standard "Proportional Integral" in which updates are made aggressively or not to changes in current multiplier based perceived signs of mismatch in the yin supply and demand.
+-----------------------------+ | Controller - CLoC188 | +-----------------------------+ | - access_control | | - shrine | | - yin_previous_price | | - yin_price_last_updated | | - i_term_last_updated | | - i_term | | - p_gain | | - i_gain | | - alpha_p | | - beta_p | | - alpha_i | | - beta_i | +-----------------------------+ | + constructor() | | + get_current_multiplier() | | + get_p_term() | | + get_i_term() | | + get_parameters() | | + update_multiplier() | | + set_p_gain() | | + set_i_gain() | | + set_alpha_p() | | + set_beta_p() | | + set_alpha_i() | | + set_beta_i() | +-----------------------------+
Equalizing the budget - The equalize function helps maintain a balance between debt and its corresponding surplus in the system. If the Shrine has a positive budget, the function mints new debt surpluses and adds them to the Equalizer. This ensures that the amount of outstanding debt (yin) matches the total surplus in the system, preventing an unsustainable situation where debt keeps growing without ever being repaid.
Normalizing the budget - The normalize
function is called to reduce the budget deficit in the shrine by burning yin from the caller.
Allocating yin balance - To allocate the yin balance, the allocate
function is called, in which the yin balance from the Equalizer is distributed to different recipients based on their designated percentages. The allocation information is gotten from the Allocator.
+-----------------------------------+ | Equalizer - CLoC120 | +-----------------------------------+ | - access_control | | - allocator | | - shrine | +-----------------------------------+ | + constructor() | | + get_allocator() | | + set_allocator() | | + equalize() | | + allocate() | | + normalize() | +-----------------------------------+
+------------------------------+ | Allocator - CLoC78 | +------------------------------+ | - access_control | | - recipients_count | | - recipients | | - percentages | +------------------------------+ | + constructor() | | + set_allocation() | | + get_allocation() | +------------------------------+
Oracle and price frequenct setup - The set_oracles
and set_update_frequency
functions are called by the admin to update the protocol's oracle address and frequency of update of prices. The current oracle in use is StarkNet's pragma oracle. The frequency of price updates (the minimal time difference for which prices from the oracle are fetched) exists within a range of about 15 seconds and 4 hours.
Yang price update - The admin/purger, by calling the update_prices
function gets the earliest valid price for the yangs and updates their prices in the shrine.
+-------------------------------------+ | Seer - CLoC154 | +-------------------------------------+ | - access_control | | - shrine | | - sentinel | | - oracles | | - last_update_prices_call_timestamp | | - update_frequency | +-------------------------------------+ | + constructor() | | + get_oracles() | | + set_oracles() | | + get_update_frequency() | | + set_update_frequency() | | + update_prices() | +-------------------------------------+
These contracts are the main interfaces through which the users interact with the protocol. Through these contracts, troves can be opened and closed, debt positions open, liquidations performed and so on.
Trove creation and closure - The open_trove
function creates new troves, deposits yang, and optionally borrows yin. It's like combining "deposit" and "forge" functions. The close_trove
functions repays all debt and withdraws all collateral from a trove. It's like combining "melt" and "withdraw" functions.
Yang deposit and withdrawal - Users can call the deposit
and withdraw
functions to deposit and withdraw yangs as collaterals into and from the trove. Any user can deposit yangs into the trove, however only the trove creator/owner can withdraw.
Yin forge and melting - For a specific yang amount, the trove owner can mint yin by calling the forge
function, increasing the trove's debt. To pay off this debt, any user can call the melt
function to burn their yin and decrease the corresponding debt amount in the trove.
Constructor | V Set Shrine and Sentinel | V External Abbot Functions | V ------------------------------- | | V V get_trove_owner() get_user_trove_ids() | | V V get_troves_count() get_trove_asset_balance() | | V V open_trove() close_trove() | | V V deposit() withdraw() | | V V forge() melt() | V Internal Abbot Functions | V assert_trove_owner() | V deposit_helper() | V withdraw_helper()
Trove liquidation and absorbtion - Users can liquidate unhealthy troves' debts using their yin and gets the corresponding collateral value and a liquidation penalty as reward. This is done by calling the liquidate
function. Absorbtion on the other hand is a way to help an unhealthy trove by using the Absorber's yin balance is used to pay down the unhealthy trove's debt. The Absorber receives the corresponding amount of collateral back, plus a liquidation penalty which is then given to the "absorb" function caller as reward. If the debt is more than the available yin, it is spread out among other users. If there is no yin, the entire debt is redistributed, but the caller still gets their reward.
+---------------------+ | Purger - CLoC361 | |---------------------| | - admin | | - shrine | | - sentinel | | - absorber | | - seer | | - penalty_scalar | +---------------------+ | + set_penalty_scalar| | + liquidate | | + absorb | +---------------------+ ^ | v +--------------------+ | Shrine | |--------------------| | - contract_address | |--------------------| | + get_trove_health | | + melt | | + get_yin | +--------------------+
Yin provision, request or removal - Users can provide yin for liquidations by calling the provide
function. Doing this provides the user incentives in form of internal shares which can later be redeemed for yang. In cases where the provided yin proves to be more than enough, the user can make a yin removal request by calling the request
function. The request is valid for a fixed period of time upon which it can be fulfilled, expired or be overwritten by making a new request. After, the request has met the required minimum timeframe, the remove
function can be called to withdraw the yin.
Collateral reaping and update - The user who had provided yin for liquidation and had received shares in return can call the reap
function to facilitate this withdrawal. After this is successful, the purger updates the accounting of the absorbed assets so as to prevent excessive reaping. It does this by calling the update
function.
Absorber shutdown - The kill
function is called by the admin to irreversibly pause the provide
function and prevents users from depositing more yins into the absorber. At this point, no more asset rewards are processed for the users.
+-----------------------------------------+ | Absorber - CLoC617 | +-----------------------------------------+ | - access_control | | - sentinel | | - shrine | | - is_live | | - current_epoch | | - absorptions_count | | - provider_last_absorption | | - provisions | | - absorption_epoch | | - total_shares | | - asset_absorption | | - epoch_share_conversion_rate | | - rewards_count | | - reward_id | | - rewards | | - cumulative_reward_amt_by_epoch | | - provider_last_reward_cumulative | | - provider_request | +-----------------------------------------+ | + constructor() | | + provide() | | + request() | | + remove() | | + reap() | | + update() | | + kill() | | + get_rewards_count() | | + get_rewards() | | + get_current_epoch() | | + get_absorptions_count() | | + get_absorption_epoch() | | + get_total_shares_for_current_epoch() | | + get_provision() | | + get_provider_last_absorption() | | + get_provider_request() | | + get_asset_absorption() | | + get_cumulative_reward_amt_by_epoch() | | + get_provider_last_reward_cumulative() | | + get_live() | | + is_operational() | | + preview_remove() | | + preview_reap() | | + set_reward() | +-----------------------------------------+
+--------------------------+ +---------------------+ | Constructor | | Shrine Dispatcher | | | | | | + shrine |-------------------->| + get_total_yin | | | | + get_debt_ceiling | | | | + inject | | | | + eject | | | | + set_debt_ceiling | +--------------------------+ +---------------------+ | | | | v v +--------------------------+ +---------------------+ | IFlashMintImpl | | IFlashBorrower | | | | | | + max_flash_loan | | + on_flash_loan | | + flash_fee | | | | + flash_loan | | | +--------------------------+ +---------------------+
These are basic contracts that store important protocol information, from which they can be accessed by any of the contracts.
Yang pair and price validity setup - By calling the set_yang_pair_id
and set_price_validity_thresholds
functions, the contract admin sets the feed address attaching it to the yang and also sets the required number of sources and freshness interval required.
+----------------------------------+ | Pragma - CLoC129 | +----------------------------------+ | - access_control | | - oracle | | - price_validity_thresholds | | - yang_pair_ids | +----------------------------------+ | + constructor() | +----------------------------------+ ^ | +----------------------------------+ | IPragmaImpl | +----------------------------------+ | + set_yang_pair_id() | | + set_price_validity_thresholds()| +----------------------------------+ ^ | +----------------------------------+ | IOracleImpl | +----------------------------------+ | + get_name() | | + get_oracle() | | + fetch_price() | +----------------------------------+ ^ | +----------------------------------+ | PragmaInternalFunctions | +----------------------------------+ | + is_valid_price_update() | +----------------------------------+
+---------------------+ +----------------------+ +----------------------------+ +---------------------+ | Trove | | YangBalance | | AssetBalance | | YangRedistribution | +---------------------+ +----------------------+ +----------------------------+ +---------------------+ | - charge_from: u64 | | - yang_id: u32 | | - address: ContractAddress | | - unit_debt: Wad | | - last_rate_era: u64| | - amount: Wad | | - amount: u128 | | - error: Wad | | - debt: Wad | +----------------------+ +----------------------------+ | - exception: bool | +---------------------+ +---------------------+ +---------------------------------+ +-----------------------------+ +-------------------------------+ | ExceptionalYangRedistribution | | DistributionInfo | | Reward | +---------------------------------+ +-----------------------------+ +-------------------------------+ | - unit_debt: Wad | | - asset_amt_per_share: u128 | | - asset: ContractAddress | | - unit_yang: Wad | | - error: u128 | | - blesser: IBlesserDispatcher | +---------------------------------+ +-----------------------------+ | - is_active: bool | +-------------------------------+ +---------------------+ +----------------------+ +-------------------------------------+ | Provision | | Request | | PragmaPricesResponse | +---------------------+ +----------------------+ +-------------------------------------+ | - epoch: u32 | | - timestamp: u64 | | - price: u128 | | - shares: Wad | | - timelock: u64 | | - decimals: u32 | +---------------------+ | - has_removed: bool | | - last_updated_timestamp: u64 | +----------------------+ | - num_sources_aggregated: u32 | | - expiration_timestamp: Option<u64> | +-------------------------------------+
KILL
, SET_REWARD
, UPDATE
. Then the contracts/admins expected to fulfill the roles are then granted access. So the purger contract is granted the UPDATE
role, while the admin gets the KILL
and SET_REWARD
functions. As a result, the admin cannot directly call the update
function in the absorber contract.mod absorber_roles { //declares the roles available in the contract, const KILL: u128 = 1; const SET_REWARD: u128 = 2; const UPDATE: u128 = 4; #[inline(always)] fn purger() -> u128 { //declares the contract/callers who can call the role UPDATE } //so this means that the purger contract can call any function that requires the admin role #[inline(always)] fn default_admin_role() -> u128 { KILL + SET_REWARD } }
+---------------------+ +-----------------------+ +---------------------+ +----------------------+ | AbsorberRoles | | AllocatorRoles | | BlesserRoles | | CaretakerRoles | |---------------------| |-----------------------| |---------------------| |----------------------| | - KILL: u128 | | - SET_ALLOCATION: u128| | - BLESS: u128 | | - SHUT: u128 | | - UPDATE: u128 | +-----------------------+ +---------------------+ +----------------------+ | - SET_REWARD: u128 | +---------------------+ +------------------------+ +----------------------+ +--------------------------------------+ +---------------------------+ | ControllerRoles | | EqualizerRoles | | PragmaRoles | | PurgerRoles | |------------------------| |----------------------| |--------------------------------------| |---------------------------| | - TUNE_CONTROLLER: u128| | - SET_ALLOCATOR: u128| | - ADD_YANG: u128 | | - SET_PENALTY_SCALAR: u128| +------------------------+ +----------------------+ | - SET_ORACLE_ADDRESS: u128 | +---------------------------+ | - SET_PRICE_VALIDITY_THRESHOLDS: u128| +--------------------------------------+ +-----------------------------+ +-------------------------------+ +--------------------------------+ | SeerRoles | | SentinelRoles | | ShineRoles | |-----------------------------| |-------------------------------| |--------------------------------| | - SET_ORACLES: u128 | | - ADD_YANG: u128 | | - ADD_YANG: u128 | | - SET_UPDATE_FREQUENCY: u128| | - ENTER: u128 | | - ADJUST_BUDGET: u128 | | - UPDATE_PRICES: u128 | | - EXIT: u128 | | - ADVANCE: u128 | +-----------------------------+ | - KILL_GATE: u128 | | - DEPOSIT: u128 | | - SET_YANG_ASSET_MAX: u128 | | - EJECT: u128 | | - UPDATE_YANG_SUSPENSION: u128| | - FORGE: u128 | +-------------------------------+ | - INJECT: u128 | | - KILL: u128 | +---------------------------------+ | - MELT: u128 | | TransmuterRoles | | - REDISTRIBUTE: u128 | |---------------------------------| | - SEIZE: u128 | | - ENABLE_RECLAIM: u128 | | - SET_DEBT_CEILING: u128 | | - KILL: u128 | | - SET_MINIMUM_TROVE_VALUE: u128| | - SETTLE: u128 | | - SET_MULTIPLIER: u128 | | - SET_CEILING: u128 | | - SET_THRESHOLD: u128 | | - SET_FEES: u128 | | - UPDATE_RATES: u128 | | - SET_PERCENTAGE_CAP: u128 | | - UPDATE_YANG_SUSPENSION: u128 | | - SET_RECEIVER: u128 | | - UPDATE_YIN_SPOT_PRICE: u128 | | - SWEEP: u128 | | - WITHDRAW: u128 | | - TOGGLE_REVERSIBILITY: u128 | +--------------------------------+ +---------------------------------+
Audit Information - For the purpose of the security review, Opus comprises fifteen smart contracts totaling over 4110 CLoC. Its core design principle is composition, enabling efficient and flexible integration. Pragma oracle is used to generate token prices and a standard PI (Proportional-Integral) controller is used adjust protocol interest rates.
Documentation and CSS - The codebase is divided into nine major sections - core, arbitrage, dao, launch, pool, price-feed, reward, stable and staking sections. There provided documentation provides a very good overview of each modules and its functions. The contracts are well commented (not necessarily to Cairo Comment Standard), and explanations where given as to expected functionalities of each function. Top tier.
Naming Conventions - The protocol uses a non-standard naming convention which made the audit process a bit challenging. Yangs, yins, era, forge, eject and so on. A glossary was however provided to explain the meaning and their possible normal equivalents.
Protocol Ecosystem - The protocol's ecosystem relies on the starknet L2 chain, for deployments. The contracts are written in cairo 1, and are to be compiled with scarb and starknet foundry. Not a very common ecosystem like the solidity based contracts.
Token Support - The protocol mainly works with ERC20 tokens. WBTC, WETH, wstETH, and yin are the main tokens interaction in the protocol. New tokens including non-standard erc20 tokens can be added by the protocol admin to function as collaterals.
Testing - The overall test coverage is about 90% and each section implements various test ideas. Consequently, this improved the modularity of the codebase and helped eliminate most of the basic bugs. There are no invariant or fuzz tests implemented.
Attack Vectors - Various points of attack exists for a protocol of this size. Token pricing manipulations, Issues from non standard ERC20 tokens like WBTC, logical errors, unfair liquidations and so on.
The protocol aims to be free of human intervention, and to a point follows through with this, with a number of autonomous modules. However, there's still a central admin role, which performs a number of important protocol functionalities. While this admin is essentially trusted, having such a centralized power puts the protocol at risks, some of which are:
50 hours
#0 - c4-pre-sort
2024-02-07T17:17:57Z
bytes032 marked the issue as sufficient quality report
#1 - c4-judge
2024-02-26T17:59:57Z
alex-ppg marked the issue as grade-a