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


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.
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