anchor
study
solana
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:
The AMM market is adapting the same mechanism of Uniswap's constant K value to decide the price of tokens.
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.
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
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.
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
We will leave the AMM curve and Fee calc for now and go for creating all related accounts to initialize a "LP POOL" first
#[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,
}
- 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(())
}
}
```
```
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.
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.
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,
}
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:
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)
}
SPL's curve library also provides three SwapCurve types out of the box:
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,
}
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)
}
...
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))
}
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(())
}
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(())
}
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),
};
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
Before start working on Swap program code. There are some important concepts:
Overall picture: See the drawing below
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,
new_swap_source_amount
new_swap_destination_amount
source_amount_swap
to the pool's source token atadestination_amount_swapped
to the person who triggers the swap.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>;