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

  1. Open deployment-info.json

  2. Copy the contract address

  3. Replace YOUR_CONTRACT_ADDRESS_HERE in the HTML file

  4. Open 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:

  1. 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);
  1. Save flattened contract:

npx hardhat flatten contracts/SimpleStorage.sol > SimpleStorage_flat.sol
  1. 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:

  1. Check error messages carefully

  2. Search our Discord

  3. 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