Complete staking registry

This commit is contained in:
Armani Ferrante 2021-01-22 17:42:59 -08:00
parent 41c3c57464
commit e4b8267697
No known key found for this signature in database
GPG Key ID: D597A80BCF8E12B7
6 changed files with 407 additions and 372 deletions

View File

@ -1,12 +1,12 @@
[package]
name = "serum-lockup"
name = "lockup"
version = "0.1.0"
description = "Created with Anchor"
edition = "2018"
[lib]
crate-type = ["cdylib", "lib"]
name = "serum_lockup"
name = "lockup"
[features]
no-entrypoint = []
@ -15,4 +15,3 @@ cpi = ["no-entrypoint"]
[dependencies]
anchor-lang = { git = "https://github.com/project-serum/anchor", features = ["derive"] }
anchor-spl = { git = "https://github.com/project-serum/anchor" }
bytemuck = "1.4.0"

View File

@ -41,7 +41,7 @@ mod lockup {
Ok(())
}
#[access_control(create_vesting_accounts(&ctx, nonce))]
#[access_control(CreateVesting::accounts(&ctx, nonce))]
pub fn create_vesting(
ctx: Context<CreateVesting>,
beneficiary: Pubkey,
@ -191,51 +191,6 @@ mod lockup {
}
}
#[access_control(is_whitelisted(transfer))]
pub fn whitelist_relay_cpi<'info>(
transfer: &WhitelistTransfer,
remaining_accounts: &[AccountInfo<'info>],
instruction_data: Vec<u8>,
) -> Result<(), Error> {
let mut meta_accounts = vec![
AccountMeta::new_readonly(*transfer.vesting.to_account_info().key, false),
AccountMeta::new(*transfer.vault.to_account_info().key, false),
AccountMeta::new_readonly(*transfer.vesting_signer.to_account_info().key, true),
AccountMeta::new_readonly(*transfer.token_program.to_account_info().key, false),
AccountMeta::new(
*transfer.whitelisted_program_vault.to_account_info().key,
false,
),
AccountMeta::new_readonly(
*transfer
.whitelisted_program_vault_authority
.to_account_info()
.key,
false,
),
];
meta_accounts.extend(remaining_accounts.iter().map(|a| {
if a.is_writable {
AccountMeta::new(*a.key, a.is_signer)
} else {
AccountMeta::new_readonly(*a.key, a.is_signer)
}
}));
let relay_instruction = Instruction {
program_id: *transfer.whitelisted_program.to_account_info().key,
accounts: meta_accounts,
data: instruction_data.to_vec(),
};
let seeds = &[
transfer.vesting.to_account_info().key.as_ref(),
&[transfer.vesting.nonce],
];
let signer = &[&seeds[..]];
solana_program::program::invoke_signed(&relay_instruction, &transfer.to_account_infos(), signer)
.map_err(Into::into)
}
#[derive(Accounts)]
pub struct SetAuthority<'info> {
#[account(mut, has_one = authority)]
@ -263,20 +218,22 @@ pub struct CreateVesting<'info> {
clock: Sysvar<'info, Clock>,
}
fn create_vesting_accounts(ctx: &Context<CreateVesting>, nonce: u8) -> Result<(), Error> {
let vault_authority = Pubkey::create_program_address(
&[
ctx.accounts.vesting.to_account_info().key.as_ref(),
&[nonce],
],
ctx.program_id,
)
.map_err(|_| ErrorCode::InvalidProgramAddress)?;
if ctx.accounts.vault.owner != vault_authority {
return Err(ErrorCode::InvalidVaultOwner)?;
}
impl<'info> CreateVesting<'info> {
fn accounts(ctx: &Context<CreateVesting>, nonce: u8) -> Result<(), Error> {
let vault_authority = Pubkey::create_program_address(
&[
ctx.accounts.vesting.to_account_info().key.as_ref(),
&[nonce],
],
ctx.program_id,
)
.map_err(|_| ErrorCode::InvalidProgramAddress)?;
if ctx.accounts.vault.owner != vault_authority {
return Err(ErrorCode::InvalidVaultOwner)?;
}
Ok(())
Ok(())
}
}
#[derive(Accounts)]
@ -307,13 +264,6 @@ pub struct WhitelistAdd<'info> {
authority: AccountInfo<'info>,
}
fn whitelist_has_capacity(ctx: &Context<WhitelistAdd>) -> Result<(), Error> {
if ctx.accounts.lockup.whitelist.len() == lockup::Lockup::WHITELIST_SIZE {
return Err(ErrorCode::WhitelistFull.into());
}
Ok(())
}
#[derive(Accounts)]
pub struct WhitelistDelete<'info> {
#[account(mut, has_one = authority)]
@ -353,26 +303,12 @@ pub struct WhitelistTransfer<'info> {
whitelisted_program_vault_authority: AccountInfo<'info>,
}
pub fn is_whitelisted<'info>(transfer: &WhitelistTransfer<'info>) -> Result<(), Error> {
if !transfer.lockup.whitelist.contains(&WhitelistEntry {
program_id: *transfer.whitelisted_program.key,
}) {
return Err(ErrorCode::WhitelistEntryNotFound.into());
}
Ok(())
}
#[derive(Accounts)]
pub struct AvailableForWithdrawal<'info> {
vesting: ProgramAccount<'info, Vesting>,
clock: Sysvar<'info, Clock>,
}
#[derive(AnchorSerialize, AnchorDeserialize, PartialEq, Default, Copy, Clone)]
pub struct WhitelistEntry {
pub program_id: Pubkey,
}
#[account]
pub struct Vesting {
/// The owner of this Vesting account.
@ -403,6 +339,11 @@ pub struct Vesting {
pub nonce: u8,
}
#[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.")]
@ -460,3 +401,64 @@ impl<'a, 'b, 'c, 'info> From<&Withdraw<'info>> for CpiContext<'a, 'b, 'c, 'info,
CpiContext::new(cpi_program, cpi_accounts)
}
}
fn whitelist_has_capacity(ctx: &Context<WhitelistAdd>) -> Result<(), Error> {
if ctx.accounts.lockup.whitelist.len() == lockup::Lockup::WHITELIST_SIZE {
return Err(ErrorCode::WhitelistFull.into());
}
Ok(())
}
#[access_control(is_whitelisted(transfer))]
pub fn whitelist_relay_cpi<'info>(
transfer: &WhitelistTransfer,
remaining_accounts: &[AccountInfo<'info>],
instruction_data: Vec<u8>,
) -> Result<(), Error> {
let mut meta_accounts = vec![
AccountMeta::new_readonly(*transfer.vesting.to_account_info().key, false),
AccountMeta::new(*transfer.vault.to_account_info().key, false),
AccountMeta::new_readonly(*transfer.vesting_signer.to_account_info().key, true),
AccountMeta::new_readonly(*transfer.token_program.to_account_info().key, false),
AccountMeta::new(
*transfer.whitelisted_program_vault.to_account_info().key,
false,
),
AccountMeta::new_readonly(
*transfer
.whitelisted_program_vault_authority
.to_account_info()
.key,
false,
),
];
meta_accounts.extend(remaining_accounts.iter().map(|a| {
if a.is_writable {
AccountMeta::new(*a.key, a.is_signer)
} else {
AccountMeta::new_readonly(*a.key, a.is_signer)
}
}));
let relay_instruction = Instruction {
program_id: *transfer.whitelisted_program.to_account_info().key,
accounts: meta_accounts,
data: instruction_data.to_vec(),
};
let seeds = &[
transfer.vesting.to_account_info().key.as_ref(),
&[transfer.vesting.nonce],
];
let signer = &[&seeds[..]];
solana_program::program::invoke_signed(&relay_instruction, &transfer.to_account_infos(), signer)
.map_err(Into::into)
}
pub fn is_whitelisted<'info>(transfer: &WhitelistTransfer<'info>) -> Result<(), Error> {
if !transfer.lockup.whitelist.contains(&WhitelistEntry {
program_id: *transfer.whitelisted_program.key,
}) {
return Err(ErrorCode::WhitelistEntryNotFound.into());
}
Ok(())
}

View File

@ -15,4 +15,4 @@ cpi = ["no-entrypoint"]
[dependencies]
anchor-lang = { git = "https://github.com/project-serum/anchor", features = ["derive"] }
anchor-spl = { git = "https://github.com/project-serum/anchor" }
serum-lockup = { path = "../lockup", features = ["cpi"] }
lockup = { path = "../lockup", features = ["cpi"] }

View File

@ -6,7 +6,7 @@
use anchor_lang::prelude::*;
use anchor_lang::solana_program::program_option::COption;
use anchor_spl::token::{self, Mint, TokenAccount, Transfer};
use serum_lockup::CreateVesting;
use lockup::{CreateVesting, Vesting};
use std::convert::Into;
#[program]
@ -15,8 +15,7 @@ mod registry {
#[state]
pub struct Registry {
/// Address of the lockup program.
lockup_program: Pubkey,
pub lockup_program: Pubkey,
}
impl Registry {
@ -25,7 +24,7 @@ mod registry {
}
}
#[access_control(initialize_accounts(&ctx, nonce))]
#[access_control(Initialize::accounts(&ctx, nonce))]
pub fn initialize(
ctx: Context<Initialize>,
mint: Pubkey,
@ -71,7 +70,7 @@ mod registry {
Ok(())
}
#[access_control(create_member_accounts(&ctx, nonce))]
#[access_control(CreateMember::accounts(&ctx, nonce))]
pub fn create_member(ctx: Context<CreateMember>, nonce: u8) -> Result<(), Error> {
let member = &mut ctx.accounts.member;
member.registrar = *ctx.accounts.registrar.to_account_info().key;
@ -93,30 +92,13 @@ mod registry {
Ok(())
}
// Deposits that can only come directly from the member beneficiary.
pub fn deposit(ctx: Context<Deposit>, amount: u64) -> Result<(), Error> {
// Deposit authority *must* match one of the balance ids.
// Similarly, the vault must match the balance id.
let vault = ctx.accounts.vault.to_account_info().key;
// Unlocked vault.
if ctx.accounts.depositor_authority.key == &ctx.accounts.member.balances.balance_id {
if vault != &ctx.accounts.member.balances.vault {
return Err(ErrorCode::InvalidVaultDeposit.into());
}
}
// Locked vault.
else if ctx.accounts.depositor_authority.key
== &ctx.accounts.member.balances_locked.balance_id
{
if vault != &ctx.accounts.member.balances_locked.vault {
return Err(ErrorCode::InvalidVaultDeposit.into());
}
}
// Unknown.
else {
return Err(ErrorCode::InvalidDepositor.into());
}
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<DepositLocked>, amount: u64) -> Result<(), Error> {
token::transfer(ctx.accounts.into(), amount).map_err(Into::into)
}
@ -126,23 +108,17 @@ mod registry {
&ctx.accounts.balances,
&ctx.accounts.balances_locked,
))]
pub fn stake(ctx: Context<Stake>, spt_amount: u64, balance_id: Pubkey) -> Result<(), Error> {
// Choose balances (locked or unlocked) based on balance_id.
pub fn stake(ctx: Context<Stake>, spt_amount: u64, locked: bool) -> Result<(), Error> {
let balances = {
if balance_id == ctx.accounts.member.beneficiary {
&ctx.accounts.balances
} else {
if locked {
&ctx.accounts.balances_locked
} else {
&ctx.accounts.balances
}
};
// Transfer tokens into the stake vault.
{
// Convert from stake-token units to mint-token units.
let token_amount = spt_amount
.checked_mul(ctx.accounts.registrar.stake_rate)
.unwrap();
let seeds = &[
ctx.accounts.registrar.to_account_info().key.as_ref(),
ctx.accounts.member.to_account_info().key.as_ref(),
@ -158,6 +134,10 @@ mod registry {
},
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)?;
}
@ -193,14 +173,13 @@ mod registry {
pub fn start_unstake(
ctx: Context<StartUnstake>,
spt_amount: u64,
balance_id: Pubkey,
locked: bool,
) -> Result<(), Error> {
// Choose balances (locked or unlocked) based on balance_id.
let balances = {
if balance_id == ctx.accounts.member.beneficiary {
&ctx.accounts.balances
} else {
if locked {
&ctx.accounts.balances_locked
} else {
&ctx.accounts.balances
}
};
@ -254,8 +233,8 @@ mod registry {
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.balance_id = balance_id;
pending_withdrawal.registrar = *ctx.accounts.registrar.to_account_info().key;
pending_withdrawal.locked = locked;
Ok(())
}
@ -267,15 +246,10 @@ mod registry {
// Select which balance set this affects.
let balances = {
if ctx.accounts.pending_withdrawal.balance_id == ctx.accounts.member.balances.balance_id
{
&ctx.accounts.member.balances
} else if ctx.accounts.pending_withdrawal.balance_id
== ctx.accounts.member.balances_locked.balance_id
{
if ctx.accounts.pending_withdrawal.locked {
&ctx.accounts.member.balances_locked
} else {
return Err(ErrorCode::Unknown.into());
&ctx.accounts.member.balances
}
};
// Check the vaults given are corrrect.
@ -314,29 +288,6 @@ mod registry {
}
pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<(), Error> {
// Deposit authority *must* match one of the balance ids.
// Similarly, the vault must match the balance id.
let vault = ctx.accounts.vault.to_account_info().key;
// Unlocked vault.
if ctx.accounts.depositor_authority.key == &ctx.accounts.member.balances.balance_id {
if vault != &ctx.accounts.member.balances.vault {
return Err(ErrorCode::InvalidVaultDeposit.into());
}
}
// Locked vault.
else if ctx.accounts.depositor_authority.key
== &ctx.accounts.member.balances_locked.balance_id
{
if vault != &ctx.accounts.member.balances_locked.vault {
return Err(ErrorCode::InvalidVaultDeposit.into());
}
}
// Unknown.
else {
return Err(ErrorCode::InvalidDepositor.into());
}
let seeds = &[
ctx.accounts.registrar.to_account_info().key.as_ref(),
ctx.accounts.member.to_account_info().key.as_ref(),
@ -354,6 +305,25 @@ mod registry {
token::transfer(cpi_ctx, amount).map_err(Into::into)
}
pub fn withdraw_locked(ctx: Context<WithdrawLocked>, amount: u64) -> Result<(), Error> {
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<DropReward>,
kind: RewardVendorKind,
@ -362,19 +332,6 @@ mod registry {
expiry_receiver: Pubkey,
nonce: u8,
) -> Result<(), Error> {
// Validate args.
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());
}
if total < ctx.accounts.pool_mint.supply {
return Err(ErrorCode::InsufficientReward.into());
}
@ -411,11 +368,11 @@ mod registry {
}
#[access_control(reward_eligible(&ctx.accounts.cmn))]
pub fn claim_reward_unlocked(ctx: Context<ClaimRewardUnlocked>) -> Result<(), Error> {
pub fn claim_reward(ctx: Context<ClaimReward>) -> Result<(), Error> {
if RewardVendorKind::Unlocked != ctx.accounts.cmn.vendor.kind {
return Err(ErrorCode::ExpectedUnlockedVendor.into());
}
// Reward to distribute.
// Reward distribution.
let spt_total =
ctx.accounts.cmn.balances.spt.amount + ctx.accounts.cmn.balances_locked.spt.amount;
let reward_amount = spt_total
@ -425,7 +382,7 @@ mod registry {
.unwrap();
assert!(reward_amount > 0);
// Vend reward to the member.
// 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(),
@ -435,8 +392,8 @@ mod registry {
let cpi_ctx = CpiContext::new_with_signer(
ctx.accounts.cmn.token_program.clone(),
token::Transfer {
to: ctx.accounts.token.to_account_info(),
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,
@ -462,14 +419,8 @@ mod registry {
period_count,
} => (end_ts, period_count),
};
// Lockup program requires the timestamp to be >= clock's timestamp.
// So update if the time has already passed. 60 seconds is arbitrary.
let end_ts = match end_ts > ctx.accounts.cmn.clock.unix_timestamp + 60 {
true => end_ts,
false => ctx.accounts.cmn.clock.unix_timestamp + 60,
};
// Calculate reward distribution.
// Reward distribution.
let spt_total =
ctx.accounts.cmn.balances.spt.amount + ctx.accounts.cmn.balances_locked.spt.amount;
let reward_amount = spt_total
@ -479,7 +430,14 @@ mod registry {
.unwrap();
assert!(reward_amount > 0);
// Vend reward to the member by creating a lockup account.
// Lockup program requires the timestamp to be >= clock's timestamp.
// So update if the time has already passed. 60 seconds is arbitrary.
let end_ts = match end_ts > ctx.accounts.cmn.clock.unix_timestamp + 60 {
true => end_ts,
false => ctx.accounts.cmn.clock.unix_timestamp + 60,
};
// 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(),
@ -487,12 +445,11 @@ mod registry {
];
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);
serum_lockup::cpi::create_vesting(
lockup::cpi::create_vesting(
cpi_ctx,
ctx.accounts.cmn.member.beneficiary,
end_ts,
@ -513,7 +470,7 @@ mod registry {
return Err(ErrorCode::VendorNotYetExpired.into());
}
// Send all remaining funds to the expiry receiver.
// 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(),
@ -523,7 +480,7 @@ mod registry {
let cpi_ctx = CpiContext::new_with_signer(
ctx.accounts.token_program.clone(),
token::Transfer {
to: ctx.accounts.token.to_account_info(),
to: ctx.accounts.expiry_receiver_token.to_account_info(),
from: ctx.accounts.vault.to_account_info(),
authority: ctx.accounts.vendor_signer.to_account_info(),
},
@ -531,6 +488,7 @@ mod registry {
);
token::transfer(cpi_ctx, ctx.accounts.vault.amount)?;
// Burn the vendor.
let vendor = &mut ctx.accounts.vendor;
vendor.expired = true;
@ -538,51 +496,6 @@ mod registry {
}
}
fn reward_eligible(cmn: &ClaimRewardCommon) -> Result<(), Error> {
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<(), Error> {
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(())
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(init)]
@ -593,20 +506,22 @@ pub struct Initialize<'info> {
rent: Sysvar<'info, Rent>,
}
fn initialize_accounts<'info>(ctx: &Context<Initialize<'info>>, nonce: u8) -> Result<(), Error> {
let registrar_signer = Pubkey::create_program_address(
&[
ctx.accounts.registrar.to_account_info().key.as_ref(),
&[nonce],
],
ctx.program_id,
)
.map_err(|_| ErrorCode::InvalidNonce)?;
if ctx.accounts.pool_mint.mint_authority != COption::Some(registrar_signer) {
return Err(ErrorCode::InvalidPoolMintAuthority.into());
impl<'info> Initialize<'info> {
fn accounts(ctx: &Context<Initialize<'info>>, nonce: u8) -> Result<(), Error> {
let registrar_signer = Pubkey::create_program_address(
&[
ctx.accounts.registrar.to_account_info().key.as_ref(),
&[nonce],
],
ctx.program_id,
)
.map_err(|_| ErrorCode::InvalidNonce)?;
if ctx.accounts.pool_mint.mint_authority != COption::Some(registrar_signer) {
return Err(ErrorCode::InvalidPoolMintAuthority.into());
}
assert!(ctx.accounts.pool_mint.supply == 0);
Ok(())
}
assert!(ctx.accounts.pool_mint.supply == 0);
Ok(())
}
#[derive(Accounts)]
@ -619,12 +534,13 @@ pub struct UpdateRegistrar<'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>,
member_signer: AccountInfo<'info>,
#[account(
"&balances.spt.owner == member_signer.key",
"balances.spt.mint == registrar.pool_mint",
@ -637,30 +553,37 @@ pub struct CreateMember<'info> {
"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>,
}
fn create_member_accounts(ctx: &Context<CreateMember>, nonce: u8) -> Result<(), Error> {
// Check the nonce + signer is correct.
let seeds = &[
ctx.accounts.registrar.to_account_info().key.as_ref(),
ctx.accounts.member.to_account_info().key.as_ref(),
&[nonce],
];
let member_signer = Pubkey::create_program_address(seeds, ctx.program_id)
.map_err(|_| ErrorCode::InvalidNonce)?;
if &member_signer != ctx.accounts.member_signer.to_account_info().key {
return Err(ErrorCode::InvalidMemberSigner.into());
}
impl<'info> CreateMember<'info> {
fn accounts(ctx: &Context<CreateMember>, nonce: u8) -> Result<(), Error> {
let seeds = &[
ctx.accounts.registrar.to_account_info().key.as_ref(),
ctx.accounts.member.to_account_info().key.as_ref(),
&[nonce],
];
let 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(())
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> {
balance_id: AccountInfo<'info>,
#[account(mut)]
spt: CpiAccount<'info, TokenAccount>,
#[account(mut, "vault.owner == spt.owner")]
@ -675,18 +598,6 @@ pub struct BalanceSandboxAccounts<'info> {
vault_pw: CpiAccount<'info, TokenAccount>,
}
impl<'info> From<&BalanceSandboxAccounts<'info>> for BalanceSandbox {
fn from(accs: &BalanceSandboxAccounts<'info>) -> Self {
Self {
balance_id: *accs.balance_id.key,
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,
}
}
}
#[derive(Accounts)]
pub struct UpdateMember<'info> {
#[account(mut, has_one = beneficiary)]
@ -697,16 +608,44 @@ pub struct UpdateMember<'info> {
#[derive(Accounts)]
pub struct Deposit<'info> {
// Lockup whitelist relay interface.
vesting: AccountInfo<'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 == &registry.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, "&vault.owner == member_signer.key")]
vault: CpiAccount<'info, TokenAccount>,
#[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(),
@ -717,25 +656,12 @@ pub struct Deposit<'info> {
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>,
#[account(belongs_to = registrar, belongs_to = beneficiary)]
member: ProgramAccount<'info, Member>,
}
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)
}
}
#[derive(Accounts)]
@ -747,7 +673,7 @@ pub struct Stake<'info> {
#[account(mut)]
pool_mint: CpiAccount<'info, Mint>,
// Member specific.
// Member.
#[account(mut, has_one = beneficiary, belongs_to = registrar)]
member: ProgramAccount<'info, Member>,
#[account(signer)]
@ -757,7 +683,7 @@ pub struct Stake<'info> {
#[account("BalanceSandbox::from(&balances_locked) == member.balances_locked")]
balances_locked: BalanceSandboxAccounts<'info>,
// Programmatic signers.
// Program signers.
#[account(
seeds = [
registrar.to_account_info().key.as_ref(),
@ -766,12 +692,7 @@ pub struct Stake<'info> {
]
)]
member_signer: AccountInfo<'info>,
#[account(
seeds = [
registrar.to_account_info().key.as_ref(),
&[registrar.nonce],
]
)]
#[account(seeds = [registrar.to_account_info().key.as_ref(), &[registrar.nonce]])]
registrar_signer: AccountInfo<'info>,
// Misc.
@ -795,17 +716,9 @@ pub struct StartUnstake<'info> {
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"
)]
#[account("BalanceSandbox::from(&balances) == member.balances")]
balances: BalanceSandboxAccounts<'info>,
#[account(
"&balances_locked.spt.owner == member_signer.key",
"balances_locked.spt.mint == registrar.pool_mint",
"balances_locked.vault.mint == registrar.mint"
)]
#[account("BalanceSandbox::from(&balances_locked) == member.balances_locked")]
balances_locked: BalanceSandboxAccounts<'info>,
// Programmatic signers.
@ -859,16 +772,50 @@ pub struct EndUnstake<'info> {
#[derive(Accounts)]
pub struct Withdraw<'info> {
// Lockup whitelist relay interface.
vesting: AccountInfo<'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>,
#[account(signer)]
depositor_authority: AccountInfo<'info>,
// Misc.
#[account("token_program.key == &token::ID")]
token_program: AccountInfo<'info>,
#[account(mut, "&vault.owner == member_signer.key")]
vault: CpiAccount<'info, TokenAccount>,
}
#[derive(Accounts)]
pub struct WithdrawLocked<'info> {
// Lockup whitelist relay interface.
#[account(
"vesting.to_account_info().owner == &registry.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(),
@ -879,11 +826,12 @@ pub struct Withdraw<'info> {
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>,
#[account(belongs_to = registrar, belongs_to = beneficiary)]
member: ProgramAccount<'info, Member>,
}
#[derive(Accounts)]
@ -894,19 +842,16 @@ pub struct DropReward<'info> {
#[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>,
@ -914,32 +859,38 @@ pub struct DropReward<'info> {
rent: Sysvar<'info, Rent>,
}
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> DropReward<'info> {
fn accounts(ctx: &Context<DropReward>, nonce: u8) -> Result<(), Error> {
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 ClaimRewardUnlocked<'info> {
pub struct ClaimReward<'info> {
cmn: ClaimRewardCommon<'info>,
// Account to send reward to.
#[account(mut)]
token: AccountInfo<'info>,
to: AccountInfo<'info>,
}
#[derive(Accounts)]
pub struct ClaimRewardLocked<'info> {
cmn: ClaimRewardCommon<'info>,
// TODO: assert on the lockup program id once deployed.
registry: ProgramState<'info, Registry>,
#[account("lockup_program.key == &registry.lockup_program")]
lockup_program: AccountInfo<'info>,
}
@ -948,9 +899,8 @@ pub struct ClaimRewardLocked<'info> {
pub struct ClaimRewardCommon<'info> {
// Stake instance.
registrar: ProgramAccount<'info, Registrar>,
// Member.
#[account(mut, belongs_to = registrar)]
#[account(mut, belongs_to = registrar, has_one = beneficiary)]
member: ProgramAccount<'info, Member>,
#[account(signer)]
beneficiary: AccountInfo<'info>,
@ -958,7 +908,6 @@ pub struct ClaimRewardCommon<'info> {
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>,
@ -972,7 +921,6 @@ pub struct ClaimRewardCommon<'info> {
]
)]
vendor_signer: AccountInfo<'info>,
// Misc.
#[account("token_program.key == &token::ID")]
token_program: AccountInfo<'info>,
@ -983,7 +931,6 @@ pub struct ClaimRewardCommon<'info> {
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>,
@ -997,13 +944,11 @@ pub struct ExpireReward<'info> {
]
)]
vendor_signer: AccountInfo<'info>,
// Receiver.
#[account(signer)]
expiry_receiver: AccountInfo<'info>,
#[account(mut)]
token: AccountInfo<'info>,
expiry_receiver_token: AccountInfo<'info>,
// Misc.
#[account("token_program.key == &token::ID")]
token_program: AccountInfo<'info>,
@ -1052,14 +997,13 @@ pub struct Member {
}
// BalanceSandbox defines isolated funds that can only be deposited/withdrawn
// into the program if the `owner` signs off on the transaction.
// 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 {
pub balance_id: Pubkey,
// Staking pool token.
pub spt: Pubkey,
// Free balance (deposit) vaults.
@ -1086,8 +1030,8 @@ pub struct PendingWithdrawal {
pub end_ts: i64,
/// The number of tokens redeemed from the staking pool.
pub amount: u64,
/// The Member account's set of vaults this withdrawal belongs to.
pub balance_id: Pubkey,
/// True if the withdrawal applies to locked balances.
pub locked: bool,
}
#[account]
@ -1209,4 +1153,104 @@ pub enum ErrorCode {
ExpectedLockedVendor,
#[msg("Unlocked reward vendor expected but a locked vendor was given.")]
ExpectedUnlockedVendor,
#[msg("Locked deposit from an invalid deposit authority.")]
InvalidVestingSigner,
}
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<(), Error> {
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<(), Error> {
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(())
}

View File

@ -353,14 +353,12 @@ describe("Lockup and Registry", () => {
const [mainTx, _balances] = await utils.createBalanceSandbox(
provider,
registrarAccount,
memberSigner,
provider.wallet.publicKey // Beneficiary,
memberSigner
);
const [lockedTx, _balancesLocked] = await utils.createBalanceSandbox(
provider,
registrarAccount,
memberSigner,
vesting.publicKey // Lockup.
memberSigner
);
balances = _balances;
@ -407,15 +405,10 @@ describe("Lockup and Registry", () => {
const depositAmount = new anchor.BN(120);
await registry.rpc.deposit(depositAmount, {
accounts: {
// Whitelist relay.
vesting: anchor.web3.SYSVAR_RENT_PUBKEY,
depositor: god,
depositorAuthority: provider.wallet.publicKey,
tokenProgram: TokenInstructions.TOKEN_PROGRAM_ID,
vault: memberAccount.balances.vault,
memberSigner,
// Program specific.
registrar: registrar.publicKey,
beneficiary: provider.wallet.publicKey,
member: member.publicKey,
},
@ -430,7 +423,7 @@ describe("Lockup and Registry", () => {
it("Stakes to a member (unlocked)", async () => {
const stakeAmount = new anchor.BN(10);
await registry.rpc.stake(stakeAmount, provider.wallet.publicKey, {
await registry.rpc.stake(stakeAmount, false, {
accounts: {
// Stake instance.
registrar: registrar.publicKey,
@ -553,9 +546,9 @@ describe("Lockup and Registry", () => {
mint,
provider.wallet.publicKey
);
await registry.rpc.claimRewardUnlocked({
await registry.rpc.claimReward({
accounts: {
token,
to: token,
cmn: {
registrar: registrar.publicKey,
@ -696,6 +689,7 @@ describe("Lockup and Registry", () => {
await registry.rpc.claimRewardLocked(nonce, {
accounts: {
registry: await registry.state.address(),
lockupProgram: lockup.programId,
cmn: {
registrar: registrar.publicKey,
@ -744,10 +738,10 @@ describe("Lockup and Registry", () => {
const pendingWithdrawal = new anchor.web3.Account();
it("Unstakes", async () => {
it("Unstakes (unlocked)", async () => {
const unstakeAmount = new anchor.BN(10);
await registry.rpc.startUnstake(unstakeAmount, provider.wallet.publicKey, {
await registry.rpc.startUnstake(unstakeAmount, false, {
accounts: {
registrar: registrar.publicKey,
rewardEventQ: rewardQ.publicKey,
@ -853,17 +847,13 @@ describe("Lockup and Registry", () => {
const withdrawAmount = new anchor.BN(100);
await registry.rpc.withdraw(withdrawAmount, {
accounts: {
// Whitelist relay.
vesting: anchor.web3.SYSVAR_RENT_PUBKEY,
depositor: token,
depositorAuthority: provider.wallet.publicKey,
tokenProgram: TokenInstructions.TOKEN_PROGRAM_ID,
registrar: registrar.publicKey,
member: member.publicKey,
beneficiary: provider.wallet.publicKey,
vault: memberAccount.balances.vault,
memberSigner,
// Program specific.
registrar: registrar.publicKey,
beneficiary: provider.wallet.publicKey,
member: member.publicKey,
depositor: token,
tokenProgram: TokenInstructions.TOKEN_PROGRAM_ID,
},
});

View File

@ -1,7 +1,8 @@
const anchor = require("@project-serum/anchor");
// const anchor = require("@project-serum/anchor");
const anchor = require("/home/armaniferrante/Documents/code/src/github.com/project-serum/anchor/ts");
const serumCmn = require("@project-serum/common");
async function createBalanceSandbox(provider, r, registrySigner, owner) {
async function createBalanceSandbox(provider, r, registrySigner) {
const spt = new anchor.web3.Account();
const vault = new anchor.web3.Account();
const vaultStake = new anchor.web3.Account();
@ -53,7 +54,6 @@ async function createBalanceSandbox(provider, r, registrySigner, owner) {
return [
tx,
{
balanceId: owner,
spt: spt.publicKey,
vault: vault.publicKey,
vaultStake: vaultStake.publicKey,