add_decimals/
lib.rs

1//! Wraps another token to give it more decimals.
2//!
3//! The resulting token is an SPL Token that has more decimals than
4//! its underlying token.
5#![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]
24/// Decimal wrapper program.
25pub mod add_decimals {
26    use super::*;
27
28    /// Initializes a new wrapper.
29    ///
30    /// The wrapper is a PDA account with the seeds:
31    ///
32    /// - `"anchor"`
33    /// - `[InitializeWrapper::underlying_mint]` (mint of the underlying asset)
34    /// - `wrapper_mint.decimals` (the number of decimals, must be greater than the decimals of the underlying's mint)
35    ///
36    /// Anyone may initialize a new wrapper. To do so:
37    ///
38    /// 1. Compute the address of the new wrapper
39    /// 2. Initialize an account for the wrapper to hold the underlying tokens.
40    /// 3. Initialize a mint for the wrapper. It is recommended to use a vanity address via `solana-keygen grind`.
41    /// 4. Run the initialize_wrapper instruction.
42    #[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    /// Deposits underlying tokens to mint wrapped tokens.
74    #[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        // Deposit underlying and mint wrapped
85        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    /// Deposits wrapped tokens to withdraw underlying tokens.
99    #[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        // Compute true withdraw amount
108        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        // Burn wrapped and withdraw underlying
114        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    /// Burn all wrapped tokens to withdraw the underlying tokens.
129    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// --------------------------------
156// Instruction accounts
157// --------------------------------
158
159/// Accounts for initializing a new wrapper.
160#[derive(Accounts)]
161pub struct InitializeWrapper<'info> {
162    /// The WrappedToken account.
163    #[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    /// Token account containing the underlying tokens.
177    pub wrapper_underlying_tokens: Account<'info, TokenAccount>,
178
179    /// Mint of the underlying token.
180    pub underlying_mint: Account<'info, Mint>,
181
182    /// Mint of the wrapper.
183    pub wrapper_mint: Account<'info, Mint>,
184
185    /// Payer of the newly created decimal wrapper.
186    #[account(mut)]
187    pub payer: Signer<'info>,
188
189    /// Rent sysvar. Required for initialization.
190    pub rent: Sysvar<'info, Rent>,
191
192    /// System program. Required for initialization.
193    pub system_program: Program<'info, System>,
194}
195
196impl<'info> InitializeWrapper<'info> {
197    /// Validates ownership of the accounts of the wrapper.
198    pub fn validate(&self) -> Result<()> {
199        // underlying account checks
200        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        // mint checks
218        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/// Accounts for withdrawing or depositing into the wrapper.
234#[derive(Accounts)]
235pub struct UserStake<'info> {
236    /// Wrapper account.
237    pub wrapper: Account<'info, WrappedToken>,
238
239    /// Mint of the wrapper.
240    #[account(mut)]
241    pub wrapper_mint: Account<'info, Mint>,
242
243    /// Wrapper's token account containing the underlying tokens.
244    #[account(mut)]
245    pub wrapper_underlying_tokens: Account<'info, TokenAccount>,
246
247    /// Owner of the user underlying token accounts.
248    pub owner: Signer<'info>,
249
250    /// User's token account for the underlying tokens.
251    #[account(mut)]
252    pub user_underlying_tokens: Account<'info, TokenAccount>,
253
254    /// User's token account for wrapped tokens.
255    #[account(mut)]
256    pub user_wrapped_tokens: Account<'info, TokenAccount>,
257
258    /// SPL Token program.
259    pub token_program: Program<'info, Token>,
260}
261
262impl<'info> Validate<'info> for UserStake<'info> {
263    /// Validates ownership of the accounts of the wrapper.
264    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/// Contains the info of a wrapped token. Immutable.
282///
283/// There are two tokens here:
284/// - the underlying token, which is the original token
285/// - the wrapped token, which is the token created that has a different number of decimals
286#[account]
287#[derive(Copy, Debug, Default)]
288pub struct WrappedToken {
289    /// Number of decimals of the wrapped token.
290    pub decimals: u8,
291    /// Amount to multiply by to wrap the token.
292    /// Cached here for performance reasons, but equivalent to `10 ** decimals`.
293    pub multiplier: u64,
294    /// Mint of the underlying token.
295    pub wrapper_underlying_mint: Pubkey,
296    /// Program token account holding the underlying token.
297    pub wrapper_underlying_tokens: Pubkey,
298    /// Mint of the token of this wrapper.
299    pub wrapper_mint: Pubkey,
300    /// Nonce field to the struct to hold the bump seed for the program derived address,
301    /// sourced from `<https://github.com/project-serum/anchor/blob/ec6888a3b9f702bc41bd3266e7dd70116df3549c/lang/attribute/account/src/lib.rs#L220-L221.>`.
302    __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    /// Gets the nonce.
317    pub fn nonce(&self) -> u8 {
318        self.__nonce
319    }
320}
321
322/// Errors.
323#[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}