leverage

package
v3.0.0-...-244b59d Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 21, 2023 License: Apache-2.0 Imports: 18 Imported by: 0

README

Leverage Module

Abstract

This document specifies the x/leverage module of the Nebula chain.

The leverage module allows users to supply and borrow assets, and implements various features to support this, such as a token accept-list, a dynamic interest rate module, incentivized liquidation of undercollateralized debt, and automatic reserve-based repayment of bad debt.

The leverage module depends directly on x/oracle for asset prices, and interacts indirectly with x/ibctransfer, x/gravity, and the cosmos x/bank module as these all affect account balances.

Contents

  1. Concepts
  2. State
  3. Queries
  4. Messages
  5. Update Registry Proposal
  6. Events
  7. Parameters
  8. EndBlock

Concepts

Accepted Assets

At the foundation of the leverage module is the Token Registry, which contains a list of accepted types.

This list is controlled by governance. Assets that are not in the token registry are nor available for borrowing or supplying.

Once added to the token registry, assets cannot be removed. In the rare case where an asset would need to be phased out, it can have supplying or borrowing disabled, or in extreme cases, be ignored by collateral and borrowed value calculations using a blacklist.

uTokens

Every base asset has an associated uToken denomination.

uTokens do not have parameters like the Token struct does, and they are always represented in account balances with a denom of UTokenPrefix + token.BaseDenom. For example, the base asset unebula is associated with the uToken denomination u/unebula.

Supplying and Borrowing

Users have the following actions available to them:

  • Supply accepted asset types to the module, receiving uTokens in exchange.

    Suppliers earn interest at an effective rate of the asset's Supplying APY as the uToken Exchange Rate increases over time.

    Additionally, for assets denominations already enabled as collateral, the supplied assets immediately become collateral as well, causing their borrow limit to increase.

    If a user is undercollateralized (borrowed value > borrow limit), collateral is eligible for liquidation and cannot be withdrawn until the user's borrows are healthy again.

    Care should be taken by undercollateralized users when supplying token amounts too small to restore the health of their borrows, as the newly supplied assets will be eligible for liquidation immediately.

  • Enable or Disable (MsgSetCollateral) a uToken denomination as collateral for borrowing.

    Enabling uTokens as collateral stores them in the leverage module account so they cannot be transferred while in use. Disabling uTokens as collateral returns them to the user's account. A user cannot disable a uToken denomination if it would reduce their Borrow Limit below their total borrowed value.

    If the user is undercollateralized (borrowed value > borrow limit), enabled collateral is eligible for liquidation and cannot be disabled until the user's borrows are healthy again.

  • MsgWithdraw supplied assets by turning in uTokens of the associated denomination. Withdraw respects the uToken Exchange Rate. A user can always withdraw non-collateral uTokens, but can only withdraw collateral-enabled uTokens if it would not reduce their Borrow Limit below their total borrowed value.

  • MsgMaxWithdraw supplied assets by automatically calculating the maximum amount that can be withdawn.

  • MsgBorrow assets of an accepted type, up to their Borrow Limit.

    Interest will accrue on borrows for as long as they are not paid off, with the amount owed increasing at a rate of the asset's Borrow APY.

  • MsgRepay assets of a borrowed type, directly reducing the amount owed.

    Repayments that exceed a borrower's amount owed in the selected denomination succeed at paying the reduced amount rather than failing outright.

  • MsgLiquidate undercollateralized borrows a different user whose total borrowed value is greater than their Liquidation Threshold.

    The liquidator must select a reward denomination present in the borrower's uToken collateral. Liquidation is limited by Close Factor and available balances, and will succeed at a reduced amount rather than fail outright when possible.

    If a borrower is way past their borrow limit, incentivized liquidation may exhaust all of their collateral and leave some debt behind. When liquidation exhausts the last of a borrower's collateral, its remaining debt is marked as bad debt in the keeper, so it can be repaid using module reserves.

Reserves

A portion of accrued interest on all borrows (determined per-token by the parameter ReserveFactor) is set aside as a reserves, which are automatically used to pay down bad debt.

Rather than being stored in a separate account, the ReserveAmount of any given token is stored in the module's state, after which point the module respects the reserved amount by treating part of the balance of the leverage module account as off-limits.

For example, if the module contains 1000 unebula and 100 unebula are reserved, then only 900 unebula are available for Borrow and Withdraw transactions. If 40 unebula of reserves are then used to pay off a bad debt, the module account will have 960 unebula with 60 unebula reserved, keeping the available balance at 900 unebula.

Oracle Rewards

At the same time reserves are accrued, an additional portion of borrow interest accrued is transferred from the leverage module account to the oracle module account to fund its reward pool. Because the transfer happens instantaneously and the accounts are separate, there is no need to module state to track the amounts.

Derived Values

Some important quantities that govern the behavior of the leverage module are derived from a combination of parameters, borrow values, and oracle prices. The math and reasoning behind these values will appear below.

As a reminder, the following values are always available as a basis for calculations:

  • Account token and uToken balances, available through the bank module.
  • Total supply of any uToken denomination, stored in leverage module State.
  • The leverage module account balance, available through the bank module.
  • Collateral uToken amounts held in the leverage module account for individual borrowers, stored in leverage module State.
  • Borrowed denominations and adjusted amounts for individual borrowers, stored in leverage module state).
  • Interest scalars for all borrowed denominations, which are used with adjusted borrow amounts
  • Total adjusted borrows summed over all borrower accounts.
  • Leverage module Parameters
  • Token parameters from the Token Registry

The more complex derived values must use the values above as a basis.

Adjusted Borrow Amounts

Borrow amounts stored in state are stored as AdjustedBorrow amounts, which can be converted to and from actual borrow amounts using the following relation:

AdjustedBorrow(denom,user) * InterestScalar(denom) = BorrowedAmount(denom,user)

When interest accrues on borrow positions, the InterestScalar of the denom is increased and the adjusted borrow amounts remain unchanged.

uToken Exchange Rate

uTokens are intended to work in the following way:

The total supply of uTokens of a given denomination, if exchanged, are worth the total amount of the associated token denomination in the lending pool, including that which has been borrowed out and any interest accrued on it.

Thus, the uToken exchange rate for a given denom and associated uDenom is calculated as:

exchangeRate(denom) = [ ModuleBalance(denom) - ReservedAmount(denom) + TotalBorrowed(denom) ] / TotalSupply(uDenom)

In state, uToken exchange rates are not stored as the can be calculated on demand.

Exchange rates satisfy the invariant exchangeRate(denom) >= 1.0

Supply Utilization

Supply utilization of a token denomination is calculated as:

supplyUtilization(denom) = TotalBorrowed(denom) / [ ModuleBalance(denom) - ReservedAmount(denom) + TotalBorrowed(denom) ]

Supply utilization ranges between zero and one in general. In edge cases where ReservedAmount(denom) > ModuleBalance(denom), utilization is taken to be 1.0.

Borrow Limit

Each token in the Token Registry has a parameter called CollateralWeight, always less than 1, which determines the portion of the token's value that goes towards a user's borrow limit, when the token is used as collateral.

A user's borrow limit is the sum of the contributions from each denomination of collateral they have deposited.

  collateral := GetBorrowerCollateral(borrower) // sdk.Coins
  for _, coin := range collateral {
    borrowLimit += GetCollateralWeight(coin.Denom) * TokenValue(coin) // TokenValue is in usd
  }
Liquidation Threshold

Each token in the Token Registry has a parameter called LiquidationThreshold, always greater than or equal to collateral weight, but less than 1, which determines the portion of the token's value that goes towards a borrower's liquidation threshold, when the token is used as collateral.

A user's liquidation threshold is the sum of the contributions from each denomination of collateral they have deposited. Any user whose borrow value is above their liquidation threshold is eligible to be liquidated.

  collateral := GetBorrowerCollateral(borrower) // sdk.Coins
  for _, coin := range collateral {
     liquidationThreshold += GetLiquidationThreshold(coin.Denom) * TokenValue(coin) // TokenValue is in usd
  }
Borrow APY

Nebula uses a dynamic interest rate model. The borrow APY for each borrowed token denomination changes based on that token Supply Utilization.

The Token struct stored in state for a given denomination defines three points on the Utilization vs Borrow APY graph:

  • At utilization = 0.0, borrow APY = Token.BaseBorrowRate
  • At utilization = Token.KinkUtilization, borrow APY = Token.KinkBorrowRate
  • At utilization = 1.0, borrow APY = Token.MaxBorrowRate

When utilization is between two of the above values, borrow APY is determined by linear interpolation between the two points. The resulting graph looks like a straight line with a "kink" in it.

Supplying APY

The interest accrued on borrows, after some of it is set aside for reserves, is distributed to all suppliers (i.e. uToken holders) of that denomination by virtue of the uToken exchange rate increasing.

While Supplying APY is never explicity used in the leverage module due to its indirect nature, it is available for querying and can be calculated:

SupplyAPY(token) = BorrowAPY(token) * SupplyUtilization(token) * [1.0 - ReserveFactor(token)]

Close Factor

When a borrower is above their borrow limit, their open borrows are eligible for liquidation. In order to reduce the severity of liquidation events that can occur to borrowers that only slightly exceed their borrow limits, a dynamic CloseFactor applies.

A CloseFactor can be between 0 and 1. For example, a CloseFactor = 0.25 means that a liquidator can at most pay back 25% of a borrower's current total borrowed value in a single transaction.

Two module parameters are required to compute a borrower's CloseFactor based on how far their TotalBorrowedValue exceeds their BorrowLimit (both of which are USD values determined using price oracles)

portionOverLimit := (TotalBorrowedValue / BorrowLimit) - 1
// e.g. (1100/1000) - 1 = 0.1, or 10% over borrow limit

if portionOverLimit > params.CompleteLiquidationThreshold {
  CloseFactor = 1.0
} else {
  CloseFactor = Interpolate(             // linear interpolation
    0.0,                                 // minimum x
    params.CompleteLiquidationThreshold, // maximum x
    params.MinimumCloseFactor,           // minimum y
    1.0,                                 // maximum y
  )
}
Total Supplied

The TotalSupplied of a token denom is the sum of all tokens supplied to the asset facility, including those that have been borrowed out and any interest accrued, minus reserves.

TotalSupplied(denom) = ModuleBalance(denom) - ReservedAmount(denom) + TotalBorrowed(denom)

State

The x/leverage module keeps the following objects in state:

  • Registered Token (Token settings)*: 0x01 | denom -> Token
  • Adjusted Borrowed Amount: 0x02 | borrowerAddress | denom -> sdk.Dec
  • Collateral Setting: 0x03 | borrowerAddress | denom -> 0x01
  • Collateral Amount: 0x04 | borrowerAddress | denom -> sdk.Int
  • Reserved Amount: 0x05 | denom -> sdk.Int
  • Last Interest Accrual (Unix Time): 0x06 -> int64
  • Bad Debt Instance: 0x07 | borrowerAddress | denom -> 0x01
  • Interest Scalar: 0x08 | denom -> sdk.Dec
  • Total Borrowed: 0x09 | denom -> sdk.Dec
  • Totak UToken Supply: 0x0A | denom -> sdk.Int

The following serialization methods are used unless otherwise stated:

  • sdk.Dec.Marshal() and sdk.Int.Marshal() for numeric types
  • []byte(denom) | 0x00 for asset and uToken denominations (strings)
  • address.MustLengthPrefix(sdk.Address) for account addresses
  • cdc.Marshal and cdc.Unmarshal for gogoproto/types.Int64Value wrapper around int64

Note that collateral settings and instances of bad debt are both tracked using a value of 0x01. In both cases, the 0x01 means true ("enabled" or "present") and a missing or deleted entry means false. No value besides 0x01 is ever stored.

Adjusted Total Borrowed

Unlike all other quantities in state, AdjustedTotalBorrowed values are not present in imported and exported genesis state.

Instead, every time an individual AdjustedBorrow is set during ImportGenesis, its respective token's AdjustedTotalBorrowed is increased by the same amount. Thus, it is indirectly imported as the sum of individual positions.

Similarly, AdjustedTotalBorrowed is never set independently during regular operations. It is modified during calls to setAdjustedBorrow, always increasing or decreasing by the change in the individual borrow being set.

Queries

See leverage query proto for list of supported queries.

Additionally, the query liquidation-targets is only enabled if the node is started with a flag:

# Enabled
nebud start --enable-liquidator-query

# Enabled
nebud start -l

# Disabled
nebud start

Messages

See leverage tx proto for list of supported messages.

Update Registry Proposal

Update-Registry gov proposal will adds the new tokens to token registry or update the existing token with new settings.

CLI
nebud tx gov submit-proposal [path-to-proposal-json] [flags]

Example:

nebud tx gov submit-proposal /path/to/proposal.json --from nebula1..

// Note `authority` will be gov module account address in proposal.json
nebud q auth module-accounts -o json | jq '.accounts[] | select(.name=="gov") | .base_account.address'

where proposal.json contains:

{
    "messages": [
        {
            "@type": "/nebula.leverage.v1.MsgGovUpdateRegistry",
            "authority": "nebula10d07y265gmmuvt4z0w9aw880jnsr700jg5w6jp",
            "title": "Update the Leverage Token Registry",
            "description": "Update the unebula token in the leverage registry.",
            "add_tokens": [
                {
                    "base_denom": "unebula",
                    "reserve_factor": "0.100000000000000000",
                    "collateral_weight": "0.050000000000000000",
                    "liquidation_threshold": "0.050000000000000000",
                    "base_borrow_rate": "0.020000000000000000",
                    "kink_borrow_rate": "0.200000000000000000",
                    "max_borrow_rate": "1.500000000000000000",
                    "kink_utilization": "0.200000000000000000",
                    "liquidation_incentive": "0.100000000000000000",
                    "symbol_denom": "NEBULA",
                    "exponent": 6,
                    "enable_msg_supply": true,
                    "enable_msg_borrow": true,
                    "blacklist": false,
                    "max_collateral_share": "0.900000000000000000",
                    "max_supply_utilization": "0.900000000000000000",
                    "min_collateral_liquidity": "0.900000000000000000",
                    "max_supply": "123123"
                },
            ],
            "update_tokens": [
                {
                    "base_denom": "uatom",
                    "reserve_factor": "0.100000000000000000",
                    "collateral_weight": "0.050000000000000000",
                    "liquidation_threshold": "0.050000000000000000",
                    "base_borrow_rate": "0.020000000000000000",
                    "kink_borrow_rate": "0.200000000000000000",
                    "max_borrow_rate": "1.500000000000000000",
                    "kink_utilization": "0.200000000000000000",
                    "liquidation_incentive": "0.100000000000000000",
                    "symbol_denom": "ATOM",
                    "exponent": 6,
                    "enable_msg_supply": true,
                    "enable_msg_borrow": true,
                    "blacklist": false,
                    "max_collateral_share": "0.900000000000000000",
                    "max_supply_utilization": "0.900000000000000000",
                    "min_collateral_liquidity": "0.900000000000000000",
                    "max_supply": "123123"
                },
            ]
        }
    ],
    "metadata": "AQ==",
    "deposit": "100unebula"
}

Events

See leverage events proto for list of supported events.

Params

See leverage module proto for list of supported module params.

End Block

Every block, the leverage module runs the following steps in order:

  • Repay bad debts using reserves
  • Accrue interest on borrows
Sweep Bad Debt

Borrowers whose entire balance of collateral has been liquidated but still owe debt are marked by their final liquidation transaction. This periodic routine sweeps up all marked address | denom bad debt entries in the keeper, performing the following steps for each:

  • Determine the about of Reserves in the borrowed denomination available to repay the debt
  • Repay the full amount owed using reserves, or the maxmimum amount available if reserves are insufficient
  • Emit a "Bad Debt Repaid" event indicating amount repaid, if nonzero
  • Emit a "Reserves Exhausted" event with the borrow amount remaining, if nonzero
Accrue Interest

At every epoch, the module recalculates Borrow APY and Supplying APY for each accepted asset type, storing them in state for easier query.

Borrow APY is then used to accrue interest on all open borrows.

After interest accrues, a portion of the amount for each denom is added to the state's ReservedAmount of each borrowed denomination.

Then, an additional portion of interest accrued is transferred from the leverage module account to the oracle module to fund its reward pool.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func AddModuleInitFlags

func AddModuleInitFlags(startCmd *cobra.Command)

AddModuleInitFlags implements servertypes.ModuleInitFlags interface.

func EndBlocker

func EndBlocker(ctx sdk.Context, k keeper.Keeper) []abci.ValidatorUpdate

EndBlocker implements EndBlock for the x/leverage module.

func ExportGenesis

func ExportGenesis(ctx sdk.Context, k keeper.Keeper) *types.GenesisState

ExportGenesis returns the x/leverage module's exported genesis state.

func InitGenesis

func InitGenesis(ctx sdk.Context, k keeper.Keeper, genState types.GenesisState)

InitGenesis initializes the x/leverage module's state from a provided genesis state.

Types

type AppModule

type AppModule struct {
	AppModuleBasic
	// contains filtered or unexported fields
}

AppModule implements the AppModule interface for the x/leverage module.

func NewAppModule

func NewAppModule(cdc codec.Codec, keeper keeper.Keeper, ak types.AccountKeeper, bk bankkeeper.Keeper) AppModule

func (AppModule) BeginBlock

func (am AppModule) BeginBlock(_ sdk.Context, _ abci.RequestBeginBlock)

BeginBlock executes all ABCI BeginBlock logic respective to the x/leverage module.

func (AppModule) ConsensusVersion

func (AppModule) ConsensusVersion() uint64

func (AppModule) EndBlock

EndBlock executes all ABCI EndBlock logic respective to the x/leverage module. It returns no validator updates.

func (AppModule) ExportGenesis

func (am AppModule) ExportGenesis(ctx sdk.Context, cdc codec.JSONCodec) json.RawMessage

ExportGenesis returns the x/leverage module's exported genesis state as raw JSON bytes.

func (AppModule) InitGenesis

func (am AppModule) InitGenesis(ctx sdk.Context, cdc codec.JSONCodec, data json.RawMessage) []abci.ValidatorUpdate

InitGenesis performs the x/leverage module's genesis initialization. It returns no validator updates.

func (AppModule) LegacyQuerierHandler

func (am AppModule) LegacyQuerierHandler(legacyQuerierCdc *codec.LegacyAmino) sdk.Querier

LegacyQuerierHandler returns a no-op legacy querier.

func (AppModule) Name

func (am AppModule) Name() string

Name returns the x/leverage module's name.

func (AppModule) QuerierRoute

func (AppModule) QuerierRoute() string

QuerierRoute returns the x/leverage module's query routing key.

func (AppModule) RegisterInvariants

func (am AppModule) RegisterInvariants(ir sdk.InvariantRegistry)

RegisterInvariants registers the x/leverage module's invariants.

func (AppModule) RegisterServices

func (am AppModule) RegisterServices(cfg module.Configurator)

RegisterServices registers gRPC services.

func (AppModule) Route deprecated

func (am AppModule) Route() sdk.Route

Deprecated: Route returns the message routing key for the x/leverage module.

func (AppModule) WeightedOperations

func (am AppModule) WeightedOperations(simState module.SimulationState) []simtypes.WeightedOperation

WeightedOperations returns the all the leverage module operations with their respective weights.

type AppModuleBasic

type AppModuleBasic struct {
	// contains filtered or unexported fields
}

AppModuleBasic implements the AppModuleBasic interface for the x/leverage module.

func NewAppModuleBasic

func NewAppModuleBasic(cdc codec.Codec) AppModuleBasic

func (AppModuleBasic) DefaultGenesis

func (AppModuleBasic) DefaultGenesis(cdc codec.JSONCodec) json.RawMessage

DefaultGenesis returns the x/leverage module's default genesis state.

func (AppModuleBasic) GetQueryCmd

func (AppModuleBasic) GetQueryCmd() *cobra.Command

GetQueryCmd returns the x/leverage module's root query command.

func (AppModuleBasic) GetTxCmd

func (a AppModuleBasic) GetTxCmd() *cobra.Command

GetTxCmd returns the x/leverage module's root tx command.

func (AppModuleBasic) Name

func (AppModuleBasic) Name() string

Name returns the x/leverage module's name.

func (AppModuleBasic) RegisterGRPCGatewayRoutes

func (AppModuleBasic) RegisterGRPCGatewayRoutes(clientCtx client.Context, mux *runtime.ServeMux)

RegisterGRPCGatewayRoutes registers the gRPC Gateway routes for the x/leverage module.

func (AppModuleBasic) RegisterInterfaces

func (a AppModuleBasic) RegisterInterfaces(reg cdctypes.InterfaceRegistry)

RegisterInterfaces registers the module's interface types.

func (AppModuleBasic) RegisterLegacyAminoCodec

func (AppModuleBasic) RegisterLegacyAminoCodec(cdc *codec.LegacyAmino)

RegisterLegacyAminoCodec registers the x/leverage module's types with a legacy Amino codec.

func (AppModuleBasic) RegisterRESTRoutes deprecated

func (AppModuleBasic) RegisterRESTRoutes(_ client.Context, _ *mux.Router)

Deprecated: RegisterRESTRoutes performs a no-op. Querying is delegated to the gRPC service.

func (AppModuleBasic) ValidateGenesis

func (AppModuleBasic) ValidateGenesis(
	cdc codec.JSONCodec,
	config client.TxEncodingConfig,
	bz json.RawMessage,
) error

ValidateGenesis performs genesis state validation for the x/leverage module.

Directories

Path Synopsis
client
cli
Package fixtures provides test data for tests
Package fixtures provides test data for tests
Package types is a reverse proxy.
Package types is a reverse proxy.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL