Interacting with EVM Smart Contracts
This guide will help you understand how to compile, deploy, and interact with Solidity smart contracts on the Nibiru EVM using `ethers.js`.
Deploying a Smart Contract
One way to deploy smart contracts on Nibiru EVM is by using the ethers
npm library. You'll need a mnemonic phrase and the EVM RPC endpoint of your Nibiru chain (e.g., localnet is 127.0.0.1:8545).
import { ethers } from "ethers"
import { config } from "dotenv"
import * as fs from "fs"
config()
const rpcEndpoint = process.env.JSON_RPC_ENDPOINT
const mnemonic = process.env.MNEMONIC
const provider = ethers.getDefaultProvider(rpcEndpoint)
const wallet = ethers.Wallet.fromPhrase(mnemonic)
const account = wallet.connect(provider)
const deployContract = async (path: string) => {
const contractJSON = JSON.parse(
fs.readFileSync(`contracts/${path}`).toString(),
)
const bytecode = contractJSON["bytecode"]
const abi = contractJSON["abi"]
const contractFactory = new ethers.ContractFactory(abi, bytecode, account)
const contract = await contractFactory.deploy()
await contract.waitForDeployment()
return contract
}
This script performs the following steps:
- Imports necessary libraries and reads environment variables.
- Sets up the RPC provider and wallet using the mnemonic phrase.
- Reads the compiled contract's JSON file to get the bytecode and ABI.
- Uses the ContractFactory to deploy the contract and waits for the deployment to complete.
Environment Setup
Ensure you have a .env file with the following variables:
JSON_RPC_ENDPOINT=http://127.0.0.1:8545
MNEMONIC=your-mnemonic-phrase-here
Basic Queries
Once your contract is deployed, you can interact with it using ethers.js. Here are some basic queries you might perform:
import { ethers } from "ethers";
const rpcEndpoint = process.env.JSON_RPC_ENDPOINT
const mnemonic = process.env.MNEMONIC
const provider = ethers.getDefaultProvider(rpcEndpoint)
const wallet = ethers.Wallet.fromPhrase(mnemonic)
const account = wallet.connect(provider)
const randomAddress = ethers.Wallet.createRandom().address;
const amountToSend = ethers.utils.parseUnits("1000", "wei");
const gasLimit = 100000; // Example gas limit
const getBalances = async (address) => {
const balance = await provider.getBalance(address);
return ethers.utils.formatUnits(balance, "wei");
};
const transferEthers = async () => {
const senderBalanceBefore = await getBalances(account.address);
const recipientBalanceBefore = await getBalances(randomAddress);
console.log(`Sender balance before: ${senderBalanceBefore}`);
console.log(`Recipient balance before: ${recipientBalanceBefore}`);
// Execute EVM transfer
const transaction = {
to: randomAddress,
value: amountToSend,
gasLimit: gasLimit,
};
const txResponse = await account.sendTransaction(transaction);
await txResponse.wait();
const senderBalanceAfter = await getBalances(account.address);
const recipientBalanceAfter = await getBalances(randomAddress);
console.log(`Sender balance after: ${senderBalanceAfter}`);
console.log(`Recipient balance after: ${recipientBalanceAfter}`);
};
transferEthers();
Executing Smart Contract Functions
To execute functions within your deployed smart contract, you can use ethers.js
to create a contract instance and call its methods.
Example: Calling a Read-Only Function
Assuming you have a simple smart contract with a getValue function:
// SimpleStorage.sol
pragma solidity ^0.8.0;
contract SimpleStorage {
uint256 private value;
// Function to set a new value. This modifies the blockchain state.
function setValue(uint256 newValue) public {
value = newValue;
}
// Function to get the current value. This does not modify the blockchain state.
function getValue() public view returns (uint256) {
return value;
}
}
Here’s how you can call the getValue function:
import { ethers } from "ethers";
import SimpleStorageABI from "./SimpleStorage.json"; // Assuming ABI is in JSON format
import { config } from "dotenv";
// Load environment variables from .env file
config();
// Set up the RPC endpoint and mnemonic phrase from environment variables
const rpcEndpoint = process.env.JSON_RPC_ENDPOINT;
const mnemonic = process.env.MNEMONIC;
// Connect to the Ethereum network using the RPC endpoint
const provider = new ethers.JsonRpcProvider(rpcEndpoint);
// Create a wallet instance using the mnemonic phrase
const wallet = ethers.Wallet.fromMnemonic(mnemonic);
// Connect the wallet to the provider
const account = wallet.connect(provider);
// Address of the deployed SimpleStorage contract
const contractAddress = "your_contract_address_here";
// Create a contract instance using the contract address, ABI, and provider
const simpleStorage = new ethers.Contract(contractAddress, SimpleStorageABI, provider);
// Function to call the read-only getValue function of the contract
const getValue = async () => {
// Call the getValue function
const value = await simpleStorage.getValue();
// Log the returned value
console.log(`Stored value: ${value}`);
};
// Execute the getValue function
getValue();
Example: Calling a State-Changing Function
To call a function that changes the contract’s state (e.g., setValue):
// Function to call the state-changing setValue function of the contract
const setValue = async (newValue) => {
// Connect the contract instance to the wallet for signing transactions
const contractWithSigner = simpleStorage.connect(account);
// Call the setValue function with the new value
const txResponse = await contractWithSigner.setValue(newValue);
// Wait for the transaction to be mined
await txResponse.wait();
// Log the new value that was set
console.log(`New value set: ${newValue}`);
};
// Execute the setValue function with a new value
setValue(42);
Sending Funds from a Smart Contract
Given the following ERC20 FunToken.sol contract:
pragma solidity ^0.8.24;
import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract FunToken is ERC20 {
// Define the initial supply of FunToken: 1,000,000 tokens
uint256 constant initialSupply = 1000000 * (10**18);
// Constructor will be called on contract creation
constructor() ERC20("FunToken", "FUN") {
// Mint the initial supply to the deployer's address
_mint(msg.sender, initialSupply);
}
}
After deploying your contract via the deploy function above,
import { deployContract } from "./deploy"; // Import the deploy function
import { ethers } from "ethers";
import FunTokenCompiled from "./FunTokenCompiled.json"; // Assuming ABI and bytecode are in JSON format
import { config } from "dotenv";
// Load environment variables from .env file
config();
// Set up the RPC endpoint and mnemonic phrase from environment variables
const rpcEndpoint = process.env.JSON_RPC_ENDPOINT;
const mnemonic = process.env.MNEMONIC;
// Connect to the Ethereum network using the RPC endpoint
const provider = new ethers.JsonRpcProvider(rpcEndpoint);
// Create a wallet instance using the mnemonic phrase
const wallet = ethers.Wallet.fromMnemonic(mnemonic);
// Connect the wallet to the provider
const account = wallet.connect(provider);
// Main function to deploy and interact with the FunToken contract
const main = async () => {
// Deploy the FunToken contract and get the contract instance
const contract = (await deployContract("FunTokenCompiled.json")) as ethers.Contract;
// Get the address of the deployed contract
const contractAddress = contract.address;
console.log(`FunToken deployed at: ${contractAddress}`);
// Generate a random address to receive tokens
const shrimpAddress = ethers.Wallet.createRandom().address;
// Define the amount of tokens to send (1,000 tokens)
const amountToSend = ethers.utils.parseUnits("1000", 18);
// Get the initial balances of the owner and the recipient
let ownerBalance = await contract.balanceOf(account.address);
let shrimpBalance = await contract.balanceOf(shrimpAddress);
// Log the initial balances
console.log(`Owner balance before: ${ethers.utils.formatUnits(ownerBalance, 18)}`);
console.log(`Shrimp balance before: ${ethers.utils.formatUnits(shrimpBalance, 18)}`);
// Connect the contract instance to the wallet for signing transactions
const contractWithSigner = contract.connect(account);
// Transfer tokens from the owner to the recipient
const tx = await contractWithSigner.transfer(shrimpAddress, amountToSend);
// Wait for the transaction to be mined
await tx.wait();
// Get the updated balances of the owner and the recipient
ownerBalance = await contract.balanceOf(account.address);
shrimpBalance = await contract.balanceOf(shrimpAddress);
// Log the updated balances
console.log(`Owner balance after: ${ethers.utils.formatUnits(ownerBalance, 18)}`);
console.log(`Shrimp balance after: ${ethers.utils.formatUnits(shrimpBalance, 18)}`);
};
// Execute the main function
main();