5/5
## Manually Deploying a Merkle Airdrop on zkSync Sepolia with Foundry This guide provides a comprehensive walkthrough for deploying a Merkle airdrop smart contract to the zkSync Sepolia testnet. We will perform these steps manually using Foundry's powerful command-line tools, `forge` and `cast`. While scripting deployments is the recommended best practice to prevent errors and conserve funds, this manual method offers valuable insight, especially when full scripting support for zkSync with Foundry is evolving. ## Core Concepts: Understanding the Building Blocks Before diving into the deployment, let's clarify the key technologies and concepts involved: * **Merkle Airdrops:** This is an efficient technique for distributing ERC20 tokens to a large list of recipients. Instead of individual transactions for each user, claims are verified against a Merkle root. This cryptographic proof significantly reduces gas costs and transaction overhead. * **zkSync:** A Layer 2 (L2) scaling solution for Ethereum, zkSync utilizes ZK-rollups to offer higher throughput and lower transaction fees while maintaining Ethereum's security. Our deployment targets the zkSync Sepolia testnet. * **Foundry (`forge` and `cast`):** A blazing fast, portable, and Solidity-native toolkit for Ethereum application development. * `forge create`: Deploys smart contracts to a specified network. * `cast call`: Executes read-only (view/pure) functions on deployed contracts without sending a transaction or consuming gas (beyond RPC node interaction). * `cast send`: Sends transactions that modify the blockchain state, such as calling state-changing functions in a smart contract. * `cast wallet sign`: Signs a message or data hash using a locally managed keystore, crucial for interactions requiring cryptographic signatures. * **Keystores in Foundry:** Foundry allows importing accounts (e.g., from MetaMask) as local keystores. This enhances security and convenience by enabling commands like `--account <alias>` (e.g., `updraft`, `updraft-2`) instead of exposing private keys directly in terminal commands. * **Environment Variables:** Sensitive or configurable data, such as RPC URLs, are best managed using an `.env` file. The `source .env` command loads these variables into the current terminal session, making them accessible to scripts and commands. * **Legacy Transactions (Type 0) & `--zksync` Flag:** When interacting with zkSync networks via Foundry, the `--legacy` flag is often necessary to specify a Type 0 transaction. The `--zksync` flag explicitly tells Foundry that the target network is a zkSync-based chain, enabling specific handling. * **EIP-712 Signatures (Implied) and Signature Splitting (V, R, S):** The claim process involves generating a message hash specific to the user and claim details, signing this hash, and then providing the signature components (V, R, S) to the smart contract. This pattern is common for secure off-chain message signing and on-chain verification, similar to EIP-712. An ECDSA signature (typically 65 bytes) is split into: * `V`: The recovery identifier. * `R`: The first 32 bytes of the signature. * `S`: The second 32 bytes of the signature. These components are passed as separate arguments to contract functions that verify signatures, such as the `claim` function in our airdrop contract. ## Prerequisites: Setting Up Your Environment Ensure you have the following configured before proceeding: 1. **Foundry Installed:** Verify your Foundry installation (`forge --version`, `cast --version`). 2. **MetaMask Accounts Imported:** Import the Ethereum accounts you intend to use for deployment (e.g., `updraft`) and claiming (e.g., `updraft-2`) into Foundry's keystore. 3. **`.env` File:** Create an `.env` file in your project root with the zkSync Sepolia RPC URL: ``` ZKSYNC_SEPOLIA_RPC_URL=https://sepolia.era.zksync.dev ``` Load these variables into your terminal session by running: ```bash source .env ``` 4. **Merkle Tree Data (`input.json`, `output.json`):** These files contain the airdrop recipient data, individual Merkle proofs, and the overall Merkle root. They are typically generated by a script (e.g., using `make merkle` if your project is set up that way). Ensure these files are up-to-date, especially the Merkle root in `output.json`. ## Step-by-Step Deployment and Interaction Guide Follow these steps to deploy and interact with your Merkle airdrop contracts. ### Step 1: Deploying the ERC20 Token Contract (`BagelToken.sol`) First, deploy the `BagelToken.sol` contract, which represents the ERC20 token to be airdropped. ```bash forge create src/BagelToken.sol:BagelToken \ --rpc-url ${ZKSYNC_SEPOLIA_RPC_URL} \ --account updraft \ --legacy \ --zksync ``` This command uses the `updraft` account (as defined in your Foundry keystore) to deploy `BagelToken`. After successful deployment, the terminal will output the deployed contract address. Capture this address and set it as an environment variable for easy use in subsequent steps: ```bash export TOKEN_ADDRESS=<deployed_token_address> ``` Replace `<deployed_token_address>` with the actual address output by `forge create`. ### Step 2: Deploying the Merkle Airdrop Contract (`MerkleAirdrop.sol`) Next, deploy the `MerkleAirdrop.sol` contract. Its constructor requires the Merkle root (which defines the set of eligible claimers and amounts) and the address of the token being airdropped. Retrieve the Merkle root from your `output.json` file. ```bash # Example: MERKLE_ROOT_VAL=$(jq -r '.merkleRoot' output.json) # Ensure MERKLE_ROOT_VAL contains the correct root from your output.json forge create src/MerkleAirdrop.sol:MerkleAirdrop \ --constructor-args <merkle_root_from_output.json> ${TOKEN_ADDRESS} \ --rpc-url ${ZKSYNC_SEPOLIA_RPC_URL} \ --account updraft \ --legacy \ --zksync ``` Replace `<merkle_root_from_output.json>` with the actual Merkle root. **Important:** If you modify the script that generates your `input.json` and `output.json` (e.g., `GenerateInput.s.sol`), remember to regenerate these files (e.g., via `make merkle`) to ensure the Merkle root is current. Capture the deployed airdrop contract address: ```bash export AIRDROP_ADDRESS=<deployed_airdrop_address> ``` Replace `<deployed_airdrop_address>` with the actual address. You might observe compiler warnings related to `ecrecover` on zkSync. This is due to zkSync's native account abstraction. For Externally Owned Accounts (EOAs) or smart contract accounts relying on ECDSA signatures, `ecrecover` generally functions as expected. ### Step 3: Retrieving the Message Hash for Claiming To claim tokens, a recipient (e.g., `updraft-2`) must sign a unique message. The hash of this message is generated by the `getMessageHash` function in the `MerkleAirdrop` contract. This function typically takes the claimant's address and the amount they are eligible to claim. Obtain these values (address of `updraft-2`, claim amount) from your `input.json` or `output.json` file corresponding to the `updraft-2` recipient. ```bash cast call ${AIRDROP_ADDRESS} "getMessageHash(address,uint256)" \ <address_of_updraft-2_from_input.json> \ <claim_amount_from_input.json> \ --rpc-url ${ZKSYNC_SEPOLIA_RPC_URL} ``` This command will output a `bytes32` message hash. Save this hash for the next step. ### Step 4: Signing the Message Hash with Foundry Keystore The `updraft-2` account (the claimant) now signs the message hash obtained in Step 3. ```bash # Let's say MESSAGE_HASH=<message_hash_from_step_3> cast wallet sign --no-hash ${MESSAGE_HASH} --account updraft-2 ``` * The `--no-hash` flag is critical here because `getMessageHash` already returned a pre-hashed message. `cast wallet sign` by default hashes its input; `--no-hash` prevents double-hashing. * `--account updraft-2` specifies that the `updraft-2` keystore entry should be used for signing. This command will output the raw 65-byte signature. ### Step 5: Splitting the Signature into V, R, S Components The raw signature must be split into its V, R, and S components to be used in the `claim` function. A helper Solidity script, `SplitSignature.s.sol`, can automate this. 1. Copy the raw signature output from Step 4 (remove the "0x" prefix) and paste it into a new file named `signature.txt`. 2. The `SplitSignature.s.sol` script might look like this: ```solidity // script/SplitSignature.s.sol pragma solidity ^0.8.0; import "forge-std/Script.sol"; import "forge-std/console.sol"; contract SplitSignature is Script { function splitSignature(bytes memory sig) internal pure returns (uint8 v, bytes32 r, bytes32 s) { require(sig.length == 65, "invalid signature length"); assembly { r := mload(add(sig, 32)) s := mload(add(sig, 64)) v := byte(0, mload(add(sig, 96))) // Loads the 65th byte } // Adjust v for Ethereum's convention (27 or 28) if it's 0 or 1 if (v < 27) { v += 27; } } function run() external { string memory sigHex = vm.readFile("signature.txt"); bytes memory sigBytes = vm.parseBytes(sigHex); // Assumes sigHex does NOT have "0x" prefix (uint8 v, bytes32 r, bytes32 s) = splitSignature(sigBytes); console.log("v value:"); console.log(v); console.log("r value:"); console.logBytes32(r); console.log("s value:"); console.logBytes32(s); } } ``` 3. Run the script: ```bash forge script script/SplitSignature.s.sol:SplitSignature ``` 4. The script will print the V, R, and S values. Capture these and set them as environment variables: ```bash export V_VAL=<v_value_output_by_script> export R_VAL=<r_value_output_by_script> export S_VAL=<s_value_output_by_script> ``` ### Step 6: Funding the Airdrop Contract with Tokens The `MerkleAirdrop` contract needs to hold enough `BagelToken`s to cover all potential claims. 1. **Mint Tokens:** The owner of `BagelToken` (the `updraft` account in this case) mints the total supply required for the airdrop to itself. Determine the `<total_airdrop_supply>` by summing all claimable amounts. ```bash # <my_metamask_address_for_updraft> is the deployer's EOA address cast send ${TOKEN_ADDRESS} "mint(address,uint256)" \ <my_metamask_address_for_updraft> \ <total_airdrop_supply> \ --account updraft \ --rpc-url ${ZKSYNC_SEPOLIA_RPC_URL} \ --legacy --zksync ``` 2. **Transfer Tokens to Airdrop Contract:** Transfer the minted tokens from the `updraft` account to the `MerkleAirdrop` contract. ```bash cast send ${TOKEN_ADDRESS} "transfer(address,uint256)" \ ${AIRDROP_ADDRESS} \ <total_airdrop_supply> \ --account updraft \ --rpc-url ${ZKSYNC_SEPOLIA_RPC_URL} \ --legacy --zksync ``` ### Step 7: Executing the Token Claim Now, anyone (here, `updraft` acts as the transaction sender, though it could be anyone) can call the `claim` function on the `MerkleAirdrop` contract, providing the necessary proof and signature for `updraft-2` to receive their tokens. The arguments for the `claim` function typically are: * Claimant's address (`<address_of_updraft-2>`) * Claim amount (`<claim_amount>`) * Merkle proof (an array of `bytes32` hashes, specific to `updraft-2`, found in `output.json`) * Signature components (`V_VAL`, `R_VAL`, `S_VAL`) ```bash # Retrieve Merkle proof elements for updraft-2 from output.json # Example: "[0xproofelement1...,0xproofelement2...]" cast send ${AIRDROP_ADDRESS} \ "claim(address,uint256,bytes32[],uint8,bytes32,bytes32)" \ <address_of_updraft-2> \ <claim_amount> \ "[<proof_element_1_for_updraft-2>,<proof_element_2_for_updraft-2>,...]" \ ${V_VAL} \ ${R_VAL} \ ${S_VAL} \ --account updraft \ --rpc-url ${ZKSYNC_SEPOLIA_RPC_URL} \ --legacy --zksync ``` Ensure the proof array is correctly formatted as a string for the command line. ### Step 8: Verifying the Airdrop Claim After the claim transaction is confirmed, verify that `updraft-2` received the tokens. 1. **Check Token Balance:** ```bash cast call ${TOKEN_ADDRESS} "balanceOf(address)" <address_of_updraft-2> \ --rpc-url ${ZKSYNC_SEPOLIA_RPC_URL} ``` This will output the balance in hexadecimal format. 2. **Convert to Decimal:** ```bash cast --to-dec <hex_balance_output_from_above> ``` Confirm this matches the expected claim amount (e.g., 25 tokens, considering decimals). 3. **Block Explorer:** You can also verify the transaction and token balance on the zkSync Sepolia block explorer. ## Key Considerations and Best Practices * **Prioritize Scripting:** For any real-world or even frequent testnet deployments, use deployment scripts (e.g., Foundry scripts). Manual steps are error-prone and can lead to wasted gas or misconfigurations. * **zkSync RPC URL:** Use official RPC URLs like `https://sepolia.era.zksync.dev`. At times, third-party RPCs might have compatibility issues or lag with newer zkSync features. * **Merkle Data Integrity:** Always ensure your `input.json` and `output.json` files (or however you manage airdrop data) are current and the Merkle root used in the `MerkleAirdrop` contract deployment matches the root derived from your intended recipient list. * **`--no-hash` with `cast wallet sign`:** This flag is crucial when the input message to `cast wallet sign` is already a hash (e.g., output from a contract's `getMessageHash` function). Omitting it would lead to signing the hash of the hash, resulting in an invalid signature. * **Signature Schemes on zkSync:** Be aware of account types (EOA vs. smart contract accounts) and their supported signature verification methods, especially concerning `ecrecover` in the context of zkSync's native Account Abstraction. * **Transaction Finality:** Transactions on zkSync (L2) are processed quickly. However, for full finality on Ethereum (L1), the transaction batch containing your L2 transaction must be finalized on L1. For testnet verification, L2 confirmation is usually sufficient. ## Useful Resources * **zkSync Era Sepolia Testnet Explorer:** Essential for visually inspecting transactions, contract deployments, and token balances. * **zkSync Account Abstraction Documentation:** `https://v2-docs.zksync.io/dev/developer-guides/aa.html` (Provides more context on `ecrecover` warnings and zkSync's account model). By following these steps, you can manually deploy and interact with a Merkle airdrop contract on the zkSync Sepolia testnet, gaining a deeper understanding of the underlying processes. Remember to transition to scripted deployments for robustness and efficiency in your projects.
A detailed command-line walkthrough to Manually Deploying Your zkSync Sepolia Merkle Airdrop with Foundry - Learn to manually deploy `BagelToken` and `MerkleAirdrop` contracts to zkSync Sepolia using `forge` and `cast` CLI tools. Master essential steps from message hash generation and signing with `cast wallet sign` to V,R,S signature splitting and executing token claims.
Previous lesson
Previous
Next lesson
Next
Give us feedback
Obtaining testnet funds -> Tenderly virtual testnets
Last updated on June 1, 2025
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