Initialize
The initialize instruction performs two main tasks:
Initializes the
Configaccount and stores all the information needed for the amm to operate correctly.Creates the
mint_lpMint account and assigns themint_authorityto theconfigaccount.
Required Accounts
Below are the accounts required for this context:
initializer: The creator of theconfigaccount. This does not necessarily have to be the authority over it as well. Must be asignerandmutable, since this account will pay for the initialization of both theconfigand themint_lp.mint_lp: The Mint account that will represent the pool’s liquidity. Themint_authorityshould be set to theconfigaccount. Must be passed asmutable.config: The configuration account being initialized. Must bemutable.systemandtokenprograms: Program accounts required to initialize the above accounts. Must beexecutable.
Since there aren’t many changes from the usual struct we create, I’ll leave the implementation to you:
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> {
//..
}
}Instruction Data
Here's the instruction data we need to pass in:
seed: A random number used for PDA (Program Derived Address) seed derivation. This allows for unique pool instances. Must be a[u64]fee: The swap fee, expressed in basis points (1 basis point = 0.01%). This fee is collected on each trade and distributed to liquidity providers. Must be a[u16]mint_x: The SPL token mint address for token X in the pool. Must be a[u8; 32]mint_y: The SPL token mint address for token Y in the pool. Must be a[u8; 32]config_bump: The bump seed used for deriving theconfigaccount PDA. Must be au8lp_bump: The bump seed used for deriving thelp_mintaccount PDA. Must be au8authority: The public key that will have administrative authority over the AMM. If not provided, the pool can be set as immutable. Must be a[u8; 32]
In this implementation, we’re handling instruction data parsing in a more flexible and low-level way than usual. For this reason we'll explain why we're making the following decisions:
#[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),
}
}
}The authority field in InitializeInstructionData is optional and can be omitted to create an immutable pool.
To facilitate this and save 32 bytes of transaction data when creating immutable pools, we check the instruction data length and parse the data accordingly; if the data is shorter, we set the authority field to None by writing 32 zero bytes to the end of the buffer; if it includes the full authority field, we cast the byte slice directly to the struct.
Instruction Logic
We begin by deserializing both the instruction_data and the accounts.
We then need to:
Create the
Configaccount using theCreateAccountinstruction from the system program and the following seeds:
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),
];Populate the
Configaccount loading it using theConfig::load_mut_unchecked()helper and then populating it with all the data needed using theconfig.set_inner()helper.Create the
Mintaccount for the lp using theCreateAccountandInitializeMint2instruction and the following seeds:
let mint_lp_seeds = [
Seed::from(b"mint_lp"),
Seed::from(self.accounts.config.key()),
Seed::from(&self.instruction_data.lp_bump),
];You should be proficient enough to this on your own, so I’ll leave the implementation to you:
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
As mentioned earlier, it might seem unusual, but we don’t need to perform explicit checks on the accounts being passed in.
This is because, in practice, the instruction will fail if something is wrong; either during a CPI (Cross-Program Invocation) or early through checks that we put into the program.
For example, consider the initializer account. We expect it to be both a signer and mutable, but if it isn’t, the CreateAccount instruction will fail automatically since it requires those properties for the payer.
Similarly, if the config account is passed with an invalid mint_x or mint_y, any attempt to deposit into the protocol will fail during the token transfer.
As you gain more experience, you’ll find that many checks can be omitted to keep instructions lightweight and optimized, relying on the system and downstream instructions to enforce constraints.