Try   HackMD
tags: anchor study solana

Create an Anchor swap program

Use Anchor framework to create a swap smart contract in Solana blockchain

Before start, make sure the following pre-requisision is done

anchor-cli 0.19.0
cargo 1.58.0 (f01b232bc 2022-01-19)
rustc 1.58.1 (db9d1b20b 2022-01-20)

We also need to have typescript and node (>12) installed.

The account interaction in the very begining will be like below:

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

The AMM market is adapting the same mechanism of Uniswap's constant K value to decide the price of tokens.

Overall structure

As it might be known, Solana chain's program does not store state. All the state is stored in sperate accounts(files). So, We will need to define what type of data we might need to store and then create its corresponding accounts.

  1. Amm account

Firstly, we will need an account which represent an AMM pool in which there are all needed information. The most important data stored in this account are

​​​​- token A token account: in which all the token A amount of the pool is
​​​​- token B token account: in which all the token B amount of the pool is
​​​​- LP mint pubkey
​​​​- token A mint
​​​​- token B mint
​​​​- LP fee account: in which all the swap comission fee is 
​​​​- fees: A struct in which the fee types and amounts are defined 
​​​​- curve: Represent the AMM mathmatic calcualtion
  1. Swap Authority

This account is a PDA derived from Swap program. At the same time it is the authority of all the #1 Amm Account's assoicat-token-account.
- PDA of swap program
- Authority of Token A ATA
- Authority of Token B ATA
- Authority of LP fee account ATA
- Mint authority of LP mint
Through this Swap Authority account, Swap Program will be allowed to sign transaction to transfer Token A, TokenB to users who want to operate swap functions. At the same time, mint and burn the LP tokens when users liquidty tokens and withdrow liquidity.

  1. token A ATA
  2. token B ATA
  3. LP mint
  4. LP ATA (fee account)

By having all these concept of the needed accounts in mind, we will be able to design the very first instruction for the swap program

Insturction#1 - Initialize

We will leave the AMM curve and Fee calc for now and go for creating all related accounts to initialize a "LP POOL" first

State account structure

#[account]
pub struct Amm {
    /// Is the swap initialized, with data written to it
    pub is_initialized: bool,
    /// Bump seed used to generate the program address / authority
    pub bump_seed: u8,
    /// Token program ID associated with the swap
    pub token_program_id: Pubkey,
    /// Address of token A liquidity account
    pub token_a_account: Pubkey,
    /// Address of token B liquidity account
    pub token_b_account: Pubkey,
    /// Address of pool token mint
    pub pool_mint: Pubkey,
    /// Address of token A mint
    pub token_a_mint: Pubkey,
    /// Address of token B mint
    pub token_b_mint: Pubkey,
    /// Address of pool fee account
    pub pool_fee_account: Pubkey,
}

Input Accounts

​​​​- Accounts input structure

​​​​```
​​​​#[derive(Accounts)]
​​​​pub struct Initialize<'info> {
​​​​    // Swap authority: A PDA (seed: amm account's pubkey) to let program manipulate swap related features for all lp pools
​​​​    #[account(mut)]
​​​​    pub authority: AccountInfo<'info>,
​​​​    // pub amm: Box<Account<'info, Amm>>,
​​​​    #[account(mut, signer)]
​​​​    pub initializer: AccountInfo<'info>,
​​​​    #[account(init, payer=initializer, space=999)]
​​​​    pub amm: Box<Account<'info, Amm>>,

​​​​    #[account(mut)]
​​​​    pub pool_mint: Box<Account<'info, Mint>>,
​​​​    // amm's token A account
​​​​    #[account(mut)]
​​​​    pub token_a: Account<'info, TokenAccount>,
​​​​    // amm's token B account
​​​​    #[account(mut)]
​​​​    pub token_b: Account<'info, TokenAccount>,
​​​​    #[account(mut)]
​​​​    pub fee_account: Account<'info, TokenAccount>,
​​​​    // The LP token ATA to which the initial LP token is sent (Owner MUST be authority)
​​​​    #[account(mut)]
​​​​    pub destination: Account<'info, TokenAccount>,
​​​​    pub token_program: AccountInfo<'info>,
​​​​    pub system_program: Program<'info, System>,
​​​​}
​​​​```
​​​​- Instruction functions

​​​​This instruction has two functions defined as below. First one is to validate all the input account. The second one is to mint the LP token to the initalizer.
​​​​```
​​​​impl<'info> Initialize<'info> {
​​​​    fn validate_input_accounts(&self, swap_authority: Pubkey) -> Result<()> {
​​​​        if self.amm.is_initialized {
​​​​            return Err(error::SwapError::AlreadyInUse.into());
​​​​        }
​​​​        // Verify if input authority pubkey is valid
​​​​        if *self.authority.key != swap_authority {
​​​​            return Err(error::SwapError::InvalidProgramAddress.into());
​​​​        }
​​​​        if *self.authority.key != self.token_a.owner || *self.authority.key != self.token_b.owner {
​​​​            return Err(error::SwapError::InvalidOwner.into());
​​​​        }
​​​​        // Fee Account & Destination Account to which The initial LP token goes MUST be owned by Authority
​​​​        if *self.authority.key == self.fee_account.owner
​​​​            && *self.authority.key == self.destination.owner
​​​​        {
​​​​            return Err(error::SwapError::InvalidOutputOwner.into());
​​​​        }
​​​​        if COption::Some(*self.authority.key) != self.pool_mint.mint_authority {
​​​​            return Err(error::SwapError::InvalidOwner.into());
​​​​        }
​​​​        if self.token_a.mint == self.token_b.mint {
​​​​            return Err(error::SwapError::RepeatedMint.into());
​​​​        }
​​​​        // Amm's A token accounts MUST NOT have any delegation
​​​​        if self.token_a.delegate.is_some() || self.token_b.delegate.is_some() {
​​​​            return Err(error::SwapError::InvalidDelegate.into());
​​​​        }
​​​​        // Amm's B token accounts MUST NOT have Close Authority
​​​​        if self.token_a.close_authority.is_some() || self.token_b.close_authority.is_some() {
​​​​            return Err(error::SwapError::InvalidCloseAuthority.into());
​​​​        }
​​​​        // Amm's LP mint supply MUST be 0
​​​​        if self.pool_mint.supply != 0 {
​​​​            return Err(error::SwapError::InvalidSupply.into());
​​​​        }
​​​​        // Amm's LP mint MUST NOT have Freeze Authority
​​​​        if self.pool_mint.freeze_authority.is_some() {
​​​​            return Err(error::SwapError::InvalidFreezeAuthority.into());
​​​​        }
​​​​        // Amm's LP mint pubkey MUST be == input Fee Account's mint
​​​​        if *self.pool_mint.to_account_info().key != self.fee_account.mint {
​​​​            return Err(error::SwapError::IncorrectPoolMint.into());
​​​​        }
​​​​        Ok(())
​​​​    }

​​​​    fn mint_create_state_account(&mut self, bump_seed: u8) -> Result<()> {
​​​​        // concatenate swap_authority's seed & bump
​​​​        let seeds = &[&self.amm.to_account_info().key.to_bytes(), &[bump_seed][..]];

​​​​        // calc initial LP mint amt
​​​​        let initial_amount = 1 as u128;

​​​​        let mint_initial_amt_cpi_ctx = CpiContext::new(
​​​​            self.token_program.clone(),
​​​​            MintTo {
​​​​                mint: self.pool_mint.to_account_info().clone(),
​​​​                to: self.destination.to_account_info().clone(),
​​​​                authority: self.authority.clone(),
​​​​            },
​​​​        );

​​​​        token::mint_to(
​​​​            mint_initial_amt_cpi_ctx.with_signer(&[&seeds[..]]),
​​​​            u64::try_from(initial_amount).unwrap(),
​​​​        )?;

​​​​        let amm = &mut self.amm;
​​​​        amm.is_initialized = true;
​​​​        amm.bump_seed = bump_seed;
​​​​        amm.token_program_id = *self.token_program.key;
​​​​        amm.token_a_account = *self.token_a.to_account_info().key;
​​​​        amm.token_b_account = *self.token_b.to_account_info().key;
​​​​        amm.pool_mint = *self.pool_mint.to_account_info().key;
​​​​        amm.token_a_mint = self.token_a.mint;
​​​​        amm.token_b_mint = self.token_b.mint;
​​​​        amm.pool_fee_account = *self.fee_account.to_account_info().key;
​​​​        // amm.fees = fees_input;
​​​​        // amm.curve = curve_input;
​​​​        Ok(())
​​​​    }
​​​​}
​​​​```

processor

​​​​```
​​​​pub fn initialize(
​​​​    ctx: Context<Initialize>,
​​​​    // fees_input: FeesInput,
​​​​    // curve_input: CurveInput,
​​​​) -> Result<()> {

​​​​    let _ = &ctx.accounts.validate_input_accounts(swap_authority)?;

​​​​    let _ = &ctx.accounts.mint_create_state_account(bump_seed)?;

​​​​    Ok(())
​​​​}
​​​​```

By doing so, we can try test to initalize a new pool.

Part2 - AMM curve & Fees

In this section, we will add the AMM curve and fees into amm state account

We simply make use of the "curve" library provided by SPL.

We will create two sections to handle the fee section.

  1. FeeInput struct:
    which define the input data format to which frontend input arguments needs to follow.

    ​​​​#[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)]
    ​​​​pub struct FeesInput {
    ​​​​    pub trade_fee_numerator: u64,
    ​​​​    pub trade_fee_denominator: u64,
    ​​​​    pub owner_trade_fee_numerator: u64,
    ​​​​    pub owner_trade_fee_denominator: u64,
    ​​​​    pub owner_withdraw_fee_numerator: u64,
    ​​​​    pub owner_withdraw_fee_denominator: u64,
    ​​​​    pub host_fee_numerator: u64,
    ​​​​    pub host_fee_denominator: u64,
    ​​​​}
    
  2. A helper function to conver FeeInput struct to CurveFees
    CurveFees struct is also introduced by SPL's curve library. It provides many helper methods on top of it such as:

    • calculate_fee: calculating swap fee
    • owner_withdraw_fee: Calculate the withdraw fee in pool tokens
    • trading_fee: Calculate the trading fee in trading tokens
    • owner_trading_fee: Calculate the owner trading fee in trading tokens
    • host_fee: Calculate the host fee based on the owner fee, only used in production situations where a program is hosted by multiple frontends
    • validate: Validate that the fees are reasonable
    ​​​​pub fn build_fees(fees_input: &FeesInput) -> Result<CurveFees> {
    ​​​​    let fees = CurveFees {
    ​​​​        trade_fee_numerator: fees_input.trade_fee_numerator,
    ​​​​        trade_fee_denominator: fees_input.trade_fee_denominator,
    ​​​​        owner_trade_fee_numerator: fees_input.owner_trade_fee_numerator,
    ​​​​        owner_trade_fee_denominator: fees_input.owner_trade_fee_denominator,
    ​​​​        owner_withdraw_fee_numerator: fees_input.owner_withdraw_fee_numerator,
    ​​​​        owner_withdraw_fee_denominator: fees_input.owner_withdraw_fee_denominator,
    ​​​​        host_fee_numerator: fees_input.host_fee_numerator,
    ​​​​        host_fee_denominator: fees_input.host_fee_denominator,
    ​​​​    };
    ​​​​    Ok(fees)
    ​​​​}
    

Add AMM curve section

SPL's curve library also provides three SwapCurve types out of the box:

  • ConstantProduct: 0: Constant product curve, Uniswap-style
  • ConstantPrice: 1: Constant price curve, always X amount of A token for 1 B token, where X is defined at init
  • Offset: 3: Offset curve, like Uniswap, but with an additional offset on the token B side
  1. We will als create a Input curve struct to let pool creater to define the prefered type.

    ​​​​#[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)]
    ​​​​pub struct CurveInput {
    ​​​​    pub curve_type: u8,
    ​​​​    pub curve_parameters: u64,
    ​​​​}
    
  2. A helper function to conver CurveInput struct to SwapCurve
    Similarly, SPL Curve lib also intruduce the SwapCurve struct with many useful helper functions out of the box:

  • swap: Subtract fees and calculate how much destination token will be provided given an amount of source token.

  • deposit_single_token_type: Get the amount of pool tokens for the deposited amount of token A or B

  • withdraw_single_token_type_exact_out: Get the amount of pool tokens for the withdrawn amount of token A or B

    ​​​​pub fn build_curve(curve_input: &CurveInput) -> Result<SwapCurve> {
    ​​​​    let curve_type = CurveType::try_from(curve_input.curve_type).unwrap();
    ​​​​    let calculator: Box<dyn CurveCalculator> = match curve_type {
    ​​​​        CurveType::ConstantProduct => Box::new(ConstantProductCurve {}),
    ​​​​        CurveType::ConstantPrice => Box::new(ConstantPriceCurve {
    ​​​​            token_b_price: curve_input.curve_parameters,
    ​​​​        }),
    ​​​​        CurveType::Stable => Box::new(StableCurve {
    ​​​​            amp: curve_input.curve_parameters,
    ​​​​        }),
    ​​​​        CurveType::Offset => Box::new(OffsetCurve {
    ​​​​            token_b_offset: curve_input.curve_parameters,
    ​​​​        }),
    ​​​​    };
    ​​​​    let curve = SwapCurve {
    ​​​​        curve_type: curve_type,
    ​​​​        calculator: calculator,
    ​​​​    };
    ​​​​    Ok(curve)
    ​​​​}
    ​​​​...
    
    

Combine things together

  1. Add two more fields, fees and curve, into amm state account struct
  2. Add two input arguments, feesInput and curveInput, into the initialize functions
  3. impliment one method on top of Initalize instruction struct
    ​​​​
    ​​​​fn validate_amm_fees_and_curve(
    ​​​​    &self,
    ​​​​    fees_input: &FeesInput,
    ​​​​    curve_input: &CurveInput,
    ​​​​) -> Result<(SwapCurve)> {
    ​​​​    let curve = build_curve(curve_input).unwrap();
    ​​​​    curve
    ​​​​        .calculator
    ​​​​        .validate_supply(self.token_a.amount, self.token_b.amount)?;
    
    ​​​​    let fees = build_fees(fees_input)?;
    ​​​​    fees.validate()?;
    ​​​​    curve.calculator.validate()?;
    ​​​​    Ok((curve))
    ​​​​}
    
  4. Modify mint_create_state_account logic to include calcuating inital LP amount by SwapCurve
    ​​​​fn mint_create_state_account(
    ​​​​    ...
    ​​​​    curve_input: CurveInput,
    ​​​​    fees_input: FeesInput,
    ​​​​    curve: &SwapCurve,
    ​​​​) -> Result<()> {
    ​​​​    ...
    ​​​​    // Calc the inital LP token amt minted to initializer
    ​​​​    let initial_amount = curve.calculator.new_pool_supply();
    ​​​​    ...
    ​​​​    amm.fees = fees_input;
    ​​​​    amm.curve = curve_input;
    ​​​​    Ok(())
    ​​​​}
    
  5. Modify initalize processer function
    ​​​​pub fn initialize(
    ​​​​    ...
    ​​​​    fees_input: FeesInput,
    ​​​​    curve_input: CurveInput,
    ​​​​) -> Result<()> {
    ​​​​    ...
    ​​​​    let curve = &ctx
    ​​​​        .accounts
    ​​​​        .validate_amm_fees_and_curve(&fees_input, &curve_input)?;
    
    ​​​​    let _ =
    ​​​​        &ctx.accounts
    ​​​​            .mint_create_state_account(bump_seed, curve_input, fees_input, curve)?;
    
    ​​​​    Ok(())
    ​​​​}
    

Test section

  1. create two more data: feesInput and curveInput as input arguments of Initalize instruction

    ​​​​const fees_input: TypeDef<
    ​​​​  {
    ​​​​    name: "FeesInput";
    ​​​​    type: {
    ​​​​      kind: "struct";
    ​​​​      fields: [
    ​​​​        {
    ​​​​          name: "tradeFeeNumerator";
    ​​​​          type: "u64";
    ​​​​        },
    ​​​​        {
    ​​​​          name: "tradeFeeDenominator";
    ​​​​          type: "u64";
    ​​​​        },
    ​​​​        {
    ​​​​          name: "ownerTradeFeeNumerator";
    ​​​​          type: "u64";
    ​​​​        },
    ​​​​        {
    ​​​​          name: "ownerTradeFeeDenominator";
    ​​​​          type: "u64";
    ​​​​        },
    ​​​​        {
    ​​​​          name: "ownerWithdrawFeeNumerator";
    ​​​​          type: "u64";
    ​​​​        },
    ​​​​        {
    ​​​​          name: "ownerWithdrawFeeDenominator";
    ​​​​          type: "u64";
    ​​​​        },
    ​​​​        {
    ​​​​          name: "hostFeeNumerator";
    ​​​​          type: "u64";
    ​​​​        },
    ​​​​        {
    ​​​​          name: "hostFeeDenominator";
    ​​​​          type: "u64";
    ​​​​        }
    ​​​​      ];
    ​​​​    };
    ​​​​  },
    ​​​​  Record<string, number>
    ​​​​> = {
    ​​​​  tradeFeeNumerator: new anchor.BN(TRADING_FEE_NUMERATOR),
    ​​​​  tradeFeeDenominator: new anchor.BN(TRADING_FEE_DENOMINATOR),
    ​​​​  ownerTradeFeeNumerator: new anchor.BN(OWNER_TRADING_FEE_NUMERATOR),
    ​​​​  ownerTradeFeeDenominator: new anchor.BN(OWNER_TRADING_FEE_DENOMINATOR),
    ​​​​  ownerWithdrawFeeNumerator: new anchor.BN(OWNER_WITHDRAW_FEE_NUMERATOR),
    ​​​​  ownerWithdrawFeeDenominator: new anchor.BN(
    ​​​​    OWNER_WITHDRAW_FEE_DENOMINATOR
    ​​​​  ),
    ​​​​  hostFeeNumerator: new anchor.BN(HOST_FEE_NUMERATOR),
    ​​​​  hostFeeDenominator: new anchor.BN(HOST_FEE_DENOMINATOR),
    ​​​​};
    ​​​​const curve_input: TypeDef<
    ​​​​  {
    ​​​​    name: "CurveInput";
    ​​​​    type: {
    ​​​​      kind: "struct";
    ​​​​      fields: [
    ​​​​        {
    ​​​​          name: "curveType";
    ​​​​          type: "u8";
    ​​​​        },
    ​​​​        {
    ​​​​          name: "curveParameters";
    ​​​​          type: "u64";
    ​​​​        }
    ​​​​      ];
    ​​​​    };
    ​​​​  },
    ​​​​  Record<string, number | u64>
    ​​​​> = {
    ​​​​  curveType: CurveType.ConstantProduct,
    ​​​​  curveParameters: new anchor.BN(0),
    ​​​​};
    
  2. Mint some tokenA & tokenB to ATA by which the curve will be able to calcuation inital LP token amount

    ​​​​await mintA.mintTo(tokenAata, payer, [payer], 100);
    ​​​​await mintB.mintTo(tokenBata, payer, [payer], 100);
    

Run anchor test to see the test result. See the source code of this section herewith

Instruction#2 - Swap

Before start working on Swap program code. There are some important concepts:

  1. Overall picture: See the drawing below

  2. As can be seen, amm Curve handle the math and out put a SwapResult object like below:

    ​​​​pub struct SwapResult {
    ​​​​    /// New amount of source token
    ​​​​    pub new_swap_source_amount: u128,
    ​​​​    /// New amount of destination token
    ​​​​    pub new_swap_destination_amount: u128,
    ​​​​    /// Amount of source token swapped (includes fees)
    ​​​​    pub source_amount_swapped: u128,
    ​​​​    /// Amount of destination token swapped
    ​​​​    pub destination_amount_swapped: u128,
    ​​​​    /// Amount of source tokens going to pool holders
    ​​​​    pub trade_fee: u128,
    ​​​​    /// Amount of source tokens going to owner
    ​​​​    pub owner_fee: u128,
    ​​​​}
    

    It means,

    • After swap, the pool's source token should change to new_swap_source_amount
    • After swap, the pool's destination token should change to new_swap_destination_amount
    • The person who triggers the swap needs should transfer source_amount_swap to the pool's source token ata
    • The pool's destination token ata should transfer destination_amount_swapped to the person who triggers the swap.
    • The trade_fee is included in
    ​​​​/// Get the amount of pool tokens for the withdrawn amount of token A or B.
    ​​​​///
    ​​​​/// This is used for single-sided withdrawals and owner trade fee
    ​​​​/// calculation. It essentially performs a withdrawal followed by a swap.
    ​​​​/// Because a swap is implicitly performed, this will change the spot price
    ​​​​/// of the pool.
    ​​​​///
    ​​​​/// See more background for the calculation at:
    ​​​​///
    ​​​​/// <https://balancer.finance/whitepaper/#single-asset-deposit-withdrawal>
    ​​​​fn withdraw_single_token_type_exact_out(
    ​​​​    &self,
    ​​​​    source_amount: u128,
    ​​​​    swap_token_a_amount: u128,
    ​​​​    swap_token_b_amount: u128,
    ​​​​    pool_supply: u128,
    ​​​​    trade_direction: TradeDirection,
    ​​​​) -> Option<u128>;