mpl_hydra/utils/validation/
mod.rs

1use crate::{
2    error::HydraError,
3    state::{Fanout, MembershipModel},
4};
5use anchor_lang::{
6    prelude::*,
7    solana_program::{instruction::Instruction, program_memory::sol_memcmp, pubkey::PUBKEY_BYTES},
8};
9use anchor_spl::token::TokenAccount;
10use mpl_token_metadata::state::{Metadata, TokenMetadataAccount};
11
12pub fn cmp_pubkeys(a: &Pubkey, b: &Pubkey) -> bool {
13    sol_memcmp(a.as_ref(), b.as_ref(), PUBKEY_BYTES) == 0
14}
15
16pub fn assert_derivation(
17    program_id: &Pubkey,
18    account: &AccountInfo,
19    path: &[&[u8]],
20    error: Option<error::Error>,
21) -> Result<u8> {
22    let (key, bump) = Pubkey::find_program_address(path, program_id);
23    if !cmp_pubkeys(&key, account.key) {
24        if let Some(err) = error {
25            msg!("Derivation {:?}", err);
26            return Err(err);
27        }
28        msg!("DerivedKeyInvalid");
29        return Err(HydraError::DerivedKeyInvalid.into());
30    }
31    Ok(bump)
32}
33
34pub fn assert_owned_by(account: &AccountInfo, owner: &Pubkey) -> Result<()> {
35    if !cmp_pubkeys(account.owner, owner) {
36        Err(HydraError::IncorrectOwner.into())
37    } else {
38        Ok(())
39    }
40}
41
42pub fn assert_membership_model(fanout: &Account<Fanout>, model: MembershipModel) -> Result<()> {
43    if fanout.membership_model != model {
44        return Err(HydraError::InvalidMembershipModel.into());
45    }
46    Ok(())
47}
48
49pub fn assert_ata(
50    account: &AccountInfo,
51    target: &Pubkey,
52    mint: &Pubkey,
53    err: Option<error::Error>,
54) -> Result<u8> {
55    assert_derivation(
56        &anchor_spl::associated_token::ID,
57        &account.to_account_info(),
58        &[
59            target.as_ref(),
60            anchor_spl::token::ID.as_ref(),
61            mint.as_ref(),
62        ],
63        err,
64    )
65}
66
67pub fn assert_shares_distributed(fanout: &Account<Fanout>) -> Result<()> {
68    if fanout.total_available_shares != 0 {
69        return Err(HydraError::SharesArentAtMax.into());
70    }
71    Ok(())
72}
73
74pub fn assert_holding(
75    owner: &AccountInfo,
76    token_account: &Account<TokenAccount>,
77    mint_info: &AccountInfo,
78) -> Result<()> {
79    assert_owned_by(mint_info, &spl_token::id())?;
80    let token_account_info = token_account.to_account_info();
81    assert_owned_by(&token_account_info, &spl_token::id())?;
82    if !cmp_pubkeys(&token_account.owner, owner.key) {
83        return Err(HydraError::IncorrectOwner.into());
84    }
85    if token_account.amount < 1 {
86        return Err(HydraError::WalletDoesNotOwnMembershipToken.into());
87    }
88    if !cmp_pubkeys(&token_account.mint, &mint_info.key()) {
89        return Err(HydraError::MintDoesNotMatch.into());
90    }
91    Ok(())
92}
93
94pub fn assert_distributed(
95    ix: Instruction,
96    subject: &Pubkey,
97    membership_model: MembershipModel,
98) -> Result<()> {
99    if !cmp_pubkeys(&ix.program_id, &crate::id()) {
100        return Err(HydraError::MustDistribute.into());
101    }
102    let instruction_id = match membership_model {
103        MembershipModel::Wallet => [252, 168, 167, 66, 40, 201, 182, 163],
104        MembershipModel::NFT => [108, 240, 68, 81, 144, 83, 58, 153],
105        MembershipModel::Token => [126, 105, 46, 135, 28, 36, 117, 212],
106    };
107    if sol_memcmp(instruction_id.as_ref(), ix.data[0..8].as_ref(), 8) != 0 {
108        return Err(HydraError::MustDistribute.into());
109    }
110    if !cmp_pubkeys(subject, &ix.accounts[1].pubkey) {
111        return Err(HydraError::MustDistribute.into());
112    }
113    Ok(())
114}
115
116pub fn assert_valid_metadata(
117    metadata_account: &AccountInfo,
118    mint: &AccountInfo,
119) -> Result<Metadata> {
120    let meta = Metadata::from_account_info(metadata_account)?;
121    if !cmp_pubkeys(&meta.mint, mint.key) {
122        return Err(HydraError::InvalidMetadata.into());
123    }
124    Ok(meta)
125}
126
127pub fn assert_owned_by_one(account: &AccountInfo, owners: Vec<&Pubkey>) -> Result<()> {
128    for o in owners {
129        let res = assert_owned_by(account, o);
130        if res.is_ok() {
131            return res;
132        }
133    }
134    Err(HydraError::IncorrectOwner.into())
135}
136
137#[cfg(test)]
138mod tests {
139    // Note this useful idiom: importing names from outer (for mod tests) scope.
140    use super::*;
141
142    #[test]
143    fn test_multi_owner_check() {
144        let owner = Pubkey::new_unique();
145        let owner1 = Pubkey::new_unique();
146        let owner2 = Pubkey::new_unique();
147        let ad = Pubkey::new_unique();
148        let actual_owner = Pubkey::new_unique();
149        let lam = &mut 10000;
150        let a = AccountInfo::new(&ad, false, false, lam, &mut [0; 0], &actual_owner, false, 0);
151
152        let e = assert_owned_by_one(&a, vec![&owner, &owner2, &owner1]);
153
154        assert!(e.is_err());
155
156        let e = assert_owned_by_one(&a, vec![&owner, &actual_owner, &owner1]);
157
158        assert!(e.is_ok());
159    }
160}