shadow_drive_user_staking/instructions/
refresh_stake.rs

1use anchor_lang::prelude::*;
2use anchor_spl::token::{Mint, Token, TokenAccount};
3
4use crate::constants::*;
5use crate::errors::ErrorCodes;
6use crate::instructions::{
7    initialize_account::{ShadowDriveStorageAccount, StorageAccount, StorageAccountV2},
8    initialize_config::StorageConfig,
9};
10
11/// This is the function that handles the `refresh_stake` ix
12pub fn handler(mut ctx: impl RefreshStake) -> Result<()> {
13    // Check if account is immutable
14    require!(
15        !ctx.check_immutable(),
16        ErrorCodes::StorageAccountMarkedImmutable
17    );
18
19    msg!("Charging user for storage");
20    ctx.refresh_stake()?;
21
22    // Unmarks for deletion if marked for deletion"
23    ctx.unmark_delete();
24
25    Ok(())
26}
27
28#[derive(Accounts)]
29/// This `RefreshStake` context is used in the instruction which allow users to
30/// top off their stake account
31pub struct RefreshStakeV1<'info> {
32    /// This is the `StorageConfig` accounts that holds all of the admin, uploader keys.
33    #[account(
34        seeds = [
35            "storage-config".as_bytes()
36        ],
37        bump,
38    )]
39    pub storage_config: Box<Account<'info, StorageConfig>>,
40
41    /// Parent storage account.
42    #[account(
43        mut,
44        seeds = [
45            "storage-account".as_bytes(),
46            &storage_account.owner_1.key().to_bytes(),
47            &storage_account.account_counter_seed.to_le_bytes()
48        ],
49        bump,
50    )]
51    pub storage_account: Box<Account<'info, StorageAccount>>,
52
53    /// File owner, user, fee-payer
54    /// Requires mutability since owner/user is fee payer.
55    #[account(mut)]
56    pub owner: Signer<'info>,
57
58    /// This is the user's token account with which they are staking
59    #[account(mut, constraint = owner_ata.mint == shdw::ID)]
60    pub owner_ata: Box<Account<'info, TokenAccount>>,
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    /// Token mint account
74    #[account(address = shdw::ID)]
75    pub token_mint: Account<'info, Mint>,
76
77    /// System Program
78    pub system_program: Program<'info, System>,
79
80    /// Token Program
81    pub token_program: Program<'info, Token>,
82}
83
84#[derive(Accounts)]
85/// This `RefreshStake` context is used in the instruction which allow users to
86/// top off their stake account
87pub struct RefreshStakeV2<'info> {
88    /// This is the `StorageConfig` accounts that holds all of the admin, uploader keys.
89    #[account(
90        seeds = [
91            "storage-config".as_bytes()
92        ],
93        bump,
94    )]
95    pub storage_config: Box<Account<'info, StorageConfig>>,
96
97    /// Parent storage account.
98    #[account(
99        mut,
100        seeds = [
101            "storage-account".as_bytes(),
102            &storage_account.owner_1.key().to_bytes(),
103            &storage_account.account_counter_seed.to_le_bytes()
104        ],
105        bump,
106    )]
107    pub storage_account: Box<Account<'info, StorageAccountV2>>,
108
109    /// File owner, user, fee-payer
110    /// Requires mutability since owner/user is fee payer.
111    #[account(mut)]
112    pub owner: Signer<'info>,
113
114    /// This is the user's token account with which they are staking
115    #[account(mut, constraint = owner_ata.mint == shdw::ID)]
116    pub owner_ata: Box<Account<'info, TokenAccount>>,
117
118    /// This token account serves as the account which holds user's stake for file storage.
119    #[account(
120        mut,
121        seeds = [
122            "stake-account".as_bytes(),
123            &storage_account.key().to_bytes()
124        ],
125        bump,
126    )]
127    pub stake_account: Box<Account<'info, TokenAccount>>,
128
129    /// Token mint account
130    #[account(address = shdw::ID)]
131    pub token_mint: Account<'info, Mint>,
132
133    /// System Program
134    pub system_program: Program<'info, System>,
135
136    /// Token Program
137    pub token_program: Program<'info, Token>,
138}
139
140fn safe_amount(storage: u64, rate_per_gib: u128) -> Result<u64> {
141    let result = (storage as u128)
142        .checked_mul(rate_per_gib as u128)
143        .unwrap()
144        .checked_div(BYTES_PER_GIB as u128)
145        .unwrap();
146    if (result as u64) as u128 == result {
147        Ok(result as u64)
148    } else {
149        err!(ErrorCodes::UnsignedIntegerCastFailed)
150    }
151}
152
153pub trait RefreshStake {
154    fn check_immutable(&self) -> bool;
155    fn refresh_stake(&mut self) -> Result<()>;
156    fn unmark_delete(&mut self);
157}
158
159impl RefreshStake for Context<'_, '_, '_, '_, RefreshStakeV1<'_>> {
160    fn check_immutable(&self) -> bool {
161        self.accounts.storage_account.check_immutable()
162    }
163    fn refresh_stake(&mut self) -> Result<()> {
164        let storage_config = &self.accounts.storage_config;
165        let storage_account = &self.accounts.storage_account;
166        let stake_account = &self.accounts.stake_account;
167
168        // Compute total cost of storage
169        let total_cost: u64 = safe_amount(
170            storage_account.storage,
171            storage_config.shades_per_gib as u128,
172        )?
173        .max(1);
174
175        // Compute refresh amount, the difference between total_cost and stake balance
176        let refresh_amount: u64 = total_cost.saturating_sub(stake_account.amount);
177
178        // Transfer SHDW
179        if refresh_amount > 0 {
180            anchor_spl::token::transfer(
181                CpiContext::new(
182                    self.accounts.token_program.to_account_info(),
183                    anchor_spl::token::Transfer {
184                        from: self.accounts.owner_ata.to_account_info(),
185                        to: self.accounts.stake_account.to_account_info(),
186                        authority: self.accounts.owner.to_account_info(),
187                    },
188                ),
189                refresh_amount,
190            )?;
191            emit!(StakeRefreshed {
192                refreshed: true,
193                account: self.accounts.storage_account.key()
194            });
195        }
196
197        Ok(())
198    }
199
200    fn unmark_delete(&mut self) {
201        let storage_account = &mut self.accounts.storage_account;
202
203        if storage_account.to_be_deleted {
204            msg!("Account was marked for deletion. Unmarking");
205
206            // Update deletion flag and reset request time
207            storage_account.to_be_deleted = false;
208            storage_account.delete_request_epoch = 0;
209        }
210    }
211}
212
213impl RefreshStake for Context<'_, '_, '_, '_, RefreshStakeV2<'_>> {
214    fn check_immutable(&self) -> bool {
215        self.accounts.storage_account.check_immutable()
216    }
217    fn refresh_stake(&mut self) -> Result<()> {
218        let storage_config = &self.accounts.storage_config;
219        let storage_account = &self.accounts.storage_account;
220        let stake_account = &self.accounts.stake_account;
221
222        // Compute total cost of storage
223        let total_cost: u64 = safe_amount(
224            storage_account.storage,
225            storage_config.shades_per_gib as u128,
226        )?
227        .max(1);
228
229        // Compute refresh amount, the difference between total_cost and stake balance
230        let refresh_amount: u64 = total_cost.saturating_sub(stake_account.amount);
231
232        // Transfer SHDW
233        if refresh_amount > 0 {
234            anchor_spl::token::transfer(
235                CpiContext::new(
236                    self.accounts.token_program.to_account_info(),
237                    anchor_spl::token::Transfer {
238                        from: self.accounts.owner_ata.to_account_info(),
239                        to: self.accounts.stake_account.to_account_info(),
240                        authority: self.accounts.owner.to_account_info(),
241                    },
242                ),
243                refresh_amount,
244            )?;
245            emit!(StakeRefreshed {
246                refreshed: true,
247                account: self.accounts.storage_account.key()
248            });
249        }
250        Ok(())
251    }
252
253    fn unmark_delete(&mut self) {
254        let storage_account = &mut self.accounts.storage_account;
255
256        if storage_account.to_be_deleted {
257            msg!("Account was marked for deletion. Unmarking");
258
259            // Update deletion flag and reset request time
260            storage_account.to_be_deleted = false;
261            storage_account.delete_request_epoch = 0;
262        }
263    }
264}
265
266#[event]
267pub struct StakeRefreshed {
268    pub refreshed: bool,
269    pub account: Pubkey,
270}