本文详细介绍了自动化做市商(AMM)的基本概念、原理及其在Solana上的实现。文章深入探讨了AMM与传统订单簿的区别、流动性池的概念、恒定乘积公式(x*y=k)、滑点以及无常损失等关键概念。此外,还探讨了AMM面临的安全风险,如抢跑交易、价格操纵和闪电贷攻击,并提供了相应的缓解措施。最后,通过一个使用Anchor框架构建Solana AMM的实例,详细展示了AMM的协议设计、代码实现和测试过程。
一个自动做市商(AMM)是一种去中心化交易所协议,它使用算法和智能合约来促进数字资产的买卖,而不需要传统的订单簿。
AMM允许用户直接与流动性池进行交易,流动性池是由用户提供的代币储备,并由一个数学定价函数管理,而不是将买方与卖方进行匹配。
订单簿是中心化交易所(以及Solana上的Serum等一些链上系统)中使用的传统机制。
它是一个买卖订单的列表:
市场匹配这些订单来执行交易。此模型需要:
订单簿
流动性衡量的是一种资产在不引起显著价格影响的情况下,可以多容易地被买入或卖出。
流动性对于任何市场都至关重要。市场的流动性越高,用户就能获得越好的价格。
流动性池是一个智能合约,它持有两种(或多种)代币的储备。这些储备用于促进交易。
在AMM中:
示例:你添加
该池现在允许其他用户根据储备交易SOL↔USDC。
订单簿 对比 AMM
包括Uniswap V2,Orca和早期版本的Raydium在内的大多数AMM的核心是一个简单但功能强大的方程式:
x * y = k
该方程式确保每次交易后,代币储备的乘积保持不变。
如果交易者向资金池添加A代币(增加x
),则他们必须移除B代币(减少y
),以使x * y = k
保持不变。
因此:
假设一个池有:
因此,k = 10 * 2000 = 20,000
你想购买1 SOL。
交易后,该池有9 SOL
。为了保持k = 20,000
,新的USDC余额必须为:
你必须添加2222.22 — 2000 = 222.22 USDC
滑点是交易的预期价格与实际执行价格之间的差额。
在AMM中,发生滑点是因为价格在你的交易期间发生变化,这是由于AMM定价算法的工作方式所致。
在使用恒定乘积公式x * y = k
的AMM中,每笔交易都会改变池的代币比率,从而改变价格。
当你交易相对于池大小的大量资金时,你的交易会移动价格,最终你支付的费用(或收到的收益)高于交易开始时的报价。
相对于池子而言,你的交易规模越大,你经历的滑点就越多。
小额交易几乎不会改变比率 → 滑点最小
大型交易会改变储备 → 滑点较高
这就是为什么更深的流动性(更大的池子)会减少滑点。
大多数AMM界面都允许你设置滑点容忍度——例如:
对于规模为x
和y
的池中Δx
代币的交换,滑点可以估计为:
滑点公式
无常损失(IL)是当池中资产的价格与存入时相比发生变化时,流动性提供者(LP)经历的未实现损失。
如果你只是持有你的代币而不是提供流动性,你将拥有更多的价值,这就是无常损失。
之所以称其为“无常”,是因为只有在提取时,损失才会变成永久性的。如果代币价格恢复到原始比率,则损失将消失。
当代币价格出现分歧时,AMM会通过自动出售上涨的代币并购买下跌的代币来重新平衡池,从而保持x * y = k
不变。
因此:
与仅持有两种代币相比,这种重新平衡会对你的总价值产生拖累。
你是在Solana上使用恒定乘积AMM(例如,Orca,Raydium)的 SOL/USDC 池中的LP。
你存入:
价格变动:
后来,SOL的价格翻了一番,达到40美元。
由于AMM必须保持x * y = k
,因此它会重新平衡:
你的新头寸是什么?
让我们计算一下价格变化之后,你的LP代币价值多少:
步骤1:新价格→ 1 SOL = 40 USDC → SOL/USDC比率变化
步骤2:AMM重新平衡;让我们假设完美的重新平衡
新的储备保持不变x * y = k
。
最初:x=10, y = 200 -> k = 2000
你现在拥有:
与 HODL 相比
如果你只是持有:
你的LP头寸价值565.6美元,而不是600美元。无常损失 = 34.4美元
用百分比表示:
IL 百分比
AMM虽然功能强大且去中心化,但具有固有的漏洞。以下是主要风险以及如何缓解这些风险的细分
它是什么?
攻击者(或验证者)在mempool中发现一笔有利可图的交易,并在你之前提交一笔类似的交易(通过支付更高的费用),从而从价格变动中获利。
例子:
你正在用大量的SOL兑换USDC。攻击者看到了这一点,并通过他们自己的SOL → USDC交易进行抢先交易,从而推高了价格。你的汇率会更差。
缓解措施:
它是什么?
价格分歧时LP遭受的损失。上面已经介绍过,但请考虑:
缓解措施:
它是什么?
AMM根据自己的池比率确定价格。小型或流动性不足的池可能会被操纵——攻击者会抬高价格以利用借贷平台或预言机。
缓解措施:
它是什么?
攻击者在单笔交易(闪电贷)中借入大量资金,暂时操纵AMM价格,并从套利或清算中获利。
缓解措施:
现在我们已经探讨了AMM的理论和经济学原理,让我们深入了解如何使用Anchor构建一个
在开始编码之前,请确保你已具备:
cargo install --git https://github.com/coral-xyz/anchor avm --locked --force
)首先,让我们创建一个新的Anchor项目:
anchor init anchor_amm
cd anchor_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,
}
现在让我们创建我们的第一个指令来初始化 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(())
}
}
要点:
set_inner()
一次初始化所有配置字段创建 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)
}
}
要点:
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)
}
}
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)
}
}
要点:
创建 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::*;
创建一个测试文件 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<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 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!