Learning Objectives:
-
Master comprehensive gas optimization techniques and strategies
-
Understand the audit process and security assessment methodologies
-
Learn about security tools, vulnerability detection, and remediation
-
Analyze real-world vulnerabilities and lessons learned
4.8.1: Understanding Gas – The Cost of Computation
What is Gas and Why Does It Matter?
Gas is the unit that measures the computational work required to execute operations on Ethereum. Every operation consumes gas, and users pay for gas in ETH. Optimizing gas is critical for both user experience and cost efficiency.
Gas Fundamentals: Gas = Computational Work × Storage Cost Key Concepts: 1. Gas Limit: Max gas user is willing to spend 2. Gas Price: Price per gas unit (in Gwei) 3. Transaction Fee = Gas Used × Gas Price Gas Cost Components: - Computation: CPU cycles - Storage: Reading/writing to blockchain - Memory: Temporary data - Code Execution: Running contract logic Why Gas Optimization Matters: 1. User Experience: - Lower fees = More users - Competitive advantage - Better accessibility 2. Business Economics: - Lower operating costs - Higher profit margins - More competitive pricing 3. Network Efficiency: - More transactions per block - Reduced congestion - Better scalability 4. Contract Design: - Better architecture - Cleaner code - Fewer vulnerabilities
Gas Cost Breakdown:
| Operation | Gas Cost | Description |
|---|---|---|
| Storage Write (SSTORE) | 20,000 | Writing to storage |
| Storage Read (SLOAD) | 2,100 | Reading from storage |
| Memory Write/Read | 3 | Cheap operations |
| External Call (CALL) | 10,000 | Calling another contract |
| Internal Call | ~50 | Calling internal function |
| Arithmetic (ADD/MUL) | 3-5 | Math operations |
| Event Emission (LOG) | 375 + 375/topic | Emitting events |
| Contract Creation | 32,000 | Deploying contract |
| Self-Destruct | 5,000 | Destroying contract |
4.8.2: Storage Optimization Techniques
Packing Variables – The Most Important Optimization
Storage is the most expensive operation in Ethereum. Packing variables reduces the number of storage slots used.
Storage Packing Principles:
1. Each Storage Slot = 32 Bytes (256 bits)
2. Variables are packed into slots when possible
3. Order matters for packing efficiency
Packing Rules:
- Smaller types can be packed together
- Variables are packed in declaration order
- Structs are packed like individual variables
Example 1: Unoptimized (3 slots)
contract Unoptimized {
uint256 a; // Slot 0 (32 bytes)
uint256 b; // Slot 1 (32 bytes)
uint256 c; // Slot 2 (32 bytes)
}
// Gas Cost: 3 storage writes = 60,000 gas
Example 2: Optimized (2 slots)
contract Optimized {
uint128 a; // Slot 0 (16 bytes)
uint128 b; // Slot 0 (16 bytes) - packed
uint256 c; // Slot 1 (32 bytes)
}
// Gas Cost: 2 storage writes = 40,000 gas
// Saving: 20,000 gas per write
Detailed Packing Examples:
Packing Multiple Variables:
// Unoptimized: 4 slots
contract Unoptimized {
uint256 a; // Slot 0
uint256 b; // Slot 1
uint128 c; // Slot 2 (16 bytes used)
uint128 d; // Slot 2 (16 bytes used) - actually packed here!
}
// Optimized: 2 slots
contract Optimized {
uint128 a; // Slot 0 (16 bytes)
uint128 b; // Slot 0 (16 bytes) - packed
uint128 c; // Slot 1 (16 bytes)
uint128 d; // Slot 1 (16 bytes) - packed
}
// Packing by type grouping
contract Optimized {
uint128 a; // Slot 0 (16 bytes)
uint128 b; // Slot 0 (16 bytes)
uint64 c; // Slot 1 (8 bytes)
uint64 d; // Slot 1 (8 bytes) - total 16 bytes
uint64 e; // Slot 1 (8 bytes) - now 24 bytes
uint64 f; // Slot 1 (8 bytes) - full 32 bytes!
}
State Variable Ordering:
Ordering Matters!
// Bad: 3 slots
contract Bad {
uint256 a; // Slot 0
uint128 b; // Slot 1 (16 bytes)
uint128 c; // Slot 1 (16 bytes) - packed with b
uint128 d; // Slot 2 (16 bytes)
}
// Good: 2 slots
contract Good {
uint128 b; // Slot 0 (16 bytes)
uint128 c; // Slot 0 (16 bytes) - packed
uint128 d; // Slot 1 (16 bytes)
uint256 a; // Slot 2 - can't pack, but better ordering
}
Using Immutable Variables
Immutable Variables:
Benefits:
- Not stored in storage (saves gas)
- Set at construction time
- Cannot be changed
// Without immutable
contract WithoutImmutable {
address public owner; // Storage write: 20,000 gas
constructor() {
owner = msg.sender;
}
}
// With immutable
contract WithImmutable {
address public immutable owner; // No storage write
constructor() {
owner = msg.sender; // Set once
}
}
// Reading immutable vs storage
function getOwner() public view returns (address) {
return owner; // Reading immutable is cheaper than storage
}
Using Constant Variables
Constants:
Benefits:
- Not stored in storage
- Replaced at compile time
- Cheapest access
// Without constant
contract WithoutConstant {
uint256 public maxSupply = 1000000; // Storage write
}
// With constant
contract WithConstant {
uint256 public constant MAX_SUPPLY = 1000000; // No storage
}
// Accessing constant
function check() public pure {
// MAX_SUPPLY is replaced with 1000000 at compile time
// No storage read!
}
4.8.3: Function Optimization Techniques
Function Visibility
Visibility Matters!
External vs Public:
- External: Cheaper (calldata)
- Public: More expensive (memory copy)
// Bad: Public function
function getData(uint256[] memory data) public view returns (uint256) {
// data is in memory (more expensive)
return data.length;
}
// Good: External function
function getData(uint256[] calldata data) external view returns (uint256) {
// data is in calldata (cheaper)
return data.length;
}
External vs Internal:
- External: More expensive (call overhead)
- Internal: Cheaper (direct jump)
// Bad: External for internal use
function _calculate(uint256 a) external pure returns (uint256) {
return a * 2;
}
// Good: Internal for internal use
function _calculate(uint256 a) internal pure returns (uint256) {
return a * 2;
}
Using View/Pure Modifiers
View and Pure Functions:
View: Reads state, no modification
Pure: No state access, no modification
Benefits:
- No gas for state reads
- No gas for execution
- Cheaper calls
// Without view
function getValue() public returns (uint256) {
return value; // Gas cost: 2,100 (SLOAD)
}
// With view
function getValue() public view returns (uint256) {
return value; // Gas cost: 0 (free)
}
// With pure
function add(uint256 a, uint256 b) public pure returns (uint256) {
return a + b; // Gas cost: minimal
}
Calldata vs Memory
Calldata vs Memory:
- Calldata: Read-only, cheaper
- Memory: Read-write, more expensive
// Bad: Memory for read-only
function process(uint256[] memory data) external {
// data is copied to memory (expensive)
for (uint256 i = 0; i < data.length; i++) {
// Process
}
}
// Good: Calldata for read-only
function process(uint256[] calldata data) external {
// data stays in calldata (cheap)
for (uint256 i = 0; i < data.length; i++) {
// Process
}
}
Short-Circuit Evaluation
Short-Circuit Evaluation: // Bad: Both conditions always evaluated require(condition1 && condition2, "Failed"); // condition1 AND condition2 both checked // Good: Cheaper condition first require(!paused && condition2, "Failed"); // Checks paused first (cheaper) // Better: Separate checks require(!paused, "Paused"); require(condition2, "Failed"); // Clearer and cheaper in some cases
4.8.4: Loop Optimization
Loop Efficiency
Loop Optimization Techniques:
1. Cache Array Length:
// Bad: Reads length each iteration
for (uint i = 0; i < array.length; i++) {
// Each iteration reads array.length (SLOAD)
}
// Good: Cache length
uint length = array.length;
for (uint i = 0; i < length; i++) {
// Only one SLOAD
}
2. Use Unchecked for Increment:
// Bad: Overflow check each iteration
for (uint i = 0; i < length; i++) {
// i++ has overflow check
}
// Good: Unchecked increment
for (uint i = 0; i < length; ) {
// Body
unchecked { i++; }
}
3. Avoid Arrays in Loops:
// Bad: Array access in loop
for (uint i = 0; i < length; i++) {
uint value = array[i]; // Array access
}
// Good: Use storage reference
uint[] storage arr = array;
for (uint i = 0; i < length; i++) {
uint value = arr[i];
}
Loop Optimization Examples:
// Bad: Unbounded loop
function distributeRewards() public {
for (uint i = 0; i < users.length; i++) {
users[i].transfer(rewards[users[i]]);
}
}
// Gas cost: O(n) where n = users.length
// Risk: Gas limit exceeded
// Good: Withdrawal pattern
mapping(address => uint256) pendingRewards;
function claimReward() public {
uint amount = pendingRewards[msg.sender];
require(amount > 0, "No reward");
pendingRewards[msg.sender] = 0;
payable(msg.sender).transfer(amount);
}
// Gas cost: Fixed per user
// No unbounded loops
4.8.5: Memory and Calldata Optimization
Memory Management:
Memory Optimization:
1. Reuse Memory Arrays:
// Bad: New memory allocation
function process(uint256[] memory data) external {
uint256[] memory temp = data; // Copies data
// Process temp
}
// Good: Work in-place
function process(uint256[] memory data) external {
// Work directly with data
for (uint i = 0; i < data.length; i++) {
data[i] = data[i] * 2;
}
}
2. Avoid Memory Copies:
// Bad: Copying to memory
function process(string memory data) external {
// data is already in memory
}
// Good: Use calldata for read-only
function process(string calldata data) external {
// data stays in calldata
}
4.8.6: Smart Contract Auditing – The Complete Process
What is a Smart Contract Audit?
A smart contract audit is a comprehensive security review of contract code to identify vulnerabilities, ensure correctness, and validate security.
Audit Process Overview: ┌─────────────────────────────────────────────────────────────────────┐ │ Audit Lifecycle │ │ │ │ Phase 1: Scoping │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ • Define audit scope │ │ │ │ • Understand functionality │ │ │ │ • Review documentation │ │ │ │ • Identify critical components │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ │ │ Phase 2: Initial Review │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ • Code walkthrough │ │ │ │ • Architecture review │ │ │ │ • Design review │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ │ │ Phase 3: Automated Analysis │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ • Static analysis (Slither) │ │ │ │ • Security scanning (Mythril) │ │ │ │ • Fuzzing (Echidna) │ │ │ │ • Symbolic execution │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ │ │ Phase 4: Manual Review │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ • Line-by-line review │ │ │ │ • Logical analysis │ │ │ │ • Security pattern check │ │ │ │ • Edge case analysis │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ │ │ Phase 5: Testing │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ • Unit tests │ │ │ │ • Integration tests │ │ │ │ • Fuzzing │ │ │ │ • Invariant checking │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ │ │ Phase 6: Reporting │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ • Vulnerability list │ │ │ │ • Risk assessment │ │ │ │ • Recommendations │ │ │ │ • Remediation validation │ │ │ └─────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────┘
Audit Scope and Requirements:
Audit Preparation: Pre-Audit Checklist: ☐ Code complete and documented ☐ All tests passing ☐ Documentation includes: - Architecture overview - Function descriptions - Security assumptions - Upgrade paths ☐ Dependencies documented ☐ Deployment scripts ready ☐ Emergency procedures defined Audit Timeline: - 2-4 weeks for medium contracts - 4-8 weeks for large contracts - 1-2 weeks for re-audits Audit Team Requirements: - Multiple auditors - Various experience levels - Different specializations
4.8.7: Security Tools – Comprehensive Guide
Slither – Static Analysis
Slither Overview: Purpose: Static analysis for security vulnerabilities Installation: pip3 install slither-analyzer Basic Usage: slither contracts/ Key Detectors: - Reentrancy: Detects reentrancy vulnerabilities - Unchecked Return: Unchecked external calls - Integer Overflow: Arithmetic overflow - Access Control: Missing access controls - Dead Code: Unused functions/variables Advanced Usage: slither contracts/ --print human-summary slither contracts/ --detect-all slither contracts/ --exclude-dependencies Custom Detectors: slither --detect reentrancy,unchecked-call
Mythril – Security Analysis
Mythril Overview: Purpose: Deep security analysis using symbolic execution Installation: pip3 install mythril Basic Usage: myth analyze 0x123... Features: - Reentrancy detection - Integer overflow detection - Unauthorized calls - Call stack depth - EVM bytecode analysis Advanced Usage: myth analyze contracts/ --all myth analyze contracts/ --gas myth analyze contracts/ --max-depth 30
Echidna – Fuzzing
Echidna Overview: Purpose: Property-based testing and fuzzing Installation: cargo install echidna Basic Usage: echidna test.sol Features: - Random input generation - Invariant checking - Stateful testing - Property verification Advanced Usage: echidna test.sol --config config.yaml echidna test.sol --test-limit 100000
Hardhat Security Plugins:
Hardhat Plugins: 1. Hardhat Gas Reporter: npm install hardhat-gas-reporter // Reports gas usage per function 2. Hardhat Coverage: npm install solidity-coverage // Reports test coverage 3. Hardhat Verify: npm install @nomiclabs/hardhat-etherscan // Verifies contracts on Etherscan 4. Hardhat Security: npm install @nomiclabs/hardhat-ganache // Security testing with Ganache
4.8.8: Common Vulnerabilities and Remediation
Vulnerability Matrix:
| Vulnerability | Severity | Detection | Prevention |
|---|---|---|---|
| Reentrancy | Critical | Slither, Mythril | Checks-effects-interactions |
| Access Control | Critical | Manual review | Role-based access |
| Integer Overflow | High | Slither, Mythril | SafeMath or 0.8.0+ |
| Front-Running | Medium | Manual review | Commit-reveal |
| Denial of Service | High | Echidna | Avoid loops |
| Oracle Manipulation | High | Manual review | Multiple oracles |
| Logic Errors | Critical | All tools | Comprehensive testing |
| Gas Limit | Medium | Echidna | Optimization |
Detailed Vulnerability Remediation:
1. Reentrancy Remediation:
// Before (Vulnerable)
function withdraw(uint256 amount) public {
require(balances[msg.sender] >= amount);
(bool success, ) = msg.sender.call{value: amount}("");
require(success);
balances[msg.sender] -= amount;
}
// After (Secure)
function withdraw(uint256 amount) public {
require(balances[msg.sender] >= amount);
balances[msg.sender] -= amount;
(bool success, ) = msg.sender.call{value: amount}("");
require(success);
}
2. Access Control Remediation:
// Before (Vulnerable)
function setOwner(address newOwner) public {
owner = newOwner;
}
// After (Secure)
function setOwner(address newOwner) public onlyOwner {
owner = newOwner;
}
3. Integer Overflow Remediation:
// Before (Vulnerable - pre-0.8.0)
function add(uint256 a, uint256 b) public pure returns (uint256) {
return a + b; // Could overflow
}
// After (Secure - 0.8.0+)
function add(uint256 a, uint256 b) public pure returns (uint256) {
return a + b; // Built-in overflow protection
}
4.8.9: Real-World Attack Analysis
Case Study 1: The DAO Attack (2016)
The DAO Attack Analysis:
Vulnerability: Reentrancy
Loss: $60M (3.6M ETH)
Date: June 2016
Attack Vector:
function splitDAO() {
uint256 balance = balances[msg.sender];
// External call BEFORE state update
msg.sender.call.value(balance)();
// State update AFTER call