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
260
261
262
263
264
265
266
267
268
269
270
271
272
273
//! Utils
use solana_program::{
    account_info::AccountInfo, clock::Clock, entrypoint::ProgramResult, msg,
    program_error::ProgramError, program_pack::Pack,
    pubkey::Pubkey,
    sysvar::Sysvar,
};
use spl_token::state::Account;

use crate::error::AccessError;
use crate::instruction::ProgramInstruction;
use crate::state::{AUTHORIZED_BOND_SELLERS, BondAccount, RoyaltyAccount};
use crate::state::{ACCESS_MINT, STAKE_BUFFER_LEN, StakeAccount, StakePoolRef};

/// Cumulate the claimable rewards from the last claimed day to the present.
/// Result is in FP32 format.
///
/// * `staker` Compute the reward for a staker or a pool owner
pub fn calc_reward_fp32(
    current_offset: u64,
    last_claimed_offset: u64,
    stake_pool: &StakePoolRef,
    staker: bool,
    allow_zero_rewards: bool,
) -> Result<u128, ProgramError> {
    let mut nb_days_to_claim = current_offset.saturating_sub(last_claimed_offset);
    msg!("Nb of days behind {}", nb_days_to_claim);
    msg!("Last claimed offset {}", last_claimed_offset);
    msg!("Current offset {}", current_offset);
    nb_days_to_claim = std::cmp::min(nb_days_to_claim, STAKE_BUFFER_LEN);
    msg!("Nb of days to claim {}", nb_days_to_claim);
    if nb_days_to_claim == 0 {
        if !allow_zero_rewards {
            msg!("No rewards to claim, no operation.");
            return Err(AccessError::NoOp.into());
        }
        return Ok(0);
    }

    if current_offset > stake_pool.header.current_day_idx as u64 {
        #[cfg(not(any(feature = "days-to-sec-10s", feature = "days-to-sec-15m")))]
        return Err(AccessError::PoolMustBeCranked.into());
    }

    msg!(
        "Stake pool current day idx wrapped {}",
        (stake_pool.header.current_day_idx as u64) % STAKE_BUFFER_LEN
    );
    // Saturating as we don't want to wrap around when there haven't been sufficient cranks
    let mut i = (stake_pool.header.current_day_idx as u64).saturating_sub(nb_days_to_claim)
        % STAKE_BUFFER_LEN;

    // Compute reward for all past days
    let mut reward: u128 = 0;
    loop {
        let curr_day_reward = if staker {
            stake_pool.balances[i as usize].stakers_reward
        } else {
            stake_pool.balances[i as usize].pool_reward
        };
        reward = reward
            .checked_add(curr_day_reward)
            .ok_or(AccessError::Overflow)?;
        i = (i + 1) % STAKE_BUFFER_LEN;
        if i == (stake_pool.header.current_day_idx as u64) % STAKE_BUFFER_LEN {
            break;
        }
    }

    msg!("Reward is {}", reward);

    if reward == 0 && !allow_zero_rewards {
        msg!("No rewards to claim, no operation.");
        return Err(AccessError::NoOp.into());
    }

    Ok(reward)
}

// returns a mask of the instructions that are supposed to be frozen
// if no instructions are provided, all instructions are frozen
#[allow(missing_docs)]
pub fn get_freeze_mask(instructions: Vec<ProgramInstruction>) -> u128 {
    if instructions.is_empty() {
        return 0;
    }
    let mut mask = u128::MAX;
    for instruction in instructions {
        let ix_mask = 1 << instruction as u32;
        mask &= !ix_mask;
    }
    mask
}

#[allow(missing_docs)]
pub fn get_unfreeze_mask(instructions: Vec<ProgramInstruction>) -> u128 {
    if instructions.is_empty() {
        return u128::MAX;
    }
    let mut mask = 0;
    for instruction in instructions {
        let ix_mask = 1 << instruction as u32;
        mask |= ix_mask;
    }
    mask
}

#[allow(missing_docs)]
pub fn check_account_key(account: &AccountInfo, key: &Pubkey, error: AccessError) -> ProgramResult {
    if account.key != key {
        return Err(error.into());
    }
    Ok(())
}

#[allow(missing_docs)]
pub fn check_account_owner(
    account: &AccountInfo,
    owner: &Pubkey,
    error: AccessError,
) -> ProgramResult {
    if account.owner != owner {
        return Err(error.into());
    }
    Ok(())
}

#[allow(missing_docs)]
pub fn check_signer(account: &AccountInfo, error: AccessError) -> ProgramResult {
    if !(account.is_signer) {
        return Err(error.into());
    }
    Ok(())
}

#[allow(missing_docs)]
pub fn assert_empty_stake_pool(stake_pool: &StakePoolRef) -> ProgramResult {
    if stake_pool.header.total_staked != 0 {
        msg!("The stake pool must be empty");
        return Err(AccessError::StakePoolMustBeEmpty.into());
    }
    Ok(())
}

#[allow(missing_docs)]
pub fn assert_empty_stake_account(stake_account: &StakeAccount) -> ProgramResult {
    if stake_account.stake_amount != 0 {
        msg!("The stake account must be empty");
        return Err(AccessError::StakeAccountMustBeEmpty.into());
    }
    Ok(())
}

#[allow(missing_docs)]
pub fn assert_valid_vault(account: &AccountInfo, vault_signer: &Pubkey) -> ProgramResult {
    let acc = Account::unpack(&account.data.borrow())?;
    if &acc.owner != vault_signer {
        msg!("The vault account should be owned by the stake pool signer");
        return Err(ProgramError::InvalidArgument);
    }
    if acc.close_authority.is_some() || acc.delegate.is_some() {
        msg!("Invalid vault account provided");
        return Err(ProgramError::InvalidArgument);
    }
    if acc.mint != ACCESS_MINT {
        msg!("Invalid ACCESS mint");
        #[cfg(not(feature = "no-mint-check"))]
        return Err(ProgramError::InvalidArgument);
    }
    Ok(())
}

#[allow(missing_docs)]
pub fn assert_uninitialized(account: &AccountInfo) -> ProgramResult {
    if !account.data_is_empty() {
        return Err(ProgramError::AccountAlreadyInitialized);
    }
    Ok(())
}

#[allow(missing_docs)]
pub fn assert_authorized_seller(seller: &AccountInfo, seller_index: usize) -> ProgramResult {
    let expected_seller = AUTHORIZED_BOND_SELLERS
        .get(seller_index)
        .ok_or(AccessError::UnauthorizedSeller)?;
    if seller.key != expected_seller {
        return Err(AccessError::UnauthorizedSeller.into());
    }
    Ok(())
}

#[allow(missing_docs)]
pub fn assert_bond_derivation(
    account: &AccountInfo,
    owner: &Pubkey,
    total_amount_sold: u64,
    program_id: &Pubkey,
) -> ProgramResult {
    let (key, _nonce) = BondAccount::create_key(owner, total_amount_sold, program_id);
    check_account_key(account, &key, AccessError::AccountNotDeterministic)?;
    Ok(())
}

#[allow(missing_docs)]
pub fn assert_valid_fee(account: &AccountInfo, owner: &Pubkey) -> ProgramResult {
    check_account_owner(account, &spl_token::ID, AccessError::WrongOwner)?;
    let acc = Account::unpack(&account.data.borrow())?;
    if owner != &acc.owner {
        msg!("Invalid fee account owner");
        return Err(ProgramError::IllegalOwner);
    }
    assert_no_close_or_delegate(&acc)?;
    Ok(())
}

#[allow(missing_docs)]
pub fn assert_no_close_or_delegate(token_account: &Account) -> ProgramResult {
    if token_account.delegate.is_some() || token_account.close_authority.is_some() {
        msg!("This token account cannot have a delegate or close authority");
        return Err(ProgramError::InvalidArgument);
    }
    Ok(())
}

///  This function checks if there is an existing royalty account.
///  Checks the relationship between the appropriate royalty account and the royalty ATA
///  Returns the royalty account data if it exists. Otherwise returns None.
pub fn retrieve_royalty_account(
    royalty_account: &AccountInfo,
    royalty_ata: Option<&AccountInfo>,
) -> Result<Option<RoyaltyAccount>, ProgramError> {
    if royalty_account.data_is_empty() {
        return Ok(None);
    }

    let royalty_account_data = RoyaltyAccount::from_account_info(royalty_account)?;
    if royalty_account_data.expiration_date < Clock::get()?.unix_timestamp as u64 {
        return Ok(None); // Royalty account has expired - no royalty split is applicable
    }

    if royalty_ata.is_none() {
        return Err(AccessError::RoyaltyAtaNotProvided.into());
    }

    check_account_owner(
        royalty_ata.unwrap(),
        &spl_token::ID,
        AccessError::WrongOwner,
    )?;

    check_account_key(
        royalty_ata.unwrap(),
        &royalty_account_data.recipient_ata,
        AccessError::RoyaltyAtaNotDeterministic,
    )?;

    Ok(Some(royalty_account_data))
}

#[allow(missing_docs)]
pub fn is_admin_renouncable_instruction(instruction: &ProgramInstruction) -> bool {
    matches!(instruction,
        ProgramInstruction::ChangeInflation |
        ProgramInstruction::AdminMint |
        ProgramInstruction::AdminFreeze |
        ProgramInstruction::ChangeCentralStateAuthority |
        ProgramInstruction::EditMetadata |
        ProgramInstruction::AdminSetupFeeSplit |
        ProgramInstruction::AdminSetProtocolFee |
        ProgramInstruction::AdminProgramFreeze |
        ProgramInstruction::AdminChangeFreezeAuthority
    )
}