Getting started
Introduction
Solidity is the primary language for Ethereum smart contracts, compiling to EVM bytecode.
Basic Contract
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract HelloWorld {
string public message = "Hello, World!";
function setMessage(string memory _msg) public {
message = _msg;
}
function getMessage() public view returns (string memory) {
return message;
}
}
Compilation
# Install Solidity compiler
npm install -g solc
# Compile contract
solc --bin --abi HelloWorld.sol
# Using Hardhat
npx hardhat compile
# Using Foundry
forge build
Contract Structure
Basic Structure
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/access/Ownable.sol";
contract MyContract is Ownable {
// State variables
uint256 public counter;
// Events
event CounterIncremented(uint256 newValue);
// Modifiers
modifier onlyEven(uint256 _value) {
require(_value % 2 == 0, "Must be even");
_;
}
// Constructor
constructor(uint256 _initial) Ownable(msg.sender) {
counter = _initial;
}
// Functions
function increment() public onlyOwner {
counter++;
emit CounterIncremented(counter);
}
}
Contract Elements
| Element | Description |
|---|---|
pragma |
Compiler version |
import |
External contracts |
contract |
Contract declaration |
state variables |
Persistent storage |
events |
Log emissions |
modifiers |
Function conditions |
constructor |
Initialization |
functions |
Contract logic |
Inheritance
contract Base {
uint256 public value;
function setValue(uint256 _value) public virtual {
value = _value;
}
}
contract Derived is Base {
// Override parent function
function setValue(uint256 _value) public override {
value = _value * 2;
}
}
// Multiple inheritance
contract Multi is Base, Ownable {
// Most derived to base order
}
Data Types
Value Types
// Boolean
bool public isActive = true;
// Integers
uint256 public unsignedInt = 42;
int256 public signedInt = -42;
uint8 public smallUint = 255;
// Address
address public owner;
address payable public recipient;
// Bytes
bytes1 public singleByte = 0xFF;
bytes32 public hash;
// Enums
enum State { Pending, Active, Closed }
State public currentState;
Reference Types
// Arrays
uint256[] public dynamicArray;
uint256[5] public fixedArray;
// Mappings
mapping(address => uint256) public balances;
mapping(address => mapping(address => uint256)) public allowances;
// Structs
struct User {
string name;
uint256 age;
bool isActive;
}
User public user;
// Strings
string public name = "Alice";
Data Locations
contract DataLocations {
uint256[] public storageArray; // state variable (storage)
function example() public {
// Memory (temporary)
uint256[] memory memArray = new uint256[](5);
// Calldata (read-only, external)
// Used in external function parameters
}
function processData(uint256[] calldata data) external pure {
// data is in calldata (gas efficient)
}
}
Functions
Function Visibility
contract Visibility {
// Public: callable internally and externally
function publicFunc() public {}
// External: only callable externally
function externalFunc() external {}
// Internal: only callable internally and by derived
function internalFunc() internal {}
// Private: only callable internally
function privateFunc() private {}
}
State Mutability
contract Mutability {
uint256 public value = 100;
// View: reads state, doesn't modify
function getValue() public view returns (uint256) {
return value;
}
// Pure: doesn't read or modify state
function add(uint256 a, uint256 b) public pure returns (uint256) {
return a + b;
}
// Payable: can receive Ether
function deposit() public payable {
// msg.value available
}
// Default: can modify state
function setValue(uint256 _value) public {
value = _value;
}
}
Function Modifiers
contract Modifiers {
address public owner;
bool public locked;
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
modifier nonReentrant() {
require(!locked, "Reentrant call");
locked = true;
_;
locked = false;
}
function restrictedAction() public onlyOwner nonReentrant {
// Function body
}
}
Events and Errors
Events
contract Events {
// Define events
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
// Emit events
function transfer(address to, uint256 amount) public {
// ... transfer logic
emit Transfer(msg.sender, to, amount);
}
}
Custom Errors (Gas Efficient)
// Define custom errors (Solidity ^0.8.4)
error Unauthorized(address caller);
error InsufficientBalance(uint256 available, uint256 required);
contract Errors {
address public owner;
function restrictedAction() public {
if (msg.sender != owner) {
revert Unauthorized(msg.sender);
}
}
}
Error Handling
contract ErrorHandling {
// require: validate conditions
function withdraw(uint256 amount) public {
require(amount > 0, "Amount must be positive");
require(balance[msg.sender] >= amount, "Insufficient balance");
// ...
}
// assert: check invariants
function transfer(uint256 amount) public {
uint256 oldBalance = balance[msg.sender];
balance[msg.sender] -= amount;
assert(balance[msg.sender] < oldBalance);
}
// revert: explicit revert
function process(uint256 value) public {
if (value > 100) {
revert("Value too high");
}
}
}
Security Patterns
Reentrancy Protection
contract ReentrancyGuard {
bool private locked;
modifier nonReentrant() {
require(!locked, "Reentrant call");
locked = true;
_;
locked = false;
}
function withdraw(uint256 amount) public nonReentrant {
// Safe withdrawal
require(balances[msg.sender] >= amount);
balances[msg.sender] -= amount;
(bool success, ) = msg.sender.call{value: amount}("");
require(success);
}
}
Checks-Effects-Interactions
contract CEI {
mapping(address => uint256) public balances;
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");
}
}
Access Control
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
contract Secured 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 adminFunction() public onlyRole(ADMIN_ROLE) {
// Admin only
}
function mint() public onlyRole(MINTER_ROLE) {
// Minter only
}
}
Common Vulnerabilities
Integer Overflow (Pre-0.8.0)
// VULNERABLE (Solidity < 0.8.0)
contract Vulnerable {
uint8 public counter = 255;
function increment() public {
counter++; // Wraps to 0
}
}
// SAFE (Solidity >= 0.8.0 or SafeMath)
import "@openzeppelin/contracts/utils/math/SafeMath.sol";
contract Safe {
using SafeMath for uint256;
uint256 public counter;
function increment() public {
counter = counter.add(1); // Reverts on overflow
}
}
Reentrancy Attack
// VULNERABLE
contract Vulnerable {
mapping(address => uint256) public balances;
function withdraw() public {
uint256 amount = balances[msg.sender];
// External call BEFORE state update
(bool success, ) = msg.sender.call{value: amount}("");
require(success);
balances[msg.sender] = 0; // TOO LATE!
}
}
// SAFE
contract Safe {
mapping(address => uint256) public balances;
bool private locked;
function withdraw() public {
require(!locked);
locked = true;
uint256 amount = balances[msg.sender];
balances[msg.sender] = 0; // Update BEFORE external call
(bool success, ) = msg.sender.call{value: amount}("");
require(success);
locked = false;
}
}
tx.origin Authentication
// VULNERABLE
contract Vulnerable {
address public owner;
function transferOwnership(address newOwner) public {
require(tx.origin == owner); // WRONG!
owner = newOwner;
}
}
// SAFE
contract Safe {
address public owner;
function transferOwnership(address newOwner) public {
require(msg.sender == owner); // CORRECT
owner = newOwner;
}
}
Gas Optimization
Storage vs Memory
contract GasOptimization {
struct User {
string name;
uint256 age;
}
User[] public users;
// EXPENSIVE: Multiple SLOAD operations
function badExample(uint256 index) public view returns (string memory) {
return users[index].name; // reads entire struct from storage
}
// OPTIMIZED: Single SLOAD, work in memory
function goodExample(uint256 index) public view returns (string memory) {
User memory user = users[index]; // load once
return user.name;
}
}
Variable Packing
// UNOPTIMIZED: 3 storage slots
contract Unoptimized {
uint8 a; // slot 0
uint256 b; // slot 1
uint8 c; // slot 2
}
// OPTIMIZED: 2 storage slots
contract Optimized {
uint8 a; // slot 0 (shared)
uint8 c; // slot 0 (shared)
uint256 b; // slot 1
}
Common Optimizations
contract Optimizations {
// Use calldata instead of memory for external functions
function process(uint256[] calldata data) external {
// More gas efficient
}
// Cache array length
function sumArray(uint256[] memory arr) public pure returns (uint256) {
uint256 total = 0;
uint256 length = arr.length; // cache length
for (uint256 i = 0; i < length; i++) {
total += arr[i];
}
return total;
}
// Use ++i instead of i++
function loop() public {
for (uint256 i = 0; i < 100; ++i) {
// ++i is cheaper
}
}
// Use unchecked for safe operations
function safeIncrement(uint256 i) public pure returns (uint256) {
unchecked {
return i + 1; // saves gas if overflow impossible
}
}
}
Advanced Patterns
Factory Pattern
contract Token {
string public name;
constructor(string memory _name) {
name = _name;
}
}
contract TokenFactory {
Token[] public tokens;
event TokenCreated(address tokenAddress, string name);
function createToken(string memory name) public returns (address) {
Token token = new Token(name);
tokens.push(token);
emit TokenCreated(address(token), name);
return address(token);
}
}
Proxy Pattern (Upgradeable)
// Using OpenZeppelin UUPS
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
contract MyContract is UUPSUpgradeable, OwnableUpgradeable {
uint256 public value;
function initialize(uint256 _value) public initializer {
__Ownable_init(msg.sender);
__UUPSUpgradeable_init();
value = _value;
}
function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
}
Pull over Push Payments
contract PullPayment {
mapping(address => uint256) public pendingWithdrawals;
// GOOD: Pull pattern
function asyncPay(address recipient, uint256 amount) internal {
pendingWithdrawals[recipient] += amount;
}
function withdraw() public {
uint256 amount = pendingWithdrawals[msg.sender];
require(amount > 0);
pendingWithdrawals[msg.sender] = 0;
(bool success, ) = msg.sender.call{value: amount}("");
require(success);
}
}
ERC Standards
ERC-20 Token
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MyToken is ERC20 {
constructor(uint256 initialSupply) ERC20("MyToken", "MTK") {
_mint(msg.sender, initialSupply);
}
function decimals() public pure override returns (uint8) {
return 18;
}
}
ERC-721 NFT
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract MyNFT is ERC721, Ownable {
uint256 private _tokenIds;
constructor() ERC721("MyNFT", "MNFT") Ownable(msg.sender) {}
function mint(address to) public onlyOwner returns (uint256) {
_tokenIds++;
_safeMint(to, _tokenIds);
return _tokenIds;
}
}
ERC-1155 Multi-Token
import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
contract MyMultiToken is ERC1155 {
uint256 public constant GOLD = 0;
uint256 public constant SILVER = 1;
constructor() ERC1155("https://api.example.com/metadata/{id}.json") {
_mint(msg.sender, GOLD, 1000, "");
_mint(msg.sender, SILVER, 5000, "");
}
}
Testing and Deployment
Hardhat Test
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("MyContract", function () {
let contract;
let owner;
let addr1;
beforeEach(async function () {
[owner, addr1] = await ethers.getSigners();
const Contract = await ethers.getContractFactory("MyContract");
contract = await Contract.deploy();
});
it("Should set the right owner", async function () {
expect(await contract.owner()).to.equal(owner.address);
});
it("Should handle transactions", async function () {
await contract.setValue(42);
expect(await contract.value()).to.equal(42);
});
});
Foundry Test
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "../src/MyContract.sol";
contract MyContractTest is Test {
MyContract public myContract;
address public owner;
function setUp() public {
owner = address(this);
myContract = new MyContract();
}
function testSetValue() public {
myContract.setValue(42);
assertEq(myContract.value(), 42);
}
function testFuzzValue(uint256 x) public {
myContract.setValue(x);
assertEq(myContract.value(), x);
}
}
Deployment Script (Hardhat)
const hre = require("hardhat");
async function main() {
const Contract = await hre.ethers.getContractFactory("MyContract");
const contract = await Contract.deploy(100); // constructor args
await contract.deployed();
console.log("Contract deployed to:", contract.address);
// Verify on Etherscan
await hre.run("verify:verify", {
address: contract.address,
constructorArguments: [100],
});
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
Common Pitfalls
Unprotected Functions
// VULNERABLE
contract Vulnerable {
address public owner;
function changeOwner(address newOwner) public {
owner = newOwner; // Anyone can call!
}
}
// SAFE
contract Safe {
address public owner;
modifier onlyOwner() {
require(msg.sender == owner);
_;
}
function changeOwner(address newOwner) public onlyOwner {
owner = newOwner;
}
}
Delegatecall Danger
// DANGEROUS: delegatecall executes in caller's context
contract Proxy {
address public implementation;
function upgrade(address newImpl) public {
// Ensure proper access control!
implementation = newImpl;
}
fallback() external payable {
address impl = implementation;
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch result
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}
}
Timestamp Dependence
// WEAK: Miners can manipulate timestamp slightly
contract Vulnerable {
function isLotteryTime() public view returns (bool) {
return block.timestamp % 10 == 0; // Manipulable
}
}
// BETTER: Use block.number or oracle
contract Better {
function isLotteryBlock() public view returns (bool) {
return block.number % 100 == 0;
}
}
Useful Global Variables
Block and Transaction
| Variable | Description |
|---|---|
block.number |
Current block number |
block.timestamp |
Current block timestamp |
block.difficulty |
Current block difficulty |
block.gaslimit |
Current block gas limit |
block.coinbase |
Current block miner address |
msg.sender |
Sender of the message |
msg.value |
Ether sent with call (wei) |
msg.data |
Complete calldata |
msg.sig |
Function signature (first 4 bytes) |
tx.origin |
Original sender (avoid!) |
tx.gasprice |
Gas price of transaction |
Address Members
address payable recipient = payable(msg.sender);
// Balance
uint256 balance = recipient.balance;
// Transfer (2300 gas, throws on failure)
recipient.transfer(1 ether);
// Send (2300 gas, returns bool)
bool success = recipient.send(1 ether);
// Call (forwards all gas, returns bool)
(bool success, bytes memory data) = recipient.call{value: 1 ether}("");
// Code checks
uint256 size;
assembly {
size := extcodesize(recipient)
}
bool isContract = size > 0;
Cryptographic Functions
// Hash functions
bytes32 hash1 = keccak256(abi.encodePacked("hello"));
bytes32 hash2 = sha256(abi.encodePacked("world"));
// Signature recovery
bytes32 messageHash = keccak256(abi.encodePacked(message));
bytes32 ethSignedHash = keccak256(
abi.encodePacked("\x19Ethereum Signed Message:\n32", messageHash)
);
address signer = ecrecover(ethSignedHash, v, r, s);
Best Practices
Code Quality
- Use latest Solidity version for security fixes
- Follow checks-effects-interactions pattern
- Implement reentrancy guards for external calls
- Use OpenZeppelin contracts for standards
- Write comprehensive tests (unit, integration, fuzzing)
- Get professional audits before mainnet
- Use events for off-chain indexing
- Document with NatSpec comments
Security Checklist
// 1. Access control on sensitive functions
modifier onlyOwner() {
require(msg.sender == owner);
_;
}
// 2. Reentrancy protection
modifier nonReentrant() {
require(!locked);
locked = true;
_;
locked = false;
}
// 3. Input validation
function transfer(address to, uint256 amount) public {
require(to != address(0), "Invalid address");
require(amount > 0, "Invalid amount");
require(balances[msg.sender] >= amount, "Insufficient balance");
// ...
}
// 4. Integer overflow protection (Solidity >= 0.8.0 automatic)
// 5. Use pull over push for payments
// 6. Avoid tx.origin for authentication
// 7. Proper event emission
// 8. Emergency stop mechanism
NatSpec Documentation
/// @title A simple token contract
/// @author Your Name
/// @notice This contract implements a basic token
/// @dev All functions are tested
contract MyToken {
/// @notice Transfer tokens to another address
/// @dev Implements checks-effects-interactions pattern
/// @param to The recipient address
/// @param amount The amount to transfer
/// @return success Whether the transfer succeeded
function transfer(address to, uint256 amount) public returns (bool success) {
// ...
}
}
Also see
- Solidity Documentation - Official Solidity docs
- OpenZeppelin Contracts - Secure smart contract library
- Ethereum Development Documentation - Ethereum developer resources
- Consensys Smart Contract Best Practices - Security guidelines
- Hardhat - Ethereum development environment
- Foundry - Blazing fast Ethereum toolkit
- Etherscan - Ethereum blockchain explorer
- Remix IDE - Online Solidity IDE
- SWC Registry - Smart contract weakness classification
- Slither - Solidity static analysis tool