//! A relatively advanced example of a staking program. If you're new to Anchor, //! it's suggested to start with the other examples. #![feature(proc_macro_hygiene)] use anchor_lang::prelude::*; use anchor_lang::solana_program::program_option::COption; use anchor_spl::token::{self, Mint, TokenAccount, Transfer}; use lockup::{CreateVesting, RealizeLock, Realizor, Vesting}; use std::convert::Into; #[program] mod registry { use super::*; #[state] pub struct Registry { pub lockup_program: Pubkey, } impl Registry { pub fn new(ctx: Context) -> Result { Ok(Registry { lockup_program: *ctx.accounts.lockup_program.key, }) } } impl<'info> RealizeLock<'info, IsRealized<'info>> for Registry { fn is_realized(ctx: Context, v: Vesting) -> ProgramResult { if let Some(realizor) = &v.realizor { if &realizor.metadata != ctx.accounts.member.to_account_info().key { return Err(ErrorCode::InvalidRealizorMetadata.into()); } assert!(ctx.accounts.member.beneficiary == v.beneficiary); let total_staked = ctx.accounts.member_spt.amount + ctx.accounts.member_spt_locked.amount; if total_staked != 0 { return Err(ErrorCode::UnrealizedReward.into()); } } Ok(()) } } #[access_control(Initialize::accounts(&ctx, nonce))] pub fn initialize( ctx: Context, mint: Pubkey, authority: Pubkey, nonce: u8, withdrawal_timelock: i64, stake_rate: u64, reward_q_len: u32, ) -> Result<()> { let registrar = &mut ctx.accounts.registrar; registrar.authority = authority; registrar.nonce = nonce; registrar.mint = mint; registrar.pool_mint = *ctx.accounts.pool_mint.to_account_info().key; registrar.stake_rate = stake_rate; registrar.reward_event_q = *ctx.accounts.reward_event_q.to_account_info().key; registrar.withdrawal_timelock = withdrawal_timelock; let reward_q = &mut ctx.accounts.reward_event_q; reward_q .events .resize(reward_q_len as usize, Default::default()); Ok(()) } pub fn update_registrar( ctx: Context, new_authority: Option, withdrawal_timelock: Option, ) -> Result<()> { let registrar = &mut ctx.accounts.registrar; if let Some(new_authority) = new_authority { registrar.authority = new_authority; } if let Some(withdrawal_timelock) = withdrawal_timelock { registrar.withdrawal_timelock = withdrawal_timelock; } Ok(()) } #[access_control(CreateMember::accounts(&ctx, nonce))] pub fn create_member(ctx: Context, nonce: u8) -> Result<()> { let member = &mut ctx.accounts.member; member.registrar = *ctx.accounts.registrar.to_account_info().key; member.beneficiary = *ctx.accounts.beneficiary.key; member.balances = (&ctx.accounts.balances).into(); member.balances_locked = (&ctx.accounts.balances_locked).into(); member.nonce = nonce; Ok(()) } pub fn update_member(ctx: Context, metadata: Option) -> Result<()> { let member = &mut ctx.accounts.member; if let Some(m) = metadata { member.metadata = m; } Ok(()) } // Deposits that can only come directly from the member beneficiary. pub fn deposit(ctx: Context, amount: u64) -> Result<()> { token::transfer(ctx.accounts.into(), amount).map_err(Into::into) } // Deposits that can only come from the beneficiary's vesting accounts. pub fn deposit_locked(ctx: Context, amount: u64) -> Result<()> { token::transfer(ctx.accounts.into(), amount).map_err(Into::into) } #[access_control(no_available_rewards( &ctx.accounts.reward_event_q, &ctx.accounts.member, &ctx.accounts.balances, &ctx.accounts.balances_locked, ))] pub fn stake(ctx: Context, spt_amount: u64, locked: bool) -> Result<()> { let balances = { if locked { &ctx.accounts.balances_locked } else { &ctx.accounts.balances } }; // Transfer tokens into the stake vault. { let seeds = &[ ctx.accounts.registrar.to_account_info().key.as_ref(), ctx.accounts.member.to_account_info().key.as_ref(), &[ctx.accounts.member.nonce], ]; let member_signer = &[&seeds[..]]; let cpi_ctx = CpiContext::new_with_signer( ctx.accounts.token_program.clone(), token::Transfer { from: balances.vault.to_account_info(), to: balances.vault_stake.to_account_info(), authority: ctx.accounts.member_signer.to_account_info(), }, member_signer, ); // Convert from stake-token units to mint-token units. let token_amount = spt_amount .checked_mul(ctx.accounts.registrar.stake_rate) .unwrap(); token::transfer(cpi_ctx, token_amount)?; } // Mint pool tokens to the staker. { let seeds = &[ ctx.accounts.registrar.to_account_info().key.as_ref(), &[ctx.accounts.registrar.nonce], ]; let registrar_signer = &[&seeds[..]]; let cpi_ctx = CpiContext::new_with_signer( ctx.accounts.token_program.clone(), token::MintTo { mint: ctx.accounts.pool_mint.to_account_info(), to: balances.spt.to_account_info(), authority: ctx.accounts.registrar_signer.to_account_info(), }, registrar_signer, ); token::mint_to(cpi_ctx, spt_amount)?; } // Update stake timestamp. let member = &mut ctx.accounts.member; member.last_stake_ts = ctx.accounts.clock.unix_timestamp; Ok(()) } #[access_control(no_available_rewards( &ctx.accounts.reward_event_q, &ctx.accounts.member, &ctx.accounts.balances, &ctx.accounts.balances_locked, ))] pub fn start_unstake(ctx: Context, spt_amount: u64, locked: bool) -> Result<()> { let balances = { if locked { &ctx.accounts.balances_locked } else { &ctx.accounts.balances } }; // Program signer. let seeds = &[ ctx.accounts.registrar.to_account_info().key.as_ref(), ctx.accounts.member.to_account_info().key.as_ref(), &[ctx.accounts.member.nonce], ]; let member_signer = &[&seeds[..]]; // Burn pool tokens. { let cpi_ctx = CpiContext::new_with_signer( ctx.accounts.token_program.clone(), token::Burn { mint: ctx.accounts.pool_mint.to_account_info(), to: balances.spt.to_account_info(), authority: ctx.accounts.member_signer.to_account_info(), }, member_signer, ); token::burn(cpi_ctx, spt_amount)?; } // Convert from stake-token units to mint-token units. let token_amount = spt_amount .checked_mul(ctx.accounts.registrar.stake_rate) .unwrap(); // Transfer tokens from the stake to pending vault. { let cpi_ctx = CpiContext::new_with_signer( ctx.accounts.token_program.clone(), token::Transfer { from: balances.vault_stake.to_account_info(), to: balances.vault_pw.to_account_info(), authority: ctx.accounts.member_signer.to_account_info(), }, member_signer, ); token::transfer(cpi_ctx, token_amount)?; } // Print receipt. let pending_withdrawal = &mut ctx.accounts.pending_withdrawal; pending_withdrawal.burned = false; pending_withdrawal.member = *ctx.accounts.member.to_account_info().key; pending_withdrawal.start_ts = ctx.accounts.clock.unix_timestamp; pending_withdrawal.end_ts = ctx.accounts.clock.unix_timestamp + ctx.accounts.registrar.withdrawal_timelock; pending_withdrawal.amount = token_amount; pending_withdrawal.pool = ctx.accounts.registrar.pool_mint; pending_withdrawal.registrar = *ctx.accounts.registrar.to_account_info().key; pending_withdrawal.locked = locked; // Update stake timestamp. let member = &mut ctx.accounts.member; member.last_stake_ts = ctx.accounts.clock.unix_timestamp; Ok(()) } pub fn end_unstake(ctx: Context) -> Result<()> { if ctx.accounts.pending_withdrawal.end_ts > ctx.accounts.clock.unix_timestamp { return Err(ErrorCode::UnstakeTimelock.into()); } // Select which balance set this affects. let balances = { if ctx.accounts.pending_withdrawal.locked { &ctx.accounts.member.balances_locked } else { &ctx.accounts.member.balances } }; // Check the vaults given are corrrect. if &balances.vault != ctx.accounts.vault.key { return Err(ErrorCode::InvalidVault.into()); } if &balances.vault_pw != ctx.accounts.vault_pw.key { return Err(ErrorCode::InvalidVault.into()); } // Transfer tokens between vaults. { let seeds = &[ ctx.accounts.registrar.to_account_info().key.as_ref(), ctx.accounts.member.to_account_info().key.as_ref(), &[ctx.accounts.member.nonce], ]; let signer = &[&seeds[..]]; let cpi_ctx = CpiContext::new_with_signer( ctx.accounts.token_program.clone(), Transfer { from: ctx.accounts.vault_pw.to_account_info(), to: ctx.accounts.vault.to_account_info(), authority: ctx.accounts.member_signer.clone(), }, signer, ); token::transfer(cpi_ctx, ctx.accounts.pending_withdrawal.amount)?; } // Burn the pending withdrawal receipt. let pending_withdrawal = &mut ctx.accounts.pending_withdrawal; pending_withdrawal.burned = true; Ok(()) } pub fn withdraw(ctx: Context, amount: u64) -> Result<()> { let seeds = &[ ctx.accounts.registrar.to_account_info().key.as_ref(), ctx.accounts.member.to_account_info().key.as_ref(), &[ctx.accounts.member.nonce], ]; let signer = &[&seeds[..]]; let cpi_accounts = Transfer { from: ctx.accounts.vault.to_account_info(), to: ctx.accounts.depositor.to_account_info(), authority: ctx.accounts.member_signer.clone(), }; let cpi_program = ctx.accounts.token_program.clone(); let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, signer); token::transfer(cpi_ctx, amount).map_err(Into::into) } pub fn withdraw_locked(ctx: Context, amount: u64) -> Result<()> { let seeds = &[ ctx.accounts.registrar.to_account_info().key.as_ref(), ctx.accounts.member.to_account_info().key.as_ref(), &[ctx.accounts.member.nonce], ]; let signer = &[&seeds[..]]; let cpi_accounts = Transfer { from: ctx.accounts.member_vault.to_account_info(), to: ctx.accounts.vesting_vault.to_account_info(), authority: ctx.accounts.member_signer.clone(), }; let cpi_program = ctx.accounts.token_program.clone(); let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, signer); token::transfer(cpi_ctx, amount).map_err(Into::into) } #[access_control(DropReward::accounts(&ctx, nonce))] pub fn drop_reward( ctx: Context, kind: RewardVendorKind, total: u64, expiry_ts: i64, expiry_receiver: Pubkey, nonce: u8, ) -> Result<()> { if total < ctx.accounts.pool_mint.supply { return Err(ErrorCode::InsufficientReward.into()); } if ctx.accounts.clock.unix_timestamp >= expiry_ts { return Err(ErrorCode::InvalidExpiry.into()); } if let RewardVendorKind::Locked { start_ts, end_ts, period_count, } = kind { if !lockup::is_valid_schedule(start_ts, end_ts, period_count) { return Err(ErrorCode::InvalidVestingSchedule.into()); } } // Transfer funds into the vendor's vault. token::transfer(ctx.accounts.into(), total)?; // Add the event to the reward queue. let reward_q = &mut ctx.accounts.reward_event_q; let cursor = reward_q.append(RewardEvent { vendor: *ctx.accounts.vendor.to_account_info().key, ts: ctx.accounts.clock.unix_timestamp, locked: kind != RewardVendorKind::Unlocked, })?; // Initialize the vendor. let vendor = &mut ctx.accounts.vendor; vendor.registrar = *ctx.accounts.registrar.to_account_info().key; vendor.vault = *ctx.accounts.vendor_vault.to_account_info().key; vendor.mint = ctx.accounts.vendor_vault.mint; vendor.nonce = nonce; vendor.pool_token_supply = ctx.accounts.pool_mint.supply; vendor.reward_event_q_cursor = cursor; vendor.start_ts = ctx.accounts.clock.unix_timestamp; vendor.expiry_ts = expiry_ts; vendor.expiry_receiver = expiry_receiver; vendor.from = *ctx.accounts.depositor_authority.key; vendor.total = total; vendor.expired = false; vendor.kind = kind; Ok(()) } #[access_control(reward_eligible(&ctx.accounts.cmn))] pub fn claim_reward(ctx: Context) -> Result<()> { if RewardVendorKind::Unlocked != ctx.accounts.cmn.vendor.kind { return Err(ErrorCode::ExpectedUnlockedVendor.into()); } // Reward distribution. let spt_total = ctx.accounts.cmn.balances.spt.amount + ctx.accounts.cmn.balances_locked.spt.amount; let reward_amount = spt_total .checked_mul(ctx.accounts.cmn.vendor.total) .unwrap() .checked_div(ctx.accounts.cmn.vendor.pool_token_supply) .unwrap(); assert!(reward_amount > 0); // Send reward to the given token account. let seeds = &[ ctx.accounts.cmn.registrar.to_account_info().key.as_ref(), ctx.accounts.cmn.vendor.to_account_info().key.as_ref(), &[ctx.accounts.cmn.vendor.nonce], ]; let signer = &[&seeds[..]]; let cpi_ctx = CpiContext::new_with_signer( ctx.accounts.cmn.token_program.clone(), token::Transfer { from: ctx.accounts.cmn.vault.to_account_info(), to: ctx.accounts.to.to_account_info(), authority: ctx.accounts.cmn.vendor_signer.to_account_info(), }, signer, ); token::transfer(cpi_ctx, reward_amount)?; // Update member as having processed the reward. let member = &mut ctx.accounts.cmn.member; member.rewards_cursor = ctx.accounts.cmn.vendor.reward_event_q_cursor + 1; Ok(()) } #[access_control(reward_eligible(&ctx.accounts.cmn))] pub fn claim_reward_locked<'a, 'b, 'c, 'info>( ctx: Context<'a, 'b, 'c, 'info, ClaimRewardLocked<'info>>, nonce: u8, ) -> Result<()> { let (start_ts, end_ts, period_count) = match ctx.accounts.cmn.vendor.kind { RewardVendorKind::Unlocked => return Err(ErrorCode::ExpectedLockedVendor.into()), RewardVendorKind::Locked { start_ts, end_ts, period_count, } => (start_ts, end_ts, period_count), }; // Reward distribution. let spt_total = ctx.accounts.cmn.balances.spt.amount + ctx.accounts.cmn.balances_locked.spt.amount; let reward_amount = spt_total .checked_mul(ctx.accounts.cmn.vendor.total) .unwrap() .checked_div(ctx.accounts.cmn.vendor.pool_token_supply) .unwrap(); assert!(reward_amount > 0); // Specify the vesting account's realizor, so that unlocks can only // execute once completely unstaked. let realizor = Some(Realizor { program: *ctx.program_id, metadata: *ctx.accounts.cmn.member.to_account_info().key, }); // CPI: Create lockup account for the member's beneficiary. let seeds = &[ ctx.accounts.cmn.registrar.to_account_info().key.as_ref(), ctx.accounts.cmn.vendor.to_account_info().key.as_ref(), &[ctx.accounts.cmn.vendor.nonce], ]; let signer = &[&seeds[..]]; let mut remaining_accounts: &[AccountInfo] = ctx.remaining_accounts; let cpi_program = ctx.accounts.lockup_program.clone(); let cpi_accounts = CreateVesting::try_accounts(ctx.accounts.lockup_program.key, &mut remaining_accounts)?; let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, signer); lockup::cpi::create_vesting( cpi_ctx, ctx.accounts.cmn.member.beneficiary, reward_amount, nonce, start_ts, end_ts, period_count, realizor, )?; // Make sure this reward can't be processed more than once. let member = &mut ctx.accounts.cmn.member; member.rewards_cursor = ctx.accounts.cmn.vendor.reward_event_q_cursor + 1; Ok(()) } pub fn expire_reward(ctx: Context) -> Result<()> { if ctx.accounts.clock.unix_timestamp < ctx.accounts.vendor.expiry_ts { return Err(ErrorCode::VendorNotYetExpired.into()); } // Send all remaining funds to the expiry receiver's token. let seeds = &[ ctx.accounts.registrar.to_account_info().key.as_ref(), ctx.accounts.vendor.to_account_info().key.as_ref(), &[ctx.accounts.vendor.nonce], ]; let signer = &[&seeds[..]]; let cpi_ctx = CpiContext::new_with_signer( ctx.accounts.token_program.clone(), token::Transfer { to: ctx.accounts.expiry_receiver_token.to_account_info(), from: ctx.accounts.vault.to_account_info(), authority: ctx.accounts.vendor_signer.to_account_info(), }, signer, ); token::transfer(cpi_ctx, ctx.accounts.vault.amount)?; // Burn the vendor. let vendor = &mut ctx.accounts.vendor; vendor.expired = true; Ok(()) } } #[derive(Accounts)] pub struct Initialize<'info> { #[account(init)] registrar: ProgramAccount<'info, Registrar>, #[account(init)] reward_event_q: ProgramAccount<'info, RewardQueue>, #[account("pool_mint.decimals == 0")] pool_mint: CpiAccount<'info, Mint>, rent: Sysvar<'info, Rent>, } impl<'info> Initialize<'info> { fn accounts(ctx: &Context>, nonce: u8) -> Result<()> { let registrar_signer = Pubkey::create_program_address( &[ ctx.accounts.registrar.to_account_info().key.as_ref(), &[nonce], ], ctx.program_id, ) .map_err(|_| ErrorCode::InvalidNonce)?; if ctx.accounts.pool_mint.mint_authority != COption::Some(registrar_signer) { return Err(ErrorCode::InvalidPoolMintAuthority.into()); } assert!(ctx.accounts.pool_mint.supply == 0); Ok(()) } } #[derive(Accounts)] pub struct UpdateRegistrar<'info> { #[account(mut, has_one = authority)] registrar: ProgramAccount<'info, Registrar>, #[account(signer)] authority: AccountInfo<'info>, } #[derive(Accounts)] pub struct CreateMember<'info> { // Stake instance. registrar: ProgramAccount<'info, Registrar>, // Member. #[account(init)] member: ProgramAccount<'info, Member>, #[account(signer)] beneficiary: AccountInfo<'info>, #[account( "&balances.spt.owner == member_signer.key", "balances.spt.mint == registrar.pool_mint", "balances.vault.mint == registrar.mint" )] balances: BalanceSandboxAccounts<'info>, #[account( "&balances_locked.spt.owner == member_signer.key", "balances_locked.spt.mint == registrar.pool_mint", "balances_locked.vault.mint == registrar.mint" )] balances_locked: BalanceSandboxAccounts<'info>, member_signer: AccountInfo<'info>, // Misc. #[account("token_program.key == &token::ID")] token_program: AccountInfo<'info>, rent: Sysvar<'info, Rent>, } impl<'info> CreateMember<'info> { fn accounts(ctx: &Context, nonce: u8) -> Result<()> { let seeds = &[ ctx.accounts.registrar.to_account_info().key.as_ref(), ctx.accounts.member.to_account_info().key.as_ref(), &[nonce], ]; let member_signer = Pubkey::create_program_address(seeds, ctx.program_id) .map_err(|_| ErrorCode::InvalidNonce)?; if &member_signer != ctx.accounts.member_signer.to_account_info().key { return Err(ErrorCode::InvalidMemberSigner.into()); } Ok(()) } } // When creating a member, the mints and owners of these accounts are correct. // Upon creation, we assign the accounts. A onetime operation. // When using a member, we check these accounts addresess are equal to the // addresses stored on the member. If so, the correct accounts were given are // correct. #[derive(Accounts, Clone)] pub struct BalanceSandboxAccounts<'info> { #[account(mut)] spt: CpiAccount<'info, TokenAccount>, #[account(mut, "vault.owner == spt.owner")] vault: CpiAccount<'info, TokenAccount>, #[account( mut, "vault_stake.owner == spt.owner", "vault_stake.mint == vault.mint" )] vault_stake: CpiAccount<'info, TokenAccount>, #[account(mut, "vault_pw.owner == spt.owner", "vault_pw.mint == vault.mint")] vault_pw: CpiAccount<'info, TokenAccount>, } #[derive(Accounts)] pub struct Ctor<'info> { lockup_program: AccountInfo<'info>, } #[derive(Accounts)] pub struct IsRealized<'info> { #[account( "&member.balances.spt == member_spt.to_account_info().key", "&member.balances_locked.spt == member_spt_locked.to_account_info().key" )] member: ProgramAccount<'info, Member>, member_spt: CpiAccount<'info, TokenAccount>, member_spt_locked: CpiAccount<'info, TokenAccount>, } #[derive(Accounts)] pub struct UpdateMember<'info> { #[account(mut, has_one = beneficiary)] member: ProgramAccount<'info, Member>, #[account(signer)] beneficiary: AccountInfo<'info>, } #[derive(Accounts)] pub struct Deposit<'info> { // Member. #[account(has_one = beneficiary)] member: ProgramAccount<'info, Member>, #[account(signer)] beneficiary: AccountInfo<'info>, #[account(mut, "vault.to_account_info().key == &member.balances.vault")] vault: CpiAccount<'info, TokenAccount>, // Depositor. #[account(mut)] depositor: AccountInfo<'info>, #[account(signer, "depositor_authority.key == &member.beneficiary")] depositor_authority: AccountInfo<'info>, // Misc. #[account("token_program.key == &token::ID")] token_program: AccountInfo<'info>, } #[derive(Accounts)] pub struct DepositLocked<'info> { // Lockup whitelist relay interface. #[account( "vesting.to_account_info().owner == ®istry.lockup_program", "vesting.beneficiary == member.beneficiary" )] vesting: CpiAccount<'info, Vesting>, #[account(mut, "vesting_vault.key == &vesting.vault")] vesting_vault: AccountInfo<'info>, // Note: no need to verify the depositor_authority since the SPL program // will fail the transaction if it's not correct. #[account(signer)] depositor_authority: AccountInfo<'info>, #[account("token_program.key == &token::ID")] token_program: AccountInfo<'info>, #[account( mut, "member_vault.to_account_info().key == &member.balances_locked.vault" )] member_vault: CpiAccount<'info, TokenAccount>, #[account( seeds = [ registrar.to_account_info().key.as_ref(), member.to_account_info().key.as_ref(), &[member.nonce], ] )] member_signer: AccountInfo<'info>, // Program specific. registry: ProgramState<'info, Registry>, registrar: ProgramAccount<'info, Registrar>, #[account(belongs_to = registrar, has_one = beneficiary)] member: ProgramAccount<'info, Member>, #[account(signer)] beneficiary: AccountInfo<'info>, } #[derive(Accounts)] pub struct Stake<'info> { // Global accounts for the staking instance. #[account(has_one = pool_mint, has_one = reward_event_q)] registrar: ProgramAccount<'info, Registrar>, reward_event_q: ProgramAccount<'info, RewardQueue>, #[account(mut)] pool_mint: CpiAccount<'info, Mint>, // Member. #[account(mut, has_one = beneficiary, belongs_to = registrar)] member: ProgramAccount<'info, Member>, #[account(signer)] beneficiary: AccountInfo<'info>, #[account("BalanceSandbox::from(&balances) == member.balances")] balances: BalanceSandboxAccounts<'info>, #[account("BalanceSandbox::from(&balances_locked) == member.balances_locked")] balances_locked: BalanceSandboxAccounts<'info>, // Program signers. #[account( seeds = [ registrar.to_account_info().key.as_ref(), member.to_account_info().key.as_ref(), &[member.nonce], ] )] member_signer: AccountInfo<'info>, #[account(seeds = [registrar.to_account_info().key.as_ref(), &[registrar.nonce]])] registrar_signer: AccountInfo<'info>, // Misc. clock: Sysvar<'info, Clock>, #[account("token_program.key == &token::ID")] token_program: AccountInfo<'info>, } #[derive(Accounts)] pub struct StartUnstake<'info> { // Stake instance globals. registrar: ProgramAccount<'info, Registrar>, reward_event_q: ProgramAccount<'info, RewardQueue>, #[account(mut)] pool_mint: AccountInfo<'info>, // Member. #[account(init)] pending_withdrawal: ProgramAccount<'info, PendingWithdrawal>, #[account(belongs_to = registrar)] member: ProgramAccount<'info, Member>, #[account(signer)] beneficiary: AccountInfo<'info>, #[account("BalanceSandbox::from(&balances) == member.balances")] balances: BalanceSandboxAccounts<'info>, #[account("BalanceSandbox::from(&balances_locked) == member.balances_locked")] balances_locked: BalanceSandboxAccounts<'info>, // Programmatic signers. #[account( seeds = [ registrar.to_account_info().key.as_ref(), member.to_account_info().key.as_ref(), &[member.nonce], ] )] member_signer: AccountInfo<'info>, // Misc. #[account("token_program.key == &token::ID")] token_program: AccountInfo<'info>, clock: Sysvar<'info, Clock>, rent: Sysvar<'info, Rent>, } #[derive(Accounts)] pub struct EndUnstake<'info> { registrar: ProgramAccount<'info, Registrar>, #[account(belongs_to = registrar, has_one = beneficiary)] member: ProgramAccount<'info, Member>, #[account(signer)] beneficiary: AccountInfo<'info>, #[account(mut, belongs_to = registrar, belongs_to = member, "!pending_withdrawal.burned")] pending_withdrawal: ProgramAccount<'info, PendingWithdrawal>, // if we had ordered maps implementing Accounts we could do a constraint like // balances.get(pending_withdrawal.balance_id).vault == vault.key #[account(mut)] vault: AccountInfo<'info>, #[account(mut)] vault_pw: AccountInfo<'info>, #[account( seeds = [ registrar.to_account_info().key.as_ref(), member.to_account_info().key.as_ref(), &[member.nonce], ] )] member_signer: AccountInfo<'info>, clock: Sysvar<'info, Clock>, #[account("token_program.key == &token::ID")] token_program: AccountInfo<'info>, } #[derive(Accounts)] pub struct Withdraw<'info> { // Stake instance. registrar: ProgramAccount<'info, Registrar>, // Member. #[account(belongs_to = registrar, has_one = beneficiary)] member: ProgramAccount<'info, Member>, #[account(signer)] beneficiary: AccountInfo<'info>, #[account(mut, "vault.to_account_info().key == &member.balances.vault")] vault: CpiAccount<'info, TokenAccount>, #[account( seeds = [ registrar.to_account_info().key.as_ref(), member.to_account_info().key.as_ref(), &[member.nonce], ] )] member_signer: AccountInfo<'info>, // Receiver. #[account(mut)] depositor: AccountInfo<'info>, // Misc. #[account("token_program.key == &token::ID")] token_program: AccountInfo<'info>, } #[derive(Accounts)] pub struct WithdrawLocked<'info> { // Lockup whitelist relay interface. #[account( "vesting.to_account_info().owner == ®istry.lockup_program", "vesting.beneficiary == member.beneficiary" )] vesting: CpiAccount<'info, Vesting>, #[account(mut, "vesting_vault.key == &vesting.vault")] vesting_vault: AccountInfo<'info>, #[account(signer)] vesting_signer: AccountInfo<'info>, #[account("token_program.key == &token::ID")] token_program: AccountInfo<'info>, #[account( mut, "member_vault.to_account_info().key == &member.balances_locked.vault" )] member_vault: CpiAccount<'info, TokenAccount>, #[account( seeds = [ registrar.to_account_info().key.as_ref(), member.to_account_info().key.as_ref(), &[member.nonce], ] )] member_signer: AccountInfo<'info>, // Program specific. registry: ProgramState<'info, Registry>, registrar: ProgramAccount<'info, Registrar>, #[account(belongs_to = registrar, has_one = beneficiary)] member: ProgramAccount<'info, Member>, #[account(signer)] beneficiary: AccountInfo<'info>, } #[derive(Accounts)] pub struct DropReward<'info> { // Staking instance. #[account(has_one = reward_event_q, has_one = pool_mint)] registrar: ProgramAccount<'info, Registrar>, #[account(mut)] reward_event_q: ProgramAccount<'info, RewardQueue>, pool_mint: CpiAccount<'info, Mint>, // Vendor. #[account(init)] vendor: ProgramAccount<'info, RewardVendor>, #[account(mut)] vendor_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>, clock: Sysvar<'info, Clock>, rent: Sysvar<'info, Rent>, } impl<'info> DropReward<'info> { fn accounts(ctx: &Context, nonce: u8) -> Result<()> { let vendor_signer = Pubkey::create_program_address( &[ ctx.accounts.registrar.to_account_info().key.as_ref(), ctx.accounts.vendor.to_account_info().key.as_ref(), &[nonce], ], ctx.program_id, ) .map_err(|_| ErrorCode::InvalidNonce)?; if vendor_signer != ctx.accounts.vendor_vault.owner { return Err(ErrorCode::InvalidVaultOwner.into()); } Ok(()) } } #[derive(Accounts)] pub struct ClaimReward<'info> { cmn: ClaimRewardCommon<'info>, // Account to send reward to. #[account(mut)] to: AccountInfo<'info>, } #[derive(Accounts)] pub struct ClaimRewardLocked<'info> { cmn: ClaimRewardCommon<'info>, registry: ProgramState<'info, Registry>, #[account("lockup_program.key == ®istry.lockup_program")] lockup_program: AccountInfo<'info>, } // Accounts common to both claim reward locked/unlocked instructions. #[derive(Accounts)] pub struct ClaimRewardCommon<'info> { // Stake instance. registrar: ProgramAccount<'info, Registrar>, // Member. #[account(mut, belongs_to = registrar, has_one = beneficiary)] member: ProgramAccount<'info, Member>, #[account(signer)] beneficiary: AccountInfo<'info>, #[account("BalanceSandbox::from(&balances) == member.balances")] balances: BalanceSandboxAccounts<'info>, #[account("BalanceSandbox::from(&balances_locked) == member.balances_locked")] balances_locked: BalanceSandboxAccounts<'info>, // Vendor. #[account(belongs_to = registrar, has_one = vault)] vendor: ProgramAccount<'info, RewardVendor>, #[account(mut)] vault: AccountInfo<'info>, #[account( seeds = [ registrar.to_account_info().key.as_ref(), vendor.to_account_info().key.as_ref(), &[vendor.nonce], ] )] vendor_signer: AccountInfo<'info>, // Misc. #[account("token_program.key == &token::ID")] token_program: AccountInfo<'info>, clock: Sysvar<'info, Clock>, } #[derive(Accounts)] pub struct ExpireReward<'info> { // Staking instance globals. registrar: ProgramAccount<'info, Registrar>, // Vendor. #[account(mut, belongs_to = registrar, has_one = vault, has_one = expiry_receiver)] vendor: ProgramAccount<'info, RewardVendor>, #[account(mut)] vault: CpiAccount<'info, TokenAccount>, #[account( seeds = [ registrar.to_account_info().key.as_ref(), vendor.to_account_info().key.as_ref(), &[vendor.nonce], ] )] vendor_signer: AccountInfo<'info>, // Receiver. #[account(signer)] expiry_receiver: AccountInfo<'info>, #[account(mut)] expiry_receiver_token: AccountInfo<'info>, // Misc. #[account("token_program.key == &token::ID")] token_program: AccountInfo<'info>, clock: Sysvar<'info, Clock>, } #[account] pub struct Registrar { /// Priviledged account. pub authority: Pubkey, /// Nonce to derive the program-derived address owning the vaults. pub nonce: u8, /// Number of seconds that must pass for a withdrawal to complete. pub withdrawal_timelock: i64, /// Global event queue for reward vendoring. pub reward_event_q: Pubkey, /// Mint of the tokens that can be staked. pub mint: Pubkey, /// Staking pool token mint. pub pool_mint: Pubkey, /// The amount of tokens (not decimal) that must be staked to get a single /// staking pool token. pub stake_rate: u64, } #[account] pub struct Member { /// Registrar the member belongs to. pub registrar: Pubkey, /// The effective owner of the Member account. pub beneficiary: Pubkey, /// Arbitrary metadata account owned by any program. pub metadata: Pubkey, /// Sets of balances owned by the Member. pub balances: BalanceSandbox, /// Locked balances owned by the Member. pub balances_locked: BalanceSandbox, /// Next position in the rewards event queue to process. pub rewards_cursor: u32, /// The clock timestamp of the last time this account staked or switched /// entities. Used as a proof to reward vendors that the Member account /// was staked at a given point in time. pub last_stake_ts: i64, /// Signer nonce. pub nonce: u8, } // BalanceSandbox defines isolated funds that can only be deposited/withdrawn // into the program. // // Once controlled by the program, the associated `Member` account's beneficiary // can send funds to/from any of the accounts within the sandbox, e.g., to // stake. #[derive(AnchorSerialize, AnchorDeserialize, Default, Debug, Clone, PartialEq)] pub struct BalanceSandbox { // Staking pool token. pub spt: Pubkey, // Free balance (deposit) vaults. pub vault: Pubkey, // Stake vaults. pub vault_stake: Pubkey, // Pending withdrawal vaults. pub vault_pw: Pubkey, } #[account] pub struct PendingWithdrawal { /// Registrar this account belongs to. pub registrar: Pubkey, /// Member this account belongs to. pub member: Pubkey, /// One time token. True if the withdrawal has been completed. pub burned: bool, /// The pool being withdrawn from. pub pool: Pubkey, /// Unix timestamp when this account was initialized. pub start_ts: i64, /// Timestamp when the pending withdrawal completes. pub end_ts: i64, /// The number of tokens redeemed from the staking pool. pub amount: u64, /// True if the withdrawal applies to locked balances. pub locked: bool, } #[account] pub struct RewardQueue { // Invariant: index is position of the next available slot. head: u32, // Invariant: index is position of the first (oldest) taken slot. // Invariant: head == tail => queue is initialized. // Invariant: index_of(head + 1) == index_of(tail) => queue is full. tail: u32, // Although a vec is used, the size is immutable. events: Vec, } impl RewardQueue { pub fn append(&mut self, event: RewardEvent) -> Result { let cursor = self.head; // Insert into next available slot. let h_idx = self.index_of(self.head); self.events[h_idx] = event; // Update head and tail counters. let is_full = self.index_of(self.head + 1) == self.index_of(self.tail); if is_full { self.tail += 1; } self.head += 1; Ok(cursor) } pub fn index_of(&self, counter: u32) -> usize { counter as usize % self.capacity() } pub fn capacity(&self) -> usize { self.events.len() } pub fn get(&self, cursor: u32) -> &RewardEvent { &self.events[cursor as usize % self.capacity()] } pub fn head(&self) -> u32 { self.head } pub fn tail(&self) -> u32 { self.tail } } #[derive(Default, Clone, Copy, Debug, AnchorSerialize, AnchorDeserialize)] pub struct RewardEvent { vendor: Pubkey, ts: i64, locked: bool, } #[account] pub struct RewardVendor { pub registrar: Pubkey, pub vault: Pubkey, pub mint: Pubkey, pub nonce: u8, pub pool_token_supply: u64, pub reward_event_q_cursor: u32, pub start_ts: i64, pub expiry_ts: i64, pub expiry_receiver: Pubkey, pub from: Pubkey, pub total: u64, pub expired: bool, pub kind: RewardVendorKind, } #[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq)] pub enum RewardVendorKind { Unlocked, Locked { start_ts: i64, end_ts: i64, period_count: u64, }, } #[error] pub enum ErrorCode { #[msg("The given reward queue has already been initialized.")] RewardQAlreadyInitialized, #[msg("The nonce given doesn't derive a valid program address.")] InvalidNonce, #[msg("Invalid pool mint authority")] InvalidPoolMintAuthority, #[msg("Member signer doesn't match the derived address.")] InvalidMemberSigner, #[msg("The given vault owner must match the signing depositor.")] InvalidVaultDeposit, #[msg("The signing depositor doesn't match either of the balance accounts")] InvalidDepositor, #[msg("The vault given does not match the vault expected.")] InvalidVault, #[msg("Invalid vault owner.")] InvalidVaultOwner, #[msg("An unknown error has occured.")] Unknown, #[msg("The unstake timelock has not yet expired.")] UnstakeTimelock, #[msg("Reward vendors must have at least one token unit per pool token")] InsufficientReward, #[msg("Reward expiry must be after the current clock timestamp.")] InvalidExpiry, #[msg("The reward vendor has been expired.")] VendorExpired, #[msg("This reward has already been processed.")] CursorAlreadyProcessed, #[msg("The account was not staked at the time of this reward.")] NotStakedDuringDrop, #[msg("The vendor is not yet eligible for expiry.")] VendorNotYetExpired, #[msg("Please collect your reward before otherwise using the program.")] RewardsNeedsProcessing, #[msg("Locked reward vendor expected but an unlocked vendor was given.")] ExpectedLockedVendor, #[msg("Unlocked reward vendor expected but a locked vendor was given.")] ExpectedUnlockedVendor, #[msg("Locked deposit from an invalid deposit authority.")] InvalidVestingSigner, #[msg("Locked rewards cannot be realized until one unstaked all tokens.")] UnrealizedReward, #[msg("The beneficiary doesn't match.")] InvalidBeneficiary, #[msg("The given member account does not match the realizor metadata.")] InvalidRealizorMetadata, #[msg("Invalid vesting schedule for the locked reward.")] InvalidVestingSchedule, } impl<'a, 'b, 'c, 'info> From<&mut Deposit<'info>> for CpiContext<'a, 'b, 'c, 'info, Transfer<'info>> { fn from(accounts: &mut Deposit<'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<&mut DepositLocked<'info>> for CpiContext<'a, 'b, 'c, 'info, Transfer<'info>> { fn from(accounts: &mut DepositLocked<'info>) -> CpiContext<'a, 'b, 'c, 'info, Transfer<'info>> { let cpi_accounts = Transfer { from: accounts.vesting_vault.clone(), to: accounts.member_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<&mut DropReward<'info>> for CpiContext<'a, 'b, 'c, 'info, Transfer<'info>> { fn from(accounts: &mut DropReward<'info>) -> CpiContext<'a, 'b, 'c, 'info, Transfer<'info>> { let cpi_accounts = Transfer { from: accounts.depositor.clone(), to: accounts.vendor_vault.to_account_info(), authority: accounts.depositor_authority.clone(), }; let cpi_program = accounts.token_program.clone(); CpiContext::new(cpi_program, cpi_accounts) } } impl<'info> From<&BalanceSandboxAccounts<'info>> for BalanceSandbox { fn from(accs: &BalanceSandboxAccounts<'info>) -> Self { Self { spt: *accs.spt.to_account_info().key, vault: *accs.vault.to_account_info().key, vault_stake: *accs.vault_stake.to_account_info().key, vault_pw: *accs.vault_pw.to_account_info().key, } } } fn reward_eligible(cmn: &ClaimRewardCommon) -> Result<()> { let vendor = &cmn.vendor; let member = &cmn.member; if vendor.expired { return Err(ErrorCode::VendorExpired.into()); } if member.rewards_cursor > vendor.reward_event_q_cursor { return Err(ErrorCode::CursorAlreadyProcessed.into()); } if member.last_stake_ts > vendor.start_ts { return Err(ErrorCode::NotStakedDuringDrop.into()); } Ok(()) } // Asserts the user calling the `Stake` instruction has no rewards available // in the reward queue. pub fn no_available_rewards<'info>( reward_q: &ProgramAccount<'info, RewardQueue>, member: &ProgramAccount<'info, Member>, balances: &BalanceSandboxAccounts<'info>, balances_locked: &BalanceSandboxAccounts<'info>, ) -> Result<()> { let mut cursor = member.rewards_cursor; // If the member's cursor is less then the tail, then the ring buffer has // overwritten those entries, so jump to the tail. let tail = reward_q.tail(); if cursor < tail { cursor = tail; } while cursor < reward_q.head() { let r_event = reward_q.get(cursor); if member.last_stake_ts < r_event.ts { if balances.spt.amount > 0 || balances_locked.spt.amount > 0 { return Err(ErrorCode::RewardsNeedsProcessing.into()); } } cursor += 1; } Ok(()) }