Hardhat & Foundry
This guide covers full configuration, testing, debugging, and CI/CD integration for both Hardhat and Foundry on Monolythium. Both frameworks are fully supported; choose the one that fits your workflow.
Hardhat Setup
Installation
mkdir my-project && cd my-project
npm init -y
npm install --save-dev \
hardhat \
@nomicfoundation/hardhat-toolbox \
hardhat-gas-reporter \
hardhat-contract-sizer \
dotenv
npx hardhat init
Select Create a TypeScript project when prompted.
Configuration
Create a .env file (add to .gitignore):
PRIVATE_KEY=your_private_key_here
TESTNET_RPC_URL=https://evm.testnet.mononodes.xyz
MAINNET_RPC_URL=https://evm.mainnet.mononodes.xyz
Full hardhat.config.ts:
import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
import "hardhat-gas-reporter";
import "hardhat-contract-sizer";
import "dotenv/config";
const config: HardhatUserConfig = {
solidity: {
version: "0.8.19",
settings: {
optimizer: {
enabled: true,
runs: 200,
},
evmVersion: "paris",
},
},
networks: {
"monolythium-testnet": {
url: process.env.TESTNET_RPC_URL || "https://evm.testnet.mononodes.xyz",
chainId: 6940,
accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
},
"monolythium-mainnet": {
url: process.env.MAINNET_RPC_URL || "https://evm.mainnet.mononodes.xyz",
chainId: 6941,
accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
},
},
etherscan: {
apiKey: {
"monolythium-testnet": "",
"monolythium-mainnet": "",
},
customChains: [
{
network: "monolythium-testnet",
chainId: 6940,
urls: {
apiURL: "https://testnet.monoscan.xyz/api",
browserURL: "https://testnet.monoscan.xyz",
},
},
{
network: "monolythium-mainnet",
chainId: 6941,
urls: {
apiURL: "https://mainnet.monoscan.xyz/api",
browserURL: "https://mainnet.monoscan.xyz",
},
},
],
},
gasReporter: {
enabled: process.env.REPORT_GAS === "true",
currency: "USD",
},
contractSizer: {
alphaSort: true,
runOnCompile: false,
},
};
export default config;
Essential Plugins
| Plugin | Purpose | Install |
|---|---|---|
@nomicfoundation/hardhat-toolbox | Core testing, ethers.js, verification, coverage | Included above |
hardhat-gas-reporter | Gas usage reports per function | Included above |
hardhat-contract-sizer | Contract bytecode size analysis | Included above |
@openzeppelin/hardhat-upgrades | Proxy deployment and upgrade safety | npm install --save-dev @openzeppelin/hardhat-upgrades |
Foundry Setup
Installation
curl -L https://foundry.paradigm.xyz | bash
foundryup
forge init my-project
cd my-project
Configuration
Full foundry.toml:
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
solc_version = "0.8.19"
optimizer = true
optimizer_runs = 200
evm_version = "paris"
gas_reports = ["*"]
ffi = false
[profile.default.fmt]
line_length = 120
tab_width = 4
bracket_spacing = true
[rpc_endpoints]
monolythium_testnet = "https://evm.testnet.mononodes.xyz"
monolythium_mainnet = "https://evm.mainnet.mononodes.xyz"
[etherscan]
monolythium_testnet = { key = "", url = "https://testnet.monoscan.xyz/api", chain = 6940 }
monolythium_mainnet = { key = "", url = "https://mainnet.monoscan.xyz/api", chain = 6941 }
Remappings
If using OpenZeppelin or other dependencies:
forge install OpenZeppelin/openzeppelin-contracts
Create remappings.txt:
@openzeppelin/=lib/openzeppelin-contracts/
Monolythium uses the paris EVM version. You must use the --legacy flag for all on-chain Foundry commands (forge create, forge script, cast send). Without it, transactions will fail because EIP-1559 dynamic fee transactions are not supported.
Testing with Hardhat
Example Test
// test/MyToken.test.ts
import { expect } from "chai";
import { ethers } from "hardhat";
import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers";
describe("MyToken", function () {
async function deployFixture() {
const [owner, alice, bob] = await ethers.getSigners();
const supply = ethers.parseEther("1000000");
const factory = await ethers.getContractFactory("MyToken");
const token = await factory.deploy(supply);
await token.waitForDeployment();
return { token, owner, alice, bob, supply };
}
describe("Deployment", function () {
it("should assign total supply to owner", async function () {
const { token, owner, supply } = await loadFixture(deployFixture);
expect(await token.balanceOf(owner.address)).to.equal(supply);
});
it("should set the correct name and symbol", async function () {
const { token } = await loadFixture(deployFixture);
expect(await token.name()).to.equal("MyToken");
expect(await token.symbol()).to.equal("MTK");
});
});
describe("Transfers", function () {
it("should transfer tokens between accounts", async function () {
const { token, owner, alice } = await loadFixture(deployFixture);
const amount = ethers.parseEther("100");
await token.transfer(alice.address, amount);
expect(await token.balanceOf(alice.address)).to.equal(amount);
});
it("should emit Transfer event", async function () {
const { token, owner, alice } = await loadFixture(deployFixture);
const amount = ethers.parseEther("100");
await expect(token.transfer(alice.address, amount))
.to.emit(token, "Transfer")
.withArgs(owner.address, alice.address, amount);
});
it("should revert on insufficient balance", async function () {
const { token, alice, bob } = await loadFixture(deployFixture);
await expect(
token.connect(alice).transfer(bob.address, 1n)
).to.be.reverted;
});
});
});
Run Tests
# Run all tests
npx hardhat test
# Run with gas reporting
REPORT_GAS=true npx hardhat test
# Run a specific test file
npx hardhat test test/MyToken.test.ts
# Run tests matching a pattern
npx hardhat test --grep "transfer"
Coverage
npx hardhat coverage
This generates a coverage report in coverage/index.html. Aim for high coverage on critical paths (token transfers, access control, state changes).
Testing with Foundry
Example Test
// test/MyToken.t.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "../src/MyToken.sol";
contract MyTokenTest is Test {
MyToken token;
address owner = address(this);
address alice = address(0x1);
address bob = address(0x2);
uint256 constant SUPPLY = 1_000_000 ether;
function setUp() public {
token = new MyToken(SUPPLY);
}
function test_InitialSupply() public view {
assertEq(token.totalSupply(), SUPPLY);
assertEq(token.balanceOf(owner), SUPPLY);
}
function test_Transfer() public {
uint256 amount = 100 ether;
token.transfer(alice, amount);
assertEq(token.balanceOf(alice), amount);
assertEq(token.balanceOf(owner), SUPPLY - amount);
}
function test_TransferEvent() public {
uint256 amount = 100 ether;
vm.expectEmit(true, true, false, true);
emit MyToken.Transfer(owner, alice, amount);
token.transfer(alice, amount);
}
function testFail_TransferInsufficientBalance() public {
vm.prank(alice);
token.transfer(bob, 1);
}
}
Fuzz Testing
Foundry excels at property-based fuzz testing:
function testFuzz_Transfer(uint256 amount) public {
// Bound the amount to the owner's balance
amount = bound(amount, 0, token.balanceOf(owner));
uint256 ownerBefore = token.balanceOf(owner);
uint256 aliceBefore = token.balanceOf(alice);
token.transfer(alice, amount);
assertEq(token.balanceOf(owner), ownerBefore - amount);
assertEq(token.balanceOf(alice), aliceBefore + amount);
}
Run Tests
# Run all tests
forge test
# Verbose output (show logs)
forge test -vv
# Very verbose (show traces for failing tests)
forge test -vvv
# Maximum verbosity (show all traces)
forge test -vvvv
# Run a specific test
forge test --match-test test_Transfer
# Run a specific contract
forge test --match-contract MyTokenTest
Gas Snapshots
# Create a gas snapshot
forge snapshot
# Compare against a previous snapshot
forge snapshot --diff
The snapshot file (.gas-snapshot) records gas usage per test. Commit it to your repository to track gas changes over time.
Debugging
Hardhat: console.log
Import hardhat/console.sol to add logging to your Solidity contracts during development:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "hardhat/console.sol";
contract MyContract {
function deposit() external payable {
console.log("Deposit from %s: %s", msg.sender, msg.value);
// ... contract logic
}
}
Logs appear in the terminal when running tests. Remove console.log statements before deploying to production to save gas.
Foundry: Traces
Run tests with maximum verbosity to see full execution traces:
forge test -vvvv --match-test test_Transfer
Output includes every opcode, storage access, and internal call:
[PASS] test_Transfer() (gas: 52391)
Traces:
[52391] MyTokenTest::test_Transfer()
├─ [29543] MyToken::transfer(0x0000...0001, 100000000000000000000)
│ ├─ emit Transfer(from: MyTokenTest, to: 0x0000...0001, amount: 100000000000000000000)
│ └─ ← true
├─ [540] MyToken::balanceOf(0x0000...0001) [staticcall]
│ └─ ← 100000000000000000000
└─ ← ()
Forking Testnet
Test against live testnet state:
# Hardhat fork
npx hardhat node --fork https://evm.testnet.mononodes.xyz
# Foundry fork
forge test --fork-url https://evm.testnet.mononodes.xyz
CI/CD Integration
GitHub Actions: Hardhat
# .github/workflows/test-hardhat.yml
name: Hardhat Tests
on:
push:
branches: [dev, prod]
pull_request:
branches: [dev, prod]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Compile contracts
run: npx hardhat compile
- name: Run tests
run: npx hardhat test
env:
REPORT_GAS: "true"
- name: Check contract sizes
run: npx hardhat size-contracts
- name: Run coverage
run: npx hardhat coverage
GitHub Actions: Foundry
# .github/workflows/test-foundry.yml
name: Foundry Tests
on:
push:
branches: [dev, prod]
pull_request:
branches: [dev, prod]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly
- name: Build
run: forge build
- name: Run tests
run: forge test -vvv
- name: Check gas snapshots
run: forge snapshot --check
- name: Run coverage
run: forge coverage
Comparison Table
| Feature | Hardhat | Foundry |
|---|---|---|
| Language | TypeScript / JavaScript | Solidity |
| Package manager | npm / yarn | Git submodules |
| Compilation speed | Moderate | Fast |
| Test language | TypeScript (Mocha + Chai) | Solidity (forge-std) |
| Fuzz testing | Via plugins | Built-in |
| Gas snapshots | Via plugin | Built-in (forge snapshot) |
| Debugging | console.log in Solidity | Execution traces (-vvvv) |
| Forking | npx hardhat node --fork | forge test --fork-url |
| Coverage | npx hardhat coverage | forge coverage |
| Contract verification | npx hardhat verify | forge verify-contract |
| Scripting | JavaScript/TypeScript | Solidity (forge script) |
| Ecosystem | Large npm plugin ecosystem | Lean, fast, Rust-based |
| Learning curve | Lower (JavaScript familiarity) | Moderate (Solidity-only tests) |
Many teams use both: Foundry for fast compilation, fuzz testing, and gas optimization; Hardhat for deployment scripts, plugin integrations, and frontend tooling.
Important Notes
Legacy Transactions
Monolythium uses the paris EVM version and does not support EIP-1559 base fee mechanics. For all on-chain interactions with Foundry, use the --legacy flag:
# Deploy
forge create --rpc-url monolythium_testnet \
--private-key $PRIVATE_KEY \
--legacy \
src/MyToken.sol:MyToken \
--constructor-args 1000000000000000000000000
# Send a transaction
cast send --rpc-url https://evm.testnet.mononodes.xyz \
--private-key $PRIVATE_KEY \
--legacy \
<contract-address> "transfer(address,uint256)" 0x... 1000000000000000000
# Run a deployment script
forge script script/Deploy.s.sol \
--rpc-url monolythium_testnet \
--broadcast \
--legacy
Hardhat automatically handles transaction type negotiation, so no extra flags are needed.
Gas Prices
The minimum gas price on Monolythium is 10000000000 wei (10 gwei equivalent in alyth terms). If you encounter "insufficient fee" errors, ensure your gas price meets this minimum:
# Foundry: set gas price explicitly
forge create --rpc-url monolythium_testnet \
--private-key $PRIVATE_KEY \
--legacy \
--gas-price 10000000000 \
src/MyToken.sol:MyToken
// Hardhat: override gas price in network config
networks: {
"monolythium-testnet": {
url: "https://evm.testnet.mononodes.xyz",
chainId: 6940,
accounts: [process.env.PRIVATE_KEY!],
gasPrice: 10000000000,
},
},
EVM Version
Always set evmVersion / evm_version to paris in your compiler settings. Using a later EVM version (e.g., shanghai) will produce bytecode with opcodes that Monolythium does not support, causing deployment failures.
Related
- Deploying Contracts -- Smart contract deployment guide
- Contract Verification -- Source code verification on Monoscan
- RPC Endpoints -- Network endpoint reference
- Gas Fees -- Fee model and gas pricing