Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Merkle Airdrop

A token airdrop contract where eligible recipients are encoded in a Merkle tree. Users claim their allocation by submitting a Merkle proof, which the contract verifies on-chain before marking the claim. Token distribution is not handled.

This example demonstrates:

  • Iterating over calldata using a while loop to process proof elements
  • Handling variable-length calldata for runtime-sized proofs
import std::storage::map_slot_hash;
import std::constructor::return_runtime;

const CLAIM_SELECTOR = 0x3d13f874;
const MERKLE_ROOT_SELECTOR = 0x2eb4a7ab;
const HAS_CLAIMED_SELECTOR = 0x73b2e80e;

const CLAIMED_TOPIC = 0xd8138f8a3f377c5259ca548e70e4c2de94f129f5a11036a15b69513cba2b426a;

const MERKLE_ROOT_SLOT = 0;
const HAS_CLAIMED_SLOT_BASE = 1;

init {
    let args_buf = @malloc_zeroed(32);
    @evm_codecopy(args_buf, @init_end_offset(), 32);
    let merkle_root = @mload32(args_buf);
    @evm_sstore(MERKLE_ROOT_SLOT, merkle_root);

    return_runtime();
}

run {
    let selector = @evm_calldataload(0) >> 224;
    if selector == CLAIM_SELECTOR {
        claim();
    } else if selector == MERKLE_ROOT_SELECTOR {
        merkle_root();
    } else if selector == HAS_CLAIMED_SELECTOR {
        has_claimed();
    } else {
        @evm_revert(@malloc_uninit(0), 0);
    }
}

const claim = fn () never {
    let buf = @malloc_uninit(64);
    let slot = get_merkle_slot();
    let claimed = @evm_sload(slot);
    if claimed != 0 {
        @evm_revert(@malloc_uninit(0), 0);
    }

    let address = @evm_calldataload(4);
    let amount = @evm_calldataload(36);
    let offset = 4 + @evm_calldataload(68);
    let proof_length = @evm_calldataload(offset);

    @mstore32(buf, address);
    @mstore32(buf +% 32, amount);
    let mut node = @evm_keccak256(buf +% 12, 52);

    let mut i = 0;
    while i < proof_length {
        let proof_element = @evm_calldataload(offset + 32 + i * 32);
        if node <= proof_element {
            @mstore32(buf, node);
            @mstore32(buf +% 32, proof_element);
        } else {
            @mstore32(buf, proof_element);
            @mstore32(buf +% 32, node);
        }
        node = @evm_keccak256(buf, 64);
        i = i + 1;
    }

    let root = @evm_sload(MERKLE_ROOT_SLOT);
    if node != root {
        @evm_revert(@malloc_uninit(0), 0);
    }

    @evm_sstore(slot, 1);
    @mstore32(buf, amount);
    @evm_log2(buf, 32, CLAIMED_TOPIC, address);
    @evm_return(@malloc_uninit(0), 0);
};

const merkle_root = fn () never {
    let buf = @malloc_uninit(32);
    @mstore32(buf, @evm_sload(MERKLE_ROOT_SLOT));
    @evm_return(buf, 32);
};

const has_claimed = fn () never {
    let buf = @malloc_uninit(64);
    let slot = get_merkle_slot();
    let claimed = @evm_sload(slot);
    @mstore32(buf, claimed);
    @evm_return(buf, 32);
};

const get_merkle_slot = fn () u256 {
    let address = @evm_calldataload(4);
    map_slot_hash(address, HAS_CLAIMED_SLOT_BASE)
};

Claiming

The claim function performs the core verification logic:

  1. Checks if the caller has already claimed by reading their storage slot
  2. Reads the address, amount, and Merkle proof from calldata
  3. Computes the leaf hash from the address and amount
  4. Iterates over the proof, sorting each node pair before hashing to compute the next layer
  5. Compares the final computed root with the stored root; if valid, marks the claim and emits an event

The proof is processed in a while loop, reading each element from calldata by its offset:

let mut i = 0;
while i < proof_length {
    let proof_element = @evm_calldataload(offset + 32 + i * 32);
    ...
    i = i + 1;
}

Leaf Computation

The leaf is computed by packing the address (20 bytes) and amount (32 bytes) into a manually allocated buffer:

let buf = @malloc_uninit(64);
@mstore32(buf, address);
@mstore32(buf +% 32, amount);
let mut node = @evm_keccak256(buf +% 12, 52);