shadow_drive_user_staking/instructions/
make_account_immutable.rs

1use crate::constants::*;
2use crate::errors::ErrorCodes;
3use crate::instructions::{
4    crank::crank,
5    initialize_account::{ShadowDriveStorageAccount, StorageAccount, StorageAccountV2},
6    initialize_config::StorageConfig,
7};
8use anchor_lang::prelude::*;
9use anchor_spl::associated_token::AssociatedToken;
10use anchor_spl::token::{Mint, Token, TokenAccount};
11use std::convert::TryInto;
12
13/// This is the function that handles the `make_account_immutable` ix
14pub fn handler(mut ctx: impl MakeAccountImmutable, storage_used: u64) -> Result<()> {
15    // Crank first, if needed and if mutable fees are on
16    let new_balance: Shades = ctx.crank(storage_used)?;
17
18    msg!("Charging user for storage; SPL transfers to emissions wallet");
19    {
20        ctx.charge_or_return_shades(new_balance)?;
21    }
22
23    msg!(
24        "Marking Storage Account as immutable: {}",
25        ctx.get_identifier()
26    );
27    {
28        ctx.mark_immutable();
29    }
30
31    Ok(())
32}
33
34#[derive(Accounts)]
35/// This `MakeAccountImmutable` context is used in the instruction which allow users to
36/// mark a storage account as immutable.
37pub struct MakeAccountImmutableV1<'info> {
38    /// This is the `StorageConfig` accounts that holds all of the admin, uploader keys.
39    /// Requires mutability to update global storage counter.
40    #[account(
41        mut,
42        seeds = [
43            "storage-config".as_bytes()
44        ],
45        bump,
46    )]
47    pub storage_config: Box<Account<'info, StorageConfig>>,
48
49    /// Parent storage account.
50    /// Requires mutability to update user storage account storage counter.
51    #[account(
52        mut,
53        seeds = [
54            "storage-account".as_bytes(),
55            &storage_account.owner_1.key().to_bytes(),
56            &storage_account.account_counter_seed.to_le_bytes()
57        ],
58        bump,
59    )]
60    pub storage_account: Box<Account<'info, StorageAccount>>,
61
62    /// This token account serves as the account which holds user's stake for file storage.
63    #[account(
64        mut,
65        seeds = [
66            "stake-account".as_bytes(),
67            &storage_account.key().to_bytes(),
68        ],
69        bump,
70    )]
71    pub stake_account: Box<Account<'info, TokenAccount>>,
72
73    /// This token account is the SHDW operator emissions wallet
74    #[account(mut, address=shdw::emissions_wallet::ID)]
75    pub emissions_wallet: Box<Account<'info, TokenAccount>>,
76
77    /// File owner, user, fee-payer
78    /// Requires mutability since owner/user is fee payer.
79    #[account(mut, constraint=storage_account.is_owner(owner.key()))]
80    pub owner: Signer<'info>,
81
82    /// Uploader needs to sign off on make immutable
83    #[account(address=storage_config.uploader)]
84    pub uploader: Signer<'info>,
85
86    /// User's token account
87    #[account(
88        init_if_needed,
89        payer = owner,
90        associated_token::mint = token_mint,
91        associated_token::authority = owner,
92    )]
93    pub owner_ata: Box<Account<'info, TokenAccount>>,
94
95    /// Token mint account
96    #[account(mut, address = shdw::ID)]
97    pub token_mint: Box<Account<'info, Mint>>,
98
99    /// System Program
100    pub system_program: Program<'info, System>,
101
102    /// Token Program
103    pub token_program: Program<'info, Token>,
104
105    /// Associated Token Program
106    pub associated_token_program: Program<'info, AssociatedToken>,
107
108    /// Rent
109    pub rent: Sysvar<'info, Rent>,
110}
111
112#[derive(Accounts)]
113/// This `MakeAccountImmutable` context is used in the instruction which allow users to
114/// mark a storage account as immutable.
115pub struct MakeAccountImmutableV2<'info> {
116    /// This is the `StorageConfig` accounts that holds all of the admin, uploader keys.
117    /// Requires mutability to update global storage counter.
118    #[account(
119        mut,
120        seeds = [
121            "storage-config".as_bytes()
122        ],
123        bump,
124    )]
125    pub storage_config: Box<Account<'info, StorageConfig>>,
126
127    /// Parent storage account.
128    /// Requires mutability to update user storage account storage counter.
129    #[account(
130        mut,
131        seeds = [
132            "storage-account".as_bytes(),
133            &storage_account.owner_1.key().to_bytes(),
134            &storage_account.account_counter_seed.to_le_bytes()
135        ],
136        bump,
137    )]
138    pub storage_account: Box<Account<'info, StorageAccountV2>>,
139
140    /// This token account serves as the account which holds user's stake for file storage.
141    #[account(
142        mut,
143        seeds = [
144            "stake-account".as_bytes(),
145            &storage_account.key().to_bytes(),
146        ],
147        bump,
148    )]
149    pub stake_account: Box<Account<'info, TokenAccount>>,
150
151    /// This token account is the SHDW operator emissions wallet
152    #[account(mut, address=shdw::emissions_wallet::ID)]
153    pub emissions_wallet: Box<Account<'info, TokenAccount>>,
154
155    /// File owner, user, fee-payer
156    /// Requires mutability since owner/user is fee payer.
157    #[account(mut, constraint=storage_account.is_owner(owner.key()))]
158    pub owner: Signer<'info>,
159
160    /// User's token account
161    #[account(
162        init_if_needed,
163        payer = owner,
164        associated_token::mint = token_mint,
165        associated_token::authority = owner,
166    )]
167    pub owner_ata: Box<Account<'info, TokenAccount>>,
168
169    /// Uploader needs to sign off on make immutable
170    #[account(address=storage_config.uploader)]
171    pub uploader: Signer<'info>,
172
173    /// Token mint account
174    #[account(mut, address = shdw::ID)]
175    pub token_mint: Box<Account<'info, Mint>>,
176
177    /// System Program
178    pub system_program: Program<'info, System>,
179
180    /// Token Program
181    pub token_program: Program<'info, Token>,
182
183    /// Associated Token Program
184    pub associated_token_program: Program<'info, AssociatedToken>,
185
186    /// Rent
187    pub rent: Sysvar<'info, Rent>,
188}
189
190type Shades = u64;
191
192pub trait MakeAccountImmutable {
193    fn crank(&mut self, storage_used: u64) -> Result<Shades>;
194    fn charge_or_return_shades(&mut self, new_balance: Shades) -> Result<()>;
195    fn get_identifier(&self) -> String;
196    fn mark_immutable(&mut self);
197}
198
199impl MakeAccountImmutable for Context<'_, '_, '_, '_, MakeAccountImmutableV1<'_>> {
200    fn crank(&mut self, storage_used: u64) -> Result<Shades> {
201        let pre_crank_balance = self.accounts.stake_account.amount;
202        let new_balance;
203        let account_info = self.accounts.storage_account.to_account_info();
204        {
205            if let Some((emission_fee, crank_fee)) = crank(
206                &self.accounts.storage_config,
207                &mut self.accounts.storage_account,
208                account_info,
209                &self.accounts.emissions_wallet,
210                &self.accounts.stake_account,
211                &self.accounts.token_program,
212                &self.accounts.owner_ata,
213                &self.accounts.token_mint,
214                *self.bumps.get("storage_config").unwrap(),
215                storage_used,
216            )? {
217                msg!("First, crank for any outstanding mutable fees (cranker = user)");
218                new_balance = pre_crank_balance
219                    .checked_sub(emission_fee.checked_add(crank_fee).unwrap())
220                    .unwrap();
221            } else {
222                new_balance = pre_crank_balance;
223            }
224        }
225
226        Ok(new_balance)
227    }
228    fn charge_or_return_shades(&mut self, new_balance: Shades) -> Result<()> {
229        let storage_config = &self.accounts.storage_config;
230        let storage_account = &self.accounts.storage_account;
231
232        // Pack seeds
233        let storage_config_seeds = [
234            "storage-config".as_bytes(),
235            &[*self.bumps.get("storage_config").unwrap()],
236        ];
237        let signer_seeds: &[&[&[u8]]] = &[&storage_config_seeds];
238
239        // Cost of storage
240        let immutable_storage_requested: u128 = storage_account.storage as u128;
241        let cost_of_storage: u64 = immutable_storage_requested
242            .checked_mul(storage_config.shades_per_gib as u128)
243            .unwrap()
244            .checked_div(BYTES_PER_GIB as u128)
245            .unwrap()
246            .try_into()
247            .unwrap();
248        msg!(
249            "User has {} bytes of storage, costing {} shades",
250            immutable_storage_requested,
251            cost_of_storage
252        );
253
254        if cost_of_storage <= new_balance {
255            msg!("User's stake account contains enough funds to cover immutable fee");
256
257            // Compute amount to return to user + amount to transfer to emissions wallet
258            let return_amount = new_balance.checked_sub(cost_of_storage).unwrap();
259            msg!(
260                "Returning {} shades to user, {} to emissions wallet",
261                return_amount,
262                cost_of_storage,
263            );
264
265            // Sanity check
266            require_eq!(
267                new_balance,
268                cost_of_storage.checked_add(return_amount).unwrap(),
269                ErrorCodes::InvalidTokenTransferAmounts
270            );
271
272            // Transfer to user, if necessary
273            if return_amount > 0 {
274                anchor_spl::token::transfer(
275                    CpiContext::new_with_signer(
276                        self.accounts.token_program.to_account_info(),
277                        anchor_spl::token::Transfer {
278                            from: self.accounts.stake_account.to_account_info(),
279                            to: self.accounts.owner_ata.to_account_info(),
280                            authority: self.accounts.storage_config.to_account_info(),
281                        },
282                        signer_seeds,
283                    ),
284                    return_amount,
285                )
286                .map_err(|_| ErrorCodes::FailedToReturnUserFunds)?;
287            }
288
289            // Transfer to emissions wallet
290            anchor_spl::token::transfer(
291                CpiContext::new_with_signer(
292                    self.accounts.token_program.to_account_info(),
293                    anchor_spl::token::Transfer {
294                        from: self.accounts.stake_account.to_account_info(),
295                        to: self.accounts.emissions_wallet.to_account_info(),
296                        authority: self.accounts.storage_config.to_account_info(),
297                    },
298                    signer_seeds,
299                ),
300                cost_of_storage,
301            )
302            .map_err(|_| ErrorCodes::FailedToTransferToEmissionsWallet)?;
303        } else {
304            let additional_shades: u64 = cost_of_storage.checked_sub(new_balance).unwrap();
305            msg!(
306                "User's stake account does not contain enough funds, charging user additional {} shades",
307                additional_shades
308            );
309
310            // Sanity check
311            require_eq!(
312                cost_of_storage,
313                additional_shades.checked_add(new_balance).unwrap(),
314                ErrorCodes::InvalidTokenTransferAmounts
315            );
316            anchor_spl::token::burn(
317                CpiContext::new_with_signer(
318                    self.accounts.token_program.to_account_info(),
319                    anchor_spl::token::Burn {
320                        mint: self.accounts.token_mint.to_account_info(),
321                        from: self.accounts.stake_account.to_account_info(),
322                        authority: self.accounts.storage_config.to_account_info(),
323                    },
324                    signer_seeds,
325                ),
326                additional_shades,
327            )
328            .map_err(|_| ErrorCodes::FailedToTransferToEmissionsWalletFromUser)?;
329
330            // Transfer to emissions wallet
331            anchor_spl::token::transfer(
332                CpiContext::new_with_signer(
333                    self.accounts.token_program.to_account_info(),
334                    anchor_spl::token::Transfer {
335                        from: self.accounts.stake_account.to_account_info(),
336                        to: self.accounts.emissions_wallet.to_account_info(),
337                        authority: self.accounts.storage_config.to_account_info(),
338                    },
339                    signer_seeds,
340                ),
341                new_balance,
342            )
343            .map_err(|_| ErrorCodes::FailedToTransferToEmissionsWallet)?;
344        }
345
346        // Close account after having emptied it
347        anchor_spl::token::close_account(CpiContext::new_with_signer(
348            self.accounts.token_program.to_account_info(),
349            anchor_spl::token::CloseAccount {
350                account: self.accounts.stake_account.to_account_info(),
351                destination: self.accounts.emissions_wallet.to_account_info(),
352                authority: self.accounts.storage_config.to_account_info(),
353            },
354            signer_seeds,
355        ))
356        .map_err(|_| ErrorCodes::FailedToCloseAccount)?;
357
358        Ok(())
359    }
360    fn get_identifier(&self) -> String {
361        self.accounts.storage_account.identifier.clone()
362    }
363    fn mark_immutable(&mut self) {
364        self.accounts.storage_account.immutable = true;
365    }
366}
367
368impl MakeAccountImmutable for Context<'_, '_, '_, '_, MakeAccountImmutableV2<'_>> {
369    fn crank(&mut self, storage_used: u64) -> Result<Shades> {
370        let pre_crank_balance = self.accounts.stake_account.amount;
371        let new_balance;
372        let account_info = self.accounts.storage_account.to_account_info();
373        {
374            if let Some((emission_fee, crank_fee)) = crank(
375                &self.accounts.storage_config,
376                &mut self.accounts.storage_account,
377                account_info,
378                &self.accounts.emissions_wallet,
379                &self.accounts.stake_account,
380                &self.accounts.token_program,
381                &self.accounts.owner_ata,
382                &self.accounts.token_mint,
383                *self.bumps.get("storage_config").unwrap(),
384                storage_used,
385            )? {
386                msg!("First, crank for any outstanding mutable fees (cranker = user)");
387                new_balance = pre_crank_balance
388                    .checked_sub(emission_fee.checked_add(crank_fee).unwrap())
389                    .unwrap();
390            } else {
391                new_balance = pre_crank_balance;
392            }
393        }
394
395        Ok(new_balance)
396    }
397    fn charge_or_return_shades(&mut self, new_balance: Shades) -> Result<()> {
398        let storage_config = &self.accounts.storage_config;
399        let storage_account = &self.accounts.storage_account;
400
401        // Pack seeds
402        let storage_config_seeds = [
403            "storage-config".as_bytes(),
404            &[*self.bumps.get("storage_config").unwrap()],
405        ];
406        let signer_seeds: &[&[&[u8]]] = &[&storage_config_seeds];
407
408        // Cost of storage
409        let immutable_storage_requested: u128 = storage_account.storage as u128;
410        let cost_of_storage: u64 = immutable_storage_requested
411            .checked_mul(storage_config.shades_per_gib as u128)
412            .unwrap()
413            .checked_div(BYTES_PER_GIB as u128)
414            .unwrap()
415            .try_into()
416            .unwrap();
417        msg!(
418            "User has {} bytes of storage, costing {} shades",
419            immutable_storage_requested,
420            cost_of_storage
421        );
422
423        if cost_of_storage <= new_balance {
424            msg!("User's stake account contains enough funds to cover immutable fee");
425
426            // Compute amount to return to user + amount to transfer to emissions wallet
427            let return_amount = new_balance.checked_sub(cost_of_storage).unwrap();
428            msg!(
429                "Returning {} shades to user, {} to emissions wallet",
430                return_amount,
431                cost_of_storage,
432            );
433
434            // Sanity check
435            require_eq!(
436                new_balance,
437                cost_of_storage.checked_add(return_amount).unwrap(),
438                ErrorCodes::InvalidTokenTransferAmounts
439            );
440
441            // Transfer to user, if necessary
442            if return_amount > 0 {
443                anchor_spl::token::transfer(
444                    CpiContext::new_with_signer(
445                        self.accounts.token_program.to_account_info(),
446                        anchor_spl::token::Transfer {
447                            from: self.accounts.stake_account.to_account_info(),
448                            to: self.accounts.owner_ata.to_account_info(),
449                            authority: self.accounts.storage_config.to_account_info(),
450                        },
451                        signer_seeds,
452                    ),
453                    return_amount,
454                )
455                .map_err(|_| ErrorCodes::FailedToReturnUserFunds)?;
456            }
457
458            // Transfer to emissions wallet
459            anchor_spl::token::transfer(
460                CpiContext::new_with_signer(
461                    self.accounts.token_program.to_account_info(),
462                    anchor_spl::token::Transfer {
463                        from: self.accounts.stake_account.to_account_info(),
464                        to: self.accounts.emissions_wallet.to_account_info(),
465                        authority: self.accounts.storage_config.to_account_info(),
466                    },
467                    signer_seeds,
468                ),
469                cost_of_storage,
470            )
471            .map_err(|_| ErrorCodes::FailedToTransferToEmissionsWallet)?;
472        } else {
473            let additional_shades: u64 = cost_of_storage.checked_sub(new_balance).unwrap();
474            msg!(
475                "User's stake account does not contain enough funds, charging user additional {} shades",
476                additional_shades
477            );
478
479            // Sanity check
480            require_eq!(
481                cost_of_storage,
482                additional_shades.checked_add(new_balance).unwrap(),
483                ErrorCodes::InvalidTokenTransferAmounts
484            );
485
486            // Transfer from user
487            anchor_spl::token::transfer(
488                CpiContext::new_with_signer(
489                    self.accounts.token_program.to_account_info(),
490                    anchor_spl::token::Transfer {
491                        from: self.accounts.owner_ata.to_account_info(),
492                        to: self.accounts.emissions_wallet.to_account_info(),
493                        authority: self.accounts.owner.to_account_info(),
494                    },
495                    signer_seeds,
496                ),
497                additional_shades,
498            )
499            .map_err(|_| ErrorCodes::FailedToTransferToEmissionsWalletFromUser)?;
500
501            // Transfer to emissions wallet
502            anchor_spl::token::transfer(
503                CpiContext::new_with_signer(
504                    self.accounts.token_program.to_account_info(),
505                    anchor_spl::token::Transfer {
506                        from: self.accounts.stake_account.to_account_info(),
507                        to: self.accounts.emissions_wallet.to_account_info(),
508                        authority: self.accounts.storage_config.to_account_info(),
509                    },
510                    signer_seeds,
511                ),
512                new_balance,
513            )
514            .map_err(|_| ErrorCodes::FailedToTransferToEmissionsWallet)?;
515        }
516
517        // Close account after having emptied it
518        anchor_spl::token::close_account(CpiContext::new_with_signer(
519            self.accounts.token_program.to_account_info(),
520            anchor_spl::token::CloseAccount {
521                account: self.accounts.stake_account.to_account_info(),
522                destination: self.accounts.emissions_wallet.to_account_info(),
523                authority: self.accounts.storage_config.to_account_info(),
524            },
525            signer_seeds,
526        ))
527        .map_err(|_| ErrorCodes::FailedToCloseAccount)?;
528
529        Ok(())
530    }
531    fn get_identifier(&self) -> String {
532        self.accounts.storage_account.identifier.clone()
533    }
534    fn mark_immutable(&mut self) {
535        self.accounts.storage_account.immutable = true;
536    }
537}