Skip to content

Commit

Permalink
enforce forbidden addresse via hardcoded blacklist (#87)
Browse files Browse the repository at this point in the history
* enforce forbidden addresse via hardcoded blacklist

* fix typo

* formatting

* one day I'll rid this world of the cancer that is prettiers

* allow specifying evm secret in testing

* fix tests by upping compute units for variance in ata creation

* enough is enough

* use pubkey! macro to reduce compute unit cost

* forbidden key test... kind of
  • Loading branch information
nonergodic authored Apr 5, 2024
1 parent b3cdc32 commit 450f18a
Show file tree
Hide file tree
Showing 11 changed files with 217 additions and 43 deletions.
1 change: 0 additions & 1 deletion .github/workflows/token-dispenser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ jobs:
with:
profile: minimal
toolchain: 1.66.1
components: rustfmt, clippy
override: true
- name: Install Solana
run: |
Expand Down
15 changes: 0 additions & 15 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions token-dispenser/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions token-dispenser/programs/token-dispenser/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
87 changes: 86 additions & 1 deletion token-dispenser/programs/token-dispenser/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
#![allow(clippy::result_large_err)]

use {
anchor_lang::{
prelude::*,
solana_program::{
pubkey,
keccak::hashv,
program::{
invoke,
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -354,6 +438,7 @@ pub enum ErrorCode {
SignatureVerificationWrongSigner,
UnauthorizedCosmosChainId,
TransferExceedsMax,
Forbidden,
}

pub fn check_claim_receipt_is_uninitialized(claim_receipt_account: &AccountInfo) -> Result<()> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -128,10 +129,24 @@ impl DispenserSimulator {
pub fn generate_test_claim_certs(
claimant: &Pubkey,
dispenser_guard: &Keypair,
use_forbidden_evm: bool,
) -> Vec<TestClaimCertificate> {
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),
Expand Down Expand Up @@ -303,6 +318,7 @@ impl DispenserSimulator {
claimants: Vec<Keypair>,
dispenser_guard: &Keypair,
max_transfer_override: Option<u64>,
use_forbidden_evm: bool,
) -> Result<
(
MerkleTree<SolanaHasher>,
Expand All @@ -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::<u64>();
(c, test_claim_certs, amount)
})
Expand Down
83 changes: 81 additions & 2 deletions token-dispenser/programs/token-dispenser/src/tests/test_claim.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -56,6 +58,7 @@ pub async fn test_claim_fails_with_wrong_accounts() {
],
&dispenser_guard,
None,
false
)
.await
.unwrap();
Expand Down Expand Up @@ -189,6 +192,7 @@ pub async fn test_claim_fails_with_insufficient_funds() {
],
&dispenser_guard,
None,
false,
)
.await
.unwrap();
Expand Down Expand Up @@ -325,6 +329,7 @@ pub async fn test_claim_fails_if_delegate_revoked() {
],
&dispenser_guard,
None,
false,
)
.await
.unwrap();
Expand Down Expand Up @@ -435,6 +440,7 @@ pub async fn test_claim_fails_with_wrong_merkle_proof() {
],
&dispenser_guard,
None,
false,
)
.await
.unwrap();
Expand Down Expand Up @@ -480,6 +486,7 @@ pub async fn test_claim_fails_with_wrong_receipt_pubkey() {
],
&dispenser_guard,
None,
false,
)
.await
.unwrap();
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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(
// &<TestClaimCertificate as Into<ClaimInfo>>::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());
// }
// _ => {}
// }
// }
// }
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,18 @@ impl TestClaimCertificate {
rand::thread_rng().gen::<u64>() % 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::<EvmPrefixedMessage, Keccak256>::from_secret(
claimant,
secret,
),
),
}
}

pub fn random_evm(claimant: &Pubkey) -> Self {
Self {
amount: Self::random_amount(),
Expand Down Expand Up @@ -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<ClaimInfo> = mock_offchain_certificates
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ClaimInfo> = mock_offchain_certificates
.iter()
Expand Down
Loading

0 comments on commit 450f18a

Please sign in to comment.