# Solidity Mastery Handbook: Zero to Advanced ## A Security-First Guide for Smart Contract Development --- ## Level 1: The Basics ### 1.1 Pragma Directive **The 'What':** A pragma directive tells the Solidity compiler which version to use when compiling your contract. It's the first line in every Solidity file and ensures version compatibility. **The Syntax:** ```solidity // Fixed version (most secure) pragma solidity 0.8.20; // Version range (use cautiously) pragma solidity ^0.8.0; // Multiple constraints pragma solidity >=0.8.0 <0.9.0; ``` **The 'Why':** Different compiler versions can introduce breaking changes, new features, or security patches. Locking to a specific version ensures your contract behaves predictably and prevents compilation with untested versions. **Security & Bugs:** - **Vulnerability:** Using `^` (caret) allows any 0.8.x version, which might include untested compiler bugs or breaking changes - **Best Practice:** Always use a fixed version (e.g., `0.8.20`) for production contracts - **Best Practice:** Use Solidity 0.8.0+ which has built-in overflow/underflow protection - **Gas Tip:** Newer versions often include gas optimizations; test before upgrading - **Warning:** Versions below 0.8.0 don't have automatic overflow checks and require SafeMath --- ### 1.2 State Variables **The 'What':** State variables are permanently stored in contract storage on the blockchain. They persist between function calls and transactions. **The Syntax:** ```solidity contract Storage { // State variables uint256 public count; // Automatically gets a getter address private owner; // Only accessible internally mapping(address => uint) balances; bool internal isActive; constructor() { owner = msg.sender; count = 0; } } ``` **The 'Why':** Use state variables to store data that must persist across transactions, like user balances, ownership records, or contract configuration. They're essential for maintaining contract state. **Security & Bugs:** - **Gas Cost:** Reading state variables costs 100-2100 gas (warm/cold); writing costs 20,000 gas (new) or 5,000 gas (update) - **Vulnerability:** Uninitialized state variables default to zero/false, which can cause logic errors - **Best Practice:** Always initialize state variables explicitly in the constructor - **Gas Optimization:** Cache state variables in memory when reading multiple times in a function - **Security:** Use `private` for sensitive data (though it's still visible on-chain) - **Vulnerability:** Never store plaintext passwords or secrets in state variables ```solidity // BAD: Multiple state reads function badExample() public view returns (uint) { return count + count + count; // 3 SLOAD operations } // GOOD: Cache in memory function goodExample() public view returns (uint) { uint256 _count = count; // 1 SLOAD return _count + _count + _count; } ``` --- ### 1.3 Data Types #### 1.3.1 Value Types **The 'What':** Value types hold their data directly and are copied when assigned or passed to functions. **The Syntax:** ```solidity contract DataTypes { // Integers uint8 smallNumber; // 0 to 255 uint256 largeNumber; // 0 to 2^256-1 (default uint) int256 signedNumber; // -2^255 to 2^255-1 // Address types address userAddress; address payable recipient; // Can receive Ether // Boolean bool isActive; // Bytes bytes32 hash; // Fixed-size byte array bytes1 singleByte; // Enums enum Status { Pending, Active, Completed } Status currentStatus; } ``` **The 'Why':** Value types are fundamental building blocks. Use smaller types like `uint8` only when packing variables in storage; otherwise, `uint256` is more gas-efficient in operations. **Security & Bugs:** - **Vulnerability:** Integer overflow/underflow (pre-0.8.0) - use SafeMath or upgrade to 0.8.0+ - **Gas Trap:** Using `uint8` instead of `uint256` in calculations costs MORE gas due to conversion - **Best Practice:** Use `uint256` for calculations, only use smaller types for storage packing - **Security:** Use `address payable` explicitly when contract needs to send Ether - **Vulnerability:** Unchecked type conversions can truncate data ```solidity // DANGEROUS (pre-0.8.0) uint8 balance = 255; balance += 1; // Wraps to 0 without SafeMath // SAFE (0.8.0+) uint8 balance = 255; balance += 1; // Reverts with overflow error // Gas inefficiency example uint8 a = 1; uint8 b = 2; uint8 c = a + b; // Costs more gas than uint256 due to checks ``` #### 1.3.2 Reference Types **The 'What':** Reference types don't contain data directly but reference a location where data is stored. Changes to one reference affect all references to the same data. **The Syntax:** ```solidity contract ReferenceTypes { // Arrays uint[] public dynamicArray; uint[5] public fixedArray; // Structs struct User { address wallet; uint256 balance; bool isActive; } User[] public users; // Strings (dynamic byte arrays) string public name; // Bytes (dynamic) bytes public data; function arrayExample() public { // Memory arrays uint[] memory tempArray = new uint[](5); // Storage reference uint[] storage storageRef = dynamicArray; storageRef.push(1); // Modifies state } } ``` **The 'Why':** Use arrays for lists, structs for complex data, and strings for text. Memory is for temporary data; storage persists on-chain. **Security & Bugs:** - **Vulnerability:** Unbounded arrays can cause out-of-gas errors in loops - **Gas Trap:** Dynamic arrays in storage are expensive; each push costs 20,000+ gas - **Best Practice:** Never loop over unbounded arrays; implement pagination - **Memory Safety:** Memory arrays have fixed size and cannot be resized - **Gas Optimization:** Use `bytes` instead of `string` when possible (strings are just bytes) - **Vulnerability:** Deleting array elements leaves gaps; use swap-and-pop pattern ```solidity // DANGEROUS: Unbounded loop function badIteration() public { for(uint i = 0; i < users.length; i++) { // Can run out of gas if users array is large } } // SAFE: Pagination pattern function safeIteration(uint start, uint count) public view returns (User[] memory) { uint end = start + count > users.length ? users.length : start + count; User[] memory page = new User[](end - start); for(uint i = start; i < end; i++) { page[i - start] = users[i]; } return page; } // Efficient delete pattern function removeUser(uint index) public { users[index] = users[users.length - 1]; // Swap with last users.pop(); // Remove last element } ``` --- ### 1.4 Mappings **The 'What':** Mappings are hash tables that store key-value pairs. They provide O(1) lookup time and are the most gas-efficient way to store associative data. **The Syntax:** ```solidity contract MappingExamples { // Simple mapping mapping(address => uint256) public balances; // Nested mapping mapping(address => mapping(address => uint256)) public allowances; // Mapping with struct values struct Account { uint256 balance; bool exists; } mapping(address => Account) public accounts; function updateBalance(address user, uint256 amount) public { balances[user] = amount; } function approve(address spender, uint256 amount) public { allowances[msg.sender][spender] = amount; } } ``` **The 'Why':** Mappings are ideal for storing user data, permissions, or any key-value relationships. They're more gas-efficient than arrays for lookups and don't have iteration overhead. **Security & Bugs:** - **Critical:** Mappings cannot be iterated; you cannot get all keys or values - **Default Values:** Non-existent keys return default values (0, false, address(0)), not errors - **Best Practice:** Use a boolean flag to check if a key exists - **Gas Efficiency:** Mappings are cheaper than arrays for sparse data - **Vulnerability:** Forgetting to check existence can lead to logic errors - **Security:** Nested mappings are common for allowance patterns (ERC-20) ```solidity // VULNERABLE: Doesn't check if user exists function badWithdraw() public { uint amount = balances[msg.sender]; // Returns 0 if not set // Could lead to logic errors } // SECURE: Check existence mapping(address => bool) public hasAccount; function safeWithdraw() public { require(hasAccount[msg.sender], "No account"); uint amount = balances[msg.sender]; require(amount > 0, "No balance"); // Safe to proceed } // Track keys if iteration needed address[] public userList; mapping(address => bool) public isUser; function addUser(address user) public { if(!isUser[user]) { userList.push(user); isUser[user] = true; } } ``` --- ### 1.5 Basic Math Operations **The 'What':** Arithmetic operations allow mathematical calculations. Solidity 0.8.0+ includes automatic overflow/underflow checks. **The Syntax:** ```solidity contract MathOperations { function basicMath(uint a, uint b) public pure returns (uint) { uint sum = a + b; // Addition (checks overflow) uint difference = a - b; // Subtraction (checks underflow) uint product = a * b; // Multiplication uint quotient = a / b; // Division (rounds down) uint remainder = a % b; // Modulo uint power = a ** b; // Exponentiation return sum; } // Unchecked block for gas optimization (use carefully!) function uncheckedMath(uint a, uint b) public pure returns (uint) { unchecked { return a + b; // No overflow check, saves gas } } } ``` **The 'Why':** Use standard math for most operations. The `unchecked` block can save gas when you're certain overflow is impossible, but use it sparingly. **Security & Bugs:** - **Pre-0.8.0 Critical:** Always use SafeMath library for versions below 0.8.0 - **Division by Zero:** Always reverts automatically in Solidity - **Rounding:** Division always rounds DOWN towards zero - **Gas Savings:** `unchecked` saves ~20 gas per operation but removes safety - **Vulnerability:** Integer overflow in token calculations can drain contracts - **Best Practice:** Only use `unchecked` in loops with guaranteed bounds - **Precision Loss:** Use fixed-point math libraries for financial calculations ```solidity // VULNERABLE (pre-0.8.0 without SafeMath) uint8 balance = 250; balance += 10; // Wraps to 4 silently // SAFE (0.8.0+) uint8 balance = 250; balance += 10; // Reverts: "Arithmetic operation overflow" // Precision loss example uint price = 100; uint discount = 33; // 33% discount uint finalPrice = price - (price * discount / 100); // 67 (precision lost) // Better: Use basis points (10000 = 100%) uint DENOMINATOR = 10000; uint discountBps = 3333; // 33.33% uint finalPrice = price - (price * discountBps / DENOMINATOR); // More precise // Safe unchecked usage function sumArray(uint[] memory arr) public pure returns (uint) { uint sum = 0; for(uint i = 0; i < arr.length;) { sum += arr[i]; unchecked { i++; } // i won't overflow array length } return sum; } ``` --- ## Level 2: Functions & Control ### 2.1 Function Visibility Specifiers **The 'What':** Visibility specifiers control who can call a function. They're crucial for access control and security. **The Syntax:** ```solidity contract VisibilityExample { uint private secretValue; // Public: callable from anywhere function publicFunc() public returns (uint) { return secretValue; } // External: only callable from outside (cheaper gas) function externalFunc(uint[] calldata data) external returns (uint) { return data[0]; } // Internal: only this contract and derived contracts function internalFunc() internal view returns (uint) { return secretValue; } // Private: only this contract function privateFunc() private view returns (uint) { return secretValue; } } ``` **The 'Why':** Use `external` for functions only called externally (saves gas on calldata). Use `public` when the function needs internal and external access. Use `internal` for shared logic in inheritance. Use `private` for truly internal-only functions. **Security & Bugs:** - **Critical Security:** Default visibility is `public` in old versions; always specify explicitly - **Gas Optimization:** `external` is cheaper than `public` when passing arrays (uses calldata) - **Vulnerability:** Functions without visibility can be called by anyone - **Best Practice:** Mark all functions with visibility; use most restrictive possible - **Common Bug:** Forgetting to restrict admin functions leads to exploits - **Security:** Private functions are still visible in bytecode; don't store secrets ```solidity // DANGEROUS: No visibility specified (old Solidity) function withdraw() { /* anyone can call */ } // SECURE: Explicit visibility with access control address public owner; modifier onlyOwner() { require(msg.sender == owner, "Not owner"); _; } function withdraw() external onlyOwner { // Only owner can call } // Gas comparison // Public function (can be called internally or externally) function publicArray(uint[] memory data) public pure returns (uint) { return data.length; // Memory copy costs gas } // External function (cheaper for external calls) function externalArray(uint[] calldata data) external pure returns (uint) { return data.length; // Direct calldata access } ``` --- ### 2.2 Function Modifiers **The 'What':** Modifiers are reusable code that runs before (or after) function execution. They're commonly used for access control, validation, and state checks. **The Syntax:** ```solidity contract ModifierExample { address public owner; bool private locked; constructor() { owner = msg.sender; } // Access control modifier modifier onlyOwner() { require(msg.sender == owner, "Not authorized"); _; // Function body executes here } // Reentrancy guard modifier nonReentrant() { require(!locked, "Reentrant call"); locked = true; _; locked = false; } // Validation modifier modifier validAddress(address addr) { require(addr != address(0), "Invalid address"); _; } // Using multiple modifiers function restrictedFunction(address recipient) external onlyOwner validAddress(recipient) nonReentrant { // Function logic } } ``` **The 'Why':** Modifiers reduce code duplication and make access control patterns reusable. They improve readability by separating validation logic from business logic. **Security & Bugs:** - **Vulnerability:** Modifiers execute before the function; order matters with multiple modifiers - **Reentrancy:** Always place `nonReentrant` modifier first - **Gas Cost:** Each modifier adds gas overhead; don't overuse - **Best Practice:** Keep modifiers simple; complex logic should be in functions - **Bug:** Forgetting `_;` means the function never executes - **Security:** State changes in modifiers should be carefully considered ```solidity // VULNERABLE: Wrong modifier order function withdraw() external nonReentrant onlyOwner { // If onlyOwner is checked first, attacker might bypass nonReentrant } // SECURE: Correct order function withdraw() external onlyOwner nonReentrant { // Access control first, then reentrancy protection } // VULNERABLE: State-changing modifier modifier badModifier() { secretValue++; // State changes in modifier are hard to track _; } // BETTER: Keep modifiers for checks only modifier goodModifier() { require(condition, "Failed check"); _; } // Advanced pattern: Modifier with cleanup modifier timedAction() { require(block.timestamp >= startTime, "Too early"); _; lastActionTime = block.timestamp; // Post-execution logic } ``` --- ### 2.3 View and Pure Functions **The 'What':** `view` functions promise not to modify state. `pure` functions promise not to read or modify state. These guarantees allow the compiler to optimize and enable free off-chain calls. **The Syntax:** ```solidity contract StateAccess { uint public value = 100; // View: reads state but doesn't modify function getValue() public view returns (uint) { return value; // Reading state is allowed } function checkBalance(address user) public view returns (uint) { return address(user).balance; // Reading blockchain state } // Pure: no state access at all function add(uint a, uint b) public pure returns (uint) { return a + b; // Only uses parameters } function hash(string memory data) public pure returns (bytes32) { return keccak256(abi.encodePacked(data)); } } ``` **The 'Why':** Use `view` for read-only operations that need state access. Use `pure` for utility functions that don't need state. Both are free when called externally (off-chain), making them perfect for getters and calculations. **Security & Bugs:** - **Gas:** `view` and `pure` are free for external calls but cost gas when called internally - **Compiler Enforcement:** Compiler prevents state modifications in `view` and reads in `pure` - **Best Practice:** Always mark functions that don't modify state as `view` or `pure` - **Bug:** Calling non-view functions from view functions costs gas and can fail - **Security:** View functions can still be exploited via reentrancy when calling external contracts - **Optimization:** Use `pure` for helpers and libraries when possible ```solidity // View function behavior function viewExample() public view returns (uint) { // ALLOWED: return value; // Read state uint balance = address(this).balance; // Read blockchain // NOT ALLOWED (compiler error): // value = 10; // Modify state // payable(msg.sender).transfer(1); // Send Ether } // Pure function behavior function pureExample(uint a) public pure returns (uint) { // ALLOWED: return a * 2; // Use parameters uint result = 5 + 5; // Local calculations // NOT ALLOWED (compiler error): // return value; // Read state // return address(this).balance; // Read blockchain } // Common mistake: marking as view when it should be pure function badPure() public view returns (uint) { return 100; // Should be pure, no state needed } function goodPure() public pure returns (uint) { return 100; // Correct } // Gas consideration function internalCallCost() public view returns (uint) { return getValue(); // Costs gas even though getValue is view } ``` --- ### 2.4 Payable Functions **The 'What':** The `payable` modifier allows a function to receive Ether. Without it, transactions sending Ether will revert. **The Syntax:** ```solidity contract PayableExample { mapping(address => uint) public balances; // Receive Ether function deposit() external payable { balances[msg.sender] += msg.value; } // Fallback function (receives Ether sent to contract directly) receive() external payable { balances[msg.sender] += msg.value; } // Fallback for calls to non-existent functions fallback() external payable { // Handle unknown function calls } // Send Ether (not payable) function withdraw(uint amount) external { require(balances[msg.sender] >= amount, "Insufficient balance"); balances[msg.sender] -= amount; // Three ways to send Ether: // 1. transfer (2300 gas, reverts on failure) payable(msg.sender).transfer(amount); // 2. send (2300 gas, returns bool) // bool success = payable(msg.sender).send(amount); // require(success, "Send failed"); // 3. call (forwards all gas, returns bool) - RECOMMENDED // (bool success, ) = payable(msg.sender).call{value: amount}(""); // require(success, "Transfer failed"); } } ``` **The 'Why':** Use `payable` when your function needs to receive Ether. Always validate amounts and update state before transferring to prevent reentrancy attacks. **Security & Bugs:** - **Critical:** Always use Checks-Effects-Interactions pattern to prevent reentrancy - **Gas Limits:** `transfer` and `send` limit gas to 2300, which can fail with smart contract recipients - **Best Practice:** Use `call` for sending Ether in modern Solidity (with reentrancy guards) - **Vulnerability:** `transfer` can break with future EVM changes; `call` is more future-proof - **Security:** Always check return values when using `call` or `send` - **Bug:** Forgetting `payable` on address types prevents sending Ether ```solidity // VULNERABLE: Reentrancy attack function vulnerableWithdraw() external { uint amount = balances[msg.sender]; (bool success, ) = msg.sender.call{value: amount}(""); // External call before state change require(success); balances[msg.sender] = 0; // TOO LATE! Attacker can reenter } // SECURE: Checks-Effects-Interactions pattern function secureWithdraw() external nonReentrant { uint amount = balances[msg.sender]; require(amount > 0, "No balance"); // Checks balances[msg.sender] = 0; // Effects (state change FIRST) (bool success, ) = msg.sender.call{value: amount}(""); // Interactions require(success, "Transfer failed"); } // Modern recommendation: use call with reentrancy guard function recommendedWithdraw() external { uint amount = balances[msg.sender]; require(amount > 0, "No balance"); balances[msg.sender] = 0; // Update state first (bool success, ) = payable(msg.sender).call{value: amount}(""); require(success, "Transfer failed"); } // receive vs fallback // receive: called when msg.data is empty receive() external payable { emit Received(msg.sender, msg.value); } // fallback: called when no other function matches fallback() external payable { emit FallbackCalled(msg.sender, msg.value, msg.data); } ``` --- ### 2.5 Error Handling: Require, Revert, and Assert **The 'What':** Solidity provides three methods for error handling. `require` validates conditions, `revert` provides custom errors, and `assert` checks invariants. **The Syntax:** ```solidity contract ErrorHandling { uint public value; address public owner; // Custom errors (gas efficient, Solidity 0.8.4+) error Unauthorized(address caller); error InsufficientBalance(uint requested, uint available); constructor() { owner = msg.sender; } // Require: validate inputs and conditions function setValue(uint newValue) external { require(msg.sender == owner, "Not owner"); require(newValue > 0, "Value must be positive"); require(newValue <= 1000, "Value too large"); value = newValue; } // Revert: explicit revert with custom error function setValueWithCustomError(uint newValue) external { if(msg.sender != owner) { revert Unauthorized(msg.sender); } if(newValue == 0 || newValue > 1000) { revert("Invalid value"); } value = newValue; } // Assert: check invariants (should never fail) function criticalOperation() external { uint oldValue = value; // ... complex operations ... assert(value >= oldValue); // Invariant: value should never decrease } } ``` **The 'Why':** Use `require` for input validation and access control. Use custom errors with `revert` for gas-efficient error handling. Use `assert` for invariants that should never be false (failing assert indicates a bug). **Security & Bugs:** - **Gas Efficiency:** Custom errors save ~2000 gas compared to string errors - **Assert vs Require:** `assert` consumes all remaining gas (pre-0.8.0), while `require` refunds unused gas - **Best Practice:** Use `require` for user errors, `assert` for internal consistency - **Vulnerability:** Error messages are visible on-chain; don't include sensitive data - **Modern Pattern:** Prefer custom errors for production contracts - **Bug:** Using `assert` for input validation wastes gas on failure ```solidity // Gas comparison // Expensive: string error function expensiveCheck(uint amount) external { require(amount > 100, "Amount must be greater than 100"); // ~50 gas for string } // Cheap: custom error error AmountTooLow(uint amount, uint minimum); function cheapCheck(uint amount) external { if(amount <= 100) { revert AmountTooLow(amount, 100); // ~30 gas, saves ~20 gas } } // WRONG: Using assert for validation function wrongAssert(uint value) external { assert(value > 0); // Bad: consumes all gas if fails } // RIGHT: Using require for validation function rightRequire(uint value) external { require(value > 0, "Value must be positive"); // Refunds gas } // Assert for invariants mapping(address => uint) public balances; uint public totalSupply; function transfer(address to, uint amount) external { require(balances[msg.sender] >= amount, "Insufficient balance"); balances[msg.sender] -= amount; balances[to] += amount; // Invariant: total supply never changes in transfer assert(balances[msg.sender] + balances[to] == totalSupply); } // Revert with conditional logic function complexValidation(uint a, uint b) external { if(a > b) { revert("A must not exceed B"); } else if(a == 0) { revert("A cannot be zero"); } else if(b > 1000) { revert("B too large"); } // Continue if all checks pass } ``` --- ## Level 3: Contract Interaction ### 3.1 Inheritance **The 'What':** Inheritance allows contracts to inherit properties and functions from parent contracts. Solidity supports multiple inheritance with a specific linearization order (C3 linearization). **The Syntax:** ```solidity // Base contract contract Ownable { address public owner; event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); constructor() { owner = msg.sender; } modifier onlyOwner() { require(msg.sender == owner, "Not owner"); _; } function transferOwnership(address newOwner) public virtual onlyOwner { require(newOwner != address(0), "Invalid address"); emit OwnershipTransferred(owner, newOwner); owner = newOwner; } } // Single inheritance contract Token is Ownable { mapping(address => uint) public balances; function mint(address to, uint amount) external onlyOwner { balances[to] += amount; } } // Multiple inheritance contract Pausable { bool public paused; modifier whenNotPaused() { require(!paused, "Contract is paused"); _; } function pause() internal { paused = true; } } contract AdvancedToken is Ownable, Pausable { mapping(address => uint) public balances; function transfer(address to, uint amount) external whenNotPaused { require(balances[msg.sender] >= amount, "Insufficient balance"); balances[msg.sender] -= amount; balances[to] += amount; } function emergencyPause() external onlyOwner { pause(); } // Override parent function function transferOwnership(address newOwner) public override onlyOwner { require(newOwner != address(0), "Invalid address"); require(!paused, "Cannot transfer while paused"); super.transferOwnership(newOwner); // Call parent implementation } } ``` **The 'Why':** Inheritance promotes code reuse and creates hierarchical relationships. Use it to implement common patterns like ownership, pausability, and access control without duplicating code. **Security & Bugs:** - **Linearization:** With multiple inheritance, Solidity uses C3 linearization (right-to-left, depth-first) - **State Variables:** Child contracts can access parent's state; be careful with visibility - **Constructor Order:** Parent constructors execute before child constructors - **Vulnerability:** Diamond problem in multiple inheritance can cause unexpected behavior - **Best Practice:** Keep inheritance hierarchies shallow (2-3 levels max) - **Security:** Override functions must include `override` keyword in 0.6.0+ - **Gas:** Deep inheritance increases deployment costs ```solidity // COMPLEX: Multiple inheritance order matters contract A { function foo() public virtual returns (string memory) { return "A"; } } contract B is A { function foo() public virtual override returns (string memory) { return "B"; } } contract C is A { function foo() public virtual override returns (string memory) { return "C"; } } // Order matters! contract D is B, C { // C3 linearization: D -> C -> B -> A // foo() will return "C" because C is rightmost function foo() public override(B, C) returns (string memory) { return super.foo(); //