Building a Secure and Gas-Efficient Airdrop System on Rootstock: A Complete Guide with Foundry.

Pandit DhamdherePandit Dhamdhere
16 min read

Welcome back, Smart contract maxis, to yet another exciting smart contract tutorial. Airdrops have become a popular method for blockchain projects to distribute tokens to users, incentivise participation, and promote community engagement. However, creating a secure and gas-efficient airdrop system is crucial to ensure the success of the project and maintain user trust. This guide will walk you through the process of building a secure and gas-efficient airdrop system on Rootstock test network, a smart contract platform that is merge-mined with Bitcoin, using Foundry, a powerful development framework for Ethereum-compatible blockchains. Before jumping right into development, let me give you a brief intro to the Rootstock Network.

Understanding Rootstock (RSK)

Rootstock is a smart contract platform that brings Ethereum's capabilities to the Bitcoin network. It allows developers to create decentralised applications while benefiting from Bitcoin's security. Rootstock uses a two-way peg mechanism to enable Bitcoin holders to use their assets on the Rootstock network, making it an attractive option for projects looking to leverage Bitcoin's liquidity and security.

Why Use Foundry?

Okay, but why are we gonna use Foundry? Foundry is a modern development framework for Ethereum and Ethereum-compatible blockchains. It provides a suite of tools for smart contract development, testing, and deployment. Some of the key features of Foundry include:

  • Fast Compilation: Foundry compiles smart contracts quickly, allowing for rapid development cycles.

  • Built-in Testing: It includes a robust testing framework that makes it easy to write and run tests for your contracts.

  • Gas Reporting: Foundry provides tools to analyse gas usage, helping developers optimise their contracts for efficiency.

Setting Up Your Development Environment

Before we move ahead, ensure you have the following installed in your system.

VsCode - https://code.visualstudio.com/download

Foundry - https://book.getfoundry.sh/getting-started/installation

NodeJS - https://nodejs.org/en/download

MetaMask - https://metamask.io/en-GB/download

Add Rootstock network to your wallet - https://dev.rootstock.io/dev-tools/wallets/metamask/

Rootstock Test faucets - https://faucet.rootstock.io/

I am assuming you have a basic understanding of Solidity smart contracts and Ethereum. Also, let me be honest, this is not a beginner-friendly tutorial. With that, let’s go ahead and start building our smart contract.

Let’s create a new empty directory called Airdrop and open this directory in VS Code, open your VS Code terminal and run the following command to initialise our Foundry project.

forge init

And you will see that a Foundry will initialise a new project ( see the image below )

Now, let me give you the Foundry folder structure intro

.github - The .github folder in Foundry contains GitHub-specific configuration files that automate various aspects of your development workflow, such as CI/CD pipelines and build verification.

lib -The lib folder in Foundry projects contains external dependencies and libraries we install, essentially Foundry's package management system.

src/ - Your Smart Contracts

  • Contains all your Solidity contract source code

  • Main business logic, token contracts, protocol implementations

  • This is where you build your actual application

script/ - Deployment & Interaction Scripts

  • Solidity scripts for deploying contracts to networks

  • Scripts for interacting with deployed contracts

  • Run with forge script command

  • Can simulate deployments locally before going live

test/ - Test Suite

  • All your contract tests are written in Solidity

  • Uses forge-std testing framework

  • Run with forge test

  • Tests inherit from the Test contract for assertions and utilities

.gitignore - Git Exclusions

  • Specifies files/folders Git should ignore

  • Common entries: cache/, out/, broadcast/, .env

  • Prevents build artifacts and sensitive data from being committed

.gitmodules - Git Submodules Configuration

  • Tracks external dependencies as Git submodules

  • Maps library names to their Git repository URLs

  • Created automatically when you install libraries

  • Ensures everyone gets the same library versions

foundry.toml - Foundry Configuration

  • Main configuration file for your Foundry project

  • Sets compiler version, optimisation settings, and test paths

  • Network RPC URLs, private keys, gas limits

  • Import remappings for clean library imports

README.md - Project Documentation

  • Project overview, setup instructions

  • How to compile, test, and deploy

  • Usage examples, API documentation

  • The first thing people see when they visit your repository

This structure gives you everything needed for a complete smart contract development lifecycle - from writing and testing to deployment and documentation.

Great! Now you have an understanding of the Foundry folder structure. Go inside your foundry project’s src directory and delete that counter.sol file and create a new file named Airdrop.sol. Also, go inside test directory and script directory and delete counter.t.sol and counter.s.sol files. Before going ahead, we need to install OpenZeppelin contract library in our Foundry Airdrop project because we will need that as an external dependency.

What is the OpenZeppelin Library?

OpenZeppelin Contracts library is a widely used library of secure, battle-tested smart contracts for Ethereum and other EVM-compatible blockchains. It provides developers with standardised implementations of common blockchain functionality like ERC20 and ERC721 tokens, access control mechanisms, and governance tools, all thoroughly audited and maintained by blockchain security experts. By offering these reusable components, OpenZeppelin allows developers to build on established security patterns rather than writing everything from scratch, significantly reducing the risk of vulnerabilities that could lead to costly hacks or exploits.

Here are the docs if you want to explore more: https://docs.openzeppelin.com/contracts/5.x/

GitHub: https://github.com/OpenZeppelin/openzeppelin-contracts

Now let’s go ahead and install OpenZeppelin’s contracts library in our project. To install, run the following command.

forge install Openzeppelin/openzeppelin-contracts

And now set our remappings to avoid some red lines and successfully compile our contracts.

Run the following command to set remappings.

forge remappings >> remappings.txt

It will automatically create a new remappings.txt file in your project’s root with our remappings.

Now all set, and we are ready to write our main Smart contract. Open the Airdrop.sol file we previously created in the src folder, and start writing our main contract.

Paste the following code in that file. I have explained all the smart contract code in comments and Natspec.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

// warning - Don't use this code in production

import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {MerkleProof} from "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";

/**
 * @title Airdrop
 * @dev A secure and gas-efficient airdrop contract that uses Merkle proofs for claim verification.
 * This contract allows for:
 * - Time-bound claim periods
 * - Merkle proof verification for gas-efficient claims
 * - Batch claims for multiple recipients
 * - Emergency withdrawal functionality
 * - Pausable operations
 * - Reentrancy protection
 */
contract Airdrop is Ownable, Pausable, ReentrancyGuard {
    /// @notice The ERC20 token being airdropped
    IERC20 public immutable token;

    /// @notice The Merkle root of the airdrop distribution
    bytes32 public merkleRoot;

    /// @notice The timestamp when claims can start
    uint256 public claimStartTime;

    /// @notice The timestamp when claims end
    uint256 public claimEndTime;

    /// @notice Mapping to track which addresses have claimed their tokens
    mapping(address => bool) public hasClaimed;

    /// @notice Emitted when a user successfully claims their tokens
    /// @param account The address that claimed the tokens
    /// @param amount The amount of tokens claimed
    event Claimed(address indexed account, uint256 amount);

    /// @notice Emitted when the Merkle root is updated
    /// @param merkleRoot The new Merkle root
    event MerkleRootUpdated(bytes32 merkleRoot);

    /// @notice Emitted when the claim period is updated
    /// @param startTime The new claim start time
    /// @param endTime The new claim end time
    event ClaimPeriodUpdated(uint256 startTime, uint256 endTime);

    /**
     * @dev Constructor initializes the airdrop contract
     * @param _token The address of the ERC20 token to be airdropped
     * @param _merkleRoot The Merkle root of the airdrop distribution
     * @param _claimStartTime The timestamp when claims can start
     * @param _claimEndTime The timestamp when claims end
     */
    constructor(
        address _token,
        bytes32 _merkleRoot,
        uint256 _claimStartTime,
        uint256 _claimEndTime
    ) Ownable(msg.sender) Pausable() ReentrancyGuard() {
        require(_token != address(0), "Invalid token address");
        require(_claimStartTime < _claimEndTime, "Invalid claim period");

        token = IERC20(_token);
        merkleRoot = _merkleRoot;
        claimStartTime = _claimStartTime;
        claimEndTime = _claimEndTime;
    }

    /**
     * @dev Pauses the contract, preventing new claims
     * @notice Only callable by the contract owner
     */
    function pause() external onlyOwner {
        _pause();
    }

    /**
     * @dev Unpauses the contract, allowing claims to resume
     * @notice Only callable by the contract owner
     */
    function unpause() external onlyOwner {
        _unpause();
    }

    /**
     * @dev Updates the Merkle root for claim verification
     * @param _merkleRoot The new Merkle root
     * @notice Only callable by the contract owner
     */
    function updateMerkleRoot(bytes32 _merkleRoot) external onlyOwner {
        merkleRoot = _merkleRoot;
        emit MerkleRootUpdated(_merkleRoot);
    }

    /**
     * @dev Updates the claim period
     * @param _startTime The new claim start time
     * @param _endTime The new claim end time
     * @notice Only callable by the contract owner
     */
    function updateClaimPeriod(
        uint256 _startTime,
        uint256 _endTime
    ) external onlyOwner {
        require(_startTime < _endTime, "Invalid claim period");
        claimStartTime = _startTime;
        claimEndTime = _endTime;
        emit ClaimPeriodUpdated(_startTime, _endTime);
    }

    /**
     * @dev Allows a user to claim their airdrop tokens
     * @param amount The amount of tokens to claim
     * @param merkleProof The Merkle proof verifying the claim
     * @notice Requires valid Merkle proof and must be within claim period
     */
    function claim(
        uint256 amount,
        bytes32[] calldata merkleProof
    ) external nonReentrant whenNotPaused {
        require(block.timestamp >= claimStartTime, "Claim not started");
        require(block.timestamp <= claimEndTime, "Claim ended");
        require(!hasClaimed[msg.sender], "Already claimed");

        bytes32 node = keccak256(abi.encodePacked(msg.sender, amount));
        require(
            MerkleProof.verify(merkleProof, merkleRoot, node),
            "Invalid proof"
        );

        hasClaimed[msg.sender] = true;
        require(token.transfer(msg.sender, amount), "Transfer failed");

        emit Claimed(msg.sender, amount);
    }

    /**
     * @dev Allows batch claiming of tokens for multiple recipients
     * @param accounts Array of recipient addresses
     * @param amounts Array of token amounts to claim
     * @param merkleProofs Array of Merkle proofs for each claim
     * @notice Requires valid Merkle proofs and must be within claim period
     */
    function batchClaim(
        address[] calldata accounts,
        uint256[] calldata amounts,
        bytes32[][] calldata merkleProofs
    ) external nonReentrant whenNotPaused {
        require(block.timestamp >= claimStartTime, "Claim not started");
        require(block.timestamp <= claimEndTime, "Claim ended");
        require(
            accounts.length == amounts.length &&
                amounts.length == merkleProofs.length,
            "Invalid input"
        );

        for (uint256 i = 0; i < accounts.length; i++) {
            if (!hasClaimed[accounts[i]]) {
                bytes32 node = keccak256(
                    abi.encodePacked(accounts[i], amounts[i])
                );
                require(
                    MerkleProof.verify(merkleProofs[i], merkleRoot, node),
                    "Invalid proof"
                );

                hasClaimed[accounts[i]] = true;
                require(
                    token.transfer(accounts[i], amounts[i]),
                    "Transfer failed"
                );

                emit Claimed(accounts[i], amounts[i]);
            }
        }
    }

    /**
     * @dev Allows the owner to withdraw tokens in case of emergency
     * @param _token The address of the token to withdraw (address(0) for native currency)
     * @notice Only callable by the contract owner
     */
    function emergencyWithdraw(address _token) external onlyOwner {
        if (_token == address(0)) {
            payable(owner()).transfer(address(this).balance);
        } else {
            IERC20(_token).transfer(
                owner(),
                IERC20(_token).balanceOf(address(this))
            );
        }
    }
}

Now let’s compile the contract and ensure our contract is compiling. To compile our smart contract, run the following command,

forge build

Congrats, you have successfully written and compiled your contract (see the image below), and now it’s time to test our contract.

To test our contract, go inside test directory and create a new file named Airdrop.t.sol and paste the following test suite code inside this file.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {Test} from "forge-std/Test.sol";
import {Airdrop} from "../src/Airdrop.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {MerkleProof} from "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";

/**
 * @title MockToken
 * @dev A simple ERC20 token implementation for testing purposes
 * @notice Mints 1,000,000 tokens to the deployer upon creation
 */
contract MockToken is ERC20 {
    constructor() ERC20("Mock Token", "MTK") {
        _mint(msg.sender, 1000000 * 10 ** decimals());
    }
}

/**
 * @title AirdropTest
 * @dev Test suite for the Airdrop contract
 * @notice Tests all major functionality including claims, batch claims, and emergency withdrawals
 */
contract AirdropTest is Test {
    /// @notice The Airdrop contract instance being tested
    Airdrop public airdrop;

    /// @notice The mock token used for testing
    MockToken public token;

    /// @notice The Merkle root for claim verification
    bytes32 public merkleRoot;

    /// @notice The start time for claims
    uint256 public claimStartTime;

    /// @notice The end time for claims
    uint256 public claimEndTime;

    /// @notice Test addresses
    address public owner = address(1);
    address public user1 = address(2);
    address public user2 = address(3);

    /// @notice Merkle tree leaves for test users
    bytes32 public leaf1;
    bytes32 public leaf2;

    /**
     * @dev Sets up the test environment
     * @notice Creates a new token, sets up Merkle tree, and deploys the airdrop contract
     */
    function setUp() public {
        vm.startPrank(owner);
        token = new MockToken();
        claimStartTime = block.timestamp + 1 days;
        claimEndTime = block.timestamp + 30 days;

        leaf1 = keccak256(abi.encodePacked(user1, uint256(100 * 10 ** 18)));
        leaf2 = keccak256(abi.encodePacked(user2, uint256(200 * 10 ** 18)));
        // Sort leaves for OpenZeppelin Merkle root
        bytes32 left = leaf1 < leaf2 ? leaf1 : leaf2;
        bytes32 right = leaf1 < leaf2 ? leaf2 : leaf1;
        merkleRoot = keccak256(abi.encodePacked(left, right));

        airdrop = new Airdrop(
            address(token),
            merkleRoot,
            claimStartTime,
            claimEndTime
        );

        token.transfer(address(airdrop), 1000 * 10 ** 18);
        vm.stopPrank();
    }

    /**
     * @dev Tests the constructor initialization
     * @notice Verifies that all constructor parameters are set correctly
     */
    function test_Constructor() public {
        assertEq(address(airdrop.token()), address(token));
        assertEq(airdrop.merkleRoot(), merkleRoot);
        assertEq(airdrop.claimStartTime(), claimStartTime);
        assertEq(airdrop.claimEndTime(), claimEndTime);
    }

    /**
     * @dev Tests the claim functionality
     * @notice Verifies that users can claim their tokens with valid Merkle proofs
     */
    function test_Claim() public {
        // user1 (left leaf): proof is [leaf2]
        bytes32[] memory proof1 = new bytes32[](1);
        proof1[0] = leaf2;
        // Check root calculation for user1
        assertEq(MerkleProof.processProof(proof1, leaf1), merkleRoot);
        vm.warp(claimStartTime + 1);
        vm.startPrank(user1);
        airdrop.claim(100 * 10 ** 18, proof1);
        assertEq(token.balanceOf(user1), 100 * 10 ** 18);
        assertTrue(airdrop.hasClaimed(user1));
        vm.stopPrank();

        // user2 (right leaf): proof is [leaf1]
        bytes32[] memory proof2 = new bytes32[](1);
        proof2[0] = leaf1;
        // Check root calculation for user2
        assertEq(MerkleProof.processProof(proof2, leaf2), merkleRoot);
        vm.startPrank(user2);
        airdrop.claim(200 * 10 ** 18, proof2);
        assertEq(token.balanceOf(user2), 200 * 10 ** 18);
        assertTrue(airdrop.hasClaimed(user2));
        vm.stopPrank();
    }

    /**
     * @dev Tests claim rejection before start time
     * @notice Verifies that claims are rejected before the claim period starts
     */
    function test_RevertWhen_ClaimBeforeStart() public {
        bytes32[] memory proof1 = new bytes32[](1);
        proof1[0] = leaf2;
        vm.startPrank(user1);
        vm.expectRevert("Claim not started");
        airdrop.claim(100 * 10 ** 18, proof1);
        vm.stopPrank();
    }

    /**
     * @dev Tests claim rejection after end time
     * @notice Verifies that claims are rejected after the claim period ends
     */
    function test_RevertWhen_ClaimAfterEnd() public {
        bytes32[] memory proof1 = new bytes32[](1);
        proof1[0] = leaf2;
        vm.warp(claimEndTime + 1);
        vm.startPrank(user1);
        vm.expectRevert("Claim ended");
        airdrop.claim(100 * 10 ** 18, proof1);
        vm.stopPrank();
    }

    /**
     * @dev Tests double claim prevention
     * @notice Verifies that users cannot claim their tokens more than once
     */
    function test_RevertWhen_DoubleClaim() public {
        bytes32[] memory proof1 = new bytes32[](1);
        proof1[0] = leaf2;
        vm.warp(claimStartTime + 1);
        vm.startPrank(user1);
        airdrop.claim(100 * 10 ** 18, proof1);
        vm.expectRevert("Already claimed");
        airdrop.claim(100 * 10 ** 18, proof1);
        vm.stopPrank();
    }

    /**
     * @dev Tests batch claim functionality
     * @notice Verifies that multiple users can claim their tokens in a single transaction
     */
    function test_BatchClaim() public {
        address[] memory accounts = new address[](2);
        uint256[] memory amounts = new uint256[](2);
        bytes32[][] memory proofs = new bytes32[][](2);
        accounts[0] = user1;
        accounts[1] = user2;
        amounts[0] = 100 * 10 ** 18;
        amounts[1] = 200 * 10 ** 18;
        proofs[0] = new bytes32[](1);
        proofs[1] = new bytes32[](1);
        proofs[0][0] = leaf2; // user1's proof
        proofs[1][0] = leaf1; // user2's proof
        // Check root calculation for both
        assertEq(MerkleProof.processProof(proofs[0], leaf1), merkleRoot);
        assertEq(MerkleProof.processProof(proofs[1], leaf2), merkleRoot);
        vm.warp(claimStartTime + 1);
        vm.startPrank(owner);
        airdrop.batchClaim(accounts, amounts, proofs);
        assertEq(token.balanceOf(user1), 100 * 10 ** 18);
        assertEq(token.balanceOf(user2), 200 * 10 ** 18);
        assertTrue(airdrop.hasClaimed(user1));
        assertTrue(airdrop.hasClaimed(user2));
        vm.stopPrank();
    }

    /**
     * @dev Tests emergency withdrawal functionality
     * @notice Verifies that the owner can withdraw tokens in case of emergency
     */
    function test_EmergencyWithdraw() public {
        vm.startPrank(owner);
        airdrop.emergencyWithdraw(address(token));
        assertEq(token.balanceOf(owner), 1000000 * 10 ** 18);
        vm.stopPrank();
    }
}

We have written some of the badass tests for our smart contract to ensure our contract is working correctly as we expected. Now it’s time to check that our tests are running correctly.

Run the following command to check.

forge test

And now you will see all our 7 comprehensive tests are passed ✅

You can also run the following command, which is optional.

forge coverage

The forge coverage command in Foundry generates code coverage reports for your Solidity smart contracts, showing which lines of code are executed during your test runs.

Congrats on building and successfully testing the Airdrop contract. But our job ain’t finished, we have to write a deployment script in order to deploy our Airdrop contract on the Rootstock test network. We're gonna do it like a professional smart contract Engineer. Before writing the deployment script, we need to create one more smart contract that’s a basic ERC20 smart contract.

Go ahead in src directory and create a new file and name it MockToken.sol, and paste the following code in this file.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";

/**
 * @title MockToken
 * @dev A simple ERC20 token implementation for testing purposes.
 * This contract inherits from OpenZeppelin's ERC20 implementation and
 * automatically mints 1,000,000 tokens to the deployer upon creation.
 */
contract MockToken is ERC20 {
    /**
     * @dev Constructor creates a new MockToken with specified name and symbol
     * @param name The name of the token
     * @param symbol The symbol of the token
     * @notice Automatically mints 1,000,000 tokens to the deployer
     */
    constructor(string memory name, string memory symbol) ERC20(name, symbol) {
        // Mint 1,000,000 tokens to the deployer
        _mint(msg.sender, 1000000 * 10 ** decimals());
    }
}

This is a very basic ERC20 smart contract. I have explained the code in comments.

Now, head over to script directory and create a new file named Airdrop.s.sol inside this file, we will write our deploy script. Paste the following solidity script code in this file.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {Script} from "forge-std/Script.sol";
import {console} from "forge-std/console.sol";
import {Airdrop} from "../src/Airdrop.sol";
import {MockToken} from "../src/MockToken.sol";

/**
 * @title DeployAirdrop
 * @dev Script for deploying the Airdrop contract and its dependencies
 * @notice This script deploys a mock token and the airdrop contract with example Merkle tree data
 */
contract DeployAirdrop is Script {
    /**
     * @dev Main deployment function
     * @notice Deploys the mock token and airdrop contract, sets up the Merkle tree,
     * and transfers initial tokens to the airdrop contract
     */
    function run() external {

        vm.startBroadcast();


        MockToken token = new MockToken("AirdropToken", "AIRDROP");

        // Set up Merkle root (example with two addresses)
        address user1 = 0x70997970C51812dc3A010C7d01b50e0d17dc79C8;
        address user2 = 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC;

        // Create Merkle tree leaves for each user
        bytes32 leaf1 = keccak256(
            abi.encodePacked(user1, uint256(100 * 10 ** 18))
        );
        bytes32 leaf2 = keccak256(
            abi.encodePacked(user2, uint256(200 * 10 ** 18))
        );

        // Sort leaves for OpenZeppelin Merkle root
        bytes32 left = leaf1 < leaf2 ? leaf1 : leaf2;
        bytes32 right = leaf1 < leaf2 ? leaf2 : leaf1;
        bytes32 merkleRoot = keccak256(abi.encodePacked(left, right));

        // Set claim period (1 day from now to 30 days from now)
        uint256 claimStartTime = block.timestamp + 1 days;
        uint256 claimEndTime = block.timestamp + 30 days;

        // Deploy airdrop contract with configured parameters
        Airdrop airdrop = new Airdrop(
            address(token),
            merkleRoot,
            claimStartTime,
            claimEndTime
        );

        // Transfer initial tokens to airdrop contract
        token.transfer(address(airdrop), 1000 * 10 ** 18);

        vm.stopBroadcast();

        // Log deployment information for verification
        console.log("Token deployed to:");
        console.log(address(token));
        console.log("Airdrop deployed to:");
        console.log(address(airdrop));
        console.log("Merkle root:");
        console.logBytes32(merkleRoot);
        console.log("Claim start time:");
        console.logUint(claimStartTime);
        console.log("Claim end time:");
        console.logUint(claimEndTime);
    }
}

Great! Our deploy script is ready! So far, we have built, tested and written a deploy script for our Airdrop contract, and we are almost ready to deploy our contract on the Rootstock network, but before going ahead, we need to finish one job that is the configuration.

Let’s do that. Open your foundry.toml file and paste the following content into this file.

[profile.default]
src = "src"
out = "out"
libs = ["lib"]
solc = "0.8.20"
evm_version = "london"


#RSK testnet RPC URL
[rpc_endpoints]
rootstock_testnet = "https://public-node.testnet.rsk.co"


# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options

Amazing!!! All set. Now you are finally ready to deploy your contract on the Rootstock network. Ensure you have a Rootstock wallet set up and funded with Rootstock tokens for transaction fees.

To add the Rootstock network to your wallet, follow the link: https://dev.rootstock.io/dev-tools/wallets/metamask/

To get Rootstock faucets, follow the link: https://faucet.rootstock.io/

Make sure you set up and imported your wallet in Foundry Key Store: read here - https://book.getfoundry.sh/reference/cast/cast-wallet-import

To deploy our Airdrop contract, go ahead and open your terminal (Make sure you are in the root directory) and run the following command.

forge script script/Airdrop.s.sol:DeployAirdrop --rpc-url rootstock_testnet --account YOUR_WALLET_NAME --sender YOUR_WALLET_ADDRESS --legacy --broadcast

Example

forge script script/Airdrop.s.sol:DeployAirdrop --rpc-url rootstock_testnet --account pandit --sender 0x339abb297eB21A0ee52E22e07DDe496c0fe98fB9 --legacy --broadcast

It will ask you for the password you set during the setup of your key store. Enter the password, and fingers crossed, there we go, you.

You can see a comprehensive report of the deployment of our Airdrop contract.

Great! Now you can visit the newly upgraded Rootstock Testnet block explorer https://explorer.testnet.rootstock.io/ and check whether our smart contract is indeed deployed or not (see the image below):

Great. Congratulations on building, testing, and deploying the Airdrop smart contract on the Rootstock network. If you're stuck anywhere, feel free to ask for help in Rootstock communities. Join the Discord and Telegram communities using the following links:

Also, always go back to the Github repo: https://github.com/panditdhamdhere/Airdrop_contract

Roostock Discord: http://discord.gg/rootstock

Rootstock Telegram: Rootstock Official Group

Rootstock Docs: https://dev.rootstock.io/

I hope you now have a pretty good understanding of how you can develop and deploy contracts on the Rootstock network. Keep building and keep writing contracts, and keep deploying on the Rootstock network. With that, I am wrapping this up, and I will come back with another exciting tutorial.

8
Subscribe to my newsletter

Read articles from Pandit Dhamdhere directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Pandit Dhamdhere
Pandit Dhamdhere

01101101 01100001 01100100 01100101