Prerequisites

  • Node.js 16+
  • Familiarity with Ethereum development
  • A wallet with testnet tokens (see testnet chain details)

Contract Preparation

The following example contract will be used throughout this guide:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

contract SimpleStorage {
    string private storedData;
    address public owner;
    
    event DataStored(string data, address indexed by);
    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
    
    constructor(string memory initialData) {
        storedData = initialData;
        owner = msg.sender;
        emit DataStored(initialData, msg.sender);
    }
    
    modifier onlyOwner() {
        require(msg.sender == owner, "Only owner can perform this action");
        _;
    }
    
    function setData(string memory data) public onlyOwner {
        storedData = data;
        emit DataStored(data, msg.sender);
    }
    
    function getData() public view returns (string memory) {
        return storedData;
    }
    
    function transferOwnership(address newOwner) public onlyOwner {
        require(newOwner != address(0), "New owner cannot be zero address");
        emit OwnershipTransferred(owner, newOwner);
        owner = newOwner;
    }
}

Deploy with Foundry

Foundry is a fast, Rust-based toolkit for Ethereum development.

  1. Install Foundry:

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

    You may have to source your .bashrc or .zshrc file

  2. Set up a new Foundry project:

    foundryup
    forge init my-plasma-project
    cd my-plasma-project
    
  3. Update foundry.toml with Plasma testnet settings:

    [profile.default]
    src = "src"
    out = "out"
    libs = ["lib"]
    solc_version = "0.8.19"
    optimizer = true
    optimizer_runs = 200
    
    [rpc_endpoints]
    plasma_testnet = "https://testnet-rpc.plasma.to"
    
  4. Create a .env file in your project root:

    PRIVATE_KEY=your_private_key_here
    RPC_URL=https://testnet-rpc.plasma.to
    
  5. Load the environment variables:

    source .env
    
  6. Save the contract code as src/SimpleStorage.sol, then deploy:

    forge create src/SimpleStorage.sol:SimpleStorage \
        --rpc-url $RPC_URL \
        --private-key $PRIVATE_KEY \
        --constructor-args "Hello, Plasma!"
    

    Foundry will output the deployment transaction hash and contract address:

    [⠊] Compiling...
    [⠢] Compiling 1 files with Solc 0.8.19
    [⠆] Solc 0.8.19 finished in 124.81ms
    Compiler run successful!
    Warning: Dry run enabled, not broadcasting transaction
    
    Contract: SimpleStorage
    Transaction: {
      "from": "0xbd828f7679656f8f830b89611c933017442f2ebf",
      "to": null,
      "maxFeePerGas": "0xf",
      "maxPriorityFeePerGas": "0x1",
      "gas": "0x69f18",
    
    [...]
    
  7. Test your deployed contract using Foundry’s cast tool. First, read the stored data:

    cast call 0x742d35Cc6610C7532C8582d4C371Acb1D5f44D7F \
        "getData()" \
        --rpc-url $RPC_URL
    
  8. Then update the data and read it again:

    # Update the stored data.
    cast send 0x742d35Cc6610C7532C8582d4C371Acb1D5f44D7F \
        "setData(string)" "Updated from Foundry" \
        --private-key $PRIVATE_KEY \
        --rpc-url $RPC_URL
    
    # Read the stored data again.
    cast call 0x742d35Cc6610C7532C8582d4C371Acb1D5f44D7F \
        "getData()" \
        --rpc-url $RPC_URL
    

    This should output something like:

    blockHash            0x68fbd2aaf1de9c577869056ca634f2103fa1695673a94d8c049d0b78d3733aac
    blockNumber          1200423
    contractAddress
    cumulativeGasUsed    21712
    effectiveGasPrice    8
    from                 0xBd828F7679656F8f830b89611C933017442F2EbF
    gasUsed              21712
    logs                 []
    logsBloom            0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
    root
    status               1 (success)
    transactionHash      0xcf5b1fcc8d19f7cf7992f6e6a8b3ade74e07afbd0da0b9fce26bda4c9503a12b
    transactionIndex     0
    type                 2
    blobGasPrice
    blobGasUsed
    to                   0x742D35Cc6610c7532C8582D4C371aCb1D5F44D7F
    0x
    

Deploy with Hardhat

Hardhat is a full-featured development framework with rich plugin support.

  1. Initialise a new Hardhat project:

    mkdir my-plasma-hardhat-project
    cd my-plasma-hardhat-project
    npm init -y
    npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox
    npm install dotenv ethers
    npx hardhat init
    

    Choose Create a JavaScript project when prompted.

  2. Update hardhat.config.js:

    require("@nomicfoundation/hardhat-toolbox");
    require("dotenv").config();
    
    /** @type import('hardhat/config').HardhatUserConfig */
    module.exports = {
      solidity: {
        version: "0.8.28",
        settings: {
          optimizer: {
            enabled: true,
            runs: 200
          }
        }
      },
      networks: {
        plasmaTestnet: {
          url: process.env.RPC_URL,
          chainId: 9746,
          accounts: [process.env.PRIVATE_KEY],
          gasPrice: 1000000000, // 1 gwei.
        }
      },
      etherscan: {
        apiKey: {
          plasmaTestnet: process.env.ETHERSCAN_API_KEY
        },
        customChains: [
          {
            network: "plasmaTestnet",
            chainId: 9746,
            urls: {
              apiURL: "https://testnet.plasmaexplorer.io/api",
              browserURL: "https://testnet.plasmaexplorer.io/"
            }
          }
        ]
      }
    };
    

We’re using Solidity version 0.8.28 here, but you can use whatever version you prefer.

  1. Create a .env file in your project root:

    PRIVATE_KEY=<your_private_key_here>
    RPC_URL=https://testnet-rpc.plasma.to
    ETHERSCAN_API_KEY=<your_api_key_for_verification>
    
  2. Make a directory to contain all your scripts:

    mkdir scripts
    
  3. Create scripts/deploy.js:

    const { ethers } = require("hardhat");
    
    async function main() {
      // Get the contract factory.
      const SimpleStorage = await ethers.getContractFactory("SimpleStorage");
      
      console.log("Deploying SimpleStorage contract...");
      
      // Deploy the contract with constructor arguments.
      const simpleStorage = await SimpleStorage.deploy("Hello, Plasma from Hardhat!");
      
      // Wait for deployment to complete.
      await simpleStorage.waitForDeployment();
      
      const contractAddress = await simpleStorage.getAddress();
      console.log("SimpleStorage deployed to:", contractAddress);
      
      // Wait a few blocks before verification.
      console.log("Waiting for block confirmations...");
      await simpleStorage.deploymentTransaction().wait(5);
      
      // Verify the contract.
      try {
        await hre.run("verify:verify", {
          address: contractAddress,
          constructorArguments: ["Hello, Plasma from Hardhat!"],
        });
        console.log("Contract verified successfully");
      } catch (error) {
        console.log("Verification failed:", error.message);
      }
    }
    
    main()
      .then(() => process.exit(0))
      .catch((error) => {
        console.error(error);
        process.exit(1);
      });
    
  4. Save your contract as contracts/SimpleStorage.sol.

  5. Deploy the contract:

    npx hardhat run scripts/deploy.js --network plasmaTestnet
    

    This should output something like:

    Deploying SimpleStorage contract...
    SimpleStorage deployed to: 0xa64600dB50A812D5944c33f8e39f257786517Aaa
    
  6. Create a test file test/SimpleStorage.js:

    const { expect } = require("chai");
    const { ethers } = require("hardhat");
    
    describe("SimpleStorage", function () {
     let simpleStorage;
     let owner;
    
     beforeEach(async function () {
       [owner] = await ethers.getSigners();
       const SimpleStorage = await ethers.getContractFactory("SimpleStorage");
       simpleStorage = await SimpleStorage.deploy("Initial data");
       await simpleStorage.waitForDeployment();
     });
    
     it("Should return the initial data", async function () {
       expect(await simpleStorage.getData()).to.equal("Initial data");
     });
    
     it("Should update data when called by owner", async function () {
       const tx = await simpleStorage.setData("Updated data");
       await tx.wait(); // Wait for transaction to be mined.
       expect(await simpleStorage.getData()).to.equal("Updated data");
     });
    
     it("Should emit DataStored event", async function () {
       const tx = await simpleStorage.setData("Test data");
       const receipt = await tx.wait();
       
       // Check that the transaction was successful.
       expect(receipt.status).to.equal(1);
       
       // Manually check for the event in the logs.
       const eventLog = receipt.logs.find(log => {
         try {
           const parsed = simpleStorage.interface.parseLog(log);
           return parsed.name === "DataStored";
         } catch {
           return false;
         }
       });
       
       expect(eventLog).to.not.be.undefined;
       
       const parsedEvent = simpleStorage.interface.parseLog(eventLog);
       expect(parsedEvent.args[0]).to.equal("Test data");
       expect(parsedEvent.args[1]).to.equal(owner.address);
     });
    });
    
  7. Run the tests:

    npx hardhat test --network plasmaTestnet
    

Deploy with Ethers.js

Ethers.js provides a minimal, programmatic deployment flow.

  1. Create a new project:

    mkdir my-plasma-ethers-project
    cd my-plasma-ethers-project
    npm init -y
    npm install ethers dotenv solc
    
  2. Create a .env file in your project root:

    PRIVATE_KEY=<your_private_key_here>
    RPC_URL=https://testnet-rpc.plasma.to
    ETHERSCAN_API_KEY=your_api_key_for_verification
    
  3. Create compile.js to compile your Solidity contract:

    const fs = require('fs');
    const solc = require('solc');
    require('dotenv').config();
    
    // Read the contract source code.
    const contractSource = fs.readFileSync('SimpleStorage.sol', 'utf8');
    
    // Prepare the input for the Solidity compiler.
    const input = {
      language: 'Solidity',
      sources: {
        'SimpleStorage.sol': {
          content: contractSource,
        },
      },
      settings: {
        outputSelection: {
          '*': {
            '*': ['*'],
          },
        },
        optimizer: {
          enabled: true,
          runs: 200,
        },
      },
    };
    
    // Compile the contract.
    const output = JSON.parse(solc.compile(JSON.stringify(input)));
    
    // Check for compilation errors.
    if (output.errors) {
      output.errors.forEach((error) => {
        console.error(error.formattedMessage);
      });
    }
    
    // Extract the contract data.
    const contract = output.contracts['SimpleStorage.sol']['SimpleStorage'];
    const abi = contract.abi;
    const bytecode = contract.evm.bytecode.object;
    
    // Save compilation artifacts.
    fs.writeFileSync('SimpleStorage.json', JSON.stringify({
      abi: abi,
      bytecode: bytecode
    }, null, 2));
    
    console.log('Contract compiled successfully!');
    
  4. Save your contract as SimpleStorage.sol, then compile:

    node compile.js
    

    This should output:

    Contract compiled successfully!
    
  5. Create deploy.js:

    const { ethers } = require('ethers');
    const fs = require('fs');
    require('dotenv').config();
    
    async function deploy() {
      // Set up provider and wallet.
      const provider = new ethers.JsonRpcProvider(process.env.RPC_URL);
      const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider);
      
      console.log('Deploying from account:', wallet.address);
      
      // Check account balance.
      const balance = await provider.getBalance(wallet.address);
      console.log('Account balance:', ethers.formatEther(balance), 'ETH');
      
      // Load compiled contract.
      const contractData = JSON.parse(fs.readFileSync('SimpleStorage.json', 'utf8'));
      
      // Create contract factory.
      const factory = new ethers.ContractFactory(
        contractData.abi,
        contractData.bytecode,
        wallet
      );
      
      // Deploy the contract.
      console.log('Deploying contract...');
      const contract = await factory.deploy("Hello, Plasma from Ethers.js!", {
        gasLimit: 500000, // Set a reasonable gas limit.
        gasPrice: ethers.parseUnits('1', 'gwei'), // 1 gwei gas price.
      });
      
      // Wait for deployment.
      await contract.waitForDeployment();
      
      const contractAddress = await contract.getAddress();
      console.log('Contract deployed to:', contractAddress);
      console.log('Transaction hash:', contract.deploymentTransaction().hash);
      
      // Save deployment info.
      const deploymentInfo = {
        contractAddress: contractAddress,
        transactionHash: contract.deploymentTransaction().hash,
        deployer: wallet.address,
        network: 'plasmaTestnet',
        timestamp: new Date().toISOString()
      };
      
      fs.writeFileSync('deployment.json', JSON.stringify(deploymentInfo, null, 2));
      
      return contract;
    }
    
    async function interact(contract) {
      console.log('\nInteracting with deployed contract...');
      
      // Read initial data.
      const initialData = await contract.getData();
      console.log('Initial data:', initialData);
      
      // Update data.
      const updateTx = await contract.setData("Updated via Ethers.js");
      await updateTx.wait();
      console.log('Data updated. Transaction hash:', updateTx.hash);
      
      // Read updated data.
      const updatedData = await contract.getData();
      console.log('Updated data:', updatedData);
      
      // Get owner.
      const owner = await contract.owner();
      console.log('Contract owner:', owner);
    }
    
    // Main execution.
    deploy()
      .then(async (contract) => {
        await interact(contract);
        console.log('\nDeployment and interaction completed successfully!');
      })
      .catch((error) => {
        console.error('Error:', error);
        process.exit(1);
      });
    
  6. Run the deployment script:

    node deploy.js
    

    This should output something like:

    Deploying from account: 0xBd828F7679656F8f830b89611C933017442F2EbF
    Account balance: 98539.897261933991888304 ETH
    Deploying contract...
    Contract deployed to: 0xf298A2A7BC526F9228B8C422D38f3c2E0D15449F
    Transaction hash: 0x3d16cc55b9148bac9ac20981d9748a0fc89861c6beb92a2f869bb09b75f685b2
    
    Interacting with deployed contract...
    Initial data: Hello, Plasma from Ethers.js!
    Data updated. Transaction hash: 0xe3328862ece5d0ca4ca84d25c4130d6749ec872c24bf76eea23dfa8c50c22505
    Updated data: Updated via Ethers.js
    Contract owner: 0xBd828F7679656F8f830b89611C933017442F2EbF
    
    Deployment and interaction completed successfully!
    

Verify Deployment

After deploying with any of the above methods, verify your contract deployment:

Check on Block Explorer

  1. Visit the Plasma testnet explorer.
  2. Search for your contract address.
  3. Verify the contract creation transaction appears.
  4. Check that the contract code is visible (if verified).

Programmatic Verification

You can create a simple verification script using ethers.js.

  1. Follow through the steps to deploy with Ethere.js.

  2. Create a new file called verify.js:

    const { ethers } = require('ethers');
    const fs = require('fs');
    require('dotenv').config();
    
    async function verifyDeployment(contractAddress) {
      const provider = new ethers.JsonRpcProvider(process.env.RPC_URL);
      
      // Check if contract exists.
      const code = await provider.getCode(contractAddress);
      if (code === '0x') {
        console.log('No contract found at address:', contractAddress);
        return false;
      }
      
      console.log('Contract found at:', contractAddress);
      console.log('Contract bytecode length:', code.length);
      
      // Load ABI and create contract instance.
      const contractData = JSON.parse(fs.readFileSync('SimpleStorage.json', 'utf8'));
      const contract = new ethers.Contract(contractAddress, contractData.abi, provider);
      
      try {
        // Test contract functionality.
        const data = await contract.getData();
        console.log('Contract data:', data);
        
        const owner = await contract.owner();
        console.log('Contract owner:', owner);
        
        return true;
      } catch (error) {
        console.log('Error interacting with contract:', error.message);
        return false;
      }
    }
    
    // Replace with your deployed contract address.
    verifyDeployment('ADDRESS_HERE');
    
  3. Replace the ADDRESS_HERE placeholder with your actual deployed contract address.

  4. Run the verify script with Node:

    node verify.js
    

    This should output something like:

    Contract found at: 0xf298A2A7BC526F9228B8C422D38f3c2E0D15449F
    Contract bytecode length: 2842
    Contract data: Updated via Ethers.js
    Contract owner: 0xBd828F7679656F8f830b89611C933017442F2EbF
    

Troubleshooting

Insufficient Funds

Error: insufficient funds for intrinsic transaction cost

Ensure your wallet has enough testnet tokens. Visit the testnet faucet.

Wrong Network Configuration

Error: network with chainId "1" doesn't match the configured chainId "9746"

Verify your network configuration matches the testnet chain details.

Gas Estimation Failure:

Error: cannot estimate gas

Set explicit gas limits in your deployment configuration.