anchor/examples/lockup/programs/lockup/src/lib.rs

533 lines
18 KiB
Rust

//! 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<WhitelistEntry>,
}
impl Lockup {
pub const WHITELIST_SIZE: usize = 10;
pub fn new(ctx: Context<Auth>) -> Result<Self> {
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<Auth>, 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<Auth>,
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<Auth>, new_authority: Pubkey) -> Result<()> {
self.authority = new_authority;
Ok(())
}
}
#[access_control(CreateVesting::accounts(&ctx, nonce))]
pub fn create_vesting(
ctx: Context<CreateVesting>,
beneficiary: Pubkey,
deposit_amount: u64,
nonce: u8,
start_ts: i64,
end_ts: i64,
period_count: u64,
realizor: Option<Realizor>,
) -> 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<Withdraw>, 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<u8>,
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<u8>,
) -> 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<AvailableForWithdrawal>) -> 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<CreateVesting>, 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<Realizor>,
}
#[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<u8>,
) -> 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<Auth>) -> 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<Withdraw>) -> 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<T>, v: Vesting) -> ProgramResult;
}