Skip to content

mrgn-0.1.6-rc2

Pre-release
Pre-release

Choose a tag to compare

@jgur-psyops jgur-psyops released this 01 Dec 17:37
22bc9f7

Summary

Implementation of a seven-point curve structure that enables deeper control of interest rates at various utilization levels. Deprecates the simple three-point plateau rate/max rate model. The new curve approach allows the admin to set up to 5 points (plus the points at util = 0 and util = 100). Each point corresponding to a utilization and base interest rate. Points must be in ascending order (util increasing, rates increasing or equal to previous). To determine what the rate is at a given utilization, we linearly interpolate between the nearest two points.

Adds a fixed-price oracle type. Banks using this type have a declared price that can be read at bank.config.fixed_price. They use no external oracle, the price is completely set by the group administrator and never goes stale. This is useful for e.g. stablecoins, assets that are worthless and/or being sunset, or assets that are extremely stable in price for some other reason.

Risk management gets a major upgrade: banks that are defunct and being sunset can now be deleveraged and unwound. Deleverage is essentially a liquidation of a healthy account, but for no profit in dollar terms. We expect that banks at risk of deleveraging will get a LOT of warning (looking at you, UXD). The operation is admin-only.

Bank Metadata: an on-chain account where you can read the plain-English token name and description of a corresponding bank.

Big readme rewrites! More guides!

Bug Fixes

  • Kamino refresh instructions are now allowed before start_liquidation in receivership liquidations.

New Accounts

  • BankMetadata - 1:1 with Banks, contains the utf-8 encoded token name and a brief description. No more running to solscan to figure out what token a bank is using! A pda with one non-static seed: the bank.

New/Changed Fields

  • Banks now have:
    • zero_util_rate - The base rate at utilization = 0. A %, as u32, out of 1000%, e.g. 100% = 0.1 * u32::MAX
    • hundred_util_rate - The base rate at full utilization (100%). A %, as u32, out of 1000%, e.g. 100% = 0.1 * u32::MAX
    • points - The base rate at various points between 0 and 100% utilization, exclusive. Exactly five points large. Unused points have util = 0. Util is a %, as u32, out of 100%, e.g. 50% = .5 * u32::MAX. Rate is a %, as u32, out of 1000%, e.g. 100% = 0.1 * u32::MAX.
    • curve_type - For all newly created banks, INTEREST_CURVE_SEVEN_POINT (1). For all banks created prior to this update, INTEREST_CURVE_LEGACY (0). After migration is completed, all banks will have INTEREST_CURVE_SEVEN_POINT.
    • OracleSetup has a new enum value: Fixed
    • bank.config.fixed_price: A WrappedI80F48, storing the declared price for Fixed banks, does nothing for banks for other OracleSetups. Note: a bank can swap from Fixed to another OracleSetup type. If it does so, this field might be populated with a junk value.
  • Groups now have:
    • deleverage_withdraw_window_cache - Limits how much the risk_admin can deleverage in a day, in the even the admin is compromised
    • risk_admin - can perform bankruptcies and deleverages
    • metadata_admin - can update bank metadata

Breaking Changes (admin instructions)

  • All config instructions affecting interest (add_pool, add_pool_with_seed, configure_bank, configure_bank_interest_only) now take fundamentally different arguments.

Breaking Changes (Everyone)

  • All instructions that require risk checks (borrow, liquidate, withdraw, etc) must now accommodate fixed-price banks. For these banks, pass just the bank in remaining_accounts, no oracle is required. See load_observation_account_metas for a Rust example. For Typescript, assuming you have a list of banks and loaded bank accounts:
   for (let i = 0; i < bankAccs.length; i++) {
     const setup = bankAccs[i].account.config.oracleSetup;
     const keys = bankAccs[i].account.config.oracleKeys;
     if ("fixed" in setup) {
       remainingAccounts.push([banks[i]]);
     } else if ("stakedWithPythPush" in setup) {
       remainingAccounts.push([banks[i], keys[0], keys[1], keys[2]]);
     } else if ("kaminoPythPush" in setup) {
       remainingAccounts.push([banks[i], keys[0], keys[1]]);
     } else if ("kaminoSwitchboardPull" in setup) {
       remainingAccounts.push([banks[i], keys[0], keys[1]]);
     } else {
       remainingAccounts.push([banks[i], keys[0]]);
     }
   }

Breaking Changes (Liquidators)

  • The classic liquidate instruction has two new arguments, now requiring the caller to declare the length of the liquidator's and liquidatee's remaining accounts slice. In normal usage, this simply looks like:
        await liquidateIx(liquidator.mrgnProgram, {
          //.... base accounts
          remaining: [
            assetOracle,
            liabOracle,
            ...liquidatorAccounts,
            ...liquidateeAccounts,
          ],
          amount: amt,
          liquidateeAccounts: liquidateeAccounts.length,
          liquidatorAccounts: liquidatorAccounts.length,
        })
  • If any asset or liability is fixed, note that you do not have to pass that oracle (see fixed oracle tests in m01/m02 for examples), e.g.:
        await liquidateIx(liquidator.mrgnProgram, {
          //.... base accounts
          remaining: [
            // no asset oracle if fixed
            // no liab oracle if fixed
            // liquidator/ee accounts also respect the fixed-oracle rules above.
            ...liquidatorAccounts,
            ...liquidateeAccounts,
          ],
          amount: amt,
          liquidateeAccounts: liquidateeAccounts.length,
          liquidatorAccounts: liquidatorAccounts.length,
        })

New/Removed instructions

  • migrate_curve (permissionless) - convert a bank from the legacy curve setup to the new seven-point approach. Does not affect bank operations in any way, the bank is immediately able to function as normal.
  • set_fixed_oracle_price (admin only) - sets a price for the bank, like a permanent oracle.
  • start/end_deleverage (risk admin only) - a permissioned liquidation: used to unwind banks that are being sunset. Functions almost identically to receivership liquidation, except that it earns no profit and can be used even on healthy accounts.
  • init_bank_metadata (permissionless) - pay the rent for a bank's metadata
  • write_bank_metadata (metadata admin only) - write token name/description to a bank's metadata
  • admin_super_withdraw (removed) - arena has been sunset, and this is no longer needed.

Additional Notes on Fixed Oracle Banks

  • The oracle_keys[0] slot of Fixed oracle banks is always pubkey default. Other oracle_keys[] fields might still be populated, e.g. the lst mint and sol pool for staked banks, or the reserve for the Kamino banks, this is to simplify switching these banks back into a normal oracle operational mode if needed.
  • Staked collateral banks can't take a fixed price. Kamino banks CAN (and so can future integrator banks).

Additional Notes on new Curves

  • add_bank_permissionless, and add_pool (kamino) now create different placeholders (using the new curve system). Staked and Kamino banks created prior to this update should still be migrated so they don't die when "accruing interest" (even though it doesn't do anything for them, since they never earn interest).
  • Banks on the legacy approach will continue to function as normal before/after migration. The legacy approach will cease to function in 1.7.

Samples

See the referenced functions for more details on how to convert to/from a float to a u32 representation or to a 5-length points slice.

Sample points creation (Rust):

points: make_points(&vec![RatePoint::new(
    p100_to_u32(I80F48!(0.9)),
    p1000_to_u32(I80F48!(1)),
)]),

Sample points creation (TS):

points: makeRatePoints([0.5], [0.6])

Audit Notes

TBD

Consolidates:

Changes since RC1

CU usage optimizations

Deploy Information

Hash - TBD
Staging - TBD
Mainnet -TBD