AMM

自动化做市商(AMM)是去中心化金融的基础构件之一,它使用户能够直接通过智能合约交换代币,而无需依赖传统的订单簿或中心化交易所。
可以将 AMM 想象成一个自运行的流动性池:用户存入代币对,AMM 使用数学公式来确定价格并促进代币之间的交换。这使得任何人都可以随时即时交易代币,而无需交易对手方。
如果仔细观察,你会发现 AMM 其实就是一个带有额外步骤、计算和逻辑的托管服务。因此,如果你错过了,请先完成匹诺曹托管挑战,然后再学习本课程。
在本次挑战中,你将实现一个简单的 AMM,包含以下四个核心指令:
初始化:通过创建配置账户并铸造代表池中份额的 LP(流动性提供者)代币来设置 AMM。
存入:允许用户向池中提供
token_x和token_y。作为回报,他们将收到与其流动性份额成比例的 LP 代币。提取:允许用户赎回其 LP 代币,以提取其在池中的
token_x和token_y份额,从而移除流动性。交换:允许任何人使用池来交易 token_x 和
token_y(或反之),并向流动性提供者支付少量费用。
注意:如果你不熟悉匹诺曹,建议先阅读匹诺曹简介,以熟悉我们将在本程序中使用的核心概念。
安装
让我们从创建一个全新的 Rust 环境开始:
# create workspace
cargo new blueshift_native_amm --lib --edition 2021
cd blueshift_native_amm添加 pinocchio、pinocchio-system、pinocchio-token、pinocchio-associated-token-account 和由 Dean 创建的 constant-product-curve 来处理我们 AMM 的所有计算:
cargo add pinocchio pinocchio-system pinocchio-token pinocchio-associated-token-account
cargo add --git="https://github.com/deanmlittle/constant-product-curve" constant-product-curve在 Cargo.toml 中声明 crate 类型,以便在 target/deploy 中生成部署工件:
[lib]
crate-type = ["lib", "cdylib"]现在您可以开始编写您的 AMM 程序了。
恒定乘积曲线
大多数 AMM 的核心是一个简单但强大的公式,称为恒定乘积曲线。该公式确保池中两种代币储备的乘积始终保持不变,即使用户进行交易或提供流动性。
公式
最常见的 AMM 公式是:x * y = k,其中:
x= 池中代币 X 的数量y= 池中代币 Y 的数量k= 一个常数(永不改变)
每当有人将一种代币兑换为另一种代币时,池会调整储备,以确保乘积 k 保持不变。这会根据供需自动调整价格曲线。
示例
假设池开始时有 100 个代币 X 和 100 个代币 Y:100 * 100 = 10,000。
如果用户想用 10 个代币 X 兑换代币 Y,池必须保持 k = 10,000。因此,如果 x_new = 110(存入后),求解 y_new:110 * y_new = 10,000,因此 y_new = 10,000 / 110 ≈ 90.91。
用户将收到 100 - 90.91 = 9.09 个代币 Y(扣除任何费用)。
流动性提供
当用户将两种代币存入池中时,他们成为流动性提供者(LP)。作为回报,他们会收到代表其池中份额的 LP 代币。
LP 代币的铸造比例与您添加的流动性成正比。
当您提取时,您会销毁您的 LP 代币以取回您在两种代币中的份额(加上从兑换中收取的费用份额)。
第一个流动性提供者设置初始比例。例如,如果您存入 100 X 和 100 Y,您可能会收到 100 个 LP 代币。
之后,如果池中已经有 100 X 和 100 Y,而您再添加 10 X 和 10 Y,您将根据您的贡献比例获得 LP 代币:share = deposit_x / total_x = 10 / 100 = 10%,因此 Amm 会向用户钱包铸造总 LP 供应量的 10%。
费用
每次交换通常会收取一小笔费用(例如 0.3%),该费用会添加到池中。这意味着 LP 可以分享交易费用,从而随着时间的推移增加其 LP 代币的价值,并激励人们提供流动性。
Template
这次我们将把程序拆分为小而集中的模块,而不是将所有内容塞入 lib.rs 中。文件夹结构大致如下:
src
├── instructions
│ ├── deposit.rs
│ ├── initialize.rs
│ ├── mod.rs
│ ├── swap.rs
│ └── withdraw.rs
├── lib.rs
└── state.rs入口点位于 lib.rs 中,看起来总是一样的:
use pinocchio::{
account_info::AccountInfo, entrypoint, program_error::ProgramError, pubkey::Pubkey,
ProgramResult,
};
entrypoint!(process_instruction);
pub mod instructions;
pub use instructions::*;
pub mod state;
pub use state::*;
// 22222222222222222222222222222222222222222222
pub const ID: Pubkey = [
0x0f, 0x1e, 0x6b, 0x14, 0x21, 0xc0, 0x4a, 0x07, 0x04, 0x31, 0x26, 0x5c, 0x19, 0xc5, 0xbb, 0xee,
0x19, 0x92, 0xba, 0xe8, 0xaf, 0xd1, 0xcd, 0x07, 0x8e, 0xf8, 0xaf, 0x70, 0x47, 0xdc, 0x11, 0xf7,
];
fn process_instruction(
_program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
match instruction_data.split_first() {
Some((Initialize::DISCRIMINATOR, data)) => {
Initialize::try_from((data, accounts))?.process()
}
Some((Deposit::DISCRIMINATOR, data)) => Deposit::try_from((data, accounts))?.process(),
Some((Withdraw::DISCRIMINATOR, data)) => Withdraw::try_from((data, accounts))?.process(),
Some((Swap::DISCRIMINATOR, data)) => Swap::try_from((data, accounts))?.process(),
_ => Err(ProgramError::InvalidInstructionData),
}
}State
我们将进入 state.rs,其中存储了我们 AMM 的所有数据。
我们将其分为三个部分:结构定义、读取辅助函数和写入辅助函数。
首先,让我们看看结构定义:
use core::mem::size_of;
use pinocchio::{account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey};
#[repr(C)]
pub struct Config {
state: u8,
seed: [u8; 8],
authority: Pubkey,
mint_x: Pubkey,
mint_y: Pubkey,
fee: [u8; 2],
config_bump: [u8; 1],
}
#[repr(u8)]
pub enum AmmState {
Uninitialized = 0u8,
Initialized = 1u8,
Disabled = 2u8,
WithdrawOnly = 3u8,
}
impl Config {
pub const LEN: usize = size_of::<Config>();
//...
}#[repr(C)] 属性确保我们的结构具有可预测的、与 C 兼容的内存布局,在不同平台和 Rust 编译器版本之间保持一致。这对于链上程序至关重要,因为数据必须可靠地序列化和反序列化。
我们将 seed(u64)和 fee(u16)存储为字节数组,而不是它们的原生类型,以确保安全的反序列化。当从账户存储中读取数据时,内存对齐没有保证,从未对齐的内存地址读取 u64 是未定义行为。通过使用字节数组并通过 from_le_bytes() 进行转换,我们确保数据可以安全读取,无论对齐情况如何,同时还保证在所有平台上始终使用一致的小端字节顺序。
Config 结构中的每个字段都有特定的用途:
state:跟踪 AMM 的当前状态(例如,未初始化、已初始化、已禁用或仅限提取)。
seed:用于程序派生地址(PDA)生成的唯一值,允许多个 AMM 以不同配置共存。
authority:对 AMM 拥有管理控制权的公钥(例如,用于暂停或升级池)。可以通过传递
[0u8; 32]将其设置为不可变。mint_x:池中代币 X 的 SPL 代币铸造地址。
mint_y:池中代币 Y 的 SPL 代币铸造地址。
fee:以基点(1 基点 = 0.01%)表示的交换费用,在每次交易中收取并分配给流动性提供者。
config_bump:用于 PDA 派生的 bump 种子,确保配置账户地址有效且唯一。保存此值以提高 PDA 派生效率。
AmmState 枚举定义了 AMM 的可能状态,使得管理池的生命周期并根据其状态限制某些操作变得更加容易。
读取辅助工具
读取辅助工具提供了对 Config 数据的安全、高效访问,并进行适当的验证和借用:
impl Config {
//...
#[inline(always)]
pub fn load(account_info: &AccountInfo) -> Result<Ref<Self>, ProgramError> {
if account_info.data_len() != Self::LEN {
return Err(ProgramError::InvalidAccountData);
}
if account_info.owner().ne(&crate::ID) {
return Err(ProgramError::InvalidAccountOwner);
}
Ok(Ref::map(account_info.try_borrow_data()?, |data| unsafe {
Self::from_bytes_unchecked(data)
}))
}
#[inline(always)]
pub unsafe fn load_unchecked(account_info: &AccountInfo) -> Result<&Self, ProgramError> {
if account_info.data_len() != Self::LEN {
return Err(ProgramError::InvalidAccountData);
}
if account_info.owner() != &crate::ID {
return Err(ProgramError::InvalidAccountOwner);
}
Ok(Self::from_bytes_unchecked(
account_info.borrow_data_unchecked(),
))
}
/// Return a `Config` from the given bytes.
///
/// # Safety
///
/// The caller must ensure that `bytes` contains a valid representation of `Config`, and
/// it is properly aligned to be interpreted as an instance of `Config`.
/// At the moment `Config` has an alignment of 1 byte.
/// This method does not perform a length validation.
#[inline(always)]
pub unsafe fn from_bytes_unchecked(bytes: &[u8]) -> &Self {
&*(bytes.as_ptr() as *const Config)
}
/// Return a mutable `Config` reference from the given bytes.
///
/// # Safety
///
/// The caller must ensure that `bytes` contains a valid representation of `Config`.
#[inline(always)]
pub unsafe fn from_bytes_unchecked_mut(bytes: &mut [u8]) -> &mut Self {
&mut *(bytes.as_mut_ptr() as *mut Config)
}
// Getter methods for safe field access
#[inline(always)]
pub fn state(&self) -> u8 { self.state }
#[inline(always)]
pub fn seed(&self) -> u64 { u64::from_le_bytes(self.seed) }
#[inline(always)]
pub fn authority(&self) -> &Pubkey { &self.authority }
#[inline(always)]
pub fn mint_x(&self) -> &Pubkey { &self.mint_x }
#[inline(always)]
pub fn mint_y(&self) -> &Pubkey { &self.mint_y }
#[inline(always)]
pub fn fee(&self) -> u16 { u16::from_le_bytes(self.fee) }
#[inline(always)]
pub fn config_bump(&self) -> [u8; 1] { self.config_bump }
}读取辅助工具的关键特性:
安全借用:
load方法返回一个Ref<Self>,安全地管理从账户数据的借用,防止数据竞争并确保内存安全。验证:
load和load_unchecked都会在允许访问结构之前验证账户数据的长度和所有者。获取方法:所有字段都通过获取方法访问,这些方法处理从字节数组到其正确类型的转换(例如,
u64::from_le_bytes用于seed)。性能:
#[inline(always)]属性确保这些频繁调用的方法被内联以实现最佳性能。
编写辅助工具
编写辅助工具提供了安全且经过验证的方法,用于修改Config数据:
impl Config {
//...
#[inline(always)]
pub fn load_mut(account_info: &AccountInfo) -> Result<RefMut<Self>, ProgramError> {
if account_info.data_len() != Self::LEN {
return Err(ProgramError::InvalidAccountData);
}
if account_info.owner().ne(&crate::ID) {
return Err(ProgramError::InvalidAccountOwner);
}
Ok(RefMut::map(account_info.try_borrow_mut_data()?, |data| unsafe {
Self::from_bytes_unchecked_mut(data)
}))
}
#[inline(always)]
pub fn set_state(&mut self, state: u8) -> Result<(), ProgramError> {
if state.ge(&(AmmState::WithdrawOnly as u8)) {
return Err(ProgramError::InvalidAccountData);
}
self.state = state as u8;
Ok(())
}
#[inline(always)]
pub fn set_fee(&mut self, fee: u16) -> Result<(), ProgramError> {
if fee.ge(&10_000) {
return Err(ProgramError::InvalidAccountData);
}
self.fee = fee.to_le_bytes();
Ok(())
}
#[inline(always)]
pub fn set_inner(
&mut self,
seed: u64,
authority: Pubkey,
mint_x: Pubkey,
mint_y: Pubkey,
fee: u16,
config_bump: [u8; 1],
) -> Result<(), ProgramError> {
self.set_state(AmmState::Initialized as u8)?;
self.set_seed(seed);
self.set_authority(authority);
self.set_mint_x(mint_x);
self.set_mint_y(mint_y);
self.set_fee(fee)?;
self.set_config_bump(config_bump);
Ok(())
}
#[inline(always)]
pub fn has_authority(&self) -> Option<Pubkey> {
let bytes = self.authority();
let chunks: &[u64; 4] = unsafe { &*(bytes.as_ptr() as *const [u64; 4]) };
if chunks.iter().any(|&x| x != 0) {
Some(self.authority)
} else {
None
}
}
}编写辅助工具的主要功能:
可变借用:
load_mut方法返回一个RefMut<Self>,安全地管理账户数据的可变借用。输入验证:像
set_state和set_fee这样的方法包含验证,以确保只存储有效值(例如,费用不能超过10,000个基点)。原子更新:
set_inner方法允许高效地一次性原子更新所有结构字段,最大限度地减少状态不一致的风险。权限检查:
has_authority方法提供了一种高效的方式来检查权限是否已设置(非零)或AMM是否不可变(全为零)。字节转换:多字节值通过像
to_le_bytes()这样的方法正确地转换为小端字节数组,以确保跨平台行为的一致性。