Move to global address lookup tables

This commit is contained in:
Christian Kamm 2022-03-03 14:30:44 +01:00
parent e7736a8c88
commit c6031acbdb
10 changed files with 178 additions and 167 deletions

View File

@ -1,3 +1,4 @@
use anchor_lang::prelude::*;
mod solana_address_lookup_table_instruction;
pub use solana_address_lookup_table_instruction::*;
use solana_program::pubkey::{Pubkey, PUBKEY_BYTES};
@ -26,3 +27,24 @@ pub fn contains(table: &[u8], pubkey: &Pubkey) -> bool {
.find(|&addr| addr == pubkey)
.is_some()
}
pub fn extend<'info>(
lookup_table_ai: AccountInfo<'info>,
authority_ai: AccountInfo<'info>,
payer_ai: AccountInfo<'info>,
signer_seeds: &[&[&[u8]]],
new_addresses: Vec<Pubkey>,
) -> std::result::Result<(), ProgramError> {
let instruction = extend_lookup_table(
lookup_table_ai.key(),
authority_ai.key(),
payer_ai.key(),
new_addresses,
);
let account_infos = [
lookup_table_ai,
authority_ai,
payer_ai,
];
solana_program::program::invoke_signed(&instruction, &account_infos, signer_seeds)
}

View File

@ -1,6 +1,5 @@
use anchor_lang::prelude::*;
use crate::address_lookup_table;
use crate::error::*;
use crate::state::*;
@ -20,80 +19,19 @@ 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,
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 the address lookup table
//
// First: Pre-pay for max-length address lookup table -- otherwise extending it
// (later, in deposit etc) will need a payer!
let rent = Rent::get()?;
let required_lamports = rent
.minimum_balance(address_lookup_table::LOOKUP_TABLE_MAX_ACCOUNT_SIZE)
.max(1)
.saturating_sub(ctx.accounts.address_lookup_table.lamports());
if required_lamports > 0 {
solana_program::program::invoke(
&solana_program::system_instruction::transfer(
&ctx.accounts.payer.key(),
&ctx.accounts.address_lookup_table.key(),
required_lamports,
),
&[
ctx.accounts.payer.to_account_info(),
ctx.accounts.address_lookup_table.to_account_info(),
ctx.accounts.system_program.to_account_info(),
],
)?;
}
// Now create the account.
// 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) = address_lookup_table::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])?;
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();
account.account_num = account_num;
account.bump = *ctx.bumps.get("account").ok_or(MangoError::SomeError)?;
Ok(())
}

View File

@ -3,7 +3,6 @@ use anchor_spl::token;
use anchor_spl::token::Token;
use anchor_spl::token::TokenAccount;
use crate::address_lookup_table;
use crate::state::*;
#[derive(Accounts)]
@ -13,7 +12,6 @@ pub struct Deposit<'info> {
#[account(
mut,
has_one = group,
has_one = address_lookup_table,
)]
pub account: AccountLoader<'info, MangoAccount>,
@ -32,11 +30,7 @@ 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> {
@ -55,72 +49,21 @@ impl<'info> Deposit<'info> {
// That would save a lot of computation that needs to go into finding the
// right index for the mint.
pub fn deposit(ctx: Context<Deposit>, amount: u64) -> Result<()> {
let (is_new_position, oracle) = {
// Find the mint's token index
let group = ctx.accounts.group.load()?;
let mint = ctx.accounts.token_account.mint;
let token_index = group.tokens.index_for_mint(&mint)?;
// Find the mint's token index
let group = ctx.accounts.group.load()?;
let mint = ctx.accounts.token_account.mint;
let token_index = group.tokens.index_for_mint(&mint)?;
// Get the account's position for that token index
let mut account = ctx.accounts.account.load_mut()?;
let position = account.indexed_positions.get_mut_or_create(token_index)?;
let is_new_position = !position.is_active();
// Get the account's position for that token index
let mut account = ctx.accounts.account.load_mut()?;
let position = account.indexed_positions.get_mut_or_create(token_index)?;
// Update the bank and position
let mut bank = ctx.accounts.bank.load_mut()?;
bank.deposit(position, amount);
// Update the bank and position
let mut bank = ctx.accounts.bank.load_mut()?;
bank.deposit(position, amount);
// Transfer the actual tokens
token::transfer(ctx.accounts.transfer_ctx(), amount)?;
(is_new_position, bank.oracle)
};
// Do we need to add (oracle, bank) to the user's address lookup table?
//
// Since they are always added as a pair, checking for one is sufficient.
let add_to_lookup_table = is_new_position
&& !address_lookup_table::contains(
&ctx.accounts.address_lookup_table.try_borrow_data()?,
&oracle,
);
if add_to_lookup_table {
// NOTE: Unfortunately extend() _requires_ a payer, even though we've already
// fully funded the address lookup table. No further transfer will be necessary.
// We'll pass the account as payer.
let mut instruction = address_lookup_table::extend_lookup_table(
ctx.accounts.address_lookup_table.key(),
ctx.accounts.account.key(),
ctx.accounts.account.key(),
vec![ctx.accounts.bank.key(), oracle],
);
// Sneakily remove the system_program account: that way any attempted transfer would error.
instruction.accounts.pop();
let account_infos = [
ctx.accounts.address_lookup_table.to_account_info(),
ctx.accounts.account.to_account_info(),
ctx.accounts.account.to_account_info(),
];
// Signing for the account is complicated because it must work as a payer which means
// a mutable borrow. Thus we must make copies of the values in the seed.
struct AccountSeedValues {
group: Pubkey,
owner: Pubkey,
account_num: u8,
bump: u8,
}
let account_seed_values = {
let account = ctx.accounts.account.load()?;
AccountSeedValues {
group: account.group,
owner: account.owner,
account_num: account.account_num,
bump: account.bump,
}
};
let account_seeds = account_seeds!(account_seed_values);
solana_program::program::invoke_signed(&instruction, &account_infos, &[account_seeds])?;
}
// Transfer the actual tokens
token::transfer(ctx.accounts.transfer_ctx(), amount)?;
Ok(())
}

View File

@ -5,6 +5,7 @@ use anchor_spl::token::TokenAccount;
use fixed::types::I80F48;
use fixed_macro::types::I80F48;
use crate::address_lookup_table;
use crate::error::*;
use crate::state::*;
@ -40,13 +41,32 @@ pub struct RegisterToken<'info> {
)]
pub vault: Account<'info, TokenAccount>,
#[account(
init,
seeds = [group.key().as_ref(), b"mintinfo".as_ref(), mint.key().as_ref()],
bump,
payer = payer,
space = 8 + std::mem::size_of::<MintInfo>(),
)]
pub mint_info: AccountLoader<'info, MintInfo>,
pub oracle: UncheckedAccount<'info>,
// Creating an address lookup table needs a recent valid slot as an
// input argument. That makes creating ALTs from governance instructions
// impossible. Hence the ALT that this instruction uses must be created
// externally and the admin is responsible for placing banks/oracles into
// sensible address lookup tables.
// constraint: must be created, have the admin authority and have free space
#[account(mut)]
pub address_lookup_table: UncheckedAccount<'info>, // TODO: wrapper?
#[account(mut)]
pub payer: Signer<'info>,
pub token_program: Program<'info, Token>,
pub system_program: Program<'info, System>,
pub address_lookup_table_program: UncheckedAccount<'info>, // TODO: force address?
pub rent: Sysvar<'info, Rent>,
}
@ -95,5 +115,29 @@ pub fn register_token(
token_index: token_index as TokenIndex,
};
let alt_previous_size =
address_lookup_table::addresses(&ctx.accounts.address_lookup_table.try_borrow_data()?)
.iter()
.count();
let mut mint_info = ctx.accounts.mint_info.load_init()?;
*mint_info = MintInfo {
mint: ctx.accounts.mint.key(),
vault: ctx.accounts.vault.key(),
oracle: ctx.accounts.oracle.key(),
bank: ctx.accounts.bank.key(),
address_lookup_table: ctx.accounts.address_lookup_table.key(),
address_lookup_table_bank_index: alt_previous_size as u8,
address_lookup_table_oracle_index: alt_previous_size as u8 + 1,
};
address_lookup_table::extend(
ctx.accounts.address_lookup_table.to_account_info(),
// TODO: is using the admin as ALT authority a good idea?
ctx.accounts.admin.to_account_info(),
ctx.accounts.payer.to_account_info(),
&[],
vec![ctx.accounts.bank.key(), ctx.accounts.oracle.key()],
)?;
Ok(())
}

View File

@ -40,12 +40,8 @@ pub mod mango_v4 {
)
}
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)
pub fn create_account(ctx: Context<CreateAccount>, account_num: u8) -> Result<()> {
instructions::create_account(ctx, account_num)
}
// todo:

View File

@ -0,0 +1,19 @@
use anchor_lang::prelude::*;
// This struct describes which address lookup table can be used to pass
// the accounts that are relevant for this mint. The idea is that clients
// can load this account to figure out which address maps to use when calling
// instructions that need banks/oracles for all active positions.
#[account(zero_copy)]
pub struct MintInfo {
// TODO: none of these pubkeys are needed, remove?
pub mint: Pubkey,
pub bank: Pubkey,
pub vault: Pubkey,
pub oracle: Pubkey,
// describe what address map relevant accounts are found on
pub address_lookup_table: Pubkey,
pub address_lookup_table_bank_index: u8,
pub address_lookup_table_oracle_index: u8,
}

View File

@ -1,6 +1,7 @@
pub use health::*;
pub use mango_account::*;
pub use mango_group::*;
pub use mint_info::*;
pub use oracle::*;
pub use token_bank::*;
@ -9,6 +10,7 @@ pub use token_bank::*;
mod health;
mod mango_account;
mod mango_group;
mod mint_info;
mod oracle;
mod token_bank;
// mod order_book_state_header;

View File

@ -92,13 +92,10 @@ impl<'keypair> ClientInstruction for WithdrawInstruction<'keypair> {
allow_borrow: self.allow_borrow,
};
// load account so we know its mint
// load accounts, find PDAs, find remainingAccounts
let token_account: TokenAccount = account_loader.load(&self.token_account).await.unwrap();
let account: MangoAccount = account_loader.load(&self.account).await.unwrap();
let lookup_table = account_loader
.load_bytes(&account.address_lookup_table)
.await
.unwrap();
let group: MangoGroup = account_loader.load(&account.group).await.unwrap();
let bank = Pubkey::find_program_address(
&[
@ -119,6 +116,30 @@ impl<'keypair> ClientInstruction for WithdrawInstruction<'keypair> {
)
.0;
// figure out all the banks/oracles that need to be passed for the health check
let mut banks = vec![];
let mut oracles = vec![];
for position in account.indexed_positions.iter_active() {
let mint_pk = group.tokens.infos[position.token_index as usize].mint;
let mint_info_pk = Pubkey::find_program_address(
&[
account.group.as_ref(),
b"mintinfo".as_ref(),
mint_pk.as_ref(),
],
&program_id,
)
.0;
let mint_info: MintInfo = account_loader.load(&mint_info_pk).await.unwrap();
let lookup_table = account_loader
.load_bytes(&mint_info.address_lookup_table)
.await
.unwrap();
let addresses = mango_v4::address_lookup_table::addresses(&lookup_table);
banks.push(addresses[mint_info.address_lookup_table_bank_index as usize]);
oracles.push(addresses[mint_info.address_lookup_table_oracle_index as usize]);
}
let accounts = Self::Accounts {
group: account.group,
account: self.account,
@ -130,15 +151,18 @@ impl<'keypair> ClientInstruction for WithdrawInstruction<'keypair> {
};
let mut instruction = make_instruction(program_id, &accounts, instruction);
instruction.accounts.extend(
mango_v4::address_lookup_table::addresses(&lookup_table)
.iter()
.map(|&pubkey| AccountMeta {
pubkey,
is_writable: false,
is_signer: false,
}),
);
instruction
.accounts
.extend(
banks
.iter()
.chain(oracles.iter())
.map(|&pubkey| AccountMeta {
pubkey,
is_writable: false,
is_signer: false,
}),
);
(accounts, instruction)
}
@ -196,11 +220,9 @@ impl<'keypair> ClientInstruction for DepositInstruction<'keypair> {
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::address_lookup_table::id(),
};
let instruction = make_instruction(program_id, &accounts, instruction);
@ -222,6 +244,7 @@ pub struct RegisterTokenInstruction<'keypair> {
pub group: Pubkey,
pub admin: &'keypair Keypair,
pub mint: Pubkey,
pub address_lookup_table: Pubkey,
pub payer: &'keypair Keypair,
}
#[async_trait::async_trait(?Send)]
@ -259,6 +282,15 @@ impl<'keypair> ClientInstruction for RegisterTokenInstruction<'keypair> {
&program_id,
)
.0;
let mint_info = Pubkey::find_program_address(
&[
self.group.as_ref(),
b"mintinfo".as_ref(),
self.mint.as_ref(),
],
&program_id,
)
.0;
let oracle = Pubkey::find_program_address(
&[b"stub_oracle".as_ref(), self.mint.as_ref()],
&program_id,
@ -271,10 +303,13 @@ impl<'keypair> ClientInstruction for RegisterTokenInstruction<'keypair> {
mint: self.mint,
bank,
vault,
mint_info,
oracle,
address_lookup_table: self.address_lookup_table,
payer: self.payer.pubkey(),
token_program: Token::id(),
system_program: System::id(),
address_lookup_table_program: mango_v4::address_lookup_table::id(),
rent: sysvar::rent::Rent::id(),
};
@ -403,7 +438,6 @@ 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,
@ -420,7 +454,6 @@ 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(
@ -433,19 +466,14 @@ impl<'keypair> ClientInstruction for CreateAccountInstruction<'keypair> {
&program_id,
)
.0;
let address_lookup_table =
mango_v4::address_lookup_table::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::address_lookup_table::id(),
};
let instruction = make_instruction(program_id, &accounts, instruction);

View File

@ -101,6 +101,23 @@ impl SolanaCookie {
return keypair.pubkey();
}
#[allow(dead_code)]
pub async fn create_address_lookup_table(
&self,
authority: &Keypair,
payer: &Keypair,
) -> Pubkey {
let (instruction, alt_address) = mango_v4::address_lookup_table::create_lookup_table(
authority.pubkey(),
payer.pubkey(),
0, // TODO: get a good recent slot value from a sysvar
);
self.process_transaction(&[instruction], Some(&[authority, payer]))
.await
.unwrap();
alt_address
}
#[allow(dead_code)]
pub async fn get_account_data(&self, address: Pubkey) -> Option<Vec<u8>> {
Some(

View File

@ -36,7 +36,6 @@ 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,
@ -68,6 +67,8 @@ async fn test_basic() -> Result<(), TransportError> {
.await
.unwrap();
let address_lookup_table = solana.create_address_lookup_table(admin, payer).await;
let register_token_accounts = send_tx(
solana,
RegisterTokenInstruction {
@ -79,6 +80,7 @@ async fn test_basic() -> Result<(), TransportError> {
group,
admin,
mint: mint0.pubkey,
address_lookup_table,
payer,
},
)