CreateAccount: Initialize an address lookup table

This commit is contained in:
Christian Kamm 2022-02-28 15:42:14 +01:00
parent c7cd564d11
commit 75092f7681
8 changed files with 288 additions and 18 deletions

View File

@ -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"

View File

@ -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<CreateAccount>, _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<CreateAccount>,
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(())
}

View File

@ -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<Account<'info, TokenAccount>>,
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> {

View File

@ -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<CreateAccount>, account_num: u8) -> Result<()> {
instructions::create_account(ctx, account_num)
pub fn create_account(
ctx: Context<CreateAccount>,
account_num: u8,
address_lookup_table_recent_slot: u64,
) -> Result<()> {
instructions::create_account(ctx, account_num, address_lookup_table_recent_slot)
}
// todo:

View File

@ -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<Pubkey> },
/// 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<Pubkey>,
) -> 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),
],
)
}

View File

@ -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;

View File

@ -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);

View File

@ -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,