Эх сурвалжийг харах

Token PDAs and CLI scripts (#400)

Armani Ferrante 4 жил өмнө
parent
commit
f067624add
46 өөрчлөгдсөн 2870 нэмэгдсэн , 183 устгасан
  1. 9 0
      .gitmodules
  2. 1 0
      .travis.yml
  3. 8 0
      CHANGELOG.md
  4. 20 0
      Cargo.lock
  5. 2 1
      Cargo.toml
  6. 59 12
      cli/src/config.rs
  7. 42 2
      cli/src/main.rs
  8. 41 0
      examples/cfo/Anchor.toml
  9. 9 0
      examples/cfo/Cargo.toml
  10. 1 0
      examples/cfo/deps/serum-dex
  11. 1 0
      examples/cfo/deps/stake
  12. 1 0
      examples/cfo/deps/swap
  13. 13 0
      examples/cfo/migrations/deploy.js
  14. 24 0
      examples/cfo/programs/cfo/Cargo.toml
  15. 2 0
      examples/cfo/programs/cfo/Xargo.toml
  16. 818 0
      examples/cfo/programs/cfo/src/lib.rs
  17. 19 0
      examples/cfo/scripts/common.sh
  18. 34 0
      examples/cfo/scripts/fees.js
  19. 20 0
      examples/cfo/scripts/list-market.js
  20. 58 0
      examples/cfo/scripts/localnet.sh
  21. 1 0
      examples/cfo/scripts/market-maker.json
  22. 16 0
      examples/cfo/scripts/trade-bot.js
  23. 202 0
      examples/cfo/tests/cfo.js
  24. 647 0
      examples/cfo/tests/utils/index.js
  25. 184 0
      examples/cfo/tests/utils/stake.js
  26. 1 0
      examples/misc/programs/misc/Cargo.toml
  27. 26 2
      examples/misc/programs/misc/src/lib.rs
  28. 59 26
      examples/misc/tests/misc.js
  29. 4 0
      lang/src/cpi_account.rs
  30. 2 0
      lang/src/error.rs
  31. 40 0
      lang/src/lib.rs
  32. 4 0
      lang/src/program_account.rs
  33. 238 99
      lang/syn/src/codegen/accounts/constraints.rs
  34. 37 9
      lang/syn/src/lib.rs
  35. 115 8
      lang/syn/src/parser/accounts/constraints.rs
  36. 28 0
      spl/src/dex.rs
  37. 1 0
      spl/src/lib.rs
  38. 13 0
      spl/src/mint.rs
  39. 4 0
      spl/src/token.rs
  40. 2 0
      ts/src/error.ts
  41. 2 8
      ts/src/program/index.ts
  42. 10 11
      ts/src/program/namespace/account.ts
  43. 1 0
      ts/src/utils/index.ts
  44. 19 0
      ts/src/utils/pubkey.ts
  45. 23 0
      ts/src/utils/token.ts
  46. 9 5
      ts/src/workspace.ts

+ 9 - 0
.gitmodules

@@ -1,3 +1,12 @@
 [submodule "examples/swap/deps/serum-dex"]
 	path = examples/swap/deps/serum-dex
 	url = https://github.com/project-serum/serum-dex
+[submodule "examples/cfo/deps/serum-dex"]
+	path = examples/cfo/deps/serum-dex
+	url = https://github.com/project-serum/serum-dex
+[submodule "examples/cfo/deps/swap"]
+	path = examples/cfo/deps/swap
+	url = https://github.com/project-serum/swap.git
+[submodule "examples/cfo/deps/stake"]
+	path = examples/cfo/deps/stake
+	url = https://github.com/project-serum/stake.git

+ 1 - 0
.travis.yml

@@ -64,6 +64,7 @@ jobs:
         - pushd examples/chat && yarn && anchor test && popd
         - pushd examples/ido-pool && yarn && anchor test && popd
         - pushd examples/swap/deps/serum-dex/dex && cargo build-bpf && cd ../../../ && anchor test && popd
+        - pushd examples/cfo && anchor run test && popd
     - <<: *examples
       name: Runs the examples 3
       script:

+ 8 - 0
CHANGELOG.md

@@ -11,6 +11,14 @@ incremented for features.
 
 ## [Unreleased]
 
+### Features
+
+* lang: Add `#[account(address = <expr>)]` constraint for asserting the address of an account ([#400](https://github.com/project-serum/anchor/pull/400)).
+* lang: Add `#[account(init, token = <mint-target>, authority = <token-owner-target>...)]` constraint for initializing SPL token accounts as program derived addresses for the program. Can be used when initialized via `seeds` or `associated` ([#400](https://github.com/project-serum/anchor/pull/400)).
+* lang: Add `associated_seeds!` macro for generating signer seeds for CPIs signed by an `#[account(associated = <target>)]` account ([#400](https://github.com/project-serum/anchor/pull/400)).
+* cli: Add `[scripts]` section to the Anchor.toml for specifying workspace scripts that can be run via `anchor run <script>` ([#400](https://github.com/project-serum/anchor/pull/400)).
+* cli: `[clusters.<network>]` table entries can now also use `{ address = <base58-str>, idl = <filepath-str> }` to specify workspace programs ([#400](https://github.com/project-serum/anchor/pull/400)).
+
 ## [0.9.0] - 2021-06-15
 
 ### Features

+ 20 - 0
Cargo.lock

@@ -214,6 +214,7 @@ dependencies = [
  "bs58",
  "heck",
  "proc-macro2 1.0.24",
+ "proc-macro2-diagnostics",
  "quote 1.0.9",
  "serde",
  "serde_json",
@@ -2270,6 +2271,19 @@ dependencies = [
  "unicode-xid 0.2.1",
 ]
 
+[[package]]
+name = "proc-macro2-diagnostics"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4bf29726d67464d49fa6224a1d07936a8c08bb3fba727c7493f6cf1616fdaada"
+dependencies = [
+ "proc-macro2 1.0.24",
+ "quote 1.0.9",
+ "syn 1.0.67",
+ "version_check",
+ "yansi",
+]
+
 [[package]]
 name = "qstring"
 version = "0.7.2"
@@ -4312,6 +4326,12 @@ dependencies = [
  "linked-hash-map",
 ]
 
+[[package]]
+name = "yansi"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fc79f4a1e39857fc00c3f662cbf2651c771f00e9c15fe2abc341806bd46bd71"
+
 [[package]]
 name = "zeroize"
 version = "1.2.0"

+ 2 - 1
Cargo.toml

@@ -15,5 +15,6 @@ members = [
     "spl",
 ]
 exclude = [
-    "examples/swap/deps/serum-dex"
+    "examples/swap/deps/serum-dex",
+    "examples/cfo/deps/serum-dex",
 ]

+ 59 - 12
cli/src/config.rs

@@ -6,6 +6,7 @@ use serde::{Deserialize, Serialize};
 use solana_sdk::pubkey::Pubkey;
 use solana_sdk::signature::Keypair;
 use std::collections::BTreeMap;
+use std::convert::TryFrom;
 use std::fs::{self, File};
 use std::io::prelude::*;
 use std::path::Path;
@@ -16,6 +17,7 @@ use std::str::FromStr;
 pub struct Config {
     pub provider: ProviderConfig,
     pub clusters: ClustersConfig,
+    pub scripts: ScriptsConfig,
     pub test: Option<Test>,
 }
 
@@ -25,6 +27,8 @@ pub struct ProviderConfig {
     pub wallet: WalletPath,
 }
 
+pub type ScriptsConfig = BTreeMap<String, String>;
+
 pub type ClustersConfig = BTreeMap<Cluster, BTreeMap<String, ProgramDeployment>>;
 
 impl Config {
@@ -100,7 +104,8 @@ impl Config {
 struct _Config {
     provider: Provider,
     test: Option<Test>,
-    clusters: Option<BTreeMap<String, BTreeMap<String, String>>>,
+    scripts: Option<ScriptsConfig>,
+    clusters: Option<BTreeMap<String, BTreeMap<String, serde_json::Value>>>,
 }
 
 #[derive(Debug, Serialize, Deserialize)]
@@ -125,6 +130,10 @@ impl ToString for Config {
                 wallet: self.provider.wallet.to_string(),
             },
             test: self.test.clone(),
+            scripts: match self.scripts.is_empty() {
+                true => None,
+                false => Some(self.scripts.clone()),
+            },
             clusters,
         };
 
@@ -143,6 +152,7 @@ impl FromStr for Config {
                 cluster: cfg.provider.cluster.parse()?,
                 wallet: shellexpand::tilde(&cfg.provider.wallet).parse()?,
             },
+            scripts: cfg.scripts.unwrap_or_else(|| BTreeMap::new()),
             test: cfg.test,
             clusters: cfg
                 .clusters
@@ -153,22 +163,27 @@ impl FromStr for Config {
 
 fn ser_clusters(
     clusters: &BTreeMap<Cluster, BTreeMap<String, ProgramDeployment>>,
-) -> BTreeMap<String, BTreeMap<String, String>> {
+) -> BTreeMap<String, BTreeMap<String, serde_json::Value>> {
     clusters
         .iter()
         .map(|(cluster, programs)| {
             let cluster = cluster.to_string();
             let programs = programs
                 .iter()
-                .map(|(name, deployment)| (name.clone(), deployment.program_id.to_string()))
-                .collect::<BTreeMap<String, String>>();
+                .map(|(name, deployment)| {
+                    (
+                        name.clone(),
+                        serde_json::to_value(&_ProgramDeployment::from(deployment)).unwrap(),
+                    )
+                })
+                .collect::<BTreeMap<String, serde_json::Value>>();
             (cluster, programs)
         })
-        .collect::<BTreeMap<String, BTreeMap<String, String>>>()
+        .collect::<BTreeMap<String, BTreeMap<String, serde_json::Value>>>()
 }
 
 fn deser_clusters(
-    clusters: BTreeMap<String, BTreeMap<String, String>>,
+    clusters: BTreeMap<String, BTreeMap<String, serde_json::Value>>,
 ) -> Result<BTreeMap<Cluster, BTreeMap<String, ProgramDeployment>>> {
     clusters
         .iter()
@@ -179,10 +194,17 @@ fn deser_clusters(
                 .map(|(name, program_id)| {
                     Ok((
                         name.clone(),
-                        ProgramDeployment {
-                            name: name.clone(),
-                            program_id: program_id.parse()?,
-                        },
+                        ProgramDeployment::try_from(match &program_id {
+                            serde_json::Value::String(address) => _ProgramDeployment {
+                                address: address.parse()?,
+                                idl: None,
+                            },
+                            serde_json::Value::Object(_) => {
+                                serde_json::from_value(program_id.clone())
+                                    .map_err(|_| anyhow!("Unable to read toml"))?
+                            }
+                            _ => return Err(anyhow!("Invalid toml type")),
+                        })?,
                     ))
                 })
                 .collect::<Result<BTreeMap<String, ProgramDeployment>>>()?;
@@ -269,8 +291,33 @@ impl Program {
 
 #[derive(Debug, Default)]
 pub struct ProgramDeployment {
-    pub name: String,
-    pub program_id: Pubkey,
+    pub address: Pubkey,
+    pub idl: Option<String>,
+}
+
+impl TryFrom<_ProgramDeployment> for ProgramDeployment {
+    type Error = anyhow::Error;
+    fn try_from(pd: _ProgramDeployment) -> Result<Self, Self::Error> {
+        Ok(ProgramDeployment {
+            address: pd.address.parse()?,
+            idl: pd.idl,
+        })
+    }
+}
+
+#[derive(Debug, Default, Serialize, Deserialize)]
+pub struct _ProgramDeployment {
+    pub address: String,
+    pub idl: Option<String>,
+}
+
+impl From<&ProgramDeployment> for _ProgramDeployment {
+    fn from(pd: &ProgramDeployment) -> Self {
+        Self {
+            address: pd.address.to_string(),
+            idl: pd.idl.clone(),
+        }
+    }
 }
 
 pub struct ProgramWorkspace {

+ 42 - 2
cli/src/main.rs

@@ -147,6 +147,11 @@ pub enum Command {
     /// Starts a node shell with an Anchor client setup according to the local
     /// config.
     Shell,
+    /// Runs the script defined by the current workspace's Anchor.toml.
+    Run {
+        /// The name of the script to run.
+        script: String,
+    },
 }
 
 #[derive(Debug, Clap)]
@@ -267,6 +272,7 @@ fn main() -> Result<()> {
         Command::Airdrop => airdrop(cfg_override),
         Command::Cluster { subcmd } => cluster(subcmd),
         Command::Shell => shell(&opts.cfg_override),
+        Command::Run { script } => run(&opts.cfg_override, script),
     }
 }
 
@@ -1613,17 +1619,31 @@ fn cluster(_cmd: ClusterCommand) -> Result<()> {
 fn shell(cfg_override: &ConfigOverride) -> Result<()> {
     with_workspace(cfg_override, |cfg, _path, _cargo| {
         let programs = {
-            let idls: HashMap<String, Idl> = read_all_programs()?
+            let mut idls: HashMap<String, Idl> = read_all_programs()?
                 .iter()
                 .map(|program| (program.idl.name.clone(), program.idl.clone()))
                 .collect();
+            // Insert all manually specified idls into the idl map.
+            cfg.clusters.get(&cfg.provider.cluster).map(|programs| {
+                let _ = programs
+                    .iter()
+                    .map(|(name, pd)| {
+                        if let Some(idl_fp) = &pd.idl {
+                            let file_str =
+                                std::fs::read_to_string(idl_fp).expect("Unable to read IDL file");
+                            let idl = serde_json::from_str(&file_str).expect("Idl not readable");
+                            idls.insert(name.clone(), idl);
+                        }
+                    })
+                    .collect::<Vec<_>>();
+            });
             match cfg.clusters.get(&cfg.provider.cluster) {
                 None => Vec::new(),
                 Some(programs) => programs
                     .iter()
                     .map(|(name, program_deployment)| ProgramWorkspace {
                         name: name.to_string(),
-                        program_id: program_deployment.program_id,
+                        program_id: program_deployment.address,
                         idl: match idls.get(name) {
                             None => {
                                 println!("Unable to find IDL for {}", name);
@@ -1655,6 +1675,26 @@ fn shell(cfg_override: &ConfigOverride) -> Result<()> {
     })
 }
 
+fn run(cfg_override: &ConfigOverride, script: String) -> Result<()> {
+    with_workspace(cfg_override, |cfg, _path, _cargo| {
+        let script = cfg
+            .scripts
+            .get(&script)
+            .ok_or(anyhow!("Unable to find script"))?;
+        let exit = std::process::Command::new("bash")
+            .arg("-c")
+            .arg(&script)
+            .stdout(Stdio::inherit())
+            .stderr(Stdio::inherit())
+            .output()
+            .unwrap();
+        if !exit.status.success() {
+            std::process::exit(exit.status.code().unwrap_or(1));
+        }
+        Ok(())
+    })
+}
+
 // with_workspace ensures the current working directory is always the top level
 // workspace directory, i.e., where the `Anchor.toml` file is located, before
 // and after the closure invocation.

+ 41 - 0
examples/cfo/Anchor.toml

@@ -0,0 +1,41 @@
+[provider]
+cluster = "localnet"
+wallet = "~/.config/solana/id.json"
+
+[clusters.localnet]
+registry = { address = "GrAkKfEpTKQuVHG2Y97Y2FF4i7y7Q5AHLK94JBy7Y5yv", idl = "./deps/stake/target/idl/registry.json" }
+lockup = { address = "6ebQNeTPZ1j7k3TtkCCtEPRvG7GQsucQrZ7sSEDQi9Ks", idl = "./deps/stake/target/idl/lockup.json" }
+
+[scripts]
+#
+# Testing.
+#
+test = "anchor run build && anchor test --skip-build"
+#
+# Build the program and all CPI dependencies.
+#
+build = "anchor run build-deps && anchor build"
+build-deps = "anchor run build-dex && anchor run build-swap && anchor run build-stake"
+build-dex = "pushd deps/serum-dex/dex/ && cargo build-bpf && popd"
+build-swap = "cd deps/swap && pwd && anchor build && cd ../../"
+build-stake = "pushd deps/stake && anchor build && popd"
+#
+# Runs a localnet with all the programs deployed.
+#
+localnet = "./scripts/localnet.sh"
+
+[[test.genesis]]
+address = "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin"
+program = "./deps/serum-dex/dex/target/deploy/serum_dex.so"
+
+[[test.genesis]]
+address = "22Y43yTVxuUkoRKdm9thyRhQ3SdgQS7c7kB6UNCiaczD"
+program = "./deps/swap/target/deploy/swap.so"
+
+[[test.genesis]]
+address = "GrAkKfEpTKQuVHG2Y97Y2FF4i7y7Q5AHLK94JBy7Y5yv"
+program = "./deps/stake/target/deploy/registry.so"
+
+[[test.genesis]]
+address = "6ebQNeTPZ1j7k3TtkCCtEPRvG7GQsucQrZ7sSEDQi9Ks"
+program = "./deps/stake/target/deploy/lockup.so"

+ 9 - 0
examples/cfo/Cargo.toml

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

+ 1 - 0
examples/cfo/deps/serum-dex

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

+ 1 - 0
examples/cfo/deps/stake

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

+ 1 - 0
examples/cfo/deps/swap

@@ -0,0 +1 @@
+Subproject commit 0382f2e27db5f95d09aec5e6df7bb01bfc8f0e7f

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

+ 24 - 0
examples/cfo/programs/cfo/Cargo.toml

@@ -0,0 +1,24 @@
+[package]
+name = "cfo"
+version = "0.1.0"
+description = "Created with Anchor"
+edition = "2018"
+
+[lib]
+crate-type = ["cdylib", "lib"]
+name = "cfo"
+
+[features]
+no-entrypoint = []
+no-idl = []
+cpi = ["no-entrypoint"]
+default = ["test"]
+test = []
+
+[dependencies]
+anchor-lang = { path = "../../../../lang" }
+anchor-spl = { path = "../../../../spl" }
+spl-token = { version ="3.1.1", features = ["no-entrypoint"] }
+swap = { path = "../../deps/swap/programs/swap", features = ["cpi"] }
+registry = { path = "../../deps/stake/programs/registry", features = ["cpi"] }
+lockup = { path = "../../deps/stake/programs/lockup", features = ["cpi"] }

+ 2 - 0
examples/cfo/programs/cfo/Xargo.toml

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

+ 818 - 0
examples/cfo/programs/cfo/src/lib.rs

@@ -0,0 +1,818 @@
+// WIP. This program has been checkpointed and is not production ready.
+
+use anchor_lang::associated_seeds;
+use anchor_lang::prelude::*;
+use anchor_lang::solana_program::sysvar::instructions as tx_instructions;
+use anchor_lang::solana_program::{system_instruction, system_program};
+use anchor_spl::token::{self, Mint, TokenAccount};
+use anchor_spl::{dex, mint};
+use registry::{Registrar, RewardVendorKind};
+use std::convert::TryInto;
+
+/// CFO is the program representing the Serum chief financial officer. It is
+/// the program responsible for collecting and distributing fees from the Serum
+/// DEX.
+#[program]
+pub mod cfo {
+    use super::*;
+
+    /// Creates a financial officer account associated with a DEX program ID.
+    #[access_control(is_distribution_valid(&d))]
+    pub fn create_officer(
+        ctx: Context<CreateOfficer>,
+        d: Distribution,
+        registrar: Pubkey,
+        msrm_registrar: Pubkey,
+    ) -> Result<()> {
+        let officer = &mut ctx.accounts.officer;
+        officer.authority = *ctx.accounts.authority.key;
+        officer.swap_program = *ctx.accounts.swap_program.key;
+        officer.dex_program = *ctx.accounts.dex_program.key;
+        officer.distribution = d;
+        officer.registrar = registrar;
+        officer.msrm_registrar = msrm_registrar;
+        officer.stake = *ctx.accounts.stake.to_account_info().key;
+        officer.treasury = *ctx.accounts.treasury.to_account_info().key;
+        officer.srm_vault = *ctx.accounts.srm_vault.to_account_info().key;
+        emit!(OfficerDidCreate {
+            pubkey: *officer.to_account_info().key,
+        });
+        Ok(())
+    }
+
+    /// Creates a deterministic token account owned by the CFO.
+    /// This should be used when a new mint is used for collecting fees.
+    /// Can only be called once per token CFO and token mint.
+    pub fn create_officer_token(_ctx: Context<CreateOfficerToken>) -> Result<()> {
+        Ok(())
+    }
+
+    /// Updates the cfo's fee distribution.
+    #[access_control(is_distribution_valid(&d))]
+    pub fn set_distribution(ctx: Context<SetDistribution>, d: Distribution) -> Result<()> {
+        ctx.accounts.officer.distribution = d.clone();
+        emit!(DistributionDidChange { distribution: d });
+        Ok(())
+    }
+
+    /// Transfers fees from the dex to the CFO.
+    pub fn sweep_fees<'info>(ctx: Context<'_, '_, '_, 'info, SweepFees<'info>>) -> Result<()> {
+        let seeds = associated_seeds! {
+            account = ctx.accounts.officer,
+            associated = ctx.accounts.dex.dex_program
+        };
+        let cpi_ctx: CpiContext<'_, '_, '_, 'info, dex::SweepFees<'info>> = (&*ctx.accounts).into();
+        dex::sweep_fees(cpi_ctx.with_signer(&[seeds]))?;
+        Ok(())
+    }
+
+    /// Convert the CFO's entire non-SRM token balance into USDC.
+    /// Assumes USDC is the quote currency.
+    #[access_control(is_not_trading(&ctx.accounts.instructions))]
+    pub fn swap_to_usdc<'info>(
+        ctx: Context<'_, '_, '_, 'info, SwapToUsdc<'info>>,
+        min_exchange_rate: ExchangeRate,
+    ) -> Result<()> {
+        let seeds = associated_seeds! {
+            account = ctx.accounts.officer,
+            associated = ctx.accounts.dex_program
+        };
+        let cpi_ctx: CpiContext<'_, '_, '_, 'info, swap::Swap<'info>> = (&*ctx.accounts).into();
+        swap::cpi::swap(
+            cpi_ctx.with_signer(&[seeds]),
+            swap::Side::Bid,
+            token::accessor::amount(&ctx.accounts.from_vault)?,
+            min_exchange_rate.into(),
+        )?;
+        Ok(())
+    }
+
+    /// Convert the CFO's entire token balance into SRM.
+    /// Assumes SRM is the base currency.
+    #[access_control(is_not_trading(&ctx.accounts.instructions))]
+    pub fn swap_to_srm<'info>(
+        ctx: Context<'_, '_, '_, 'info, SwapToSrm<'info>>,
+        min_exchange_rate: ExchangeRate,
+    ) -> Result<()> {
+        let seeds = associated_seeds! {
+            account = ctx.accounts.officer,
+            associated = ctx.accounts.dex_program
+        };
+        let cpi_ctx: CpiContext<'_, '_, '_, 'info, swap::Swap<'info>> = (&*ctx.accounts).into();
+        swap::cpi::swap(
+            cpi_ctx.with_signer(&[seeds]),
+            swap::Side::Bid,
+            token::accessor::amount(&ctx.accounts.from_vault)?,
+            min_exchange_rate.into(),
+        )?;
+        Ok(())
+    }
+
+    /// Distributes srm tokens to the various categories. Before calling this,
+    /// one must convert the fees into SRM via the swap APIs.
+    #[access_control(is_distribution_ready(&ctx.accounts))]
+    pub fn distribute<'info>(ctx: Context<'_, '_, '_, 'info, Distribute<'info>>) -> Result<()> {
+        let total_fees = ctx.accounts.srm_vault.amount;
+        let seeds = associated_seeds! {
+            account = ctx.accounts.officer,
+            associated = ctx.accounts.dex_program
+        };
+
+        // Burn.
+        let burn_amount: u64 = u128::from(total_fees)
+            .checked_mul(ctx.accounts.officer.distribution.burn.into())
+            .unwrap()
+            .checked_div(100)
+            .unwrap()
+            .try_into()
+            .map_err(|_| ErrorCode::U128CannotConvert)?;
+        token::burn(ctx.accounts.into_burn().with_signer(&[seeds]), burn_amount)?;
+
+        // Stake.
+        let stake_amount: u64 = u128::from(total_fees)
+            .checked_mul(ctx.accounts.officer.distribution.stake.into())
+            .unwrap()
+            .checked_div(100)
+            .unwrap()
+            .try_into()
+            .map_err(|_| ErrorCode::U128CannotConvert)?;
+        token::transfer(
+            ctx.accounts.into_stake_transfer().with_signer(&[seeds]),
+            stake_amount,
+        )?;
+
+        // Treasury.
+        let treasury_amount: u64 = u128::from(total_fees)
+            .checked_mul(ctx.accounts.officer.distribution.treasury.into())
+            .unwrap()
+            .checked_div(100)
+            .unwrap()
+            .try_into()
+            .map_err(|_| ErrorCode::U128CannotConvert)?;
+        token::transfer(
+            ctx.accounts.into_treasury_transfer().with_signer(&[seeds]),
+            treasury_amount,
+        )?;
+
+        Ok(())
+    }
+
+    #[access_control(is_stake_reward_ready(&ctx.accounts))]
+    pub fn drop_stake_reward<'info>(
+        ctx: Context<'_, '_, '_, 'info, DropStakeReward<'info>>,
+    ) -> Result<()> {
+        // Common reward parameters.
+        let expiry_ts = 1853942400; // 9/30/2028.
+        let expiry_receiver = *ctx.accounts.officer.to_account_info().key;
+        let locked_kind = {
+            let start_ts = 1633017600; // 9/30/2021.
+            let end_ts = 1822320000; // 9/30/2027.
+            let period_count = 2191;
+            RewardVendorKind::Locked {
+                start_ts,
+                end_ts,
+                period_count,
+            }
+        };
+        let seeds = associated_seeds! {
+            account = ctx.accounts.officer,
+            associated = ctx.accounts.dex_program
+        };
+
+        // Total amount staked denominated in SRM (i.e. MSRM is converted to
+        // SRM)
+        let total_pool_value = u128::from(ctx.accounts.srm.pool_mint.supply)
+            .checked_mul(500)
+            .unwrap()
+            .checked_add(
+                u128::from(ctx.accounts.msrm.pool_mint.supply)
+                    .checked_mul(1_000_000)
+                    .unwrap(),
+            )
+            .unwrap();
+
+        // Total reward split between both the SRM and MSRM stake pools.
+        let total_reward_amount = u128::from(ctx.accounts.stake.amount);
+
+        // Proportion of the reward going to the srm pool.
+        //
+        // total_reward_amount * (srm_pool_value / total_pool_value)
+        //
+        let srm_amount: u64 = u128::from(ctx.accounts.srm.pool_mint.supply)
+            .checked_mul(500)
+            .unwrap()
+            .checked_mul(total_reward_amount)
+            .unwrap()
+            .checked_div(total_pool_value)
+            .unwrap()
+            .try_into()
+            .map_err(|_| ErrorCode::U128CannotConvert)?;
+
+        // Proportion of the reward going to the msrm pool.
+        //
+        // total_reward_amount * (msrm_pool_value / total_pool_value)
+        //
+        let msrm_amount = u128::from(ctx.accounts.msrm.pool_mint.supply)
+            .checked_mul(total_reward_amount)
+            .unwrap()
+            .checked_div(total_pool_value)
+            .unwrap()
+            .try_into()
+            .map_err(|_| ErrorCode::U128CannotConvert)?;
+
+        // SRM drop.
+        {
+            // Drop locked reward.
+            let (_, nonce) = Pubkey::find_program_address(
+                &[
+                    ctx.accounts.srm.registrar.to_account_info().key.as_ref(),
+                    ctx.accounts.srm.vendor.to_account_info().key.as_ref(),
+                ],
+                ctx.accounts.token_program.key,
+            );
+            registry::cpi::drop_reward(
+                ctx.accounts.into_srm_reward().with_signer(&[seeds]),
+                locked_kind.clone(),
+                srm_amount.try_into().unwrap(),
+                expiry_ts,
+                expiry_receiver,
+                nonce,
+            )?;
+
+            // Drop unlocked reward.
+            registry::cpi::drop_reward(
+                ctx.accounts.into_srm_reward().with_signer(&[seeds]),
+                RewardVendorKind::Unlocked,
+                srm_amount,
+                expiry_ts,
+                expiry_receiver,
+                nonce,
+            )?;
+        }
+
+        // MSRM drop.
+        {
+            // Drop locked reward.
+            let (_, nonce) = Pubkey::find_program_address(
+                &[
+                    ctx.accounts.msrm.registrar.to_account_info().key.as_ref(),
+                    ctx.accounts.msrm.vendor.to_account_info().key.as_ref(),
+                ],
+                ctx.accounts.token_program.key,
+            );
+            registry::cpi::drop_reward(
+                ctx.accounts.into_msrm_reward().with_signer(&[seeds]),
+                locked_kind,
+                msrm_amount,
+                expiry_ts,
+                expiry_receiver,
+                nonce,
+            )?;
+
+            // Drop unlocked reward.
+            registry::cpi::drop_reward(
+                ctx.accounts.into_msrm_reward().with_signer(&[seeds]),
+                RewardVendorKind::Unlocked,
+                msrm_amount,
+                expiry_ts,
+                expiry_receiver,
+                nonce,
+            )?;
+        }
+
+        Ok(())
+    }
+}
+
+// Context accounts.
+
+#[derive(Accounts)]
+pub struct CreateOfficer<'info> {
+    #[account(init, associated = dex_program, payer = authority)]
+    officer: ProgramAccount<'info, Officer>,
+    #[account(
+        init,
+        token = mint,
+        associated = officer, with = b"vault",
+        space = TokenAccount::LEN,
+        payer = authority,
+    )]
+    srm_vault: CpiAccount<'info, TokenAccount>,
+    #[account(
+        init,
+        token = mint,
+        associated = officer, with = b"stake",
+        space = TokenAccount::LEN,
+        payer = authority,
+    )]
+    stake: CpiAccount<'info, TokenAccount>,
+    #[account(
+        init,
+        token = mint,
+        associated = officer, with = b"treasury",
+        space = TokenAccount::LEN,
+        payer = authority,
+    )]
+    treasury: CpiAccount<'info, TokenAccount>,
+    #[account(signer)]
+    authority: AccountInfo<'info>,
+    #[cfg_attr(
+        not(feature = "test"),
+        account(address = mint::SRM),
+    )]
+    mint: AccountInfo<'info>,
+    #[account(executable)]
+    dex_program: AccountInfo<'info>,
+    #[account(executable)]
+    swap_program: AccountInfo<'info>,
+    #[account(address = system_program::ID)]
+    system_program: AccountInfo<'info>,
+    #[account(address = spl_token::ID)]
+    token_program: AccountInfo<'info>,
+    rent: Sysvar<'info, Rent>,
+}
+
+#[derive(Accounts)]
+pub struct CreateOfficerToken<'info> {
+    officer: ProgramAccount<'info, Officer>,
+    #[account(
+        init,
+        token = mint,
+        associated = officer, with = mint,
+        space = TokenAccount::LEN,
+        payer = payer,
+    )]
+    token: CpiAccount<'info, TokenAccount>,
+    #[account(owner = token_program)]
+    mint: AccountInfo<'info>,
+    #[account(mut, signer)]
+    payer: AccountInfo<'info>,
+    #[account(address = system_program::ID)]
+    system_program: AccountInfo<'info>,
+    #[account(address = spl_token::ID)]
+    token_program: AccountInfo<'info>,
+    rent: Sysvar<'info, Rent>,
+}
+
+#[derive(Accounts)]
+pub struct SetDistribution<'info> {
+    #[account(has_one = authority)]
+    officer: ProgramAccount<'info, Officer>,
+    #[account(signer)]
+    authority: AccountInfo<'info>,
+}
+
+#[derive(Accounts)]
+pub struct SweepFees<'info> {
+    #[account(associated = dex.dex_program)]
+    officer: ProgramAccount<'info, Officer>,
+    #[account(
+        mut,
+        owner = dex.token_program,
+        associated = officer, with = mint,
+    )]
+    sweep_vault: AccountInfo<'info>,
+    mint: AccountInfo<'info>,
+    dex: Dex<'info>,
+}
+
+#[derive(Accounts)]
+pub struct Dex<'info> {
+    #[account(mut)]
+    market: AccountInfo<'info>,
+    #[account(mut)]
+    pc_vault: AccountInfo<'info>,
+    sweep_authority: AccountInfo<'info>,
+    vault_signer: AccountInfo<'info>,
+    dex_program: AccountInfo<'info>,
+    #[account(address = spl_token::ID)]
+    token_program: AccountInfo<'info>,
+}
+
+#[derive(Accounts)]
+pub struct SwapToUsdc<'info> {
+    #[account(associated = dex_program)]
+    officer: ProgramAccount<'info, Officer>,
+    market: DexMarketAccounts<'info>,
+    #[account(
+        owner = token_program,
+        constraint = &officer.treasury != from_vault.key,
+        constraint = &officer.stake != from_vault.key,
+    )]
+    from_vault: AccountInfo<'info>,
+    #[account(owner = token_program)]
+    quote_vault: AccountInfo<'info>,
+    #[account(associated = officer, with = mint::USDC)]
+    usdc_vault: AccountInfo<'info>,
+    #[account(address = swap::ID)]
+    swap_program: AccountInfo<'info>,
+    #[account(address = dex::ID)]
+    dex_program: AccountInfo<'info>,
+    #[account(address = token::ID)]
+    token_program: AccountInfo<'info>,
+    rent: Sysvar<'info, Rent>,
+    #[account(address = tx_instructions::ID)]
+    instructions: AccountInfo<'info>,
+}
+
+#[derive(Accounts)]
+pub struct SwapToSrm<'info> {
+    #[account(associated = dex_program)]
+    officer: ProgramAccount<'info, Officer>,
+    market: DexMarketAccounts<'info>,
+    #[account(
+        owner = token_program,
+        constraint = &officer.treasury != from_vault.key,
+        constraint = &officer.stake != from_vault.key,
+    )]
+    from_vault: AccountInfo<'info>,
+    #[account(owner = token_program)]
+    quote_vault: AccountInfo<'info>,
+    #[account(
+        associated = officer,
+        with = mint::SRM,
+        constraint = &officer.treasury != from_vault.key,
+        constraint = &officer.stake != from_vault.key,
+    )]
+    srm_vault: AccountInfo<'info>,
+    #[account(address = swap::ID)]
+    swap_program: AccountInfo<'info>,
+    #[account(address = dex::ID)]
+    dex_program: AccountInfo<'info>,
+    #[account(address = token::ID)]
+    token_program: AccountInfo<'info>,
+    rent: Sysvar<'info, Rent>,
+    #[account(address = tx_instructions::ID)]
+    instructions: AccountInfo<'info>,
+}
+
+#[derive(Accounts)]
+pub struct DexMarketAccounts<'info> {
+    #[account(mut)]
+    market: AccountInfo<'info>,
+    #[account(mut)]
+    open_orders: AccountInfo<'info>,
+    #[account(mut)]
+    request_queue: AccountInfo<'info>,
+    #[account(mut)]
+    event_queue: AccountInfo<'info>,
+    #[account(mut)]
+    bids: AccountInfo<'info>,
+    #[account(mut)]
+    asks: AccountInfo<'info>,
+    // The `spl_token::Account` that funds will be taken from, i.e., transferred
+    // from the user into the market's vault.
+    //
+    // For bids, this is the base currency. For asks, the quote.
+    #[account(mut)]
+    order_payer_token_account: AccountInfo<'info>,
+    // Also known as the "base" currency. For a given A/B market,
+    // this is the vault for the A mint.
+    #[account(mut)]
+    coin_vault: AccountInfo<'info>,
+    // Also known as the "quote" currency. For a given A/B market,
+    // this is the vault for the B mint.
+    #[account(mut)]
+    pc_vault: AccountInfo<'info>,
+    // PDA owner of the DEX's token accounts for base + quote currencies.
+    vault_signer: AccountInfo<'info>,
+    // User wallets.
+    #[account(mut)]
+    coin_wallet: AccountInfo<'info>,
+}
+
+#[derive(Accounts)]
+pub struct Distribute<'info> {
+    #[account(has_one = treasury, has_one = stake)]
+    officer: ProgramAccount<'info, Officer>,
+    treasury: AccountInfo<'info>,
+    stake: AccountInfo<'info>,
+    #[account(
+        owner = token_program,
+        constraint = srm_vault.mint == mint::SRM,
+    )]
+    srm_vault: CpiAccount<'info, TokenAccount>,
+    #[account(address = mint::SRM)]
+    mint: AccountInfo<'info>,
+    #[account(address = spl_token::ID)]
+    token_program: AccountInfo<'info>,
+    #[account(address = dex::ID)]
+    dex_program: AccountInfo<'info>,
+}
+
+#[derive(Accounts)]
+pub struct DropStakeReward<'info> {
+    #[account(
+        has_one = stake,
+        constraint = srm.registrar.key == &officer.registrar,
+        constraint = msrm.registrar.key == &officer.msrm_registrar,
+    )]
+    officer: ProgramAccount<'info, Officer>,
+    #[account(associated = officer, with = b"stake", with = mint)]
+    stake: CpiAccount<'info, TokenAccount>,
+    #[cfg_attr(
+        not(feature = "test"),
+        account(address = mint::SRM),
+    )]
+    mint: AccountInfo<'info>,
+    srm: DropStakeRewardPool<'info>,
+    msrm: DropStakeRewardPool<'info>,
+    #[account(owner = registry_program)]
+    msrm_registrar: CpiAccount<'info, Registrar>,
+    #[account(address = token::ID)]
+    token_program: AccountInfo<'info>,
+    #[account(address = registry::ID)]
+    registry_program: AccountInfo<'info>,
+    #[account(address = lockup::ID)]
+    lockup_program: AccountInfo<'info>,
+    #[account(address = dex::ID)]
+    dex_program: AccountInfo<'info>,
+    clock: Sysvar<'info, Clock>,
+    rent: Sysvar<'info, Rent>,
+}
+
+// Don't bother doing validation on the individual accounts. Allow the stake
+// program to handle it.
+#[derive(Accounts)]
+pub struct DropStakeRewardPool<'info> {
+    registrar: AccountInfo<'info>,
+    reward_event_q: AccountInfo<'info>,
+    pool_mint: CpiAccount<'info, Mint>,
+    vendor: AccountInfo<'info>,
+    vendor_vault: AccountInfo<'info>,
+}
+
+// Accounts.
+
+#[associated]
+#[derive(Default)]
+pub struct Officer {
+    // Priviledged account.
+    pub authority: Pubkey,
+    // Vault holding the officer's SRM tokens prior to distribution.
+    pub srm_vault: Pubkey,
+    // Escrow SRM vault holding tokens which are dropped onto stakers.
+    pub stake: Pubkey,
+    // SRM token account to send treasury earned tokens to.
+    pub treasury: Pubkey,
+    // Defines the fee distribution, i.e., what percent each fee category gets.
+    pub distribution: Distribution,
+    // Swap frontend for the dex.
+    pub swap_program: Pubkey,
+    // Dex program the officer is associated with.
+    pub dex_program: Pubkey,
+    // SRM stake pool address
+    pub registrar: Pubkey,
+    // MSRM stake pool address.
+    pub msrm_registrar: Pubkey,
+}
+
+#[derive(AnchorSerialize, AnchorDeserialize, Default, Clone)]
+pub struct Distribution {
+    burn: u8,
+    stake: u8,
+    treasury: u8,
+}
+
+// CpiContext transformations.
+
+impl<'info> From<&SweepFees<'info>> for CpiContext<'_, '_, '_, 'info, dex::SweepFees<'info>> {
+    fn from(sweep: &SweepFees<'info>) -> Self {
+        let program = sweep.dex.dex_program.to_account_info();
+        let accounts = dex::SweepFees {
+            market: sweep.dex.market.to_account_info(),
+            pc_vault: sweep.dex.pc_vault.to_account_info(),
+            sweep_authority: sweep.dex.sweep_authority.to_account_info(),
+            sweep_receiver: sweep.sweep_vault.to_account_info(),
+            vault_signer: sweep.dex.vault_signer.to_account_info(),
+            token_program: sweep.dex.token_program.to_account_info(),
+        };
+        CpiContext::new(program, accounts)
+    }
+}
+
+impl<'info> From<&SwapToSrm<'info>> for CpiContext<'_, '_, '_, 'info, swap::Swap<'info>> {
+    fn from(accs: &SwapToSrm<'info>) -> Self {
+        let program = accs.swap_program.to_account_info();
+        let accounts = swap::Swap {
+            market: swap::MarketAccounts {
+                market: accs.market.market.clone(),
+                open_orders: accs.market.open_orders.clone(),
+                request_queue: accs.market.request_queue.clone(),
+                event_queue: accs.market.event_queue.clone(),
+                bids: accs.market.bids.clone(),
+                asks: accs.market.asks.clone(),
+                order_payer_token_account: accs.market.order_payer_token_account.clone(),
+                coin_vault: accs.market.coin_vault.clone(),
+                pc_vault: accs.market.pc_vault.clone(),
+                vault_signer: accs.market.vault_signer.clone(),
+                coin_wallet: accs.srm_vault.clone(),
+            },
+            authority: accs.officer.to_account_info(),
+            pc_wallet: accs.from_vault.to_account_info(),
+            dex_program: accs.dex_program.to_account_info(),
+            token_program: accs.token_program.to_account_info(),
+            rent: accs.rent.to_account_info(),
+        };
+        CpiContext::new(program, accounts)
+    }
+}
+
+impl<'info> From<&SwapToUsdc<'info>> for CpiContext<'_, '_, '_, 'info, swap::Swap<'info>> {
+    fn from(accs: &SwapToUsdc<'info>) -> Self {
+        let program = accs.swap_program.to_account_info();
+        let accounts = swap::Swap {
+            market: swap::MarketAccounts {
+                market: accs.market.market.clone(),
+                open_orders: accs.market.open_orders.clone(),
+                request_queue: accs.market.request_queue.clone(),
+                event_queue: accs.market.event_queue.clone(),
+                bids: accs.market.bids.clone(),
+                asks: accs.market.asks.clone(),
+                order_payer_token_account: accs.market.order_payer_token_account.clone(),
+                coin_vault: accs.market.coin_vault.clone(),
+                pc_vault: accs.market.pc_vault.clone(),
+                vault_signer: accs.market.vault_signer.clone(),
+                coin_wallet: accs.from_vault.to_account_info(),
+            },
+            authority: accs.officer.to_account_info(),
+            pc_wallet: accs.usdc_vault.clone(),
+            dex_program: accs.dex_program.to_account_info(),
+            token_program: accs.token_program.to_account_info(),
+            rent: accs.rent.to_account_info(),
+        };
+        CpiContext::new(program, accounts)
+    }
+}
+
+impl<'info> From<&Distribute<'info>> for CpiContext<'_, '_, '_, 'info, token::Burn<'info>> {
+    fn from(accs: &Distribute<'info>) -> Self {
+        let program = accs.token_program.to_account_info();
+        let accounts = token::Burn {
+            mint: accs.mint.to_account_info(),
+            to: accs.srm_vault.to_account_info(),
+            authority: accs.officer.to_account_info(),
+        };
+        CpiContext::new(program, accounts)
+    }
+}
+
+impl<'info> DropStakeReward<'info> {
+    fn into_srm_reward(&self) -> CpiContext<'_, '_, '_, 'info, registry::DropReward<'info>> {
+        let program = self.registry_program.clone();
+        let accounts = registry::DropReward {
+            registrar: ProgramAccount::try_from(&self.srm.registrar).unwrap(),
+            reward_event_q: ProgramAccount::try_from(&self.srm.reward_event_q).unwrap(),
+            pool_mint: self.srm.pool_mint.clone(),
+            vendor: ProgramAccount::try_from(&self.srm.vendor).unwrap(),
+            vendor_vault: CpiAccount::try_from(&self.srm.vendor_vault).unwrap(),
+            depositor: self.stake.to_account_info(),
+            depositor_authority: self.officer.to_account_info(),
+            token_program: self.token_program.clone(),
+            clock: self.clock.clone(),
+            rent: self.rent.clone(),
+        };
+        CpiContext::new(program, accounts)
+    }
+
+    fn into_msrm_reward(&self) -> CpiContext<'_, '_, '_, 'info, registry::DropReward<'info>> {
+        let program = self.registry_program.clone();
+        let accounts = registry::DropReward {
+            registrar: ProgramAccount::try_from(&self.msrm.registrar).unwrap(),
+            reward_event_q: ProgramAccount::try_from(&self.msrm.reward_event_q).unwrap(),
+            pool_mint: self.msrm.pool_mint.clone(),
+            vendor: ProgramAccount::try_from(&self.msrm.vendor).unwrap(),
+            vendor_vault: CpiAccount::try_from(&self.msrm.vendor_vault).unwrap(),
+            depositor: self.stake.to_account_info(),
+            depositor_authority: self.officer.to_account_info(),
+            token_program: self.token_program.clone(),
+            clock: self.clock.clone(),
+            rent: self.rent.clone(),
+        };
+        CpiContext::new(program, accounts)
+    }
+}
+
+impl<'info> Distribute<'info> {
+    fn into_burn(&self) -> CpiContext<'_, '_, '_, 'info, token::Burn<'info>> {
+        let program = self.token_program.clone();
+        let accounts = token::Burn {
+            mint: self.mint.clone(),
+            to: self.srm_vault.to_account_info(),
+            authority: self.officer.to_account_info(),
+        };
+        CpiContext::new(program, accounts)
+    }
+
+    fn into_stake_transfer(&self) -> CpiContext<'_, '_, '_, 'info, token::Transfer<'info>> {
+        let program = self.token_program.clone();
+        let accounts = token::Transfer {
+            from: self.srm_vault.to_account_info(),
+            to: self.stake.to_account_info(),
+            authority: self.officer.to_account_info(),
+        };
+        CpiContext::new(program, accounts)
+    }
+
+    fn into_treasury_transfer(&self) -> CpiContext<'_, '_, '_, 'info, token::Transfer<'info>> {
+        let program = self.token_program.clone();
+        let accounts = token::Transfer {
+            from: self.srm_vault.to_account_info(),
+            to: self.treasury.to_account_info(),
+            authority: self.officer.to_account_info(),
+        };
+        CpiContext::new(program, accounts)
+    }
+}
+
+// Events.
+
+#[event]
+pub struct DistributionDidChange {
+    distribution: Distribution,
+}
+
+#[event]
+pub struct OfficerDidCreate {
+    pubkey: Pubkey,
+}
+
+// Error.
+
+#[error]
+pub enum ErrorCode {
+    #[msg("Distribution does not add to 100")]
+    InvalidDistribution,
+    #[msg("u128 cannot be converted into u64")]
+    U128CannotConvert,
+    #[msg("Only one instruction is allowed for this transaction")]
+    TooManyInstructions,
+    #[msg("Not enough SRM has been accumulated to distribute")]
+    InsufficientDistributionAmount,
+    #[msg("Must drop more SRM onto the stake pool")]
+    InsufficientStakeReward,
+}
+
+// Access control.
+
+fn is_distribution_valid(d: &Distribution) -> Result<()> {
+    if d.burn + d.stake + d.treasury != 100 {
+        return Err(ErrorCode::InvalidDistribution.into());
+    }
+    Ok(())
+}
+
+fn is_distribution_ready(accounts: &Distribute) -> Result<()> {
+    if accounts.srm_vault.amount < 1_000_000 {
+        return Err(ErrorCode::InsufficientDistributionAmount.into());
+    }
+    Ok(())
+}
+
+// `ixs` must be the Instructions sysvar.
+fn is_not_trading(ixs: &AccountInfo) -> Result<()> {
+    let data = ixs.try_borrow_data()?;
+    match tx_instructions::load_instruction_at(1, &data) {
+        Ok(_) => Err(ErrorCode::TooManyInstructions.into()),
+        Err(_) => Ok(()),
+    }
+}
+
+fn is_stake_reward_ready(accounts: &DropStakeReward) -> Result<()> {
+    // Min drop is 15,0000 SRM.
+    let min_reward: u64 = 15_000_000_000;
+    if accounts.stake.amount < min_reward {
+        return Err(ErrorCode::InsufficientStakeReward.into());
+    }
+    Ok(())
+}
+
+// Redefintions.
+//
+// The following types are redefined so that they can be parsed into the IDL,
+// since Anchor doesn't yet support idl parsing across multiple crates.
+
+#[derive(AnchorSerialize, AnchorDeserialize)]
+pub struct ExchangeRate {
+    rate: u64,
+    from_decimals: u8,
+    quote_decimals: u8,
+    strict: bool,
+}
+
+impl From<ExchangeRate> for swap::ExchangeRate {
+    fn from(e: ExchangeRate) -> Self {
+        let ExchangeRate {
+            rate,
+            from_decimals,
+            quote_decimals,
+            strict,
+        } = e;
+        Self {
+            rate,
+            from_decimals,
+            quote_decimals,
+            strict,
+        }
+    }
+}

+ 19 - 0
examples/cfo/scripts/common.sh

@@ -0,0 +1,19 @@
+cleanup() {
+    pkill -P $$ || true
+    wait || true
+}
+
+trap_add() {
+    trap_add_cmd=$1; shift || fatal "${FUNCNAME} usage error"
+    for trap_add_name in "$@"; do
+        trap -- "$(
+            extract_trap_cmd() { printf '%s\n' "${3:-}"; }
+            eval "extract_trap_cmd $(trap -p "${trap_add_name}")"
+            printf '%s\n' "${trap_add_cmd}"
+        )" "${trap_add_name}" \
+            || fatal "unable to add to trap ${trap_add_name}"
+    done
+}
+
+declare -f -t trap_add
+trap_add 'cleanup' EXIT

+ 34 - 0
examples/cfo/scripts/fees.js

@@ -0,0 +1,34 @@
+#!/usr/bin/env node
+
+const process = require("process");
+const fs = require("fs");
+const anchor = require("@project-serum/anchor");
+const { Market, OpenOrders } = require("@project-serum/serum");
+const Account = anchor.web3.Account;
+const Program = anchor.Program;
+const provider = anchor.Provider.local();
+const secret = JSON.parse(fs.readFileSync("./scripts/market-maker.json"));
+const MARKET_MAKER = new Account(secret);
+const PublicKey = anchor.web3.PublicKey;
+
+const DEX_PID = new PublicKey("9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin");
+
+async function main() {
+  const market = new PublicKey(process.argv[2]);
+  while (true) {
+    let marketClient = await Market.load(
+      provider.connection,
+      market,
+      { commitment: "recent" },
+      DEX_PID
+    );
+    console.log("Fees: ", marketClient._decoded.quoteFeesAccrued.toString());
+    await sleep(3000);
+  }
+}
+
+main();
+
+function sleep(ms) {
+  return new Promise((resolve) => setTimeout(resolve, ms));
+}

+ 20 - 0
examples/cfo/scripts/list-market.js

@@ -0,0 +1,20 @@
+#!/usr/bin/env node
+
+// Script to list a market, logging the address to stdout.
+
+const utils = require("../tests/utils");
+const fs = require("fs");
+const anchor = require("@project-serum/anchor");
+const provider = anchor.Provider.local();
+
+async function main() {
+  ORDERBOOK_ENV = await utils.initMarket({
+    provider,
+  });
+  const out = {
+    market: ORDERBOOK_ENV.marketA._decoded.ownAddress.toString(),
+  };
+  console.log(JSON.stringify(out));
+}
+
+main();

+ 58 - 0
examples/cfo/scripts/localnet.sh

@@ -0,0 +1,58 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+source scripts/common.sh
+
+DEX_PID="9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin"
+PAYER_FILEPATH="$HOME/.config/solana/id.json"
+CRANK="/home/armaniferrante/Documents/code/src/github.com/project-serum/serum-dex/target/debug/crank"
+VALIDATOR_OUT="./validator-stdout.txt"
+CRANK_LOGS="crank-logs.txt"
+CRANK_STDOUT="crank-stdout.txt"
+TRADE_BOT_STDOUT="trade-bot-stdout.txt"
+FEES_STDOUT="fees.txt"
+
+main () {
+		echo "Cleaning old output files..."
+		rm -rf test-ledger
+		rm -f $TRADE_BOT_STDOUT
+		rm -f $FEES_STDOUT
+		rm -f $VALIDATOR_OUT
+		rm -f $CRANK_LOGS && touch $CRANK_LOGS
+
+		echo "Starting local network..."
+		solana-test-validator \
+				--bpf-program 9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin ./deps/serum-dex/dex/target/deploy/serum_dex.so \
+				--bpf-program 22Y43yTVxuUkoRKdm9thyRhQ3SdgQS7c7kB6UNCiaczD ./deps/swap/target/deploy/swap.so \
+				--bpf-program GrAkKfEpTKQuVHG2Y97Y2FF4i7y7Q5AHLK94JBy7Y5yv ./deps/stake/target/deploy/registry.so \
+				--bpf-program 6ebQNeTPZ1j7k3TtkCCtEPRvG7GQsucQrZ7sSEDQi9Ks ./deps/stake/target/deploy/lockup.so \
+				--bpf-program 5CHQcwNhkFiFXXM8HakHi8cB7AKP3M3GPdEBDeRJBWQq ./target/deploy/cfo.so > $VALIDATOR_OUT &
+		sleep 2
+
+		echo "Listing market..."
+		market=$(./scripts/list-market.js | jq -r .market)
+		sleep 2
+		echo "Market listed $market"
+
+		echo "Running crank..."
+		$CRANK localnet consume-events \
+					-c $market \
+					-d $DEX_PID -e 5 \
+					--log-directory $CRANK_LOGS \
+					--market $market \
+					--num-workers 1 \
+					--payer $PAYER_FILEPATH \
+					--pc-wallet $market > $CRANK_STDOUT &
+		echo "Running trade bot..."
+		./scripts/trade-bot.js $market > $TRADE_BOT_STDOUT &
+
+		echo "Running fees listener..."
+		./scripts/fees.js $market > $FEES_STDOUT &
+
+		echo "Localnet running..."
+		echo "Ctl-c to exit."
+		wait
+}
+
+main

+ 1 - 0
examples/cfo/scripts/market-maker.json

@@ -0,0 +1 @@
+[13,174,53,150,78,228,12,98,170,254,212,211,125,193,2,241,97,137,49,209,189,199,27,215,220,65,57,203,215,93,105,203,217,32,5,194,157,118,162,47,102,126,235,65,99,80,56,231,217,114,25,225,239,140,169,92,150,146,211,218,183,139,9,104]

+ 16 - 0
examples/cfo/scripts/trade-bot.js

@@ -0,0 +1,16 @@
+#!/usr/bin/env node
+
+// Script to infinitely post orders that are immediately filled.
+
+const process = require("process");
+const anchor = require("@project-serum/anchor");
+const PublicKey = anchor.web3.PublicKey;
+const { runTradeBot } = require("../tests/utils");
+
+async function main() {
+  const market = new PublicKey(process.argv[2]);
+  const provider = anchor.Provider.local();
+  runTradeBot(market, provider);
+}
+
+main();

+ 202 - 0
examples/cfo/tests/cfo.js

@@ -0,0 +1,202 @@
+const assert = require("assert");
+const { Token } = require("@solana/spl-token");
+const anchor = require("@project-serum/anchor");
+const serumCmn = require("@project-serum/common");
+const { Market } = require("@project-serum/serum");
+const { PublicKey, SystemProgram, SYSVAR_RENT_PUBKEY } = anchor.web3;
+const utils = require("./utils");
+const { setupStakePool } = require("./utils/stake");
+
+const DEX_PID = new PublicKey("9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin");
+const SWAP_PID = new PublicKey("22Y43yTVxuUkoRKdm9thyRhQ3SdgQS7c7kB6UNCiaczD");
+const TOKEN_PID = new PublicKey("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA");
+const REGISTRY_PID = new PublicKey(
+  "GrAkKfEpTKQuVHG2Y97Y2FF4i7y7Q5AHLK94JBy7Y5yv"
+);
+const LOCKUP_PID = new PublicKey(
+  "6ebQNeTPZ1j7k3TtkCCtEPRvG7GQsucQrZ7sSEDQi9Ks"
+);
+const FEES = "6160355581";
+
+describe("cfo", () => {
+  anchor.setProvider(anchor.Provider.env());
+
+  const program = anchor.workspace.Cfo;
+  let officer;
+  let TOKEN_CLIENT;
+  let officerAccount;
+  const sweepAuthority = program.provider.wallet.publicKey;
+
+  // Accounts used to setup the orderbook.
+  let ORDERBOOK_ENV,
+    // Accounts used for A -> USDC swap transactions.
+    SWAP_A_USDC_ACCOUNTS,
+    // Accounts used for  USDC -> A swap transactions.
+    SWAP_USDC_A_ACCOUNTS,
+    // Serum DEX vault PDA for market A/USDC.
+    marketAVaultSigner,
+    // Serum DEX vault PDA for market B/USDC.
+    marketBVaultSigner;
+
+  let registrar, msrmRegistrar;
+
+  it("BOILERPLATE: Sets up a market with funded fees", async () => {
+    ORDERBOOK_ENV = await utils.initMarket({
+      provider: program.provider,
+    });
+    console.log("Token A: ", ORDERBOOK_ENV.marketA.baseMintAddress.toString());
+    console.log(
+      "Token USDC: ",
+      ORDERBOOK_ENV.marketA.quoteMintAddress.toString()
+    );
+    TOKEN_CLIENT = new Token(
+      program.provider.connection,
+      ORDERBOOK_ENV.usdc,
+      TOKEN_PID,
+      program.provider.wallet.payer
+    );
+
+    await TOKEN_CLIENT.transfer(
+      ORDERBOOK_ENV.godUsdc,
+      ORDERBOOK_ENV.marketA._decoded.quoteVault,
+      program.provider.wallet.payer,
+      [],
+      10000000000000
+    );
+
+    const tokenAccount = await TOKEN_CLIENT.getAccountInfo(
+      ORDERBOOK_ENV.marketA._decoded.quoteVault
+    );
+    assert.ok(tokenAccount.amount.toString() === "10000902263700");
+  });
+
+  it("BOILERPLATE: Executes trades to generate fees", async () => {
+    await utils.runTradeBot(
+      ORDERBOOK_ENV.marketA._decoded.ownAddress,
+      program.provider,
+      1
+    );
+    let marketClient = await Market.load(
+      program.provider.connection,
+      ORDERBOOK_ENV.marketA._decoded.ownAddress,
+      { commitment: "recent" },
+      DEX_PID
+    );
+    assert.ok(marketClient._decoded.quoteFeesAccrued.toString() === FEES);
+  });
+
+  it("BOILERPLATE: Sets up the staking pools", async () => {
+    await setupStakePool(ORDERBOOK_ENV.mintA, ORDERBOOK_ENV.godA);
+    registrar = ORDERBOOK_ENV.usdc;
+    msrmRegistrar = registrar;
+  });
+
+  it("Creates a CFO!", async () => {
+    let distribution = {
+      burn: 80,
+      stake: 20,
+      treasury: 0,
+    };
+    officer = await program.account.officer.associatedAddress(DEX_PID);
+    const srmVault = await anchor.utils.publicKey.associated(
+      program.programId,
+      officer,
+      anchor.utils.bytes.utf8.encode("vault"),
+    );
+    const stake = await anchor.utils.publicKey.associated(
+      program.programId,
+      officer,
+      anchor.utils.bytes.utf8.encode("stake"),
+    );
+    const treasury = await anchor.utils.publicKey.associated(
+      program.programId,
+      officer,
+      Buffer.from(anchor.utils.bytes.utf8.encode("treasury")),
+    );
+    await program.rpc.createOfficer(distribution, registrar, msrmRegistrar, {
+      accounts: {
+        officer,
+        srmVault,
+        stake,
+        treasury,
+        mint: ORDERBOOK_ENV.mintA,
+        authority: program.provider.wallet.publicKey,
+        dexProgram: DEX_PID,
+        swapProgram: SWAP_PID,
+        tokenProgram: TOKEN_PID,
+        systemProgram: SystemProgram.programId,
+        rent: SYSVAR_RENT_PUBKEY,
+      },
+    });
+
+    officerAccount = await program.account.officer.associated(DEX_PID);
+    assert.ok(
+      officerAccount.authority.equals(program.provider.wallet.publicKey)
+    );
+    assert.ok(
+      JSON.stringify(officerAccount.distribution) ===
+        JSON.stringify(distribution)
+    );
+  });
+
+  it("Creates a token account for the officer associated with the market", async () => {
+    const token = await anchor.utils.publicKey.associated(
+      program.programId,
+      officer,
+      ORDERBOOK_ENV.usdc
+    );
+    await program.rpc.createOfficerToken({
+      accounts: {
+        officer,
+        token,
+        mint: ORDERBOOK_ENV.usdc,
+        payer: program.provider.wallet.publicKey,
+        systemProgram: SystemProgram.programId,
+        tokenProgram: TOKEN_PID,
+        rent: SYSVAR_RENT_PUBKEY,
+      },
+    });
+    const tokenAccount = await TOKEN_CLIENT.getAccountInfo(token);
+    assert.ok(tokenAccount.state === 1);
+    assert.ok(tokenAccount.isInitialized);
+  });
+
+  it("Sweeps fees", async () => {
+    const sweepVault = await anchor.utils.publicKey.associated(
+      program.programId,
+      officer,
+      ORDERBOOK_ENV.usdc
+    );
+    const beforeTokenAccount = await serumCmn.getTokenAccount(
+      program.provider,
+      sweepVault
+    );
+    await program.rpc.sweepFees({
+      accounts: {
+        officer,
+        sweepVault,
+        mint: ORDERBOOK_ENV.usdc,
+        dex: {
+          market: ORDERBOOK_ENV.marketA._decoded.ownAddress,
+          pcVault: ORDERBOOK_ENV.marketA._decoded.quoteVault,
+          sweepAuthority,
+          vaultSigner: ORDERBOOK_ENV.vaultSigner,
+          dexProgram: DEX_PID,
+          tokenProgram: TOKEN_PID,
+        },
+      },
+    });
+    const afterTokenAccount = await serumCmn.getTokenAccount(
+      program.provider,
+      sweepVault
+    );
+    assert.ok(
+      afterTokenAccount.amount.sub(beforeTokenAccount.amount).toString() ===
+        FEES
+    );
+  });
+
+  it("TODO", async () => {
+    // todo
+  });
+});

+ 647 - 0
examples/cfo/tests/utils/index.js

@@ -0,0 +1,647 @@
+// 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 secret = JSON.parse(
+  require("fs").readFileSync("./scripts/market-maker.json")
+);
+const MARKET_MAKER = new Account(secret);
+
+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,
+  });
+
+  return {
+    marketA: MARKET_A_USDC,
+    vaultSigner,
+    marketMaker,
+    mintA: MINT_A,
+    usdc: USDC,
+    godA: GOD_A,
+    godUsdc: GOD_USDC,
+  };
+}
+
+// Creates everything needed for an orderbook to be running
+//
+// * Mints for both the base and quote currencies.
+// * Lists the market.
+// * Provides resting orders on the market.
+//
+// Returns a client that can be used to interact with the market
+// (and some other data, e.g., the mints and market maker account).
+async function initOrderbook({ provider, bids, asks }) {
+  if (!bids || !asks) {
+    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],
+    ];
+    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],
+    ];
+  }
+  // Create base and quote currency mints.
+  const decimals = 6;
+  const [MINT_A, GOD_A] = await serumCmn.createMintAndVault(
+    provider,
+    new BN(1000000000000000),
+    undefined,
+    decimals
+  );
+  const [USDC, GOD_USDC] = await serumCmn.createMintAndVault(
+    provider,
+    new BN(1000000000000000),
+    undefined,
+    decimals
+  );
+
+  // Create a funded account to act as market maker.
+  const amount = 100000 * 10 ** decimals;
+  const marketMaker = await fundAccount({
+    provider,
+    mints: [
+      { god: GOD_A, mint: MINT_A, amount, decimals },
+      { god: GOD_USDC, mint: USDC, amount, decimals },
+    ],
+  });
+
+  [marketClient, 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,
+  });
+
+  return {
+    marketClient,
+    baseMint: MINT_A,
+    quoteMint: USDC,
+    marketMaker,
+    vaultSigner,
+  };
+}
+
+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");
+}
+
+async function runTradeBot(market, provider, iterations = undefined) {
+  let marketClient = await Market.load(
+    provider.connection,
+    market,
+    { commitment: "recent" },
+    DEX_PID
+  );
+  const baseTokenUser1 = (
+    await marketClient.getTokenAccountsByOwnerForMint(
+      provider.connection,
+      MARKET_MAKER.publicKey,
+      marketClient.baseMintAddress
+    )
+  )[0].pubkey;
+  const quoteTokenUser1 = (
+    await marketClient.getTokenAccountsByOwnerForMint(
+      provider.connection,
+      MARKET_MAKER.publicKey,
+      marketClient.quoteMintAddress
+    )
+  )[0].pubkey;
+
+  const baseTokenUser2 = (
+    await marketClient.getTokenAccountsByOwnerForMint(
+      provider.connection,
+      provider.wallet.publicKey,
+      marketClient.baseMintAddress
+    )
+  )[0].pubkey;
+  const quoteTokenUser2 = (
+    await marketClient.getTokenAccountsByOwnerForMint(
+      provider.connection,
+      provider.wallet.publicKey,
+      marketClient.quoteMintAddress
+    )
+  )[0].pubkey;
+
+  const makerOpenOrdersUser1 = (
+    await OpenOrders.findForMarketAndOwner(
+      provider.connection,
+      market,
+      MARKET_MAKER.publicKey,
+      DEX_PID
+    )
+  )[0];
+  makerOpenOrdersUser2 = (
+    await OpenOrders.findForMarketAndOwner(
+      provider.connection,
+      market,
+      provider.wallet.publicKey,
+      DEX_PID
+    )
+  )[0];
+
+  const price = 6.041;
+  const size = 700000.8;
+
+  let maker = MARKET_MAKER;
+  let taker = provider.wallet.payer;
+  let baseToken = baseTokenUser1;
+  let quoteToken = quoteTokenUser2;
+  let makerOpenOrders = makerOpenOrdersUser1;
+
+  let k = 1;
+
+  while (true) {
+    if (iterations && k > iterations) {
+      break;
+    }
+    const clientId = new anchor.BN(k);
+    if (k % 5 === 0) {
+      if (maker.publicKey.equals(MARKET_MAKER.publicKey)) {
+        maker = provider.wallet.payer;
+        makerOpenOrders = makerOpenOrdersUser2;
+        taker = MARKET_MAKER;
+        baseToken = baseTokenUser2;
+        quoteToken = quoteTokenUser1;
+      } else {
+        maker = MARKET_MAKER;
+        makerOpenOrders = makerOpenOrdersUser1;
+        taker = provider.wallet.payer;
+        baseToken = baseTokenUser1;
+        quoteToken = quoteTokenUser2;
+      }
+    }
+
+    // Post ask.
+    const { transaction: tx_ask, signers: sigs_ask } =
+      await marketClient.makePlaceOrderTransaction(provider.connection, {
+        owner: maker,
+        payer: baseToken,
+        side: "sell",
+        price,
+        size,
+        orderType: "postOnly",
+        clientId,
+        openOrdersAddressKey: undefined,
+        openOrdersAccount: undefined,
+        feeDiscountPubkey: null,
+        selfTradeBehavior: "abortTransaction",
+      });
+    let txSig = await provider.send(tx_ask, sigs_ask.concat(maker));
+    console.log("Ask", txSig);
+
+    // Take.
+    const { transaction: tx_bid, signers: sigs_bid } =
+      await marketClient.makePlaceOrderTransaction(provider.connection, {
+        owner: taker,
+        payer: quoteToken,
+        side: "buy",
+        price,
+        size,
+        orderType: "ioc",
+        clientId: undefined,
+        openOrdersAddressKey: undefined,
+        openOrdersAccount: undefined,
+        feeDiscountPubkey: null,
+        selfTradeBehavior: "abortTransaction",
+      });
+    txSig = await provider.send(tx_bid, sigs_bid.concat(taker));
+    console.log("Bid", txSig);
+
+    await sleep(1000);
+
+    // Cancel anything remaining.
+    try {
+      txSig = await marketClient.cancelOrderByClientId(
+        provider.connection,
+        maker,
+        makerOpenOrders.address,
+        clientId
+      );
+      console.log("Cancelled the rest", txSig);
+      await sleep(1000);
+    } catch (e) {
+      console.log("Unable to cancel order", e);
+    }
+    k += 1;
+
+    // If the open orders account wasn't previously initialized, it is now.
+    if (makerOpenOrdersUser2 === undefined) {
+      makerOpenOrdersUser2 = (
+        await OpenOrders.findForMarketAndOwner(
+          provider.connection,
+          market,
+          provider.wallet.publicKey,
+          DEX_PID
+        )
+      )[0];
+    }
+  }
+}
+
+function sleep(ms) {
+  return new Promise((resolve) => setTimeout(resolve, ms));
+}
+
+module.exports = {
+  fundAccount,
+  initMarket,
+  initOrderbook,
+  setupMarket,
+  DEX_PID,
+  getVaultOwnerAndNonce,
+  runTradeBot,
+};

+ 184 - 0
examples/cfo/tests/utils/stake.js

@@ -0,0 +1,184 @@
+const anchor = require("@project-serum/anchor");
+const serumCmn = require("@project-serum/common");
+const TokenInstructions = require("@project-serum/serum").TokenInstructions;
+const utils = require("../../deps/stake/tests/utils");
+
+const lockup = anchor.workspace.Lockup;
+const registry = anchor.workspace.Registry;
+const provider = anchor.Provider.env();
+
+let lockupAddress = null;
+let mint = null;
+let god = null;
+
+let registrarAccount = null;
+let registrarSigner = null;
+let nonce = null;
+let poolMint = null;
+
+const registrar = new anchor.web3.Account();
+const rewardQ = new anchor.web3.Account();
+const withdrawalTimelock = new anchor.BN(4);
+const stakeRate = new anchor.BN(2);
+const rewardQLen = 170;
+let member = null;
+
+let memberAccount = null;
+let memberSigner = null;
+let balances = null;
+let balancesLocked = null;
+
+const WHITELIST_SIZE = 10;
+
+async function setupStakePool(mint, god) {
+  // Registry genesis.
+  const [_registrarSigner, _nonce] =
+    await anchor.web3.PublicKey.findProgramAddress(
+      [registrar.publicKey.toBuffer()],
+      registry.programId
+    );
+  registrarSigner = _registrarSigner;
+  nonce = _nonce;
+  poolMint = await serumCmn.createMint(provider, registrarSigner);
+
+  try {
+    // Init registry.
+    await registry.state.rpc.new({
+      accounts: { lockupProgram: lockup.programId },
+    });
+
+    // Init lockup.
+    await lockup.state.rpc.new({
+      accounts: {
+        authority: provider.wallet.publicKey,
+      },
+    });
+  } catch (err) {
+    // Skip errors for convenience when developing locally,
+    // since the state constructors can only be called once.
+  }
+
+  // Initialize stake pool.
+  await registry.rpc.initialize(
+    mint,
+    provider.wallet.publicKey,
+    nonce,
+    withdrawalTimelock,
+    stakeRate,
+    rewardQLen,
+    {
+      accounts: {
+        registrar: registrar.publicKey,
+        poolMint,
+        rewardEventQ: rewardQ.publicKey,
+        rent: anchor.web3.SYSVAR_RENT_PUBKEY,
+      },
+      signers: [registrar, rewardQ],
+      instructions: [
+        await registry.account.registrar.createInstruction(registrar),
+        await registry.account.rewardQueue.createInstruction(rewardQ, 8250),
+      ],
+    }
+  );
+  registrarAccount = await registry.account.registrar.fetch(
+    registrar.publicKey
+  );
+  console.log("Registrar", registrar.publicKey.toString());
+  console.log("Wallet", registry.provider.wallet.publicKey.toString());
+  // Create account for staker.
+  const seed = anchor.utils.sha256
+    .hash(`${registrar.publicKey.toString()}:Member`)
+    .slice(0, 32);
+  member = await anchor.web3.PublicKey.createWithSeed(
+    registry.provider.wallet.publicKey,
+    seed,
+    registry.programId
+  );
+  const [_memberSigner, nonce2] =
+    await anchor.web3.PublicKey.findProgramAddress(
+      [registrar.publicKey.toBuffer(), member.toBuffer()],
+      registry.programId
+    );
+  memberSigner = _memberSigner;
+  const [mainTx, _balances] = await utils.createBalanceSandbox(
+    provider,
+    registrarAccount,
+    memberSigner
+  );
+  const [lockedTx, _balancesLocked] = await utils.createBalanceSandbox(
+    provider,
+    registrarAccount,
+    memberSigner
+  );
+  balances = _balances;
+  balancesLocked = _balancesLocked;
+  const tx = registry.transaction.createMember(nonce2, {
+    accounts: {
+      registrar: registrar.publicKey,
+      member: member,
+      beneficiary: provider.wallet.publicKey,
+      memberSigner,
+      balances,
+      balancesLocked,
+      tokenProgram: TokenInstructions.TOKEN_PROGRAM_ID,
+      rent: anchor.web3.SYSVAR_RENT_PUBKEY,
+    },
+    instructions: [
+      anchor.web3.SystemProgram.createAccountWithSeed({
+        fromPubkey: registry.provider.wallet.publicKey,
+        newAccountPubkey: member,
+        basePubkey: registry.provider.wallet.publicKey,
+        seed,
+        lamports:
+          await registry.provider.connection.getMinimumBalanceForRentExemption(
+            registry.account.member.size
+          ),
+        space: registry.account.member.size,
+        programId: registry.programId,
+      }),
+    ],
+  });
+  const signers = [provider.wallet.payer];
+  const allTxs = [mainTx, lockedTx, { tx, signers }];
+  await provider.sendAll(allTxs);
+  memberAccount = await registry.account.member.fetch(member);
+
+  // Deposit into stake program.
+  const depositAmount = new anchor.BN(120);
+  await registry.rpc.deposit(depositAmount, {
+    accounts: {
+      depositor: god,
+      depositorAuthority: provider.wallet.publicKey,
+      tokenProgram: TokenInstructions.TOKEN_PROGRAM_ID,
+      vault: memberAccount.balances.vault,
+      beneficiary: provider.wallet.publicKey,
+      member: member,
+    },
+  });
+
+  // Stake.
+  const stakeAmount = new anchor.BN(10);
+  await registry.rpc.stake(stakeAmount, false, {
+    accounts: {
+      // Stake instance.
+      registrar: registrar.publicKey,
+      rewardEventQ: rewardQ.publicKey,
+      poolMint,
+      // Member.
+      member: member,
+      beneficiary: provider.wallet.publicKey,
+      balances,
+      balancesLocked,
+      // Program signers.
+      memberSigner,
+      registrarSigner,
+      // Misc.
+      clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
+      tokenProgram: TokenInstructions.TOKEN_PROGRAM_ID,
+    },
+  });
+}
+
+module.exports = {
+  setupStakePool,
+};

+ 1 - 0
examples/misc/programs/misc/Cargo.toml

@@ -16,4 +16,5 @@ default = []
 
 [dependencies]
 anchor-lang = { path = "../../../../lang" }
+anchor-spl = { path = "../../../../spl" }
 misc2 = { path = "../misc2", features = ["cpi"] }

+ 26 - 2
examples/misc/programs/misc/src/lib.rs

@@ -2,6 +2,7 @@
 //! It's not too instructive/coherent by itself, so please see other examples.
 
 use anchor_lang::prelude::*;
+use anchor_spl::token::{Mint, TokenAccount};
 use misc2::misc2::MyState as Misc2State;
 use misc2::Auth;
 
@@ -123,6 +124,29 @@ pub mod misc {
         acc.data = 1234;
         Ok(())
     }
+
+    pub fn test_token_seeds_init(_ctx: Context<TestTokenSeedsInit>, _nonce: u8) -> ProgramResult {
+        Ok(())
+    }
+}
+
+#[derive(Accounts)]
+#[instruction(nonce: u8)]
+pub struct TestTokenSeedsInit<'info> {
+    #[account(
+        init,
+        token = mint,
+        authority = authority,
+        seeds = [b"my-token-seed".as_ref(), &[nonce]],
+        payer = authority,
+        space = TokenAccount::LEN,
+    )]
+    pub my_pda: CpiAccount<'info, TokenAccount>,
+    pub mint: CpiAccount<'info, Mint>,
+    pub authority: AccountInfo<'info>,
+    pub system_program: AccountInfo<'info>,
+    pub rent: Sysvar<'info, Rent>,
+    pub token_program: AccountInfo<'info>,
 }
 
 #[derive(Accounts)]
@@ -219,7 +243,7 @@ pub struct TestClose<'info> {
 // the program.
 #[derive(Accounts)]
 pub struct TestInitAssociatedAccount<'info> {
-    #[account(init, associated = authority, with = state, with = data)]
+    #[account(init, associated = authority, with = state, with = data, with = b"my-seed")]
     my_account: ProgramAccount<'info, TestData>,
     #[account(mut, signer)]
     authority: AccountInfo<'info>,
@@ -231,7 +255,7 @@ pub struct TestInitAssociatedAccount<'info> {
 
 #[derive(Accounts)]
 pub struct TestAssociatedAccount<'info> {
-    #[account(mut, associated = authority, with = state, with = data)]
+    #[account(mut, associated = authority, with = state, with = data, with = b"my-seed")]
     my_account: ProgramAccount<'info, TestData>,
     #[account(mut, signer)]
     authority: AccountInfo<'info>,

+ 59 - 26
examples/misc/tests/misc.js

@@ -2,6 +2,7 @@ const anchor = require("@project-serum/anchor");
 const PublicKey = anchor.web3.PublicKey;
 const serumCmn = require("@project-serum/common");
 const assert = require("assert");
+const { TOKEN_PROGRAM_ID, Token } = require("@solana/spl-token");
 
 describe("misc", () => {
   // Configure the client to use the local cluster.
@@ -140,18 +141,17 @@ describe("misc", () => {
 
     // Manual associated address calculation for test only. Clients should use
     // the generated methods.
-    const [
-      associatedAccount,
-      nonce,
-    ] = await anchor.web3.PublicKey.findProgramAddress(
-      [
-        Buffer.from([97, 110, 99, 104, 111, 114]), // b"anchor".
-        program.provider.wallet.publicKey.toBuffer(),
-        state.toBuffer(),
-        data.publicKey.toBuffer(),
-      ],
-      program.programId
-    );
+    const [associatedAccount, nonce] =
+      await anchor.web3.PublicKey.findProgramAddress(
+        [
+          anchor.utils.bytes.utf8.encode("anchor"),
+          program.provider.wallet.publicKey.toBuffer(),
+          state.toBuffer(),
+          data.publicKey.toBuffer(),
+          anchor.utils.bytes.utf8.encode("my-seed"),
+        ],
+        program.programId
+      );
     await assert.rejects(
       async () => {
         await program.account.testData.fetch(associatedAccount);
@@ -178,25 +178,25 @@ describe("misc", () => {
     const account = await program.account.testData.associated(
       program.provider.wallet.publicKey,
       state,
-      data.publicKey
+      data.publicKey,
+      anchor.utils.bytes.utf8.encode("my-seed")
     );
     assert.ok(account.data.toNumber() === 1234);
   });
 
   it("Can use an associated program account", async () => {
     const state = await program.state.address();
-    const [
-      associatedAccount,
-      nonce,
-    ] = await anchor.web3.PublicKey.findProgramAddress(
-      [
-        Buffer.from([97, 110, 99, 104, 111, 114]), // b"anchor".
-        program.provider.wallet.publicKey.toBuffer(),
-        state.toBuffer(),
-        data.publicKey.toBuffer(),
-      ],
-      program.programId
-    );
+    const [associatedAccount, nonce] =
+      await anchor.web3.PublicKey.findProgramAddress(
+        [
+          anchor.utils.bytes.utf8.encode("anchor"),
+          program.provider.wallet.publicKey.toBuffer(),
+          state.toBuffer(),
+          data.publicKey.toBuffer(),
+          anchor.utils.bytes.utf8.encode("my-seed"),
+        ],
+        program.programId
+      );
     await program.rpc.testAssociatedAccount(new anchor.BN(5), {
       accounts: {
         myAccount: associatedAccount,
@@ -209,7 +209,8 @@ describe("misc", () => {
     const account = await program.account.testData.associated(
       program.provider.wallet.publicKey,
       state,
-      data.publicKey
+      data.publicKey,
+      anchor.utils.bytes.utf8.encode("my-seed")
     );
     assert.ok(account.data.toNumber() === 5);
   });
@@ -402,4 +403,36 @@ describe("misc", () => {
     assert.ok(myPdaAccount.data === 1234);
     assert.ok((myPdaAccount.bump = bump));
   });
+
+  it("Can create a token account from seeds pda", async () => {
+    const mint = await Token.createMint(
+      program.provider.connection,
+      program.provider.wallet.payer,
+      program.provider.wallet.publicKey,
+      null,
+      0,
+      TOKEN_PROGRAM_ID
+    );
+    const [myPda, bump] = await PublicKey.findProgramAddress(
+      [Buffer.from(anchor.utils.bytes.utf8.encode("my-token-seed"))],
+      program.programId
+    );
+    await program.rpc.testTokenSeedsInit(bump, {
+      accounts: {
+        myPda,
+        mint: mint.publicKey,
+        authority: program.provider.wallet.publicKey,
+        systemProgram: anchor.web3.SystemProgram.programId,
+        rent: anchor.web3.SYSVAR_RENT_PUBKEY,
+        tokenProgram: TOKEN_PROGRAM_ID,
+      },
+    });
+
+    const account = await mint.getAccountInfo(myPda);
+    assert.ok(account.state === 1);
+    assert.ok(account.amount.toNumber() === 0);
+    assert.ok(account.isInitialized);
+    assert.ok(account.owner.equals(program.provider.wallet.publicKey));
+    assert.ok(account.mint.equals(mint.publicKey));
+  });
 });

+ 4 - 0
lang/src/cpi_account.rs

@@ -30,6 +30,10 @@ impl<'a, T: AccountDeserialize + Clone> CpiAccount<'a, T> {
         ))
     }
 
+    pub fn try_from_init(info: &AccountInfo<'a>) -> Result<CpiAccount<'a, T>, ProgramError> {
+        Self::try_from(info)
+    }
+
     /// Reloads the account from storage. This is useful, for example, when
     /// observing side effects after CPI.
     pub fn reload(&self) -> Result<CpiAccount<'a, T>, ProgramError> {

+ 2 - 0
lang/src/error.rs

@@ -44,6 +44,8 @@ pub enum ErrorCode {
     ConstraintAssociatedInit,
     #[msg("A close constraint was violated")]
     ConstraintClose,
+    #[msg("An address constraint was violated")]
+    ConstraintAddress,
 
     // Accounts.
     #[msg("The account discriminator was already set on this account")]

+ 40 - 0
lang/src/lib.rs

@@ -210,6 +210,25 @@ pub trait Bump {
     fn seed(&self) -> u8;
 }
 
+pub trait Key {
+    fn key(&self) -> Pubkey;
+}
+
+impl<'info, T> Key for T
+where
+    T: ToAccountInfo<'info>,
+{
+    fn key(&self) -> Pubkey {
+        *self.to_account_info().key
+    }
+}
+
+impl Key for Pubkey {
+    fn key(&self) -> Pubkey {
+        *self
+    }
+}
+
 /// The prelude contains all commonly used components of the crate.
 /// All programs should include it via `anchor_lang::prelude::*;`.
 pub mod prelude {
@@ -287,3 +306,24 @@ pub mod __private {
     pub use crate::state::PROGRAM_STATE_SEED;
     pub const CLOSED_ACCOUNT_DISCRIMINATOR: [u8; 8] = [255, 255, 255, 255, 255, 255, 255, 255];
 }
+
+/// Returns the program-derived-address seeds used for creating the associated
+/// account.
+#[macro_export]
+macro_rules! associated_seeds {
+    (account = $pda:expr, associated = $associated:expr) => {
+        &[
+            b"anchor".as_ref(),
+            $associated.to_account_info().key.as_ref(),
+            &[anchor_lang::Bump::seed(&*$pda)],
+        ]
+    };
+    (account = $pda:expr, associated = $associated:expr, $(with = $with:expr),+) => {
+        &[
+            b"anchor".as_ref(),
+            $associated.to_account_info().key.as_ref(),
+            $($with.to_account_info().key.as_ref()),+,
+            &[anchor_lang::Bump::seed(&*$pda)][..],
+        ]
+    };
+}

+ 4 - 0
lang/src/program_account.rs

@@ -61,6 +61,10 @@ impl<'a, T: AccountSerialize + AccountDeserialize + Clone> ProgramAccount<'a, T>
             T::try_deserialize_unchecked(&mut data)?,
         ))
     }
+
+    pub fn into_inner(self) -> T {
+        self.inner.account
+    }
 }
 
 impl<'info, T> Accounts<'info> for ProgramAccount<'info, T>

+ 238 - 99
lang/syn/src/codegen/accounts/constraints.rs

@@ -1,12 +1,12 @@
 use crate::{
-    CompositeField, Constraint, ConstraintAssociatedGroup, ConstraintBelongsTo, ConstraintClose,
-    ConstraintExecutable, ConstraintGroup, ConstraintInit, ConstraintLiteral, ConstraintMut,
-    ConstraintOwner, ConstraintRaw, ConstraintRentExempt, ConstraintSeedsGroup, ConstraintSigner,
-    ConstraintState, Field, Ty,
+    CompositeField, Constraint, ConstraintAddress, ConstraintAssociatedGroup, ConstraintBelongsTo,
+    ConstraintClose, ConstraintExecutable, ConstraintGroup, ConstraintInit, ConstraintLiteral,
+    ConstraintMut, ConstraintOwner, ConstraintRaw, ConstraintRentExempt, ConstraintSeedsGroup,
+    ConstraintSigner, ConstraintState, Field, PdaKind, Ty,
 };
 use proc_macro2_diagnostics::SpanDiagnosticExt;
 use quote::quote;
-use syn::LitInt;
+use syn::Expr;
 
 pub fn generate(f: &Field) -> proc_macro2::TokenStream {
     let checks: Vec<proc_macro2::TokenStream> = linearize(&f.constraints)
@@ -53,6 +53,7 @@ pub fn linearize(c_group: &ConstraintGroup) -> Vec<Constraint> {
         state,
         associated,
         close,
+        address,
     } = c_group.clone();
 
     let mut constraints = Vec::new();
@@ -100,6 +101,9 @@ pub fn linearize(c_group: &ConstraintGroup) -> Vec<Constraint> {
     if let Some(c) = close {
         constraints.push(Constraint::Close(c));
     }
+    if let Some(c) = address {
+        constraints.push(Constraint::Address(c));
+    }
     constraints
 }
 
@@ -118,6 +122,7 @@ fn generate_constraint(f: &Field, c: &Constraint) -> proc_macro2::TokenStream {
         Constraint::State(c) => generate_constraint_state(f, c),
         Constraint::AssociatedGroup(c) => generate_constraint_associated(f, c),
         Constraint::Close(c) => generate_constraint_close(f, c),
+        Constraint::Address(c) => generate_constraint_address(f, c),
     }
 }
 
@@ -129,6 +134,16 @@ fn generate_constraint_composite(_f: &CompositeField, c: &Constraint) -> proc_ma
     }
 }
 
+fn generate_constraint_address(f: &Field, c: &ConstraintAddress) -> proc_macro2::TokenStream {
+    let field = &f.ident;
+    let addr = &c.address;
+    quote! {
+        if #field.to_account_info().key != &#addr {
+            return Err(anchor_lang::__private::ErrorCode::ConstraintAddress.into());
+        }
+    }
+}
+
 pub fn generate_constraint_init(_f: &Field, _c: &ConstraintInit) -> proc_macro2::TokenStream {
     quote! {}
 }
@@ -232,11 +247,8 @@ pub fn generate_constraint_rent_exempt(
     c: &ConstraintRentExempt,
 ) -> proc_macro2::TokenStream {
     let ident = &f.ident;
-    let info = match f.ty {
-        Ty::AccountInfo => quote! { #ident },
-        Ty::ProgramAccount(_) => quote! { #ident.to_account_info() },
-        Ty::Loader(_) => quote! { #ident.to_account_info() },
-        _ => panic!("Invalid syntax: rent exemption cannot be specified."),
+    let info = quote! {
+        #ident.to_account_info()
     };
     match c {
         ConstraintRentExempt::Skip => quote! {},
@@ -263,15 +275,22 @@ fn generate_constraint_seeds_init(f: &Field, c: &ConstraintSeedsGroup) -> proc_m
             let payer = #p.to_account_info();
         }
     };
+    let seeds_constraint = generate_constraint_seeds_address(f, c);
     let seeds_with_nonce = {
         let s = &c.seeds;
-        let seeds_constraint = generate_constraint_seeds_address(f, c);
         quote! {
-            #seeds_constraint
-            let seeds = [#s];
+            [#s]
         }
     };
-    generate_pda(f, seeds_with_nonce, payer, &c.space, false)
+    generate_pda(
+        f,
+        seeds_constraint,
+        seeds_with_nonce,
+        payer,
+        &c.space,
+        false,
+        &c.kind,
+    )
 }
 
 fn generate_constraint_seeds_address(
@@ -315,44 +334,78 @@ pub fn generate_constraint_associated_init(
             let payer = #p.to_account_info();
         },
     };
-    let associated_seeds_constraint = generate_constraint_associated_seeds(f, c);
-    let seeds_with_nonce = if c.associated_seeds.is_empty() {
-        quote! {
-            #associated_seeds_constraint
-            let seeds = [
-                &b"anchor"[..],
-                #associated_target.to_account_info().key.as_ref(),
-                &[nonce],
-            ];
-        }
-    } else {
-        let seeds = to_seeds_tts(&c.associated_seeds);
-        quote! {
-            #associated_seeds_constraint
-            let seeds = [
-                &b"anchor"[..],
-                #associated_target.to_account_info().key.as_ref(),
-                #seeds
-                &[nonce],
-            ];
+    let seeds_constraint = generate_constraint_associated_seeds(f, c);
+    let seeds_with_nonce = {
+        if c.associated_seeds.is_empty() {
+            quote! {
+                [
+                    &b"anchor"[..],
+                    #associated_target.to_account_info().key.as_ref(),
+                    &[nonce],
+                ]
+            }
+        } else {
+            let seeds = to_seeds_tts(&c.associated_seeds);
+            quote! {
+                [
+                    &b"anchor"[..],
+                    #associated_target.to_account_info().key.as_ref(),
+                    #seeds
+                    &[nonce],
+                ]
+            }
         }
     };
-    generate_pda(f, seeds_with_nonce, payer, &c.space, true)
+
+    generate_pda(
+        f,
+        seeds_constraint,
+        seeds_with_nonce,
+        payer,
+        &c.space,
+        true,
+        &c.kind,
+    )
+}
+
+fn parse_ty(f: &Field) -> (&syn::Ident, proc_macro2::TokenStream, bool) {
+    match &f.ty {
+        Ty::ProgramAccount(ty) => (
+            &ty.account_ident,
+            quote! {
+                anchor_lang::ProgramAccount
+            },
+            false,
+        ),
+        Ty::Loader(ty) => (
+            &ty.account_ident,
+            quote! {
+                anchor_lang::Loader
+            },
+            true,
+        ),
+        Ty::CpiAccount(ty) => (
+            &ty.account_ident,
+            quote! {
+                anchor_lang::CpiAccount
+            },
+            false,
+        ),
+        _ => panic!("Invalid type for initializing a program derived address"),
+    }
 }
 
 pub fn generate_pda(
     f: &Field,
+    seeds_constraint: proc_macro2::TokenStream,
     seeds_with_nonce: proc_macro2::TokenStream,
     payer: proc_macro2::TokenStream,
-    space: &Option<LitInt>,
+    space: &Option<Expr>,
     assign_nonce: bool,
+    kind: &PdaKind,
 ) -> proc_macro2::TokenStream {
     let field = &f.ident;
-    let (account_ty, is_zero_copy) = match &f.ty {
-        Ty::ProgramAccount(ty) => (&ty.account_ident, false),
-        Ty::Loader(ty) => (&ty.account_ident, true),
-        _ => panic!("Invalid type for initializing a program derived address"),
-    };
+    let (account_ty, account_wrapper_ty, is_zero_copy) = parse_ty(&f);
 
     let space = match space {
         // If no explicit space param was given, serialize the type to bytes
@@ -375,64 +428,133 @@ pub fn generate_pda(
         },
     };
 
-    let account_wrapper_ty = match is_zero_copy {
-        false => quote! {
-            anchor_lang::ProgramAccount
-        },
-        true => quote! {
-            anchor_lang::Loader
-        },
-    };
     let nonce_assignment = match assign_nonce {
         false => quote! {},
-        true => match is_zero_copy {
-            false => quote! {
-                pa.__nonce = nonce;
-            },
-            // Zero copy is not deserialized, so the data must be lazy loaded.
-            true => quote! {
-                pa.load_init()?.__nonce = nonce;
+        true => match &f.ty {
+            Ty::CpiAccount(_) => quote! {},
+            _ => match is_zero_copy {
+                false => quote! {
+                    pa.__nonce = nonce;
+                },
+                // Zero copy is not deserialized, so the data must be lazy loaded.
+                true => quote! {
+                    pa.load_init()?.__nonce = nonce;
+                },
             },
         },
     };
 
-    quote! {
-        let #field: #account_wrapper_ty<#account_ty> = {
-            #space
-            #payer
-
-            let lamports = rent.minimum_balance(space);
-            let ix = anchor_lang::solana_program::system_instruction::create_account(
-                payer.to_account_info().key,
-                #field.to_account_info().key,
-                lamports,
-                space as u64,
-                program_id,
-            );
+    match kind {
+        PdaKind::Token { owner, mint } => quote! {
+            let #field: #account_wrapper_ty<#account_ty> = {
+                #space
+                #payer
+                #seeds_constraint
+
+                // Fund the account for rent exemption.
+                let required_lamports = rent
+                    .minimum_balance(anchor_spl::token::TokenAccount::LEN)
+                    .max(1)
+                    .saturating_sub(#field.to_account_info().lamports());
+                if required_lamports > 0 {
+                    anchor_lang::solana_program::program::invoke(
+                        &anchor_lang::solana_program::system_instruction::transfer(
+                            payer.to_account_info().key,
+                            #field.to_account_info().key,
+                            required_lamports,
+                        ),
+                        &[
+                            payer.to_account_info(),
+                            #field.to_account_info(),
+                            system_program.to_account_info().clone(),
+                        ],
+                    )?;
+                }
 
-            #seeds_with_nonce
-            let signer = &[&seeds[..]];
-            anchor_lang::solana_program::program::invoke_signed(
-                &ix,
-                &[
-
-                    #field.to_account_info(),
-                    payer.to_account_info(),
-                    system_program.to_account_info(),
-                ],
-                signer,
-            ).map_err(|e| {
-                anchor_lang::solana_program::msg!("Unable to create associated account");
-                e
-            })?;
-            // 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(),
-            )?;
-            #nonce_assignment
-            pa
-        };
+                // Allocate space.
+                anchor_lang::solana_program::program::invoke_signed(
+                    &anchor_lang::solana_program::system_instruction::allocate(
+                        #field.to_account_info().key,
+                        anchor_spl::token::TokenAccount::LEN as u64,
+                    ),
+                    &[
+                        #field.to_account_info(),
+                        system_program.clone(),
+                    ],
+                    &[&#seeds_with_nonce[..]],
+                )?;
+
+                // Assign to the spl token program.
+                let __ix = anchor_lang::solana_program::system_instruction::assign(
+                    #field.to_account_info().key,
+                    token_program.to_account_info().key,
+                );
+                anchor_lang::solana_program::program::invoke_signed(
+                    &__ix,
+                    &[
+                        #field.to_account_info(),
+                        system_program.to_account_info(),
+                    ],
+                    &[&#seeds_with_nonce[..]],
+                )?;
+
+                // Initialize the token account.
+                let cpi_program = token_program.to_account_info();
+                let accounts = anchor_spl::token::InitializeAccount {
+                    account: #field.to_account_info(),
+                    mint: #mint.to_account_info(),
+                    authority: #owner.to_account_info(),
+                    rent: rent.to_account_info(),
+                };
+                let cpi_ctx = CpiContext::new(cpi_program, accounts);
+                anchor_spl::token::initialize_account(cpi_ctx)?;
+                anchor_lang::CpiAccount::try_from_init(
+                    &#field.to_account_info(),
+                )?
+            };
+        },
+        PdaKind::Program => {
+            quote! {
+                let #field: #account_wrapper_ty<#account_ty> = {
+                    #space
+                    #payer
+                    #seeds_constraint
+
+                    let lamports = rent.minimum_balance(space);
+                    let ix = anchor_lang::solana_program::system_instruction::create_account(
+                        payer.to_account_info().key,
+                        #field.to_account_info().key,
+                        lamports,
+                        space as u64,
+                        program_id,
+                    );
+
+
+                    anchor_lang::solana_program::program::invoke_signed(
+                        &ix,
+                        &[
+
+                            #field.to_account_info(),
+                            payer.to_account_info(),
+                            system_program.to_account_info(),
+                        ],
+                        &[&#seeds_with_nonce[..]]
+                    ).map_err(|e| {
+                        anchor_lang::solana_program::msg!("Unable to create associated account");
+                        e
+                    })?;
+
+                    // 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(),
+                    )?;
+
+                    #nonce_assignment
+                    pa
+                };
+            }
+        }
     }
 }
 
@@ -455,7 +577,13 @@ pub fn generate_constraint_associated_seeds(
             #seeds
         }
     };
-    let associated_field = if c.is_init {
+
+    let is_find_nonce = match &f.ty {
+        Ty::CpiAccount(_) => true,
+        Ty::AccountInfo => true,
+        _ => c.is_init,
+    };
+    let associated_field = if is_find_nonce {
         quote! {
             let (__associated_field, nonce) = Pubkey::find_program_address(
                 &[#seeds_no_nonce],
@@ -518,16 +646,27 @@ pub fn generate_constraint_state(f: &Field, c: &ConstraintState) -> proc_macro2:
 }
 
 // Returns the inner part of the seeds slice as a token stream.
-fn to_seeds_tts(seeds: &[syn::Ident]) -> proc_macro2::TokenStream {
+fn to_seeds_tts(seeds: &[syn::Expr]) -> proc_macro2::TokenStream {
     assert!(seeds.len() > 0);
     let seed_0 = &seeds[0];
-    let mut tts = quote! {
-        #seed_0.to_account_info().key.as_ref(),
+    let mut tts = match seed_0 {
+        syn::Expr::Path(_) => quote! {
+            anchor_lang::Key::key(&#seed_0).as_ref(),
+        },
+        _ => quote! {
+            #seed_0,
+        },
     };
     for seed in &seeds[1..] {
-        tts = quote! {
-            #tts
-            #seed.to_account_info().key.as_ref(),
+        tts = match seed {
+            syn::Expr::Path(_) => quote! {
+                #tts
+                anchor_lang::Key::key(&#seed).as_ref(),
+            },
+            _ => quote! {
+                #tts
+                #seed,
+            },
         };
     }
     tts

+ 37 - 9
lang/syn/src/lib.rs

@@ -268,6 +268,7 @@ pub struct ConstraintGroup {
     literal: Vec<ConstraintLiteral>,
     raw: Vec<ConstraintRaw>,
     close: Option<ConstraintClose>,
+    address: Option<ConstraintAddress>,
 }
 
 impl ConstraintGroup {
@@ -306,6 +307,7 @@ pub enum Constraint {
     State(ConstraintState),
     AssociatedGroup(ConstraintAssociatedGroup),
     Close(ConstraintClose),
+    Address(ConstraintAddress),
 }
 
 // Constraint token is a single keyword in a `#[account(<TOKEN>)]` attribute.
@@ -327,6 +329,9 @@ pub enum ConstraintToken {
     AssociatedPayer(Context<ConstraintAssociatedPayer>),
     AssociatedSpace(Context<ConstraintAssociatedSpace>),
     AssociatedWith(Context<ConstraintAssociatedWith>),
+    Address(Context<ConstraintAddress>),
+    TokenMint(Context<ConstraintTokenMint>),
+    TokenAuthority(Context<ConstraintTokenAuthority>),
 }
 
 impl Parse for ConstraintToken {
@@ -346,7 +351,7 @@ pub struct ConstraintSigner {}
 
 #[derive(Debug, Clone)]
 pub struct ConstraintBelongsTo {
-    pub join_target: Ident,
+    pub join_target: Expr,
 }
 
 #[derive(Debug, Clone)]
@@ -361,7 +366,12 @@ pub struct ConstraintRaw {
 
 #[derive(Debug, Clone)]
 pub struct ConstraintOwner {
-    pub owner_target: Ident,
+    pub owner_target: Expr,
+}
+
+#[derive(Debug, Clone)]
+pub struct ConstraintAddress {
+    pub address: Expr,
 }
 
 #[derive(Debug, Clone)]
@@ -375,7 +385,8 @@ pub struct ConstraintSeedsGroup {
     pub is_init: bool,
     pub seeds: Punctuated<Expr, Token![,]>,
     pub payer: Option<Ident>,
-    pub space: Option<LitInt>,
+    pub space: Option<Expr>,
+    pub kind: PdaKind,
 }
 
 #[derive(Debug, Clone)]
@@ -394,15 +405,16 @@ pub struct ConstraintState {
 #[derive(Debug, Clone)]
 pub struct ConstraintAssociatedGroup {
     pub is_init: bool,
-    pub associated_target: Ident,
-    pub associated_seeds: Vec<Ident>,
+    pub associated_target: Expr,
+    pub associated_seeds: Vec<Expr>,
     pub payer: Option<Ident>,
-    pub space: Option<LitInt>,
+    pub space: Option<Expr>,
+    pub kind: PdaKind,
 }
 
 #[derive(Debug, Clone)]
 pub struct ConstraintAssociated {
-    pub target: Ident,
+    pub target: Expr,
 }
 
 #[derive(Debug, Clone)]
@@ -412,12 +424,18 @@ pub struct ConstraintAssociatedPayer {
 
 #[derive(Debug, Clone)]
 pub struct ConstraintAssociatedWith {
-    pub target: Ident,
+    pub target: Expr,
 }
 
 #[derive(Debug, Clone)]
 pub struct ConstraintAssociatedSpace {
-    pub space: LitInt,
+    pub space: Expr,
+}
+
+#[derive(Debug, Clone)]
+pub enum PdaKind {
+    Program,
+    Token { owner: Expr, mint: Expr },
 }
 
 #[derive(Debug, Clone)]
@@ -425,6 +443,16 @@ pub struct ConstraintClose {
     pub sol_dest: Ident,
 }
 
+#[derive(Debug, Clone)]
+pub struct ConstraintTokenMint {
+    mint: Expr,
+}
+
+#[derive(Debug, Clone)]
+pub struct ConstraintTokenAuthority {
+    auth: Expr,
+}
+
 // Syntaxt context object for preserving metadata about the inner item.
 #[derive(Debug, Clone)]
 pub struct Context<T> {

+ 115 - 8
lang/syn/src/parser/accounts/constraints.rs

@@ -1,9 +1,10 @@
 use crate::{
-    ConstraintAssociated, ConstraintAssociatedGroup, ConstraintAssociatedPayer,
+    ConstraintAddress, ConstraintAssociated, ConstraintAssociatedGroup, ConstraintAssociatedPayer,
     ConstraintAssociatedSpace, ConstraintAssociatedWith, ConstraintBelongsTo, ConstraintClose,
     ConstraintExecutable, ConstraintGroup, ConstraintInit, ConstraintLiteral, ConstraintMut,
     ConstraintOwner, ConstraintRaw, ConstraintRentExempt, ConstraintSeeds, ConstraintSeedsGroup,
-    ConstraintSigner, ConstraintState, ConstraintToken, Context, Ty,
+    ConstraintSigner, ConstraintState, ConstraintToken, ConstraintTokenAuthority,
+    ConstraintTokenMint, Context, PdaKind, Ty,
 };
 use syn::ext::IdentExt;
 use syn::parse::{Error as ParseError, Parse, ParseStream, Result as ParseResult};
@@ -154,6 +155,24 @@ pub fn parse_token(stream: ParseStream) -> ParseResult<ConstraintToken> {
                         sol_dest: stream.parse()?,
                     },
                 )),
+                "address" => ConstraintToken::Address(Context::new(
+                    span,
+                    ConstraintAddress {
+                        address: stream.parse()?,
+                    },
+                )),
+                "token" => ConstraintToken::TokenMint(Context::new(
+                    ident.span(),
+                    ConstraintTokenMint {
+                        mint: stream.parse()?,
+                    },
+                )),
+                "authority" => ConstraintToken::TokenAuthority(Context::new(
+                    ident.span(),
+                    ConstraintTokenAuthority {
+                        auth: stream.parse()?,
+                    },
+                )),
                 _ => Err(ParseError::new(ident.span(), "Invalid attribute"))?,
             }
         }
@@ -181,6 +200,9 @@ pub struct ConstraintGroupBuilder<'ty> {
     pub associated_space: Option<Context<ConstraintAssociatedSpace>>,
     pub associated_with: Vec<Context<ConstraintAssociatedWith>>,
     pub close: Option<Context<ConstraintClose>>,
+    pub address: Option<Context<ConstraintAddress>>,
+    pub token_mint: Option<Context<ConstraintTokenMint>>,
+    pub token_authority: Option<Context<ConstraintTokenAuthority>>,
 }
 
 impl<'ty> ConstraintGroupBuilder<'ty> {
@@ -203,6 +225,9 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
             associated_space: None,
             associated_with: Vec::new(),
             close: None,
+            address: None,
+            token_mint: None,
+            token_authority: None,
         }
     }
     pub fn build(mut self) -> ParseResult<ConstraintGroup> {
@@ -233,6 +258,15 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
             }
         }
 
+        if let Some(token_mint) = &self.token_mint {
+            if self.init.is_none() || (self.associated.is_none() && self.seeds.is_none()) {
+                return Err(ParseError::new(
+                    token_mint.span(),
+                    "init is required for a pda token",
+                ));
+            }
+        }
+
         let ConstraintGroupBuilder {
             f_ty: _,
             init,
@@ -251,6 +285,9 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
             associated_space,
             associated_with,
             close,
+            address,
+            token_mint,
+            token_authority,
         } = self;
 
         // Converts Option<Context<T>> -> Option<T>.
@@ -279,12 +316,29 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
             raw: into_inner_vec!(raw),
             owner: into_inner!(owner),
             rent_exempt: into_inner!(rent_exempt),
-            seeds: seeds.map(|c| ConstraintSeedsGroup {
-                is_init,
-                seeds: c.into_inner().seeds,
-                payer: into_inner!(associated_payer.clone()).map(|a| a.target),
-                space: associated_space.clone().map(|s| s.space.clone()),
-            }),
+            seeds: seeds
+                .map(|c| {
+                    Ok(ConstraintSeedsGroup {
+                        is_init,
+                        seeds: c.into_inner().seeds,
+                        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,
+                            Some(tm) => PdaKind::Token {
+                                mint: tm.clone().into_inner().mint,
+                                owner: match &token_authority {
+                                    Some(a) => a.clone().into_inner().auth,
+                                    None => return Err(ParseError::new(
+                                        tm.span(),
+                                        "authority must be provided to initialize a token program derived address"
+                                    )),
+                                },
+                            },
+                        },
+                    })
+                })
+                .transpose()?,
             executable: into_inner!(executable),
             state: into_inner!(state),
             associated: associated.map(|associated| ConstraintAssociatedGroup {
@@ -293,8 +347,19 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
                 associated_seeds: associated_with.iter().map(|s| s.target.clone()).collect(),
                 payer: associated_payer.map(|p| p.target.clone()),
                 space: associated_space.map(|s| s.space.clone()),
+                kind: match token_mint {
+                    None => PdaKind::Program,
+                    Some(tm) => PdaKind::Token {
+                        mint: tm.into_inner().mint,
+                        owner: match token_authority {
+                            Some(a) => a.into_inner().auth,
+                            None => associated.target.clone(),
+                        },
+                    },
+                },
             }),
             close: into_inner!(close),
+            address: into_inner!(address),
         })
     }
 
@@ -316,6 +381,9 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
             ConstraintToken::AssociatedSpace(c) => self.add_associated_space(c),
             ConstraintToken::AssociatedWith(c) => self.add_associated_with(c),
             ConstraintToken::Close(c) => self.add_close(c),
+            ConstraintToken::Address(c) => self.add_address(c),
+            ConstraintToken::TokenAuthority(c) => self.add_token_authority(c),
+            ConstraintToken::TokenMint(c) => self.add_token_mint(c),
         }
     }
 
@@ -349,6 +417,45 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
         Ok(())
     }
 
+    fn add_address(&mut self, c: Context<ConstraintAddress>) -> ParseResult<()> {
+        if self.address.is_some() {
+            return Err(ParseError::new(c.span(), "address already provided"));
+        }
+        self.address.replace(c);
+        Ok(())
+    }
+
+    fn add_token_mint(&mut self, c: Context<ConstraintTokenMint>) -> ParseResult<()> {
+        if self.token_mint.is_some() {
+            return Err(ParseError::new(c.span(), "token mint already provided"));
+        }
+        if self.init.is_none() {
+            return Err(ParseError::new(
+                c.span(),
+                "init must be provided before token",
+            ));
+        }
+        self.token_mint.replace(c);
+        Ok(())
+    }
+
+    fn add_token_authority(&mut self, c: Context<ConstraintTokenAuthority>) -> ParseResult<()> {
+        if self.token_authority.is_some() {
+            return Err(ParseError::new(
+                c.span(),
+                "token authority already provided",
+            ));
+        }
+        if self.token_mint.is_none() {
+            return Err(ParseError::new(
+                c.span(),
+                "token must bne provided before authority",
+            ));
+        }
+        self.token_authority.replace(c);
+        Ok(())
+    }
+
     fn add_mut(&mut self, c: Context<ConstraintMut>) -> ParseResult<()> {
         if self.mutable.is_some() {
             return Err(ParseError::new(c.span(), "mut already provided"));

+ 28 - 0
spl/src/dex.rs

@@ -117,6 +117,24 @@ pub fn close_open_orders<'info>(
     Ok(())
 }
 
+pub fn sweep_fees<'info>(ctx: CpiContext<'_, '_, '_, 'info, SweepFees<'info>>) -> ProgramResult {
+    let ix = serum_dex::instruction::sweep_fees(
+        &ID,
+        ctx.accounts.market.key,
+        ctx.accounts.pc_vault.key,
+        ctx.accounts.sweep_authority.key,
+        ctx.accounts.sweep_receiver.key,
+        ctx.accounts.vault_signer.key,
+        ctx.accounts.token_program.key,
+    )?;
+    solana_program::program::invoke_signed(
+        &ix,
+        &ToAccountInfos::to_account_infos(&ctx),
+        ctx.signer_seeds,
+    )?;
+    Ok(())
+}
+
 #[derive(Accounts)]
 pub struct NewOrderV3<'info> {
     pub market: AccountInfo<'info>,
@@ -167,3 +185,13 @@ pub struct CloseOpenOrders<'info> {
     pub destination: AccountInfo<'info>,
     pub market: AccountInfo<'info>,
 }
+
+#[derive(Accounts)]
+pub struct SweepFees<'info> {
+    pub market: AccountInfo<'info>,
+    pub pc_vault: AccountInfo<'info>,
+    pub sweep_authority: AccountInfo<'info>,
+    pub sweep_receiver: AccountInfo<'info>,
+    pub vault_signer: AccountInfo<'info>,
+    pub token_program: AccountInfo<'info>,
+}

+ 1 - 0
spl/src/lib.rs

@@ -1,3 +1,4 @@
 pub mod dex;
+pub mod mint;
 pub mod shmem;
 pub mod token;

+ 13 - 0
spl/src/mint.rs

@@ -0,0 +1,13 @@
+use anchor_lang::solana_program::declare_id;
+
+pub use srm::ID as SRM;
+mod srm {
+    use super::*;
+    declare_id!("SRMuApVNdxXokk5GT7XD5cUUgXMBCoAz2LHeuAoKWRt");
+}
+
+pub use usdc::ID as USDC;
+mod usdc {
+    use super::*;
+    declare_id!("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v");
+}

+ 4 - 0
spl/src/token.rs

@@ -201,6 +201,10 @@ pub struct SetAuthority<'info> {
 #[derive(Clone)]
 pub struct TokenAccount(spl_token::state::Account);
 
+impl TokenAccount {
+    pub const LEN: usize = spl_token::state::Account::LEN;
+}
+
 impl anchor_lang::AccountDeserialize for TokenAccount {
     fn try_deserialize(buf: &mut &[u8]) -> Result<Self, ProgramError> {
         TokenAccount::try_deserialize_unchecked(buf)

+ 2 - 0
ts/src/error.ts

@@ -69,6 +69,7 @@ const LangErrorCode = {
   ConstraintAssociated: 149,
   ConstraintAssociatedInit: 150,
   ConstraintClose: 151,
+  ConstraintAddress: 152,
 
   // Accounts.
   AccountDiscriminatorAlreadySet: 160,
@@ -132,6 +133,7 @@ const LangErrorMessage = new Map([
     "An associated init constraint was violated",
   ],
   [LangErrorCode.ConstraintClose, "A close constraint was violated"],
+  [LangErrorCode.ConstraintAddress, "An address constraint was violated"],
 
   // Accounts.
   [

+ 2 - 8
ts/src/program/index.ts

@@ -250,14 +250,8 @@ export class Program {
     this._coder = new Coder(idl);
 
     // Dynamic namespaces.
-    const [
-      rpc,
-      instruction,
-      transaction,
-      account,
-      simulate,
-      state,
-    ] = NamespaceFactory.build(idl, this._coder, programId, this._provider);
+    const [rpc, instruction, transaction, account, simulate, state] =
+      NamespaceFactory.build(idl, this._coder, programId, this._provider);
     this.rpc = rpc;
     this.instruction = instruction;
     this.transaction = transaction;

+ 10 - 11
ts/src/program/namespace/account.ts

@@ -17,6 +17,7 @@ import Coder, {
 } from "../../coder";
 import { Subscription, Address, translateAddress } from "../common";
 import { getProvider } from "../../";
+import * as pubkeyUtil from "../../utils/pubkey";
 
 export default class AccountFactory {
   public static build(
@@ -234,9 +235,10 @@ export class AccountClient {
       fromPubkey: this._provider.wallet.publicKey,
       newAccountPubkey: signer.publicKey,
       space: sizeOverride ?? size,
-      lamports: await this._provider.connection.getMinimumBalanceForRentExemption(
-        sizeOverride ?? size
-      ),
+      lamports:
+        await this._provider.connection.getMinimumBalanceForRentExemption(
+          sizeOverride ?? size
+        ),
       programId: this._programId,
     });
   }
@@ -245,7 +247,7 @@ export class AccountClient {
    * Function returning the associated account. Args are keys to associate.
    * Order matters.
    */
-  async associated(...args: PublicKey[]): Promise<any> {
+  async associated(...args: Array<PublicKey | Buffer>): Promise<any> {
     const addr = await this.associatedAddress(...args);
     return await this.fetch(addr);
   }
@@ -254,13 +256,10 @@ export class AccountClient {
    * Function returning the associated address. Args are keys to associate.
    * Order matters.
    */
-  async associatedAddress(...args: PublicKey[]): Promise<PublicKey> {
-    let seeds = [Buffer.from([97, 110, 99, 104, 111, 114])]; // b"anchor".
-    args.forEach((arg) => {
-      seeds.push(translateAddress(arg).toBuffer());
-    });
-    const [assoc] = await PublicKey.findProgramAddress(seeds, this._programId);
-    return assoc;
+  async associatedAddress(
+    ...args: Array<PublicKey | Buffer>
+  ): Promise<PublicKey> {
+    return await pubkeyUtil.associated(this._programId, ...args);
   }
 }
 

+ 1 - 0
ts/src/utils/index.ts

@@ -2,3 +2,4 @@ export * as sha256 from "./sha256";
 export * as rpc from "./rpc";
 export * as publicKey from "./pubkey";
 export * as bytes from "./bytes";
+export * as token from "./token";

+ 19 - 0
ts/src/utils/pubkey.ts

@@ -1,6 +1,7 @@
 import BN from "bn.js";
 import { sha256 as sha256Sync } from "js-sha256";
 import { PublicKey } from "@solana/web3.js";
+import { Address, translateAddress } from "../program/common";
 
 // Sync version of web3.PublicKey.createWithSeed.
 export function createWithSeedSync(
@@ -76,3 +77,21 @@ const toBuffer = (arr: Buffer | Uint8Array | Array<number>): Buffer => {
     return Buffer.from(arr);
   }
 };
+
+export async function associated(
+  programId: Address,
+  ...args: Array<PublicKey | Buffer>
+): Promise<PublicKey> {
+  let seeds = [Buffer.from([97, 110, 99, 104, 111, 114])]; // b"anchor".
+  args.forEach((arg) => {
+    seeds.push(
+      // @ts-ignore
+      arg.buffer !== undefined ? arg : translateAddress(arg).toBuffer()
+    );
+  });
+  const [assoc] = await PublicKey.findProgramAddress(
+    seeds,
+    translateAddress(programId)
+  );
+  return assoc;
+}

+ 23 - 0
ts/src/utils/token.ts

@@ -0,0 +1,23 @@
+import { PublicKey } from "@solana/web3.js";
+
+const TOKEN_PROGRAM_ID = new PublicKey(
+  "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
+);
+const ASSOCIATED_PROGRAM_ID = new PublicKey(
+  "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
+);
+
+export async function associatedAddress({
+  mint,
+  owner,
+}: {
+  mint: PublicKey;
+  owner: PublicKey;
+}): Promise<PublicKey> {
+  return (
+    await PublicKey.findProgramAddress(
+      [owner.toBuffer(), TOKEN_PROGRAM_ID.toBuffer(), mint.toBuffer()],
+      ASSOCIATED_PROGRAM_ID
+    )
+  )[0];
+}

+ 9 - 5
ts/src/workspace.ts

@@ -84,16 +84,20 @@ const workspace = new Proxy({} as any, {
 
 function attachWorkspaceOverride(
   workspaceCache: { [key: string]: Program },
-  overrideConfig: { [key: string]: string },
+  overrideConfig: { [key: string]: string | { address: string; idl?: string } },
   idlMap: Map<string, Idl>
 ) {
   Object.keys(overrideConfig).forEach((programName) => {
     const wsProgramName = camelCase(programName, { pascalCase: true });
-    const overrideAddress = new PublicKey(overrideConfig[programName]);
-    workspaceCache[wsProgramName] = new Program(
-      idlMap.get(programName),
-      overrideAddress
+    const entry = overrideConfig[programName];
+    const overrideAddress = new PublicKey(
+      typeof entry === "string" ? entry : entry.address
     );
+    let idl = idlMap.get(programName);
+    if (typeof entry !== "string" && entry.idl) {
+      idl = JSON.parse(require("fs").readFileSync(entry.idl, "utf-8"));
+    }
+    workspaceCache[wsProgramName] = new Program(idl, overrideAddress);
   });
 }