Add payer accounts to pda-creation instructions (#119)

An extra account can be passed to
- create_mango_account
- create_spot_open_orders
which will take the lamport cost for creating the new account.

If you want to compose on mango and have a PDA account as an owner, it's
impossible to also use it as a payer because accounts with data cannot
pay for PDA creation.
This commit is contained in:
Christian Kamm 2022-01-27 17:40:58 +01:00 committed by GitHub
parent b206e3d7b3
commit 750d80b29f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 160 additions and 22 deletions

View File

@ -836,12 +836,13 @@ pub enum MangoInstruction {
/// Create a PDA mango account for a user
///
/// Accounts expected by this instruction (4):
/// Accounts expected by this instruction (5):
///
/// 0. `[writable]` mango_group_ai - MangoGroup that this mango account is for
/// 1. `[writable]` mango_account_ai - the mango account data
/// 2. `[signer]` owner_ai - Solana account of owner of the mango account
/// 3. `[]` system_prog_ai - System program
/// 4. `[signer, writable]` payer_ai - pays for the PDA creation
CreateMangoAccount {
account_num: u64,
},
@ -912,7 +913,7 @@ pub enum MangoInstruction {
/// Create an OpenOrders PDA and initialize it with InitOpenOrders call to serum dex
///
/// Accounts expected by this instruction (8):
/// Accounts expected by this instruction (9):
///
/// 0. `[]` mango_group_ai - MangoGroup that this mango account is for
/// 1. `[writable]` mango_account_ai - MangoAccount
@ -922,6 +923,7 @@ pub enum MangoInstruction {
/// 5. `[]` spot_market_ai - dex MarketState account
/// 6. `[]` signer_ai - Group Signer Account
/// 7. `[]` system_prog_ai - System program
/// 8. `[signer, writable]` payer_ai - pays for the PDA creation
CreateSpotOpenOrders, // instruction 60
}
@ -1539,13 +1541,15 @@ pub fn create_mango_account(
mango_account_pk: &Pubkey,
owner_pk: &Pubkey,
system_prog_pk: &Pubkey,
payer_pk: &Pubkey,
account_num: u64,
) -> Result<Instruction, ProgramError> {
let accounts = vec![
AccountMeta::new(*mango_group_pk, false),
AccountMeta::new(*mango_account_pk, false),
AccountMeta::new(*owner_pk, true),
AccountMeta::new_readonly(*owner_pk, true),
AccountMeta::new_readonly(*system_prog_pk, false),
AccountMeta::new(*payer_pk, true),
];
let instr = MangoInstruction::CreateMangoAccount { account_num };
@ -2221,16 +2225,18 @@ pub fn create_spot_open_orders(
open_orders_pk: &Pubkey,
spot_market_pk: &Pubkey,
signer_pk: &Pubkey,
payer_pk: &Pubkey,
) -> Result<Instruction, ProgramError> {
let accounts = vec![
AccountMeta::new_readonly(*mango_group_pk, false),
AccountMeta::new(*mango_account_pk, false),
AccountMeta::new(*owner_pk, true),
AccountMeta::new_readonly(*owner_pk, true),
AccountMeta::new_readonly(*dex_prog_pk, false),
AccountMeta::new(*open_orders_pk, false),
AccountMeta::new_readonly(*spot_market_pk, false),
AccountMeta::new_readonly(*signer_pk, false),
AccountMeta::new_readonly(solana_program::system_program::ID, false),
AccountMeta::new(*payer_pk, true),
];
let instr = MangoInstruction::CreateSpotOpenOrders;

View File

@ -736,7 +736,7 @@ impl Processor {
event_queue_ai, // write
bids_ai, // write
asks_ai, // write
mngo_mint_ai, // read
mngo_mint_ai, // read
mngo_vault_ai, // write
admin_ai, // signer (write if admin has SOL and no data)
signer_ai, // write (if admin has data and is owned by governance)
@ -1420,17 +1420,22 @@ impl Processor {
/// Call the init_open_orders instruction in serum dex and add this OpenOrders account to margin account
fn create_spot_open_orders(program_id: &Pubkey, accounts: &[AccountInfo]) -> MangoResult {
const NUM_FIXED: usize = 8;
let accounts = array_ref![accounts, 0, NUM_FIXED];
let fixed_accounts = array_ref![accounts, 0, NUM_FIXED];
let [
mango_group_ai, // read
mango_account_ai, // write
owner_ai, // read
owner_ai, // read (write if no payer passed) & signer
dex_prog_ai, // read
open_orders_ai, // write
spot_market_ai, // read
signer_ai, // read
system_prog_ai, // read
] = accounts;
] = fixed_accounts;
let payer_ai = if accounts.len() > NUM_FIXED {
&accounts[NUM_FIXED] // write & signer
} else {
owner_ai
};
check!(
system_prog_ai.key == &solana_program::system_program::id(),
MangoErrorCode::InvalidProgramId
@ -1451,13 +1456,14 @@ impl Processor {
MangoErrorCode::InvalidOwner
)?;
check!(owner_ai.is_signer, MangoErrorCode::InvalidSignerKey)?;
check!(payer_ai.is_signer, MangoErrorCode::InvalidSignerKey)?;
check!(!mango_account.is_bankrupt, MangoErrorCode::Bankrupt)?;
let open_orders_seeds: &[&[u8]] =
&[&mango_account_ai.key.as_ref(), &market_index.to_le_bytes(), b"OpenOrders"];
seed_and_create_pda(
program_id,
owner_ai,
payer_ai,
&Rent::get()?,
size_of::<serum_dex::state::OpenOrders>() + 12,
dex_prog_ai.key,
@ -5227,18 +5233,24 @@ impl Processor {
account_num: u64,
) -> MangoResult {
const NUM_FIXED: usize = 4;
let accounts = array_ref![accounts, 0, NUM_FIXED];
let fixed_accounts = array_ref![accounts, 0, NUM_FIXED];
let [
mango_group_ai, // write
mango_account_ai, // write
owner_ai, // write & signer
owner_ai, // read (write if no payer passed) & signer
system_prog_ai, // read
] = accounts;
] = fixed_accounts;
let payer_ai = if accounts.len() > NUM_FIXED {
&accounts[NUM_FIXED] // write & signer
} else {
owner_ai
};
check!(
system_prog_ai.key == &solana_program::system_program::id(),
MangoErrorCode::InvalidProgramId
)?;
check!(owner_ai.is_signer, MangoErrorCode::SignerNecessary)?;
check!(payer_ai.is_signer, MangoErrorCode::SignerNecessary)?;
let mut mango_group = MangoGroup::load_mut_checked(mango_group_ai, program_id)?;
check!(
@ -5249,10 +5261,9 @@ impl Processor {
let mango_account_seeds: &[&[u8]] =
&[&mango_group_ai.key.as_ref(), &owner_ai.key.as_ref(), &account_num.to_le_bytes()];
// TODO - test passing in MangoAccount that already has some data in it
seed_and_create_pda(
program_id,
owner_ai,
payer_ai,
&rent,
size_of::<MangoAccount>(),
program_id,

View File

@ -16,6 +16,7 @@ use solana_program::{
};
use solana_program_test::*;
use solana_sdk::{
account::ReadableAccount,
instruction::Instruction,
signature::{Keypair, Signer},
transaction::Transaction,
@ -404,6 +405,11 @@ impl MangoProgramTest {
return self.context.payer.pubkey();
}
#[allow(dead_code)]
pub async fn get_lamport_balance(&mut self, address: Pubkey) -> u64 {
self.context.banks_client.get_account(address).await.unwrap().unwrap().lamports()
}
#[allow(dead_code)]
pub async fn get_token_balance(&mut self, address: Pubkey) -> u64 {
let token = self.context.banks_client.get_account(address).await.unwrap().unwrap();
@ -1007,36 +1013,89 @@ impl MangoProgramTest {
open_orders_pk
}
#[allow(dead_code)]
pub async fn create_mango_account(
&mut self,
mango_group_pk: &Pubkey,
user_index: usize,
account_num: u64,
payer: Option<&Keypair>,
) -> Pubkey {
let owner_key = &self.users[user_index];
let owner_pk = owner_key.pubkey();
let seeds: &[&[u8]] =
&[&mango_group_pk.as_ref(), &owner_pk.as_ref(), &account_num.to_le_bytes()];
let (mango_account_pk, _) = Pubkey::find_program_address(seeds, &self.mango_program_id);
let mut instruction = create_mango_account(
&self.mango_program_id,
mango_group_pk,
&mango_account_pk,
&owner_pk,
&solana_sdk::system_program::id(),
&payer.map(|k| k.pubkey()).unwrap_or(owner_pk),
account_num,
)
.unwrap();
// Allow testing the compatibility case with no payer
if payer.is_none() {
instruction.accounts.pop();
instruction.accounts[2].is_writable = true; // owner pays lamports
}
let instructions = vec![instruction];
let owner_key_c = Keypair::from_base58_string(&owner_key.to_base58_string());
let mut signers = vec![&owner_key_c];
if let Some(payer_key) = payer {
signers.push(payer_key);
}
self.process_transaction(&instructions, Some(&signers)).await.unwrap();
mango_account_pk
}
#[allow(dead_code)]
pub async fn create_spot_open_orders(
&mut self,
mango_group_pk: &Pubkey,
mango_group: &MangoGroup,
mango_account_pk: &Pubkey,
mango_account: &MangoAccount,
user_index: usize,
market_index: usize,
payer: Option<&Keypair>,
) -> Pubkey {
let open_orders_seeds: &[&[u8]] =
&[&mango_account_pk.as_ref(), &market_index.to_le_bytes(), b"OpenOrders"];
let (open_orders_pk, _) =
Pubkey::find_program_address(open_orders_seeds, &self.mango_program_id);
let create_spot_open_orders_instruction = create_spot_open_orders(
let owner_key = &self.users[user_index];
let owner_pk = owner_key.pubkey();
let mut instruction = create_spot_open_orders(
&self.mango_program_id,
mango_group_pk,
mango_account_pk,
&mango_account.owner,
&owner_pk,
&self.serum_program_id,
&open_orders_pk,
&mango_group.spot_markets[market_index].spot_market,
&mango_group.signer_key,
&payer.map(|k| k.pubkey()).unwrap_or(owner_pk),
)
.unwrap();
let instructions = vec![create_spot_open_orders_instruction];
let user = Keypair::from_bytes(&self.users[user_index].to_bytes()).unwrap();
let signers = vec![&user];
// Allow testing the compatibility case with no payer
if payer.is_none() {
instruction.accounts.pop();
instruction.accounts[2].is_writable = true; // owner pays lamports
}
let instructions = vec![instruction];
let owner_key_c = Keypair::from_bytes(&owner_key.to_bytes()).unwrap();
let mut signers = vec![&owner_key_c];
if let Some(payer_key) = payer {
signers.push(payer_key);
}
self.process_transaction(&instructions, Some(&signers)).await.unwrap();
open_orders_pk
}
@ -1217,9 +1276,9 @@ impl MangoProgramTest {
&mango_group_pk,
&mango_group,
&mango_account_pk,
&mango_account,
user_index,
x,
None,
)
.await,
);
@ -1305,9 +1364,9 @@ impl MangoProgramTest {
&mango_group_pk,
&mango_group,
&mango_account_pk,
&mango_account,
user_index,
x,
None,
)
.await,
);

View File

@ -0,0 +1,62 @@
use solana_program_test::*;
use solana_sdk::signature::Signer;
use solana_sdk::signer::keypair::Keypair;
use mango::state::{MangoAccount, ZERO_I80F48};
use program_test::cookies::*;
use program_test::*;
mod program_test;
#[tokio::test]
async fn test_create_account() {
// === Arrange ===
let config = MangoProgramTestConfig { compute_limit: 200_000, num_users: 2, num_mints: 2 };
let mut test = MangoProgramTest::start_new(&config).await;
let mut mango_group_cookie = MangoGroupCookie::default(&mut test).await;
let num_precreated_mango_users = 0; // create manually
mango_group_cookie
.full_setup(&mut test, num_precreated_mango_users, config.num_mints - 1)
.await;
let mango_group_pk = &mango_group_cookie.address;
//
// paid for by owner (test.users[0])
//
let account0_pk = test.create_mango_account(mango_group_pk, 0, 0, None).await;
test.create_spot_open_orders(
mango_group_pk,
&mango_group_cookie.mango_group,
&account0_pk,
0,
0,
None,
)
.await;
//
// paid for by separate payer (test.users[1]) still owned by test.users[0]
//
let payer = Keypair::from_base58_string(&test.users[1].to_base58_string());
let payer_lamports = test.get_lamport_balance(payer.pubkey()).await;
let owner_lamports = test.get_lamport_balance(test.users[0].pubkey()).await;
let account1_pk = test.create_mango_account(mango_group_pk, 0, 1, Some(&payer)).await;
let account1 = test.load_account::<MangoAccount>(account1_pk).await;
assert_eq!(account1.owner, test.users[0].pubkey());
assert_eq!(test.get_lamport_balance(test.users[0].pubkey()).await, owner_lamports);
assert!(test.get_lamport_balance(payer.pubkey()).await < payer_lamports);
assert!(account0_pk != account1_pk);
test.create_spot_open_orders(
mango_group_pk,
&mango_group_cookie.mango_group,
&account1_pk,
0,
0,
Some(&payer),
)
.await;
assert_eq!(test.get_lamport_balance(test.users[0].pubkey()).await, owner_lamports);
}