Deploy Your First Contract
Overview
This tutorial will guide you through deploying your first smart contract on OPN Chain. We'll create a simple storage contract, deploy it, and interact with it using various tools.
What you'll learn:
Writing a basic smart contract
Compiling with Solidity
Deploying to OPN testnet
Interacting with your contract
Verifying your contract
Time required: 20 minutes
Prerequisites
Before starting, ensure you have:
[x] MetaMask installed and connected to OPN testnet
[x] Test OPN tokens from the faucet
[x] Node.js 16+ installed
[x] Basic command line knowledge
Step 1: Set Up Your Project
Create Project Directory
mkdir my-first-contract
cd my-first-contract
npm init -y
Install Dependencies
npm install --save-dev hardhat @nomiclabs/hardhat-waffle ethereum-waffle chai @nomiclabs/hardhat-ethers ethers
npm install dotenv
Initialize Hardhat
npx hardhat
Select:
"Create a JavaScript project"
Press Enter for all prompts
Type 'y' to install sample project dependencies
Configure Hardhat for OPN
Create/update hardhat.config.js
:
require("@nomiclabs/hardhat-waffle");
require("dotenv").config();
module.exports = {
solidity: "0.8.30",
networks: {
opnTestnet: {
url: "https://testnet-rpc.iopn.tech",
chainId: 984,
accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
}
}
};
Set Up Environment Variables
Create .env
file:
PRIVATE_KEY=your_wallet_private_key_here
⚠️ Security Note: Never commit your .env
file! Add it to .gitignore
:
echo ".env" >> .gitignore
Step 2: Write Your Smart Contract
Create the Contract
Delete the sample contracts and create contracts/SimpleStorage.sol
:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
/**
* @title SimpleStorage
* @dev Store & retrieve value in a variable
* @custom:dev-run-script ./scripts/deploy.js
*/
contract SimpleStorage {
uint256 private storedValue;
// Event emitted when value changes
event ValueChanged(uint256 oldValue, uint256 newValue, address indexed changer);
// Constructor to set initial value
constructor(uint256 _initialValue) {
storedValue = _initialValue;
emit ValueChanged(0, _initialValue, msg.sender);
}
/**
* @dev Store a new value
* @param _value value to store
*/
function set(uint256 _value) public {
uint256 oldValue = storedValue;
storedValue = _value;
emit ValueChanged(oldValue, _value, msg.sender);
}
/**
* @dev Return the stored value
* @return value of 'storedValue'
*/
function get() public view returns (uint256) {
return storedValue;
}
/**
* @dev Increment stored value by one
*/
function increment() public {
uint256 oldValue = storedValue;
storedValue = storedValue + 1;
emit ValueChanged(oldValue, storedValue, msg.sender);
}
/**
* @dev Check if value equals a number
* @param _value value to compare
* @return true if values match
*/
function equals(uint256 _value) public view returns (bool) {
return storedValue == _value;
}
}
Understanding the Contract
This contract:
Stores a single number (
storedValue
)Allows anyone to update the value
Emits events when the value changes
Provides functions to read and modify the value
Step 3: Compile the Contract
Compile
npx hardhat compile
Expected output:
Compiled 1 Solidity file successfully
The compiled artifacts are saved in artifacts/
directory.
Check Compilation
ls artifacts/contracts/SimpleStorage.sol/
You should see SimpleStorage.json
containing the ABI and bytecode.
Step 4: Write Tests
Create Test File
Create test/SimpleStorage.test.js
:
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("SimpleStorage", function () {
let simpleStorage;
let owner;
let addr1;
beforeEach(async function () {
// Get signers
[owner, addr1] = await ethers.getSigners();
// Deploy contract
const SimpleStorage = await ethers.getContractFactory("SimpleStorage");
simpleStorage = await SimpleStorage.deploy(42);
await simpleStorage.deployed();
});
describe("Deployment", function () {
it("Should set the initial value", async function () {
expect(await simpleStorage.get()).to.equal(42);
});
it("Should emit ValueChanged event on deployment", async function () {
const SimpleStorage = await ethers.getContractFactory("SimpleStorage");
const contract = await SimpleStorage.deploy(100);
await expect(contract.deployTransaction)
.to.emit(contract, "ValueChanged")
.withArgs(0, 100, owner.address);
});
});
describe("Set function", function () {
it("Should update stored value", async function () {
await simpleStorage.set(123);
expect(await simpleStorage.get()).to.equal(123);
});
it("Should emit ValueChanged event", async function () {
await expect(simpleStorage.set(456))
.to.emit(simpleStorage, "ValueChanged")
.withArgs(42, 456, owner.address);
});
it("Should allow any address to set value", async function () {
await simpleStorage.connect(addr1).set(789);
expect(await simpleStorage.get()).to.equal(789);
});
});
describe("Increment function", function () {
it("Should increment value by 1", async function () {
await simpleStorage.increment();
expect(await simpleStorage.get()).to.equal(43);
});
it("Should handle multiple increments", async function () {
await simpleStorage.increment();
await simpleStorage.increment();
await simpleStorage.increment();
expect(await simpleStorage.get()).to.equal(45);
});
});
describe("Equals function", function () {
it("Should return true for matching value", async function () {
expect(await simpleStorage.equals(42)).to.equal(true);
});
it("Should return false for non-matching value", async function () {
expect(await simpleStorage.equals(99)).to.equal(false);
});
});
describe("Gas usage", function () {
it("Should use reasonable gas for operations", async function () {
const setTx = await simpleStorage.set(999);
const setReceipt = await setTx.wait();
console.log("Set gas used:", setReceipt.gasUsed.toString());
const incrementTx = await simpleStorage.increment();
const incrementReceipt = await incrementTx.wait();
console.log("Increment gas used:", incrementReceipt.gasUsed.toString());
});
});
});
Run Tests
npx hardhat test
Expected output:
SimpleStorage
Deployment
✓ Should set the initial value
✓ Should emit ValueChanged event on deployment
Set function
✓ Should update stored value
✓ Should emit ValueChanged event
✓ Should allow any address to set value
Increment function
✓ Should increment value by 1
✓ Should handle multiple increments
Equals function
✓ Should return true for matching value
✓ Should return false for non-matching value
Gas usage
Set gas used: 43954
Increment gas used: 41842
✓ Should use reasonable gas for operations
10 passing (2s)
Step 5: Deploy to OPN Testnet
Create Deployment Script
Create scripts/deploy.js
:
const hre = require("hardhat");
async function main() {
console.log("Deploying to OPN Testnet...\n");
// Get deployer account
const [deployer] = await ethers.getSigners();
console.log("Deploying contracts with account:", deployer.address);
// Check balance
const balance = await deployer.getBalance();
console.log("Account balance:", ethers.utils.formatEther(balance), "OPN\n");
// Deploy contract
const SimpleStorage = await ethers.getContractFactory("SimpleStorage");
console.log("Deploying SimpleStorage...");
const simpleStorage = await SimpleStorage.deploy(42);
await simpleStorage.deployed();
console.log("SimpleStorage deployed to:", simpleStorage.address);
console.log("Transaction hash:", simpleStorage.deployTransaction.hash);
// Wait for confirmations
console.log("\nWaiting for confirmations...");
await simpleStorage.deployTransaction.wait(5);
console.log("Confirmed!\n");
// Verify deployment
const value = await simpleStorage.get();
console.log("Initial value:", value.toString());
// Save deployment info
const fs = require("fs");
const deploymentInfo = {
network: "OPN Testnet",
chainId: 984,
contract: "SimpleStorage",
address: simpleStorage.address,
deployer: deployer.address,
deploymentTx: simpleStorage.deployTransaction.hash,
timestamp: new Date().toISOString(),
initialValue: value.toString(),
abi: SimpleStorage.interface.format('json')
};
fs.writeFileSync(
"deployment-info.json",
JSON.stringify(deploymentInfo, null, 2)
);
console.log("\nDeployment info saved to deployment-info.json");
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
Deploy
npx hardhat run scripts/deploy.js --network opnTestnet
Expected output:
Deploying to OPN Testnet...
Deploying contracts with account: 0xYourAddress
Account balance: 9.876543210 OPN
Deploying SimpleStorage...
SimpleStorage deployed to: 0xContractAddress
Transaction hash: 0xTransactionHash
Waiting for confirmations...
Confirmed!
Initial value: 42
Deployment info saved to deployment-info.json
🎉 Congratulations! You've deployed your first smart contract on OPN Chain!
Step 6: Interact with Your Contract
Create Interaction Script
Create scripts/interact.js
:
const hre = require("hardhat");
const deploymentInfo = require("../deployment-info.json");
async function main() {
console.log("Interacting with SimpleStorage on OPN Testnet...\n");
// Get signer
const [signer] = await ethers.getSigners();
// Connect to deployed contract
const simpleStorage = await ethers.getContractAt(
"SimpleStorage",
deploymentInfo.address,
signer
);
// Read current value
console.log("Current value:", (await simpleStorage.get()).toString());
// Set new value
console.log("\nSetting value to 123...");
const setTx = await simpleStorage.set(123);
console.log("Transaction hash:", setTx.hash);
// Wait for confirmation
const receipt = await setTx.wait();
console.log("Confirmed in block:", receipt.blockNumber);
console.log("Gas used:", receipt.gasUsed.toString());
// Read updated value
console.log("\nNew value:", (await simpleStorage.get()).toString());
// Increment value
console.log("\nIncrementing value...");
const incrementTx = await simpleStorage.increment();
await incrementTx.wait();
console.log("After increment:", (await simpleStorage.get()).toString());
// Check equals
const equals124 = await simpleStorage.equals(124);
console.log("\nEquals 124?", equals124);
// Listen for events
console.log("\n Listening for ValueChanged events...");
simpleStorage.on("ValueChanged", (oldValue, newValue, changer) => {
console.log(`Value changed from ${oldValue} to ${newValue} by ${changer}`);
});
// Trigger an event
await simpleStorage.set(200);
// Wait a bit for event
await new Promise(resolve => setTimeout(resolve, 5000));
// Remove listener
simpleStorage.removeAllListeners();
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
Run Interaction
npx hardhat run scripts/interact.js --network opnTestnet
Step 7: Interact via Web Interface
Create Simple Web Interface
Create interface.html
:
<!DOCTYPE html>
<html>
<head>
<title>SimpleStorage DApp</title>
<script src="https://cdn.jsdelivr.net/npm/web3@latest/dist/web3.min.js"></script>
<style>
body {
font-family: Arial, sans-serif;
max-width: 600px;
margin: 50px auto;
padding: 20px;
}
.container {
border: 1px solid #ddd;
padding: 20px;
border-radius: 8px;
}
button {
background: #4CAF50;
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
margin: 5px;
}
button:hover {
background: #45a049;
}
input {
padding: 8px;
margin: 5px;
border: 1px solid #ddd;
border-radius: 4px;
}
#status {
margin-top: 20px;
padding: 10px;
background: #f0f0f0;
border-radius: 4px;
}
</style>
</head>
<body>
<div class="container">
<h1>SimpleStorage on OPN Chain</h1>
<div>
<h3>Contract Address</h3>
<p id="contractAddress">Not connected</p>
</div>
<div>
<h3>Current Value</h3>
<p id="currentValue">?</p>
<button onclick="getValue()">Refresh Value</button>
</div>
<div>
<h3>Set New Value</h3>
<input type="number" id="newValue" placeholder="Enter new value">
<button onclick="setValue()">Set Value</button>
</div>
<div>
<h3>Increment</h3>
<button onclick="incrementValue()">Increment by 1</button>
</div>
<div id="status"></div>
</div>
<script>
// Replace with your contract address
const CONTRACT_ADDRESS = 'YOUR_CONTRACT_ADDRESS_HERE';
// Contract ABI (paste from deployment-info.json)
const CONTRACT_ABI = [
{
"inputs": [{"internalType": "uint256", "name": "_initialValue", "type": "uint256"}],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{"indexed": false, "internalType": "uint256", "name": "oldValue", "type": "uint256"},
{"indexed": false, "internalType": "uint256", "name": "newValue", "type": "uint256"},
{"indexed": true, "internalType": "address", "name": "changer", "type": "address"}
],
"name": "ValueChanged",
"type": "event"
},
{
"inputs": [{"internalType": "uint256", "name": "_value", "type": "uint256"}],
"name": "equals",
"outputs": [{"internalType": "bool", "name": "", "type": "bool"}],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "get",
"outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "increment",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [{"internalType": "uint256", "name": "_value", "type": "uint256"}],
"name": "set",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
];
let web3;
let contract;
let account;
async function init() {
if (typeof window.ethereum !== 'undefined') {
web3 = new Web3(window.ethereum);
try {
// Request accounts
const accounts = await window.ethereum.request({
method: 'eth_requestAccounts'
});
account = accounts[0];
// Check network
const chainId = await web3.eth.getChainId();
if (chainId !== 984) {
alert('Please switch to OPN Testnet in MetaMask!');
return;
}
// Initialize contract
contract = new web3.eth.Contract(CONTRACT_ABI, CONTRACT_ADDRESS);
document.getElementById('contractAddress').textContent = CONTRACT_ADDRESS;
// Get initial value
await getValue();
// Listen for events
contract.events.ValueChanged()
.on('data', (event) => {
console.log('Value changed:', event.returnValues);
getValue();
updateStatus(`Value changed from ${event.returnValues.oldValue} to ${event.returnValues.newValue}`);
});
} catch (error) {
console.error('Error:', error);
alert('Error connecting to MetaMask!');
}
} else {
alert('Please install MetaMask!');
}
}
async function getValue() {
try {
const value = await contract.methods.get().call();
document.getElementById('currentValue').textContent = value;
} catch (error) {
console.error('Error getting value:', error);
updateStatus('Error getting value: ' + error.message);
}
}
async function setValue() {
const newValue = document.getElementById('newValue').value;
if (!newValue) {
alert('Please enter a value!');
return;
}
try {
updateStatus('Sending transaction...');
const tx = await contract.methods.set(newValue).send({
from: account,
gas: 100000,
gasPrice: web3.utils.toWei('7', 'gwei')
});
updateStatus(`Transaction successful! Hash: ${tx.transactionHash}`);
document.getElementById('newValue').value = '';
} catch (error) {
console.error('Error setting value:', error);
updateStatus('Error: ' + error.message);
}
}
async function incrementValue() {
try {
updateStatus('Incrementing value...');
const tx = await contract.methods.increment().send({
from: account,
gas: 100000,
gasPrice: web3.utils.toWei('7', 'gwei')
});
updateStatus(`Increment successful! Hash: ${tx.transactionHash}`);
} catch (error) {
console.error('Error incrementing:', error);
updateStatus('Error: ' + error.message);
}
}
function updateStatus(message) {
document.getElementById('status').innerHTML =
`<strong>Status:</strong> ${message}<br>
<small>${new Date().toLocaleTimeString()}</small>`;
}
// Initialize on load
window.addEventListener('load', init);
</script>
</body>
</html>
Update Contract Address
Open
deployment-info.json
Copy the contract address
Replace
YOUR_CONTRACT_ADDRESS_HERE
in the HTML fileOpen
interface.html
in your browser
Step 8: Verify Your Contract (Optional)
Why Verify?
Contract verification:
Makes source code public
Enables direct interaction on block explorer
Builds trust with users
Allows easy contract reading
Manual Verification Steps
Until automated verification is available:
Prepare verification info:
// Create verify-info.js
const info = {
address: "YOUR_CONTRACT_ADDRESS",
constructorArguments: [42],
contract: "contracts/SimpleStorage.sol:SimpleStorage",
compiler: "v0.8.30+commit.73712a01",
optimizer: false
};
console.log("Verification info:", info);
Save flattened contract:
npx hardhat flatten contracts/SimpleStorage.sol > SimpleStorage_flat.sol
When block explorer launches:
Navigate to your contract
Click "Verify Contract"
Paste flattened source
Enter constructor arguments
Submit verification
Advanced Topics
Gas Optimization
Optimize your contract for OPN's gas costs:
// Gas-optimized version
contract OptimizedStorage {
uint256 private value;
// Pack multiple updates in one transaction
function batchUpdate(uint256[] calldata values) external {
uint256 finalValue = value;
for (uint i = 0; i < values.length; i++) {
finalValue = values[i];
}
value = finalValue;
}
}
Using Events Efficiently
contract EventOptimized {
// Index important parameters for filtering
event Transfer(
address indexed from,
address indexed to,
uint256 value
);
// Use events for off-chain data
event Metadata(string dataHash);
}
Upgradeable Contracts
For production, consider upgradeable patterns:
// Using OpenZeppelin upgradeable contracts
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
contract SimpleStorageV2 is Initializable {
uint256 private value;
function initialize(uint256 _value) public initializer {
value = _value;
}
}
Troubleshooting
Common Issues
"Insufficient funds" error:
Check your wallet has OPN tokens
Visit the faucet for more tokens
"Network error" when deploying:
Verify RPC URL:
https://testnet-rpc.iopn.tech
Check your internet connection
Try increasing gas limit
"Nonce too high" error:
# Reset Hardhat's nonce tracking
npx hardhat clean
rm -rf cache/
Contract not verified:
Ensure exact compiler version match
Check constructor arguments encoding
Try flattening the contract
Getting Help
If you encounter issues:
Check error messages carefully
Search our Discord
Review FAQs
Summary
In this tutorial, you:
✅ Set up a Hardhat project
✅ Wrote a smart contract
✅ Compiled and tested it
✅ Deployed to OPN testnet
✅ Interacted via scripts and web interface
✅ Learned about verification
You're now ready to build more complex applications on OPN Chain!
Share your success! Tweet your deployed contract address with #OPNChain
Last updated