Introduction
Solidity is the primary programming language for building smart contracts on Ethereum and EVM-compatible blockchains. As of 2025, over $100 billion is locked in smart contracts across various DeFi protocols, making secure development paramount. This comprehensive guide takes you from fundamentals to production-ready smart contract development.
What You’ll Learn:
- Core Solidity concepts and syntax
- Smart contract design patterns and architectures
- Security best practices and common vulnerabilities
- Gas optimization techniques to reduce costs
- Complete development workflow from setup to deployment
- Testing strategies and debugging techniques
- Real-world examples and production considerations
Prerequisites:
- Basic programming knowledge (JavaScript or Python recommended)
- Understanding of blockchain fundamentals
- Familiarity with command line tools
- Node.js installed (version 16 or higher)
Core Concepts and Terminology
Smart Contract: Self-executing code deployed on a blockchain that automatically executes when conditions are met.
Solidity: Statically-typed programming language designed for writing smart contracts on Ethereum.
EVM (Ethereum Virtual Machine): The runtime environment that executes smart contracts on Ethereum.
Gas: The computational cost of executing operations on Ethereum, measured in wei (smallest ETH unit).
Wei: The smallest unit of Ether (1 ETH = 10^18 wei).
State Variables: Data stored permanently on the blockchain as part of the contract’s state.
Functions: Executable code blocks that can read or modify contract state.
Modifiers: Reusable code patterns that can be applied to functions to add functionality.
Events: Logs that smart contracts emit to notify external systems of state changes.
ABI (Application Binary Interface): The interface specification for interacting with smart contracts.
Bytecode: The compiled machine-readable version of Solidity code that runs on the EVM.
Reentrancy: A security vulnerability where a function can be called recursively before the first call completes.
Smart Contract Architecture
Smart Contract Deployment Flow
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Solidity Source Code โ
โ (contract.sol) โ
โโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโผโโโโโโโโโ
โ Solidity Compilerโ
โ (solc) โ
โโโโโโโโโโฌโโโโโโโโโ
โ
โโโโโโโโโโผโโโโโโโโโโโโโโโโโ
โ Bytecode + ABI โ
โ (Compiled Contract) โ
โโโโโโโโโโฌโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโผโโโโโโโโโโโโโโโโโ
โ Deploy to Blockchain โ
โ (Transaction) โ
โโโโโโโโโโฌโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโผโโโโโโโโโโโโโโโโโ
โ Contract Address โ
โ (Deployed on Chain) โ
โโโโโโโโโโฌโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโผโโโโโโโโโโโโโโโโโ
โ External Interactions โ
โ (Calls & Transactions) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโ
Development Environment Setup
Before writing Solidity code, set up a proper development environment.
Installing Node.js and npm
# Ubuntu/Debian
sudo apt update
sudo apt install nodejs npm
# macOS (using Homebrew)
brew install node
# Verify installation
node --version
npm --version
Setting Up Hardhat (Recommended)
Hardhat is the most popular Ethereum development environment:
# Create project directory
mkdir my-smart-contract
cd my-smart-contract
# Initialize npm project
npm init -y
# Install Hardhat
npm install --save-dev hardhat
# Initialize Hardhat project
npx hardhat init
# Choose "Create a JavaScript project"
# Install dependencies
npm install --save-dev @nomicfoundation/hardhat-toolbox
npm install @openzeppelin/contracts
Project Structure
my-smart-contract/
โโโ contracts/ # Solidity contracts
โโโ scripts/ # Deployment scripts
โโโ test/ # Test files
โโโ hardhat.config.js # Configuration
โโโ package.json
Hardhat Configuration
// hardhat.config.js
require("@nomicfoundation/hardhat-toolbox");
module.exports = {
solidity: {
version: "0.8.20",
settings: {
optimizer: {
enabled: true,
runs: 200
}
}
},
networks: {
hardhat: {},
sepolia: {
url: process.env.SEPOLIA_URL || "",
accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : []
}
},
etherscan: {
apiKey: process.env.ETHERSCAN_API_KEY
}
};
Alternative: Truffle Setup
# Install Truffle globally
npm install -g truffle
# Create project
mkdir my-truffle-project
cd my-truffle-project
truffle init
IDE Setup: VS Code Extensions
Install these VS Code extensions for optimal development:
- Solidity by Juan Blanco - Syntax highlighting and IntelliSense
- Hardhat Solidity - Hardhat integration
- Solidity Visual Developer - Enhanced visualization
- Prettier - Code formatter - Code formatting
// .vscode/settings.json
{
"solidity.compileUsingRemoteVersion": "v0.8.20",
"solidity.formatter": "prettier",
"editor.formatOnSave": true
}
Solidity Fundamentals
Basic Contract Structure
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SimpleStorage {
// State variables (stored on blockchain)
uint256 public storedValue;
address public owner;
// Events (logs for external systems)
event ValueChanged(uint256 newValue, address indexed changer);
// Constructor (runs once at deployment)
constructor() {
owner = msg.sender;
storedValue = 0;
}
// Function to update value
function setValue(uint256 newValue) public {
require(msg.sender == owner, "Only owner can set value");
storedValue = newValue;
emit ValueChanged(newValue, msg.sender);
}
// View function (reads state, no gas cost)
function getValue() public view returns (uint256) {
return storedValue;
}
}
Data Types and Variables
contract DataTypes {
// Unsigned integers
uint8 smallNumber = 255; // 0 to 255 (8 bits)
uint256 largeNumber = 1000; // 0 to 2^256-1 (256 bits, default)
// Signed integers
int256 negativeNumber = -100; // -(2^255) to 2^255-1
// Boolean
bool isActive = true; // true or false
// Address (20 bytes / 160 bits)
address userAddress = 0x1234567890123456789012345678901234567890;
address payable recipient = payable(userAddress); // Can receive Ether
// Strings and bytes
string message = "Hello, Solidity!"; // UTF-8 encoded
bytes32 fixedBytes = "data"; // Fixed-size byte array
bytes dynamicBytes; // Dynamic byte array
// Arrays
uint256[] dynamicArray; // Dynamic array
uint256[5] fixedArray; // Fixed-size array
// Mappings (key-value store)
mapping(address => uint256) public balances;
mapping(address => mapping(address => bool)) public approvals; // Nested mapping
// Structs (custom types)
struct User {
string name;
uint256 balance;
bool active;
}
User[] public users; // Array of structs
mapping(address => User) public userMap; // Mapping to struct
// Enums (custom state values)
enum Status { Pending, Active, Completed, Cancelled }
Status public currentStatus = Status.Pending;
// Constants and immutables (gas efficient)
uint256 public constant MAX_SUPPLY = 1000000; // Set at compile time
address public immutable CREATOR; // Set at deployment
constructor() {
CREATOR = msg.sender;
}
// Working with arrays
function arrayOperations() public {
// Add element
dynamicArray.push(100);
// Access element
uint256 value = dynamicArray[0];
// Get length
uint256 length = dynamicArray.length;
// Remove last element
dynamicArray.pop();
}
// Working with structs
function createUser(string memory _name) public {
users.push(User({
name: _name,
balance: 0,
active: true
}));
userMap[msg.sender] = User(_name, 0, true);
}
}
Smart Contract Patterns
1. Access Control Pattern
contract AccessControl {
address public owner;
mapping(address => bool) public admins;
modifier onlyOwner() {
require(msg.sender == owner, "Only owner");
_;
}
modifier onlyAdmin() {
require(admins[msg.sender], "Only admin");
_;
}
constructor() {
owner = msg.sender;
}
function addAdmin(address admin) public onlyOwner {
admins[admin] = true;
}
function removeAdmin(address admin) public onlyOwner {
admins[admin] = false;
}
function adminFunction() public onlyAdmin {
// Only admins can call this
}
}
2. Reentrancy Protection Pattern
contract ReentrancyProtection {
mapping(address => uint256) balances;
bool private locked;
modifier nonReentrant() {
require(!locked, "No reentrancy");
locked = true;
_;
locked = false;
}
function withdraw(uint256 amount) public nonReentrant {
require(balances[msg.sender] >= amount, "Insufficient balance");
// Update state before external call
balances[msg.sender] -= amount;
// External call (safe from reentrancy)
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
}
3. Upgradeable Contract Pattern
// Proxy contract
contract Proxy {
address public implementation;
address public owner;
constructor(address _implementation) {
implementation = _implementation;
owner = msg.sender;
}
function upgrade(address newImplementation) public {
require(msg.sender == owner, "Only owner");
implementation = newImplementation;
}
fallback() external payable {
address impl = implementation;
assembly {
let ptr := mload(0x40)
calldatacopy(ptr, 0, calldatasize())
let result := delegatecall(gas(), impl, ptr, calldatasize(), 0, 0)
returndatacopy(ptr, 0, returndatasize())
switch result
case 0 { revert(ptr, returndatasize()) }
default { return(ptr, returndatasize()) }
}
}
}
Advanced Solidity Features
Function Types and Visibility
contract FunctionTypes {
uint256 private data;
// Public: Can be called internally and externally
function publicFunction() public returns (uint256) {
return data;
}
// External: Can only be called from outside (more gas efficient)
function externalFunction() external returns (uint256) {
return data;
}
// Internal: Can be called within contract and derived contracts
function internalFunction() internal returns (uint256) {
return data;
}
// Private: Only within this contract
function privateFunction() private returns (uint256) {
return data;
}
// Pure: Doesn't read or modify state
function pureMath(uint256 a, uint256 b) public pure returns (uint256) {
return a + b;
}
// View: Reads state but doesn't modify
function viewData() public view returns (uint256) {
return data;
}
// Payable: Can receive Ether
function deposit() public payable {
// msg.value contains sent Ether
}
}
Inheritance and Abstract Contracts
// Abstract contract (cannot be deployed)
abstract contract BaseContract {
uint256 public value;
// Abstract function (must be implemented by derived contracts)
function calculate() public virtual returns (uint256);
// Concrete function (can be overridden)
function setValue(uint256 _value) public virtual {
value = _value;
}
}
// Interface (all functions are abstract)
interface IToken {
function transfer(address to, uint256 amount) external returns (bool);
function balanceOf(address account) external view returns (uint256);
}
// Derived contract
contract DerivedContract is BaseContract {
// Override abstract function
function calculate() public override returns (uint256) {
return value * 2;
}
// Override and extend
function setValue(uint256 _value) public override {
require(_value > 0, "Value must be positive");
super.setValue(_value); // Call parent implementation
}
}
// Multiple inheritance
contract MultiInheritance is BaseContract, IToken {
mapping(address => uint256) private balances;
function calculate() public override returns (uint256) {
return value;
}
function transfer(address to, uint256 amount) external override returns (bool) {
balances[msg.sender] -= amount;
balances[to] += amount;
return true;
}
function balanceOf(address account) external view override returns (uint256) {
return balances[account];
}
}
Error Handling
contract ErrorHandling {
// Custom errors (gas efficient in Solidity 0.8.4+)
error InsufficientBalance(uint256 available, uint256 required);
error Unauthorized(address caller);
mapping(address => uint256) public balances;
address public owner;
constructor() {
owner = msg.sender;
}
// Using custom errors
function withdraw(uint256 amount) public {
if (balances[msg.sender] < amount) {
revert InsufficientBalance({
available: balances[msg.sender],
required: amount
});
}
balances[msg.sender] -= amount;
}
// Using require (more gas, provides string message)
function requireExample(uint256 amount) public view {
require(amount > 0, "Amount must be positive");
require(msg.sender == owner, "Only owner can call");
}
// Using assert (for invariant checks)
function assertExample() public view {
assert(address(this).balance >= 0); // Should always be true
}
// Try-catch for external calls
function tryExternalCall(address target) public returns (bool) {
try IToken(target).transfer(msg.sender, 100) returns (bool success) {
return success;
} catch Error(string memory reason) {
// Catch revert with reason string
return false;
} catch (bytes memory lowLevelData) {
// Catch all other errors
return false;
}
}
}
Libraries
// Library for safe math operations (example for pre-0.8 versions)
library SafeMath {
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
require(c >= a, "SafeMath: addition overflow");
return c;
}
function mul(uint256 a, uint256 b) internal pure returns (uint256) {
if (a == 0) return 0;
uint256 c = a * b;
require(c / a == b, "SafeMath: multiplication overflow");
return c;
}
}
// Using libraries
contract UsingLibrary {
using SafeMath for uint256;
function calculate(uint256 a, uint256 b) public pure returns (uint256) {
return a.add(b); // Library function as method
}
}
// Library with state
library AddressUtils {
function isContract(address account) internal view returns (bool) {
uint256 size;
assembly {
size := extcodesize(account)
}
return size > 0;
}
}
Gas Optimization Techniques
1. Storage Optimization
// โ Inefficient: Uses 3 storage slots
contract Inefficient {
uint256 value1; // Slot 0
uint256 value2; // Slot 1
uint256 value3; // Slot 2
}
// โ
Efficient: Uses 1 storage slot
contract Optimized {
uint64 value1; // Slot 0 (0-63 bits)
uint64 value2; // Slot 0 (64-127 bits)
uint128 value3; // Slot 0 (128-255 bits)
}
2. Function Optimization
// โ Inefficient: Reads storage multiple times
function inefficient() public view returns (uint256) {
uint256 total = 0;
for (uint i = 0; i < 100; i++) {
total += data[i]; // Storage read each iteration
}
return total;
}
// โ
Efficient: Caches storage value
function optimized() public view returns (uint256) {
uint256[] memory cachedData = data; // Cache in memory
uint256 total = 0;
for (uint i = 0; i < 100; i++) {
total += cachedData[i]; // Memory read (cheaper)
}
return total;
}
3. Loop Optimization
// โ Inefficient: Reads length each iteration
for (uint i = 0; i < array.length; i++) {
// Process array[i]
}
// โ
Efficient: Cache length
uint256 length = array.length;
for (uint i = 0; i < length; i++) {
// Process array[i]
}
4. Using Calldata for Read-Only Parameters
// โ Less efficient: memory parameter
function processData(string memory data) public pure returns (uint256) {
return bytes(data).length;
}
// โ
More efficient: calldata parameter (for external functions)
function processDataOptimized(string calldata data) external pure returns (uint256) {
return bytes(data).length;
}
5. Short-Circuit Evaluation
// โ Less efficient: evaluates both conditions
if (expensiveCheck() && cheapCheck()) {
// ...
}
// โ
More efficient: cheap check first
if (cheapCheck() && expensiveCheck()) {
// Short-circuits if cheapCheck() is false
}
6. Batch Operations
contract BatchOperations {
mapping(address => uint256) public balances;
// โ Inefficient: multiple transactions
function transfer(address to, uint256 amount) external {
balances[msg.sender] -= amount;
balances[to] += amount;
}
// โ
Efficient: batch transfer
function batchTransfer(address[] calldata recipients, uint256[] calldata amounts)
external
{
require(recipients.length == amounts.length, "Length mismatch");
for (uint256 i = 0; i < recipients.length; i++) {
balances[msg.sender] -= amounts[i];
balances[recipients[i]] += amounts[i];
}
}
}
7. Use Events Instead of Storage
// โ Expensive: storing history on-chain
contract ExpensiveHistory {
struct Action {
address user;
uint256 timestamp;
string action;
}
Action[] public history; // Very expensive
function recordAction(string memory action) public {
history.push(Action(msg.sender, block.timestamp, action));
}
}
// โ
Cheaper: use events for history
contract CheapHistory {
event ActionRecorded(address indexed user, uint256 timestamp, string action);
function recordAction(string memory action) public {
emit ActionRecorded(msg.sender, block.timestamp, action);
// Events can be queried off-chain
}
}
Testing Smart Contracts
Unit Testing with Hardhat
Create a test file in test/MyContract.test.js:
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("SimpleStorage", function () {
let simpleStorage;
let owner;
let addr1;
beforeEach(async function () {
// Get signers
[owner, addr1] = await ethers.getSigners();
// Deploy contract
const SimpleStorage = await ethers.getContractFactory("SimpleStorage");
simpleStorage = await SimpleStorage.deploy();
await simpleStorage.waitForDeployment();
});
describe("Deployment", function () {
it("Should set the right owner", async function () {
expect(await simpleStorage.owner()).to.equal(owner.address);
});
it("Should initialize value to 0", async function () {
expect(await simpleStorage.storedValue()).to.equal(0);
});
});
describe("setValue", function () {
it("Should set value when called by owner", async function () {
await simpleStorage.setValue(42);
expect(await simpleStorage.storedValue()).to.equal(42);
});
it("Should emit ValueChanged event", async function () {
await expect(simpleStorage.setValue(42))
.to.emit(simpleStorage, "ValueChanged")
.withArgs(42, owner.address);
});
it("Should revert when called by non-owner", async function () {
await expect(simpleStorage.connect(addr1).setValue(42))
.to.be.revertedWith("Only owner can set value");
});
});
describe("Gas usage", function () {
it("Should track gas usage", async function () {
const tx = await simpleStorage.setValue(100);
const receipt = await tx.wait();
console.log(`Gas used: ${receipt.gasUsed.toString()}`);
});
});
});
Running Tests
# Run all tests
npx hardhat test
# Run specific test file
npx hardhat test test/MyContract.test.js
# Show gas report
npm install --save-dev hardhat-gas-reporter
npx hardhat test --gas-reporter
# Check code coverage
npm install --save-dev solidity-coverage
npx hardhat coverage
Integration Testing
// Testing interactions between multiple contracts
describe("Token Integration", function () {
let token, marketplace;
beforeEach(async function () {
const Token = await ethers.getContractFactory("SimpleToken");
token = await Token.deploy(1000000);
const Marketplace = await ethers.getContractFactory("Marketplace");
marketplace = await Marketplace.deploy(token.target);
// Approve marketplace to spend tokens
await token.approve(marketplace.target, 1000);
});
it("Should allow marketplace to transfer tokens", async function () {
await marketplace.buyItem(1, 100);
// Verify token transfer occurred
});
});
Fork Testing (Test Against Mainnet State)
// hardhat.config.js
module.exports = {
networks: {
hardhat: {
forking: {
url: `https://eth-mainnet.alchemyapi.io/v2/${ALCHEMY_KEY}`,
blockNumber: 18000000 // Optional: pin to specific block
}
}
}
};
// Test file
describe("Fork test", function () {
it("Should interact with mainnet contracts", async function () {
const uniswapRouter = await ethers.getContractAt(
"IUniswapV2Router",
"0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D"
);
// Test against real Uniswap
});
});
Security Best Practices
1. Input Validation
function transfer(address to, uint256 amount) public {
// Validate inputs
require(to != address(0), "Invalid recipient");
require(amount > 0, "Amount must be positive");
require(balances[msg.sender] >= amount, "Insufficient balance");
// Execute transfer
balances[msg.sender] -= amount;
balances[to] += amount;
}
2. Checks-Effects-Interactions Pattern
function withdraw(uint256 amount) public {
// 1. Checks
require(balances[msg.sender] >= amount, "Insufficient balance");
// 2. Effects (update state)
balances[msg.sender] -= amount;
// 3. Interactions (external calls)
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
3. Safe Math Operations
// Solidity 0.8+ has built-in overflow protection
contract SafeMath {
function add(uint256 a, uint256 b) public pure returns (uint256) {
// Automatically reverts on overflow
return a + b;
}
// For older versions, use SafeMath library
// using SafeMath for uint256;
// uint256 result = a.add(b);
}
4. Access Control with OpenZeppelin
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
// Simple ownership
contract OwnableContract is Ownable {
constructor() Ownable(msg.sender) {}
function restrictedFunction() public onlyOwner {
// Only owner can call
}
}
// Role-based access control
contract RoleBasedContract is AccessControl {
bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
constructor() {
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
_grantRole(ADMIN_ROLE, msg.sender);
}
function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) {
// Only minters can call
}
function adminFunction() public onlyRole(ADMIN_ROLE) {
// Only admins can call
}
}
5. Secure Random Numbers
// โ INSECURE: Predictable randomness
contract InsecureRandom {
function random() public view returns (uint256) {
// Never use this in production!
return uint256(keccak256(abi.encodePacked(block.timestamp, block.prevrandao)));
}
}
// โ
SECURE: Using Chainlink VRF
import "@chainlink/contracts/src/v0.8/VRFConsumerBase.sol";
contract SecureRandom is VRFConsumerBase {
bytes32 internal keyHash;
uint256 internal fee;
uint256 public randomResult;
constructor()
VRFConsumerBase(
0x8103B0A8A00be2DDC778e6e7eaa21791Cd364625, // VRF Coordinator
0x326C977E6efc84E512bB9C30f76E30c160eD06FB // LINK Token
)
{
keyHash = 0x79d3d8832d904592c0bf9818b621522c988bb8b0c05cdc3b15aea1b6e8db0c15;
fee = 0.1 * 10 ** 18; // 0.1 LINK
}
function getRandomNumber() public returns (bytes32 requestId) {
require(LINK.balanceOf(address(this)) >= fee, "Not enough LINK");
return requestRandomness(keyHash, fee);
}
function fulfillRandomness(bytes32 requestId, uint256 randomness) internal override {
randomResult = randomness;
}
}
6. Front-Running Protection
contract FrontRunningProtection {
// Commit-reveal scheme
mapping(address => bytes32) public commitments;
mapping(address => uint256) public revealDeadlines;
// Step 1: Commit to a value (hash of value + secret)
function commit(bytes32 commitment) public {
commitments[msg.sender] = commitment;
revealDeadlines[msg.sender] = block.timestamp + 1 hours;
}
// Step 2: Reveal the value after commit period
function reveal(uint256 value, bytes32 secret) public {
require(block.timestamp <= revealDeadlines[msg.sender], "Deadline passed");
require(
keccak256(abi.encodePacked(value, secret)) == commitments[msg.sender],
"Invalid reveal"
);
// Process the revealed value
delete commitments[msg.sender];
}
}
7. Oracle Security
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
contract PriceConsumer {
AggregatorV3Interface internal priceFeed;
constructor() {
// ETH/USD Price Feed on Ethereum mainnet
priceFeed = AggregatorV3Interface(0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419);
}
function getLatestPrice() public view returns (int) {
(
uint80 roundID,
int price,
uint startedAt,
uint timeStamp,
uint80 answeredInRound
) = priceFeed.latestRoundData();
// Validate oracle data
require(price > 0, "Invalid price");
require(timeStamp > 0, "Round not complete");
require(answeredInRound >= roundID, "Stale price");
return price;
}
}
Common Pitfalls
Pitfall 1: Reentrancy Attacks
Problem: External calls can recursively call back into the contract.
Solution: Use checks-effects-interactions pattern or reentrancy guards.
Pitfall 2: Integer Overflow/Underflow
Problem: Values wrap around at boundaries (pre-0.8).
Solution: Use Solidity 0.8+ or SafeMath library.
Pitfall 3: Delegatecall Vulnerabilities
Problem: Delegatecall executes code in caller’s context.
Solution: Carefully validate delegatecall targets and storage layout.
Pitfall 4: Timestamp Dependence
Problem: Miners can manipulate block timestamps.
Solution: Don’t rely on precise timestamps for critical logic.
Deployment and Verification
Deploying to Testnet
Create a deployment script scripts/deploy.js:
const hre = require("hardhat");
async function main() {
console.log("Deploying SimpleToken...");
// Get contract factory
const SimpleToken = await hre.ethers.getContractFactory("SimpleToken");
// Deploy contract
const token = await SimpleToken.deploy(1000000);
await token.waitForDeployment();
console.log(`SimpleToken deployed to: ${token.target}`);
// Wait for block confirmations
console.log("Waiting for block confirmations...");
await token.deploymentTransaction().wait(5);
// Verify on Etherscan
console.log("Verifying contract on Etherscan...");
await hre.run("verify:verify", {
address: token.target,
constructorArguments: [1000000]
});
console.log("Contract verified!");
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
Environment Variables
Create .env file:
# Network URLs
SEPOLIA_URL=https://eth-sepolia.g.alchemy.com/v2/YOUR_ALCHEMY_KEY
MAINNET_URL=https://eth-mainnet.g.alchemy.com/v2/YOUR_ALCHEMY_KEY
# Private key (NEVER commit this!)
PRIVATE_KEY=your_private_key_here
# Etherscan API key for verification
ETHERSCAN_API_KEY=your_etherscan_api_key
Deploy Commands
# Deploy to local network
npx hardhat run scripts/deploy.js
# Deploy to Sepolia testnet
npx hardhat run scripts/deploy.js --network sepolia
# Deploy to mainnet
npx hardhat run scripts/deploy.js --network mainnet
# Verify contract manually
npx hardhat verify --network sepolia CONTRACT_ADDRESS "constructor_arg"
Estimating Gas Costs
// In your deployment script
const gasPrice = await ethers.provider.getFeeData();
console.log(`Current gas price: ${ethers.formatUnits(gasPrice.gasPrice, "gwei")} gwei`);
// Estimate deployment cost
const deploymentTx = await SimpleToken.getDeployTransaction(1000000);
const estimatedGas = await ethers.provider.estimateGas(deploymentTx);
const cost = estimatedGas * gasPrice.gasPrice;
console.log(`Estimated deployment cost: ${ethers.formatEther(cost)} ETH`);
Contract Interaction After Deployment
// scripts/interact.js
const hre = require("hardhat");
async function main() {
const contractAddress = "0x..."; // Your deployed contract
// Get contract instance
const SimpleToken = await hre.ethers.getContractFactory("SimpleToken");
const token = SimpleToken.attach(contractAddress);
// Read contract state
const totalSupply = await token.totalSupply();
console.log(`Total Supply: ${ethers.formatEther(totalSupply)}`);
// Send transaction
const tx = await token.transfer("0x...", ethers.parseEther("100"));
await tx.wait();
console.log(`Transfer completed: ${tx.hash}`);
}
main().catch(console.error);
Debugging and Troubleshooting
Console Logging in Contracts
import "hardhat/console.sol";
contract DebugContract {
function debugFunction(uint256 value) public {
console.log("Function called with:", value);
console.log("msg.sender:", msg.sender);
console.log("block.timestamp:", block.timestamp);
}
}
Common Errors and Solutions
Error: “Gas estimation failed”
// Solution: Catch the error and see the revert reason
try {
const tx = await contract.someFunction();
} catch (error) {
console.log("Revert reason:", error.message);
}
Error: “Nonce too high”
# Solution: Reset your account in Hardhat
npx hardhat node --reset
Error: “Transaction underpriced”
// Solution: Increase gas price
const tx = await contract.someFunction({
gasPrice: ethers.parseUnits("50", "gwei")
});
Using Hardhat Network
# Start local blockchain
npx hardhat node
# In another terminal, run scripts against local network
npx hardhat run scripts/deploy.js --network localhost
# Use Hardhat console
npx hardhat console --network localhost
Debugging with Remix IDE
- Go to remix.ethereum.org
- Create new file and paste your Solidity code
- Compile the contract
- Use the debugger to step through transactions
- Check variable values at each step
Real-World Example: ERC20 Token
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SimpleToken {
string public name = "Simple Token";
string public symbol = "SIM";
uint8 public decimals = 18;
uint256 public totalSupply;
mapping(address => uint256) public balances;
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 ** uint256(decimals);
balances[msg.sender] = totalSupply;
}
function transfer(address to, uint256 value) public returns (bool) {
require(to != address(0), "Invalid address");
require(balances[msg.sender] >= value, "Insufficient balance");
balances[msg.sender] -= value;
balances[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;
}
function transferFrom(address from, address to, uint256 value) public returns (bool) {
require(from != address(0), "Invalid address");
require(to != address(0), "Invalid address");
require(balances[from] >= value, "Insufficient balance");
require(allowance[from][msg.sender] >= value, "Allowance exceeded");
balances[from] -= value;
balances[to] += value;
allowance[from][msg.sender] -= value;
emit Transfer(from, to, value);
return true;
}
}
Advanced Real-World Example: NFT Marketplace
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract NFTMarketplace is ReentrancyGuard, Ownable {
struct Listing {
address seller;
address nftContract;
uint256 tokenId;
uint256 price;
bool active;
}
// Listing ID => Listing
mapping(uint256 => Listing) public listings;
uint256 public listingCounter;
// Platform fee (2.5%)
uint256 public platformFee = 250; // Basis points (250 = 2.5%)
uint256 public constant FEE_DENOMINATOR = 10000;
event ItemListed(
uint256 indexed listingId,
address indexed seller,
address indexed nftContract,
uint256 tokenId,
uint256 price
);
event ItemSold(
uint256 indexed listingId,
address indexed buyer,
uint256 price
);
event ListingCancelled(uint256 indexed listingId);
constructor() Ownable(msg.sender) {}
function listItem(
address nftContract,
uint256 tokenId,
uint256 price
) external returns (uint256) {
require(price > 0, "Price must be greater than 0");
IERC721 nft = IERC721(nftContract);
require(nft.ownerOf(tokenId) == msg.sender, "Not token owner");
require(
nft.isApprovedForAll(msg.sender, address(this)) ||
nft.getApproved(tokenId) == address(this),
"Marketplace not approved"
);
uint256 listingId = listingCounter++;
listings[listingId] = Listing({
seller: msg.sender,
nftContract: nftContract,
tokenId: tokenId,
price: price,
active: true
});
emit ItemListed(listingId, msg.sender, nftContract, tokenId, price);
return listingId;
}
function buyItem(uint256 listingId) external payable nonReentrant {
Listing storage listing = listings[listingId];
require(listing.active, "Listing not active");
require(msg.value == listing.price, "Incorrect payment amount");
require(msg.sender != listing.seller, "Cannot buy own listing");
// Mark as inactive
listing.active = false;
// Calculate fees
uint256 fee = (listing.price * platformFee) / FEE_DENOMINATOR;
uint256 sellerProceeds = listing.price - fee;
// Transfer NFT to buyer
IERC721(listing.nftContract).safeTransferFrom(
listing.seller,
msg.sender,
listing.tokenId
);
// Transfer funds
(bool successSeller, ) = listing.seller.call{value: sellerProceeds}("");
require(successSeller, "Seller payment failed");
emit ItemSold(listingId, msg.sender, listing.price);
}
function cancelListing(uint256 listingId) external {
Listing storage listing = listings[listingId];
require(listing.active, "Listing not active");
require(listing.seller == msg.sender, "Not listing owner");
listing.active = false;
emit ListingCancelled(listingId);
}
function updatePlatformFee(uint256 newFee) external onlyOwner {
require(newFee <= 1000, "Fee too high"); // Max 10%
platformFee = newFee;
}
function withdrawFees() external onlyOwner {
uint256 balance = address(this).balance;
(bool success, ) = owner().call{value: balance}("");
require(success, "Withdrawal failed");
}
receive() external payable {}
}
Production Considerations
1. Security Audits
Before mainnet deployment:
- Automated Analysis: Use Slither, Mythril, or Echidna
- Professional Audit: Hire firms like OpenZeppelin, Trail of Bits, or ConsenSys Diligence
- Bug Bounty: Launch on Immunefi or Code4rena
- Formal Verification: For critical contracts
# Install Slither
pip install slither-analyzer
# Run analysis
slither contracts/MyContract.sol
# Install Mythril
pip install mythril
# Analyze contract
myth analyze contracts/MyContract.sol
2. Upgradeability Patterns
Transparent Proxy Pattern (OpenZeppelin):
import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol";
// Deploy implementation
MyContract implementation = new MyContract();
// Deploy proxy
TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(
address(implementation),
address(proxyAdmin),
""
);
// Upgrade to new implementation
proxyAdmin.upgrade(proxy, newImplementation);
3. Monitoring and Alerts
// Using ethers.js to monitor events
const contract = new ethers.Contract(address, abi, provider);
// Listen for Transfer events
contract.on("Transfer", (from, to, amount, event) => {
console.log(`Transfer: ${from} -> ${to}: ${amount}`);
// Send alert if large transfer
if (amount > ethers.parseEther("1000")) {
sendAlert(`Large transfer detected: ${amount}`);
}
});
// Monitor for unusual patterns
contract.on("*", (event) => {
// Log all events for analysis
database.logEvent(event);
});
4. Gas Limits and Block Gas Limit
// Be aware of block gas limit (30 million gas on Ethereum)
contract GasConsideration {
// โ Dangerous: unbounded loop
function dangerousLoop(address[] memory users) public {
for (uint i = 0; i < users.length; i++) {
// Could exceed block gas limit
}
}
// โ
Safe: bounded loop or pagination
function safeLoop(address[] memory users, uint256 start, uint256 end) public {
require(end <= users.length, "Invalid range");
require(end - start <= 100, "Batch too large");
for (uint i = start; i < end; i++) {
// Process in batches
}
}
}
5. Multi-Signature Wallets
contract MultiSigWallet {
address[] public owners;
uint256 public required;
mapping(uint256 => Transaction) public transactions;
mapping(uint256 => mapping(address => bool)) public confirmations;
uint256 public transactionCount;
struct Transaction {
address destination;
uint256 value;
bytes data;
bool executed;
}
constructor(address[] memory _owners, uint256 _required) {
require(_owners.length > 0, "Owners required");
require(_required > 0 && _required <= _owners.length, "Invalid required");
owners = _owners;
required = _required;
}
function submitTransaction(address destination, uint256 value, bytes memory data)
public
returns (uint256)
{
uint256 txId = transactionCount++;
transactions[txId] = Transaction({
destination: destination,
value: value,
data: data,
executed: false
});
confirmTransaction(txId);
return txId;
}
function confirmTransaction(uint256 txId) public {
require(isOwner(msg.sender), "Not owner");
confirmations[txId][msg.sender] = true;
if (isConfirmed(txId)) {
executeTransaction(txId);
}
}
function executeTransaction(uint256 txId) internal {
Transaction storage txn = transactions[txId];
require(!txn.executed, "Already executed");
txn.executed = true;
(bool success, ) = txn.destination.call{value: txn.value}(txn.data);
require(success, "Execution failed");
}
function isConfirmed(uint256 txId) public view returns (bool) {
uint256 count = 0;
for (uint256 i = 0; i < owners.length; i++) {
if (confirmations[txId][owners[i]]) {
count++;
}
}
return count >= required;
}
function isOwner(address account) public view returns (bool) {
for (uint256 i = 0; i < owners.length; i++) {
if (owners[i] == account) {
return true;
}
}
return false;
}
receive() external payable {}
}
Pros and Cons vs Alternatives
Solidity vs Vyper
| Aspect | Solidity | Vyper |
|---|---|---|
| Adoption | โ Dominant | โ ๏ธ Limited |
| Syntax | JavaScript-like | Python-like |
| Security | โ ๏ธ Complex | โ Simpler |
| Performance | โ Optimized | โ ๏ธ Slower |
| Ecosystem | โ Largest | โ ๏ธ Small |
Solidity vs Rust (for Solana)
| Aspect | Solidity | Rust |
|---|---|---|
| Blockchain | Ethereum | Solana |
| Learning Curve | โ ๏ธ Medium | โ Steep |
| Performance | โ ๏ธ Slower | โ Faster |
| Security | โ ๏ธ Manual | โ Compiler-enforced |
| Ecosystem | โ Largest | โ ๏ธ Growing |
Resources and Further Learning
Official Documentation
Learning Resources
- CryptoZombies - Interactive Solidity tutorial
- Hardhat Documentation
- Truffle Suite
Security Resources
Alternative Technologies
- Vyper: Python-like smart contract language
- Rust (Solana): High-performance blockchain
- Move (Aptos/Sui): Resource-oriented language
- Cairo (StarkNet): Zero-knowledge proof language
Performance Benchmarks
Gas Costs Comparison (Ethereum Mainnet)
| Operation | Gas Cost | ETH Cost* | USD Cost* |
|---|---|---|---|
| Simple transfer (EOA to EOA) | 21,000 | 0.00042 | $0.84 |
| ERC20 transfer | 65,000 | 0.0013 | $2.60 |
| ERC721 mint | 80,000 | 0.0016 | $3.20 |
| Uniswap swap | 150,000 | 0.003 | $6.00 |
| Deploy ERC20 | 1,200,000 | 0.024 | $48.00 |
| Deploy complex contract | 3,000,000 | 0.06 | $120.00 |
*Assuming 20 gwei gas price and $2,000 ETH
Storage Costs
// Cost to store data on-chain
contract StorageCosts {
// Setting storage from zero to non-zero: 20,000 gas
uint256 public value; // ~$0.80 to initialize
// Each additional 32 bytes: 20,000 gas
uint256[10] public array; // ~$8.00 to fill
// Mapping storage per entry: 20,000 gas
mapping(address => uint256) public balances;
}
Best Practices Checklist
Pre-Deployment
- All functions have proper access control
- Input validation on all public/external functions
- Reentrancy guards on functions with external calls
- Events emitted for all state changes
- Custom errors used instead of require strings (gas savings)
- SafeMath or Solidity 0.8+ for arithmetic
- No hardcoded addresses (use constructor parameters)
- Gas optimizations applied
- Comprehensive test coverage (>90%)
- Integration tests with other contracts
- Tested on fork of mainnet
- Static analysis tools run (Slither, Mythril)
- Manual code review completed
- Documentation written
- Natspec comments added
Deployment
- Deployed to testnet first
- Verified on block explorer
- Interaction testing on testnet
- Professional security audit completed
- Audit findings resolved
- Multi-sig wallet set as owner
- Emergency pause mechanism tested
- Upgrade mechanism tested (if applicable)
- Gas costs calculated and acceptable
- Deployment script tested
Post-Deployment
- Contract verified on Etherscan
- Event monitoring set up
- Alert system configured
- Documentation published
- Bug bounty program launched
- Community notified
- Frontend integration tested
- Backup plans documented
- Incident response plan ready
Common Interview Questions
Q: What’s the difference between transfer(), send(), and call() for sending Ether?
A:
transfer(): 2300 gas stipend, reverts on failure (deprecated)send(): 2300 gas stipend, returns bool (deprecated)call(): Forwards all gas, returns bool (recommended)
// Recommended approach
(bool success, ) = recipient.call{value: amount}("");
require(success, "Transfer failed");
Q: What is the difference between memory and storage?
A:
storage: Persistent state on blockchain (expensive)memory: Temporary during function execution (cheaper)calldata: Read-only function parameters (cheapest)
Q: How do you prevent reentrancy attacks?
A: Use checks-effects-interactions pattern or reentrancy guards:
- Check conditions
- Update state
- Make external calls
Q: What are the main differences between view, pure, and regular functions?
A:
view: Reads state, no modifications, no gas cost when called externallypure: No state read or write, no gas cost when called externally- Regular: Can modify state, costs gas
Q: How does gas optimization work in Solidity?
A: Key strategies:
- Pack storage variables
- Use events instead of storage for history
- Cache storage variables in memory
- Use
calldatafor read-only arrays - Batch operations
- Use custom errors instead of strings
Emerging Trends and Future
Layer 2 Solutions
- Optimistic Rollups: Arbitrum, Optimism (10-100x cheaper)
- ZK-Rollups: zkSync, StarkNet (100-1000x cheaper)
- Sidechains: Polygon, Gnosis Chain (faster, cheaper)
EVM Improvements
- EIP-4844 (Proto-Danksharding): Reduced rollup costs
- Account Abstraction (EIP-4337): Better UX
- Verkle Trees: Reduced state storage
New Languages and Tools
- Fe: Python-inspired, focusing on safety
- Yul: Low-level intermediate language
- Formal Verification: Mathematical proof of correctness
Web3 Integration
// Modern Web3 integration with ethers.js v6
import { ethers } from 'ethers';
// Connect to wallet
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
// Interact with contract
const contract = new ethers.Contract(address, abi, signer);
const tx = await contract.transfer(recipient, amount);
const receipt = await tx.wait();
console.log(`Transaction hash: ${receipt.hash}`);
Conclusion
Solidity remains the dominant language for smart contract development with an unmatched ecosystem. Success in Solidity development requires:
Technical Excellence:
- Deep understanding of EVM mechanics
- Security-first mindset
- Gas optimization expertise
- Comprehensive testing practices
Professional Practices:
- Thorough documentation
- Code audits and reviews
- Continuous monitoring
- Community engagement
Continuous Learning:
- Stay updated with EIPs
- Follow security disclosures
- Study successful projects
- Participate in bug bounties
Recommended Learning Path:
- Week 1-2: Solidity basics, data types, functions
- Week 3-4: Smart contract patterns, security basics
- Week 5-6: Testing with Hardhat, deployment
- Week 7-8: Advanced patterns, gas optimization
- Week 9-10: Real project development
- Week 11-12: Security deep dive, auditing
Career Opportunities:
- Smart Contract Developer: $80k-$200k+
- Security Auditor: $120k-$300k+
- Protocol Engineer: $150k-$400k+
- DeFi Developer: $100k-$250k+
Essential Resources to Bookmark:
The blockchain industry continues to evolve rapidly. Smart contract development in 2025 and beyond will focus on:
- Scalability: Layer 2 solutions becoming standard
- Security: Formal verification gaining adoption
- UX: Account abstraction simplifying interactions
- Interoperability: Cross-chain communication protocols
- Sustainability: More efficient consensus mechanisms
Start building todayโthe future of decentralized applications depends on skilled Solidity developers who prioritize security, efficiency, and user experience.
Comments