Blockchain Development Tooling: Foundry, Hardhat, Testing & CI/CD - Blockchain Development Hub

Foundry (Forge, Cast, Anvil, Chisel) Installation & Setup # Install curl -L https://foundry.paradigm.xyz | bash foundryup # Project structure forge init my-project # my-project/ # ├── src/ # Contracts # ├── test/ # Tests (Solidity) # ├── script/ # Deployment scripts # ├── lib/ # Dependencies (git submodules) # ├── foundry.toml # Config # └── remappings.txt # Import remappings Forge: Testing & Building # Build forge build # Test (fast, parallel, fuzzing) forge test # Run all tests forge test -vvvv # Verbose (logs, traces) forge test --match-test testDeposit # Specific test forge test --fork-url $RPC_URL # Fork mainnet forge test --fork-block-number 19000000 # Specific block # Fuzzing forge test --fuzz-runs 10000 # Invariant testing forge test --invariant-runs 100 --invariant-depth 1000 # Gas report forge test --gas-report # Coverage forge coverage forge coverage --report lcov # For CI Cast: RPC Swiss Army Knife # Block queries cast block 19000000 --rpc-url $RPC_URL cast block-number --rpc-url $RPC_URL # Transaction queries cast tx 0x... --rpc-url $RPC_URL cast receipt 0x... --rpc-url $RPC_URL # Contract interaction cast call 0xContract "balanceOf(address)" 0xUser --rpc-url $RPC_URL cast send 0xContract "transfer(address,uint256)" 0xTo 100 --rpc-url $RPC_URL --private-key $PK # ABI encoding cast calldata "transfer(address,uint256)" 0xTo 100 cast abi-encode "f(uint256,string)" 123 "hello" # Conversions cast --to-bytes32 "hello" cast --from-wei 1000000000000000000 cast --to-wei 1 ether cast --to-dec 0x123 cast --to-hex 123 # Wallet utilities cast wallet new cast wallet address --private-key $PK cast wallet sign "message" --private-key $PK cast wallet verify "message" 0xSig --address 0xAddr Anvil: Local Testnet # Start local node anvil # Default: 10 accounts, 10000 ETH each, chainId 31337 # Fork mainnet anvil --fork-url $RPC_URL --fork-block-number 19000000 # Chain ID: 31337 (or specify --chain-id 1 for mainnet fork) # Custom configuration anvil --accounts 20 --balance 10000 --block-time 2 --gas-limit 30000000 # Impersonate account (for testing) cast rpc anvil_impersonateAccount 0xWhale --rpc-url http://localhost:8545 cast send 0xContract "function()" --from 0xWhale --unlocked --rpc-url http://localhost:8545 # Set storage cast rpc anvil_setStorageAt 0xContract 0xSlot 0xValue --rpc-url http://localhost:8545 # Snapshot & revert cast rpc evm_snapshot --rpc-url http://localhost:8545 cast rpc evm_revert 0xSnapshotId --rpc-url http://localhost:8545 Chisel: Solidity REPL # Interactive Solidity shell chisel # Commands !help # Help !chains # List chains !fork # Fork mainnet !contracts # List deployed contracts !clear # Clear state # Usage ➜ uint256 x = 123 ➜ x * 2 Result: 246 ➜ address addr = 0x123... ➜ addr.balance Result: 10000000000000000000000 foundry.toml Configuration [profile.default] src = "src" out = "out" libs = ["lib"] test = "test" script = "script # Solidity version solc_version = "0.8.20" # Optimizer optimizer = true optimizer_runs = 200 # Via IR (better optimization) via_ir = true # Remappings remappings = [ "@openzeppelin/=lib/openzeppelin-contracts/", "forge-std/=lib/forge-std/src/", "ds-test/=lib/ds-test/src/" ] # Fuzz settings [fuzz] runs = 10000 max_test_rejection = 10000 # Invariant settings [invariant] runs = 100 depth = 1000 fail_on_revert = false # Gas reports [gas_reports] enabled = true ignore = ["forge-std/"] # Etherscan verification [etherscan] api_key = "${ETHERSCAN_API_KEY}" chain_ids = [1, 5, 11155111, 137, 80001, 42161, 421613, 10, 420, 8453, 84531] Hardhat + TypeScript Setup # Install npm init -y npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox npx hardhat init # Project structure # hardhat.config.ts # contracts/ # Solidity contracts # test/ # TypeScript tests # scripts/ # Deployment scripts # ignition/ # Hardhat Ignition modules # typechain-types/ # Generated types hardhat.config.ts import { HardhatUserConfig } from "hardhat/config"; import "@nomicfoundation/hardhat-toolbox"; import "hardhat-gas-reporter"; import "solidity-coverage"; import "hardhat-deploy"; import "hardhat-contract-sizer"; import * as dotenv from "dotenv"; dotenv.config(); const config: HardhatUserConfig = { solidity: { version: "0.8.20", settings: { optimizer: { enabled: true, runs: 200 }, viaIR: true, }, }, networks: { hardhat: { forking: { url: process.env.MAINNET_RPC_URL || "", blockNumber: 19000000, }, chainId: 31337, allowUnlimitedContractSize: true, }, mainnet: { url: process.env.MAINNET_RPC_URL || "", accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [], chainId: 1, }, sepolia: { url: process.env.SEPOLIA_RPC_URL || "", accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [], chainId: 11155111, }, arbitrum: { url: process.env.ARBITRUM_RPC_URL || "", accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [], chainId: 42161, }, base: { url: process.env.BASE_RPC_URL || "", accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [], chainId: 8453, }, }, gasReporter: { enabled: true, currency: "USD", coinmarketcap: process.env.COINMARKETCAP_API_KEY, excludeContracts: ["forge-std", "mock"], }, etherscan: { apiKey: { mainnet: process.env.ETHERSCAN_API_KEY || "", sepolia: process.env.ETHERSCAN_API_KEY || "", arbitrum: process.env.ARBISCAN_API_KEY || "", base: process.env.BASESCAN_API_KEY || "", }, }, contractSizer: { alphaSort: true, runOnCompile: true, disambiguatePaths: false, }, typechain: { outDir: "typechain-types", target: "ethers-v6", }, }; export default config; TypeScript Testing // test/MyContract.ts import { expect } from "chai"; import { ethers, upgrades } from "hardhat"; import { Signer } from "ethers"; import { MyContract, ERC20Mock } from "../typechain-types"; describe("MyContract", () => { let contract: MyContract; let token: ERC20Mock; let owner: Signer, alice: Signer, bob: Signer; beforeEach(async () => { [owner, alice, bob] = await ethers.getSigners(); // Deploy mock ERC20 const Token = await ethers.getContractFactory("ERC20Mock"); token = await Token.deploy("Test Token", "TEST", 18); await token.mint(await alice.getAddress(), ethers.parseEther("10000")); await token.mint(await bob.getAddress(), ethers.parseEther("10000")); // Deploy contract const Contract = await ethers.getContractFactory("MyContract"); contract = await Contract.deploy(token.target, ethers.parseEther("1000")); await token.connect(alice).approve(contract.target, ethers.MaxUint256); }); it("should deposit and track balance", async () => { await contract.connect(alice).deposit(ethers.parseEther("100")); expect(await contract.balances(await alice.getAddress())).to.equal(ethers.parseEther("100")); }); it("should revert on withdraw exceeding limit", async () => { await contract.connect(alice).deposit(ethers.parseEther("100")); await expect( contract.connect(alice).withdraw(ethers.parseEther("2000")) ).to.be.revertedWithCustomError(contract, "AmountExceedsLimit"); }); }); Hardhat Ignition (Declarative Deployment) // ignition/modules/MyContract.ts import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; const MyContractModule = buildModule("MyContractModule", (m) => { const token = m.contract("ERC20Mock", ["Test Token", "TEST", 18]); const contract = m.contract("MyContract", [token, ethers.parseEther("1000")]); // Mint tokens to deployer m.call(token, "mint", [m.getAccount(0), ethers.parseEther("100000")]); // Approve contract m.call(token, "approve", [contract, ethers.MaxUint256]); return { contract, token }; }); export default MyContractModule; // Deploy npx hardhat ignition deploy ignition/modules/MyContract.ts --network sepolia Advanced Testing Strategies Fuzzing (Property-Based Testing) // Foundry: automatic input generation function testFuzzDeposit(uint256 amount) public { vm.assume(amount > 0 && amount <= 10000 ether); vm.startPrank(alice); token.approve(address(contract), amount); contract.deposit(amount); vm.stopPrank(); assertEq(contract.balances(alice), amount); } // Echidna (advanced fuzzing) # echidna.yaml testMode: "assertion" testLimit: 100000 filterBlacklist: ["testFail"] corpusDir: "corpus" # Contract must have: function echidna_test_*() public returns (bool) function echidna_test_balance_never_exceeds_deposit() public view returns (bool) { return totalDeposited >= totalBalances; } Invariant Testing // Foundry invariant testing contract MyContractInvariantTest is Test { MyContract contract; function setUp() public { contract = new MyContract(...); } // Invariant: total user balances == contract token balance function invariant_balancesMatchContract() public { // Sum all user balances (track users in setUp) // assertEq(sumUserBalances, token.balanceOf(address(contract))); } // Invariant: no user balance exceeds their deposits function invariant_noNegativeBalances() public { for (address user : allUsers) { assertLe(contract.balances(user), userDeposits[user]); } } } // Run: forge test --invariant-runs 100 --invariant-depth 1000 Fork Testing (Mainnet Integration) // Test against live mainnet state forge test --fork-url $MAINNET_RPC_URL --fork-block-number 19000000 // Interact with real contracts import { IUniswapV3Pool } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; function testUniswapIntegration() public { IUniswapV3Pool pool = IUniswapV3Pool(0x8ad599c3A0ff1De082011EFDDc58f1908eb6e6D8); // USDC/ETH 0.3% (uint160 sqrtPriceX96, , , , , , ) = pool.slot0(); console.log("Current sqrtPriceX96:", sqrtPriceX96); // Simulate swap // pool.swap(...) } // Impersonate whale for testing vm.startPrank(0xWhaleAddress); token.transfer(address(this), 1000 ether); vm.stopPrank(); Mutation Testing # Mutatest: mutate source code, check if tests catch it # Install: pip install mutatest mutatest --runner "forge test" --source "src/MyContract.sol" # Mutations: # - Arithmetic: + → -, * → / # - Comparisons: < → >, == → != # - Boolean: true → false # - Constants: 1 → 0, 100 → 0 # Output: mutation score (killed / total mutants) # Target: >90% mutation score Debugging & Tracing Foundry Debugging # Verbose test output forge test -vvvv # Full traces with logs, storage changes, calls # Debug specific transaction forge debug 0xTxHash --rpc-url $RPC_URL # Interactive debugger forge debug 0xTxHash --rpc-url $RPC_URL --interactive # Commands: step, next, continue, print, storage, memory, stack # Trace call cast trace 0xTxHash --rpc-url $RPC_URL # Pretty print trace cast trace 0xTxHash --rpc-url $RPC_URL --json | jq # Storage inspection cast storage 0xContract 0xSlot --rpc-url $RPC_URL cast storage 0xContract --rpc-url $RPC_URL # All slots (if known) Tenderly / Blockscout Debugging # Tenderly: visual debugger, gas profiler, simulation # 1. Connect GitHub repo # 2. Push transaction hash or simulate # 3. View: call trace, state changes, gas breakdown # Tenderly CLI tenderly login tenderly push --project slug --contracts contracts/ tenderly verify --address 0xContract --network mainnet # Simulation API curl -X POST https://api.tenderly.co/api/v1/account/{account}/project/{project}/simulate \ -H "X-Access-Key: $TENDERLY_KEY" \ -d '{ "network_id": "1", "from": "0xUser", "to": "0xContract", "input": "0x...", "value": "0", "block_number": "latest" }' Hardhat Console & Debugging # Hardhat console (interactive) npx hardhat console --network sepolia # In console: const contract = await ethers.getContractAt("MyContract", "0x..."); await contract.deposit(ethers.parseEther("100")); // Hardhat trace npx hardhat trace --network sepolia 0xTxHash # Hardhat stack traces npx hardhat test --stack-traces Visual Debugging (VS Code + Solidity) # VS Code extensions: # - solidity (Juan Blanco): syntax, IntelliSense, debugging # - hardhat-solidity: Hardhat integration # launch.json for debugging tests { "version": "0.2.0", "configurations": [ { "type": "node", "request": "launch", "name": "Debug Hardhat Tests", "program": "${workspaceFolder}/node_modules/.bin/hardhat", "args": ["test", "--grep", "testName"], "console": "integratedTerminal", "env": { "NODE_OPTIONS": "--inspect" } } ] } Deployment & Verification Foundry Deployment (Script) // script/Deploy.s.sol contract DeployScript is Script { function run() external { vm.startBroadcast(); // Deploy dependencies ERC20Mock token = new ERC20Mock("Token", "TKN", 18); token.mint(msg.sender, 1000000 ether); // Deploy main contract MyContract contract = new MyContract(token, 1000 ether); // Configure token.approve(address(contract), type(uint256).max); vm.stopBroadcast(); // Output addresses for verification console.log("Token:", address(token)); console.log("Contract:", address(contract)); } } // Deploy forge script script/Deploy.s.sol --rpc-url $RPC_URL --private-key $PK --broadcast --verify --etherscan-api-key $ETHERSCAN_API_KEY Hardhat Deploy + Verify // scripts/deploy.ts import { ethers } from "hardhat"; async function main() { const [deployer] = await ethers.getSigners(); console.log("Deploying with:", deployer.address); const Token = await ethers.getContractFactory("ERC20Mock"); const token = await Token.deploy("Token", "TKN", 18); await token.mint(deployer.address, ethers.parseEther("1000000")); const Contract = await ethers.getContractFactory("MyContract"); const contract = await Contract.deploy(token.target, ethers.parseEther("1000")); await token.approve(contract.target, ethers.MaxUint256); console.log("Token:", token.target); console.log("Contract:", contract.target); // Wait for confirmations await contract.deploymentTransaction()?.wait(5); // Verify await hre.run("verify:verify", { address: contract.target, constructorArguments: [token.target, ethers.parseEther("1000")], }); } main().catch((error) => { console.error(error); process.exitCode = 1; }); Proxy Deployment (UUPS) // script/DeployProxy.s.sol contract DeployProxyScript is Script { function run() external { vm.startBroadcast(); // Deploy implementation MyContractV1 impl = new MyContractV1(); // Deploy ERC

Comments

Popular posts from this blog

Firmware Testing Strategies: Hardware-in-the-Loop, Simulation, and CI/CD Pipelines | Embedded Systems Hub

IoT Protocols Compared: MQTT, CoAP, HTTP/2, WebSocket - Performance, Overhead, and Use Cases | Embedded Systems Hub

Embedded Security: Secure Boot, ARM TrustZone, and OTA Update Integrity | Embedded Systems Hub