# 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(); //