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.
-
Install Foundry:
curl -L https://foundry.paradigm.xyz | bash
You may have to source your .bashrc
or .zshrc
file
-
Set up a new Foundry project:
foundryup
forge init my-plasma-project
cd my-plasma-project
-
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"
-
Create a .env
file in your project root:
PRIVATE_KEY=your_private_key_here
RPC_URL=https://testnet-rpc.plasma.to
-
Load the environment variables:
-
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",
[...]
-
Test your deployed contract using Foundry’s cast tool. First, read the stored data:
cast call 0x742d35Cc6610C7532C8582d4C371Acb1D5f44D7F \
"getData()" \
--rpc-url $RPC_URL
-
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.
-
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.
-
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.
-
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>
-
Make a directory to contain all your scripts:
-
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);
});
-
Save your contract as contracts/SimpleStorage.sol
.
-
Deploy the contract:
npx hardhat run scripts/deploy.js --network plasmaTestnet
This should output something like:
Deploying SimpleStorage contract...
SimpleStorage deployed to: 0xa64600dB50A812D5944c33f8e39f257786517Aaa
-
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);
});
});
-
Run the tests:
npx hardhat test --network plasmaTestnet
Deploy with Ethers.js
Ethers.js provides a minimal, programmatic deployment flow.
-
Create a new project:
mkdir my-plasma-ethers-project
cd my-plasma-ethers-project
npm init -y
npm install ethers dotenv solc
-
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
-
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!');
-
Save your contract as SimpleStorage.sol
, then compile:
This should output:
Contract compiled successfully!
-
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);
});
-
Run the deployment script:
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
- Visit the Plasma testnet explorer.
- Search for your contract address.
- Verify the contract creation transaction appears.
- Check that the contract code is visible (if verified).
Programmatic Verification
You can create a simple verification script using ethers.js.
-
Follow through the steps to deploy with Ethere.js.
-
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');
-
Replace the ADDRESS_HERE
placeholder with your actual deployed contract address.
-
Run the verify script with Node:
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.