This project demonstrates common vulnerabilities when using cryptographic primitives like Merkle trees and digital signatures in smart contracts. It showcases two specific attack vectors:
- Merkle Replay Attack: Exploiting a flaw in an airdrop contract that uses Merkle trees
- Multisig Threshold Attack: Compromising a multisig wallet with a vulnerable threshold verification
The merkle_replay_attack
module demonstrates a vulnerability in airdrop contracts that use Merkle trees for verification but don't track which users have already claimed their tokens.
The Airdrop
contract verifies that a user is eligible for an airdrop by checking if their data (address and amount) is included in a Merkle tree. However, it lacks a mechanism to track which users have already claimed their tokens. This allows users to submit the same proof multiple times and claim the airdrop repeatedly.
In the test_replay_attack.cairo
test, we demonstrate:
- Alice legitimately claims her airdrop with a valid proof
- The attacker legitimately claims their airdrop with a valid proof
- The attacker then repeatedly claims their airdrop with the same proof, stealing tokens
To fix this vulnerability, the contract should maintain a record of addresses that have already claimed their tokens, for example:
#[storage]
struct Storage {
merkle_root: felt252,
claimed: Map<ContractAddress, bool>,
owner: ContractAddress,
token: IERC20MintingAndBurningDispatcher
}
fn claim_airdrop(...) {
// Check if already claimed
assert(!self.claimed.read(to), 'Already claimed');
// Verify proof
// ...
// Mark as claimed
self.claimed.write(to, true);
// Mint tokens
// ...
}
The multisig_threshold_attack
module demonstrates a vulnerability in multisig wallets that don't properly validate the uniqueness of signatures.
The Multisig
contract requires a certain number of signatures (threshold) to execute a transaction. However, it doesn't check if the same signer is used multiple times. This means if an attacker compromises one private key, they can reuse that signature to meet the threshold requirement.
In the test_threshold_attack.cairo
test, we demonstrate:
- A multisig wallet is set up with two signers (Alice and Bob) and a threshold of 2
- The attacker only has access to Alice's private key
- The attacker creates a malicious transaction and signs it with Alice's key
- The attacker duplicates Alice's signature to satisfy the threshold requirement
- The contract accepts the transaction since it sees two signatures, even though they're from the same signer
The contract should check that each signature comes from a unique signer:
fn is_valid_signature_span(
self: @ContractState, hash: felt252, signature: Span<felt252>,
) -> bool {
let threshold = self.threshold.read();
assert(threshold != 0, 'Uninitialized');
let mut signatures = deserialize_signatures(signature)
.expect('signature/invalid-len');
// Make sure we have the correct number of signatures
assert(threshold == signatures.len(), 'signature/invalid-len');
// Track used signers to prevent reuse
let mut used_signers: Array<felt252> = array![];
// Verify each signature
for signature_ref in signatures {
let signature = *signature_ref;
let signer = signature.signer;
// Check signer hasn't been used already
for used in used_signers.span() {
assert(*used != signer, 'signer/already-used');
}
used_signers.append(signer);
if !self.is_valid_signer_signature(
hash,
signer,
signature.signature_r,
signature.signature_s,
) { return false; }
}
true
}
The utils.merkle_tree
module provides utility functions for creating and verifying Merkle trees and proofs.
-
Data Structure: The
AddressAmount
struct represents a leaf in the Merkle tree, containing an address and an amount. -
Predefined Data: The module includes predefined data for Alice and the Attacker, which are used to generate leaves in the Merkle tree.
-
Core Functions:
generate_leaves()
: Creates an array of leaves by hashing each address-amount pairgenerate_merkle_root()
: Computes the Merkle root from the leavesgenerate_alice_proof()
: Generates a Merkle proof for Alice's leafgenerate_attacker_proof()
: Generates a Merkle proof for the Attacker's leaf
-
Generate Leaves:
let leaves = generate_leaves();
-
Generate the Merkle Root:
let merkle_root = generate_merkle_root();
-
Generate Proofs:
let alice_proof = generate_alice_proof(); let attacker_proof = generate_attacker_proof();
-
Verify Proofs:
let mut merkle_tree: MerkleTree<Hasher> = MerkleTreeTrait::new(); let alice_leaf = core::pedersen::pedersen(ALICE_DATA.address, ALICE_DATA.amount); let is_valid = merkle_tree.verify(merkle_root, alice_leaf, alice_proof);
When working with cryptographic primitives like Merkle trees and digital signatures, keep these best practices in mind:
- Track claimed airdrops: Always maintain a record of addresses that have already claimed tokens
- Validate unique signers: Ensure each signature in a multisig wallet comes from a unique signer
- Use nonces: Implement nonces to prevent replay attacks
- Comprehensive testing: Test edge cases and potential attack vectors thoroughly
You can run the tests using the Starknet Foundry's snforge
tool:
scarb test