delete vest program (#16795)

This commit is contained in:
Jeff Washington (jwash) 2021-04-26 08:50:46 -05:00 committed by GitHub
parent 97812570e7
commit 6d160768d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 2 additions and 1303 deletions

18
Cargo.lock generated
View File

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

View File

@ -49,7 +49,6 @@ members = [
"programs/noop",
"programs/ownable",
"programs/stake",
"programs/vest",
"programs/vote",
"remote-wallet",
"ramp-tps",

View File

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

View File

@ -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![]
};

View File

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

View File

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

View File

@ -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"]

View File

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

View File

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

View File

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

View File

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

View File

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