From 5018e98a9c27622880bad4fa0c9acf7546ae85ba Mon Sep 17 00:00:00 2001 From: Armani Ferrante Date: Fri, 9 Jul 2021 18:42:05 -0700 Subject: [PATCH] examples: Permissioned markets (#483) --- .gitmodules | 3 + CHANGELOG.md | 7 + Cargo.toml | 1 + examples/permissioned-markets/Anchor.toml | 13 + examples/permissioned-markets/Cargo.toml | 8 + examples/permissioned-markets/deps/serum-dex | 1 + .../permissioned-markets/migrations/deploy.js | 13 + .../programs/permissioned-markets/Cargo.toml | 21 + .../programs/permissioned-markets/Xargo.toml | 2 + .../programs/permissioned-markets/src/lib.rs | 345 ++++++++++++++ .../tests/permissioned-markets.js | 284 ++++++++++++ .../permissioned-markets/tests/utils/index.js | 427 ++++++++++++++++++ lang/derive/accounts/src/lib.rs | 2 +- lang/src/lib.rs | 39 +- lang/syn/src/codegen/accounts/constraints.rs | 160 +++++-- lang/syn/src/codegen/program/dispatch.rs | 18 +- lang/syn/src/codegen/program/entry.rs | 16 +- lang/syn/src/lib.rs | 9 +- lang/syn/src/parser/accounts/constraints.rs | 48 +- 19 files changed, 1350 insertions(+), 67 deletions(-) create mode 100644 examples/permissioned-markets/Anchor.toml create mode 100644 examples/permissioned-markets/Cargo.toml create mode 160000 examples/permissioned-markets/deps/serum-dex create mode 100644 examples/permissioned-markets/migrations/deploy.js create mode 100644 examples/permissioned-markets/programs/permissioned-markets/Cargo.toml create mode 100644 examples/permissioned-markets/programs/permissioned-markets/Xargo.toml create mode 100644 examples/permissioned-markets/programs/permissioned-markets/src/lib.rs create mode 100644 examples/permissioned-markets/tests/permissioned-markets.js create mode 100644 examples/permissioned-markets/tests/utils/index.js diff --git a/.gitmodules b/.gitmodules index 6b3c3a728..e27738f6c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 507d3963a..66f4959d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Cargo.toml b/Cargo.toml index 0d31e71e8..1a59bb879 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,4 +17,5 @@ members = [ exclude = [ "examples/swap/deps/serum-dex", "examples/cfo/deps/serum-dex", + "examples/permissioned-markets/deps/serum-dex", ] diff --git a/examples/permissioned-markets/Anchor.toml b/examples/permissioned-markets/Anchor.toml new file mode 100644 index 000000000..5cd9fd82f --- /dev/null +++ b/examples/permissioned-markets/Anchor.toml @@ -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" diff --git a/examples/permissioned-markets/Cargo.toml b/examples/permissioned-markets/Cargo.toml new file mode 100644 index 000000000..60fff2a21 --- /dev/null +++ b/examples/permissioned-markets/Cargo.toml @@ -0,0 +1,8 @@ +[workspace] +members = [ + "programs/*" +] + +exclude = [ + "deps/serum-dex", +] \ No newline at end of file diff --git a/examples/permissioned-markets/deps/serum-dex b/examples/permissioned-markets/deps/serum-dex new file mode 160000 index 000000000..1f6d58670 --- /dev/null +++ b/examples/permissioned-markets/deps/serum-dex @@ -0,0 +1 @@ +Subproject commit 1f6d5867019e242a470deed79cddca0d1f15e0a3 diff --git a/examples/permissioned-markets/migrations/deploy.js b/examples/permissioned-markets/migrations/deploy.js new file mode 100644 index 000000000..7cca27191 --- /dev/null +++ b/examples/permissioned-markets/migrations/deploy.js @@ -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. +} diff --git a/examples/permissioned-markets/programs/permissioned-markets/Cargo.toml b/examples/permissioned-markets/programs/permissioned-markets/Cargo.toml new file mode 100644 index 000000000..b8eb761df --- /dev/null +++ b/examples/permissioned-markets/programs/permissioned-markets/Cargo.toml @@ -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" \ No newline at end of file diff --git a/examples/permissioned-markets/programs/permissioned-markets/Xargo.toml b/examples/permissioned-markets/programs/permissioned-markets/Xargo.toml new file mode 100644 index 000000000..475fb71ed --- /dev/null +++ b/examples/permissioned-markets/programs/permissioned-markets/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] diff --git a/examples/permissioned-markets/programs/permissioned-markets/src/lib.rs b/examples/permissioned-markets/programs/permissioned-markets/src/lib.rs new file mode 100644 index 000000000..46be416ca --- /dev/null +++ b/examples/permissioned-markets/programs/permissioned-markets/src/lib.rs @@ -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, 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::() + 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"); +} diff --git a/examples/permissioned-markets/tests/permissioned-markets.js b/examples/permissioned-markets/tests/permissioned-markets.js new file mode 100644 index 000000000..927a65a25 --- /dev/null +++ b/examples/permissioned-markets/tests/permissioned-markets.js @@ -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; +} diff --git a/examples/permissioned-markets/tests/utils/index.js b/examples/permissioned-markets/tests/utils/index.js new file mode 100644 index 000000000..43e2fb5e1 --- /dev/null +++ b/examples/permissioned-markets/tests/utils/index.js @@ -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, +}; diff --git a/lang/derive/accounts/src/lib.rs b/lang/derive/accounts/src/lib.rs index eb218019b..653fb3252 100644 --- a/lang/derive/accounts/src/lib.rs +++ b/lang/derive/accounts/src/lib.rs @@ -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 = )]` | 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 . | /// | `#[account(has_one = )]` | On `ProgramAccount` or `CpiAccount` structs | Checks the `target` field on the account matches the `target` field in the struct deriving `Accounts`. | -/// | `#[account(seeds = [])]` | On `AccountInfo` structs | Seeds for the program derived address an `AccountInfo` struct represents. | +/// | `#[account(seeds = [], bump? = , payer? = , space? = , owner? = )]` | 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 = )]` | On any type deriving `Accounts` | Executes the given code as a constraint. The expression should evaluate to a boolean. | /// | `#[account("")]` | Deprecated | Executes the given code literal as a constraint. The literal should evaluate to a boolean. | /// | `#[account(rent_exempt = )]` | 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. | diff --git a/lang/src/lib.rs b/lang/src/lib.rs index a4b111a0a..14c2f5ae4 100644 --- a/lang/src/lib.rs +++ b/lang/src/lib.rs @@ -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()); + } + }; +} diff --git a/lang/syn/src/codegen/accounts/constraints.rs b/lang/syn/src/codegen/accounts/constraints.rs index 0184d75f3..0c0bbc9bb 100644 --- a/lang/syn/src/codegen/accounts/constraints.rs +++ b/lang/syn/src/codegen/accounts/constraints.rs @@ -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 diff --git a/lang/syn/src/codegen/program/dispatch.rs b/lang/syn/src/codegen/program/dispatch.rs index 41ae8ca29..53102616f 100644 --- a/lang/syn/src/codegen/program/dispatch.rs +++ b/lang/syn/src/codegen/program/dispatch.rs @@ -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 { 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) } }) } diff --git a/lang/syn/src/codegen/program/entry.rs b/lang/syn/src/codegen/program/entry.rs index 36c237212..1e2bdeab1 100644 --- a/lang/syn/src/codegen/program/entry.rs +++ b/lang/syn/src/codegen/program/entry.rs @@ -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 diff --git a/lang/syn/src/lib.rs b/lang/syn/src/lib.rs index 5c59cb89f..ae354a85d 100644 --- a/lang/syn/src/lib.rs +++ b/lang/syn/src/lib.rs @@ -341,6 +341,7 @@ pub enum ConstraintToken { Address(Context), TokenMint(Context), TokenAuthority(Context), + Bump(Context), } impl Parse for ConstraintToken { @@ -396,6 +397,7 @@ pub struct ConstraintSeedsGroup { pub payer: Option, pub space: Option, pub kind: PdaKind, + pub bump: Option, } #[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 }, 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 { diff --git a/lang/syn/src/parser/accounts/constraints.rs b/lang/syn/src/parser/accounts/constraints.rs index 00fb6acf0..06ad6a7ef 100644 --- a/lang/syn/src/parser/accounts/constraints.rs +++ b/lang/syn/src/parser/accounts/constraints.rs @@ -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 { 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>, pub token_mint: Option>, pub token_authority: Option>, + pub bump: Option>, } 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 { @@ -298,6 +299,7 @@ impl<'ty> ConstraintGroupBuilder<'ty> { address, token_mint, token_authority, + bump, } = self; // Converts Option> -> Option. @@ -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) -> 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) -> ParseResult<()> { if self.token_authority.is_some() { return Err(ParseError::new(