Skip to Content
EVMEVM with Foundry

Sei EVM Smart Contract Development with Foundry

This tutorial will guide you through setting up Foundry for Sei EVM development and using OpenZeppelin contracts to build secure, standardized smart contracts. We’ll cover environment setup, contract creation, deployment, and show how to leverage OpenZeppelin’s pre-built components with the powerful Foundry toolkit.

🚀

It is highly recommended that you deploy to testnet (atlantic-2) first and verify everything works as expected before committing to mainnet. Doing so helps you catch bugs early, avoid unnecessary gas costs, and keep your users safe.

Table of Contents

Prerequisites

Before we begin, ensure you have the following:

  • Foundry installed on your system
  • A basic understanding of Solidity and smart contract development
  • A wallet with SEI tokens for gas fees
  • Node.js (v16.0.0 or later) for ethers.js interactions

Setting Up Your Development Environment

First, let’s install Foundry if you haven’t already. Follow the installation guide or use the quick install command:

curl -L https://foundry.paradigm.xyz | bash foundryup

Create a new Foundry project:

# Create a new directory for your project mkdir sei-foundry-project cd sei-foundry-project # Initialize a new Foundry project forge init --no-git # Initialize git repository (required for installing dependencies) git init

This will create a standard Foundry project structure with src/, test/, and script/ directories.

Configuring Foundry for Sei EVM

Create a foundry.toml file in your project root to configure Foundry for Sei networks:

foundry.toml
[profile.default] src = "src" out = "out" libs = ["lib"] solc_version = "0.8.28" optimizer = true optimizer_runs = 200 # Sei testnet configuration [rpc_endpoints] sei_testnet = "https://evm-rpc-testnet.sei-apis.com" sei_mainnet = "https://evm-rpc.sei-apis.com"

Create a .env file to store your private key and other sensitive information:

.env
PRIVATE_KEY=your_private_key_here SEI_TESTNET_RPC=https://evm-rpc-testnet.sei-apis.com SEI_MAINNET_RPC=https://evm-rpc.sei-apis.com

Add .env to your .gitignore file to prevent committing sensitive information such as your PRIVATE_KEY and potentially lose funds.

Using OpenZeppelin Contracts

OpenZeppelin provides a library of secure, tested smart contract components. Let’s install OpenZeppelin contracts:

forge install OpenZeppelin/openzeppelin-contracts --no-commit

Create a remappings.txt file to properly map the imports:

forge remappings > remappings.txt

Creating and Deploying Smart Contracts

Let’s create different types of smart contracts. Choose from the tabs below based on what you want to build:

Let’s start with a simple counter contract. Update the default src/Counter.sol file:

src/Counter.sol
// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.22; contract Counter { uint256 public number; address public owner; event NumberChanged(uint256 newNumber, address changedBy); constructor() { owner = msg.sender; } modifier onlyOwner() { require(msg.sender == owner, "Only owner can call this function"); _; } function setNumber(uint256 newNumber) public { number = newNumber; emit NumberChanged(newNumber, msg.sender); } function increment() public { number++; emit NumberChanged(number, msg.sender); } function getCount() public view returns (uint256) { return number; } function reset() public onlyOwner { number = 0; emit NumberChanged(0, msg.sender); } }

Create a deployment script in script/DeployCounter.s.sol:

script/DeployCounter.s.sol
// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.22; import {Script} from "forge-std/Script.sol"; import {Counter} from "../src/Counter.sol"; contract DeployCounter is Script { function run() external returns (Counter) { vm.startBroadcast(); Counter counter = new Counter(); vm.stopBroadcast(); return counter; } }

Testing Your Smart Contracts

Foundry provides excellent testing capabilities. Let’s create comprehensive tests for our contracts.

Update test/Counter.t.sol:

test/Counter.t.sol
// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.22; import {Test, console} from "forge-std/Test.sol"; import {Counter} from "../src/Counter.sol"; contract CounterTest is Test { Counter public counter; address public owner; address public user; function setUp() public { owner = address(this); user = address(0x1); counter = new Counter(); counter.setNumber(0); } function test_Increment() public { counter.increment(); assertEq(counter.number(), 1); } function test_SetNumber() public { counter.setNumber(42); assertEq(counter.number(), 42); } function test_GetCount() public { uint256 initialCount = counter.getCount(); counter.increment(); assertEq(counter.getCount(), initialCount + 1); } function test_Reset() public { counter.setNumber(100); counter.reset(); assertEq(counter.number(), 0); } function test_OnlyOwnerCanReset() public { vm.prank(user); vm.expectRevert("Only owner can call this function"); counter.reset(); } function test_EventEmitted() public { vm.expectEmit(true, true, false, true); emit Counter.NumberChanged(42, address(this)); counter.setNumber(42); } function testFuzz_SetNumber(uint256 x) public { counter.setNumber(x); assertEq(counter.number(), x); } }

Run your tests with:

forge test

For more verbose output:

forge test -vvv

Deploying to Sei Testnet and Mainnet

Now let’s deploy our contracts to the Sei networks. You can use either Forge scripts or direct deployment commands.

Deploy to Sei testnet:

forge script script/DeployCounter.s.sol --rpc-url $SEI_TESTNET_RPC --private-key $PRIVATE_KEY --broadcast

Deploy to Sei mainnet:

forge script script/DeployCounter.s.sol --rpc-url $SEI_MAINNET_RPC --private-key $PRIVATE_KEY --broadcast

Using Direct Forge Create Commands

Alternatively, you can deploy directly:

# Deploy Counter to testnet forge create --rpc-url $SEI_TESTNET_RPC --private-key $PRIVATE_KEY src/Counter.sol:Counter # Deploy SeiToken to testnet forge create --rpc-url $SEI_TESTNET_RPC --private-key $PRIVATE_KEY src/SeiToken.sol:SeiToken --constructor-args $(cast abi-encode "constructor(address)" "YOUR_ADDRESS") # Deploy SeiNFT to testnet forge create --rpc-url $SEI_TESTNET_RPC --private-key $PRIVATE_KEY src/SeiNFT.sol:SeiNFT --constructor-args $(cast abi-encode "constructor(address,string)" "YOUR_ADDRESS" "https://your-metadata-server.com/metadata/")

Successful deployment will output:

[⠒] Compiling... No files changed, compilation skipped Deployer: 0xYOUR_DEPLOYER_ADDRESS Deployed to: 0xYOUR_CONTRACT_ADDRESS Transaction hash: 0xYOUR_TX_HASH

Interacting with Deployed Contracts

Once deployed, you can interact with your contracts using Foundry’s cast tool or ethers.js.

Using Cast Commands

# Query the counter value cast call $CONTRACT_ADDRESS "getCount()(uint256)" --rpc-url $SEI_TESTNET_RPC # Increment the counter cast send $CONTRACT_ADDRESS "increment()" --private-key $PRIVATE_KEY --rpc-url $SEI_TESTNET_RPC # Set a specific number cast send $CONTRACT_ADDRESS "setNumber(uint256)" 42 --private-key $PRIVATE_KEY --rpc-url $SEI_TESTNET_RPC # Check ERC20 token balance cast call $TOKEN_ADDRESS "balanceOf(address)(uint256)" $YOUR_ADDRESS --rpc-url $SEI_TESTNET_RPC # Transfer ERC20 tokens cast send $TOKEN_ADDRESS "transfer(address,uint256)" $RECIPIENT_ADDRESS 1000 --private-key $PRIVATE_KEY --rpc-url $SEI_TESTNET_RPC

Using ethers.js

Create a Node.js script to interact with your deployed contracts:

interact.js
import { ethers } from 'ethers'; const privateKey = process.env.PRIVATE_KEY; const evmRpcEndpoint = process.env.SEI_TESTNET_RPC; const provider = new ethers.JsonRpcProvider(evmRpcEndpoint); const signer = new ethers.Wallet(privateKey, provider); // Counter contract interaction const counterAbi = ['function getCount() public view returns (uint256)', 'function increment() public', 'function setNumber(uint256 newNumber) public', 'event NumberChanged(uint256 newNumber, address changedBy)']; const counterAddress = 'YOUR_COUNTER_CONTRACT_ADDRESS'; const counterContract = new ethers.Contract(counterAddress, counterAbi, signer); // ERC20 token interaction const tokenAbi = ['function balanceOf(address owner) view returns (uint256)', 'function transfer(address to, uint256 amount) returns (bool)', 'function mint(address to, uint256 amount)', 'event Transfer(address indexed from, address indexed to, uint256 value)']; const tokenAddress = 'YOUR_TOKEN_CONTRACT_ADDRESS'; const tokenContract = new ethers.Contract(tokenAddress, tokenAbi, signer); async function interactWithContracts() { try { // Counter interactions console.log('Current count:', await counterContract.getCount()); const incrementTx = await counterContract.increment(); await incrementTx.wait(); console.log('Incremented! New count:', await counterContract.getCount()); // Token interactions const balance = await tokenContract.balanceOf(signer.address); console.log('Token balance:', ethers.formatEther(balance)); // Transfer tokens const transferTx = await tokenContract.transfer('0xRecipientAddress', ethers.parseEther('100')); await transferTx.wait(); console.log('Tokens transferred!'); } catch (error) { console.error('Error:', error); } } interactWithContracts();
Foundry generates the ABI for contracts in the out folder. You can use this ABI to interact with contracts from other tools like ethers.js or web3.js.
Last updated on