diff --git a/Cargo.lock b/Cargo.lock index c0c1b097..f3c32b76 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -57,6 +57,7 @@ dependencies = [ "anyhow", "proc-macro2 1.0.24", "quote 1.0.8", + "regex", "syn 1.0.57", ] @@ -181,6 +182,7 @@ name = "anchor-spl" version = "0.2.0" dependencies = [ "anchor-lang", + "solana-program", "spl-token 3.0.1", ] diff --git a/examples/lockup/programs/lockup/src/calculator.rs b/examples/lockup/programs/lockup/src/calculator.rs index 5b564924..c897fbd6 100644 --- a/examples/lockup/programs/lockup/src/calculator.rs +++ b/examples/lockup/programs/lockup/src/calculator.rs @@ -7,7 +7,7 @@ pub fn available_for_withdrawal(vesting: &Vesting, current_ts: i64) -> u64 { } // The amount of funds currently in the vault. -pub fn balance(vesting: &Vesting) -> u64 { +fn balance(vesting: &Vesting) -> u64 { vesting .outstanding .checked_sub(vesting.whitelist_owned) @@ -33,12 +33,13 @@ fn withdrawn_amount(vesting: &Vesting) -> u64 { // Returns the total vested amount up to the given ts, assuming zero // withdrawals and zero funds sent to other programs. fn total_vested(vesting: &Vesting, current_ts: i64) -> u64 { - assert!(current_ts >= vesting.start_ts); - - if current_ts >= vesting.end_ts { - return vesting.start_balance; + if current_ts < vesting.start_ts { + 0 + } else if current_ts >= vesting.end_ts { + 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 { diff --git a/examples/lockup/programs/lockup/src/lib.rs b/examples/lockup/programs/lockup/src/lib.rs index f59c8058..a555b658 100644 --- a/examples/lockup/programs/lockup/src/lib.rs +++ b/examples/lockup/programs/lockup/src/lib.rs @@ -4,8 +4,8 @@ #![feature(proc_macro_hygiene)] use anchor_lang::prelude::*; -use anchor_lang::solana_program; use anchor_lang::solana_program::instruction::Instruction; +use anchor_lang::solana_program::program; use anchor_spl::token::{self, TokenAccount, Transfer}; mod calculator; @@ -18,7 +18,8 @@ pub mod lockup { pub struct Lockup { /// The key with the ability to change the whitelist. 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, } @@ -70,25 +71,19 @@ pub mod lockup { pub fn create_vesting( ctx: Context, beneficiary: Pubkey, - end_ts: i64, - period_count: u64, deposit_amount: u64, nonce: u8, + start_ts: i64, + end_ts: i64, + period_count: u64, realizor: Option, ) -> 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 { 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; vesting.beneficiary = beneficiary; vesting.mint = ctx.accounts.vault.mint; @@ -96,7 +91,8 @@ pub mod lockup { vesting.period_count = period_count; vesting.start_balance = deposit_amount; 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.whitelist_owned = 0; vesting.grantor = *ctx.accounts.depositor_authority.key; @@ -321,9 +317,10 @@ pub struct Vesting { /// originally deposited. pub start_balance: u64, /// The unix timestamp at which this vesting account was created. + pub created_ts: i64, + /// The time at which vesting begins. pub start_ts: i64, - /// The ts at which all the tokens associated with this account - /// should be vested. + /// The time at which all tokens are vested. pub end_ts: i64, /// The number of times vesting will occur. For example, if vesting /// is once a year over seven years, this will be 7. @@ -400,6 +397,8 @@ pub enum ErrorCode { InvalidLockRealizor, #[msg("You have not realized this vesting account.")] UnrealizedVesting, + #[msg("Invalid vesting schedule given.")] + InvalidSchedule, } impl<'a, 'b, 'c, 'info> From<&mut CreateVesting<'info>> @@ -471,8 +470,7 @@ pub fn whitelist_relay_cpi<'info>( let signer = &[&seeds[..]]; let mut accounts = transfer.to_account_infos(); accounts.extend_from_slice(&remaining_accounts); - solana_program::program::invoke_signed(&relay_instruction, &accounts, signer) - .map_err(Into::into) + program::invoke_signed(&relay_instruction, &accounts, signer).map_err(Into::into) } pub fn is_whitelisted<'info>(transfer: &WhitelistTransfer<'info>) -> Result<()> { @@ -491,10 +489,23 @@ fn whitelist_auth(lockup: &Lockup, ctx: &Context) -> Result<()> { 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 // is application dependent. For example, in the case of staking, one must first // unstake before being able to earn locked tokens. -fn is_realized<'info>(ctx: &Context) -> Result<()> { +fn is_realized(ctx: &Context) -> Result<()> { if let Some(realizor) = &ctx.accounts.vesting.realizor { let cpi_program = { let p = ctx.remaining_accounts[0].clone(); diff --git a/examples/lockup/programs/registry/src/lib.rs b/examples/lockup/programs/registry/src/lib.rs index 83f4a4ba..472e58cb 100644 --- a/examples/lockup/programs/registry/src/lib.rs +++ b/examples/lockup/programs/registry/src/lib.rs @@ -358,6 +358,16 @@ mod registry { if ctx.accounts.clock.unix_timestamp >= expiry_ts { 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. token::transfer(ctx.accounts.into(), total)?; @@ -384,7 +394,7 @@ mod registry { vendor.from = *ctx.accounts.depositor_authority.key; vendor.total = total; vendor.expired = false; - vendor.kind = kind.clone(); + vendor.kind = kind; Ok(()) } @@ -434,12 +444,13 @@ mod registry { ctx: Context<'a, 'b, 'c, 'info, ClaimRewardLocked<'info>>, nonce: u8, ) -> 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::Locked { + start_ts, end_ts, period_count, - } => (end_ts, period_count), + } => (start_ts, end_ts, period_count), }; // Reward distribution. @@ -452,19 +463,6 @@ mod registry { .unwrap(); 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 // execute once completely unstaked. let realizor = Some(Realizor { @@ -487,10 +485,11 @@ mod registry { lockup::cpi::create_vesting( cpi_ctx, ctx.accounts.cmn.member.beneficiary, - end_ts, - period_count, reward_amount, nonce, + start_ts, + end_ts, + period_count, realizor, )?; @@ -1165,7 +1164,11 @@ pub struct RewardVendor { #[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq)] pub enum RewardVendorKind { Unlocked, - Locked { end_ts: i64, period_count: u64 }, + Locked { + start_ts: i64, + end_ts: i64, + period_count: u64, + }, } #[error] @@ -1216,6 +1219,8 @@ pub enum ErrorCode { InvalidBeneficiary, #[msg("The given member account does not match the realizor metadata.")] InvalidRealizorMetadata, + #[msg("Invalid vesting schedule for the locked reward.")] + InvalidVestingSchedule, } impl<'a, 'b, 'c, 'info> From<&mut Deposit<'info>> diff --git a/examples/lockup/tests/lockup.js b/examples/lockup/tests/lockup.js index e35521e7..6cbc3b4e 100644 --- a/examples/lockup/tests/lockup.js +++ b/examples/lockup/tests/lockup.js @@ -12,6 +12,7 @@ describe("Lockup and Registry", () => { anchor.setProvider(provider); const lockup = anchor.workspace.Lockup; + const linear = anchor.workspace.Linear; const registry = anchor.workspace.Registry; let lockupAddress = null; @@ -138,9 +139,10 @@ describe("Lockup and Registry", () => { let vestingSigner = null; it("Creates a vesting account", async () => { - const beneficiary = provider.wallet.publicKey; - const endTs = new anchor.BN(Date.now() / 1000 + 5); + const startTs = new anchor.BN(Date.now() / 1000); + const endTs = new anchor.BN(startTs.toNumber() + 5); const periodCount = new anchor.BN(2); + const beneficiary = provider.wallet.publicKey; const depositAmount = new anchor.BN(100); const vault = new anchor.web3.Account(); @@ -155,10 +157,11 @@ describe("Lockup and Registry", () => { await lockup.rpc.createVesting( beneficiary, - endTs, - periodCount, depositAmount, nonce, + startTs, + endTs, + periodCount, null, // Lock realizor is None. { accounts: { @@ -190,11 +193,11 @@ describe("Lockup and Registry", () => { assert.ok(vestingAccount.grantor.equals(provider.wallet.publicKey)); assert.ok(vestingAccount.outstanding.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.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); }); @@ -582,6 +585,7 @@ describe("Lockup and Registry", () => { it("Drops a locked reward", async () => { lockedRewardKind = { locked: { + startTs: new anchor.BN(Date.now() / 1000), endTs: new anchor.BN(Date.now() / 1000 + 6), periodCount: new anchor.BN(2), }, diff --git a/lang/src/sysvar.rs b/lang/src/sysvar.rs index b45223ce..8cf9bb50 100644 --- a/lang/src/sysvar.rs +++ b/lang/src/sysvar.rs @@ -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> { fn try_accounts( _program_id: &Pubkey, diff --git a/lang/syn/src/codegen/program.rs b/lang/syn/src/codegen/program.rs index 89c1c22d..68f2cc5f 100644 --- a/lang/syn/src/codegen/program.rs +++ b/lang/syn/src/codegen/program.rs @@ -273,6 +273,12 @@ pub fn generate_non_inlined_handlers(program: &Program) -> proc_macro2::TokenStr 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 // invocations. #[inline(never)]