RegisterToken: Pass token_index explicitly

This guarantees stability when a gov proposal adds a new token and a new
market in one transaction. This way the RegisterMarket instruction knows
exactly what index the new token will have.

Previously, the new token index was generated automatically, meaning
you couldn't be sure what index a new token would get in advance.
This commit is contained in:
Christian Kamm 2022-03-14 13:19:50 +01:00
parent 2502f0ac1b
commit 1cd0f8d6be
13 changed files with 118 additions and 75 deletions

View File

@ -19,7 +19,8 @@ pub struct Deposit<'info> {
mut,
has_one = group,
has_one = vault,
constraint = bank.load()?.mint == token_account.mint,
// the mints of bank/vault/token_account are implicitly the same because
// spl::token::transfer succeeds between token_account and vault
)]
pub bank: AccountLoader<'info, Bank>,
@ -49,10 +50,7 @@ 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<()> {
// 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)?;
let token_index = ctx.accounts.bank.load()?.token_index as usize;
// Get the account's position for that token index
let mut account = ctx.accounts.account.load_mut()?;

View File

@ -2,6 +2,7 @@ use anchor_lang::prelude::*;
use anchor_spl::dex;
use anchor_spl::token::{Token, TokenAccount};
use dex::serum_dex;
use serum_dex::matching::Side;
use crate::error::*;
use crate::state::*;
@ -49,7 +50,8 @@ pub struct PlaceSerumOrder<'info> {
#[account(mut)]
pub market_quote_vault: UncheckedAccount<'info>,
// TODO: everything
// TODO: everything; do we need to pass both, or just payer?
// these are all potentially mut too, if we settle immediately?
pub quote_bank: AccountLoader<'info, Bank>,
pub quote_vault: Account<'info, TokenAccount>,
pub base_bank: AccountLoader<'info, Bank>,
@ -63,6 +65,11 @@ pub fn place_serum_order(
ctx: Context<PlaceSerumOrder>,
order: serum_dex::instruction::NewOrderInstructionV3,
) -> Result<()> {
let order_payer_token_account = match order.side {
Side::Ask => ctx.accounts.base_vault.to_account_info(),
Side::Bid => ctx.accounts.quote_vault.to_account_info(),
};
let context = CpiContext::new(
ctx.accounts.serum_program.to_account_info(),
dex::NewOrderV3 {
@ -80,8 +87,7 @@ pub fn place_serum_order(
// user accounts
open_orders: ctx.accounts.open_orders.to_account_info(),
open_orders_authority: ctx.accounts.serum_market.to_account_info(),
// TODO: vary based on bid or ask
order_payer_token_account: ctx.accounts.quote_vault.to_account_info(),
order_payer_token_account,
},
);

View File

@ -12,6 +12,7 @@ use crate::state::*;
const INDEX_START: I80F48 = I80F48!(1_000_000);
#[derive(Accounts)]
#[instruction(token_index: TokenIndex)]
pub struct RegisterToken<'info> {
#[account(
mut,
@ -24,7 +25,7 @@ pub struct RegisterToken<'info> {
#[account(
init,
seeds = [group.key().as_ref(), b"TokenBank".as_ref(), mint.key().as_ref()],
seeds = [group.key().as_ref(), b"TokenBank".as_ref(), &token_index.to_le_bytes()],
bump,
payer = payer,
space = 8 + std::mem::size_of::<Bank>(),
@ -33,7 +34,7 @@ pub struct RegisterToken<'info> {
#[account(
init,
seeds = [group.key().as_ref(), b"TokenVault".as_ref(), mint.key().as_ref()],
seeds = [group.key().as_ref(), b"TokenVault".as_ref(), &token_index.to_le_bytes()],
bump,
token::authority = group,
token::mint = mint,
@ -74,6 +75,7 @@ pub struct RegisterToken<'info> {
// overwriting config as long as the mint account stays the same?
pub fn register_token(
ctx: Context<RegisterToken>,
token_index: TokenIndex,
decimals: u8,
maint_asset_weight: f32,
init_asset_weight: f32,
@ -82,15 +84,7 @@ pub fn register_token(
) -> Result<()> {
let mut group = ctx.accounts.group.load_mut()?;
// TODO: Error if mint is already configured (techincally, init of vault will fail)
// TOOD: Error type
// TODO: Should be a function: Tokens::add() or so
let token_index = group
.tokens
.infos
.iter()
.position(|ti| !ti.is_valid())
.ok_or(MangoError::SomeError)?;
group.tokens.infos[token_index] = TokenInfo {
group.tokens.infos[token_index as usize] = TokenInfo {
mint: ctx.accounts.mint.key(),
decimals,
bank_bump: *ctx.bumps.get("bank").ok_or(MangoError::SomeError)?, // TODO: error
@ -113,7 +107,7 @@ pub fn register_token(
maint_liab_weight: I80F48::from_num(maint_liab_weight),
init_liab_weight: I80F48::from_num(init_liab_weight),
dust: I80F48::ZERO,
token_index: token_index as TokenIndex,
token_index,
};
let alt_previous_size =
@ -126,6 +120,7 @@ pub fn register_token(
vault: ctx.accounts.vault.key(),
oracle: ctx.accounts.oracle.key(),
bank: ctx.accounts.bank.key(),
token_index,
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,

View File

@ -22,7 +22,8 @@ pub struct Withdraw<'info> {
mut,
has_one = group,
has_one = vault,
constraint = bank.load()?.mint == token_account.mint,
// the mints of bank/vault/token_account are implicitly the same because
// spl::token::transfer succeeds between token_account and vault
)]
pub bank: AccountLoader<'info, Bank>,
@ -51,10 +52,10 @@ impl<'info> Withdraw<'info> {
// That would save a lot of computation that needs to go into finding the
// right index for the mint.
pub fn withdraw(ctx: Context<Withdraw>, amount: u64, allow_borrow: bool) -> Result<()> {
// 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 token_index = ctx.accounts.bank.load()?.token_index as usize;
// Get the account's position for that token index
let mut account = ctx.accounts.account.load_mut()?;

View File

@ -28,6 +28,7 @@ pub mod mango_v4 {
pub fn register_token(
ctx: Context<RegisterToken>,
token_index: TokenIndex,
decimals: u8,
maint_asset_weight: f32,
init_asset_weight: f32,
@ -36,6 +37,7 @@ pub mod mango_v4 {
) -> Result<()> {
instructions::register_token(
ctx,
token_index,
decimals,
maint_asset_weight,
init_asset_weight,

View File

@ -132,12 +132,7 @@ impl SerumOpenOrdersMap {
}
pub fn create(&mut self, market_index: SerumMarketIndex) -> Result<&mut SerumOpenOrders> {
if self
.values
.iter()
.find(|p| p.is_active_for_market(market_index))
.is_some()
{
if self.find(market_index).is_some() {
return err!(MangoError::SomeError); // exists already
}
if let Some(v) = self.values.iter_mut().find(|p| !p.is_active()) {
@ -158,6 +153,12 @@ impl SerumOpenOrdersMap {
pub fn iter_active(&self) -> impl Iterator<Item = &SerumOpenOrders> {
self.values.iter().filter(|p| p.is_active())
}
pub fn find(&self, market_index: SerumMarketIndex) -> Option<&SerumOpenOrders> {
self.values
.iter()
.find(|p| p.is_active_for_market(market_index))
}
}
#[account(zero_copy)]

View File

@ -1,5 +1,7 @@
use anchor_lang::prelude::*;
use super::TokenIndex;
// 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
@ -11,6 +13,7 @@ pub struct MintInfo {
pub bank: Pubkey,
pub vault: Pubkey,
pub oracle: Pubkey,
pub token_index: TokenIndex,
// describe what address map relevant accounts are found on
pub address_lookup_table: Pubkey,

View File

@ -234,30 +234,21 @@ impl<'keypair> ClientInstruction for WithdrawInstruction<'keypair> {
// 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 bank = Pubkey::find_program_address(
let mint_info = Pubkey::find_program_address(
&[
account.group.as_ref(),
b"TokenBank".as_ref(),
token_account.mint.as_ref(),
],
&program_id,
)
.0;
let vault = Pubkey::find_program_address(
&[
account.group.as_ref(),
b"TokenVault".as_ref(),
b"MintInfo".as_ref(),
token_account.mint.as_ref(),
],
&program_id,
)
.0;
let mint_info: MintInfo = account_loader.load(&mint_info).await.unwrap();
let health_check_metas = derive_health_check_remaining_account_metas(
&account_loader,
&account,
Some(bank),
Some(mint_info.bank),
false,
)
.await;
@ -266,8 +257,8 @@ impl<'keypair> ClientInstruction for WithdrawInstruction<'keypair> {
group: account.group,
account: self.account,
owner: self.owner.pubkey(),
bank,
vault,
bank: mint_info.bank,
vault: mint_info.vault,
token_account: self.token_account,
token_program: Token::id(),
};
@ -306,30 +297,21 @@ 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(
let mint_info = Pubkey::find_program_address(
&[
account.group.as_ref(),
b"TokenBank".as_ref(),
token_account.mint.as_ref(),
],
&program_id,
)
.0;
let vault = Pubkey::find_program_address(
&[
account.group.as_ref(),
b"TokenVault".as_ref(),
b"MintInfo".as_ref(),
token_account.mint.as_ref(),
],
&program_id,
)
.0;
let mint_info: MintInfo = account_loader.load(&mint_info).await.unwrap();
let health_check_metas = derive_health_check_remaining_account_metas(
&account_loader,
&account,
Some(bank),
Some(mint_info.bank),
false,
)
.await;
@ -337,8 +319,8 @@ impl<'keypair> ClientInstruction for DepositInstruction<'keypair> {
let accounts = Self::Accounts {
group: account.group,
account: self.account,
bank,
vault,
bank: mint_info.bank,
vault: mint_info.vault,
token_account: self.token_account,
token_authority: self.token_authority.pubkey(),
token_program: Token::id(),
@ -356,6 +338,7 @@ impl<'keypair> ClientInstruction for DepositInstruction<'keypair> {
}
pub struct RegisterTokenInstruction<'keypair> {
pub token_index: TokenIndex,
pub decimals: u8,
pub maint_asset_weight: f32,
pub init_asset_weight: f32,
@ -378,6 +361,7 @@ impl<'keypair> ClientInstruction for RegisterTokenInstruction<'keypair> {
) -> (Self::Accounts, instruction::Instruction) {
let program_id = mango_v4::id();
let instruction = Self::Instruction {
token_index: self.token_index,
decimals: self.decimals,
maint_asset_weight: self.maint_asset_weight,
init_asset_weight: self.init_asset_weight,
@ -389,7 +373,7 @@ impl<'keypair> ClientInstruction for RegisterTokenInstruction<'keypair> {
&[
self.group.as_ref(),
b"TokenBank".as_ref(),
self.mint.as_ref(),
&self.token_index.to_le_bytes(),
],
&program_id,
)
@ -398,7 +382,7 @@ impl<'keypair> ClientInstruction for RegisterTokenInstruction<'keypair> {
&[
self.group.as_ref(),
b"TokenVault".as_ref(),
self.mint.as_ref(),
&self.token_index.to_le_bytes(),
],
&program_id,
)
@ -708,3 +692,46 @@ impl<'keypair> ClientInstruction for CreateSerumOpenOrdersInstruction<'keypair>
vec![self.owner, self.payer]
}
}
/*
pub struct PlaceSerumOrderInstruction<'keypair> {
pub account: Pubkey,
pub serum_market: Pubkey,
pub owner: &'keypair Keypair,
}
#[async_trait::async_trait(?Send)]
impl<'keypair> ClientInstruction for PlaceSerumOrderInstruction<'keypair> {
type Accounts = mango_v4::accounts::PlaceSerumOrder;
type Instruction = mango_v4::instruction::PlaceSerumOrder;
async fn to_instruction(
&self,
account_loader: impl ClientAccountLoader + 'async_trait,
) -> (Self::Accounts, instruction::Instruction) {
let program_id = mango_v4::id();
let instruction = Self::Instruction {};
let account: MangoAccount = account_loader.load(&self.account).await.unwrap();
let serum_market: SerumMarket = account_loader.load(&self.serum_market).await.unwrap();
let open_orders = account.serum_open_orders_map.find(serum_market.market_index).unwrap().open_orders;
let accounts = Self::Accounts {
group: account.group,
account: self.account,
open_orders,
serum_market: self.serum_market,
serum_program: serum_market.serum_program,
serum_market_external: serum_market.serum_market_external,
owner: self.owner.pubkey(),
payer: self.payer.pubkey(),
system_program: System::id(),
rent: sysvar::rent::Rent::id(),
};
let instruction = make_instruction(program_id, &accounts, instruction);
(accounts, instruction)
}
fn signers(&self) -> Vec<&Keypair> {
vec![self.owner]
}
}*/

View File

@ -72,6 +72,7 @@ async fn test_basic() -> Result<(), TransportError> {
let register_token_accounts = send_tx(
solana,
RegisterTokenInstruction {
token_index: 0,
decimals: mint0.decimals,
maint_asset_weight: 0.9,
init_asset_weight: 0.8,

View File

@ -5,6 +5,7 @@ use solana_program_test::*;
use solana_sdk::signature::Keypair;
use mango_v4::address_lookup_table;
use mango_v4::state::*;
use program_test::*;
mod program_test;
@ -51,7 +52,7 @@ async fn test_group_address_lookup_tables() -> Result<()> {
// SETUP: Register three mints (and make oracles for them)
//
let register_mint = |mint: MintCookie, address_lookup_table: Pubkey| async move {
let register_mint = |index: TokenIndex, mint: MintCookie, address_lookup_table: Pubkey| async move {
let create_stub_oracle_accounts = send_tx(
solana,
CreateStubOracle {
@ -75,6 +76,7 @@ async fn test_group_address_lookup_tables() -> Result<()> {
let register_token_accounts = send_tx(
solana,
RegisterTokenInstruction {
token_index: index,
decimals: mint.decimals,
maint_asset_weight: 0.9,
init_asset_weight: 0.8,
@ -100,9 +102,9 @@ async fn test_group_address_lookup_tables() -> Result<()> {
solana.advance_by_slots(1).await; // to get a different address
let address_lookup_table2 = solana.create_address_lookup_table(admin, payer).await;
let (oracle0, bank0) = register_mint(mint0.clone(), address_lookup_table1).await;
let (oracle1, bank1) = register_mint(mint1.clone(), address_lookup_table1).await;
let (oracle2, bank2) = register_mint(mint2.clone(), address_lookup_table2).await;
let (oracle0, bank0) = register_mint(0, mint0.clone(), address_lookup_table1).await;
let (oracle1, bank1) = register_mint(1, mint1.clone(), address_lookup_table1).await;
let (oracle2, bank2) = register_mint(2, mint2.clone(), address_lookup_table2).await;
// check the resulting address maps
let data = solana

View File

@ -87,6 +87,7 @@ async fn test_margin_trade() -> Result<(), TransportError> {
let register_token_accounts = send_tx(
solana,
RegisterTokenInstruction {
token_index: 0,
decimals: mint0.decimals,
maint_asset_weight: 0.9,
init_asset_weight: 0.8,

View File

@ -65,7 +65,7 @@ async fn test_position_lifetime() -> Result<()> {
let address_lookup_table = solana.create_address_lookup_table(admin, payer).await;
let register_mint = |mint: MintCookie| async move {
let register_mint = |index: TokenIndex, mint: MintCookie| async move {
let create_stub_oracle_accounts = send_tx(
solana,
CreateStubOracle {
@ -89,6 +89,7 @@ async fn test_position_lifetime() -> Result<()> {
let register_token_accounts = send_tx(
solana,
RegisterTokenInstruction {
token_index: index,
decimals: mint.decimals,
maint_asset_weight: 0.9,
init_asset_weight: 0.8,
@ -107,9 +108,9 @@ async fn test_position_lifetime() -> Result<()> {
(oracle, bank)
};
register_mint(mint0.clone()).await;
register_mint(mint1.clone()).await;
register_mint(mint2.clone()).await;
register_mint(0, mint0.clone()).await;
register_mint(1, mint1.clone()).await;
register_mint(2, mint2.clone()).await;
//
// SETUP: Put some tokens into the funding account to allow borrowing

View File

@ -51,7 +51,7 @@ async fn test_serum() -> Result<(), TransportError> {
// SETUP: Register mints (and make oracles for them)
//
let register_mint = |mint: MintCookie, address_lookup_table: Pubkey| async move {
let register_mint = |index: TokenIndex, mint: MintCookie, address_lookup_table: Pubkey| async move {
let create_stub_oracle_accounts = send_tx(
solana,
CreateStubOracle {
@ -75,6 +75,7 @@ async fn test_serum() -> Result<(), TransportError> {
let register_token_accounts = send_tx(
solana,
RegisterTokenInstruction {
token_index: index,
decimals: mint.decimals,
maint_asset_weight: 0.9,
init_asset_weight: 0.8,
@ -95,8 +96,12 @@ async fn test_serum() -> Result<(), TransportError> {
};
let address_lookup_table = solana.create_address_lookup_table(admin, payer).await;
let (_oracle0, _bank0) = register_mint(mint0.clone(), address_lookup_table).await;
let (_oracle1, _bank1) = register_mint(mint1.clone(), address_lookup_table).await;
let base_token_index = 0;
let (_oracle0, _bank0) =
register_mint(base_token_index, mint0.clone(), address_lookup_table).await;
let quote_token_index = 1;
let (_oracle1, _bank1) =
register_mint(quote_token_index, mint1.clone(), address_lookup_table).await;
//
// TEST: Register a serum market
@ -108,8 +113,8 @@ async fn test_serum() -> Result<(), TransportError> {
admin,
serum_program: context.serum.program_id,
serum_market_external: serum_market_cookie.market,
base_token_index: 0, // TODO: better way of getting these numbers
quote_token_index: 1,
base_token_index,
quote_token_index,
payer,
},
)