safe: Use linear unlock function for Vesting accounts
This commit is contained in:
parent
30174aefe3
commit
1e01aa3cc7
|
@ -7,7 +7,7 @@ edition = "2018"
|
|||
|
||||
[features]
|
||||
program = ["solana-client-gen/program", "spl-token/program", "serum-common/program"]
|
||||
client = ["solana-client-gen/client", "spl-token/default", "serum-common/client"]
|
||||
client = ["solana-client-gen/client", "spl-token/default", "serum-common/client", "lazy_static"]
|
||||
client-ext = []
|
||||
test = ["rand", "solana-client-gen/client", "spl-token/default"]
|
||||
strict = []
|
||||
|
@ -22,5 +22,8 @@ solana-client-gen = { path = "../solana-client-gen" }
|
|||
serum-common = { path = "../common" }
|
||||
bytemuck = "1.4.0"
|
||||
|
||||
# Client only.
|
||||
lazy_static = { version = "1.4.0", optional = true }
|
||||
|
||||
# Used for testing.
|
||||
rand = { version = "0.7.3", optional = true }
|
||||
|
|
|
@ -4,6 +4,7 @@ use serum_safe::error::{SafeError, SafeErrorCode};
|
|||
use solana_sdk::account_info::{next_account_info, AccountInfo};
|
||||
use solana_sdk::info;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use solana_sdk::sysvar::clock::Clock;
|
||||
use solana_sdk::sysvar::rent::Rent;
|
||||
use solana_sdk::sysvar::Sysvar;
|
||||
use spl_token::pack::Pack as TokenPack;
|
||||
|
@ -13,8 +14,9 @@ pub fn handler<'a>(
|
|||
program_id: &'a Pubkey,
|
||||
accounts: &'a [AccountInfo<'a>],
|
||||
vesting_acc_beneficiary: Pubkey,
|
||||
vesting_slots: Vec<u64>,
|
||||
vesting_amounts: Vec<u64>,
|
||||
end_slot: u64,
|
||||
period_count: u64,
|
||||
deposit_amount: u64,
|
||||
) -> Result<(), SafeError> {
|
||||
info!("handler: deposit");
|
||||
|
||||
|
@ -27,11 +29,14 @@ pub fn handler<'a>(
|
|||
let safe_acc_info = next_account_info(acc_infos)?;
|
||||
let token_program_acc_info = next_account_info(acc_infos)?;
|
||||
let rent_acc_info = next_account_info(acc_infos)?;
|
||||
let clock_acc_info = next_account_info(acc_infos)?;
|
||||
let clock_slot = Clock::from_account_info(clock_acc_info)?.slot;
|
||||
|
||||
access_control(AccessControlRequest {
|
||||
vesting_slots: &vesting_slots,
|
||||
vesting_amounts: &vesting_amounts,
|
||||
program_id,
|
||||
end_slot,
|
||||
period_count,
|
||||
deposit_amount,
|
||||
vesting_acc_info,
|
||||
safe_acc_info,
|
||||
depositor_acc_info,
|
||||
|
@ -39,6 +44,8 @@ pub fn handler<'a>(
|
|||
safe_vault_acc_info,
|
||||
token_program_acc_info,
|
||||
rent_acc_info,
|
||||
clock_acc_info,
|
||||
clock_slot,
|
||||
})?;
|
||||
|
||||
// Same deal with unpack_unchecked. See the comment in `access_control`
|
||||
|
@ -47,8 +54,10 @@ pub fn handler<'a>(
|
|||
&mut vesting_acc_info.try_borrow_mut_data()?,
|
||||
&mut |vesting_acc: &mut Vesting| {
|
||||
state_transition(StateTransitionRequest {
|
||||
vesting_slots: vesting_slots.clone(),
|
||||
vesting_amounts: vesting_amounts.clone(),
|
||||
clock_slot,
|
||||
end_slot,
|
||||
period_count,
|
||||
deposit_amount,
|
||||
vesting_acc,
|
||||
vesting_acc_beneficiary,
|
||||
safe_acc_info,
|
||||
|
@ -64,11 +73,14 @@ pub fn handler<'a>(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn access_control<'a, 'b>(req: AccessControlRequest<'a, 'b>) -> Result<(), SafeError> {
|
||||
fn access_control<'a>(req: AccessControlRequest<'a>) -> Result<(), SafeError> {
|
||||
info!("access-control: deposit");
|
||||
|
||||
let AccessControlRequest {
|
||||
program_id,
|
||||
end_slot,
|
||||
period_count,
|
||||
deposit_amount,
|
||||
vesting_acc_info,
|
||||
safe_acc_info,
|
||||
depositor_acc_info,
|
||||
|
@ -76,8 +88,8 @@ fn access_control<'a, 'b>(req: AccessControlRequest<'a, 'b>) -> Result<(), SafeE
|
|||
depositor_authority_acc_info,
|
||||
token_program_acc_info,
|
||||
rent_acc_info,
|
||||
vesting_slots,
|
||||
vesting_amounts,
|
||||
clock_acc_info,
|
||||
clock_slot,
|
||||
} = req;
|
||||
|
||||
// Depositor authorization.
|
||||
|
@ -118,53 +130,18 @@ fn access_control<'a, 'b>(req: AccessControlRequest<'a, 'b>) -> Result<(), SafeE
|
|||
|
||||
// Vesting.
|
||||
{
|
||||
let vesting_data = vesting_acc_info.try_borrow_data()?;
|
||||
|
||||
// Check the account's data-dependent size is correct before unpacking.
|
||||
if vesting_data.len() != Vesting::size_dyn(vesting_slots.len())? as usize {
|
||||
return Err(SafeErrorCode::VestingAccountDataInvalid)?;
|
||||
}
|
||||
// Perform an unpack_unchecked--that is, unsafe--deserialization.
|
||||
//
|
||||
// We might lose information when deserializing from all zeroes, because
|
||||
// Vesting has variable length Vecs (i.e., if you deserializ vec![0; 100]),
|
||||
// it can deserialize to vec![0; 0], depending on the serializer. This
|
||||
// is the case for bincode serialization. In other words, we might *not*
|
||||
// use the entire data array upon deserializing here.
|
||||
//
|
||||
// As a result, we follow this with a check on the slots and amounts to
|
||||
// guarantee that all subsequent instructions deal with non-zero vecs
|
||||
// (thus making our serialization size deterministic). And so all further
|
||||
// instructions should use the safe `unpack` variant method.
|
||||
//
|
||||
// This latter check is nice to have anyway, to prevent useless deposits.
|
||||
//
|
||||
// Switch serializers if this is a problem.
|
||||
let vesting = Vesting::unpack_unchecked(&vesting_data)?;
|
||||
if vesting.initialized {
|
||||
return Err(SafeErrorCode::AlreadyInitialized)?;
|
||||
}
|
||||
if !vesting_slots
|
||||
.iter()
|
||||
.filter(|slot| **slot == 0)
|
||||
.collect::<Vec<&u64>>()
|
||||
.is_empty()
|
||||
{
|
||||
return Err(SafeErrorCode::InvalidVestingSlots)?;
|
||||
}
|
||||
if !vesting_amounts
|
||||
.iter()
|
||||
.filter(|slot| **slot == 0)
|
||||
.collect::<Vec<&u64>>()
|
||||
.is_empty()
|
||||
{
|
||||
return Err(SafeErrorCode::InvalidVestingAmounts)?;
|
||||
}
|
||||
if vesting_acc_info.owner != program_id {
|
||||
return Err(SafeErrorCode::NotOwnedByProgram)?;
|
||||
}
|
||||
let vesting = Vesting::unpack(&vesting_acc_info.try_borrow_data()?)?;
|
||||
if vesting.initialized {
|
||||
return Err(SafeErrorCode::AlreadyInitialized)?;
|
||||
}
|
||||
let rent = Rent::from_account_info(rent_acc_info)?;
|
||||
if !rent.is_exempt(vesting_acc_info.lamports(), vesting_data.len()) {
|
||||
if !rent.is_exempt(
|
||||
vesting_acc_info.lamports(),
|
||||
vesting_acc_info.try_data_len()?,
|
||||
) {
|
||||
return Err(SafeErrorCode::NotRentExempt)?;
|
||||
}
|
||||
}
|
||||
|
@ -183,6 +160,22 @@ fn access_control<'a, 'b>(req: AccessControlRequest<'a, 'b>) -> Result<(), SafeE
|
|||
}
|
||||
}
|
||||
|
||||
// Vesting schedule.
|
||||
{
|
||||
if *clock_acc_info.key != solana_sdk::sysvar::clock::id() {
|
||||
return Err(SafeErrorCode::InvalidClock)?;
|
||||
}
|
||||
if end_slot <= clock_slot {
|
||||
return Err(SafeErrorCode::InvalidSlot)?;
|
||||
}
|
||||
if period_count == 0 {
|
||||
return Err(SafeErrorCode::InvalidPeriod)?;
|
||||
}
|
||||
if deposit_amount == 0 {
|
||||
return Err(SafeErrorCode::InvalidDepositAmount)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Depositor.
|
||||
{
|
||||
let depositor = spl_token::state::Account::unpack(&depositor_acc_info.try_borrow_data()?)?;
|
||||
|
@ -201,11 +194,13 @@ fn state_transition<'a, 'b>(req: StateTransitionRequest<'a, 'b>) -> Result<(), S
|
|||
info!("state-transition: deposit");
|
||||
|
||||
let StateTransitionRequest {
|
||||
clock_slot,
|
||||
end_slot,
|
||||
period_count,
|
||||
deposit_amount,
|
||||
vesting_acc,
|
||||
vesting_acc_beneficiary,
|
||||
safe_acc_info,
|
||||
vesting_slots,
|
||||
vesting_amounts,
|
||||
depositor_acc_info,
|
||||
safe_vault_acc_info,
|
||||
depositor_authority_acc_info,
|
||||
|
@ -217,8 +212,12 @@ fn state_transition<'a, 'b>(req: StateTransitionRequest<'a, 'b>) -> Result<(), S
|
|||
vesting_acc.safe = safe_acc_info.key.clone();
|
||||
vesting_acc.beneficiary = vesting_acc_beneficiary;
|
||||
vesting_acc.initialized = true;
|
||||
vesting_acc.slots = vesting_slots.clone();
|
||||
vesting_acc.amounts = vesting_amounts.clone();
|
||||
vesting_acc.locked_outstanding = 0;
|
||||
vesting_acc.period_count = period_count;
|
||||
vesting_acc.start_balance = deposit_amount;
|
||||
vesting_acc.end_slot = end_slot;
|
||||
vesting_acc.start_slot = clock_slot;
|
||||
vesting_acc.balance = deposit_amount;
|
||||
}
|
||||
|
||||
// Now transfer SPL funds from the depositor, to the
|
||||
|
@ -226,15 +225,13 @@ fn state_transition<'a, 'b>(req: StateTransitionRequest<'a, 'b>) -> Result<(), S
|
|||
{
|
||||
info!("invoke SPL token transfer");
|
||||
|
||||
let total_deposit = vesting_amounts.iter().sum();
|
||||
|
||||
let deposit_instruction = spl_token::instruction::transfer(
|
||||
&spl_token::ID,
|
||||
depositor_acc_info.key,
|
||||
safe_vault_acc_info.key,
|
||||
depositor_authority_acc_info.key,
|
||||
&[],
|
||||
total_deposit,
|
||||
deposit_amount,
|
||||
)?;
|
||||
solana_sdk::program::invoke_signed(
|
||||
&deposit_instruction,
|
||||
|
@ -253,8 +250,11 @@ fn state_transition<'a, 'b>(req: StateTransitionRequest<'a, 'b>) -> Result<(), S
|
|||
Ok(())
|
||||
}
|
||||
|
||||
struct AccessControlRequest<'a, 'b> {
|
||||
struct AccessControlRequest<'a> {
|
||||
program_id: &'a Pubkey,
|
||||
end_slot: u64,
|
||||
period_count: u64,
|
||||
deposit_amount: u64,
|
||||
vesting_acc_info: &'a AccountInfo<'a>,
|
||||
safe_acc_info: &'a AccountInfo<'a>,
|
||||
depositor_acc_info: &'a AccountInfo<'a>,
|
||||
|
@ -262,16 +262,18 @@ struct AccessControlRequest<'a, 'b> {
|
|||
safe_vault_acc_info: &'a AccountInfo<'a>,
|
||||
token_program_acc_info: &'a AccountInfo<'a>,
|
||||
rent_acc_info: &'a AccountInfo<'a>,
|
||||
vesting_slots: &'b [u64],
|
||||
vesting_amounts: &'b [u64],
|
||||
clock_acc_info: &'a AccountInfo<'a>,
|
||||
clock_slot: u64,
|
||||
}
|
||||
|
||||
struct StateTransitionRequest<'a, 'b> {
|
||||
clock_slot: u64,
|
||||
end_slot: u64,
|
||||
period_count: u64,
|
||||
deposit_amount: u64,
|
||||
vesting_acc: &'b mut Vesting,
|
||||
vesting_acc_beneficiary: Pubkey,
|
||||
safe_acc_info: &'a AccountInfo<'a>,
|
||||
vesting_slots: Vec<u64>,
|
||||
vesting_amounts: Vec<u64>,
|
||||
depositor_acc_info: &'a AccountInfo<'a>,
|
||||
safe_vault_acc_info: &'a AccountInfo<'a>,
|
||||
depositor_authority_acc_info: &'a AccountInfo<'a>,
|
||||
|
|
|
@ -33,15 +33,17 @@ fn process_instruction<'a>(
|
|||
initialize::handler(program_id, accounts, authority, nonce)
|
||||
}
|
||||
SafeInstruction::Deposit {
|
||||
vesting_account_beneficiary,
|
||||
vesting_slots,
|
||||
vesting_amounts,
|
||||
beneficiary,
|
||||
end_slot,
|
||||
period_count,
|
||||
deposit_amount,
|
||||
} => deposit::handler(
|
||||
program_id,
|
||||
accounts,
|
||||
vesting_account_beneficiary,
|
||||
vesting_slots,
|
||||
vesting_amounts,
|
||||
beneficiary,
|
||||
end_slot,
|
||||
period_count,
|
||||
deposit_amount,
|
||||
),
|
||||
SafeInstruction::MintLocked {
|
||||
token_account_owner,
|
||||
|
|
|
@ -102,7 +102,7 @@ fn access_control<'a>(req: AccessControlRequest<'a>) -> Result<(), SafeError> {
|
|||
}
|
||||
// Do we have sufficient balance?
|
||||
if vesting.available_for_mint() < 1 {
|
||||
return Err(SafeErrorCode::InsufficientBalance)?;
|
||||
return Err(SafeErrorCode::InsufficientMintBalance)?;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -127,7 +127,7 @@ fn access_control<'a>(req: AccessControlRequest<'a>) -> Result<(), SafeError> {
|
|||
// Do we have sufficient balance?
|
||||
let clock = Clock::from_account_info(clock_acc_info)?;
|
||||
if amount > vesting.available_for_withdrawal(clock.slot) {
|
||||
return Err(SafeErrorCode::InsufficientBalance)?;
|
||||
return Err(SafeErrorCode::InsufficientWithdrawalBalance)?;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
//! mod accounts defines the storage layout for the accounts used by this program.
|
||||
|
||||
mod mint_receipt;
|
||||
mod safe;
|
||||
mod token_vault;
|
||||
mod vesting;
|
||||
pub mod mint_receipt;
|
||||
pub mod safe;
|
||||
pub mod token_vault;
|
||||
pub mod vesting;
|
||||
|
||||
pub use mint_receipt::MintReceipt;
|
||||
pub use safe::Safe;
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
use crate::error::SafeError;
|
||||
use solana_client_gen::solana_sdk::pubkey::Pubkey;
|
||||
use std::cmp::Ordering;
|
||||
|
||||
#[cfg(feature = "client")]
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref SIZE: u64 = Vesting::default()
|
||||
.size()
|
||||
.expect("Vesting has a fixed size");
|
||||
}
|
||||
|
||||
/// The Vesting account represents a single deposit of a token
|
||||
/// available for withdrawal over a period of time determined by
|
||||
/// a vesting schedule.
|
||||
///
|
||||
/// Note that, unlike other accounts, this account is dynamically
|
||||
/// sized, which clients must consider when creating these accounts.
|
||||
/// use the `size_dyn` method to determine how large the account
|
||||
/// data should be.
|
||||
#[derive(Default, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Vesting {
|
||||
/// The Safe instance this account is associated with.
|
||||
|
@ -18,70 +18,84 @@ pub struct Vesting {
|
|||
pub beneficiary: Pubkey,
|
||||
/// True iff the vesting account has been initialized via deposit.
|
||||
pub initialized: bool,
|
||||
/// The amount of locked SRM outstanding.
|
||||
/// The amount of locked SRM minted and in circulation.
|
||||
pub locked_outstanding: u64,
|
||||
/// The Solana slots at which each amount vests.
|
||||
pub slots: Vec<u64>,
|
||||
/// The amount that vests at each slot.
|
||||
pub amounts: Vec<u64>,
|
||||
/// The outstanding SRM deposit backing this vesting account.
|
||||
pub balance: u64,
|
||||
/// The starting balance of this vesting account, i.e., how much was
|
||||
/// originally deposited.
|
||||
pub start_balance: u64,
|
||||
/// The slot at which this vesting account was created.
|
||||
pub start_slot: u64,
|
||||
/// The slot at which all the tokens associated with this account
|
||||
/// should be vested.
|
||||
pub end_slot: u64,
|
||||
/// The number of times vesting will occur. For example, if vesting
|
||||
/// is once a year over seven years, this will be 7.
|
||||
pub period_count: u64,
|
||||
}
|
||||
|
||||
impl Vesting {
|
||||
/// Returns the total deposit in this vesting account.
|
||||
pub fn total(&self) -> u64 {
|
||||
self.amounts.iter().sum()
|
||||
/// Deducts the given amount from the vesting account upon withdrawal.
|
||||
pub fn deduct(&mut self, amount: u64) {
|
||||
self.balance -= amount;
|
||||
}
|
||||
|
||||
/// Returns the amount available for minting locked token NFTs.
|
||||
pub fn available_for_mint(&self) -> u64 {
|
||||
self.total() - self.locked_outstanding
|
||||
self.balance - self.locked_outstanding
|
||||
}
|
||||
|
||||
/// Returns the amount available for withdrawal as of the given slot.
|
||||
pub fn available_for_withdrawal(&self, slot: u64) -> u64 {
|
||||
self.vested_amount(slot) - self.locked_outstanding
|
||||
pub fn available_for_withdrawal(&self, current_slot: u64) -> u64 {
|
||||
std::cmp::min(self.balance_vested(current_slot), self.available_for_mint())
|
||||
}
|
||||
|
||||
/// Returns the total vested amount up to the given slot. This is not
|
||||
/// necessarily available for withdrawal.
|
||||
pub fn vested_amount(&self, slot: u64) -> u64 {
|
||||
self.slots
|
||||
.iter()
|
||||
.filter(|s| **s <= slot)
|
||||
.enumerate()
|
||||
.map(|(idx, _slot)| self.amounts[idx])
|
||||
.sum()
|
||||
// The outstanding SRM deposit associated with this account that has not
|
||||
// been withdraw. Does not consider outstanding lSRM in circulation.
|
||||
fn balance_vested(&self, current_slot: u64) -> u64 {
|
||||
self.total_vested(current_slot) - self.withdrawn_amount()
|
||||
}
|
||||
|
||||
/// Deducts the given amount from the vesting account from the earliest
|
||||
/// vesting slots.
|
||||
pub fn deduct(&mut self, mut amount: u64) {
|
||||
for k in 0..self.amounts.len() {
|
||||
match amount.cmp(&self.amounts[k]) {
|
||||
Ordering::Less => {
|
||||
self.amounts[k] -= amount;
|
||||
return;
|
||||
}
|
||||
Ordering::Equal => {
|
||||
self.amounts[k] = 0;
|
||||
return;
|
||||
}
|
||||
Ordering::Greater => {
|
||||
let old = self.amounts[k];
|
||||
self.amounts[k] = 0;
|
||||
amount -= old;
|
||||
}
|
||||
}
|
||||
// Returns the total vested amount up to the given slot.
|
||||
fn total_vested(&self, current_slot: u64) -> u64 {
|
||||
assert!(current_slot >= self.start_slot);
|
||||
|
||||
if current_slot >= self.end_slot {
|
||||
return self.start_balance;
|
||||
}
|
||||
self.linear_unlock(current_slot)
|
||||
}
|
||||
|
||||
/// Returns the dynamic size of the account's data array, assuming it has
|
||||
/// `slot_account` vesting periods.
|
||||
pub fn size_dyn(slot_count: usize) -> Result<u64, SafeError> {
|
||||
let mut d: Vesting = Default::default();
|
||||
d.slots = vec![0u64; slot_count];
|
||||
d.amounts = vec![0u64; slot_count];
|
||||
Ok(d.size()?)
|
||||
// Returns the amount withdrawn from this vesting account.
|
||||
fn withdrawn_amount(&self) -> u64 {
|
||||
self.start_balance - self.balance
|
||||
}
|
||||
|
||||
fn linear_unlock(&self, current_slot: u64) -> u64 {
|
||||
let (end_slot, start_slot) = {
|
||||
// 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_slot - self.start_slot) % self.period_count;
|
||||
if overflow != 0 {
|
||||
(self.end_slot, self.start_slot - overflow)
|
||||
} else {
|
||||
(self.end_slot, self.start_slot)
|
||||
}
|
||||
};
|
||||
|
||||
let vested_period_count = {
|
||||
let period = (end_slot - start_slot) / self.period_count;
|
||||
let current_period_count = (current_slot - start_slot) / period;
|
||||
std::cmp::min(current_period_count, self.period_count)
|
||||
};
|
||||
let reward_per_period = self.start_balance / self.period_count;
|
||||
|
||||
return vested_period_count * reward_per_period;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -98,22 +112,28 @@ mod tests {
|
|||
// Given a vesting account.
|
||||
let safe = Keypair::generate(&mut OsRng).pubkey();
|
||||
let beneficiary = Keypair::generate(&mut OsRng).pubkey();
|
||||
let amounts = vec![1, 2, 3, 4];
|
||||
let slots = vec![5, 6, 7, 8];
|
||||
let initialized = true;
|
||||
let locked_outstanding = 99;
|
||||
let start_balance = 10;
|
||||
let balance = start_balance;
|
||||
let start_slot = 11;
|
||||
let end_slot = 12;
|
||||
let period_count = 13;
|
||||
let vesting_acc = Vesting {
|
||||
safe,
|
||||
beneficiary,
|
||||
initialized,
|
||||
locked_outstanding,
|
||||
amounts: amounts.clone(),
|
||||
slots: slots.clone(),
|
||||
balance,
|
||||
start_balance,
|
||||
start_slot,
|
||||
end_slot,
|
||||
period_count,
|
||||
};
|
||||
|
||||
// When I pack it into a slice.
|
||||
let mut dst = vec![];
|
||||
dst.resize(Vesting::size_dyn(slots.len()).unwrap() as usize, 0u8);
|
||||
dst.resize(Vesting::default().size().unwrap() as usize, 0u8);
|
||||
Vesting::pack(vesting_acc, &mut dst).unwrap();
|
||||
|
||||
// Then I can unpack it from a slice.
|
||||
|
@ -121,27 +141,23 @@ mod tests {
|
|||
assert_eq!(va.safe, safe);
|
||||
assert_eq!(va.beneficiary, beneficiary);
|
||||
assert_eq!(va.locked_outstanding, locked_outstanding);
|
||||
|
||||
assert_eq!(va.amounts.len(), amounts.len());
|
||||
assert_eq!(va.slots.len(), slots.len());
|
||||
let match_amounts = va
|
||||
.amounts
|
||||
.iter()
|
||||
.zip(&amounts)
|
||||
.filter(|&(a, b)| a == b)
|
||||
.count();
|
||||
assert_eq!(va.amounts.len(), match_amounts);
|
||||
let match_slots = va.slots.iter().zip(&slots).filter(|&(a, b)| a == b).count();
|
||||
assert_eq!(va.amounts.len(), match_slots);
|
||||
assert_eq!(va.initialized, initialized);
|
||||
assert_eq!(va.start_balance, start_balance);
|
||||
assert_eq!(va.balance, balance);
|
||||
assert_eq!(va.start_slot, start_slot);
|
||||
assert_eq!(va.end_slot, end_slot);
|
||||
assert_eq!(va.period_count, period_count);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn available_for_withdrawal() {
|
||||
let safe = Keypair::generate(&mut OsRng).pubkey();
|
||||
let beneficiary = Keypair::generate(&mut OsRng).pubkey();
|
||||
let amounts = vec![1, 2, 3, 4];
|
||||
let slots = vec![5, 6, 7, 8];
|
||||
let balance = 10;
|
||||
let start_balance = 10;
|
||||
let start_slot = 10;
|
||||
let end_slot = 20;
|
||||
let period_count = 5;
|
||||
let initialized = true;
|
||||
let locked_outstanding = 0;
|
||||
let vesting_acc = Vesting {
|
||||
|
@ -149,39 +165,34 @@ mod tests {
|
|||
beneficiary,
|
||||
initialized,
|
||||
locked_outstanding,
|
||||
amounts: amounts.clone(),
|
||||
slots: slots.clone(),
|
||||
balance,
|
||||
start_balance,
|
||||
start_slot,
|
||||
end_slot,
|
||||
period_count,
|
||||
};
|
||||
assert_eq!(0, vesting_acc.available_for_withdrawal(4));
|
||||
assert_eq!(1, vesting_acc.available_for_withdrawal(5));
|
||||
assert_eq!(3, vesting_acc.available_for_withdrawal(6));
|
||||
assert_eq!(10, vesting_acc.available_for_withdrawal(8));
|
||||
assert_eq!(0, vesting_acc.available_for_withdrawal(10));
|
||||
assert_eq!(0, vesting_acc.available_for_withdrawal(11));
|
||||
assert_eq!(2, vesting_acc.available_for_withdrawal(12));
|
||||
assert_eq!(2, vesting_acc.available_for_withdrawal(13));
|
||||
assert_eq!(4, vesting_acc.available_for_withdrawal(14));
|
||||
assert_eq!(8, vesting_acc.available_for_withdrawal(19));
|
||||
assert_eq!(10, vesting_acc.available_for_withdrawal(20));
|
||||
assert_eq!(10, vesting_acc.available_for_withdrawal(100));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unpack_zeroes_size() {
|
||||
let og_size = Vesting::size_dyn(5).unwrap();
|
||||
fn unpack_zeroes() {
|
||||
let og_size = Vesting::default().size().unwrap();
|
||||
let zero_data = vec![0; og_size as usize];
|
||||
let r = Vesting::unpack(&zero_data);
|
||||
match r {
|
||||
Ok(_) => panic!("expect error"),
|
||||
Err(e) => assert_eq!(e, ProgramError::InvalidAccountData),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unpack_unchecked_zeroes_size() {
|
||||
let og_size = Vesting::size_dyn(5).unwrap();
|
||||
let zero_data = vec![0; og_size as usize];
|
||||
let r = Vesting::unpack_unchecked(&zero_data).unwrap();
|
||||
let r = Vesting::unpack(&zero_data).unwrap();
|
||||
assert_eq!(r.initialized, false);
|
||||
assert_eq!(r.safe, Pubkey::new(&[0; 32]));
|
||||
assert_eq!(r.beneficiary, Pubkey::new(&[0; 32]));
|
||||
assert_eq!(r.locked_outstanding, 0);
|
||||
// Notice how we lose information here when deserializing from
|
||||
// all zeroes.
|
||||
assert_eq!(r.slots.len(), 0);
|
||||
assert_eq!(r.amounts.len(), 0);
|
||||
assert_eq!(r.balance, 0);
|
||||
assert_eq!(r.start_slot, 0);
|
||||
assert_eq!(r.end_slot, 0);
|
||||
assert_eq!(r.period_count, 0);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
//! The client_ext module extends the auto-generated program client.
|
||||
|
||||
use crate::accounts::vesting;
|
||||
use crate::accounts::{MintReceipt, Safe};
|
||||
use serum_common::pack::Pack;
|
||||
use solana_client_gen::prelude::*;
|
||||
|
|
|
@ -23,7 +23,7 @@ pub enum SafeErrorCode {
|
|||
SafeDataInvalid = 8,
|
||||
NotSignedByAuthority = 11,
|
||||
WrongNumberOfAccounts = 12,
|
||||
InsufficientBalance = 13,
|
||||
InsufficientMintBalance = 13,
|
||||
Unauthorized = 14,
|
||||
MintAlreadyInitialized = 15,
|
||||
ReceiptAlreadyInitialized = 16,
|
||||
|
@ -40,12 +40,15 @@ pub enum SafeErrorCode {
|
|||
InvalidSerialization = 27,
|
||||
SizeNotAvailable = 28,
|
||||
UnitializedTokenMint = 29,
|
||||
InvalidVestingSlots = 30,
|
||||
InvalidVestingAmounts = 31,
|
||||
InvalidSlot = 30,
|
||||
InvalidClock = 31,
|
||||
InvalidRentSysvar = 32,
|
||||
InvalidMint = 33,
|
||||
WrongSafe = 34,
|
||||
WrongVestingAccount = 35,
|
||||
InvalidDepositAmount = 36,
|
||||
InvalidPeriod = 37,
|
||||
InsufficientWithdrawalBalance = 38,
|
||||
Unknown = 1000,
|
||||
}
|
||||
|
||||
|
|
|
@ -46,15 +46,20 @@ pub mod instruction {
|
|||
/// 4. `[]` Safe instance.
|
||||
/// 5. `[]` SPL token program.
|
||||
/// 6. `[]` Rent sysvar.
|
||||
#[cfg_attr(feature = "client", create_account(..))]
|
||||
/// 7. `[]` Clock sysvar.
|
||||
#[cfg_attr(feature = "client", create_account(*vesting::SIZE))]
|
||||
Deposit {
|
||||
/// The beneficiary of the vesting account, i.e.,
|
||||
/// the user who will own the SRM upon vesting.
|
||||
vesting_account_beneficiary: Pubkey,
|
||||
/// The Solana slot number at which point a vesting amount unlocks.
|
||||
vesting_slots: Vec<u64>,
|
||||
/// The amount of SRM to release for each vesting_slot.
|
||||
vesting_amounts: Vec<u64>,
|
||||
beneficiary: Pubkey,
|
||||
/// The Solana slot number at which point the entire deposit will
|
||||
/// be vested.
|
||||
end_slot: u64,
|
||||
/// The number of vesting periods for the account. For example,
|
||||
/// a vesting yearly over seven years would make this 7.
|
||||
period_count: u64,
|
||||
/// The amount to deposit into the vesting account.
|
||||
deposit_amount: u64,
|
||||
},
|
||||
/// Withdraw withdraws the given amount from the given vesting
|
||||
/// account subject to a vesting schedule.
|
||||
|
|
Loading…
Reference in New Issue