Liquidity Accounting State
Learn about structures that represent the pools of liquidity and their associated accounting for trades on Imperial.
State
The program has a global State singleton that owns all objects. It doesn't really do anything except store the global administrator for the program. Every State can have multiple protocol token accounts which are ATAs, one for each mint being used by the program, to collect fees during the Lifecycle of the program. These can be created by an endpoint in the program.
#[account]
pub struct State {
pub admin: Pubkey,
}
Pool
States own all Pools, each Pool represents a coupling of one LP token to multiple stablecoin Custodies, with the assumption in the current draft of the program being that each stablecoin is 1:1 with each other stablecoin and each Pool must have a matching Oracle used for pricing. For instance, "BTC-USD-IV" would be a Pool containing only USD stablecoins, used for BTC Implied Volatility trading.
Here is the structure of the Pool:
#[account]
pub struct Pool {
pub name: String,
pub custodies: Vec<Pubkey>, // Explained during Custody section
pub crank_authority: Pubkey, // Who has right to crank thru PositionRequests
pub aum_usd: u64, // Unused presently
pub limit: PoolLimit, // Discussed below
pub fees: PoolFees, // Discussed below
pub pool_apr: PoolApr, // Discussed below
pub realized_fee_usd: u64, // Keeps track of fees not added back to base yet
pub lp_mint: Pubkey, // Mint address for LP tokens
pub lp_token_account: Pubkey, // Token account for LP tokens
pub state: Pubkey, // Add state pubkey
pub product: Product, // Add product field
pub oracle: Pubkey, // Oracle pubkey for the pool
}
Pools have a few sub-structures to them, enumerated in the document below and referenced in the above struct (PoolLimit, PoolFees, and PoolApr). To learn more about realized_fee_usd, see the Fee Collection Lifecycle.
Product
The different products that can be used for Positions taken against a Pool.
#[derive(AnchorSerialize, AnchorDeserialize, Clone, Copy, PartialEq, Debug)]
pub enum Product {
Iv,
Rv,
RvDiffIv,
}
Pool Limits
The PoolLimits object sets constraints on the size of different things in the VolPerp program both at the Pool and Custody levels (you will see that the Custody also has one of these objects). Custody will compare it's Pool parent value, with the least extreme of the two values winning out for any given set of logic. An example of this would be the lower of the two max_leverage being selected for use.
#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
pub struct PoolLimit {
pub pool_profit_load_percentage_bps: u32,
pub max_long_size: u64,
pub max_short_size: u64,
// Ex: 20 means 20x is the max leverage allowed.
pub max_leverage: u64,
pub max_aum_usd: u64, // Unused
pub min_funding_rate: i64, // Unused
pub max_funding_rate: i64, // Unused
pub trade_impact_fee_scalar: u64,
pub trade_blockage_utilization_limit: u64,
}
Some of these are explanatory, but pool_profit_load_percentage_bps has deeper effects on trading. This is the "percentage of the maximum size of a trade that is required to be allocated by the system as locked tokens when the trade is created." To understand, let's break it down:
There is a max_size the loan + collateral + profit can be for every trade that is determined by some logic.
That number of tokens to be set aside ("locked") is based on a percentage of the maximum size.
This percentage is the pool_profit_load_percentage_bps.
The next item worthy of discussion is the trade_impact_fee_scalar. This is a simulated fee meant to represent the impact to the non-existent order book when a new Position enters it, to bring the program into fee parity with centralized systems.
Finally, there is trade_blockage_utilization_limit. This represents a percentage of locked tokens the total, also known as base in Custody parlance. When this percentage is near, swaps of stablecoins out of the Pool will fail for this this Custody's mint address because it will bump up against the utilization limit since it will effectively increase the utilization of the Pool from it's present state, which is already near the limit. To clear this, Positions need to close or LPs need to add more stablecoins.
Pool Fees
The PoolFees object contains the different fee settings for lifecycle events in the program.
#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
pub struct PoolFees {
pub swap_fee_bps: u64,
pub tax_fee_bps: u64, // Unused
pub increase_position_bps: u64,
pub decrease_position_bps: u64,
pub add_remove_liquidity_bps: u64, // Unused
pub protocol_share_bps: u64,
pub positive_collateral_liquidator_reward_bps: u64,
pub positive_collateral_protocol_reward_bps: u64,
pub positive_collateral_pool_reward_bps: u64,
pub negative_collateral_liquidation_reward_bps: u64, // Needs renaming to liquidator
pub insurance_fund_liquidation_fee_bps: u64,
pub insurance_fund_fee_bps: u64, // unused
pub borrow_fee_liquidation_fund_contribution_dps: u64,
}
Some things that may not be self evident are the difference between positive and negative collateral rewards - this has to do with the Liquidation Engine. In Imperial, a Position that exceeds it's allowable max_size can be liquidated just like one that has losses so large that it has exceeded it's maximum leverage allowance.
The rewards, however, for a "positive collateral" position are different than for a negative one, and the Pool, liquidator, and user receive different portions depending on the pool settings.
Pool APR
An almost fully deprecated object, inspired by the Jupiter Perpetual API. We have it for backwards compatibility.
#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
pub struct PoolApr {
pub last_updated: i64, // Only updated when pool settings are changed
pub fee_apr_bps: u64, // unused
pub realized_fee_usd: u64, // Unused, replaced by pool level copy
}
Custody
Custodies are the primary has-many child relationship of Pool and there is one per stablecoin token mint in the Pool. So if the "BTC-USD-IV" Pool (Bitcoin USD for Implied VOL) wants to settle in USDC and USDT, it will have two Custodies, one for tether and one for Circle. Each Custody is tied to two token accounts primarily - an insurance token PDA that is used to pay for losses over and above the locked_token amount allotted by the trade, and a primary ATA token account containing the actual tokens of the Custody.
The insurance token account is filled up during various Lifecycle events discussed here.
#[account]
pub struct Custody {
pub pool: Pubkey,
pub mint: Pubkey,
pub custody_token_account: Pubkey,
pub decimals: u8,
pub is_stable: bool,
pub limit: PoolLimit,
pub permissions: PermissionsData,
pub target_ratio_bps: u64, // unused
pub assets: AssetsData,
pub funding_rate_state: FundingRateState,
pub jump_rate_state: JumpRateState,
}
Custody has a few child objects that we'll go over, but one notable one as mentioned earlier is a copy of PoolLimit, that overrides the parent PoolLimit when it has values that are less extreme than the Pool.
Permissions Data
This allows, at a Custody level, the fine-tuned control over what traders can and cannot do with a Custody. Generally all of these will be true but may be shut off during black swan events.
#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
pub struct PermissionsData {
pub allow_open_position: bool,
pub allow_close_position: bool,
pub allow_pnl_withdrawal: bool,
pub allow_collateral_withdrawal: bool,
pub allow_size_change: bool,
pub allow_leverage_change: bool,
pub allow_adding_collateral: bool,
}
Assets Data
This data structure is super important, so we kept the in-depth comments from the code in the structure to assist with comprehension and present it as is. The AssetsData is like a live ledger of the Custody's LP accounts, the tokens it's holding as collateral for traders, it's locked tokens for those traders' positions, trader (user) losses that have been incurred during it's lifetime, impairments that have been incurred during it's lifetime (see below), and insurance accounting, among other things.
This is a very important data structure that can be monitored for a sense of the health of the Custody.
#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
pub struct AssetsData {
/// Total tokens in the insurance fund
pub insurance: u64,
/// Total tokens lost when we owe profits to users that aren't covered by locked_amount, or the insurance fund, and we have to pay them.
pub impairment_losses: u64,
/// Losses incurred by users when they are owed profits and we are unable to pay them because we have no tokens between
/// the insurance fund or the custody with which to do it.
pub user_losses: u64,
/// Total tokens collateral + base provided by swap
pub owned: u64,
/// When users swap in tokens for LP tokens, this goes up or down
pub base: u64,
/// Increased by some % of the maximum total value of a trade when initiated (fees not included)
/// These tokens cannot then be used by some other trade until the position is closed.
pub locked: u64,
/// total tokens longed
pub longed: u64,
/// total shorted
pub shorted: u64,
/// Total loans across the custody
pub guaranteed_usd: u64,
/// total tokens that can be reinvested in LP purchases. owned + fee_reserves = total tokens in the token account.
pub fee_reserves: u64,
}
Funding Rate State
This object keeps track of when an action was last run (mostly update_position) that included this Custody, and essentially turns that timestamp into an interest rate-time that can be used to back into an owed cumulative borrow tracker for any given Position. This cumulative tracker effect is accomplished by the fact that the Position knows it's own last borrow timestamp, and the Custody knows it's own "all-time" cumulative borrow, so the two can be compared and an owed amount can be deduced from the difference.
See the Concepts section on how borrowing is done for more on this.
#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
pub struct FundingRateState {
pub cumulative_interest_rate: u64,
pub last_updated: i64,
pub last_borrow_rate: u64,
}
Jump Rate State
This structure is used for calculating the borrow_rate at any point in time using the dual slope utilization model. It is located on the Custody.
#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
pub struct JumpRateState {
pub min_rate_bps: u64,
pub max_rate_bps: u64,
pub target_rate_bps: u64,
pub target_utilization_rate: u64,
}
How is this done? First, we grab the current utilization_rate of the Custody and compare it to the target_utilization_rate (bps) given in the JumpRateState.
If it is less than the target, we calculate the lower_slope of the dual slope utilization model to find the borrow_rate, which is a shallower slope designed to give borrowers an edge until the Custody reaches target_utilization. It's math is given by:
let lower_slope = (target_rate_bps - min_rate_bps) / target_utilization_rate
If it is more, we have the upper_slope, designed to disincentivize borrowing, given by:
let upper_slope = (max_rate_bps - target_rate_bps) / (10_000 - target_utilization_rate)
This "dual slope" is really two lines stapled together, representing different borrow_rates, each segment taking place over some portion of a 10_000 bps strip along the x axis (x being borrow_rate).
If using the lower_slope to derive borrow_rate, we multiply it by the current utilization_rate bps of the Custody, and then add it it to the min_rate_bps of the Custody to determine the borrow rate:
let borrow_rate = utilization * lower_slope + min_rate_bps
If using the upper_slope to derive borrow_rate, we take the difference between the utilization of the Custody and it's target, and multiply that by the upper_slope, and then add that to the target_rate_bps and that is the borrow_rate.
let borrow_rate = (utilization - target_utilization_rate) * upper_slope + target_rate_bps
Last updated