Hardhat is a flexible and extensible Ethereum development environment that is fully compatible with Cosmos EVM. It’s an excellent choice for teams with JavaScript/TypeScript expertise or those who need complex deployment and testing workflows.
Project Setup and Configuration
Initialize and configure a new Hardhat project for Cosmos EVM development.
1. Installation
mkdir cosmos-evm-hardhat
cd cosmos-evm-hardhat
npm init -y
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox @openzeppelin/contracts
npx hardhat init # Select "Create a TypeScript project"
See all 5 lines
2. Configuration
Modify hardhat.config.ts
to include networks and settings for Cosmos EVM.
import { HardhatUserConfig } from "hardhat/config" ;
import "@nomicfoundation/hardhat-toolbox" ;
import "dotenv/config" ;
const config : HardhatUserConfig = {
solidity: {
version: "0.8.24" ,
settings: {
optimizer: {
enabled: true ,
runs: 200 ,
},
evmVersion: "istanbul"
},
},
networks: {
local: {
url: "http://127.0.0.1:8545" ,
chainId: 4321 , // Default local chain ID
accounts: process . env . PRIVATE_KEY ? [ process . env . PRIVATE_KEY ] : [],
gasPrice: 20000000000 ,
},
testnet: {
url: "https://devnet-1-evmrpc.ib.skip.build" ,
chainId: 4321 , // Cosmos EVM devnet chain ID
accounts: process . env . PRIVATE_KEY ? [ process . env . PRIVATE_KEY ] : [],
}
},
gasReporter: {
enabled: process . env . REPORT_GAS !== undefined ,
currency: "USD" ,
},
etherscan: {
apiKey: {
cosmosEvmTestnet: process . env . ETHERSCAN_API_KEY || "dummy_key"
},
customChains: [
{
network: "cosmosEvmTestnet" ,
chainId: 4321 , // Cosmos EVM devnet chain ID
urls: {
apiURL: "https://devnet-1-lcd.ib.skip.build/api" ,
browserURL: "https://devnet-1-lcd.ib.skip.build"
}
}
]
}
};
export default config ;
See all 50 lines
TypeScript Integration
Hardhat’s first-class TypeScript support enables type-safe contract interactions and tests.
1. Writing a Contract
Create a contract in the contracts/
directory. For this example, we’ll use a simple LiquidStakingVault
.
contracts/LiquidStakingVault.sol
// contracts/LiquidStakingVault.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24 ;
import "@openzeppelin/contracts/access/Ownable.sol" ;
// Interface for the staking precompile
interface IStaking {
function delegate ( address validator , uint256 amount ) external returns ( bool );
function undelegate ( address validator , uint256 amount ) external returns ( bool , uint64 );
}
contract LiquidStakingVault is Ownable {
IStaking constant STAKING = IStaking ( 0x0000000000000000000000000000000000000800 );
mapping ( address => uint256 ) public stakedBalance;
address public primaryValidator;
uint256 public totalStaked;
event Staked ( address indexed user , uint256 amount );
constructor ( address _primaryValidator ) Ownable (msg.sender) {
primaryValidator = _primaryValidator;
}
function stake () external payable {
require ( msg .value > 0 , "Must stake positive amount" );
bool success = STAKING. delegate (primaryValidator, msg .value);
require (success, "Delegation failed" );
stakedBalance[ msg.sender ] += msg .value;
totalStaked += msg .value;
emit Staked ( msg.sender , msg .value);
}
}
See all 34 lines
2. Writing Tests
Create type-safe tests in the test/
directory.
test/LiquidStakingVault.test.ts
// test/LiquidStakingVault.test.ts
import { expect } from "chai" ;
import { ethers } from "hardhat" ;
import { LiquidStakingVault } from "../typechain-types" ;
import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers" ;
describe ( "LiquidStakingVault" , function () {
let vault : LiquidStakingVault ;
let owner : SignerWithAddress ;
let user1 : SignerWithAddress ;
const STAKING_PRECOMPILE = "0x0000000000000000000000000000000000000800" ;
const VALIDATOR_ADDRESS = "0x1234567890123456789012345678901234567890" ;
beforeEach ( async function () {
[ owner , user1 ] = await ethers . getSigners ();
const VaultFactory = await ethers . getContractFactory ( "LiquidStakingVault" );
vault = await VaultFactory . deploy ( VALIDATOR_ADDRESS );
await vault . waitForDeployment ();
// Mock the staking precompile's delegate function to always return true
// This bytecode is a minimal contract that returns true for any call
const successBytecode = "0x6080604052348015600f57600080fd5b50600160005560016000f3" ;
await ethers . provider . send ( "hardhat_setCode" , [
STAKING_PRECOMPILE ,
successBytecode ,
]);
});
it ( "Should allow a user to stake tokens" , async function () {
const stakeAmount = ethers . parseEther ( "1.0" );
await expect ( vault . connect ( user1 ). stake ({ value: stakeAmount }))
. to . emit ( vault , "Staked" )
. withArgs ( user1 . address , stakeAmount );
expect ( await vault . stakedBalance ( user1 . address )). to . equal ( stakeAmount );
expect ( await vault . totalStaked ()). to . equal ( stakeAmount );
});
});
See all 41 lines
Run your tests:
Deployment Scripts
Create a deployment script in the scripts/
directory to deploy your contract to a live network.
// scripts/deploy.ts
import { ethers , network } from "hardhat" ;
import { writeFileSync } from "fs" ;
async function main () {
const [ deployer ] = await ethers . getSigners ();
console . log ( "Deploying contracts with the account:" , deployer . address );
const validatorAddress = process . env . VALIDATOR_ADDRESS || "0x0000000000000000000000000000000000000000" ;
const VaultFactory = await ethers . getContractFactory ( "LiquidStakingVault" );
const vault = await VaultFactory . deploy ( validatorAddress );
await vault . waitForDeployment ();
const vaultAddress = await vault . getAddress ();
console . log ( "LiquidStakingVault deployed to:" , vaultAddress );
// Save deployment information
const deploymentInfo = {
contractAddress: vaultAddress ,
deployer: deployer . address ,
network: network . name ,
chainId: network . config . chainId ,
};
writeFileSync (
`deployments/ ${ network . name } -deployment.json` ,
JSON . stringify ( deploymentInfo , null , 2 )
);
// Optional: Verify contract on Etherscan-compatible explorer
if ( network . name !== "local" && process . env . ETHERSCAN_API_KEY ) {
console . log ( "Waiting for block confirmations before verification..." );
// await vault.deploymentTransaction()?.wait(5); // Wait for 5 blocks
await new Promise ( resolve => setTimeout ( resolve , 30000 )); // Or wait 30 seconds
await hre . run ( "verify:verify" , {
address: vaultAddress ,
constructorArguments: [ validatorAddress ],
});
}
}
main ()
. then (() => process . exit ( 0 ))
. catch (( error ) => {
console . error ( error );
process . exit ( 1 );
});
See all 50 lines
Run the deployment script:
npx hardhat run scripts/deploy.ts --network testnet