#deal #cpi #account #market #index #credix #pre-mainnet

credix_client

Crate to help perform CPI calls to the Credix program

17 releases (8 breaking)

0.9.1 Sep 13, 2023
0.8.0 Jun 13, 2023
0.7.6 Aug 31, 2023
0.6.0 Feb 24, 2023
0.4.0 Nov 18, 2022

#5 in #cpi

GPL-3.0-or-later

61KB
341 lines

Credix Rust client

A crate to interact with the Credix program via CPI

NOTE: This crate was generated with the help of the Anchor generated IDL and adds a few extra functions in addition to the generated code. To know certain pda seeds or which accounts are mutable, please refer back to the Credix program IDL. It is available for each environment in the source of this crate.

Environments

Pre-mainnet

  • Pre-mainnet: This is the Solana devnet testing environment for Credix programs. It is usually very close to the mainnet environment. Before releasing any feature to mainnet, the pre-mainnet program is updated. Address: crdRi38zEhQdzpsxnKur73WHBM9BSvXMSfGcbLyJCdP.

Markets

  • Market with seed credix-marketplace on pre-mainnet should be targeted for testing without withdraw epochs.
  • Market with seed withdraw-epochs on pre-mainnet should be targeted for testing with withdraw epochs.

Configuring the client

To target our pre-mainnet environment you can enable the pre-mainnet feature

credix_client = { version="0.8.0", features = ["pre-mainnet"], default-features = false }

Mainnet

  • Mainnet: This is our production environment. Address: CRDx2YkdtYtGZXGHZ59wNv1EwKHQndnRc1gT4p8i2vPX

Note: To use any market on any Credix environment, you will need an active Credix pass with certain permissions set. Contact Credix to have one issued.

Configuring the client

To target our mainnet environment you can enable the mainnet feature

credix_client = { version="0.8.0", features = ["mainnet"], default-features = false }

Local development:

Getting the Credix binary

When creating a local setup it's advised to grab the pre-mainnet binary from the Solana devnet cluster when preparing for upcoming Credix mainnet releases. This can be done using following command:

solana program dump -u d crdRi38zEhQdzpsxnKur73WHBM9BSvXMSfGcbLyJCdP ./credix.so

Day to day stable testing can be done by grabbing the mainnet binary using following command:

solana program dump -u m CRDx2YkdtYtGZXGHZ59wNv1EwKHQndnRc1gT4p8i2vPX ./credix.so

Preparing the local cluster

To get to a working local environment certain instructions have to be called to set up everything. The following instructions should be called in the same order.

You can find more cargo docs information for each instruction.

You can find more cargo docs information for each account.

Credix program preparation

initialize_program_state

Initializes the ProgramState account that is shared across the entire Credix ecosystem for management purposes. This will allow you to create markets.

initialize_market

Creates a market.

issue_credix_pass

This gives permissions to any participant in the market. It can only be issued by either a key that is listed as a pass issuer in the MarketAdmins account or by the multisig specified in the ProgramState.

Any user interaction with a market requires an active Credix pass.

Market interactions

deposit_funds

This deposits funds into the pool in exchange for LP tokens.

create_withdraw_epoch

Creates a withdraw epoch. This needs to happen before anyone can participate in an epoch. The epoch needs to be created when the previous one is completely over.

create_withdraw_request

Creates a withdraw request. This is where you request a certain amount to withdraw. Depending on the amounts requested by other participants in the epoch and the money available in the market pool, a certain amount will be made available in a first instance. Then a second round happens where amounts that weren't withdrawn become available to the rest of the participants. The duration of the different phases and other parameters are specified on the market level.

redeem_request

Here we actually withdraw funds during either the redeem phase or the free for all phase.

Affecting the LP price

So far no interest repayments have happened so no change in the LP price has occurred. To include this scenario in your tests you need to do a few steps. Again, following instructions need to be done in order and before the withdraw epoch setup to see the effect and more information can be found in the same places as described before.

For convenience's sake we will provide some basic configurations to use to be able to trigger an interest repayment.

create_deal

Creates a deal.

Example config
    max_funding_duration: 255,
    deal_name: "Simple Deal",
    arrangement_fees: 0,
    arrangement_fee_percentage: Fraction::new(0,100),
    migrated: false,
set_repayment_schedule

Sets the repayment schedule. This defines when repayments need to happen and according to what waterfall they need to be processed.

Example config
let now = system_time::now();
// now will contain the current unix timestamp in ms.
let principal = 1000000;

let config_for_instruction= RepaymentScheduleConfig {
            periods: vec![
                RepaymentPeriodInput {
                    waterfall_index: 0,
                    accrual_in_days: 30,
                    principal_expected: None,
                    time_frame: TimeFrame {
                        start: now,
                        end: now + 30 * SECONDS_IN_DAY - 1,
                    },
                },
                RepaymentPeriodInput {
                    waterfall_index: 1,
                    accrual_in_days: 30,
                   principal_expected: Some(principal),
                    time_frame: TimeFrame {
                        start: now + 30 * SECONDS_IN_DAY,
                        end: now + 60 * SECONDS_IN_DAY - 1,
                    },
                }
            ],
            start_ts: now,
            daycount_convention: DaycountConvention::Act360,
            waterfall_definitions: vec![
                DistributionWaterfall {
                    waterfall_type: DistributionWaterfallType::Revolving,
                    tiers: vec![
                        WaterfallTier {
                            allocations: vec![RepaymentAllocation::Interest],
                            tranche_indices: vec![0],
                            charge: true,
                            slash: false,
                        },
                    ],
                },
                DistributionWaterfall {
                    waterfall_type: DistributionWaterfallType::Amortization,
                    tiers: vec![
                        WaterfallTier {
                            allocations: vec![
                             RepaymentAllocation::Principal,
                             RepaymentAllocation::Interest
                            ],
                            tranche_indices: vec![0],
                            charge: true,
                            slash: false,
                        }
                    ],
                },
            ],
    };
set_tranches

Here we define the composition of the deal in tranches. Tranches are the investment opportunities of a deal.

Example config

This includes a tranche that is funded by the pool. This means that when the deal is activated, money will flow from the market's liquidity pool towards the deal.

vec![
    TrancheConfig {
        index: 0,
        max_deposit_percentage: Fraction::new(1, 1).unwrap(),
        early_withdrawal_principal: true,
        funded_by_liquidity_pool: true,
        name: "A".to_string(),
        size: principal, // the variable we declared earlier
        interest: Fraction::new(12, 10).unwrap(),
        interest_performance_fee: Fraction::new(1, 10).unwrap(),
        principal_performance_fee: Fraction::new(0, 1).unwrap(),
        membership_fee: Fraction::new(1, 10).unwrap(),
        late_interest: Fraction::new(1, 10).unwrap(),
        early_principal: Fraction::new(1, 10).unwrap(),
        late_principal: Fraction::new(1, 10).unwrap(),
    },
],
open_deal

Once a deal has been fully configured, we have to change it's status to allow investors to invest into it. This opens the deal for funding.

activate_deal

Once a deal has been fully funded (which is automatically the case using the configs we provided), a deal needs to be activated. This marks the moment funds move from the liquidity pool towards the deal to fund the pool funded tranches. The actual borrowed amount becomes available to the borrower.

repay_deal

This allows the borrower to repay the deal. At this point interest will flow towards the pool and the LP price should rise and your investment should have RoI.

Getting the LP price

The LP token price is the TVL divided by the total supply of LP tokens. The TVL is equal to the total outstanding credit and the funds in the liquidity pool token account.

It can be found on-chain using following calculation:

let pool_liquidity = liquidity_pool_token_account.amount;
let outstanding_credit = global_market_state.pool_outstanding_credit;
let tvl = pool_liquidity + outstanding_credit;

let lp_supply = lp_token_mint.supply;

let lp_price = tvl / lp_supply;

Examples

On-chain

This is an example rust program made with anchor, here we have CPI for deposit, withdraw, create withdraw request and redeem withdraw request.

use anchor_lang::prelude::*;
use credix_client::cpi::accounts::{
    CreateWithdrawRequest, DepositFunds, RedeemWithdrawRequest, WithdrawFunds,
};
use credix_client::cpi::{
    create_withdraw_request, deposit_funds, redeem_withdraw_request, withdraw_funds,
};

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod integrator {
    use super::*;

    pub fn deposit_cpi(ctx: Context<DepositFundsCpi>, amount: u64) -> Result<()> {
        let cpi_context = CpiContext::new(
            ctx.accounts.credix_program.to_account_info(),
            DepositFunds {
                investor: ctx.accounts.investor.to_account_info(),
                global_market_state: ctx.accounts.global_market_state.to_account_info(),
                signing_authority: ctx.accounts.signing_authority.to_account_info(),
                investor_token_account: ctx.accounts.investor_token_account.to_account_info(),
                credix_pass: ctx.accounts.credix_pass.to_account_info(),
                investor_lp_token_account: ctx.accounts.investor_lp_token_account.to_account_info(),
                liquidity_pool_token_account: ctx
                    .accounts
                    .liquidity_pool_token_account
                    .to_account_info(),
                lp_token_mint: ctx.accounts.lp_token_mint.to_account_info(),
                rent: ctx.accounts.rent.to_account_info(),
                system_program: ctx.accounts.system_program.to_account_info(),
                token_program: ctx.accounts.token_program.to_account_info(),
                associated_token_program: ctx.accounts.associated_token_program.to_account_info(),
                base_token_mint: ctx.accounts.base_token_mint.to_account_info(),
            },
        );

        deposit_funds(cpi_context, amount)?;

        Ok(())
    }

    pub fn withdraw_cpi(ctx: Context<WithdrawFundsCpi>, amount: u64) -> Result<()> {
        let cpi_context = CpiContext::new(
            ctx.accounts.credix_program.to_account_info(),
            WithdrawFunds {
                investor: ctx.accounts.investor.to_account_info(),
                global_market_state: ctx.accounts.global_market_state.to_account_info(),
                signing_authority: ctx.accounts.signing_authority.to_account_info(),
                investor_lp_token_account: ctx.accounts.investor_lp_token_account.to_account_info(),
                investor_token_account: ctx.accounts.investor_token_account.to_account_info(),
                liquidity_pool_token_account: ctx
                    .accounts
                    .liquidity_pool_token_account
                    .to_account_info(),
                lp_token_mint: ctx.accounts.lp_token_mint.to_account_info(),
                program_state: ctx.accounts.program_state.to_account_info(),
                credix_treasury: ctx.accounts.credix_treasury.to_account_info(),
                credix_treasury_token_account: ctx
                    .accounts
                    .credix_treasury_token_account
                    .to_account_info(),
                treasury_pool_token_account: ctx
                    .accounts
                    .treasury_pool_token_account
                    .to_account_info(),
                credix_pass: ctx.accounts.credix_pass.to_account_info(),
                base_token_mint: ctx.accounts.base_token_mint.to_account_info(),
                associated_token_program: ctx.accounts.associated_token_program.to_account_info(),
                token_program: ctx.accounts.token_program.to_account_info(),
                rent: ctx.accounts.rent.to_account_info(),
                system_program: ctx.accounts.system_program.to_account_info(),
            },
        );

        withdraw_funds(cpi_context, amount)?;

        Ok(())
    }

    pub fn create_withdraw_request_cpi(
        ctx: Context<WithdrawRequestCpi>,
        amount: u64,
    ) -> Result<()> {
        let accounts = ctx.accounts;
        let cpi_context = CpiContext::new(
            accounts.credix.to_account_info(),
            CreateWithdrawRequest {
                payer: accounts.payer.to_account_info(),
                investor: accounts.investor.to_account_info(),
                credix_pass: accounts.credix_pass.to_account_info(),
                global_market_state: accounts.global_market_state.to_account_info(),
                investor_lp_token_account: accounts.investor_lp_token_account.to_account_info(),
                liquidity_pool_token_account: accounts
                    .liquidity_pool_token_account
                    .to_account_info(),
                lp_token_mint: accounts.lp_token_mint.to_account_info(),
                signing_authority: accounts.signing_authority.to_account_info(),
                system_program: accounts.system_program.to_account_info(),
                withdraw_epoch: accounts.withdraw_epoch.to_account_info(),
            },
        );

        create_withdraw_request(cpi_context, amount)
    }

    pub fn redeem_request_cpi(ctx: Context<RedeemRequestCpi>, amount: u64) -> Result<()> {
        let accounts = ctx.accounts;
        let cpi_context = CpiContext::new(
            accounts.credix.to_account_info(),
            RedeemWithdrawRequest {
                investor: accounts.investor.to_account_info(),
                credix_pass: accounts.credix_pass.to_account_info(),
                global_market_state: accounts.global_market_state.to_account_info(),
                investor_lp_token_account: accounts.investor_lp_token_account.to_account_info(),
                liquidity_pool_token_account: accounts
                    .liquidity_pool_token_account
                    .to_account_info(),
                lp_token_mint: accounts.lp_token_mint.to_account_info(),
                signing_authority: accounts.signing_authority.to_account_info(),
                system_program: accounts.system_program.to_account_info(),
                withdraw_epoch: accounts.withdraw_epoch.to_account_info(),
                associated_token_program: accounts.associated_token_program.to_account_info(),
                base_token_mint: accounts.base_token_mint.to_account_info(),
                credix_treasury: accounts.credix_treasury.to_account_info(),
                credix_treasury_token_account: accounts
                    .credix_treasury_token_account
                    .to_account_info(),
                program_state: accounts.program_state.to_account_info(),
                investor_token_account: accounts.investor_token_account.to_account_info(),
                rent: accounts.rent.to_account_info(),
                token_program: accounts.token_program.to_account_info(),
                treasury_pool_token_account: accounts.treasury_pool_token_account.to_account_info(),
            },
        );

        redeem_withdraw_request(cpi_context, amount)
    }
}

#[derive(Accounts)]
pub struct DepositFundsCpi<'info> {
    #[account(mut)]
    pub investor: Signer<'info>,
    pub credix_program: Program<'info, Credix>,
    /// CHECK: test program
    pub global_market_state: AccountInfo<'info>,
    /// CHECK: test program
    pub signing_authority: AccountInfo<'info>,
    /// CHECK: test program
    #[account(mut)]
    pub investor_token_account: AccountInfo<'info>,
    /// CHECK: test program
    #[account(mut)]
    pub liquidity_pool_token_account: AccountInfo<'info>,
    /// CHECK: test program
    #[account(mut)]
    pub lp_token_mint: AccountInfo<'info>,
    /// CHECK: test program
    #[account(mut)]
    pub investor_lp_token_account: AccountInfo<'info>,
    /// CHECK: test program
    pub credix_pass: AccountInfo<'info>,
    /// CHECK: test program
    pub base_token_mint: AccountInfo<'info>,
    /// CHECK: test program
    pub associated_token_program: AccountInfo<'info>,
    pub rent: Sysvar<'info, Rent>,
    /// CHECK: test program
    pub token_program: AccountInfo<'info>,
    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct WithdrawFundsCpi<'info> {
    #[account(mut)]
    pub investor: Signer<'info>,
    pub credix_program: Program<'info, Credix>,
    #[account(mut)]
    /// CHECK: test program
    pub global_market_state: AccountInfo<'info>,
    /// CHECK: test program
    pub program_state: AccountInfo<'info>,
    /// CHECK: test program
    pub signing_authority: AccountInfo<'info>,
    #[account(mut)]
    /// CHECK: test program
    pub investor_lp_token_account: AccountInfo<'info>,
    #[account(mut)]
    /// CHECK: test program
    pub investor_token_account: AccountInfo<'info>,
    #[account(mut)]
    /// CHECK: test program
    pub liquidity_pool_token_account: AccountInfo<'info>,
    /// CHECK: test program
    pub credix_treasury: AccountInfo<'info>,
    #[account(mut)]
    /// CHECK: test program
    pub credix_treasury_token_account: AccountInfo<'info>,
    #[account(mut)]
    /// CHECK: test program
    pub treasury_pool_token_account: AccountInfo<'info>,
    #[account(mut)]
    /// CHECK: test program
    pub lp_token_mint: AccountInfo<'info>,
    #[account(mut)]
    /// CHECK: test program
    pub credix_pass: AccountInfo<'info>,
    /// CHECK: test program
    pub base_token_mint: AccountInfo<'info>,
    /// CHECK: test program
    pub associated_token_program: AccountInfo<'info>,
    /// CHECK: test program
    pub token_program: AccountInfo<'info>,
    pub rent: Sysvar<'info, Rent>,
    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct WithdrawRequestCpi<'info> {
    #[account(mut)]
    pub payer: Signer<'info>,
    #[account(mut)]
    pub investor: Signer<'info>,
    /// CHECK:
    pub global_market_state: AccountInfo<'info>,
    /// CHECK:
    pub signing_authority: AccountInfo<'info>,
    /// CHECK:
    #[account()]
    pub credix_pass: AccountInfo<'info>,
    /// CHECK:
    #[account(mut)]
    pub withdraw_epoch: AccountInfo<'info>,
    /// CHECK:
    #[account()]
    pub investor_lp_token_account: AccountInfo<'info>,
    /// CHECK:
    #[account()]
    pub liquidity_pool_token_account: AccountInfo<'info>,
    /// CHECK:
    #[account()]
    pub lp_token_mint: AccountInfo<'info>,
    pub credix: Program<'info, Credix>,
    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct RedeemRequestCpi<'info> {
    #[account(mut)]
    pub investor: Signer<'info>,
    /// CHECK:
    #[account(mut)]
    pub global_market_state: AccountInfo<'info>,
    /// CHECK:
    #[account(mut)]
    pub withdraw_epoch: AccountInfo<'info>,
    /// CHECK:
    #[account()]
    pub program_state: AccountInfo<'info>,
    /// CHECK: The check happens when verifying the PDA address.
    #[account()]
    pub signing_authority: AccountInfo<'info>,
    /// CHECK:
    #[account(mut)]
    pub investor_lp_token_account: AccountInfo<'info>,
    /// CHECK:
    #[account(mut)]
    pub investor_token_account: AccountInfo<'info>,
    /// CHECK:
    #[account(mut)]
    pub liquidity_pool_token_account: AccountInfo<'info>,
    /// CHECK: The address of account is known.
    #[account()]
    pub credix_treasury: AccountInfo<'info>,
    /// CHECK:
    #[account(mut)]
    pub credix_treasury_token_account: AccountInfo<'info>,
    /// CHECK:
    #[account(mut)]
    pub treasury_pool_token_account: AccountInfo<'info>,
    /// CHECK:
    #[account(mut)]
    pub lp_token_mint: AccountInfo<'info>,
    /// CHECK:
    #[account(mut)]
    pub credix_pass: AccountInfo<'info>,
    /// CHECK:
    #[account()]
    pub base_token_mint: AccountInfo<'info>,
    /// CHECK:
    pub associated_token_program: AccountInfo<'info>,
    /// CHECK:
    pub token_program: AccountInfo<'info>,
    pub rent: Sysvar<'info, Rent>,
    pub system_program: Program<'info, System>,
    pub credix: Program<'info, Credix>,
}

Off-chain

Typescript

For off-chain development we provide a Typescript client to help with gathering different accounts. See the README of that package to get started with it.

...
const marketName = "credix-marketplace";
const market = await client.fetchMarket(marketName);
 Our client can help you with finding the keys of following accounts
const globalMarketState = market.address;
const programState = (await client.fetchProgramState()).address;
const signingAuthority = (await market.generateSigningAuthorityPDA())[0];
const investorLpTokenAccount = await market.findLPTokenAccount(investor);
const investorTokenAccount = await market.findBaseTokenAccount(investor);
const liquidityPoolTokenAccount = await market.findLiquidityPoolTokenAccount();
const credixTreasury = (await client.fetchProgramState()).credixTreasury;
const credixTreasuryTokenAccount = await market.findBaseTokenAccount(credixTreasury);
const treasuryPoolTokenAccount = market.treasury;
const lpTokenMint = market.lpMintPK;
const credixPass = (await market.fetchCredixPass(investor)).address
const baseTokenMint = await market.baseMintPK;

// Withdraw Epoch related accounts
const market = await client.fetchMarket(marketName);
const withdrawEpochAddress = WithdrawEpoch.generatePDA(market, market.latestWithdrawEpochIdx);
...

Rust

This crate also provides functions to help generate PDA's. See the Rust docs

Disclaimer

These examples are provided as is. Do not blindly copy paste for use in production.

Dependencies

~21–30MB
~523K SLoC