Rust
Pinocchio AMM

Pinocchio AMM

13 Graduates

AMM

匹诺曹 AMM 挑战

自动化做市商(AMM)是去中心化金融的基础构件之一,它使用户能够直接通过智能合约交换代币,而无需依赖传统的订单簿或中心化交易所。

可以将 AMM 想象成一个自运行的流动性池:用户存入代币对,AMM 使用数学公式来确定价格并促进代币之间的交换。这使得任何人都可以随时即时交易代币,而无需交易对手方。

如果仔细观察,你会发现 AMM 其实就是一个带有额外步骤、计算和逻辑的托管服务。因此,如果你错过了,请先完成匹诺曹托管挑战,然后再学习本课程。

在本次挑战中,你将实现一个简单的 AMM,包含以下四个核心指令:

  • 初始化:通过创建配置账户并铸造代表池中份额的 LP(流动性提供者)代币来设置 AMM。

  • 存入:允许用户向池中提供 token_xtoken_y。作为回报,他们将收到与其流动性份额成比例的 LP 代币。

  • 提取:允许用户赎回其 LP 代币,以提取其在池中的 token_xtoken_y 份额,从而移除流动性。

  • 交换:允许任何人使用池来交易 token_x 和 token_y(或反之),并向流动性提供者支付少量费用。

注意:如果你不熟悉匹诺曹,建议先阅读匹诺曹简介,以熟悉我们将在本程序中使用的核心概念。

安装

让我们从创建一个全新的 Rust 环境开始:

# create workspace
cargo new blueshift_native_amm --lib --edition 2021
cd blueshift_native_amm

添加 pinocchiopinocchio-systempinocchio-tokenpinocchio-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 中生成部署工件:

toml
[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_new110 * 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 中。文件夹结构大致如下:

text
src
├── instructions
│       ├── deposit.rs
│       ├── initialize.rs
│       ├── mod.rs
│       ├── swap.rs
│       └── withdraw.rs
├── lib.rs
└── state.rs

入口点位于 lib.rs 中,看起来总是一样的:

rust
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 的所有数据。

我们将其分为三个部分:结构定义、读取辅助函数和写入辅助函数。

首先,让我们看看结构定义:

rust
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 数据的安全、高效访问,并进行适当的验证和借用:

rust
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>,安全地管理从账户数据的借用,防止数据竞争并确保内存安全。

  • 验证:loadload_unchecked 都会在允许访问结构之前验证账户数据的长度和所有者。

  • 获取方法:所有字段都通过获取方法访问,这些方法处理从字节数组到其正确类型的转换(例如,u64::from_le_bytes 用于 seed)。

  • 性能:#[inline(always)] 属性确保这些频繁调用的方法被内联以实现最佳性能。

编写辅助工具

编写辅助工具提供了安全且经过验证的方法,用于修改Config数据:

rust
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_stateset_fee这样的方法包含验证,以确保只存储有效值(例如,费用不能超过10,000个基点)。

  • 原子更新:set_inner方法允许高效地一次性原子更新所有结构字段,最大限度地减少状态不一致的风险。

  • 权限检查:has_authority方法提供了一种高效的方式来检查权限是否已设置(非零)或AMM是否不可变(全为零)。

  • 字节转换:多字节值通过像to_le_bytes()这样的方法正确地转换为小端字节数组,以确保跨平台行为的一致性。

Next Page初始化
或者跳到挑战
准备接受挑战了吗?
Blueshift © 2025Commit: e573eab