소스 검색

examples: Permissioned markets (#483)

Armani Ferrante 4 년 전
부모
커밋
5018e98a9c

+ 3 - 0
.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

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

+ 1 - 0
Cargo.toml

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

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

+ 8 - 0
examples/permissioned-markets/Cargo.toml

@@ -0,0 +1,8 @@
+[workspace]
+members = [
+    "programs/*"
+]
+
+exclude = [
+    "deps/serum-dex",
+]

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

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

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

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

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

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

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

+ 284 - 0
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;
+}

+ 427 - 0
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,
+};

+ 1 - 1
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 = <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. |

+ 35 - 4
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());
+        }
+    };
+}

+ 124 - 36
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,
-            quote! {
-                anchor_lang::ProgramAccount
-            },
-            false,
-        ),
-        Ty::Loader(ty) => (
-            &ty.account_type_path,
-            quote! {
-                anchor_lang::Loader
-            },
-            true,
-        ),
-        Ty::CpiAccount(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::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

+ 16 - 2
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<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)
         }
     })
 }

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

+ 8 - 1
lang/syn/src/lib.rs

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

+ 38 - 10
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<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(