diff --git a/Cargo.lock b/Cargo.lock index 13511c92cf..ba5a81d3fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2811,7 +2811,7 @@ dependencies = [ [[package]] name = "serum_dex" version = "0.3.1" -source = "git+https://github.com/project-serum/serum-dex?tag=v0.3.1#7d1d41538417aa8721aabea9503bf9d99eab7cc4" +source = "git+https://github.com/project-serum/serum-dex?branch=armani/auth#2037a646f82e689f8e7a00c8a34b30e20253ba11" dependencies = [ "arrayref", "bincode", diff --git a/examples/permissioned-markets/Anchor.toml b/examples/permissioned-markets/Anchor.toml index 5cd9fd82f0..959ea7b4c7 100644 --- a/examples/permissioned-markets/Anchor.toml +++ b/examples/permissioned-markets/Anchor.toml @@ -2,12 +2,6 @@ cluster = "localnet" wallet = "~/.config/solana/id.json" -[scripts] -test = "anchor run build && anchor test" -build = "anchor run build-deps && anchor build" -build-deps = "anchor run build-dex" -build-dex = "pushd deps/serum-dex/dex/ && cargo build-bpf && popd" - [[test.genesis]] address = "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin" program = "./deps/serum-dex/dex/target/deploy/serum_dex.so" diff --git a/examples/permissioned-markets/Makefile b/examples/permissioned-markets/Makefile new file mode 100644 index 0000000000..5a934bfc52 --- /dev/null +++ b/examples/permissioned-markets/Makefile @@ -0,0 +1,13 @@ +.PHONY: test build build-deps build-dex + +test: build + anchor test + +build: build-dex + anchor build + +build-dex: + cd deps/serum-dex/dex/ && cargo build-bpf && cd ../../../ + +localnet: + ./scripts/localnet.sh diff --git a/examples/permissioned-markets/deps/serum-dex b/examples/permissioned-markets/deps/serum-dex index 1f6d586701..2037a646f8 160000 --- a/examples/permissioned-markets/deps/serum-dex +++ b/examples/permissioned-markets/deps/serum-dex @@ -1 +1 @@ -Subproject commit 1f6d5867019e242a470deed79cddca0d1f15e0a3 +Subproject commit 2037a646f82e689f8e7a00c8a34b30e20253ba11 diff --git a/examples/permissioned-markets/programs/permissioned-markets-middleware/Cargo.toml b/examples/permissioned-markets/programs/permissioned-markets-middleware/Cargo.toml new file mode 100644 index 0000000000..ac4cbba0d4 --- /dev/null +++ b/examples/permissioned-markets/programs/permissioned-markets-middleware/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "permissioned-markets-middleware" +version = "0.1.0" +description = "Created with Anchor" +edition = "2018" + +[lib] +crate-type = ["cdylib", "lib"] +name = "permissioned_markets_middleware" + +[features] +no-entrypoint = [] +no-idl = [] +cpi = ["no-entrypoint"] +default = [] + +[dependencies] +anchor-lang = { path = "../../../../lang" } +anchor-spl = { path = "../../../../spl" } +solana-program = "1.7.4" diff --git a/examples/permissioned-markets/programs/permissioned-markets-middleware/Xargo.toml b/examples/permissioned-markets/programs/permissioned-markets-middleware/Xargo.toml new file mode 100644 index 0000000000..475fb71ed1 --- /dev/null +++ b/examples/permissioned-markets/programs/permissioned-markets-middleware/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] diff --git a/examples/permissioned-markets/programs/permissioned-markets-middleware/src/lib.rs b/examples/permissioned-markets/programs/permissioned-markets-middleware/src/lib.rs new file mode 100644 index 0000000000..2140566517 --- /dev/null +++ b/examples/permissioned-markets/programs/permissioned-markets-middleware/src/lib.rs @@ -0,0 +1,155 @@ +// Note. This example depends on unreleased Serum DEX changes. + +use anchor_lang::prelude::*; +use anchor_spl::dex::serum_dex::instruction::{CancelOrderInstructionV2, NewOrderInstructionV3}; +use anchor_spl::dex::{ + Context, Logger, MarketMiddleware, MarketProxy, OpenOrdersPda, ReferralFees, +}; +use solana_program::account_info::AccountInfo; +use solana_program::entrypoint::ProgramResult; +use solana_program::pubkey::Pubkey; +use solana_program::sysvar::rent; + +/// # Permissioned Markets +/// +/// This demonstrates how to create "permissioned markets" on Serum via a proxy. +/// A permissioned market is a regular Serum market with an additional +/// open orders authority, which must sign every transaction to create or +/// close an open orders account. +/// +/// In practice, what this means is that one can create a program that acts +/// as this authority *and* that marks its own PDAs as the *owner* of all +/// created open orders accounts, making the program the sole arbiter over +/// who can trade on a given market. +/// +/// For example, this example forces all trades that execute on this market +/// to set the referral to a hardcoded address--`referral::ID`--and requires +/// the client to pass in an identity token, authorizing the user. +/// +/// # Extending the proxy via middleware +/// +/// To implement a custom proxy, one can implement the `MarketMiddleware` trait +/// to intercept, modify, and perform any access control on DEX requests before +/// they get forwarded to the orderbook. These middleware can be mixed and +/// matched. Note, however, that the order of middleware matters since they can +/// mutate the request. +/// +/// One useful pattern is to treat the request like layers of an onion, where +/// each middleware unwraps the request by stripping accounts and instruction +/// data before relaying it to the next middleware and ultimately to the +/// orderbook. This allows one to easily extend the behavior of a proxy by +/// adding a custom middleware that may process information that is unknown to +/// any other middleware or to the DEX. +/// +/// After adding a middleware, the only additional requirement, of course, is +/// to make sure the client sending transactions does the same, but in reverse. +/// It should wrap the transaction in the opposite order. For convenience, an +/// identical abstraction is provided in the JavaScript client. +/// +/// # Alternatives to middleware +/// +/// Note that this middleware abstraction is not required to host a +/// permissioned market. One could write a regular program that manages the PDAs +/// and CPI invocations onesself, if desired. +#[program] +pub mod permissioned_markets_middleware { + use super::*; + pub fn entry(program_id: &Pubkey, accounts: &[AccountInfo], data: &[u8]) -> ProgramResult { + MarketProxy::new() + .middleware(&mut Logger) + .middleware(&mut Identity) + .middleware(&mut ReferralFees::new(referral::ID)) + .middleware(&mut OpenOrdersPda::new()) + .run(program_id, accounts, data) + } +} + +/// Performs token based authorization, confirming the identity of the user. +/// The identity token must be given as the fist account. +struct Identity; + +impl MarketMiddleware for Identity { + /// Accounts: + /// + /// 0. Authorization token. + /// .. + fn init_open_orders(&self, ctx: &mut Context) -> ProgramResult { + verify_and_strip_auth(ctx) + } + + /// Accounts: + /// + /// 0. Authorization token. + /// .. + fn new_order_v3(&self, ctx: &mut Context, _ix: &NewOrderInstructionV3) -> ProgramResult { + verify_and_strip_auth(ctx) + } + + /// Accounts: + /// + /// 0. Authorization token. + /// .. + fn cancel_order_v2(&self, ctx: &mut Context, _ix: &CancelOrderInstructionV2) -> ProgramResult { + verify_and_strip_auth(ctx) + } + + /// Accounts: + /// + /// 0. Authorization token. + /// .. + fn cancel_order_by_client_id_v2(&self, ctx: &mut Context, _client_id: u64) -> ProgramResult { + verify_and_strip_auth(ctx) + } + + /// Accounts: + /// + /// 0. Authorization token. + /// .. + fn settle_funds(&self, ctx: &mut Context) -> ProgramResult { + verify_and_strip_auth(ctx) + } + + /// Accounts: + /// + /// 0. Authorization token. + /// .. + fn close_open_orders(&self, ctx: &mut Context) -> ProgramResult { + verify_and_strip_auth(ctx) + } + + /// Accounts: + /// + /// 0. Authorization token. + /// .. + fn fallback(&self, ctx: &mut Context) -> ProgramResult { + verify_and_strip_auth(ctx) + } +} + +// Utils. + +fn verify_and_strip_auth(ctx: &mut Context) -> ProgramResult { + // The rent sysvar is used as a dummy example of an identity token. + let auth = &ctx.accounts[0]; + require!(auth.key == &rent::ID, InvalidAuth); + + // Strip off the account before possing on the message. + ctx.accounts = (&ctx.accounts[1..]).to_vec(); + + Ok(()) +} + +// Error. + +#[error] +pub enum ErrorCode { + #[msg("Invalid auth token provided")] + InvalidAuth, +} + +// Constants. + +pub mod referral { + // This is a dummy address for testing. Do not use in production. + solana_program::declare_id!("3oSfkjQZKCneYvsCTZc9HViGAPqR8pYr4h9YeGB5ZxHf"); +} diff --git a/examples/permissioned-markets/programs/permissioned-markets/Cargo.toml b/examples/permissioned-markets/programs/permissioned-markets/Cargo.toml index b8eb761df5..034b9d19e2 100644 --- a/examples/permissioned-markets/programs/permissioned-markets/Cargo.toml +++ b/examples/permissioned-markets/programs/permissioned-markets/Cargo.toml @@ -18,4 +18,5 @@ default = [] anchor-lang = { path = "../../../../lang" } anchor-spl = { path = "../../../../spl" } serum_dex = { path = "../../deps/serum-dex/dex", features = ["no-entrypoint"] } -solana-program = "1.7.4" \ No newline at end of file +solana-program = "1.7.4" +spl-token = { version = "3.1.1", features = ["no-entrypoint"] } diff --git a/examples/permissioned-markets/programs/permissioned-markets/src/lib.rs b/examples/permissioned-markets/programs/permissioned-markets/src/lib.rs index 46be416ca4..a8e7161b01 100644 --- a/examples/permissioned-markets/programs/permissioned-markets/src/lib.rs +++ b/examples/permissioned-markets/programs/permissioned-markets/src/lib.rs @@ -3,79 +3,112 @@ use anchor_lang::prelude::*; use anchor_spl::dex; use serum_dex::instruction::MarketInstruction; +use serum_dex::matching::Side; use serum_dex::state::OpenOrders; use solana_program::instruction::Instruction; use solana_program::program; use solana_program::system_program; +use solana_program::sysvar::rent; use std::mem::size_of; -/// This demonstrates how to create "permissioned markets" on Serum. A -/// permissioned market is a regular Serum market with an additional -/// open orders authority, which must sign every transaction to create or -/// close an open orders account. +/// A low level example of permissioned markets. /// -/// In practice, what this means is that one can create a program that acts -/// as this authority *and* that marks its own PDAs as the *owner* of all -/// created open orders accounts, making the program the sole arbiter over -/// who can trade on a given market. +/// It's recommended to instead study `programs/permissioned-markets-middleware` +/// in this workspace, which achieves the same functionality in a simpler, more +/// extendable fashion via a middleware abstraction. This program achieves +/// mostly the same proxy + middleware functionality, but in a much uglier way. /// -/// For example, this example forces all trades that execute on this market -/// to set the referral to a hardcoded address, i.e., `fee_owner::ID`. +/// This example is provided as a (very) rough guide for how to might implement +/// a permissioned market in a raw program, which may be useful in the +/// unexpected case that the middleware abstraction does not fit ones use case. +/// +/// Note that a fallback function is used here as the entrypoint instead of +/// higher level Anchor instruction handers. This is done to keep the example +/// consistent with `programs/permissioned-markets-middleware`. A program +/// with explicit instruction handlers would work, though then one would lose +/// the middleware abstraction, which may or may not be acceptibl depending on +/// your use case. #[program] pub mod permissioned_markets { use super::*; - /// Creates an open orders account controlled by this program on behalf of - /// the user. - /// - /// Note that although the owner of the open orders account is the dex - /// program, This instruction must be executed within this program, rather - /// than a relay, because it initializes a PDA. - pub fn init_account(ctx: Context, bump: u8, bump_init: u8) -> Result<()> { - let cpi_ctx = CpiContext::from(&*ctx.accounts); - let seeds = open_orders_authority! { - program = ctx.program_id, - market = ctx.accounts.market.key, - authority = ctx.accounts.authority.key, - bump = bump - }; - let seeds_init = open_orders_init_authority! { - program = ctx.program_id, - market = ctx.accounts.market.key, - bump = bump_init - }; - dex::init_open_orders(cpi_ctx.with_signer(&[seeds, seeds_init]))?; - Ok(()) - } - - /// Fallback function to relay calls to the serum DEX. - /// - /// For instructions requiring an open orders authority, checks for - /// a user signature and then swaps the account info for one controlled - /// by the program. - /// - /// Note: the "authority" of each open orders account is the account - /// itself, since it's a PDA. #[access_control(is_serum(accounts))] pub fn dex_instruction( program_id: &Pubkey, accounts: &[AccountInfo], - data: &[u8], + mut data: &[u8], ) -> ProgramResult { - require!(accounts.len() >= 1, NotEnoughAccounts); + require!(!accounts.is_empty(), NotEnoughAccounts); + + // Strip instruction data. + let bumps = { + // Strip the discriminator off the data, which is provided by the client + // for prepending extra instruction data. + let disc = data[0]; + data = &data[1..]; + + // For the init open orders instruction, bump seeds are provided. + if disc == 0 { + let bump = data[0]; + let bump_init = data[1]; + data = &data[2..]; // Strip bumps off. + Some((bump, bump_init)) + } else { + None + } + }; - let dex_acc_info = &accounts[0]; - let dex_accounts = &accounts[1..]; - let mut acc_infos = dex_accounts.to_vec(); + // Strip accounts. + let (dex, mut acc_infos) = { + // First account is the dex executable--used for CPI. + let dex = &accounts[0]; + + // Second account is the auth token. + let auth_token = &accounts[1]; + if auth_token.key != &rent::ID { + // Rent sysvar as dummy example. + return Err(ErrorCode::InvalidAuthToken.into()); + } + + // Strip. + let acc_infos = (&accounts[2..]).to_vec(); + + (dex, acc_infos) + }; + + let mut pre_instruction: Option = None; + let mut post_instruction: Option = None; // Decode instruction. - let ix = MarketInstruction::unpack(data).ok_or_else(|| ErrorCode::CannotUnpack)?; + let ix = MarketInstruction::unpack(data).ok_or(ErrorCode::CannotUnpack)?; // Swap the user's account, which is in the open orders authority // position, for the program's PDA (the real authority). let (market, user) = match ix { - MarketInstruction::NewOrderV3(_) => { - require!(dex_accounts.len() >= 12, NotEnoughAccounts); + MarketInstruction::InitOpenOrders => { + let (market, user) = { + let market = &acc_infos[4]; + let user = &acc_infos[3]; + + let (bump, bump_init) = bumps.as_ref().unwrap(); + + // Initialize PDA. + let mut accounts = &acc_infos[..]; + InitAccount::try_accounts(program_id, &mut accounts, &[*bump, *bump_init])?; + + (*market.key, *user.key) + }; + // Chop off the first two accounts used initializing the PDA. + acc_infos = (&acc_infos[2..]).to_vec(); + + // Set signers. + acc_infos[1] = prepare_pda(&acc_infos[0]); + acc_infos[4].is_signer = true; + + (market, user) + } + MarketInstruction::NewOrderV3(ix) => { + require!(acc_infos.len() >= 12, NotEnoughAccounts); let (market, user) = { let market = &acc_infos[0]; @@ -88,12 +121,60 @@ pub mod permissioned_markets { (*market.key, *user.key) }; + // Pre-instruction to approve delegate. + { + let market = &acc_infos[0]; + let user = &acc_infos[7]; + let open_orders = &acc_infos[1]; + let token_account_payer = &acc_infos[6]; + 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(), + ]; + pre_instruction = Some((ix, accounts, Vec::new())); + }; + + // Post-instruction to revoke delegate. + { + let user = &acc_infos[7]; + let token_account_payer = &acc_infos[6]; + let ix = spl_token::instruction::revoke( + &spl_token::ID, + token_account_payer.key, + user.key, + &[], + )?; + let accounts = vec![token_account_payer.clone(), user.clone()]; + post_instruction = Some((ix, accounts, Vec::new())); + } + acc_infos[7] = prepare_pda(&acc_infos[1]); (market, user) } MarketInstruction::CancelOrderV2(_) => { - require!(dex_accounts.len() >= 6, NotEnoughAccounts); + require!(acc_infos.len() >= 6, NotEnoughAccounts); let (market, user) = { let market = &acc_infos[0]; @@ -111,7 +192,7 @@ pub mod permissioned_markets { (market, user) } MarketInstruction::CancelOrderByClientIdV2(_) => { - require!(dex_accounts.len() >= 6, NotEnoughAccounts); + require!(acc_infos.len() >= 6, NotEnoughAccounts); let (market, user) = { let market = &acc_infos[0]; @@ -129,12 +210,12 @@ pub mod permissioned_markets { (market, user) } MarketInstruction::SettleFunds => { - require!(dex_accounts.len() >= 10, NotEnoughAccounts); + require!(acc_infos.len() >= 10, NotEnoughAccounts); let (market, user) = { let market = &acc_infos[0]; let user = &acc_infos[2]; - let referral = &dex_accounts[9]; + let referral = &acc_infos[9]; if !DISABLE_REFERRAL && referral.key != &referral::ID { return Err(ErrorCode::InvalidReferral.into()); @@ -151,7 +232,7 @@ pub mod permissioned_markets { (market, user) } MarketInstruction::CloseOpenOrders => { - require!(dex_accounts.len() >= 4, NotEnoughAccounts); + require!(acc_infos.len() >= 4, NotEnoughAccounts); let (market, user) = { let market = &acc_infos[3]; @@ -171,6 +252,19 @@ pub mod permissioned_markets { _ => return Err(ErrorCode::InvalidInstruction.into()), }; + // Execute pre instruction. + if let Some((ix, accounts, seeds)) = pre_instruction { + let tmp_signers: Vec> = seeds + .iter() + .map(|seeds| { + let seeds: Vec<&[u8]> = seeds.iter().map(|seed| &seed[..]).collect(); + seeds + }) + .collect(); + let signers: Vec<&[&[u8]]> = tmp_signers.iter().map(|seeds| &seeds[..]).collect(); + program::invoke_signed(&ix, &accounts, &signers)?; + } + // CPI to the dex. let dex_accounts = acc_infos .iter() @@ -180,7 +274,6 @@ pub mod permissioned_markets { is_writable: acc.is_writable, }) .collect(); - acc_infos.push(dex_acc_info.clone()); let ix = Instruction { data: data.to_vec(), accounts: dex_accounts, @@ -188,10 +281,31 @@ pub mod permissioned_markets { }; let seeds = open_orders_authority! { program = program_id, + dex_program = dex.key, market = market, authority = user }; - program::invoke_signed(&ix, &acc_infos, &[seeds]) + let seeds_init = open_orders_init_authority! { + program = program_id, + dex_program = dex.key, + market = market + }; + program::invoke_signed(&ix, &acc_infos, &[seeds, seeds_init])?; + + // Execute post instruction. + if let Some((ix, accounts, seeds)) = post_instruction { + let tmp_signers: Vec> = seeds + .iter() + .map(|seeds| { + let seeds: Vec<&[u8]> = seeds.iter().map(|seed| &seed[..]).collect(); + seeds + }) + .collect(); + let signers: Vec<&[&[u8]]> = tmp_signers.iter().map(|seeds| &seeds[..]).collect(); + program::invoke_signed(&ix, &accounts, &signers)?; + } + + Ok(()) } } @@ -200,11 +314,13 @@ pub mod permissioned_markets { #[derive(Accounts)] #[instruction(bump: u8, bump_init: u8)] pub struct InitAccount<'info> { - #[account(seeds = [b"open-orders-init", market.key.as_ref(), &[bump_init]])] - pub open_orders_init_authority: AccountInfo<'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", market.key.as_ref(), authority.key.as_ref()], + seeds = [b"open-orders", dex_program.key.as_ref(), market.key.as_ref(), authority.key.as_ref()], bump = bump, payer = authority, owner = dex::ID, @@ -215,34 +331,16 @@ pub struct InitAccount<'info> { pub authority: AccountInfo<'info>, pub market: AccountInfo<'info>, pub rent: Sysvar<'info, Rent>, - #[account(address = system_program::ID)] - pub system_program: AccountInfo<'info>, - #[account(address = dex::ID)] - pub dex_program: AccountInfo<'info>, -} - -// CpiContext transformations. - -impl<'info> From<&InitAccount<'info>> - for CpiContext<'_, '_, '_, 'info, dex::InitOpenOrders<'info>> -{ - fn from(accs: &InitAccount<'info>) -> Self { - // TODO: add the open orders init authority account here once the - // dex is upgraded. - let accounts = dex::InitOpenOrders { - open_orders: accs.open_orders.clone(), - authority: accs.open_orders.clone(), - market: accs.market.clone(), - rent: accs.rent.to_account_info(), - }; - let program = accs.dex_program.clone(); - CpiContext::new(program, accounts) - } + #[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>, } // Access control modifiers. -fn is_serum<'info>(accounts: &[AccountInfo<'info>]) -> Result<()> { +fn is_serum(accounts: &[AccountInfo]) -> Result<()> { let dex_acc_info = &accounts[0]; if dex_acc_info.key != &dex::ID { return Err(ErrorCode::InvalidDexPid.into()); @@ -266,29 +364,53 @@ pub enum ErrorCode { UnauthorizedUser, #[msg("Not enough accounts were provided")] NotEnoughAccounts, + #[msg("Invalid auth token provided")] + InvalidAuthToken, +} + +// Utils. + +fn prepare_pda<'info>(acc_info: &AccountInfo<'info>) -> AccountInfo<'info> { + let mut acc_info = acc_info.clone(); + acc_info.is_signer = true; + acc_info } // Macros. -/// Returns the seeds used for creating the open orders account PDA. +/// Returns the seeds used for a user's open orders account PDA. #[macro_export] macro_rules! open_orders_authority { - (program = $program:expr, market = $market:expr, authority = $authority:expr, bump = $bump:expr) => { + ( + program = $program:expr, + dex_program = $dex_program:expr, + market = $market:expr, + authority = $authority:expr, + bump = $bump:expr + ) => { &[ b"open-orders".as_ref(), + $dex_program.as_ref(), $market.as_ref(), $authority.as_ref(), &[$bump], ] }; - (program = $program:expr, market = $market:expr, authority = $authority:expr) => { + ( + program = $program:expr, + dex_program = $dex_program:expr, + market = $market:expr, + authority = $authority:expr + ) => { &[ b"open-orders".as_ref(), + $dex_program.as_ref(), $market.as_ref(), $authority.as_ref(), &[Pubkey::find_program_address( &[ b"open-orders".as_ref(), + $dex_program.as_ref(), $market.as_ref(), $authority.as_ref(), ], @@ -304,28 +426,36 @@ macro_rules! open_orders_authority { /// the DEX market. #[macro_export] macro_rules! open_orders_init_authority { - (program = $program:expr, market = $market:expr) => { + ( + program = $program:expr, + dex_program = $dex_program:expr, + market = $market:expr, + bump = $bump:expr + ) => { + &[ + b"open-orders-init".as_ref(), + $dex_program.as_ref().as_ref(), + $market.as_ref().as_ref(), + &[$bump], + ] + }; + + (program = $program:expr, dex_program = $dex_program:expr, market = $market:expr) => { &[ b"open-orders-init".as_ref(), + $dex_program.as_ref(), $market.as_ref(), &[Pubkey::find_program_address( - &[b"open-orders-init".as_ref(), $market.as_ref()], + &[ + b"open-orders-init".as_ref(), + $dex_program.as_ref(), + $market.as_ref(), + ], $program, ) .1], ] }; - (program = $program:expr, market = $market:expr, bump = $bump:expr) => { - &[b"open-orders-init".as_ref(), $market.as_ref(), &[$bump]] - }; -} - -// Utils. - -fn prepare_pda<'info>(acc_info: &AccountInfo<'info>) -> AccountInfo<'info> { - let mut acc_info = acc_info.clone(); - acc_info.is_signer = true; - acc_info } // Constants. @@ -343,3 +473,5 @@ const DISABLE_REFERRAL: bool = true; pub mod referral { solana_program::declare_id!("2k1bb16Hu7ocviT2KC3wcCgETtnC8tEUuvFBH4C5xStG"); } + +type CpiInstruction<'info> = (Instruction, Vec>, Vec>>); diff --git a/examples/permissioned-markets/tests/permissioned-markets.js b/examples/permissioned-markets/tests/permissioned-markets.js index 927a65a256..b7fbb604a7 100644 --- a/examples/permissioned-markets/tests/permissioned-markets.js +++ b/examples/permissioned-markets/tests/permissioned-markets.js @@ -3,123 +3,159 @@ const { Token, TOKEN_PROGRAM_ID } = require("@solana/spl-token"); const anchor = require("@project-serum/anchor"); const serum = require("@project-serum/serum"); const { BN } = anchor; -const { Transaction, TransactionInstruction } = anchor.web3; -const { DexInstructions, OpenOrders, Market } = serum; -const { PublicKey, SystemProgram, SYSVAR_RENT_PUBKEY } = anchor.web3; +const { + Keypair, + Transaction, + TransactionInstruction, + PublicKey, + SystemProgram, + SYSVAR_RENT_PUBKEY, +} = anchor.web3; +const { + DexInstructions, + OpenOrders, + OpenOrdersPda, + Logger, + ReferralFees, + MarketProxyBuilder, +} = serum; const { initMarket, sleep } = require("./utils"); const DEX_PID = new PublicKey("9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin"); -const REFERRAL = new PublicKey("2k1bb16Hu7ocviT2KC3wcCgETtnC8tEUuvFBH4C5xStG"); +const REFERRAL_AUTHORITY = new PublicKey( + "3oSfkjQZKCneYvsCTZc9HViGAPqR8pYr4h9YeGB5ZxHf" +); describe("permissioned-markets", () => { // Anchor client setup. const provider = anchor.Provider.env(); anchor.setProvider(provider); - const program = anchor.workspace.PermissionedMarkets; + const programs = [ + anchor.workspace.PermissionedMarkets, + anchor.workspace.PermissionedMarketsMiddleware, + ]; - // Token clients. - let usdcClient; + programs.forEach((program, index) => { + // Token client. + let usdcClient; - // Global DEX accounts and clients shared accross all tests. - let marketClient, tokenAccount, usdcAccount; - let openOrders, openOrdersBump, openOrdersInitAuthority, openOrdersBumpinit; - let usdcPosted; - let marketMakerOpenOrders; + // Global DEX accounts and clients shared accross all tests. + let marketProxy, tokenAccount, usdcAccount; + let openOrders, openOrdersBump, openOrdersInitAuthority, openOrdersBumpinit; + let usdcPosted; + let referralTokenAddress; - it("BOILERPLATE: Initializes an orderbook", async () => { - const { - marketMakerOpenOrders: mmOo, - marketA, - godA, - godUsdc, - usdc, - } = await initMarket({ provider }); - marketClient = marketA; - marketClient._programId = program.programId; - usdcAccount = godUsdc; - tokenAccount = godA; - marketMakerOpenOrders = mmOo; + it("BOILERPLATE: Initializes an orderbook", async () => { + const getAuthority = async (market) => { + return ( + await PublicKey.findProgramAddress( + [ + anchor.utils.bytes.utf8.encode("open-orders-init"), + DEX_PID.toBuffer(), + market.toBuffer(), + ], + program.programId + ) + )[0]; + }; + const marketLoader = async (market) => { + return new MarketProxyBuilder() + .middleware( + new OpenOrdersPda({ + proxyProgramId: program.programId, + dexProgramId: DEX_PID, + }) + ) + .middleware(new ReferralFees()) + .middleware(new Identity()) + .middleware(new Logger()) + .load({ + connection: provider.connection, + market, + dexProgramId: DEX_PID, + proxyProgramId: program.programId, + options: { commitment: "recent" }, + }); + }; + const { marketA, godA, godUsdc, usdc } = await initMarket({ + provider, + getAuthority, + proxyProgramId: program.programId, + marketLoader, + }); + marketProxy = marketA; + usdcAccount = godUsdc; + tokenAccount = godA; - usdcClient = new Token( - provider.connection, - usdc, - TOKEN_PROGRAM_ID, - provider.wallet.payer - ); - }); + usdcClient = new Token( + provider.connection, + usdc, + TOKEN_PROGRAM_ID, + provider.wallet.payer + ); - it("BOILERPLATE: Calculates open orders addresses", async () => { - const [_openOrders, bump] = await PublicKey.findProgramAddress( - [ - anchor.utils.bytes.utf8.encode("open-orders"), - marketClient.address.toBuffer(), - program.provider.wallet.publicKey.toBuffer(), - ], - program.programId - ); - const [ - _openOrdersInitAuthority, - bumpInit, - ] = await PublicKey.findProgramAddress( - [ - anchor.utils.bytes.utf8.encode("open-orders-init"), - marketClient.address.toBuffer(), - ], - program.programId - ); + referral = await usdcClient.createAccount(REFERRAL_AUTHORITY); + }); - // Save global variables re-used across tests. - openOrders = _openOrders; - openOrdersBump = bump; - openOrdersInitAuthority = _openOrdersInitAuthority; - openOrdersBumpInit = bumpInit; - }); + it("BOILERPLATE: Calculates open orders addresses", async () => { + const [_openOrders, bump] = await PublicKey.findProgramAddress( + [ + anchor.utils.bytes.utf8.encode("open-orders"), + DEX_PID.toBuffer(), + marketProxy.market.address.toBuffer(), + program.provider.wallet.publicKey.toBuffer(), + ], + program.programId + ); + const [ + _openOrdersInitAuthority, + bumpInit, + ] = await PublicKey.findProgramAddress( + [ + anchor.utils.bytes.utf8.encode("open-orders-init"), + DEX_PID.toBuffer(), + marketProxy.market.address.toBuffer(), + ], + program.programId + ); - it("Creates an open orders account", async () => { - await program.rpc.initAccount(openOrdersBump, openOrdersBumpInit, { - accounts: { - openOrdersInitAuthority, - openOrders, - authority: program.provider.wallet.publicKey, - market: marketClient.address, - rent: SYSVAR_RENT_PUBKEY, - systemProgram: SystemProgram.programId, - dexProgram: DEX_PID, - }, + // Save global variables re-used across tests. + openOrders = _openOrders; + openOrdersBump = bump; + openOrdersInitAuthority = _openOrdersInitAuthority; + openOrdersBumpInit = bumpInit; }); - const account = await provider.connection.getAccountInfo(openOrders); - assert.ok(account.owner.toString() === DEX_PID.toString()); - }); + it("Creates an open orders account", async () => { + const tx = new Transaction(); + tx.add( + await marketProxy.instruction.initOpenOrders( + program.provider.wallet.publicKey, + marketProxy.market.address, + marketProxy.market.address, // Dummy. Replaced by middleware. + marketProxy.market.address // Dummy. Replaced by middleware. + ) + ); + await provider.send(tx); - it("Posts a bid on the orderbook", async () => { - const size = 1; - const price = 1; + const account = await provider.connection.getAccountInfo(openOrders); + assert.ok(account.owner.toString() === DEX_PID.toString()); + }); - // The amount of USDC transferred into the dex for the trade. - usdcPosted = new BN(marketClient._decoded.quoteLotSize.toNumber()).mul( - marketClient - .baseSizeNumberToLots(size) - .mul(marketClient.priceNumberToLots(price)) - ); + it("Posts a bid on the orderbook", async () => { + const size = 1; + const price = 1; + usdcPosted = new BN( + marketProxy.market._decoded.quoteLotSize.toNumber() + ).mul( + marketProxy.market + .baseSizeNumberToLots(size) + .mul(marketProxy.market.priceNumberToLots(price)) + ); - // Note: Prepend delegate approve to the tx since the owner of the token - // account must match the owner of the open orders account. We - // can probably hide this in the serum client. - const tx = new Transaction(); - tx.add( - Token.createApproveInstruction( - TOKEN_PROGRAM_ID, - usdcAccount, - openOrders, - program.provider.wallet.publicKey, - [], - usdcPosted.toNumber() - ) - ); - tx.add( - serumProxy( - marketClient.makePlaceOrderInstruction(program.provider.connection, { + const tx = new Transaction(); + tx.add( + marketProxy.instruction.newOrderV3({ owner: program.provider.wallet.publicKey, payer: usdcAccount, side: "buy", @@ -130,155 +166,135 @@ describe("permissioned-markets", () => { openOrdersAddressKey: openOrders, selfTradeBehavior: "abortTransaction", }) - ) - ); - await provider.send(tx); - }); - - it("Cancels a bid on the orderbook", async () => { - // Given. - const beforeOoAccount = await OpenOrders.load( - provider.connection, - openOrders, - DEX_PID - ); - - // When. - const tx = new Transaction(); - tx.add( - serumProxy( - ( - await marketClient.makeCancelOrderByClientIdTransaction( - program.provider.connection, - program.provider.wallet.publicKey, - openOrders, - new BN(999) - ) - ).instructions[0] - ) - ); - await provider.send(tx); - - // Then. - const afterOoAccount = await OpenOrders.load( - provider.connection, - openOrders, - DEX_PID - ); + ); + await provider.send(tx); + }); - assert.ok(beforeOoAccount.quoteTokenFree.eq(new BN(0))); - assert.ok(beforeOoAccount.quoteTokenTotal.eq(usdcPosted)); - assert.ok(afterOoAccount.quoteTokenFree.eq(usdcPosted)); - assert.ok(afterOoAccount.quoteTokenTotal.eq(usdcPosted)); - }); + it("Cancels a bid on the orderbook", async () => { + // Given. + const beforeOoAccount = await OpenOrders.load( + provider.connection, + openOrders, + DEX_PID + ); - // Need to crank the cancel so that we can close later. - it("Cranks the cancel transaction", async () => { - // TODO: can do this in a single transaction if we covert the pubkey bytes - // into a [u64; 4] array and sort. I'm lazy though. - let eq = await marketClient.loadEventQueue(provider.connection); - while (eq.length > 0) { + // When. const tx = new Transaction(); tx.add( - DexInstructions.consumeEvents({ - market: marketClient._decoded.ownAddress, - eventQueue: marketClient._decoded.eventQueue, - coinFee: marketClient._decoded.eventQueue, - pcFee: marketClient._decoded.eventQueue, - openOrdersAccounts: [eq[0].openOrders], - limit: 1, - programId: DEX_PID, - }) + await marketProxy.instruction.cancelOrderByClientId( + program.provider.wallet.publicKey, + openOrders, + new BN(999) + ) ); await provider.send(tx); - eq = await marketClient.loadEventQueue(provider.connection); - } - }); - it("Settles funds on the orderbook", async () => { - // Given. - const beforeTokenAccount = await usdcClient.getAccountInfo(usdcAccount); + // Then. + const afterOoAccount = await OpenOrders.load( + provider.connection, + openOrders, + DEX_PID + ); + assert.ok(beforeOoAccount.quoteTokenFree.eq(new BN(0))); + assert.ok(beforeOoAccount.quoteTokenTotal.eq(usdcPosted)); + assert.ok(afterOoAccount.quoteTokenFree.eq(usdcPosted)); + assert.ok(afterOoAccount.quoteTokenTotal.eq(usdcPosted)); + }); + + // Need to crank the cancel so that we can close later. + it("Cranks the cancel transaction", async () => { + // TODO: can do this in a single transaction if we covert the pubkey bytes + // into a [u64; 4] array and sort. I'm lazy though. + let eq = await marketProxy.market.loadEventQueue(provider.connection); + while (eq.length > 0) { + const tx = new Transaction(); + tx.add( + marketProxy.market.makeConsumeEventsInstruction([eq[0].openOrders], 1) + ); + await provider.send(tx); + eq = await marketProxy.market.loadEventQueue(provider.connection); + } + }); + + it("Settles funds on the orderbook", async () => { + // Given. + const beforeTokenAccount = await usdcClient.getAccountInfo(usdcAccount); - // When. - const tx = new Transaction(); - tx.add( - serumProxy( - DexInstructions.settleFunds({ - market: marketClient._decoded.ownAddress, + // When. + const tx = new Transaction(); + tx.add( + await marketProxy.instruction.settleFunds( openOrders, - owner: provider.wallet.publicKey, - baseVault: marketClient._decoded.baseVault, - quoteVault: marketClient._decoded.quoteVault, - baseWallet: tokenAccount, - quoteWallet: usdcAccount, - vaultSigner: await PublicKey.createProgramAddress( - [ - marketClient.address.toBuffer(), - marketClient._decoded.vaultSignerNonce.toArrayLike( - Buffer, - "le", - 8 - ), - ], - DEX_PID - ), - programId: program.programId, - referrerQuoteWallet: usdcAccount, - }) - ) - ); - await provider.send(tx); + provider.wallet.publicKey, + tokenAccount, + usdcAccount, + referral + ) + ); + await provider.send(tx); - // Then. - const afterTokenAccount = await usdcClient.getAccountInfo(usdcAccount); - assert.ok( - afterTokenAccount.amount.sub(beforeTokenAccount.amount).toNumber() === - usdcPosted.toNumber() - ); - }); + // Then. + const afterTokenAccount = await usdcClient.getAccountInfo(usdcAccount); + assert.ok( + afterTokenAccount.amount.sub(beforeTokenAccount.amount).toNumber() === + usdcPosted.toNumber() + ); + }); - it("Closes an open orders account", async () => { - // Given. - const beforeAccount = await program.provider.connection.getAccountInfo( - program.provider.wallet.publicKey - ); + it("Closes an open orders account", async () => { + // Given. + const beforeAccount = await program.provider.connection.getAccountInfo( + program.provider.wallet.publicKey + ); - // When. - const tx = new Transaction(); - tx.add( - serumProxy( - DexInstructions.closeOpenOrders({ - market: marketClient._decoded.ownAddress, + // When. + const tx = new Transaction(); + tx.add( + marketProxy.instruction.closeOpenOrders( openOrders, - owner: program.provider.wallet.publicKey, - solWallet: program.provider.wallet.publicKey, - programId: program.programId, - }) - ) - ); - await provider.send(tx); + provider.wallet.publicKey, + provider.wallet.publicKey + ) + ); + await provider.send(tx); - // Then. - const afterAccount = await program.provider.connection.getAccountInfo( - program.provider.wallet.publicKey - ); - const closedAccount = await program.provider.connection.getAccountInfo( - openOrders - ); - assert.ok(23352768 === afterAccount.lamports - beforeAccount.lamports); - assert.ok(closedAccount === null); + // Then. + const afterAccount = await program.provider.connection.getAccountInfo( + program.provider.wallet.publicKey + ); + const closedAccount = await program.provider.connection.getAccountInfo( + openOrders + ); + assert.ok(23352768 === afterAccount.lamports - beforeAccount.lamports); + assert.ok(closedAccount === null); + }); }); }); -// Adds the serum dex account to the instruction so that proxies can -// relay (CPI requires the executable account). -// -// TODO: we should add flag in the dex client that says if a proxy is being -// used, and if so, do this automatically. -function serumProxy(ix) { - ix.keys = [ - { pubkey: DEX_PID, isWritable: false, isSigner: false }, - ...ix.keys, - ]; - return ix; +// Dummy identity middleware used for testing. +class Identity { + initOpenOrders(ix) { + this.proxy(ix); + } + newOrderV3(ix) { + this.proxy(ix); + } + cancelOrderV2(ix) { + this.proxy(ix); + } + cancelOrderByClientIdV2(ix) { + this.proxy(ix); + } + settleFunds(ix) { + this.proxy(ix); + } + closeOpenOrders(ix) { + this.proxy(ix); + } + proxy(ix) { + ix.keys = [ + { pubkey: SYSVAR_RENT_PUBKEY, isWritable: false, isSigner: false }, + ...ix.keys, + ]; + } } diff --git a/examples/permissioned-markets/tests/utils/index.js b/examples/permissioned-markets/tests/utils/index.js index 43e2fb5e14..12236b100a 100644 --- a/examples/permissioned-markets/tests/utils/index.js +++ b/examples/permissioned-markets/tests/utils/index.js @@ -6,22 +6,36 @@ const Token = require("@solana/spl-token").Token; const TOKEN_PROGRAM_ID = require("@solana/spl-token").TOKEN_PROGRAM_ID; -const TokenInstructions = require("@project-serum/serum").TokenInstructions; -const { Market, OpenOrders } = require("@project-serum/serum"); -const DexInstructions = require("@project-serum/serum").DexInstructions; -const web3 = require("@project-serum/anchor").web3; -const Connection = web3.Connection; +const serum = require('@project-serum/serum'); +const { + DexInstructions, + TokenInstructions, + MarketProxy, + OpenOrders, + OpenOrdersPda, + MARKET_STATE_LAYOUT_V3, +} = serum; const anchor = require("@project-serum/anchor"); const BN = anchor.BN; +const web3 = anchor.web3; +const { + SYSVAR_RENT_PUBKEY, + COnnection, + Account, + Transaction, + PublicKey, + SystemProgram, +} = web3; const serumCmn = require("@project-serum/common"); -const Account = web3.Account; -const Transaction = web3.Transaction; -const PublicKey = web3.PublicKey; -const SystemProgram = web3.SystemProgram; const DEX_PID = new PublicKey("9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin"); const MARKET_MAKER = new Account(); -async function initMarket({ provider }) { +async function initMarket({ + provider, + getAuthority, + proxyProgramId, + marketLoader, +}) { // Setup mints with initial tokens owned by the provider. const decimals = 6; const [MINT_A, GOD_A] = await serumCmn.createMintAndVault( @@ -79,22 +93,14 @@ async function initMarket({ provider }) { bids, asks, provider, + getAuthority, + proxyProgramId, + marketLoader, }); - - const marketMakerOpenOrders = ( - await OpenOrders.findForMarketAndOwner( - provider.connection, - MARKET_A_USDC.address, - marketMaker.account.publicKey, - DEX_PID - ) - )[0].address; - return { marketA: MARKET_A_USDC, vaultSigner, marketMaker, - marketMakerOpenOrders, mintA: MINT_A, usdc: USDC, godA: GOD_A, @@ -171,6 +177,9 @@ async function setupMarket({ quoteMint, bids, asks, + getAuthority, + proxyProgramId, + marketLoader, }) { const [marketAPublicKey, vaultOwner] = await listMarket({ connection: provider.connection, @@ -181,55 +190,9 @@ async function setupMarket({ quoteLotSize: 100, dexProgramId: DEX_PID, feeRateBps: 0, + getAuthority, }); - const MARKET_A_USDC = await Market.load( - provider.connection, - marketAPublicKey, - { commitment: "recent" }, - DEX_PID - ); - for (let k = 0; k < asks.length; k += 1) { - let ask = asks[k]; - const { - transaction, - signers, - } = await MARKET_A_USDC.makePlaceOrderTransaction(provider.connection, { - owner: marketMaker.account, - payer: marketMaker.baseToken, - side: "sell", - price: ask[0], - size: ask[1], - orderType: "postOnly", - clientId: undefined, - openOrdersAddressKey: undefined, - openOrdersAccount: undefined, - feeDiscountPubkey: null, - selfTradeBehavior: "abortTransaction", - }); - await provider.send(transaction, signers.concat(marketMaker.account)); - } - - for (let k = 0; k < bids.length; k += 1) { - let bid = bids[k]; - const { - transaction, - signers, - } = await MARKET_A_USDC.makePlaceOrderTransaction(provider.connection, { - owner: marketMaker.account, - payer: marketMaker.quoteToken, - side: "buy", - price: bid[0], - size: bid[1], - orderType: "postOnly", - clientId: undefined, - openOrdersAddressKey: undefined, - openOrdersAccount: undefined, - feeDiscountPubkey: null, - selfTradeBehavior: "abortTransaction", - }); - await provider.send(transaction, signers.concat(marketMaker.account)); - } - + const MARKET_A_USDC = await marketLoader(marketAPublicKey); return [MARKET_A_USDC, vaultOwner]; } @@ -242,6 +205,7 @@ async function listMarket({ quoteLotSize, dexProgramId, feeRateBps, + getAuthority, }) { const market = new Account(); const requestQueue = new Account(); @@ -291,9 +255,9 @@ async function listMarket({ fromPubkey: wallet.publicKey, newAccountPubkey: market.publicKey, lamports: await connection.getMinimumBalanceForRentExemption( - Market.getLayout(dexProgramId).span + MARKET_STATE_LAYOUT_V3.span ), - space: Market.getLayout(dexProgramId).span, + space: MARKET_STATE_LAYOUT_V3.span, programId: dexProgramId, }), SystemProgram.createAccount({ @@ -340,25 +304,19 @@ async function listMarket({ vaultSignerNonce, quoteDustThreshold, programId: dexProgramId, + authority: await getAuthority(market.publicKey), }) ); - const signedTransactions = await signTransactions({ - transactionsAndSigners: [ - { transaction: tx1, signers: [baseVault, quoteVault] }, - { - transaction: tx2, - signers: [market, requestQueue, eventQueue, bids, asks], - }, - ], - wallet, - connection, - }); - for (let signedTransaction of signedTransactions) { - await sendAndConfirmRawTransaction( - connection, - signedTransaction.serialize() - ); + const transactions = [ + { transaction: tx1, signers: [baseVault, quoteVault] }, + { + transaction: tx2, + signers: [market, requestQueue, eventQueue, bids, asks], + }, + ]; + for (let tx of transactions) { + await anchor.getProvider().send(tx.transaction, tx.signers); } const acc = await connection.getAccountInfo(market.publicKey); @@ -386,17 +344,6 @@ async function signTransactions({ ); } -async function sendAndConfirmRawTransaction( - connection, - raw, - commitment = "recent" -) { - let tx = await connection.sendRawTransaction(raw, { - skipPreflight: true, - }); - return await connection.confirmTransaction(tx, commitment); -} - async function getVaultOwnerAndNonce(marketPublicKey, dexProgramId = DEX_PID) { const nonce = new BN(0); while (nonce.toNumber() < 255) { diff --git a/lang/src/lib.rs b/lang/src/lib.rs index 14c2f5ae40..b58f261f6e 100644 --- a/lang/src/lib.rs +++ b/lang/src/lib.rs @@ -357,4 +357,9 @@ macro_rules! require { return Err(crate::ErrorCode::$error.into()); } }; + ($invariant:expr, $error:expr $(,)?) => { + if !($invariant) { + return Err($error.into()); + } + }; } diff --git a/spl/Cargo.toml b/spl/Cargo.toml index e803e50774..cdba200c05 100644 --- a/spl/Cargo.toml +++ b/spl/Cargo.toml @@ -12,6 +12,6 @@ devnet = [] [dependencies] anchor-lang = { path = "../lang", version = "0.11.1", features = ["derive"] } lazy_static = "1.4.0" -serum_dex = { git = "https://github.com/project-serum/serum-dex", tag = "v0.3.1", version = "0.3.1", features = ["no-entrypoint"] } +serum_dex = { git = "https://github.com/project-serum/serum-dex", branch = "armani/auth", version = "0.3.1", features = ["no-entrypoint"] } solana-program = "1.7.4" spl-token = { version = "3.1.1", features = ["no-entrypoint"] } diff --git a/spl/src/dex.rs b/spl/src/dex/cpi.rs similarity index 97% rename from spl/src/dex.rs rename to spl/src/dex/cpi.rs index 2f0fc66b16..e85254f03b 100644 --- a/spl/src/dex.rs +++ b/spl/src/dex/cpi.rs @@ -5,8 +5,6 @@ use serum_dex::instruction::SelfTradeBehavior; use serum_dex::matching::{OrderType, Side}; use std::num::NonZeroU64; -pub use serum_dex; - #[cfg(not(feature = "devnet"))] anchor_lang::solana_program::declare_id!("9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin"); @@ -115,6 +113,7 @@ pub fn init_open_orders<'info>( ctx.accounts.open_orders.key, ctx.accounts.authority.key, ctx.accounts.market.key, + ctx.remaining_accounts.first().map(|acc| acc.key), )?; solana_program::program::invoke_signed( &ix, @@ -205,6 +204,8 @@ pub struct SettleFunds<'info> { pub token_program: AccountInfo<'info>, } +/// To use an (optional) market authority, add it as the first account of the +/// CpiContext's `remaining_accounts` Vec. #[derive(Accounts)] pub struct InitOpenOrders<'info> { pub open_orders: AccountInfo<'info>, diff --git a/spl/src/dex/middleware.rs b/spl/src/dex/middleware.rs new file mode 100644 index 0000000000..9db9e3d673 --- /dev/null +++ b/spl/src/dex/middleware.rs @@ -0,0 +1,524 @@ +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>, + pub seeds: Seeds, + // Instructions to execute *prior* to the DEX relay CPI. + pub pre_instructions: Vec<(Instruction, Vec>, Seeds)>, + // Instructions to execution *after* the DEX relay CPI. + pub post_instructions: Vec<(Instruction, Vec>, Seeds)>, +} + +type Seeds = Vec>>; + +impl<'a, 'info> Context<'a, 'info> { + pub fn new( + program_id: &'a Pubkey, + dex_program_id: &'a Pubkey, + accounts: Vec>, + ) -> 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(()) + } + + /// 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(()) + } +} + +/// 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(()) + } +} + +/// 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] +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::() + 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; diff --git a/spl/src/dex/mod.rs b/spl/src/dex/mod.rs new file mode 100644 index 0000000000..dd6ef9f718 --- /dev/null +++ b/spl/src/dex/mod.rs @@ -0,0 +1,8 @@ +mod cpi; +mod middleware; +mod proxy; + +pub use cpi::*; +pub use middleware::*; +pub use proxy::*; +pub use serum_dex; diff --git a/spl/src/dex/proxy.rs b/spl/src/dex/proxy.rs new file mode 100644 index 0000000000..fdaff03c1f --- /dev/null +++ b/spl/src/dex/proxy.rs @@ -0,0 +1,171 @@ +use crate::dex; +use crate::dex::middleware::{Context, ErrorCode, MarketMiddleware}; +use anchor_lang::prelude::*; +use anchor_lang::solana_program::program; +use anchor_lang::solana_program::pubkey::Pubkey; +use serum_dex::instruction::*; + +/// MarketProxy provides an abstraction for implementing proxy programs to the +/// Serum orderbook, allowing one to implement a middleware for the purposes +/// of intercepting and modifying requests before being relayed to the +/// orderbook. +/// +/// The only requirement for a middleware is that, when all are done processing, +/// a valid DEX instruction--accounts and instruction data--must be left to +/// forward to the orderbook program. +#[derive(Default)] +pub struct MarketProxy<'a> { + middlewares: Vec<&'a mut dyn MarketMiddleware>, +} + +impl<'a> MarketProxy<'a> { + /// Constructs a new `MarketProxy`. + pub fn new() -> Self { + Self { + middlewares: Vec::new(), + } + } + + /// Builder method for adding a middleware to the proxy. + pub fn middleware(mut self, mw: &'a mut dyn MarketMiddleware) -> Self { + self.middlewares.push(mw); + self + } + + /// Entrypoint to the program. + pub fn run( + mut self, + program_id: &Pubkey, + accounts: &[AccountInfo], + data: &[u8], + ) -> ProgramResult { + let mut ix_data = data; + + // First account is the Serum DEX executable--used for CPI. + let dex = &accounts[0]; + require!(dex.key == &dex::ID, ErrorCode::InvalidTargetProgram); + let acc_infos = (&accounts[1..]).to_vec(); + + // Process the instruction data. + for mw in &mut self.middlewares { + mw.instruction(&mut ix_data)?; + } + + // Request context. + let mut ctx = Context::new(program_id, dex.key, acc_infos); + + // Decode instruction. + let ix = MarketInstruction::unpack(ix_data); + + // Method dispatch. + match ix { + Some(MarketInstruction::InitOpenOrders) => { + require!(ctx.accounts.len() >= 4, ErrorCode::NotEnoughAccounts); + for mw in &self.middlewares { + mw.init_open_orders(&mut ctx)?; + } + } + Some(MarketInstruction::NewOrderV3(ix)) => { + require!(ctx.accounts.len() >= 12, ErrorCode::NotEnoughAccounts); + for mw in &self.middlewares { + mw.new_order_v3(&mut ctx, &ix)?; + } + } + Some(MarketInstruction::CancelOrderV2(ix)) => { + require!(ctx.accounts.len() >= 6, ErrorCode::NotEnoughAccounts); + for mw in &self.middlewares { + mw.cancel_order_v2(&mut ctx, &ix)?; + } + } + Some(MarketInstruction::CancelOrderByClientIdV2(ix)) => { + require!(ctx.accounts.len() >= 6, ErrorCode::NotEnoughAccounts); + for mw in &self.middlewares { + mw.cancel_order_by_client_id_v2(&mut ctx, ix)?; + } + } + Some(MarketInstruction::SettleFunds) => { + require!(ctx.accounts.len() >= 10, ErrorCode::NotEnoughAccounts); + for mw in &self.middlewares { + mw.settle_funds(&mut ctx)?; + } + } + Some(MarketInstruction::CloseOpenOrders) => { + require!(ctx.accounts.len() >= 4, ErrorCode::NotEnoughAccounts); + for mw in &self.middlewares { + mw.close_open_orders(&mut ctx)?; + } + } + _ => { + for mw in &self.middlewares { + mw.fallback(&mut ctx)?; + } + return Ok(()); + } + }; + + // Extract the middleware adjusted context. + let Context { + seeds, + accounts, + pre_instructions, + post_instructions, + .. + } = ctx; + + // Execute pre instructions. + for (ix, acc_infos, seeds) in pre_instructions { + let tmp_signers: Vec> = seeds + .iter() + .map(|seeds| { + let seeds: Vec<&[u8]> = seeds.iter().map(|seed| &seed[..]).collect(); + seeds + }) + .collect(); + let signers: Vec<&[&[u8]]> = tmp_signers.iter().map(|seeds| &seeds[..]).collect(); + program::invoke_signed(&ix, &acc_infos, &signers)?; + } + + // Execute the main dex relay. + { + let tmp_signers: Vec> = seeds + .iter() + .map(|seeds| { + let seeds: Vec<&[u8]> = seeds.iter().map(|seed| &seed[..]).collect(); + seeds + }) + .collect(); + let signers: Vec<&[&[u8]]> = tmp_signers.iter().map(|seeds| &seeds[..]).collect(); + + // CPI to the DEX. + let dex_accounts = accounts + .iter() + .map(|acc| AccountMeta { + pubkey: *acc.key, + is_signer: acc.is_signer, + is_writable: acc.is_writable, + }) + .collect(); + let ix = anchor_lang::solana_program::instruction::Instruction { + data: ix_data.to_vec(), + accounts: dex_accounts, + program_id: dex::ID, + }; + program::invoke_signed(&ix, &accounts, &signers)?; + } + + // Execute post instructions. + for (ix, acc_infos, seeds) in post_instructions { + let tmp_signers: Vec> = seeds + .iter() + .map(|seeds| { + let seeds: Vec<&[u8]> = seeds.iter().map(|seed| &seed[..]).collect(); + seeds + }) + .collect(); + let signers: Vec<&[&[u8]]> = tmp_signers.iter().map(|seeds| &seeds[..]).collect(); + program::invoke_signed(&ix, &acc_infos, &signers)?; + } + + Ok(()) + } +} diff --git a/spl/src/token.rs b/spl/src/token.rs index 5b7e79b3d0..e098ddb772 100644 --- a/spl/src/token.rs +++ b/spl/src/token.rs @@ -262,4 +262,11 @@ pub mod accessor { mint_bytes.copy_from_slice(&bytes[..32]); Ok(Pubkey::new_from_array(mint_bytes)) } + + pub fn authority(account: &AccountInfo) -> Result { + let bytes = account.try_borrow_data()?; + let mut owner_bytes = [0u8; 32]; + owner_bytes.copy_from_slice(&bytes[32..64]); + Ok(Pubkey::new_from_array(owner_bytes)) + } }