solana_farm_sdk/program/
account.rs

1//! Common accounts management functions
2
3use {
4    crate::{
5        error::FarmError,
6        id::zero,
7        math,
8        pack::check_data_len,
9        program::clock,
10        token::{OraclePrice, OracleType},
11        traits::Packed,
12    },
13    arrayref::{array_ref, array_refs},
14    pyth_client::{PriceStatus, PriceType},
15    solana_program::{
16        account_info::AccountInfo, entrypoint::ProgramResult, msg, program::invoke,
17        program_error::ProgramError, program_pack::Pack, pubkey::Pubkey, system_instruction,
18        sysvar, sysvar::Sysvar,
19    },
20    spl_token::state::{Account, Mint},
21    std::cmp::Ordering,
22};
23
24/// Returns Token Mint supply.
25/// Extrats supply field without unpacking entire struct.
26pub fn get_token_supply(token_mint: &AccountInfo) -> Result<u64, ProgramError> {
27    let data = token_mint.try_borrow_data()?;
28    check_data_len(&data, spl_token::state::Mint::get_packed_len())?;
29    let supply = array_ref![data, 36, 8];
30
31    Ok(u64::from_le_bytes(*supply))
32}
33
34/// Returns Token decimals.
35/// Extrats decimals field without unpacking entire struct.
36pub fn get_token_decimals(token_mint: &AccountInfo) -> Result<u8, ProgramError> {
37    let data = token_mint.try_borrow_data()?;
38    check_data_len(&data, spl_token::state::Mint::get_packed_len())?;
39    let decimals = array_ref![data, 44, 1];
40
41    Ok(decimals[0])
42}
43
44/// Returns Tokens balance.
45/// Extrats balance field without unpacking entire struct.
46pub fn get_token_balance(token_account: &AccountInfo) -> Result<u64, ProgramError> {
47    let data = token_account.try_borrow_data()?;
48    check_data_len(&data, spl_token::state::Account::get_packed_len())?;
49    let amount = array_ref![data, 64, 8];
50
51    Ok(u64::from_le_bytes(*amount))
52}
53
54/// Returns Token account owner.
55/// Extrats owner field without unpacking entire struct.
56pub fn get_token_account_owner(token_account: &AccountInfo) -> Result<Pubkey, ProgramError> {
57    let data = token_account.try_borrow_data()?;
58    check_data_len(&data, spl_token::state::Account::get_packed_len())?;
59    let owner = array_ref![data, 32, 32];
60
61    Ok(Pubkey::new_from_array(*owner))
62}
63
64/// Checks Token account owner
65pub fn check_token_account_owner(
66    token_account: &AccountInfo,
67    expected_owner: &Pubkey,
68) -> Result<bool, ProgramError> {
69    Ok(token_account.owner == &spl_token::id()
70        && get_token_account_owner(token_account)? == *expected_owner)
71}
72
73/// Checks Token account owner
74pub fn check_token_account_owner_or_zero(
75    token_account: &AccountInfo,
76    expected_owner: &Pubkey,
77) -> Result<bool, ProgramError> {
78    Ok(token_account.key == &zero::id()
79        || (token_account.owner == &spl_token::id()
80            && get_token_account_owner(token_account)? == *expected_owner))
81}
82
83/// Returns Token account mint.
84/// Extrats mint field without unpacking entire struct.
85pub fn get_token_account_mint(token_account: &AccountInfo) -> Result<Pubkey, ProgramError> {
86    let data = token_account.try_borrow_data()?;
87    check_data_len(&data, spl_token::state::Account::get_packed_len())?;
88    let mint = array_ref![data, 0, 32];
89
90    Ok(Pubkey::new_from_array(*mint))
91}
92
93/// Returns Mint authority.
94/// Extrats authority field without unpacking entire struct.
95pub fn get_mint_authority(token_mint: &AccountInfo) -> Result<Option<Pubkey>, ProgramError> {
96    let data = token_mint.try_borrow_data()?;
97    check_data_len(&data, spl_token::state::Mint::get_packed_len())?;
98
99    let data = array_ref![data, 0, 36];
100    let (tag, authority) = array_refs![data, 4, 32];
101    match *tag {
102        [0, 0, 0, 0] => Ok(None),
103        [1, 0, 0, 0] => Ok(Some(Pubkey::new_from_array(*authority))),
104        _ => Err(ProgramError::InvalidAccountData),
105    }
106}
107
108/// Checks mint authority
109pub fn check_mint_authority(
110    mint_account: &AccountInfo,
111    expected_authority: Option<Pubkey>,
112) -> Result<bool, ProgramError> {
113    Ok(mint_account.owner == &spl_token::id()
114        && get_mint_authority(mint_account)? == expected_authority)
115}
116
117pub fn is_empty(account: &AccountInfo) -> Result<bool, ProgramError> {
118    Ok(account.data_is_empty() || account.try_lamports()? == 0)
119}
120
121pub fn exists(account: &AccountInfo) -> Result<bool, ProgramError> {
122    Ok(account.try_lamports()? > 0)
123}
124
125pub fn get_balance_increase(
126    account: &AccountInfo,
127    previous_balance: u64,
128) -> Result<u64, ProgramError> {
129    let balance = get_token_balance(account)?;
130    if let Some(res) = balance.checked_sub(previous_balance) {
131        Ok(res)
132    } else {
133        msg!(
134            "Error: Balance decrease was not expected. Account: {}",
135            account.key
136        );
137        Err(FarmError::UnexpectedBalanceDecrease.into())
138    }
139}
140
141pub fn get_balance_decrease(
142    account: &AccountInfo,
143    previous_balance: u64,
144) -> Result<u64, ProgramError> {
145    let balance = get_token_balance(account)?;
146    if let Some(res) = previous_balance.checked_sub(balance) {
147        Ok(res)
148    } else {
149        msg!(
150            "Error: Balance increase was not expected. Account: {}",
151            account.key
152        );
153        Err(FarmError::UnexpectedBalanceIncrease.into())
154    }
155}
156
157pub fn check_tokens_spent(
158    account: &AccountInfo,
159    previous_balance: u64,
160    max_amount_spent: u64,
161) -> Result<u64, ProgramError> {
162    let tokens_spent = get_balance_decrease(account, previous_balance)?;
163    if tokens_spent > max_amount_spent {
164        msg!(
165            "Error: Invoked program overspent. Account: {}, max expected: {}, actual: {}",
166            account.key,
167            max_amount_spent,
168            tokens_spent
169        );
170        Err(FarmError::ProgramOverspent.into())
171    } else {
172        Ok(tokens_spent)
173    }
174}
175
176pub fn check_tokens_received(
177    account: &AccountInfo,
178    previous_balance: u64,
179    min_amount_received: u64,
180) -> Result<u64, ProgramError> {
181    let tokens_received = get_balance_increase(account, previous_balance)?;
182    if tokens_received < min_amount_received {
183        msg!(
184            "Error: Not enough tokens returned by invoked program. Account: {}, min expected: {}, actual: {}",
185            account.key,
186            min_amount_received,
187            tokens_received
188        );
189        Err(FarmError::ProgramInsufficientTransfer.into())
190    } else {
191        Ok(tokens_received)
192    }
193}
194
195/// Returns Token Mint data.
196pub fn get_token_mint(token_mint: &AccountInfo) -> Result<Mint, ProgramError> {
197    let data = token_mint.try_borrow_data()?;
198    Mint::unpack(&data)
199}
200
201/// Returns Token Account data.
202pub fn get_token_account(token_account: &AccountInfo) -> Result<Account, ProgramError> {
203    let data = token_account.try_borrow_data()?;
204    Account::unpack(&data)
205}
206
207/// Returns token pair ratio, optimized for on-chain.
208pub fn get_token_ratio<'a, 'b>(
209    token_a_balance: u64,
210    token_b_balance: u64,
211    token_a_mint: &'a AccountInfo<'b>,
212    token_b_mint: &'a AccountInfo<'b>,
213) -> Result<f64, ProgramError> {
214    get_token_ratio_with_decimals(
215        token_a_balance,
216        token_b_balance,
217        get_token_decimals(token_a_mint)?,
218        get_token_decimals(token_b_mint)?,
219    )
220}
221
222/// Returns token pair ratio, uses decimals instead of mints
223pub fn get_token_ratio_with_decimals(
224    token_a_balance: u64,
225    token_b_balance: u64,
226    token_a_decimals: u8,
227    token_b_decimals: u8,
228) -> Result<f64, ProgramError> {
229    if token_a_balance == 0 || token_b_balance == 0 {
230        return Ok(0.0);
231    }
232
233    Ok(token_b_balance as f64 / token_a_balance as f64
234        * math::checked_powi(10.0, token_a_decimals as i32 - token_b_decimals as i32)?)
235}
236
237/// Returns token pair ratio
238pub fn get_token_pair_ratio<'a, 'b>(
239    token_a_account: &'a AccountInfo<'b>,
240    token_b_account: &'a AccountInfo<'b>,
241) -> Result<f64, ProgramError> {
242    let token_a_balance = get_token_balance(token_a_account)?;
243    let token_b_balance = get_token_balance(token_b_account)?;
244    if token_a_balance == 0 || token_b_balance == 0 {
245        return Ok(0.0);
246    }
247    Ok(token_b_balance as f64 / token_a_balance as f64)
248}
249
250pub fn to_ui_amount(amount: u64, decimals: u8) -> f64 {
251    let mut ui_amount = amount as f64;
252    for _ in 0..decimals {
253        ui_amount /= 10.0;
254    }
255    ui_amount
256}
257
258pub fn to_token_amount(ui_amount: f64, decimals: u8) -> Result<u64, ProgramError> {
259    let mut amount = ui_amount;
260    for _ in 0..decimals {
261        amount *= 10.0;
262    }
263    math::checked_as_u64(amount)
264}
265
266pub fn to_amount_with_new_decimals(
267    amount: u64,
268    original_decimals: u8,
269    new_decimals: u8,
270) -> Result<u64, ProgramError> {
271    match new_decimals.cmp(&original_decimals) {
272        Ordering::Greater => {
273            let exponent = new_decimals.checked_sub(original_decimals).unwrap();
274            math::checked_mul(amount, math::checked_pow(10u64, exponent as usize)?)
275        }
276        Ordering::Less => {
277            let exponent = original_decimals.checked_sub(new_decimals).unwrap();
278            math::checked_div(amount, math::checked_pow(10u64, exponent as usize)?)
279        }
280        Ordering::Equal => Ok(amount),
281    }
282}
283
284pub fn init_token_account<'a, 'b>(
285    funding_account: &'a AccountInfo<'b>,
286    target_account: &'a AccountInfo<'b>,
287    mint_account: &'a AccountInfo<'b>,
288    owner_account: &'a AccountInfo<'b>,
289    rent_program: &'a AccountInfo<'b>,
290    seed: &str,
291) -> ProgramResult {
292    if exists(target_account)? {
293        if !check_token_account_owner(target_account, owner_account.key)? {
294            return Err(ProgramError::IllegalOwner);
295        }
296        if target_account.data_len() != spl_token::state::Account::get_packed_len()
297            || mint_account.key != &get_token_account_mint(target_account)?
298        {
299            return Err(ProgramError::InvalidAccountData);
300        }
301        return Ok(());
302    }
303
304    init_system_account(
305        funding_account,
306        target_account,
307        &spl_token::id(),
308        seed,
309        spl_token::state::Account::get_packed_len(),
310    )?;
311
312    invoke(
313        &spl_token::instruction::initialize_account(
314            &spl_token::id(),
315            target_account.key,
316            mint_account.key,
317            owner_account.key,
318        )?,
319        &[
320            target_account.clone(),
321            mint_account.clone(),
322            owner_account.clone(),
323            rent_program.clone(),
324        ],
325    )
326}
327
328pub fn close_token_account<'a, 'b>(
329    receiving_account: &'a AccountInfo<'b>,
330    target_account: &'a AccountInfo<'b>,
331    authority_account: &'a AccountInfo<'b>,
332) -> ProgramResult {
333    if !exists(target_account)? {
334        return Ok(());
335    }
336
337    invoke(
338        &spl_token::instruction::close_account(
339            &spl_token::id(),
340            target_account.key,
341            receiving_account.key,
342            authority_account.key,
343            &[],
344        )?,
345        &[
346            target_account.clone(),
347            receiving_account.clone(),
348            authority_account.clone(),
349        ],
350    )
351}
352
353pub fn transfer_sol_from_owned<'a, 'b>(
354    program_owned_source_account: &'a AccountInfo<'b>,
355    destination_account: &'a AccountInfo<'b>,
356    amount: u64,
357) -> ProgramResult {
358    **destination_account.try_borrow_mut_lamports()? = destination_account
359        .try_lamports()?
360        .checked_add(amount)
361        .ok_or(ProgramError::InsufficientFunds)?;
362    let source_balance = program_owned_source_account.try_lamports()?;
363    if source_balance < amount {
364        msg!(
365            "Error: Not enough funds to withdraw {} lamports from {}",
366            amount,
367            program_owned_source_account.key
368        );
369        return Err(ProgramError::InsufficientFunds);
370    }
371    **program_owned_source_account.try_borrow_mut_lamports()? = source_balance
372        .checked_sub(amount)
373        .ok_or(ProgramError::InsufficientFunds)?;
374
375    Ok(())
376}
377
378pub fn transfer_sol<'a, 'b>(
379    source_account: &'a AccountInfo<'b>,
380    destination_account: &'a AccountInfo<'b>,
381    amount: u64,
382) -> ProgramResult {
383    if source_account.try_lamports()? < amount {
384        msg!(
385            "Error: Not enough funds to withdraw {} lamports from {}",
386            amount,
387            source_account.key
388        );
389        return Err(ProgramError::InsufficientFunds);
390    }
391    invoke(
392        &system_instruction::transfer(source_account.key, destination_account.key, amount),
393        &[source_account.clone(), destination_account.clone()],
394    )
395}
396
397pub fn transfer_tokens<'a, 'b>(
398    source_account: &'a AccountInfo<'b>,
399    destination_account: &'a AccountInfo<'b>,
400    authority_account: &'a AccountInfo<'b>,
401    amount: u64,
402) -> ProgramResult {
403    invoke(
404        &spl_token::instruction::transfer(
405            &spl_token::id(),
406            source_account.key,
407            destination_account.key,
408            authority_account.key,
409            &[],
410            amount,
411        )?,
412        &[
413            source_account.clone(),
414            destination_account.clone(),
415            authority_account.clone(),
416        ],
417    )
418}
419
420pub fn burn_tokens<'a, 'b>(
421    from_token_account: &'a AccountInfo<'b>,
422    mint_account: &'a AccountInfo<'b>,
423    authority_account: &'a AccountInfo<'b>,
424    amount: u64,
425) -> ProgramResult {
426    invoke(
427        &spl_token::instruction::burn(
428            &spl_token::id(),
429            from_token_account.key,
430            mint_account.key,
431            authority_account.key,
432            &[],
433            amount,
434        )?,
435        &[
436            from_token_account.clone(),
437            mint_account.clone(),
438            authority_account.clone(),
439        ],
440    )
441}
442
443pub fn approve_delegate<'a, 'b>(
444    source_account: &'a AccountInfo<'b>,
445    delegate_account: &'a AccountInfo<'b>,
446    authority_account: &'a AccountInfo<'b>,
447    amount: u64,
448) -> ProgramResult {
449    invoke(
450        &spl_token::instruction::approve(
451            &spl_token::id(),
452            source_account.key,
453            delegate_account.key,
454            authority_account.key,
455            &[],
456            amount,
457        )?,
458        &[
459            source_account.clone(),
460            delegate_account.clone(),
461            authority_account.clone(),
462        ],
463    )
464}
465
466pub fn revoke_delegate<'a, 'b>(
467    source_account: &'a AccountInfo<'b>,
468    authority_account: &'a AccountInfo<'b>,
469) -> ProgramResult {
470    invoke(
471        &spl_token::instruction::revoke(
472            &spl_token::id(),
473            source_account.key,
474            authority_account.key,
475            &[],
476        )?,
477        &[source_account.clone(), authority_account.clone()],
478    )
479}
480
481pub fn init_system_account<'a, 'b>(
482    funding_account: &'a AccountInfo<'b>,
483    target_account: &'a AccountInfo<'b>,
484    owner_key: &Pubkey,
485    seed: &str,
486    data_size: usize,
487) -> ProgramResult {
488    if exists(target_account)? {
489        if target_account.owner != owner_key {
490            return Err(ProgramError::IllegalOwner);
491        }
492        if target_account.data_len() != data_size {
493            return Err(ProgramError::InvalidAccountData);
494        }
495        return Ok(());
496    }
497
498    let derived_account = Pubkey::create_with_seed(funding_account.key, seed, owner_key)?;
499    if target_account.key != &derived_account {
500        return Err(ProgramError::InvalidSeeds);
501    }
502
503    let min_balance = sysvar::rent::Rent::get()
504        .unwrap()
505        .minimum_balance(data_size);
506    invoke(
507        &system_instruction::create_account_with_seed(
508            funding_account.key,
509            target_account.key,
510            funding_account.key,
511            seed,
512            min_balance,
513            data_size as u64,
514            owner_key,
515        ),
516        &[funding_account.clone(), target_account.clone()],
517    )
518}
519
520pub fn close_system_account<'a, 'b>(
521    receiving_account: &'a AccountInfo<'b>,
522    target_account: &'a AccountInfo<'b>,
523    authority_account: &Pubkey,
524) -> ProgramResult {
525    if *target_account.owner != *authority_account {
526        return Err(ProgramError::IllegalOwner);
527    }
528    let cur_balance = target_account.try_lamports()?;
529    transfer_sol_from_owned(target_account, receiving_account, cur_balance)?;
530
531    if target_account.data_len() > 2000 {
532        target_account.try_borrow_mut_data()?[..2000].fill(0);
533    } else {
534        target_account.try_borrow_mut_data()?.fill(0);
535    }
536
537    Ok(())
538}
539
540pub fn get_oracle_price(
541    oracle_type: OracleType,
542    oracle_account: &AccountInfo,
543    max_price_error: f64,
544    max_price_age_sec: u64,
545) -> Result<OraclePrice, ProgramError> {
546    match oracle_type {
547        OracleType::Pyth => get_pyth_price(oracle_account, max_price_error, max_price_age_sec),
548        _ => Err(ProgramError::UnsupportedSysvar),
549    }
550}
551
552pub fn get_pyth_price(
553    pyth_price_info: &AccountInfo,
554    max_price_error: f64,
555    max_price_age_sec: u64,
556) -> Result<OraclePrice, ProgramError> {
557    if is_empty(pyth_price_info)? {
558        msg!("Error: Invalid Pyth oracle account");
559        return Err(FarmError::OracleInvalidAccount.into());
560    }
561
562    let pyth_price_data = &pyth_price_info.try_borrow_data()?;
563    let pyth_price = pyth_client::load_price(pyth_price_data)?;
564
565    if !matches!(pyth_price.agg.status, PriceStatus::Trading)
566        || !matches!(pyth_price.ptype, PriceType::Price)
567    {
568        msg!("Error: Pyth oracle price has invalid state");
569        return Err(FarmError::OracleInvalidState.into());
570    }
571
572    let last_update_age_sec = math::checked_mul(
573        math::checked_sub(clock::get_slot()?, pyth_price.valid_slot)?,
574        solana_program::clock::DEFAULT_MS_PER_SLOT,
575    )? / 1000;
576    if last_update_age_sec > max_price_age_sec {
577        msg!("Error: Pyth oracle price is stale");
578        return Err(FarmError::OracleStalePrice.into());
579    }
580
581    if pyth_price.agg.price <= 0
582        || pyth_price.agg.conf as f64 / pyth_price.agg.price as f64 > max_price_error
583    {
584        msg!("Error: Pyth oracle price is out of bounds");
585        return Err(FarmError::OracleInvalidPrice.into());
586    }
587
588    Ok(OraclePrice {
589        // price is i64 and > 0 per check above
590        price: pyth_price.agg.price as u64,
591        exponent: pyth_price.expo,
592    })
593}
594
595// Converts token amount to USD using price oracle
596pub fn get_asset_value_usd(
597    amount: u64,
598    decimals: u8,
599    oracle_type: OracleType,
600    oracle_account: &AccountInfo,
601    max_price_error: f64,
602    max_price_age_sec: u64,
603) -> Result<f64, ProgramError> {
604    if amount == 0 {
605        return Ok(0.0);
606    }
607    let oracle_price = get_oracle_price(
608        oracle_type,
609        oracle_account,
610        max_price_error,
611        max_price_age_sec,
612    )?;
613
614    Ok(amount as f64 * oracle_price.price as f64
615        / math::checked_powi(10.0, decimals as i32 - oracle_price.exponent)?)
616}
617
618// Converts USD amount to tokens using price oracle
619pub fn get_asset_value_tokens(
620    usd_amount: f64,
621    token_decimals: u8,
622    oracle_type: OracleType,
623    oracle_account: &AccountInfo,
624    max_price_error: f64,
625    max_price_age_sec: u64,
626) -> Result<u64, ProgramError> {
627    if usd_amount == 0.0 {
628        return Ok(0);
629    }
630    let oracle_price = get_oracle_price(
631        oracle_type,
632        oracle_account,
633        max_price_error,
634        max_price_age_sec,
635    )?;
636
637    // oracle_price.price guaranteed > 0
638    math::checked_as_u64(
639        usd_amount as f64 / oracle_price.price as f64
640            * math::checked_powi(10.0, token_decimals as i32 - oracle_price.exponent)?,
641    )
642}
643
644pub fn unpack<T: Packed>(account: &AccountInfo, name: &str) -> Result<T, ProgramError> {
645    if let Ok(object) = T::unpack(&account.try_borrow_data()?) {
646        Ok(object)
647    } else {
648        msg!("Error: Failed to load {} metadata", name);
649        Err(ProgramError::InvalidAccountData)
650    }
651}
652
653#[cfg(test)]
654mod tests {
655    use super::*;
656    use spl_token::state::{Account, Mint};
657
658    #[test]
659    fn test_mint_supply_offset() {
660        let mint = Mint {
661            supply: 1234567891011,
662            ..Mint::default()
663        };
664        let mut packed: [u8; 82] = [0; 82];
665        Mint::pack(mint, &mut packed).unwrap();
666
667        let supply = array_ref![packed, 36, 8];
668        assert_eq!(1234567891011, u64::from_le_bytes(*supply));
669    }
670
671    #[test]
672    fn test_mint_decimals_offset() {
673        let mint = Mint {
674            decimals: 123,
675            ..Mint::default()
676        };
677        let mut packed: [u8; 82] = [0; 82];
678        Mint::pack(mint, &mut packed).unwrap();
679
680        let decimals = array_ref![packed, 44, 1];
681        assert_eq!(123, decimals[0]);
682    }
683
684    #[test]
685    fn test_account_amount_offset() {
686        let account = Account {
687            amount: 1234567891011,
688            ..Account::default()
689        };
690        let mut packed: [u8; 165] = [0; 165];
691        Account::pack(account, &mut packed).unwrap();
692
693        let amount = array_ref![packed, 64, 8];
694        assert_eq!(1234567891011, u64::from_le_bytes(*amount));
695    }
696}