safe: Use linear unlock function for Vesting accounts

This commit is contained in:
armaniferrante 2020-09-24 19:57:07 -07:00 committed by Armani Ferrante
parent 30174aefe3
commit 1e01aa3cc7
10 changed files with 209 additions and 182 deletions

View File

@ -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 }

View File

@ -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>,

View File

@ -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,

View File

@ -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)?;
}
}

View File

@ -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)?;
}
}

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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::*;

View File

@ -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,
}

View File

@ -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.