Web 3.0: Exploring Smart Contracts with Solidity
Web3, blockchain, smart contracts—the buzzwords are everywhere. As a developer, I wanted to understand what’s actually there. Here’s what I learned exploring Solidity.
What Are Smart Contracts?
Smart contracts are programs stored on a blockchain:
// A simple smart contract
contract SimpleStorage {
uint256 storedData;
function set(uint256 x) public {
storedData = x;
}
function get() public view returns (uint256) {
return storedData;
}
}
Key properties:
- Immutable: Once deployed, code can’t change
- Deterministic: Same inputs = same outputs
- Decentralized: Runs on all nodes
- Transparent: Code is public
Solidity Basics
Solidity is the primary language for Ethereum smart contracts:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Counter {
uint256 private count;
address public owner;
event CountChanged(uint256 newCount);
constructor() {
owner = msg.sender;
count = 0;
}
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
function increment() public {
count += 1;
emit CountChanged(count);
}
function getCount() public view returns (uint256) {
return count;
}
}
Key Concepts
State Variables: Permanently stored on blockchain
uint256 public balance;
mapping(address => uint256) public balances;
Functions: Contract methods
function transfer(address to, uint256 amount) public {
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
balances[to] += amount;
}
Events: Logs for off-chain consumption
event Transfer(address indexed from, address indexed to, uint256 amount);
emit Transfer(msg.sender, to, amount);
Modifiers: Reusable condition checks
modifier onlyOwner() {
require(msg.sender == owner, "Not authorized");
_;
}
Development Environment
Hardhat Setup
npm init -y
npm install --save-dev hardhat
npx hardhat
// hardhat.config.js
module.exports = {
solidity: "0.8.17",
networks: {
hardhat: {},
goerli: {
url: process.env.GOERLI_URL,
accounts: [process.env.PRIVATE_KEY]
}
}
};
Testing
const { expect } = require("chai");
describe("Counter", function () {
it("Should increment count", async function () {
const Counter = await ethers.getContractFactory("Counter");
const counter = await Counter.deploy();
await counter.deployed();
await counter.increment();
expect(await counter.getCount()).to.equal(1);
});
});
A Simple Token
ERC-20 token implementation:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SimpleToken {
string public name = "Simple Token";
string public symbol = "SIMP";
uint8 public decimals = 18;
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
constructor(uint256 _initialSupply) {
totalSupply = _initialSupply * 10 ** decimals;
balanceOf[msg.sender] = totalSupply;
}
function transfer(address to, uint256 value) public returns (bool) {
require(balanceOf[msg.sender] >= value, "Insufficient balance");
balanceOf[msg.sender] -= value;
balanceOf[to] += value;
emit Transfer(msg.sender, to, value);
return true;
}
function approve(address spender, uint256 value) public returns (bool) {
allowance[msg.sender][spender] = value;
emit Approval(msg.sender, spender, value);
return true;
}
}
Security Considerations
Reentrancy Attack
The most famous vulnerability:
// VULNERABLE
function withdraw() public {
uint256 amount = balances[msg.sender];
(bool success, ) = msg.sender.call{value: amount}("");
require(success);
balances[msg.sender] = 0; // Updated AFTER external call
}
// SAFE
function withdrawSafe() public {
uint256 amount = balances[msg.sender];
balances[msg.sender] = 0; // Updated BEFORE external call
(bool success, ) = msg.sender.call{value: amount}("");
require(success);
}
Integer Overflow
Solidity 0.8+ has built-in overflow protection:
// Pre-0.8: Would wrap around
uint8 x = 255;
x += 1; // Now reverts instead of becoming 0
Access Control
// Use OpenZeppelin's battle-tested contracts
import "@openzeppelin/contracts/access/Ownable.sol";
contract MyContract is Ownable {
function sensitiveAction() public onlyOwner {
// Only owner can call
}
}
Gas Costs
Every operation costs gas:
| Operation | Gas Cost |
|---|---|
| Store value | ~20,000 |
| Read value | ~200 |
| Transfer ETH | 21,000 |
| Deploy contract | 100,000+ |
Gas optimization matters:
// Expensive: Multiple storage writes
function inefficient(uint256[] memory values) public {
for (uint i = 0; i < values.length; i++) {
total += values[i]; // Storage write each iteration
}
}
// Better: Single storage write
function efficient(uint256[] memory values) public {
uint256 sum = 0;
for (uint i = 0; i < values.length; i++) {
sum += values[i]; // Memory only
}
total = sum; // One storage write
}
My Honest Assessment
What’s Interesting
- Programmable money is novel
- Immutability creates interesting constraints
- DeFi protocols are clever engineering
What’s Concerning
- Immutable bugs: No fixing production issues
- Gas costs: Expensive operations limit use cases
- Scalability: Ethereum handles ~15 TPS
- UX: Wallets, gas fees, confirmations are confusing
Use Cases That Make Sense
- Asset tokenization
- Cross-border payments
- Programmable escrow
- Supply chain verification
Use Cases That Don’t (Yet)
- General web applications
- High-frequency operations
- Anything needing reversibility
Final Thoughts
Smart contracts are interesting technology with real limitations. The developer experience is improving, but the constraints are fundamental.
Learn Solidity to understand the space. Deploy with caution.
Code is law, until you find a bug.