//! A relatively advanced example of a lockup program. If you're new to Anchor, //! it's suggested to start with the other examples. use anchor_lang::prelude::*; use anchor_lang::solana_program::instruction::Instruction; use anchor_lang::solana_program::program; use anchor_spl::token::{self, TokenAccount, Transfer}; mod calculator; #[program] pub mod lockup { use super::*; #[state] pub struct Lockup { /// The key with the ability to change the whitelist. pub authority: Pubkey, /// List of programs locked tokens can be sent to. These programs /// are completely trusted to maintain the locked property. pub whitelist: Vec, } impl Lockup { pub const WHITELIST_SIZE: usize = 10; pub fn new(ctx: Context) -> Result { let mut whitelist = vec![]; whitelist.resize(Self::WHITELIST_SIZE, Default::default()); Ok(Lockup { authority: *ctx.accounts.authority.key, whitelist, }) } #[access_control(whitelist_auth(self, &ctx))] pub fn whitelist_add(&mut self, ctx: Context, entry: WhitelistEntry) -> Result<()> { if self.whitelist.len() == Self::WHITELIST_SIZE { return Err(ErrorCode::WhitelistFull.into()); } if self.whitelist.contains(&entry) { return Err(ErrorCode::WhitelistEntryAlreadyExists.into()); } self.whitelist.push(entry); Ok(()) } #[access_control(whitelist_auth(self, &ctx))] pub fn whitelist_delete( &mut self, ctx: Context, entry: WhitelistEntry, ) -> Result<()> { if !self.whitelist.contains(&entry) { return Err(ErrorCode::WhitelistEntryNotFound.into()); } self.whitelist.retain(|e| e != &entry); Ok(()) } #[access_control(whitelist_auth(self, &ctx))] pub fn set_authority(&mut self, ctx: Context, new_authority: Pubkey) -> Result<()> { self.authority = new_authority; Ok(()) } } #[access_control(CreateVesting::accounts(&ctx, nonce))] pub fn create_vesting( ctx: Context, beneficiary: Pubkey, deposit_amount: u64, nonce: u8, start_ts: i64, end_ts: i64, period_count: u64, realizor: Option, ) -> Result<()> { if deposit_amount == 0 { return Err(ErrorCode::InvalidDepositAmount.into()); } if !is_valid_schedule(start_ts, end_ts, period_count) { return Err(ErrorCode::InvalidSchedule.into()); } let vesting = &mut ctx.accounts.vesting; vesting.beneficiary = beneficiary; vesting.mint = ctx.accounts.vault.mint; vesting.vault = *ctx.accounts.vault.to_account_info().key; vesting.period_count = period_count; vesting.start_balance = deposit_amount; vesting.end_ts = end_ts; vesting.start_ts = start_ts; vesting.created_ts = ctx.accounts.clock.unix_timestamp; vesting.outstanding = deposit_amount; vesting.whitelist_owned = 0; vesting.grantor = *ctx.accounts.depositor_authority.key; vesting.nonce = nonce; vesting.realizor = realizor; token::transfer(ctx.accounts.into(), deposit_amount)?; Ok(()) } #[access_control(is_realized(&ctx))] pub fn withdraw(ctx: Context, amount: u64) -> Result<()> { // Has the given amount vested? if amount > calculator::available_for_withdrawal( &ctx.accounts.vesting, ctx.accounts.clock.unix_timestamp, ) { return Err(ErrorCode::InsufficientWithdrawalBalance.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; Ok(()) } // Sends funds from the lockup program to a whitelisted program. pub fn whitelist_withdraw<'a, 'b, 'c, 'info>( ctx: Context<'a, 'b, 'c, 'info, WhitelistWithdraw<'info>>, instruction_data: Vec, amount: u64, ) -> Result<()> { 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(()) } // Sends funds from a whitelisted program back to the lockup program. pub fn whitelist_deposit<'a, 'b, 'c, 'info>( ctx: Context<'a, 'b, 'c, 'info, WhitelistDeposit<'info>>, instruction_data: Vec, ) -> Result<()> { 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 > ctx.accounts.transfer.vesting.whitelist_owned { return Err(ErrorCode::WhitelistDepositOverflow)?; } // Bookkeeping. ctx.accounts.transfer.vesting.whitelist_owned -= deposit_amount; Ok(()) } // Convenience function for UI's to calculate the withdrawable amount. pub fn available_for_withdrawal(ctx: Context) -> Result<()> { let available = calculator::available_for_withdrawal( &ctx.accounts.vesting, ctx.accounts.clock.unix_timestamp, ); // Log as string so that JS can read as a BN. msg!(&format!("{{ \"result\": \"{}\" }}", available)); Ok(()) } } #[derive(Accounts)] pub struct Auth<'info> { #[account(signer)] authority: AccountInfo<'info>, } #[derive(Accounts)] pub struct CreateVesting<'info> { // Vesting. #[account(init)] vesting: ProgramAccount<'info, Vesting>, #[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>, } impl<'info> CreateVesting<'info> { fn accounts(ctx: &Context, nonce: u8) -> Result<()> { 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(()) } } // All accounts not included here, i.e., the "remaining accounts" should be // ordered according to the realization interface. #[derive(Accounts)] pub struct Withdraw<'info> { // Vesting. #[account(mut, has_one = beneficiary, has_one = vault)] vesting: ProgramAccount<'info, Vesting>, #[account(signer)] beneficiary: AccountInfo<'info>, #[account(mut)] vault: CpiAccount<'info, TokenAccount>, #[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 WhitelistWithdraw<'info> { transfer: WhitelistTransfer<'info>, } #[derive(Accounts)] pub struct WhitelistDeposit<'info> { transfer: WhitelistTransfer<'info>, } #[derive(Accounts)] pub struct WhitelistTransfer<'info> { lockup: ProgramState<'info, Lockup>, #[account(signer)] beneficiary: AccountInfo<'info>, whitelisted_program: AccountInfo<'info>, // Whitelist interface. #[account(mut, has_one = beneficiary, has_one = vault)] vesting: ProgramAccount<'info, Vesting>, #[account(mut, "&vault.owner == vesting_signer.key")] vault: CpiAccount<'info, TokenAccount>, #[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>, } #[derive(Accounts)] pub struct AvailableForWithdrawal<'info> { vesting: ProgramAccount<'info, Vesting>, clock: Sysvar<'info, Clock>, } #[account] pub struct Vesting { /// The owner of this Vesting account. pub beneficiary: Pubkey, /// The mint of the SPL token locked up. pub mint: Pubkey, /// 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 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, /// The unix timestamp at which this vesting account was created. pub created_ts: i64, /// The time at which vesting begins. pub start_ts: i64, /// The time at which all tokens are vested. pub end_ts: i64, /// The number of times vesting will occur. For example, if vesting /// is once a year over seven years, this will be 7. pub period_count: u64, /// The amount of tokens in custody of whitelisted programs. pub whitelist_owned: u64, /// Signer nonce. pub nonce: u8, /// The program that determines when the locked account is **realized**. /// In addition to the lockup schedule, the program provides the ability /// for applications to determine when locked tokens are considered earned. /// For example, when earning locked tokens via the staking program, one /// cannot receive the tokens until unstaking. As a result, if one never /// unstakes, one would never actually receive the locked tokens. pub realizor: Option, } #[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] pub struct Realizor { /// Program to invoke to check a realization condition. This program must /// implement the `RealizeLock` trait. pub program: Pubkey, /// Address of an arbitrary piece of metadata interpretable by the realizor /// program. For example, when a vesting account is allocated, the program /// can define its realization condition as a function of some account /// state. The metadata is the address of that account. /// /// In the case of staking, the metadata is a `Member` account address. When /// the realization condition is checked, the staking program will check the /// `Member` account defined by the `metadata` has no staked tokens. pub metadata: Pubkey, } #[derive(AnchorSerialize, AnchorDeserialize, PartialEq, Default, Copy, Clone)] pub struct WhitelistEntry { pub program_id: Pubkey, } #[error] pub enum ErrorCode { #[msg("Vesting end must be greater than the current unix timestamp.")] InvalidTimestamp, #[msg("The number of vesting periods must be greater than zero.")] InvalidPeriod, #[msg("The vesting deposit amount must be greater than zero.")] InvalidDepositAmount, #[msg("The Whitelist entry is not a valid program address.")] InvalidWhitelistEntry, #[msg("Invalid program address. Did you provide the correct nonce?")] InvalidProgramAddress, #[msg("Invalid vault owner.")] InvalidVaultOwner, #[msg("Vault amount must be zero.")] InvalidVaultAmount, #[msg("Insufficient withdrawal balance.")] InsufficientWithdrawalBalance, #[msg("Whitelist is full")] WhitelistFull, #[msg("Whitelist entry already exists")] WhitelistEntryAlreadyExists, #[msg("Balance must go up when performing a whitelist deposit")] InsufficientWhitelistDepositAmount, #[msg("Cannot deposit more than withdrawn")] WhitelistDepositOverflow, #[msg("Tried to withdraw over the specified limit")] WhitelistWithdrawLimit, #[msg("Whitelist entry not found.")] WhitelistEntryNotFound, #[msg("You do not have sufficient permissions to perform this action.")] Unauthorized, #[msg("You are unable to realize projected rewards until unstaking.")] UnableToWithdrawWhileStaked, #[msg("The given lock realizor doesn't match the vesting account.")] InvalidLockRealizor, #[msg("You have not realized this vesting account.")] UnrealizedVesting, #[msg("Invalid vesting schedule given.")] InvalidSchedule, } impl<'a, 'b, 'c, 'info> From<&mut CreateVesting<'info>> for CpiContext<'a, 'b, 'c, 'info, Transfer<'info>> { fn from(accounts: &mut CreateVesting<'info>) -> CpiContext<'a, 'b, 'c, 'info, Transfer<'info>> { let cpi_accounts = Transfer { from: accounts.depositor.clone(), to: accounts.vault.to_account_info(), authority: accounts.depositor_authority.clone(), }; let cpi_program = accounts.token_program.clone(); CpiContext::new(cpi_program, cpi_accounts) } } 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.vesting_signer.to_account_info(), }; let cpi_program = accounts.token_program.to_account_info(); CpiContext::new(cpi_program, cpi_accounts) } } #[access_control(is_whitelisted(transfer))] pub fn whitelist_relay_cpi<'info>( transfer: &WhitelistTransfer<'info>, remaining_accounts: &[AccountInfo<'info>], instruction_data: Vec, ) -> Result<()> { 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[..]]; let mut accounts = transfer.to_account_infos(); accounts.extend_from_slice(&remaining_accounts); program::invoke_signed(&relay_instruction, &accounts, signer).map_err(Into::into) } pub fn is_whitelisted<'info>(transfer: &WhitelistTransfer<'info>) -> Result<()> { if !transfer.lockup.whitelist.contains(&WhitelistEntry { program_id: *transfer.whitelisted_program.key, }) { return Err(ErrorCode::WhitelistEntryNotFound.into()); } Ok(()) } fn whitelist_auth(lockup: &Lockup, ctx: &Context) -> Result<()> { if &lockup.authority != ctx.accounts.authority.key { return Err(ErrorCode::Unauthorized.into()); } Ok(()) } pub fn is_valid_schedule(start_ts: i64, end_ts: i64, period_count: u64) -> bool { if end_ts <= start_ts { return false; } if period_count > (end_ts - start_ts) as u64 { return false; } if period_count == 0 { return false; } true } // Returns Ok if the locked vesting account has been "realized". Realization // is application dependent. For example, in the case of staking, one must first // unstake before being able to earn locked tokens. fn is_realized(ctx: &Context) -> Result<()> { if let Some(realizor) = &ctx.accounts.vesting.realizor { let cpi_program = { let p = ctx.remaining_accounts[0].clone(); if p.key != &realizor.program { return Err(ErrorCode::InvalidLockRealizor.into()); } p }; let cpi_accounts = ctx.remaining_accounts.to_vec()[1..].to_vec(); let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts); let vesting = (*ctx.accounts.vesting).clone(); realize_lock::is_realized(cpi_ctx, vesting).map_err(|_| ErrorCode::UnrealizedVesting)?; } Ok(()) } /// RealizeLock defines the interface an external program must implement if /// they want to define a "realization condition" on a locked vesting account. /// This condition must be satisfied *even if a vesting schedule has /// completed*. Otherwise the user can never earn the locked funds. For example, /// in the case of the staking program, one cannot received a locked reward /// until one has completely unstaked. #[interface] pub trait RealizeLock<'info, T: Accounts<'info>> { fn is_realized(ctx: Context, v: Vesting) -> ProgramResult; }