diff --git a/.github/workflows/token-dispenser.yml b/.github/workflows/token-dispenser.yml index cbd4b658..1cc8d42b 100644 --- a/.github/workflows/token-dispenser.yml +++ b/.github/workflows/token-dispenser.yml @@ -14,7 +14,6 @@ jobs: with: profile: minimal toolchain: 1.66.1 - components: rustfmt, clippy override: true - name: Install Solana run: | diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 25126c5e..f93d2e18 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,21 +10,6 @@ repos: frontend/public/orb.png | frontend/package-lock.json )$ - - repo: local - hooks: - # Hooks for the token dispenser - - id: cargo-fmt-token-dispenser - name: Cargo format for token dispenser - language: "rust" - entry: cargo +nightly-2023-07-23 fmt --manifest-path ./token-dispenser/Cargo.toml --all -- --config-path ./token-dispenser/rustfmt.toml - pass_filenames: false - files: token-dispenser - - id: cargo-clippy-token-dispenser - name: Cargo clippy for token dispenser - language: "rust" - entry: cargo +nightly-2023-07-23 clippy --manifest-path ./token-dispenser/Cargo.toml --tests --fix --allow-dirty --allow-staged -- -D warnings - pass_filenames: false - files: token-dispenser - repo: https://github.com/pre-commit/mirrors-prettier rev: "v2.7.1" hooks: diff --git a/token-dispenser/Cargo.lock b/token-dispenser/Cargo.lock index 3f35ce1e..00cb93e0 100644 --- a/token-dispenser/Cargo.lock +++ b/token-dispenser/Cargo.lock @@ -4581,6 +4581,7 @@ dependencies = [ "serde_json", "sha3 0.10.8", "solana-address-lookup-table-program", + "solana-program", "solana-program-test", "solana-sdk", "spl-associated-token-account", diff --git a/token-dispenser/programs/token-dispenser/Cargo.toml b/token-dispenser/programs/token-dispenser/Cargo.toml index 2aab576a..63d41dce 100644 --- a/token-dispenser/programs/token-dispenser/Cargo.toml +++ b/token-dispenser/programs/token-dispenser/Cargo.toml @@ -28,6 +28,7 @@ serde = "1.0.166" serde_json = "1.0.99" sha3 = "0.10.8" residua-uleb128 = "0.2.0" +solana-program = "1.14.16" solana-address-lookup-table-program = "1.14.16" [dev-dependencies] diff --git a/token-dispenser/programs/token-dispenser/src/lib.rs b/token-dispenser/programs/token-dispenser/src/lib.rs index 3cdbc97d..85678151 100644 --- a/token-dispenser/programs/token-dispenser/src/lib.rs +++ b/token-dispenser/programs/token-dispenser/src/lib.rs @@ -1,9 +1,9 @@ #![allow(clippy::result_large_err)] - use { anchor_lang::{ prelude::*, solana_program::{ + pubkey, keccak::hashv, program::{ invoke, @@ -73,6 +73,70 @@ mod tests; mod ecosystems; +//butt ugly but straightforward +const FORBIDDEN_SOL: &[Pubkey] = &[ + pubkey!("5XiqTJQBTZKcGjcbCydZvf9NzhE2R3g7GDx1yKHxs8jd"), + pubkey!("74YpKKAScQky4YouDXMfnGnXFbUQcccp958B8R8eQrvV"), + pubkey!("Esmx2QjmDZMjJ15yBJ2nhqisjEt7Gqro4jSkofdoVsvY"), + pubkey!("ALDxR5NXJLruoRNQDk88AiF9FXyTN3iQ9E8NQB73zSoh"), + pubkey!("8ggviFegLUzsddm9ShyMy42TiDYyH9yDDS3gSGdejND7"), + pubkey!("4nBEtJKz99WJKqNYmdmpogayqcvXBQ2PxrkwgjYENhjt"), + pubkey!("G3udanrxk8stVe8Se2zXmJ3QwU8GSFJMn28mTfn8t1kq"), + pubkey!("AgJddDJLt17nHyXDCpyGELxwsZZQPqfUsuwzoiqVGJwD"), + pubkey!("CxegPrfn2ge5dNiQberUrQJkHCcimeR4VXkeawcFBBka"), + pubkey!("HuiYfmAceFkmhu3yP8t3a6VMYfw3VSX2Ymqqj9M2k9ib"), + pubkey!("B9UAHnGTS31u3vpaTM79eyQowMMjYP3uzn6XQucAYRv7"), + pubkey!("3SEn2DertMoxBEq1MY4Fg27LsQmkdFGQH4yzmEGfsS6e"), + pubkey!("6D7fgzpPZXtDB6Zqg3xRwfbohzerbytB2U5pFchnVuzw"), + pubkey!("76w4SBe2of2wWUsx2FjkkwD29rRznfvEkBa1upSbTAWH"), + pubkey!("61wJT43nWMUpDR92wC7pmo6xoJRh2s4kCYRBq4d5XQHZ"), + pubkey!("6sEk1enayZBGFyNvvJMTP7qs5S3uC7KLrQWaEk38hSHH"), +]; + +const FORBIDDEN_EVM: &[[u8; EvmPubkey::LEN]] = &[ + [ //0x748e1932a18dc7adce63ab7e8e705004128402fd + 0x74, 0x8e, 0x19, 0x32, 0xa1, 0x8d, 0xc7, 0xad, 0xce, 0x63, + 0xab, 0x7e, 0x8e, 0x70, 0x50, 0x04, 0x12, 0x84, 0x02, 0xfd, + ], + [ //0x2fc617e933a52713247ce25730f6695920b3befe + 0x2f, 0xc6, 0x17, 0xe9, 0x33, 0xa5, 0x27, 0x13, 0x24, 0x7c, + 0xe2, 0x57, 0x30, 0xf6, 0x69, 0x59, 0x20, 0xb3, 0xbe, 0xfe, + ], +]; + +// -- Keys used for testing -- +// +//I'd have loved to use #[cfg(test)] but when running `cargo test-bpf` as we +// are for testing, it first compiles the program without it and so the +// real addresses end up in the program that the local test validator +// tests against. I couldn't find any doc on whether test-bpf sets some +// other feature that I could use to conditionally compile the real keys +// into the program, and so here we are. Another piece of garbage in the +// landfill. +//So to run the forbidden key tests, comment out the real keys and uncomment +// the forbidden keys along with the test at the bottom of test_claims (you +// can also find the private keys for our two test wallets there). +// +// const FORBIDDEN_SOL: &[Pubkey] = &[ +// pubkey!("UnsedTest1111111111111111111111111111111111"), +// pubkey!("bW5NFQvLwCKo826zr8m8DXHtUYkCF6Nn53QoKXLmW7b"), +// pubkey!("UnsedTest1111111111111111111111111111111112"), +// ]; +// const FORBIDDEN_EVM: &[[u8; EvmPubkey::LEN]] = &[ +// [ //0x7e577e577e577e577e577e577e577e577e577e57 +// 0x7e, 0x57, 0x7e, 0x57, 0x7e, 0x57, 0x7e, 0x57, 0x7e, 0x57, +// 0x7e, 0x57, 0x7e, 0x57, 0x7e, 0x57, 0x7e, 0x57, 0x7e, 0x57, +// ], +// [ //0xd3E739d874789CB4545dD745eb391BE54A5505e2 +// 0xd3, 0xe7, 0x39, 0xd8, 0x74, 0x78, 0x9c, 0xb4, 0x54, 0x5d, +// 0xd7, 0x45, 0xeb, 0x39, 0x1b, 0xe5, 0x4a, 0x55, 0x05, 0xe2, +// ], +// [ //0x00007e577e577e577e577e577e577e577e570000 +// 0x00, 0x00, 0x7e, 0x57, 0x7e, 0x57, 0x7e, 0x57, 0x7e, 0x57, +// 0x7e, 0x57, 0x7e, 0x57, 0x7e, 0x57, 0x7e, 0x57, 0x00, 0x00, +// ], +// ]; + declare_id!("Wapq3Hpv2aSKjWrh4pM8eweh8jVJB7D1nLBw9ikjVYx"); const CONFIG_SEED: &[u8] = b"config"; @@ -120,6 +184,26 @@ pub mod token_dispenser { let treasury = &mut ctx.accounts.treasury; let claimant_fund = &ctx.accounts.claimant_fund; + match claim_certificate.proof_of_identity { + IdentityCertificate::Solana => { + let claimant_key = ctx.accounts.claimant.key; + require!( + !FORBIDDEN_SOL + .iter() + .any(|&key| *claimant_key == key), + ErrorCode::Forbidden + ); + } + IdentityCertificate::Evm { pubkey, .. } => { + let pubkey_bytes = &pubkey.as_bytes(); + require!( + !FORBIDDEN_EVM.iter().any(|addr| *pubkey_bytes == *addr), + ErrorCode::Forbidden + ); + } + _ => {} + } + // Check that the identity corresponding to the leaf has authorized the claimant let claim_info = claim_certificate.checked_into_claim_info( &ctx.accounts.sysvar_instruction, @@ -354,6 +438,7 @@ pub enum ErrorCode { SignatureVerificationWrongSigner, UnauthorizedCosmosChainId, TransferExceedsMax, + Forbidden, } pub fn check_claim_receipt_is_uninitialized(claim_receipt_account: &AccountInfo) -> Result<()> { diff --git a/token-dispenser/programs/token-dispenser/src/tests/dispenser_simulator.rs b/token-dispenser/programs/token-dispenser/src/tests/dispenser_simulator.rs index f2b41a80..67587b8d 100644 --- a/token-dispenser/programs/token-dispenser/src/tests/dispenser_simulator.rs +++ b/token-dispenser/programs/token-dispenser/src/tests/dispenser_simulator.rs @@ -90,7 +90,8 @@ pub struct DispenserSimulator { impl DispenserSimulator { pub async fn new() -> Self { - let program_test = ProgramTest::new("token_dispenser", crate::id(), None); + let mut program_test = ProgramTest::new("token_dispenser", crate::id(), None); + program_test.set_compute_max_units(400000); let (banks_client, genesis_keypair, recent_blockhash) = program_test.start().await; let mint_keypair = Keypair::new(); let pyth_mint_authority = Keypair::new(); @@ -128,10 +129,24 @@ impl DispenserSimulator { pub fn generate_test_claim_certs( claimant: &Pubkey, dispenser_guard: &Keypair, + use_forbidden_evm: bool, ) -> Vec { let keypair = ed25519_dalek::Keypair::from_bytes(&dispenser_guard.to_bytes()).unwrap(); vec![ - TestClaimCertificate::random_evm(claimant), + if use_forbidden_evm { + TestClaimCertificate::secret_evm( + claimant, + libsecp256k1::SecretKey::parse(&[ + //secret key of 0xd3E739d874789CB4545dD745eb391BE54A5505e2 + 0x37, 0x06, 0x41, 0xae, 0xc0, 0xcb, 0x42, 0x2f, + 0x36, 0x5d, 0x33, 0xe6, 0xc6, 0x1a, 0xdd, 0x34, + 0x3b, 0xa1, 0x55, 0x7e, 0xcb, 0xe2, 0x86, 0x17, + 0x8e, 0xa0, 0xb0, 0x05, 0x34, 0x87, 0x40, 0x04 + ]).unwrap() + ) + } else { + TestClaimCertificate::random_evm(claimant) + }, TestClaimCertificate::random_cosmos(claimant), TestClaimCertificate::random_discord(claimant, &keypair), TestClaimCertificate::random_aptos(claimant), @@ -303,6 +318,7 @@ impl DispenserSimulator { claimants: Vec, dispenser_guard: &Keypair, max_transfer_override: Option, + use_forbidden_evm: bool, ) -> Result< ( MerkleTree, @@ -318,8 +334,11 @@ impl DispenserSimulator { .into_iter() .map(|c| { let pubkey = c.pubkey(); - let test_claim_certs = - DispenserSimulator::generate_test_claim_certs(&pubkey, dispenser_guard); + let test_claim_certs = DispenserSimulator::generate_test_claim_certs( + &pubkey, + dispenser_guard, + use_forbidden_evm + ); let amount = test_claim_certs.iter().map(|y| y.amount).sum::(); (c, test_claim_certs, amount) }) diff --git a/token-dispenser/programs/token-dispenser/src/tests/test_claim.rs b/token-dispenser/programs/token-dispenser/src/tests/test_claim.rs index 15d11872..d1265cd9 100644 --- a/token-dispenser/programs/token-dispenser/src/tests/test_claim.rs +++ b/token-dispenser/programs/token-dispenser/src/tests/test_claim.rs @@ -40,6 +40,8 @@ use { }, }; +#[allow(unused_imports)] //used by commented out test at the bottom +use crate::tests::test_happy_path::TestIdentityCertificate; #[tokio::test] pub async fn test_claim_fails_with_wrong_accounts() { @@ -56,6 +58,7 @@ pub async fn test_claim_fails_with_wrong_accounts() { ], &dispenser_guard, None, + false ) .await .unwrap(); @@ -189,6 +192,7 @@ pub async fn test_claim_fails_with_insufficient_funds() { ], &dispenser_guard, None, + false, ) .await .unwrap(); @@ -325,6 +329,7 @@ pub async fn test_claim_fails_if_delegate_revoked() { ], &dispenser_guard, None, + false, ) .await .unwrap(); @@ -435,6 +440,7 @@ pub async fn test_claim_fails_with_wrong_merkle_proof() { ], &dispenser_guard, None, + false, ) .await .unwrap(); @@ -480,6 +486,7 @@ pub async fn test_claim_fails_with_wrong_receipt_pubkey() { ], &dispenser_guard, None, + false, ) .await .unwrap(); @@ -515,7 +522,14 @@ pub async fn test_claim_works_if_receipt_has_balance() { let claimant_1 = Keypair::new(); let (merkle_tree, mock_offchain_certificates_and_claimants) = simulator - .initialize_with_claimants(vec![copy_keypair(&claimant_1)], &dispenser_guard, None) + .initialize_with_claimants( + vec![ + copy_keypair(&claimant_1) + ], + &dispenser_guard, + None, + false, + ) .await .unwrap(); @@ -586,7 +600,12 @@ pub async fn test_claim_fails_if_exceeds_max_transfer() { let claimant = Keypair::new(); let (merkle_tree, mock_offchain_certificates_and_claimants) = simulator - .initialize_with_claimants(vec![copy_keypair(&claimant)], &dispenser_guard, Some(0)) + .initialize_with_claimants( + vec![copy_keypair(&claimant)], + &dispenser_guard, + Some(0), + false, + ) .await .unwrap(); @@ -620,3 +639,63 @@ pub async fn test_claim_fails_if_exceeds_max_transfer() { assert!(simulator.get_account(receipt_pda).await.is_none()); } } + + +//comment this in and switch the forbidden addresses in lib.rs to the test addresses +// to test this. Also, see the comment there for additional information +// #[tokio::test] +// pub async fn test_claim_fails_if_using_forbidden_wallet() { +// let dispenser_guard: Keypair = Keypair::new(); + +// let mut simulator = DispenserSimulator::new().await; + +// //pubkey: bW5NFQvLwCKo826zr8m8DXHtUYkCF6Nn53QoKXLmW7b +// let claimant = Keypair::from_base58_string( +// "4qwZKb6x1bW8v4zoJ5AACT3qaYfs9TzsEXJ7nHWPA1dZHc13pYwMuT61ZrJVtq5C2LY4z6udpYmHnPVNm3PgKwbP" +// ); + +// let (merkle_tree, mock_offchain_certificates_and_claimants) = simulator +// .initialize_with_claimants( +// vec![copy_keypair(&claimant)], +// &dispenser_guard, +// None, +// true, +// ) +// .await +// .unwrap(); + +// let (_, offchain_claim_certificates, _) = &mock_offchain_certificates_and_claimants[0]; +// for offchain_claim_certificate in offchain_claim_certificates { +// match offchain_claim_certificate.off_chain_proof_of_identity { +// TestIdentityCertificate::Solana(..) | TestIdentityCertificate::Evm(..) => { +// let receipt_pda = get_receipt_pda( +// &>::into(offchain_claim_certificate.clone()) +// .try_to_vec() +// .unwrap(), +// ) +// .0; +// assert!(simulator.get_account(receipt_pda).await.is_none()); + +// let ix_index_error = offchain_claim_certificate.as_instruction_error_index(&merkle_tree); +// assert_eq!( +// simulator +// .claim( +// &claimant, +// offchain_claim_certificate, +// &merkle_tree, +// None, +// None, +// None +// ) +// .await +// .unwrap_err() +// .unwrap(), +// ErrorCode::Forbidden.into_transaction_error(ix_index_error) +// ); + +// assert!(simulator.get_account(receipt_pda).await.is_none()); +// } +// _ => {} +// } +// } +// } diff --git a/token-dispenser/programs/token-dispenser/src/tests/test_happy_path.rs b/token-dispenser/programs/token-dispenser/src/tests/test_happy_path.rs index 45725094..bdd96f85 100644 --- a/token-dispenser/programs/token-dispenser/src/tests/test_happy_path.rs +++ b/token-dispenser/programs/token-dispenser/src/tests/test_happy_path.rs @@ -83,6 +83,18 @@ impl TestClaimCertificate { rand::thread_rng().gen::() % MAX_AMOUNT } + pub fn secret_evm(claimant: &Pubkey, secret: libsecp256k1::SecretKey) -> Self { + Self { + amount: Self::random_amount(), + off_chain_proof_of_identity: TestIdentityCertificate::Evm( + Secp256k1TestIdentityCertificate::::from_secret( + claimant, + secret, + ), + ), + } + } + pub fn random_evm(claimant: &Pubkey) -> Self { Self { amount: Self::random_amount(), @@ -248,6 +260,7 @@ pub async fn test_happy_path() { let mock_offchain_certificates = DispenserSimulator::generate_test_claim_certs( &simulator.genesis_keypair.pubkey(), &dispenser_guard, + false, ); let merkle_items: Vec = mock_offchain_certificates diff --git a/token-dispenser/programs/token-dispenser/src/tests/test_initialize.rs b/token-dispenser/programs/token-dispenser/src/tests/test_initialize.rs index 939f1497..f7e4ea16 100644 --- a/token-dispenser/programs/token-dispenser/src/tests/test_initialize.rs +++ b/token-dispenser/programs/token-dispenser/src/tests/test_initialize.rs @@ -24,7 +24,7 @@ pub async fn test_initialize_fails_with_incorrect_accounts() { let claimant = simulator.genesis_keypair.pubkey(); let mock_offchain_certificates = - DispenserSimulator::generate_test_claim_certs(&claimant, &dispenser_guard); + DispenserSimulator::generate_test_claim_certs(&claimant, &dispenser_guard, false); let merkle_items: Vec = mock_offchain_certificates .iter() diff --git a/token-dispenser/programs/token-dispenser/src/tests/test_secp256k1.rs b/token-dispenser/programs/token-dispenser/src/tests/test_secp256k1.rs index 83ac44b9..8c70caed 100644 --- a/token-dispenser/programs/token-dispenser/src/tests/test_secp256k1.rs +++ b/token-dispenser/programs/token-dispenser/src/tests/test_secp256k1.rs @@ -124,6 +124,17 @@ impl Secp256k1TestIdentityCertificate { _hasher: PhantomData, } } + + pub fn from_secret(claimant: &Pubkey, secret: libsecp256k1::SecretKey) -> Self { + let message = EvmPrefixedMessage::from(get_expected_payload(claimant).as_str()); + let (signature, recovery_id) = libsecp256k1::sign(&Self::hash_message(&message), &secret); + Self { + message, + signature, + recovery_id, + _hasher: PhantomData, + } + } } #[tokio::test] diff --git a/token-dispenser/rustfmt.toml b/token-dispenser/rustfmt.toml deleted file mode 100644 index 793e0a9f..00000000 --- a/token-dispenser/rustfmt.toml +++ /dev/null @@ -1,19 +0,0 @@ -edition = "2021" - -# Merge all imports into a clean vertical list of module imports. -imports_granularity = "One" -group_imports = "One" -imports_layout = "Vertical" - -# Better grep-ability. -empty_item_single_line = false - -# Consistent pipe layout. -match_arm_leading_pipes = "Preserve" - -# Align Fields -enum_discrim_align_threshold = 80 -struct_field_align_threshold = 80 - -# Allow up to two blank lines for visual grouping. -blank_lines_upper_bound = 2