Token deposit: Split into signed and permissionless ix

Token accounts are a limited resource, so allowing other users to make
use of them can cause problems.
This commit is contained in:
Christian Kamm 2022-09-30 13:21:01 +02:00
parent bafaf73745
commit b906e3dc78
16 changed files with 402 additions and 71 deletions

View File

@ -344,6 +344,7 @@ impl MangoClient {
&mango_v4::accounts::TokenDeposit {
group: self.group(),
account: self.mango_account_address,
owner: self.owner(),
bank: mint_info.first_bank(),
vault: mint_info.first_vault(),
oracle: mint_info.oracle,

View File

@ -11,8 +11,9 @@ use crate::util::checked_math as cm;
use crate::logs::{DepositLog, TokenBalanceLog};
// Same as TokenDeposit, but without the owner signing
#[derive(Accounts)]
pub struct TokenDeposit<'info> {
pub struct TokenDepositIntoExisting<'info> {
pub group: AccountLoader<'info, Group>,
#[account(mut, has_one = group)]
@ -41,8 +42,50 @@ pub struct TokenDeposit<'info> {
pub token_program: Program<'info, Token>,
}
impl<'info> TokenDeposit<'info> {
pub fn transfer_ctx(&self) -> CpiContext<'_, '_, '_, 'info, token::Transfer<'info>> {
#[derive(Accounts)]
pub struct TokenDeposit<'info> {
pub group: AccountLoader<'info, Group>,
#[account(mut, has_one = group, has_one = owner)]
pub account: AccountLoaderDynamic<'info, MangoAccount>,
pub owner: Signer<'info>,
#[account(
mut,
has_one = group,
has_one = vault,
has_one = oracle,
// 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>,
#[account(mut)]
pub vault: Account<'info, TokenAccount>,
/// CHECK: The oracle can be one of several different account types
pub oracle: UncheckedAccount<'info>,
#[account(mut)]
pub token_account: Box<Account<'info, TokenAccount>>,
pub token_authority: Signer<'info>,
pub token_program: Program<'info, Token>,
}
struct DepositCommon<'a, 'info> {
pub group: &'a AccountLoader<'info, Group>,
pub account: &'a AccountLoaderDynamic<'info, MangoAccount>,
pub bank: &'a AccountLoader<'info, Bank>,
pub vault: &'a Account<'info, TokenAccount>,
pub oracle: &'a UncheckedAccount<'info>,
pub token_account: &'a Box<Account<'info, TokenAccount>>,
pub token_authority: &'a Signer<'info>,
pub token_program: &'a Program<'info, Token>,
}
impl<'a, 'info> DepositCommon<'a, 'info> {
fn transfer_ctx(&self) -> CpiContext<'_, '_, '_, 'info, token::Transfer<'info>> {
let program = self.token_program.to_account_info();
let accounts = token::Transfer {
from: self.token_account.to_account_info(),
@ -51,78 +94,119 @@ impl<'info> TokenDeposit<'info> {
};
CpiContext::new(program, accounts)
}
fn deposit_into_existing(
&self,
remaining_accounts: &[AccountInfo],
amount: u64,
allow_token_account_closure: bool,
) -> Result<()> {
require_msg!(amount > 0, "deposit amount must be positive");
let token_index = self.bank.load()?.token_index;
// Get the account's position for that token index
let mut account = self.account.load_mut()?;
let (position, raw_token_index) = account.token_position_mut(token_index)?;
let amount_i80f48 = I80F48::from(amount);
let position_is_active = {
let mut bank = self.bank.load_mut()?;
bank.deposit(position, amount_i80f48)?
};
// Transfer the actual tokens
token::transfer(self.transfer_ctx(), amount)?;
let indexed_position = position.indexed_position;
let bank = self.bank.load()?;
let oracle_price = bank.oracle_price(&AccountInfoRef::borrow(self.oracle.as_ref())?)?;
// Update the net deposits - adjust by price so different tokens are on the same basis (in USD terms)
let amount_usd = cm!(amount_i80f48 * oracle_price).to_num::<i64>();
cm!(account.fixed.net_deposits += amount_usd);
emit!(TokenBalanceLog {
mango_group: self.group.key(),
mango_account: self.account.key(),
token_index,
indexed_position: indexed_position.to_bits(),
deposit_index: bank.deposit_index.to_bits(),
borrow_index: bank.borrow_index.to_bits(),
});
//
// Health computation
//
// Since depositing can only increase health, we can skip the usual pre-health computation.
// Also, TokenDeposit is one of the rare instructions that is allowed even during being_liquidated.
//
if !account.fixed.is_in_health_region() {
let retriever =
new_fixed_order_account_retriever(remaining_accounts, &account.borrow())?;
let health = compute_health(&account.borrow(), HealthType::Init, &retriever)
.context("post-deposit init health")?;
msg!("health: {}", health);
account.fixed.maybe_recover_from_being_liquidated(health);
}
//
// Deactivate the position only after the health check because the user passed in
// remaining_accounts for all banks/oracles, including the account that will now be
// deactivated.
// Deposits can deactivate a position if they cancel out a previous borrow.
//
if allow_token_account_closure && !position_is_active {
account.deactivate_token_position_and_log(raw_token_index, self.account.key());
}
emit!(DepositLog {
mango_group: self.group.key(),
mango_account: self.account.key(),
signer: self.token_authority.key(),
token_index,
quantity: amount,
price: oracle_price.to_bits(),
});
Ok(())
}
}
pub fn token_deposit(ctx: Context<TokenDeposit>, amount: u64) -> Result<()> {
require_msg!(amount > 0, "deposit amount must be positive");
let token_index = ctx.accounts.bank.load()?.token_index;
// Get the account's position for that token index
let mut account = ctx.accounts.account.load_mut()?;
let (position, raw_token_index, _active_token_index) =
{
let token_index = ctx.accounts.bank.load()?.token_index;
let mut account = ctx.accounts.account.load_mut()?;
account.ensure_token_position(token_index)?;
let amount_i80f48 = I80F48::from(amount);
let position_is_active = {
let mut bank = ctx.accounts.bank.load_mut()?;
bank.deposit(position, amount_i80f48)?
};
// Transfer the actual tokens
token::transfer(ctx.accounts.transfer_ctx(), amount)?;
let indexed_position = position.indexed_position;
let bank = ctx.accounts.bank.load()?;
let oracle_price = bank.oracle_price(&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?)?;
// Update the net deposits - adjust by price so different tokens are on the same basis (in USD terms)
let amount_usd = cm!(amount_i80f48 * oracle_price).to_num::<i64>();
cm!(account.fixed.net_deposits += amount_usd);
emit!(TokenBalanceLog {
mango_group: ctx.accounts.group.key(),
mango_account: ctx.accounts.account.key(),
token_index,
indexed_position: indexed_position.to_bits(),
deposit_index: bank.deposit_index.to_bits(),
borrow_index: bank.borrow_index.to_bits(),
});
//
// Health computation
//
// Since depositing can only increase health, we can skip the usual pre-health computation.
// Also, TokenDeposit is one of the rare instructions that is allowed even during being_liquidated.
//
if !account.fixed.is_in_health_region() {
let retriever =
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
let health = compute_health(&account.borrow(), HealthType::Init, &retriever)
.context("post-deposit init health")?;
msg!("health: {}", health);
account.fixed.maybe_recover_from_being_liquidated(health);
}
//
// Deactivate the position only after the health check because the user passed in
// remaining_accounts for all banks/oracles, including the account that will now be
// deactivated.
// Deposits can deactivate a position if they cancel out a previous borrow.
//
if !position_is_active {
account.deactivate_token_position_and_log(raw_token_index, ctx.accounts.account.key());
DepositCommon {
group: &ctx.accounts.group,
account: &ctx.accounts.account,
bank: &ctx.accounts.bank,
vault: &ctx.accounts.vault,
oracle: &ctx.accounts.oracle,
token_account: &ctx.accounts.token_account,
token_authority: &ctx.accounts.token_authority,
token_program: &ctx.accounts.token_program,
}
emit!(DepositLog {
mango_group: ctx.accounts.group.key(),
mango_account: ctx.accounts.account.key(),
signer: ctx.accounts.token_authority.key(),
token_index,
quantity: amount,
price: oracle_price.to_bits(),
});
Ok(())
.deposit_into_existing(ctx.remaining_accounts, amount, true)
}
pub fn token_deposit_into_existing(
ctx: Context<TokenDepositIntoExisting>,
amount: u64,
) -> Result<()> {
DepositCommon {
group: &ctx.accounts.group,
account: &ctx.accounts.account,
bank: &ctx.accounts.bank,
vault: &ctx.accounts.vault,
oracle: &ctx.accounts.oracle,
token_account: &ctx.accounts.token_account,
token_authority: &ctx.accounts.token_authority,
token_program: &ctx.accounts.token_program,
}
.deposit_into_existing(ctx.remaining_accounts, amount, false)
}

View File

@ -211,6 +211,13 @@ pub mod mango_v4 {
instructions::token_deposit(ctx, amount)
}
pub fn token_deposit_into_existing(
ctx: Context<TokenDepositIntoExisting>,
amount: u64,
) -> Result<()> {
instructions::token_deposit_into_existing(ctx, amount)
}
pub fn token_withdraw(
ctx: Context<TokenWithdraw>,
amount: u64,

View File

@ -564,6 +564,7 @@ pub struct TokenDepositInstruction {
pub amount: u64,
pub account: Pubkey,
pub owner: TestKeypair,
pub token_account: Pubkey,
pub token_authority: TestKeypair,
pub bank_index: usize,
@ -607,6 +608,76 @@ impl ClientInstruction for TokenDepositInstruction {
)
.await;
let accounts = Self::Accounts {
group: account.fixed.group,
account: self.account,
owner: self.owner.pubkey(),
bank: mint_info.banks[self.bank_index],
vault: mint_info.vaults[self.bank_index],
oracle: mint_info.oracle,
token_account: self.token_account,
token_authority: self.token_authority.pubkey(),
token_program: Token::id(),
};
let mut instruction = make_instruction(program_id, &accounts, instruction);
instruction.accounts.extend(health_check_metas.into_iter());
(accounts, instruction)
}
fn signers(&self) -> Vec<TestKeypair> {
vec![self.token_authority, self.owner]
}
}
pub struct TokenDepositIntoExistingInstruction {
pub amount: u64,
pub account: Pubkey,
pub token_account: Pubkey,
pub token_authority: TestKeypair,
pub bank_index: usize,
}
#[async_trait::async_trait(?Send)]
impl ClientInstruction for TokenDepositIntoExistingInstruction {
type Accounts = mango_v4::accounts::TokenDepositIntoExisting;
type Instruction = mango_v4::instruction::TokenDepositIntoExisting;
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 {
amount: self.amount,
};
// load account so we know its mint
let token_account: TokenAccount = account_loader.load(&self.token_account).await.unwrap();
let account = account_loader
.load_mango_account(&self.account)
.await
.unwrap();
let mint_info = Pubkey::find_program_address(
&[
b"MintInfo".as_ref(),
account.fixed.group.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(mint_info.banks[self.bank_index]),
false,
None,
)
.await;
let accounts = Self::Accounts {
group: account.fixed.group,
account: self.account,

View File

@ -178,6 +178,7 @@ pub async fn create_funded_account(
TokenDepositInstruction {
amount: amounts,
account,
owner,
token_account: payer.token_accounts[mint.index],
token_authority: payer.key,
bank_index,

View File

@ -59,6 +59,7 @@ async fn test_bankrupt_tokens_socialize_loss() -> Result<(), TransportError> {
TokenDepositInstruction {
amount: 10,
account: vault_account,
owner,
token_account: payer_mint_accounts[0],
token_authority: payer.clone(),
bank_index: 0,
@ -94,6 +95,7 @@ async fn test_bankrupt_tokens_socialize_loss() -> Result<(), TransportError> {
TokenDepositInstruction {
amount: deposit1_amount,
account,
owner,
token_account: payer_mint_accounts[2],
token_authority: payer.clone(),
bank_index: 0,
@ -106,6 +108,7 @@ async fn test_bankrupt_tokens_socialize_loss() -> Result<(), TransportError> {
TokenDepositInstruction {
amount: deposit2_amount,
account,
owner,
token_account: payer_mint_accounts[3],
token_authority: payer.clone(),
bank_index: 0,
@ -354,6 +357,7 @@ async fn test_bankrupt_tokens_insurance_fund() -> Result<(), TransportError> {
TokenDepositInstruction {
amount: vault_amount,
account: vault_account,
owner,
token_account,
token_authority: payer.clone(),
bank_index: 1,
@ -369,6 +373,7 @@ async fn test_bankrupt_tokens_insurance_fund() -> Result<(), TransportError> {
TokenDepositInstruction {
amount: 10,
account: vault_account,
owner,
token_account: payer_mint_accounts[0],
token_authority: payer.clone(),
bank_index: 0,
@ -404,6 +409,7 @@ async fn test_bankrupt_tokens_insurance_fund() -> Result<(), TransportError> {
TokenDepositInstruction {
amount: deposit1_amount,
account,
owner,
token_account: payer_mint_accounts[2],
token_authority: payer.clone(),
bank_index: 0,
@ -416,6 +422,7 @@ async fn test_bankrupt_tokens_insurance_fund() -> Result<(), TransportError> {
TokenDepositInstruction {
amount: deposit2_amount,
account,
owner,
token_account: payer_mint_accounts[3],
token_authority: payer.clone(),
bank_index: 0,

View File

@ -84,6 +84,7 @@ async fn test_basic() -> Result<(), TransportError> {
TokenDepositInstruction {
amount: deposit_amount,
account,
owner,
token_account: payer_mint0_account,
token_authority: payer.clone(),
bank_index: 0,

View File

@ -145,6 +145,7 @@ async fn test_health_compute_serum() -> Result<(), TransportError> {
TokenDepositInstruction {
amount: 10,
account,
owner,
token_account: payer_mint_accounts[0],
token_authority: payer.clone(),
bank_index: 0,
@ -264,6 +265,7 @@ async fn test_health_compute_perp() -> Result<(), TransportError> {
TokenDepositInstruction {
amount: 10,
account,
owner,
token_account: payer_mint_accounts[0],
token_authority: payer.clone(),
bank_index: 0,

View File

@ -63,6 +63,7 @@ async fn test_health_wrap() -> Result<(), TransportError> {
TokenDepositInstruction {
amount: 1,
account,
owner,
token_account: payer_mint_accounts[0],
token_authority: payer.clone(),
bank_index: 0,
@ -89,6 +90,7 @@ async fn test_health_wrap() -> Result<(), TransportError> {
tx.add_instruction(TokenDepositInstruction {
amount: repay_amount,
account,
owner,
token_account: payer_mint_accounts[1],
token_authority: payer.clone(),
bank_index: 0,

View File

@ -94,6 +94,7 @@ async fn test_liq_perps_force_cancel() -> Result<(), TransportError> {
TokenDepositInstruction {
amount: 1,
account,
owner,
token_account: payer_mint_accounts[1],
token_authority: payer,
bank_index: 0,
@ -307,6 +308,7 @@ async fn test_liq_perps_base_position_and_bankruptcy() -> Result<(), TransportEr
TokenDepositInstruction {
amount: 1,
account,
owner,
token_account: payer_mint_accounts[1],
token_authority: payer,
bank_index: 0,

View File

@ -233,6 +233,7 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
TokenDepositInstruction {
amount: 100000,
account: vault_account,
owner,
token_account,
token_authority: payer.clone(),
bank_index: 0,
@ -269,6 +270,7 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
TokenDepositInstruction {
amount: deposit1_amount,
account,
owner,
token_account: payer_mint_accounts[2],
token_authority: payer.clone(),
bank_index: 0,
@ -281,6 +283,7 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
TokenDepositInstruction {
amount: deposit2_amount,
account,
owner,
token_account: payer_mint_accounts[3],
token_authority: payer.clone(),
bank_index: 0,

View File

@ -89,6 +89,7 @@ async fn test_margin_trade() -> Result<(), BanksClientError> {
TokenDepositInstruction {
amount: deposit_amount_initial,
account,
owner,
token_account: payer_mint0_account,
token_authority: payer.clone(),
bank_index: 0,

View File

@ -80,6 +80,7 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> {
TokenDepositInstruction {
amount: deposit_amount,
account: account_0,
owner,
token_account: payer_mint_accounts[0],
token_authority: payer.clone(),
bank_index: 0,
@ -93,6 +94,7 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> {
TokenDepositInstruction {
amount: deposit_amount,
account: account_0,
owner,
token_account: payer_mint_accounts[1],
token_authority: payer.clone(),
bank_index: 0,
@ -110,6 +112,7 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> {
TokenDepositInstruction {
amount: deposit_amount,
account: account_1,
owner,
token_account: payer_mint_accounts[0],
token_authority: payer.clone(),
bank_index: 0,
@ -123,6 +126,7 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> {
TokenDepositInstruction {
amount: deposit_amount,
account: account_1,
owner,
token_account: payer_mint_accounts[1],
token_authority: payer.clone(),
bank_index: 0,

View File

@ -71,14 +71,30 @@ async fn test_position_lifetime() -> Result<()> {
{
let start_balance = solana.token_account_balance(payer_mint_accounts[0]).await;
// this activates the positions
let deposit_amount = 100;
// cannot deposit_into_existing if no token deposit exists
assert!(send_tx(
solana,
TokenDepositIntoExistingInstruction {
amount: deposit_amount,
account,
token_account: payer_mint_accounts[0],
token_authority: payer,
bank_index: 0,
}
)
.await
.is_err());
// this activates the positions
for &payer_token in payer_mint_accounts {
send_tx(
solana,
TokenDepositInstruction {
amount: deposit_amount,
account,
owner,
token_account: payer_token,
token_authority: payer.clone(),
bank_index: 0,
@ -88,6 +104,20 @@ async fn test_position_lifetime() -> Result<()> {
.unwrap();
}
// now depositing into an active account works
send_tx(
solana,
TokenDepositIntoExistingInstruction {
amount: deposit_amount,
account,
token_account: payer_mint_accounts[0],
token_authority: payer,
bank_index: 0,
},
)
.await
.unwrap();
// this closes the positions
for &payer_token in payer_mint_accounts {
send_tx(
@ -131,6 +161,7 @@ async fn test_position_lifetime() -> Result<()> {
TokenDepositInstruction {
amount: collateral_amount,
account,
owner,
token_account: payer_mint_accounts[0],
token_authority: payer.clone(),
bank_index: 0,
@ -167,6 +198,7 @@ async fn test_position_lifetime() -> Result<()> {
// deposit withdraw amount + some more to cover loan origination fees
amount: borrow_amount + 2,
account,
owner,
token_account: payer_mint_accounts[1],
token_authority: payer.clone(),
bank_index: 0,

View File

@ -809,6 +809,7 @@ export class MangoClient {
.accounts({
group: group.publicKey,
account: mangoAccount.publicKey,
owner: mangoAccount.owner,
bank: bank.publicKey,
vault: bank.vault,
oracle: bank.oracle,

View File

@ -1076,6 +1076,62 @@ export type MangoV4 = {
},
{
"name": "tokenDeposit",
"accounts": [
{
"name": "group",
"isMut": false,
"isSigner": false
},
{
"name": "account",
"isMut": true,
"isSigner": false
},
{
"name": "owner",
"isMut": false,
"isSigner": true
},
{
"name": "bank",
"isMut": true,
"isSigner": false
},
{
"name": "vault",
"isMut": true,
"isSigner": false
},
{
"name": "oracle",
"isMut": false,
"isSigner": false
},
{
"name": "tokenAccount",
"isMut": true,
"isSigner": false
},
{
"name": "tokenAuthority",
"isMut": false,
"isSigner": true
},
{
"name": "tokenProgram",
"isMut": false,
"isSigner": false
}
],
"args": [
{
"name": "amount",
"type": "u64"
}
]
},
{
"name": "tokenDepositIntoExisting",
"accounts": [
{
"name": "group",
@ -7282,6 +7338,62 @@ export const IDL: MangoV4 = {
},
{
"name": "tokenDeposit",
"accounts": [
{
"name": "group",
"isMut": false,
"isSigner": false
},
{
"name": "account",
"isMut": true,
"isSigner": false
},
{
"name": "owner",
"isMut": false,
"isSigner": true
},
{
"name": "bank",
"isMut": true,
"isSigner": false
},
{
"name": "vault",
"isMut": true,
"isSigner": false
},
{
"name": "oracle",
"isMut": false,
"isSigner": false
},
{
"name": "tokenAccount",
"isMut": true,
"isSigner": false
},
{
"name": "tokenAuthority",
"isMut": false,
"isSigner": true
},
{
"name": "tokenProgram",
"isMut": false,
"isSigner": false
}
],
"args": [
{
"name": "amount",
"type": "u64"
}
]
},
{
"name": "tokenDepositIntoExisting",
"accounts": [
{
"name": "group",