parent
0c3ff6b75c
commit
5617162cb6
|
@ -1919,6 +1919,16 @@ dependencies = [
|
||||||
"version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
"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]]
|
[[package]]
|
||||||
name = "num-derive"
|
name = "num-derive"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
|
@ -3887,6 +3897,22 @@ dependencies = [
|
||||||
"tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"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]]
|
[[package]]
|
||||||
name = "solana-vote-api"
|
name = "solana-vote-api"
|
||||||
version = "0.20.0"
|
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 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 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 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-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-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"
|
"checksum num-traits 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)" = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31"
|
||||||
|
|
|
@ -42,6 +42,7 @@ members = [
|
||||||
"programs/stake_tests",
|
"programs/stake_tests",
|
||||||
"programs/storage_api",
|
"programs/storage_api",
|
||||||
"programs/storage_program",
|
"programs/storage_program",
|
||||||
|
"programs/vest_api",
|
||||||
"programs/vote_api",
|
"programs/vote_api",
|
||||||
"programs/vote_program",
|
"programs/vote_program",
|
||||||
"replicator",
|
"replicator",
|
||||||
|
|
|
@ -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"
|
|
@ -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)
|
||||||
|
}
|
|
@ -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"
|
||||||
|
);
|
|
@ -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)
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue