Solidity A Comprehensive Guide
Solidity Language Features: A Comprehensive Guide
Introduction
Solidity is a contract-oriented programming language designed for writing smart contracts on the Ethereum blockchain. It is statically typed, supports inheritance, and provides a rich set of features for secure and efficient blockchain development.
This guide covers the core features of Solidity, helping you understand how to build robust smart contracts.
1. Pragma Directive
The pragma
keyword specifies the Solidity compiler version to use.
pragma solidity ^0.8.0;
This ensures that your contract compiles with Solidity 0.8.0 or any newer minor version. Using the caret symbol (^) allows compatibility with future patch versions but not with breaking changes.
2. Data Types
Solidity supports several data types:
โ Boolean
- Type:
bool
- Size: 1 byte
- Range:
true
orfalse
- Usage: Represents simple logical values.
๐ข Unsigned Integers
Unsigned integers (uint
) can only store non-negative numbers. You can choose their size in 8-bit increments.
uint8
โ 1 byte โ 0 to 2โธโ1 (0 to 255)uint16
โ 2 bytes โ 0 to 65,535uint32
โ 4 bytes โ 0 to 4,294,967,295uint64
โ 8 bytes โ up to ~1.84 ร 10ยนโนuint128
โ 16 bytes โ up to 3.4 ร 10ยณโธuint256
/uint
โ 32 bytes โ up to 1.15 ร 10โทโท
โ Use these for balances, counters, and timestamps.
โ Signed Integers
Signed integers (int
) allow both positive and negative values.
int8
โ 1 byte โ โ128 to 127int16
โ 2 bytes โ โ32,768 to 32,767int32
โ 4 bytes โ โ2,147,483,648 to 2,147,483,647int64
โ 8 bytes โ โ2โถยณ to 2โถยณโ1int128
โ 16 bytes โ โ2ยนยฒโท to 2ยนยฒโทโ1int256
/int
โ 32 bytes โ โ2ยฒโตโต to 2ยฒโตโตโ1
โ Use these when working with signed values like temperature, profit/loss, etc.
๐ฎ Address Types
address
- Size: 20 bytes (160 bits)
- Purpose: Stores Ethereum addresses (EOA or contract)
- Ether Transfers: โ Cannot receive Ether directly
address payable
- Size: 20 bytes (160 bits)
- Purpose: Same as
address
, but can receive Ether - Ether Transfers: โ
Use
.transfer()
or.send()
๐งต Byte Arrays
Fixed-Size (bytes1
to bytes32
)
- Size: 1 to 32 bytes
- Purpose: Binary data of known size
- Example:
bytes32
is commonly used forkeccak256
hashes
Dynamic-Size (bytes
)
- Size: Dynamic
- Purpose: Flexible byte data
- Example: File contents, encoded data
โ๏ธ String
- Type:
string
- Size: Dynamic
- Encoding: UTF-8
- Use: Names, messages, text data
- โ ๏ธ Note: Expensive for manipulation and comparison
๐ฆ Arrays
- Fixed-size array:
uint[3]
- Dynamic-size array:
uint[]
โ Use arrays to store ordered collections of elements.
๐งญ Mapping
- Type:
mapping(KeyType => ValueType)
- Size: Dynamic
- Use: Efficient key-value lookup
- Note: Not iterable, no length or enumeration
Example:
mapping(address => uint) public balances;
### Integer Types
- **uint** (Unsigned integer): Positive whole numbers only
- **int** (Signed integer): Can be positive or negative
- **uint8 to uint256** / **int8 to int256** (Specifies bit size in 8-bit increments)
- Default sizes are uint256 and int256 if not specified
Example:
```solidity
uint256 public myNumber = 100;
int8 public smallSigned = -10; // Range from -128 to 127
Boolean
Simple true/false values:
bool public isActive = true;
bool public isCompleted = false;
Address
Used to store Ethereum addresses (20 bytes):
address public owner;
address payable public beneficiary; // Can receive Ether
String & Bytes
string public name = "Solidity";
bytes32 public hash = keccak256("Hello");
bytes public dynamicBytes = "Dynamic content";
Fixed-size Arrays
Arrays with predetermined length:
uint[5] public staticArray = [1, 2, 3, 4, 5];
Dynamic Arrays
Arrays that can change in size:
uint[] public dynamicArray;
3. Variables
State Variables
Stored permanently on the blockchain.
uint256 public count;
string public projectName;
Local Variables
Exist only during function execution.
function getValue() public pure returns (uint) {
uint tempValue = 10;
uint result = tempValue * 2;
return result;
}
Global Variables
Provide blockchain-related information.
uint public currentBlock = block.number;
address public sender = msg.sender;
uint public timestamp = block.timestamp;
uint public chainId = block.chainid;
Constants
Values that cannot be changed after contract deployment:
uint256 public constant MAX_SUPPLY = 1000000;
address public constant DEAD_ADDRESS = 0x000000000000000000000000000000000000dEaD;
4. Functions
Functions allow interaction with the contract.
Basic Function
function setNumber(uint _number) public {
count = _number;
}
Return Values
function getSum(uint a, uint b) public pure returns (uint) {
return a + b;
}
// Multiple return values
function getMinMax(uint a, uint b) public pure returns (uint min, uint max) {
if (a > b) {
return (b, a);
} else {
return (a, b);
}
}
Function Visibility
public
โ Accessible externally and internallyprivate
โ Accessible only within the contractinternal
โ Accessible within contract and derived contractsexternal
โ Accessible only externally (more gas efficient for large data)
Function Modifiers
pure
โ Does not read or modify stateview
โ Reads but doesn't modify statepayable
โ Can receive Ether
// External payable function
function deposit() external payable {
// Function accepts ETH
}
// View function
function getBalance() public view returns (uint) {
return address(this).balance;
}
Function Overloading
Multiple functions with the same name but different parameters:
function getValue(uint id) public view returns (uint) {
return values[id];
}
function getValue(string memory name) public view returns (uint) {
return nameValues[name];
}
5. Modifiers
Used to change function behavior.
modifier onlyOwner() {
require(msg.sender == owner, "Not the owner");
_; // Continues execution at the function
}
// Using the modifier
function withdrawFunds() public onlyOwner {
payable(owner).transfer(address(this).balance);
}
Modifiers with Parameters
modifier minimum(uint amount) {
require(msg.value >= amount, "Not enough Ether provided");
_;
}
function purchase() public payable minimum(0.01 ether) {
// Execute purchase logic
}
6. Events
Used to log information on the blockchain that can be efficiently accessed by off-chain applications.
event Transfer(address indexed from, address indexed to, uint256 value);
function transfer(address to, uint256 amount) public {
balances[msg.sender] -= amount;
balances[to] += amount;
emit Transfer(msg.sender, to, amount); // Logging the transfer
}
Indexed Parameters
Up to three parameters can be marked as indexed
, making them more efficiently searchable:
event UserAction(address indexed user, string action, uint256 indexed timestamp);
7. Structs and Enums
Structs
Custom data structures:
struct User {
string name;
uint age;
address wallet;
bool isActive;
}
User public user = User("Alice", 25, msg.sender, true);
// Struct arrays and mappings
User[] public users;
mapping(address => User) public usersByAddress;
function addUser(string memory _name, uint _age) public {
User memory newUser = User(_name, _age, msg.sender, true);
users.push(newUser);
usersByAddress[msg.sender] = newUser;
}
Enums
Custom type with a finite set of values:
enum Status { Pending, Approved, Rejected, Canceled }
Status public orderStatus = Status.Pending;
function approve() public onlyOwner {
orderStatus = Status.Approved;
}
function reject() public onlyOwner {
orderStatus = Status.Rejected;
}
8. Mappings
Key-value storage for efficient data retrieval.
mapping(address => uint) public balances;
mapping(address => mapping(address => uint)) public allowances; // Nested mapping
Example usage:
function transfer(address to, uint amount) public {
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
balances[to] += amount;
}
function approve(address spender, uint amount) public {
allowances[msg.sender][spender] = amount;
}
9. Inheritance
Allows contracts to inherit properties from other contracts.
contract Ownable {
address public owner;
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "Not the owner");
_;
}
function transferOwnership(address newOwner) public onlyOwner {
require(newOwner != address(0), "Invalid address");
owner = newOwner;
}
}
contract MyToken is Ownable {
string public name;
mapping(address => uint) public balances;
constructor(string memory _name) {
name = _name;
}
// Uses the onlyOwner modifier from parent contract
function mint(address to, uint amount) public onlyOwner {
balances[to] += amount;
}
}
Multiple Inheritance
contract A {
function foo() public pure virtual returns (string memory) {
return "A";
}
}
contract B {
function foo() public pure virtual returns (string memory) {
return "B";
}
}
// C inherits from both A and B
contract C is A, B {
// Must override function that exists in multiple parent contracts
function foo() public pure override(A, B) returns (string memory) {
return "C";
}
}
Abstract Contracts
Contracts that contain at least one function without implementation:
abstract contract BaseContract {
function calculateValue() public virtual returns (uint);
}
contract ImplementedContract is BaseContract {
function calculateValue() public pure override returns (uint) {
return 100;
}
}
Interfaces
Similar to abstract contracts but with more restrictions:
interface IERC20 {
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address to, uint256 amount) external returns (bool);
}
10. Error Handling
Using require
, assert
, and revert
for different error scenarios:
require()
Used for input validation and checking conditions:
function withdraw(uint amount) public {
require(amount > 0, "Amount must be greater than 0");
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
payable(msg.sender).transfer(amount);
}
assert()
Used for internal error checking and invariant validation:
function safeOperation() public {
uint startBalance = address(this).balance;
// Perform operations
// Ensures no funds were lost
assert(address(this).balance >= startBalance);
}
revert()
Used to revert a transaction with an error message:
function complexCheck(uint x) public {
if (x < 10) {
revert("Value too small");
} else if (x > 100) {
revert("Value too large");
}
// Continue with function
}
Custom Errors (Solidity 0.8.4+)
More gas-efficient than string error messages:
error InsufficientBalance(address user, uint256 available, uint256 required);
function transfer(address to, uint256 amount) public {
if (balances[msg.sender] < amount) {
revert InsufficientBalance(msg.sender, balances[msg.sender], amount);
}
balances[msg.sender] -= amount;
balances[to] += amount;
}
11. Contract Creation and Interaction
Contract Constructor
Executes once when the contract is deployed:
contract Token {
string public name;
address public owner;
constructor(string memory _name) {
name = _name;
owner = msg.sender;
}
}
Creating Contracts from Contracts
contract Factory {
Token[] public tokens;
function createToken(string memory name) public {
Token newToken = new Token(name);
tokens.push(newToken);
}
}
Contract Interaction
interface ICounter {
function increment() external;
function getCount() external view returns (uint);
}
contract Caller {
function incrementCounter(address counterAddress) public {
ICounter counter = ICounter(counterAddress);
counter.increment();
}
function getCountFromCounter(address counterAddress) public view returns (uint) {
return ICounter(counterAddress).getCount();
}
}
12. Gas Optimization Techniques
Storage vs Memory
function processUsers(User[] storage users) internal {
// References storage directly - gas intensive but persistent
}
function processUsersInMemory(User[] memory users) public pure {
// Works with memory copy - more gas efficient for read-only operations
}
Packing Variables
// Inefficient - uses multiple storage slots
uint8 public a;
uint256 public b;
uint8 public c;
// Efficient - packs variables into a single storage slot
uint8 public x;
uint8 public y;
uint8 public z;
Using unchecked
for Gas Savings (Solidity 0.8.0+)
function increment() public {
// With overflow checks (default in 0.8.0+)
count += 1;
// Without overflow checks (more gas efficient)
unchecked {
count += 1;
}
}
Solidity Language Features: A Comprehensive Guide
[...previous content continues above...]
13. Libraries
Reusable code that can be deployed once and used by multiple contracts:
library SafeMath {
function add(uint a, uint b) internal pure returns (uint) {
uint c = a + b;
require(c >= a, "Addition overflow");
return c;
}
function sub(uint a, uint b) internal pure returns (uint) {
require(b <= a, "Subtraction overflow");
return a - b;
}
function mul(uint a, uint b) internal pure returns (uint) {
if (a == 0) return 0;
uint c = a * b;
require(c / a == b, "Multiplication overflow");
return c;
}
}
contract UsingLibrary {
using SafeMath for uint;
function doAdd(uint x, uint y) public pure returns (uint) {
return x.add(y); // Uses the library function
}
function doSub(uint x, uint y) public pure returns (uint) {
return x.sub(y);
}
}
Types of Library Functions
library ArrayUtils {
// Internal functions can access storage
function removeByValue(uint[] storage array, uint value) internal returns (bool) {
for (uint i = 0; i < array.length; i++) {
if (array[i] == value) {
array[i] = array[array.length - 1];
array.pop();
return true;
}
}
return false;
}
// Pure functions don't access storage
function sum(uint[] memory array) internal pure returns (uint) {
uint total = 0;
for (uint i = 0; i < array.length; i++) {
total += array[i];
}
return total;
}
}
Library Deployment
Libraries can be deployed independently and linked to contracts:
// During compilation/deployment, the address of the deployed library
// will be filled in place of the placeholder
using AdvancedMath for uint;
function complexCalculation(uint x) public pure returns (uint) {
// Calls the library function
return x.calculateComplex();
}
14. Security Best Practices
Reentrancy Protection
contract Secure {
mapping(address => uint) public balances;
bool private locked;
modifier nonReentrant() {
require(!locked, "Reentrant call");
locked = true;
_;
locked = false;
}
function withdraw() public nonReentrant {
uint amount = balances[msg.sender];
require(amount > 0, "No balance");
balances[msg.sender] = 0; // Update state before external call
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
}
Check-Effects-Interactions Pattern
function transfer(address payable recipient, uint amount) public {
// Checks
require(balances[msg.sender] >= amount, "Insufficient balance");
// Effects
balances[msg.sender] -= amount;
balances[recipient] += amount;
// Interactions (external calls)
emit Transfer(msg.sender, recipient, amount);
}
Integer Overflow/Underflow Protection
Automatic in Solidity 0.8.0+, but can be implemented manually in earlier versions:
// For Solidity < 0.8.0
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
require(c >= a, "SafeMath: addition overflow");
return c;
}
Access Control Patterns
contract AccessControl {
address public admin;
mapping(address => bool) public operators;
constructor() {
admin = msg.sender;
operators[msg.sender] = true;
}
modifier onlyAdmin() {
require(msg.sender == admin, "Not admin");
_;
}
modifier onlyOperator() {
require(operators[msg.sender], "Not operator");
_;
}
function addOperator(address operator) public onlyAdmin {
operators[operator] = true;
}
function removeOperator(address operator) public onlyAdmin {
operators[operator] = false;
}
}
Avoiding TX.ORIGIN
// Vulnerable to phishing
function badWithdraw() public {
require(tx.origin == owner, "Not owner"); // VULNERABLE
// ...
}
// Secure approach
function goodWithdraw() public {
require(msg.sender == owner, "Not owner"); // SECURE
// ...
}
15. Advanced Features
Fallback and Receive Functions
// Called when no function matches the call data
fallback() external payable {
emit FallbackCalled(msg.sender, msg.value);
}
// Called when Ether is sent to the contract with empty calldata
receive() external payable {
emit EtherReceived(msg.sender, msg.value);
}
Assembly
Low-level inline assembly:
function getUpper(uint x) public pure returns (uint) {
uint result;
assembly {
// Get upper 128 bits
result := shr(128, x)
}
return result;
}
function callWithCustomGas() public returns (bool success) {
address target = 0x123...;
bytes memory data = abi.encodeWithSignature("someFunction()");
assembly {
success := call(
21000, // Gas limit
target, // Target address
0, // No ETH sent
add(data, 0x20), // Input data offset
mload(data), // Input data size
0, // Output offset (not used)
0 // Output size (not used)
)
}
}
Using Delegatecall
function execute(address _target, bytes memory _data) public returns (bytes memory) {
(bool success, bytes memory result) = _target.delegatecall(_data);
require(success, "Delegatecall failed");
return result;
}
Time-Based Logic
contract TimeLock {
uint public lockTime = 1 weeks;
mapping(address => uint) public lockUntil;
function lock() public payable {
require(msg.value > 0, "Must lock some ETH");
lockUntil[msg.sender] = block.timestamp + lockTime;
}
function withdraw() public {
require(block.timestamp >= lockUntil[msg.sender], "Still locked");
require(lockUntil[msg.sender] > 0, "No funds locked");
uint amount = address(this).balance;
payable(msg.sender).transfer(amount);
lockUntil[msg.sender] = 0;
}
}
Create2 for Predictable Addresses
function deployWithCreate2(bytes32 salt, bytes memory bytecode) public returns (address) {
address addr;
assembly {
addr := create2(
0, // No ETH sent
add(bytecode, 0x20), // Bytecode offset
mload(bytecode), // Bytecode length
salt // Salt for address generation
)
}
require(addr != address(0), "Deployment failed");
return addr;
}
16. Contract Patterns
Proxy Pattern
contract Proxy {
address implementation;
address owner;
constructor(address _implementation) {
implementation = _implementation;
owner = msg.sender;
}
function upgrade(address _newImplementation) public {
require(msg.sender == owner, "Not authorized");
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)
let size := returndatasize()
returndatacopy(ptr, 0, size)
switch result
case 0 { revert(ptr, size) }
default { return(ptr, size) }
}
}
receive() external payable {}
}
Factory Pattern
contract Token {
string public name;
address public owner;
constructor(string memory _name, address _owner) {
name = _name;
owner = _owner;
}
}
contract TokenFactory {
mapping(address => address[]) public createdTokens;
event TokenCreated(address indexed creator, address tokenAddress, string name);
function createToken(string memory name) public returns (address) {
Token newToken = new Token(name, msg.sender);
address tokenAddress = address(newToken);
createdTokens[msg.sender].push(tokenAddress);
emit TokenCreated(msg.sender, tokenAddress, name);
return tokenAddress;
}
function getTokenCount(address creator) public view returns (uint) {
return createdTokens[creator].length;
}
}
Withdrawal Pattern
contract Auction {
address public highestBidder;
uint public highestBid;
mapping(address => uint) public pendingReturns;
function bid() public payable {
require(msg.value > highestBid, "Bid too low");
if (highestBidder != address(0)) {
// Add to pending returns instead of immediately sending
pendingReturns[highestBidder] += highestBid;
}
highestBidder = msg.sender;
highestBid = msg.value;
}
// Users call this to withdraw their funds
function withdraw() public {
uint amount = pendingReturns[msg.sender];
if (amount > 0) {
pendingReturns[msg.sender] = 0;
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Withdrawal failed");
}
}
}
17. Ethereum Token Standards
ERC-20 Token
contract ERC20Token {
string public name;
string public symbol;
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(string memory _name, string memory _symbol, uint256 _initialSupply) {
name = _name;
symbol = _symbol;
totalSupply = _initialSupply * 10**uint256(decimals);
balanceOf[msg.sender] = totalSupply;
}
function transfer(address to, uint256 amount) public returns (bool) {
require(to != address(0), "Zero address");
require(balanceOf[msg.sender] >= amount, "Insufficient balance");
balanceOf[msg.sender] -= amount;
balanceOf[to] += amount;
emit Transfer(msg.sender, to, amount);
return true;
}
function approve(address spender, uint256 amount) public returns (bool) {
allowance[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
function transferFrom(address from, address to, uint256 amount) public returns (bool) {
require(from != address(0), "Zero address");
require(to != address(0), "Zero address");
require(balanceOf[from] >= amount, "Insufficient balance");
require(allowance[from][msg.sender] >= amount, "Insufficient allowance");
balanceOf[from] -= amount;
balanceOf[to] += amount;
allowance[from][msg.sender] -= amount;
emit Transfer(from, to, amount);
return true;
}
}
ERC-721 NFT
contract ERC721Token {
string public name;
string public symbol;
mapping(uint256 => address) private _owners;
mapping(address => uint256) private _balances;
mapping(uint256 => address) private _tokenApprovals;
mapping(address => mapping(address => bool)) private _operatorApprovals;
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
constructor(string memory _name, string memory _symbol) {
name = _name;
symbol = _symbol;
}
function balanceOf(address owner) public view returns (uint256) {
require(owner != address(0), "Zero address");
return _balances[owner];
}
function ownerOf(uint256 tokenId) public view returns (address) {
address owner = _owners[tokenId];
require(owner != address(0), "Invalid token ID");
return owner;
}
function approve(address to, uint256 tokenId) public {
address owner = ownerOf(tokenId);
require(msg.sender == owner || isApprovedForAll(owner, msg.sender), "Not authorized");
_tokenApprovals[tokenId] = to;
emit Approval(owner, to, tokenId);
}
function getApproved(uint256 tokenId) public view returns (address) {
require(_owners[tokenId] != address(0), "Invalid token ID");
return _tokenApprovals[tokenId];
}
function setApprovalForAll(address operator, bool approved) public {
require(operator != msg.sender, "Self approval");
_operatorApprovals[msg.sender][operator] = approved;
emit ApprovalForAll(msg.sender, operator, approved);
}
function isApprovedForAll(address owner, address operator) public view returns (bool) {
return _operatorApprovals[owner][operator];
}
function transferFrom(address from, address to, uint256 tokenId) public {
address owner = ownerOf(tokenId);
require(
msg.sender == owner ||
getApproved(tokenId) == msg.sender ||
isApprovedForAll(owner, msg.sender),
"Not authorized"
);
require(owner == from, "Not owner");
require(to != address(0), "Zero address");
_tokenApprovals[tokenId] = address(0);
_balances[from] -= 1;
_balances[to] += 1;
_owners[tokenId] = to;
emit Transfer(from, to, tokenId);
}
function _mint(address to, uint256 tokenId) internal {
require(to != address(0), "Zero address");
require(_owners[tokenId] ==