"Code is law — and on Ethereum, it runs forever."
What Is a Smart Contract?
A smart contract is a self-executing program stored on a blockchain. It runs automatically when predefined conditions are met. No middlemen, no downtime, no censorship.
- Immutable (once deployed, the logic doesn't change)
- Transparent (anyone can read the code)
- Trustless (no central authority needed)
- Global (accessible from anywhere on Earth)
Solidity
Solidity is the primary language for writing smart contracts on Ethereum and EVM-compatible chains like Polygon, BNB Chain, Avalanche, Base and others.
It's a statically-typed, contract-oriented language with syntax that feels familiar if you've used JavaScript, C++, or Java.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract HelloBlockchain {
string public message = "gm, world";
function setMessage(string calldata _msg) external {
message = _msg;
}
}
Core Concepts
1. State Variables & Storage
Everything stored on-chain costs gas. Be deliberate.
contract Bank {
mapping(address => uint256) public balances; // stored on-chain
uint256 public totalDeposits;
}
2. Functions & Visibility
| Modifier | Who Can Call |
|---|---|
public |
Anyone (internal + external) |
external |
Only from outside the contract |
internal |
Only this contract + children |
private |
Only this contract |
3. Payable Functions — Receiving ETH
function deposit() external payable {
balances[msg.sender] += msg.value;
totalDeposits += msg.value;
}
msg.sender = the caller's address
msg.value = ETH sent with the call (in wei)
4. Events — Your On-Chain Logs
Events are cheap to emit and essential for off-chain apps to track activity.
event Deposited(address indexed user, uint256 amount);
function deposit() external payable {
balances[msg.sender] += msg.value;
emit Deposited(msg.sender, msg.value);
}
5. Modifiers — Reusable Guards
modifier onlyOwner() {
require(msg.sender == owner, "Not the owner");
_;
}
function withdraw(uint256 amount) external onlyOwner {
payable(owner).transfer(amount);
}
A Real Example: Simple Token
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract SimpleToken {
string public name = "DevToken";
string public symbol = "DEV";
uint8 public decimals = 18;
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
event Transfer(address indexed from, address indexed to, uint256 value);
constructor(uint256 _initialSupply) {
totalSupply = _initialSupply * 10 ** decimals;
balanceOf[msg.sender] = totalSupply;
}
function transfer(address to, uint256 amount) external returns (bool) {
require(balanceOf[msg.sender] >= amount, "Insufficient balance");
balanceOf[msg.sender] -= amount;
balanceOf[to] += amount;
emit Transfer(msg.sender, to, amount);
return true;
}
}
This is the essence of an ERC-20 token. The real standard adds approve, transferFrom, and allowance but this gives you the foundation.
Security: The Non-Negotiables
Smart contract bugs are permanent and public. The stakes are high.
Reentrancy Attack
The infamous bug behind the $60M DAO hack (2016).
// VULNERABLE
function withdraw() external {
uint256 amount = balances[msg.sender];
(bool success,) = msg.sender.call{value: amount}(""); // external call BEFORE state update
balances[msg.sender] = 0; // too late
}
// SAFE
function withdraw() external {
uint256 amount = balances[msg.sender];
balances[msg.sender] = 0; // 1. Update state
(bool success,) = msg.sender.call{value: amount}(""); // 2. Then interact
require(success, "Transfer failed");
}
Rule: Always update state before making external calls.
Other Common Pitfalls
- Integer overflow/underflow — Solidity 0.8+ handles this automatically with built-in checks
-
tx.origin vs msg.sender — Never use
tx.originfor authorization -
Unchecked return values — Always check the return value of
call() -
Timestamp dependence —
block.timestampcan be manipulated slightly by miners
The Developer Toolchain
| Tool | Purpose |
|---|---|
| Hardhat | Local dev environment, testing, deployment |
| Foundry | Blazing-fast testing in Solidity itself |
| Remix IDE | Browser-based IDE, great for prototyping |
| OpenZeppelin | Battle-tested contract libraries |
| Ethers.js / Viem | JavaScript libraries to interact with contracts |
| Alchemy / Infura | Node providers for mainnet/testnet access |
Quick Start with Hardhat
mkdir my-contract && cd my-contract
npm init -y
npm install --save-dev hardhat
npx hardhat init
Deploy a Contract
// scripts/deploy.js
const { ethers } = require("hardhat");
async function main() {
const Token = await ethers.getContractFactory("SimpleToken");
const token = await Token.deploy(1_000_000);
await token.waitForDeployment();
console.log("Deployed to:", await token.getAddress());
}
main().catch(console.error);
npx hardhat run scripts/deploy.js --network sepolia
Gas Optimization Tips
Gas efficiency = lower costs for your users = competitive advantage.
// Expensive: reading from storage in a loop
for (uint i = 0; i < users.length; i++) {
total += balances[users[i]];
}
// Cheaper: cache storage reads in memory
uint256 len = users.length;
for (uint i = 0; i < len; i++) {
total += balances[users[i]];
}
Other wins:
- Use
uint256over smaller types ,the EVM works in 32-byte slots. - Mark functions
vieworpurewhen they don't modify state - Use
calldatainstead ofmemoryfor external function parameters - Pack struct variables to fit in fewer storage slots
- Emit events instead of storing data you only need off-chain
Contract Standards Worth Knowing
| Standard | What It Is |
|---|---|
| ERC-20 | Fungible tokens (USDC, DAI, UNI) |
| ERC-721 | Non-fungible tokens / NFTs |
| ERC-1155 | Multi-token standard (games, etc.) |
| ERC-4626 | Tokenized vaults (DeFi yield) |
| EIP-2612 | Gasless approvals via signatures |
OpenZeppelin has production-ready implementations of all of these:
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MyToken is ERC20 {
constructor() ERC20("MyToken", "MTK") {
_mint(msg.sender, 1_000_000 * 10 ** decimals());
}
}
Testing
// test/Token.test.js (Hardhat + Ethers)
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("SimpleToken", function () {
it("should assign total supply to deployer", async function () {
const [owner] = await ethers.getSigners();
const Token = await ethers.getContractFactory("SimpleToken");
const token = await Token.deploy(1000);
const balance = await token.balanceOf(owner.address);
expect(balance).to.equal(await token.totalSupply());
});
it("should transfer tokens correctly", async function () {
const [owner, recipient] = await ethers.getSigners();
const Token = await ethers.getContractFactory("SimpleToken");
const token = await Token.deploy(1000);
await token.transfer(recipient.address, ethers.parseUnits("100", 18));
const recipientBalance = await token.balanceOf(recipient.address);
expect(recipientBalance).to.equal(ethers.parseUnits("100", 18));
});
});
Final Thoughts
Smart contract development is one of the most high-stakes disciplines in software. A bug that slips through in a web app might annoy users. A bug in a smart contract holding $10M can be catastrophic and irreversible.
The upside? You're building programmable money, unstoppable applications, and trustless infrastructure that can outlive any company or server.
Top comments (0)