diff --git a/common/src/lib.rs b/common/src/lib.rs index 355dbac..2a71af0 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -4,6 +4,8 @@ pub mod client; #[macro_use] pub mod pack; +#[cfg(feature = "program")] +pub mod program; // TODO: Import the shared_mem crate instead of hardcoding here. // The shared memory program is awaiting audit and so is not deployed diff --git a/registry/program/src/common.rs b/common/src/program.rs similarity index 89% rename from registry/program/src/common.rs rename to common/src/program.rs index 7eca167..2b414d5 100644 --- a/registry/program/src/common.rs +++ b/common/src/program.rs @@ -1,5 +1,5 @@ -use serum_registry::error::RegistryError; use solana_sdk::account_info::AccountInfo; +use solana_sdk::program_error::ProgramError; use spl_token::instruction as token_instruction; use std::convert::Into; @@ -10,7 +10,7 @@ pub fn invoke_token_transfer<'a, 'b>( tok_program_acc_info: &'a AccountInfo<'b>, signer_seeds: &[&[&[u8]]], amount: u64, -) -> Result<(), RegistryError> { +) -> Result<(), ProgramError> { let transfer_instr = token_instruction::transfer( &spl_token::ID, from_acc_info.key, @@ -29,5 +29,4 @@ pub fn invoke_token_transfer<'a, 'b>( ], signer_seeds, ) - .map_err(Into::into) } diff --git a/lockup/cli/src/lib.rs b/lockup/cli/src/lib.rs index 5f2b14e..8bb667a 100644 --- a/lockup/cli/src/lib.rs +++ b/lockup/cli/src/lib.rs @@ -59,12 +59,12 @@ pub enum SubCommand { #[clap(short = 'a', long)] deposit_amount: u64, }, - /// Redeem a claimed token receipt for an amount of vested tokens. - Redeem { - /// The amount of vested tokens to redeem. + /// Withdraw vested tokens. + Withdraw { + /// The amount of vested tokens to withdraw. #[clap(short, long)] amount: u64, - /// Vesting account to redeem from. + /// Vesting account to withdraw from. #[clap(short, long)] vesting: Pubkey, /// Token account to send the vested tokens to. @@ -171,7 +171,7 @@ pub fn run(opts: Opts) -> Result<()> { println!("{:#?}", resp); Ok(()) } - SubCommand::Redeem { + SubCommand::Withdraw { vesting, amount, token_account, @@ -180,7 +180,7 @@ pub fn run(opts: Opts) -> Result<()> { let client = ctx.connect::(opts.cmd.pid)?; let vesting_account = client.vesting(&vesting)?; let safe = vesting_account.safe; - let resp = client.redeem(RedeemRequest { + let resp = client.withdraw(WithdrawRequest { beneficiary: &beneficiary, vesting, token_account, @@ -207,7 +207,7 @@ fn account_cmd(ctx: &Context, pid: Pubkey, cmd: AccountsCommand) -> Result<()> { let current_ts = client.rpc().get_block_time(client.rpc().get_slot()?)?; let amount = vault.available_for_withdrawal(current_ts); - println!("Redeemable balance: {:?}", amount); + println!("Withdrawable balance: {:?}", amount); println!("Whitelistable balance: {:?}", amount); Ok(()) diff --git a/lockup/client/src/lib.rs b/lockup/client/src/lib.rs index 6ca8997..51a7998 100644 --- a/lockup/client/src/lib.rs +++ b/lockup/client/src/lib.rs @@ -108,6 +108,7 @@ impl Client { safe, whitelist_program, mut relay_accounts, + whitelist_program_vault, whitelist_program_vault_authority, delegate_amount, relay_data, @@ -123,7 +124,6 @@ impl Client { AccountMeta::new_readonly(safe, false), AccountMeta::new_readonly(whitelist, false), AccountMeta::new_readonly(whitelist_program, false), - AccountMeta::new_readonly(whitelist_program_vault_authority, false), // Below are relay accounts. AccountMeta::new(vault, false), AccountMeta::new_readonly( @@ -131,6 +131,8 @@ impl Client { false, ), AccountMeta::new_readonly(spl_token::ID, false), + AccountMeta::new(whitelist_program_vault, false), + AccountMeta::new_readonly(whitelist_program_vault_authority, false), ]; accounts.append(&mut relay_accounts); @@ -156,6 +158,7 @@ impl Client { vesting, safe, whitelist_program, + whitelist_program_vault, whitelist_program_vault_authority, relay_data, mut relay_accounts, @@ -180,6 +183,7 @@ impl Client { false, ), AccountMeta::new_readonly(spl_token::ID, false), + AccountMeta::new(whitelist_program_vault, false), AccountMeta::new_readonly(whitelist_program_vault_authority, false), ]; // Program specific relay. @@ -195,8 +199,8 @@ impl Client { Ok(WhitelistDepositResponse { tx }) } - pub fn redeem(&self, req: RedeemRequest) -> Result { - let RedeemRequest { + pub fn withdraw(&self, req: WithdrawRequest) -> Result { + let WithdrawRequest { beneficiary, vesting, token_account, @@ -221,8 +225,8 @@ impl Client { let signers = [self.payer(), &beneficiary]; let tx = self .inner - .redeem_with_signers(&signers, &accounts, amount)?; - Ok(RedeemResponse { tx }) + .withdraw_with_signers(&signers, &accounts, amount)?; + Ok(WithdrawResponse { tx }) } pub fn set_authority( @@ -281,7 +285,7 @@ impl Client { ®istry_pid, ) .map_err(|_| anyhow!("unable to create vault authority"))?; - let vault = match is_mega { + let whitelist_program_vault = match is_mega { false => r.vault, true => r.mega_vault, }; @@ -291,7 +295,6 @@ impl Client { AccountMeta::new(entity, false), AccountMeta::new_readonly(registrar, false), AccountMeta::new_readonly(solana_sdk::sysvar::clock::ID, false), - AccountMeta::new(vault, false), ]; let (pool_accs, _) = r_client.common_pool_accounts(pool_program_id, registrar, false)?; relay_accounts.extend_from_slice(&pool_accs); @@ -302,6 +305,7 @@ impl Client { safe, whitelist_program: registry_pid, relay_accounts, + whitelist_program_vault, whitelist_program_vault_authority, delegate_amount: amount, relay_data, @@ -346,7 +350,7 @@ impl Client { ®istry_pid, ) .map_err(|_| anyhow!("unable to create vault authority"))?; - let vault = match is_mega { + let whitelist_program_vault = match is_mega { false => r.vault, true => r.mega_vault, }; @@ -356,7 +360,6 @@ impl Client { AccountMeta::new(entity, false), AccountMeta::new_readonly(registrar, false), AccountMeta::new_readonly(solana_sdk::sysvar::clock::ID, false), - AccountMeta::new(vault, false), ]; let (pool_accs, _) = r_client.common_pool_accounts(pool_program_id, registrar, is_mega)?; relay_accounts.extend_from_slice(&pool_accs); @@ -366,6 +369,7 @@ impl Client { vesting, safe, whitelist_program: registry_pid, + whitelist_program_vault, whitelist_program_vault_authority, relay_data, relay_accounts, @@ -509,9 +513,10 @@ pub struct WhitelistWithdrawRequest<'a> { pub vesting: Pubkey, pub safe: Pubkey, pub whitelist_program: Pubkey, - pub relay_accounts: Vec, + pub whitelist_program_vault: Pubkey, pub whitelist_program_vault_authority: Pubkey, pub delegate_amount: u64, + pub relay_accounts: Vec, pub relay_data: Vec, pub relay_signers: Vec<&'a Keypair>, } @@ -526,6 +531,7 @@ pub struct WhitelistDepositRequest<'a> { pub vesting: Pubkey, pub safe: Pubkey, pub whitelist_program: Pubkey, + pub whitelist_program_vault: Pubkey, pub whitelist_program_vault_authority: Pubkey, pub relay_accounts: Vec, pub relay_data: Vec, @@ -537,7 +543,7 @@ pub struct WhitelistDepositResponse { pub tx: Signature, } -pub struct RedeemRequest<'a> { +pub struct WithdrawRequest<'a> { pub beneficiary: &'a Keypair, pub vesting: Pubkey, pub token_account: Pubkey, @@ -546,7 +552,7 @@ pub struct RedeemRequest<'a> { } #[derive(Debug)] -pub struct RedeemResponse { +pub struct WithdrawResponse { pub tx: Signature, } diff --git a/lockup/program/src/available_for_withdrawal.rs b/lockup/program/src/available_for_withdrawal.rs index 98062b7..3d6c5eb 100644 --- a/lockup/program/src/available_for_withdrawal.rs +++ b/lockup/program/src/available_for_withdrawal.rs @@ -1,4 +1,4 @@ -use crate::access_control; +use crate::common::access_control; use serum_common::pack::Pack; use serum_lockup::accounts::Vesting; use serum_lockup::error::LockupError; @@ -6,6 +6,7 @@ use solana_program::info; use solana_sdk::account_info::{next_account_info, AccountInfo}; use solana_sdk::pubkey::Pubkey; +// Convenience instruction for UI's. pub fn handler(_program_id: &Pubkey, accounts: &[AccountInfo]) -> Result<(), LockupError> { let acc_infos = &mut accounts.iter(); diff --git a/lockup/program/src/access_control.rs b/lockup/program/src/common/access_control.rs similarity index 91% rename from lockup/program/src/access_control.rs rename to lockup/program/src/common/access_control.rs index 90d6fa1..fc5f9ab 100644 --- a/lockup/program/src/access_control.rs +++ b/lockup/program/src/common/access_control.rs @@ -48,14 +48,13 @@ pub fn whitelist<'a>( Ok(wl) } -/// Access control on any instruction mutating an existing Vesting account. pub fn vesting( program_id: &Pubkey, - safe: &Pubkey, + safe_acc_info: &AccountInfo, vesting_acc_info: &AccountInfo, vesting_acc_beneficiary_info: &AccountInfo, ) -> Result { - let vesting = vesting_raw(program_id, safe, vesting_acc_info)?; + let vesting = _vesting(program_id, safe_acc_info, vesting_acc_info)?; if vesting.beneficiary != *vesting_acc_beneficiary_info.key { return Err(LockupErrorCode::Unauthorized)?; @@ -64,9 +63,9 @@ pub fn vesting( Ok(vesting) } -pub fn vesting_raw( +fn _vesting( program_id: &Pubkey, - safe: &Pubkey, + safe_acc_info: &AccountInfo, vesting_acc_info: &AccountInfo, ) -> Result { let mut data: &[u8] = &vesting_acc_info.try_borrow_data()?; @@ -78,36 +77,20 @@ pub fn vesting_raw( if !vesting.initialized { return Err(LockupErrorCode::NotInitialized)?; } - if vesting.safe != *safe { + if vesting.safe != *safe_acc_info.key { return Err(LockupErrorCode::WrongSafe)?; } Ok(vesting) } -pub fn rent(acc_info: &AccountInfo) -> Result { - if *acc_info.key != solana_sdk::sysvar::rent::id() { - return Err(LockupErrorCode::InvalidRentSysvar)?; - } - Rent::from_account_info(acc_info).map_err(Into::into) -} - -pub fn clock(acc_info: &AccountInfo) -> Result { - if *acc_info.key != solana_sdk::sysvar::clock::id() { - return Err(LockupErrorCode::InvalidClockSysvar)?; - } - Clock::from_account_info(acc_info).map_err(Into::into) -} - pub fn safe(acc_info: &AccountInfo, program_id: &Pubkey) -> Result { if acc_info.owner != program_id { return Err(LockupErrorCode::InvalidAccountOwner)?; } - let safe = Safe::unpack(&acc_info.try_borrow_data()?)?; if !safe.initialized { return Err(LockupErrorCode::NotInitialized)?; } - Ok(safe) } @@ -119,8 +102,10 @@ pub fn vault( safe_acc_info: &AccountInfo, program_id: &Pubkey, ) -> Result { - let vesting = vesting_raw(program_id, safe_acc_info.key, vesting_acc_info)?; + let vesting = _vesting(program_id, safe_acc_info, vesting_acc_info)?; + let vault = token(acc_info)?; + let va = vault_authority( program_id, vault_authority_acc_info, @@ -132,14 +117,11 @@ pub fn vault( if va != vault.owner { return Err(LockupErrorCode::InvalidVault)?; } - if va != *vault_authority_acc_info.key { - return Err(LockupErrorCode::InvalidVault)?; - } Ok(vault) } -pub fn vault_authority( +fn vault_authority( program_id: &Pubkey, vault_authority_acc_info: &AccountInfo, beneficiary_acc_info: &AccountInfo, @@ -170,3 +152,17 @@ pub fn token(acc_info: &AccountInfo) -> Result { Ok(token) } + +pub fn rent(acc_info: &AccountInfo) -> Result { + if *acc_info.key != solana_sdk::sysvar::rent::id() { + return Err(LockupErrorCode::InvalidRentSysvar)?; + } + Rent::from_account_info(acc_info).map_err(Into::into) +} + +pub fn clock(acc_info: &AccountInfo) -> Result { + if *acc_info.key != solana_sdk::sysvar::clock::id() { + return Err(LockupErrorCode::InvalidClockSysvar)?; + } + Clock::from_account_info(acc_info).map_err(Into::into) +} diff --git a/lockup/program/src/common/mod.rs b/lockup/program/src/common/mod.rs new file mode 100644 index 0000000..af2c212 --- /dev/null +++ b/lockup/program/src/common/mod.rs @@ -0,0 +1,43 @@ +use serum_lockup::accounts::vault; +use serum_lockup::accounts::Vesting; +use solana_sdk::account_info::AccountInfo; +use solana_sdk::entrypoint::ProgramResult; +use solana_sdk::instruction::Instruction; +use solana_sdk::pubkey::Pubkey; + +pub mod access_control; + +// Prepends the program's unique TAG identifier before making the signed +// cross program invocation to a *trusted* program on the whitelist. +// +// The trusted program should perform three steps of validation: +// +// 1. Check for the TAG identifier in the first 8 bytes of the instruction data. +// If present, then authentication must be done on the following two steps. +// 2. Check accounts[1] is signed. +// 3. Check accounts[1] is the correct program derived address for the vesting +// account, i.e., signer seeds == [safe_address, beneficiary_address, nonce]. +// +// If all of these hold, a program can trust the instruction was invoked +// by a the lockup program on behalf of a vesting account. +// +// Importantly, it's the responsibility of the trusted program to maintain the +// locked invariant preserved by this program and to return the funds at an +// unspecified point in the future for unlocking. Any bug in the trusted program +// can result in locked funds becoming unlocked, so take care when adding to the +// whitelist. +pub fn whitelist_cpi( + mut instruction: Instruction, + safe: &Pubkey, + beneficiary_acc_info: &AccountInfo, + vesting: &Vesting, + accounts: &[AccountInfo], +) -> ProgramResult { + let mut data = serum_lockup::instruction::TAG.to_le_bytes().to_vec(); + data.extend(instruction.data); + + instruction.data = data; + + let signer_seeds = vault::signer_seeds(safe, beneficiary_acc_info.key, &vesting.nonce); + solana_sdk::program::invoke_signed(&instruction, accounts, &[&signer_seeds]) +} diff --git a/lockup/program/src/create_vesting.rs b/lockup/program/src/create_vesting.rs index d71f713..80f412a 100644 --- a/lockup/program/src/create_vesting.rs +++ b/lockup/program/src/create_vesting.rs @@ -1,5 +1,6 @@ -use crate::access_control; +use crate::common::access_control; use serum_common::pack::Pack; +use serum_common::program::invoke_token_transfer; use serum_lockup::accounts::{vault, Vesting}; use serum_lockup::error::{LockupError, LockupErrorCode}; use solana_program::info; @@ -47,13 +48,13 @@ pub fn handler( Vesting::unpack_unchecked_mut( &mut vesting_acc_info.try_borrow_mut_data()?, - &mut |vesting_acc: &mut Vesting| { + &mut |vesting: &mut Vesting| { state_transition(StateTransitionRequest { clock_ts, end_ts, period_count, deposit_amount, - vesting_acc, + vesting, beneficiary, safe_acc_info, depositor_acc_info, @@ -94,8 +95,8 @@ fn access_control(req: AccessControlRequest) -> Result Result Result<(), LockupError> { end_ts, period_count, deposit_amount, - vesting_acc, + vesting, beneficiary, safe_acc_info, depositor_acc_info, @@ -169,60 +170,45 @@ fn state_transition(req: StateTransitionRequest) -> Result<(), LockupError> { } = req; // Initialize account. - { - vesting_acc.safe = safe_acc_info.key.clone(); - vesting_acc.beneficiary = beneficiary; - vesting_acc.initialized = true; - vesting_acc.mint = vault.mint; - vesting_acc.vault = *vault_acc_info.key; - vesting_acc.period_count = period_count; - vesting_acc.start_balance = deposit_amount; - vesting_acc.end_ts = end_ts; - vesting_acc.start_ts = clock_ts; - vesting_acc.balance = deposit_amount; - vesting_acc.whitelist_owned = 0; - vesting_acc.grantor = *depositor_authority_acc_info.key; - vesting_acc.nonce = nonce; - } + vesting.safe = safe_acc_info.key.clone(); + vesting.beneficiary = beneficiary; + vesting.initialized = true; + vesting.mint = vault.mint; + vesting.vault = *vault_acc_info.key; + vesting.period_count = period_count; + vesting.start_balance = deposit_amount; + vesting.end_ts = end_ts; + vesting.start_ts = clock_ts; + vesting.outstanding = deposit_amount; + vesting.whitelist_owned = 0; + vesting.grantor = *depositor_authority_acc_info.key; + vesting.nonce = nonce; - // Now transfer SPL funds from the depositor, to the - // program-controlled vault. - { - let deposit_instruction = spl_token::instruction::transfer( - &spl_token::ID, - depositor_acc_info.key, - vault_acc_info.key, - depositor_authority_acc_info.key, - &[], - deposit_amount, - )?; - solana_sdk::program::invoke_signed( - &deposit_instruction, - &[ - depositor_acc_info.clone(), - depositor_authority_acc_info.clone(), - vault_acc_info.clone(), - token_program_acc_info.clone(), - ], - &[], - )?; - } + // Transfer funds to vault. + invoke_token_transfer( + depositor_acc_info, + vault_acc_info, + depositor_authority_acc_info, + token_program_acc_info, + &[], + deposit_amount, + )?; Ok(()) } struct AccessControlRequest<'a, 'b> { - program_id: &'a Pubkey, - beneficiary: Pubkey, - end_ts: i64, - period_count: u64, - deposit_amount: u64, vesting_acc_info: &'a AccountInfo<'b>, safe_acc_info: &'a AccountInfo<'b>, depositor_authority_acc_info: &'a AccountInfo<'b>, vault_acc_info: &'a AccountInfo<'b>, rent_acc_info: &'a AccountInfo<'b>, clock_acc_info: &'a AccountInfo<'b>, + program_id: &'a Pubkey, + beneficiary: Pubkey, + end_ts: i64, + period_count: u64, + deposit_amount: u64, nonce: u8, } @@ -232,17 +218,17 @@ struct AccessControlResponse { } struct StateTransitionRequest<'a, 'b, 'c> { + depositor_authority_acc_info: &'a AccountInfo<'b>, + depositor_acc_info: &'a AccountInfo<'b>, + token_program_acc_info: &'a AccountInfo<'b>, + safe_acc_info: &'a AccountInfo<'b>, + vault_acc_info: &'a AccountInfo<'b>, + vesting: &'c mut Vesting, + vault: TokenAccount, + beneficiary: Pubkey, clock_ts: i64, end_ts: i64, period_count: u64, deposit_amount: u64, nonce: u8, - vesting_acc: &'c mut Vesting, - vault: TokenAccount, - vault_acc_info: &'a AccountInfo<'b>, - beneficiary: Pubkey, - safe_acc_info: &'a AccountInfo<'b>, - depositor_acc_info: &'a AccountInfo<'b>, - depositor_authority_acc_info: &'a AccountInfo<'b>, - token_program_acc_info: &'a AccountInfo<'b>, } diff --git a/lockup/program/src/initialize.rs b/lockup/program/src/initialize.rs index c44532f..5f44fe1 100644 --- a/lockup/program/src/initialize.rs +++ b/lockup/program/src/initialize.rs @@ -1,4 +1,4 @@ -use crate::access_control; +use crate::common::access_control; use serum_common::pack::Pack; use serum_lockup::accounts::{Safe, Whitelist}; use serum_lockup::error::{LockupError, LockupErrorCode}; @@ -31,11 +31,10 @@ pub fn handler( &mut safe_acc_info.try_borrow_mut_data()?, &mut |safe: &mut Safe| { state_transition(StateTransitionRequest { - safe, - safe_addr: safe_acc_info.key, - whitelist: Whitelist::new(whitelist_acc_info.clone())?, - whitelist_addr: whitelist_acc_info.key, authority, + safe, + safe_acc_info, + whitelist_acc_info, }) .map_err(Into::into) }, @@ -73,7 +72,7 @@ fn access_control(req: AccessControlRequest) -> Result<(), LockupError> { } } - // Whitelist (not yet set on Safe). + // Whitelist (uninitialized). { if whitelist_acc_info.owner != program_id { return Err(LockupErrorCode::InvalidAccountOwner)?; @@ -97,36 +96,33 @@ fn state_transition(req: StateTransitionRequest) -> Result<(), LockupError> { let StateTransitionRequest { safe, - safe_addr, + safe_acc_info, authority, - whitelist, - whitelist_addr, + whitelist_acc_info, } = req; // Initialize Safe. safe.initialized = true; safe.authority = authority; - safe.whitelist = *whitelist_addr; + safe.whitelist = *whitelist_acc_info.key; // Inittialize Whitelist. - whitelist.set_safe(safe_addr)?; - - info!("state-transition: success"); + let whitelist = Whitelist::new(whitelist_acc_info.clone())?; + whitelist.set_safe(safe_acc_info.key)?; Ok(()) } struct AccessControlRequest<'a, 'b> { - program_id: &'a Pubkey, safe_acc_info: &'a AccountInfo<'b>, whitelist_acc_info: &'a AccountInfo<'b>, rent_acc_info: &'a AccountInfo<'b>, + program_id: &'a Pubkey, } struct StateTransitionRequest<'a, 'b> { + safe_acc_info: &'a AccountInfo<'b>, + whitelist_acc_info: &'a AccountInfo<'b>, safe: &'a mut Safe, - safe_addr: &'a Pubkey, - whitelist_addr: &'a Pubkey, - whitelist: Whitelist<'b>, authority: Pubkey, } diff --git a/lockup/program/src/lib.rs b/lockup/program/src/lib.rs index 46425cf..3829a96 100644 --- a/lockup/program/src/lib.rs +++ b/lockup/program/src/lib.rs @@ -1,5 +1,3 @@ -//! Program entrypoint. - #![cfg_attr(feature = "strict", deny(warnings))] use serum_common::pack::Pack; @@ -9,16 +7,16 @@ use solana_sdk::account_info::AccountInfo; use solana_sdk::entrypoint::ProgramResult; use solana_sdk::pubkey::Pubkey; -pub(crate) mod access_control; mod available_for_withdrawal; +mod common; mod create_vesting; mod initialize; -mod redeem; mod set_authority; mod whitelist_add; mod whitelist_delete; mod whitelist_deposit; mod whitelist_withdraw; +mod withdraw; solana_sdk::entrypoint!(entry); fn entry(program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult { @@ -44,7 +42,7 @@ fn entry(program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8]) deposit_amount, nonce, ), - LockupInstruction::Redeem { amount } => redeem::handler(program_id, accounts, amount), + LockupInstruction::Withdraw { amount } => withdraw::handler(program_id, accounts, amount), LockupInstruction::WhitelistWithdraw { amount, instruction_data, diff --git a/lockup/program/src/set_authority.rs b/lockup/program/src/set_authority.rs index ab554a7..b611d55 100644 --- a/lockup/program/src/set_authority.rs +++ b/lockup/program/src/set_authority.rs @@ -1,4 +1,4 @@ -use crate::access_control; +use crate::common::access_control; use serum_common::pack::Pack; use serum_lockup::accounts::Safe; use serum_lockup::error::LockupError; @@ -51,17 +51,9 @@ fn access_control(req: AccessControlRequest) -> Result<(), LockupError> { // Governance authorization. let _ = access_control::governance(program_id, safe_acc_info, safe_authority_acc_info)?; - info!("access-control: success"); - Ok(()) } -struct AccessControlRequest<'a, 'b> { - program_id: &'a Pubkey, - safe_acc_info: &'a AccountInfo<'b>, - safe_authority_acc_info: &'a AccountInfo<'b>, -} - fn state_transition(req: StateTransitionRequest) -> Result<(), LockupError> { info!("state-transition: set_authority"); @@ -72,11 +64,15 @@ fn state_transition(req: StateTransitionRequest) -> Result<(), LockupError> { safe_acc.authority = new_authority; - info!("state-transition: success"); - Ok(()) } +struct AccessControlRequest<'a, 'b> { + program_id: &'a Pubkey, + safe_acc_info: &'a AccountInfo<'b>, + safe_authority_acc_info: &'a AccountInfo<'b>, +} + struct StateTransitionRequest<'a> { safe_acc: &'a mut Safe, new_authority: Pubkey, diff --git a/lockup/program/src/whitelist_add.rs b/lockup/program/src/whitelist_add.rs index db6a080..464651e 100644 --- a/lockup/program/src/whitelist_add.rs +++ b/lockup/program/src/whitelist_add.rs @@ -1,4 +1,4 @@ -use crate::access_control; +use crate::common::access_control; use serum_lockup::accounts::{Whitelist, WhitelistEntry}; use serum_lockup::error::{LockupError, LockupErrorCode}; use solana_program::info; @@ -54,8 +54,6 @@ fn access_control(req: AccessControlRequest) -> Result<(), LockupError> { // Must be a valid derived address. let _ = wl_entry.derived_address()?; - info!("access-control: success"); - Ok(()) } @@ -71,8 +69,6 @@ fn state_transition(req: StateTransitionRequest) -> Result<(), LockupError> { .push(wl_entry)? .ok_or(LockupErrorCode::WhitelistFull)?; - info!("state-transition: success"); - Ok(()) } diff --git a/lockup/program/src/whitelist_delete.rs b/lockup/program/src/whitelist_delete.rs index 426e7d1..aff41c3 100644 --- a/lockup/program/src/whitelist_delete.rs +++ b/lockup/program/src/whitelist_delete.rs @@ -1,4 +1,4 @@ -use crate::access_control; +use crate::common::access_control; use serum_lockup::accounts::{Whitelist, WhitelistEntry}; use serum_lockup::error::{LockupError, LockupErrorCode}; use solana_program::info; @@ -50,8 +50,6 @@ fn access_control(req: AccessControlRequest) -> Result<(), LockupError> { let _ = access_control::whitelist(whitelist_acc_info.clone(), safe_acc_info, &safe, program_id)?; - info!("access-control: success"); - Ok(()) } @@ -67,8 +65,6 @@ fn state_transition(req: StateTransitionRequest) -> Result<(), LockupError> { .delete(wl_entry)? .ok_or(LockupErrorCode::WhitelistNotFound)?; - info!("state-transition: success"); - Ok(()) } diff --git a/lockup/program/src/whitelist_deposit.rs b/lockup/program/src/whitelist_deposit.rs index 42b49a8..cd0237b 100644 --- a/lockup/program/src/whitelist_deposit.rs +++ b/lockup/program/src/whitelist_deposit.rs @@ -1,6 +1,6 @@ -use crate::access_control; +use crate::common::{access_control, whitelist_cpi}; use serum_common::pack::Pack; -use serum_lockup::accounts::{vault, Vesting}; +use serum_lockup::accounts::Vesting; use serum_lockup::error::{LockupError, LockupErrorCode}; use solana_program::info; use solana_sdk::account_info::{next_account_info, AccountInfo}; @@ -8,11 +8,12 @@ use solana_sdk::instruction::{AccountMeta, Instruction}; use solana_sdk::program_pack::Pack as TokenPack; use solana_sdk::pubkey::Pubkey; use std::convert::Into; +use std::iter::Iterator; pub fn handler( program_id: &Pubkey, accounts: &[AccountInfo], - instruction_data: Vec, + ref instruction_data: Vec, ) -> Result<(), LockupError> { info!("handler: whitelist_deposit"); @@ -30,10 +31,11 @@ pub fn handler( let vault_acc_info = next_account_info(acc_infos)?; let vault_auth_acc_info = next_account_info(acc_infos)?; let tok_prog_acc_info = next_account_info(acc_infos)?; + let wl_prog_vault_acc_info = next_account_info(acc_infos)?; let wl_prog_vault_authority_acc_info = next_account_info(acc_infos)?; // Program specific. - let remaining_relay_accs: Vec<&AccountInfo> = acc_infos.collect(); + let remaining_relay_accs = acc_infos; access_control(AccessControlRequest { program_id, @@ -41,6 +43,7 @@ pub fn handler( vesting_acc_info, wl_acc_info, wl_prog_acc_info, + wl_prog_vault_acc_info, wl_prog_vault_authority_acc_info, safe_acc_info, vault_acc_info, @@ -52,17 +55,17 @@ pub fn handler( &mut |vesting: &mut Vesting| { state_transition(StateTransitionRequest { accounts, - instruction_data: instruction_data.clone(), - safe_acc: safe_acc_info.key, - nonce: vesting.nonce, + instruction_data, + safe_acc_info, wl_prog_acc_info, + wl_prog_vault_acc_info, wl_prog_vault_authority_acc_info, vault_acc_info, vault_auth_acc_info, tok_prog_acc_info, vesting, beneficiary_acc_info, - remaining_relay_accs: remaining_relay_accs.clone(), + remaining_relay_accs, }) .map_err(Into::into) }, @@ -80,6 +83,7 @@ fn access_control(req: AccessControlRequest) -> Result<(), LockupError> { vesting_acc_info, wl_acc_info, wl_prog_acc_info, + wl_prog_vault_acc_info, wl_prog_vault_authority_acc_info, safe_acc_info, vault_acc_info, @@ -105,20 +109,25 @@ fn access_control(req: AccessControlRequest) -> Result<(), LockupError> { )?; let _vesting = access_control::vesting( program_id, - safe_acc_info.key, + safe_acc_info, vesting_acc_info, beneficiary_acc_info, )?; // WhitelistDeposit checks. + // + // Is the given program on the whitelist? let entry = whitelist .get_derived(wl_prog_vault_authority_acc_info.key)? .ok_or(LockupErrorCode::WhitelistNotFound)?; if entry.program_id() != *wl_prog_acc_info.key { return Err(LockupErrorCode::WhitelistInvalidProgramId)?; } - - info!("access-control: success"); + // Is the vault owned by this whitelisted authority? + let wl_vault = access_control::token(wl_prog_vault_acc_info)?; + if &wl_vault.owner != wl_prog_vault_authority_acc_info.key { + return Err(LockupErrorCode::InvalidTokenAccountOwner)?; + } Ok(()) } @@ -130,18 +139,17 @@ fn state_transition(req: StateTransitionRequest) -> Result<(), LockupError> { vesting, instruction_data, accounts, - nonce, - safe_acc, + safe_acc_info, vault_acc_info, vault_auth_acc_info, wl_prog_acc_info, + wl_prog_vault_acc_info, wl_prog_vault_authority_acc_info, remaining_relay_accs, tok_prog_acc_info, beneficiary_acc_info, } = req; - // Check before balance. let before_amount = { let vault = spl_token::state::Account::unpack(&vault_acc_info.try_borrow_data()?)?; vault.amount @@ -153,44 +161,47 @@ fn state_transition(req: StateTransitionRequest) -> Result<(), LockupError> { AccountMeta::new(*vault_acc_info.key, false), AccountMeta::new_readonly(*vault_auth_acc_info.key, true), AccountMeta::new_readonly(*tok_prog_acc_info.key, false), + AccountMeta::new(*wl_prog_vault_acc_info.key, false), AccountMeta::new_readonly(*wl_prog_vault_authority_acc_info.key, false), ]; - for a in remaining_relay_accs { + meta_accounts.extend(remaining_relay_accs.map(|a| { if a.is_writable { - meta_accounts.push(AccountMeta::new(*a.key, a.is_signer)); + AccountMeta::new(*a.key, a.is_signer) } else { - meta_accounts.push(AccountMeta::new_readonly(*a.key, a.is_signer)); + AccountMeta::new_readonly(*a.key, a.is_signer) } - } - let mut data = serum_lockup::instruction::TAG.to_le_bytes().to_vec(); - data.extend(instruction_data); + })); let relay_instruction = Instruction { program_id: *wl_prog_acc_info.key, accounts: meta_accounts, - data, + data: instruction_data.to_vec(), }; - let signer_seeds = vault::signer_seeds(safe_acc, beneficiary_acc_info.key, &nonce); - solana_sdk::program::invoke_signed(&relay_instruction, &accounts[..], &[&signer_seeds])?; + whitelist_cpi( + relay_instruction, + safe_acc_info.key, + beneficiary_acc_info, + vesting, + accounts, + )?; } - // Update vesting account with the deposit. - { + let after_amount = { let vault = spl_token::state::Account::unpack(&vault_acc_info.try_borrow_data()?)?; - let deposit_amount = vault.amount - before_amount; + vault.amount + }; - // Safety checks. - // - // Balance must go up. - if deposit_amount <= 0 { - return Err(LockupErrorCode::InsufficientDepositAmount)?; - } - // Cannot deposit more than withdrawn. - if deposit_amount > vesting.whitelist_owned { - return Err(LockupErrorCode::DepositOverflow)?; - } - - vesting.whitelist_owned -= deposit_amount; + // Deposit safety checks. + let deposit_amount = after_amount - before_amount; + // Balance must go up. + if deposit_amount <= 0 { + return Err(LockupErrorCode::InsufficientDepositAmount)?; } + // Cannot deposit more than withdrawn. + if deposit_amount > vesting.whitelist_owned { + return Err(LockupErrorCode::DepositOverflow)?; + } + // Book keeping. + vesting.whitelist_owned -= deposit_amount; Ok(()) } @@ -199,6 +210,7 @@ struct AccessControlRequest<'a, 'b> { program_id: &'a Pubkey, wl_acc_info: &'a AccountInfo<'b>, wl_prog_acc_info: &'a AccountInfo<'b>, + wl_prog_vault_acc_info: &'a AccountInfo<'b>, wl_prog_vault_authority_acc_info: &'a AccountInfo<'b>, beneficiary_acc_info: &'a AccountInfo<'b>, vesting_acc_info: &'a AccountInfo<'b>, @@ -208,16 +220,16 @@ struct AccessControlRequest<'a, 'b> { } struct StateTransitionRequest<'a, 'b, 'c> { - instruction_data: Vec, - vesting: &'c mut Vesting, + remaining_relay_accs: &'c mut dyn Iterator>, accounts: &'a [AccountInfo<'b>], - nonce: u8, - safe_acc: &'a Pubkey, + safe_acc_info: &'a AccountInfo<'b>, vault_acc_info: &'a AccountInfo<'b>, vault_auth_acc_info: &'a AccountInfo<'b>, wl_prog_acc_info: &'a AccountInfo<'b>, + wl_prog_vault_acc_info: &'a AccountInfo<'b>, wl_prog_vault_authority_acc_info: &'a AccountInfo<'b>, - remaining_relay_accs: Vec<&'a AccountInfo<'b>>, tok_prog_acc_info: &'a AccountInfo<'b>, beneficiary_acc_info: &'a AccountInfo<'b>, + instruction_data: &'c [u8], + vesting: &'c mut Vesting, } diff --git a/lockup/program/src/whitelist_withdraw.rs b/lockup/program/src/whitelist_withdraw.rs index 3737271..46bd21b 100644 --- a/lockup/program/src/whitelist_withdraw.rs +++ b/lockup/program/src/whitelist_withdraw.rs @@ -1,19 +1,20 @@ -use crate::access_control; +use crate::common::{access_control, whitelist_cpi}; use serum_common::pack::Pack; -use serum_lockup::accounts::{vault, Vesting}; +use serum_lockup::accounts::Vesting; use serum_lockup::error::{LockupError, LockupErrorCode}; use solana_program::info; use solana_sdk::account_info::{next_account_info, AccountInfo}; use solana_sdk::instruction::{AccountMeta, Instruction}; use solana_sdk::program_pack::Pack as TokenPack; use solana_sdk::pubkey::Pubkey; +use spl_token::state::Account as TokenAccount; use std::convert::Into; pub fn handler( program_id: &Pubkey, accounts: &[AccountInfo], amount: u64, - instruction_data: Vec, + ref instruction_data: Vec, ) -> Result<(), LockupError> { info!("handler: whitelist_withdraw"); @@ -24,7 +25,6 @@ pub fn handler( let safe_acc_info = next_account_info(acc_infos)?; let wl_acc_info = next_account_info(acc_infos)?; let wl_prog_acc_info = next_account_info(acc_infos)?; - let wl_prog_vault_authority_acc_info = next_account_info(acc_infos)?; // Below accounts are relayed. @@ -32,9 +32,11 @@ pub fn handler( let vault_acc_info = next_account_info(acc_infos)?; let vault_auth_acc_info = next_account_info(acc_infos)?; let tok_prog_acc_info = next_account_info(acc_infos)?; + let wl_prog_vault_acc_info = next_account_info(acc_infos)?; + let wl_prog_vault_authority_acc_info = next_account_info(acc_infos)?; // Program specific. - let remaining_relay_accs: Vec<&AccountInfo> = acc_infos.collect(); + let remaining_relay_accs = acc_infos; access_control(AccessControlRequest { program_id, @@ -42,6 +44,7 @@ pub fn handler( vesting_acc_info, wl_acc_info, wl_prog_acc_info, + wl_prog_vault_acc_info, wl_prog_vault_authority_acc_info, safe_acc_info, vault_acc_info, @@ -55,17 +58,17 @@ pub fn handler( state_transition(StateTransitionRequest { accounts, amount, - instruction_data: instruction_data.clone(), - safe_acc: safe_acc_info.key, - nonce: vesting.nonce, + instruction_data, + safe_acc_info, wl_prog_acc_info, + wl_prog_vault_acc_info, wl_prog_vault_authority_acc_info, vault_acc_info, vault_auth_acc_info, tok_prog_acc_info, vesting, beneficiary_acc_info, - remaining_relay_accs: remaining_relay_accs.clone(), + remaining_relay_accs, }) .map_err(Into::into) }, @@ -82,6 +85,7 @@ fn access_control(req: AccessControlRequest) -> Result<(), LockupError> { beneficiary_acc_info, vesting_acc_info, wl_acc_info, + wl_prog_vault_acc_info, wl_prog_acc_info, wl_prog_vault_authority_acc_info, safe_acc_info, @@ -99,7 +103,7 @@ fn access_control(req: AccessControlRequest) -> Result<(), LockupError> { let safe = access_control::safe(safe_acc_info, program_id)?; let whitelist = access_control::whitelist(wl_acc_info.clone(), safe_acc_info, &safe, program_id)?; - let _ = access_control::vault( + let _vault = access_control::vault( vault_acc_info, vault_auth_acc_info, vesting_acc_info, @@ -109,21 +113,29 @@ fn access_control(req: AccessControlRequest) -> Result<(), LockupError> { )?; let vesting = access_control::vesting( program_id, - safe_acc_info.key, + safe_acc_info, vesting_acc_info, beneficiary_acc_info, )?; // WhitelistWithdraw checks. - if amount > vesting.available_for_whitelist() { + // + // Do we have sufficient balance? + if amount > vesting.balance() { return Err(LockupErrorCode::InsufficientWhitelistBalance)?; } + // Is the given program on the whitelist? let entry = whitelist .get_derived(wl_prog_vault_authority_acc_info.key)? .ok_or(LockupErrorCode::WhitelistNotFound)?; if entry.program_id() != *wl_prog_acc_info.key { return Err(LockupErrorCode::WhitelistInvalidProgramId)?; } + // Is the vault owned by this whitelisted authority? + let wl_vault = access_control::token(wl_prog_vault_acc_info)?; + if &wl_vault.owner != wl_prog_vault_authority_acc_info.key { + return Err(LockupErrorCode::InvalidTokenAccountOwner)?; + } Ok(()) } @@ -137,68 +149,70 @@ fn state_transition(req: StateTransitionRequest) -> Result<(), LockupError> { beneficiary_acc_info, accounts, amount, - nonce, - safe_acc, + safe_acc_info, vault_acc_info, wl_prog_acc_info, - wl_prog_vault_authority_acc_info: _, + wl_prog_vault_acc_info, + wl_prog_vault_authority_acc_info, remaining_relay_accs, tok_prog_acc_info, vault_auth_acc_info, } = req; let before_amount = { - let vault = spl_token::state::Account::unpack(&vault_acc_info.try_borrow_data()?)?; + let vault = TokenAccount::unpack(&vault_acc_info.try_borrow_data()?)?; vault.amount }; // Invoke relay. { - let signer_seeds = vault::signer_seeds(safe_acc, beneficiary_acc_info.key, &nonce); let mut meta_accounts = vec![ AccountMeta::new(*vault_acc_info.key, false), AccountMeta::new_readonly(*vault_auth_acc_info.key, true), AccountMeta::new_readonly(*tok_prog_acc_info.key, false), + AccountMeta::new(*wl_prog_vault_acc_info.key, false), + AccountMeta::new_readonly(*wl_prog_vault_authority_acc_info.key, false), ]; - for a in remaining_relay_accs { + meta_accounts.extend(remaining_relay_accs.map(|a| { if a.is_writable { - meta_accounts.push(AccountMeta::new(*a.key, a.is_signer)); + AccountMeta::new(*a.key, a.is_signer) } else { - meta_accounts.push(AccountMeta::new_readonly(*a.key, a.is_signer)); + AccountMeta::new_readonly(*a.key, a.is_signer) } - } - let mut data = serum_lockup::instruction::TAG.to_le_bytes().to_vec(); - data.extend(instruction_data); + })); let relay_instruction = Instruction { program_id: *wl_prog_acc_info.key, accounts: meta_accounts, - data, + data: instruction_data.to_vec(), }; - - solana_sdk::program::invoke_signed(&relay_instruction, &accounts[..], &[&signer_seeds])?; + whitelist_cpi( + relay_instruction, + safe_acc_info.key, + beneficiary_acc_info, + vesting, + accounts, + )?; } - // Check the amount transferred is valid. If not abort. - let amount_transferred = { - let after_amount = { - let vault = spl_token::state::Account::unpack(&vault_acc_info.try_borrow_data()?)?; - vault.amount - }; - before_amount - after_amount + let after_amount = { + let vault = TokenAccount::unpack(&vault_acc_info.try_borrow_data()?)?; + vault.amount }; + // Withdrawal safety checks. + let amount_transferred = before_amount - after_amount; + // Is the amount transferred valid? if amount_transferred > amount { return Err(LockupErrorCode::InsufficientAmount)?; } - - // Update vesting account. + // Book keeping. vesting.whitelist_owned += amount_transferred; Ok(()) } struct AccessControlRequest<'a, 'b> { - program_id: &'a Pubkey, + wl_prog_vault_acc_info: &'a AccountInfo<'b>, beneficiary_acc_info: &'a AccountInfo<'b>, vesting_acc_info: &'a AccountInfo<'b>, safe_acc_info: &'a AccountInfo<'b>, @@ -207,21 +221,22 @@ struct AccessControlRequest<'a, 'b> { wl_acc_info: &'a AccountInfo<'b>, wl_prog_acc_info: &'a AccountInfo<'b>, wl_prog_vault_authority_acc_info: &'a AccountInfo<'b>, + program_id: &'a Pubkey, amount: u64, } struct StateTransitionRequest<'a, 'b, 'c> { - instruction_data: Vec, - vesting: &'c mut Vesting, + remaining_relay_accs: &'c mut dyn Iterator>, accounts: &'a [AccountInfo<'b>], - amount: u64, - nonce: u8, - safe_acc: &'a Pubkey, vault_acc_info: &'a AccountInfo<'b>, wl_prog_acc_info: &'a AccountInfo<'b>, + wl_prog_vault_acc_info: &'a AccountInfo<'b>, wl_prog_vault_authority_acc_info: &'a AccountInfo<'b>, - remaining_relay_accs: Vec<&'a AccountInfo<'b>>, tok_prog_acc_info: &'a AccountInfo<'b>, vault_auth_acc_info: &'a AccountInfo<'b>, beneficiary_acc_info: &'a AccountInfo<'b>, + safe_acc_info: &'a AccountInfo<'b>, + instruction_data: &'c [u8], + vesting: &'c mut Vesting, + amount: u64, } diff --git a/lockup/program/src/redeem.rs b/lockup/program/src/withdraw.rs similarity index 68% rename from lockup/program/src/redeem.rs rename to lockup/program/src/withdraw.rs index 22f8363..99b6473 100644 --- a/lockup/program/src/redeem.rs +++ b/lockup/program/src/withdraw.rs @@ -1,5 +1,6 @@ -use crate::access_control; +use crate::common::access_control; use serum_common::pack::Pack; +use serum_common::program::invoke_token_transfer; use serum_lockup::accounts::{vault, Vesting}; use serum_lockup::error::{LockupError, LockupErrorCode}; use solana_program::info; @@ -12,13 +13,13 @@ pub fn handler( accounts: &[AccountInfo], amount: u64, ) -> Result<(), LockupError> { - info!("handler: redeem"); + info!("handler: withdraw"); let acc_infos = &mut accounts.iter(); let beneficiary_acc_info = next_account_info(acc_infos)?; let vesting_acc_info = next_account_info(acc_infos)?; - let beneficiary_token_acc_info = next_account_info(acc_infos)?; + let token_acc_info = next_account_info(acc_infos)?; let vault_acc_info = next_account_info(acc_infos)?; let vault_authority_acc_info = next_account_info(acc_infos)?; let safe_acc_info = next_account_info(acc_infos)?; @@ -44,7 +45,7 @@ pub fn handler( vesting, vault_acc_info, vault_authority_acc_info, - beneficiary_token_acc_info, + token_acc_info, safe_acc_info, token_program_acc_info, beneficiary_acc_info, @@ -56,7 +57,7 @@ pub fn handler( } fn access_control(req: AccessControlRequest) -> Result<(), LockupError> { - info!("access-control: redeem"); + info!("access-control: withdraw"); let AccessControlRequest { program_id, @@ -69,20 +70,20 @@ fn access_control(req: AccessControlRequest) -> Result<(), LockupError> { clock_acc_info, } = req; - // Beneficiary authorization. + // Authorization. if !beneficiary_acc_info.is_signer { return Err(LockupErrorCode::Unauthorized)?; } // Account validation. - let _ = access_control::safe(safe_acc_info, program_id)?; + let _safe = access_control::safe(safe_acc_info, program_id)?; let vesting = access_control::vesting( program_id, - safe_acc_info.key, + safe_acc_info, vesting_acc_info, beneficiary_acc_info, )?; - let _ = access_control::vault( + let _vault = access_control::vault( vault_acc_info, vault_authority_acc_info, vesting_acc_info, @@ -90,62 +91,44 @@ fn access_control(req: AccessControlRequest) -> Result<(), LockupError> { safe_acc_info, program_id, )?; + let clock = access_control::clock(clock_acc_info)?; - // Redemption checks. - { - let clock = access_control::clock(clock_acc_info)?; - if amount == 0 || amount > vesting.available_for_withdrawal(clock.unix_timestamp) { - return Err(LockupErrorCode::InsufficientWithdrawalBalance)?; - } + // Withdrawal checks. + if amount == 0 || amount > vesting.available_for_withdrawal(clock.unix_timestamp) { + return Err(LockupErrorCode::InsufficientWithdrawalBalance)?; } Ok(()) } fn state_transition(req: StateTransitionRequest) -> Result<(), LockupError> { - info!("state-transition: redeem"); + info!("state-transition: withdraw"); let StateTransitionRequest { vesting, amount, vault_acc_info, vault_authority_acc_info, - beneficiary_token_acc_info, + token_acc_info, safe_acc_info, token_program_acc_info, beneficiary_acc_info, } = req; - // Remove the withdrawn token from the vesting account. - { - vesting.deduct(amount); - } - // Transfer token from the vault to the user address. - { - let withdraw_instruction = spl_token::instruction::transfer( - &spl_token::ID, - vault_acc_info.key, - beneficiary_token_acc_info.key, - &vault_authority_acc_info.key, - &[], - amount, - )?; + let signer_seeds = + vault::signer_seeds(safe_acc_info.key, beneficiary_acc_info.key, &vesting.nonce); + invoke_token_transfer( + vault_acc_info, + token_acc_info, + vault_authority_acc_info, + token_program_acc_info, + &[&signer_seeds], + amount, + )?; - let signer_seeds = - vault::signer_seeds(safe_acc_info.key, beneficiary_acc_info.key, &vesting.nonce); - - solana_sdk::program::invoke_signed( - &withdraw_instruction, - &[ - vault_acc_info.clone(), - beneficiary_token_acc_info.clone(), - vault_authority_acc_info.clone(), - token_program_acc_info.clone(), - ], - &[&signer_seeds], - )?; - } + // Update bookeeping. + vesting.outstanding -= amount; Ok(()) } @@ -165,7 +148,7 @@ struct StateTransitionRequest<'a, 'b, 'c> { amount: u64, vesting: &'c mut Vesting, safe_acc_info: &'a AccountInfo<'b>, - beneficiary_token_acc_info: &'a AccountInfo<'b>, + token_acc_info: &'a AccountInfo<'b>, vault_acc_info: &'a AccountInfo<'b>, vault_authority_acc_info: &'a AccountInfo<'b>, token_program_acc_info: &'a AccountInfo<'b>, diff --git a/lockup/src/accounts/vesting.rs b/lockup/src/accounts/vesting.rs index 4f675e3..4542145 100644 --- a/lockup/src/accounts/vesting.rs +++ b/lockup/src/accounts/vesting.rs @@ -18,15 +18,15 @@ pub struct Vesting { /// The owner of this Vesting account. If not set, then the account /// is allocated but needs to be assigned. pub beneficiary: Pubkey, - /// The mint of the SPL token the safe is storing, e.g., the SRM mint. + /// The mint of the SPL token locked up. pub mint: Pubkey, - /// Address of the token vault controlled by the Safe. + /// Address of the account's token vault. pub vault: Pubkey, /// The owner of the token account funding this account. pub grantor: Pubkey, /// The outstanding SRM deposit backing this vesting account. All - /// withdrawals/redemptions will deduct this balance. - pub balance: u64, + /// withdrawals will deduct this balance. + pub outstanding: u64, /// The starting balance of this vesting account, i.e., how much was /// originally deposited. pub start_balance: u64, @@ -40,38 +40,31 @@ pub struct Vesting { pub period_count: u64, /// The amount of tokens in custody of whitelisted programs. pub whitelist_owned: u64, - /// + /// Signer nonce. pub nonce: u8, } impl Vesting { - /// Deducts the given amount from the vesting account upon - /// withdrawal/redemption. - pub fn deduct(&mut self, amount: u64) { - self.balance -= amount; - } - - /// Returns the amount available for withdrawal as of the given ts. - /// The amount for withdrawal is not necessarily the balance vested - /// since funds can be sent to whitelisted programs. For this reason, - /// take minimum of the availble balance vested and the available balance - /// for sending to whitelisted programs. pub fn available_for_withdrawal(&self, current_ts: i64) -> u64 { - std::cmp::min( - self.balance_vested(current_ts), - self.available_for_whitelist(), - ) + std::cmp::min(self.outstanding_vested(current_ts), self.balance()) } - /// Amount available for whitelisted programs to transfer. - pub fn available_for_whitelist(&self) -> u64 { - self.balance - self.whitelist_owned + // The amount of funds left in the vault. + pub fn balance(&self) -> u64 { + self.outstanding.checked_sub(self.whitelist_owned).unwrap() } - // The amount vested that's available for withdrawal, if no funds were ever - // sent to another program. - fn balance_vested(&self, current_ts: i64) -> u64 { - self.total_vested(current_ts) - self.withdrawn_amount() + // The amount of outstanding locked tokens vested. Note that these + // tokens might have been transferred to whitelisted programs. + fn outstanding_vested(&self, current_ts: i64) -> u64 { + self.total_vested(current_ts) + .checked_sub(self.withdrawn_amount()) + .unwrap() + } + + // Returns the amount withdrawn from this vesting account. + fn withdrawn_amount(&self) -> u64 { + self.start_balance.checked_sub(self.outstanding).unwrap() } // Returns the total vested amount up to the given ts, assuming zero @@ -82,38 +75,49 @@ impl Vesting { if current_ts >= self.end_ts { return self.start_balance; } - self.linear_unlock(current_ts) + self.linear_unlock(current_ts).unwrap() } - // Returns the amount withdrawn from this vesting account. - fn withdrawn_amount(&self) -> u64 { - self.start_balance - self.balance - } + fn linear_unlock(&self, current_ts: i64) -> Option { + // LLVM doesn't support signed division. + let current_ts = current_ts as u64; + let start_ts = self.start_ts as u64; + let end_ts = self.end_ts as u64; - fn linear_unlock(&self, current_ts: i64) -> u64 { - let (end_ts, start_ts) = { - // If we can't perfectly partition the vesting window, - // push the start window back so that we can. - // - // This has the effect of making the first vesting period act as - // a minor "cliff" that vests slightly more than the rest of the - // periods. - let overflow = (self.end_ts - self.start_ts) as u64 % self.period_count; - if overflow != 0 { - (self.end_ts, self.start_ts - overflow as i64) - } else { - (self.end_ts, self.start_ts) - } - }; + // If we can't perfectly partition the vesting window, + // push the start of the window window back so that we can. + // + // This has the effect of making the first vesting period shorter + // than the rest. + let shifted_start_ts = + start_ts.checked_sub(end_ts.checked_sub(start_ts)? % self.period_count)?; - let vested_period_count = { - let period = (end_ts - start_ts) as u64 / self.period_count; - let current_period_count = (current_ts - start_ts) as u64 / period; + // Similarly, if we can't perfectly divide up the vesting rewards + // then make the first period act as a cliff, earning slightly more than + // subsequent periods. + let reward_overflow = self.start_balance % self.period_count; + + // Reward per period ignoring the overflow. + let reward_per_period = (self.start_balance.checked_sub(reward_overflow)?) + .checked_div(self.period_count) + .unwrap(); + + // Number of vesting periods that have passed. + let current_period = { + let period_secs = + (end_ts.checked_sub(shifted_start_ts)?).checked_div(self.period_count)?; + let current_period_count = + (current_ts.checked_sub(shifted_start_ts)?).checked_div(period_secs)?; std::cmp::min(current_period_count, self.period_count) }; - let reward_per_period = self.start_balance / self.period_count; - vested_period_count * reward_per_period + if current_period == 0 { + return Some(0); + } + + current_period + .checked_mul(reward_per_period)? + .checked_add(reward_overflow) } } @@ -132,20 +136,20 @@ mod tests { let beneficiary = Keypair::generate(&mut OsRng).pubkey(); let initialized = true; let start_balance = 10; - let balance = start_balance; + let outstanding = start_balance; let start_ts = 11; let end_ts = 12; let period_count = 13; let whitelist_owned = 14; - let grantor = Pubkey::new_rand(); - let mint = Pubkey::new_rand(); - let vault = Pubkey::new_rand(); + let grantor = Pubkey::new_unique(); + let mint = Pubkey::new_unique(); + let vault = Pubkey::new_unique(); let nonce = 0; let vesting_acc = Vesting { safe, beneficiary, initialized, - balance, + outstanding, start_balance, start_ts, end_ts, @@ -169,7 +173,7 @@ mod tests { assert_eq!(va.beneficiary, beneficiary); assert_eq!(va.initialized, initialized); assert_eq!(va.start_balance, start_balance); - assert_eq!(va.balance, balance); + assert_eq!(va.outstanding, outstanding); assert_eq!(va.start_ts, start_ts); assert_eq!(va.end_ts, end_ts); assert_eq!(va.period_count, period_count); @@ -181,23 +185,23 @@ mod tests { fn available_for_withdrawal() { let safe = Keypair::generate(&mut OsRng).pubkey(); let beneficiary = Keypair::generate(&mut OsRng).pubkey(); - let balance = 10; + let outstanding = 10; let start_balance = 10; let start_ts = 10; let end_ts = 20; let period_count = 5; let initialized = true; let whitelist_owned = 0; - let grantor = Pubkey::new_rand(); - let mint = Pubkey::new_rand(); - let vault = Pubkey::new_rand(); + let grantor = Pubkey::new_unique(); + let mint = Pubkey::new_unique(); + let vault = Pubkey::new_unique(); let nonce = 0; let vesting_acc = Vesting { safe, beneficiary, initialized, whitelist_owned, - balance, + outstanding, start_balance, start_ts, end_ts, @@ -217,6 +221,51 @@ mod tests { assert_eq!(10, vesting_acc.available_for_withdrawal(100)); } + #[test] + fn available_for_withdrawal_cliff() { + let safe = Keypair::generate(&mut OsRng).pubkey(); + let beneficiary = Keypair::generate(&mut OsRng).pubkey(); + let outstanding = 11; + let start_balance = 11; + let start_ts = 10; + let end_ts = 20; + let period_count = 10; + let initialized = true; + let whitelist_owned = 0; + let grantor = Pubkey::new_unique(); + let mint = Pubkey::new_unique(); + let vault = Pubkey::new_unique(); + let nonce = 0; + let vesting_acc = Vesting { + safe, + beneficiary, + initialized, + whitelist_owned, + outstanding, + start_balance, + start_ts, + end_ts, + period_count, + grantor, + mint, + nonce, + vault, + }; + assert_eq!(0, vesting_acc.available_for_withdrawal(10)); + assert_eq!(2, vesting_acc.available_for_withdrawal(11)); + assert_eq!(3, vesting_acc.available_for_withdrawal(12)); + assert_eq!(4, vesting_acc.available_for_withdrawal(13)); + assert_eq!(5, vesting_acc.available_for_withdrawal(14)); + assert_eq!(6, vesting_acc.available_for_withdrawal(15)); + assert_eq!(7, vesting_acc.available_for_withdrawal(16)); + assert_eq!(8, vesting_acc.available_for_withdrawal(17)); + assert_eq!(9, vesting_acc.available_for_withdrawal(18)); + assert_eq!(10, vesting_acc.available_for_withdrawal(19)); + assert_eq!(11, vesting_acc.available_for_withdrawal(20)); + assert_eq!(11, vesting_acc.available_for_withdrawal(21)); + assert_eq!(11, vesting_acc.available_for_withdrawal(2100)); + } + #[test] fn unpack_zeroes() { let og_size = Vesting::default().size().unwrap(); @@ -226,7 +275,7 @@ mod tests { assert_eq!(r.initialized, false); assert_eq!(r.safe, Pubkey::new(&[0; 32])); assert_eq!(r.beneficiary, Pubkey::new(&[0; 32])); - assert_eq!(r.balance, 0); + assert_eq!(r.outstanding, 0); assert_eq!(r.start_ts, 0); assert_eq!(r.end_ts, 0); assert_eq!(r.period_count, 0); diff --git a/lockup/src/lib.rs b/lockup/src/lib.rs index 6b98760..1aeb947 100644 --- a/lockup/src/lib.rs +++ b/lockup/src/lib.rs @@ -59,8 +59,7 @@ pub mod instruction { /// 5 `[]` Safe. /// 8. `[]` SPL token program. /// 9. `[]` Clock sysvar. - // todo: rename - Redeem { amount: u64 }, + Withdraw { amount: u64 }, /// Accounts: /// /// 0. `[signer]` Beneficiary. diff --git a/lockup/tests/lifecycle.rs b/lockup/tests/lifecycle.rs index 73bd60a..af769c2 100644 --- a/lockup/tests/lifecycle.rs +++ b/lockup/tests/lifecycle.rs @@ -116,7 +116,7 @@ fn lifecycle() { pass_time(client.rpc(), wait_ts); } - // Redeem 10 SRM. + // Withdraw 10 SRM. // // Current state: // @@ -134,22 +134,22 @@ fn lifecycle() { ) .unwrap(); - let redeem_amount = 10; + let withdraw_amount = 10; let _ = client - .redeem(RedeemRequest { + .withdraw(WithdrawRequest { beneficiary: &expected_beneficiary, vesting, token_account: bene_tok_acc.pubkey(), safe: safe_acc, - amount: redeem_amount, + amount: withdraw_amount, }) .unwrap(); // The SRM account should be increased. let bene_tok = rpc::account_token_unpacked::(client.rpc(), &bene_tok_acc.pubkey()); - assert_eq!(bene_tok.amount, redeem_amount); + assert_eq!(bene_tok.amount, withdraw_amount); } } diff --git a/registry/client/src/lib.rs b/registry/client/src/lib.rs index 7ee1f19..28a461b 100644 --- a/registry/client/src/lib.rs +++ b/registry/client/src/lib.rs @@ -273,18 +273,20 @@ impl Client { pool_program_id, } = req; let vault = self.vault_for(®istrar, &depositor)?; + let vault_acc = rpc::get_token_account::(self.inner.rpc(), &vault)?; let mut accounts = vec![ // Whitelist relay interface, AccountMeta::new(depositor, false), AccountMeta::new(depositor_authority.pubkey(), true), AccountMeta::new_readonly(spl_token::ID, false), + AccountMeta::new(vault, false), + AccountMeta::new(vault_acc.owner, false), // Program specific. AccountMeta::new(member, false), AccountMeta::new_readonly(beneficiary.pubkey(), true), AccountMeta::new(entity, false), AccountMeta::new_readonly(registrar, false), AccountMeta::new_readonly(solana_sdk::sysvar::clock::ID, false), - AccountMeta::new(vault, false), ]; let (pool_accs, _) = self.common_pool_accounts(pool_program_id, registrar, false)?; accounts.extend_from_slice(&pool_accs); @@ -307,14 +309,15 @@ impl Client { amount, pool_program_id, } = req; - let r = self.registrar(®istrar)?; - let vault_acc = rpc::get_token_account::(self.inner.rpc(), &r.vault)?; let vault = self.vault_for(®istrar, &depositor)?; + let r = self.registrar(®istrar)?; + let vault_acc = rpc::get_token_account::(self.inner.rpc(), &vault)?; let mut accounts = vec![ // Whitelist relay interface. AccountMeta::new(depositor, false), AccountMeta::new_readonly(beneficiary.pubkey(), true), AccountMeta::new_readonly(spl_token::ID, false), + AccountMeta::new(vault, false), AccountMeta::new(vault_acc.owner, false), // Program specific. AccountMeta::new(member, false), @@ -322,7 +325,6 @@ impl Client { AccountMeta::new(entity, false), AccountMeta::new_readonly(registrar, false), AccountMeta::new_readonly(solana_sdk::sysvar::clock::ID, false), - AccountMeta::new(vault, false), ]; let is_mega = vault == r.mega_vault; // TODO: remove is_mega. let (pool_accs, _) = self.common_pool_accounts(pool_program_id, registrar, is_mega)?; diff --git a/registry/program/src/entity.rs b/registry/program/src/common/entity.rs similarity index 100% rename from registry/program/src/entity.rs rename to registry/program/src/common/entity.rs diff --git a/registry/program/src/common/mod.rs b/registry/program/src/common/mod.rs new file mode 100644 index 0000000..8397b67 --- /dev/null +++ b/registry/program/src/common/mod.rs @@ -0,0 +1,2 @@ +pub mod entity; +pub mod pool; diff --git a/registry/program/src/pool.rs b/registry/program/src/common/pool.rs similarity index 100% rename from registry/program/src/pool.rs rename to registry/program/src/common/pool.rs diff --git a/registry/program/src/deposit.rs b/registry/program/src/deposit.rs index d65e419..5da4c17 100644 --- a/registry/program/src/deposit.rs +++ b/registry/program/src/deposit.rs @@ -1,7 +1,7 @@ -use crate::common::invoke_token_transfer; -use crate::entity::{with_entity, EntityContext}; -use crate::pool::{pool_check, Pool, PoolConfig}; +use crate::common::entity::{with_entity, EntityContext}; +use crate::common::pool::{pool_check, Pool, PoolConfig}; use serum_common::pack::Pack; +use serum_common::program::invoke_token_transfer; use serum_registry::access_control; use serum_registry::accounts::{Entity, Member, Registrar}; use serum_registry::error::{RegistryError, RegistryErrorCode}; @@ -26,6 +26,8 @@ pub fn handler( let depositor_acc_info = next_account_info(acc_infos)?; let depositor_authority_acc_info = next_account_info(acc_infos)?; let token_program_acc_info = next_account_info(acc_infos)?; + let vault_acc_info = next_account_info(acc_infos)?; + let vault_authority_acc_info = next_account_info(acc_infos)?; // Program specfic. let member_acc_info = next_account_info(acc_infos)?; @@ -33,7 +35,6 @@ pub fn handler( let entity_acc_info = next_account_info(acc_infos)?; let registrar_acc_info = next_account_info(acc_infos)?; let clock_acc_info = next_account_info(acc_infos)?; - let vault_acc_info = next_account_info(acc_infos)?; let pool = &Pool::parse_accounts(acc_infos, PoolConfig::GetBasket)?; @@ -54,6 +55,7 @@ pub fn handler( beneficiary_acc_info, entity_acc_info, vault_acc_info, + vault_authority_acc_info, program_id, registrar_acc_info, registrar, @@ -94,6 +96,7 @@ fn access_control(req: AccessControlRequest) -> Result Result { entity_acc_info: &'a AccountInfo<'b>, vault_acc_info: &'a AccountInfo<'b>, registrar_acc_info: &'a AccountInfo<'b>, + vault_authority_acc_info: &'a AccountInfo<'b>, program_id: &'a Pubkey, pool: &'c Pool<'a, 'b>, registrar: &'c Registrar, diff --git a/registry/program/src/drop_locked_reward.rs b/registry/program/src/drop_locked_reward.rs index 9582df5..317ce74 100644 --- a/registry/program/src/drop_locked_reward.rs +++ b/registry/program/src/drop_locked_reward.rs @@ -1,6 +1,6 @@ -use crate::common::invoke_token_transfer; use borsh::BorshDeserialize; use serum_common::pack::Pack; +use serum_common::program::invoke_token_transfer; use serum_pool_schema::PoolState; use serum_registry::access_control; use serum_registry::accounts::reward_queue::Ring; diff --git a/registry/program/src/drop_pool_reward.rs b/registry/program/src/drop_pool_reward.rs index 4b9c278..1e53fb5 100644 --- a/registry/program/src/drop_pool_reward.rs +++ b/registry/program/src/drop_pool_reward.rs @@ -1,4 +1,4 @@ -use crate::common::invoke_token_transfer; +use serum_common::program::invoke_token_transfer; use serum_registry::accounts::reward_queue::Ring; use serum_registry::accounts::{RewardEvent, RewardEventQueue}; use serum_registry::error::{RegistryError, RegistryErrorCode}; diff --git a/registry/program/src/lib.rs b/registry/program/src/lib.rs index d15883f..cb67278 100644 --- a/registry/program/src/lib.rs +++ b/registry/program/src/lib.rs @@ -15,10 +15,8 @@ mod deposit; mod drop_locked_reward; mod drop_pool_reward; mod end_stake_withdrawal; -mod entity; mod initialize; mod mark_generation; -mod pool; mod slash; mod stake; mod start_stake_withdrawal; diff --git a/registry/program/src/mark_generation.rs b/registry/program/src/mark_generation.rs index 293ae18..2ddefd7 100644 --- a/registry/program/src/mark_generation.rs +++ b/registry/program/src/mark_generation.rs @@ -1,4 +1,4 @@ -use crate::pool::{pool_check_get_basket, Pool, PoolConfig}; +use crate::common::pool::{pool_check_get_basket, Pool, PoolConfig}; use serum_common::pack::Pack; use serum_registry::access_control; use serum_registry::accounts::{Entity, Generation}; diff --git a/registry/program/src/slash.rs b/registry/program/src/slash.rs index 7833b91..cfa4bb8 100644 --- a/registry/program/src/slash.rs +++ b/registry/program/src/slash.rs @@ -1,7 +1,7 @@ -use crate::common::invoke_token_transfer; -use crate::entity::{with_entity, EntityContext}; -use crate::pool::{pool_check, Pool, PoolConfig}; +use crate::common::entity::{with_entity, EntityContext}; +use crate::common::pool::{pool_check, Pool, PoolConfig}; use serum_common::pack::Pack; +use serum_common::program::invoke_token_transfer; use serum_registry::access_control; use serum_registry::accounts::{vault, Entity, Member, Registrar}; use serum_registry::error::RegistryError; @@ -121,7 +121,7 @@ fn access_control(req: AccessControlRequest) -> Result<(), RegistryError> { // Account validation. let _entity = access_control::entity(entity_acc_info, registrar_acc_info, program_id)?; let member = access_control::member_raw(member_acc_info, entity_acc_info, program_id)?; - let _vault = access_control::vault_join( + let _vault = access_control::vault_authenticated( vault_acc_info, vault_authority_acc_info, registrar_acc_info, @@ -129,7 +129,7 @@ fn access_control(req: AccessControlRequest) -> Result<(), RegistryError> { program_id, )?; if let Some(mega_vault_acc_info) = mega_vault_acc_info { - let _mega_vault = access_control::vault_join( + let _mega_vault = access_control::vault_authenticated( mega_vault_acc_info, vault_authority_acc_info, registrar_acc_info, diff --git a/registry/program/src/stake.rs b/registry/program/src/stake.rs index d70a827..ed82940 100644 --- a/registry/program/src/stake.rs +++ b/registry/program/src/stake.rs @@ -1,5 +1,5 @@ -use crate::entity::{with_entity, EntityContext}; -use crate::pool::{pool_check_create, Pool, PoolConfig}; +use crate::common::entity::{with_entity, EntityContext}; +use crate::common::pool::{pool_check_create, Pool, PoolConfig}; use serum_common::pack::Pack; use serum_registry::access_control; use serum_registry::accounts::{Entity, Member, Registrar}; diff --git a/registry/program/src/start_stake_withdrawal.rs b/registry/program/src/start_stake_withdrawal.rs index bf4e576..2ce55e3 100644 --- a/registry/program/src/start_stake_withdrawal.rs +++ b/registry/program/src/start_stake_withdrawal.rs @@ -1,7 +1,7 @@ -use crate::common::invoke_token_transfer; -use crate::entity::{with_entity, EntityContext}; -use crate::pool::{pool_check, Pool, PoolConfig}; +use crate::common::entity::{with_entity, EntityContext}; +use crate::common::pool::{pool_check, Pool, PoolConfig}; use serum_common::pack::Pack; +use serum_common::program::invoke_token_transfer; use serum_registry::access_control; use serum_registry::accounts::entity::{EntityState, PoolPrices}; use serum_registry::accounts::pending_withdrawal::PendingPayment; @@ -158,7 +158,7 @@ fn access_control(req: AccessControlRequest) -> Result Result Result Result