본문으로 건너뛰기

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

PluginPurposeInstall
@nomicfoundation/hardhat-toolboxCore testing, ethers.js, verification, coverageIncluded above
hardhat-gas-reporterGas usage reports per functionIncluded above
hardhat-contract-sizerContract bytecode size analysisIncluded above
@openzeppelin/hardhat-upgradesProxy deployment and upgrade safetynpm 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/
Legacy Transactions Required

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

FeatureHardhatFoundry
LanguageTypeScript / JavaScriptSolidity
Package managernpm / yarnGit submodules
Compilation speedModerateFast
Test languageTypeScript (Mocha + Chai)Solidity (forge-std)
Fuzz testingVia pluginsBuilt-in
Gas snapshotsVia pluginBuilt-in (forge snapshot)
Debuggingconsole.log in SolidityExecution traces (-vvvv)
Forkingnpx hardhat node --forkforge test --fork-url
Coveragenpx hardhat coverageforge coverage
Contract verificationnpx hardhat verifyforge verify-contract
ScriptingJavaScript/TypeScriptSolidity (forge script)
EcosystemLarge npm plugin ecosystemLean, fast, Rust-based
Learning curveLower (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.