lockup/
lib.rs

1//! A token lockup program for linear release with cliff.
2#![deny(rustdoc::all)]
3#![allow(rustdoc::missing_doc_code_examples)]
4#![allow(deprecated)]
5
6/// Returns the program-derived-address seeds used for creating the associated
7/// account.
8macro_rules! associated_seeds {
9    ($state:expr, $($with:expr),+) => {
10        &[
11            b"anchor".as_ref(),
12            $($with),+,
13            &[$state.nonce()],
14        ]
15    };
16}
17
18use anchor_lang::prelude::*;
19use anchor_lang::{accounts::cpi_state::CpiState, solana_program::pubkey::PUBKEY_BYTES};
20use anchor_spl::token::{self, Mint, Token, TokenAccount};
21use mint_proxy::mint_proxy::MintProxy;
22use mint_proxy::MinterInfo;
23use vipers::prelude::*;
24
25pub mod calculator;
26
27declare_id!("LockKXdYQVMbhhckwH3BxoYJ9FYatcZjwNGEuCwY33Q");
28
29/// Saber token lockup program.
30#[program]
31pub mod lockup {
32    use super::*;
33
34    #[state]
35    pub struct Lockup {
36        /// Owner that controls/creates the lockup.
37        pub owner: Pubkey,
38        /// Next owner.
39        pub pending_owner: Pubkey,
40    }
41
42    impl Lockup {
43        /// Initializes the [Lockup].
44        pub fn new(ctx: Context<Initialize>) -> Result<Lockup> {
45            Ok(Lockup {
46                owner: ctx.accounts.auth.owner.key(),
47                pending_owner: Pubkey::default(),
48            })
49        }
50
51        /// Creates a new [Release].
52        #[access_control(check_auth(self, &ctx.accounts.auth))]
53        pub fn create_release(
54            &self,
55            ctx: Context<CreateRelease>,
56            release_amount: u64,
57            start_ts: i64,
58            end_ts: i64,
59        ) -> Result<()> {
60            require!(release_amount != 0, InvalidDepositAmount);
61            require!(is_valid_schedule(start_ts, end_ts), InvalidSchedule);
62
63            // minter_info validations
64            require!(
65                *ctx.accounts.minter_info.to_account_info().owner
66                    == ctx.accounts.mint_proxy_program.key(),
67                MinterInfoProgramMismatch
68            );
69            require!(
70                ctx.accounts.minter_info.allowance >= release_amount,
71                MinterAllowanceTooLow
72            );
73            require!(
74                ctx.accounts.minter_info.minter == ctx.accounts.release.key(),
75                MinterUnauthorized
76            );
77
78            let release = &mut ctx.accounts.release;
79            release.beneficiary = ctx.accounts.beneficiary.key();
80            release.mint = ctx.accounts.mint.key();
81            release.mint_proxy_program = ctx.accounts.mint_proxy_program.key();
82            release.minter_info = ctx.accounts.minter_info.key();
83            release.start_balance = release_amount;
84            release.end_ts = end_ts;
85            release.start_ts = start_ts;
86            release.created_ts = Clock::get()?.unix_timestamp;
87            release.outstanding = release_amount;
88            release.__nonce = *unwrap_int!(ctx.bumps.get("release"));
89
90            emit!(ReleaseCreatedEvent {
91                beneficiary: release.beneficiary,
92                mint: release.mint,
93                release_amount,
94                created_at: release.created_ts,
95                start_at: release.start_ts,
96                end_at: release.end_ts,
97            });
98
99            Ok(())
100        }
101
102        /// Revokes a [Release].
103        #[access_control(check_auth(self, &ctx.accounts.auth))]
104        pub fn revoke_release(&self, ctx: Context<RevokeRelease>) -> Result<()> {
105            require!(
106                ctx.accounts.release.outstanding == ctx.accounts.release.start_balance,
107                ReleaseAlreadyRedeemedFrom
108            );
109            Ok(())
110        }
111
112        /// Transfers ownership of the [Lockup] to another account.
113        #[access_control(check_auth(self, &ctx.accounts))]
114        pub fn transfer_ownership(&mut self, ctx: Context<Auth>, next_owner: Pubkey) -> Result<()> {
115            self.pending_owner = next_owner;
116            Ok(())
117        }
118
119        /// Accepts the new ownership of the [Lockup].
120        pub fn accept_ownership(&mut self, ctx: Context<Auth>) -> Result<()> {
121            require!(ctx.accounts.owner.is_signer, Unauthorized);
122            require!(
123                self.pending_owner == ctx.accounts.owner.key(),
124                PendingOwnerMismatch
125            );
126            self.owner = self.pending_owner;
127            self.pending_owner = Pubkey::default();
128            Ok(())
129        }
130
131        /// Withdraws all available [Release] tokens.
132        pub fn withdraw(&self, ctx: Context<Withdraw>) -> Result<()> {
133            ctx.accounts.validate()?;
134
135            // calculate amount to withdraw
136            let release = &ctx.accounts.release;
137            let amount =
138                calculator::available_for_withdrawal(release, Clock::get()?.unix_timestamp);
139
140            // Short circuit if withdraw amount is zero.
141            if amount == 0 {
142                return Ok(());
143            }
144
145            require!(
146                ctx.accounts.minter_info.allowance >= amount,
147                MinterAllowanceTooLow
148            );
149
150            // Mint rewards
151            let cpi_accounts = mint_proxy::cpi::accounts::PerformMint {
152                proxy_mint_authority: ctx.accounts.proxy_mint_authority.to_account_info(),
153                minter: ctx.accounts.release.to_account_info(),
154                token_mint: ctx.accounts.token_mint.to_account_info(),
155                destination: ctx.accounts.token_account.to_account_info(),
156                minter_info: ctx.accounts.minter_info.to_account_info(),
157                token_program: ctx.accounts.token_program.to_account_info(),
158            };
159            let beneficiary_key = ctx.accounts.beneficiary.key().to_bytes();
160            let seeds = associated_seeds!(ctx.accounts.release, &beneficiary_key);
161            let signer_seeds = &[&seeds[..]];
162            let cpi_program = ctx.accounts.mint_proxy_program.to_account_info();
163            let cpi_state_context =
164                CpiContext::new_with_signer(cpi_program, cpi_accounts, signer_seeds);
165            mint_proxy::invoke_perform_mint(
166                cpi_state_context,
167                ctx.accounts.mint_proxy_state.to_account_info(),
168                amount,
169            )?;
170
171            // Bookkeeping.
172            let release = &mut ctx.accounts.release;
173            release.outstanding = unwrap_int!(release.outstanding.checked_sub(amount));
174
175            emit!(WithdrawEvent {
176                beneficiary: release.beneficiary,
177                mint: release.mint,
178                outstanding_amount: release.outstanding,
179                withdraw_amount: amount,
180                timestamp: Clock::get()?.unix_timestamp
181            });
182
183            Ok(())
184        }
185
186        /// Withdraws tokens from the [Release] with an amount.
187        pub fn withdraw_with_amount(&self, ctx: Context<Withdraw>, amount: u64) -> Result<()> {
188            // Short circuit if withdraw amount is zero.
189            if amount == 0 {
190                return Ok(());
191            }
192
193            ctx.accounts.validate()?;
194
195            let amount_released = calculator::available_for_withdrawal(
196                &ctx.accounts.release,
197                Clock::get()?.unix_timestamp,
198            );
199            // Has the given amount released?
200            require!(amount <= amount_released, InsufficientWithdrawalBalance);
201            // Enough mint allowance for mint?
202            require!(
203                ctx.accounts.minter_info.allowance >= amount,
204                MinterAllowanceTooLow
205            );
206
207            // Mint rewards
208            let cpi_accounts = mint_proxy::cpi::accounts::PerformMint {
209                proxy_mint_authority: ctx.accounts.proxy_mint_authority.to_account_info(),
210                minter: ctx.accounts.release.to_account_info(),
211                token_mint: ctx.accounts.token_mint.to_account_info(),
212                destination: ctx.accounts.token_account.to_account_info(),
213                minter_info: ctx.accounts.minter_info.to_account_info(),
214                token_program: ctx.accounts.token_program.to_account_info(),
215            };
216            let beneficiary_key = ctx.accounts.beneficiary.key().to_bytes();
217            let seeds = associated_seeds!(ctx.accounts.release, &beneficiary_key);
218            let signer_seeds = &[&seeds[..]];
219            let cpi_program = ctx.accounts.mint_proxy_program.to_account_info();
220            let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, signer_seeds);
221            mint_proxy::invoke_perform_mint(
222                cpi_ctx,
223                ctx.accounts.mint_proxy_state.to_account_info(),
224                amount,
225            )?;
226
227            let release = &mut ctx.accounts.release;
228            // Bookkeeping.
229            release.outstanding = unwrap_int!(release.outstanding.checked_sub(amount));
230
231            emit!(WithdrawEvent {
232                beneficiary: release.beneficiary,
233                mint: release.mint,
234                outstanding_amount: release.outstanding,
235                withdraw_amount: amount,
236                timestamp: Clock::get()?.unix_timestamp
237            });
238
239            Ok(())
240        }
241    }
242
243    /// Convenience function for UI's to calculate the withdrawable amount.
244    pub fn available_for_withdrawal(ctx: Context<AvailableForWithdrawal>) -> Result<()> {
245        let available = calculator::available_for_withdrawal(
246            &ctx.accounts.release,
247            ctx.accounts.clock.unix_timestamp,
248        );
249        // Log as string so that JS can read as a BN.
250        msg!(&format!("{{ \"result\": \"{}\" }}", available));
251        Ok(())
252    }
253}
254
255#[derive(Accounts)]
256pub struct Auth<'info> {
257    pub owner: Signer<'info>,
258}
259
260#[derive(Accounts)]
261pub struct Initialize<'info> {
262    pub auth: Auth<'info>,
263    pub mint_proxy_state: CpiState<'info, MintProxy>,
264    pub mint_proxy_program: Program<'info, mint_proxy::program::MintProxy>,
265}
266
267#[derive(Accounts)]
268pub struct CreateRelease<'info> {
269    /// Authentication for authority of the [lockup::Lockup].
270    pub auth: Auth<'info>,
271    /// Minter info account.
272    pub minter_info: Account<'info, MinterInfo>,
273    /// Account able to withdraw from the [Release].
274    /// CHECK: Arbitrary.
275    pub beneficiary: UncheckedAccount<'info>,
276    /// [Release] account.
277    #[account(
278        init,
279        seeds = [
280            b"anchor".as_ref(),
281            beneficiary.key().as_ref()
282        ],
283        bump,
284        space = 8 + Release::LEN,
285        payer = payer
286    )]
287    pub release: Account<'info, Release>,
288    /// Token to be released.
289    pub mint: Account<'info, Mint>,
290    /// Mint proxy program.
291    pub mint_proxy_program: Program<'info, mint_proxy::program::MintProxy>,
292    /// Payer for the [Release] account creation.
293    #[account(mut)]
294    pub payer: Signer<'info>,
295    /// System program.
296    pub system_program: Program<'info, System>,
297    /// Rent sysvar.
298    pub rent: Sysvar<'info, Rent>,
299}
300
301#[derive(Accounts)]
302pub struct RevokeRelease<'info> {
303    /// Authentication for authority of the [lockup::Lockup].
304    pub auth: Auth<'info>,
305    /// [Release] account.
306    #[account(mut, close = payer)]
307    pub release: Account<'info, Release>,
308    /// Recipient of the [Release] account lamports.
309    /// CHECK: Arbitrary.
310    pub payer: UncheckedAccount<'info>,
311}
312
313#[derive(Accounts)]
314pub struct Withdraw<'info> {
315    /// Mint authority of the proxy.
316    /// CHECK: Arbitrary.
317    pub proxy_mint_authority: UncheckedAccount<'info>,
318    /// Mint of the token unlocked.
319    #[account(mut)]
320    pub token_mint: Account<'info, Mint>,
321    /// Owner of the [Release].
322    pub beneficiary: Signer<'info>,
323    /// [Release].
324    #[account(mut, has_one = beneficiary)]
325    pub release: Account<'info, Release>,
326    /// Beneficiary token account.
327    #[account(mut)]
328    pub token_account: Account<'info, TokenAccount>,
329    /// Token program.
330    pub token_program: Program<'info, Token>,
331    /// Clock sysvar, now unused and may be any account.
332    /// CHECK: Arbitrary.
333    pub unused_clock: UncheckedAccount<'info>,
334    /// Minter info.
335    #[account(mut)]
336    pub minter_info: Account<'info, MinterInfo>,
337    /// Mint proxy program.
338    pub mint_proxy_program: Program<'info, mint_proxy::program::MintProxy>,
339    /// Mint proxy state.
340    pub mint_proxy_state: CpiState<'info, mint_proxy::mint_proxy::MintProxy>,
341}
342
343impl<'info> Withdraw<'info> {
344    fn validate(&self) -> Result<()> {
345        // proxy_mint_authority validations
346        assert_keys_eq!(
347            self.proxy_mint_authority,
348            self.mint_proxy_state.proxy_mint_authority,
349            ProxyMintAuthorityMismatch
350        );
351
352        // token_mint validations
353        require!(self.token_mint.key() == self.release.mint, InvalidTokenMint);
354        require!(
355            self.token_mint.key() == self.mint_proxy_state.token_mint,
356            MintProxyMintMismatch
357        );
358
359        // beneficiary validations
360        require!(
361            self.beneficiary.key() == self.release.beneficiary,
362            InvalidBeneficiary,
363        );
364        require!(self.beneficiary.is_signer, InvalidBeneficiary);
365
366        // release validations
367        require!(
368            self.release.key() == self.minter_info.minter,
369            ReleaseMismatch,
370        );
371
372        // token_account validations
373        require!(
374            self.token_account.mint == self.release.mint,
375            DestinationMintMismatch,
376        );
377
378        // token_program validations
379        require!(self.token_program.key() == token::ID, TokenProgramMismatch,);
380
381        // minter_info validations
382        require!(
383            self.minter_info.key() == self.release.minter_info,
384            MinterInfoMismatch
385        );
386
387        // mint_proxy_program validations
388        require!(
389            self.mint_proxy_program.key() == self.release.mint_proxy_program,
390            InvalidMintProxyProgram
391        );
392
393        Ok(())
394    }
395}
396
397#[derive(Accounts)]
398pub struct AvailableForWithdrawal<'info> {
399    pub release: Account<'info, Release>,
400    pub clock: Sysvar<'info, Clock>,
401}
402
403/// Contains information about a beneficiary and the tokens it can claim
404/// + its release schedule.
405#[account]
406#[derive(Default)]
407pub struct Release {
408    /// The owner of this [Release] account.
409    pub beneficiary: Pubkey,
410    /// The mint of the SPL token locked up.
411    pub mint: Pubkey,
412    /// The mint proxy program.
413    pub mint_proxy_program: Pubkey,
414    /// The [mint_proxy::MinterInfo].
415    pub minter_info: Pubkey,
416    /// The outstanding SBR deposit backing this release account. All
417    /// withdrawals will deduct this balance.
418    pub outstanding: u64,
419    /// The starting balance of this release account, i.e., how much was
420    /// originally deposited.
421    pub start_balance: u64,
422    /// The unix timestamp at which this release account was created.
423    pub created_ts: i64,
424    /// The time at which release begins.
425    pub start_ts: i64,
426    /// The time at which all tokens are released.
427    pub end_ts: i64,
428    /// Nonce field to the struct to hold the bump seed for the program derived address,
429    /// sourced from `<https://github.com/project-serum/anchor/blob/ec6888a3b9f702bc41bd3266e7dd70116df3549c/lang/attribute/account/src/lib.rs#L220-L221.>`.
430    __nonce: u8,
431}
432
433impl Release {
434    pub const LEN: usize = PUBKEY_BYTES * 4 + 8 + 8 + 8 + 8 + 8 + 1;
435
436    /// Gets the nonce.
437    pub fn nonce(&self) -> u8 {
438        self.__nonce
439    }
440}
441
442fn check_auth(lockup: &Lockup, auth: &Auth) -> Result<()> {
443    require!(
444        auth.owner.is_signer && lockup.owner == auth.owner.key(),
445        Unauthorized
446    );
447    Ok(())
448}
449
450#[event]
451pub struct ReleaseCreatedEvent {
452    #[index]
453    pub beneficiary: Pubkey,
454    #[index]
455    pub mint: Pubkey,
456
457    pub release_amount: u64,
458    pub created_at: i64,
459    pub start_at: i64,
460    pub end_at: i64,
461}
462
463#[event]
464pub struct WithdrawEvent {
465    #[index]
466    pub beneficiary: Pubkey,
467    #[index]
468    pub mint: Pubkey,
469
470    pub outstanding_amount: u64,
471    pub withdraw_amount: u64,
472    pub timestamp: i64,
473}
474
475#[error_code]
476pub enum ErrorCode {
477    #[msg("The provided beneficiary was not valid.")]
478    InvalidBeneficiary,
479    #[msg("The release deposit amount must be greater than zero.")]
480    InvalidDepositAmount,
481    #[msg("The Whitelist entry is not a valid program address.")]
482    InvalidProgramAddress,
483    #[msg("Invalid release schedule given.")]
484    InvalidSchedule,
485    #[msg("The provided token mint did not match the mint on the release account.")]
486    InvalidTokenMint,
487    #[msg("Insufficient withdrawal balance.")]
488    InsufficientWithdrawalBalance,
489    #[msg("Unauthorized access.")]
490    Unauthorized,
491    #[msg("Pending owner mismatch.")]
492    PendingOwnerMismatch,
493    #[msg("The mint proxy program provided was not valid.")]
494    InvalidMintProxyProgram,
495    #[msg("The Release must be an authorized minter on the mint proxy.")]
496    MinterUnauthorized,
497    #[msg("The minter info is not owned by the expected mint proxy.")]
498    MinterInfoProgramMismatch,
499    #[msg("The minter must have an allowance of at least the release amount.")]
500    MinterAllowanceTooLow,
501    #[msg("Minter info mismatch")]
502    MinterInfoMismatch,
503
504    #[msg("Release mismatch")]
505    ReleaseMismatch,
506    #[msg("Proxy mint authority mismatch")]
507    ProxyMintAuthorityMismatch,
508    #[msg("Mint proxy mint mismatch")]
509    MintProxyMintMismatch,
510    #[msg("Withdraw destination mint mismatch")]
511    DestinationMintMismatch,
512    #[msg("Token program mismatch")]
513    TokenProgramMismatch,
514    #[msg("Release already redeemed from")]
515    ReleaseAlreadyRedeemedFrom,
516
517    #[msg("U64 overflow.")]
518    U64Overflow,
519}
520
521pub fn is_valid_schedule(start_ts: i64, end_ts: i64) -> bool {
522    end_ts > start_ts
523}