dex: Move in permissioned markets (#189)

This commit is contained in:
Armani Ferrante 2021-10-31 07:31:06 -05:00 committed by GitHub
parent 0c730d678f
commit 298bcd409c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 2998 additions and 780 deletions

3
.gitignore vendored
View File

@ -4,3 +4,6 @@ target/
*~
*.swp
*.swo
node_modules
yarn.lock
.anchor

View File

@ -1,51 +1,38 @@
dist: bionic
language: shell
os: linux
language: rust
rust:
- stable
cache: cargo
env:
global:
- NODE_VERSION="v14.7.0"
- SOLANA_VERSION="v1.6.18"
- SOLANA_VERSION="v1.8.0"
_defaults: &defaults
cache: false
services:
- docker
before_install:
- scripts/travis/run-docker.sh
before_cache:
- scripts/travis/stop-docker.sh
_localnet: &localnet
language: rust
rust:
- stable
before_script:
- rustup component add rustfmt clippy
- nvm install $NODE_VERSION
- sudo apt-get install -y pkg-config build-essential libudev-dev
- sudo apt-get install -y pkg-config build-essential libudev-dev jq
- sh -c "$(curl -sSfL https://release.solana.com/${SOLANA_VERSION}/install)"
- export PATH="/home/travis/.local/share/solana/install/active_release/bin:$PATH"
- export PATH="/home/travis/.local/share/solana/install/active_release/bin:$PATH"
- yes | solana-keygen new
jobs:
include:
- <<: *defaults
name: Dex tests
<<: *localnet
script:
- docker exec dev ./scripts/travis/dex-tests.sh
- ./scripts/travis/dex-tests.sh
- <<: *defaults
name: Permissioned Dex tests
script:
- cd dex/tests/permissioned/ && yarn && yarn build && yarn test && cd ../../../
- <<: *defaults
name: Fmt and Common Tests
script:
- docker exec dev cargo build
- docker exec dev cargo fmt -- --check
- docker exec -w=/workdir/common dev cargo test --features client,strict
- <<: *localnet
name: Permissioned Markets Tests
script:
- mkdir -p /tmp/tests && cd /tmp/tests
- git clone --recursive https://github.com/project-serum/permissioned-markets-quickstart.git
- cd permissioned-markets-quickstart
- yarn
- yarn build
- yarn test
- cargo build
- cargo fmt -- --check
- cargo test --features client,strict

1457
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -3,6 +3,7 @@ members = [
"assert-owner",
"common",
"dex/crank",
"dex/permissioned",
"pool",
"pool/schema",
"pool/examples/admin-controlled",

View File

@ -76,6 +76,5 @@ The easiest way to run a local cluster is to use [solana-test-validator](https:/
* `assert-owner`: Solana utility program for checking account ownership.
* `common`: Common rust utilities.
* `dex`: Serum DEX program and client utility.
* `docker`: Docker image definitions.
* `pool`: Serum pool protocol.
* `scripts`: Bash scripts for development.

View File

@ -0,0 +1,10 @@
[package]
name = "serum-dex-permissioned"
version = "0.1.0"
edition = "2018"
[dependencies]
anchor-lang = "0.18.0"
anchor-spl = "0.18.0"
serum_dex = { path = "../" }
spl-token = { version = "3.1.1", features = ["no-entrypoint"] }

View File

@ -0,0 +1,5 @@
mod middleware;
mod proxy;
pub use middleware::*;
pub use proxy::*;

View File

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

View File

@ -0,0 +1,177 @@
use crate::{Context, ErrorCode, MarketMiddleware};
use anchor_lang::prelude::*;
use anchor_lang::solana_program::program;
use anchor_lang::solana_program::pubkey::Pubkey;
use anchor_spl::dex;
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)?;
}
}
Some(MarketInstruction::Prune(limit)) => {
require!(ctx.accounts.len() >= 7, ErrorCode::NotEnoughAccounts);
for mw in &self.middlewares {
mw.prune(&mut ctx, limit)?;
}
}
_ => {
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

@ -0,0 +1,10 @@
[provider]
cluster = "localnet"
wallet = "~/.config/solana/id.json"
[[test.genesis]]
address = "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin"
program = "../../target/deploy/serum_dex.so"
[scripts]
test = "npx mocha -t 1000000 tests/"

View File

@ -0,0 +1,4 @@
[workspace]
members = [
"programs/*"
]

View File

@ -0,0 +1,33 @@
{
"name": "permissioned-markets-quickstart",
"version": "1.0.0",
"description": "This repo demonstrates how to create \"permissioned markets\" on Serum via a proxy smart contract. A permissioned market is a regular Serum market with an additional open orders authority, which must sign every transaction to create an open orders account.",
"main": "index.js",
"directories": {
"test": "tests"
},
"scripts": {
"test": "anchor test",
"localnet": "./scripts/localnet.sh",
"build": "yarn build:dex && anchor build",
"build:dex": "cd ../../ && cargo build-bpf && cd tests/permissioned/"
},
"repository": {
"type": "git",
"url": "git+https://github.com/project-serum/permissioned-markets-quickstart.git"
},
"author": "",
"license": "ISC",
"bugs": {
"url": "https://github.com/project-serum/permissioned-markets-quickstart/issues"
},
"homepage": "https://github.com/project-serum/permissioned-markets-quickstart#readme",
"devDependencies": {
"@project-serum/anchor": "^0.18.0",
"@project-serum/anchor-cli": "^0.18.2",
"@project-serum/common": "^0.0.1-beta.3",
"@project-serum/serum": "^0.13.55",
"@solana/spl-token": "^0.1.6",
"mocha": "^9.0.3"
}
}

View File

@ -0,0 +1,20 @@
[package]
name = "permissioned-markets"
version = "0.1.0"
description = "Created with Anchor"
edition = "2018"
[lib]
crate-type = ["cdylib", "lib"]
name = "permissioned_markets"
[features]
no-entrypoint = []
no-idl = []
cpi = ["no-entrypoint"]
default = []
[dependencies]
anchor-lang = "0.13.2"
anchor-spl = "0.13.2"
solana-program = "=1.7.8"

View File

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

View File

@ -0,0 +1,233 @@
// 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 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 {
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 Identity {
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 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 (revoked).
/// ..
fn prune(&self, ctx: &mut Context, _limit: u16) -> ProgramResult {
verify_revoked_and_strip_auth(ctx)?;
// Sign with the prune authority.
let market = &ctx.accounts[0];
ctx.seeds.push(prune_authority! {
program = ctx.program_id,
dex_program = ctx.dex_program_id,
market = market.key
});
ctx.accounts[3] = Self::prepare_pda(&ctx.accounts[3]);
Ok(())
}
/// 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(())
}
fn verify_revoked_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, TokenNotRevoked);
// Strip off the account before possing on the message.
ctx.accounts = (&ctx.accounts[1..]).to_vec();
Ok(())
}
// Macros.
/// Returns the seeds used for the prune authority.
#[macro_export]
macro_rules! prune_authority {
(
program = $program:expr,
dex_program = $dex_program:expr,
market = $market:expr,
bump = $bump:expr
) => {
vec![
b"prune".to_vec(),
$dex_program.as_ref().to_vec(),
$market.as_ref().to_vec(),
vec![$bump],
]
};
(
program = $program:expr,
dex_program = $dex_program:expr,
market = $market:expr
) => {
vec![
b"prune".to_vec(),
$dex_program.as_ref().to_vec(),
$market.as_ref().to_vec(),
vec![
Pubkey::find_program_address(
&[b"prune".as_ref(), $dex_program.as_ref(), $market.as_ref()],
$program,
)
.1,
],
]
};
}
// Error.
#[error]
pub enum ErrorCode {
#[msg("Invalid auth token provided")]
InvalidAuth,
#[msg("Auth token not revoked")]
TokenNotRevoked,
}
// Constants.
pub mod referral {
// This is a dummy address for testing. Do not use in production.
solana_program::declare_id!("3oSfkjQZKCneYvsCTZc9HViGAPqR8pYr4h9YeGB5ZxHf");
}

View File

@ -0,0 +1,351 @@
const assert = require("assert");
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 {
Keypair,
Transaction,
TransactionInstruction,
PublicKey,
SystemProgram,
SYSVAR_RENT_PUBKEY,
} = anchor.web3;
const {
DexInstructions,
OpenOrders,
OpenOrdersPda,
Logger,
ReferralFees,
MarketProxyBuilder,
} = serum;
const { genesis, sleep } = require("./utils");
const DEX_PID = new PublicKey("9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin");
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;
// Token client.
let usdcClient;
// 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 { marketProxyClient, godA, godUsdc, usdc } = await genesis({
provider,
proxyProgramId: program.programId,
});
marketProxy = marketProxyClient;
usdcAccount = godUsdc;
tokenAccount = godA;
usdcClient = new Token(
provider.connection,
usdc,
TOKEN_PROGRAM_ID,
provider.wallet.payer
);
referral = await usdcClient.createAccount(REFERRAL_AUTHORITY);
});
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
);
// Save global variables re-used across tests.
openOrders = _openOrders;
openOrdersBump = bump;
openOrdersInitAuthority = _openOrdersInitAuthority;
openOrdersBumpInit = bumpInit;
});
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);
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",
price,
size,
orderType: "postOnly",
clientId: new BN(999),
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(
await marketProxy.instruction.cancelOrderByClientId(
program.provider.wallet.publicKey,
openOrders,
new BN(999)
)
);
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));
});
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,
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()
);
});
// Need to crank the cancel so that we can close later.
it("Cranks the cancel transaction", async () => {
await crankEventQueue(provider, marketProxy);
});
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);
});
it("Re-opens 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);
const account = await provider.connection.getAccountInfo(openOrders);
assert.ok(account.owner.toString() === DEX_PID.toString());
});
it("Posts several bids and asks on the orderbook", async () => {
const size = 10;
const price = 2;
for (let k = 0; k < 10; k += 1) {
const tx = new Transaction();
tx.add(
marketProxy.instruction.newOrderV3({
owner: program.provider.wallet.publicKey,
payer: usdcAccount,
side: "buy",
price,
size,
orderType: "postOnly",
clientId: new BN(999),
openOrdersAddressKey: openOrders,
selfTradeBehavior: "abortTransaction",
})
);
await provider.send(tx);
}
const sizeAsk = 10;
const priceAsk = 10;
for (let k = 0; k < 10; k += 1) {
const txAsk = new Transaction();
txAsk.add(
marketProxy.instruction.newOrderV3({
owner: program.provider.wallet.publicKey,
payer: tokenAccount,
side: "sell",
price: priceAsk,
size: sizeAsk,
orderType: "postOnly",
clientId: new BN(1000),
openOrdersAddressKey: openOrders,
selfTradeBehavior: "abortTransaction",
})
);
await provider.send(txAsk);
}
});
it("Prunes the orderbook", async () => {
const tx = new Transaction();
tx.add(
marketProxy.instruction.prune(openOrders, provider.wallet.publicKey)
);
await provider.send(tx);
});
it("Settles the account", async () => {
const tx = new Transaction();
tx.add(
await marketProxy.instruction.settleFunds(
openOrders,
provider.wallet.publicKey,
tokenAccount,
usdcAccount,
referral
)
);
await provider.send(tx);
});
it("Cranks the prune transaction", async () => {
await crankEventQueue(provider, marketProxy);
});
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);
});
});
async function crankEventQueue(provider, marketProxy) {
// 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);
}
}

View File

@ -0,0 +1,81 @@
const { PublicKey, Account } = require("@project-serum/anchor").web3;
const DEX_PID = new PublicKey("9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin");
// This msut be kept in sync with `scripts/localnet.sh`.
const PROGRAM_KP = new Account([
168,
86,
206,
125,
127,
105,
201,
250,
37,
102,
161,
124,
80,
181,
60,
2,
166,
123,
176,
161,
228,
188,
134,
186,
158,
68,
197,
240,
202,
193,
174,
234,
167,
123,
252,
186,
72,
51,
203,
70,
153,
234,
190,
2,
134,
184,
197,
156,
113,
8,
65,
1,
83,
220,
152,
62,
200,
174,
40,
180,
218,
61,
224,
6,
]);
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
module.exports = {
sleep,
DEX_PID,
PROGRAM_KP,
};

View File

@ -0,0 +1,97 @@
const anchor = require("@project-serum/anchor");
const BN = anchor.BN;
const { Account, Transaction, SystemProgram } = anchor.web3;
const serumCmn = require("@project-serum/common");
const { TOKEN_PROGRAM_ID, Token } = require("@solana/spl-token");
const DECIMALS = 6;
// Creates mints and a token account funded with each mint.
async function createMintGods(provider, mintCount) {
// Setup mints with initial tokens owned by the provider.
let mintGods = [];
for (let k = 0; k < mintCount; k += 1) {
const [mint, god] = await serumCmn.createMintAndVault(
provider,
new BN("1000000000000000000"),
undefined,
DECIMALS
);
mintGods.push({ mint, god });
}
return mintGods;
}
async function createFundedAccount(provider, mints, newAccount) {
if (!newAccount) {
newAccount = new Account();
}
const marketMaker = {
tokens: {},
account: newAccount,
};
// Transfer lamports to market maker.
await provider.send(
(() => {
const tx = new Transaction();
tx.add(
SystemProgram.transfer({
fromPubkey: provider.wallet.publicKey,
toPubkey: newAccount.publicKey,
lamports: 100000000000,
})
);
return tx;
})()
);
// Transfer SPL tokens to the market maker.
for (let k = 0; k < mints.length; k += 1) {
const { mint, god, amount } = mints[k];
let MINT_A = mint;
let GOD_A = god;
// Setup token accounts owned by the market maker.
const mintAClient = new Token(
provider.connection,
MINT_A,
TOKEN_PROGRAM_ID,
provider.wallet.payer // node only
);
const marketMakerTokenA = await mintAClient.createAccount(
newAccount.publicKey
);
await provider.send(
(() => {
const tx = new Transaction();
tx.add(
Token.createTransferCheckedInstruction(
TOKEN_PROGRAM_ID,
GOD_A,
MINT_A,
marketMakerTokenA,
provider.wallet.publicKey,
[],
amount,
DECIMALS
)
);
return tx;
})()
);
marketMaker.tokens[mint.toString()] = marketMakerTokenA;
}
return marketMaker;
}
module.exports = {
createMintGods,
createFundedAccount,
DECIMALS,
};

View File

@ -0,0 +1,98 @@
const { BN } = require("@project-serum/anchor");
const { PublicKey } = require("@project-serum/anchor").web3;
const marketProxy = require("./market-proxy");
const marketLister = require("./market-lister");
const faucet = require("./faucet");
const { DEX_PID } = require("./common");
const marketMaker = require("./market-maker");
// Initializes the genesis state for the tests and localnetwork.
async function genesis({ provider, proxyProgramId }) {
//
// Create all mints and funded god accounts.
//
const mintGods = await faucet.createMintGods(provider, 2);
const [mintGodA, mintGodB] = mintGods;
//
// Fund an additional account.
//
const fundedAccount = await faucet.createFundedAccount(
provider,
mintGods.map((mintGod) => {
return {
...mintGod,
amount: new BN("10000000000000").muln(10 ** faucet.DECIMALS),
};
}),
marketMaker.KEYPAIR
);
//
// Structure the market maker object.
//
const marketMakerAccounts = {
...fundedAccount,
baseToken: fundedAccount.tokens[mintGodA.mint.toString()],
quoteToken: fundedAccount.tokens[mintGodB.mint.toString()],
};
//
// List the market.
//
const [marketAPublicKey] = await marketLister.list({
connection: provider.connection,
wallet: provider.wallet,
baseMint: mintGodA.mint,
quoteMint: mintGodB.mint,
baseLotSize: 100000,
quoteLotSize: 100,
dexProgramId: DEX_PID,
proxyProgramId,
feeRateBps: 0,
});
//
// Load a proxy client for the market.
//
const marketProxyClient = await marketProxy.load(
provider.connection,
proxyProgramId,
DEX_PID,
marketAPublicKey
);
//
// Market maker initializes an open orders account.
//
await marketMaker.initOpenOrders(
provider,
marketProxyClient,
marketMakerAccounts
);
//
// Market maker posts trades on the orderbook.
//
await marketMaker.postOrders(
provider,
marketProxyClient,
marketMakerAccounts
);
//
// Done.
//
return {
marketProxyClient,
mintA: mintGodA.mint,
usdc: mintGodB.mint,
godA: mintGodA.god,
godUsdc: mintGodB.god,
};
}
module.exports = {
genesis,
DEX_PID,
};

View File

@ -0,0 +1,244 @@
const anchor = require("@project-serum/anchor");
const { BN } = anchor;
const {
Account,
PublicKey,
Transaction,
SystemProgram,
} = require("@project-serum/anchor").web3;
const { TOKEN_PROGRAM_ID } = require("@solana/spl-token");
const serum = require("@project-serum/serum");
const {
DexInstructions,
TokenInstructions,
OpenOrdersPda,
MARKET_STATE_LAYOUT_V3,
} = serum;
const { Identity } = require("./market-proxy");
const { DEX_PID } = require("./common");
// Creates a market on the dex.
async function list({
connection,
wallet,
baseMint,
quoteMint,
baseLotSize,
quoteLotSize,
dexProgramId,
proxyProgramId,
feeRateBps,
}) {
const market = MARKET_KP;
const requestQueue = new Account();
const eventQueue = new Account();
const bids = new Account();
const asks = new Account();
const baseVault = new Account();
const quoteVault = new Account();
const quoteDustThreshold = new BN(100);
const [vaultOwner, vaultSignerNonce] = await getVaultOwnerAndNonce(
market.publicKey,
dexProgramId
);
const tx1 = new Transaction();
tx1.add(
SystemProgram.createAccount({
fromPubkey: wallet.publicKey,
newAccountPubkey: baseVault.publicKey,
lamports: await connection.getMinimumBalanceForRentExemption(165),
space: 165,
programId: TOKEN_PROGRAM_ID,
}),
SystemProgram.createAccount({
fromPubkey: wallet.publicKey,
newAccountPubkey: quoteVault.publicKey,
lamports: await connection.getMinimumBalanceForRentExemption(165),
space: 165,
programId: TOKEN_PROGRAM_ID,
}),
TokenInstructions.initializeAccount({
account: baseVault.publicKey,
mint: baseMint,
owner: vaultOwner,
}),
TokenInstructions.initializeAccount({
account: quoteVault.publicKey,
mint: quoteMint,
owner: vaultOwner,
})
);
const tx2 = new Transaction();
tx2.add(
SystemProgram.createAccount({
fromPubkey: wallet.publicKey,
newAccountPubkey: market.publicKey,
lamports: await connection.getMinimumBalanceForRentExemption(
MARKET_STATE_LAYOUT_V3.span
),
space: MARKET_STATE_LAYOUT_V3.span,
programId: dexProgramId,
}),
SystemProgram.createAccount({
fromPubkey: wallet.publicKey,
newAccountPubkey: requestQueue.publicKey,
lamports: await connection.getMinimumBalanceForRentExemption(5120 + 12),
space: 5120 + 12,
programId: dexProgramId,
}),
SystemProgram.createAccount({
fromPubkey: wallet.publicKey,
newAccountPubkey: eventQueue.publicKey,
lamports: await connection.getMinimumBalanceForRentExemption(262144 + 12),
space: 262144 + 12,
programId: dexProgramId,
}),
SystemProgram.createAccount({
fromPubkey: wallet.publicKey,
newAccountPubkey: bids.publicKey,
lamports: await connection.getMinimumBalanceForRentExemption(65536 + 12),
space: 65536 + 12,
programId: dexProgramId,
}),
SystemProgram.createAccount({
fromPubkey: wallet.publicKey,
newAccountPubkey: asks.publicKey,
lamports: await connection.getMinimumBalanceForRentExemption(65536 + 12),
space: 65536 + 12,
programId: dexProgramId,
}),
DexInstructions.initializeMarket({
market: market.publicKey,
requestQueue: requestQueue.publicKey,
eventQueue: eventQueue.publicKey,
bids: bids.publicKey,
asks: asks.publicKey,
baseVault: baseVault.publicKey,
quoteVault: quoteVault.publicKey,
baseMint,
quoteMint,
baseLotSize: new BN(baseLotSize),
quoteLotSize: new BN(quoteLotSize),
feeRateBps,
vaultSignerNonce,
quoteDustThreshold,
programId: dexProgramId,
authority: await OpenOrdersPda.marketAuthority(
market.publicKey,
DEX_PID,
proxyProgramId
),
pruneAuthority: await Identity.pruneAuthority(
market.publicKey,
DEX_PID,
proxyProgramId
),
})
);
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);
return [market.publicKey, vaultOwner];
}
async function getVaultOwnerAndNonce(marketPublicKey, dexProgramId = DEX_PID) {
const nonce = new BN(0);
while (nonce.toNumber() < 255) {
try {
const vaultOwner = await PublicKey.createProgramAddress(
[marketPublicKey.toBuffer(), nonce.toArrayLike(Buffer, "le", 8)],
dexProgramId
);
return [vaultOwner, nonce];
} catch (e) {
nonce.iaddn(1);
}
}
throw new Error("Unable to find nonce");
}
// Dummy keypair for a consistent market address. Helpful when doing UI work.
// Don't use in production.
const MARKET_KP = new Account([
13,
174,
53,
150,
78,
228,
12,
98,
170,
254,
212,
211,
125,
193,
2,
241,
97,
137,
49,
209,
189,
199,
27,
215,
220,
65,
57,
203,
215,
93,
105,
203,
217,
32,
5,
194,
157,
118,
162,
47,
102,
126,
235,
65,
99,
80,
56,
231,
217,
114,
25,
225,
239,
140,
169,
92,
150,
146,
211,
218,
183,
139,
9,
104,
]);
module.exports = {
list,
};

View File

@ -0,0 +1,160 @@
const { Account, Transaction } = require("@project-serum/anchor").web3;
const { OpenOrdersPda } = require("@project-serum/serum");
// Dummy keypair.
const KEYPAIR = new Account([
54,
213,
91,
255,
163,
120,
88,
183,
223,
23,
220,
204,
82,
117,
212,
214,
118,
184,
2,
29,
89,
149,
22,
233,
108,
177,
60,
249,
218,
166,
30,
221,
59,
168,
233,
123,
204,
37,
123,
124,
86,
176,
214,
12,
63,
195,
231,
15,
1,
143,
7,
7,
232,
38,
69,
214,
45,
58,
115,
55,
129,
25,
228,
30,
]);
async function initOpenOrders(provider, marketProxy, marketMakerAccounts) {
const tx = new Transaction();
tx.add(
marketProxy.instruction.initOpenOrders(
marketMakerAccounts.account.publicKey,
marketProxy.market.address,
marketProxy.market.address, // Dummy. Replaced by middleware.
marketProxy.market.address // Dummy. Replaced by middleware.
)
);
let signers = [marketMakerAccounts.account];
await provider.send(tx, signers);
}
async function postOrders(provider, marketProxy, marketMakerAccounts) {
const asks = [
[6.041, 7.8],
[6.051, 72.3],
[6.055, 5.4],
[6.067, 15.7],
[6.077, 390.0],
[6.09, 24.0],
[6.11, 36.3],
[6.133, 300.0],
[6.167, 687.8],
];
const bids = [
[6.004, 8.5],
[5.995, 12.9],
[5.987, 6.2],
[5.978, 15.3],
[5.965, 82.8],
[5.961, 25.4],
];
const openOrdersAddressKey = await OpenOrdersPda.openOrdersAddress(
marketProxy.market.address,
marketMakerAccounts.account.publicKey,
marketProxy.dexProgramId,
marketProxy.proxyProgramId
);
// Use an explicit signer because the provider wallet, which pays for
// the tx, is different from the market maker wallet.
let signers = [marketMakerAccounts.account];
for (let k = 0; k < asks.length; k += 1) {
let ask = asks[k];
const tx = new Transaction();
tx.add(
await marketProxy.instruction.newOrderV3({
owner: marketMakerAccounts.account.publicKey,
payer: marketMakerAccounts.baseToken,
side: "sell",
price: ask[0],
size: ask[1],
orderType: "postOnly",
clientId: undefined,
openOrdersAddressKey,
feeDiscountPubkey: null,
selfTradeBehavior: "abortTransaction",
})
);
await provider.send(tx, signers);
}
for (let k = 0; k < bids.length; k += 1) {
let bid = bids[k];
const tx = new Transaction();
tx.add(
await marketProxy.instruction.newOrderV3({
owner: marketMakerAccounts.account.publicKey,
payer: marketMakerAccounts.quoteToken,
side: "buy",
price: bid[0],
size: bid[1],
orderType: "postOnly",
clientId: undefined,
openOrdersAddressKey,
feeDiscountPubkey: null,
selfTradeBehavior: "abortTransaction",
})
);
await provider.send(tx, signers);
}
}
module.exports = {
postOrders,
initOpenOrders,
KEYPAIR,
};

View File

@ -0,0 +1,90 @@
const anchor = require("@project-serum/anchor");
const {
PublicKey,
SYSVAR_RENT_PUBKEY,
SYSVAR_CLOCK_PUBKEY,
} = require("@solana/web3.js");
const {
OpenOrders,
OpenOrdersPda,
Logger,
ReferralFees,
MarketProxyBuilder,
} = require("@project-serum/serum");
// Returns a client for the market proxy.
//
// If changing the program, one will likely need to change the builder/middleware
// here as well.
async function load(connection, proxyProgramId, dexProgramId, market) {
return new MarketProxyBuilder()
.middleware(
new OpenOrdersPda({
proxyProgramId,
dexProgramId,
})
)
.middleware(new ReferralFees())
.middleware(new Identity())
.middleware(new Logger())
.load({
connection,
market,
dexProgramId,
proxyProgramId,
options: { commitment: "recent" },
});
}
// 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);
}
prune(ix) {
this.proxyRevoked(ix);
}
proxy(ix) {
ix.keys = [
{ pubkey: SYSVAR_RENT_PUBKEY, isWritable: false, isSigner: false },
...ix.keys,
];
}
proxyRevoked(ix) {
ix.keys = [
{ pubkey: SYSVAR_CLOCK_PUBKEY, isWritable: false, isSigner: false },
...ix.keys,
];
}
static async pruneAuthority(market, dexProgramId, proxyProgramId) {
const [addr] = await PublicKey.findProgramAddress(
[
anchor.utils.bytes.utf8.encode("prune"),
dexProgramId.toBuffer(),
market.toBuffer(),
],
proxyProgramId
);
return addr;
}
}
module.exports = {
load,
Identity,
};

View File

@ -1,19 +0,0 @@
IMG_ORG ?= projectserum
IMG_VER ?= latest
WORKDIR=$(PWD)
.PHONY: development development-push development-shell
default:
development: development/Dockerfile
@docker build $@ -t $(IMG_ORG)/$@:$(IMG_VER)
development-push:
@docker push $(IMG_ORG)/development:$(IMG_VER)
development-shell:
@docker run -ti --rm --net=host \
-v $(WORKDIR)/..:/workdir \
$(IMG_ORG)/development:$(IMG_VER) bash

View File

@ -1,24 +0,0 @@
# Docker
A development docker image is used in CI to build and test the project.
## Development
To build the development image run
```
make development
```
To push to dockerhub, assuming you have push access, run
```
make development-push
```
To run the development image locally (if you don't want to install all the
dependencies yourself), run
```
make development-shell
```

View File

@ -1,32 +0,0 @@
FROM ubuntu:18.04
ARG DEBIAN_FRONTEND=noninteractive
ARG SOLANA_CHANNEL=v1.2.17
ARG SOLANA_CLI=v1.6.18
ENV HOME="/root"
ENV PATH="${HOME}/.cargo/bin:${PATH}"
ENV PATH="${HOME}/.local/share/solana/install/active_release/bin:${PATH}"
# Install base utilities.
RUN mkdir -p /workdir && mkdir -p /tmp && \
apt-get update -qq && apt-get upgrade -qq && apt-get install -qq \
build-essential git curl wget jq pkg-config python3-pip \
libssl-dev libudev-dev
# Install rust.
RUN curl "https://sh.rustup.rs" -sfo rustup.sh && \
sh rustup.sh -y && \
rustup component add rustfmt clippy
# Install Solana tools.
RUN curl -sSf https://raw.githubusercontent.com/solana-labs/solana/${SOLANA_CLI}/install/solana-install-init.sh | sh -s - ${SOLANA_CLI} && \
# BPF sdk.
curl -L --retry 5 --retry-delay 2 -o bpf-sdk.tar.bz2 http://solana-sdk.s3.amazonaws.com/${SOLANA_CHANNEL}/bpf-sdk.tar.bz2 && \
rm -rf bpf-sdk && \
mkdir -p bpf-sdk && \
tar jxf bpf-sdk.tar.bz2 && \
rm -f bpf-sdk.tar.bz2
WORKDIR /workdir

View File

@ -48,7 +48,7 @@ dex_whole_shebang() {
#
# Deploy the program.
#
local dex_program_id="$(solana deploy --output json-compact --url ${CLUSTER_URL} dex/target/bpfel-unknown-unknown/release/serum_dex.so | jq .programId -r)"
local dex_program_id="$(solana deploy --output json-compact --url ${CLUSTER_URL} dex/target/deploy/serum_dex.so | jq .programId -r)"
#
# Run the whole-shebang.
#

View File

@ -1,24 +0,0 @@
#!/bin/bash
set -euxo pipefail
main() {
docker pull projectserum/development:latest
#
# Bind the relevant host directories to the docker image so that the
# files are synced.
#
docker volume create --driver local \
--opt type=none \
--opt device=$TRAVIS_BUILD_DIR \
--opt o=bind \
workdir
#
# Start the container.
#
docker run -it -d --net host --name dev \
-v workdir:/workdir \
projectserum/development:latest bash
}
main

View File

@ -1,9 +0,0 @@
#!/bin/bash
set -euxo pipefail
main() {
docker stop dev
}
main