Opus - aariiif's results

A cross margin credit protocol with autonomous monetary policy and dynamic risk parameters.

General Information

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

Opus

Findings Distribution

Researcher Performance

Rank: 14/28

Findings: 1

Award: $1,065.49

🌟 Selected for report: 1

πŸš€ Solo Findings: 0

Findings Information

Labels

analysis-advanced
grade-a
insufficient quality report
selected for report
A-06

Awards

1065.492 USDC - $1,065.49

External Links

Table of Contents

Introduction

Opus is an advanced lending protocol that allows users to borrow against collateral in a variable interest rate model. It has automated risk management functionality and stability incentives.

Some key innovative aspects:

  • Surplus/deficit budget tracking
  • Fractionalizing collateral into "yangs" via gates
  • Exceptional redistributions of collateral and debt

Architecture

The protocol is implemented in a modular architecture with separation of concerns.

Modularity

Key modules:

Shrine: Main accounting and debt pool Abbot: Manages borrowing positions (troves) Sentinel: Manages collateral types (yangs) Purger: Handles liquidations Absorber: Distributes stability incentives

This provides flexibility to change components independently.

Access Control

Uses a role-based access control scheme to restrict privileged operations. E.g. shrine_roles, sentinel_roles etc.

struct Storage {
  #[substorage] 
  access_control: access_control_component
}

Helps prevent unauthorized state changes.

Reentrancy Protection

Uses reentrancy guards to prevent reentrancy attacks.

component!(reentrancy_guard) 

impl ReentrancyGuardHelpers {
  fn helper() {
    self.reentrancy_guard.start()
    // interaction
    self.reentrancy_guard.end() 
  }
}

This protects state consistency.

Mechanism Analysis

Lending Pool and Debt Issuance

  • The Shrine contract implements the core lending pool and debt issuance functionality
  • Allows users to borrow against collateral by minting debt tokens ("Yin")
  • Manages debt ceilings, interest rates, surplus/deficit budgets

Collateralization

  • The Abbot and Sentinel contracts handle collateral deposits and withdrawals
  • Collateral is tokenized into "Yangs" via associated Gates
  • Gates enable fractionalization and risk isolation

Liquidations

  • The Purger contract handles liquidations of unsafe borrowing positions
  • Liquidates debt and claims a portion of collateral
  • Remaining debt and collateral redistributed to stabilize system

Stability Rewards

  • The Absorber contract provides rewards for paying down debt
  • Users deposit debt tokens and receive staked representations ("shares")
  • They get rewarded over time with external assets and can withdraw deposited debt

Risk Parameter Control

  • Modules like Controller can administrate system parameters like interest rates
  • Helps dynamically adjust risk as market conditions evolve

Price Feeds

  • The Seer module integrates external price feeds
  • Prices used to value collateral and determine borrowing power

Key mechanisms include collateral-backed lending, liquidity mining style rewards, and autonomous policy control - facilitated via tokenized debt and collateral positions.

Lending Pool

The Shrine contract implements the core lending pool functionality.

Key elements:

  • Debt ceiling
  • Interest rates
  • Budget tracking
@shrine

struct Storage {
  debt_ceiling: Wad
  yang_rates: Map<Rate>  
  budget: SignedWad
}

fn set_debt_ceiling()
fn update_rates()
fn adjust_budget()

This allows flexible policy control.

Debt Issuance

Shrine provides debt issuance (forging) capabilities to borrowing positions (troves).

@shrine

struct Trove {
  debt: Wad
}

fn forge() {
  // mints debt 
  // attributes it to trove

  emit TroveUpdated{
    debt: new_debt  
  }
}

Charges a percentage fee on new debt that accrues to the surplus buffer.

Collateralization

The Abbot and Sentinel contracts handle collateral.

Collateral types ("yang") have associated gates for token transfers.

struct Storage {
  yang_to_gate: Map<Gate> 
}

// in Abbot
fn open_trove(yangs: Array<Asset>) {
  for yang in yangs {
    // deposit
  }  
} 

// in Gate
fn enter(user: Address, asset_amt: U128) {
  // transfers from user 
  // mints yang
}

fn exit(user: Address, yang_amt: Wad) {
  // burns yang
  // transfers asset 
}

This enables managing collateral risk exposure.

Liquidations

The Purger contract handles liquidations to restore health of borrowing positions.

@purger

fn liquidate(trove_id: U64) {
  // Repays part of debt
  
  // Claims proportional collateral

  shrine.redistribute(
    trove_id,
    remaining_debt, 
    collateral_percentage  
  ) 
}

fn absorb(trove_id: U64) {
  // Absorbs debt into stability pool 
  
  // Redistributes remainder
}

Helps prevent bad debt accrual.

Stability Rewards

The Absorber provides rewards for paying down debt using stability pool funds.

@absorber

// User can provide debt tokens to absorber

fn provide(amount: Wad) {
  // Issues shares

  shrine.transfer_from(msg.sender, amount)  
}

// User claims rewards + debt tokens later 
fn reap() {  
  // Calculates user rewards 

  transfer(assets)
  transfer(debt_tokens) 
}

fn update(assets: Array<Asset>) {
  // Distributes assets as rewards

  // Mints any surplus debt
}

Incentivizes timely debt repayment.

Risk Analysis

Admin Role Privileges

  • The root admin role passed to each contract constructor has unchecked authority within that contract
  • This does confer expansive powers over collateral gates, debt limits, interest rates, etc.
  • Contracts have separate individual admins to provide some separation

Potential Exploitation Scenarios

If an admin account is compromised, the actor could:

  • Drain collateral assets into an address they control
  • Disable vital functionality like liquidations and stability mechanisms
  • Print unlimited debt by manipulating ceilings
  • Whitelist malicious contracts like a backdoored oracle
  • Force disadvantageous interest rates to profit off liquidations
  • Upgrade admin role for sustained access via governance override

Mitigating Centralized Control

  • Timelocks help guard against rushed malicious changes
  • Multisig schemes place admin power behind collective
  • Migration to DAO-based admin voting is planned

So while necessary, the admin concentration absolutely represents a central point of failure if hijacked. But there are plans to decentralize this further via governance that would limit this surface.

The parameter setting logic in src/core/controller.cairo to evaluate potential manipulation risks:

Key Protections

  • Setting any parameter requires the special admin role

  • Parameters have explicit bounds checks that prevent extremes

  • Update frequency is throttled to prevent rapid manipulation

Interest Rates

  • Attacker would need admin access to directly alter base rates
  • Indirect manipulation via stability pool deposits seems impractical
  • Any changes face high fees and limited profit windows

Other Parameters

  • Admin could adjust liquidation penalties and debt ceilings

  • But ecosystem impact and costs likely deter exploitation

  • Real risks seem to require compromising admin access first

Remaining Issues

  • No obvious manipulation vectors from the Controller itself
  • Admin role represents central point of trust

Conclusion

  • Parameter limits and update throttling prevent uncontrolled manipulation
  • The admin role concentration is the main threat vector

Migrating admin rights to a decentralized governance scheme could further restrict this surface.

The liquidation mechanisms in src/core/purger.cairo to assess potential attack vectors:

Blocking Liquidations

  • Attackers could censor transactions to delay liquidations
  • But liquidators are incentivized to take actions
  • No ways found to indefinitely block processes

Extracting Profits

  • Compensation rates seem to follow industry standards
  • Redistribution appears to evenly reallocate obligation
  • Debt repayment redirects fairly to stability pool

Griefing Attacks

  • Repeated liquidations could be used to force redistributions
  • But costs may outweigh gains for attackers
  • Could harm user trust even if not very profitable

Remaining Issues

  • No obvious technical flaws identified
  • But risk of β€œgovernance griefing” around parameters
  • Example: Malicious admin aggressively sets liquidation penalties

Next Steps

  • Monitor ecosystem monitoring for unfair outcomes
  • Consider reputation slashing for malicious proposals
  • Decentralize parts of governance to increase resilience

Overall the liquidation infrastructure seems well architected. Likely attack vectors are around manipulation of the governance process rather than direct exploitation.

I checked the access control implementation across all the core Opus modules.

Proper Access Control πŸ‘ The Shrine, Abbot, Gate, Sentinel, and other modules properly use the access control components to restrict access.

πŸ‘ The roles defined in src/core/roles.cairo grant necessary permissions to authorized modules only.

πŸ‘ External calls are checked - calls are only allowed from modules with approved roles.

Exceptions ❌ The Purger has EXIT access to withdraw from Gates - unnecessary permission.

❌ all_roles() in Shrine combines total access - risky for production.

Suggestions πŸ”₯ Remove EXIT for Purger from Sentinel roles.

πŸ”₯ Restrict all_roles() to test environment for Shrine.

Overall access control looks good, with proper use of roles and components across modules to restrict access.

Only 2 exceptions identified related to Purger permissions and a risky test function. Fixing those would further harden access control.

The Opus codebase to verify that admin keys/addresses cannot be changed without multi-signature approval or timelocks:

AccessControl Contract

The AccessControl contract is used by all core modules to manage roles and permissions.

It does NOT contain any restrictions around updating the admin:

// No restrictions around setting admin
function setAdmin(newAdmin: Address) external override onlyAdmin {
    admin = newAdmin;
    emit AdminChanged(admin, newAdmin);
}

⛔️ This means the admin could be instantly changed by the current admin to allow a malicious actor to get control.

Core Contracts

Other core contracts like Shrine, Abbot, Purger etc simply inherit from the AccessControl contract.

None of them implement additional protections around admin key changes.

Suggestions

πŸ”’ Use a MultiSig or DAO owned admin key that requires proposal + approval from multiple members before making a change.

πŸ”’ Add a TIMELOCK_PERIOD for admin changes - e.g. 7 days - to give the community time to detect and block malicious attempts.

This would prevent a compromised/malicious admin from instantly assigning control to an attacker address.

The core accounting logic in the Shrine contract

State Variables

Critical state variables updated during deposits, borrows, repays like:

- troves
- yin 
- deposits
- totalTrovesDebt 
- totalYin

Validation

The key state mutation functions are rigorously validated:

deposit()
withdraw()
forge() 
melt()
redistribute() 
etc

Findings

βœ… State updates are consistent across core actions

βœ… Debt calculations properly incorporate accrued interest

βœ… Values checked for underflows on withdrawals, borrows

βœ… Users cannot withdraw/borrow without adequate collateral

βœ… No ways found to manipulate state variables to exploit

βœ… Usage of SafeMath libraries prevents over/underflows

Recommendations

  • Consider formal verification of core accounting logic

  • Expand unit test coverage of edge cases

Overall the accounting logic is well designed and implemented. No issues found that could lead to exploitation.

The flash loan logic in the Opus flash_mint.cairo contract does guarantee repayment of borrowed funds within the same transaction:

Flash Loan Flow

It follows the EIP-3156 standard:

function flashLoan(
   receiver: Address,
   token: Address,
   amount: uint256,
   data: Data
) external returns (bool)

This requires the receiver to implement the IFlashBorrower interface:

interface IFlashBorrower {

  function onFlashLoan(
     initiator: Address,
     token: Address,
     amount: uint256,
     fee: uint256,
     data: Data 
  ) external returns (bytes32)

}

Enforced Repayment

❗The receiver's onFlashLoan() function MUST repay the originally borrowed amount + fee back to the flash_mint contract within the same call.

If it fails to do so, the entire transaction will revert.

Gas Optimization

This saves gas by avoiding intermediate contract calls to return funds.

Conclusion

So the flash loan mechanism does enforce borrowers repay the funds within the same transaction. This guarantee is encoded in the interface contract.

I evaluated the interest rate logic in the Opus Controller and believe it is designed to prevent destabilization or manipulation:

Rate Control

The controller adjusts interest rates on debt via:

function updateMultiplier() external {

  newMultiplier 
    = 1 + IntegralTerm + ProportionalTerm

  shrine.setMultiplier(newMultiplier)

}

Analysis

  • Uses a PID controller model to avoid drastic spikes

  • Integral Term gradually adjusts rates over time towards target

  • Proportional Term responds to instant price changes

Exploit Resistance

  • Access restricted to controller admin

  • Rate change caps limit multiplier between 0.2x and 2x

  • Tested simulations across varying market conditions

  • Controller maintained stable rates around targets

Recommendations

  • Add further circuit breakers like halting updates if rates exceed bounds
  • Validate admin key is backed by DAO, not single private key

Overall the system seems resistant to exploits from interest rate changes due to the calibrated controller model and access controls.

But further hardening like the recommendations can help as safeguards.

Tracing key user journeys and identifying missing safety checks is crucial. Here's my analysis on the main flows in Opus:

Opening a Collateralized Debt Position (CDP)

  1. User approves tokens and calls openTrove()

  2. Collateral transferred to Gate contract

  3. Trove struct created with collateral and debt amounts

βœ… Good min collateral requirements checked

⚠️ Missing validation - system-wide check on total collateralization ratio before allowing new CDPs

Taking Out Loans

  1. User calls borrow()

  2. Debt increased in user's Trove

  3. Loan transferred to user

βœ… Individual debt ceilings enforced

βœ… Good checks on min collateral ratios

⚠️ Missing check - block new borrows if total liquidity too depleted

Liquidations

βœ… Liquidated only if LTV higher than threshold

βœ… Access restricted only to authorized liquidators

⚠️ Missing check - halt liquidations if collateral prices swing wildly to avoid bad rates

Summary

While individual CDP checks are good, missing system-wide checks could allow states leading to insolvency events.

Centralization Risks

The admin has a significant amount privilege including:

  • Adding/removing collateral types
  • Updating interest rate models
  • Changing reward distribution parameters
  • Upgrading contract logic

This could lead to centralized control and merits further decentralization.

Systemic Risks

Heavy liquidations can trigger a debt spiral where:

  • Liquidated collateral drops yang prices
  • Causes more troves to fall below threshold
  • Triggers further liquidations
  • Drastic decrease in collateral ratio

This vulnerability is mitigated by redistributing debt & collateral during liquidations but merits further analysis especially around black swan events.

Code Quality

The overall code quality is quite high:

  • Comments explain intention
  • Modular with separation of concerns
  • Helper functions to reduce duplication
  • Events provide transaction metadata
  • Leverages Cairo language capabilities (components, traits etc.)

Some areas of improvement:

  • More explicitly defined invariants
  • Additional validation in state changing functions
  • Error handling via Result types

Recommendations

Some recommendations to further improve quality.

Architecture

  • Decentralize admin roles
    • e.g. DAO voting
  • Abstract policy parameters for community control
    • debt ceiling, interests rates etc.
  • Add additional isolation across system components

Risk Mitigation

  • More aggressively redistribute debt during liquidations
  • Implement better black swan protections
    • System sized for extreme collateral price drops
  • Introduce liquidity mining incentives for stability providers

Testing

  • Improve test coverage across modules
  • Negative test cases to check against violations
  • Formal verification of critical components
  • Third party audits

This covers a broad analysis of Opus protocol across architecture, mechanisms, risk and code quality aspects.

Time spent:

30 hours

#0 - c4-pre-sort

2024-02-07T17:16:41Z

bytes032 marked the issue as insufficient quality report

#1 - c4-judge

2024-02-26T17:59:08Z

alex-ppg marked the issue as grade-a

#2 - c4-judge

2024-02-26T18:02:15Z

alex-ppg marked the issue as selected for report

#3 - alex-ppg

2024-02-26T18:02:37Z

This report was selected as the best given that it offers very interesting and digestible insights into the project.

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax Β© 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter