Project Setup & Smart Contract Deployment
This page covers everything from scaffolding the project to deploying the InclusiveDeFi contract on Rootstock Testnet. By the end of this page, you will have a live contract address on Chain ID 31 that the relay server can interact with.
InclusiveDeFi is just a name of the contract; developers can change it as needed.
Prerequisites
Before starting, ensure you have the following:
- Node.js v18+
npmorpnpm- A funded Rootstock Testnet wallet. Get tRBTC from the Rootstock Testnet Faucet.
- Basic familiarity with Solidity and Hardhat is recommended.
Scaffolding the Project
This project uses Hardhat 3 Beta with the mocha + ethers toolbox. The folder structure is automatically generated by Hardhat's initializer, so no manual setup is required.
Run the following in your terminal to scaffold a new Hardhat 3 project:
mkdir ussd-rsk && cd ussd-rsk
npx hardhat --init
When prompted, select the TypeScript + mocha + ethers template. Hardhat will generate the base project structure:
ussd-rsk/
├── contracts/ ← Place your Solidity contracts here
├── ignition/
│ └── modules/ ← Hardhat Ignition deployment modules
├── test/ ← Mocha test files
├── hardhat.config.ts ← Network and compiler configuration
├── tsconfig.json
└── package.json
After scaffolding, install dependencies:
npm install
Then add the additional runtime dependencies needed for the relay server:
npm install express dotenv
npm install --save-dev @types/express tsx
npm install --save-dev @nomicfoundation/hardhat-toolbox-mocha-ethers
Your final package.json should look like this:
{
"name": "ussd-rsk",
"version": "1.0.0",
"type": "module",
"scripts": {
"start-bridge": "tsx index.ts"
},
"dependencies": {
"dotenv": "^17.3.1",
"express": "^5.2.1"
},
"devDependencies": {
"@nomicfoundation/hardhat-toolbox-mocha-ethers": "^3.0.3",
"@types/express": "^5.0.6",
"@types/node": "^22.19.11",
"chai": "^5.1.2",
"ethers": "^6.16.0",
"hardhat": "^3.1.11",
"tsx": "^4.21.0",
"typescript": "~5.8.0"
}
}
Environment Variables
Add a .env file to the project root with the following variables:
PRIVATE_KEY=your_relayer_wallet_private_key_here
RSK_TESTNET_RPC=https://rpc.rootstock.io/
Never commit .env to version control. It contains the relayer private key.
Add .env to your .gitignore immediately:
node_modules/
dist/
artifacts/
cache/
.env
Hardhat Configuration
Update hardhat.config.ts to include Rootstock(RSK) Testnet as a named HTTP network:
import hardhatToolboxMochaEthersPlugin from "@nomicfoundation/hardhat-toolbox-mocha-ethers";
import { configVariable, defineConfig } from "hardhat/config";
import * as dotenv from "dotenv";
dotenv.config();
export default defineConfig({
plugins: [hardhatToolboxMochaEthersPlugin],
solidity: {
profiles: {
default: {
version: "0.8.28",
},
production: {
version: "0.8.28",
settings: {
optimizer: { enabled: true, runs: 200 },
},
},
},
},
networks: {
// Local simulated networks for testing
hardhatMainnet: {
type: "edr-simulated",
chainType: "l1",
},
hardhatOp: {
type: "edr-simulated",
chainType: "op",
},
// RSK Testnet - Chain ID 31
rskTestnet: {
type: "http",
chainType: "l1",
url: "https://rpc.rootstock.io/",
chainId: 31,
accounts: [process.env.PRIVATE_KEY!],
},
},
});
Rootstock is fully EVM-compatible; therefore, set chainType: "l1". The public node at https://rpc.rootstock.io/ is rate-limited and intended for development only. Use a dedicated RPC endpoint for production environments.
The Smart Contract
Create contracts/InclusiveDeFi.sol, which serves as the on-chain core of the system. It maintains internal balance and loan state for all users who interact through the relay:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract InclusiveDeFi {
mapping(address => uint256) public balances;
mapping(address => uint256) public loans;
event Transfer(address indexed from, address indexed to, uint256 amount);
event LoanIssued(address indexed user, uint256 amount);
// P2P Transfer - moves balance between two internal accounts
function transfer(address to, uint256 amount) public {
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
balances[to] += amount;
emit Transfer(msg.sender, to, amount);
}
// Micro-Loan - issues 0.01 tRBTC, one active loan per address
function applyForLoan() public {
require(loans[msg.sender] == 0, "Existing loan active");
uint256 loanAmount = 0.01 ether;
loans[msg.sender] = loanAmount;
balances[msg.sender] += loanAmount;
emit LoanIssued(msg.sender, loanAmount);
}
// Read - returns internal balance for any address
function getBalance(address user) public view returns (uint256) {
return balances[user];
}
// Deposit tRBTC into the contract (used for seeding and testing)
function deposit() public payable {
balances[msg.sender] += msg.value;
}
}
Contract Design Notes
The contract uses internal accounting and does not implement ERC-20 or hold externally transferred tokens. All balances tracked in getBalance() are denominated in wei (tRBTC) and represent funds that have been deposited via deposit() or credited via applyForLoan().
The transfer() function moves value between two entries in the getBalance() mapping. It does not send native tRBTC but updates internal records. This is intentional, as it allows the relayer to call transfer() on behalf of any user without requiring the user to hold tRBTC for gas.
applyForLoan() credits the loan amount to the caller's getBalance() entry without requiring collateral. This is suitable for a demo only a production system must add a repayment flow along with either collateral requirements or a credit-scoring oracle before enabling this feature.
Ignition Deployment Module
Create ignition/modules/InclusiveDeFi.ts as the Hardhat Ignition module that describes how the contract should be deployed:
import { buildModule } from "@nomicfoundation/hardhat-ignition/modules";
const InclusiveDeFiModule = buildModule("InclusiveDeFiModule", (m) => {
const inclusiveDeFi = m.contract("InclusiveDeFi");
return { inclusiveDeFi };
});
export default InclusiveDeFiModule;
Deploying to Rootstock Testnet
Run the following command to deploy:
npx hardhat ignition deploy --network rskTestnet ignition/modules/InclusiveDeFi.ts
On success, Hardhat Ignition outputs the deployed contract address and writes deployment artifacts to ignition/deployments/chain-31/:
Deployed Addresses
InclusiveDeFiModule#InclusiveDeFi: 0xYourDeployedContractAddress
The deployed_addresses.json file under ignition/deployments/chain-31/ will also record this address for future reference.
Copy this contract address. You will need to paste it into CONTRACT_ADDRESS in index.ts before starting the relay server.
Verifying the Deployment
To verify your deployment, search for your contract address on the Rootstock Testnet Explorer to view the transaction, bytecode, and ABI. You can also inspect ignition/deployments/chain-31/journal.jsonl, a successful deployment entry ends with:
{
"futureId": "InclusiveDeFiModule#InclusiveDeFi",
"result": {
"address": "0xYourDeployedContractAddress",
"type": "SUCCESS"
},
"type": "DEPLOYMENT_EXECUTION_STATE_COMPLETE"
}
Next Steps
Once your contract is deployed to Rootstock Testnet, follow the Relay Server & Gateway Integration guide to build the USSD bridge for feature phone users.