Build, Test and Deploy Giveaway smart contract on Rootstock Testnet using Foundry.


Hi Smart Contract Maxis, in this tutorial, we will build, test, and deploy the Giveaway smart contract on Rootstock testnet using the blazing fast smart contract development framework Foundry. So let’s get started, before moving ahead, let me give you a brief intro to the Rootstock network.
What is Rootstock Network?
Rootstock Network (RSK) is a smart contract platform that's built as a sidechain to the Bitcoin blockchain. Here are the key aspects of Rootstock to remember.
Bitcoin Sidechain: It's connected to the Bitcoin blockchain but allows for smart contract functionality that isn't natively available on Bitcoin.
Smart Bitcoin (RBTC): The native token of the Rootstock network is RBTC, which is pegged 1:1 with Bitcoin through a two-way peg mechanism.
Smart Contracts: Unlike Bitcoin's limited scripting capabilities, Rootstock is compatible with Ethereum's virtual machine (EVM), allowing developers to create and deploy smart contracts similar to those on Ethereum.
Merged Mining: Rootstock uses merged mining with Bitcoin, which means Bitcoin miners can mine on the Rootstock chain simultaneously without additional computational work.
DeFi on Bitcoin: Rootstock enables decentralized finance (DeFi) applications on top of Bitcoin's security, allowing for lending, borrowing, trading, and other financial services.
Security Model: It leverages Bitcoin's security through merged mining while adding its own security mechanisms for the sidechain.
Rootstock aims to bring smart contract functionality to the Bitcoin ecosystem while maintaining Bitcoin's security properties, essentially creating a way for Bitcoin to compete with platforms like Ethereum in terms of programmability and application development.
Well, hope you have a pretty good understanding of the RSK network now let’s set our development environment and start building our contract.
Setting Up 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
RSK Test faucets - https://faucet.rootstock.io/
I am assuming you have a basic understanding of Solidity smart contracts and Ethereum. With that let’s go ahead and start building our smart contract.
Let’s create a new empty directory called Giveaway
, open this directory in VS Code, and run the following command to initialize our Foundry project.
forge init
And boom, you will see your foundry project is ready with src
, test
, script
directories with some basic contracts. Go ahead and delete counter.sol
in src
directory, counter.t.sol
in test
directory and counter.s.sol
in script
directory. We are creating our brand new contracts.
Now, we are going to use Openzeppelin’s smart contracts library.
What is OpenZeppelin Library?
OpenZeppelin Contracts is a widely used library of secure, battle-tested smart contracts for Ethereum and other EVM-compatible blockchains. It provides developers with standardized 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 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
Amazing, you just installed OpenZeppelin library we still need to do one important task before moving ahead and building our smart contract and that is we have to set up our remappings.
What is this? Well, In Foundry, "remappings" are a configuration feature that simplifies import paths in your Solidity code. They provide shortcuts for referencing external libraries and contracts, making your import statements cleaner and more maintainable. After, all we're gonna do it like a professional smart contract developer. So follow along with me. To set our remappings run the following command.
forge remappings >> remappings.txt
It will automatically create a new remappings.txt
file in your project’s root with setting our remappings.
If you made this so far… you should be proud of yourself and we are ready to start writing our smart contract. For that let’s create a new directory called interfaces
in src
directory and within it create a file and name it IGiveawayEngine.sol
and paste the following Solidity code into this file.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface IGiveawayEngine {
struct Giveaway {
address tokenAddress;
uint256 amount;
uint256 startTime;
uint256 endTime;
bool isActive;
uint256 totalClaimed;
uint256 remainingAmount;
mapping(address => bool) hasClaimed;
}
event GiveawayCreated(
uint256 indexed giveawayId,
address tokenAddress,
uint256 amount
);
event GiveawayClaimed(
uint256 indexed giveawayId,
address indexed user,
uint256 amount
);
event GiveawayExpired(uint256 indexed giveawayId);
event GiveawayClosed(uint256 indexed giveawayId, uint256 remainingAmount);
function createGiveaway(
address _tokenAddress,
uint256 _amount,
uint256 _duration
) external returns (uint256);
function claimGiveaway(uint256 _giveawayId) external;
function getGiveaway(
uint256 _giveawayId
)
external
view
returns (
address tokenAddress,
uint256 amount,
uint256 startTime,
uint256 endTime,
bool isActive
);
function hasUserClaimed(
uint256 _giveawayId,
address _user
) external view returns (bool);
}
Great, we created an interface for our smart contract. If you did not get what we created what what is this interface shit? Then let me explain to you in a nutshell, In smart contracts, an interface is a special type of contract declaration that defines a set of functions without implementing them. Interfaces serve as a blueprint or contract specifications that other contracts can implement. that is what we created.
Let’s build our actual smart contract now, go ahead and create our main beast smart contract. In src
directory create a new file, name it GiveawayEngine.sol
, and paste the following code (make sure you are creating a file in src
directory and not interfaces).
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
// ⚠️ warning - Don't use this code in production.
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {IGiveawayEngine} from "./interfaces/IGiveawayEngine.sol";
/// @title GiveawayEngine
/// @author -- your name eg - pandit
/// @notice A smart contract that manages token giveaways on the Rootstock blockchain
/// @dev Implements the IGiveawayEngine interface and inherits from Ownable
/// @custom:security-contact security@rootstock.io
contract GiveawayEngine is IGiveawayEngine, Ownable {
/// @notice Mapping of giveaway ID to Giveaway struct
mapping(uint256 => Giveaway) public giveaways;
/// @notice Counter for total number of giveaways created
uint256 public giveawayCount;
/// @notice Initializes the contract and sets the deployer as the owner
constructor() Ownable(msg.sender) {}
/// @notice Creates a new token giveaway
/// @dev Only callable by the contract owner
/// @param _tokenAddress The address of the ERC20 token to be given away
/// @param _amount The amount of tokens to be given away
/// @param _duration The duration of the giveaway in seconds
/// @return The ID of the newly created giveaway
/// @custom:throws "Invalid token address" if token address is zero address
/// @custom:throws "Amount must be greater than 0" if amount is zero
/// @custom:throws "Duration must be greater than 0" if duration is zero
/// @custom:throws "Token transfer failed" if token transfer from owner fails
function createGiveaway(
address _tokenAddress,
uint256 _amount,
uint256 _duration
) external override onlyOwner returns (uint256) {
require(_tokenAddress != address(0), "Invalid token address");
require(_amount > 0, "Amount must be greater than 0");
require(_duration > 0, "Duration must be greater than 0");
uint256 giveawayId = giveawayCount++;
Giveaway storage giveaway = giveaways[giveawayId];
giveaway.tokenAddress = _tokenAddress;
giveaway.amount = _amount;
giveaway.startTime = block.timestamp;
giveaway.endTime = block.timestamp + _duration;
giveaway.isActive = true;
giveaway.totalClaimed = 0;
giveaway.remainingAmount = _amount;
// Transfer tokens from the owner to the contract
IERC20 token = IERC20(_tokenAddress);
require(
token.transferFrom(msg.sender, address(this), _amount),
"Token transfer failed"
);
emit GiveawayCreated(giveawayId, tokenAddress, amount);
return giveawayId;
}
/// @notice Allows users to claim their share of a giveaway
/// @dev Transfers tokens directly to the claimer's address
/// @param _giveawayId The ID of the giveaway to claim
/// @custom:throws "Giveaway is not active" if giveaway is inactive
/// @custom:throws "Giveaway has expired" if current time is past end time
/// @custom:throws "Already claimed" if user has already claimed this giveaway
/// @custom:throws "Token transfer failed" if token transfer to claimer fails
function claimGiveaway(uint256 _giveawayId) external override {
Giveaway storage giveaway = giveaways[_giveawayId];
// Update giveaway status if it has expired
if (block.timestamp > giveaway.endTime) {
giveaway.isActive = false;
revert("Giveaway has expired");
}
require(giveaway.isActive, "Giveaway is not active");
require(!giveaway.hasClaimed[msg.sender], "Already claimed");
giveaway.hasClaimed[msg.sender] = true;
giveaway.totalClaimed++;
// Calculate claim amount based on total claims
uint256 claimAmount;
if (giveaway.totalClaimed == 1) {
claimAmount = giveaway.amount;
} else if (giveaway.totalClaimed == 2) {
claimAmount = giveaway.amount / 2;
} else if (giveaway.totalClaimed == 3) {
claimAmount = giveaway.amount / 3;
} else {
claimAmount = giveaway.amount / giveaway.totalClaimed;
}
IERC20 token = IERC20(giveaway.tokenAddress);
uint256 balance = token.balanceOf(address(this));
require(balance >= claimAmount, "No tokens left to claim");
bool success = token.transfer(msg.sender, claimAmount);
require(success, "Token transfer failed");
emit GiveawayClaimed(_giveawayId, msg.sender, claimAmount);
}
/// @notice Allows the owner to close an expired giveaway and withdraw remaining tokens
/// @dev Only callable by the contract owner
/// @param _giveawayId The ID of the giveaway to close
/// @custom:throws "Giveaway is still active" if giveaway hasn't expired
/// @custom:throws "No tokens to withdraw" if all tokens have been claimed
function closeGiveaway(uint256 _giveawayId) external onlyOwner {
Giveaway storage giveaway = giveaways[_giveawayId];
require(block.timestamp > giveaway.endTime, "Giveaway is still active");
giveaway.isActive = false;
IERC20 token = IERC20(giveaway.tokenAddress);
uint256 remainingBalance = token.balanceOf(address(this));
require(remainingBalance > 0, "No tokens to withdraw");
require(
token.transfer(owner(), remainingBalance),
"Token transfer failed"
);
emit GiveawayClosed(_giveawayId, remainingBalance);
}
/// @notice Retrieves the details of a specific giveaway
/// @param _giveawayId The ID of the giveaway to query
/// @return tokenAddress The address of the token being given away
/// @return amount The amount of tokens in the giveaway
/// @return startTime The timestamp when the giveaway started
/// @return endTime The timestamp when the giveaway ends
/// @return isActive Whether the giveaway is currently active
function getGiveaway(
uint256 _giveawayId
)
external
view
override
returns (
address tokenAddress,
uint256 amount,
uint256 startTime,
uint256 endTime,
bool isActive
)
{
Giveaway storage giveaway = giveaways[_giveawayId];
return (
giveaway.tokenAddress,
giveaway.amount,
giveaway.startTime,
giveaway.endTime,
giveaway.isActive
);
}
/// @notice Checks if a user has claimed a specific giveaway
/// @param _giveawayId The ID of the giveaway to check
/// @param _user The address of the user to check
/// @return True if the user has claimed the giveaway, false otherwise
function hasUserClaimed(
uint256 _giveawayId,
address _user
) external view override returns (bool) {
return giveaways[_giveawayId].hasClaimed[_user];
}
}
Great. For a better understanding of smart contract. I added comments explaining everything about the smart contract. We just finished writing smart contract and let’s see our contract compiles correctly without any errors. Run the following command.
forge build
If you see the Compiler run successful! like the above image in green, then congratulations, you have successfully built the contract. Amazing now it’s time to write some badass tests and check if our contract is behaving as we expected. Go ahead and create a new file in test
directory and name it GiveawayEngine.t.sol
and paste the following test contract into this file.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "../src/GiveawayEngine.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MockERC20 is ERC20 {
constructor() ERC20("Mock Token", "MOCK") {
_mint(msg.sender, 1000000 10 * decimals());
}
function mint(address to, uint256 amount) public {
_mint(to, amount);
}
}
contract GiveawayEngineTest is Test {
GiveawayEngine public giveaway;
MockERC20 public token;
address public owner = address(1);
address public user1 = address(2);
address public user2 = address(3);
address public user3 = address(4);
function setUp() public {
vm.startPrank(owner);
token = new MockERC20();
giveaway = new GiveawayEngine();
vm.stopPrank();
}
function testCreateGiveaway() public {
vm.startPrank(owner);
token.approve(address(giveaway), 1000);
uint256 giveawayId = giveaway.createGiveaway(address(token), 1000, 3600);
(address tokenAddress, uint256 amount, , , bool isActive) = giveaway.getGiveaway(giveawayId);
assertEq(tokenAddress, address(token));
assertEq(amount, 1000);
assertTrue(isActive);
}
function testMultipleClaimers() public {
vm.startPrank(owner);
token.approve(address(giveaway), 2000);
uint256 giveawayId = giveaway.createGiveaway(address(token), 1000, 3600);
// Mint extra tokens to the contract for subsequent claims
token.mint(address(giveaway), 1000);
vm.stopPrank();
// First claim
vm.startPrank(user1);
giveaway.claimGiveaway(giveawayId);
assertEq(token.balanceOf(user1), 1000);
vm.stopPrank();
// Second claim
vm.startPrank(user2);
giveaway.claimGiveaway(giveawayId);
assertEq(token.balanceOf(user2), 500);
vm.stopPrank();
// Third claim
vm.startPrank(user3);
giveaway.claimGiveaway(giveawayId);
assertEq(token.balanceOf(user3), 333);
vm.stopPrank();
}
function testEmptyBalance() public {
vm.startPrank(owner);
token.approve(address(giveaway), 1000);
uint256 giveawayId = giveaway.createGiveaway(address(token), 1000, 3600);
vm.stopPrank();
// First claim
vm.startPrank(user1);
giveaway.claimGiveaway(giveawayId);
assertEq(token.balanceOf(user1), 1000);
vm.stopPrank();
// Try to claim again with empty balance
vm.startPrank(user2);
vm.expectRevert("No tokens left to claim");
giveaway.claimGiveaway(giveawayId);
vm.stopPrank();
}
function testCancellation() public {
vm.startPrank(owner);
token.approve(address(giveaway), 1000);
uint256 giveawayId = giveaway.createGiveaway(address(token), 1000, 3600);
vm.stopPrank();
// Fast forward time to expire the giveaway
vm.warp(block.timestamp + 3601);
// Close the giveaway and withdraw remaining tokens
vm.startPrank(owner);
giveaway.closeGiveaway(giveawayId);
assertEq(token.balanceOf(owner), 1000000 10 * token.decimals());
vm.stopPrank();
// Try to claim after cancellation
vm.startPrank(user1);
vm.expectRevert("Giveaway has expired");
giveaway.claimGiveaway(giveawayId);
vm.stopPrank();
}
function testDoubleClaim() public {
vm.startPrank(owner);
token.approve(address(giveaway), 1000);
uint256 giveawayId = giveaway.createGiveaway(address(token), 1000, 3600);
vm.stopPrank();
// First claim
vm.startPrank(user1);
giveaway.claimGiveaway(giveawayId);
vm.stopPrank();
// Try to claim again
vm.startPrank(user1);
vm.expectRevert("Already claimed");
giveaway.claimGiveaway(giveawayId);
vm.stopPrank();
}
function testExpiredGiveaway() public {
vm.startPrank(owner);
token.approve(address(giveaway), 1000);
uint256 giveawayId = giveaway.createGiveaway(address(token), 1000, 3600);
vm.stopPrank();
// Fast forward time
vm.warp(block.timestamp + 3601);
// Try to claim after expiration
vm.startPrank(user1);
vm.expectRevert("Giveaway has expired");
giveaway.claimGiveaway(giveawayId);
vm.stopPrank();
}
function testCloseActiveGiveaway() public {
vm.startPrank(owner);
token.approve(address(giveaway), 1000);
uint256 giveawayId = giveaway.createGiveaway(address(token), 1000, 3600);
vm.stopPrank();
// Try to close an active giveaway
vm.startPrank(owner);
vm.expectRevert("Giveaway is still active");
giveaway.closeGiveaway(giveawayId);
vm.stopPrank();
}
function testCloseEmptyGiveaway() public {
vm.startPrank(owner);
token.approve(address(giveaway), 1000);
uint256 giveawayId = giveaway.createGiveaway(address(token), 1000, 3600);
vm.stopPrank();
// Claim all tokens
vm.startPrank(user1);
giveaway.claimGiveaway(giveawayId);
vm.stopPrank();
// Fast forward time
vm.warp(block.timestamp + 3601);
// Try to close giveaway with no remaining tokens
vm.startPrank(owner);
vm.expectRevert("No tokens to withdraw");
giveaway.closeGiveaway(giveawayId);
vm.stopPrank();
}
}
Incredible, then run the following command
forge test
You will see all your 8 tests passing in your terminal.
Amazing, this is so amazing, you just wrote and tested the Giveaway Solidity smart contract, and now it’s time to make our smart contract ready to deploy on RSK (Rootstock test network).
Let’s create a new file in script
directory and name it Deploy.s.sol
and paste the following solidity script into this file.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Script, console} from "forge-std/Script.sol";
import {GiveawayEngine} from "../src/GiveawayEngine.sol";
contract DeployScript is Script {
function run() external {
// Start broadcasting transactions
vm.startBroadcast();
// Deploy the GiveawayEngine contract
GiveawayEngine giveawayEngine = new GiveawayEngine();
// Log the deployed address
console.log("GiveawayEngine deployed at:", address(giveawayEngine));
// Stop broadcasting transactions
vm.stopBroadcast();
}
}
Amazing!! We finished writing our deploy script. But before going ahead and deploying our contract we need to finish some important tasks. Go inside your foundry.toml
file and set evm version to London.
Your foundry.toml
file should look like this:
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
evm_version = "london"
[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
Great, now create a .env
file in your projects root and declare 2 environment variables as follows:
PRIVATE_KEY="0x_YOUR_PRIVATE_KEY"
RPC_URL="https://public-node.testnet.rsk.co"
Go to MetaMask - account details - show private key - enter your password and copy the private key and paste it right front of PRIVATE_KEY
variable we declared in our .env
file.
Great work.
At this point, your projects folder structure looks like this (see image below)
We are ready to deploy our smart contract on the Rootstock test network but before going ahead ensure you have enough faucets (test tokens) to pay the gas fees of our deployment on chain. Let’s grab this.
Go to https://faucet.rootstock.io/ open your MetaMask copy your address, paste it there complete the captcha, and wait a couple of moments and your wallet automatically fill up with some tRBTC which will look like this.
Awesome. That is enough to deploy our contract. Now we are all set and ready for deployment.
To deploy our smart contract go back to vs code open your terminal, make sure you’re in projects root and run the following command
forge script script/Deploy.s.sol --rpc-url $RPC_URL --private-key $PRIVATE_KEY --broadcast --legacy
And BOOOOOMMMMMM! You will see output like this in our terminal. Your contract address might be different than this.
To check our contract is indeed deployed on Rootstock testnet copy the contract address from your terminal, go to https://explorer.testnet.rootstock.io/, paste the contract address in the search bar, and hit enter. You will get all the information related to your deployment here on Block Explorer.
Massive congratulations on writing, testing, and deploying a smart contract using foundry on the Rootstock Test network.
Hopefully, you enjoyed this tutorial and learned how to deploy a contract on the Rootstock network. If you are stuck anywhere don’t hesitate to ask help in the Rootstock community.
Join the community:
Discord: https://discord.gg/QRw2XCE6m7
Telegram: @rskofficialcommunity
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 00100000 01111001 01101111 01110101 00100000 01101100 01101111 01101111 01101011