Browse Source

examples, spl: Permissioned markets via proxy middleware (#519)

Armani Ferrante 4 years ago
parent
commit
615764b9c8

+ 1 - 1
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",

+ 0 - 6
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"

+ 13 - 0
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

+ 1 - 1
examples/permissioned-markets/deps/serum-dex

@@ -1 +1 @@
-Subproject commit 1f6d5867019e242a470deed79cddca0d1f15e0a3
+Subproject commit 2037a646f82e689f8e7a00c8a34b30e20253ba11

+ 20 - 0
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"

+ 2 - 0
examples/permissioned-markets/programs/permissioned-markets-middleware/Xargo.toml

@@ -0,0 +1,2 @@
+[target.bpfel-unknown-unknown.dependencies.std]
+features = []

+ 155 - 0
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");
+}

+ 2 - 1
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"
+solana-program = "1.7.4"
+spl-token = { version = "3.1.1", features = ["no-entrypoint"] }

+ 231 - 99
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<InitAccount>, 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<CpiInstruction> = None;
+        let mut post_instruction: Option<CpiInstruction> = 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<Vec<&[u8]>> = 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<Vec<&[u8]>> = 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<AccountInfo<'info>>, Vec<Vec<Vec<u8>>>);

+ 247 - 231
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,
+    ];
+  }
 }

+ 45 - 98
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) {

+ 5 - 0
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());
+        }
+    };
 }

+ 1 - 1
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"] }

+ 3 - 2
spl/src/dex.rs → 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>,

+ 524 - 0
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<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(())
+    }
+
+    /// 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::<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;

+ 8 - 0
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;

+ 171 - 0
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<Vec<&[u8]>> = 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<Vec<&[u8]>> = 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<Vec<&[u8]>> = 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(())
+    }
+}

+ 7 - 0
spl/src/token.rs

@@ -292,4 +292,11 @@ pub mod accessor {
         mint_bytes.copy_from_slice(&bytes[..32]);
         Ok(Pubkey::new_from_array(mint_bytes))
     }
+
+    pub fn authority(account: &AccountInfo) -> Result<Pubkey, ProgramError> {
+        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))
+    }
 }