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

This commit is contained in:
Armani Ferrante 2021-07-18 11:39:56 -07:00 committed by GitHub
parent bb89cc3f72
commit 615764b9c8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 1442 additions and 446 deletions

2
Cargo.lock generated
View File

@ -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",

View File

@ -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"

View File

@ -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 @@
Subproject commit 1f6d5867019e242a470deed79cddca0d1f15e0a3
Subproject commit 2037a646f82e689f8e7a00c8a34b30e20253ba11

View File

@ -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"

View File

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

View File

@ -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");
}

View File

@ -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"] }

View File

@ -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);
let dex_acc_info = &accounts[0];
let dex_accounts = &accounts[1..];
let mut acc_infos = dex_accounts.to_vec();
// 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
}
};
// 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>>>);

View File

@ -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
);
// Save global variables re-used across tests.
openOrders = _openOrders;
openOrdersBump = bump;
openOrdersInitAuthority = _openOrdersInitAuthority;
openOrdersBumpInit = bumpInit;
});
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,
},
referral = await usdcClient.createAccount(REFERRAL_AUTHORITY);
});
const account = await provider.connection.getAccountInfo(openOrders);
assert.ok(account.owner.toString() === DEX_PID.toString());
});
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("Posts a bid on the orderbook", async () => {
const size = 1;
const price = 1;
// Save global variables re-used across tests.
openOrders = _openOrders;
openOrdersBump = bump;
openOrdersInitAuthority = _openOrdersInitAuthority;
openOrdersBumpInit = bumpInit;
});
// 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("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);
// 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 account = await provider.connection.getAccountInfo(openOrders);
assert.ok(account.owner.toString() === DEX_PID.toString());
});
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))
);
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
);
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 marketClient.loadEventQueue(provider.connection);
while (eq.length > 0) {
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 provider.send(tx);
eq = await marketClient.loadEventQueue(provider.connection);
}
});
});
it("Settles funds on the orderbook", async () => {
// Given.
const beforeTokenAccount = await usdcClient.getAccountInfo(usdcAccount);
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(
DexInstructions.settleFunds({
market: marketClient._decoded.ownAddress,
// When.
const tx = new Transaction();
tx.add(
await marketProxy.instruction.cancelOrderByClientId(
program.provider.wallet.publicKey,
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);
new BN(999)
)
);
await provider.send(tx);
// Then.
const afterTokenAccount = await usdcClient.getAccountInfo(usdcAccount);
assert.ok(
afterTokenAccount.amount.sub(beforeTokenAccount.amount).toNumber() ===
usdcPosted.toNumber()
);
});
// 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));
});
it("Closes an open orders account", async () => {
// Given.
const beforeAccount = await program.provider.connection.getAccountInfo(
program.provider.wallet.publicKey
);
// 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);
}
});
// When.
const tx = new Transaction();
tx.add(
serumProxy(
DexInstructions.closeOpenOrders({
market: marketClient._decoded.ownAddress,
it("Settles funds on the orderbook", async () => {
// Given.
const beforeTokenAccount = await usdcClient.getAccountInfo(usdcAccount);
// When.
const tx = new Transaction();
tx.add(
await marketProxy.instruction.settleFunds(
openOrders,
owner: program.provider.wallet.publicKey,
solWallet: program.provider.wallet.publicKey,
programId: program.programId,
})
)
);
await provider.send(tx);
provider.wallet.publicKey,
tokenAccount,
usdcAccount,
referral
)
);
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 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
);
// When.
const tx = new Transaction();
tx.add(
marketProxy.instruction.closeOpenOrders(
openOrders,
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);
});
});
});
// 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,
];
}
}

View File

@ -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) {

View File

@ -357,4 +357,9 @@ macro_rules! require {
return Err(crate::ErrorCode::$error.into());
}
};
($invariant:expr, $error:expr $(,)?) => {
if !($invariant) {
return Err($error.into());
}
};
}

View File

@ -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"] }

View File

@ -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
spl/src/dex/middleware.rs Normal file
View File

@ -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
spl/src/dex/mod.rs Normal file
View File

@ -0,0 +1,8 @@
mod cpi;
mod middleware;
mod proxy;
pub use cpi::*;
pub use middleware::*;
pub use proxy::*;
pub use serum_dex;

171
spl/src/dex/proxy.rs Normal file
View File

@ -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(())
}
}

View File

@ -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))
}
}