-
-
Notifications
You must be signed in to change notification settings - Fork 198
security: cross-chain airdrop eligibility uses hardcoded mock balances #2127
Description
Cross-chain airdrop chain adapters return hardcoded mock balances — eligibility checks bypassed
Summary
The SolanaAdapter and BaseAdapter in cross-chain-airdrop/src/chain_adapter.rs return hardcoded constant values from get_balance() and get_wallet_age(), ignoring the wallet address parameter entirely. This means every wallet that passes address format validation is treated as having sufficient balance and age to be eligible for an airdrop claim.
The code contains commented-out RPC call skeletons but the live implementations are:
// SolanaAdapter
async fn get_balance(&self, _address: &str) -> Result<u64> {
Ok(200_000_000) // 0.2 SOL mock — always passes 0.1 SOL minimum
}
async fn get_wallet_age(&self, _address: &str) -> Result<u64> {
Ok(10 * 24 * 60 * 60) // 10 days mock — always passes 7-day minimum
}
// BaseAdapter
async fn get_balance(&self, _address: &str) -> Result<u64> {
Ok(20_000_000_000_000_000) // 0.02 ETH mock — always passes 0.01 ETH minimum
}
async fn get_wallet_age(&self, _address: &str) -> Result<u64> {
Ok(14 * 24 * 60 * 60) // 14 days mock — always passes 7-day minimum
}Affected Code
- File:
cross-chain-airdrop/src/chain_adapter.rs - Methods:
SolanaAdapter::get_balance(),SolanaAdapter::get_wallet_age(),BaseAdapter::get_balance(),BaseAdapter::get_wallet_age() - Call path:
ChainAdapter::verify_wallet()→get_balance()+get_wallet_age()→WalletVerification { meets_minimum_balance: true, meets_age_requirement: true }
Impact
Any syntactically valid wallet address (passing validate_address()) is treated as eligible regardless of actual on-chain balance or age. The VerificationPipeline::check_eligibility() and process_claim() flows will report any wallet as meeting balance and age requirements.
The Python server implementation (node/airdrop_v2.py) makes real RPC calls and is not affected by this specific issue. However, the Rust crate is independently usable as both a library and CLI (airdrop-cli), and its verification is entirely mocked.
Reproduction
- Run
cargo run -- verify-address --chain solana --address 7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU - Observe: "Balance: 0.200000000 SOL", "Wallet age: 10 days", "Meets minimum balance: true"
- Run with any other valid Solana address — same result
- Run with any valid Base address (e.g.
0x1234567890123456789012345678901234567890) — same result: "Balance: 0.020000000000000000 ETH", "Wallet age: 14 days"
No actual RPC calls are made. The balance and age values are hardcoded constants.
Distinction from Dedup-on-Restart Issue
This is a separate vulnerability from the previously addressed dedup-on-restart issue (ClaimStore persistence). That issue was about duplicate prevention state being lost after restart. This issue is about eligibility verification being entirely fake — the wallet balance and age checks never execute against real chain data. They are orthogonal: one defeats deduplication, the other defeats eligibility verification.
Fix
Replace the hardcoded mock returns with actual JSON-RPC calls using the existing reqwest dependency (already in Cargo.toml):
- Solana balance:
POSTto RPC URL with{"method": "getBalance", "params": [address]}, parseresult.value - Base balance:
POSTto RPC URL with{"method": "eth_getBalance", "params": [address, "latest"]}, parse hexresult - Wallet age: Return
0as a conservative default (age determination requires historical transaction data viagetSignaturesForAddress/ Basescan, which is significantly more complex). This means wallets fail the age gate unless the policy level disables it — safer than returning a mock that always passes.
Files Changed
cross-chain-airdrop/src/chain_adapter.rs— replaced 4 mock methods with real RPC calls (Solana/Base balance) + conservative age defaults
Tests
- 6 new unit tests using mockito HTTP mocking:
test_solana_get_balance_from_rpc— verifies correct parsing of Solana RPC responsetest_solana_get_balance_zero_on_missing— verifies zero balance on empty accounttest_solana_get_wallet_age_returns_zero— verifies conservative age defaulttest_base_get_balance_from_rpc— verifies correct parsing of eth_getBalance hex responsetest_base_get_balance_zero_on_empty— verifies zero balance on empty accounttest_base_get_wallet_age_returns_zero— verifies conservative age default
- All 49 tests pass with
--features sqlite-store(33 unit + 3 CLI + 12 integration + 1 doc-test) - All 46 tests pass without the feature (backward compatible)
Compatibility
- Behavioral change: Wallets with zero or low balance will now correctly fail eligibility checks (previously all wallets passed). This is the intended behavior.
- Wallet age gate: Now returns 0 (conservative). Wallets will fail the age requirement unless the policy minimum is set to 0. This is safer than the previous mock that always passed.
- No API breakage: All public signatures unchanged.
ChainAdaptertrait interface identical. - No new dependencies: Uses existing
reqwest(already required for GitHub verification and bridge client).