5/5
## Creating a Merkle Airdrop Claiming Script with Signature Authorization This lesson guides you through building a Solidity script using Foundry to interact with a deployed Merkle Airdrop contract. Specifically, we'll focus on creating a script that allows one account (the transaction sender) to claim an airdrop on behalf of another account (the `CLAIMING_ADDRESS`). This is made possible by the `CLAIMING_ADDRESS` pre-signing a message authorizing this action, enabling scenarios like gasless claims for end-users. ## Understanding the Signature-Based Claiming Process The core concept we'll implement involves a two-step authorization and claim process: 1. **User Authorization:** An account eligible for the airdrop (and thus included in the Merkle tree) signs a specific message. This signature acts as their consent. 2. **Third-Party Claim:** Another account (the one executing our script) takes this signature and uses it to call the `claim` function on the airdrop contract. This transaction claims the tokens on behalf of the original signing account. This mechanism is powerful because it allows the end-user (the one who signs the message) to avoid paying gas fees for the claim transaction. A third party, such as a project team or a dedicated service, can cover these costs by running the interaction script. ## Setting Up the Interaction Script File First, we need to create the script file within our Foundry project. 1. Navigate to your project's `script` directory. 2. Create a new Solidity file named `Interact.s.sol`. This file will house the logic for our claim interaction. ## Initial Script Boilerplate and Imports Every Foundry script requires some standard setup. Let's add the necessary boilerplate and import statements to `Interact.s.sol`. ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import { Script } from "forge-std/Script.sol"; import { DevOpsTools } from "foundry-devops/src/DevOpsTools.sol"; ``` * **SPDX License and Pragma:** Standard Solidity practice, defining the license and compiler version. * **`Script` Import:** This is fundamental for any Foundry script, providing access to core scripting functionalities like `vm` cheatcodes. * **`DevOpsTools` Import:** We'll use this utility from the `foundry-devops` library. It helps in easily fetching information about previous contract deployments, such as the address of our `MerkleAirdrop` contract. **A Note on `foundry-devops` Import Paths and Remappings:** You might have encountered longer import paths for libraries like `lib/foundry-devops/src/DevOpsTools.sol` in other projects. To simplify these, a remapping can be added to your `foundry.toml` file. If your project has been refactored to use such remappings, it would look something like this: ```toml // foundry.toml [profile.default] # ... other configurations ... remappings = [ # ... other remappings ... 'foundry-devops/=lib/foundry-devops/' ] ``` This remapping allows for cleaner import statements, like the one used above: `foundry-devops/src/DevOpsTools.sol`. ## Defining the Script Contract and Entry Point Next, we'll define the contract that will contain our interaction logic and its main execution function. ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import { Script } from "forge-std/Script.sol"; import { DevOpsTools } from "foundry-devops/src/DevOpsTools.sol"; import { MerkleAirdrop } from "../../src/MerkleAirdrop.sol"; // Relative path to your MerkleAirdrop contract contract ClaimAirdrop is Script { function run() external { // Script logic will go here } } ``` * **Import `MerkleAirdrop`:** To interact with our `MerkleAirdrop` contract, the script needs its Application Binary Interface (ABI). We import the contract definition directly. The path `../../src/MerkleAirdrop.sol` is relative to the `script/Interact.s.sol` file and assumes your `MerkleAirdrop.sol` contract is in the `src` directory. Adjust this path if your project structure differs. * **`ClaimAirdrop` Contract:** We define a new contract, `ClaimAirdrop`, that inherits from Foundry's `Script` contract. * **`run()` Function:** This `external` function is the main entry point that Foundry will execute when this script is run. ## Fetching the Deployed Airdrop Contract Address Inside the `run()` function, our first step is to get the address of the most recently deployed `MerkleAirdrop` contract. We'll use the `DevOpsTools` utility for this. ```solidity // ... (imports and contract definition above) ... contract ClaimAirdrop is Script { function run() external { address mostRecentlyDeployed = DevOpsTools.get_most_recent_deployment("MerkleAirdrop", block.chainid); claimAirdrop(mostRecentlyDeployed); } // ... (claimAirdrop function will be defined below) ... } ``` * `DevOpsTools.get_most_recent_deployment("MerkleAirdrop", block.chainid)`: This function call retrieves the address. * `"MerkleAirdrop"`: This string should match the name of your contract as it was deployed (typically the filename without the `.sol` extension). * `block.chainid`: This ensures that we fetch a deployment from the correct blockchain (e.g., local Anvil, a testnet, or mainnet). * We then call a helper function, `claimAirdrop`, passing the fetched address. This promotes modularity in our script. ## Implementing the `claimAirdrop` Helper Function Let's define the `claimAirdrop` function, which will encapsulate the core logic for interacting with the airdrop contract. ```solidity // ... (run function above) ... function claimAirdrop(address airdropContractAddress) public { vm.startBroadcast(); // Prepare Foundry to send transactions // The actual call to MerkleAirdrop(airdropContractAddress).claim(...) will be added here vm.stopBroadcast(); // Submit the broadcasted transactions } // ... (closing curly brace for ClaimAirdrop contract) ... ``` * `airdropContractAddress`: This parameter receives the address of our deployed `MerkleAirdrop` contract. * `vm.startBroadcast()`: This is a Foundry cheatcode. It tells Foundry to start collecting any subsequent state-changing contract calls. * `vm.stopBroadcast()`: This cheatcode tells Foundry to package all collected calls into one or more transactions and send them to the network. * The comment indicates where we will place the actual call to the `claim` function of the `MerkleAirdrop` contract. The `claim` function typically requires several parameters: * `CLAIMING_ADDRESS`: The address that is eligible for and will receive the airdrop. * `CLAIMING_AMOUNT`: The amount of tokens to be claimed by `CLAIMING_ADDRESS`. * `proof`: The Merkle proof (an array of `bytes32` values) that cryptographically verifies the eligibility of `CLAIMING_ADDRESS` for `CLAIMING_AMOUNT`. * `v`, `r`, `s`: These are the three components of an EIP-712 compliant digital signature, provided by the `CLAIMING_ADDRESS` to authorize this transaction. ## Declaring Variables for the `claim` Call Inside the `claimAirdrop` function, before the `vm.startBroadcast()`, we need to define the variables that will be passed to the `claim` function. ```solidity // ... (inside ClaimAirdrop contract) ... function claimAirdrop(address airdropContractAddress) public { // Define parameters for the claim function address CLAIMING_ADDRESS = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266; // Example address uint256 CLAIMING_AMOUNT = 25 * 1e18; // Example: 25 tokens with 18 decimals // Merkle proof will be defined next vm.startBroadcast(); // MerkleAirdrop(airdropContractAddress).claim(CLAIMING_ADDRESS, CLAIMING_AMOUNT, proof, v, r, s); vm.stopBroadcast(); } ``` * **`CLAIMING_ADDRESS`**: This is the address for whom the airdrop is being claimed. This address *must* be one of the addresses included in your `input.json` file during the Merkle tree generation phase. For this example, `0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266` is used, which often corresponds to the default Anvil account 0. This is the account that will need to sign the authorization message. * **Important:** If you need to claim for different addresses, you must ensure they were part of the original dataset used to generate the Merkle tree. Modifying the eligible addresses requires: 1. Updating your input generation mechanism (e.g., a `GenerateInput.s.sol` script). 2. Re-running the input generation script to create a new `input.json`. 3. Re-running your Merkle tree generation script (e.g., `MakeMerkle.s.sol`) to create a new `output.json` containing the new proofs. * **`CLAIMING_AMOUNT`**: This is the specific amount of tokens that `CLAIMING_ADDRESS` is eligible for, according to the Merkle tree. Here, `25 * 1e18` represents 25 tokens, assuming the token uses 18 decimal places. ## Populating the Merkle Proof The Merkle proof is crucial for verifying the claim. It's unique to each `CLAIMING_ADDRESS` and `CLAIMING_AMOUNT` combination. This proof is found in the `output.json` file generated by your Merkle tree construction script (e.g., `MakeMerkle.s.sol`). ```solidity // ... (inside claimAirdrop function, after CLAIMING_AMOUNT) ... // Merkle Proof for CLAIMING_ADDRESS and CLAIMING_AMOUNT // These values are copied from the output.json generated by MakeMerkle.s.sol // for the specific CLAIMING_ADDRESS (0xf39...) bytes32 PROOF_ONE = 0xd1445c931158119d0449ffcac3c947d028c359c34a664d95962b3b55c6ad; // Example proof element bytes32 PROOF_TWO = 0xe5ebd1e1b5a5478a944eca36a9a954ac3b68216875f6524caa71d87896576; // Example proof element bytes32[] memory proof = new bytes32[](2); // Assuming a proof length of 2 for this example proof[0] = PROOF_ONE; proof[1] = PROOF_TWO; // v, r, s signature components are still needed vm.startBroadcast(); // MerkleAirdrop(airdropContractAddress).claim(CLAIMING_ADDRESS, CLAIMING_AMOUNT, proof, v, r, s); vm.stopBroadcast(); ``` 1. **Declare Proof Elements:** We declare individual `bytes32` variables (`PROOF_ONE`, `PROOF_TWO`) to hold parts of the proof. The number of elements depends on the depth of your Merkle tree and the position of the leaf. 2. **Initialize Proof Array:** We create a dynamic array `bytes32[] memory proof`. The size (e.g., `2` in this example) must match the number of proof elements required for the specific `CLAIMING_ADDRESS`. 3. **Fetch from `output.json`:** * Open your `output.json` file. * Locate the entry corresponding to the `CLAIMING_ADDRESS` you're using (e.g., `0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266`). This address might be listed as `inputs[0]`, `inputs[1]`, etc., depending on its order in your original `input.json`. * Copy the `proof` array values from the JSON into your script (e.g., `PROOF_ONE`, `PROOF_TWO`). 4. **Assign to Array:** Populate the `proof` array with these copied values. Now, our script has the `CLAIMING_ADDRESS`, `CLAIMING_AMOUNT`, and the corresponding `proof`. The final pieces missing are the `v`, `r`, and `s` components of the signature. ## Generating the Signature (V, R, S Components) To authorize the claim on behalf of `CLAIMING_ADDRESS`, this address must sign a message. The resulting signature consists of three parts: `v`, `r`, and `s`. There are two primary ways to generate this signature when working with Foundry: 1. **Using `vm.sign(privateKey, digest)`:** * This is a Foundry cheatcode that can be used directly within a script or test. * It requires the private key of the account that needs to sign (i.e., the private key for `CLAIMING_ADDRESS`). * It also requires the EIP-712 compliant digest of the message to be signed. * This method is convenient for local development with Anvil (where private keys are known) or on chains where Foundry scripts can securely access private keys. * **Limitation:** This approach might not be suitable for all scenarios, especially on certain L2s like ZKsync (at the time of some recordings) where script execution environments or private key handling might differ. It's also less practical if you want to avoid embedding private keys directly in scripts or if the signing needs to happen completely off-chain by an end-user. 2. **Using `cast wallet sign <MESSAGE_TO_SIGN> --private-key <PRIVATE_KEY>`:** * `cast` is a powerful command-line interface (CLI) tool that is part of the Foundry suite. * The `cast wallet sign` command allows you to sign an arbitrary message (or a pre-computed digest) using a provided private key. * This command outputs a single `bytes` string, which is the concatenated `r`, `s`, and `v` components of the signature (`r` + `s` + `v`). * This combined signature byte string will then need to be parsed within our Solidity script to extract the individual `v`, `r`, and `s` values required by the `claim` function. For the subsequent steps in implementing our claim script, we will explore how to generate the EIP-712 digest and then use `cast wallet sign` to produce the signature components. We will then incorporate these components into our `Interact.s.sol` script to complete the `claim` call. At this point, our `Interact.s.sol` script is well-structured and contains most of the static data needed for the claim. The next critical step is to handle the dynamic signature generation and utilization.
A detailed walkthrough to Creating a Merkle Airdrop Claiming Script with Signature Authorization - Construct a Foundry script in Solidity that allows a third party to claim Merkle airdrops on behalf of users via signature authorization, supporting gasless claims. You'll learn to set up the interaction script, retrieve deployment data, and assemble claim parameters like Merkle proofs and signature components.
Previous lesson
Previous
Next lesson
Next
Give us feedback
Course Overview
About the course
Advanced smart contract development
How to develop a stablecoin
How to develop a DeFi protocol
How to develop a DAO
Advanced smart contracts testing
Fuzz testing
Manual verification
Web3 Developer Relations
$85,000 - $125,000 (avg. salary)
Web3 developer
$60,000 - $150,000 (avg. salary)
Smart Contract Engineer
$100,000 - $150,000 (avg. salary)
Smart Contract Auditor
$100,000 - $200,000 (avg. salary)
Security researcher
$49,999 - $120,000 (avg. salary)
Web3 engineer, educator, and Cyfrin co-founder. Patrick's smart contract development and security courses have helped hundreds of thousands of engineers kickstarting their careers into web3.
Guest lecturers:
Last updated on May 20, 2025
Solidity Developer
Advanced FoundryDuration: 36min
Duration: 3h 06min
Duration: 5h 02min
Duration: 6h 02min
Duration: 2h 47min
Duration: 1h 23min
Duration: 4h 28min
Duration: 1h 19min
Duration: 1h 10min
Course Overview
About the course
Advanced smart contract development
How to develop a stablecoin
How to develop a DeFi protocol
How to develop a DAO
Advanced smart contracts testing
Fuzz testing
Manual verification
Web3 Developer Relations
$85,000 - $125,000 (avg. salary)
Web3 developer
$60,000 - $150,000 (avg. salary)
Smart Contract Engineer
$100,000 - $150,000 (avg. salary)
Smart Contract Auditor
$100,000 - $200,000 (avg. salary)
Security researcher
$49,999 - $120,000 (avg. salary)
Web3 engineer, educator, and Cyfrin co-founder. Patrick's smart contract development and security courses have helped hundreds of thousands of engineers kickstarting their careers into web3.
Guest lecturers:
Last updated on May 20, 2025