examples: Permissioned markets (#483)

This commit is contained in:
Armani Ferrante 2021-07-09 18:42:05 -07:00 committed by GitHub
parent 46cccd8946
commit 5018e98a9c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 1350 additions and 67 deletions

3
.gitmodules vendored
View File

@ -10,3 +10,6 @@
[submodule "examples/cfo/deps/stake"]
path = examples/cfo/deps/stake
url = https://github.com/project-serum/stake.git
[submodule "examples/permissioned-markets/deps/serum-dex"]
path = examples/permissioned-markets/deps/serum-dex
url = https://github.com/project-serum/serum-dex

View File

@ -11,8 +11,15 @@ incremented for features.
## [Unreleased]
### Features
* lang: Adds `require` macro for specifying assertions that return error codes on failure ([#483](https://github.com/project-serum/anchor/pull/483)).
* lang: Allow one to specify arbitrary programs as the owner when creating PDA ([#483](https://github.com/project-serum/anchor/pull/483)).
* lang: A new `bump` keyword is added to the accounts constraints, which is used to add an optional bump seed to the end of a `seeds` array. When used in conjunction with *both* `init` and `seeds`, then the program executes `find_program_address` to assert that the given bump is the canonical bump ([#483](https://github.com/project-serum/anchor/pull/483)).
### Fixes
* lang: Preserve all instruction data for fallback functions ([#483](https://github.com/project-serum/anchor/pull/483)).
* ts: Event listener not firing when creating associated accounts ([#356](https://github.com/project-serum/anchor/issues/356)).
## [0.11.0] - 2021-07-03

View File

@ -17,4 +17,5 @@ members = [
exclude = [
"examples/swap/deps/serum-dex",
"examples/cfo/deps/serum-dex",
"examples/permissioned-markets/deps/serum-dex",
]

View File

@ -0,0 +1,13 @@
[provider]
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,8 @@
[workspace]
members = [
"programs/*"
]
exclude = [
"deps/serum-dex",
]

@ -0,0 +1 @@
Subproject commit 1f6d5867019e242a470deed79cddca0d1f15e0a3

View File

@ -0,0 +1,13 @@
// Migrations are an early feature. Currently, they're nothing more than this
// single deploy script that's invoked from the CLI, injecting a provider
// configured from the workspace's Anchor.toml.
const anchor = require("@project-serum/anchor");
module.exports = async function (provider) {
// Configure client to use the provider.
anchor.setProvider(provider);
// Add your deploy script here.
}

View File

@ -0,0 +1,21 @@
[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 = { path = "../../../../lang" }
anchor-spl = { path = "../../../../spl" }
serum_dex = { path = "../../deps/serum-dex/dex", features = ["no-entrypoint"] }
solana-program = "1.7.4"

View File

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

View File

@ -0,0 +1,345 @@
// Note. This example depends on unreleased Serum DEX changes.
use anchor_lang::prelude::*;
use anchor_spl::dex;
use serum_dex::instruction::MarketInstruction;
use serum_dex::state::OpenOrders;
use solana_program::instruction::Instruction;
use solana_program::program;
use solana_program::system_program;
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.
///
/// 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, i.e., `fee_owner::ID`.
#[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],
) -> ProgramResult {
require!(accounts.len() >= 1, NotEnoughAccounts);
let dex_acc_info = &accounts[0];
let dex_accounts = &accounts[1..];
let mut acc_infos = dex_accounts.to_vec();
// Decode instruction.
let ix = MarketInstruction::unpack(data).ok_or_else(|| 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);
let (market, user) = {
let market = &acc_infos[0];
let user = &acc_infos[7];
if !user.is_signer {
return Err(ErrorCode::UnauthorizedUser.into());
}
(*market.key, *user.key)
};
acc_infos[7] = prepare_pda(&acc_infos[1]);
(market, user)
}
MarketInstruction::CancelOrderV2(_) => {
require!(dex_accounts.len() >= 6, NotEnoughAccounts);
let (market, user) = {
let market = &acc_infos[0];
let user = &acc_infos[4];
if !user.is_signer {
return Err(ErrorCode::UnauthorizedUser.into());
}
(*market.key, *user.key)
};
acc_infos[4] = prepare_pda(&acc_infos[3]);
(market, user)
}
MarketInstruction::CancelOrderByClientIdV2(_) => {
require!(dex_accounts.len() >= 6, NotEnoughAccounts);
let (market, user) = {
let market = &acc_infos[0];
let user = &acc_infos[4];
if !user.is_signer {
return Err(ErrorCode::UnauthorizedUser.into());
}
(*market.key, *user.key)
};
acc_infos[4] = prepare_pda(&acc_infos[3]);
(market, user)
}
MarketInstruction::SettleFunds => {
require!(dex_accounts.len() >= 10, NotEnoughAccounts);
let (market, user) = {
let market = &acc_infos[0];
let user = &acc_infos[2];
let referral = &dex_accounts[9];
if !DISABLE_REFERRAL && referral.key != &referral::ID {
return Err(ErrorCode::InvalidReferral.into());
}
if !user.is_signer {
return Err(ErrorCode::UnauthorizedUser.into());
}
(*market.key, *user.key)
};
acc_infos[2] = prepare_pda(&acc_infos[1]);
(market, user)
}
MarketInstruction::CloseOpenOrders => {
require!(dex_accounts.len() >= 4, NotEnoughAccounts);
let (market, user) = {
let market = &acc_infos[3];
let user = &acc_infos[1];
if !user.is_signer {
return Err(ErrorCode::UnauthorizedUser.into());
}
(*market.key, *user.key)
};
acc_infos[1] = prepare_pda(&acc_infos[0]);
(market, user)
}
_ => return Err(ErrorCode::InvalidInstruction.into()),
};
// CPI to the dex.
let dex_accounts = acc_infos
.iter()
.map(|acc| AccountMeta {
pubkey: *acc.key,
is_signer: acc.is_signer,
is_writable: acc.is_writable,
})
.collect();
acc_infos.push(dex_acc_info.clone());
let ix = Instruction {
data: data.to_vec(),
accounts: dex_accounts,
program_id: dex::ID,
};
let seeds = open_orders_authority! {
program = program_id,
market = market,
authority = user
};
program::invoke_signed(&ix, &acc_infos, &[seeds])
}
}
// Accounts context.
#[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(
init,
seeds = [b"open-orders", 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(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)
}
}
// Access control modifiers.
fn is_serum<'info>(accounts: &[AccountInfo<'info>]) -> Result<()> {
let dex_acc_info = &accounts[0];
if dex_acc_info.key != &dex::ID {
return Err(ErrorCode::InvalidDexPid.into());
}
Ok(())
}
// Error.
#[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,
}
// Macros.
/// Returns the seeds used for creating the open orders account PDA.
#[macro_export]
macro_rules! open_orders_authority {
(program = $program:expr, market = $market:expr, authority = $authority:expr, bump = $bump:expr) => {
&[
b"open-orders".as_ref(),
$market.as_ref(),
$authority.as_ref(),
&[$bump],
]
};
(program = $program:expr, market = $market:expr, authority = $authority:expr) => {
&[
b"open-orders".as_ref(),
$market.as_ref(),
$authority.as_ref(),
&[Pubkey::find_program_address(
&[
b"open-orders".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, market = $market:expr) => {
&[
b"open-orders-init".as_ref(),
$market.as_ref(),
&[Pubkey::find_program_address(
&[b"open-orders-init".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.
// Padding added to every serum account.
//
// b"serum".len() + b"padding".len().
const SERUM_PADDING: usize = 12;
// True if we don't care about referral access control (for testing).
const DISABLE_REFERRAL: bool = true;
/// The address that will receive all fees for all markets controlled by this
/// program. Note: this is a dummy address. Do not use in production.
pub mod referral {
solana_program::declare_id!("2k1bb16Hu7ocviT2KC3wcCgETtnC8tEUuvFBH4C5xStG");
}

View File

@ -0,0 +1,284 @@
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 { Transaction, TransactionInstruction } = anchor.web3;
const { DexInstructions, OpenOrders, Market } = serum;
const { PublicKey, SystemProgram, SYSVAR_RENT_PUBKEY } = anchor.web3;
const { initMarket, sleep } = require("./utils");
const DEX_PID = new PublicKey("9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin");
const REFERRAL = new PublicKey("2k1bb16Hu7ocviT2KC3wcCgETtnC8tEUuvFBH4C5xStG");
describe("permissioned-markets", () => {
// Anchor client setup.
const provider = anchor.Provider.env();
anchor.setProvider(provider);
const program = anchor.workspace.PermissionedMarkets;
// Token clients.
let usdcClient;
// Global DEX accounts and clients shared accross all tests.
let marketClient, tokenAccount, usdcAccount;
let openOrders, openOrdersBump, openOrdersInitAuthority, openOrdersBumpinit;
let usdcPosted;
let marketMakerOpenOrders;
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;
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,
},
});
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;
// 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))
);
// 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, {
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(
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);
// When.
const tx = new Transaction();
tx.add(
serumProxy(
DexInstructions.settleFunds({
market: marketClient._decoded.ownAddress,
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);
// 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(
serumProxy(
DexInstructions.closeOpenOrders({
market: marketClient._decoded.ownAddress,
openOrders,
owner: program.provider.wallet.publicKey,
solWallet: program.provider.wallet.publicKey,
programId: program.programId,
})
)
);
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;
}

View File

@ -0,0 +1,427 @@
// Boilerplate utils to bootstrap an orderbook for testing on a localnet.
// not super relevant to the point of the example, though may be useful to
// include into your own workspace for testing.
//
// TODO: Modernize all these apis. This is all quite clunky.
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 anchor = require("@project-serum/anchor");
const BN = anchor.BN;
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 }) {
// Setup mints with initial tokens owned by the provider.
const decimals = 6;
const [MINT_A, GOD_A] = await serumCmn.createMintAndVault(
provider,
new BN("1000000000000000000"),
undefined,
decimals
);
const [USDC, GOD_USDC] = await serumCmn.createMintAndVault(
provider,
new BN("1000000000000000000"),
undefined,
decimals
);
// Create a funded account to act as market maker.
const amount = new BN("10000000000000").muln(10 ** decimals);
const marketMaker = await fundAccount({
provider,
mints: [
{ god: GOD_A, mint: MINT_A, amount, decimals },
{ god: GOD_USDC, mint: USDC, amount, decimals },
],
});
// Setup A/USDC with resting orders.
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],
];
[MARKET_A_USDC, vaultSigner] = await setupMarket({
baseMint: MINT_A,
quoteMint: USDC,
marketMaker: {
account: marketMaker.account,
baseToken: marketMaker.tokens[MINT_A.toString()],
quoteToken: marketMaker.tokens[USDC.toString()],
},
bids,
asks,
provider,
});
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,
godUsdc: GOD_USDC,
};
}
async function fundAccount({ provider, mints }) {
const marketMaker = {
tokens: {},
account: MARKET_MAKER,
};
// Transfer lamports to market maker.
await provider.send(
(() => {
const tx = new Transaction();
tx.add(
SystemProgram.transfer({
fromPubkey: provider.wallet.publicKey,
toPubkey: MARKET_MAKER.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, decimals } = 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(
MARKET_MAKER.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;
}
async function setupMarket({
provider,
marketMaker,
baseMint,
quoteMint,
bids,
asks,
}) {
const [marketAPublicKey, vaultOwner] = await listMarket({
connection: provider.connection,
wallet: provider.wallet,
baseMint: baseMint,
quoteMint: quoteMint,
baseLotSize: 100000,
quoteLotSize: 100,
dexProgramId: DEX_PID,
feeRateBps: 0,
});
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));
}
return [MARKET_A_USDC, vaultOwner];
}
async function listMarket({
connection,
wallet,
baseMint,
quoteMint,
baseLotSize,
quoteLotSize,
dexProgramId,
feeRateBps,
}) {
const market = new Account();
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.getLayout(dexProgramId).span
),
space: Market.getLayout(dexProgramId).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,
})
);
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 acc = await connection.getAccountInfo(market.publicKey);
return [market.publicKey, vaultOwner];
}
async function signTransactions({
transactionsAndSigners,
wallet,
connection,
}) {
const blockhash = (await connection.getRecentBlockhash("max")).blockhash;
transactionsAndSigners.forEach(({ transaction, signers = [] }) => {
transaction.recentBlockhash = blockhash;
transaction.setSigners(
wallet.publicKey,
...signers.map((s) => s.publicKey)
);
if (signers?.length > 0) {
transaction.partialSign(...signers);
}
});
return await wallet.signAllTransactions(
transactionsAndSigners.map(({ transaction }) => transaction)
);
}
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) {
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");
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
module.exports = {
fundAccount,
initMarket,
setupMarket,
DEX_PID,
getVaultOwnerAndNonce,
sleep,
};

View File

@ -42,7 +42,7 @@ use syn::parse_macro_input;
/// | `#[account(init)]` | On `ProgramAccount` structs. | Marks the account as being initialized, skipping the account discriminator check. When using `init`, a `rent` `Sysvar` must be present in the `Accounts` struct. |
/// | `#[account(close = <target>)]` | On `ProgramAccount` and `Loader` structs. | Marks the account as being closed at the end of the instruction's execution, sending the rent exemption lamports to the specified <target>. |
/// | `#[account(has_one = <target>)]` | On `ProgramAccount` or `CpiAccount` structs | Checks the `target` field on the account matches the `target` field in the struct deriving `Accounts`. |
/// | `#[account(seeds = [<seeds>])]` | On `AccountInfo` structs | Seeds for the program derived address an `AccountInfo` struct represents. |
/// | `#[account(seeds = [<seeds>], bump? = <target>, payer? = <target>, space? = <target>, owner? = <target>)]` | On `AccountInfo` structs | Seeds for the program derived address an `AccountInfo` struct represents. If bump is provided, then appends it to the seeds. On initialization, validates the given bump is the bump provided by `Pubkey::find_program_address`.|
/// | `#[account(constraint = <expression>)]` | On any type deriving `Accounts` | Executes the given code as a constraint. The expression should evaluate to a boolean. |
/// | `#[account("<literal>")]` | Deprecated | Executes the given code literal as a constraint. The literal should evaluate to a boolean. |
/// | `#[account(rent_exempt = <skip>)]` | On `AccountInfo` or `ProgramAccount` structs | Optional attribute to skip the rent exemption check. By default, all accounts marked with `#[account(init)]` will be rent exempt, and so this should rarely (if ever) be used. Similarly, omitting `= skip` will mark the account rent exempt. |

View File

@ -233,10 +233,10 @@ impl Key for Pubkey {
/// All programs should include it via `anchor_lang::prelude::*;`.
pub mod prelude {
pub use super::{
access_control, account, associated, emit, error, event, interface, program, state,
zero_copy, AccountDeserialize, AccountSerialize, Accounts, AccountsExit, AccountsInit,
AnchorDeserialize, AnchorSerialize, Context, CpiAccount, CpiContext, CpiState,
CpiStateContext, Loader, ProgramAccount, ProgramState, Sysvar, ToAccountInfo,
access_control, account, associated, emit, error, event, interface, program, require,
state, zero_copy, AccountDeserialize, AccountSerialize, Accounts, AccountsExit,
AccountsInit, AnchorDeserialize, AnchorSerialize, Context, CpiAccount, CpiContext,
CpiState, CpiStateContext, Loader, ProgramAccount, ProgramState, Sysvar, ToAccountInfo,
ToAccountInfos, ToAccountMetas,
};
@ -327,3 +327,34 @@ macro_rules! associated_seeds {
]
};
}
/// Ensures a condition is true, otherwise returns the given error.
/// Use this with a custom error type.
///
/// # Example
///
/// After defining an `ErrorCode`
///
/// ```ignore
/// #[error]
/// pub struct ErrorCode {
/// InvalidArgument,
/// }
/// ```
///
/// One can write a `require` assertion as
///
/// ```ignore
/// require!(condition, InvalidArgument);
/// ```
///
/// which would exit the program with the `InvalidArgument` error code if
/// `condition` is false.
#[macro_export]
macro_rules! require {
($invariant:expr, $error:tt $(,)?) => {
if !($invariant) {
return Err(crate::ErrorCode::$error.into());
}
};
}

View File

@ -265,8 +265,13 @@ fn generate_constraint_seeds_init(f: &Field, c: &ConstraintSeedsGroup) -> proc_m
let seeds_constraint = generate_constraint_seeds_address(f, c);
let seeds_with_nonce = {
let s = &c.seeds;
quote! {
[#s]
match c.bump.as_ref() {
None => quote! {
[#s]
},
Some(b) => quote! {
[#s, &[#b]]
},
}
};
generate_pda(
@ -285,14 +290,47 @@ fn generate_constraint_seeds_address(
c: &ConstraintSeedsGroup,
) -> proc_macro2::TokenStream {
let name = &f.ident;
let seeds = &c.seeds;
quote! {
let __program_signer = Pubkey::create_program_address(
&[#seeds],
program_id,
).map_err(|_| anchor_lang::__private::ErrorCode::ConstraintSeeds)?;
if #name.to_account_info().key != &__program_signer {
return Err(anchor_lang::__private::ErrorCode::ConstraintSeeds.into());
// If the bump is provided on *initialization*, then force it to be the
// canonical nonce.
if c.is_init && c.bump.is_some() {
let s = &c.seeds;
let b = c.bump.as_ref().unwrap();
quote! {
let (__program_signer, __bump) = anchor_lang::solana_program::pubkey::Pubkey::find_program_address(
&[#s],
program_id,
);
if #name.to_account_info().key != &__program_signer {
return Err(anchor_lang::__private::ErrorCode::ConstraintSeeds.into());
}
if __bump != #b {
return Err(anchor_lang::__private::ErrorCode::ConstraintSeeds.into());
}
}
} else {
let seeds = match c.bump.as_ref() {
None => {
let s = &c.seeds;
quote! {
[#s]
}
}
Some(b) => {
let s = &c.seeds;
quote! {
[#s, &[#b]]
}
}
};
quote! {
let __program_signer = Pubkey::create_program_address(
&#seeds,
program_id,
).map_err(|_| anchor_lang::__private::ErrorCode::ConstraintSeeds)?;
if #name.to_account_info().key != &__program_signer {
return Err(anchor_lang::__private::ErrorCode::ConstraintSeeds.into());
}
}
}
}
@ -355,27 +393,49 @@ pub fn generate_constraint_associated_init(
)
}
fn parse_ty(f: &Field) -> (&syn::TypePath, proc_macro2::TokenStream, bool) {
fn parse_ty(f: &Field) -> (proc_macro2::TokenStream, proc_macro2::TokenStream, bool) {
match &f.ty {
Ty::ProgramAccount(ty) => (
&ty.account_type_path,
Ty::ProgramAccount(ty) => {
let ident = &ty.account_type_path;
(
quote! {
#ident
},
quote! {
anchor_lang::ProgramAccount
},
false,
)
}
Ty::Loader(ty) => {
let ident = &ty.account_type_path;
(
quote! {
#ident
},
quote! {
anchor_lang::Loader
},
true,
)
}
Ty::CpiAccount(ty) => {
let ident = &ty.account_type_path;
(
quote! {
#ident
},
quote! {
anchor_lang::CpiAccount
},
false,
)
}
Ty::AccountInfo => (
quote! {
anchor_lang::ProgramAccount
},
false,
),
Ty::Loader(ty) => (
&ty.account_type_path,
quote! {
anchor_lang::Loader
},
true,
),
Ty::CpiAccount(ty) => (
&ty.account_type_path,
quote! {
anchor_lang::CpiAccount
AccountInfo
},
quote! {},
false,
),
_ => panic!("Invalid type for initializing a program derived address"),
@ -431,9 +491,30 @@ pub fn generate_pda(
},
};
let (combined_account_ty, try_from) = match f.ty {
Ty::AccountInfo => (
quote! {
AccountInfo
},
quote! {
#field.to_account_info()
},
),
_ => (
quote! {
#account_wrapper_ty<#account_ty>
},
quote! {
#account_wrapper_ty::try_from_init(
&#field.to_account_info(),
)?
},
),
};
match kind {
PdaKind::Token { owner, mint } => quote! {
let #field: #account_wrapper_ty<#account_ty> = {
let #field: #combined_account_ty = {
#space
#payer
#seeds_constraint
@ -500,9 +581,19 @@ pub fn generate_pda(
)?
};
},
PdaKind::Program => {
PdaKind::Program { owner } => {
// Owner of the account being created. If not specified,
// default to the currently executing program.
let owner = match owner {
None => quote! {
program_id
},
Some(o) => quote! {
&#o
},
};
quote! {
let #field: #account_wrapper_ty<#account_ty> = {
let #field = {
#space
#payer
#seeds_constraint
@ -513,10 +604,9 @@ pub fn generate_pda(
#field.to_account_info().key,
lamports,
space as u64,
program_id,
#owner,
);
anchor_lang::solana_program::program::invoke_signed(
&ix,
&[
@ -533,9 +623,7 @@ pub fn generate_pda(
// For now, we assume all accounts created with the `associated`
// attribute have a `nonce` field in their account.
let mut pa: #account_wrapper_ty<#account_ty> = #account_wrapper_ty::try_from_init(
&#field.to_account_info(),
)?;
let mut pa: #combined_account_ty = #try_from;
#nonce_assignment
pa

View File

@ -135,7 +135,21 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream {
/// With this 8 byte identifier, Anchor performs method dispatch,
/// matching the given 8 byte identifier to the associated method
/// handler, which leads to user defined code being eventually invoked.
fn dispatch(program_id: &Pubkey, accounts: &[AccountInfo], sighash: [u8; 8], ix_data: &[u8]) -> ProgramResult {
fn dispatch(
program_id: &Pubkey,
accounts: &[AccountInfo],
data: &[u8],
) -> ProgramResult {
// Split the instruction data into the first 8 byte method
// identifier (sighash) and the serialized instruction data.
let mut ix_data: &[u8] = data;
let sighash: [u8; 8] = {
let mut sighash: [u8; 8] = [0; 8];
sighash.copy_from_slice(&ix_data[..8]);
ix_data = &ix_data[8..];
sighash
};
// If the method identifier is the IDL tag, then execute an IDL
// instruction, injected into all Anchor programs.
if cfg!(not(feature = "no-idl")) {
@ -167,7 +181,7 @@ pub fn gen_fallback(program: &Program) -> Option<proc_macro2::TokenStream> {
let method = &fallback_fn.raw_method;
let fn_name = &method.sig.ident;
quote! {
#program_name::#fn_name(program_id, accounts, ix_data)
#program_name::#fn_name(program_id, accounts, data)
}
})
}

View File

@ -50,26 +50,16 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream {
/// The `entry` function here, defines the standard entry to a Solana
/// program, where execution begins.
#[cfg(not(feature = "no-entrypoint"))]
fn entry(program_id: &Pubkey, accounts: &[AccountInfo], ix_data: &[u8]) -> ProgramResult {
fn entry(program_id: &Pubkey, accounts: &[AccountInfo], data: &[u8]) -> ProgramResult {
#[cfg(feature = "anchor-debug")]
{
msg!("anchor-debug is active");
}
if ix_data.len() < 8 {
if data.len() < 8 {
return #fallback_maybe
}
// Split the instruction data into the first 8 byte method
// identifier (sighash) and the serialized instruction data.
let mut ix_data: &[u8] = ix_data;
let sighash: [u8; 8] = {
let mut sighash: [u8; 8] = [0; 8];
sighash.copy_from_slice(&ix_data[..8]);
ix_data = &ix_data[8..];
sighash
};
dispatch(program_id, accounts, sighash, ix_data)
dispatch(program_id, accounts, data)
.map_err(|e| {
anchor_lang::solana_program::msg!(&e.to_string());
e

View File

@ -341,6 +341,7 @@ pub enum ConstraintToken {
Address(Context<ConstraintAddress>),
TokenMint(Context<ConstraintTokenMint>),
TokenAuthority(Context<ConstraintTokenAuthority>),
Bump(Context<ConstraintTokenBump>),
}
impl Parse for ConstraintToken {
@ -396,6 +397,7 @@ pub struct ConstraintSeedsGroup {
pub payer: Option<Ident>,
pub space: Option<Expr>,
pub kind: PdaKind,
pub bump: Option<Expr>,
}
#[derive(Debug, Clone)]
@ -444,7 +446,7 @@ pub struct ConstraintAssociatedSpace {
#[derive(Debug, Clone)]
#[allow(clippy::large_enum_variant)]
pub enum PdaKind {
Program,
Program { owner: Option<Expr> },
Token { owner: Expr, mint: Expr },
}
@ -463,6 +465,11 @@ pub struct ConstraintTokenAuthority {
auth: Expr,
}
#[derive(Debug, Clone)]
pub struct ConstraintTokenBump {
bump: Expr,
}
// Syntaxt context object for preserving metadata about the inner item.
#[derive(Debug, Clone)]
pub struct Context<T> {

View File

@ -1,11 +1,4 @@
use crate::{
ConstraintAddress, ConstraintAssociated, ConstraintAssociatedGroup, ConstraintAssociatedPayer,
ConstraintAssociatedSpace, ConstraintAssociatedWith, ConstraintClose, ConstraintExecutable,
ConstraintGroup, ConstraintHasOne, ConstraintInit, ConstraintLiteral, ConstraintMut,
ConstraintOwner, ConstraintRaw, ConstraintRentExempt, ConstraintSeeds, ConstraintSeedsGroup,
ConstraintSigner, ConstraintState, ConstraintToken, ConstraintTokenAuthority,
ConstraintTokenMint, Context, PdaKind, Ty,
};
use crate::*;
use syn::ext::IdentExt;
use syn::parse::{Error as ParseError, Parse, ParseStream, Result as ParseResult};
use syn::punctuated::Punctuated;
@ -183,6 +176,12 @@ pub fn parse_token(stream: ParseStream) -> ParseResult<ConstraintToken> {
auth: stream.parse()?,
},
)),
"bump" => ConstraintToken::Bump(Context::new(
ident.span(),
ConstraintTokenBump {
bump: stream.parse()?,
},
)),
_ => return Err(ParseError::new(ident.span(), "Invalid attribute")),
}
}
@ -213,6 +212,7 @@ pub struct ConstraintGroupBuilder<'ty> {
pub address: Option<Context<ConstraintAddress>>,
pub token_mint: Option<Context<ConstraintTokenMint>>,
pub token_authority: Option<Context<ConstraintTokenAuthority>>,
pub bump: Option<Context<ConstraintTokenBump>>,
}
impl<'ty> ConstraintGroupBuilder<'ty> {
@ -238,6 +238,7 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
address: None,
token_mint: None,
token_authority: None,
bump: None,
}
}
pub fn build(mut self) -> ParseResult<ConstraintGroup> {
@ -298,6 +299,7 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
address,
token_mint,
token_authority,
bump,
} = self;
// Converts Option<Context<T>> -> Option<T>.
@ -316,6 +318,14 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
};
}
let (owner, pda_owner) = {
if seeds.is_some() || associated.is_some() {
(None, owner.map(|o| o.owner_target.clone()))
} else {
(owner, None)
}
};
let is_init = init.is_some();
Ok(ConstraintGroup {
init: into_inner!(init),
@ -334,7 +344,9 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
payer: into_inner!(associated_payer.clone()).map(|a| a.target),
space: associated_space.clone().map(|s| s.space.clone()),
kind: match &token_mint {
None => PdaKind::Program,
None => PdaKind::Program {
owner: pda_owner.clone(),
},
Some(tm) => PdaKind::Token {
mint: tm.clone().into_inner().mint,
owner: match &token_authority {
@ -346,6 +358,7 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
},
},
},
bump: into_inner!(bump).map(|b| b.bump),
})
})
.transpose()?,
@ -358,7 +371,7 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
payer: associated_payer.map(|p| p.target.clone()),
space: associated_space.map(|s| s.space.clone()),
kind: match token_mint {
None => PdaKind::Program,
None => PdaKind::Program { owner: pda_owner },
Some(tm) => PdaKind::Token {
mint: tm.into_inner().mint,
owner: match token_authority {
@ -394,6 +407,7 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
ConstraintToken::Address(c) => self.add_address(c),
ConstraintToken::TokenAuthority(c) => self.add_token_authority(c),
ConstraintToken::TokenMint(c) => self.add_token_mint(c),
ConstraintToken::Bump(c) => self.add_bump(c),
}
}
@ -449,6 +463,20 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
Ok(())
}
fn add_bump(&mut self, c: Context<ConstraintTokenBump>) -> ParseResult<()> {
if self.bump.is_some() {
return Err(ParseError::new(c.span(), "bump already provided"));
}
if self.seeds.is_none() {
return Err(ParseError::new(
c.span(),
"seeds must be provided before bump",
));
}
self.bump.replace(c);
Ok(())
}
fn add_token_authority(&mut self, c: Context<ConstraintTokenAuthority>) -> ParseResult<()> {
if self.token_authority.is_some() {
return Err(ParseError::new(