3/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.
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.
The core concept we'll implement involves a two-step authorization and claim process:
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.
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.
First, we need to create the script file within our Foundry project.
Navigate to your project's script
directory.
Create a new Solidity file named Interact.s.sol
.
This file will house the logic for our claim interaction.
Every Foundry script requires some standard setup. Let's add the necessary boilerplate and import statements to Interact.s.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:
This remapping allows for cleaner import statements, like the one used above: foundry-devops/src/DevOpsTools.sol
.
Next, we'll define the contract that will contain our interaction logic and its main execution function.
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.
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.
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.
claimAirdrop
Helper FunctionLet's define the claimAirdrop
function, which will encapsulate the core logic for interacting with the airdrop 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.
claim
CallInside the claimAirdrop
function, before the vm.startBroadcast()
, we need to define the variables that will be passed to the claim
function.
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:
Updating your input generation mechanism (e.g., a GenerateInput.s.sol
script).
Re-running the input generation script to create a new input.json
.
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.
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
).
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.
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
.
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
).
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.
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:
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.
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 July 10, 2025
Duration: 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 July 10, 2025