1#![deny(rustdoc::all)]
3#![allow(rustdoc::missing_doc_code_examples)]
4#![allow(deprecated)]
5
6macro_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#[program]
31pub mod lockup {
32 use super::*;
33
34 #[state]
35 pub struct Lockup {
36 pub owner: Pubkey,
38 pub pending_owner: Pubkey,
40 }
41
42 impl Lockup {
43 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 #[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 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 #[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 #[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 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 pub fn withdraw(&self, ctx: Context<Withdraw>) -> Result<()> {
133 ctx.accounts.validate()?;
134
135 let release = &ctx.accounts.release;
137 let amount =
138 calculator::available_for_withdrawal(release, Clock::get()?.unix_timestamp);
139
140 if amount == 0 {
142 return Ok(());
143 }
144
145 require!(
146 ctx.accounts.minter_info.allowance >= amount,
147 MinterAllowanceTooLow
148 );
149
150 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 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 pub fn withdraw_with_amount(&self, ctx: Context<Withdraw>, amount: u64) -> Result<()> {
188 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 require!(amount <= amount_released, InsufficientWithdrawalBalance);
201 require!(
203 ctx.accounts.minter_info.allowance >= amount,
204 MinterAllowanceTooLow
205 );
206
207 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 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 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 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 pub auth: Auth<'info>,
271 pub minter_info: Account<'info, MinterInfo>,
273 pub beneficiary: UncheckedAccount<'info>,
276 #[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 pub mint: Account<'info, Mint>,
290 pub mint_proxy_program: Program<'info, mint_proxy::program::MintProxy>,
292 #[account(mut)]
294 pub payer: Signer<'info>,
295 pub system_program: Program<'info, System>,
297 pub rent: Sysvar<'info, Rent>,
299}
300
301#[derive(Accounts)]
302pub struct RevokeRelease<'info> {
303 pub auth: Auth<'info>,
305 #[account(mut, close = payer)]
307 pub release: Account<'info, Release>,
308 pub payer: UncheckedAccount<'info>,
311}
312
313#[derive(Accounts)]
314pub struct Withdraw<'info> {
315 pub proxy_mint_authority: UncheckedAccount<'info>,
318 #[account(mut)]
320 pub token_mint: Account<'info, Mint>,
321 pub beneficiary: Signer<'info>,
323 #[account(mut, has_one = beneficiary)]
325 pub release: Account<'info, Release>,
326 #[account(mut)]
328 pub token_account: Account<'info, TokenAccount>,
329 pub token_program: Program<'info, Token>,
331 pub unused_clock: UncheckedAccount<'info>,
334 #[account(mut)]
336 pub minter_info: Account<'info, MinterInfo>,
337 pub mint_proxy_program: Program<'info, mint_proxy::program::MintProxy>,
339 pub mint_proxy_state: CpiState<'info, mint_proxy::mint_proxy::MintProxy>,
341}
342
343impl<'info> Withdraw<'info> {
344 fn validate(&self) -> Result<()> {
345 assert_keys_eq!(
347 self.proxy_mint_authority,
348 self.mint_proxy_state.proxy_mint_authority,
349 ProxyMintAuthorityMismatch
350 );
351
352 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 require!(
361 self.beneficiary.key() == self.release.beneficiary,
362 InvalidBeneficiary,
363 );
364 require!(self.beneficiary.is_signer, InvalidBeneficiary);
365
366 require!(
368 self.release.key() == self.minter_info.minter,
369 ReleaseMismatch,
370 );
371
372 require!(
374 self.token_account.mint == self.release.mint,
375 DestinationMintMismatch,
376 );
377
378 require!(self.token_program.key() == token::ID, TokenProgramMismatch,);
380
381 require!(
383 self.minter_info.key() == self.release.minter_info,
384 MinterInfoMismatch
385 );
386
387 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#[account]
406#[derive(Default)]
407pub struct Release {
408 pub beneficiary: Pubkey,
410 pub mint: Pubkey,
412 pub mint_proxy_program: Pubkey,
414 pub minter_info: Pubkey,
416 pub outstanding: u64,
419 pub start_balance: u64,
422 pub created_ts: i64,
424 pub start_ts: i64,
426 pub end_ts: i64,
428 __nonce: u8,
431}
432
433impl Release {
434 pub const LEN: usize = PUBKEY_BYTES * 4 + 8 + 8 + 8 + 8 + 8 + 1;
435
436 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}