Add Vest program (#5987)

automerge
This commit is contained in:
Greg Fitzgerald 2019-10-04 15:43:50 -06:00 committed by Grimes
parent 0c3ff6b75c
commit 5617162cb6
9 changed files with 1020 additions and 0 deletions

27
Cargo.lock generated
View File

@ -1919,6 +1919,16 @@ dependencies = [
"version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "num-derive"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 0.15.42 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "num-derive"
version = "0.3.0"
@ -3887,6 +3897,22 @@ dependencies = [
"tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "solana-vest-api"
version = "0.20.0-pre0"
dependencies = [
"bincode 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"chrono 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
"num-derive 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)",
"num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_derive 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)",
"solana-config-api 0.20.0",
"solana-runtime 0.20.0",
"solana-sdk 0.20.0",
]
[[package]]
name = "solana-vote-api"
version = "0.20.0"
@ -5543,6 +5569,7 @@ dependencies = [
"checksum nix 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3b2e0b4f3320ed72aaedb9a5ac838690a8047c7b275da22711fddff4f8a14229"
"checksum nodrop 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "2f9667ddcc6cc8a43afc9b7917599d7216aa09c463919ea32c59ed6cac8bc945"
"checksum nom 4.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6"
"checksum num-derive 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "eafd0b45c5537c3ba526f79d3e75120036502bebacbb3f3220914067ce39dbf2"
"checksum num-derive 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0c8b15b261814f992e33760b1fca9fe8b693d8a65299f20c9901688636cfb746"
"checksum num-integer 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)" = "b85e541ef8255f6cf42bbfe4ef361305c6c135d10919ecc26126c4e5ae94bc09"
"checksum num-traits 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)" = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31"

View File

@ -42,6 +42,7 @@ members = [
"programs/stake_tests",
"programs/storage_api",
"programs/storage_program",
"programs/vest_api",
"programs/vote_api",
"programs/vote_program",
"replicator",

View File

@ -0,0 +1,27 @@
[package]
name = "solana-vest-api"
version = "0.20.0-pre0"
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.1.4"
chrono = { version = "0.4.9", features = ["serde"] }
log = "0.4.8"
num-derive = "0.2"
num-traits = "0.2"
serde = "1.0.100"
serde_derive = "1.0.100"
solana-sdk = { path = "../../sdk", version = "0.20.0-pre0" }
solana-config-api = { path = "../config_api", version = "0.20.0-pre0" }
[dev-dependencies]
solana-runtime = { path = "../../runtime", version = "0.20.0-pre0" }
[lib]
crate-type = ["lib"]
name = "solana_budget_api"

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_api::{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 dt: DateTime<Utc>,
}
impl Default for DateConfig {
fn default() -> Self {
Self {
dt: Utc.timestamp(0, 0),
}
}
}
impl DateConfig {
pub fn new(dt: Date<Utc>) -> Self {
Self {
dt: dt.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, dt: Date<Utc>) -> Instruction {
let date_config = DateConfig::new(dt);
config_instruction::store(&date_pubkey, true, vec![], &date_config)
}

View File

@ -0,0 +1,15 @@
pub mod date_instruction;
pub mod vest_instruction;
pub mod vest_processor;
pub mod vest_schedule;
pub mod vest_state;
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::solana_name_id!(
VEST_PROGRAM_ID,
"Vest111111111111111111111111111111111111111"
);

View File

@ -0,0 +1,144 @@
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"
}
}
impl std::fmt::Display for VestError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"{}",
match self {
VestError::DestinationMissing => "destination missing",
VestError::Unauthorized => "unauthorized",
}
)
}
}
impl std::error::Error for 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 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(
terminator_pubkey: &Pubkey,
payee_pubkey: &Pubkey,
contract_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(
&terminator_pubkey,
contract_pubkey,
lamports,
space,
&id(),
),
initialize_account(
terminator_pubkey,
payee_pubkey,
contract_pubkey,
start_date,
date_pubkey,
lamports,
),
]
}
pub fn set_payee(old_payee: &Pubkey, contract: &Pubkey, new_payee: &Pubkey) -> Instruction {
let account_metas = vec![
AccountMeta::new(*old_payee, true),
AccountMeta::new(*contract, false),
];
Instruction::new(id(), &VestInstruction::SetPayee(*new_payee), account_metas)
}
pub fn terminate(from: &Pubkey, contract: &Pubkey, to: &Pubkey) -> Instruction {
let mut account_metas = vec![
AccountMeta::new(*from, true),
AccountMeta::new(*contract, false),
];
if from != to {
account_metas.push(AccountMeta::new_credit_only(*to, false));
}
Instruction::new(id(), &VestInstruction::Terminate, account_metas)
}
pub fn redeem_tokens(date_pubkey: &Pubkey, contract: &Pubkey, to: &Pubkey) -> Instruction {
let account_metas = vec![
AccountMeta::new_credit_only(*date_pubkey, false),
AccountMeta::new(*contract, false),
AccountMeta::new_credit_only(*to, false),
];
Instruction::new(id(), &VestInstruction::RedeemTokens, account_metas)
}

View File

@ -0,0 +1,472 @@
//! vest program
use crate::date_instruction::DateConfig;
use crate::{
vest_instruction::{VestError, VestInstruction},
vest_state::VestState,
};
use bincode::deserialize;
use chrono::prelude::*;
use solana_config_api::get_config_data;
use solana_sdk::{
account::{Account, KeyedAccount},
instruction::InstructionError,
pubkey::Pubkey,
};
fn parse_date_account(
keyed_account: &mut KeyedAccount,
expected_pubkey: &Pubkey,
) -> Result<Date<Utc>, InstructionError> {
if keyed_account.account.owner != solana_config_api::id() {
return Err(InstructionError::IncorrectProgramId);
}
let account = parse_account(keyed_account, expected_pubkey)?;
let config_data =
get_config_data(&account.data).map_err(|_| InstructionError::InvalidAccountData)?;
let date_config =
deserialize::<DateConfig>(config_data).map_err(|_| InstructionError::InvalidAccountData)?;
Ok(date_config.dt.date())
}
fn parse_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 parse_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);
}
parse_account(keyed_account, expected_pubkey)
}
pub fn process_instruction(
_program_id: &Pubkey,
keyed_accounts: &mut [KeyedAccount],
data: &[u8],
) -> Result<(), InstructionError> {
let instruction = deserialize(data).map_err(|_| InstructionError::InvalidInstructionData)?;
match instruction {
VestInstruction::InitializeAccount {
terminator_pubkey,
payee_pubkey,
start_date_time,
date_pubkey,
total_lamports,
} => {
let contract_account = &mut keyed_accounts[0].account;
let vest_state = VestState {
terminator_pubkey,
payee_pubkey,
start_date_time,
date_pubkey,
total_lamports,
redeemed_lamports: 0,
};
vest_state.serialize(&mut contract_account.data)
}
VestInstruction::SetPayee(payee_pubkey) => {
let (old_payee_keyed_account, contract_keyed_account) = match keyed_accounts {
[ka0, ka1] => (ka0, ka1),
_ => return Err(InstructionError::InvalidArgument),
};
let contract_account = &mut contract_keyed_account.account;
let mut vest_state = VestState::deserialize(&contract_account.data)?;
parse_signed_account(old_payee_keyed_account, &vest_state.payee_pubkey)?;
vest_state.payee_pubkey = payee_pubkey;
vest_state.serialize(&mut contract_account.data)
}
VestInstruction::RedeemTokens => {
let (date_keyed_account, contract_keyed_account, payee_keyed_account) =
match keyed_accounts {
[ka0, ka1, ka2] => (ka0, ka1, ka2),
_ => return Err(InstructionError::InvalidArgument),
};
let contract_account = &mut contract_keyed_account.account;
let mut vest_state = VestState::deserialize(&contract_account.data)?;
let current_date = parse_date_account(date_keyed_account, &vest_state.date_pubkey)?;
let payee_account = parse_account(payee_keyed_account, &vest_state.payee_pubkey)?;
vest_state.redeem_tokens(current_date, contract_account, payee_account);
vest_state.serialize(&mut contract_account.data)
}
VestInstruction::Terminate => {
let (terminator_keyed_account, contract_keyed_account, payee_keyed_account) =
match keyed_accounts {
[ka0, ka1] => (ka0, ka1, None),
[ka0, ka1, ka2] => (ka0, ka1, Some(ka2)),
_ => return Err(InstructionError::InvalidArgument),
};
let contract_account = &mut contract_keyed_account.account;
let mut vest_state = VestState::deserialize(&contract_account.data)?;
let terminator_account =
parse_signed_account(terminator_keyed_account, &vest_state.terminator_pubkey)?;
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_block::create_genesis_block;
use solana_sdk::hash::hash;
use solana_sdk::message::Message;
use solana_sdk::signature::{Keypair, KeypairUtil, Signature};
use solana_sdk::transport::TransportError;
use std::sync::Arc;
fn create_bank(lamports: u64) -> (Bank, Keypair) {
let (genesis_block, mint_keypair) = create_genesis_block(lamports);
let mut bank = Bank::new(&genesis_block);
bank.add_instruction_processor(
solana_config_api::id(),
solana_config_api::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,
payer_keypair: &Keypair,
date_keypair: &Keypair,
dt: Date<Utc>,
) -> Result<Signature, TransportError> {
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, dt));
let message = Message::new(instructions);
bank_client.send_message(&[&payer_keypair, &date_keypair], message)
}
fn store_date(
bank_client: &BankClient,
payer_keypair: &Keypair,
date_keypair: &Keypair,
dt: Date<Utc>,
) -> Result<Signature, TransportError> {
let date_pubkey = date_keypair.pubkey();
let instruction = date_instruction::store(&date_pubkey, dt);
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,
payer_keypair: &Keypair,
payee_pubkey: &Pubkey,
contract_pubkey: &Pubkey,
start_date: Date<Utc>,
date_pubkey: &Pubkey,
lamports: u64,
) -> Result<Signature, TransportError> {
let instructions = vest_instruction::create_account(
&payer_keypair.pubkey(),
&payee_pubkey,
&contract_pubkey,
start_date,
&date_pubkey,
lamports,
);
let message = Message::new(instructions);
bank_client.send_message(&[&payer_keypair], message)
}
fn send_set_payee(
bank_client: &BankClient,
old_payee_keypair: &Keypair,
contract_pubkey: &Pubkey,
new_payee_pubkey: &Pubkey,
) -> Result<Signature, TransportError> {
let instruction = vest_instruction::set_payee(
&old_payee_keypair.pubkey(),
&contract_pubkey,
&new_payee_pubkey,
);
bank_client.send_instruction(&old_payee_keypair, instruction)
}
fn send_redeem_tokens(
bank_client: &BankClient,
payer_keypair: &Keypair,
payee_pubkey: &Pubkey,
contract_pubkey: &Pubkey,
date_pubkey: &Pubkey,
) -> Result<Signature, TransportError> {
let instruction =
vest_instruction::redeem_tokens(&date_pubkey, &contract_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_parse_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_api::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!(
parse_account(&mut keyed_account, &mallory_pubkey).unwrap_err(),
VestError::Unauthorized.into()
);
}
#[test]
fn test_parse_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_api::id());
let mut keyed_account = KeyedAccount::new(&date_pubkey, false, &mut account); // <-- Attack! Unsigned transaction.
assert_eq!(
parse_signed_account(&mut keyed_account, &date_pubkey).unwrap_err(),
InstructionError::MissingRequiredSignature.into()
);
}
#[test]
fn test_parse_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!(
parse_date_account(&mut keyed_account, &date_pubkey).unwrap_err(),
InstructionError::IncorrectProgramId
);
}
#[test]
fn test_parse_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_api::id()); // <-- Attack! Zero space.
let mut keyed_account = KeyedAccount::new(&date_pubkey, false, &mut account);
assert_eq!(
parse_date_account(&mut keyed_account, &date_pubkey).unwrap_err(),
InstructionError::InvalidAccountData
);
}
#[test]
fn test_parse_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_api::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!(
parse_date_account(&mut keyed_account, &date_pubkey).unwrap_err(),
InstructionError::InvalidAccountData
);
}
#[test]
fn test_parse_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_api::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!(
parse_date_account(&mut keyed_account, &date_pubkey).unwrap_err(),
InstructionError::InvalidAccountData
);
}
#[test]
fn test_set_payee() {
let (bank_client, alice_keypair) = create_bank_client(38);
let date_pubkey = Pubkey::new_rand();
let contract_pubkey = Pubkey::new_rand();
let bob_keypair = Keypair::new();
let bob_pubkey = bob_keypair.pubkey();
let start_date = Utc.ymd(2018, 1, 1);
create_vest_account(
&bank_client,
&alice_keypair,
&bob_pubkey,
&contract_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,
&mallory_keypair,
&contract_pubkey,
&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,
&bob_keypair,
&contract_pubkey,
&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, &alice_keypair, &date_keypair, current_date).unwrap();
let contract_pubkey = Pubkey::new_rand();
let bob_pubkey = Pubkey::new_rand();
let start_date = Utc.ymd(2018, 1, 1);
create_vest_account(
&bank_client,
&alice_keypair,
&bob_pubkey,
&contract_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,
&alice_keypair,
&bob_pubkey,
&contract_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,
&alice_keypair,
&date_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,
&alice_keypair,
&bob_pubkey,
&contract_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_cancel_payment() {
let (bank_client, alice_keypair) = create_bank_client(3);
let alice_pubkey = alice_keypair.pubkey();
let contract_pubkey = Pubkey::new_rand();
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, &alice_keypair, &date_keypair, current_date).unwrap();
create_vest_account(
&bank_client,
&alice_keypair,
&bob_pubkey,
&contract_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(&alice_pubkey, &contract_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);
}
}

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,
current_date: Date<Utc>,
contract_account: &mut Account,
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)
);
}
}