548 lines
15 KiB
Rust
548 lines
15 KiB
Rust
use crate::{dex, open_orders_authority, open_orders_init_authority, token};
|
|
use anchor_lang::prelude::*;
|
|
use anchor_lang::solana_program::instruction::Instruction;
|
|
use anchor_lang::solana_program::system_program;
|
|
use anchor_lang::Accounts;
|
|
use serum_dex::instruction::*;
|
|
use serum_dex::matching::Side;
|
|
use serum_dex::state::OpenOrders;
|
|
use std::mem::size_of;
|
|
|
|
/// Per request context. Can be used to share data between middleware handlers.
|
|
pub struct Context<'a, 'info> {
|
|
pub program_id: &'a Pubkey,
|
|
pub dex_program_id: &'a Pubkey,
|
|
pub accounts: Vec<AccountInfo<'info>>,
|
|
pub seeds: Seeds,
|
|
// Instructions to execute *prior* to the DEX relay CPI.
|
|
pub pre_instructions: Vec<(Instruction, Vec<AccountInfo<'info>>, Seeds)>,
|
|
// Instructions to execution *after* the DEX relay CPI.
|
|
pub post_instructions: Vec<(Instruction, Vec<AccountInfo<'info>>, Seeds)>,
|
|
}
|
|
|
|
type Seeds = Vec<Vec<Vec<u8>>>;
|
|
|
|
impl<'a, 'info> Context<'a, 'info> {
|
|
pub fn new(
|
|
program_id: &'a Pubkey,
|
|
dex_program_id: &'a Pubkey,
|
|
accounts: Vec<AccountInfo<'info>>,
|
|
) -> Self {
|
|
Self {
|
|
program_id,
|
|
dex_program_id,
|
|
accounts,
|
|
seeds: Vec::new(),
|
|
pre_instructions: Vec::new(),
|
|
post_instructions: Vec::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Implementing this trait allows one to hook into requests to the Serum DEX
|
|
/// via a frontend proxy.
|
|
pub trait MarketMiddleware {
|
|
/// Called before any instruction, giving middleware access to the raw
|
|
/// instruction data. This can be used to access extra data that is
|
|
/// prepended to the DEX data, allowing one to expand the capabilities of
|
|
/// any instruction by reading the instruction data here and then
|
|
/// using it in any of the method handlers.
|
|
fn instruction(&mut self, _data: &mut &[u8]) -> ProgramResult {
|
|
Ok(())
|
|
}
|
|
|
|
fn init_open_orders(&self, _ctx: &mut Context) -> ProgramResult {
|
|
Ok(())
|
|
}
|
|
|
|
fn new_order_v3(&self, _ctx: &mut Context, _ix: &NewOrderInstructionV3) -> ProgramResult {
|
|
Ok(())
|
|
}
|
|
|
|
fn cancel_order_v2(&self, _ctx: &mut Context, _ix: &CancelOrderInstructionV2) -> ProgramResult {
|
|
Ok(())
|
|
}
|
|
|
|
fn cancel_order_by_client_id_v2(&self, _ctx: &mut Context, _client_id: u64) -> ProgramResult {
|
|
Ok(())
|
|
}
|
|
|
|
fn settle_funds(&self, _ctx: &mut Context) -> ProgramResult {
|
|
Ok(())
|
|
}
|
|
|
|
fn close_open_orders(&self, _ctx: &mut Context) -> ProgramResult {
|
|
Ok(())
|
|
}
|
|
|
|
fn prune(&self, _ctx: &mut Context, _limit: u16) -> ProgramResult {
|
|
Ok(())
|
|
}
|
|
|
|
/// Called when the instruction data doesn't match any DEX instruction.
|
|
fn fallback(&self, _ctx: &mut Context) -> ProgramResult {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Checks that the given open orders account signs the transaction and then
|
|
/// replaces it with the open orders account, which must be a PDA.
|
|
#[derive(Default)]
|
|
pub struct OpenOrdersPda {
|
|
bump: u8,
|
|
bump_init: u8,
|
|
}
|
|
|
|
impl OpenOrdersPda {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
bump: 0,
|
|
bump_init: 0,
|
|
}
|
|
}
|
|
fn prepare_pda<'info>(acc_info: &AccountInfo<'info>) -> AccountInfo<'info> {
|
|
let mut acc_info = acc_info.clone();
|
|
acc_info.is_signer = true;
|
|
acc_info
|
|
}
|
|
}
|
|
|
|
impl MarketMiddleware for OpenOrdersPda {
|
|
fn instruction(&mut self, data: &mut &[u8]) -> ProgramResult {
|
|
// Strip the discriminator.
|
|
let disc = data[0];
|
|
*data = &data[1..];
|
|
|
|
// Discriminator == 0 implies it's the init instruction.
|
|
if disc == 0 {
|
|
self.bump = data[0];
|
|
self.bump_init = data[1];
|
|
*data = &data[2..];
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Accounts:
|
|
///
|
|
/// 0. Dex program.
|
|
/// 1. System program.
|
|
/// .. serum_dex::MarketInstruction::InitOpenOrders.
|
|
///
|
|
/// Data:
|
|
///
|
|
/// 0. Discriminant.
|
|
/// 1..2 Borsh(struct { bump: u8, bump_init: u8 }).
|
|
/// ..
|
|
fn init_open_orders<'a, 'info>(&self, ctx: &mut Context<'a, 'info>) -> ProgramResult {
|
|
let market = &ctx.accounts[4];
|
|
let user = &ctx.accounts[3];
|
|
|
|
// Initialize PDA.
|
|
let mut accounts = &ctx.accounts[..];
|
|
InitAccount::try_accounts(ctx.program_id, &mut accounts, &[self.bump, self.bump_init])?;
|
|
|
|
// Add signer to context.
|
|
ctx.seeds.push(open_orders_authority! {
|
|
program = ctx.program_id,
|
|
dex_program = ctx.dex_program_id,
|
|
market = market.key,
|
|
authority = user.key,
|
|
bump = self.bump
|
|
});
|
|
ctx.seeds.push(open_orders_init_authority! {
|
|
program = ctx.program_id,
|
|
dex_program = ctx.dex_program_id,
|
|
market = market.key,
|
|
bump = self.bump_init
|
|
});
|
|
|
|
// Chop off the first two accounts needed for initializing the PDA.
|
|
ctx.accounts = (&ctx.accounts[2..]).to_vec();
|
|
|
|
// Set PDAs.
|
|
ctx.accounts[1] = Self::prepare_pda(&ctx.accounts[0]);
|
|
ctx.accounts[4].is_signer = true;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Accounts:
|
|
///
|
|
/// ..
|
|
///
|
|
/// Data:
|
|
///
|
|
/// 0. Discriminant.
|
|
/// ..
|
|
fn new_order_v3(&self, ctx: &mut Context, ix: &NewOrderInstructionV3) -> ProgramResult {
|
|
// The user must authorize the tx.
|
|
let user = &ctx.accounts[7];
|
|
if !user.is_signer {
|
|
return Err(ErrorCode::UnauthorizedUser.into());
|
|
}
|
|
|
|
let market = &ctx.accounts[0];
|
|
let open_orders = &ctx.accounts[1];
|
|
let token_account_payer = &ctx.accounts[6];
|
|
|
|
// Pre: Give the PDA delegate access.
|
|
let pre_instruction = {
|
|
let amount = match ix.side {
|
|
Side::Bid => ix.max_native_pc_qty_including_fees.get(),
|
|
Side::Ask => {
|
|
// +5 for padding.
|
|
let coin_lot_idx = 5 + 43 * 8;
|
|
let data = market.try_borrow_data()?;
|
|
let mut coin_lot_array = [0u8; 8];
|
|
coin_lot_array.copy_from_slice(&data[coin_lot_idx..coin_lot_idx + 8]);
|
|
let coin_lot_size = u64::from_le_bytes(coin_lot_array);
|
|
ix.max_coin_qty.get().checked_mul(coin_lot_size).unwrap()
|
|
}
|
|
};
|
|
let ix = spl_token::instruction::approve(
|
|
&spl_token::ID,
|
|
token_account_payer.key,
|
|
open_orders.key,
|
|
user.key,
|
|
&[],
|
|
amount,
|
|
)?;
|
|
let accounts = vec![
|
|
token_account_payer.clone(),
|
|
open_orders.clone(),
|
|
user.clone(),
|
|
];
|
|
(ix, accounts, Vec::new())
|
|
};
|
|
ctx.pre_instructions.push(pre_instruction);
|
|
|
|
// Post: Revoke the PDA's delegate access.
|
|
let post_instruction = {
|
|
let ix = spl_token::instruction::revoke(
|
|
&spl_token::ID,
|
|
token_account_payer.key,
|
|
user.key,
|
|
&[],
|
|
)?;
|
|
let accounts = vec![token_account_payer.clone(), user.clone()];
|
|
(ix, accounts, Vec::new())
|
|
};
|
|
ctx.post_instructions.push(post_instruction);
|
|
|
|
// Proxy: PDA must sign the new order.
|
|
ctx.seeds.push(open_orders_authority! {
|
|
program = ctx.program_id,
|
|
dex_program = ctx.dex_program_id,
|
|
market = market.key,
|
|
authority = user.key
|
|
});
|
|
ctx.accounts[7] = Self::prepare_pda(open_orders);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Accounts:
|
|
///
|
|
/// ..
|
|
///
|
|
/// Data:
|
|
///
|
|
/// 0. Discriminant.
|
|
/// ..
|
|
fn cancel_order_v2(&self, ctx: &mut Context, _ix: &CancelOrderInstructionV2) -> ProgramResult {
|
|
let market = &ctx.accounts[0];
|
|
let user = &ctx.accounts[4];
|
|
if !user.is_signer {
|
|
return Err(ErrorCode::UnauthorizedUser.into());
|
|
}
|
|
|
|
ctx.seeds.push(open_orders_authority! {
|
|
program = ctx.program_id,
|
|
dex_program = ctx.dex_program_id,
|
|
market = market.key,
|
|
authority = user.key
|
|
});
|
|
|
|
ctx.accounts[4] = Self::prepare_pda(&ctx.accounts[3]);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Accounts:
|
|
///
|
|
/// ..
|
|
///
|
|
/// Data:
|
|
///
|
|
/// 0. Discriminant.
|
|
/// ..
|
|
fn cancel_order_by_client_id_v2(&self, ctx: &mut Context, _client_id: u64) -> ProgramResult {
|
|
let market = &ctx.accounts[0];
|
|
let user = &ctx.accounts[4];
|
|
if !user.is_signer {
|
|
return Err(ErrorCode::UnauthorizedUser.into());
|
|
}
|
|
|
|
ctx.seeds.push(open_orders_authority! {
|
|
program = ctx.program_id,
|
|
dex_program = ctx.dex_program_id,
|
|
market = market.key,
|
|
authority = user.key
|
|
});
|
|
|
|
ctx.accounts[4] = Self::prepare_pda(&ctx.accounts[3]);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Accounts:
|
|
///
|
|
/// ..
|
|
///
|
|
/// Data:
|
|
///
|
|
/// 0. Discriminant.
|
|
/// ..
|
|
fn settle_funds(&self, ctx: &mut Context) -> ProgramResult {
|
|
let market = &ctx.accounts[0];
|
|
let user = &ctx.accounts[2];
|
|
if !user.is_signer {
|
|
return Err(ErrorCode::UnauthorizedUser.into());
|
|
}
|
|
|
|
ctx.seeds.push(open_orders_authority! {
|
|
program = ctx.program_id,
|
|
dex_program = ctx.dex_program_id,
|
|
market = market.key,
|
|
authority = user.key
|
|
});
|
|
|
|
ctx.accounts[2] = Self::prepare_pda(&ctx.accounts[1]);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Accounts:
|
|
///
|
|
/// ..
|
|
///
|
|
/// Data:
|
|
///
|
|
/// 0. Discriminant.
|
|
/// ..
|
|
fn close_open_orders(&self, ctx: &mut Context) -> ProgramResult {
|
|
let market = &ctx.accounts[3];
|
|
let user = &ctx.accounts[1];
|
|
if !user.is_signer {
|
|
return Err(ErrorCode::UnauthorizedUser.into());
|
|
}
|
|
|
|
ctx.seeds.push(open_orders_authority! {
|
|
program = ctx.program_id,
|
|
dex_program = ctx.dex_program_id,
|
|
market = market.key,
|
|
authority = user.key
|
|
});
|
|
|
|
ctx.accounts[1] = Self::prepare_pda(&ctx.accounts[0]);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Accounts:
|
|
///
|
|
/// ..
|
|
///
|
|
/// Data:
|
|
///
|
|
/// 0. Discriminant.
|
|
/// ..
|
|
fn prune(&self, ctx: &mut Context, _limit: u16) -> ProgramResult {
|
|
// Set owner of open orders to be itself.
|
|
ctx.accounts[5] = ctx.accounts[4].clone();
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Logs each request.
|
|
pub struct Logger;
|
|
impl MarketMiddleware for Logger {
|
|
fn init_open_orders(&self, _ctx: &mut Context) -> ProgramResult {
|
|
msg!("proxying open orders");
|
|
Ok(())
|
|
}
|
|
|
|
fn new_order_v3(&self, _ctx: &mut Context, ix: &NewOrderInstructionV3) -> ProgramResult {
|
|
msg!("proxying new order v3 {:?}", ix);
|
|
Ok(())
|
|
}
|
|
|
|
fn cancel_order_v2(&self, _ctx: &mut Context, ix: &CancelOrderInstructionV2) -> ProgramResult {
|
|
msg!("proxying cancel order v2 {:?}", ix);
|
|
Ok(())
|
|
}
|
|
|
|
fn cancel_order_by_client_id_v2(&self, _ctx: &mut Context, client_id: u64) -> ProgramResult {
|
|
msg!("proxying cancel order by client id v2 {:?}", client_id);
|
|
Ok(())
|
|
}
|
|
|
|
fn settle_funds(&self, _ctx: &mut Context) -> ProgramResult {
|
|
msg!("proxying cancel order by client id v2");
|
|
Ok(())
|
|
}
|
|
|
|
fn close_open_orders(&self, _ctx: &mut Context) -> ProgramResult {
|
|
msg!("proxying close open orders");
|
|
Ok(())
|
|
}
|
|
|
|
fn prune(&self, _ctx: &mut Context, limit: u16) -> ProgramResult {
|
|
msg!("proxying prune {:?}", limit);
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Enforces referal fees being sent to the configured address.
|
|
pub struct ReferralFees {
|
|
referral: Pubkey,
|
|
}
|
|
|
|
impl ReferralFees {
|
|
pub fn new(referral: Pubkey) -> Self {
|
|
Self { referral }
|
|
}
|
|
}
|
|
|
|
impl MarketMiddleware for ReferralFees {
|
|
/// Accounts:
|
|
///
|
|
/// .. serum_dex::MarketInstruction::SettleFunds.
|
|
fn settle_funds(&self, ctx: &mut Context) -> ProgramResult {
|
|
let referral = token::accessor::authority(&ctx.accounts[9])?;
|
|
require!(referral == self.referral, ErrorCode::InvalidReferral);
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
// Macros.
|
|
|
|
/// Returns the seeds used for a user's open orders account PDA.
|
|
#[macro_export]
|
|
macro_rules! open_orders_authority {
|
|
(
|
|
program = $program:expr,
|
|
dex_program = $dex_program:expr,
|
|
market = $market:expr,
|
|
authority = $authority:expr,
|
|
bump = $bump:expr
|
|
) => {
|
|
vec![
|
|
b"open-orders".to_vec(),
|
|
$dex_program.as_ref().to_vec(),
|
|
$market.as_ref().to_vec(),
|
|
$authority.as_ref().to_vec(),
|
|
vec![$bump],
|
|
]
|
|
};
|
|
(
|
|
program = $program:expr,
|
|
dex_program = $dex_program:expr,
|
|
market = $market:expr,
|
|
authority = $authority:expr
|
|
) => {
|
|
vec![
|
|
b"open-orders".to_vec(),
|
|
$dex_program.as_ref().to_vec(),
|
|
$market.as_ref().to_vec(),
|
|
$authority.as_ref().to_vec(),
|
|
vec![
|
|
Pubkey::find_program_address(
|
|
&[
|
|
b"open-orders".as_ref(),
|
|
$dex_program.as_ref(),
|
|
$market.as_ref(),
|
|
$authority.as_ref(),
|
|
],
|
|
$program,
|
|
)
|
|
.1,
|
|
],
|
|
]
|
|
};
|
|
}
|
|
|
|
/// Returns the seeds used for the open orders init authority.
|
|
/// This is the account that must sign to create a new open orders account on
|
|
/// the DEX market.
|
|
#[macro_export]
|
|
macro_rules! open_orders_init_authority {
|
|
(
|
|
program = $program:expr,
|
|
dex_program = $dex_program:expr,
|
|
market = $market:expr,
|
|
bump = $bump:expr
|
|
) => {
|
|
vec![
|
|
b"open-orders-init".to_vec(),
|
|
$dex_program.as_ref().to_vec(),
|
|
$market.as_ref().to_vec(),
|
|
vec![$bump],
|
|
]
|
|
};
|
|
}
|
|
|
|
// Errors.
|
|
|
|
#[error(offset = 500)]
|
|
pub enum ErrorCode {
|
|
#[msg("Program ID does not match the Serum DEX")]
|
|
InvalidDexPid,
|
|
#[msg("Invalid instruction given")]
|
|
InvalidInstruction,
|
|
#[msg("Could not unpack the instruction")]
|
|
CannotUnpack,
|
|
#[msg("Invalid referral address given")]
|
|
InvalidReferral,
|
|
#[msg("The user didn't sign")]
|
|
UnauthorizedUser,
|
|
#[msg("Not enough accounts were provided")]
|
|
NotEnoughAccounts,
|
|
#[msg("Invalid target program ID")]
|
|
InvalidTargetProgram,
|
|
}
|
|
|
|
#[derive(Accounts)]
|
|
#[instruction(bump: u8, bump_init: u8)]
|
|
pub struct InitAccount<'info> {
|
|
#[account(address = dex::ID)]
|
|
pub dex_program: AccountInfo<'info>,
|
|
#[account(address = system_program::ID)]
|
|
pub system_program: AccountInfo<'info>,
|
|
#[account(
|
|
init,
|
|
seeds = [b"open-orders", dex_program.key.as_ref(), market.key.as_ref(), authority.key.as_ref()],
|
|
bump = bump,
|
|
payer = authority,
|
|
owner = dex::ID,
|
|
space = size_of::<OpenOrders>() + SERUM_PADDING,
|
|
)]
|
|
pub open_orders: AccountInfo<'info>,
|
|
#[account(signer)]
|
|
pub authority: AccountInfo<'info>,
|
|
pub market: AccountInfo<'info>,
|
|
pub rent: Sysvar<'info, Rent>,
|
|
#[account(
|
|
seeds = [b"open-orders-init", dex_program.key.as_ref(), market.key.as_ref()],
|
|
bump = bump_init,
|
|
)]
|
|
pub open_orders_init_authority: AccountInfo<'info>,
|
|
}
|
|
|
|
// Constants.
|
|
|
|
// Padding added to every serum account.
|
|
//
|
|
// b"serum".len() + b"padding".len().
|
|
const SERUM_PADDING: usize = 12;
|