Solana上的自动化做市商 AMM

本文详细介绍了自动化做市商(AMM)的基本概念、原理及其在Solana上的实现。文章深入探讨了AMM与传统订单簿的区别、流动性池的概念、恒定乘积公式(x*y=k)、滑点以及无常损失等关键概念。此外,还探讨了AMM面临的安全风险,如抢跑交易、价格操纵和闪电贷攻击,并提供了相应的缓解措施。最后,通过一个使用Anchor框架构建Solana AMM的实例,详细展示了AMM的协议设计、代码实现和测试过程。

一个自动做市商(AMM)是一种去中心化交易所协议,它使用算法和智能合约来促进数字资产的买卖,而不需要传统的订单簿。

AMM允许用户直接与流动性池进行交易,流动性池是由用户提供的代币储备,并由一个数学定价函数管理,而不是将买方与卖方进行匹配。

分解 DeFi 术语

订单簿

订单簿是中心化交易所(以及Solana上的Serum等一些链上系统)中使用的传统机制。

它是一个买卖订单的列表:

  • 买单(Bid):用户希望以某个价格购买资产。
  • 卖单(Ask):用户希望以某个价格出售资产。

市场匹配这些订单来执行交易。此模型需要:

  • 不断更新订单
  • 匹配引擎
  • 活跃的做市商

订单簿

流动性

流动性衡量的是一种资产在不引起显著价格影响的情况下,可以多容易地被买入或卖出。

  • 高流动性意味着价差小,滑点低。
  • 低流动性意味着更多的波动和交易中的高滑点。

流动性对于任何市场都至关重要。市场的流动性越高,用户就能获得越好的价格。

流动性池

流动性池是一个智能合约,它持有两种(或多种)代币的储备。这些储备用于促进交易。

在AMM中:

  • 用户(称为流动性提供者)存入代币对(如USDC/SOL)。
  • AMM使用一种定价算法(例如,恒定乘积)来确定兑换率。
  • 交易者针对池子交换一种代币以换取另一种代币,而不是与另一个人。

示例:你添加

  • 10 SOL
  • 1,000 USDC

该池现在允许其他用户根据储备交易SOL↔USDC。

AMM 对比 订单簿

订单簿 对比 AMM

恒定乘积公式

包括Uniswap V2Orca和早期版本的Raydium在内的大多数AMM的核心是一个简单但功能强大的方程式:

x * y = k

x * y = k

  • x: A代币的储备
  • y: B代币的储备
  • k: 常数(锁定的总流动性,不变)

该方程式确保每次交易后,代币储备的乘积保持不变。

这意味着什么?

如果交易者向资金池添加A代币(增加x),则他们必须移除B代币(减少y),以使x * y = k保持不变。

因此:

  • 你购买的某种代币越多,其价格就越高。
  • 这以算法方式模拟了供求关系

例子:

假设一个池有:

  • 10 SOL
  • 2,000 USDC

因此,k = 10 * 2000 = 20,000

你想购买1 SOL。

交易后,该池有9 SOL。为了保持k = 20,000,新的USDC余额必须为:

你必须添加2222.22 — 2000 = 222.22 USDC

  • 每个SOL支付的价格 = 222.22 USDC,而不是200 USDC
  • 这是由价格曲线引起的滑点

什么是滑点?

滑点是交易的预期价格实际执行价格之间的差额。

在AMM中,发生滑点是因为价格在你的交易期间发生变化,这是由于AMM定价算法的工作方式所致。

为什么AMM中会发生滑点?

在使用恒定乘积公式x * y = k的AMM中,每笔交易都会改变池的代币比率,从而改变价格

当你交易相对于池大小的大量资金时,你的交易会移动价格,最终你支付的费用(或收到的收益)高于交易开始时的报价。

滑点和交易规模

相对于池子而言,你的交易规模越大,你经历的滑点越多

小额交易几乎不会改变比率 → 滑点最小

大型交易会改变储备 → 滑点较高

这就是为什么更深的流动性(更大的池子)会减少滑点。

交易者如何管理滑点

大多数AMM界面都允许你设置滑点容忍度——例如:

  • 0.5%
  • 1%
  • 3%

滑点公式(近似值)

对于规模为xy的池中Δx代币的交换,滑点可以估计为:

滑点公式

无常损失

无常损失(IL)是当池中资产的价格与存入时相比发生变化时,流动性提供者(LP)经历的未实现损失

如果你只是持有你的代币而不是提供流动性,你将拥有更多的价值,这就是无常损失。

之所以称其为“无常”,是因为只有在提取时,损失才会变成永久性的。如果代币价格恢复到原始比率,则损失将消失。

为什么会发生IL?

当代币价格出现分歧时,AMM会通过自动出售上涨的代币并购买下跌的代币来重新平衡池,从而保持x * y = k不变。

因此:

  • 你最终持有的升值代币更少,并且
  • 贬值的代币更多

与仅持有两种代币相比,这种重新平衡会对你的总价值产生拖累。

例子

你是在Solana上使用恒定乘积AMM(例如,Orca,Raydium)的 SOL/USDC 池中的LP。

你存入:

  • 10 SOL,每个20美元→ 200美元
  • 200 USDC
  • 存入的总价值= 400美元
  • 当时,1 SOL = 20 USDC

价格变动:

后来,SOL的价格翻了一番,达到40美元。

由于AMM必须保持x * y = k,因此它会重新平衡:

  • 出售SOL并购买USDC以反映新的比率
  • 你的池份额现在持有较少的SOL和更多的USDC

你的新头寸是什么?

让我们计算一下价格变化之后,你的LP代币价值多少:

步骤1:新价格→ 1 SOL = 40 USDC → SOL/USDC比率变化

步骤2:AMM重新平衡;让我们假设完美的重新平衡

新的储备保持不变x * y = k

最初:x=10, y = 200 -> k = 2000

你现在拥有:

  • ~7.07 SOL
  • ~282.8 USDC
  • 总价值 = $ 565.6

与 HODL 相比

如果你只是持有:

  • 10 SOL,每个40美元= 400美元
  • 200 USDC
  • 总计= 600美元

你的LP头寸价值565.6美元,而不是600美元。无常损失 = 34.4美元

用百分比表示:

IL 百分比

安全风险

AMM虽然功能强大且去中心化,但具有固有的漏洞。以下是主要风险以及如何缓解这些风险的细分

抢先交易与MEV(矿工/验证者可提取价值)

它是什么?

攻击者(或验证者)在mempool中发现一笔有利可图的交易,并在你之前提交一笔类似的交易(通过支付更高的费用),从而从价格变动中获利。

例子:

你正在用大量的SOL兑换USDC。攻击者看到了这一点,并通过他们自己的SOL → USDC交易进行抢先交易,从而推高了价格。你的汇率会更差。

缓解措施:

  • 使用批量拍卖TWAP(时间加权平均价格)订单
  • 私人交易中继器(例如,以太坊的Flashbots)
  • 使用Solana的本地费用市场来隔离每个区块的交易影响

无常损失(修订版)

它是什么?

价格分歧时LP遭受的损失。上面已经介绍过,但请考虑:

缓解措施:

  • 使用集中流动性范围(Uniswap V3 / Orca Whirlpools)
  • 选择低波动性对(例如,USDC–DAI,ETH–stETH)
  • 叠加协议激励措施以抵消IL(流动性挖矿)

价格操纵

它是什么?

AMM根据自己的池比率确定价格。小型或流动性不足的池可能会被操纵——攻击者会抬高价格以利用借贷平台或预言机。

缓解措施:

  • TWAP预言机(来自AMM的时间加权平均价格)
  • 使用外部预言机数据馈送(例如,Solana上的Pyth)
  • 限制在关键计算中AMM现货价格的使用

闪电贷攻击

它是什么?

攻击者在单笔交易(闪电贷)中借入大量资金,暂时操纵AMM价格,并从套利或清算中获利。

缓解措施:

  • 要求LP在池中停留最短时间
  • 对借贷平台使用基于预言机的定价
  • 监控异常交易量/峰值并应用速率限制

使用Anchor构建你的第一个 Solana AMM

现在我们已经探讨了AMM的理论和经济学原理,让我们深入了解如何使用Anchor构建一个

前提条件

在开始编码之前,请确保你已具备:

  • 安装了Rust和Cargo
  • Solana CLI 工具
  • Anchor 框架 ( cargo install --git https://github.com/coral-xyz/anchor avm --locked --force)
  • 对Solana帐户和PDA的基本了解

项目设置

首先,让我们创建一个新的Anchor项目:

anchor init anchor_amm
cd anchor_amm

协议设计

AMM 设计

定义我们的 AMM 配置

首先,让我们定义我们的 amm 需要的核心帐户结构。打开 programs/anchor_amm/src/states/mod.rs 并添加:

use anchor_lang::prelude::*;

##[account]
##[derive(InitSpace)]
pub struct Config {
    pub authority: Option<Pubkey>,
    pub mint_x: Pubkey,
    pub mint_y: Pubkey,
    pub fee: u16,
    pub config_bump: u8,
    pub lp_bump: u8,
}

指令 1:初始化配置

现在让我们创建我们的第一个指令来初始化 AMM。 创建 programs/anchor_amm/src/instructions/initialize.rs

use anchor_lang::prelude::*;
use anchor_spl::{
    associated_token::AssociatedToken,
    token::{Mint, Token, TokenAccount},
};

use crate::states::Config;

##[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(mut)]
    pub admin: Signer<'info>,

    pub mint_x: Account<'info, Mint>,
    pub mint_y: Account<'info, Mint>,

    #[account(\
        init,\
        payer = admin,\
        seeds = [b"config"],\
        bump,\
        space = Config::INIT_SPACE\
    )]
    pub config: Account<'info, Config>,

    #[account(\
        init,\
        payer = admin,\
        mint::decimals = 6,\
        mint::authority = config.key(),\
        seeds = [b"lp", config.key().as_ref()],\
        bump,\
    )]
    pub mint_lp: Account<'info, Mint>,

    #[account(\
        init,\
        payer = admin,\
        associated_token::mint = mint_x,\
        associated_token::authority = config,\
        associated_token::token_program = token_program,\
    )]
    pub vault_x: Account<'info, TokenAccount>,

    #[account(\
        init,\
        payer = admin,\
        associated_token::mint = mint_y,\
        associated_token::authority = config,\
        associated_token::token_program = token_program,\
    )]
    pub vault_y: Account<'info, TokenAccount>,

    pub token_program: Program<'info, Token>,
    pub associated_token_program: Program<'info, AssociatedToken>,
    pub system_program: Program<'info, System>,
}

impl<'info> Initialize<'info> {
    pub fn initialize(
        &mut self,
        fee: u16,
        authority: Option<Pubkey>,
        bumps: &InitializeBumps,
    ) -> Result<()> {
        self.config.set_inner(Config {
            authority,
            mint_x: self.mint_x.key(),
            mint_y: self.mint_y.key(),
            fee,
            config_bump: bumps.config,
            lp_bump: bumps.mint_lp,
        });

        Ok(())
    }
}

要点:

  • 我们创建将存储 AMM 参数的主配置 PDA
  • LP 代币 mint 是使用 config 作为其 authority 创建的
  • 创建两个 vault 帐户以持有实际代币
  • 我们使用 set_inner() 一次初始化所有配置字段

指令 2:存款(添加流动性)

创建 programs/anchor_amm/src/instructions/deposit.rs

use anchor_lang::prelude::*;
use anchor_spl::{
    associated_token::AssociatedToken,
    token::{mint_to, transfer_checked, Mint, MintTo, Token, TokenAccount, TransferChecked},
};
use constant_product_curve::ConstantProduct;

use crate::errors::AmmError;
use crate::states::Config;

##[derive(Accounts)]
pub struct Deposit<'info> {
    #[account(mut)]
    pub user: Signer<'info>,

    #[account(mint::token_program = token_program)]
    pub mint_x: Account<'info, Mint>,

    #[account(mint::token_program = token_program)]
    pub mint_y: Account<'info, Mint>,

    #[account(\
        seeds = [b"config"],\
        bump = config.config_bump,\
        has_one = mint_x,\
        has_one = mint_y,\
    )]
    pub config: Account<'info, Config>,

    #[account(\
        mut,\
        seeds = [b"lp", config.key().as_ref()],\
        bump = config.lp_bump\
    )]
    pub mint_lp: Account<'info, Mint>,

    #[account(\
        mut,\
        associated_token::mint = mint_x,\
        associated_token::authority = config,\
        associated_token::token_program = token_program,\
    )]
    pub vault_x: Account<'info, TokenAccount>,

    #[account(\
        mut,\
        associated_token::mint = mint_y,\
        associated_token::authority = config,\
        associated_token::token_program = token_program,\
    )]
    pub vault_y: Account<'info, TokenAccount>,

    #[account(\
        mut,\
        associated_token::mint = mint_x,\
        associated_token::authority = user,\
        associated_token::token_program = token_program\
    )]
    pub user_token_account_x: Account<'info, TokenAccount>,

    #[account(\
        mut,\
        associated_token::mint = mint_y,\
        associated_token::authority = user,\
        associated_token::token_program = token_program\
    )]
    pub user_token_account_y: Account<'info, TokenAccount>,

    #[account(\
        init_if_needed,\
        payer = user,\
        associated_token::mint = mint_lp,\
        associated_token::authority = user,\
        associated_token::token_program = token_program\
    )]
    pub user_token_account_lp: Account<'info, TokenAccount>,

    pub token_program: Program<'info, Token>,
    pub associated_token_program: Program<'info, AssociatedToken>,
    pub system_program: Program<'info, System>,
}

impl<'info> Deposit<'info> {
    pub fn deposit(&mut self, amount: u64, max_x: u64, max_y: u64) -> Result<()> {
        require!(amount > 0, AmmError::InvalidAmount);

        // Calculate deposit amounts based on current pool ratio
        // 根据当前池比率计算存款金额
        let (x, y) = if self.mint_lp.supply == 0 {
            // First deposit - accept provided maximums
            // 首次存款 - 接受提供的最大值
            (max_x, max_y)
        } else {
            // Calculate proportional amounts based on current pool
            // 根据当前池计算成比例的金额
            let amounts = ConstantProduct::xy_deposit_amounts_from_l(
                self.vault_x.amount,
                self.vault_y.amount,
                self.mint_lp.supply,
                amount,
                6, // rounding precision
                   // 舍入精度
            )
            .unwrap();
            (amounts.x, amounts.y)
        };

        // Check slippage protection
        // 检查滑点保护
        require!(x <= max_x && y <= max_y, AmmError::SlippageExceeded);

        // Transfer tokens from user to vaults
        // 将代币从用户转移到 vault
        self.deposit_token_x(x)?;
        self.deposit_token_y(y)?;

        // Mint LP tokens to user
        // 将 LP 代币 mint 给用户
        self.mint_lp_tokens(amount)?;

        Ok(())
    }

    fn deposit_token_x(&mut self, amount: u64) -> Result<()> {
        let cpi_program = self.token_program.to_account_info();
        let cpi_accounts = TransferChecked {
            from: self.user_token_account_x.to_account_info(),
            to: self.vault_x.to_account_info(),
            mint: self.mint_x.to_account_info(),
            authority: self.user.to_account_info(),
        };

        let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);
        transfer_checked(cpi_ctx, amount, self.mint_x.decimals)
    }

    fn deposit_token_y(&mut self, amount: u64) -> Result<()> {
        let cpi_program = self.token_program.to_account_info();
        let cpi_accounts = TransferChecked {
            from: self.user_token_account_y.to_account_info(),
            to: self.vault_y.to_account_info(),
            mint: self.mint_y.to_account_info(),
            authority: self.user.to_account_info(),
        };

        let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);
        transfer_checked(cpi_ctx, amount, self.mint_y.decimals)
    }

    fn mint_lp_tokens(&mut self, amount: u64) -> Result<()> {
        let cpi_program = self.token_program.to_account_info();
        let cpi_accounts = MintTo {
            mint: self.mint_lp.to_account_info(),
            to: self.user_token_account_lp.to_account_info(),
            authority: self.config.to_account_info(),
        };

        let signer_seeds: &[&[&[u8]]] = &[&[\
            b"config",\
            &[self.config.config_bump],\
        ]];

        let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, signer_seeds);
        mint_to(cpi_ctx, amount)
    }
}

要点:

  • 对于首次存款,我们使用提供的最大金额
  • 对于后续存款,我们计算成比例的金额以保持池比率
  • 滑点保护确保用户不会存入超过其最大金额
  • 将 LP 代币 mint 以表示用户在池中的份额

指令 3:提款(移除流动性)

use anchor_lang::prelude::*;
use anchor_spl::{
    associated_token::AssociatedToken,
    token::{burn, transfer_checked, Burn, Mint, Token, TokenAccount, TransferChecked},
};
use constant_product_curve::ConstantProduct;

use crate::errors::AmmError;
use crate::states::Config;

##[derive(Accounts)]
pub struct Withdraw<'info> {
    #[account(mut)]
    pub user: Signer<'info>,

    #[account(mint::token_program = token_program)]
    pub mint_x: Account<'info, Mint>,

    #[account(mint::token_program = token_program)]
    pub mint_y: Account<'info, Mint>,

    #[account(\
        seeds = [b"config"],\
        bump = config.config_bump,\
        has_one = mint_x,\
        has_one = mint_y,\
    )]
    pub config: Account<'info, Config>,

    #[account(\
        mut,\
        seeds = [b"lp", config.key().as_ref()],\
        bump = config.lp_bump\
    )]
    pub mint_lp: Account<'info, Mint>,

    #[account(\
        mut,\
        associated_token::mint = mint_x,\
        associated_token::authority = config,\
        associated_token::token_program = token_program,\
    )]
    pub vault_x: Account<'info, TokenAccount>,

    #[account(\
        mut,\
        associated_token::mint = mint_y,\
        associated_token::authority = config,\
        associated_token::token_program = token_program,\
    )]
    pub vault_y: Account<'info, TokenAccount>,

    #[account(\
        mut,\
        associated_token::mint = mint_x,\
        associated_token::authority = user,\
        associated_token::token_program = token_program\
    )]
    pub user_token_account_x: Account<'info, TokenAccount>,

    #[account(\
        mut,\
        associated_token::mint = mint_y,\
        associated_token::authority = user,\
        associated_token::token_program = token_program\
    )]
    pub user_token_account_y: Account<'info, TokenAccount>,

    #[account(\
        mut,\
        associated_token::mint = mint_lp,\
        associated_token::authority = user,\
        associated_token::token_program = token_program\
    )]
    pub user_token_account_lp: Account<'info, TokenAccount>,

    pub token_program: Program<'info, Token>,
    pub associated_token_program: Program<'info, AssociatedToken>,
    pub system_program: Program<'info, System>,
}

impl<'info> Withdraw<'info> {
    pub fn withdraw(&mut self, amount: u64, min_x: u64, min_y: u64) -> Result<()> {
        require!(amount != 0, AmmError::InvalidAmount);

        let (x, y) = match self.mint_lp.supply == 0
            && self.vault_x.amount == 0
            && self.vault_y.amount == 0
        {
            true => (min_x, min_y),
            false => {
                let amounts = ConstantProduct::xy_withdraw_amounts_from_l(
                    self.vault_x.amount,
                    self.vault_y.amount,
                    self.mint_lp.supply,
                    amount,
                    6,
                ).unwrap();
                (amounts.x, amounts.y)
            }
        };

        require!(x >= min_x && y >= min_y, AmmError::SlippageExceeded);

        self.burn_lp_tokens(amount)?;
        self.withdraw_tokens(x, true)?;
        self.withdraw_tokens(y, false)
    }

    pub fn burn_lp_tokens(&mut self, amount: u64) -> Result<()> {
        let cpi_program = self.token_program.to_account_info();
        let cpi_accounts = Burn {
            mint: self.mint_lp.to_account_info(),
            from: self.user_token_account_lp.to_account_info(),
            authority: self.user.to_account_info(),
        };

        let signer_seeds: &[&[&[u8]]] = &[&[\
            b"config",\
            &[self.config.config_bump],\
        ]];

        let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, signer_seeds);
        burn(cpi_ctx, amount)
    }

    pub fn withdraw_tokens(&mut self, amount: u64, is_x: bool) -> Result<()> {
        let (from, to, mint, decimals) = match is_x {
            true => (
                self.vault_x.to_account_info(),
                self.user_token_account_x.to_account_info(),
                self.mint_x.to_account_info(),
                self.mint_x.decimals,
            ),
            false => (
                self.vault_y.to_account_info(),
                self.user_token_account_y.to_account_info(),
                self.mint_y.to_account_info(),
                self.mint_y.decimals,
            ),
        };

        let cpi_program = self.token_program.to_account_info();
        let cpi_accounts = TransferChecked {
            from,
            to,
            mint,
            authority: self.config.to_account_info(),
        };

        let signer_seeds: &[&[&[u8]]] = &[&[\
            b"config",\
            &[self.config.config_bump],\
        ]];

        let cpi_context = CpiContext::new_with_signer(cpi_program, cpi_accounts, signer_seeds);
        transfer_checked(cpi_context, amount, decimals)
    }
}

要点:

  • 烧毁用户的 LP 代币
  • 使用恒定乘积曲线计算成比例的提款金额
  • 将代币从 vault 转回给用户
  • 包括最低金额的滑点保护

指令 4:交换

use anchor_lang::prelude::*;
use anchor_spl::{
    associated_token::AssociatedToken,
    token::{transfer_checked, Mint, Token, TokenAccount, TransferChecked},
};
use constant_product_curve::{ConstantProduct, LiquidityPair};

use crate::errors::AmmError;
use crate::states::Config;

##[derive(Accounts)]
pub struct Swap<'info> {
    #[account(mut)]
    pub user: Signer<'info>,

    #[account(mint::token_program = token_program)]
    pub mint_x: Account<'info, Mint>,

    #[account(mint::token_program = token_program)]
    pub mint_y: Account<'info, Mint>,

    #[account(\
        seeds = [b"config"],\
        bump = config.config_bump,\
        has_one = mint_x,\
        has_one = mint_y,\
    )]
    pub config: Account<'info, Config>,

    #[account(\
        seeds = [b"lp", config.key().as_ref()],\
        bump = config.lp_bump\
    )]
    pub mint_lp: Account<'info, Mint>,

    #[account(\
        mut,\
        associated_token::mint = mint_x,\
        associated_token::authority = config,\
        associated_token::token_program = token_program,\
    )]
    pub vault_x: Account<'info, TokenAccount>,

    #[account(\
        mut,\
        associated_token::mint = mint_y,\
        associated_token::authority = config,\
        associated_token::token_program = token_program,\
    )]
    pub vault_y: Account<'info, TokenAccount>,

    #[account(\
        init_if_needed,\
        payer = user,\
        associated_token::mint = mint_x,\
        associated_token::authority = user,\
        associated_token::token_program = token_program\
    )]
    pub user_token_account_x: Account<'info, TokenAccount>,

    #[account(\
        init_if_needed,\
        payer = user,\
        associated_token::mint = mint_y,\
        associated_token::authority = user,\
        associated_token::token_program = token_program\
    )]
    pub user_token_account_y: Account<'info, TokenAccount>,

    pub token_program: Program<'info, Token>,
    pub associated_token_program: Program<'info, AssociatedToken>,
    pub system_program: Program<'info, System>,
}

impl<'info> Swap<'info> {
    pub fn swap(&mut self, is_x: bool, amount_in: u64, min_amount_out: u64) -> Result<()> {
        require!(amount_in > 0, AmmError::InvalidAmount);

        let mut curve = ConstantProduct::init(
            self.vault_x.amount,
            self.vault_y.amount,
            self.mint_lp.supply,
            self.config.fee,
            None,
        )
        .map_err(AmmError::from)?;

        let p = if is_x {
            LiquidityPair::X
        } else {
            LiquidityPair::Y
        };

        let swap_result = curve
            .swap(p, amount_in, min_amount_out)
            .map_err(AmmError::from)?;

        require!(swap_result.deposit != 0, AmmError::InvalidAmount);
        require!(swap_result.withdraw != 0, AmmError::InvalidAmount);

        self.deposit_token(is_x, swap_result.deposit)?;
        self.withdraw_token(!is_x, swap_result.withdraw)?;

        Ok(())
    }

    pub fn deposit_token(&mut self, is_x: bool, amount: u64) -> Result<()> {
        let (from, to, mint, decimals) = if is_x {
            (
                self.user_token_account_x.to_account_info(),
                self.vault_x.to_account_info(),
                self.mint_x.to_account_info(),
                self.mint_x.decimals,
            )
        } else {
            (
                self.user_token_account_y.to_account_info(),
                self.vault_y.to_account_info(),
                self.mint_y.to_account_info(),
                self.mint_y.decimals,
            )
        };

        let cpi_program = self.token_program.to_account_info();
        let cpi_accounts = TransferChecked {
            from,
            to,
            authority: self.user.to_account_info(),
            mint,
        };

        let cpi_context = CpiContext::new(cpi_program, cpi_accounts);
        transfer_checked(cpi_context, amount, decimals)
    }

    pub fn withdraw_token(&mut self, is_x: bool, amount: u64) -> Result<()> {
        let (from, to, mint, decimals) = if is_x {
            (
                self.vault_x.to_account_info(),
                self.user_token_account_x.to_account_info(),
                self.mint_x.to_account_info(),
                self.mint_x.decimals,
            )
        } else {
            (
                self.vault_y.to_account_info(),
                self.user_token_account_y.to_account_info(),
                self.mint_y.to_account_info(),
                self.mint_y.decimals,
            )
        };

        let cpi_program = self.token_program.to_account_info();
        let cpi_accounts = TransferChecked {
            from,
            to,
            mint,
            authority: self.config.to_account_info(),
        };

        let signer_seeds: &[&[&[u8]]] = &[&[\
            b"config",\
            &[self.config.config_bump],\
        ]];

        let cpi_context = CpiContext::new_with_signer(cpi_program, cpi_accounts, signer_seeds);
        transfer_checked(cpi_context, amount, decimals)
    }
}

要点:

  • 使用恒定乘积曲线来计算交换金额
  • 包括滑点保护
  • 存``` pub fn swap( ctx: Context<Swap>, is_x: bool, amount_in: u64, min_amount_out: u64 ) -> Result<()> { ctx.accounts.swap(is_x, amount_in, min_amount_out) } }

模块结构

创建 programs/anchor_amm/src/instructions/mod.rs:

pub mod initialize;
pub mod deposit;
pub mod withdraw;
pub mod swap;

pub use initialize::*;
pub use deposit::*;
pub use withdraw::*;
pub use swap::*;

测试你的 AMM

创建一个测试文件 test/anchor_amm.ts 来验证你的 AMM 是否正常工作:

import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { Amm } from "../target/types/amm";
import {
  createMint,
  getOrCreateAssociatedTokenAccount,
  mintTo,
  getAssociatedTokenAddress,
} from "@solana/spl-token";

describe("amm initialize", () => {
  const provider = anchor.AnchorProvider.env();
  anchor.setProvider(provider);

  const program = anchor.workspace.amm as Program&lt;Amm>;
  const connection = provider.connection;
  const admin = provider.wallet;

  let mintX: anchor.web3.PublicKey;
  let mintY: anchor.web3.PublicKey;
  let mintLp: anchor.web3.PublicKey;

  let configPda: anchor.web3.PublicKey;
  let vaultX: anchor.web3.PublicKey;
  let vaultY: anchor.web3.PublicKey;

  let userAtaX: anchor.web3.PublicKey;
  let userAtaY: anchor.web3.PublicKey;
  let userLpAta: anchor.web3.PublicKey;

  const seed = new anchor.BN(42);
  const fee = 30;

  it("Initializes the AMM pool", async () => {
    mintX = await createMint(connection, admin.payer, admin.publicKey, null, 6);
    mintY = await createMint(connection, admin.payer, admin.publicKey, null, 6);

    [configPda] = anchor.web3.PublicKey.findProgramAddressSync(
      [Buffer.from("config"), seed.toArrayLike(Buffer, "le", 8)],
      program.programId
    );

    [mintLp] = anchor.web3.PublicKey.findProgramAddressSync(
      [Buffer.from("lp"), configPda.toBuffer()],
      program.programId
    );

    vaultX = await getAssociatedTokenAddress(mintX, configPda, true);
    vaultY = await getAssociatedTokenAddress(mintY, configPda, true);

    const tx = await program.methods
      .initialize(fee, null)
      .accountsPartial({
        admin: admin.publicKey,
        mintX,
        mintY,
        config: configPda,
        mintLp,
        vaultX,
        vaultY,
        tokenProgram: anchor.utils.token.TOKEN_PROGRAM_ID,
        associatedTokenProgram: anchor.utils.token.ASSOCIATED_PROGRAM_ID,
        systemProgram: anchor.web3.SystemProgram.programId,
      })
      .rpc();
    console.log(`https://explorer.solana.com/tx/${tx}?cluster=devnet`);

    console.log("✅ AMM initialized successfully");

    // Create user token accounts and mint initial balances
    const ataX = await getOrCreateAssociatedTokenAccount(connection, admin.payer, mintX, admin.publicKey);
    const ataY = await getOrCreateAssociatedTokenAccount(connection, admin.payer, mintY, admin.publicKey);
    const ataLp = await getOrCreateAssociatedTokenAccount(connection, admin.payer, mintLp, admin.publicKey);

    await mintTo(connection, admin.payer, mintX, ataX.address, admin.payer, 1_000_000);
    await mintTo(connection, admin.payer, mintY, ataY.address, admin.payer, 1_000_000);

    userAtaX = ataX.address;
    userAtaY = ataY.address;
    userLpAta = ataLp.address;
  });

  it("Deposits liquidity into the pool", async () => {
    const depositAmount = new anchor.BN(1_000_000);
    const maxX = new anchor.BN(500_000);
    const maxY = new anchor.BN(500_000);

    const tx = await program.methods
      .deposit(depositAmount, maxX, maxY)
      .accountsPartial({
        user: admin.publicKey,
        userTokenAccountX: userAtaX,
        userTokenAccountY: userAtaY,
        userTokenAccountLp: userLpAta,
        config: configPda,
        vaultX,
        vaultY,
        mintLp,
        tokenProgram: anchor.utils.token.TOKEN_PROGRAM_ID,
      })
      .rpc();

    console.log(`https://explorer.solana.com/tx/${tx}?cluster=devnet`);
    console.log("✅ Deposited liquidity");
  });

  it("Withdraws liquidity from the pool", async () => {
    const withdrawAmount = new anchor.BN(500_000);
    const minX = new anchor.BN(100_000);
    const minY = new anchor.BN(100_000);

    const tx = await program.methods
      .withdraw(withdrawAmount, minX, minY)
      .accountsPartial({
        user: admin.publicKey,
        userTokenAccountX: userAtaX,
        userTokenAccountY: userAtaY,
        userTokenAccountLp: userLpAta,
        config: configPda,
        vaultX,
        vaultY,
        mintLp,
        tokenProgram: anchor.utils.token.TOKEN_PROGRAM_ID,
      })
      .signers([admin.payer])
      .rpc();

    console.log(`https://explorer.solana.com/tx/${tx}?cluster=devnet`);
    console.log("✅ Withdrawn liquidity");
  });

  it("Swaps token X for token Y", async () => {
    const amountIn = new anchor.BN(100_000);
    const minOut = new anchor.BN(50_000);

    const tx = await program.methods
      .swap(true, amountIn, minOut)
      .accountsPartial({
        user: admin.publicKey,
        mintX,
        mintY,
        userTokenAccountX: userAtaX,
        userTokenAccountY: userAtaY,
        config: configPda,
        vaultX,
        vaultY,
        mintLp,
        tokenProgram: anchor.utils.token.TOKEN_PROGRAM_ID,
        associatedTokenProgram: anchor.utils.token.ASSOCIATED_PROGRAM_ID,
        systemProgram: anchor.web3.SystemProgram.programId,
      })
      .rpc();

    console.log(`https://explorer.solana.com/tx/${tx}?cluster=devnet`);
    console.log("✅ Swapped X for Y");
  });
});

运行测试

  • 构建程序:
anchor build
  • 运行测试:
anchor test

主要收获

PDA 使用: 我们将 PDA 用于配置账户、LP 铸币 权限和金库权限,确保确定性地址和安全的程序控制

跨程序调用: 我们通过 CPI 调用与 SPL Token 程序交互,进行所有 token 转移、铸币销毁

账户验证: Anchor 的约束系统,结合种子(seeds)、bump 和 has_one 验证,确保我们正在使用正确的账户

数学定价: 我们实现了恒定乘积公式 (x * y = k) ,用于自动价格发现和 swap 计算

错误处理: 自定义错误为滑点限制、无效数量和流动性不足的情况提供清晰的反馈

结论

AMM 是 Solana 上去中心化交易的基础模式。我们已经探索了如何构建一个完整的自动化做市商,包括流动性提供、token swap 和费用收集,所有这些都使用 Anchor。

从这里开始,你可以扩展到更高级的用例,如集中流动性范围、多个费用层级、治理 token 奖励、闪电贷集成和预言机价格馈送。这些模式使 AMM 成为 DeFi 中现实世界应用的强大工具,从简单的 token 交换到复杂的金融衍生品,无所不能。

如果你对去中心化基础设施、链上数据系统或使用预言机网络构建真实世界项目感兴趣,请关注:

  • 原文链接: blog.blockmagnates.com/a...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
blockmagnates
blockmagnates
The New Crypto Publication on The Block