Merge native programs parts into one unit (#7047)

This commit is contained in:
Jack May
2019-11-20 10:12:43 -08:00
committed by GitHub
parent 42da1ce4e2
commit d184d3a732
71 changed files with 244 additions and 335 deletions

27
programs/vest/Cargo.toml Normal file
View File

@@ -0,0 +1,27 @@
[package]
name = "solana-vest-program"
version = "0.21.0"
description = "Solana Vest program API"
authors = ["Solana Maintainers <maintainers@solana.com>"]
repository = "https://github.com/solana-labs/solana"
license = "Apache-2.0"
homepage = "https://solana.com/"
edition = "2018"
[dependencies]
bincode = "1.2.0"
chrono = { version = "0.4.9", features = ["serde"] }
log = "0.4.8"
num-derive = "0.2"
num-traits = "0.2"
serde = "1.0.102"
serde_derive = "1.0.102"
solana-sdk = { path = "../../sdk", version = "0.21.0" }
solana-config-program = { path = "../config", version = "0.21.0" }
[dev-dependencies]
solana-runtime = { path = "../../runtime", version = "0.21.0" }
[lib]
crate-type = ["lib"]
name = "solana_vest_program"

View File

@@ -0,0 +1,58 @@
///
/// A library for creating a trusted date oracle.
///
use bincode::{deserialize, serialized_size};
use chrono::{
prelude::{Date, DateTime, TimeZone, Utc},
serde::ts_seconds,
};
use serde_derive::{Deserialize, Serialize};
use solana_config_program::{config_instruction, ConfigState};
use solana_sdk::{instruction::Instruction, pubkey::Pubkey};
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
pub struct DateConfig {
#[serde(with = "ts_seconds")]
pub date_time: DateTime<Utc>,
}
impl Default for DateConfig {
fn default() -> Self {
Self {
date_time: Utc.timestamp(0, 0),
}
}
}
impl DateConfig {
pub fn new(date: Date<Utc>) -> Self {
Self {
date_time: date.and_hms(0, 0, 0),
}
}
pub fn deserialize(input: &[u8]) -> Option<Self> {
deserialize(input).ok()
}
}
impl ConfigState for DateConfig {
fn max_space() -> u64 {
serialized_size(&Self::default()).unwrap()
}
}
/// Create a date account. The date is set to the Unix epoch.
pub fn create_account(
payer_pubkey: &Pubkey,
date_pubkey: &Pubkey,
lamports: u64,
) -> Vec<Instruction> {
config_instruction::create_account::<DateConfig>(payer_pubkey, date_pubkey, lamports, vec![])
}
/// Set the date in the date account. The account pubkey must be signed in the
/// transaction containing this instruction.
pub fn store(date_pubkey: &Pubkey, date: Date<Utc>) -> Instruction {
let date_config = DateConfig::new(date);
config_instruction::store(&date_pubkey, true, vec![], &date_config)
}

19
programs/vest/src/lib.rs Normal file
View File

@@ -0,0 +1,19 @@
pub mod date_instruction;
pub mod vest_instruction;
pub mod vest_processor;
pub mod vest_schedule;
pub mod vest_state;
use crate::vest_processor::process_instruction;
const VEST_PROGRAM_ID: [u8; 32] = [
7, 87, 23, 47, 219, 236, 238, 33, 137, 188, 215, 141, 32, 229, 155, 195, 133, 124, 23, 232,
113, 153, 252, 252, 111, 5, 187, 128, 0, 0, 0, 0,
];
solana_sdk::declare_program!(
VEST_PROGRAM_ID,
"Vest111111111111111111111111111111111111111",
solana_vest_program,
process_instruction
);

View File

@@ -0,0 +1,140 @@
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::{
instruction::{AccountMeta, Instruction, InstructionError},
instruction_processor_utils::DecodeError,
pubkey::Pubkey,
system_instruction,
};
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, FromPrimitive)]
pub enum VestError {
DestinationMissing,
Unauthorized,
}
impl From<VestError> for InstructionError {
fn from(e: VestError) -> Self {
InstructionError::CustomError(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,
}
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(
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(
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(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(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(id(), &VestInstruction::Terminate, account_metas)
}

View File

@@ -0,0 +1,634 @@
//! vest program
use crate::date_instruction::DateConfig;
use crate::{
vest_instruction::{VestError, VestInstruction},
vest_state::VestState,
};
use chrono::prelude::*;
use solana_config_program::get_config_data;
use solana_sdk::{
account::{Account, KeyedAccount},
instruction::InstructionError,
instruction_processor_utils::{limited_deserialize, next_keyed_account},
pubkey::Pubkey,
};
fn verify_date_account(
keyed_account: &mut KeyedAccount,
expected_pubkey: &Pubkey,
) -> Result<Date<Utc>, InstructionError> {
if keyed_account.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 mut KeyedAccount,
expected_pubkey: &Pubkey,
) -> Result<&'a mut Account, InstructionError> {
if keyed_account.unsigned_key() != expected_pubkey {
return Err(VestError::Unauthorized.into());
}
Ok(keyed_account.account)
}
fn verify_signed_account<'a>(
keyed_account: &'a mut KeyedAccount,
expected_pubkey: &Pubkey,
) -> Result<&'a mut Account, 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,
keyed_accounts: &mut [KeyedAccount],
data: &[u8],
) -> Result<(), InstructionError> {
let keyed_accounts_iter = &mut keyed_accounts.iter_mut();
let contract_account = &mut next_keyed_account(keyed_accounts_iter)?.account;
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,
redeemed_lamports: 0,
}
} else {
VestState::deserialize(&contract_account.data)?
};
match instruction {
VestInstruction::InitializeAccount { .. } => {}
VestInstruction::SetTerminator(new_pubkey) => {
verify_signed_account(
next_keyed_account(keyed_accounts_iter)?,
&vest_state.terminator_pubkey,
)?;
vest_state.terminator_pubkey = new_pubkey;
}
VestInstruction::SetPayee(new_pubkey) => {
verify_signed_account(
next_keyed_account(keyed_accounts_iter)?,
&vest_state.payee_pubkey,
)?;
vest_state.payee_pubkey = new_pubkey;
}
VestInstruction::RedeemTokens => {
let current_date = verify_date_account(
next_keyed_account(keyed_accounts_iter)?,
&vest_state.date_pubkey,
)?;
let payee_account = verify_account(
next_keyed_account(keyed_accounts_iter)?,
&vest_state.payee_pubkey,
)?;
vest_state.redeem_tokens(contract_account, current_date, payee_account);
}
VestInstruction::Terminate => {
let terminator_account = verify_signed_account(
next_keyed_account(keyed_accounts_iter)?,
&vest_state.terminator_pubkey,
)?;
let payee_keyed_account = keyed_accounts_iter.next();
let payee_account = if let Some(payee_keyed_account) = payee_keyed_account {
&mut payee_keyed_account.account
} else {
terminator_account
};
vest_state.terminate(contract_account, payee_account);
}
}
vest_state.serialize(&mut contract_account.data)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::date_instruction;
use crate::id;
use crate::vest_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, KeypairUtil, Signature};
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_instruction_processor(
solana_config_program::id(),
solana_config_program::config_processor::process_instruction,
);
bank.add_instruction_processor(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);
bank_client.send_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_with_payer(vec![instruction], Some(&payer_keypair.pubkey()));
bank_client.send_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);
bank_client.send_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_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_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_with_payer(vec![instruction], Some(&payer_keypair.pubkey()));
bank_client.send_message(&[&payer_keypair], message)
}
#[test]
fn test_verify_account_unauthorized() {
// Ensure client can't sneak in with an untrusted date account.
let date_pubkey = Pubkey::new_rand();
let mut account = Account::new(1, 0, &solana_config_program::id());
let mut keyed_account = KeyedAccount::new(&date_pubkey, false, &mut account);
let mallory_pubkey = Pubkey::new_rand(); // <-- Attack! Not the expected account.
assert_eq!(
verify_account(&mut 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 = Pubkey::new_rand();
let mut account = Account::new(1, 0, &solana_config_program::id());
let mut keyed_account = KeyedAccount::new(&date_pubkey, false, &mut account); // <-- Attack! Unsigned transaction.
assert_eq!(
verify_signed_account(&mut keyed_account, &date_pubkey).unwrap_err(),
InstructionError::MissingRequiredSignature.into()
);
}
#[test]
fn test_verify_date_account_incorrect_program_id() {
// Ensure client can't sneak in with a non-Config account.
let date_pubkey = Pubkey::new_rand();
let mut account = Account::new(1, 0, &id()); // <-- Attack! Pass Vest account where Config account is expected.
let mut keyed_account = KeyedAccount::new(&date_pubkey, false, &mut account);
assert_eq!(
verify_date_account(&mut 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 = Pubkey::new_rand();
let mut account = Account::new(1, 0, &solana_config_program::id()); // <-- Attack! Zero space.
let mut keyed_account = KeyedAccount::new(&date_pubkey, false, &mut account);
assert_eq!(
verify_date_account(&mut 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 = Pubkey::new_rand();
let mut account = Account::new(1, 1, &solana_config_program::id()); // Attack! 1 byte, enough to sneak by `get_config_data()`, but not DateConfig deserialize.
let mut keyed_account = KeyedAccount::new(&date_pubkey, false, &mut account);
assert_eq!(
verify_date_account(&mut 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 = Pubkey::new_rand();
let mut account = Account::new(1, 1, &solana_config_program::id()); // Attack! 1 byte, enough to sneak by `get_config_data()`, but not DateConfig deserialize.
let mut keyed_account = KeyedAccount::new(&date_pubkey, false, &mut account);
assert_eq!(
verify_date_account(&mut 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(),
&Pubkey::new_rand(),
&contract_keypair.pubkey(),
&Pubkey::new_rand(),
Utc::now().date(),
&Pubkey::new_rand(),
1,
);
instructions[1].accounts = vec![]; // <!-- Attack! Prevent accounts from being passed into processor.
let message = Message::new(instructions);
assert_eq!(
bank_client
.send_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 = 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 = 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(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 transfered to.
bank_client
.transfer(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 = 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 = 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 = 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(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 transfered to.
bank_client
.transfer(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 = 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 = 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_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 = 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 = Pubkey::new_rand();
let instruction =
vest_instruction::terminate(&contract_pubkey, &alice_pubkey, &carol_pubkey);
bank_client
.send_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

@@ -0,0 +1,166 @@
//! 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

@@ -0,0 +1,110 @@
//! 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::Account, instruction::InstructionError, pubkey::Pubkey};
#[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,
}
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,
}
}
}
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)
}
/// Redeem vested tokens.
pub fn redeem_tokens(
&mut self,
contract_account: &mut Account,
current_date: Date<Utc>,
payee_account: &mut Account,
) {
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>();
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;
}
/// Terminate the contract and return all tokens to the given pubkey.
pub fn terminate(&mut self, contract_account: &mut Account, payee_account: &mut Account) {
payee_account.lamports += contract_account.lamports;
contract_account.lamports = 0;
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::id;
use solana_sdk::account::Account;
#[test]
fn test_serializer() {
let mut a = Account::new(0, 512, &id());
let b = VestState::default();
b.serialize(&mut a.data).unwrap();
let c = VestState::deserialize(&a.data).unwrap();
assert_eq!(b, c);
}
#[test]
fn test_serializer_data_too_small() {
let mut a = Account::new(0, 1, &id());
let b = VestState::default();
assert_eq!(
b.serialize(&mut a.data),
Err(InstructionError::AccountDataTooSmall)
);
}
}