examples/lockup: Specify start date of vesting schedule
This commit is contained in:
parent
a780002683
commit
2499195523
|
@ -57,6 +57,7 @@ dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"proc-macro2 1.0.24",
|
"proc-macro2 1.0.24",
|
||||||
"quote 1.0.8",
|
"quote 1.0.8",
|
||||||
|
"regex",
|
||||||
"syn 1.0.57",
|
"syn 1.0.57",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -181,6 +182,7 @@ name = "anchor-spl"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anchor-lang",
|
"anchor-lang",
|
||||||
|
"solana-program",
|
||||||
"spl-token 3.0.1",
|
"spl-token 3.0.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ pub fn available_for_withdrawal(vesting: &Vesting, current_ts: i64) -> u64 {
|
||||||
}
|
}
|
||||||
|
|
||||||
// The amount of funds currently in the vault.
|
// The amount of funds currently in the vault.
|
||||||
pub fn balance(vesting: &Vesting) -> u64 {
|
fn balance(vesting: &Vesting) -> u64 {
|
||||||
vesting
|
vesting
|
||||||
.outstanding
|
.outstanding
|
||||||
.checked_sub(vesting.whitelist_owned)
|
.checked_sub(vesting.whitelist_owned)
|
||||||
|
@ -33,13 +33,14 @@ fn withdrawn_amount(vesting: &Vesting) -> u64 {
|
||||||
// Returns the total vested amount up to the given ts, assuming zero
|
// Returns the total vested amount up to the given ts, assuming zero
|
||||||
// withdrawals and zero funds sent to other programs.
|
// withdrawals and zero funds sent to other programs.
|
||||||
fn total_vested(vesting: &Vesting, current_ts: i64) -> u64 {
|
fn total_vested(vesting: &Vesting, current_ts: i64) -> u64 {
|
||||||
assert!(current_ts >= vesting.start_ts);
|
if current_ts < vesting.start_ts {
|
||||||
|
0
|
||||||
if current_ts >= vesting.end_ts {
|
} else if current_ts >= vesting.end_ts {
|
||||||
return vesting.start_balance;
|
vesting.start_balance
|
||||||
}
|
} else {
|
||||||
linear_unlock(vesting, current_ts).unwrap()
|
linear_unlock(vesting, current_ts).unwrap()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn linear_unlock(vesting: &Vesting, current_ts: i64) -> Option<u64> {
|
fn linear_unlock(vesting: &Vesting, current_ts: i64) -> Option<u64> {
|
||||||
// Signed division not supported.
|
// Signed division not supported.
|
||||||
|
|
|
@ -4,8 +4,8 @@
|
||||||
#![feature(proc_macro_hygiene)]
|
#![feature(proc_macro_hygiene)]
|
||||||
|
|
||||||
use anchor_lang::prelude::*;
|
use anchor_lang::prelude::*;
|
||||||
use anchor_lang::solana_program;
|
|
||||||
use anchor_lang::solana_program::instruction::Instruction;
|
use anchor_lang::solana_program::instruction::Instruction;
|
||||||
|
use anchor_lang::solana_program::program;
|
||||||
use anchor_spl::token::{self, TokenAccount, Transfer};
|
use anchor_spl::token::{self, TokenAccount, Transfer};
|
||||||
|
|
||||||
mod calculator;
|
mod calculator;
|
||||||
|
@ -18,7 +18,8 @@ pub mod lockup {
|
||||||
pub struct Lockup {
|
pub struct Lockup {
|
||||||
/// The key with the ability to change the whitelist.
|
/// The key with the ability to change the whitelist.
|
||||||
pub authority: Pubkey,
|
pub authority: Pubkey,
|
||||||
/// Valid programs the program can relay transactions to.
|
/// List of programs locked tokens can be sent to. These programs
|
||||||
|
/// are completely trusted to maintain the locked property.
|
||||||
pub whitelist: Vec<WhitelistEntry>,
|
pub whitelist: Vec<WhitelistEntry>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,25 +71,19 @@ pub mod lockup {
|
||||||
pub fn create_vesting(
|
pub fn create_vesting(
|
||||||
ctx: Context<CreateVesting>,
|
ctx: Context<CreateVesting>,
|
||||||
beneficiary: Pubkey,
|
beneficiary: Pubkey,
|
||||||
end_ts: i64,
|
|
||||||
period_count: u64,
|
|
||||||
deposit_amount: u64,
|
deposit_amount: u64,
|
||||||
nonce: u8,
|
nonce: u8,
|
||||||
|
start_ts: i64,
|
||||||
|
end_ts: i64,
|
||||||
|
period_count: u64,
|
||||||
realizor: Option<Realizor>,
|
realizor: Option<Realizor>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
if end_ts <= ctx.accounts.clock.unix_timestamp {
|
|
||||||
return Err(ErrorCode::InvalidTimestamp.into());
|
|
||||||
}
|
|
||||||
if period_count > (end_ts - ctx.accounts.clock.unix_timestamp) as u64 {
|
|
||||||
return Err(ErrorCode::InvalidPeriod.into());
|
|
||||||
}
|
|
||||||
if period_count == 0 {
|
|
||||||
return Err(ErrorCode::InvalidPeriod.into());
|
|
||||||
}
|
|
||||||
if deposit_amount == 0 {
|
if deposit_amount == 0 {
|
||||||
return Err(ErrorCode::InvalidDepositAmount.into());
|
return Err(ErrorCode::InvalidDepositAmount.into());
|
||||||
}
|
}
|
||||||
|
if !is_valid_schedule(start_ts, end_ts, period_count) {
|
||||||
|
return Err(ErrorCode::InvalidSchedule.into());
|
||||||
|
}
|
||||||
let vesting = &mut ctx.accounts.vesting;
|
let vesting = &mut ctx.accounts.vesting;
|
||||||
vesting.beneficiary = beneficiary;
|
vesting.beneficiary = beneficiary;
|
||||||
vesting.mint = ctx.accounts.vault.mint;
|
vesting.mint = ctx.accounts.vault.mint;
|
||||||
|
@ -96,7 +91,8 @@ pub mod lockup {
|
||||||
vesting.period_count = period_count;
|
vesting.period_count = period_count;
|
||||||
vesting.start_balance = deposit_amount;
|
vesting.start_balance = deposit_amount;
|
||||||
vesting.end_ts = end_ts;
|
vesting.end_ts = end_ts;
|
||||||
vesting.start_ts = ctx.accounts.clock.unix_timestamp;
|
vesting.start_ts = start_ts;
|
||||||
|
vesting.created_ts = ctx.accounts.clock.unix_timestamp;
|
||||||
vesting.outstanding = deposit_amount;
|
vesting.outstanding = deposit_amount;
|
||||||
vesting.whitelist_owned = 0;
|
vesting.whitelist_owned = 0;
|
||||||
vesting.grantor = *ctx.accounts.depositor_authority.key;
|
vesting.grantor = *ctx.accounts.depositor_authority.key;
|
||||||
|
@ -321,9 +317,10 @@ pub struct Vesting {
|
||||||
/// originally deposited.
|
/// originally deposited.
|
||||||
pub start_balance: u64,
|
pub start_balance: u64,
|
||||||
/// The unix timestamp at which this vesting account was created.
|
/// The unix timestamp at which this vesting account was created.
|
||||||
|
pub created_ts: i64,
|
||||||
|
/// The time at which vesting begins.
|
||||||
pub start_ts: i64,
|
pub start_ts: i64,
|
||||||
/// The ts at which all the tokens associated with this account
|
/// The time at which all tokens are vested.
|
||||||
/// should be vested.
|
|
||||||
pub end_ts: i64,
|
pub end_ts: i64,
|
||||||
/// The number of times vesting will occur. For example, if vesting
|
/// The number of times vesting will occur. For example, if vesting
|
||||||
/// is once a year over seven years, this will be 7.
|
/// is once a year over seven years, this will be 7.
|
||||||
|
@ -400,6 +397,8 @@ pub enum ErrorCode {
|
||||||
InvalidLockRealizor,
|
InvalidLockRealizor,
|
||||||
#[msg("You have not realized this vesting account.")]
|
#[msg("You have not realized this vesting account.")]
|
||||||
UnrealizedVesting,
|
UnrealizedVesting,
|
||||||
|
#[msg("Invalid vesting schedule given.")]
|
||||||
|
InvalidSchedule,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, 'b, 'c, 'info> From<&mut CreateVesting<'info>>
|
impl<'a, 'b, 'c, 'info> From<&mut CreateVesting<'info>>
|
||||||
|
@ -471,8 +470,7 @@ pub fn whitelist_relay_cpi<'info>(
|
||||||
let signer = &[&seeds[..]];
|
let signer = &[&seeds[..]];
|
||||||
let mut accounts = transfer.to_account_infos();
|
let mut accounts = transfer.to_account_infos();
|
||||||
accounts.extend_from_slice(&remaining_accounts);
|
accounts.extend_from_slice(&remaining_accounts);
|
||||||
solana_program::program::invoke_signed(&relay_instruction, &accounts, signer)
|
program::invoke_signed(&relay_instruction, &accounts, signer).map_err(Into::into)
|
||||||
.map_err(Into::into)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_whitelisted<'info>(transfer: &WhitelistTransfer<'info>) -> Result<()> {
|
pub fn is_whitelisted<'info>(transfer: &WhitelistTransfer<'info>) -> Result<()> {
|
||||||
|
@ -491,10 +489,23 @@ fn whitelist_auth(lockup: &Lockup, ctx: &Context<Auth>) -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_valid_schedule(start_ts: i64, end_ts: i64, period_count: u64) -> bool {
|
||||||
|
if end_ts <= start_ts {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if period_count > (end_ts - start_ts) as u64 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if period_count == 0 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
// Returns Ok if the locked vesting account has been "realized". Realization
|
// Returns Ok if the locked vesting account has been "realized". Realization
|
||||||
// is application dependent. For example, in the case of staking, one must first
|
// is application dependent. For example, in the case of staking, one must first
|
||||||
// unstake before being able to earn locked tokens.
|
// unstake before being able to earn locked tokens.
|
||||||
fn is_realized<'info>(ctx: &Context<Withdraw>) -> Result<()> {
|
fn is_realized(ctx: &Context<Withdraw>) -> Result<()> {
|
||||||
if let Some(realizor) = &ctx.accounts.vesting.realizor {
|
if let Some(realizor) = &ctx.accounts.vesting.realizor {
|
||||||
let cpi_program = {
|
let cpi_program = {
|
||||||
let p = ctx.remaining_accounts[0].clone();
|
let p = ctx.remaining_accounts[0].clone();
|
||||||
|
|
|
@ -358,6 +358,16 @@ mod registry {
|
||||||
if ctx.accounts.clock.unix_timestamp >= expiry_ts {
|
if ctx.accounts.clock.unix_timestamp >= expiry_ts {
|
||||||
return Err(ErrorCode::InvalidExpiry.into());
|
return Err(ErrorCode::InvalidExpiry.into());
|
||||||
}
|
}
|
||||||
|
if let RewardVendorKind::Locked {
|
||||||
|
start_ts,
|
||||||
|
end_ts,
|
||||||
|
period_count,
|
||||||
|
} = kind
|
||||||
|
{
|
||||||
|
if !lockup::is_valid_schedule(start_ts, end_ts, period_count) {
|
||||||
|
return Err(ErrorCode::InvalidVestingSchedule.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Transfer funds into the vendor's vault.
|
// Transfer funds into the vendor's vault.
|
||||||
token::transfer(ctx.accounts.into(), total)?;
|
token::transfer(ctx.accounts.into(), total)?;
|
||||||
|
@ -384,7 +394,7 @@ mod registry {
|
||||||
vendor.from = *ctx.accounts.depositor_authority.key;
|
vendor.from = *ctx.accounts.depositor_authority.key;
|
||||||
vendor.total = total;
|
vendor.total = total;
|
||||||
vendor.expired = false;
|
vendor.expired = false;
|
||||||
vendor.kind = kind.clone();
|
vendor.kind = kind;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -434,12 +444,13 @@ mod registry {
|
||||||
ctx: Context<'a, 'b, 'c, 'info, ClaimRewardLocked<'info>>,
|
ctx: Context<'a, 'b, 'c, 'info, ClaimRewardLocked<'info>>,
|
||||||
nonce: u8,
|
nonce: u8,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let (end_ts, period_count) = match ctx.accounts.cmn.vendor.kind {
|
let (start_ts, end_ts, period_count) = match ctx.accounts.cmn.vendor.kind {
|
||||||
RewardVendorKind::Unlocked => return Err(ErrorCode::ExpectedLockedVendor.into()),
|
RewardVendorKind::Unlocked => return Err(ErrorCode::ExpectedLockedVendor.into()),
|
||||||
RewardVendorKind::Locked {
|
RewardVendorKind::Locked {
|
||||||
|
start_ts,
|
||||||
end_ts,
|
end_ts,
|
||||||
period_count,
|
period_count,
|
||||||
} => (end_ts, period_count),
|
} => (start_ts, end_ts, period_count),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Reward distribution.
|
// Reward distribution.
|
||||||
|
@ -452,19 +463,6 @@ mod registry {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(reward_amount > 0);
|
assert!(reward_amount > 0);
|
||||||
|
|
||||||
// The lockup program requires the timestamp to be >= clock's timestamp.
|
|
||||||
// So update if the time has already passed.
|
|
||||||
//
|
|
||||||
// If the reward is within `period_count` seconds of fully vesting, then
|
|
||||||
// we bump the `end_ts` because, otherwise, the vesting account would
|
|
||||||
// fail to be created. Vesting must have no more frequently than the
|
|
||||||
// smallest unit of time, once per second, expressed as
|
|
||||||
// `period_count <= end_ts - start_ts`.
|
|
||||||
let end_ts = match end_ts < ctx.accounts.cmn.clock.unix_timestamp + period_count as i64 {
|
|
||||||
true => ctx.accounts.cmn.clock.unix_timestamp + period_count as i64,
|
|
||||||
false => end_ts,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Specify the vesting account's realizor, so that unlocks can only
|
// Specify the vesting account's realizor, so that unlocks can only
|
||||||
// execute once completely unstaked.
|
// execute once completely unstaked.
|
||||||
let realizor = Some(Realizor {
|
let realizor = Some(Realizor {
|
||||||
|
@ -487,10 +485,11 @@ mod registry {
|
||||||
lockup::cpi::create_vesting(
|
lockup::cpi::create_vesting(
|
||||||
cpi_ctx,
|
cpi_ctx,
|
||||||
ctx.accounts.cmn.member.beneficiary,
|
ctx.accounts.cmn.member.beneficiary,
|
||||||
end_ts,
|
|
||||||
period_count,
|
|
||||||
reward_amount,
|
reward_amount,
|
||||||
nonce,
|
nonce,
|
||||||
|
start_ts,
|
||||||
|
end_ts,
|
||||||
|
period_count,
|
||||||
realizor,
|
realizor,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
@ -1165,7 +1164,11 @@ pub struct RewardVendor {
|
||||||
#[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq)]
|
#[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq)]
|
||||||
pub enum RewardVendorKind {
|
pub enum RewardVendorKind {
|
||||||
Unlocked,
|
Unlocked,
|
||||||
Locked { end_ts: i64, period_count: u64 },
|
Locked {
|
||||||
|
start_ts: i64,
|
||||||
|
end_ts: i64,
|
||||||
|
period_count: u64,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[error]
|
#[error]
|
||||||
|
@ -1216,6 +1219,8 @@ pub enum ErrorCode {
|
||||||
InvalidBeneficiary,
|
InvalidBeneficiary,
|
||||||
#[msg("The given member account does not match the realizor metadata.")]
|
#[msg("The given member account does not match the realizor metadata.")]
|
||||||
InvalidRealizorMetadata,
|
InvalidRealizorMetadata,
|
||||||
|
#[msg("Invalid vesting schedule for the locked reward.")]
|
||||||
|
InvalidVestingSchedule,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, 'b, 'c, 'info> From<&mut Deposit<'info>>
|
impl<'a, 'b, 'c, 'info> From<&mut Deposit<'info>>
|
||||||
|
|
|
@ -12,6 +12,7 @@ describe("Lockup and Registry", () => {
|
||||||
anchor.setProvider(provider);
|
anchor.setProvider(provider);
|
||||||
|
|
||||||
const lockup = anchor.workspace.Lockup;
|
const lockup = anchor.workspace.Lockup;
|
||||||
|
const linear = anchor.workspace.Linear;
|
||||||
const registry = anchor.workspace.Registry;
|
const registry = anchor.workspace.Registry;
|
||||||
|
|
||||||
let lockupAddress = null;
|
let lockupAddress = null;
|
||||||
|
@ -138,9 +139,10 @@ describe("Lockup and Registry", () => {
|
||||||
let vestingSigner = null;
|
let vestingSigner = null;
|
||||||
|
|
||||||
it("Creates a vesting account", async () => {
|
it("Creates a vesting account", async () => {
|
||||||
const beneficiary = provider.wallet.publicKey;
|
const startTs = new anchor.BN(Date.now() / 1000);
|
||||||
const endTs = new anchor.BN(Date.now() / 1000 + 5);
|
const endTs = new anchor.BN(startTs.toNumber() + 5);
|
||||||
const periodCount = new anchor.BN(2);
|
const periodCount = new anchor.BN(2);
|
||||||
|
const beneficiary = provider.wallet.publicKey;
|
||||||
const depositAmount = new anchor.BN(100);
|
const depositAmount = new anchor.BN(100);
|
||||||
|
|
||||||
const vault = new anchor.web3.Account();
|
const vault = new anchor.web3.Account();
|
||||||
|
@ -155,10 +157,11 @@ describe("Lockup and Registry", () => {
|
||||||
|
|
||||||
await lockup.rpc.createVesting(
|
await lockup.rpc.createVesting(
|
||||||
beneficiary,
|
beneficiary,
|
||||||
endTs,
|
|
||||||
periodCount,
|
|
||||||
depositAmount,
|
depositAmount,
|
||||||
nonce,
|
nonce,
|
||||||
|
startTs,
|
||||||
|
endTs,
|
||||||
|
periodCount,
|
||||||
null, // Lock realizor is None.
|
null, // Lock realizor is None.
|
||||||
{
|
{
|
||||||
accounts: {
|
accounts: {
|
||||||
|
@ -190,11 +193,11 @@ describe("Lockup and Registry", () => {
|
||||||
assert.ok(vestingAccount.grantor.equals(provider.wallet.publicKey));
|
assert.ok(vestingAccount.grantor.equals(provider.wallet.publicKey));
|
||||||
assert.ok(vestingAccount.outstanding.eq(depositAmount));
|
assert.ok(vestingAccount.outstanding.eq(depositAmount));
|
||||||
assert.ok(vestingAccount.startBalance.eq(depositAmount));
|
assert.ok(vestingAccount.startBalance.eq(depositAmount));
|
||||||
assert.ok(vestingAccount.endTs.eq(endTs));
|
|
||||||
assert.ok(vestingAccount.periodCount.eq(periodCount));
|
|
||||||
assert.ok(vestingAccount.whitelistOwned.eq(new anchor.BN(0)));
|
assert.ok(vestingAccount.whitelistOwned.eq(new anchor.BN(0)));
|
||||||
assert.equal(vestingAccount.nonce, nonce);
|
assert.equal(vestingAccount.nonce, nonce);
|
||||||
assert.ok(endTs.gt(vestingAccount.startTs));
|
assert.ok(vestingAccount.createdTs.gt(new anchor.BN(0)));
|
||||||
|
assert.ok(vestingAccount.startTs.eq(startTs));
|
||||||
|
assert.ok(vestingAccount.endTs.eq(endTs));
|
||||||
assert.ok(vestingAccount.realizor === null);
|
assert.ok(vestingAccount.realizor === null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -582,6 +585,7 @@ describe("Lockup and Registry", () => {
|
||||||
it("Drops a locked reward", async () => {
|
it("Drops a locked reward", async () => {
|
||||||
lockedRewardKind = {
|
lockedRewardKind = {
|
||||||
locked: {
|
locked: {
|
||||||
|
startTs: new anchor.BN(Date.now() / 1000),
|
||||||
endTs: new anchor.BN(Date.now() / 1000 + 6),
|
endTs: new anchor.BN(Date.now() / 1000 + 6),
|
||||||
periodCount: new anchor.BN(2),
|
periodCount: new anchor.BN(2),
|
||||||
},
|
},
|
||||||
|
|
|
@ -23,6 +23,15 @@ impl<'info, T: solana_program::sysvar::Sysvar> Sysvar<'info, T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'info, T: solana_program::sysvar::Sysvar> Clone for Sysvar<'info, T> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
info: self.info.clone(),
|
||||||
|
account: T::from_account_info(&self.info).unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<'info, T: solana_program::sysvar::Sysvar> Accounts<'info> for Sysvar<'info, T> {
|
impl<'info, T: solana_program::sysvar::Sysvar> Accounts<'info> for Sysvar<'info, T> {
|
||||||
fn try_accounts(
|
fn try_accounts(
|
||||||
_program_id: &Pubkey,
|
_program_id: &Pubkey,
|
||||||
|
|
|
@ -273,6 +273,12 @@ pub fn generate_non_inlined_handlers(program: &Program) -> proc_macro2::TokenStr
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline(never)]
|
||||||
|
#[cfg(feature = "no-idl")]
|
||||||
|
pub fn __idl(program_id: &Pubkey, accounts: &[AccountInfo], idl_ix_data: &[u8]) -> ProgramResult {
|
||||||
|
Err(anchor_lang::solana_program::program_error::ProgramError::Custom(99))
|
||||||
|
}
|
||||||
|
|
||||||
// One time IDL account initializer. Will faill on subsequent
|
// One time IDL account initializer. Will faill on subsequent
|
||||||
// invocations.
|
// invocations.
|
||||||
#[inline(never)]
|
#[inline(never)]
|
||||||
|
|
Loading…
Reference in New Issue