Version: Solidity ^0.8.20 | Reading Time: 50 minutes | Prerequisites: None
Solidity has two fundamental categories of data types with completely different behaviors:
| Category | Stored | Passed By | Modification Behavior | Data Location Needed? |
|---|---|---|---|---|
| Value Types | Stack (cheap) | Value (copy) | Modifying copy doesn't affect original | ❌ No |
| Reference Types | Storage/Memory/Calldata | Reference (pointer) | Modifying reference affects all pointers | ✅ Yes |
Value types are the basic building blocks. They are always copied when assigned or passed to functions.
bool public isActive = true;
bool public isPaused = false;
// Operations
bool result = true && false; // false
bool result2 = true || false; // true
bool result3 = !true; // false
Key Point: bool is 1 byte, stored on stack, copied by value.
// Unsigned integers (positive only) - VALUE TYPES
uint256 public count = 0; // 32 bytes, 0 to 2^256-1
uint8 public small = 255; // 1 byte, 0 to 255
uint16 public medium = 65535; // 2 bytes
uint32 public large = 4294967295; // 4 bytes
// Signed integers (positive & negative) - VALUE TYPES
int256 public temperature = -10; // 32 bytes, -2^255 to 2^255-1
int8 public tiny = -128; // 1 byte, -128 to 127
⚠️ CRITICAL: Always use explicit sizes (uint256 not uint) per official style guide.
// Regular address - VALUE TYPE
address public wallet = 0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb;
// Payable address (can receive ETH) - VALUE TYPE
address payable public recipient;
// Special addresses
address public caller = msg.sender; // Who called function
address public thisContract = address(this); // This contract
Key Point: address is 20 bytes, always copied by value.
bytes1 public singleByte = 0x01; // 1 byte - VALUE TYPE
bytes2 public twoBytes = 0x1234; // 2 bytes - VALUE TYPE
bytes4 public selector; // 4 bytes - VALUE TYPE
bytes8 public shortHash; // 8 bytes - VALUE TYPE
bytes32 public fullHash; // 32 bytes - VALUE TYPE
// Common use: function selectors
bytes4 public transferSelector = bytes4(keccak256("transfer(address,uint256)"));
Key Point: bytes1 through bytes32 are value types - fixed size, stored on stack.
enum Status { Pending, Active, Completed, Cancelled } // VALUE TYPE
enum Priority { Low, Medium, High, Urgent } // VALUE TYPE
Status public currentStatus = Status.Pending;
Priority public taskPriority = Priority.High;
// Internally stored as uint8
uint8 statusNumber = uint8(Status.Active); // 1
Key Point: Enums are internally uint8, making them value types.
Reference types store a pointer/address to data. Multiple variables can point to the same data. Must specify data location (storage/memory/calldata).
bytes public data; // Dynamic size - REFERENCE TYPE!
function manipulateBytes(bytes calldata input) external {
// input is in calldata (read-only)
// Can convert to memory if needed
bytes memory copy = bytes(input); // Explicit copy to memory
}
⚠️ CRITICAL DISTINCTION:
- bytes1 to bytes32 = Value types (fixed size)
- bytes (no number) = Reference type (dynamic array)
string public name = "Solidity"; // Dynamic UTF-8 - REFERENCE TYPE!
// Limitations:
// - Cannot get length directly
// - Cannot index characters directly
// - Expensive to manipulate
function getStringLength(string calldata str) external pure returns (uint256) {
// Must convert to bytes to get length
return bytes(str).length;
}
Key Point: string is essentially bytes with UTF-8 validation. Always a reference type.
// Dynamic array - REFERENCE TYPE
uint256[] public numbers;
string[] public names;
address[] public participants;
// Fixed-size array - ALSO REFERENCE TYPE!
uint256[5] public fixedArray;
uint256[3] public initialized = [1, 2, 3];
Key Point: Both dynamic and fixed-size arrays are reference types requiring data location.
struct User { // Defines a structure
string name; // string is reference
uint256 age; // uint256 is value
address wallet; // address is value
}
User public user; // user is a reference type variable
function createUser(string calldata _name, uint256 _age) external {
// Must specify storage or memory
User memory newUser = User(_name, _age, msg.sender); // memory
user = newUser; // Copy to storage
}
Key Point: Structs can contain both value and reference types. The struct itself is a reference type.
// Mapping - always in storage, always reference type
mapping(address => uint256) public balances;
mapping(address => User) public users;
mapping(address => mapping(address => uint256)) public allowances;
Key Point: Mappings are always in storage. You cannot have a memory mapping.
| Type | Category | Size | Storage | Example |
|---|---|---|---|---|
bool | ✅ Value | 1 bit | Stack | true |
uint/int | ✅ Value | 8-256 bits | Stack | uint256 |
address | ✅ Value | 160 bits | Stack | msg.sender |
bytes1-32 | ✅ Value | 1-32 bytes | Stack | bytes32 |
enum | ✅ Value | 8 bits | Stack | Status.Pending |
bytes | 🔵 Reference | Dynamic | s/m/c | bytes calldata |
string | 🔵 Reference | Dynamic | s/m/c | string memory |
T[] | 🔵 Reference | Dynamic/Fixed | s/m/c | uint256[] memory |
struct | 🔵 Reference | Sum members | s/m/c | User storage |
mapping | 🔵 Reference | Dynamic | storage only | mapping(k => v) |
contract TypeBehavior {
// VALUE TYPE EXAMPLE
function demonstrateValue() external pure returns (uint256, uint256) {
uint256 a = 100; // Value type
uint256 b = a; // b gets COPY of a (100)
b = 999; // Changing b does NOT affect a
return (a, b); // Returns (100, 999)
}
// REFERENCE TYPE EXAMPLE
uint256[] public storageArray;
function demonstrateReference() external {
storageArray.push(100);
storageArray.push(200);
// Get reference to storage
uint256[] storage ref = storageArray;
ref[0] = 999; // Changes storageArray[0] too!
// Now storageArray is [999, 200]
}
function demonstrateMemoryCopy() external pure returns (uint256, uint256) {
uint256[] memory arr1 = new uint256[](2);
arr1[0] = 100;
arr1[1] = 200;
uint256[] memory arr2 = arr1; // Copy, not reference!
arr2[0] = 999;
// arr1[0] is still 100 because memory creates copies
return (arr1[0], arr2[0]); // Returns (100, 999)
}
}
| Location | Persistence | Gas Cost | When to Use |
|---|---|---|---|
| storage | Permanent | 💰💰💰 Highest | State variables |
| memory | Temporary | 💰 Cheap | Calculations, return values |
| calldata | Temporary | ✅ Cheapest | External inputs (read-only) |
⚠️ Only reference types need data location! Value types are always on the stack.
contract StorageExample {
// These are automatically in storage (state variables)
uint256[] public numbers; // storage
mapping(address => uint256) public balances; // storage
string public message; // storage
bytes public data; // storage
struct User {
string name;
uint256 age;
}
User public user; // storage
function writeToStorage() external {
// Writing to storage is expensive (~20,000 gas for SSTORE)
numbers.push(100); // storage write
message = "Hello"; // storage write
}
}
Key Point: State variables are always in storage. Writing to storage costs significant gas.
contract MemoryExample {
uint256[] public numbers; // storage (state variable)
function process(uint256[] memory tempArray) public pure returns (uint256) {
// tempArray is in memory (temporary)
// modifications here don't affect the original
uint256 sum = 0; // sum is on stack (value type)
for (uint256 i = 0; i < tempArray.length; i++) {
sum += tempArray[i];
}
// Create new memory array
uint256[] memory newArray = new uint256[](3);
newArray[0] = 100;
newArray[1] = 200;
newArray[2] = 300;
return sum; // All memory is cleared after function ends
}
function getNumbers() external view returns (uint256[] memory) {
// Return storage array as memory copy
return numbers; // Copies from storage to memory
}
function demonstrateMemory() external pure returns (uint256, uint256) {
uint256[] memory arr1 = new uint256[](3);
arr1[0] = 100;
uint256[] memory arr2 = arr1;
arr2[0] = 999;
return (arr1[0], arr2[0]); // Returns (100, 999) - Copy!
}
}
contract CalldataExample {
// ✅ CORRECT: Use calldata for external function inputs
function processData(string calldata input) external pure returns (bytes32) {
// input is read-only, lives in calldata (cheapest)
return keccak256(bytes(input));
}
// ❌ WRONG: memory costs more gas for external functions
function processExpensive(string memory input) external pure returns (bytes32) {
// This copies calldata to memory unnecessarily
return keccak256(bytes(input));
}
// ✅ Array example with calldata
function sumArray(uint256[] calldata arr) external pure returns (uint256) {
uint256 sum = 0;
for (uint256 i = 0; i < arr.length; i++) {
sum += arr[i];
}
return sum;
}
}
contract Comparison {
function withMemory(string memory data) external pure returns (uint256) {
// 1. data comes in as calldata (external function)
// 2. Solidity copies it to memory
// 3. You work with the memory copy
// 4. Costs extra gas for the copy
return bytes(data).length;
}
function withCalldata(string calldata data) external pure returns (uint256) {
// 1. data stays in calldata
// 2. You work directly with it
// 3. No copy needed
// 4. Cheapest option
return bytes(data).length;
}
}
Gas Savings: calldata can save 2000+ gas per string/array parameter compared to memory.
contract DecisionTree {
uint256[] public stateArray; // storage (automatic)
// Rule 1: State variables → storage (automatic)
// Rule 2: External function params → calldata (cheapest)
function externalFunc(uint256[] calldata input) external { }
// Rule 3: Public function params → memory
function publicFunc(uint256[] memory input) public { }
// Rule 4: Return values → memory
function getArray() external view returns (uint256[] memory) {
return stateArray;
}
// Rule 5: Temporary variables → memory
function tempArray() external pure {
uint256[] memory temp = new uint256[](5);
}
// Rule 6: Value types → no location needed (always stack)
function valueTypes(uint256 num, address addr) external pure { }
}
contract DataLocationMistakes {
uint256[] public arr;
// ❌ MISTAKE 1: Trying to assign storage to memory directly
function mistake1() external view {
uint256[] memory mem = arr; // Creates copy, not reference
}
// ❌ MISTAKE 2: Returning storage reference
function mistake2() external view returns (uint256[] storage) {
// return arr; // ❌ Can't return storage reference to external
}
// ✅ CORRECT: Return memory copy
function correct2() external view returns (uint256[] memory) {
return arr; // Returns copy in memory
}
// ❌ MISTAKE 4: Forgetting that memory creates copies
function mistake4() external pure returns (uint256[] memory) {
uint256[] memory a = new uint256[](1);
a[0] = 100;
uint256[] memory b = a;
b[0] = 999;
return a; // Returns [100], not [999]!
}
}
uint256[] public numbers; // Dynamic array
string[] public names; // Array of strings
// Adding/removing
numbers.push(100); // Add to end
uint256 popped = numbers.pop(); // Remove last
// Access
uint256 length = numbers.length; // Get size
numbers[0] = 999; // Update
// Delete (sets to zero, doesn't remove index)
delete numbers[0]; // numbers[0] is now 0, array length unchanged
uint256[5] public fixedArray; // Always 5 elements
uint256[3] public initialized = [1, 2, 3];
// Cannot push/pop on fixed arrays
fixedArray[0] = 100;
⚠️ WARNING: Looping through large arrays can exceed block gas limit (30M gas) and brick your contract!
contract BadArray {
address[] public users;
mapping(address => uint256) public balances;
function distributeRewards() external {
// DANGER: If users.length > 1000, this will fail!
for (uint256 i = 0; i < users.length; i++) {
balances[users[i]] += 1; // Each iteration costs ~50k gas
}
}
}
10,000 users × 50,000 gas = 500M gas ❌ EXCEEDS BLOCK LIMIT (30M)!
contract PullPattern {
mapping(address => uint256) public pendingRewards;
function claimReward() external {
uint256 amount = pendingRewards[msg.sender];
require(amount > 0, "No reward");
pendingRewards[msg.sender] = 0;
(bool success, ) = payable(msg.sender).call{value: amount}("");
require(success, "Transfer failed");
}
}
Benefits: Gas paid by claimer, unlimited users, safe.
contract Pagination {
address[] public users;
uint256 public lastProcessedIndex;
function processBatch(uint256 batchSize) external {
uint256 end = lastProcessedIndex + batchSize;
if (end > users.length) end = users.length;
for (uint256 i = lastProcessedIndex; i < end; i++) {
// Process users[i]
}
lastProcessedIndex = end;
}
}
contract MerkleAirdrop {
bytes32 public merkleRoot;
function claim(uint256 amount, bytes32[] calldata merkleProof) external {
bytes32 leaf = keccak256(abi.encodePacked(msg.sender, amount));
require(verify(merkleProof, merkleRoot, leaf), "Invalid proof");
// ... Logic
}
}
Benefits: Handless millions of users, only 1 storage slot needed.
contract OffChainComputation {
function updateBalances(address[] calldata users, uint256[] calldata balances, bytes calldata sig) external {
require(verifySignature(users, balances, sig), "Invalid sig");
for (uint256 i = 0; i < users.length; i++) {
// ... Logic
}
}
}
| Scenario | Users | Solution | Why |
|---|---|---|---|
| Rewards | < 100 | Direct loop | Simple |
| Rewards | 100-1000 | Pull pattern | Scalable |
| Rewards | > 1000 | Merkle proofs | Minimal storage |
| Data proc | Any | Pagination | Controlled gas |
| Math | Any | Off-chain | Free computation |
contract ArrayBestPractices {
// ✅ Track size manually for safety
uint256 public userCount;
mapping(uint256 => address) public userAtIndex;
// ✅ Prefer mappings for lookups (O(1) vs O(n))
mapping(address => bool) public isUser;
// ❌ AVOID: Deleting middle elements (requires shifting)
function removeMiddleExpensive(uint256 index) external {
for (uint256 i = index; i < numbers.length - 1; i++) {
numbers[i] = numbers[i + 1];
}
numbers.pop();
}
// ✅ BETTER: Swap with last and pop (O(1) but unordered)
function removeFast(uint256 index) external {
numbers[index] = numbers[numbers.length - 1];
numbers.pop();
}
}
contract MappingExamples {
mapping(address => uint256) public balances;
mapping(address => mapping(address => uint256)) public allowances;
// ❌ CANNOT: Check if key exists directly
// Scores[user] != 0 is wrong if 0 is a valid score!
// ✅ SOLUTION: Track existence separately
mapping(address => bool) public hasScore;
// ❌ CANNOT: Iterate over mappings
// ✅ SOLUTION: Track keys in array
address[] public allPlayers;
}
contract StructExamples {
struct Task {
string description; // Reference
uint256 deadline; // Value
address assignee; // Value
bool completed; // Value
}
Task[] public tasks;
function updateTask(uint256 index) external {
// Get storage reference (modifies original)
Task storage task = tasks[index];
task.completed = true;
// vs memory (creates copy, doesn't save)
Task memory taskCopy = tasks[index];
taskCopy.completed = false; // Changes copy only!
}
}
bool, int, int8...int256, uint, uint8...uint256
address, address payable
bytes1, bytes2...bytes32, enum
bytes, string, T[], T[k], struct, mapping
⚠️ DISTINCTION: bytes32 is value type. bytes is reference type.
function, view, pure, returns, return
virtual, override, modifier, constructor
receive, fallback
public, private, internal, external
Order: visibility → mutability → virtual → override.
storage, memory, calldata
if, else, else if, for, while, do, break, continue, return, try, catch
require(cond, "msg");
assert(cond);
revert("msg");
revert CustomError();
error CustomError();
Modern Best Practice: Use custom errors with revert.
contract, abstract, interface, library, using, import, is
msg.sender, msg.value, msg.data, msg.sig
tx.origin, tx.gasprice, gasleft()
block.timestamp, block.number, block.difficulty, block.gaslimit, block.coinbase, blockhash()
address(this), this
constant, immutable
wei, gwei, szabo, finney, ether
seconds, minutes, hours, days, weeks
⚠️ Avoid "years" - doesn't account for leap years.
contract ModernOwnable {
address public immutable owner;
error NotOwner();
constructor() { owner = msg.sender; }
modifier onlyOwner() {
if (msg.sender != owner) revert NotOwner();
_;
}
}
immutable saves ~2,100 gas per read.
contract ReentrancyGuard {
bool private _locked;
error ReentrantCall();
modifier nonReentrant() {
if (_locked) revert ReentrantCall();
_locked = true;
_;
_locked = false;
}
}
contract SafeWithdrawal {
function withdraw(uint256 amount) external nonReentrant {
// 1. CHECKS
if (balances[msg.sender] < amount) revert Insufficient();
// 2. EFFECTS (State updates FIRST)
balances[msg.sender] -= amount;
// 3. INTERACTIONS (External calls LAST)
(bool success, ) = payable(msg.sender).call{value: amount}("");
if (!success) revert TransferFailed();
}
}
// ✅ MODERN: Use call() with reentrancy protection
(bool success, ) = to.call{value: amount}("");
// Events (Past Tense, SubjectVerb)
event OwnerUpdated(address indexed prev, address indexed next);
transfer() is limited to 2300 gas and can break with contract wallets.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract TaskManager {
enum Priority { Low, Medium, High, Urgent }
enum Status { Pending, InProgress, Completed, Cancelled }
struct Task {
string description;
uint256 deadline;
Status status;
Priority priority;
address assignee;
bool exists;
}
address public immutable owner;
uint256 private _nextTaskId;
mapping(uint256 => Task) private _tasks;
mapping(address => uint256[]) private _userTaskIds;
event TaskCreated(uint256 indexed taskId, address indexed creator, string description, uint256 deadline);
event TaskStatusUpdated(uint256 indexed taskId, Status newStatus, address updater);
event TaskAssigned(uint256 indexed taskId, address indexed newAssignee);
error Unauthorized();
error TaskNotFound(uint256 taskId);
error InvalidDeadline();
error InvalidStatusTransition();
error ZeroAddress();
modifier onlyOwner() {
if (msg.sender != owner) revert Unauthorized();
_;
}
modifier taskExists(uint256 taskId) {
if (!_tasks[taskId].exists) revert TaskNotFound(taskId);
_;
}
constructor() {
owner = msg.sender;
_nextTaskId = 1;
}
function createTask(string calldata description, uint256 deadline, Priority priority) external returns (uint256 taskId) {
if (deadline <= block.timestamp) revert InvalidDeadline();
unchecked { taskId = _nextTaskId++; }
_tasks[taskId] = Task({
description: description,
deadline: deadline,
status: Status.Pending,
priority: priority,
assignee: msg.sender,
exists: true
});
_userTaskIds[msg.sender].push(taskId);
emit TaskCreated(taskId, msg.sender, description, deadline);
}
function updateStatus(uint256 taskId, Status newStatus) external taskExists(taskId) {
Task storage task = _tasks[taskId];
if (task.assignee != msg.sender) revert Unauthorized();
if (!_isValidTransition(task.status, newStatus)) revert InvalidStatusTransition();
task.status = newStatus;
emit TaskStatusUpdated(taskId, newStatus, msg.sender);
}
function assignTask(uint256 taskId, address newAssignee) external onlyOwner taskExists(taskId) {
if (newAssignee == address(0)) revert ZeroAddress();
_tasks[taskId].assignee = newAssignee;
emit TaskAssigned(taskId, newAssignee);
}
function getMyTasks() external view returns (uint256[] memory) {
return _userTaskIds[msg.sender];
}
function getTask(uint256 taskId) external view taskExists(taskId) returns (Task memory) {
return _tasks[taskId];
}
function _isValidTransition(Status current, Status next) internal pure returns (bool) {
if (current == Status.Pending && next == Status.InProgress) return true;
if (current == Status.InProgress && next == Status.Completed) return true;
if (current == Status.Pending && next == Status.Cancelled) return true;
return false;
}
}
// ❌ WRONG
function bad(string memory str) external {
string memory copy = str;
str = "changed"; // Doesn't affect copy
}
// ❌ WRONG: Vulnerable to phishing
require(tx.origin == owner);
// ✅ CORRECT
require(msg.sender == owner);
// ❌ WRONG
uint256 result = 5 / 2; // Result is 2
// ✅ CORRECT: Multiply first
uint256 result = (5 * 100) / 2; // 250
// ❌ WRONG
msg.sender.call{value: 1 ether}(""); // Silent failure!
// ✅ CORRECT
(bool success, ) = msg.sender.call{value: 1 ether}("");
require(success);
// ❌ WRONG: Wastes gas
function process(string memory data) external { }
// ✅ CORRECT: Use calldata
function process(string calldata data) external { }
// ❌ WRONG: Cannot receive ETH
function deposit() external { }
// ✅ CORRECT
function deposit() external payable { }
| Type | Size | Default | Category |
|---|---|---|---|
bool | 1 bit | false | Value |
uint256 | 32 bytes | 0 | Value |
address | 20 bytes | 0x0 | Value |
bytes32 | 32 bytes | 0x0 | Value |
string | Dynamic | "" | Reference |
mapping | Dynamic | - | Reference |
| Operation | Gas |
|---|---|
| calldata read | 3 |
| storage read (cold) | 2100 |
| storage write | 20,000 |
| Custom error | ~30 |
Is it state var? → public/private/internal
Called internally? → public/internal
Called externally? → external
State var? → storage
External param? → calldata
Return value? → memory
| Element | Convention | Example |
|---|---|---|
| Contracts | PascalCase | TaskManager |
| Events | PascalCase | TaskCreated |
| Functions | camelCase | createTask |
| Constants | UPPER_CASE | MAX_VAL |
msg.sender not tx.origincall() used for ETH transfers?immutable for constructor values?