Ver código fonte

lang, examples: UncheckedAccount and Cfo updates (#745)

Armani Ferrante 4 anos atrás
pai
commit
c004b0f99c

+ 3 - 6
README.md

@@ -65,18 +65,15 @@ mod counter {
 pub struct Initialize<'info> {
     #[account(init, payer = authority, space = 48)]
     pub counter: Account<'info, Counter>,
-    #[account(signer)]
-    pub authority: AccountInfo<'info>,
-    #[account(address = system_program::ID)]
-    pub system_program: AccountInfo<'info>,
+    pub authority: Signer<'info>,
+    pub system_program: Program<'info, System>,
 }
 
 #[derive(Accounts)]
 pub struct Increment<'info> {
     #[account(mut, has_one = authority)]
     pub counter: Account<'info, Counter>,
-    #[account(signer)]
-    pub authority: AccountInfo<'info>,
+    pub authority: Signer<'info>,
 }
 
 #[account]

+ 3 - 0
lang/src/lib.rs

@@ -50,6 +50,7 @@ mod signer;
 pub mod state;
 mod system_program;
 mod sysvar;
+mod unchecked_account;
 mod vec;
 
 pub use crate::account::Account;
@@ -74,6 +75,7 @@ pub use crate::signer::Signer;
 pub use crate::state::ProgramState;
 pub use crate::system_program::System;
 pub use crate::sysvar::Sysvar;
+pub use crate::unchecked_account::UncheckedAccount;
 pub use anchor_attribute_access_control::access_control;
 pub use anchor_attribute_account::{account, declare_id, zero_copy};
 pub use anchor_attribute_error::error;
@@ -247,6 +249,7 @@ pub mod prelude {
         state, zero_copy, Account, AccountDeserialize, AccountSerialize, Accounts, AccountsExit,
         AnchorDeserialize, AnchorSerialize, Context, CpiContext, Key, Loader, Owner, Program,
         ProgramAccount, Signer, System, Sysvar, ToAccountInfo, ToAccountInfos, ToAccountMetas,
+        UncheckedAccount,
     };
 
     #[allow(deprecated)]

+ 77 - 0
lang/src/unchecked_account.rs

@@ -0,0 +1,77 @@
+use crate::error::ErrorCode;
+use crate::{Accounts, AccountsExit, Key, ToAccountInfo, ToAccountInfos, ToAccountMetas};
+use solana_program::account_info::AccountInfo;
+use solana_program::entrypoint::ProgramResult;
+use solana_program::instruction::AccountMeta;
+use solana_program::program_error::ProgramError;
+use solana_program::pubkey::Pubkey;
+use std::ops::Deref;
+
+/// Explicit wrapper for AccountInfo types.
+#[derive(Clone)]
+pub struct UncheckedAccount<'info>(AccountInfo<'info>);
+
+impl<'info> UncheckedAccount<'info> {
+    pub fn try_from(acc_info: AccountInfo<'info>) -> Self {
+        Self(acc_info)
+    }
+}
+
+impl<'info> Accounts<'info> for UncheckedAccount<'info> {
+    fn try_accounts(
+        _program_id: &Pubkey,
+        accounts: &mut &[AccountInfo<'info>],
+        _ix_data: &[u8],
+    ) -> Result<Self, ProgramError> {
+        if accounts.is_empty() {
+            return Err(ErrorCode::AccountNotEnoughKeys.into());
+        }
+        let account = &accounts[0];
+        *accounts = &accounts[1..];
+        Ok(UncheckedAccount(account.clone()))
+    }
+}
+
+impl<'info> ToAccountMetas for UncheckedAccount<'info> {
+    fn to_account_metas(&self, is_signer: Option<bool>) -> Vec<AccountMeta> {
+        let is_signer = is_signer.unwrap_or(self.is_signer);
+        let meta = match self.is_writable {
+            false => AccountMeta::new_readonly(*self.key, is_signer),
+            true => AccountMeta::new(*self.key, is_signer),
+        };
+        vec![meta]
+    }
+}
+
+impl<'info> ToAccountInfos<'info> for UncheckedAccount<'info> {
+    fn to_account_infos(&self) -> Vec<AccountInfo<'info>> {
+        vec![self.0.clone()]
+    }
+}
+
+impl<'info> ToAccountInfo<'info> for UncheckedAccount<'info> {
+    fn to_account_info(&self) -> AccountInfo<'info> {
+        self.0.clone()
+    }
+}
+
+impl<'info> AccountsExit<'info> for UncheckedAccount<'info> {
+    fn exit(&self, _program_id: &Pubkey) -> ProgramResult {
+        // no-op
+        Ok(())
+    }
+}
+
+impl<'info> Key for UncheckedAccount<'info> {
+    fn key(&self) -> Pubkey {
+        *self.key
+    }
+}
+
+impl<'info> Deref for UncheckedAccount<'info> {
+    type Target = AccountInfo<'info>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}

+ 5 - 5
lang/syn/src/codegen/accounts/constraints.rs

@@ -143,7 +143,7 @@ pub fn generate_constraint_init(f: &Field, c: &ConstraintInitGroup) -> proc_macr
 pub fn generate_constraint_zeroed(f: &Field, _c: &ConstraintZeroed) -> proc_macro2::TokenStream {
     let field = &f.ident;
     let ty_decl = f.ty_decl();
-    let from_account_info = f.from_account_info(None);
+    let from_account_info = f.from_account_info_unchecked(None);
     quote! {
         let #field: #ty_decl = {
             let mut __data: &[u8] = &#field.try_borrow_data()?;
@@ -374,7 +374,7 @@ pub fn generate_init(
 ) -> proc_macro2::TokenStream {
     let field = &f.ident;
     let ty_decl = f.ty_decl();
-    let from_account_info = f.from_account_info(Some(kind));
+    let from_account_info = f.from_account_info_unchecked(Some(kind));
     match kind {
         InitKind::Token { owner, mint } => {
             let create_account = generate_create_account(
@@ -401,7 +401,7 @@ pub fn generate_init(
                     };
                     let cpi_ctx = CpiContext::new(cpi_program, accounts);
                     anchor_spl::token::initialize_account(cpi_ctx)?;
-                    let mut pa: #ty_decl = #from_account_info;
+                    let pa: #ty_decl = #from_account_info;
                     pa
                 };
             }
@@ -429,7 +429,7 @@ pub fn generate_init(
                     };
                     let cpi_ctx = CpiContext::new(cpi_program, accounts);
                     anchor_spl::token::initialize_mint(cpi_ctx, #decimals, &#owner.to_account_info().key, None)?;
-                    let mut pa: #ty_decl = #from_account_info;
+                    let pa: #ty_decl = #from_account_info;
                     pa
                 };
             }
@@ -476,7 +476,7 @@ pub fn generate_init(
                     #space
                     #payer
                     #create_account
-                    let mut pa: #ty_decl = #from_account_info;
+                    let pa: #ty_decl = #from_account_info;
                     pa
                 };
             }

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

@@ -178,6 +178,9 @@ impl Field {
             Ty::AccountInfo => quote! {
                 AccountInfo
             },
+            Ty::UncheckedAccount => quote! {
+                UncheckedAccount
+            },
             Ty::Signer => quote! {
                 Signer
             },
@@ -217,11 +220,14 @@ impl Field {
 
     // TODO: remove the option once `CpiAccount` is completely removed (not
     //       just deprecated).
-    pub fn from_account_info(&self, kind: Option<&InitKind>) -> proc_macro2::TokenStream {
+    pub fn from_account_info_unchecked(&self, kind: Option<&InitKind>) -> proc_macro2::TokenStream {
         let field = &self.ident;
         let container_ty = self.container_ty();
         match &self.ty {
             Ty::AccountInfo => quote! { #field.to_account_info() },
+            Ty::UncheckedAccount => {
+                quote! { UncheckedAccount::try_from(#field.to_account_info()) }
+            }
             Ty::Account(AccountTy { boxed, .. }) => {
                 if *boxed {
                     quote! {
@@ -276,6 +282,7 @@ impl Field {
             Ty::ProgramState(_) => quote! { anchor_lang::ProgramState },
             Ty::Program(_) => quote! { anchor_lang::Program },
             Ty::AccountInfo => quote! {},
+            Ty::UncheckedAccount => quote! {},
             Ty::Signer => quote! {},
         }
     }
@@ -286,6 +293,9 @@ impl Field {
             Ty::AccountInfo => quote! {
                 AccountInfo
             },
+            Ty::UncheckedAccount => quote! {
+                UncheckedAccount
+            },
             Ty::Signer => quote! {
                 Signer
             },
@@ -360,6 +370,7 @@ pub struct CompositeField {
 #[derive(Debug, PartialEq)]
 pub enum Ty {
     AccountInfo,
+    UncheckedAccount,
     ProgramState(ProgramStateTy),
     CpiState(CpiStateTy),
     ProgramAccount(ProgramAccountTy),

+ 0 - 1
lang/syn/src/parser/accounts/constraints.rs

@@ -18,7 +18,6 @@ pub fn parse(
         }
     }
     let account_constraints = constraints.build()?;
-
     let mut constraints = ConstraintGroupBuilder::new(f_ty);
     for attr in f.attrs.iter().filter(is_instruction) {
         if !has_instruction_api {

+ 2 - 0
lang/syn/src/parser/accounts/mod.rs

@@ -71,6 +71,7 @@ fn is_field_primitive(f: &syn::Field) -> ParseResult<bool> {
             | "CpiAccount"
             | "Sysvar"
             | "AccountInfo"
+            | "UncheckedAccount"
             | "CpiState"
             | "Loader"
             | "Account"
@@ -92,6 +93,7 @@ fn parse_ty(f: &syn::Field) -> ParseResult<Ty> {
         "CpiAccount" => Ty::CpiAccount(parse_cpi_account(&path)?),
         "Sysvar" => Ty::Sysvar(parse_sysvar(&path)?),
         "AccountInfo" => Ty::AccountInfo,
+        "UncheckedAccount" => Ty::UncheckedAccount,
         "Loader" => Ty::Loader(parse_program_account_zero_copy(&path)?),
         "Account" => Ty::Account(parse_account_ty(&path)?),
         "Program" => Ty::Program(parse_program_ty(&path)?),

+ 2 - 1
tests/cfo/programs/cfo/Cargo.toml

@@ -18,7 +18,8 @@ test = []
 [dependencies]
 anchor-lang = { path = "../../../../lang" }
 anchor-spl = { path = "../../../../spl" }
-spl-token = { version ="3.1.1", features = ["no-entrypoint"] }
+spl-token = { version = "3.1.1", features = ["no-entrypoint"] }
 swap = { path = "../../deps/swap/programs/swap", features = ["cpi"] }
+serum_dex = { path = "../../deps/serum-dex/dex", features = ["no-entrypoint"] }
 registry = { path = "../../deps/stake/programs/registry", features = ["cpi"] }
 lockup = { path = "../../deps/stake/programs/lockup", features = ["cpi"] }

+ 278 - 149
tests/cfo/programs/cfo/src/lib.rs

@@ -3,12 +3,13 @@
 use anchor_lang::prelude::*;
 use anchor_lang::solana_program::sysvar::instructions as tx_instructions;
 use anchor_spl::dex::{self, Dex};
-use anchor_spl::mint;
 use anchor_spl::token::{self, Mint, Token, TokenAccount};
 use lockup::program::Lockup;
 use registry::program::Registry;
 use registry::{Registrar, RewardVendorKind};
+use serum_dex::state::OpenOrders;
 use std::convert::TryInto;
+use std::mem::size_of;
 use swap::program::Swap;
 
 declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
@@ -46,6 +47,17 @@ pub mod cfo {
         Ok(())
     }
 
+    /// Creates a market authorization token.
+    pub fn authorize_market(ctx: Context<AuthorizeMarket>, bump: u8) -> Result<()> {
+        ctx.accounts.market_auth.bump = bump;
+        Ok(())
+    }
+
+    /// Revokes a market authorization token.
+    pub fn revoke_market(_ctx: Context<RevokeMarket>) -> Result<()> {
+        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.
@@ -53,6 +65,19 @@ pub mod cfo {
         Ok(())
     }
 
+    /// Creates an open orders account for the given market.
+    pub fn create_officer_open_orders(
+        ctx: Context<CreateOfficerOpenOrders>,
+        _bump: u8,
+    ) -> Result<()> {
+        let seeds = [
+            ctx.accounts.dex_program.key.as_ref(),
+            &[ctx.accounts.officer.bumps.bump],
+        ];
+        let cpi_ctx = CpiContext::from(&*ctx.accounts);
+        dex::init_open_orders(cpi_ctx.with_signer(&[&seeds])).map_err(Into::into)
+    }
+
     /// Updates the cfo's fee distribution.
     #[access_control(is_distribution_valid(&d))]
     pub fn set_distribution(ctx: Context<SetDistribution>, d: Distribution) -> Result<()> {
@@ -63,13 +88,12 @@ pub mod cfo {
 
     /// Transfers fees from the dex to the CFO.
     pub fn sweep_fees<'info>(ctx: Context<'_, '_, '_, 'info, SweepFees<'info>>) -> Result<()> {
+        let cpi_ctx = CpiContext::from(&*ctx.accounts);
         let seeds = [
             ctx.accounts.dex.dex_program.key.as_ref(),
             &[ctx.accounts.officer.bumps.bump],
         ];
-        let cpi_ctx = CpiContext::from(&*ctx.accounts);
-        dex::sweep_fees(cpi_ctx.with_signer(&[&seeds[..]]))?;
-        Ok(())
+        dex::sweep_fees(cpi_ctx.with_signer(&[&seeds])).map_err(Into::into)
     }
 
     /// Convert the CFO's entire non-SRM token balance into USDC.
@@ -85,12 +109,12 @@ pub mod cfo {
         ];
         let cpi_ctx = CpiContext::from(&*ctx.accounts);
         swap::cpi::swap(
-            cpi_ctx.with_signer(&[&seeds[..]]),
-            swap::Side::Bid,
-            token::accessor::amount(&ctx.accounts.from_vault)?,
+            cpi_ctx.with_signer(&[&seeds]),
+            swap::Side::Ask,
+            ctx.accounts.from_vault.amount,
             min_exchange_rate.into(),
-        )?;
-        Ok(())
+        )
+        .map_err(Into::into)
     }
 
     /// Convert the CFO's entire token balance into SRM.
@@ -104,14 +128,14 @@ pub mod cfo {
             ctx.accounts.dex_program.key.as_ref(),
             &[ctx.accounts.officer.bumps.bump],
         ];
-        let cpi_ctx: CpiContext<'_, '_, '_, 'info, swap::Swap<'info>> = (&*ctx.accounts).into();
+        let cpi_ctx = CpiContext::from(&*ctx.accounts);
         swap::cpi::swap(
-            cpi_ctx.with_signer(&[&seeds[..]]),
+            cpi_ctx.with_signer(&[&seeds]),
             swap::Side::Bid,
-            token::accessor::amount(&ctx.accounts.from_vault)?,
+            ctx.accounts.usdc_vault.amount,
             min_exchange_rate.into(),
-        )?;
-        Ok(())
+        )
+        .map_err(Into::into)
     }
 
     /// Distributes srm tokens to the various categories. Before calling this,
@@ -132,10 +156,7 @@ pub mod cfo {
             .unwrap()
             .try_into()
             .map_err(|_| ErrorCode::U128CannotConvert)?;
-        token::burn(
-            ctx.accounts.into_burn().with_signer(&[&seeds[..]]),
-            burn_amount,
-        )?;
+        token::burn(ctx.accounts.into_burn().with_signer(&[&seeds]), burn_amount)?;
 
         // Stake.
         let stake_amount: u64 = u128::from(total_fees)
@@ -146,9 +167,7 @@ pub mod cfo {
             .try_into()
             .map_err(|_| ErrorCode::U128CannotConvert)?;
         token::transfer(
-            ctx.accounts
-                .into_stake_transfer()
-                .with_signer(&[&seeds[..]]),
+            ctx.accounts.into_stake_transfer().with_signer(&[&seeds]),
             stake_amount,
         )?;
 
@@ -161,9 +180,7 @@ pub mod cfo {
             .try_into()
             .map_err(|_| ErrorCode::U128CannotConvert)?;
         token::transfer(
-            ctx.accounts
-                .into_treasury_transfer()
-                .with_signer(&[&seeds[..]]),
+            ctx.accounts.into_treasury_transfer().with_signer(&[&seeds]),
             treasury_amount,
         )?;
 
@@ -311,19 +328,28 @@ pub struct CreateOfficer<'info> {
     officer: Box<Account<'info, Officer>>,
     #[account(
         init,
-        seeds = [b"vault", officer.key().as_ref()],
+        seeds = [b"token", officer.key().as_ref(), srm_mint.key().as_ref()],
         bump = bumps.srm,
         payer = authority,
-        token::mint = mint,
+        token::mint = srm_mint,
         token::authority = officer,
     )]
     srm_vault: Box<Account<'info, TokenAccount>>,
+    #[account(
+        init,
+        seeds = [b"token", officer.key().as_ref(), usdc_mint.key().as_ref()],
+        bump = bumps.usdc,
+        payer = authority,
+        token::mint = usdc_mint,
+        token::authority = officer,
+    )]
+    usdc_vault: Box<Account<'info, TokenAccount>>,
     #[account(
         init,
         seeds = [b"stake", officer.key().as_ref()],
         bump = bumps.stake,
         payer = authority,
-        token::mint = mint,
+        token::mint = srm_mint,
         token::authority = officer,
     )]
     stake: Box<Account<'info, TokenAccount>>,
@@ -332,16 +358,21 @@ pub struct CreateOfficer<'info> {
         seeds = [b"treasury", officer.key().as_ref()],
         bump = bumps.treasury,
         payer = authority,
-        token::mint = mint,
+        token::mint = srm_mint,
         token::authority = officer,
     )]
     treasury: Box<Account<'info, TokenAccount>>,
-    authority: AccountInfo<'info>,
+    authority: Signer<'info>,
     #[cfg_attr(
         not(feature = "test"),
         account(address = mint::SRM),
     )]
-    mint: Box<Account<'info, Mint>>,
+    srm_mint: Box<Account<'info, Mint>>,
+    #[cfg_attr(
+        not(feature = "test"),
+        account(address = mint::USDC),
+    )]
+    usdc_mint: Box<Account<'info, Mint>>,
     dex_program: Program<'info, Dex>,
     swap_program: Program<'info, Swap>,
     system_program: Program<'info, System>,
@@ -349,21 +380,49 @@ pub struct CreateOfficer<'info> {
     rent: Sysvar<'info, Rent>,
 }
 
+#[derive(Accounts)]
+#[instruction(bump: u8)]
+pub struct AuthorizeMarket<'info> {
+    #[account(has_one = authority)]
+    officer: Account<'info, Officer>,
+    authority: Signer<'info>,
+    #[account(
+        init,
+        payer = payer,
+        seeds = [b"market-auth", officer.key().as_ref(), market.key.as_ref()],
+        bump = bump,
+    )]
+    market_auth: Account<'info, MarketAuth>,
+    payer: Signer<'info>,
+    // Not read or written to so not validated.
+    market: UncheckedAccount<'info>,
+    system_program: Program<'info, System>,
+}
+
+#[derive(Accounts)]
+pub struct RevokeMarket<'info> {
+    #[account(has_one = authority)]
+    pub officer: Account<'info, Officer>,
+    pub authority: Signer<'info>,
+    #[account(mut, close = payer)]
+    pub auth: Account<'info, MarketAuth>,
+    pub payer: Signer<'info>,
+}
+
 #[derive(Accounts)]
 #[instruction(bump: u8)]
 pub struct CreateOfficerToken<'info> {
     officer: Account<'info, Officer>,
     #[account(
         init,
-        seeds = [officer.key().as_ref(), mint.key().as_ref()],
+        seeds = [b"token", officer.key().as_ref(), mint.key().as_ref()],
         bump = bump,
         token::mint = mint,
         token::authority = officer,
         payer = payer,
     )]
     token: Account<'info, TokenAccount>,
-    #[account(owner = spl_token::ID)]
-    mint: AccountInfo<'info>,
+    mint: Account<'info, Mint>,
     #[account(mut)]
     payer: Signer<'info>,
     system_program: Program<'info, System>,
@@ -371,6 +430,28 @@ pub struct CreateOfficerToken<'info> {
     rent: Sysvar<'info, Rent>,
 }
 
+#[derive(Accounts)]
+#[instruction(bump: u8)]
+pub struct CreateOfficerOpenOrders<'info> {
+    officer: Account<'info, Officer>,
+    #[account(
+        init,
+        seeds = [b"open-orders", officer.key().as_ref(), market.key.as_ref()],
+        bump = bump,
+        space = 12 + size_of::<OpenOrders>(),
+        payer = payer,
+        owner = dex::ID,
+    )]
+    open_orders: UncheckedAccount<'info>,
+    #[account(mut)]
+    payer: Signer<'info>,
+    dex_program: Program<'info, Dex>,
+    system_program: Program<'info, System>,
+    rent: Sysvar<'info, Rent>,
+    // Used for CPI. Not read or written so not validated.
+    market: UncheckedAccount<'info>,
+}
+
 #[derive(Accounts)]
 pub struct SetDistribution<'info> {
     #[account(has_one = authority)]
@@ -387,23 +468,24 @@ pub struct SweepFees<'info> {
     officer: Account<'info, Officer>,
     #[account(
         mut,
-        owner = spl_token::ID,
-        seeds = [officer.key().as_ref(), mint.key().as_ref()],
+        seeds = [b"token", officer.key().as_ref(), mint.key().as_ref()],
         bump,
     )]
     sweep_vault: Account<'info, TokenAccount>,
-    mint: AccountInfo<'info>,
+    mint: Account<'info, Mint>,
     dex: DexAccounts<'info>,
 }
 
+// DexAccounts are safe because they are used for CPI only.
+// They are not read or written and so are not validated.
 #[derive(Accounts)]
 pub struct DexAccounts<'info> {
     #[account(mut)]
-    market: AccountInfo<'info>,
+    market: UncheckedAccount<'info>,
     #[account(mut)]
-    pc_vault: AccountInfo<'info>,
-    sweep_authority: AccountInfo<'info>,
-    vault_signer: AccountInfo<'info>,
+    pc_vault: UncheckedAccount<'info>,
+    sweep_authority: UncheckedAccount<'info>,
+    vault_signer: UncheckedAccount<'info>,
     dex_program: Program<'info, Dex>,
     token_program: Program<'info, Token>,
 }
@@ -414,102 +496,123 @@ pub struct SwapToUsdc<'info> {
         seeds = [dex_program.key.as_ref()],
         bump = officer.bumps.bump,
     )]
-    officer: Account<'info, Officer>,
+    officer: Box<Account<'info, Officer>>,
     market: DexMarketAccounts<'info>,
     #[account(
-        owner = spl_token::ID,
-        constraint = &officer.treasury != from_vault.key,
-        constraint = &officer.stake != from_vault.key,
+        seeds = [b"market-auth", officer.key().as_ref(), market.market.key.as_ref()],
+        bump = market_auth.bump,
     )]
-    from_vault: AccountInfo<'info>,
-    #[account(owner = spl_token::ID)]
-    quote_vault: AccountInfo<'info>,
-    #[account(seeds = [officer.key().as_ref(), mint::USDC.as_ref()], bump)]
-    usdc_vault: AccountInfo<'info>,
+    market_auth: Account<'info, MarketAuth>,
+    #[account(
+        mut,
+        constraint = &officer.treasury != &from_vault.key(),
+        constraint = &officer.stake != &from_vault.key(),
+    )]
+    from_vault: Box<Account<'info, TokenAccount>>,
+    #[account(
+        mut,
+        seeds = [b"token", officer.key().as_ref(), usdc_mint.key().as_ref()],
+        bump,
+    )]
+    usdc_vault: Box<Account<'info, TokenAccount>>,
+    #[cfg_attr(not(feature = "test"), account(address = mint::USDC))]
+    usdc_mint: Box<Account<'info, Mint>>,
     swap_program: Program<'info, Swap>,
     dex_program: Program<'info, Dex>,
     token_program: Program<'info, Token>,
     #[account(address = tx_instructions::ID)]
-    instructions: AccountInfo<'info>,
+    instructions: UncheckedAccount<'info>,
     rent: Sysvar<'info, Rent>,
 }
 
 #[derive(Accounts)]
+#[instruction(bump: u8)]
 pub struct SwapToSrm<'info> {
     #[account(
         seeds = [dex_program.key.as_ref()],
         bump = officer.bumps.bump,
     )]
-    officer: Account<'info, Officer>,
+    officer: Box<Account<'info, Officer>>,
     market: DexMarketAccounts<'info>,
     #[account(
-        owner = spl_token::ID,
-        constraint = &officer.treasury != from_vault.key,
-        constraint = &officer.stake != from_vault.key,
+        seeds = [b"market-auth", officer.key().as_ref(), market.market.key.as_ref()],
+        bump = market_auth.bump,
     )]
-    from_vault: AccountInfo<'info>,
-    #[account(owner = spl_token::ID)]
-    quote_vault: AccountInfo<'info>,
+    market_auth: Account<'info, MarketAuth>,
     #[account(
-        seeds = [officer.key().as_ref(), mint::SRM.as_ref()],
+        mut,
+        seeds = [b"token", officer.key().as_ref(), usdc_mint.key().as_ref()],
+        bump,
+    )]
+    usdc_vault: Box<Account<'info, TokenAccount>>,
+    #[account(
+        mut,
+        seeds = [b"token", officer.key().as_ref(), srm_mint.key().as_ref()],
         bump,
-        constraint = &officer.treasury != from_vault.key,
-        constraint = &officer.stake != from_vault.key,
     )]
-    srm_vault: AccountInfo<'info>,
+    srm_vault: Box<Account<'info, TokenAccount>>,
+    #[cfg_attr(not(feature = "test"), account(address = mint::SRM))]
+    srm_mint: Box<Account<'info, Mint>>,
+    #[cfg_attr(not(feature = "test"), account(address = mint::USDC))]
+    usdc_mint: Box<Account<'info, Mint>>,
     swap_program: Program<'info, Swap>,
     dex_program: Program<'info, Dex>,
     token_program: Program<'info, Token>,
     #[account(address = tx_instructions::ID)]
-    instructions: AccountInfo<'info>,
+    instructions: UncheckedAccount<'info>,
     rent: Sysvar<'info, Rent>,
 }
 
+// Dex accounts are used for CPI only.
+// They are not read or written and so are not validated.
 #[derive(Accounts)]
 pub struct DexMarketAccounts<'info> {
     #[account(mut)]
-    market: AccountInfo<'info>,
+    market: UncheckedAccount<'info>,
     #[account(mut)]
-    open_orders: AccountInfo<'info>,
+    open_orders: UncheckedAccount<'info>,
     #[account(mut)]
-    request_queue: AccountInfo<'info>,
+    request_queue: UncheckedAccount<'info>,
     #[account(mut)]
-    event_queue: AccountInfo<'info>,
+    event_queue: UncheckedAccount<'info>,
     #[account(mut)]
-    bids: AccountInfo<'info>,
+    bids: UncheckedAccount<'info>,
     #[account(mut)]
-    asks: AccountInfo<'info>,
+    asks: UncheckedAccount<'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>,
+    order_payer_token_account: UncheckedAccount<'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>,
+    coin_vault: UncheckedAccount<'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>,
+    pc_vault: UncheckedAccount<'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>,
+    vault_signer: UncheckedAccount<'info>,
 }
 
 #[derive(Accounts)]
 pub struct Distribute<'info> {
-    #[account(has_one = treasury, has_one = stake)]
+    #[account(
+        has_one = srm_vault,
+        has_one = treasury,
+        has_one = stake,
+    )]
     officer: Account<'info, Officer>,
-    treasury: AccountInfo<'info>,
-    stake: AccountInfo<'info>,
-    #[account(constraint = srm_vault.mint == mint::SRM)]
+    #[account(mut)]
+    treasury: Account<'info, TokenAccount>,
+    #[account(mut)]
+    stake: Account<'info, TokenAccount>,
+    #[account(mut)]
     srm_vault: Account<'info, TokenAccount>,
-    #[account(address = mint::SRM)]
-    mint: AccountInfo<'info>,
+    #[account(mut)]
+    srm_mint: Account<'info, Mint>,
     token_program: Program<'info, Token>,
     dex_program: Program<'info, Dex>,
 }
@@ -531,7 +634,7 @@ pub struct DropStakeReward<'info> {
         not(feature = "test"),
         account(address = mint::SRM),
     )]
-    mint: AccountInfo<'info>,
+    mint: UncheckedAccount<'info>,
     srm: DropStakeRewardPool<'info>,
     msrm: DropStakeRewardPool<'info>,
     msrm_registrar: Box<Account<'info, Registrar>>,
@@ -547,15 +650,19 @@ pub struct DropStakeReward<'info> {
 // program to handle it.
 #[derive(Accounts)]
 pub struct DropStakeRewardPool<'info> {
-    registrar: AccountInfo<'info>,
-    reward_event_q: AccountInfo<'info>,
+    registrar: UncheckedAccount<'info>,
+    reward_event_q: UncheckedAccount<'info>,
     pool_mint: Account<'info, Mint>,
-    vendor: AccountInfo<'info>,
-    vendor_vault: AccountInfo<'info>,
+    vendor: UncheckedAccount<'info>,
+    vendor_vault: UncheckedAccount<'info>,
 }
 
 // Accounts.
 
+/// Officer represents a deployed instance of the CFO mechanism. It is tied
+/// to a single deployment of the dex program.
+///
+/// PDA - [dex_program_id].
 #[account]
 #[derive(Default)]
 pub struct Officer {
@@ -581,10 +688,29 @@ pub struct Officer {
     pub bumps: OfficerBumps,
 }
 
+/// MarketAuth represents an authorization token created by the Officer
+/// authority. This is used as an authorization token which allows one to
+/// permissionlessly invoke the swap instructions on the market. Without this
+/// one would be able to create their own market with prices unfavorable
+/// to the smart contract (and subsequently swap on it).
+///
+/// Because a PDA is used here, the account existing (without a tombstone) is
+/// proof of the validity of a given market, which means that anyone can use
+/// the vault here to swap.
+///
+/// PDA - [b"market-auth", officer, market_address]
+#[account]
+#[derive(Default)]
+pub struct MarketAuth {
+    // Bump seed for this account's PDA.
+    pub bump: u8,
+}
+
 #[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)]
 pub struct OfficerBumps {
     pub bump: u8,
     pub srm: u8,
+    pub usdc: u8,
     pub stake: u8,
     pub treasury: u8,
 }
@@ -598,6 +724,21 @@ pub struct Distribution {
 
 // CpiContext transformations.
 
+impl<'info> From<&CreateOfficerOpenOrders<'info>>
+    for CpiContext<'_, '_, '_, 'info, dex::InitOpenOrders<'info>>
+{
+    fn from(accs: &CreateOfficerOpenOrders<'info>) -> Self {
+        let program = accs.dex_program.to_account_info();
+        let accounts = dex::InitOpenOrders {
+            open_orders: accs.open_orders.to_account_info(),
+            authority: accs.officer.to_account_info(),
+            market: accs.market.to_account_info(),
+            rent: accs.rent.to_account_info(),
+        };
+        CpiContext::new(program, accounts)
+    }
+}
+
 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();
@@ -618,20 +759,20 @@ impl<'info> From<&SwapToSrm<'info>> for CpiContext<'_, '_, '_, 'info, swap::Swap
         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(),
+                market: accs.market.market.to_account_info(),
+                open_orders: accs.market.open_orders.to_account_info(),
+                request_queue: accs.market.request_queue.to_account_info(),
+                event_queue: accs.market.event_queue.to_account_info(),
+                bids: accs.market.bids.to_account_info(),
+                asks: accs.market.asks.to_account_info(),
+                order_payer_token_account: accs.market.order_payer_token_account.to_account_info(),
+                coin_vault: accs.market.coin_vault.to_account_info(),
+                pc_vault: accs.market.pc_vault.to_account_info(),
+                vault_signer: accs.market.vault_signer.to_account_info(),
+                coin_wallet: accs.srm_vault.to_account_info(),
             },
             authority: accs.officer.to_account_info(),
-            pc_wallet: accs.from_vault.to_account_info(),
+            pc_wallet: accs.usdc_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(),
@@ -645,20 +786,20 @@ impl<'info> From<&SwapToUsdc<'info>> for CpiContext<'_, '_, '_, 'info, swap::Swa
         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(),
+                market: accs.market.market.to_account_info(),
+                open_orders: accs.market.open_orders.to_account_info(),
+                request_queue: accs.market.request_queue.to_account_info(),
+                event_queue: accs.market.event_queue.to_account_info(),
+                bids: accs.market.bids.to_account_info(),
+                asks: accs.market.asks.to_account_info(),
+                order_payer_token_account: accs.market.order_payer_token_account.to_account_info(),
+                coin_vault: accs.market.coin_vault.to_account_info(),
+                pc_vault: accs.market.pc_vault.to_account_info(),
+                vault_signer: accs.market.vault_signer.to_account_info(),
                 coin_wallet: accs.from_vault.to_account_info(),
             },
             authority: accs.officer.to_account_info(),
-            pc_wallet: accs.usdc_vault.clone(),
+            pc_wallet: accs.usdc_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(),
@@ -667,15 +808,35 @@ impl<'info> From<&SwapToUsdc<'info>> for CpiContext<'_, '_, '_, 'info, swap::Swa
     }
 }
 
-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();
+impl<'info> Distribute<'info> {
+    fn into_burn(&self) -> CpiContext<'_, '_, '_, 'info, token::Burn<'info>> {
+        let program = self.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(),
+            mint: self.srm_mint.to_account_info(),
+            to: self.srm_vault.to_account_info(),
+            authority: self.officer.to_account_info(),
         };
-        CpiContext::new(program.to_account_info(), accounts)
+        CpiContext::new(program, accounts)
+    }
+
+    fn into_stake_transfer(&self) -> CpiContext<'_, '_, '_, 'info, token::Transfer<'info>> {
+        let program = self.token_program.to_account_info();
+        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.to_account_info();
+        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)
     }
 }
 
@@ -717,38 +878,6 @@ impl<'info> DropStakeReward<'info> {
     }
 }
 
-impl<'info> Distribute<'info> {
-    fn into_burn(&self) -> CpiContext<'_, '_, '_, 'info, token::Burn<'info>> {
-        let program = self.token_program.to_account_info();
-        let accounts = token::Burn {
-            mint: self.mint.clone(),
-            to: self.srm_vault.to_account_info(),
-            authority: self.officer.to_account_info(),
-        };
-        CpiContext::new(program.to_account_info(), accounts)
-    }
-
-    fn into_stake_transfer(&self) -> CpiContext<'_, '_, '_, 'info, token::Transfer<'info>> {
-        let program = self.token_program.to_account_info();
-        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.to_account_info(), accounts)
-    }
-
-    fn into_treasury_transfer(&self) -> CpiContext<'_, '_, '_, 'info, token::Transfer<'info>> {
-        let program = self.token_program.to_account_info();
-        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.to_account_info(), accounts)
-    }
-}
-
 // Events.
 
 #[event]
@@ -794,7 +923,7 @@ fn is_distribution_ready(accounts: &Distribute) -> Result<()> {
 }
 
 // `ixs` must be the Instructions sysvar.
-fn is_not_trading(ixs: &AccountInfo) -> Result<()> {
+fn is_not_trading(ixs: &UncheckedAccount) -> Result<()> {
     let data = ixs.try_borrow_data()?;
     match tx_instructions::load_instruction_at(1, &data) {
         Ok(_) => Err(ErrorCode::TooManyInstructions.into()),

+ 327 - 38
tests/cfo/tests/cfo.js

@@ -3,7 +3,8 @@ 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 utf8 = anchor.utils.bytes.utf8;
+const { PublicKey, SystemProgram, Keypair, SYSVAR_RENT_PUBKEY } = anchor.web3;
 const utils = require("./utils");
 const { setupStakePool } = require("./utils/stake");
 
@@ -16,16 +17,26 @@ const REGISTRY_PID = new PublicKey(
 const LOCKUP_PID = new PublicKey(
   "6ebQNeTPZ1j7k3TtkCCtEPRvG7GQsucQrZ7sSEDQi9Ks"
 );
+const SYSVAR_INSTRUCTIONS_PUBKEY = new PublicKey(
+  "Sysvar1nstructions1111111111111111111111111"
+);
 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;
+  let officer, srmVault, usdcVault, bVault, stake, treasury;
+  let officerBump, srmBump, usdcBump, bBump, stakeBump, treasuryBump;
+  let openOrders, openOrdersBump;
+  let openOrdersB, openOrdersBumpB;
+  let USDC_TOKEN_CLIENT, A_TOKEN_CLIENT, B_TOKEN_CLIENT;
+  let officerAccount;
+  let marketAClient, marketBClient;
+  let marketAuth, marketAuthBump;
+  let marketAuthB, marketAuthBumpB;
+  let distribution;
 
   // Accounts used to setup the orderbook.
   let ORDERBOOK_ENV,
@@ -49,14 +60,26 @@ describe("cfo", () => {
       "Token USDC: ",
       ORDERBOOK_ENV.marketA.quoteMintAddress.toString()
     );
-    TOKEN_CLIENT = new Token(
+    USDC_TOKEN_CLIENT = new Token(
       program.provider.connection,
       ORDERBOOK_ENV.usdc,
       TOKEN_PID,
       program.provider.wallet.payer
     );
+    SRM_TOKEN_CLIENT = new Token(
+      program.provider.connection,
+      ORDERBOOK_ENV.mintA,
+      TOKEN_PID,
+      program.provider.wallet.payer
+    );
+    B_TOKEN_CLIENT = new Token(
+      program.provider.connection,
+      ORDERBOOK_ENV.mintB,
+      TOKEN_PID,
+      program.provider.wallet.payer
+    );
 
-    await TOKEN_CLIENT.transfer(
+    await USDC_TOKEN_CLIENT.transfer(
       ORDERBOOK_ENV.godUsdc,
       ORDERBOOK_ENV.marketA._decoded.quoteVault,
       program.provider.wallet.payer,
@@ -64,7 +87,7 @@ describe("cfo", () => {
       10000000000000
     );
 
-    const tokenAccount = await TOKEN_CLIENT.getAccountInfo(
+    const tokenAccount = await USDC_TOKEN_CLIENT.getAccountInfo(
       ORDERBOOK_ENV.marketA._decoded.quoteVault
     );
     assert.ok(tokenAccount.amount.toString() === "10000902263700");
@@ -76,13 +99,19 @@ describe("cfo", () => {
       program.provider,
       1
     );
-    let marketClient = await Market.load(
+    marketAClient = await Market.load(
       program.provider.connection,
-      ORDERBOOK_ENV.marketA._decoded.ownAddress,
+      ORDERBOOK_ENV.marketA.address,
+      { commitment: "recent" },
+      DEX_PID
+    );
+    marketBClient = await Market.load(
+      program.provider.connection,
+      ORDERBOOK_ENV.marketB.address,
       { commitment: "recent" },
       DEX_PID
     );
-    assert.ok(marketClient._decoded.quoteFeesAccrued.toString() === FEES);
+    assert.ok(marketAClient._decoded.quoteFeesAccrued.toString() === FEES);
   });
 
   it("BOILERPLATE: Sets up the staking pools", async () => {
@@ -91,35 +120,108 @@ describe("cfo", () => {
     msrmRegistrar = registrar;
   });
 
-  it("Creates a CFO!", async () => {
-    let distribution = {
-      burn: 80,
-      stake: 20,
-      treasury: 0,
-    };
-    const [_officer, officerBump] = await PublicKey.findProgramAddress(
+  it("BOILERPLATE: Finds PDA addresses", async () => {
+    const [_officer, _officerBump] = await PublicKey.findProgramAddress(
       [DEX_PID.toBuffer()],
       program.programId
     );
-    officer = _officer;
-    const [srmVault, srmBump] = await PublicKey.findProgramAddress(
-      [anchor.utils.bytes.utf8.encode("vault"), officer.toBuffer()],
+    const [_openOrders, _openOrdersBump] = await PublicKey.findProgramAddress(
+      [
+        utf8.encode("open-orders"),
+        _officer.toBuffer(),
+        ORDERBOOK_ENV.marketA.address.toBuffer(),
+      ],
       program.programId
     );
-    const [stake, stakeBump] = await PublicKey.findProgramAddress(
-      [anchor.utils.bytes.utf8.encode("stake"), officer.toBuffer()],
+    const [_openOrdersB, _openOrdersBumpB] = await PublicKey.findProgramAddress(
+      [
+        utf8.encode("open-orders"),
+        _officer.toBuffer(),
+        ORDERBOOK_ENV.marketB.address.toBuffer(),
+      ],
+      program.programId
+    );
+    const [_srmVault, _srmBump] = await PublicKey.findProgramAddress(
+      [
+        utf8.encode("token"),
+        _officer.toBuffer(),
+        ORDERBOOK_ENV.mintA.toBuffer(),
+      ],
+      program.programId
+    );
+    const [_bVault, _bBump] = await PublicKey.findProgramAddress(
+      [
+        utf8.encode("token"),
+        _officer.toBuffer(),
+        ORDERBOOK_ENV.mintB.toBuffer(),
+      ],
       program.programId
     );
-    const [treasury, treasuryBump] = await PublicKey.findProgramAddress(
+    const [_usdcVault, _usdcBump] = await PublicKey.findProgramAddress(
       [
-        Buffer.from(anchor.utils.bytes.utf8.encode("treasury")),
-        officer.toBuffer(),
+        utf8.encode("token"),
+        _officer.toBuffer(),
+        ORDERBOOK_ENV.usdc.toBuffer(),
       ],
       program.programId
     );
+    const [_stake, _stakeBump] = await PublicKey.findProgramAddress(
+      [utf8.encode("stake"), _officer.toBuffer()],
+      program.programId
+    );
+    const [_treasury, _treasuryBump] = await PublicKey.findProgramAddress(
+      [utf8.encode("treasury"), _officer.toBuffer()],
+      program.programId
+    );
+    const [_marketAuth, _marketAuthBump] = await PublicKey.findProgramAddress(
+      [
+        utf8.encode("market-auth"),
+        _officer.toBuffer(),
+        ORDERBOOK_ENV.marketA.address.toBuffer(),
+      ],
+      program.programId
+    );
+    const [_marketAuthB, _marketAuthBumpB] = await PublicKey.findProgramAddress(
+      [
+        utf8.encode("market-auth"),
+        _officer.toBuffer(),
+        ORDERBOOK_ENV.marketB.address.toBuffer(),
+      ],
+      program.programId
+    );
+
+    officer = _officer;
+    officerBump = _officerBump;
+    openOrders = _openOrders;
+    openOrdersBump = _openOrdersBump;
+    openOrdersB = _openOrdersB;
+    openOrdersBumpB = _openOrdersBumpB;
+    srmVault = _srmVault;
+    srmBump = _srmBump;
+    usdcVault = _usdcVault;
+    usdcBump = _usdcBump;
+    bVault = _bVault;
+    bBump = _bBump;
+    stake = _stake;
+    stakeBump = _stakeBump;
+    treasury = _treasury;
+    treasuryBump = _treasuryBump;
+    marketAuth = _marketAuth;
+    marketAuthBump = _marketAuthBump;
+    marketAuthB = _marketAuthB;
+    marketAuthBumpB = _marketAuthBumpB;
+  });
+
+  it("Creates a CFO!", async () => {
+    distribution = {
+      burn: 80,
+      stake: 20,
+      treasury: 0,
+    };
     const bumps = {
       bump: officerBump,
       srm: srmBump,
+      usdc: usdcBump,
       stake: stakeBump,
       treasury: treasuryBump,
     };
@@ -132,9 +234,11 @@ describe("cfo", () => {
         accounts: {
           officer,
           srmVault,
+          usdcVault,
           stake,
           treasury,
-          mint: ORDERBOOK_ENV.mintA,
+          srmMint: ORDERBOOK_ENV.mintA,
+          usdcMint: ORDERBOOK_ENV.usdc,
           authority: program.provider.wallet.publicKey,
           dexProgram: DEX_PID,
           swapProgram: SWAP_PID,
@@ -156,29 +260,50 @@ describe("cfo", () => {
   });
 
   it("Creates a token account for the officer associated with the market", async () => {
-    const [token, bump] = await PublicKey.findProgramAddress(
-      [officer.toBuffer(), ORDERBOOK_ENV.usdc.toBuffer()],
-      program.programId
-    );
-    await program.rpc.createOfficerToken(bump, {
+    await program.rpc.createOfficerToken(bBump, {
       accounts: {
         officer,
-        token,
-        mint: ORDERBOOK_ENV.usdc,
+        token: bVault,
+        mint: ORDERBOOK_ENV.mintB,
         payer: program.provider.wallet.publicKey,
         systemProgram: SystemProgram.programId,
         tokenProgram: TOKEN_PID,
         rent: SYSVAR_RENT_PUBKEY,
       },
     });
-    const tokenAccount = await TOKEN_CLIENT.getAccountInfo(token);
+    const tokenAccount = await B_TOKEN_CLIENT.getAccountInfo(bVault);
     assert.ok(tokenAccount.state === 1);
     assert.ok(tokenAccount.isInitialized);
   });
 
+  it("Creates an open orders account for the officer", async () => {
+    await program.rpc.createOfficerOpenOrders(openOrdersBump, {
+      accounts: {
+        officer,
+        openOrders,
+        payer: program.provider.wallet.publicKey,
+        dexProgram: DEX_PID,
+        systemProgram: SystemProgram.programId,
+        rent: SYSVAR_RENT_PUBKEY,
+        market: ORDERBOOK_ENV.marketA.address,
+      },
+    });
+    await program.rpc.createOfficerOpenOrders(openOrdersBumpB, {
+      accounts: {
+        officer,
+        openOrders: openOrdersB,
+        payer: program.provider.wallet.publicKey,
+        dexProgram: DEX_PID,
+        systemProgram: SystemProgram.programId,
+        rent: SYSVAR_RENT_PUBKEY,
+        market: ORDERBOOK_ENV.marketB.address,
+      },
+    });
+  });
+
   it("Sweeps fees", async () => {
     const [sweepVault, bump] = await PublicKey.findProgramAddress(
-      [officer.toBuffer(), ORDERBOOK_ENV.usdc.toBuffer()],
+      [utf8.encode("token"), officer.toBuffer(), ORDERBOOK_ENV.usdc.toBuffer()],
       program.programId
     );
     const beforeTokenAccount = await serumCmn.getTokenAccount(
@@ -194,7 +319,7 @@ describe("cfo", () => {
           market: ORDERBOOK_ENV.marketA._decoded.ownAddress,
           pcVault: ORDERBOOK_ENV.marketA._decoded.quoteVault,
           sweepAuthority,
-          vaultSigner: ORDERBOOK_ENV.vaultSigner,
+          vaultSigner: ORDERBOOK_ENV.marketAVaultSigner,
           dexProgram: DEX_PID,
           tokenProgram: TOKEN_PID,
         },
@@ -210,7 +335,171 @@ describe("cfo", () => {
     );
   });
 
-  it("TODO", async () => {
-    // todo
+  it("Creates a market auth token", async () => {
+    await program.rpc.authorizeMarket(marketAuthBump, {
+      accounts: {
+        officer,
+        authority: program.provider.wallet.publicKey,
+        marketAuth,
+        payer: program.provider.wallet.publicKey,
+        market: ORDERBOOK_ENV.marketA.address,
+        systemProgram: SystemProgram.programId,
+      },
+    });
+    await program.rpc.authorizeMarket(marketAuthBumpB, {
+      accounts: {
+        officer,
+        authority: program.provider.wallet.publicKey,
+        marketAuth: marketAuthB,
+        payer: program.provider.wallet.publicKey,
+        market: ORDERBOOK_ENV.marketB.address,
+        systemProgram: SystemProgram.programId,
+      },
+    });
+  });
+
+  it("Transfers into the mintB vault", async () => {
+    await B_TOKEN_CLIENT.transfer(
+      ORDERBOOK_ENV.godB,
+      bVault,
+      program.provider.wallet.payer,
+      [],
+      616035558100
+    );
+  });
+
+  it("Swaps from B token to USDC", async () => {
+    const bVaultBefore = await B_TOKEN_CLIENT.getAccountInfo(bVault);
+    const usdcVaultBefore = await USDC_TOKEN_CLIENT.getAccountInfo(usdcVault);
+
+    const minExchangeRate = {
+      rate: new anchor.BN(0),
+      fromDecimals: 6,
+      quoteDecimals: 6,
+      strict: false,
+    };
+    await program.rpc.swapToUsdc(minExchangeRate, {
+      accounts: {
+        officer,
+        market: {
+          market: marketBClient.address,
+          openOrders: openOrdersB,
+          requestQueue: marketBClient.decoded.requestQueue,
+          eventQueue: marketBClient.decoded.eventQueue,
+          bids: marketBClient.decoded.bids,
+          asks: marketBClient.decoded.asks,
+          orderPayerTokenAccount: bVault,
+          coinVault: marketBClient.decoded.baseVault,
+          pcVault: marketBClient.decoded.quoteVault,
+          vaultSigner: ORDERBOOK_ENV.marketBVaultSigner,
+        },
+        marketAuth: marketAuthB,
+        usdcVault,
+        fromVault: bVault,
+        usdcMint: ORDERBOOK_ENV.usdc,
+        swapProgram: SWAP_PID,
+        dexProgram: DEX_PID,
+        tokenProgram: TOKEN_PID,
+        instructions: SYSVAR_INSTRUCTIONS_PUBKEY,
+        rent: SYSVAR_RENT_PUBKEY,
+      },
+    });
+
+    const bVaultAfter = await B_TOKEN_CLIENT.getAccountInfo(bVault);
+    const usdcVaultAfter = await USDC_TOKEN_CLIENT.getAccountInfo(usdcVault);
+
+    assert.ok(bVaultBefore.amount.toNumber() === 616035558100);
+    assert.ok(usdcVaultBefore.amount.toNumber() === 6160355581);
+    assert.ok(bVaultAfter.amount.toNumber() === 615884458100);
+    assert.ok(usdcVaultAfter.amount.toNumber() === 7060634298);
+  });
+
+  it("Swaps to SRM", async () => {
+    const srmVaultBefore = await SRM_TOKEN_CLIENT.getAccountInfo(srmVault);
+    const usdcVaultBefore = await USDC_TOKEN_CLIENT.getAccountInfo(usdcVault);
+
+    const minExchangeRate = {
+      rate: new anchor.BN(0),
+      fromDecimals: 6,
+      quoteDecimals: 6,
+      strict: false,
+    };
+    await program.rpc.swapToSrm(minExchangeRate, {
+      accounts: {
+        officer,
+        market: {
+          market: marketAClient.address,
+          openOrders,
+          requestQueue: marketAClient.decoded.requestQueue,
+          eventQueue: marketAClient.decoded.eventQueue,
+          bids: marketAClient.decoded.bids,
+          asks: marketAClient.decoded.asks,
+          orderPayerTokenAccount: usdcVault,
+          coinVault: marketAClient.decoded.baseVault,
+          pcVault: marketAClient.decoded.quoteVault,
+          vaultSigner: ORDERBOOK_ENV.marketAVaultSigner,
+        },
+        marketAuth,
+        usdcVault,
+        srmVault,
+        usdcMint: ORDERBOOK_ENV.usdc,
+        srmMint: ORDERBOOK_ENV.mintA,
+        swapProgram: SWAP_PID,
+        dexProgram: DEX_PID,
+        tokenProgram: TOKEN_PID,
+        instructions: SYSVAR_INSTRUCTIONS_PUBKEY,
+        rent: SYSVAR_RENT_PUBKEY,
+      },
+    });
+
+    const srmVaultAfter = await SRM_TOKEN_CLIENT.getAccountInfo(srmVault);
+    const usdcVaultAfter = await USDC_TOKEN_CLIENT.getAccountInfo(usdcVault);
+
+    assert.ok(srmVaultBefore.amount.toNumber() === 0);
+    assert.ok(srmVaultAfter.amount.toNumber() === 1152000000);
+    assert.ok(usdcVaultBefore.amount.toNumber() === 7060634298);
+    assert.ok(usdcVaultAfter.amount.toNumber() === 530863);
+  });
+
+  it("Distributes the tokens to categories", async () => {
+    const srmVaultBefore = await SRM_TOKEN_CLIENT.getAccountInfo(srmVault);
+    const treasuryBefore = await SRM_TOKEN_CLIENT.getAccountInfo(treasury);
+    const stakeBefore = await SRM_TOKEN_CLIENT.getAccountInfo(stake);
+    const mintInfoBefore = await SRM_TOKEN_CLIENT.getMintInfo();
+
+    await program.rpc.distribute({
+      accounts: {
+        officer,
+        treasury,
+        stake,
+        srmVault,
+        srmMint: ORDERBOOK_ENV.mintA,
+        tokenProgram: TOKEN_PID,
+        dexProgram: DEX_PID,
+      },
+    });
+
+    const srmVaultAfter = await SRM_TOKEN_CLIENT.getAccountInfo(srmVault);
+    const treasuryAfter = await SRM_TOKEN_CLIENT.getAccountInfo(treasury);
+    const stakeAfter = await SRM_TOKEN_CLIENT.getAccountInfo(stake);
+    const mintInfoAfter = await SRM_TOKEN_CLIENT.getMintInfo();
+
+    const beforeAmount = 1152000000;
+    assert.ok(srmVaultBefore.amount.toNumber() === beforeAmount);
+    assert.ok(srmVaultAfter.amount.toNumber() === 0); // Fully distributed.
+    assert.ok(
+      stakeAfter.amount.toNumber() ===
+        beforeAmount * (distribution.stake / 100.0)
+    );
+    assert.ok(
+      treasuryAfter.amount.toNumber() ===
+        beforeAmount * (distribution.treasury / 100.0)
+    );
+    // Check burn amount.
+    assert.ok(mintInfoBefore.supply.toString() === "1000000000000000000");
+    assert.ok(
+      mintInfoBefore.supply.sub(mintInfoAfter.supply).toNumber() ===
+        beforeAmount * (distribution.burn / 100.0)
+    );
   });
 });

+ 21 - 76
tests/cfo/tests/utils/index.js

@@ -33,6 +33,12 @@ async function initMarket({ provider }) {
     undefined,
     decimals
   );
+  const [MINT_B, GOD_B] = await serumCmn.createMintAndVault(
+    provider,
+    new BN("1000000000000000000"),
+    undefined,
+    decimals
+  );
   const [USDC, GOD_USDC] = await serumCmn.createMintAndVault(
     provider,
     new BN("1000000000000000000"),
@@ -46,6 +52,7 @@ async function initMarket({ provider }) {
     provider,
     mints: [
       { god: GOD_A, mint: MINT_A, amount, decimals },
+      { god: GOD_B, mint: MINT_B, amount, decimals },
       { god: GOD_USDC, mint: USDC, amount, decimals },
     ],
   });
@@ -71,7 +78,7 @@ async function initMarket({ provider }) {
     [5.961, 25.4],
   ];
 
-  [MARKET_A_USDC, vaultSigner] = await setupMarket({
+  [MARKET_A_USDC, marketAVaultSigner] = await setupMarket({
     baseMint: MINT_A,
     quoteMint: USDC,
     marketMaker: {
@@ -83,79 +90,12 @@ async function initMarket({ provider }) {
     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,
+  [MARKET_B_USDC, marketBVaultSigner] = await setupMarket({
+    baseMint: MINT_B,
     quoteMint: USDC,
     marketMaker: {
       account: marketMaker.account,
-      baseToken: marketMaker.tokens[MINT_A.toString()],
+      baseToken: marketMaker.tokens[MINT_B.toString()],
       quoteToken: marketMaker.tokens[USDC.toString()],
     },
     bids,
@@ -164,11 +104,17 @@ async function initOrderbook({ provider, bids, asks }) {
   });
 
   return {
-    marketClient,
-    baseMint: MINT_A,
-    quoteMint: USDC,
+    marketA: MARKET_A_USDC,
+    marketAVaultSigner,
+    marketB: MARKET_B_USDC,
+    marketBVaultSigner,
     marketMaker,
-    vaultSigner,
+    mintA: MINT_A,
+    mintB: MINT_B,
+    usdc: USDC,
+    godA: GOD_A,
+    godB: GOD_B,
+    godUsdc: GOD_USDC,
   };
 }
 
@@ -647,7 +593,6 @@ function sleep(ms) {
 module.exports = {
   fundAccount,
   initMarket,
-  initOrderbook,
   setupMarket,
   DEX_PID,
   getVaultOwnerAndNonce,