Browse Source

Program ctor functions and state structs (#33)

Armani Ferrante 4 years ago
parent
commit
e8efd04412

+ 12 - 0
Cargo.lock

@@ -86,6 +86,17 @@ dependencies = [
  "syn 1.0.57",
 ]
 
+[[package]]
+name = "anchor-attribute-state"
+version = "0.0.0-alpha.0"
+dependencies = [
+ "anchor-syn",
+ "anyhow",
+ "proc-macro2 1.0.24",
+ "quote 1.0.8",
+ "syn 1.0.57",
+]
+
 [[package]]
 name = "anchor-cli"
 version = "0.1.0"
@@ -125,6 +136,7 @@ dependencies = [
  "anchor-attribute-account",
  "anchor-attribute-error",
  "anchor-attribute-program",
+ "anchor-attribute-state",
  "anchor-derive-accounts",
  "serum-borsh",
  "solana-program",

+ 1 - 0
Cargo.toml

@@ -16,6 +16,7 @@ anchor-attribute-access-control = { path = "./attribute/access-control", version
 anchor-attribute-account = { path = "./attribute/account", version = "0.0.0-alpha.0" }
 anchor-attribute-error = { path = "./attribute/error" }
 anchor-attribute-program = { path = "./attribute/program", version = "0.0.0-alpha.0" }
+anchor-attribute-state = { path = "./attribute/state", version = "0.0.0-alpha.0" }
 anchor-derive-accounts = { path = "./derive/accounts", version = "0.0.0-alpha.0" }
 serum-borsh = { version = "0.8.0-serum.1", features = ["serum-program"] }
 solana-program = "1.4.3"

+ 12 - 5
attribute/account/src/lib.rs

@@ -6,15 +6,22 @@ use syn::parse_macro_input;
 /// A data structure representing a Solana account.
 #[proc_macro_attribute]
 pub fn account(
-    _args: proc_macro::TokenStream,
+    args: proc_macro::TokenStream,
     input: proc_macro::TokenStream,
 ) -> proc_macro::TokenStream {
+    let namespace = args.to_string().replace("\"", "");
+
     let account_strct = parse_macro_input!(input as syn::ItemStruct);
     let account_name = &account_strct.ident;
-    // Namespace the discriminator to prevent future collisions, e.g.,
-    // if we (for some unforseen reason) wanted to hash other parts of the
-    // program.
-    let discriminator_preimage = format!("account:{}", account_name.to_string());
+
+    // Namespace the discriminator to prevent collisions.
+    let discriminator_preimage = {
+        if namespace == "" {
+            format!("account:{}", account_name.to_string())
+        } else {
+            format!("{}:{}", namespace, account_name.to_string())
+        }
+    };
 
     let coder = quote! {
         impl anchor_lang::AccountSerialize for #account_name {

+ 18 - 0
attribute/state/Cargo.toml

@@ -0,0 +1,18 @@
+[package]
+name = "anchor-attribute-state"
+version = "0.0.0-alpha.0"
+authors = ["Serum Foundation <foundation@projectserum.com>"]
+repository = "https://github.com/project-serum/anchor"
+license = "Apache-2.0"
+description = "Attribute for defining a program state struct"
+edition = "2018"
+
+[lib]
+proc-macro = true
+
+[dependencies]
+proc-macro2 = "1.0"
+quote = "1.0"
+syn = { version = "=1.0.57", features = ["full"] }
+anyhow = "1.0.32"
+anchor-syn = { path = "../../syn", version = "0.0.0-alpha.0" }

+ 17 - 0
attribute/state/src/lib.rs

@@ -0,0 +1,17 @@
+extern crate proc_macro;
+
+use quote::quote;
+use syn::parse_macro_input;
+
+#[proc_macro_attribute]
+pub fn state(
+    _args: proc_macro::TokenStream,
+    input: proc_macro::TokenStream,
+) -> proc_macro::TokenStream {
+    let item_struct = parse_macro_input!(input as syn::ItemStruct);
+
+    proc_macro::TokenStream::from(quote! {
+        #[account("state")]
+        #item_struct
+    })
+}

+ 189 - 284
examples/lockup/programs/lockup/src/lib.rs

@@ -13,23 +13,35 @@ mod calculator;
 #[program]
 mod lockup {
     use super::*;
-    pub fn initialize(ctx: Context<Initialize>, authority: Pubkey) -> Result<(), Error> {
-        let safe = &mut ctx.accounts.safe;
-        let whitelist = &mut ctx.accounts.whitelist;
 
-        safe.authority = authority;
-        safe.whitelist = *whitelist.to_account_info().key;
-        whitelist.safe = *safe.to_account_info().key;
+    #[state]
+    pub struct Lockup {
+        /// The key with the ability to change the whitelist.
+        pub authority: Pubkey,
+        /// Valid programs the program can relay transactions to.
+        pub whitelist: Vec<WhitelistEntry>,
+    }
 
-        Ok(())
+    impl Lockup {
+        pub const WHITELIST_SIZE: usize = 5;
+
+        pub fn new(authority: Pubkey) -> Result<Self, Error> {
+            let mut whitelist = vec![];
+            whitelist.resize(Self::WHITELIST_SIZE, Default::default());
+            Ok(Lockup {
+                authority,
+                whitelist,
+            })
+        }
     }
 
     pub fn set_authority(ctx: Context<SetAuthority>, new_authority: Pubkey) -> Result<(), Error> {
-        let safe = &mut ctx.accounts.safe;
-        safe.authority = new_authority;
+        let lockup = &mut ctx.accounts.lockup;
+        lockup.authority = new_authority;
         Ok(())
     }
 
+    #[access_control(create_vesting_accounts(&ctx, nonce))]
     pub fn create_vesting(
         ctx: Context<CreateVesting>,
         beneficiary: Pubkey,
@@ -38,7 +50,6 @@ mod lockup {
         deposit_amount: u64,
         nonce: u8,
     ) -> Result<(), Error> {
-        // Vesting scheudle.
         if end_ts <= ctx.accounts.clock.unix_timestamp {
             return Err(ErrorCode::InvalidTimestamp.into());
         }
@@ -48,26 +59,8 @@ mod lockup {
         if deposit_amount == 0 {
             return Err(ErrorCode::InvalidDepositAmount.into());
         }
-        // Vault.
-        let vault_authority = Pubkey::create_program_address(
-            &vault_signer_seeds(
-                ctx.accounts.safe.to_account_info().key,
-                &beneficiary,
-                &nonce,
-            ),
-            ctx.program_id,
-        )
-        .map_err(|_| ErrorCode::InvalidProgramAddress)?;
-        if ctx.accounts.vault.owner != vault_authority {
-            return Err(ErrorCode::InvalidVaultOwner)?;
-        }
-        if ctx.accounts.vault.amount != 0 {
-            return Err(ErrorCode::InvalidVaultAmount)?;
-        }
 
         let vesting = &mut ctx.accounts.vesting;
-
-        vesting.safe = *ctx.accounts.safe.to_account_info().key;
         vesting.beneficiary = beneficiary;
         vesting.mint = ctx.accounts.vault.mint;
         vesting.vault = *ctx.accounts.vault.to_account_info().key;
@@ -86,9 +79,7 @@ mod lockup {
     }
 
     pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<(), Error> {
-        if amount == 0 {
-            return Err(ErrorCode::InvalidVaultAmount.into());
-        }
+        // Has the given amount vested?
         if amount
             > calculator::available_for_withdrawal(
                 &ctx.accounts.vesting,
@@ -98,38 +89,28 @@ mod lockup {
             return Err(ErrorCode::InsufficienWithdrawalBalance.into());
         }
 
+        // Transfer funds out.
+        let seeds = &[
+            ctx.accounts.vesting.to_account_info().key.as_ref(),
+            &[ctx.accounts.vesting.nonce],
+        ];
+        let signer = &[&seeds[..]];
+        let cpi_ctx = CpiContext::from(&*ctx.accounts).with_signer(signer);
+        token::transfer(cpi_ctx, amount)?;
+
+        // Bookeeping.
         let vesting = &mut ctx.accounts.vesting;
         vesting.outstanding -= amount;
 
-        let nonce = ctx.accounts.vesting.nonce;
-        let signer = &[&vault_signer_seeds(
-            ctx.accounts.safe.to_account_info().key,
-            ctx.accounts.beneficiary.key,
-            &nonce,
-        )[..]];
-        let cpi_ctx = CpiContext::from(ctx.accounts).with_signer(signer);
-        token::transfer(cpi_ctx, amount)?;
-
         Ok(())
     }
 
+    #[access_control(whitelist_has_capacity(&ctx))]
     pub fn whitelist_add(ctx: Context<WhitelistAdd>, entry: WhitelistEntry) -> Result<(), Error> {
-        if ctx.accounts.whitelist.entries.len() == 5 {
-            return Err(ErrorCode::WhitelistFull.into());
-        }
-        let entry_derived_address = entry.derived_address()?;
-        let mut items = ctx.accounts.whitelist.entries.iter().filter_map(|entry| {
-            let da = entry.derived_address().expect("always valid");
-            match da == entry_derived_address {
-                false => None,
-                true => Some(entry),
-            }
-        });
-        if items.next().is_some() {
+        if ctx.accounts.lockup.whitelist.contains(&entry) {
             return Err(ErrorCode::WhitelistEntryAlreadyExists.into());
         }
-        ctx.accounts.whitelist.entries.push(entry);
-
+        ctx.accounts.lockup.whitelist.push(entry);
         Ok(())
     }
 
@@ -137,21 +118,35 @@ mod lockup {
         ctx: Context<WhitelistAdd>,
         entry: WhitelistEntry,
     ) -> Result<(), Error> {
-        let entry_derived_address = entry.derived_address()?;
-
-        let whitelist = &mut ctx.accounts.whitelist;
-        whitelist.entries = whitelist
-            .entries
-            .clone()
-            .into_iter()
-            .filter_map(|e: WhitelistEntry| {
-                if e.derived_address().expect("always valid") == entry_derived_address {
-                    None
-                } else {
-                    Some(e)
-                }
-            })
-            .collect::<Vec<WhitelistEntry>>();
+        if !ctx.accounts.lockup.whitelist.contains(&entry) {
+            return Err(ErrorCode::WhitelistEntryNotFound.into());
+        }
+        ctx.accounts.lockup.whitelist.retain(|e| e != &entry);
+        Ok(())
+    }
+
+    // Sends funds from the lockup program to a whitelisted program.
+    pub fn whitelist_withdraw(
+        ctx: Context<WhitelistWithdraw>,
+        instruction_data: Vec<u8>,
+        amount: u64,
+    ) -> Result<(), Error> {
+        let before_amount = ctx.accounts.transfer.vault.amount;
+        whitelist_relay_cpi(
+            &ctx.accounts.transfer,
+            ctx.remaining_accounts,
+            instruction_data,
+        )?;
+        let after_amount = ctx.accounts.transfer.vault.reload()?.amount;
+
+        // CPI safety checks.
+        let withdraw_amount = before_amount - after_amount;
+        if withdraw_amount > amount {
+            return Err(ErrorCode::WhitelistWithdrawLimit)?;
+        }
+
+        // Bookeeping.
+        ctx.accounts.transfer.vesting.whitelist_owned += withdraw_amount;
 
         Ok(())
     }
@@ -161,124 +156,25 @@ mod lockup {
         ctx: Context<WhitelistDeposit>,
         instruction_data: Vec<u8>,
     ) -> Result<(), Error> {
-        let accounts = ctx.accounts;
-
-        let before_amount = accounts.vault.amount;
-
-        // Invoke opaque relay.
-        {
-            let mut meta_accounts = vec![
-                AccountMeta::new_readonly(*accounts.vesting.to_account_info().key, false),
-                AccountMeta::new(*accounts.vault.to_account_info().key, false),
-                AccountMeta::new_readonly(*accounts.vault_authority.to_account_info().key, true),
-                AccountMeta::new_readonly(*accounts.token_program.to_account_info().key, false),
-                AccountMeta::new(*accounts.whitelisted_program.to_account_info().key, false),
-                AccountMeta::new_readonly(
-                    *accounts
-                        .whitelisted_program_vault_authority
-                        .to_account_info()
-                        .key,
-                    false,
-                ),
-            ];
-            meta_accounts.extend(ctx.remaining_accounts.iter().map(|a| {
-                if a.is_writable {
-                    AccountMeta::new(*a.key, a.is_signer)
-                } else {
-                    AccountMeta::new_readonly(*a.key, a.is_signer)
-                }
-            }));
-            let relay_instruction = Instruction {
-                program_id: *accounts.whitelisted_program.to_account_info().key,
-                accounts: meta_accounts,
-                data: instruction_data.to_vec(),
-            };
-
-            let signer_seeds = &[];
-            solana_program::program::invoke_signed(
-                &relay_instruction,
-                &accounts.to_account_infos(),
-                signer_seeds,
-            )?;
-        }
-
-        let after_amount = accounts.vault.reload()?.amount;
-
-        // Deposit safety checks.
+        let before_amount = ctx.accounts.transfer.vault.amount;
+        whitelist_relay_cpi(
+            &ctx.accounts.transfer,
+            ctx.remaining_accounts,
+            instruction_data,
+        )?;
+        let after_amount = ctx.accounts.transfer.vault.reload()?.amount;
+
+        // CPI safety checks.
         let deposit_amount = after_amount - before_amount;
         if deposit_amount <= 0 {
             return Err(ErrorCode::InsufficientWhitelistDepositAmount)?;
         }
-        if deposit_amount > accounts.vesting.whitelist_owned {
+        if deposit_amount > ctx.accounts.transfer.vesting.whitelist_owned {
             return Err(ErrorCode::WhitelistDepositOverflow)?;
         }
 
         // Bookkeeping.
-        accounts.vesting.whitelist_owned -= deposit_amount;
-
-        Ok(())
-    }
-
-    // Sends funds from the lockup program to a whitelisted program.
-    pub fn whitelist_withdraw(
-        ctx: Context<WhitelistWithdraw>,
-        instruction_data: Vec<u8>,
-        amount: u64,
-    ) -> Result<(), Error> {
-        let accounts = ctx.accounts;
-
-        let before_amount = accounts.vault.amount;
-
-        // Invoke opaque relay.
-        {
-            let mut meta_accounts = vec![
-                AccountMeta::new_readonly(*accounts.vesting.to_account_info().key, false),
-                AccountMeta::new(*accounts.vault.to_account_info().key, false),
-                AccountMeta::new_readonly(*accounts.vault_authority.to_account_info().key, true),
-                AccountMeta::new_readonly(*accounts.token_program.to_account_info().key, false),
-                AccountMeta::new(
-                    *accounts.whitelisted_program_vault.to_account_info().key,
-                    false,
-                ),
-                AccountMeta::new_readonly(
-                    *accounts
-                        .whitelisted_program_vault_authority
-                        .to_account_info()
-                        .key,
-                    false,
-                ),
-            ];
-            meta_accounts.extend(ctx.remaining_accounts.iter().map(|a| {
-                if a.is_writable {
-                    AccountMeta::new(*a.key, a.is_signer)
-                } else {
-                    AccountMeta::new_readonly(*a.key, a.is_signer)
-                }
-            }));
-            let relay_instruction = Instruction {
-                program_id: *accounts.whitelisted_program.to_account_info().key,
-                accounts: meta_accounts,
-                data: instruction_data.to_vec(),
-            };
-
-            let signer_seeds = &[];
-            solana_program::program::invoke_signed(
-                &relay_instruction,
-                &accounts.to_account_infos(),
-                signer_seeds,
-            )?;
-        }
-
-        let after_amount = accounts.vault.reload()?.amount;
-
-        // Withdrawal safety checks.
-        let amount_transferred = before_amount - after_amount;
-        if amount_transferred > amount {
-            return Err(ErrorCode::WhitelistWithdrawLimit)?;
-        }
-
-        // Bookeeping.
-        accounts.vesting.whitelist_owned += amount_transferred;
+        ctx.accounts.transfer.vesting.whitelist_owned -= deposit_amount;
 
         Ok(())
     }
@@ -295,169 +191,190 @@ mod lockup {
     }
 }
 
-#[derive(Accounts)]
-pub struct Initialize<'info> {
-    #[account(init)]
-    safe: ProgramAccount<'info, Safe>,
-    #[account(init)]
-    whitelist: ProgramAccount<'info, Whitelist>,
-    rent: Sysvar<'info, Rent>,
+#[access_control(is_whitelisted(transfer))]
+pub fn whitelist_relay_cpi<'info>(
+    transfer: &WhitelistTransfer,
+    remaining_accounts: &[AccountInfo<'info>],
+    instruction_data: Vec<u8>,
+) -> Result<(), Error> {
+    let mut meta_accounts = vec![
+        AccountMeta::new_readonly(*transfer.vesting.to_account_info().key, false),
+        AccountMeta::new(*transfer.vault.to_account_info().key, false),
+        AccountMeta::new_readonly(*transfer.vesting_signer.to_account_info().key, true),
+        AccountMeta::new_readonly(*transfer.token_program.to_account_info().key, false),
+        AccountMeta::new(
+            *transfer.whitelisted_program_vault.to_account_info().key,
+            false,
+        ),
+        AccountMeta::new_readonly(
+            *transfer
+                .whitelisted_program_vault_authority
+                .to_account_info()
+                .key,
+            false,
+        ),
+    ];
+    meta_accounts.extend(remaining_accounts.iter().map(|a| {
+        if a.is_writable {
+            AccountMeta::new(*a.key, a.is_signer)
+        } else {
+            AccountMeta::new_readonly(*a.key, a.is_signer)
+        }
+    }));
+    let relay_instruction = Instruction {
+        program_id: *transfer.whitelisted_program.to_account_info().key,
+        accounts: meta_accounts,
+        data: instruction_data.to_vec(),
+    };
+
+    let seeds = &[
+        transfer.vesting.to_account_info().key.as_ref(),
+        &[transfer.vesting.nonce],
+    ];
+    let signer = &[&seeds[..]];
+    solana_program::program::invoke_signed(&relay_instruction, &transfer.to_account_infos(), signer)
+        .map_err(Into::into)
 }
 
 #[derive(Accounts)]
 pub struct SetAuthority<'info> {
+    #[account(mut, has_one = authority)]
+    lockup: ProgramState<'info, Lockup>,
     #[account(signer)]
     authority: AccountInfo<'info>,
-    #[account(mut, "&safe.authority == authority.key")]
-    safe: ProgramAccount<'info, Safe>,
 }
 
 #[derive(Accounts)]
 pub struct CreateVesting<'info> {
+    // Vesting.
     #[account(init)]
     vesting: ProgramAccount<'info, Vesting>,
-    safe: ProgramAccount<'info, Safe>,
     #[account(mut)]
     vault: CpiAccount<'info, TokenAccount>,
+    // Depositor.
     #[account(mut)]
     depositor: AccountInfo<'info>,
     #[account(signer)]
     depositor_authority: AccountInfo<'info>,
+    // Misc.
+    #[account("token_program.key == &token::ID")]
     token_program: AccountInfo<'info>,
     rent: Sysvar<'info, Rent>,
     clock: Sysvar<'info, Clock>,
 }
 
+fn create_vesting_accounts(ctx: &Context<CreateVesting>, nonce: u8) -> Result<(), Error> {
+    let vault_authority = Pubkey::create_program_address(
+        &[
+            ctx.accounts.vesting.to_account_info().key.as_ref(),
+            &[nonce],
+        ],
+        ctx.program_id,
+    )
+    .map_err(|_| ErrorCode::InvalidProgramAddress)?;
+    if ctx.accounts.vault.owner != vault_authority {
+        return Err(ErrorCode::InvalidVaultOwner)?;
+    }
+
+    Ok(())
+}
+
 #[derive(Accounts)]
 pub struct Withdraw<'info> {
-    safe: ProgramAccount<'info, Safe>,
-    #[account(mut, belongs_to = safe)]
+    // Vesting.
+    #[account(mut, has_one = beneficiary, has_one = vault)]
     vesting: ProgramAccount<'info, Vesting>,
-    #[account(signer, "beneficiary.key == &vesting.beneficiary")]
+    #[account(signer)]
     beneficiary: AccountInfo<'info>,
     #[account(mut)]
-    token: CpiAccount<'info, TokenAccount>,
-    #[account(mut)]
     vault: CpiAccount<'info, TokenAccount>,
-    vault_authority: AccountInfo<'info>,
+    #[account(seeds = [vesting.to_account_info().key.as_ref(), &[vesting.nonce]])]
+    vesting_signer: AccountInfo<'info>,
+    // Withdraw receiving target..
+    #[account(mut)]
+    token: CpiAccount<'info, TokenAccount>,
+    // Misc.
+    #[account("token_program.key == &token::ID")]
     token_program: AccountInfo<'info>,
     clock: Sysvar<'info, Clock>,
 }
 
 #[derive(Accounts)]
 pub struct WhitelistAdd<'info> {
+    #[account(mut, has_one = authority)]
+    lockup: ProgramState<'info, Lockup>,
     #[account(signer)]
     authority: AccountInfo<'info>,
-    #[account("&safe.authority == authority.key")]
-    safe: ProgramAccount<'info, Safe>,
-    #[account(mut, belongs_to = safe)]
-    whitelist: ProgramAccount<'info, Whitelist>,
+}
+
+fn whitelist_has_capacity(ctx: &Context<WhitelistAdd>) -> Result<(), Error> {
+    if ctx.accounts.lockup.whitelist.len() == lockup::Lockup::WHITELIST_SIZE {
+        return Err(ErrorCode::WhitelistFull.into());
+    }
+    Ok(())
 }
 
 #[derive(Accounts)]
 pub struct WhitelistDelete<'info> {
+    #[account(mut, has_one = authority)]
+    lockup: ProgramState<'info, Lockup>,
     #[account(signer)]
     authority: AccountInfo<'info>,
-    #[account("&safe.authority == authority.key")]
-    safe: ProgramAccount<'info, Safe>,
-    #[account(mut, belongs_to = safe)]
-    whitelist: ProgramAccount<'info, Whitelist>,
 }
 
 #[derive(Accounts)]
-pub struct WhitelistDeposit<'info> {
-    #[account(signer)]
-    beneficiary: AccountInfo<'info>,
-    safe: ProgramAccount<'info, Safe>,
-    #[account(belongs_to = safe)]
-    whitelist: ProgramAccount<'info, Whitelist>,
-    whitelisted_program: AccountInfo<'info>,
+pub struct WhitelistWithdraw<'info> {
+    transfer: WhitelistTransfer<'info>,
+}
 
-    // Whitelist interface.
-    #[account(
-				mut,
-				belongs_to = safe,
-				"&vesting.beneficiary == beneficiary.key",
-		)]
-    vesting: ProgramAccount<'info, Vesting>,
-    #[account(mut, "&vesting.vault == vault.to_account_info().key")]
-    vault: CpiAccount<'info, TokenAccount>,
-    vault_authority: AccountInfo<'info>,
-    token_program: AccountInfo<'info>,
-    #[account(mut)]
-    whitelisted_program_vault: AccountInfo<'info>,
-    whitelisted_program_vault_authority: AccountInfo<'info>,
+#[derive(Accounts)]
+pub struct WhitelistDeposit<'info> {
+    transfer: WhitelistTransfer<'info>,
 }
 
 #[derive(Accounts)]
-pub struct WhitelistWithdraw<'info> {
+pub struct WhitelistTransfer<'info> {
+    lockup: ProgramState<'info, Lockup>,
     #[account(signer)]
     beneficiary: AccountInfo<'info>,
-    safe: ProgramAccount<'info, Safe>,
-    #[account(belongs_to = safe)]
-    whitelist: ProgramAccount<'info, Whitelist>,
     whitelisted_program: AccountInfo<'info>,
 
     // Whitelist interface.
-    #[account(
-				mut,
-				belongs_to = safe,
-				"&vesting.beneficiary == beneficiary.key",
-		)]
+    #[account(mut, has_one = beneficiary, has_one = vault)]
     vesting: ProgramAccount<'info, Vesting>,
-    #[account(mut, "&vesting.vault == vault.to_account_info().key")]
+    #[account(mut, "&vault.owner == vesting_signer.key")]
     vault: CpiAccount<'info, TokenAccount>,
-    vault_authority: AccountInfo<'info>,
+    #[account(seeds = [vesting.to_account_info().key.as_ref(), &[vesting.nonce]])]
+    vesting_signer: AccountInfo<'info>,
+    #[account("token_program.key == &token::ID")]
     token_program: AccountInfo<'info>,
     #[account(mut)]
     whitelisted_program_vault: AccountInfo<'info>,
     whitelisted_program_vault_authority: AccountInfo<'info>,
 }
 
+pub fn is_whitelisted<'info>(transfer: &WhitelistTransfer<'info>) -> Result<(), Error> {
+    if !transfer.lockup.whitelist.contains(&WhitelistEntry {
+        program_id: *transfer.whitelisted_program.key,
+    }) {
+        return Err(ErrorCode::WhitelistEntryNotFound.into());
+    }
+    Ok(())
+}
+
 #[derive(Accounts)]
 pub struct AvailableForWithdrawal<'info> {
     vesting: ProgramAccount<'info, Vesting>,
     clock: Sysvar<'info, Clock>,
 }
 
-#[account]
-pub struct Safe {
-    /// The key with the ability to change the whitelist.
-    pub authority: Pubkey,
-    /// The whitelist of valid programs the Safe can relay transactions to.
-    pub whitelist: Pubkey,
-}
-
-#[account]
-pub struct Whitelist {
-    pub safe: Pubkey,
-    pub entries: Vec<WhitelistEntry>,
-}
-
-#[derive(AnchorSerialize, AnchorDeserialize, Default, Copy, Clone)]
+#[derive(AnchorSerialize, AnchorDeserialize, PartialEq, Default, Copy, Clone)]
 pub struct WhitelistEntry {
     pub program_id: Pubkey,
-    pub instance: Option<Pubkey>,
-    pub nonce: u8,
-}
-
-impl WhitelistEntry {
-    pub fn derived_address(&self) -> Result<Pubkey, Error> {
-        let pk = {
-            if let Some(i) = self.instance {
-                Pubkey::create_program_address(&[i.as_ref(), &[self.nonce]], &self.program_id)
-            } else {
-                Pubkey::create_program_address(&[&[self.nonce]], &self.program_id)
-            }
-        };
-        pk.map_err(|_| ErrorCode::InvalidWhitelistEntry.into())
-    }
 }
 
 #[account]
 pub struct Vesting {
-    /// The Safe instance this account is associated with.
-    pub safe: Pubkey,
     /// The owner of this Vesting account.
     pub beneficiary: Pubkey,
     /// The mint of the SPL token locked up.
@@ -514,6 +431,8 @@ pub enum ErrorCode {
     WhitelistDepositOverflow,
     #[msg("Tried to withdraw over the specified limit")]
     WhitelistWithdrawLimit,
+    #[msg("Whitelist entry not found.")]
+    WhitelistEntryNotFound,
 }
 
 impl<'a, 'b, 'c, 'info> From<&mut CreateVesting<'info>>
@@ -530,28 +449,14 @@ impl<'a, 'b, 'c, 'info> From<&mut CreateVesting<'info>>
     }
 }
 
-impl<'a, 'b, 'c, 'info> From<&mut Withdraw<'info>>
-    for CpiContext<'a, 'b, 'c, 'info, Transfer<'info>>
-{
-    fn from(accounts: &mut Withdraw<'info>) -> CpiContext<'a, 'b, 'c, 'info, Transfer<'info>> {
+impl<'a, 'b, 'c, 'info> From<&Withdraw<'info>> for CpiContext<'a, 'b, 'c, 'info, Transfer<'info>> {
+    fn from(accounts: &Withdraw<'info>) -> CpiContext<'a, 'b, 'c, 'info, Transfer<'info>> {
         let cpi_accounts = Transfer {
             from: accounts.vault.to_account_info(),
             to: accounts.token.to_account_info(),
-            authority: accounts.vault_authority.to_account_info(),
+            authority: accounts.vesting_signer.to_account_info(),
         };
         let cpi_program = accounts.token_program.to_account_info();
         CpiContext::new(cpi_program, cpi_accounts)
     }
 }
-
-fn vault_signer_seeds<'a>(
-    safe: &'a Pubkey,
-    beneficiary: &'a Pubkey,
-    nonce: &'a u8,
-) -> [&'a [u8]; 3] {
-    [
-        safe.as_ref(),
-        beneficiary.as_ref(),
-        bytemuck::bytes_of(nonce),
-    ]
-}

+ 51 - 73
examples/lockup/programs/registry/src/lib.rs

@@ -13,28 +13,28 @@ use std::convert::Into;
 mod registry {
     use super::*;
 
+    #[state]
+    pub struct Registry {
+        /// Address of the lockup program.
+        lockup_program: Pubkey,
+    }
+
+    impl Registry {
+        pub fn new<'info>(lockup_program: Pubkey) -> Result<Self, Error> {
+            Ok(Registry { lockup_program })
+        }
+    }
+
+    #[access_control(initialize_accounts(&ctx, nonce))]
     pub fn initialize(
         ctx: Context<Initialize>,
         mint: Pubkey,
         authority: Pubkey,
         nonce: u8,
         withdrawal_timelock: i64,
-        max_stake: u64,
         stake_rate: u64,
         reward_q_len: u32,
     ) -> Result<(), Error> {
-        let vault_authority = Pubkey::create_program_address(
-            &[
-                ctx.accounts.registrar.to_account_info().key.as_ref(),
-                &[nonce],
-            ],
-            ctx.program_id,
-        )
-        .map_err(|_| ErrorCode::InvalidNonce)?;
-        if ctx.accounts.pool_mint.mint_authority != COption::Some(vault_authority) {
-            return Err(ErrorCode::InvalidPoolMintAuthority.into());
-        }
-
         let registrar = &mut ctx.accounts.registrar;
 
         registrar.authority = authority;
@@ -44,7 +44,6 @@ mod registry {
         registrar.stake_rate = stake_rate;
         registrar.reward_event_q = *ctx.accounts.reward_event_q.to_account_info().key;
         registrar.withdrawal_timelock = withdrawal_timelock;
-        registrar.max_stake = max_stake;
 
         let reward_q = &mut ctx.accounts.reward_event_q;
         reward_q
@@ -58,7 +57,6 @@ mod registry {
         ctx: Context<UpdateRegistrar>,
         new_authority: Option<Pubkey>,
         withdrawal_timelock: Option<i64>,
-        max_stake: Option<u64>,
     ) -> Result<(), Error> {
         let registrar = &mut ctx.accounts.registrar;
 
@@ -70,62 +68,17 @@ mod registry {
             registrar.withdrawal_timelock = withdrawal_timelock;
         }
 
-        if let Some(max_stake) = max_stake {
-            registrar.max_stake = max_stake;
-        }
-
         Ok(())
     }
 
+    #[access_control(create_member_accounts(&ctx, nonce))]
     pub fn create_member(ctx: Context<CreateMember>, nonce: u8) -> Result<(), Error> {
-        let seeds = &[
-            ctx.accounts.registrar.to_account_info().key.as_ref(),
-            ctx.accounts.member.to_account_info().key.as_ref(),
-            &[nonce],
-        ];
-        let signer = &[&seeds[..]];
-
-        // Check the nonce + signer is correct.
-        let member_signer = Pubkey::create_program_address(seeds, ctx.program_id)
-            .map_err(|_| ErrorCode::InvalidNonce)?;
-        if &member_signer != ctx.accounts.member_signer.to_account_info().key {
-            return Err(ErrorCode::InvalidMemberSigner.into());
-        }
-
-        // Initialize member.
         let member = &mut ctx.accounts.member;
         member.registrar = *ctx.accounts.registrar.to_account_info().key;
         member.beneficiary = *ctx.accounts.beneficiary.key;
         member.balances = (&ctx.accounts.balances).into();
         member.balances_locked = (&ctx.accounts.balances_locked).into();
         member.nonce = nonce;
-
-        // Set delegate on staking tokens.
-        let (spt_approve, locked_spt_approve) = {
-            (
-                CpiContext::new_with_signer(
-                    ctx.accounts.token_program.clone(),
-                    token::Approve {
-                        to: ctx.accounts.balances.spt.to_account_info(),
-                        delegate: ctx.accounts.beneficiary.to_account_info(),
-                        authority: ctx.accounts.member_signer.to_account_info(),
-                    },
-                    signer,
-                ),
-                CpiContext::new_with_signer(
-                    ctx.accounts.token_program.clone(),
-                    token::Approve {
-                        to: ctx.accounts.balances_locked.spt.to_account_info(),
-                        delegate: ctx.accounts.beneficiary.to_account_info(),
-                        authority: ctx.accounts.member_signer.to_account_info(),
-                    },
-                    signer,
-                ),
-            )
-        };
-        token::approve(spt_approve, 0)?;
-        token::approve(locked_spt_approve, 0)?;
-
         Ok(())
     }
 
@@ -634,12 +587,28 @@ pub fn no_available_rewards<'info>(
 pub struct Initialize<'info> {
     #[account(init)]
     registrar: ProgramAccount<'info, Registrar>,
-    pool_mint: CpiAccount<'info, Mint>,
     #[account(init)]
     reward_event_q: ProgramAccount<'info, RewardQueue>,
+    pool_mint: CpiAccount<'info, Mint>,
     rent: Sysvar<'info, Rent>,
 }
 
+fn initialize_accounts<'info>(ctx: &Context<Initialize<'info>>, nonce: u8) -> Result<(), Error> {
+    let registrar_signer = Pubkey::create_program_address(
+        &[
+            ctx.accounts.registrar.to_account_info().key.as_ref(),
+            &[nonce],
+        ],
+        ctx.program_id,
+    )
+    .map_err(|_| ErrorCode::InvalidNonce)?;
+    if ctx.accounts.pool_mint.mint_authority != COption::Some(registrar_signer) {
+        return Err(ErrorCode::InvalidPoolMintAuthority.into());
+    }
+    assert!(ctx.accounts.pool_mint.supply == 0);
+    Ok(())
+}
+
 #[derive(Accounts)]
 pub struct UpdateRegistrar<'info> {
     #[account(mut, has_one = authority)]
@@ -655,22 +624,17 @@ pub struct CreateMember<'info> {
     member: ProgramAccount<'info, Member>,
     #[account(signer)]
     beneficiary: AccountInfo<'info>,
-    // Must be verified against the user given nonce.
     member_signer: AccountInfo<'info>,
     #[account(
-        "balances.balance_id.key == beneficiary.key",
         "&balances.spt.owner == member_signer.key",
         "balances.spt.mint == registrar.pool_mint",
-        "balances.vault.mint == registrar.mint",
-        "balances.spt.delegate == COption::None"
+        "balances.vault.mint == registrar.mint"
     )]
     balances: BalanceSandboxAccounts<'info>,
     #[account(
-        // Locked balance_id is unchecked; it's determined by the lockup program.
         "&balances_locked.spt.owner == member_signer.key",
         "balances_locked.spt.mint == registrar.pool_mint",
-        "balances_locked.vault.mint == registrar.mint",
-        "balances_locked.spt.delegate == COption::None"
+        "balances_locked.vault.mint == registrar.mint"
     )]
     balances_locked: BalanceSandboxAccounts<'info>,
     #[account("token_program.key == &token::ID")]
@@ -678,6 +642,22 @@ pub struct CreateMember<'info> {
     rent: Sysvar<'info, Rent>,
 }
 
+fn create_member_accounts(ctx: &Context<CreateMember>, nonce: u8) -> Result<(), Error> {
+    // Check the nonce + signer is correct.
+    let seeds = &[
+        ctx.accounts.registrar.to_account_info().key.as_ref(),
+        ctx.accounts.member.to_account_info().key.as_ref(),
+        &[nonce],
+    ];
+    let member_signer = Pubkey::create_program_address(seeds, ctx.program_id)
+        .map_err(|_| ErrorCode::InvalidNonce)?;
+    if &member_signer != ctx.accounts.member_signer.to_account_info().key {
+        return Err(ErrorCode::InvalidMemberSigner.into());
+    }
+
+    Ok(())
+}
+
 #[derive(Accounts, Clone)]
 pub struct BalanceSandboxAccounts<'info> {
     balance_id: AccountInfo<'info>,
@@ -718,7 +698,7 @@ pub struct UpdateMember<'info> {
 #[derive(Accounts)]
 pub struct Deposit<'info> {
     // Lockup whitelist relay interface.
-    dummy_vesting: AccountInfo<'info>,
+    vesting: AccountInfo<'info>,
     #[account(mut)]
     depositor: AccountInfo<'info>,
     #[account(signer)]
@@ -880,7 +860,7 @@ pub struct EndUnstake<'info> {
 #[derive(Accounts)]
 pub struct Withdraw<'info> {
     // Lockup whitelist relay interface.
-    dummy_vesting: AccountInfo<'info>,
+    vesting: AccountInfo<'info>,
     #[account(mut)]
     depositor: AccountInfo<'info>,
     #[account(signer)]
@@ -1036,8 +1016,6 @@ pub struct Registrar {
     pub authority: Pubkey,
     /// Nonce to derive the program-derived address owning the vaults.
     pub nonce: u8,
-    /// The maximum stake per member, denominated in the mint.
-    pub max_stake: u64,
     /// Number of seconds that must pass for a withdrawal to complete.
     pub withdrawal_timelock: i64,
     /// Global event queue for reward vendoring.

+ 61 - 65
examples/lockup/tests/lockup.js

@@ -13,8 +13,7 @@ describe("Lockup and Registry", () => {
   const lockup = anchor.workspace.Lockup;
   const registry = anchor.workspace.Registry;
 
-  const safe = new anchor.web3.Account();
-  const whitelist = new anchor.web3.Account();
+  let lockupAddress = null;
 
   let mint = null;
   let god = null;
@@ -29,28 +28,26 @@ describe("Lockup and Registry", () => {
   });
 
   it("Is initialized!", async () => {
-    await lockup.rpc.initialize(provider.wallet.publicKey, {
+    await lockup.state.rpc.new(provider.wallet.publicKey);
+
+    lockupAddress = await lockup.state.address();
+    const lockupAccount = await lockup.state();
+
+    assert.ok(lockupAccount.authority.equals(provider.wallet.publicKey));
+    assert.ok(lockupAccount.whitelist.length === 5);
+    lockupAccount.whitelist.forEach((e) => {
+      assert.ok(e.programId.equals(new anchor.web3.PublicKey()));
+    });
+  });
+
+  it("Deletes the default whitelisted addresses", async () => {
+    const defaultEntry = { programId: new anchor.web3.PublicKey() };
+    await lockup.rpc.whitelistDelete(defaultEntry, {
       accounts: {
-        safe: safe.publicKey,
-        whitelist: whitelist.publicKey,
-        rent: anchor.web3.SYSVAR_RENT_PUBKEY,
+        authority: provider.wallet.publicKey,
+        lockup: lockupAddress,
       },
-      signers: [safe, whitelist],
-      instructions: [
-        await lockup.account.safe.createInstruction(safe),
-        await lockup.account.whitelist.createInstruction(whitelist, 1000),
-      ],
     });
-    const safeAccount = await lockup.account.safe(safe.publicKey);
-    const whitelistAccount = await lockup.account.whitelist(
-      whitelist.publicKey
-    );
-
-    assert.ok(safeAccount.authority.equals(provider.wallet.publicKey));
-    assert.ok(safeAccount.whitelist.equals(whitelist.publicKey));
-
-    assert.ok(whitelistAccount.safe.equals(safe.publicKey));
-    assert.ok(whitelistAccount.entries.length === 0);
   });
 
   it("Sets a new authority", async () => {
@@ -58,23 +55,23 @@ describe("Lockup and Registry", () => {
     await lockup.rpc.setAuthority(newAuthority.publicKey, {
       accounts: {
         authority: provider.wallet.publicKey,
-        safe: safe.publicKey,
+        lockup: lockupAddress,
       },
     });
 
-    let safeAccount = await lockup.account.safe(safe.publicKey);
-    assert.ok(safeAccount.authority.equals(newAuthority.publicKey));
+    let lockupAccount = await lockup.state();
+    assert.ok(lockupAccount.authority.equals(newAuthority.publicKey));
 
     await lockup.rpc.setAuthority(provider.wallet.publicKey, {
       accounts: {
         authority: newAuthority.publicKey,
-        safe: safe.publicKey,
+        lockup: lockupAddress,
       },
       signers: [newAuthority],
     });
 
-    safeAccount = await lockup.account.safe(safe.publicKey);
-    assert.ok(safeAccount.authority.equals(provider.wallet.publicKey));
+    lockupAccount = await lockup.state();
+    assert.ok(lockupAccount.authority.equals(provider.wallet.publicKey));
   });
 
   let e0 = null;
@@ -86,15 +83,8 @@ describe("Lockup and Registry", () => {
   it("Adds to the whitelist", async () => {
     const generateEntry = async () => {
       let programId = new anchor.web3.Account().publicKey;
-      let instance = new anchor.web3.Account().publicKey;
-      let [_, nonce] = await anchor.web3.PublicKey.findProgramAddress(
-        [instance.toBuffer()],
-        programId
-      );
       return {
         programId,
-        instance,
-        nonce,
       };
     };
     e0 = await generateEntry();
@@ -106,25 +96,24 @@ describe("Lockup and Registry", () => {
 
     const accounts = {
       authority: provider.wallet.publicKey,
-      safe: safe.publicKey,
-      whitelist: whitelist.publicKey,
+      lockup: lockupAddress,
     };
 
     await lockup.rpc.whitelistAdd(e0, { accounts });
 
-    let whitelistAccount = await lockup.account.whitelist(whitelist.publicKey);
+    let lockupAccount = await lockup.state();
 
-    assert.ok(whitelistAccount.entries.length === 1);
-    assert.deepEqual(whitelistAccount.entries, [e0]);
+    assert.ok(lockupAccount.whitelist.length === 1);
+    assert.deepEqual(lockupAccount.whitelist, [e0]);
 
     await lockup.rpc.whitelistAdd(e1, { accounts });
     await lockup.rpc.whitelistAdd(e2, { accounts });
     await lockup.rpc.whitelistAdd(e3, { accounts });
     await lockup.rpc.whitelistAdd(e4, { accounts });
 
-    whitelistAccount = await lockup.account.whitelist(whitelist.publicKey);
+    lockupAccount = await lockup.state();
 
-    assert.deepEqual(whitelistAccount.entries, [e0, e1, e2, e3, e4]);
+    assert.deepEqual(lockupAccount.whitelist, [e0, e1, e2, e3, e4]);
 
     await assert.rejects(
       async () => {
@@ -142,17 +131,16 @@ describe("Lockup and Registry", () => {
     await lockup.rpc.whitelistDelete(e0, {
       accounts: {
         authority: provider.wallet.publicKey,
-        safe: safe.publicKey,
-        whitelist: whitelist.publicKey,
+        lockup: lockupAddress,
       },
     });
-    let whitelistAccount = await lockup.account.whitelist(whitelist.publicKey);
-    assert.deepEqual(whitelistAccount.entries, [e1, e2, e3, e4]);
+    let lockupAccount = await lockup.state();
+    assert.deepEqual(lockupAccount.whitelist, [e1, e2, e3, e4]);
   });
 
   const vesting = new anchor.web3.Account();
   let vestingAccount = null;
-  let vaultAuthority = null;
+  let vestingSigner = null;
 
   it("Creates a vesting account", async () => {
     const beneficiary = provider.wallet.publicKey;
@@ -162,13 +150,13 @@ describe("Lockup and Registry", () => {
 
     const vault = new anchor.web3.Account();
     let [
-      _vaultAuthority,
+      _vestingSigner,
       nonce,
     ] = await anchor.web3.PublicKey.findProgramAddress(
-      [safe.publicKey.toBuffer(), beneficiary.toBuffer()],
+      [vesting.publicKey.toBuffer()],
       lockup.programId
     );
-    vaultAuthority = _vaultAuthority;
+    vestingSigner = _vestingSigner;
 
     await lockup.rpc.createVesting(
       beneficiary,
@@ -179,7 +167,6 @@ describe("Lockup and Registry", () => {
       {
         accounts: {
           vesting: vesting.publicKey,
-          safe: safe.publicKey,
           vault: vault.publicKey,
           depositor: god,
           depositorAuthority: provider.wallet.publicKey,
@@ -194,7 +181,7 @@ describe("Lockup and Registry", () => {
             provider,
             vault.publicKey,
             mint,
-            vaultAuthority
+            vestingSigner
           )),
         ],
       }
@@ -202,7 +189,6 @@ describe("Lockup and Registry", () => {
 
     vestingAccount = await lockup.account.vesting(vesting.publicKey);
 
-    assert.ok(vestingAccount.safe.equals(safe.publicKey));
     assert.ok(vestingAccount.beneficiary.equals(provider.wallet.publicKey));
     assert.ok(vestingAccount.mint.equals(mint));
     assert.ok(vestingAccount.grantor.equals(provider.wallet.publicKey));
@@ -220,12 +206,11 @@ describe("Lockup and Registry", () => {
       async () => {
         await lockup.rpc.withdraw(new anchor.BN(100), {
           accounts: {
-            safe: safe.publicKey,
             vesting: vesting.publicKey,
             beneficiary: provider.wallet.publicKey,
             token: god,
             vault: vestingAccount.vault,
-            vaultAuthority: vaultAuthority,
+            vestingSigner: vestingSigner,
             tokenProgram: TokenInstructions.TOKEN_PROGRAM_ID,
             clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
           },
@@ -252,12 +237,11 @@ describe("Lockup and Registry", () => {
 
     await lockup.rpc.withdraw(new anchor.BN(100), {
       accounts: {
-        safe: safe.publicKey,
         vesting: vesting.publicKey,
         beneficiary: provider.wallet.publicKey,
         token,
         vault: vestingAccount.vault,
-        vaultAuthority: vaultAuthority,
+        vestingSigner,
         tokenProgram: TokenInstructions.TOKEN_PROGRAM_ID,
         clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
       },
@@ -278,8 +262,7 @@ describe("Lockup and Registry", () => {
 
   const registrar = new anchor.web3.Account();
   const rewardQ = new anchor.web3.Account();
-  const withdrawalTimelock = new anchor.BN(5);
-  const maxStake = new anchor.BN("1000000000000000000");
+  const withdrawalTimelock = new anchor.BN(4);
   const stakeRate = new anchor.BN(2);
   const rewardQLen = 100;
   let registrarAccount = null;
@@ -300,13 +283,29 @@ describe("Lockup and Registry", () => {
     poolMint = await serumCmn.createMint(provider, registrarSigner);
   });
 
+  it("Initializes registry's global state", async () => {
+    await registry.state.rpc.new(lockup.programId);
+
+    const state = await registry.state();
+    assert.ok(state.lockupProgram.equals(lockup.programId));
+
+    // Should not allow a second initializatoin.
+    await assert.rejects(
+      async () => {
+        await registry.state.rpc.new(lockup.programId);
+      },
+      (err) => {
+        return true;
+      }
+    );
+  });
+
   it("Initializes the registrar", async () => {
     await registry.rpc.initialize(
       mint,
       provider.wallet.publicKey,
       nonce,
       withdrawalTimelock,
-      maxStake,
       stakeRate,
       rewardQLen,
       {
@@ -333,7 +332,6 @@ describe("Lockup and Registry", () => {
     assert.ok(registrarAccount.stakeRate.eq(stakeRate));
     assert.ok(registrarAccount.rewardEventQ.equals(rewardQ.publicKey));
     assert.ok(registrarAccount.withdrawalTimelock.eq(withdrawalTimelock));
-    assert.ok(registrarAccount.maxStake.eq(maxStake));
   });
 
   const member = new anchor.web3.Account();
@@ -410,7 +408,7 @@ describe("Lockup and Registry", () => {
     await registry.rpc.deposit(depositAmount, {
       accounts: {
         // Whitelist relay.
-        dummyVesting: anchor.web3.SYSVAR_RENT_PUBKEY,
+        vesting: anchor.web3.SYSVAR_RENT_PUBKEY,
         depositor: god,
         depositorAuthority: provider.wallet.publicKey,
         tokenProgram: TokenInstructions.TOKEN_PROGRAM_ID,
@@ -677,13 +675,12 @@ describe("Lockup and Registry", () => {
       vendoredVestingSigner,
       nonce,
     ] = await anchor.web3.PublicKey.findProgramAddress(
-      [safe.publicKey.toBuffer(), provider.wallet.publicKey.toBuffer()],
+      [vendoredVesting.publicKey.toBuffer()],
       lockup.programId
     );
     const remainingAccounts = lockup.instruction.createVesting
       .accounts({
         vesting: vendoredVesting.publicKey,
-        safe: safe.publicKey,
         vault: vendoredVestingVault.publicKey,
         depositor: lockedVendorVault.publicKey,
         depositorAuthority: lockedVendorSigner,
@@ -733,7 +730,6 @@ describe("Lockup and Registry", () => {
       vendoredVesting.publicKey
     );
 
-    assert.ok(lockupAccount.safe.equals(safe.publicKey));
     assert.ok(lockupAccount.beneficiary.equals(provider.wallet.publicKey));
     assert.ok(lockupAccount.mint.equals(mint));
     assert.ok(lockupAccount.vault.equals(vendoredVestingVault.publicKey));
@@ -858,7 +854,7 @@ describe("Lockup and Registry", () => {
     await registry.rpc.withdraw(withdrawAmount, {
       accounts: {
         // Whitelist relay.
-        dummyVesting: anchor.web3.SYSVAR_RENT_PUBKEY,
+        vesting: anchor.web3.SYSVAR_RENT_PUBKEY,
         depositor: token,
         depositorAuthority: provider.wallet.publicKey,
         tokenProgram: TokenInstructions.TOKEN_PROGRAM_ID,

+ 17 - 0
src/ctor.rs

@@ -0,0 +1,17 @@
+use crate::{Accounts, Sysvar};
+use solana_program::account_info::AccountInfo;
+use solana_program::sysvar::rent::Rent;
+
+// Needed for the `Accounts` macro.
+use crate as anchor_lang;
+
+#[derive(Accounts)]
+pub struct Ctor<'info> {
+    pub from: AccountInfo<'info>,
+    #[account(mut)]
+    pub to: AccountInfo<'info>,
+    pub base: AccountInfo<'info>,
+    pub system_program: AccountInfo<'info>,
+    pub program: AccountInfo<'info>,
+    pub rent: Sysvar<'info, Rent>,
+}

+ 9 - 3
src/lib.rs

@@ -31,18 +31,23 @@ mod account_info;
 mod boxed;
 mod context;
 mod cpi_account;
+mod ctor;
 mod error;
 mod program_account;
+mod state;
 mod sysvar;
 
 pub use crate::context::{Context, CpiContext};
 pub use crate::cpi_account::CpiAccount;
+pub use crate::ctor::Ctor;
 pub use crate::program_account::ProgramAccount;
+pub use crate::state::ProgramState;
 pub use crate::sysvar::Sysvar;
 pub use anchor_attribute_access_control::access_control;
 pub use anchor_attribute_account::account;
 pub use anchor_attribute_error::error;
 pub use anchor_attribute_program::program;
+pub use anchor_attribute_state::state;
 pub use anchor_derive_accounts::Accounts;
 /// Default serialization format for anchor instructions and accounts.
 pub use borsh::{BorshDeserialize as AnchorDeserialize, BorshSerialize as AnchorSerialize};
@@ -129,9 +134,10 @@ pub trait AccountDeserialize: Sized {
 /// All programs should include it via `anchor_lang::prelude::*;`.
 pub mod prelude {
     pub use super::{
-        access_control, account, error, program, AccountDeserialize, AccountSerialize, Accounts,
-        AccountsExit, AccountsInit, AnchorDeserialize, AnchorSerialize, Context, CpiAccount,
-        CpiContext, ProgramAccount, Sysvar, ToAccountInfo, ToAccountInfos, ToAccountMetas,
+        access_control, account, error, program, state, AccountDeserialize, AccountSerialize,
+        Accounts, AccountsExit, AccountsInit, AnchorDeserialize, AnchorSerialize, Context,
+        CpiAccount, CpiContext, Ctor, ProgramAccount, ProgramState, Sysvar, ToAccountInfo,
+        ToAccountInfos, ToAccountMetas,
     };
 
     pub use borsh;

+ 143 - 0
src/state.rs

@@ -0,0 +1,143 @@
+use crate::{
+    AccountDeserialize, AccountSerialize, Accounts, AccountsExit, CpiAccount, 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, DerefMut};
+
+/// Boxed container for the program state singleton.
+#[derive(Clone)]
+pub struct ProgramState<'info, T: AccountSerialize + AccountDeserialize + Clone> {
+    inner: Box<Inner<'info, T>>,
+}
+
+#[derive(Clone)]
+struct Inner<'info, T: AccountSerialize + AccountDeserialize + Clone> {
+    info: AccountInfo<'info>,
+    account: T,
+}
+
+impl<'a, T: AccountSerialize + AccountDeserialize + Clone> ProgramState<'a, T> {
+    pub fn new(info: AccountInfo<'a>, account: T) -> ProgramState<'a, T> {
+        Self {
+            inner: Box::new(Inner { info, account }),
+        }
+    }
+
+    /// Deserializes the given `info` into a `ProgramState`.
+    #[inline(never)]
+    pub fn try_from(info: &AccountInfo<'a>) -> Result<ProgramState<'a, T>, ProgramError> {
+        let mut data: &[u8] = &info.try_borrow_data()?;
+        Ok(ProgramState::new(
+            info.clone(),
+            T::try_deserialize(&mut data)?,
+        ))
+    }
+
+    pub fn seed() -> &'static str {
+        "unversioned"
+    }
+
+    pub fn address(program_id: &Pubkey) -> Pubkey {
+        let (base, _nonce) = Pubkey::find_program_address(&[], program_id);
+        let seed = Self::seed();
+        let owner = program_id;
+        Pubkey::create_with_seed(&base, seed, owner).unwrap()
+    }
+}
+
+impl<'info, T> Accounts<'info> for ProgramState<'info, T>
+where
+    T: AccountSerialize + AccountDeserialize + Clone,
+{
+    #[inline(never)]
+    fn try_accounts(
+        program_id: &Pubkey,
+        accounts: &mut &[AccountInfo<'info>],
+    ) -> Result<Self, ProgramError> {
+        if accounts.len() == 0 {
+            return Err(ProgramError::NotEnoughAccountKeys);
+        }
+        let account = &accounts[0];
+        *accounts = &accounts[1..];
+
+        if account.key != &Self::address(program_id) {
+            return Err(ProgramError::Custom(1)); // todo: proper error.
+        }
+
+        let pa = ProgramState::try_from(account)?;
+        if pa.inner.info.owner != program_id {
+            return Err(ProgramError::Custom(1)); // todo: proper error.
+        }
+        Ok(pa)
+    }
+}
+
+impl<'info, T: AccountSerialize + AccountDeserialize + Clone> ToAccountMetas
+    for ProgramState<'info, T>
+{
+    fn to_account_metas(&self, is_signer: Option<bool>) -> Vec<AccountMeta> {
+        let is_signer = is_signer.unwrap_or(self.inner.info.is_signer);
+        let meta = match self.inner.info.is_writable {
+            false => AccountMeta::new_readonly(*self.inner.info.key, is_signer),
+            true => AccountMeta::new(*self.inner.info.key, is_signer),
+        };
+        vec![meta]
+    }
+}
+
+impl<'info, T: AccountSerialize + AccountDeserialize + Clone> ToAccountInfos<'info>
+    for ProgramState<'info, T>
+{
+    fn to_account_infos(&self) -> Vec<AccountInfo<'info>> {
+        vec![self.inner.info.clone()]
+    }
+}
+
+impl<'info, T: AccountSerialize + AccountDeserialize + Clone> ToAccountInfo<'info>
+    for ProgramState<'info, T>
+{
+    fn to_account_info(&self) -> AccountInfo<'info> {
+        self.inner.info.clone()
+    }
+}
+
+impl<'a, T: AccountSerialize + AccountDeserialize + Clone> Deref for ProgramState<'a, T> {
+    type Target = T;
+
+    fn deref(&self) -> &Self::Target {
+        &(*self.inner).account
+    }
+}
+
+impl<'a, T: AccountSerialize + AccountDeserialize + Clone> DerefMut for ProgramState<'a, T> {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut DerefMut::deref_mut(&mut self.inner).account
+    }
+}
+
+impl<'info, T> From<CpiAccount<'info, T>> for ProgramState<'info, T>
+where
+    T: AccountSerialize + AccountDeserialize + Clone,
+{
+    fn from(a: CpiAccount<'info, T>) -> Self {
+        Self::new(a.to_account_info(), Deref::deref(&a).clone())
+    }
+}
+
+impl<'info, T: AccountSerialize + AccountDeserialize + Clone> AccountsExit<'info>
+    for ProgramState<'info, T>
+{
+    fn exit(&self, _program_id: &Pubkey) -> ProgramResult {
+        let info = self.to_account_info();
+        let mut data = info.try_borrow_mut_data()?;
+        let dst: &mut [u8] = &mut data;
+        let mut cursor = std::io::Cursor::new(dst);
+        self.inner.account.try_serialize(&mut cursor)?;
+        Ok(())
+    }
+}

+ 152 - 1
syn/src/codegen/program.rs

@@ -1,4 +1,5 @@
-use crate::{Program, Rpc};
+use crate::parser;
+use crate::{Program, Rpc, State};
 use heck::CamelCase;
 use quote::quote;
 
@@ -42,6 +43,30 @@ pub fn generate(program: Program) -> proc_macro2::TokenStream {
     }
 }
 pub fn generate_dispatch(program: &Program) -> proc_macro2::TokenStream {
+    let ctor_state_dispatch_arm = match &program.state {
+        None => quote! { /* no-op */ },
+        Some(state) => {
+            let variant_arm = generate_ctor_variant(program, state);
+            let ctor_args = generate_ctor_args(state);
+            quote! {
+                __private::instruction::#variant_arm => {
+                    __private::__ctor(program_id, accounts, #(#ctor_args),*)
+                }
+            }
+        }
+    };
+    let state_dispatch_arms: Vec<proc_macro2::TokenStream> = match &program.state {
+        None => vec![quote! { /* no-op */}],
+        Some(s) => s
+            .methods
+            .iter()
+            .map(|m| {
+                quote! {
+                        // todo
+                }
+            })
+            .collect(),
+    };
     let dispatch_arms: Vec<proc_macro2::TokenStream> = program
         .rpcs
         .iter()
@@ -59,6 +84,10 @@ pub fn generate_dispatch(program: &Program) -> proc_macro2::TokenStream {
 
     quote! {
         match ix {
+            #ctor_state_dispatch_arm
+
+            #(#state_dispatch_arms),*
+
             #(#dispatch_arms),*
         }
     }
@@ -69,6 +98,62 @@ pub fn generate_dispatch(program: &Program) -> proc_macro2::TokenStream {
 // so.
 pub fn generate_non_inlined_handlers(program: &Program) -> proc_macro2::TokenStream {
     let program_name = &program.name;
+    let non_inlined_ctor: proc_macro2::TokenStream = match &program.state {
+        None => quote! {},
+        Some(state) => {
+            let ctor_typed_args = generate_ctor_typed_args(state);
+            let ctor_untyped_args = generate_ctor_args(state);
+            let name = &state.strct.ident;
+            let mod_name = &program.name;
+            quote! {
+                #[inline(never)]
+                pub fn __ctor(program_id: &Pubkey, accounts: &[AccountInfo], #(#ctor_typed_args),*) -> ProgramResult {
+                    let mut accounts: &[AccountInfo] = accounts;
+                    let ctor_accounts = anchor_lang::Ctor::try_accounts(program_id, &mut accounts)?;
+
+                    let instance = #mod_name::#name::new(#(#ctor_untyped_args),*)?;
+
+                    let from = ctor_accounts.from.key;
+                    let (base, nonce) = Pubkey::find_program_address(&[], ctor_accounts.program.key);
+                    let seed = anchor_lang::ProgramState::<#name>::seed();
+                    let owner = ctor_accounts.program.key;
+                    let to = Pubkey::create_with_seed(&base, seed, owner).unwrap();
+                    let space = 1000; // todo
+                    let lamports = ctor_accounts.rent.minimum_balance(space);
+                    let seeds = &[&[nonce][..]];
+
+                    // Create the new program owned account (from within the program).
+                    let ix = anchor_lang::solana_program::system_instruction::create_account_with_seed(
+                        from,
+                        &to,
+                        &base,
+                        seed,
+                        lamports,
+                        space as u64,
+                        owner,
+                    );
+                    anchor_lang::solana_program::program::invoke_signed(
+                        &ix,
+                        &[
+                            ctor_accounts.from.clone(),
+                            ctor_accounts.to.clone(),
+                            ctor_accounts.base.clone(),
+                            ctor_accounts.system_program.clone(),
+                        ],
+                        &[seeds],
+                    )?;
+
+                    // Serialize the state and save it to storage.
+                    let mut data = ctor_accounts.to.try_borrow_mut_data()?;
+                    let dst: &mut [u8] = &mut data;
+                    let mut cursor = std::io::Cursor::new(dst);
+                    instance.try_serialize(&mut cursor)?;
+
+                    Ok(())
+                }
+            }
+        }
+    };
     let non_inlined_handlers: Vec<proc_macro2::TokenStream> = program
         .rpcs
         .iter()
@@ -98,10 +183,74 @@ pub fn generate_non_inlined_handlers(program: &Program) -> proc_macro2::TokenStr
         .collect();
 
     quote! {
+        #non_inlined_ctor
+
         #(#non_inlined_handlers)*
     }
 }
 
+pub fn generate_ctor_variant(program: &Program, state: &State) -> proc_macro2::TokenStream {
+    let enum_name = instruction_enum_name(program);
+    let ctor_args = generate_ctor_args(state);
+    if ctor_args.len() == 0 {
+        quote! {
+            #enum_name::__Ctor
+        }
+    } else {
+        quote! {
+            #enum_name::__Ctor {
+                #(#ctor_args),*
+            }
+        }
+    }
+}
+
+pub fn generate_ctor_typed_variant_with_comma(program: &Program) -> proc_macro2::TokenStream {
+    match &program.state {
+        None => quote! {},
+        Some(state) => {
+            let ctor_args = generate_ctor_typed_args(state);
+            if ctor_args.len() == 0 {
+                quote! {
+                    __Ctor
+                }
+            } else {
+                quote! {
+                    __Ctor {
+                        #(#ctor_args),*
+                    },
+                }
+            }
+        }
+    }
+}
+
+fn generate_ctor_typed_args(state: &State) -> Vec<syn::PatType> {
+    state
+        .ctor
+        .sig
+        .inputs
+        .iter()
+        .map(|arg: &syn::FnArg| match arg {
+            syn::FnArg::Typed(pat_ty) => pat_ty.clone(),
+            _ => panic!(""),
+        })
+        .collect()
+}
+
+fn generate_ctor_args(state: &State) -> Vec<Box<syn::Pat>> {
+    state
+        .ctor
+        .sig
+        .inputs
+        .iter()
+        .map(|arg: &syn::FnArg| match arg {
+            syn::FnArg::Typed(pat_ty) => pat_ty.pat.clone(),
+            _ => panic!(""),
+        })
+        .collect()
+}
+
 pub fn generate_ix_variant(program: &Program, rpc: &Rpc) -> proc_macro2::TokenStream {
     let enum_name = instruction_enum_name(program);
     let rpc_arg_names: Vec<&syn::Ident> = rpc.args.iter().map(|arg| &arg.name).collect();
@@ -131,6 +280,7 @@ pub fn generate_methods(program: &Program) -> proc_macro2::TokenStream {
 
 pub fn generate_instruction(program: &Program) -> proc_macro2::TokenStream {
     let enum_name = instruction_enum_name(program);
+    let ctor_variant = generate_ctor_typed_variant_with_comma(program);
     let variants: Vec<proc_macro2::TokenStream> = program
         .rpcs
         .iter()
@@ -160,6 +310,7 @@ pub fn generate_instruction(program: &Program) -> proc_macro2::TokenStream {
             use super::*;
             #[derive(AnchorSerialize, AnchorDeserialize)]
             pub enum #enum_name {
+                #ctor_variant
                 #(#variants),*
             }
         }

+ 17 - 0
syn/src/idl.rs

@@ -5,6 +5,8 @@ pub struct Idl {
     pub version: String,
     pub name: String,
     pub instructions: Vec<IdlInstruction>,
+    #[serde(skip_serializing_if = "Option::is_none", default)]
+    pub state: Option<IdlState>,
     #[serde(skip_serializing_if = "Vec::is_empty", default)]
     pub accounts: Vec<IdlTypeDef>,
     #[serde(skip_serializing_if = "Vec::is_empty", default)]
@@ -15,6 +17,21 @@ pub struct Idl {
     pub metadata: Option<serde_json::Value>,
 }
 
+#[derive(Debug, Serialize, Deserialize)]
+pub struct IdlState {
+    #[serde(rename = "struct")]
+    pub strct: IdlTypeDef,
+    pub methods: Vec<IdlStateMethod>,
+}
+
+// IdlStateMethods are similar to instructions, except they only allow
+// for a single account, the state account.
+#[derive(Debug, Serialize, Deserialize)]
+pub struct IdlStateMethod {
+    pub name: String,
+    pub args: Vec<IdlField>,
+}
+
 #[derive(Debug, Serialize, Deserialize)]
 pub struct IdlInstruction {
     pub name: String,

+ 23 - 0
syn/src/lib.rs

@@ -15,11 +15,22 @@ pub mod parser;
 
 #[derive(Debug)]
 pub struct Program {
+    pub state: Option<State>,
     pub rpcs: Vec<Rpc>,
     pub name: syn::Ident,
     pub program_mod: syn::ItemMod,
 }
 
+// State struct singleton.
+#[derive(Debug)]
+pub struct State {
+    pub name: String,
+    pub strct: syn::ItemStruct,
+    pub impl_block: syn::ItemImpl,
+    pub methods: Vec<Rpc>,
+    pub ctor: syn::ImplItemMethod,
+}
+
 #[derive(Debug)]
 pub struct Rpc {
     pub raw_method: syn::ItemFn,
@@ -148,6 +159,12 @@ impl Field {
 
         let ty = match &self.ty {
             Ty::AccountInfo => quote! { AccountInfo },
+            Ty::ProgramState(ty) => {
+                let account = &ty.account_ident;
+                quote! {
+                    ProgramState<#account>
+                }
+            }
             Ty::ProgramAccount(ty) => {
                 let account = &ty.account_ident;
                 quote! {
@@ -189,6 +206,7 @@ impl Field {
 #[derive(Debug, PartialEq)]
 pub enum Ty {
     AccountInfo,
+    ProgramState(ProgramStateTy),
     ProgramAccount(ProgramAccountTy),
     CpiAccount(CpiAccountTy),
     Sysvar(SysvarTy),
@@ -208,6 +226,11 @@ pub enum SysvarTy {
     Rewards,
 }
 
+#[derive(Debug, PartialEq)]
+pub struct ProgramStateTy {
+    pub account_ident: syn::Ident,
+}
+
 #[derive(Debug, PartialEq)]
 pub struct ProgramAccountTy {
     // The struct type of the account.

+ 8 - 2
syn/src/parser/accounts.rs

@@ -1,7 +1,7 @@
 use crate::{
     AccountField, AccountsStruct, CompositeField, Constraint, ConstraintBelongsTo,
     ConstraintLiteral, ConstraintOwner, ConstraintRentExempt, ConstraintSeeds, ConstraintSigner,
-    CpiAccountTy, Field, ProgramAccountTy, SysvarTy, Ty,
+    CpiAccountTy, Field, ProgramAccountTy, ProgramStateTy, SysvarTy, Ty,
 };
 
 pub fn parse(strct: &syn::ItemStruct) -> AccountsStruct {
@@ -70,7 +70,7 @@ fn parse_field(f: &syn::Field, anchor: Option<&syn::Attribute>) -> AccountField
 
 fn is_field_primitive(f: &syn::Field) -> bool {
     match ident_string(f).as_str() {
-        "ProgramAccount" | "CpiAccount" | "Sysvar" | "AccountInfo" => true,
+        "ProgramState" | "ProgramAccount" | "CpiAccount" | "Sysvar" | "AccountInfo" => true,
         _ => false,
     }
 }
@@ -81,6 +81,7 @@ fn parse_ty(f: &syn::Field) -> Ty {
         _ => panic!("invalid account syntax"),
     };
     match ident_string(f).as_str() {
+        "ProgramState" => Ty::ProgramState(parse_program_state(&path)),
         "ProgramAccount" => Ty::ProgramAccount(parse_program_account(&path)),
         "CpiAccount" => Ty::CpiAccount(parse_cpi_account(&path)),
         "Sysvar" => Ty::Sysvar(parse_sysvar(&path)),
@@ -100,6 +101,11 @@ fn ident_string(f: &syn::Field) -> String {
     segments.ident.to_string()
 }
 
+fn parse_program_state(path: &syn::Path) -> ProgramStateTy {
+    let account_ident = parse_account(&path);
+    ProgramStateTy { account_ident }
+}
+
 fn parse_cpi_account(path: &syn::Path) -> CpiAccountTy {
     let account_ident = parse_account(path);
     CpiAccountTy { account_ident }

+ 75 - 2
syn/src/parser/file.rs

@@ -1,6 +1,6 @@
 use crate::idl::*;
-use crate::parser::{accounts, error, program};
-use crate::AccountsStruct;
+use crate::parser::{self, accounts, error, program};
+use crate::{AccountsStruct, Rpc};
 use anyhow::Result;
 use heck::MixedCase;
 use quote::ToTokens;
@@ -22,6 +22,78 @@ pub fn parse(filename: impl AsRef<Path>) -> Result<Idl> {
 
     let p = program::parse(parse_program_mod(&f));
 
+    let state = p.state.map(|state| {
+        let mut methods = state
+            .methods
+            .iter()
+            .map(|method: &Rpc| {
+                let name = method.ident.to_string();
+                let args = method
+                    .args
+                    .iter()
+                    .map(|arg| {
+                        let mut tts = proc_macro2::TokenStream::new();
+                        arg.raw_arg.ty.to_tokens(&mut tts);
+                        let ty = tts.to_string().parse().unwrap();
+                        IdlField {
+                            name: arg.name.to_string().to_mixed_case(),
+                            ty,
+                        }
+                    })
+                    .collect::<Vec<_>>();
+                IdlStateMethod { name, args }
+            })
+            .collect::<Vec<_>>();
+        let ctor = {
+            let name = "new".to_string();
+            let args = state
+                .ctor
+                .sig
+                .inputs
+                .iter()
+                .map(|arg: &syn::FnArg| match arg {
+                    syn::FnArg::Typed(arg_typed) => {
+                        let mut tts = proc_macro2::TokenStream::new();
+                        arg_typed.ty.to_tokens(&mut tts);
+                        let ty = tts.to_string().parse().unwrap();
+                        IdlField {
+                            name: parser::tts_to_string(&arg_typed.pat).to_mixed_case(),
+                            ty,
+                        }
+                    }
+                    _ => panic!("Invalid syntax"),
+                })
+                .collect();
+            IdlStateMethod { name, args }
+        };
+
+        methods.insert(0, ctor);
+
+        let strct = {
+            let fields = match state.strct.fields {
+                syn::Fields::Named(f_named) => f_named
+                    .named
+                    .iter()
+                    .map(|f: &syn::Field| {
+                        let mut tts = proc_macro2::TokenStream::new();
+                        f.ty.to_tokens(&mut tts);
+                        let ty = tts.to_string().parse().unwrap();
+                        IdlField {
+                            name: f.ident.as_ref().unwrap().to_string().to_mixed_case(),
+                            ty,
+                        }
+                    })
+                    .collect::<Vec<IdlField>>(),
+                _ => panic!("State must be a struct"),
+            };
+            IdlTypeDef {
+                name: state.name,
+                ty: IdlTypeDefTy::Struct { fields },
+            }
+        };
+
+        IdlState { strct, methods }
+    });
     let error = parse_error_enum(&f).map(|mut e| error::parse(&mut e));
     let error_codes = error.as_ref().map(|e| {
         e.codes
@@ -97,6 +169,7 @@ pub fn parse(filename: impl AsRef<Path>) -> Result<Idl> {
     Ok(Idl {
         version: "0.0.0".to_string(),
         name: p.name.to_string(),
+        state,
         instructions,
         types,
         accounts,

+ 6 - 0
syn/src/parser/mod.rs

@@ -3,3 +3,9 @@ pub mod error;
 #[cfg(feature = "idl")]
 pub mod file;
 pub mod program;
+
+pub fn tts_to_string<T: quote::ToTokens>(item: T) -> String {
+    let mut tts = proc_macro2::TokenStream::new();
+    item.to_tokens(&mut tts);
+    tts.to_string()
+}

+ 79 - 6
syn/src/parser/program.rs

@@ -1,12 +1,84 @@
-use crate::{Program, Rpc, RpcArg};
+use crate::parser;
+use crate::{Program, Rpc, RpcArg, State};
 
 pub fn parse(program_mod: syn::ItemMod) -> Program {
     let mod_ident = &program_mod.ident;
-    let methods: Vec<&syn::ItemFn> = program_mod
-        .content
-        .as_ref()
-        .unwrap()
-        .1
+
+    let mod_content = &program_mod.content.as_ref().unwrap().1;
+
+    // Parse the state struct singleton.
+    let state: Option<State> = {
+        let strct: Option<&syn::ItemStruct> = mod_content
+            .iter()
+            .filter_map(|item| match item {
+                syn::Item::Struct(item_strct) => {
+                    let attrs = &item_strct.attrs;
+                    if attrs.is_empty() {
+                        return None;
+                    }
+                    let attr_label = attrs[0].path.get_ident().map(|i| i.to_string());
+                    if attr_label != Some("state".to_string()) {
+                        return None;
+                    }
+
+                    Some(item_strct)
+                }
+                _ => None,
+            })
+            .next();
+
+        let impl_block: Option<&syn::ItemImpl> = strct.map(|strct| {
+            let item_impl = mod_content
+                .iter()
+                .filter_map(|item| match item {
+                    syn::Item::Impl(item_impl) => {
+                        let impl_ty_str = parser::tts_to_string(&item_impl.self_ty);
+                        let strct_name = strct.ident.to_string();
+                        if strct_name != impl_ty_str {
+                            return None;
+                        }
+                        Some(item_impl)
+                    }
+                    _ => None,
+                })
+                .next()
+                .expect("Must provide an implementation");
+            item_impl
+        });
+
+        strct.map(|strct| {
+            // Chop off the `#[state]` attribute. It's just a marker.
+            let mut strct = strct.clone();
+            strct.attrs = vec![];
+
+            let impl_block = impl_block.expect("Must exist if struct exists").clone();
+            let ctor = impl_block
+                .items
+                .iter()
+                .filter_map(|item: &syn::ImplItem| match item {
+                    syn::ImplItem::Method(m) => {
+                        if m.sig.ident.to_string() == "new" {
+                            Some(m)
+                        } else {
+                            None
+                        }
+                    }
+                    _ => None,
+                })
+                .next()
+                .expect("Must exist if struct exists")
+                .clone();
+            State {
+                name: strct.ident.to_string(),
+                strct: strct.clone(),
+                impl_block,
+                ctor,
+                methods: vec![], // todo
+            }
+        })
+    };
+
+    let methods: Vec<&syn::ItemFn> = mod_content
         .iter()
         .filter_map(|item| match item {
             syn::Item::Fn(item_fn) => Some(item_fn),
@@ -50,6 +122,7 @@ pub fn parse(program_mod: syn::ItemMod) -> Program {
         .collect();
 
     Program {
+        state,
         rpcs,
         name: mod_ident.clone(),
         program_mod,

+ 54 - 7
ts/src/coder.ts

@@ -1,7 +1,14 @@
 import camelCase from "camelcase";
 import { Layout } from "buffer-layout";
 import * as borsh from "@project-serum/borsh";
-import { Idl, IdlField, IdlTypeDef, IdlEnumVariant, IdlType } from "./idl";
+import {
+  Idl,
+  IdlField,
+  IdlTypeDef,
+  IdlEnumVariant,
+  IdlType,
+  IdlStateMethod,
+} from "./idl";
 import { IdlError } from "./error";
 
 /**
@@ -23,10 +30,18 @@ export default class Coder {
    */
   readonly types: TypesCoder;
 
+  /**
+   * Coder for state structs.
+   */
+  readonly state: StateCoder;
+
   constructor(idl: Idl) {
     this.instruction = new InstructionCoder(idl);
     this.accounts = new AccountsCoder(idl);
     this.types = new TypesCoder(idl);
+    if (idl.state) {
+      this.state = new StateCoder(idl);
+    }
   }
 }
 
@@ -54,13 +69,24 @@ class InstructionCoder<T = any> {
   }
 
   private static parseIxLayout(idl: Idl): Layout {
-    let ixLayouts = idl.instructions.map((ix) => {
-      let fieldLayouts = ix.args.map((arg: IdlField) =>
-        IdlCoder.fieldLayout(arg, idl.types)
+    let stateMethods = idl.state ? idl.state.methods : [];
+    let ixLayouts = stateMethods
+      .map((m: IdlStateMethod) => {
+        let fieldLayouts = m.args.map((arg: IdlField) =>
+          IdlCoder.fieldLayout(arg, idl.types)
+        );
+        const name = camelCase(m.name);
+        return borsh.struct(fieldLayouts, name);
+      })
+      .concat(
+        idl.instructions.map((ix) => {
+          let fieldLayouts = ix.args.map((arg: IdlField) =>
+            IdlCoder.fieldLayout(arg, idl.types)
+          );
+          const name = camelCase(ix.name);
+          return borsh.struct(fieldLayouts, name);
+        })
       );
-      const name = camelCase(ix.name);
-      return borsh.struct(fieldLayouts, name);
-    });
     return borsh.rustEnum(ixLayouts);
   }
 }
@@ -135,6 +161,27 @@ class TypesCoder {
   }
 }
 
+class StateCoder {
+  private layout: Layout;
+
+  public constructor(idl: Idl) {
+    if (idl.state === undefined) {
+      throw new Error("Idl state not defined.");
+    }
+    this.layout = IdlCoder.typeDefLayout(idl.state.struct, idl.types);
+  }
+
+  public encode<T = any>(account: T): Buffer {
+    const buffer = Buffer.alloc(1000); // TODO: use a tighter buffer.
+    const len = this.layout.encode(account, buffer);
+    return buffer.slice(0, len);
+  }
+
+  public decode<T = any>(ix: Buffer): T {
+    return this.layout.decode(ix);
+  }
+}
+
 class IdlCoder {
   public static fieldLayout(field: IdlField, types?: IdlTypeDef[]): Layout {
     const fieldName =

+ 13 - 0
ts/src/idl.ts

@@ -2,6 +2,7 @@ export type Idl = {
   version: string;
   name: string;
   instructions: IdlInstruction[];
+  state?: IdlState;
   accounts?: IdlTypeDef[];
   types?: IdlTypeDef[];
   errors?: IdlErrorCode[];
@@ -13,6 +14,18 @@ export type IdlInstruction = {
   args: IdlField[];
 };
 
+// IdlStateMethods are similar to instructions, except they only allow
+// for a single account, the state account.
+export type IdlState = {
+  struct: IdlTypeDef;
+  methods: IdlStateMethod[];
+};
+
+export type IdlStateMethod = {
+  name: string;
+  args: IdlField[];
+};
+
 export type IdlAccountItem = IdlAccount | IdlAccounts;
 
 export type IdlAccount = {

+ 12 - 2
ts/src/program.ts

@@ -2,7 +2,7 @@ import { PublicKey } from "@solana/web3.js";
 import { RpcFactory } from "./rpc";
 import { Idl } from "./idl";
 import Coder from "./coder";
-import { Rpcs, Ixs, Txs, Accounts } from "./rpc";
+import { Rpcs, Ixs, Txs, Accounts, State } from "./rpc";
 
 /**
  * Program is the IDL deserialized representation of a Solana program.
@@ -44,6 +44,11 @@ export class Program {
    */
   readonly coder: Coder;
 
+  /**
+   * Object with state account accessors and rpcs.
+   */
+  readonly state: State;
+
   public constructor(idl: Idl, programId: PublicKey) {
     this.idl = idl;
     this.programId = programId;
@@ -52,11 +57,16 @@ export class Program {
     const coder = new Coder(idl);
 
     // Build the dynamic RPC functions.
-    const [rpcs, ixs, txs, accounts] = RpcFactory.build(idl, coder, programId);
+    const [rpcs, ixs, txs, accounts, state] = RpcFactory.build(
+      idl,
+      coder,
+      programId
+    );
     this.rpc = rpcs;
     this.instruction = ixs;
     this.transaction = txs;
     this.account = accounts;
     this.coder = coder;
+    this.state = state;
   }
 }

+ 112 - 3
ts/src/rpc.ts

@@ -8,6 +8,7 @@ import {
   Transaction,
   TransactionSignature,
   TransactionInstruction,
+  SYSVAR_RENT_PUBKEY,
 } from "@solana/web3.js";
 import { sha256 } from "crypto-hash";
 import {
@@ -19,6 +20,7 @@ import {
   IdlField,
   IdlEnumVariant,
   IdlAccountItem,
+  IdlStateMethod,
 } from "./idl";
 import { IdlError, ProgramError } from "./error";
 import Coder from "./coder";
@@ -105,6 +107,11 @@ type RpcAccounts = {
   [key: string]: PublicKey | RpcAccounts;
 };
 
+export type State = {
+  address: () => Promise<PublicKey>;
+  rpc: Rpcs;
+};
+
 /**
  * RpcFactory builds an Rpcs object for a given IDL.
  */
@@ -118,12 +125,13 @@ export class RpcFactory {
     idl: Idl,
     coder: Coder,
     programId: PublicKey
-  ): [Rpcs, Ixs, Txs, Accounts] {
+  ): [Rpcs, Ixs, Txs, Accounts, State] {
     const idlErrors = parseIdlErrors(idl);
 
     const rpcs: Rpcs = {};
     const ixFns: Ixs = {};
     const txFns: Txs = {};
+    const state = RpcFactory.buildState(idl, coder, programId);
 
     idl.instructions.forEach((idlIx) => {
       // Function to create a raw `TransactionInstruction`.
@@ -143,7 +151,107 @@ export class RpcFactory {
       ? RpcFactory.buildAccounts(idl, coder, programId)
       : {};
 
-    return [rpcs, ixFns, txFns, accountFns];
+    return [rpcs, ixFns, txFns, accountFns, state];
+  }
+
+  private static buildState(
+    idl: Idl,
+    coder: Coder,
+    programId: PublicKey
+  ): State | undefined {
+    if (idl.state === undefined) {
+      return undefined;
+    }
+    let address = async () => {
+      let [registrySigner, _nonce] = await PublicKey.findProgramAddress(
+        [],
+        programId
+      );
+      return PublicKey.createWithSeed(registrySigner, "unversioned", programId);
+    };
+
+    const rpc: Rpcs = {};
+    idl.state.methods.forEach((m: IdlStateMethod) => {
+      if (m.name !== "new") {
+        throw new Error("State struct mutatation not yet implemented.");
+      }
+      // Ctor `new` method.
+      rpc[m.name] = async (...args: any[]): Promise<TransactionSignature> => {
+        const tx = new Transaction();
+        const [programSigner, _nonce] = await PublicKey.findProgramAddress(
+          [],
+          programId
+        );
+        const ix = new TransactionInstruction({
+          keys: [
+            {
+              pubkey: getProvider().wallet.publicKey,
+              isWritable: false,
+              isSigner: true,
+            },
+            { pubkey: await address(), isWritable: true, isSigner: false },
+            { pubkey: programSigner, isWritable: false, isSigner: false },
+            {
+              pubkey: SystemProgram.programId,
+              isWritable: false,
+              isSigner: false,
+            },
+
+            { pubkey: programId, isWritable: false, isSigner: false },
+            { pubkey: SYSVAR_RENT_PUBKEY, isWritable: false, isSigner: false },
+          ],
+          programId,
+          data: coder.instruction.encode(toInstruction(m, ...args)),
+        });
+
+        tx.add(ix);
+
+        const provider = getProvider();
+        if (provider === null) {
+          throw new Error("Provider not found");
+        }
+        try {
+          const txSig = await provider.send(tx);
+          return txSig;
+        } catch (err) {
+          // TODO: translate error.
+          throw err;
+        }
+      };
+    });
+
+    // Fetches the state object from the blockchain.
+    const state = async (): Promise<any> => {
+      const addr = await address();
+      const provider = getProvider();
+      if (provider === null) {
+        throw new Error("Provider not set");
+      }
+      const accountInfo = await provider.connection.getAccountInfo(addr);
+      if (accountInfo === null) {
+        throw new Error(`Entity does not exist ${address}`);
+      }
+      // Assert the account discriminator is correct.
+      const expectedDiscriminator = Buffer.from(
+        (
+          await sha256(`state:${idl.state.struct.name}`, {
+            outputFormat: "buffer",
+          })
+        ).slice(0, 8)
+      );
+      const discriminator = accountInfo.data.slice(0, 8);
+      if (expectedDiscriminator.compare(discriminator)) {
+        throw new Error("Invalid account discriminator");
+      }
+      // Chop off the discriminator before decoding.
+      const data = accountInfo.data.slice(8);
+      return coder.state.decode(data);
+    };
+
+    state["address"] = address;
+    state["rpc"] = rpc;
+
+    return state;
   }
 
   private static buildIx(
@@ -361,7 +469,8 @@ function splitArgsAndCtx(
   return [args, options];
 }
 
-function toInstruction(idlIx: IdlInstruction, ...args: any[]) {
+// Allow either IdLInstruction or IdlStateMethod since the types share fields.
+function toInstruction(idlIx: IdlInstruction | IdlStateMethod, ...args: any[]) {
   if (idlIx.args.length != args.length) {
     throw new Error("Invalid argument length");
   }