lockup: Cleanup

This commit is contained in:
armaniferrante 2020-12-01 10:34:50 -08:00
parent 030037eeed
commit e1d88669d3
No known key found for this signature in database
GPG Key ID: 58BEF301E91F7828
34 changed files with 478 additions and 397 deletions

View File

@ -4,6 +4,8 @@
pub mod client;
#[macro_use]
pub mod pack;
#[cfg(feature = "program")]
pub mod program;
// TODO: Import the shared_mem crate instead of hardcoding here.
// The shared memory program is awaiting audit and so is not deployed

View File

@ -1,5 +1,5 @@
use serum_registry::error::RegistryError;
use solana_sdk::account_info::AccountInfo;
use solana_sdk::program_error::ProgramError;
use spl_token::instruction as token_instruction;
use std::convert::Into;
@ -10,7 +10,7 @@ pub fn invoke_token_transfer<'a, 'b>(
tok_program_acc_info: &'a AccountInfo<'b>,
signer_seeds: &[&[&[u8]]],
amount: u64,
) -> Result<(), RegistryError> {
) -> Result<(), ProgramError> {
let transfer_instr = token_instruction::transfer(
&spl_token::ID,
from_acc_info.key,
@ -29,5 +29,4 @@ pub fn invoke_token_transfer<'a, 'b>(
],
signer_seeds,
)
.map_err(Into::into)
}

View File

@ -59,12 +59,12 @@ pub enum SubCommand {
#[clap(short = 'a', long)]
deposit_amount: u64,
},
/// Redeem a claimed token receipt for an amount of vested tokens.
Redeem {
/// The amount of vested tokens to redeem.
/// Withdraw vested tokens.
Withdraw {
/// The amount of vested tokens to withdraw.
#[clap(short, long)]
amount: u64,
/// Vesting account to redeem from.
/// Vesting account to withdraw from.
#[clap(short, long)]
vesting: Pubkey,
/// Token account to send the vested tokens to.
@ -171,7 +171,7 @@ pub fn run(opts: Opts) -> Result<()> {
println!("{:#?}", resp);
Ok(())
}
SubCommand::Redeem {
SubCommand::Withdraw {
vesting,
amount,
token_account,
@ -180,7 +180,7 @@ pub fn run(opts: Opts) -> Result<()> {
let client = ctx.connect::<Client>(opts.cmd.pid)?;
let vesting_account = client.vesting(&vesting)?;
let safe = vesting_account.safe;
let resp = client.redeem(RedeemRequest {
let resp = client.withdraw(WithdrawRequest {
beneficiary: &beneficiary,
vesting,
token_account,
@ -207,7 +207,7 @@ fn account_cmd(ctx: &Context, pid: Pubkey, cmd: AccountsCommand) -> Result<()> {
let current_ts = client.rpc().get_block_time(client.rpc().get_slot()?)?;
let amount = vault.available_for_withdrawal(current_ts);
println!("Redeemable balance: {:?}", amount);
println!("Withdrawable balance: {:?}", amount);
println!("Whitelistable balance: {:?}", amount);
Ok(())

View File

@ -108,6 +108,7 @@ impl Client {
safe,
whitelist_program,
mut relay_accounts,
whitelist_program_vault,
whitelist_program_vault_authority,
delegate_amount,
relay_data,
@ -123,7 +124,6 @@ impl Client {
AccountMeta::new_readonly(safe, false),
AccountMeta::new_readonly(whitelist, false),
AccountMeta::new_readonly(whitelist_program, false),
AccountMeta::new_readonly(whitelist_program_vault_authority, false),
// Below are relay accounts.
AccountMeta::new(vault, false),
AccountMeta::new_readonly(
@ -131,6 +131,8 @@ impl Client {
false,
),
AccountMeta::new_readonly(spl_token::ID, false),
AccountMeta::new(whitelist_program_vault, false),
AccountMeta::new_readonly(whitelist_program_vault_authority, false),
];
accounts.append(&mut relay_accounts);
@ -156,6 +158,7 @@ impl Client {
vesting,
safe,
whitelist_program,
whitelist_program_vault,
whitelist_program_vault_authority,
relay_data,
mut relay_accounts,
@ -180,6 +183,7 @@ impl Client {
false,
),
AccountMeta::new_readonly(spl_token::ID, false),
AccountMeta::new(whitelist_program_vault, false),
AccountMeta::new_readonly(whitelist_program_vault_authority, false),
];
// Program specific relay.
@ -195,8 +199,8 @@ impl Client {
Ok(WhitelistDepositResponse { tx })
}
pub fn redeem(&self, req: RedeemRequest) -> Result<RedeemResponse, ClientError> {
let RedeemRequest {
pub fn withdraw(&self, req: WithdrawRequest) -> Result<WithdrawResponse, ClientError> {
let WithdrawRequest {
beneficiary,
vesting,
token_account,
@ -221,8 +225,8 @@ impl Client {
let signers = [self.payer(), &beneficiary];
let tx = self
.inner
.redeem_with_signers(&signers, &accounts, amount)?;
Ok(RedeemResponse { tx })
.withdraw_with_signers(&signers, &accounts, amount)?;
Ok(WithdrawResponse { tx })
}
pub fn set_authority(
@ -281,7 +285,7 @@ impl Client {
&registry_pid,
)
.map_err(|_| anyhow!("unable to create vault authority"))?;
let vault = match is_mega {
let whitelist_program_vault = match is_mega {
false => r.vault,
true => r.mega_vault,
};
@ -291,7 +295,6 @@ impl Client {
AccountMeta::new(entity, false),
AccountMeta::new_readonly(registrar, false),
AccountMeta::new_readonly(solana_sdk::sysvar::clock::ID, false),
AccountMeta::new(vault, false),
];
let (pool_accs, _) = r_client.common_pool_accounts(pool_program_id, registrar, false)?;
relay_accounts.extend_from_slice(&pool_accs);
@ -302,6 +305,7 @@ impl Client {
safe,
whitelist_program: registry_pid,
relay_accounts,
whitelist_program_vault,
whitelist_program_vault_authority,
delegate_amount: amount,
relay_data,
@ -346,7 +350,7 @@ impl Client {
&registry_pid,
)
.map_err(|_| anyhow!("unable to create vault authority"))?;
let vault = match is_mega {
let whitelist_program_vault = match is_mega {
false => r.vault,
true => r.mega_vault,
};
@ -356,7 +360,6 @@ impl Client {
AccountMeta::new(entity, false),
AccountMeta::new_readonly(registrar, false),
AccountMeta::new_readonly(solana_sdk::sysvar::clock::ID, false),
AccountMeta::new(vault, false),
];
let (pool_accs, _) = r_client.common_pool_accounts(pool_program_id, registrar, is_mega)?;
relay_accounts.extend_from_slice(&pool_accs);
@ -366,6 +369,7 @@ impl Client {
vesting,
safe,
whitelist_program: registry_pid,
whitelist_program_vault,
whitelist_program_vault_authority,
relay_data,
relay_accounts,
@ -509,9 +513,10 @@ pub struct WhitelistWithdrawRequest<'a> {
pub vesting: Pubkey,
pub safe: Pubkey,
pub whitelist_program: Pubkey,
pub relay_accounts: Vec<AccountMeta>,
pub whitelist_program_vault: Pubkey,
pub whitelist_program_vault_authority: Pubkey,
pub delegate_amount: u64,
pub relay_accounts: Vec<AccountMeta>,
pub relay_data: Vec<u8>,
pub relay_signers: Vec<&'a Keypair>,
}
@ -526,6 +531,7 @@ pub struct WhitelistDepositRequest<'a> {
pub vesting: Pubkey,
pub safe: Pubkey,
pub whitelist_program: Pubkey,
pub whitelist_program_vault: Pubkey,
pub whitelist_program_vault_authority: Pubkey,
pub relay_accounts: Vec<AccountMeta>,
pub relay_data: Vec<u8>,
@ -537,7 +543,7 @@ pub struct WhitelistDepositResponse {
pub tx: Signature,
}
pub struct RedeemRequest<'a> {
pub struct WithdrawRequest<'a> {
pub beneficiary: &'a Keypair,
pub vesting: Pubkey,
pub token_account: Pubkey,
@ -546,7 +552,7 @@ pub struct RedeemRequest<'a> {
}
#[derive(Debug)]
pub struct RedeemResponse {
pub struct WithdrawResponse {
pub tx: Signature,
}

View File

@ -1,4 +1,4 @@
use crate::access_control;
use crate::common::access_control;
use serum_common::pack::Pack;
use serum_lockup::accounts::Vesting;
use serum_lockup::error::LockupError;
@ -6,6 +6,7 @@ use solana_program::info;
use solana_sdk::account_info::{next_account_info, AccountInfo};
use solana_sdk::pubkey::Pubkey;
// Convenience instruction for UI's.
pub fn handler(_program_id: &Pubkey, accounts: &[AccountInfo]) -> Result<(), LockupError> {
let acc_infos = &mut accounts.iter();

View File

@ -48,14 +48,13 @@ pub fn whitelist<'a>(
Ok(wl)
}
/// Access control on any instruction mutating an existing Vesting account.
pub fn vesting(
program_id: &Pubkey,
safe: &Pubkey,
safe_acc_info: &AccountInfo,
vesting_acc_info: &AccountInfo,
vesting_acc_beneficiary_info: &AccountInfo,
) -> Result<Vesting, LockupError> {
let vesting = vesting_raw(program_id, safe, vesting_acc_info)?;
let vesting = _vesting(program_id, safe_acc_info, vesting_acc_info)?;
if vesting.beneficiary != *vesting_acc_beneficiary_info.key {
return Err(LockupErrorCode::Unauthorized)?;
@ -64,9 +63,9 @@ pub fn vesting(
Ok(vesting)
}
pub fn vesting_raw(
fn _vesting(
program_id: &Pubkey,
safe: &Pubkey,
safe_acc_info: &AccountInfo,
vesting_acc_info: &AccountInfo,
) -> Result<Vesting, LockupError> {
let mut data: &[u8] = &vesting_acc_info.try_borrow_data()?;
@ -78,36 +77,20 @@ pub fn vesting_raw(
if !vesting.initialized {
return Err(LockupErrorCode::NotInitialized)?;
}
if vesting.safe != *safe {
if vesting.safe != *safe_acc_info.key {
return Err(LockupErrorCode::WrongSafe)?;
}
Ok(vesting)
}
pub fn rent(acc_info: &AccountInfo) -> Result<Rent, LockupError> {
if *acc_info.key != solana_sdk::sysvar::rent::id() {
return Err(LockupErrorCode::InvalidRentSysvar)?;
}
Rent::from_account_info(acc_info).map_err(Into::into)
}
pub fn clock(acc_info: &AccountInfo) -> Result<Clock, LockupError> {
if *acc_info.key != solana_sdk::sysvar::clock::id() {
return Err(LockupErrorCode::InvalidClockSysvar)?;
}
Clock::from_account_info(acc_info).map_err(Into::into)
}
pub fn safe(acc_info: &AccountInfo, program_id: &Pubkey) -> Result<Safe, LockupError> {
if acc_info.owner != program_id {
return Err(LockupErrorCode::InvalidAccountOwner)?;
}
let safe = Safe::unpack(&acc_info.try_borrow_data()?)?;
if !safe.initialized {
return Err(LockupErrorCode::NotInitialized)?;
}
Ok(safe)
}
@ -119,8 +102,10 @@ pub fn vault(
safe_acc_info: &AccountInfo,
program_id: &Pubkey,
) -> Result<TokenAccount, LockupError> {
let vesting = vesting_raw(program_id, safe_acc_info.key, vesting_acc_info)?;
let vesting = _vesting(program_id, safe_acc_info, vesting_acc_info)?;
let vault = token(acc_info)?;
let va = vault_authority(
program_id,
vault_authority_acc_info,
@ -132,14 +117,11 @@ pub fn vault(
if va != vault.owner {
return Err(LockupErrorCode::InvalidVault)?;
}
if va != *vault_authority_acc_info.key {
return Err(LockupErrorCode::InvalidVault)?;
}
Ok(vault)
}
pub fn vault_authority(
fn vault_authority(
program_id: &Pubkey,
vault_authority_acc_info: &AccountInfo,
beneficiary_acc_info: &AccountInfo,
@ -170,3 +152,17 @@ pub fn token(acc_info: &AccountInfo) -> Result<TokenAccount, LockupError> {
Ok(token)
}
pub fn rent(acc_info: &AccountInfo) -> Result<Rent, LockupError> {
if *acc_info.key != solana_sdk::sysvar::rent::id() {
return Err(LockupErrorCode::InvalidRentSysvar)?;
}
Rent::from_account_info(acc_info).map_err(Into::into)
}
pub fn clock(acc_info: &AccountInfo) -> Result<Clock, LockupError> {
if *acc_info.key != solana_sdk::sysvar::clock::id() {
return Err(LockupErrorCode::InvalidClockSysvar)?;
}
Clock::from_account_info(acc_info).map_err(Into::into)
}

View File

@ -0,0 +1,43 @@
use serum_lockup::accounts::vault;
use serum_lockup::accounts::Vesting;
use solana_sdk::account_info::AccountInfo;
use solana_sdk::entrypoint::ProgramResult;
use solana_sdk::instruction::Instruction;
use solana_sdk::pubkey::Pubkey;
pub mod access_control;
// Prepends the program's unique TAG identifier before making the signed
// cross program invocation to a *trusted* program on the whitelist.
//
// The trusted program should perform three steps of validation:
//
// 1. Check for the TAG identifier in the first 8 bytes of the instruction data.
// If present, then authentication must be done on the following two steps.
// 2. Check accounts[1] is signed.
// 3. Check accounts[1] is the correct program derived address for the vesting
// account, i.e., signer seeds == [safe_address, beneficiary_address, nonce].
//
// If all of these hold, a program can trust the instruction was invoked
// by a the lockup program on behalf of a vesting account.
//
// Importantly, it's the responsibility of the trusted program to maintain the
// locked invariant preserved by this program and to return the funds at an
// unspecified point in the future for unlocking. Any bug in the trusted program
// can result in locked funds becoming unlocked, so take care when adding to the
// whitelist.
pub fn whitelist_cpi(
mut instruction: Instruction,
safe: &Pubkey,
beneficiary_acc_info: &AccountInfo,
vesting: &Vesting,
accounts: &[AccountInfo],
) -> ProgramResult {
let mut data = serum_lockup::instruction::TAG.to_le_bytes().to_vec();
data.extend(instruction.data);
instruction.data = data;
let signer_seeds = vault::signer_seeds(safe, beneficiary_acc_info.key, &vesting.nonce);
solana_sdk::program::invoke_signed(&instruction, accounts, &[&signer_seeds])
}

View File

@ -1,5 +1,6 @@
use crate::access_control;
use crate::common::access_control;
use serum_common::pack::Pack;
use serum_common::program::invoke_token_transfer;
use serum_lockup::accounts::{vault, Vesting};
use serum_lockup::error::{LockupError, LockupErrorCode};
use solana_program::info;
@ -47,13 +48,13 @@ pub fn handler(
Vesting::unpack_unchecked_mut(
&mut vesting_acc_info.try_borrow_mut_data()?,
&mut |vesting_acc: &mut Vesting| {
&mut |vesting: &mut Vesting| {
state_transition(StateTransitionRequest {
clock_ts,
end_ts,
period_count,
deposit_amount,
vesting_acc,
vesting,
beneficiary,
safe_acc_info,
depositor_acc_info,
@ -94,8 +95,8 @@ fn access_control(req: AccessControlRequest) -> Result<AccessControlResponse, Lo
}
// Account validation.
let rent = access_control::rent(rent_acc_info)?;
let _safe = access_control::safe(safe_acc_info, program_id)?;
let rent = access_control::rent(rent_acc_info)?;
let clock_ts = access_control::clock(&clock_acc_info)?.unix_timestamp;
// Initialize checks.
@ -129,14 +130,14 @@ fn access_control(req: AccessControlRequest) -> Result<AccessControlResponse, Lo
return Err(LockupErrorCode::InvalidDepositAmount)?;
}
}
// Vault.
// Vault owned by program.
let vault = {
let vault = access_control::token(vault_acc_info)?;
let vault_authority = Pubkey::create_program_address(
&vault::signer_seeds(safe_acc_info.key, &beneficiary, &nonce),
program_id,
)
.map_err(|_| LockupErrorCode::InvalidVaultNonce)?;
let vault = access_control::token(vault_acc_info)?;
if vault.owner != vault_authority {
return Err(LockupErrorCode::InvalidAccountOwner)?;
}
@ -157,7 +158,7 @@ fn state_transition(req: StateTransitionRequest) -> Result<(), LockupError> {
end_ts,
period_count,
deposit_amount,
vesting_acc,
vesting,
beneficiary,
safe_acc_info,
depositor_acc_info,
@ -169,60 +170,45 @@ fn state_transition(req: StateTransitionRequest) -> Result<(), LockupError> {
} = req;
// Initialize account.
{
vesting_acc.safe = safe_acc_info.key.clone();
vesting_acc.beneficiary = beneficiary;
vesting_acc.initialized = true;
vesting_acc.mint = vault.mint;
vesting_acc.vault = *vault_acc_info.key;
vesting_acc.period_count = period_count;
vesting_acc.start_balance = deposit_amount;
vesting_acc.end_ts = end_ts;
vesting_acc.start_ts = clock_ts;
vesting_acc.balance = deposit_amount;
vesting_acc.whitelist_owned = 0;
vesting_acc.grantor = *depositor_authority_acc_info.key;
vesting_acc.nonce = nonce;
}
vesting.safe = safe_acc_info.key.clone();
vesting.beneficiary = beneficiary;
vesting.initialized = true;
vesting.mint = vault.mint;
vesting.vault = *vault_acc_info.key;
vesting.period_count = period_count;
vesting.start_balance = deposit_amount;
vesting.end_ts = end_ts;
vesting.start_ts = clock_ts;
vesting.outstanding = deposit_amount;
vesting.whitelist_owned = 0;
vesting.grantor = *depositor_authority_acc_info.key;
vesting.nonce = nonce;
// Now transfer SPL funds from the depositor, to the
// program-controlled vault.
{
let deposit_instruction = spl_token::instruction::transfer(
&spl_token::ID,
depositor_acc_info.key,
vault_acc_info.key,
depositor_authority_acc_info.key,
&[],
deposit_amount,
)?;
solana_sdk::program::invoke_signed(
&deposit_instruction,
&[
depositor_acc_info.clone(),
depositor_authority_acc_info.clone(),
vault_acc_info.clone(),
token_program_acc_info.clone(),
],
&[],
)?;
}
// Transfer funds to vault.
invoke_token_transfer(
depositor_acc_info,
vault_acc_info,
depositor_authority_acc_info,
token_program_acc_info,
&[],
deposit_amount,
)?;
Ok(())
}
struct AccessControlRequest<'a, 'b> {
program_id: &'a Pubkey,
beneficiary: Pubkey,
end_ts: i64,
period_count: u64,
deposit_amount: u64,
vesting_acc_info: &'a AccountInfo<'b>,
safe_acc_info: &'a AccountInfo<'b>,
depositor_authority_acc_info: &'a AccountInfo<'b>,
vault_acc_info: &'a AccountInfo<'b>,
rent_acc_info: &'a AccountInfo<'b>,
clock_acc_info: &'a AccountInfo<'b>,
program_id: &'a Pubkey,
beneficiary: Pubkey,
end_ts: i64,
period_count: u64,
deposit_amount: u64,
nonce: u8,
}
@ -232,17 +218,17 @@ struct AccessControlResponse {
}
struct StateTransitionRequest<'a, 'b, 'c> {
depositor_authority_acc_info: &'a AccountInfo<'b>,
depositor_acc_info: &'a AccountInfo<'b>,
token_program_acc_info: &'a AccountInfo<'b>,
safe_acc_info: &'a AccountInfo<'b>,
vault_acc_info: &'a AccountInfo<'b>,
vesting: &'c mut Vesting,
vault: TokenAccount,
beneficiary: Pubkey,
clock_ts: i64,
end_ts: i64,
period_count: u64,
deposit_amount: u64,
nonce: u8,
vesting_acc: &'c mut Vesting,
vault: TokenAccount,
vault_acc_info: &'a AccountInfo<'b>,
beneficiary: Pubkey,
safe_acc_info: &'a AccountInfo<'b>,
depositor_acc_info: &'a AccountInfo<'b>,
depositor_authority_acc_info: &'a AccountInfo<'b>,
token_program_acc_info: &'a AccountInfo<'b>,
}

View File

@ -1,4 +1,4 @@
use crate::access_control;
use crate::common::access_control;
use serum_common::pack::Pack;
use serum_lockup::accounts::{Safe, Whitelist};
use serum_lockup::error::{LockupError, LockupErrorCode};
@ -31,11 +31,10 @@ pub fn handler(
&mut safe_acc_info.try_borrow_mut_data()?,
&mut |safe: &mut Safe| {
state_transition(StateTransitionRequest {
safe,
safe_addr: safe_acc_info.key,
whitelist: Whitelist::new(whitelist_acc_info.clone())?,
whitelist_addr: whitelist_acc_info.key,
authority,
safe,
safe_acc_info,
whitelist_acc_info,
})
.map_err(Into::into)
},
@ -73,7 +72,7 @@ fn access_control(req: AccessControlRequest) -> Result<(), LockupError> {
}
}
// Whitelist (not yet set on Safe).
// Whitelist (uninitialized).
{
if whitelist_acc_info.owner != program_id {
return Err(LockupErrorCode::InvalidAccountOwner)?;
@ -97,36 +96,33 @@ fn state_transition(req: StateTransitionRequest) -> Result<(), LockupError> {
let StateTransitionRequest {
safe,
safe_addr,
safe_acc_info,
authority,
whitelist,
whitelist_addr,
whitelist_acc_info,
} = req;
// Initialize Safe.
safe.initialized = true;
safe.authority = authority;
safe.whitelist = *whitelist_addr;
safe.whitelist = *whitelist_acc_info.key;
// Inittialize Whitelist.
whitelist.set_safe(safe_addr)?;
info!("state-transition: success");
let whitelist = Whitelist::new(whitelist_acc_info.clone())?;
whitelist.set_safe(safe_acc_info.key)?;
Ok(())
}
struct AccessControlRequest<'a, 'b> {
program_id: &'a Pubkey,
safe_acc_info: &'a AccountInfo<'b>,
whitelist_acc_info: &'a AccountInfo<'b>,
rent_acc_info: &'a AccountInfo<'b>,
program_id: &'a Pubkey,
}
struct StateTransitionRequest<'a, 'b> {
safe_acc_info: &'a AccountInfo<'b>,
whitelist_acc_info: &'a AccountInfo<'b>,
safe: &'a mut Safe,
safe_addr: &'a Pubkey,
whitelist_addr: &'a Pubkey,
whitelist: Whitelist<'b>,
authority: Pubkey,
}

View File

@ -1,5 +1,3 @@
//! Program entrypoint.
#![cfg_attr(feature = "strict", deny(warnings))]
use serum_common::pack::Pack;
@ -9,16 +7,16 @@ use solana_sdk::account_info::AccountInfo;
use solana_sdk::entrypoint::ProgramResult;
use solana_sdk::pubkey::Pubkey;
pub(crate) mod access_control;
mod available_for_withdrawal;
mod common;
mod create_vesting;
mod initialize;
mod redeem;
mod set_authority;
mod whitelist_add;
mod whitelist_delete;
mod whitelist_deposit;
mod whitelist_withdraw;
mod withdraw;
solana_sdk::entrypoint!(entry);
fn entry(program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult {
@ -44,7 +42,7 @@ fn entry(program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8])
deposit_amount,
nonce,
),
LockupInstruction::Redeem { amount } => redeem::handler(program_id, accounts, amount),
LockupInstruction::Withdraw { amount } => withdraw::handler(program_id, accounts, amount),
LockupInstruction::WhitelistWithdraw {
amount,
instruction_data,

View File

@ -1,4 +1,4 @@
use crate::access_control;
use crate::common::access_control;
use serum_common::pack::Pack;
use serum_lockup::accounts::Safe;
use serum_lockup::error::LockupError;
@ -51,17 +51,9 @@ fn access_control(req: AccessControlRequest) -> Result<(), LockupError> {
// Governance authorization.
let _ = access_control::governance(program_id, safe_acc_info, safe_authority_acc_info)?;
info!("access-control: success");
Ok(())
}
struct AccessControlRequest<'a, 'b> {
program_id: &'a Pubkey,
safe_acc_info: &'a AccountInfo<'b>,
safe_authority_acc_info: &'a AccountInfo<'b>,
}
fn state_transition(req: StateTransitionRequest) -> Result<(), LockupError> {
info!("state-transition: set_authority");
@ -72,11 +64,15 @@ fn state_transition(req: StateTransitionRequest) -> Result<(), LockupError> {
safe_acc.authority = new_authority;
info!("state-transition: success");
Ok(())
}
struct AccessControlRequest<'a, 'b> {
program_id: &'a Pubkey,
safe_acc_info: &'a AccountInfo<'b>,
safe_authority_acc_info: &'a AccountInfo<'b>,
}
struct StateTransitionRequest<'a> {
safe_acc: &'a mut Safe,
new_authority: Pubkey,

View File

@ -1,4 +1,4 @@
use crate::access_control;
use crate::common::access_control;
use serum_lockup::accounts::{Whitelist, WhitelistEntry};
use serum_lockup::error::{LockupError, LockupErrorCode};
use solana_program::info;
@ -54,8 +54,6 @@ fn access_control(req: AccessControlRequest) -> Result<(), LockupError> {
// Must be a valid derived address.
let _ = wl_entry.derived_address()?;
info!("access-control: success");
Ok(())
}
@ -71,8 +69,6 @@ fn state_transition(req: StateTransitionRequest) -> Result<(), LockupError> {
.push(wl_entry)?
.ok_or(LockupErrorCode::WhitelistFull)?;
info!("state-transition: success");
Ok(())
}

View File

@ -1,4 +1,4 @@
use crate::access_control;
use crate::common::access_control;
use serum_lockup::accounts::{Whitelist, WhitelistEntry};
use serum_lockup::error::{LockupError, LockupErrorCode};
use solana_program::info;
@ -50,8 +50,6 @@ fn access_control(req: AccessControlRequest) -> Result<(), LockupError> {
let _ =
access_control::whitelist(whitelist_acc_info.clone(), safe_acc_info, &safe, program_id)?;
info!("access-control: success");
Ok(())
}
@ -67,8 +65,6 @@ fn state_transition(req: StateTransitionRequest) -> Result<(), LockupError> {
.delete(wl_entry)?
.ok_or(LockupErrorCode::WhitelistNotFound)?;
info!("state-transition: success");
Ok(())
}

View File

@ -1,6 +1,6 @@
use crate::access_control;
use crate::common::{access_control, whitelist_cpi};
use serum_common::pack::Pack;
use serum_lockup::accounts::{vault, Vesting};
use serum_lockup::accounts::Vesting;
use serum_lockup::error::{LockupError, LockupErrorCode};
use solana_program::info;
use solana_sdk::account_info::{next_account_info, AccountInfo};
@ -8,11 +8,12 @@ use solana_sdk::instruction::{AccountMeta, Instruction};
use solana_sdk::program_pack::Pack as TokenPack;
use solana_sdk::pubkey::Pubkey;
use std::convert::Into;
use std::iter::Iterator;
pub fn handler(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: Vec<u8>,
ref instruction_data: Vec<u8>,
) -> Result<(), LockupError> {
info!("handler: whitelist_deposit");
@ -30,10 +31,11 @@ pub fn handler(
let vault_acc_info = next_account_info(acc_infos)?;
let vault_auth_acc_info = next_account_info(acc_infos)?;
let tok_prog_acc_info = next_account_info(acc_infos)?;
let wl_prog_vault_acc_info = next_account_info(acc_infos)?;
let wl_prog_vault_authority_acc_info = next_account_info(acc_infos)?;
// Program specific.
let remaining_relay_accs: Vec<&AccountInfo> = acc_infos.collect();
let remaining_relay_accs = acc_infos;
access_control(AccessControlRequest {
program_id,
@ -41,6 +43,7 @@ pub fn handler(
vesting_acc_info,
wl_acc_info,
wl_prog_acc_info,
wl_prog_vault_acc_info,
wl_prog_vault_authority_acc_info,
safe_acc_info,
vault_acc_info,
@ -52,17 +55,17 @@ pub fn handler(
&mut |vesting: &mut Vesting| {
state_transition(StateTransitionRequest {
accounts,
instruction_data: instruction_data.clone(),
safe_acc: safe_acc_info.key,
nonce: vesting.nonce,
instruction_data,
safe_acc_info,
wl_prog_acc_info,
wl_prog_vault_acc_info,
wl_prog_vault_authority_acc_info,
vault_acc_info,
vault_auth_acc_info,
tok_prog_acc_info,
vesting,
beneficiary_acc_info,
remaining_relay_accs: remaining_relay_accs.clone(),
remaining_relay_accs,
})
.map_err(Into::into)
},
@ -80,6 +83,7 @@ fn access_control(req: AccessControlRequest) -> Result<(), LockupError> {
vesting_acc_info,
wl_acc_info,
wl_prog_acc_info,
wl_prog_vault_acc_info,
wl_prog_vault_authority_acc_info,
safe_acc_info,
vault_acc_info,
@ -105,20 +109,25 @@ fn access_control(req: AccessControlRequest) -> Result<(), LockupError> {
)?;
let _vesting = access_control::vesting(
program_id,
safe_acc_info.key,
safe_acc_info,
vesting_acc_info,
beneficiary_acc_info,
)?;
// WhitelistDeposit checks.
//
// Is the given program on the whitelist?
let entry = whitelist
.get_derived(wl_prog_vault_authority_acc_info.key)?
.ok_or(LockupErrorCode::WhitelistNotFound)?;
if entry.program_id() != *wl_prog_acc_info.key {
return Err(LockupErrorCode::WhitelistInvalidProgramId)?;
}
info!("access-control: success");
// Is the vault owned by this whitelisted authority?
let wl_vault = access_control::token(wl_prog_vault_acc_info)?;
if &wl_vault.owner != wl_prog_vault_authority_acc_info.key {
return Err(LockupErrorCode::InvalidTokenAccountOwner)?;
}
Ok(())
}
@ -130,18 +139,17 @@ fn state_transition(req: StateTransitionRequest) -> Result<(), LockupError> {
vesting,
instruction_data,
accounts,
nonce,
safe_acc,
safe_acc_info,
vault_acc_info,
vault_auth_acc_info,
wl_prog_acc_info,
wl_prog_vault_acc_info,
wl_prog_vault_authority_acc_info,
remaining_relay_accs,
tok_prog_acc_info,
beneficiary_acc_info,
} = req;
// Check before balance.
let before_amount = {
let vault = spl_token::state::Account::unpack(&vault_acc_info.try_borrow_data()?)?;
vault.amount
@ -153,44 +161,47 @@ fn state_transition(req: StateTransitionRequest) -> Result<(), LockupError> {
AccountMeta::new(*vault_acc_info.key, false),
AccountMeta::new_readonly(*vault_auth_acc_info.key, true),
AccountMeta::new_readonly(*tok_prog_acc_info.key, false),
AccountMeta::new(*wl_prog_vault_acc_info.key, false),
AccountMeta::new_readonly(*wl_prog_vault_authority_acc_info.key, false),
];
for a in remaining_relay_accs {
meta_accounts.extend(remaining_relay_accs.map(|a| {
if a.is_writable {
meta_accounts.push(AccountMeta::new(*a.key, a.is_signer));
AccountMeta::new(*a.key, a.is_signer)
} else {
meta_accounts.push(AccountMeta::new_readonly(*a.key, a.is_signer));
AccountMeta::new_readonly(*a.key, a.is_signer)
}
}
let mut data = serum_lockup::instruction::TAG.to_le_bytes().to_vec();
data.extend(instruction_data);
}));
let relay_instruction = Instruction {
program_id: *wl_prog_acc_info.key,
accounts: meta_accounts,
data,
data: instruction_data.to_vec(),
};
let signer_seeds = vault::signer_seeds(safe_acc, beneficiary_acc_info.key, &nonce);
solana_sdk::program::invoke_signed(&relay_instruction, &accounts[..], &[&signer_seeds])?;
whitelist_cpi(
relay_instruction,
safe_acc_info.key,
beneficiary_acc_info,
vesting,
accounts,
)?;
}
// Update vesting account with the deposit.
{
let after_amount = {
let vault = spl_token::state::Account::unpack(&vault_acc_info.try_borrow_data()?)?;
let deposit_amount = vault.amount - before_amount;
vault.amount
};
// Safety checks.
//
// Balance must go up.
if deposit_amount <= 0 {
return Err(LockupErrorCode::InsufficientDepositAmount)?;
}
// Cannot deposit more than withdrawn.
if deposit_amount > vesting.whitelist_owned {
return Err(LockupErrorCode::DepositOverflow)?;
}
vesting.whitelist_owned -= deposit_amount;
// Deposit safety checks.
let deposit_amount = after_amount - before_amount;
// Balance must go up.
if deposit_amount <= 0 {
return Err(LockupErrorCode::InsufficientDepositAmount)?;
}
// Cannot deposit more than withdrawn.
if deposit_amount > vesting.whitelist_owned {
return Err(LockupErrorCode::DepositOverflow)?;
}
// Book keeping.
vesting.whitelist_owned -= deposit_amount;
Ok(())
}
@ -199,6 +210,7 @@ struct AccessControlRequest<'a, 'b> {
program_id: &'a Pubkey,
wl_acc_info: &'a AccountInfo<'b>,
wl_prog_acc_info: &'a AccountInfo<'b>,
wl_prog_vault_acc_info: &'a AccountInfo<'b>,
wl_prog_vault_authority_acc_info: &'a AccountInfo<'b>,
beneficiary_acc_info: &'a AccountInfo<'b>,
vesting_acc_info: &'a AccountInfo<'b>,
@ -208,16 +220,16 @@ struct AccessControlRequest<'a, 'b> {
}
struct StateTransitionRequest<'a, 'b, 'c> {
instruction_data: Vec<u8>,
vesting: &'c mut Vesting,
remaining_relay_accs: &'c mut dyn Iterator<Item = &'a AccountInfo<'b>>,
accounts: &'a [AccountInfo<'b>],
nonce: u8,
safe_acc: &'a Pubkey,
safe_acc_info: &'a AccountInfo<'b>,
vault_acc_info: &'a AccountInfo<'b>,
vault_auth_acc_info: &'a AccountInfo<'b>,
wl_prog_acc_info: &'a AccountInfo<'b>,
wl_prog_vault_acc_info: &'a AccountInfo<'b>,
wl_prog_vault_authority_acc_info: &'a AccountInfo<'b>,
remaining_relay_accs: Vec<&'a AccountInfo<'b>>,
tok_prog_acc_info: &'a AccountInfo<'b>,
beneficiary_acc_info: &'a AccountInfo<'b>,
instruction_data: &'c [u8],
vesting: &'c mut Vesting,
}

View File

@ -1,19 +1,20 @@
use crate::access_control;
use crate::common::{access_control, whitelist_cpi};
use serum_common::pack::Pack;
use serum_lockup::accounts::{vault, Vesting};
use serum_lockup::accounts::Vesting;
use serum_lockup::error::{LockupError, LockupErrorCode};
use solana_program::info;
use solana_sdk::account_info::{next_account_info, AccountInfo};
use solana_sdk::instruction::{AccountMeta, Instruction};
use solana_sdk::program_pack::Pack as TokenPack;
use solana_sdk::pubkey::Pubkey;
use spl_token::state::Account as TokenAccount;
use std::convert::Into;
pub fn handler(
program_id: &Pubkey,
accounts: &[AccountInfo],
amount: u64,
instruction_data: Vec<u8>,
ref instruction_data: Vec<u8>,
) -> Result<(), LockupError> {
info!("handler: whitelist_withdraw");
@ -24,7 +25,6 @@ pub fn handler(
let safe_acc_info = next_account_info(acc_infos)?;
let wl_acc_info = next_account_info(acc_infos)?;
let wl_prog_acc_info = next_account_info(acc_infos)?;
let wl_prog_vault_authority_acc_info = next_account_info(acc_infos)?;
// Below accounts are relayed.
@ -32,9 +32,11 @@ pub fn handler(
let vault_acc_info = next_account_info(acc_infos)?;
let vault_auth_acc_info = next_account_info(acc_infos)?;
let tok_prog_acc_info = next_account_info(acc_infos)?;
let wl_prog_vault_acc_info = next_account_info(acc_infos)?;
let wl_prog_vault_authority_acc_info = next_account_info(acc_infos)?;
// Program specific.
let remaining_relay_accs: Vec<&AccountInfo> = acc_infos.collect();
let remaining_relay_accs = acc_infos;
access_control(AccessControlRequest {
program_id,
@ -42,6 +44,7 @@ pub fn handler(
vesting_acc_info,
wl_acc_info,
wl_prog_acc_info,
wl_prog_vault_acc_info,
wl_prog_vault_authority_acc_info,
safe_acc_info,
vault_acc_info,
@ -55,17 +58,17 @@ pub fn handler(
state_transition(StateTransitionRequest {
accounts,
amount,
instruction_data: instruction_data.clone(),
safe_acc: safe_acc_info.key,
nonce: vesting.nonce,
instruction_data,
safe_acc_info,
wl_prog_acc_info,
wl_prog_vault_acc_info,
wl_prog_vault_authority_acc_info,
vault_acc_info,
vault_auth_acc_info,
tok_prog_acc_info,
vesting,
beneficiary_acc_info,
remaining_relay_accs: remaining_relay_accs.clone(),
remaining_relay_accs,
})
.map_err(Into::into)
},
@ -82,6 +85,7 @@ fn access_control(req: AccessControlRequest) -> Result<(), LockupError> {
beneficiary_acc_info,
vesting_acc_info,
wl_acc_info,
wl_prog_vault_acc_info,
wl_prog_acc_info,
wl_prog_vault_authority_acc_info,
safe_acc_info,
@ -99,7 +103,7 @@ fn access_control(req: AccessControlRequest) -> Result<(), LockupError> {
let safe = access_control::safe(safe_acc_info, program_id)?;
let whitelist =
access_control::whitelist(wl_acc_info.clone(), safe_acc_info, &safe, program_id)?;
let _ = access_control::vault(
let _vault = access_control::vault(
vault_acc_info,
vault_auth_acc_info,
vesting_acc_info,
@ -109,21 +113,29 @@ fn access_control(req: AccessControlRequest) -> Result<(), LockupError> {
)?;
let vesting = access_control::vesting(
program_id,
safe_acc_info.key,
safe_acc_info,
vesting_acc_info,
beneficiary_acc_info,
)?;
// WhitelistWithdraw checks.
if amount > vesting.available_for_whitelist() {
//
// Do we have sufficient balance?
if amount > vesting.balance() {
return Err(LockupErrorCode::InsufficientWhitelistBalance)?;
}
// Is the given program on the whitelist?
let entry = whitelist
.get_derived(wl_prog_vault_authority_acc_info.key)?
.ok_or(LockupErrorCode::WhitelistNotFound)?;
if entry.program_id() != *wl_prog_acc_info.key {
return Err(LockupErrorCode::WhitelistInvalidProgramId)?;
}
// Is the vault owned by this whitelisted authority?
let wl_vault = access_control::token(wl_prog_vault_acc_info)?;
if &wl_vault.owner != wl_prog_vault_authority_acc_info.key {
return Err(LockupErrorCode::InvalidTokenAccountOwner)?;
}
Ok(())
}
@ -137,68 +149,70 @@ fn state_transition(req: StateTransitionRequest) -> Result<(), LockupError> {
beneficiary_acc_info,
accounts,
amount,
nonce,
safe_acc,
safe_acc_info,
vault_acc_info,
wl_prog_acc_info,
wl_prog_vault_authority_acc_info: _,
wl_prog_vault_acc_info,
wl_prog_vault_authority_acc_info,
remaining_relay_accs,
tok_prog_acc_info,
vault_auth_acc_info,
} = req;
let before_amount = {
let vault = spl_token::state::Account::unpack(&vault_acc_info.try_borrow_data()?)?;
let vault = TokenAccount::unpack(&vault_acc_info.try_borrow_data()?)?;
vault.amount
};
// Invoke relay.
{
let signer_seeds = vault::signer_seeds(safe_acc, beneficiary_acc_info.key, &nonce);
let mut meta_accounts = vec![
AccountMeta::new(*vault_acc_info.key, false),
AccountMeta::new_readonly(*vault_auth_acc_info.key, true),
AccountMeta::new_readonly(*tok_prog_acc_info.key, false),
AccountMeta::new(*wl_prog_vault_acc_info.key, false),
AccountMeta::new_readonly(*wl_prog_vault_authority_acc_info.key, false),
];
for a in remaining_relay_accs {
meta_accounts.extend(remaining_relay_accs.map(|a| {
if a.is_writable {
meta_accounts.push(AccountMeta::new(*a.key, a.is_signer));
AccountMeta::new(*a.key, a.is_signer)
} else {
meta_accounts.push(AccountMeta::new_readonly(*a.key, a.is_signer));
AccountMeta::new_readonly(*a.key, a.is_signer)
}
}
let mut data = serum_lockup::instruction::TAG.to_le_bytes().to_vec();
data.extend(instruction_data);
}));
let relay_instruction = Instruction {
program_id: *wl_prog_acc_info.key,
accounts: meta_accounts,
data,
data: instruction_data.to_vec(),
};
solana_sdk::program::invoke_signed(&relay_instruction, &accounts[..], &[&signer_seeds])?;
whitelist_cpi(
relay_instruction,
safe_acc_info.key,
beneficiary_acc_info,
vesting,
accounts,
)?;
}
// Check the amount transferred is valid. If not abort.
let amount_transferred = {
let after_amount = {
let vault = spl_token::state::Account::unpack(&vault_acc_info.try_borrow_data()?)?;
vault.amount
};
before_amount - after_amount
let after_amount = {
let vault = TokenAccount::unpack(&vault_acc_info.try_borrow_data()?)?;
vault.amount
};
// Withdrawal safety checks.
let amount_transferred = before_amount - after_amount;
// Is the amount transferred valid?
if amount_transferred > amount {
return Err(LockupErrorCode::InsufficientAmount)?;
}
// Update vesting account.
// Book keeping.
vesting.whitelist_owned += amount_transferred;
Ok(())
}
struct AccessControlRequest<'a, 'b> {
program_id: &'a Pubkey,
wl_prog_vault_acc_info: &'a AccountInfo<'b>,
beneficiary_acc_info: &'a AccountInfo<'b>,
vesting_acc_info: &'a AccountInfo<'b>,
safe_acc_info: &'a AccountInfo<'b>,
@ -207,21 +221,22 @@ struct AccessControlRequest<'a, 'b> {
wl_acc_info: &'a AccountInfo<'b>,
wl_prog_acc_info: &'a AccountInfo<'b>,
wl_prog_vault_authority_acc_info: &'a AccountInfo<'b>,
program_id: &'a Pubkey,
amount: u64,
}
struct StateTransitionRequest<'a, 'b, 'c> {
instruction_data: Vec<u8>,
vesting: &'c mut Vesting,
remaining_relay_accs: &'c mut dyn Iterator<Item = &'a AccountInfo<'b>>,
accounts: &'a [AccountInfo<'b>],
amount: u64,
nonce: u8,
safe_acc: &'a Pubkey,
vault_acc_info: &'a AccountInfo<'b>,
wl_prog_acc_info: &'a AccountInfo<'b>,
wl_prog_vault_acc_info: &'a AccountInfo<'b>,
wl_prog_vault_authority_acc_info: &'a AccountInfo<'b>,
remaining_relay_accs: Vec<&'a AccountInfo<'b>>,
tok_prog_acc_info: &'a AccountInfo<'b>,
vault_auth_acc_info: &'a AccountInfo<'b>,
beneficiary_acc_info: &'a AccountInfo<'b>,
safe_acc_info: &'a AccountInfo<'b>,
instruction_data: &'c [u8],
vesting: &'c mut Vesting,
amount: u64,
}

View File

@ -1,5 +1,6 @@
use crate::access_control;
use crate::common::access_control;
use serum_common::pack::Pack;
use serum_common::program::invoke_token_transfer;
use serum_lockup::accounts::{vault, Vesting};
use serum_lockup::error::{LockupError, LockupErrorCode};
use solana_program::info;
@ -12,13 +13,13 @@ pub fn handler(
accounts: &[AccountInfo],
amount: u64,
) -> Result<(), LockupError> {
info!("handler: redeem");
info!("handler: withdraw");
let acc_infos = &mut accounts.iter();
let beneficiary_acc_info = next_account_info(acc_infos)?;
let vesting_acc_info = next_account_info(acc_infos)?;
let beneficiary_token_acc_info = next_account_info(acc_infos)?;
let token_acc_info = next_account_info(acc_infos)?;
let vault_acc_info = next_account_info(acc_infos)?;
let vault_authority_acc_info = next_account_info(acc_infos)?;
let safe_acc_info = next_account_info(acc_infos)?;
@ -44,7 +45,7 @@ pub fn handler(
vesting,
vault_acc_info,
vault_authority_acc_info,
beneficiary_token_acc_info,
token_acc_info,
safe_acc_info,
token_program_acc_info,
beneficiary_acc_info,
@ -56,7 +57,7 @@ pub fn handler(
}
fn access_control(req: AccessControlRequest) -> Result<(), LockupError> {
info!("access-control: redeem");
info!("access-control: withdraw");
let AccessControlRequest {
program_id,
@ -69,20 +70,20 @@ fn access_control(req: AccessControlRequest) -> Result<(), LockupError> {
clock_acc_info,
} = req;
// Beneficiary authorization.
// Authorization.
if !beneficiary_acc_info.is_signer {
return Err(LockupErrorCode::Unauthorized)?;
}
// Account validation.
let _ = access_control::safe(safe_acc_info, program_id)?;
let _safe = access_control::safe(safe_acc_info, program_id)?;
let vesting = access_control::vesting(
program_id,
safe_acc_info.key,
safe_acc_info,
vesting_acc_info,
beneficiary_acc_info,
)?;
let _ = access_control::vault(
let _vault = access_control::vault(
vault_acc_info,
vault_authority_acc_info,
vesting_acc_info,
@ -90,62 +91,44 @@ fn access_control(req: AccessControlRequest) -> Result<(), LockupError> {
safe_acc_info,
program_id,
)?;
let clock = access_control::clock(clock_acc_info)?;
// Redemption checks.
{
let clock = access_control::clock(clock_acc_info)?;
if amount == 0 || amount > vesting.available_for_withdrawal(clock.unix_timestamp) {
return Err(LockupErrorCode::InsufficientWithdrawalBalance)?;
}
// Withdrawal checks.
if amount == 0 || amount > vesting.available_for_withdrawal(clock.unix_timestamp) {
return Err(LockupErrorCode::InsufficientWithdrawalBalance)?;
}
Ok(())
}
fn state_transition(req: StateTransitionRequest) -> Result<(), LockupError> {
info!("state-transition: redeem");
info!("state-transition: withdraw");
let StateTransitionRequest {
vesting,
amount,
vault_acc_info,
vault_authority_acc_info,
beneficiary_token_acc_info,
token_acc_info,
safe_acc_info,
token_program_acc_info,
beneficiary_acc_info,
} = req;
// Remove the withdrawn token from the vesting account.
{
vesting.deduct(amount);
}
// Transfer token from the vault to the user address.
{
let withdraw_instruction = spl_token::instruction::transfer(
&spl_token::ID,
vault_acc_info.key,
beneficiary_token_acc_info.key,
&vault_authority_acc_info.key,
&[],
amount,
)?;
let signer_seeds =
vault::signer_seeds(safe_acc_info.key, beneficiary_acc_info.key, &vesting.nonce);
invoke_token_transfer(
vault_acc_info,
token_acc_info,
vault_authority_acc_info,
token_program_acc_info,
&[&signer_seeds],
amount,
)?;
let signer_seeds =
vault::signer_seeds(safe_acc_info.key, beneficiary_acc_info.key, &vesting.nonce);
solana_sdk::program::invoke_signed(
&withdraw_instruction,
&[
vault_acc_info.clone(),
beneficiary_token_acc_info.clone(),
vault_authority_acc_info.clone(),
token_program_acc_info.clone(),
],
&[&signer_seeds],
)?;
}
// Update bookeeping.
vesting.outstanding -= amount;
Ok(())
}
@ -165,7 +148,7 @@ struct StateTransitionRequest<'a, 'b, 'c> {
amount: u64,
vesting: &'c mut Vesting,
safe_acc_info: &'a AccountInfo<'b>,
beneficiary_token_acc_info: &'a AccountInfo<'b>,
token_acc_info: &'a AccountInfo<'b>,
vault_acc_info: &'a AccountInfo<'b>,
vault_authority_acc_info: &'a AccountInfo<'b>,
token_program_acc_info: &'a AccountInfo<'b>,

View File

@ -18,15 +18,15 @@ pub struct Vesting {
/// The owner of this Vesting account. If not set, then the account
/// is allocated but needs to be assigned.
pub beneficiary: Pubkey,
/// The mint of the SPL token the safe is storing, e.g., the SRM mint.
/// The mint of the SPL token locked up.
pub mint: Pubkey,
/// Address of the token vault controlled by the Safe.
/// Address of the account's token vault.
pub vault: Pubkey,
/// The owner of the token account funding this account.
pub grantor: Pubkey,
/// The outstanding SRM deposit backing this vesting account. All
/// withdrawals/redemptions will deduct this balance.
pub balance: u64,
/// withdrawals will deduct this balance.
pub outstanding: u64,
/// The starting balance of this vesting account, i.e., how much was
/// originally deposited.
pub start_balance: u64,
@ -40,38 +40,31 @@ pub struct Vesting {
pub period_count: u64,
/// The amount of tokens in custody of whitelisted programs.
pub whitelist_owned: u64,
///
/// Signer nonce.
pub nonce: u8,
}
impl Vesting {
/// Deducts the given amount from the vesting account upon
/// withdrawal/redemption.
pub fn deduct(&mut self, amount: u64) {
self.balance -= amount;
}
/// Returns the amount available for withdrawal as of the given ts.
/// The amount for withdrawal is not necessarily the balance vested
/// since funds can be sent to whitelisted programs. For this reason,
/// take minimum of the availble balance vested and the available balance
/// for sending to whitelisted programs.
pub fn available_for_withdrawal(&self, current_ts: i64) -> u64 {
std::cmp::min(
self.balance_vested(current_ts),
self.available_for_whitelist(),
)
std::cmp::min(self.outstanding_vested(current_ts), self.balance())
}
/// Amount available for whitelisted programs to transfer.
pub fn available_for_whitelist(&self) -> u64 {
self.balance - self.whitelist_owned
// The amount of funds left in the vault.
pub fn balance(&self) -> u64 {
self.outstanding.checked_sub(self.whitelist_owned).unwrap()
}
// The amount vested that's available for withdrawal, if no funds were ever
// sent to another program.
fn balance_vested(&self, current_ts: i64) -> u64 {
self.total_vested(current_ts) - self.withdrawn_amount()
// The amount of outstanding locked tokens vested. Note that these
// tokens might have been transferred to whitelisted programs.
fn outstanding_vested(&self, current_ts: i64) -> u64 {
self.total_vested(current_ts)
.checked_sub(self.withdrawn_amount())
.unwrap()
}
// Returns the amount withdrawn from this vesting account.
fn withdrawn_amount(&self) -> u64 {
self.start_balance.checked_sub(self.outstanding).unwrap()
}
// Returns the total vested amount up to the given ts, assuming zero
@ -82,38 +75,49 @@ impl Vesting {
if current_ts >= self.end_ts {
return self.start_balance;
}
self.linear_unlock(current_ts)
self.linear_unlock(current_ts).unwrap()
}
// Returns the amount withdrawn from this vesting account.
fn withdrawn_amount(&self) -> u64 {
self.start_balance - self.balance
}
fn linear_unlock(&self, current_ts: i64) -> Option<u64> {
// LLVM doesn't support signed division.
let current_ts = current_ts as u64;
let start_ts = self.start_ts as u64;
let end_ts = self.end_ts as u64;
fn linear_unlock(&self, current_ts: i64) -> u64 {
let (end_ts, start_ts) = {
// If we can't perfectly partition the vesting window,
// push the start window back so that we can.
//
// This has the effect of making the first vesting period act as
// a minor "cliff" that vests slightly more than the rest of the
// periods.
let overflow = (self.end_ts - self.start_ts) as u64 % self.period_count;
if overflow != 0 {
(self.end_ts, self.start_ts - overflow as i64)
} else {
(self.end_ts, self.start_ts)
}
};
// If we can't perfectly partition the vesting window,
// push the start of the window window back so that we can.
//
// This has the effect of making the first vesting period shorter
// than the rest.
let shifted_start_ts =
start_ts.checked_sub(end_ts.checked_sub(start_ts)? % self.period_count)?;
let vested_period_count = {
let period = (end_ts - start_ts) as u64 / self.period_count;
let current_period_count = (current_ts - start_ts) as u64 / period;
// Similarly, if we can't perfectly divide up the vesting rewards
// then make the first period act as a cliff, earning slightly more than
// subsequent periods.
let reward_overflow = self.start_balance % self.period_count;
// Reward per period ignoring the overflow.
let reward_per_period = (self.start_balance.checked_sub(reward_overflow)?)
.checked_div(self.period_count)
.unwrap();
// Number of vesting periods that have passed.
let current_period = {
let period_secs =
(end_ts.checked_sub(shifted_start_ts)?).checked_div(self.period_count)?;
let current_period_count =
(current_ts.checked_sub(shifted_start_ts)?).checked_div(period_secs)?;
std::cmp::min(current_period_count, self.period_count)
};
let reward_per_period = self.start_balance / self.period_count;
vested_period_count * reward_per_period
if current_period == 0 {
return Some(0);
}
current_period
.checked_mul(reward_per_period)?
.checked_add(reward_overflow)
}
}
@ -132,20 +136,20 @@ mod tests {
let beneficiary = Keypair::generate(&mut OsRng).pubkey();
let initialized = true;
let start_balance = 10;
let balance = start_balance;
let outstanding = start_balance;
let start_ts = 11;
let end_ts = 12;
let period_count = 13;
let whitelist_owned = 14;
let grantor = Pubkey::new_rand();
let mint = Pubkey::new_rand();
let vault = Pubkey::new_rand();
let grantor = Pubkey::new_unique();
let mint = Pubkey::new_unique();
let vault = Pubkey::new_unique();
let nonce = 0;
let vesting_acc = Vesting {
safe,
beneficiary,
initialized,
balance,
outstanding,
start_balance,
start_ts,
end_ts,
@ -169,7 +173,7 @@ mod tests {
assert_eq!(va.beneficiary, beneficiary);
assert_eq!(va.initialized, initialized);
assert_eq!(va.start_balance, start_balance);
assert_eq!(va.balance, balance);
assert_eq!(va.outstanding, outstanding);
assert_eq!(va.start_ts, start_ts);
assert_eq!(va.end_ts, end_ts);
assert_eq!(va.period_count, period_count);
@ -181,23 +185,23 @@ mod tests {
fn available_for_withdrawal() {
let safe = Keypair::generate(&mut OsRng).pubkey();
let beneficiary = Keypair::generate(&mut OsRng).pubkey();
let balance = 10;
let outstanding = 10;
let start_balance = 10;
let start_ts = 10;
let end_ts = 20;
let period_count = 5;
let initialized = true;
let whitelist_owned = 0;
let grantor = Pubkey::new_rand();
let mint = Pubkey::new_rand();
let vault = Pubkey::new_rand();
let grantor = Pubkey::new_unique();
let mint = Pubkey::new_unique();
let vault = Pubkey::new_unique();
let nonce = 0;
let vesting_acc = Vesting {
safe,
beneficiary,
initialized,
whitelist_owned,
balance,
outstanding,
start_balance,
start_ts,
end_ts,
@ -217,6 +221,51 @@ mod tests {
assert_eq!(10, vesting_acc.available_for_withdrawal(100));
}
#[test]
fn available_for_withdrawal_cliff() {
let safe = Keypair::generate(&mut OsRng).pubkey();
let beneficiary = Keypair::generate(&mut OsRng).pubkey();
let outstanding = 11;
let start_balance = 11;
let start_ts = 10;
let end_ts = 20;
let period_count = 10;
let initialized = true;
let whitelist_owned = 0;
let grantor = Pubkey::new_unique();
let mint = Pubkey::new_unique();
let vault = Pubkey::new_unique();
let nonce = 0;
let vesting_acc = Vesting {
safe,
beneficiary,
initialized,
whitelist_owned,
outstanding,
start_balance,
start_ts,
end_ts,
period_count,
grantor,
mint,
nonce,
vault,
};
assert_eq!(0, vesting_acc.available_for_withdrawal(10));
assert_eq!(2, vesting_acc.available_for_withdrawal(11));
assert_eq!(3, vesting_acc.available_for_withdrawal(12));
assert_eq!(4, vesting_acc.available_for_withdrawal(13));
assert_eq!(5, vesting_acc.available_for_withdrawal(14));
assert_eq!(6, vesting_acc.available_for_withdrawal(15));
assert_eq!(7, vesting_acc.available_for_withdrawal(16));
assert_eq!(8, vesting_acc.available_for_withdrawal(17));
assert_eq!(9, vesting_acc.available_for_withdrawal(18));
assert_eq!(10, vesting_acc.available_for_withdrawal(19));
assert_eq!(11, vesting_acc.available_for_withdrawal(20));
assert_eq!(11, vesting_acc.available_for_withdrawal(21));
assert_eq!(11, vesting_acc.available_for_withdrawal(2100));
}
#[test]
fn unpack_zeroes() {
let og_size = Vesting::default().size().unwrap();
@ -226,7 +275,7 @@ mod tests {
assert_eq!(r.initialized, false);
assert_eq!(r.safe, Pubkey::new(&[0; 32]));
assert_eq!(r.beneficiary, Pubkey::new(&[0; 32]));
assert_eq!(r.balance, 0);
assert_eq!(r.outstanding, 0);
assert_eq!(r.start_ts, 0);
assert_eq!(r.end_ts, 0);
assert_eq!(r.period_count, 0);

View File

@ -59,8 +59,7 @@ pub mod instruction {
/// 5 `[]` Safe.
/// 8. `[]` SPL token program.
/// 9. `[]` Clock sysvar.
// todo: rename
Redeem { amount: u64 },
Withdraw { amount: u64 },
/// Accounts:
///
/// 0. `[signer]` Beneficiary.

View File

@ -116,7 +116,7 @@ fn lifecycle() {
pass_time(client.rpc(), wait_ts);
}
// Redeem 10 SRM.
// Withdraw 10 SRM.
//
// Current state:
//
@ -134,22 +134,22 @@ fn lifecycle() {
)
.unwrap();
let redeem_amount = 10;
let withdraw_amount = 10;
let _ = client
.redeem(RedeemRequest {
.withdraw(WithdrawRequest {
beneficiary: &expected_beneficiary,
vesting,
token_account: bene_tok_acc.pubkey(),
safe: safe_acc,
amount: redeem_amount,
amount: withdraw_amount,
})
.unwrap();
// The SRM account should be increased.
let bene_tok =
rpc::account_token_unpacked::<TokenAccount>(client.rpc(), &bene_tok_acc.pubkey());
assert_eq!(bene_tok.amount, redeem_amount);
assert_eq!(bene_tok.amount, withdraw_amount);
}
}

View File

@ -273,18 +273,20 @@ impl Client {
pool_program_id,
} = req;
let vault = self.vault_for(&registrar, &depositor)?;
let vault_acc = rpc::get_token_account::<TokenAccount>(self.inner.rpc(), &vault)?;
let mut accounts = vec![
// Whitelist relay interface,
AccountMeta::new(depositor, false),
AccountMeta::new(depositor_authority.pubkey(), true),
AccountMeta::new_readonly(spl_token::ID, false),
AccountMeta::new(vault, false),
AccountMeta::new(vault_acc.owner, false),
// Program specific.
AccountMeta::new(member, false),
AccountMeta::new_readonly(beneficiary.pubkey(), true),
AccountMeta::new(entity, false),
AccountMeta::new_readonly(registrar, false),
AccountMeta::new_readonly(solana_sdk::sysvar::clock::ID, false),
AccountMeta::new(vault, false),
];
let (pool_accs, _) = self.common_pool_accounts(pool_program_id, registrar, false)?;
accounts.extend_from_slice(&pool_accs);
@ -307,14 +309,15 @@ impl Client {
amount,
pool_program_id,
} = req;
let r = self.registrar(&registrar)?;
let vault_acc = rpc::get_token_account::<TokenAccount>(self.inner.rpc(), &r.vault)?;
let vault = self.vault_for(&registrar, &depositor)?;
let r = self.registrar(&registrar)?;
let vault_acc = rpc::get_token_account::<TokenAccount>(self.inner.rpc(), &vault)?;
let mut accounts = vec![
// Whitelist relay interface.
AccountMeta::new(depositor, false),
AccountMeta::new_readonly(beneficiary.pubkey(), true),
AccountMeta::new_readonly(spl_token::ID, false),
AccountMeta::new(vault, false),
AccountMeta::new(vault_acc.owner, false),
// Program specific.
AccountMeta::new(member, false),
@ -322,7 +325,6 @@ impl Client {
AccountMeta::new(entity, false),
AccountMeta::new_readonly(registrar, false),
AccountMeta::new_readonly(solana_sdk::sysvar::clock::ID, false),
AccountMeta::new(vault, false),
];
let is_mega = vault == r.mega_vault; // TODO: remove is_mega.
let (pool_accs, _) = self.common_pool_accounts(pool_program_id, registrar, is_mega)?;

View File

@ -0,0 +1,2 @@
pub mod entity;
pub mod pool;

View File

@ -1,7 +1,7 @@
use crate::common::invoke_token_transfer;
use crate::entity::{with_entity, EntityContext};
use crate::pool::{pool_check, Pool, PoolConfig};
use crate::common::entity::{with_entity, EntityContext};
use crate::common::pool::{pool_check, Pool, PoolConfig};
use serum_common::pack::Pack;
use serum_common::program::invoke_token_transfer;
use serum_registry::access_control;
use serum_registry::accounts::{Entity, Member, Registrar};
use serum_registry::error::{RegistryError, RegistryErrorCode};
@ -26,6 +26,8 @@ pub fn handler(
let depositor_acc_info = next_account_info(acc_infos)?;
let depositor_authority_acc_info = next_account_info(acc_infos)?;
let token_program_acc_info = next_account_info(acc_infos)?;
let vault_acc_info = next_account_info(acc_infos)?;
let vault_authority_acc_info = next_account_info(acc_infos)?;
// Program specfic.
let member_acc_info = next_account_info(acc_infos)?;
@ -33,7 +35,6 @@ pub fn handler(
let entity_acc_info = next_account_info(acc_infos)?;
let registrar_acc_info = next_account_info(acc_infos)?;
let clock_acc_info = next_account_info(acc_infos)?;
let vault_acc_info = next_account_info(acc_infos)?;
let pool = &Pool::parse_accounts(acc_infos, PoolConfig::GetBasket)?;
@ -54,6 +55,7 @@ pub fn handler(
beneficiary_acc_info,
entity_acc_info,
vault_acc_info,
vault_authority_acc_info,
program_id,
registrar_acc_info,
registrar,
@ -94,6 +96,7 @@ fn access_control(req: AccessControlRequest) -> Result<AccessControlResponse, Re
beneficiary_acc_info,
entity_acc_info,
vault_acc_info,
vault_authority_acc_info,
registrar_acc_info,
registrar,
program_id,
@ -116,13 +119,19 @@ fn access_control(req: AccessControlRequest) -> Result<AccessControlResponse, Re
beneficiary_acc_info,
program_id,
)?;
let _vault = access_control::vault(vault_acc_info, registrar_acc_info, registrar, program_id)?;
let depositor = access_control::token(depositor_acc_info, depositor_authority_acc_info.key)?;
let _vault = access_control::vault_authenticated(
vault_acc_info,
vault_authority_acc_info,
registrar_acc_info,
registrar,
program_id,
)?;
pool_check(program_id, pool, registrar_acc_info, registrar, &member)?;
// Deposit specific.
//
// Authenticate the delegate boolean.
let depositor = access_control::token(depositor_acc_info, depositor_authority_acc_info.key)?;
if delegate != (depositor.owner == member.balances.delegate.owner) {
return Err(RegistryErrorCode::DepositorOwnerDelegateMismatch)?;
}
@ -171,6 +180,7 @@ struct AccessControlRequest<'a, 'b, 'c> {
entity_acc_info: &'a AccountInfo<'b>,
vault_acc_info: &'a AccountInfo<'b>,
registrar_acc_info: &'a AccountInfo<'b>,
vault_authority_acc_info: &'a AccountInfo<'b>,
program_id: &'a Pubkey,
pool: &'c Pool<'a, 'b>,
registrar: &'c Registrar,

View File

@ -1,6 +1,6 @@
use crate::common::invoke_token_transfer;
use borsh::BorshDeserialize;
use serum_common::pack::Pack;
use serum_common::program::invoke_token_transfer;
use serum_pool_schema::PoolState;
use serum_registry::access_control;
use serum_registry::accounts::reward_queue::Ring;

View File

@ -1,4 +1,4 @@
use crate::common::invoke_token_transfer;
use serum_common::program::invoke_token_transfer;
use serum_registry::accounts::reward_queue::Ring;
use serum_registry::accounts::{RewardEvent, RewardEventQueue};
use serum_registry::error::{RegistryError, RegistryErrorCode};

View File

@ -15,10 +15,8 @@ mod deposit;
mod drop_locked_reward;
mod drop_pool_reward;
mod end_stake_withdrawal;
mod entity;
mod initialize;
mod mark_generation;
mod pool;
mod slash;
mod stake;
mod start_stake_withdrawal;

View File

@ -1,4 +1,4 @@
use crate::pool::{pool_check_get_basket, Pool, PoolConfig};
use crate::common::pool::{pool_check_get_basket, Pool, PoolConfig};
use serum_common::pack::Pack;
use serum_registry::access_control;
use serum_registry::accounts::{Entity, Generation};

View File

@ -1,7 +1,7 @@
use crate::common::invoke_token_transfer;
use crate::entity::{with_entity, EntityContext};
use crate::pool::{pool_check, Pool, PoolConfig};
use crate::common::entity::{with_entity, EntityContext};
use crate::common::pool::{pool_check, Pool, PoolConfig};
use serum_common::pack::Pack;
use serum_common::program::invoke_token_transfer;
use serum_registry::access_control;
use serum_registry::accounts::{vault, Entity, Member, Registrar};
use serum_registry::error::RegistryError;
@ -121,7 +121,7 @@ fn access_control(req: AccessControlRequest) -> Result<(), RegistryError> {
// Account validation.
let _entity = access_control::entity(entity_acc_info, registrar_acc_info, program_id)?;
let member = access_control::member_raw(member_acc_info, entity_acc_info, program_id)?;
let _vault = access_control::vault_join(
let _vault = access_control::vault_authenticated(
vault_acc_info,
vault_authority_acc_info,
registrar_acc_info,
@ -129,7 +129,7 @@ fn access_control(req: AccessControlRequest) -> Result<(), RegistryError> {
program_id,
)?;
if let Some(mega_vault_acc_info) = mega_vault_acc_info {
let _mega_vault = access_control::vault_join(
let _mega_vault = access_control::vault_authenticated(
mega_vault_acc_info,
vault_authority_acc_info,
registrar_acc_info,

View File

@ -1,5 +1,5 @@
use crate::entity::{with_entity, EntityContext};
use crate::pool::{pool_check_create, Pool, PoolConfig};
use crate::common::entity::{with_entity, EntityContext};
use crate::common::pool::{pool_check_create, Pool, PoolConfig};
use serum_common::pack::Pack;
use serum_registry::access_control;
use serum_registry::accounts::{Entity, Member, Registrar};

View File

@ -1,7 +1,7 @@
use crate::common::invoke_token_transfer;
use crate::entity::{with_entity, EntityContext};
use crate::pool::{pool_check, Pool, PoolConfig};
use crate::common::entity::{with_entity, EntityContext};
use crate::common::pool::{pool_check, Pool, PoolConfig};
use serum_common::pack::Pack;
use serum_common::program::invoke_token_transfer;
use serum_registry::access_control;
use serum_registry::accounts::entity::{EntityState, PoolPrices};
use serum_registry::accounts::pending_withdrawal::PendingPayment;
@ -158,7 +158,7 @@ fn access_control(req: AccessControlRequest) -> Result<AccessControlResponse, Re
.map_or(Ok(None), |res| res.map(Some))?;
pool_check(program_id, pool, registrar_acc_info, registrar, &member)?;
let _vault = access_control::vault_join(
let _vault = access_control::vault_authenticated(
vault_acc_info,
vault_authority_acc_info,
registrar_acc_info,
@ -166,7 +166,7 @@ fn access_control(req: AccessControlRequest) -> Result<AccessControlResponse, Re
program_id,
)?;
if let Some(mega_vault_acc_info) = mega_vault_acc_info {
let _mega_vault = access_control::vault_join(
let _mega_vault = access_control::vault_authenticated(
mega_vault_acc_info,
vault_authority_acc_info,
registrar_acc_info,

View File

@ -1,4 +1,4 @@
use crate::pool::{pool_check, Pool, PoolConfig};
use crate::common::pool::{pool_check, Pool, PoolConfig};
use serum_common::pack::*;
use serum_registry::access_control;
use serum_registry::accounts::{Entity, Member, Registrar};

View File

@ -1,7 +1,7 @@
use crate::common::invoke_token_transfer;
use crate::entity::{with_entity, EntityContext};
use crate::pool::{pool_check, Pool, PoolConfig};
use crate::common::entity::{with_entity, EntityContext};
use crate::common::pool::{pool_check, Pool, PoolConfig};
use serum_common::pack::Pack;
use serum_common::program::invoke_token_transfer;
use serum_registry::access_control;
use serum_registry::accounts::{vault, Entity, Member, Registrar};
use serum_registry::error::{RegistryError, RegistryErrorCode};
@ -26,6 +26,7 @@ pub fn handler(
let depositor_acc_info = next_account_info(acc_infos)?;
let depositor_authority_acc_info = next_account_info(acc_infos)?;
let token_program_acc_info = next_account_info(acc_infos)?;
let vault_acc_info = next_account_info(acc_infos)?;
let vault_authority_acc_info = next_account_info(acc_infos)?;
// Program specfic.
@ -34,7 +35,6 @@ pub fn handler(
let entity_acc_info = next_account_info(acc_infos)?;
let registrar_acc_info = next_account_info(acc_infos)?;
let clock_acc_info = next_account_info(acc_infos)?;
let vault_acc_info = next_account_info(acc_infos)?;
let pool = &Pool::parse_accounts(acc_infos, PoolConfig::GetBasket)?;
@ -111,12 +111,6 @@ fn access_control(req: AccessControlRequest) -> Result<AccessControlResponse, Re
return Err(RegistryErrorCode::Unauthorized)?;
}
if !depositor_authority_acc_info.is_signer {
// This check is not strictly necessary. It is used to prevent people
// from shooting themselves in the foot, i.e., withdrawing to a delegate
// program without the delegate program signing.
//
// In the common case (i.e. withdrawing without a delegate program),
// this authority will be the same as the beneficiary.
return Err(RegistryErrorCode::Unauthorized)?;
}
@ -127,25 +121,27 @@ fn access_control(req: AccessControlRequest) -> Result<AccessControlResponse, Re
beneficiary_acc_info,
program_id,
)?;
let _vault = access_control::vault_join(
let _vault = access_control::vault_authenticated(
vault_acc_info,
vault_authority_acc_info,
registrar_acc_info,
&registrar,
program_id,
)?;
let depositor = access_control::token(depositor_acc_info, depositor_authority_acc_info.key)?;
pool_check(program_id, pool, registrar_acc_info, &registrar, &member)?;
// Withdraw specific.
//
// Authenticate the delegate boolean.
let depositor = access_control::token(depositor_acc_info, depositor_authority_acc_info.key)?;
if delegate != (depositor.owner == member.balances.delegate.owner) {
return Err(RegistryErrorCode::DepositorOwnerDelegateMismatch)?;
}
// Do we have enough funds for the withdrawal?
let is_mega = registrar.is_mega(*vault_acc_info.key)?;
if !member.can_withdraw(&pool.prices(), amount, is_mega, depositor.owner)? {
return Err(RegistryErrorCode::InsufficientBalance)?;
}
// Authenticate the delegate boolean.
if delegate != (depositor.owner == member.balances.delegate.owner) {
return Err(RegistryErrorCode::DepositorOwnerDelegateMismatch)?;
}
Ok(AccessControlResponse { depositor })
}

View File

@ -214,7 +214,7 @@ pub fn generation_check(
Ok(())
}
pub fn vault_join(
pub fn vault_authenticated(
acc_info: &AccountInfo,
vault_authority_acc_info: &AccountInfo,
registrar_acc_info: &AccountInfo,