delete vest program (#16795)
This commit is contained in:
parent
97812570e7
commit
6d160768d7
|
@ -4503,7 +4503,6 @@ dependencies = [
|
|||
"solana-sdk",
|
||||
"solana-stake-program",
|
||||
"solana-version",
|
||||
"solana-vest-program",
|
||||
"solana-vote-program",
|
||||
"tempfile",
|
||||
]
|
||||
|
@ -4687,7 +4686,6 @@ dependencies = [
|
|||
"solana-runtime",
|
||||
"solana-sdk",
|
||||
"solana-stake-program",
|
||||
"solana-vest-program",
|
||||
"solana-vote-program",
|
||||
"tempfile",
|
||||
]
|
||||
|
@ -5416,22 +5414,6 @@ dependencies = [
|
|||
"solana-sdk",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solana-vest-program"
|
||||
version = "1.7.0"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"chrono",
|
||||
"num-derive",
|
||||
"num-traits",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"solana-config-program",
|
||||
"solana-runtime",
|
||||
"solana-sdk",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solana-vote-program"
|
||||
version = "1.7.0"
|
||||
|
|
|
@ -49,7 +49,6 @@ members = [
|
|||
"programs/noop",
|
||||
"programs/ownable",
|
||||
"programs/stake",
|
||||
"programs/vest",
|
||||
"programs/vote",
|
||||
"remote-wallet",
|
||||
"ramp-tps",
|
||||
|
|
|
@ -26,7 +26,6 @@ solana-runtime = { path = "../runtime", version = "=1.7.0" }
|
|||
solana-sdk = { path = "../sdk", version = "=1.7.0" }
|
||||
solana-stake-program = { path = "../programs/stake", version = "=1.7.0" }
|
||||
solana-version = { path = "../version", version = "=1.7.0" }
|
||||
solana-vest-program = { path = "../programs/vest", version = "=1.7.0" }
|
||||
solana-vote-program = { path = "../programs/vote", version = "=1.7.0" }
|
||||
tempfile = "3.1.0"
|
||||
|
||||
|
|
|
@ -5,8 +5,6 @@
|
|||
extern crate solana_budget_program;
|
||||
#[macro_use]
|
||||
extern crate solana_exchange_program;
|
||||
#[macro_use]
|
||||
extern crate solana_vest_program;
|
||||
|
||||
use clap::{crate_description, crate_name, value_t, value_t_or_exit, App, Arg, ArgMatches};
|
||||
use solana_clap_utils::{
|
||||
|
@ -494,11 +492,7 @@ fn main() -> Result<(), Box<dyn error::Error>> {
|
|||
);
|
||||
|
||||
let native_instruction_processors = if cluster_type == ClusterType::Development {
|
||||
vec![
|
||||
solana_vest_program!(),
|
||||
solana_budget_program!(),
|
||||
solana_exchange_program!(),
|
||||
]
|
||||
vec![solana_budget_program!(), solana_exchange_program!()]
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
|
|
@ -28,7 +28,6 @@ solana-logger = { path = "../logger", version = "=1.7.0" }
|
|||
solana-runtime = { path = "../runtime", version = "=1.7.0" }
|
||||
solana-sdk = { path = "../sdk", version = "=1.7.0" }
|
||||
solana-stake-program = { path = "../programs/stake", version = "=1.7.0" }
|
||||
solana-vest-program = { path = "../programs/vest", version = "=1.7.0" }
|
||||
solana-vote-program = { path = "../programs/vote", version = "=1.7.0" }
|
||||
tempfile = "3.1.0"
|
||||
solana-rayon-threadlimit = { path = "../rayon-threadlimit", version = "=1.7.0" }
|
||||
|
|
|
@ -1070,12 +1070,7 @@ fn test_mainnet_beta_cluster_type() {
|
|||
}
|
||||
|
||||
// Programs that are not available at epoch 0
|
||||
for program_id in [
|
||||
&solana_sdk::bpf_loader_upgradeable::id(),
|
||||
&solana_vest_program::id(),
|
||||
]
|
||||
.iter()
|
||||
{
|
||||
for program_id in [&solana_sdk::bpf_loader_upgradeable::id()].iter() {
|
||||
assert_eq!(
|
||||
(
|
||||
program_id,
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
[package]
|
||||
name = "solana-vest-program"
|
||||
version = "1.7.0"
|
||||
description = "Solana Vest program"
|
||||
authors = ["Solana Maintainers <maintainers@solana.foundation>"]
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://solana.com/"
|
||||
documentation = "https://docs.rs/solana-vest-program"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
bincode = "1.3.1"
|
||||
chrono = { version = "0.4.11", features = ["serde"] }
|
||||
num-derive = "0.3"
|
||||
num-traits = "0.2"
|
||||
serde = "1.0.122"
|
||||
serde_derive = "1.0.103"
|
||||
solana-sdk = { path = "../../sdk", version = "=1.7.0" }
|
||||
solana-config-program = { path = "../config", version = "=1.7.0" }
|
||||
thiserror = "1.0"
|
||||
|
||||
[dev-dependencies]
|
||||
solana-runtime = { path = "../../runtime", version = "=1.7.0" }
|
||||
|
||||
[lib]
|
||||
crate-type = ["lib"]
|
||||
name = "solana_vest_program"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
|
@ -1,13 +0,0 @@
|
|||
#![allow(clippy::integer_arithmetic)]
|
||||
pub mod vest_instruction;
|
||||
pub mod vest_processor;
|
||||
pub mod vest_schedule;
|
||||
pub mod vest_state;
|
||||
|
||||
use crate::vest_processor::process_instruction;
|
||||
|
||||
solana_sdk::declare_program!(
|
||||
"Vest111111111111111111111111111111111111111",
|
||||
solana_vest_program,
|
||||
process_instruction
|
||||
);
|
|
@ -1,171 +0,0 @@
|
|||
use crate::{id, vest_state::VestState};
|
||||
use bincode::serialized_size;
|
||||
use chrono::prelude::{Date, DateTime, Utc};
|
||||
use num_derive::FromPrimitive;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use solana_sdk::{
|
||||
decode_error::DecodeError,
|
||||
instruction::{AccountMeta, Instruction, InstructionError},
|
||||
pubkey::Pubkey,
|
||||
system_instruction,
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug, Clone, PartialEq, FromPrimitive)]
|
||||
pub enum VestError {
|
||||
#[error("destination missing")]
|
||||
DestinationMissing,
|
||||
|
||||
#[error("unauthorized")]
|
||||
Unauthorized,
|
||||
}
|
||||
|
||||
impl From<VestError> for InstructionError {
|
||||
fn from(e: VestError) -> Self {
|
||||
InstructionError::Custom(e as u32)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> DecodeError<T> for VestError {
|
||||
fn type_of() -> &'static str {
|
||||
"VestError"
|
||||
}
|
||||
}
|
||||
|
||||
/// An instruction to progress the smart contract.
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||
pub enum VestInstruction {
|
||||
/// Declare and instantiate a vesting schedule
|
||||
InitializeAccount {
|
||||
terminator_pubkey: Pubkey, // The address authorized to terminate this contract with a signed Terminate instruction
|
||||
payee_pubkey: Pubkey, // The address authorized to redeem vested tokens
|
||||
start_date_time: DateTime<Utc>, // The day from which the vesting contract begins
|
||||
date_pubkey: Pubkey, // Address of an account containing a trusted date, used to drive the vesting schedule
|
||||
total_lamports: u64, // The number of lamports to send the payee if the schedule completes
|
||||
},
|
||||
|
||||
/// Change the terminator pubkey
|
||||
SetTerminator(Pubkey),
|
||||
|
||||
/// Change the payee pubkey
|
||||
SetPayee(Pubkey),
|
||||
|
||||
/// Load an account and pass its data to the contract for inspection.
|
||||
RedeemTokens,
|
||||
|
||||
/// Tell the contract that the `InitializeAccount` with `Signature` has been
|
||||
/// signed by the containing transaction's `Pubkey`.
|
||||
Terminate,
|
||||
|
||||
/// Reduce total_lamports by the given number of lamports. Tokens that have
|
||||
/// already vested are unaffected. Use this instead of `Terminate` to minimize
|
||||
/// the number of token transfers.
|
||||
Renege(u64),
|
||||
|
||||
/// Mark all available tokens as redeemable, regardless of the date.
|
||||
VestAll,
|
||||
}
|
||||
|
||||
fn initialize_account(
|
||||
terminator_pubkey: &Pubkey,
|
||||
payee_pubkey: &Pubkey,
|
||||
contract_pubkey: &Pubkey,
|
||||
start_date: Date<Utc>,
|
||||
date_pubkey: &Pubkey,
|
||||
total_lamports: u64,
|
||||
) -> Instruction {
|
||||
let keys = vec![AccountMeta::new(*contract_pubkey, false)];
|
||||
Instruction::new_with_bincode(
|
||||
id(),
|
||||
&VestInstruction::InitializeAccount {
|
||||
terminator_pubkey: *terminator_pubkey,
|
||||
payee_pubkey: *payee_pubkey,
|
||||
start_date_time: start_date.and_hms(0, 0, 0),
|
||||
date_pubkey: *date_pubkey,
|
||||
total_lamports,
|
||||
},
|
||||
keys,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn create_account(
|
||||
payer_pubkey: &Pubkey,
|
||||
terminator_pubkey: &Pubkey,
|
||||
contract_pubkey: &Pubkey,
|
||||
payee_pubkey: &Pubkey,
|
||||
start_date: Date<Utc>,
|
||||
date_pubkey: &Pubkey,
|
||||
lamports: u64,
|
||||
) -> Vec<Instruction> {
|
||||
let space = serialized_size(&VestState::default()).unwrap();
|
||||
vec![
|
||||
system_instruction::create_account(&payer_pubkey, contract_pubkey, lamports, space, &id()),
|
||||
initialize_account(
|
||||
terminator_pubkey,
|
||||
payee_pubkey,
|
||||
contract_pubkey,
|
||||
start_date,
|
||||
date_pubkey,
|
||||
lamports,
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
pub fn set_terminator(contract: &Pubkey, old_pubkey: &Pubkey, new_pubkey: &Pubkey) -> Instruction {
|
||||
let account_metas = vec![
|
||||
AccountMeta::new(*contract, false),
|
||||
AccountMeta::new(*old_pubkey, true),
|
||||
];
|
||||
Instruction::new_with_bincode(
|
||||
id(),
|
||||
&VestInstruction::SetTerminator(*new_pubkey),
|
||||
account_metas,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn set_payee(contract: &Pubkey, old_pubkey: &Pubkey, new_pubkey: &Pubkey) -> Instruction {
|
||||
let account_metas = vec![
|
||||
AccountMeta::new(*contract, false),
|
||||
AccountMeta::new(*old_pubkey, true),
|
||||
];
|
||||
Instruction::new_with_bincode(id(), &VestInstruction::SetPayee(*new_pubkey), account_metas)
|
||||
}
|
||||
|
||||
pub fn redeem_tokens(contract: &Pubkey, date_pubkey: &Pubkey, to: &Pubkey) -> Instruction {
|
||||
let account_metas = vec![
|
||||
AccountMeta::new(*contract, false),
|
||||
AccountMeta::new_readonly(*date_pubkey, false),
|
||||
AccountMeta::new(*to, false),
|
||||
];
|
||||
Instruction::new_with_bincode(id(), &VestInstruction::RedeemTokens, account_metas)
|
||||
}
|
||||
|
||||
pub fn terminate(contract: &Pubkey, from: &Pubkey, to: &Pubkey) -> Instruction {
|
||||
let mut account_metas = vec![
|
||||
AccountMeta::new(*contract, false),
|
||||
AccountMeta::new(*from, true),
|
||||
];
|
||||
if from != to {
|
||||
account_metas.push(AccountMeta::new(*to, false));
|
||||
}
|
||||
Instruction::new_with_bincode(id(), &VestInstruction::Terminate, account_metas)
|
||||
}
|
||||
|
||||
pub fn renege(contract: &Pubkey, from: &Pubkey, to: &Pubkey, lamports: u64) -> Instruction {
|
||||
let mut account_metas = vec![
|
||||
AccountMeta::new(*contract, false),
|
||||
AccountMeta::new(*from, true),
|
||||
];
|
||||
if from != to {
|
||||
account_metas.push(AccountMeta::new(*to, false));
|
||||
}
|
||||
Instruction::new_with_bincode(id(), &VestInstruction::Renege(lamports), account_metas)
|
||||
}
|
||||
|
||||
pub fn vest_all(contract: &Pubkey, from: &Pubkey) -> Instruction {
|
||||
let account_metas = vec![
|
||||
AccountMeta::new(*contract, false),
|
||||
AccountMeta::new(*from, true),
|
||||
];
|
||||
Instruction::new_with_bincode(id(), &VestInstruction::VestAll, account_metas)
|
||||
}
|
|
@ -1,697 +0,0 @@
|
|||
//! vest program
|
||||
use crate::{
|
||||
vest_instruction::{VestError, VestInstruction},
|
||||
vest_state::VestState,
|
||||
};
|
||||
use chrono::prelude::*;
|
||||
use solana_config_program::date_instruction::DateConfig;
|
||||
use solana_config_program::get_config_data;
|
||||
use solana_sdk::{
|
||||
account::{AccountSharedData, ReadableAccount, WritableAccount},
|
||||
feature_set,
|
||||
instruction::InstructionError,
|
||||
keyed_account::{keyed_account_at_index, KeyedAccount},
|
||||
process_instruction::InvokeContext,
|
||||
program_utils::limited_deserialize,
|
||||
pubkey::Pubkey,
|
||||
};
|
||||
use std::cell::RefMut;
|
||||
|
||||
fn verify_date_account(
|
||||
keyed_account: &KeyedAccount,
|
||||
expected_pubkey: &Pubkey,
|
||||
) -> Result<Date<Utc>, InstructionError> {
|
||||
if keyed_account.owner()? != solana_config_program::id() {
|
||||
return Err(InstructionError::IncorrectProgramId);
|
||||
}
|
||||
|
||||
let account = verify_account(keyed_account, expected_pubkey)?;
|
||||
|
||||
let config_data =
|
||||
get_config_data(&account.data()).map_err(|_| InstructionError::InvalidAccountData)?;
|
||||
let date_config =
|
||||
DateConfig::deserialize(config_data).ok_or(InstructionError::InvalidAccountData)?;
|
||||
|
||||
Ok(date_config.date_time.date())
|
||||
}
|
||||
|
||||
fn verify_account<'a>(
|
||||
keyed_account: &'a KeyedAccount,
|
||||
expected_pubkey: &Pubkey,
|
||||
) -> Result<RefMut<'a, AccountSharedData>, InstructionError> {
|
||||
if keyed_account.unsigned_key() != expected_pubkey {
|
||||
return Err(VestError::Unauthorized.into());
|
||||
}
|
||||
|
||||
keyed_account.try_account_ref_mut()
|
||||
}
|
||||
|
||||
fn verify_signed_account<'a>(
|
||||
keyed_account: &'a KeyedAccount,
|
||||
expected_pubkey: &Pubkey,
|
||||
) -> Result<RefMut<'a, AccountSharedData>, InstructionError> {
|
||||
if keyed_account.signer_key().is_none() {
|
||||
return Err(InstructionError::MissingRequiredSignature);
|
||||
}
|
||||
|
||||
verify_account(keyed_account, expected_pubkey)
|
||||
}
|
||||
|
||||
pub fn process_instruction(
|
||||
_program_id: &Pubkey,
|
||||
data: &[u8],
|
||||
invoke_context: &mut dyn InvokeContext,
|
||||
) -> Result<(), InstructionError> {
|
||||
let keyed_accounts = invoke_context.get_keyed_accounts()?;
|
||||
|
||||
let contract_account = &mut keyed_account_at_index(keyed_accounts, 0)?.try_account_ref_mut()?;
|
||||
if invoke_context.is_feature_active(&feature_set::check_program_owner::id())
|
||||
&& contract_account.owner != crate::id()
|
||||
{
|
||||
return Err(InstructionError::InvalidAccountOwner);
|
||||
}
|
||||
|
||||
let instruction = limited_deserialize(data)?;
|
||||
|
||||
let mut vest_state = if let VestInstruction::InitializeAccount {
|
||||
terminator_pubkey,
|
||||
payee_pubkey,
|
||||
start_date_time,
|
||||
date_pubkey,
|
||||
total_lamports,
|
||||
} = instruction
|
||||
{
|
||||
VestState {
|
||||
terminator_pubkey,
|
||||
payee_pubkey,
|
||||
start_date_time,
|
||||
date_pubkey,
|
||||
total_lamports,
|
||||
..VestState::default()
|
||||
}
|
||||
} else {
|
||||
VestState::deserialize(&contract_account.data())?
|
||||
};
|
||||
|
||||
match instruction {
|
||||
VestInstruction::InitializeAccount { .. } => {}
|
||||
VestInstruction::SetTerminator(new_pubkey) => {
|
||||
verify_signed_account(
|
||||
keyed_account_at_index(keyed_accounts, 1)?,
|
||||
&vest_state.terminator_pubkey,
|
||||
)?;
|
||||
vest_state.terminator_pubkey = new_pubkey;
|
||||
}
|
||||
VestInstruction::SetPayee(new_pubkey) => {
|
||||
verify_signed_account(
|
||||
keyed_account_at_index(keyed_accounts, 1)?,
|
||||
&vest_state.payee_pubkey,
|
||||
)?;
|
||||
vest_state.payee_pubkey = new_pubkey;
|
||||
}
|
||||
VestInstruction::RedeemTokens => {
|
||||
let current_date = verify_date_account(
|
||||
keyed_account_at_index(keyed_accounts, 1)?,
|
||||
&vest_state.date_pubkey,
|
||||
)?;
|
||||
let mut payee_account = verify_account(
|
||||
keyed_account_at_index(keyed_accounts, 2)?,
|
||||
&vest_state.payee_pubkey,
|
||||
)?;
|
||||
vest_state.redeem_tokens(contract_account, current_date, &mut payee_account);
|
||||
}
|
||||
VestInstruction::Terminate | VestInstruction::Renege(_) => {
|
||||
let lamports = if let VestInstruction::Renege(lamports) = instruction {
|
||||
lamports
|
||||
} else {
|
||||
contract_account.lamports
|
||||
};
|
||||
let terminator_account = verify_signed_account(
|
||||
keyed_account_at_index(keyed_accounts, 1)?,
|
||||
&vest_state.terminator_pubkey,
|
||||
)?;
|
||||
let payee_keyed_account = keyed_account_at_index(keyed_accounts, 2);
|
||||
let mut payee_account = if let Ok(payee_keyed_account) = payee_keyed_account {
|
||||
payee_keyed_account.try_account_ref_mut()?
|
||||
} else {
|
||||
terminator_account
|
||||
};
|
||||
vest_state.renege(contract_account, &mut payee_account, lamports);
|
||||
}
|
||||
VestInstruction::VestAll => {
|
||||
verify_signed_account(
|
||||
keyed_account_at_index(keyed_accounts, 1)?,
|
||||
&vest_state.terminator_pubkey,
|
||||
)?;
|
||||
vest_state.vest_all();
|
||||
}
|
||||
}
|
||||
|
||||
vest_state.serialize(contract_account.data_as_mut_slice())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::id;
|
||||
use crate::vest_instruction;
|
||||
use solana_config_program::date_instruction;
|
||||
use solana_runtime::bank::Bank;
|
||||
use solana_runtime::bank_client::BankClient;
|
||||
use solana_sdk::client::SyncClient;
|
||||
use solana_sdk::genesis_config::create_genesis_config;
|
||||
use solana_sdk::hash::hash;
|
||||
use solana_sdk::message::Message;
|
||||
use solana_sdk::signature::{Keypair, Signature, Signer};
|
||||
use solana_sdk::transaction::TransactionError;
|
||||
use solana_sdk::transport::Result;
|
||||
use std::sync::Arc;
|
||||
|
||||
fn create_bank(lamports: u64) -> (Bank, Keypair) {
|
||||
let (genesis_config, mint_keypair) = create_genesis_config(lamports);
|
||||
let mut bank = Bank::new(&genesis_config);
|
||||
bank.add_builtin("vest_program", id(), process_instruction);
|
||||
(bank, mint_keypair)
|
||||
}
|
||||
|
||||
fn create_bank_client(lamports: u64) -> (BankClient, Keypair) {
|
||||
let (bank, mint_keypair) = create_bank(lamports);
|
||||
(BankClient::new(bank), mint_keypair)
|
||||
}
|
||||
|
||||
/// Create a config account and use it as a date oracle.
|
||||
fn create_date_account(
|
||||
bank_client: &BankClient,
|
||||
date_keypair: &Keypair,
|
||||
payer_keypair: &Keypair,
|
||||
date: Date<Utc>,
|
||||
) -> Result<Signature> {
|
||||
let date_pubkey = date_keypair.pubkey();
|
||||
|
||||
let mut instructions =
|
||||
date_instruction::create_account(&payer_keypair.pubkey(), &date_pubkey, 1);
|
||||
instructions.push(date_instruction::store(&date_pubkey, date));
|
||||
|
||||
let message = Message::new(&instructions, Some(&payer_keypair.pubkey()));
|
||||
bank_client.send_and_confirm_message(&[payer_keypair, date_keypair], message)
|
||||
}
|
||||
|
||||
fn store_date(
|
||||
bank_client: &BankClient,
|
||||
date_keypair: &Keypair,
|
||||
payer_keypair: &Keypair,
|
||||
date: Date<Utc>,
|
||||
) -> Result<Signature> {
|
||||
let date_pubkey = date_keypair.pubkey();
|
||||
let instruction = date_instruction::store(&date_pubkey, date);
|
||||
let message = Message::new(&[instruction], Some(&payer_keypair.pubkey()));
|
||||
bank_client.send_and_confirm_message(&[payer_keypair, date_keypair], message)
|
||||
}
|
||||
|
||||
fn create_vest_account(
|
||||
bank_client: &BankClient,
|
||||
contract_keypair: &Keypair,
|
||||
payer_keypair: &Keypair,
|
||||
terminator_pubkey: &Pubkey,
|
||||
payee_pubkey: &Pubkey,
|
||||
start_date: Date<Utc>,
|
||||
date_pubkey: &Pubkey,
|
||||
lamports: u64,
|
||||
) -> Result<Signature> {
|
||||
let instructions = vest_instruction::create_account(
|
||||
&payer_keypair.pubkey(),
|
||||
&terminator_pubkey,
|
||||
&contract_keypair.pubkey(),
|
||||
&payee_pubkey,
|
||||
start_date,
|
||||
&date_pubkey,
|
||||
lamports,
|
||||
);
|
||||
let message = Message::new(&instructions, Some(&payer_keypair.pubkey()));
|
||||
bank_client.send_and_confirm_message(&[payer_keypair, contract_keypair], message)
|
||||
}
|
||||
|
||||
fn send_set_terminator(
|
||||
bank_client: &BankClient,
|
||||
contract_pubkey: &Pubkey,
|
||||
old_keypair: &Keypair,
|
||||
new_pubkey: &Pubkey,
|
||||
) -> Result<Signature> {
|
||||
let instruction =
|
||||
vest_instruction::set_terminator(&contract_pubkey, &old_keypair.pubkey(), &new_pubkey);
|
||||
bank_client.send_and_confirm_instruction(&old_keypair, instruction)
|
||||
}
|
||||
|
||||
fn send_set_payee(
|
||||
bank_client: &BankClient,
|
||||
contract_pubkey: &Pubkey,
|
||||
old_keypair: &Keypair,
|
||||
new_pubkey: &Pubkey,
|
||||
) -> Result<Signature> {
|
||||
let instruction =
|
||||
vest_instruction::set_payee(&contract_pubkey, &old_keypair.pubkey(), &new_pubkey);
|
||||
bank_client.send_and_confirm_instruction(&old_keypair, instruction)
|
||||
}
|
||||
|
||||
fn send_redeem_tokens(
|
||||
bank_client: &BankClient,
|
||||
contract_pubkey: &Pubkey,
|
||||
payer_keypair: &Keypair,
|
||||
payee_pubkey: &Pubkey,
|
||||
date_pubkey: &Pubkey,
|
||||
) -> Result<Signature> {
|
||||
let instruction =
|
||||
vest_instruction::redeem_tokens(&contract_pubkey, &date_pubkey, &payee_pubkey);
|
||||
let message = Message::new(&[instruction], Some(&payer_keypair.pubkey()));
|
||||
bank_client.send_and_confirm_message(&[payer_keypair], message)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_verify_account_unauthorized() {
|
||||
// Ensure client can't sneak in with an untrusted date account.
|
||||
let date_pubkey = solana_sdk::pubkey::new_rand();
|
||||
let account = AccountSharedData::new_ref(1, 0, &solana_config_program::id());
|
||||
let keyed_account = KeyedAccount::new(&date_pubkey, false, &account);
|
||||
|
||||
let mallory_pubkey = solana_sdk::pubkey::new_rand(); // <-- Attack! Not the expected account.
|
||||
assert_eq!(
|
||||
verify_account(&keyed_account, &mallory_pubkey).unwrap_err(),
|
||||
VestError::Unauthorized.into()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_verify_signed_account_missing_signature() {
|
||||
// Ensure client can't sneak in with an unsigned account.
|
||||
let date_pubkey = solana_sdk::pubkey::new_rand();
|
||||
let account = AccountSharedData::new_ref(1, 0, &solana_config_program::id());
|
||||
let keyed_account = KeyedAccount::new(&date_pubkey, false, &account); // <-- Attack! Unsigned transaction.
|
||||
|
||||
assert_eq!(
|
||||
verify_signed_account(&keyed_account, &date_pubkey).unwrap_err(),
|
||||
InstructionError::MissingRequiredSignature
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_verify_date_account_incorrect_program_id() {
|
||||
// Ensure client can't sneak in with a non-Config account.
|
||||
let date_pubkey = solana_sdk::pubkey::new_rand();
|
||||
let account = AccountSharedData::new_ref(1, 0, &id()); // <-- Attack! Pass Vest account where Config account is expected.
|
||||
let keyed_account = KeyedAccount::new(&date_pubkey, false, &account);
|
||||
assert_eq!(
|
||||
verify_date_account(&keyed_account, &date_pubkey).unwrap_err(),
|
||||
InstructionError::IncorrectProgramId
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_verify_date_account_uninitialized_config() {
|
||||
// Ensure no panic when `get_config_data()` returns an error.
|
||||
let date_pubkey = solana_sdk::pubkey::new_rand();
|
||||
let account = AccountSharedData::new_ref(1, 0, &solana_config_program::id()); // <-- Attack! Zero space.
|
||||
let keyed_account = KeyedAccount::new(&date_pubkey, false, &account);
|
||||
assert_eq!(
|
||||
verify_date_account(&keyed_account, &date_pubkey).unwrap_err(),
|
||||
InstructionError::InvalidAccountData
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_verify_date_account_invalid_date_config() {
|
||||
// Ensure no panic when `deserialize::<DateConfig>()` returns an error.
|
||||
let date_pubkey = solana_sdk::pubkey::new_rand();
|
||||
let account = AccountSharedData::new_ref(1, 1, &solana_config_program::id()); // Attack! 1 byte, enough to sneak by `get_config_data()`, but not DateConfig deserialize.
|
||||
let keyed_account = KeyedAccount::new(&date_pubkey, false, &account);
|
||||
assert_eq!(
|
||||
verify_date_account(&keyed_account, &date_pubkey).unwrap_err(),
|
||||
InstructionError::InvalidAccountData
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_verify_date_account_deserialize() {
|
||||
// Ensure no panic when `deserialize::<DateConfig>()` returns an error.
|
||||
let date_pubkey = solana_sdk::pubkey::new_rand();
|
||||
let account = AccountSharedData::new_ref(1, 1, &solana_config_program::id()); // Attack! 1 byte, enough to sneak by `get_config_data()`, but not DateConfig deserialize.
|
||||
let keyed_account = KeyedAccount::new(&date_pubkey, false, &account);
|
||||
assert_eq!(
|
||||
verify_date_account(&keyed_account, &date_pubkey).unwrap_err(),
|
||||
InstructionError::InvalidAccountData
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_initialize_no_panic() {
|
||||
let (bank_client, alice_keypair) = create_bank_client(3);
|
||||
|
||||
let contract_keypair = Keypair::new();
|
||||
|
||||
let mut instructions = vest_instruction::create_account(
|
||||
&alice_keypair.pubkey(),
|
||||
&solana_sdk::pubkey::new_rand(),
|
||||
&contract_keypair.pubkey(),
|
||||
&solana_sdk::pubkey::new_rand(),
|
||||
Utc::now().date(),
|
||||
&solana_sdk::pubkey::new_rand(),
|
||||
1,
|
||||
);
|
||||
instructions[1].accounts = vec![]; // <!-- Attack! Prevent accounts from being passed into processor.
|
||||
|
||||
let message = Message::new(&instructions, Some(&alice_keypair.pubkey()));
|
||||
assert_eq!(
|
||||
bank_client
|
||||
.send_and_confirm_message(&[&alice_keypair, &contract_keypair], message)
|
||||
.unwrap_err()
|
||||
.unwrap(),
|
||||
TransactionError::InstructionError(1, InstructionError::NotEnoughAccountKeys)
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn test_set_payee_and_terminator() {
|
||||
let (bank_client, alice_keypair) = create_bank_client(39);
|
||||
let alice_pubkey = alice_keypair.pubkey();
|
||||
let date_pubkey = solana_sdk::pubkey::new_rand();
|
||||
let contract_keypair = Keypair::new();
|
||||
let contract_pubkey = contract_keypair.pubkey();
|
||||
let bob_keypair = Keypair::new();
|
||||
let bob_pubkey = bob_keypair.pubkey();
|
||||
let start_date = Utc.ymd(2018, 1, 1);
|
||||
|
||||
create_vest_account(
|
||||
&bank_client,
|
||||
&contract_keypair,
|
||||
&alice_keypair,
|
||||
&alice_pubkey,
|
||||
&bob_pubkey,
|
||||
start_date,
|
||||
&date_pubkey,
|
||||
36,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let new_bob_pubkey = solana_sdk::pubkey::new_rand();
|
||||
|
||||
// Ensure some rando can't change the payee.
|
||||
// Transfer bob a token to pay the transaction fee.
|
||||
let mallory_keypair = Keypair::new();
|
||||
bank_client
|
||||
.transfer_and_confirm(1, &alice_keypair, &mallory_keypair.pubkey())
|
||||
.unwrap();
|
||||
send_set_payee(
|
||||
&bank_client,
|
||||
&contract_pubkey,
|
||||
&mallory_keypair,
|
||||
&new_bob_pubkey,
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
// Ensure bob can update which account he wants vested funds transferred to.
|
||||
bank_client
|
||||
.transfer_and_confirm(1, &alice_keypair, &bob_pubkey)
|
||||
.unwrap();
|
||||
send_set_payee(
|
||||
&bank_client,
|
||||
&contract_pubkey,
|
||||
&bob_keypair,
|
||||
&new_bob_pubkey,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Ensure the rando can't change the terminator either.
|
||||
let new_alice_pubkey = solana_sdk::pubkey::new_rand();
|
||||
send_set_terminator(
|
||||
&bank_client,
|
||||
&contract_pubkey,
|
||||
&mallory_keypair,
|
||||
&new_alice_pubkey,
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
// Ensure alice can update which pubkey she uses to terminate contracts.
|
||||
send_set_terminator(
|
||||
&bank_client,
|
||||
&contract_pubkey,
|
||||
&alice_keypair,
|
||||
&new_alice_pubkey,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set_payee() {
|
||||
let (bank_client, alice_keypair) = create_bank_client(38);
|
||||
let alice_pubkey = alice_keypair.pubkey();
|
||||
let date_pubkey = solana_sdk::pubkey::new_rand();
|
||||
let contract_keypair = Keypair::new();
|
||||
let contract_pubkey = contract_keypair.pubkey();
|
||||
let bob_keypair = Keypair::new();
|
||||
let bob_pubkey = bob_keypair.pubkey();
|
||||
let start_date = Utc.ymd(2018, 1, 1);
|
||||
|
||||
create_vest_account(
|
||||
&bank_client,
|
||||
&contract_keypair,
|
||||
&alice_keypair,
|
||||
&alice_pubkey,
|
||||
&bob_pubkey,
|
||||
start_date,
|
||||
&date_pubkey,
|
||||
36,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let new_bob_pubkey = solana_sdk::pubkey::new_rand();
|
||||
|
||||
// Ensure some rando can't change the payee.
|
||||
// Transfer bob a token to pay the transaction fee.
|
||||
let mallory_keypair = Keypair::new();
|
||||
bank_client
|
||||
.transfer_and_confirm(1, &alice_keypair, &mallory_keypair.pubkey())
|
||||
.unwrap();
|
||||
send_set_payee(
|
||||
&bank_client,
|
||||
&contract_pubkey,
|
||||
&mallory_keypair,
|
||||
&new_bob_pubkey,
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
// Ensure bob can update which account he wants vested funds transferred to.
|
||||
bank_client
|
||||
.transfer_and_confirm(1, &alice_keypair, &bob_pubkey)
|
||||
.unwrap();
|
||||
send_set_payee(
|
||||
&bank_client,
|
||||
&contract_pubkey,
|
||||
&bob_keypair,
|
||||
&new_bob_pubkey,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_redeem_tokens() {
|
||||
let (bank, alice_keypair) = create_bank(38);
|
||||
let bank = Arc::new(bank);
|
||||
let bank_client = BankClient::new_shared(&bank);
|
||||
let alice_pubkey = alice_keypair.pubkey();
|
||||
|
||||
let date_keypair = Keypair::new();
|
||||
let date_pubkey = date_keypair.pubkey();
|
||||
|
||||
let current_date = Utc.ymd(2019, 1, 1);
|
||||
create_date_account(&bank_client, &date_keypair, &alice_keypair, current_date).unwrap();
|
||||
|
||||
let contract_keypair = Keypair::new();
|
||||
let contract_pubkey = contract_keypair.pubkey();
|
||||
let bob_pubkey = solana_sdk::pubkey::new_rand();
|
||||
let start_date = Utc.ymd(2018, 1, 1);
|
||||
|
||||
create_vest_account(
|
||||
&bank_client,
|
||||
&contract_keypair,
|
||||
&alice_keypair,
|
||||
&alice_pubkey,
|
||||
&bob_pubkey,
|
||||
start_date,
|
||||
&date_pubkey,
|
||||
36,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(bank_client.get_balance(&alice_pubkey).unwrap(), 1);
|
||||
assert_eq!(bank_client.get_balance(&contract_pubkey).unwrap(), 36);
|
||||
|
||||
send_redeem_tokens(
|
||||
&bank_client,
|
||||
&contract_pubkey,
|
||||
&alice_keypair,
|
||||
&bob_pubkey,
|
||||
&date_pubkey,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(bank_client.get_balance(&alice_pubkey).unwrap(), 1);
|
||||
assert_eq!(bank_client.get_balance(&contract_pubkey).unwrap(), 24);
|
||||
assert_eq!(bank_client.get_balance(&bob_pubkey).unwrap(), 12);
|
||||
|
||||
// Update the date oracle and redeem more tokens
|
||||
store_date(
|
||||
&bank_client,
|
||||
&date_keypair,
|
||||
&alice_keypair,
|
||||
Utc.ymd(2019, 2, 1),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Force a new blockhash so that there's not a duplicate signature.
|
||||
for _ in 0..bank.ticks_per_slot() {
|
||||
bank.register_tick(&hash(&[1]));
|
||||
}
|
||||
|
||||
send_redeem_tokens(
|
||||
&bank_client,
|
||||
&contract_pubkey,
|
||||
&alice_keypair,
|
||||
&bob_pubkey,
|
||||
&date_pubkey,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(bank_client.get_balance(&alice_pubkey).unwrap(), 1);
|
||||
assert_eq!(bank_client.get_balance(&contract_pubkey).unwrap(), 23);
|
||||
assert_eq!(bank_client.get_balance(&bob_pubkey).unwrap(), 13);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_terminate_and_refund() {
|
||||
let (bank_client, alice_keypair) = create_bank_client(3);
|
||||
let alice_pubkey = alice_keypair.pubkey();
|
||||
let contract_keypair = Keypair::new();
|
||||
let contract_pubkey = contract_keypair.pubkey();
|
||||
let bob_pubkey = solana_sdk::pubkey::new_rand();
|
||||
let start_date = Utc::now().date();
|
||||
|
||||
let date_keypair = Keypair::new();
|
||||
let date_pubkey = date_keypair.pubkey();
|
||||
|
||||
let current_date = Utc.ymd(2019, 1, 1);
|
||||
create_date_account(&bank_client, &date_keypair, &alice_keypair, current_date).unwrap();
|
||||
|
||||
create_vest_account(
|
||||
&bank_client,
|
||||
&contract_keypair,
|
||||
&alice_keypair,
|
||||
&alice_pubkey,
|
||||
&bob_pubkey,
|
||||
start_date,
|
||||
&date_pubkey,
|
||||
1,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(bank_client.get_balance(&alice_pubkey).unwrap(), 1);
|
||||
assert_eq!(bank_client.get_balance(&contract_pubkey).unwrap(), 1);
|
||||
|
||||
// Now, terminate the transaction. alice gets her funds back
|
||||
// Note: that tokens up until the oracle date are *not* redeemed automatically.
|
||||
let instruction =
|
||||
vest_instruction::terminate(&contract_pubkey, &alice_pubkey, &alice_pubkey);
|
||||
bank_client
|
||||
.send_and_confirm_instruction(&alice_keypair, instruction)
|
||||
.unwrap();
|
||||
assert_eq!(bank_client.get_balance(&alice_pubkey).unwrap(), 2);
|
||||
assert_eq!(
|
||||
bank_client.get_account_data(&contract_pubkey).unwrap(),
|
||||
None
|
||||
);
|
||||
assert_eq!(bank_client.get_account_data(&bob_pubkey).unwrap(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_terminate_and_send_funds() {
|
||||
let (bank_client, alice_keypair) = create_bank_client(3);
|
||||
let alice_pubkey = alice_keypair.pubkey();
|
||||
let contract_keypair = Keypair::new();
|
||||
let contract_pubkey = contract_keypair.pubkey();
|
||||
let bob_pubkey = solana_sdk::pubkey::new_rand();
|
||||
let start_date = Utc::now().date();
|
||||
|
||||
let date_keypair = Keypair::new();
|
||||
let date_pubkey = date_keypair.pubkey();
|
||||
|
||||
let current_date = Utc.ymd(2019, 1, 1);
|
||||
create_date_account(&bank_client, &date_keypair, &alice_keypair, current_date).unwrap();
|
||||
|
||||
create_vest_account(
|
||||
&bank_client,
|
||||
&contract_keypair,
|
||||
&alice_keypair,
|
||||
&alice_pubkey,
|
||||
&bob_pubkey,
|
||||
start_date,
|
||||
&date_pubkey,
|
||||
1,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(bank_client.get_balance(&alice_pubkey).unwrap(), 1);
|
||||
assert_eq!(bank_client.get_balance(&contract_pubkey).unwrap(), 1);
|
||||
|
||||
// Now, terminate the transaction. carol gets the funds.
|
||||
let carol_pubkey = solana_sdk::pubkey::new_rand();
|
||||
let instruction =
|
||||
vest_instruction::terminate(&contract_pubkey, &alice_pubkey, &carol_pubkey);
|
||||
bank_client
|
||||
.send_and_confirm_instruction(&alice_keypair, instruction)
|
||||
.unwrap();
|
||||
assert_eq!(bank_client.get_balance(&alice_pubkey).unwrap(), 1);
|
||||
assert_eq!(bank_client.get_balance(&carol_pubkey).unwrap(), 1);
|
||||
assert_eq!(
|
||||
bank_client.get_account_data(&contract_pubkey).unwrap(),
|
||||
None
|
||||
);
|
||||
assert_eq!(bank_client.get_account_data(&bob_pubkey).unwrap(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_renege_and_send_funds() {
|
||||
let (bank_client, alice_keypair) = create_bank_client(3);
|
||||
let alice_pubkey = alice_keypair.pubkey();
|
||||
let contract_keypair = Keypair::new();
|
||||
let contract_pubkey = contract_keypair.pubkey();
|
||||
let bob_pubkey = solana_sdk::pubkey::new_rand();
|
||||
let start_date = Utc::now().date();
|
||||
|
||||
let date_keypair = Keypair::new();
|
||||
let date_pubkey = date_keypair.pubkey();
|
||||
|
||||
let current_date = Utc.ymd(2019, 1, 1);
|
||||
create_date_account(&bank_client, &date_keypair, &alice_keypair, current_date).unwrap();
|
||||
|
||||
create_vest_account(
|
||||
&bank_client,
|
||||
&contract_keypair,
|
||||
&alice_keypair,
|
||||
&alice_pubkey,
|
||||
&bob_pubkey,
|
||||
start_date,
|
||||
&date_pubkey,
|
||||
1,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(bank_client.get_balance(&alice_pubkey).unwrap(), 1);
|
||||
assert_eq!(bank_client.get_balance(&contract_pubkey).unwrap(), 1);
|
||||
|
||||
// Now, renege on a token. carol gets it.
|
||||
let carol_pubkey = solana_sdk::pubkey::new_rand();
|
||||
let instruction =
|
||||
vest_instruction::renege(&contract_pubkey, &alice_pubkey, &carol_pubkey, 1);
|
||||
bank_client
|
||||
.send_and_confirm_instruction(&alice_keypair, instruction)
|
||||
.unwrap();
|
||||
assert_eq!(bank_client.get_balance(&alice_pubkey).unwrap(), 1);
|
||||
assert_eq!(bank_client.get_balance(&carol_pubkey).unwrap(), 1);
|
||||
assert_eq!(
|
||||
bank_client.get_account_data(&contract_pubkey).unwrap(),
|
||||
None
|
||||
);
|
||||
assert_eq!(bank_client.get_account_data(&bob_pubkey).unwrap(), None);
|
||||
}
|
||||
}
|
|
@ -1,166 +0,0 @@
|
|||
//! A library for creating vesting schedules
|
||||
|
||||
use chrono::prelude::*;
|
||||
|
||||
/// Return the date that is 'n' months from 'start'.
|
||||
fn get_month(start: Date<Utc>, n: u32) -> Date<Utc> {
|
||||
let year = start.year() + (start.month0() + n) as i32 / 12;
|
||||
let month0 = (start.month0() + n) % 12;
|
||||
|
||||
// For those that started on the 31st, pay out on the latest day of the month.
|
||||
let mut dt = None;
|
||||
let mut days_back = 0;
|
||||
while dt.is_none() {
|
||||
dt = Utc
|
||||
.ymd_opt(year, month0 + 1, start.day() - days_back)
|
||||
.single();
|
||||
days_back += 1;
|
||||
}
|
||||
dt.unwrap()
|
||||
}
|
||||
|
||||
/// Integer division that also returns the remainder.
|
||||
fn div(dividend: u64, divisor: u64) -> (u64, u64) {
|
||||
(dividend / divisor, dividend % divisor)
|
||||
}
|
||||
|
||||
/// Return a list of contract messages and a list of vesting-date/lamports pairs.
|
||||
pub fn create_vesting_schedule(start_date: Date<Utc>, mut lamports: u64) -> Vec<(Date<Utc>, u64)> {
|
||||
let mut schedule = vec![];
|
||||
|
||||
// 1/3 vest after one year from start date.
|
||||
let (mut stipend, remainder) = div(lamports, 3);
|
||||
stipend += remainder;
|
||||
|
||||
let dt = get_month(start_date, 12);
|
||||
schedule.push((dt, stipend));
|
||||
|
||||
lamports -= stipend;
|
||||
|
||||
// Remaining 66% vest monthly after one year.
|
||||
let payments = 24u32;
|
||||
let (stipend, remainder) = div(lamports, u64::from(payments));
|
||||
for n in 0..payments {
|
||||
let mut stipend = stipend;
|
||||
if u64::from(n) < remainder {
|
||||
stipend += 1;
|
||||
}
|
||||
let dt = get_month(start_date, n + 13);
|
||||
schedule.push((dt, stipend));
|
||||
lamports -= stipend;
|
||||
}
|
||||
assert_eq!(lamports, 0);
|
||||
|
||||
schedule
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_get_month() {
|
||||
let start = Utc.ymd(2018, 1, 31);
|
||||
assert_eq!(get_month(start, 0), Utc.ymd(2018, 1, 31));
|
||||
assert_eq!(get_month(start, 1), Utc.ymd(2018, 2, 28));
|
||||
assert_eq!(get_month(start, 2), Utc.ymd(2018, 3, 31));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_vesting_schedule() {
|
||||
assert_eq!(
|
||||
create_vesting_schedule(Utc.ymd(2018, 1, 1), 36_000),
|
||||
vec![
|
||||
(Utc.ymd(2019, 1, 1), 12000),
|
||||
(Utc.ymd(2019, 2, 1), 1000),
|
||||
(Utc.ymd(2019, 3, 1), 1000),
|
||||
(Utc.ymd(2019, 4, 1), 1000),
|
||||
(Utc.ymd(2019, 5, 1), 1000),
|
||||
(Utc.ymd(2019, 6, 1), 1000),
|
||||
(Utc.ymd(2019, 7, 1), 1000),
|
||||
(Utc.ymd(2019, 8, 1), 1000),
|
||||
(Utc.ymd(2019, 9, 1), 1000),
|
||||
(Utc.ymd(2019, 10, 1), 1000),
|
||||
(Utc.ymd(2019, 11, 1), 1000),
|
||||
(Utc.ymd(2019, 12, 1), 1000),
|
||||
(Utc.ymd(2020, 1, 1), 1000),
|
||||
(Utc.ymd(2020, 2, 1), 1000),
|
||||
(Utc.ymd(2020, 3, 1), 1000),
|
||||
(Utc.ymd(2020, 4, 1), 1000),
|
||||
(Utc.ymd(2020, 5, 1), 1000),
|
||||
(Utc.ymd(2020, 6, 1), 1000),
|
||||
(Utc.ymd(2020, 7, 1), 1000),
|
||||
(Utc.ymd(2020, 8, 1), 1000),
|
||||
(Utc.ymd(2020, 9, 1), 1000),
|
||||
(Utc.ymd(2020, 10, 1), 1000),
|
||||
(Utc.ymd(2020, 11, 1), 1000),
|
||||
(Utc.ymd(2020, 12, 1), 1000),
|
||||
(Utc.ymd(2021, 1, 1), 1000),
|
||||
]
|
||||
);
|
||||
|
||||
// Ensure vesting date is sensible if start date was at the end of the month.
|
||||
assert_eq!(
|
||||
create_vesting_schedule(Utc.ymd(2018, 1, 31), 36_000),
|
||||
vec![
|
||||
(Utc.ymd(2019, 1, 31), 12000),
|
||||
(Utc.ymd(2019, 2, 28), 1000),
|
||||
(Utc.ymd(2019, 3, 31), 1000),
|
||||
(Utc.ymd(2019, 4, 30), 1000),
|
||||
(Utc.ymd(2019, 5, 31), 1000),
|
||||
(Utc.ymd(2019, 6, 30), 1000),
|
||||
(Utc.ymd(2019, 7, 31), 1000),
|
||||
(Utc.ymd(2019, 8, 31), 1000),
|
||||
(Utc.ymd(2019, 9, 30), 1000),
|
||||
(Utc.ymd(2019, 10, 31), 1000),
|
||||
(Utc.ymd(2019, 11, 30), 1000),
|
||||
(Utc.ymd(2019, 12, 31), 1000),
|
||||
(Utc.ymd(2020, 1, 31), 1000),
|
||||
(Utc.ymd(2020, 2, 29), 1000), // Leap year
|
||||
(Utc.ymd(2020, 3, 31), 1000),
|
||||
(Utc.ymd(2020, 4, 30), 1000),
|
||||
(Utc.ymd(2020, 5, 31), 1000),
|
||||
(Utc.ymd(2020, 6, 30), 1000),
|
||||
(Utc.ymd(2020, 7, 31), 1000),
|
||||
(Utc.ymd(2020, 8, 31), 1000),
|
||||
(Utc.ymd(2020, 9, 30), 1000),
|
||||
(Utc.ymd(2020, 10, 31), 1000),
|
||||
(Utc.ymd(2020, 11, 30), 1000),
|
||||
(Utc.ymd(2020, 12, 31), 1000),
|
||||
(Utc.ymd(2021, 1, 31), 1000),
|
||||
]
|
||||
);
|
||||
|
||||
// Awkward numbers
|
||||
assert_eq!(
|
||||
create_vesting_schedule(Utc.ymd(2018, 1, 1), 123_123),
|
||||
vec![
|
||||
(Utc.ymd(2019, 1, 1), 41041), // floor(123_123 / 3) + 123_123 % 3
|
||||
(Utc.ymd(2019, 2, 1), 3421), // ceil(82_082 / 24)
|
||||
(Utc.ymd(2019, 3, 1), 3421), // ceil(82_082 / 24)
|
||||
(Utc.ymd(2019, 4, 1), 3420), // floor(82_082 / 24)
|
||||
(Utc.ymd(2019, 5, 1), 3420),
|
||||
(Utc.ymd(2019, 6, 1), 3420),
|
||||
(Utc.ymd(2019, 7, 1), 3420),
|
||||
(Utc.ymd(2019, 8, 1), 3420),
|
||||
(Utc.ymd(2019, 9, 1), 3420),
|
||||
(Utc.ymd(2019, 10, 1), 3420),
|
||||
(Utc.ymd(2019, 11, 1), 3420),
|
||||
(Utc.ymd(2019, 12, 1), 3420),
|
||||
(Utc.ymd(2020, 1, 1), 3420),
|
||||
(Utc.ymd(2020, 2, 1), 3420),
|
||||
(Utc.ymd(2020, 3, 1), 3420),
|
||||
(Utc.ymd(2020, 4, 1), 3420),
|
||||
(Utc.ymd(2020, 5, 1), 3420),
|
||||
(Utc.ymd(2020, 6, 1), 3420),
|
||||
(Utc.ymd(2020, 7, 1), 3420),
|
||||
(Utc.ymd(2020, 8, 1), 3420),
|
||||
(Utc.ymd(2020, 9, 1), 3420),
|
||||
(Utc.ymd(2020, 10, 1), 3420),
|
||||
(Utc.ymd(2020, 11, 1), 3420),
|
||||
(Utc.ymd(2020, 12, 1), 3420),
|
||||
(Utc.ymd(2021, 1, 1), 3420),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,191 +0,0 @@
|
|||
//! vest state
|
||||
use crate::vest_schedule::create_vesting_schedule;
|
||||
use bincode::{self, deserialize, serialize_into};
|
||||
use chrono::prelude::*;
|
||||
use chrono::{
|
||||
prelude::{DateTime, TimeZone, Utc},
|
||||
serde::ts_seconds,
|
||||
};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use solana_sdk::{account::AccountSharedData, instruction::InstructionError, pubkey::Pubkey};
|
||||
use std::cmp::min;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
pub struct VestState {
|
||||
/// The address authorized to terminate this contract with a signed Terminate instruction
|
||||
pub terminator_pubkey: Pubkey,
|
||||
|
||||
/// The address authorized to redeem vested tokens
|
||||
pub payee_pubkey: Pubkey,
|
||||
|
||||
/// The day from which the vesting contract begins
|
||||
#[serde(with = "ts_seconds")]
|
||||
pub start_date_time: DateTime<Utc>,
|
||||
|
||||
/// Address of an account containing a trusted date, used to drive the vesting schedule
|
||||
pub date_pubkey: Pubkey,
|
||||
|
||||
/// The number of lamports to send the payee if the schedule completes
|
||||
pub total_lamports: u64,
|
||||
|
||||
/// The number of lamports the payee has already redeemed
|
||||
pub redeemed_lamports: u64,
|
||||
|
||||
/// The number of lamports the terminator repurchased
|
||||
pub reneged_lamports: u64,
|
||||
|
||||
/// True if the terminator has declared this contract fully vested.
|
||||
pub is_fully_vested: bool,
|
||||
}
|
||||
|
||||
impl Default for VestState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
terminator_pubkey: Pubkey::default(),
|
||||
payee_pubkey: Pubkey::default(),
|
||||
start_date_time: Utc.timestamp(0, 0),
|
||||
date_pubkey: Pubkey::default(),
|
||||
total_lamports: 0,
|
||||
redeemed_lamports: 0,
|
||||
reneged_lamports: 0,
|
||||
is_fully_vested: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl VestState {
|
||||
pub fn serialize(&self, output: &mut [u8]) -> Result<(), InstructionError> {
|
||||
serialize_into(output, self).map_err(|_| InstructionError::AccountDataTooSmall)
|
||||
}
|
||||
|
||||
pub fn deserialize(input: &[u8]) -> Result<Self, InstructionError> {
|
||||
deserialize(input).map_err(|_| InstructionError::InvalidAccountData)
|
||||
}
|
||||
|
||||
fn calc_vested_lamports(&self, current_date: Date<Utc>) -> u64 {
|
||||
let total_lamports_after_reneged = self.total_lamports - self.reneged_lamports;
|
||||
if self.is_fully_vested {
|
||||
return total_lamports_after_reneged;
|
||||
}
|
||||
|
||||
let schedule = create_vesting_schedule(self.start_date_time.date(), self.total_lamports);
|
||||
|
||||
let vested_lamports = schedule
|
||||
.into_iter()
|
||||
.take_while(|(dt, _)| *dt <= current_date)
|
||||
.map(|(_, lamports)| lamports)
|
||||
.sum::<u64>();
|
||||
|
||||
min(vested_lamports, total_lamports_after_reneged)
|
||||
}
|
||||
|
||||
/// Redeem vested tokens.
|
||||
pub fn redeem_tokens(
|
||||
&mut self,
|
||||
contract_account: &mut AccountSharedData,
|
||||
current_date: Date<Utc>,
|
||||
payee_account: &mut AccountSharedData,
|
||||
) {
|
||||
let vested_lamports = self.calc_vested_lamports(current_date);
|
||||
let redeemable_lamports = vested_lamports.saturating_sub(self.redeemed_lamports);
|
||||
|
||||
contract_account.lamports -= redeemable_lamports;
|
||||
payee_account.lamports += redeemable_lamports;
|
||||
|
||||
self.redeemed_lamports += redeemable_lamports;
|
||||
}
|
||||
|
||||
/// Renege on the given number of tokens and send them to the given payee.
|
||||
pub fn renege(
|
||||
&mut self,
|
||||
contract_account: &mut AccountSharedData,
|
||||
payee_account: &mut AccountSharedData,
|
||||
lamports: u64,
|
||||
) {
|
||||
let reneged_lamports = min(contract_account.lamports, lamports);
|
||||
payee_account.lamports += reneged_lamports;
|
||||
contract_account.lamports -= reneged_lamports;
|
||||
|
||||
self.reneged_lamports += reneged_lamports;
|
||||
}
|
||||
|
||||
/// Mark this contract as fully vested, regardless of the date.
|
||||
pub fn vest_all(&mut self) {
|
||||
self.is_fully_vested = true;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::id;
|
||||
use solana_sdk::account::{AccountSharedData, ReadableAccount, WritableAccount};
|
||||
use solana_sdk::system_program;
|
||||
|
||||
#[test]
|
||||
fn test_serializer() {
|
||||
let mut a = AccountSharedData::new(0, 512, &id());
|
||||
let b = VestState::default();
|
||||
b.serialize(a.data_as_mut_slice()).unwrap();
|
||||
let c = VestState::deserialize(&a.data()).unwrap();
|
||||
assert_eq!(b, c);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serializer_data_too_small() {
|
||||
let mut a = AccountSharedData::new(0, 1, &id());
|
||||
let b = VestState::default();
|
||||
assert_eq!(
|
||||
b.serialize(a.data_as_mut_slice()),
|
||||
Err(InstructionError::AccountDataTooSmall)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_schedule_after_renege() {
|
||||
let total_lamports = 3;
|
||||
let mut contract_account = AccountSharedData::new(total_lamports, 512, &id());
|
||||
let mut payee_account = AccountSharedData::new(0, 0, &system_program::id());
|
||||
let mut vest_state = VestState {
|
||||
total_lamports,
|
||||
start_date_time: Utc.ymd(2019, 1, 1).and_hms(0, 0, 0),
|
||||
..VestState::default()
|
||||
};
|
||||
vest_state
|
||||
.serialize(contract_account.data_as_mut_slice())
|
||||
.unwrap();
|
||||
let current_date = Utc.ymd(2020, 1, 1);
|
||||
assert_eq!(vest_state.calc_vested_lamports(current_date), 1);
|
||||
|
||||
// Verify vesting schedule is calculated with original amount.
|
||||
vest_state.renege(&mut contract_account, &mut payee_account, 1);
|
||||
assert_eq!(vest_state.calc_vested_lamports(current_date), 1);
|
||||
assert_eq!(vest_state.reneged_lamports, 1);
|
||||
|
||||
// Verify reneged tokens aren't redeemable.
|
||||
assert_eq!(vest_state.calc_vested_lamports(Utc.ymd(2022, 1, 1)), 2);
|
||||
|
||||
// Verify reneged tokens aren't redeemable after fully vesting.
|
||||
vest_state.vest_all();
|
||||
assert_eq!(vest_state.calc_vested_lamports(Utc.ymd(2022, 1, 1)), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vest_all() {
|
||||
let total_lamports = 3;
|
||||
let mut contract_account = AccountSharedData::new(total_lamports, 512, &id());
|
||||
let mut vest_state = VestState {
|
||||
total_lamports,
|
||||
start_date_time: Utc.ymd(2019, 1, 1).and_hms(0, 0, 0),
|
||||
..VestState::default()
|
||||
};
|
||||
vest_state
|
||||
.serialize(contract_account.data_as_mut_slice())
|
||||
.unwrap();
|
||||
let current_date = Utc.ymd(2020, 1, 1);
|
||||
assert_eq!(vest_state.calc_vested_lamports(current_date), 1);
|
||||
|
||||
vest_state.vest_all();
|
||||
assert_eq!(vest_state.calc_vested_lamports(current_date), 3);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue