From 75092f76819192eb8301209e38df6081248948f1 Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Mon, 28 Feb 2022 15:42:14 +0100 Subject: [PATCH] CreateAccount: Initialize an address lookup table --- programs/mango-v4/Cargo.toml | 4 +- .../src/instructions/create_account.rs | 56 +++++- programs/mango-v4/src/instructions/deposit.rs | 5 + programs/mango-v4/src/lib.rs | 9 +- ...solana_address_lookup_table_instruction.rs | 180 ++++++++++++++++++ programs/mango-v4/src/state/mango_account.rs | 20 ++ .../tests/program_test/mango_client.rs | 29 ++- programs/mango-v4/tests/test_basic.rs | 3 +- 8 files changed, 288 insertions(+), 18 deletions(-) create mode 100644 programs/mango-v4/src/solana_address_lookup_table_instruction.rs diff --git a/programs/mango-v4/Cargo.toml b/programs/mango-v4/Cargo.toml index 69b65b093..de0cce31b 100644 --- a/programs/mango-v4/Cargo.toml +++ b/programs/mango-v4/Cargo.toml @@ -25,16 +25,16 @@ anchor-spl = "0.22.0" fixed = { version = "=1.11.0", features = ["serde", "borsh"] } fixed-macro = "^1.1.1" static_assertions = "1.1" +solana-program = "1.9.5" +serde = "^1.0" [dev-dependencies] solana-sdk = { version = "1.9.5", default-features = false } -solana-program = "1.9.5" solana-program-test = "1.9.5" solana-logger = "1.9.5" spl-token = { version = "^3.0.0", features = ["no-entrypoint"] } spl-associated-token-account = { version = "^1.0.3", features = ["no-entrypoint"] } bytemuck = "^1.7.2" -serde = "^1.0" bincode = "^1.3.1" log = "0.4.14" env_logger = "0.9.0" diff --git a/programs/mango-v4/src/instructions/create_account.rs b/programs/mango-v4/src/instructions/create_account.rs index 9625ba036..ae30d4cad 100644 --- a/programs/mango-v4/src/instructions/create_account.rs +++ b/programs/mango-v4/src/instructions/create_account.rs @@ -1,5 +1,7 @@ use anchor_lang::prelude::*; +use crate::error::*; +use crate::solana_address_lookup_table_instruction; use crate::state::*; #[derive(Accounts)] @@ -18,16 +20,62 @@ pub struct CreateAccount<'info> { pub owner: Signer<'info>, + // We can't use anchor's `init` here because the create_lookup_table instruction + // expects an unallocated table. + // Even though this is a PDA, we can't use anchor's `seeds` here because the + // address must be based on a recent slot hash, and create_lookup_table() will + // validate in anyway. + #[account(mut)] + pub address_lookup_table: UncheckedAccount<'info>, // TODO: wrapper? + #[account(mut)] pub payer: Signer<'info>, pub system_program: Program<'info, System>, pub rent: Sysvar<'info, Rent>, + pub address_lookup_table_program: UncheckedAccount<'info>, // TODO: force address? } -pub fn create_account(ctx: Context, _account_num: u8) -> Result<()> { - let mut account = ctx.accounts.account.load_init()?; - account.group = ctx.accounts.group.key(); - account.owner = ctx.accounts.owner.key(); +pub fn create_account( + ctx: Context, + account_num: u8, + address_lookup_table_recent_slot: u64, +) -> Result<()> { + { + let mut account = ctx.accounts.account.load_init()?; + account.group = ctx.accounts.group.key(); + account.owner = ctx.accounts.owner.key(); + account.address_lookup_table = ctx.accounts.address_lookup_table.key(); + account.account_num = account_num; + account.bump = *ctx.bumps.get("account").ok_or(MangoError::SomeError)?; + } + + // + // Setup address lookup tables initial state: + // - one is active and empty + // - other one is deacivated + // + // TODO: We could save some CU here by not using create_lookup_table(): + // it - unnecessarily - derives the lookup table address again. + let (instruction, _expected_adress_map_address) = + solana_address_lookup_table_instruction::create_lookup_table( + ctx.accounts.account.key(), + ctx.accounts.payer.key(), + address_lookup_table_recent_slot, + ); + let account_infos = [ + ctx.accounts.address_lookup_table.to_account_info(), + ctx.accounts.account.to_account_info(), + ctx.accounts.payer.to_account_info(), + ctx.accounts.system_program.to_account_info(), + ]; + // Anchor only sets the discriminator after this function finishes, + // calling load() right now would cause an error. But we _do_ need an immutable borrow + // so hack it by calling exit() early (which only sets the discriminator) + ctx.accounts.account.exit(&crate::id())?; + let account = ctx.accounts.account.load()?; + let seeds = account_seeds!(account); + solana_program::program::invoke_signed(&instruction, &account_infos, &[seeds])?; + Ok(()) } diff --git a/programs/mango-v4/src/instructions/deposit.rs b/programs/mango-v4/src/instructions/deposit.rs index 90737ff02..e01836212 100644 --- a/programs/mango-v4/src/instructions/deposit.rs +++ b/programs/mango-v4/src/instructions/deposit.rs @@ -12,6 +12,7 @@ pub struct Deposit<'info> { #[account( mut, has_one = group, + has_one = address_lookup_table, )] pub account: AccountLoader<'info, MangoAccount>, @@ -30,7 +31,11 @@ pub struct Deposit<'info> { pub token_account: Box>, pub token_authority: Signer<'info>, + #[account(mut)] + pub address_lookup_table: UncheckedAccount<'info>, // TODO: wrapper? + pub token_program: Program<'info, Token>, + pub address_lookup_table_program: UncheckedAccount<'info>, // TODO: force address? } impl<'info> Deposit<'info> { diff --git a/programs/mango-v4/src/lib.rs b/programs/mango-v4/src/lib.rs index bc68684fc..17f96db21 100644 --- a/programs/mango-v4/src/lib.rs +++ b/programs/mango-v4/src/lib.rs @@ -8,6 +8,7 @@ use instructions::*; mod error; mod instructions; +pub mod solana_address_lookup_table_instruction; pub mod state; declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"); @@ -38,8 +39,12 @@ pub mod mango_v4 { ) } - pub fn create_account(ctx: Context, account_num: u8) -> Result<()> { - instructions::create_account(ctx, account_num) + pub fn create_account( + ctx: Context, + account_num: u8, + address_lookup_table_recent_slot: u64, + ) -> Result<()> { + instructions::create_account(ctx, account_num, address_lookup_table_recent_slot) } // todo: diff --git a/programs/mango-v4/src/solana_address_lookup_table_instruction.rs b/programs/mango-v4/src/solana_address_lookup_table_instruction.rs new file mode 100644 index 000000000..01eecd4f1 --- /dev/null +++ b/programs/mango-v4/src/solana_address_lookup_table_instruction.rs @@ -0,0 +1,180 @@ +// +// NOTE: This is a copy from solana's programs/address-lookup-table/src/instruction.rs +// Unfortunately we can't import the module since it depends on solana_sdk! +// +use { + serde::{Deserialize, Serialize}, + solana_program::{ + clock::Slot, + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, + system_program, + }, + std::str::FromStr, +}; + +pub fn id() -> Pubkey { + Pubkey::from_str(&"AddressLookupTab1e1111111111111111111111111").unwrap() +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +pub enum ProgramInstruction { + /// Create an address lookup table + /// + /// # Account references + /// 0. `[WRITE]` Uninitialized address lookup table account + /// 1. `[SIGNER]` Account used to derive and control the new address lookup table. + /// 2. `[SIGNER, WRITE]` Account that will fund the new address lookup table. + /// 3. `[]` System program for CPI. + CreateLookupTable { + /// A recent slot must be used in the derivation path + /// for each initialized table. When closing table accounts, + /// the initialization slot must no longer be "recent" to prevent + /// address tables from being recreated with reordered or + /// otherwise malicious addresses. + recent_slot: Slot, + /// Address tables are always initialized at program-derived + /// addresses using the funding address, recent blockhash, and + /// the user-passed `bump_seed`. + bump_seed: u8, + }, + + /// Permanently freeze an address lookup table, making it immutable. + /// + /// # Account references + /// 0. `[WRITE]` Address lookup table account to freeze + /// 1. `[SIGNER]` Current authority + FreezeLookupTable, + + /// Extend an address lookup table with new addresses + /// + /// # Account references + /// 0. `[WRITE]` Address lookup table account to extend + /// 1. `[SIGNER]` Current authority + /// 2. `[SIGNER, WRITE]` Account that will fund the table reallocation + /// 3. `[]` System program for CPI. + ExtendLookupTable { new_addresses: Vec }, + + /// Deactivate an address lookup table, making it unusable and + /// eligible for closure after a short period of time. + /// + /// # Account references + /// 0. `[WRITE]` Address lookup table account to deactivate + /// 1. `[SIGNER]` Current authority + DeactivateLookupTable, + + /// Close an address lookup table account + /// + /// # Account references + /// 0. `[WRITE]` Address lookup table account to close + /// 1. `[SIGNER]` Current authority + /// 2. `[WRITE]` Recipient of closed account lamports + CloseLookupTable, +} + +/// Derives the address of an address table account from a wallet address and a recent block's slot. +pub fn derive_lookup_table_address( + authority_address: &Pubkey, + recent_block_slot: Slot, +) -> (Pubkey, u8) { + Pubkey::find_program_address( + &[authority_address.as_ref(), &recent_block_slot.to_le_bytes()], + &id(), + ) +} + +/// Constructs an instruction to create a table account and returns +/// the instruction and the table account's derived address. +pub fn create_lookup_table( + authority_address: Pubkey, + payer_address: Pubkey, + recent_slot: Slot, +) -> (Instruction, Pubkey) { + let (lookup_table_address, bump_seed) = + derive_lookup_table_address(&authority_address, recent_slot); + let instruction = Instruction::new_with_bincode( + id(), + &ProgramInstruction::CreateLookupTable { + recent_slot, + bump_seed, + }, + vec![ + AccountMeta::new(lookup_table_address, false), + AccountMeta::new_readonly(authority_address, true), + AccountMeta::new(payer_address, true), + AccountMeta::new_readonly(system_program::id(), false), + ], + ); + + (instruction, lookup_table_address) +} + +/// Constructs an instruction that freezes an address lookup +/// table so that it can never be closed or extended again. Empty +/// lookup tables cannot be frozen. +pub fn freeze_lookup_table(lookup_table_address: Pubkey, authority_address: Pubkey) -> Instruction { + Instruction::new_with_bincode( + id(), + &ProgramInstruction::FreezeLookupTable, + vec![ + AccountMeta::new(lookup_table_address, false), + AccountMeta::new_readonly(authority_address, true), + ], + ) +} + +/// Constructs an instruction which extends an address lookup +/// table account with new addresses. +pub fn extend_lookup_table( + lookup_table_address: Pubkey, + authority_address: Pubkey, + payer_address: Pubkey, + new_addresses: Vec, +) -> Instruction { + Instruction::new_with_bincode( + id(), + &ProgramInstruction::ExtendLookupTable { new_addresses }, + vec![ + AccountMeta::new(lookup_table_address, false), + AccountMeta::new_readonly(authority_address, true), + AccountMeta::new(payer_address, true), + AccountMeta::new_readonly(system_program::id(), false), + ], + ) +} + +/// Constructs an instruction that deactivates an address lookup +/// table so that it cannot be extended again and will be unusable +/// and eligible for closure after a short amount of time. +pub fn deactivate_lookup_table( + lookup_table_address: Pubkey, + authority_address: Pubkey, +) -> Instruction { + Instruction::new_with_bincode( + id(), + &ProgramInstruction::DeactivateLookupTable, + vec![ + AccountMeta::new(lookup_table_address, false), + AccountMeta::new_readonly(authority_address, true), + ], + ) +} + +/// Returns an instruction that closes an address lookup table +/// account. The account will be deallocated and the lamports +/// will be drained to the recipient address. +pub fn close_lookup_table( + lookup_table_address: Pubkey, + authority_address: Pubkey, + recipient_address: Pubkey, +) -> Instruction { + Instruction::new_with_bincode( + id(), + &ProgramInstruction::CloseLookupTable, + vec![ + AccountMeta::new(lookup_table_address, false), + AccountMeta::new_readonly(authority_address, true), + AccountMeta::new(recipient_address, false), + ], + ) +} diff --git a/programs/mango-v4/src/state/mango_account.rs b/programs/mango-v4/src/state/mango_account.rs index 2692c5f42..0a01be19c 100644 --- a/programs/mango-v4/src/state/mango_account.rs +++ b/programs/mango-v4/src/state/mango_account.rs @@ -91,6 +91,8 @@ pub struct MangoAccount { // Alternative authority/signer of transactions for a mango account pub delegate: Pubkey, + pub address_lookup_table: Pubkey, + // pub in_margin_basket: [bool; MAX_PAIRS], // pub num_in_margin_basket: u8, // TODO: this should be a separate struct for convenient use, like MangoGroup::tokens @@ -111,7 +113,25 @@ pub struct MangoAccount { /// This account cannot do anything except go through `resolve_bankruptcy` pub is_bankrupt: bool, + pub account_num: u8, + pub bump: u8, + // pub info: [u8; INFO_LEN], // TODO: Info could be in a separate PDA? pub reserved: [u8; 5], } // TODO: static assert the size and alignment + +#[macro_export] +macro_rules! account_seeds { + ( $account:expr ) => { + &[ + $account.group.as_ref(), + b"account".as_ref(), + $account.owner.as_ref(), + &$account.account_num.to_le_bytes(), + &[$account.bump], + ] + }; +} + +pub use account_seeds; diff --git a/programs/mango-v4/tests/program_test/mango_client.rs b/programs/mango-v4/tests/program_test/mango_client.rs index e30987299..11579585f 100644 --- a/programs/mango-v4/tests/program_test/mango_client.rs +++ b/programs/mango-v4/tests/program_test/mango_client.rs @@ -7,6 +7,7 @@ use solana_sdk::signature::{Keypair, Signer}; use solana_sdk::transport::TransportError; use super::solana::SolanaCookie; +use mango_v4::state::*; #[async_trait::async_trait(?Send)] pub trait ClientAccountLoader { @@ -71,7 +72,6 @@ pub struct WithdrawInstruction<'keypair> { pub amount: u64, pub allow_borrow: bool, - pub group: Pubkey, pub account: Pubkey, pub owner: &'keypair Keypair, pub token_account: Pubkey, @@ -94,10 +94,11 @@ impl<'keypair> ClientInstruction for WithdrawInstruction<'keypair> { // load account so we know its mint let token_account: TokenAccount = account_loader.load(&self.token_account).await.unwrap(); + let account: MangoAccount = account_loader.load(&self.account).await.unwrap(); let bank = Pubkey::find_program_address( &[ - self.group.as_ref(), + account.group.as_ref(), b"tokenbank".as_ref(), token_account.mint.as_ref(), ], @@ -106,7 +107,7 @@ impl<'keypair> ClientInstruction for WithdrawInstruction<'keypair> { .0; let vault = Pubkey::find_program_address( &[ - self.group.as_ref(), + account.group.as_ref(), b"tokenvault".as_ref(), token_account.mint.as_ref(), ], @@ -115,7 +116,7 @@ impl<'keypair> ClientInstruction for WithdrawInstruction<'keypair> { .0; let accounts = Self::Accounts { - group: self.group, + group: account.group, account: self.account, owner: self.owner.pubkey(), bank, @@ -144,7 +145,6 @@ impl<'keypair> ClientInstruction for WithdrawInstruction<'keypair> { pub struct DepositInstruction<'keypair> { pub amount: u64, - pub group: Pubkey, pub account: Pubkey, pub token_account: Pubkey, pub token_authority: &'keypair Keypair, @@ -164,10 +164,11 @@ impl<'keypair> ClientInstruction for DepositInstruction<'keypair> { // load account so we know its mint let token_account: TokenAccount = account_loader.load(&self.token_account).await.unwrap(); + let account: MangoAccount = account_loader.load(&self.account).await.unwrap(); let bank = Pubkey::find_program_address( &[ - self.group.as_ref(), + account.group.as_ref(), b"tokenbank".as_ref(), token_account.mint.as_ref(), ], @@ -176,7 +177,7 @@ impl<'keypair> ClientInstruction for DepositInstruction<'keypair> { .0; let vault = Pubkey::find_program_address( &[ - self.group.as_ref(), + account.group.as_ref(), b"tokenvault".as_ref(), token_account.mint.as_ref(), ], @@ -185,13 +186,15 @@ impl<'keypair> ClientInstruction for DepositInstruction<'keypair> { .0; let accounts = Self::Accounts { - group: self.group, + group: account.group, account: self.account, bank, vault, + address_lookup_table: account.address_lookup_table, token_account: self.token_account, token_authority: self.token_authority.pubkey(), token_program: Token::id(), + address_lookup_table_program: mango_v4::solana_address_lookup_table_instruction::id(), }; let instruction = make_instruction(program_id, &accounts, instruction); @@ -318,6 +321,7 @@ impl<'keypair> ClientInstruction for CreateGroupInstruction<'keypair> { pub struct CreateAccountInstruction<'keypair> { pub account_num: u8, + pub recent_slot: u64, pub group: Pubkey, pub owner: &'keypair Keypair, @@ -334,6 +338,7 @@ impl<'keypair> ClientInstruction for CreateAccountInstruction<'keypair> { let program_id = mango_v4::id(); let instruction = mango_v4::instruction::CreateAccount { account_num: self.account_num, + address_lookup_table_recent_slot: self.recent_slot, }; let account = Pubkey::find_program_address( @@ -346,14 +351,22 @@ impl<'keypair> ClientInstruction for CreateAccountInstruction<'keypair> { &program_id, ) .0; + let address_lookup_table = + mango_v4::solana_address_lookup_table_instruction::derive_lookup_table_address( + &account, + self.recent_slot, + ) + .0; let accounts = mango_v4::accounts::CreateAccount { group: self.group, owner: self.owner.pubkey(), account, + address_lookup_table, payer: self.payer.pubkey(), system_program: System::id(), rent: sysvar::rent::Rent::id(), + address_lookup_table_program: mango_v4::solana_address_lookup_table_instruction::id(), }; let instruction = make_instruction(program_id, &accounts, instruction); diff --git a/programs/mango-v4/tests/test_basic.rs b/programs/mango-v4/tests/test_basic.rs index 222bef898..fbfd25582 100644 --- a/programs/mango-v4/tests/test_basic.rs +++ b/programs/mango-v4/tests/test_basic.rs @@ -38,6 +38,7 @@ async fn test_basic() -> Result<(), TransportError> { solana, CreateAccountInstruction { account_num: 0, + recent_slot: 0, // TODO: get a real recent_slot, probably from SlotHistory group, owner, payer, @@ -77,7 +78,6 @@ async fn test_basic() -> Result<(), TransportError> { solana, DepositInstruction { amount: deposit_amount, - group, account, token_account: payer_mint0_account, token_authority: payer, @@ -115,7 +115,6 @@ async fn test_basic() -> Result<(), TransportError> { WithdrawInstruction { amount: withdraw_amount, allow_borrow: true, - group, account, owner, token_account: payer_mint0_account,