1#![deny(clippy::unwrap_used)]
6#![deny(rustdoc::all)]
7#![allow(rustdoc::missing_doc_code_examples)]
8
9use anchor_lang::{prelude::*, solana_program::pubkey::PUBKEY_BYTES};
10use anchor_spl::token::{Mint, Token, TokenAccount};
11use continuation_router::{ActionType, RouterActionProcessor};
12use vipers::prelude::*;
13use vipers::program_err;
14
15mod events;
16mod transfer;
17
18pub use events::*;
19
20declare_id!("DecZY86MU5Gj7kppfUCEmd4LbXXuyZH1yHaP2NTqdiZB");
21
22#[allow(deprecated)]
23#[program]
24pub mod add_decimals {
26 use super::*;
27
28 #[access_control(ctx.accounts.validate())]
43 pub fn initialize_wrapper(ctx: Context<InitializeWrapper>, _nonce: u8) -> Result<()> {
44 let decimals = ctx.accounts.wrapper_mint.decimals;
45 require!(
46 decimals >= ctx.accounts.underlying_mint.decimals,
47 InitWrapperDecimalsTooLow
48 );
49
50 let added_decimals =
51 unwrap_int!(decimals.checked_sub(ctx.accounts.underlying_mint.decimals));
52 let multiplier = unwrap_int!(10u64.checked_pow(added_decimals as u32));
53
54 let wrapper = &mut ctx.accounts.wrapper;
55 wrapper.__nonce = unwrap_bump!(ctx, "wrapper");
56 wrapper.decimals = decimals;
57 wrapper.multiplier = multiplier;
58 wrapper.wrapper_underlying_mint = ctx.accounts.underlying_mint.key();
59 wrapper.wrapper_underlying_tokens = ctx.accounts.wrapper_underlying_tokens.key();
60 wrapper.wrapper_mint = ctx.accounts.wrapper_mint.key();
61
62 emit!(InitEvent {
63 payer: ctx.accounts.payer.key(),
64 decimals,
65 multiplier,
66 wrapper_underlying_mint: wrapper.wrapper_underlying_mint,
67 wrapper_underlying_tokens: wrapper.wrapper_underlying_tokens,
68 wrapper_mint: wrapper.wrapper_mint,
69 });
70 Ok(())
71 }
72
73 #[access_control(ctx.accounts.validate())]
75 pub fn deposit(ctx: Context<UserStake>, deposit_amount: u64) -> Result<()> {
76 require!(deposit_amount > 0, ZeroAmount);
77 require!(
78 ctx.accounts.user_underlying_tokens.amount >= deposit_amount,
79 InsufficientUnderlyingBalance
80 );
81
82 let mint_amount = unwrap_int!(ctx.accounts.wrapper.to_wrapped_amount(deposit_amount));
83
84 ctx.accounts.deposit_underlying(deposit_amount)?;
86 ctx.accounts.mint_wrapped(mint_amount)?;
87
88 emit!(DepositEvent {
89 owner: ctx.accounts.user_underlying_tokens.owner,
90 underlying_mint: ctx.accounts.user_underlying_tokens.mint,
91 wrapped_mint: ctx.accounts.user_wrapped_tokens.mint,
92 deposit_amount,
93 mint_amount
94 });
95 Ok(())
96 }
97
98 #[access_control(ctx.accounts.validate())]
100 pub fn withdraw(ctx: Context<UserStake>, max_burn_amount: u64) -> Result<()> {
101 require!(max_burn_amount > 0, ZeroAmount);
102 require!(
103 ctx.accounts.user_wrapped_tokens.amount >= max_burn_amount,
104 InsufficientWrappedBalance
105 );
106
107 let withdraw_amount =
109 unwrap_int!(ctx.accounts.wrapper.to_underlying_amount(max_burn_amount),);
110 let burn_amount = unwrap_int!(ctx.accounts.wrapper.to_wrapped_amount(withdraw_amount),);
111 let dust_amount = unwrap_int!(max_burn_amount.checked_sub(burn_amount));
112
113 ctx.accounts.burn_wrapped(burn_amount)?;
115 ctx.accounts.withdraw_underlying(withdraw_amount)?;
116
117 emit!(WithdrawEvent {
118 owner: ctx.accounts.user_underlying_tokens.owner,
119 underlying_mint: ctx.accounts.user_underlying_tokens.mint,
120 wrapped_mint: ctx.accounts.user_wrapped_tokens.mint,
121 withdraw_amount,
122 burn_amount,
123 dust_amount,
124 });
125 Ok(())
126 }
127
128 pub fn withdraw_all(ctx: Context<UserStake>) -> Result<()> {
130 let max_burn_amount = ctx.accounts.user_wrapped_tokens.amount;
131 withdraw(ctx, max_burn_amount)
132 }
133
134 #[state]
135 pub struct AddDecimals;
136
137 impl<'info> RouterActionProcessor<'info, UserStake<'info>> for AddDecimals {
138 fn process_action(
139 ctx: Context<UserStake>,
140 action: u16,
141 amount_in: u64,
142 _minimum_amount_out: u64,
143 ) -> Result<()> {
144 let action_type = try_or_err!(ActionType::try_from(action), UnknownAction);
145 msg!("Router action received: {:?}", action_type);
146 match action_type {
147 ActionType::ADWithdraw => withdraw(ctx, amount_in),
148 ActionType::ADDeposit => deposit(ctx, amount_in),
149 _ => program_err!(UnknownAction),
150 }
151 }
152 }
153}
154
155#[derive(Accounts)]
161pub struct InitializeWrapper<'info> {
162 #[account(
164 init,
165 seeds = [
166 b"anchor".as_ref(),
167 underlying_mint.to_account_info().key.as_ref(),
168 &[wrapper_mint.decimals]
169 ],
170 bump,
171 space = 8 + WrappedToken::LEN,
172 payer = payer
173 )]
174 pub wrapper: Account<'info, WrappedToken>,
175
176 pub wrapper_underlying_tokens: Account<'info, TokenAccount>,
178
179 pub underlying_mint: Account<'info, Mint>,
181
182 pub wrapper_mint: Account<'info, Mint>,
184
185 #[account(mut)]
187 pub payer: Signer<'info>,
188
189 pub rent: Sysvar<'info, Rent>,
191
192 pub system_program: Program<'info, System>,
194}
195
196impl<'info> InitializeWrapper<'info> {
197 pub fn validate(&self) -> Result<()> {
199 require!(
201 self.wrapper_underlying_tokens.amount == 0,
202 InitNonEmptyAccount
203 );
204 assert_keys_eq!(
205 self.wrapper_underlying_tokens.owner,
206 self.wrapper,
207 InitWrapperUnderlyingOwnerMismatch
208 );
209 assert_keys_eq!(
210 self.wrapper_underlying_tokens.mint,
211 self.underlying_mint,
212 InitWrapperUnderlyingMintMismatch
213 );
214 invariant!(self.wrapper_underlying_tokens.delegate.is_none());
215 invariant!(self.wrapper_underlying_tokens.close_authority.is_none());
216
217 assert_keys_eq!(
219 self.wrapper_mint.mint_authority.unwrap(),
220 self.wrapper,
221 InitMintAuthorityMismatch
222 );
223 assert_keys_eq!(
224 self.wrapper_mint.freeze_authority.unwrap(),
225 self.wrapper,
226 InitFreezeAuthorityMismatch
227 );
228 require!(self.wrapper_mint.supply == 0, InitWrapperSupplyNonZero);
229 Ok(())
230 }
231}
232
233#[derive(Accounts)]
235pub struct UserStake<'info> {
236 pub wrapper: Account<'info, WrappedToken>,
238
239 #[account(mut)]
241 pub wrapper_mint: Account<'info, Mint>,
242
243 #[account(mut)]
245 pub wrapper_underlying_tokens: Account<'info, TokenAccount>,
246
247 pub owner: Signer<'info>,
249
250 #[account(mut)]
252 pub user_underlying_tokens: Account<'info, TokenAccount>,
253
254 #[account(mut)]
256 pub user_wrapped_tokens: Account<'info, TokenAccount>,
257
258 pub token_program: Program<'info, Token>,
260}
261
262impl<'info> Validate<'info> for UserStake<'info> {
263 fn validate(&self) -> Result<()> {
265 assert_keys_eq!(self.wrapper.wrapper_mint, self.wrapper_mint);
266 assert_keys_eq!(
267 self.wrapper.wrapper_underlying_tokens,
268 self.wrapper_underlying_tokens
269 );
270 assert_keys_eq!(self.user_underlying_tokens.owner, self.owner);
271 assert_keys_eq!(
272 self.user_underlying_tokens.mint,
273 self.wrapper.wrapper_underlying_mint
274 );
275 assert_keys_eq!(self.user_wrapped_tokens.owner, self.owner);
276 assert_keys_eq!(self.user_wrapped_tokens.mint, self.wrapper_mint);
277 Ok(())
278 }
279}
280
281#[account]
287#[derive(Copy, Debug, Default)]
288pub struct WrappedToken {
289 pub decimals: u8,
291 pub multiplier: u64,
294 pub wrapper_underlying_mint: Pubkey,
296 pub wrapper_underlying_tokens: Pubkey,
298 pub wrapper_mint: Pubkey,
300 __nonce: u8,
303}
304
305impl WrappedToken {
306 pub const LEN: usize = 1 + 8 + PUBKEY_BYTES * 3 + 1;
307
308 pub fn to_wrapped_amount(&self, amount: u64) -> Option<u64> {
309 self.multiplier.checked_mul(amount)
310 }
311
312 pub fn to_underlying_amount(&self, amount: u64) -> Option<u64> {
313 amount.checked_div(self.multiplier)
314 }
315
316 pub fn nonce(&self) -> u8 {
318 self.__nonce
319 }
320}
321
322#[error_code]
324#[derive(Eq, PartialEq)]
325pub enum ErrorCode {
326 #[msg("Wrapper underlying tokens account must be empty.")]
327 InitNonEmptyAccount,
328 #[msg("Supply of the wrapper mint is non-zero")]
329 InitWrapperSupplyNonZero,
330 #[msg("Owner of the wrapper underlying tokens account must be the wrapper")]
331 InitWrapperUnderlyingOwnerMismatch,
332 #[msg("Underlying mint does not match underlying tokens account mint")]
333 InitWrapperUnderlyingMintMismatch,
334 #[msg("Mint authority mismatch")]
335 InitMintAuthorityMismatch,
336 #[msg("Initial decimals too high")]
337 InitMultiplierOverflow,
338 #[msg("The number of target decimals must be greater than or equal to the underlying asset's decimals.")]
339 InitWrapperDecimalsTooLow,
340
341 #[msg("Mint amount overflow. This error happens when the token cannot support this many decimals added to the token.")]
342 MintAmountOverflow,
343 #[msg("Failed to convert burn amount from withdraw amount.")]
344 InvalidBurnAmount,
345 #[msg("Failed to convert withdraw amount from wrapped amount.")]
346 InvalidWithdrawAmount,
347 #[msg("User does not have enough underlying tokens")]
348 InsufficientUnderlyingBalance,
349 #[msg("User does not have enough wrapped tokens")]
350 InsufficientWrappedBalance,
351 #[msg("Cannot send zero tokens")]
352 ZeroAmount,
353
354 #[msg("Unknown router action")]
355 UnknownAction,
356
357 #[msg("Freeze authority mismatch")]
358 InitFreezeAuthorityMismatch,
359}
360
361#[cfg(test)]
362#[allow(clippy::unwrap_used)]
363mod test {
364 use super::*;
365 use proptest::prelude::*;
366
367 const MAX_TOKEN_DECIMALS: u8 = 9;
368
369 proptest! {
370 #[test]
371 fn test_wrapped_token(
372 nonce in 0..u8::MAX,
373 amount in 0..u64::MAX,
374 (underlying, desired) in underlying_and_desired(),
375 ) {
376 let added_decimals = desired - underlying;
377 let multiplier = 10u64.checked_pow(added_decimals as u32);
378 prop_assume!(multiplier.is_some());
379
380 let wrapped_token = WrappedToken {
381 __nonce: nonce,
382 decimals: desired,
383 multiplier: multiplier.unwrap(),
384 wrapper_underlying_mint: Pubkey::default(),
385 wrapper_underlying_tokens: Pubkey::default(),
386 wrapper_mint: Pubkey::default(),
387 };
388 let wrapped_amount = wrapped_token.to_wrapped_amount(amount);
389 if wrapped_amount.is_some() {
390 assert_eq!(wrapped_amount.unwrap() / amount, wrapped_token.multiplier);
391 assert_eq!(wrapped_token.to_underlying_amount(wrapped_amount.unwrap()).unwrap(), amount);
392 }
393 }
394 }
395
396 prop_compose! {
397 fn underlying_and_desired()
398 (desired in 0..=MAX_TOKEN_DECIMALS)
399 (underlying in 0..=desired, desired in Just(desired)) -> (u8, u8) {
400 (underlying, desired)
401 }
402 }
403}