Understanding theory is not enough. Deploying a smart contract in practice requires a clear process: initializing the project, writing the contract, testing, configuring networks, writing deployment scripts, verifying on a block explorer, and performing final checks before going to mainnet.
In this part, we will walk through every step, from local development to testnet and mainnet deployment, including how to interact with your contract after deployment. You will also get a final checklist before moving to production, where even a small mistake can cause significant losses.
1. Hands-on: Deploying a Smart Contract Step by Step

This guide uses Hardhat v3 with TypeScript and viem – a modern stack recommended for new projects.
1.1. Initialize the project
# Create project folder
mkdir my-first-contract && cd my-first-contract
# Install Hardhat and viem toolbox
npm init -y
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox-viem typescript viem dotenv
# Initialize Hardhat project
npx hardhat init
# → Select "TypeScript project"
Project structure after initialization:
├── contracts/ ← Solidity smart contracts
├── ignition/modules/ ← Hardhat Ignition (deploy modules)
├── scripts/ ← Manual deployment scripts
├── test/ ← Unit tests
├── hardhat.config.ts
└── package.json
1.2. Write the smart contract
Create contracts/MyContract.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
contract MyContract {
uint256 private _value;
event ValueChanged(uint256 newValue);
function store(uint256 newValue) public {
_value = newValue;
emit ValueChanged(newValue);
}
function retrieve() public view returns (uint256) {
return _value;
}
}
1.3. Writes tests
Hardhat v3 uses node:test (built into Node.js) combined with viem – no need for Mocha, Chai, or ethers.js.
Create test/MyContract.ts:
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { network } from "hardhat";
describe("MyContract", async function () {
const { viem } = await network.connect();
const publicClient = await viem.getPublicClient();
it("Initial value should be 0", async function () {
const myContract = await viem.deployContract("MyContract");
assert.equal(await myContract.read.retrieve(), 0n);
});
it("Should store and retrieve value", async function () {
const myContract = await viem.deployContract("MyContract");
await myContract.write.store([42n]);
assert.equal(await myContract.read.retrieve(), 42n);
});
it("Should emit ValueChanged event", async function () {
const myContract = await viem.deployContract("MyContract");
await viem.assertions.emitWithArgs(
myContract.write.store([100n]),
myContract,
"ValueChanged",
[100n],
);
});
it("Multiple stores — only last value persists", async function () {
const myContract = await viem.deployContract("MyContract");
const deploymentBlock = await publicClient.getBlockNumber();
await myContract.write.store([10n]);
await myContract.write.store([20n]);
await myContract.write.store([30n]);
assert.equal(await myContract.read.retrieve(), 30n);
const events = await publicClient.getContractEvents({
address: myContract.address,
abi: myContract.abi,
eventName: "ValueChanged",
fromBlock: deploymentBlock,
strict: true,
});
assert.equal(events.length, 3);
});
});
Note: viem uses bigint for Solidity integers —-write 42n instead of 42.
Run tests:
npx hardhat test
1.4. Configure network
Update hardhat.config.ts:
(Structure and config identical to original — preserved fully in translation.)
Key differences from Hardhat v2:
- Uses
defineConfig()instead ofmodule.exports - Uses
configVariable()instead ofprocess.env - Verify config under
verify.etherscan
Create .env file (add to .gitignore):
SEPOLIA_RPC_URL=...
MAINNET_RPC_URL=...
SEPOLIA_PRIVATE_KEY=...
MAINNET_PRIVATE_KEY=...
ETHERSCAN_API_KEY=...
⚠ WARNING: Never commit private keys to Git. In production, use hardware wallets or key management services (KMS).
1.5. Write deployment script
Create scripts/deploy.ts:
(Full translated script preserved; functionality unchanged.)
Key differences from Hardhat v2:
- Uses top-level
await(ESM style) - Uses
network.connect()instead ofhre.ethers viem.deployContract()waits for confirmation automatically- Saves deployment info to
deployments/<chainId>.jsonfor CI/CD usage
1.6. Deploy!
npx hardhat run scripts/deploy.ts --network sepolia
Then verify:
npx hardhat verify --network sepolia <contractAddress>
2. Verify and Interact with the Contract After Deployment
Deployment is only half the journey. Next comes validation.
2.1. Check on block explorer
Verify:
- Deployment transaction status = Success
- Source code verified
- Constructor arguments match expectations
2.2. Interact via script
Create scripts/interact.ts:
(Full interaction script translated faithfully.)
Run:
npx hardhat run scripts/interact.ts --network sepolia
2.3. Interact via etherscan UI
If verified, you can:
- Use “Read Contract” for view functions (free)
- Use “Write Contract” for state changes (requires wallet + gas)
2.4. Interact via foundry cast
# Read (free)
cast call <address> "retrieve()(uint256)" --rpc-url $SEPOLIA_RPC_URL
# Write (gas required)
cast send <address> "store(uint256)" 42 \
--rpc-url $SEPOLIA_RPC_URL \
--private-key $SEPOLIA_PRIVATE_KEY
3. Checklist Before Mainnet Deployment
Before deploying with real funds, ensure:
- All unit tests pass (coverage > 90%)
- Static analysis (Slither/Mythril) shows no critical findings
- Fully tested on testnet
- Source code verified
- Gas optimization reviewed
- Private key managed securely
- Upgrade plan prepared (proxy pattern if needed)
- Third-party audit completed (for high-value projects)
- Monitoring and alerting configured
Conclusion
Deploying a smart contract is not just running a script – it is a full lifecycle: coding, testing, security analysis, deployment, verification, and monitoring.
A few key takeaways:
Test first, deploy later. There is no “Undo” on blockchain. Mainnet transactions cost real money and are irreversible.
Security is not optional. A minor bug can cost millions.
Automation is your ally. CI/CD pipelines reduce human error, enforce consistency, and allow teams to move faster without compromising safety.




