Rust
Pinocchio AMM

Pinocchio AMM

13 Graduates

Ініціалізація

Інструкція initialize виконує два основних завдання:

  • Ініціалізує обліковий запис Config та зберігає всю інформацію, необхідну для правильної роботи amm.

  • Створює обліковий запис Mint mint_lp та призначає mint_authority обліковому запису config.

Ми не будемо ініціалізувати жодні Associated Token Accounts (ATAs) тут, оскільки це часто непотрібно і може бути марнотратним. У наступних інструкціях deposit, withdraw та swap ми перевіримо, що токени депонуються у правильні ATAs. Однак, вам слід створити допоміжну функцію "initializeAccount" на фронтенді для генерації цих облікових записів за потреби.

Required Accounts

Нижче наведено облікові записи, необхідні для цього контексту:

  • initializer: Творець облікового запису config. Це не обов'язково має бути також орган влади над ним. Повинен бути signer та mutable, оскільки цей обліковий запис оплачуватиме ініціалізацію як config, так і mint_lp.

  • mint_lp: Обліковий запис Mint, який представлятиме ліквідність пулу. mint_authority має бути встановлений на обліковий запис config. Повинен бути переданий як mutable.

  • config: Обліковий запис конфігурації, що ініціалізується. Повинен бути mutable.

  • Програми system та token: Програмні облікові записи, необхідні для ініціалізації вищезазначених облікових записів. Повинні бути 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 (Program Derived Address). Це дозволяє створювати унікальні екземпляри пулів. Має бути типу [u64]

  • fee: Комісія за обмін, виражена в базисних пунктах (1 базисний пункт = 0,01%). Ця комісія стягується за кожну угоду і розподіляється між постачальниками ліквідності. Має бути типу [u16]

  • mint_x: Адреса монети SPL токена X у пулі. Має бути типу [u8; 32]

  • mint_y: Адреса монети SPL токена Y у пулі. Має бути типу [u8; 32]

  • config_bump: Значення bump seed, яке використовується для отримання PDA облікового запису config. Має бути типу u8

  • lp_bump: Значення bump seed, яке використовується для отримання PDA облікового запису lp_mint. Має бути типу u8

  • authority: Публічний ключ, який матиме адміністративні повноваження над AMM. Якщо не вказано, пул можна встановити як незмінний. Має бути типу [u8; 32]

Як бачите, деякі з цих полів можна отримати по-різному. Наприклад, ми могли б отримати mint_x шляхом передачі облікового запису Mint і безпосереднього зчитування з нього, або генерувати значення 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),
        }
    }
}

Поле authority в InitializeInstructionData є необов'язковим і може бути пропущене для створення незмінного пулу.

Щоб полегшити це та заощадити 32 байти даних транзакції при створенні незмінних пулів, ми перевіряємо довжину даних інструкції та аналізуємо дані відповідно; якщо дані коротші, ми встановлюємо поле authority як None, записуючи 32 нульових байти в кінець буфера; якщо воно включає повне поле authority, ми безпосередньо перетворюємо байтовий зріз на структуру.

Instruction Logic

Ми починаємо з десеріалізації як instruction_data, так і accounts.

Потім нам потрібно:

  • Створити обліковий запис Config за допомогою інструкції CreateAccount із системної програми та наступних сідів:

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, завантаживши його за допомогою хелпера Config::load_mut_unchecked() і потім заповнивши його всіма необхідними даними за допомогою хелпера config.set_inner().

  • Створити обліковий запис Mint для lp, використовуючи інструкції CreateAccount та InitializeMint2 і наступні сіди:

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

mint_authority для mint_lp є обліковий запис config

Ви повинні бути достатньо кваліфікованими, щоб зробити це самостійно, тому я залишу реалізацію вам:

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(())
    }
}

Security

Як згадувалося раніше, це може здатися незвичним, але нам не потрібно виконувати явні перевірки переданих облікових записів.

Це тому, що на практиці інструкція не виконається, якщо щось не так; або під час CPI (Cross-Program Invocation), або раніше через перевірки, які ми вбудували в програму.

Наприклад, розглянемо обліковий запис initializer. Ми очікуємо, що він буде одночасно signer та mutable, але якщо це не так, інструкція CreateAccount автоматично завершиться невдачею, оскільки вона вимагає цих властивостей для payer.

Аналогічно, якщо обліковий запис config передається з недійсним mint_x або mint_y, будь-яка спроба внести депозит у протокол завершиться невдачею під час переказу токенів.

З набуттям досвіду ви виявите, що багато перевірок можна опустити, щоб зробити інструкції легшими та оптимізованими, покладаючись на систему та наступні інструкції для забезпечення обмежень.

Next PageДепозит
АБО ПЕРЕЙТИ ДО ЗАВДАННЯ
Готові прийняти завдання?
Blueshift © 2025Commit: e573eab