1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
use anchor_lang::prelude::*;
use anchor_spl::token::{self, Mint, MintTo, Token, TokenAccount};
use solana_farm_sdk::{id::zero, program::account, program::protocol::raydium};
use solana_program::program_error::ProgramError;
use vipers::{assert_keys_eq, invariant, Validate};

use crate::{gen_vault_signer_seeds, UserInfo, Vault, VaultStakeEvent};

#[derive(Accounts)]
pub struct Stake<'info> {
    // Vault Information
    #[account(mut)]
    pub vault: Box<Account<'info, Vault>>,
    // The LP Token Mint of the vault.
    #[account(mut)]
    pub vault_lp_token_mint: Box<Account<'info, Mint>>,
    #[account(mut)]
    pub lp_funding_account: Box<Account<'info, TokenAccount>>,
    // User information
    // The following should be passed in by the user of this strategy.
    pub user_account: Signer<'info>,

    // The user info account keeps track of the use of this vault.
    #[account(mut)]
    pub user_info_account: Box<Account<'info, UserInfo>>,

    #[account(mut)]
    pub vault_lp_token_account: Box<Account<'info, TokenAccount>>,

    // The Raydium Farm Account
    #[account(mut)]
    /// CHECK: Raydium verifies
    pub farm: UncheckedAccount<'info>,

    /// Raydium Farm Metadata
    /// CHECK: Raydium verifies
    pub farm_authority: UncheckedAccount<'info>,

    // For Farming Metadata.
    #[account(mut)]
    /// CHECK: Raydium verifies
    pub stake_info_account: UncheckedAccount<'info>,

    // The following are farm reward vaults.
    // They are owned by raydium not delta.
    #[account(mut)]
    pub farm_lp_token_account: Box<Account<'info, TokenAccount>>,
    #[account(mut)]
    pub farm_reward_token_a_account: Box<Account<'info, TokenAccount>>,
    #[account(mut)]
    /// CHECK: Raydium verifies
    pub farm_reward_token_b_account: UncheckedAccount<'info>,

    // The following are custody accounts owned by the vault.
    // they are used for harvesting.
    #[account(mut)]
    pub pool_lp_custody_account: Box<Account<'info, TokenAccount>>,
    #[account(mut)]
    pub farm_token_a_reward_custody_account: Box<Account<'info, TokenAccount>>,
    #[account(mut)]
    pub farm_token_b_reward_custody_account: Box<Account<'info, TokenAccount>>,

    // Raydium Farm Program.
    // Make sure this is the same as in the vault.
    /// CHECK: Raydium verifies
    pub farm_program: UncheckedAccount<'info>,

    // SPL Token Program
    pub spl_token_program: Program<'info, Token>,

    // Clock Program
    pub clock_program: Sysvar<'info, Clock>,
}

impl<'info> Stake<'info> {
    fn stake(&mut self, lp_tokens_received: u64) -> Result<()> {
        account::transfer_tokens(
            &self.lp_funding_account.to_account_info(),
            &self.pool_lp_custody_account.to_account_info(),
            &self.user_account,
            lp_tokens_received,
        )?;

        let initial_lp_token_custody_balance = self.pool_lp_custody_account.amount;

        // // Stake LP tokens
        let seed = gen_vault_signer_seeds!(self.vault);
        let signer_seeds = &[&seed[..]];

        let dual_rewards = self.farm_reward_token_b_account.key() != zero::id();
        let initial_token_a_reward_balance = self.farm_token_a_reward_custody_account.amount;
        let initial_token_b_reward_balance = if dual_rewards {
            self.farm_token_b_reward_custody_account.amount
        } else {
            0
        };

        msg!("Stake LP tokens");
        let stake_balance =
            raydium::get_stake_account_balance(&self.stake_info_account.to_account_info())?;

        msg!("starting to stake...");

        raydium::stake_with_seeds(
            &[
                self.vault.to_account_info(),
                self.stake_info_account.to_account_info(),
                // first the farm lp and reward custody accounts - all should be owned by the vault.
                self.pool_lp_custody_account.to_account_info(),
                self.farm_token_a_reward_custody_account.to_account_info(),
                self.farm_token_b_reward_custody_account.to_account_info(),
                self.farm_program.to_account_info(),
                // then comes the farm lp and reward accounts.
                // these live in the farm and are owned by raydium.
                self.farm_lp_token_account.to_account_info(),
                self.farm_reward_token_a_account.to_account_info(),
                self.farm_reward_token_b_account.to_account_info(),
                // solana information
                self.clock_program.to_account_info(),
                self.spl_token_program.to_account_info(),
                // farm information.
                self.farm.to_account_info(),
                self.farm_authority.to_account_info(),
            ],
            signer_seeds,
            lp_tokens_received,
        )?;

        msg!("Finished staking.");
        invariant!(
            initial_lp_token_custody_balance == self.pool_lp_custody_account.amount,
            "Error: Stake instruction didn't result in expected amount of LP tokens spent"
        );

        // update Vault stats
        let token_a_rewards = account::get_balance_increase(
            &self.farm_token_a_reward_custody_account.to_account_info(),
            initial_token_a_reward_balance,
        )?;
        let token_b_rewards = if dual_rewards {
            account::get_balance_increase(
                &self.farm_token_b_reward_custody_account.to_account_info(),
                initial_token_b_reward_balance,
            )?
        } else {
            0
        };
        msg!(
            "Update Vault stats. token_a_rewards: {}, token_b_rewards: {}",
            token_a_rewards,
            token_b_rewards
        );
        self.vault.add_rewards(token_a_rewards, token_b_rewards)?;

        // compute Vault tokens to mint
        let vault_token_supply_amount = self.vault_lp_token_mint.supply;
        let vt_to_mint = if vault_token_supply_amount == 0 || stake_balance == 0 {
            lp_tokens_received
        } else {
            account::to_token_amount(
                lp_tokens_received as f64 / stake_balance as f64
                    * self.vault_lp_token_mint.supply as f64,
                0,
            )?
        };

        // mint vault tokens to user
        msg!(
            "Mint Vault tokens to the user. vt_to_mint: {}, vt_supply_amount: {}, stake_balance: {}",
            vt_to_mint, vault_token_supply_amount,
            stake_balance
        );
        if vt_to_mint == 0 {
            msg!("Error: Add liquidity instruction didn't result in Vault tokens mint");
            return Err(ProgramError::Custom(170).into());
        }

        let mint_to_accounts = MintTo {
            mint: self.vault_lp_token_mint.to_account_info(),
            to: self.vault_lp_token_account.to_account_info(),
            authority: self.vault.to_account_info(),
        };
        let cpi_ctx = CpiContext::new_with_signer(
            self.spl_token_program.to_account_info(),
            mint_to_accounts,
            signer_seeds,
        );
        token::mint_to(cpi_ctx, vt_to_mint)?;
        msg!("Completed add liquidity and stake.");

        Ok(())
    }
}

pub fn handler(ctx: Context<Stake>, amount: u64) -> Result<()> {
    ctx.accounts.stake(amount)?;
    emit!(VaultStakeEvent {
        vault: ctx.accounts.vault.key(),
        amount,
    });
    Ok(())
}

impl<'info> Validate<'info> for Stake<'info> {
    fn validate(&self) -> Result<()> {
        // make sure the user info account is for this vault.
        assert_keys_eq!(self.user_info_account.vault, self.vault);

        assert_keys_eq!(self.vault_lp_token_account.owner, self.user_account,);

        // make sure user is same user as user info account
        assert_keys_eq!(self.user_info_account.user_account, self.user_account);

        // make sure that the vault token account is the same mint as the vault lp mint.
        assert_keys_eq!(
            self.vault.vault_lp_token_mint,
            self.vault_lp_token_account.mint,
            "Vault lp token account mint is incorrect."
        );

        assert_keys_eq!(self.lp_funding_account.mint, self.vault.pool_lp_token_mint);

        // make sure farm metadata is same as specified in vault
        assert_keys_eq!(self.farm, self.vault.farm);
        assert_keys_eq!(self.farm_authority, self.vault.farm_authority);

        // TODO - not specifying stake info account because it can be different on devnet and on mainnet.

        // Farm Vaults
        assert_keys_eq!(self.farm_lp_token_account, self.vault.farm_lp_token_account);
        assert_keys_eq!(
            self.farm_reward_token_a_account,
            self.vault.farm_reward_token_a_account
        );
        assert_keys_eq!(
            self.farm_reward_token_b_account,
            self.vault.farm_reward_token_b_account
        );

        // custody accounts

        assert_keys_eq!(
            self.pool_lp_custody_account,
            self.vault.pool_lp_custody_account
        );
        assert_keys_eq!(
            self.farm_token_a_reward_custody_account,
            self.vault.farm_token_a_reward_custody_account
        );
        assert_keys_eq!(
            self.farm_token_b_reward_custody_account,
            self.vault.farm_token_b_reward_custody_account
        );

        assert_keys_eq!(self.farm_program, self.vault.farm_program_id);

        Ok(())
    }
}