Skip to main content

shadow_drive_user_staking/instructions/
decrease_storage.rs

1use anchor_lang::prelude::*;
2use anchor_spl::token::{Mint, Token, TokenAccount};
3
4use crate::constants::*;
5use crate::errors::ErrorCodes;
6use crate::instructions::{
7    crank::crank,
8    initialize_account::{ShadowDriveStorageAccount, StorageAccount, StorageAccountV2},
9    initialize_config::StorageConfig,
10};
11
12/// This is the function that handles the `decrease_storage` ix
13pub fn handler(
14    mut ctx: impl DecreaseStorage,
15    remove_storage: u64,
16    storage_used: u64,
17) -> Result<()> {
18    // Check if account is immutable
19    require!(
20        !ctx.is_immutable(),
21        ErrorCodes::StorageAccountMarkedImmutable
22    );
23
24    msg!("Unstaking funds");
25    {
26        ctx.unstake(remove_storage, storage_used)?;
27    }
28
29    msg!(
30        "Decreasing storage on StorageAccount: {}",
31        ctx.get_identifier()
32    );
33    {
34        // Update total storage and storage_available
35        // NOTE: Now that we are not tracking storage on-chain in v1.5, this is the wrong condition,
36        // as it should check storage_available > remove_storage. It is up to the uploader server to
37        // check this condition! For now, we do this minimal sanity check whether the storage removed
38        // is smaller than the total storage on-chain.
39        require_gte!(
40            ctx.get_current_storage(),
41            remove_storage,
42            ErrorCodes::RemovingTooMuchStorage
43        );
44        ctx.remove_storage(remove_storage)?;
45    }
46
47    msg!("Setting unstake info");
48    {
49        ctx.record_unstake_info()?;
50    }
51
52    Ok(())
53}
54
55#[derive(Accounts)]
56/// This `DecreaseStorage` context is used in the instruction which allow users to begin
57/// to unstake funds, decreasing their available storage.
58pub struct DecreaseStorageV1<'info> {
59    /// This is the `StorageConfig` accounts that holds all of the admin, uploader keys.
60    #[account(
61        mut,
62        seeds = [
63            "storage-config".as_bytes()
64        ],
65        bump,
66    )]
67    pub storage_config: Box<Account<'info, StorageConfig>>,
68
69    /// Parent storage account.
70    #[account(
71        mut,
72        seeds = [
73            "storage-account".as_bytes(),
74            &storage_account.owner_1.key().to_bytes(),
75            &storage_account.account_counter_seed.to_le_bytes()
76        ],
77        bump,
78    )]
79    pub storage_account: Box<Account<'info, StorageAccount>>,
80
81    /// Account which stores time, epoch last unstaked
82    #[account(
83        init_if_needed,
84        space = std::mem::size_of::<UnstakeInfo>() + 8,
85        payer = owner,
86        seeds = [
87            "unstake-info".as_bytes(),
88            &storage_account.key().to_bytes(),
89        ],
90        bump,
91    )]
92    pub unstake_info: Box<Account<'info, UnstakeInfo>>,
93
94    /// Account which stores SHDW when unstaking
95    #[account(
96        init_if_needed,
97        payer = owner,
98        seeds = [
99            "unstake-account".as_bytes(),
100            &storage_account.key().to_bytes(),
101        ],
102        bump,
103        token::mint = token_mint,
104        token::authority = storage_config,
105    )]
106    pub unstake_account: Box<Account<'info, TokenAccount>>,
107
108    /// File owner, user, fee-payer
109    /// Requires mutability since owner/user is fee payer.
110    #[account(mut, constraint=storage_account.is_owner(owner.key()))]
111    pub owner: Signer<'info>,
112
113    /// User's ATA
114    #[account(
115        mut,
116        constraint = {
117            owner_ata.owner == owner.key()
118            && owner_ata.mint == token_mint.key()
119        }
120    )]
121    pub owner_ata: Box<Account<'info, TokenAccount>>,
122
123    /// This token account serves as the account which holds user's stake for file storage.
124    #[account(
125        mut,
126        seeds = [
127            "stake-account".as_bytes(),
128            &storage_account.key().to_bytes(),
129        ],
130        bump,
131    )]
132    pub stake_account: Box<Account<'info, TokenAccount>>,
133
134    /// Token mint account
135    #[account(mut, address = shdw::ID)]
136    pub token_mint: Account<'info, Mint>,
137
138    /// Uploader needs to sign off on decrease storage
139    #[account(constraint = uploader.key() == storage_config.uploader)]
140    pub uploader: Signer<'info>,
141
142    /// Token account holding operator emission funds
143    #[account(mut, address = shdw::emissions_wallet::ID)]
144    pub emissions_wallet: Box<Account<'info, TokenAccount>>,
145
146    /// System Program
147    pub system_program: Program<'info, System>,
148
149    /// Token Program
150    pub token_program: Program<'info, Token>,
151
152    /// Rent Program
153    pub rent: Sysvar<'info, Rent>,
154}
155
156#[derive(Accounts)]
157/// This `DecreaseStorage` context is used in the instruction which allow users to begin
158/// to unstake funds, decreasing their available storage.
159pub struct DecreaseStorageV2<'info> {
160    /// This is the `StorageConfig` accounts that holds all of the admin, uploader keys.
161    #[account(
162        mut,
163        seeds = [
164            "storage-config".as_bytes()
165        ],
166        bump,
167    )]
168    pub storage_config: Box<Account<'info, StorageConfig>>,
169
170    /// Parent storage account.
171    #[account(
172        mut,
173        seeds = [
174            "storage-account".as_bytes(),
175            &storage_account.owner_1.key().to_bytes(),
176            &storage_account.account_counter_seed.to_le_bytes()
177        ],
178        bump,
179    )]
180    pub storage_account: Account<'info, StorageAccountV2>,
181
182    /// Account which stores time, epoch last unstaked
183    #[account(
184        init_if_needed,
185        space = std::mem::size_of::<UnstakeInfo>() + 8,
186        payer = owner,
187        seeds = [
188            "unstake-info".as_bytes(),
189            &storage_account.key().to_bytes(),
190        ],
191        bump,
192    )]
193    pub unstake_info: Box<Account<'info, UnstakeInfo>>,
194
195    /// Account which stores SHDW when unstaking
196    #[account(
197        init_if_needed,
198        payer = owner,
199        seeds = [
200            "unstake-account".as_bytes(),
201            &storage_account.key().to_bytes(),
202        ],
203        bump,
204        token::mint = token_mint,
205        token::authority = storage_config,
206    )]
207    pub unstake_account: Box<Account<'info, TokenAccount>>,
208
209    /// File owner, user, fee-payer
210    /// Requires mutability since owner/user is fee payer.
211    #[account(mut, constraint=storage_account.is_owner(owner.key()))]
212    pub owner: Signer<'info>,
213
214    /// User's ATA
215    #[account(
216        mut,
217        constraint = {
218            owner_ata.owner == owner.key()
219            && owner_ata.mint == token_mint.key()
220        }
221    )]
222    pub owner_ata: Box<Account<'info, TokenAccount>>,
223
224    /// This token account serves as the account which holds user's stake for file storage.
225    #[account(
226        mut,
227        seeds = [
228            "stake-account".as_bytes(),
229            &storage_account.key().to_bytes(),
230        ],
231        bump,
232    )]
233    pub stake_account: Box<Account<'info, TokenAccount>>,
234
235    /// Token mint account
236    #[account(mut, address = shdw::ID)]
237    pub token_mint: Account<'info, Mint>,
238
239    /// Uploader needs to sign off on decrease storage
240    #[account(constraint = uploader.key() == storage_config.uploader)]
241    pub uploader: Signer<'info>,
242
243    /// Token account holding operator emission funds
244    #[account(mut, address = shdw::emissions_wallet::ID)]
245    pub emissions_wallet: Box<Account<'info, TokenAccount>>,
246
247    /// System Program
248    pub system_program: Program<'info, System>,
249
250    /// Token Program
251    pub token_program: Program<'info, Token>,
252
253    /// Rent Program
254    pub rent: Sysvar<'info, Rent>,
255}
256
257#[account]
258pub struct UnstakeInfo {
259    pub time_last_unstaked: i64,
260    pub epoch_last_unstaked: u64,
261    pub unstaker: Pubkey,
262}
263
264fn safe_mul_div(a: u64, b: u64, c: u64) -> Result<u64> {
265    let result = (a as u128)
266        .checked_mul(b as u128)
267        .unwrap()
268        .checked_div(c as u128)
269        .unwrap();
270    if (result as u64) as u128 == result {
271        Ok(result as u64)
272    } else {
273        err!(ErrorCodes::UnsignedIntegerCastFailed)
274    }
275}
276
277pub trait DecreaseStorage {
278    fn is_immutable(&self) -> bool;
279    fn get_identifier(&self) -> String;
280    fn get_current_storage(&self) -> u64;
281    fn unstake(&mut self, remove_storage: u64, storage_used: u64) -> Result<()>;
282    fn remove_storage(&mut self, remove_storage: u64) -> Result<()>;
283    fn record_unstake_info(&mut self) -> Result<()>;
284}
285
286impl DecreaseStorage for Context<'_, '_, '_, '_, DecreaseStorageV1<'_>> {
287    fn is_immutable(&self) -> bool {
288        self.accounts.storage_account.immutable
289    }
290    fn get_identifier(&self) -> String {
291        self.accounts.storage_account.identifier.clone()
292    }
293    fn get_current_storage(&self) -> u64 {
294        self.accounts.storage_account.storage
295    }
296    fn unstake(&mut self, remove_storage: u64, storage_used: u64) -> Result<()> {
297        // Compute unstake amount, up to total stake (in case of any rounding issues)
298        let mut unstake_amount = safe_mul_div(
299            remove_storage,
300            self.accounts.stake_account.amount,
301            self.accounts.storage_account.storage,
302        )
303        .unwrap()
304        .min(self.accounts.stake_account.amount);
305
306        // Transfer SHDW
307        let storage_config_seeds = [
308            "storage-config".as_bytes(),
309            &[*self.bumps.get("storage_config").unwrap()],
310        ];
311
312        // If mutable account fees are on, charge any outstanding fees.
313        // Note: Cranker fee goes to emissions wallet.
314        let account_info = self.accounts.storage_account.to_account_info();
315        if let Some((emission_fee, crank_fee)) = crank(
316            &self.accounts.storage_config,
317            &mut self.accounts.storage_account,
318            account_info,
319            &self.accounts.emissions_wallet,
320            &self.accounts.stake_account,
321            &self.accounts.token_program,
322            &self.accounts.owner_ata,
323            &self.accounts.token_mint,
324            *self.bumps.get("storage_config").unwrap(),
325            storage_used,
326        )? {
327            // Subtract fee from return
328            let fee = emission_fee.checked_add(crank_fee).unwrap();
329            unstake_amount = unstake_amount.saturating_sub(fee);
330        }
331
332        let signer_seeds: &[&[&[u8]]] = &[&storage_config_seeds];
333        anchor_spl::token::transfer(
334            CpiContext::new_with_signer(
335                self.accounts.token_program.to_account_info(),
336                anchor_spl::token::Transfer {
337                    from: self.accounts.stake_account.to_account_info(),
338                    to: self.accounts.unstake_account.to_account_info(),
339                    authority: self.accounts.storage_config.to_account_info(),
340                },
341                signer_seeds,
342            ),
343            unstake_amount,
344        )
345    }
346    fn remove_storage(&mut self, remove_storage: u64) -> Result<()> {
347        self.accounts.storage_account.storage = self
348            .accounts
349            .storage_account
350            .storage
351            .checked_sub(remove_storage)
352            .unwrap();
353        Ok(())
354    }
355    fn record_unstake_info(&mut self) -> Result<()> {
356        // Get clock
357        let clock = Clock::get().unwrap();
358
359        // Recored time, epoch, unstaker
360        self.accounts.unstake_info.time_last_unstaked = clock.unix_timestamp;
361        self.accounts.unstake_info.epoch_last_unstaked = clock.epoch;
362        self.accounts.unstake_info.unstaker = self.accounts.owner.key();
363
364        Ok(())
365    }
366}
367
368impl DecreaseStorage for Context<'_, '_, '_, '_, DecreaseStorageV2<'_>> {
369    fn is_immutable(&self) -> bool {
370        self.accounts.storage_account.immutable
371    }
372    fn get_identifier(&self) -> String {
373        self.accounts.storage_account.identifier.clone()
374    }
375    fn get_current_storage(&self) -> u64 {
376        self.accounts.storage_account.storage
377    }
378    fn unstake(&mut self, remove_storage: u64, storage_used: u64) -> Result<()> {
379        // Compute unstake amount, up to total stake (in case of any rounding issues)
380        let mut unstake_amount = safe_mul_div(
381            remove_storage,
382            self.accounts.stake_account.amount,
383            self.accounts.storage_account.storage,
384        )
385        .unwrap()
386        .min(self.accounts.stake_account.amount);
387
388        // Transfer SHDW
389        let storage_config_seeds = [
390            "storage-config".as_bytes(),
391            &[*self.bumps.get("storage_config").unwrap()],
392        ];
393
394        // If mutable account fees are on, charge any outstanding fees.
395        // Note: Cranker fee goes to emissions wallet.
396        let account_info = self.accounts.storage_account.to_account_info();
397        if let Some((emission_fee, crank_fee)) = crank(
398            &self.accounts.storage_config,
399            &mut self.accounts.storage_account,
400            account_info,
401            &self.accounts.emissions_wallet,
402            &self.accounts.stake_account,
403            &self.accounts.token_program,
404            &self.accounts.owner_ata,
405            &self.accounts.token_mint,
406            *self.bumps.get("storage_config").unwrap(),
407            storage_used,
408        )? {
409            // Subtract fee from return
410            let fee = emission_fee.checked_add(crank_fee).unwrap();
411            unstake_amount = unstake_amount.saturating_sub(fee);
412        }
413
414        let signer_seeds: &[&[&[u8]]] = &[&storage_config_seeds];
415        anchor_spl::token::transfer(
416            CpiContext::new_with_signer(
417                self.accounts.token_program.to_account_info(),
418                anchor_spl::token::Transfer {
419                    from: self.accounts.stake_account.to_account_info(),
420                    to: self.accounts.unstake_account.to_account_info(),
421                    authority: self.accounts.storage_config.to_account_info(),
422                },
423                signer_seeds,
424            ),
425            unstake_amount,
426        )
427    }
428    fn remove_storage(&mut self, remove_storage: u64) -> Result<()> {
429        self.accounts.storage_account.storage = self
430            .accounts
431            .storage_account
432            .storage
433            .checked_sub(remove_storage)
434            .unwrap();
435        Ok(())
436    }
437    fn record_unstake_info(&mut self) -> Result<()> {
438        // Get clock
439        let clock = Clock::get().unwrap();
440
441        // Recored time, epoch, unstaker
442        self.accounts.unstake_info.time_last_unstaked = clock.unix_timestamp;
443        self.accounts.unstake_info.epoch_last_unstaked = clock.epoch;
444        self.accounts.unstake_info.unstaker = self.accounts.owner.key();
445
446        Ok(())
447    }
448}