Rust
Pinocchio AMM

Pinocchio AMM

13 Graduates

初始化

initialize 指令执行两个主要任务:

  • 初始化 Config 账户,并存储 AMM 正常运行所需的所有信息。

  • 创建 mint_lp 铸币账户,并将 mint_authority 分配给 config 账户。

我们不会在这里初始化任何关联代币账户(ATAs),因为这通常是没有必要的,并且可能会浪费资源。在后续的 depositwithdrawswap 指令中,我们会检查代币是否存入了正确的 ATAs。然而,您应该在前端创建一个“initializeAccount”辅助工具,以按需生成这些账户。

所需账户

以下是此上下文所需的账户:

  • initializerconfig 账户的创建者。这不一定也必须是其权限持有者。必须是 signermutable,因为此账户将支付 configmint_lp 的初始化费用。

  • mint_lp:代表池流动性的铸币账户。mint_authority 应设置为 config 账户。必须作为 mutable 传递。

  • config:正在初始化的配置账户。必须是 mutable

  • systemtoken 程序:初始化上述账户所需的程序账户。必须是 executable

随着经验的积累,您会注意到许多这些检查可以省略,而依赖于 CPI 本身强制执行的约束。例如,对于此账户结构,不需要任何显式检查;如果不满足约束,程序将默认失败。在我们梳理逻辑的过程中,我会指出这些细微差别。

由于与我们通常创建的结构体相比没有太多变化,我将把实现部分留给你:

rust
pub struct InitializeAccounts<'a> {
    pub initializer: &'a AccountInfo,
    pub mint_lp: &'a AccountInfo,
    pub config: &'a AccountInfo,
}

impl<'a> TryFrom<&'a [AccountInfo]> for InitializeAccounts<'a> {
  type Error = ProgramError;

  fn try_from(accounts: &'a [AccountInfo]) -> Result<Self, Self::Error> {
    //..
  }
}

你需要传入上述讨论的所有账户,但并不是所有账户都需要包含在 InitializeAccounts 结构体中,因为在实现中你可能并不需要直接引用每个账户。

指令数据

以下是我们需要传入的指令数据:

  • seed:用于PDA(程序派生地址)种子推导的随机数。这允许创建唯一的池实例。必须是一个 [u64]

  • fee:以基点表示的交换费(1基点=0.01%)。此费用在每次交易中收取,并分配给流动性提供者。必须是一个 [u16]

  • mint_x:池中代币X的SPL代币铸造地址。必须是一个 [u8; 32]

  • mint_y:池中代币Y的SPL代币铸造地址。必须是一个 [u8; 32]

  • config_bump:用于推导 config 账户PDA的bump种子。必须是一个 u8

  • lp_bump:用于推导 lp_mint 账户PDA的bump种子。必须是一个 u8

  • authority:将拥有AMM管理权限的公钥。如果未提供,池可以设置为不可变。必须是一个 [u8; 32]

如你所见,这些字段中的一些可以通过不同方式推导。例如,我们可以通过传入 Mint 账户并直接从中读取来获取 mint_x,或者在程序中直接生成 bump 值。然而,通过显式传递这些字段,我们的目标是创建一个最优化和高效的程序。

在此实现中,我们以比通常更灵活和底层的方式处理指令数据解析。因此,我们将解释为什么做出以下决策:

rust
#[repr(C, packed)]
pub struct InitializeInstructionData {
    pub seed: u64,
    pub fee: u16,
    pub mint_x: [u8; 32],
    pub mint_y: [u8; 32],
    pub config_bump: [u8; 1],
    pub lp_bump: [u8; 1],
    pub authority: [u8; 32],
}

impl TryFrom<&[u8]> for InitializeInstructionData {
    type Error = ProgramError;

    fn try_from(data: &[u8]) -> Result<Self, Self::Error> {
        const INITIALIZE_DATA_LEN_WITH_AUTHORITY: usize = size_of::<InitializeInstructionData>();
        const INITIALIZE_DATA_LEN: usize =
            INITIALIZE_DATA_LEN_WITH_AUTHORITY - size_of::<[u8; 32]>();

        match data.len() {
            INITIALIZE_DATA_LEN_WITH_AUTHORITY => {
                Ok(unsafe { (data.as_ptr() as *const Self).read_unaligned() })
            }
            INITIALIZE_DATA_LEN => {
                // If the authority is not present, we need to build the buffer and add it at the end before transmuting to the struct
                let mut raw: MaybeUninit<[u8; INITIALIZE_DATA_LEN_WITH_AUTHORITY]> = MaybeUninit::uninit();
                let raw_ptr = raw.as_mut_ptr() as *mut u8;
                unsafe {
                    // Copy the provided data
                    core::ptr::copy_nonoverlapping(data.as_ptr(), raw_ptr, INITIALIZE_DATA_LEN);
                    // Add the authority to the end of the buffer
                    core::ptr::write_bytes(raw_ptr.add(INITIALIZE_DATA_LEN), 0, 32);
                    // Now transmute to the struct
                    Ok((raw.as_ptr() as *const Self).read_unaligned())
                }
            }
            _ => Err(ProgramError::InvalidInstructionData),
        }
    }
}

InitializeInstructionData 中的 authority 字段是可选的,可以省略以创建不可变池。

为了实现这一点并在创建不可变池时节省 32 字节的交易数据,我们会检查指令数据的长度并相应地解析数据;如果数据较短,我们将 authority 字段设置为 None,通过向缓冲区末尾写入 32 个零字节;如果包含完整的 authority 字段,我们会将字节切片直接转换为结构体。

指令逻辑

我们首先反序列化 instruction_dataaccounts

然后我们需要:

  • 使用系统程序中的 CreateAccount 指令和以下种子创建 Config 账户:

rust
let seed_binding = self.instruction_data.seed.to_le_bytes();
let config_seeds = [
    Seed::from(b"config"),
    Seed::from(&seed_binding),
    Seed::from(&self.instruction_data.mint_x),
    Seed::from(&self.instruction_data.mint_y),
    Seed::from(&self.instruction_data.config_bump),
];
  • 使用 Config::load_mut_unchecked() 辅助工具加载 Config 账户并填充所需的所有数据,然后使用 config.set_inner() 辅助工具填充。

  • 使用 CreateAccountInitializeMint2 指令以及以下种子为 lp 创建 Mint 账户:

rust
let mint_lp_seeds = [
    Seed::from(b"mint_lp"),
    Seed::from(self.accounts.config.key()),
    Seed::from(&self.instruction_data.lp_bump),
];

mint_lpmint_authorityconfig 账户

你应该足够熟练,可以自行完成这一部分,因此我将实现留给你:

rust
pub struct Initialize<'a> {
    pub accounts: InitializeAccounts<'a>,
    pub instruction_data: InitializeInstructionData,
}

impl<'a> TryFrom<(&'a [u8], &'a [AccountInfo])> for Initialize<'a> {
    type Error = ProgramError;

    fn try_from((data, accounts): (&'a [u8], &'a [AccountInfo])) -> Result<Self, Self::Error> {
        let accounts = InitializeAccounts::try_from(accounts)?;
        let instruction_data: InitializeInstructionData = InitializeInstructionData::try_from(data)?;

        Ok(Self {
            accounts,
            instruction_data,
        })
    }
}

impl<'a> Initialize<'a> {
    pub const DISCRIMINATOR: &'a u8 = &0;

    pub fn process(&mut self) -> ProgramResult {
      //..

      Ok(())
    }
}

安全性

如前所述,这可能看起来不寻常,但我们不需要对传入的账户进行显式检查。

这是因为在实际操作中,如果有问题,指令会失败;要么在 CPI(跨程序调用)期间,要么通过我们在程序中设置的早期检查失败。

例如,考虑 initializer 账户。我们期望它既是 signer 又是 mutable,但如果不是,CreateAccount 指令将会自动失败,因为它需要这些属性来满足 payer 的要求。

同样地,如果传递的 config 账户具有无效的 mint_xmint_y,任何尝试向协议中存入资金的操作都会在代币转移期间失败。

随着经验的积累,您会发现可以省略许多检查,以保持指令的轻量化和优化,依赖系统和下游指令来强制执行约束。

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