Learning Objectives:

  • Master common smart contract vulnerabilities

  • Understand attack vectors and mitigation strategies

  • Learn security patterns and best practices

  • Analyze real-world attacks and lessons


4.4.1: Security Overview

The Importance of Security:

t
Security Considerations:

1. Financial Impact:
   - Millions of dollars at risk
   - Direct financial loss
   - Reputation damage

2. Immutability:
   - Contracts cannot be changed
   - Bugs are permanent
   - Upgrades require careful design

3. Transparency:
   - Code is public
   - Anyone can audit
   - Attackers can study code

4. Complexity:
   - Smart contracts are complex
   - Interactions are complex
   - Easy to overlook vulnerabilities

Common Attack Vectors:

 
 
Attack Type Impact Severity
Reentrancy Fund theft Critical
Integer Overflow Unexpected behavior High
Access Control Unauthorized access Critical
Front-running Manipulation Medium
Denial of Service Contract unusable High
Oracle Manipulation Incorrect data High
Phishing Private key theft Critical
Flash Loans Price manipulation High

4.4.2: Reentrancy Attacks

The Vulnerability:

Vulnerable Contract:

contract VulnerableBank {
    mapping(address => uint256) public balances;
    
    function withdraw(uint256 amount) public {
        require(balances[msg.sender] >= amount, "Insufficient balance");
        
        // Vulnerability: Call before state update
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");
        
        balances[msg.sender] -= amount;  // State update after call
    }
}

// Attacker Contract
contract Attacker {
    VulnerableBank public bank;
    
    constructor(address _bank) {
        bank = VulnerableBank(_bank);
    }
    
    fallback() external payable {
        if (address(bank).balance >= 1 ether) {
            bank.withdraw(1 ether);  // Reentrant call
        }
    }
    
    function attack() public {
        bank.withdraw(1 ether);
    }
}

Mitigation: Checks-Effects-Interactions:

text
Secure Contract:

contract SecureBank {
    mapping(address => uint256) public balances;
    
    function withdraw(uint256 amount) public {
        require(balances[msg.sender] >= amount, "Insufficient balance");
        
        // Effect: Update state first
        balances[msg.sender] -= amount;
        
        // Interaction: External call after state update
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");
    }
}

Reentrancy Guard:

Using ReentrancyGuard:

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract SecureContract is ReentrancyGuard {
    mapping(address => uint256) public balances;
    
    function withdraw(uint256 amount) public nonReentrant {
        require(balances[msg.sender] >= amount, "Insufficient balance");
        balances[msg.sender] -= amount;
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");
    }
}

4.4.3: Integer Overflow/Underflow

The Vulnerability (Pre-0.8.0):

text
Vulnerable Contract (Solidity <0.8.0):

contract OverflowExample {
    uint8 public maxValue = 255;
    
    function increment() public {
        // Can cause overflow: 255 + 1 = 0
        maxValue++;
    }
    
    function decrement() public {
        // Can cause underflow: 0 - 1 = 255
        maxValue--;
    }
}

Mitigation (Solidity 0.8.0+):

text
Solidity 0.8.0+ automatically checks for overflow/underflow.

contract SecureContract {
    uint256 public value = type(uint256).max;
    
    function increment() public {
        // This will revert automatically
        value++;
    }
    
    function safeIncrement() public {
        require(value < type(uint256).max, "Overflow");
        value++;
    }
}

// Using unchecked for safe operations
function uncheckedExample(uint256 a, uint256 b) public pure returns (uint256) {
    unchecked {
        // No overflow check (cheaper gas)
        return a + b;
    }
}

4.4.4: Access Control

Basic Access Control:

 
Access Control Vulnerabilities:

contract InsecureContract {
    address public owner;
    
    function setOwner(address _newOwner) public {
        owner = _newOwner;  // Anyone can change owner!
    }
    
    function withdraw() public {
        require(msg.sender == owner, "Not owner");
        // Withdraw logic
    }
}

Secure Access Control:

Using OpenZeppelin Ownable:

import "@openzeppelin/contracts/access/Ownable.sol";

contract SecureContract is Ownable {
    function withdraw() public onlyOwner {
        // Only owner can call
    }
}

// Role-Based Access Control
import "@openzeppelin/contracts/access/AccessControl.sol";

contract RBAC is AccessControl {
    bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
    bytes32 public constant USER_ROLE = keccak256("USER_ROLE");
    
    constructor() {
        _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
        _grantRole(ADMIN_ROLE, msg.sender);
    }
    
    function adminOnly() public onlyRole(ADMIN_ROLE) {
        // Only admins can call
    }
    
    function userOnly() public onlyRole(USER_ROLE) {
        // Only users with USER_ROLE can call
    }
}

4.4.5: Front-Running

The Vulnerability:

Vulnerable to Front-Running:

contract FrontRunExample {
    mapping(bytes32 => bool) public secrets;
    
    // Bad: Reveals secret before claiming
    function submitSecret(bytes32 hash) public {
        secrets[hash] = true;
    }
    
    function claimSecret(bytes32 hash, string memory secret) public {
        require(secrets[hash], "Not submitted");
        require(hash == keccak256(abi.encodePacked(secret)), "Invalid secret");
        // Claim logic
    }
}

Mitigation – Commit-Reveal:

 
Commit-Reveal Pattern:

contract CommitReveal {
    struct Commit {
        bytes32 commitment;
        uint256 timestamp;
        bool revealed;
    }
    
    mapping(address => Commit) public commits;
    
    function commit(bytes32 _commitment) public {
        commits[msg.sender] = Commit(_commitment, block.timestamp, false);
    }
    
    function reveal(string memory _secret) public {
        Commit storage commit = commits[msg.sender];
        require(commit.timestamp > 0, "No commit");
        require(!commit.revealed, "Already revealed");
        
        bytes32 computed = keccak256(abi.encodePacked(_secret, msg.sender));
        require(computed == commit.commitment, "Invalid secret");
        
        commit.revealed = true;
        // Reveal logic
    }
}

4.4.6: Denial of Service (DoS)

Common DoS Vectors:

DoS Vulnerability - Unbounded Loop:

contract DoSVulnerable {
    address[] public users;
    
    function distribute() public {
        // Can run out of gas
        for (uint256 i = 0; i < users.length; i++) {
            users[i].call{value: 1 ether}("");
        }
    }
}

Mitigation:

DoS Prevention:

contract DoSSecure {
    mapping(address => uint256) public pendingWithdrawals;
    address[] public users;
    
    function withdraw() public {
        uint256 amount = pendingWithdrawals[msg.sender];
        require(amount > 0, "No balance");
        pendingWithdrawals[msg.sender] = 0;
        payable(msg.sender).transfer(amount);
    }
    
    // If users need to be processed individually
    function processUsers(address[] memory _users) public {
        for (uint256 i = 0; i < _users.length; i++) {
            // Process each user
            processUser(_users[i]);
        }
    }
}

4.4.7: Oracle Manipulation

The Vulnerability:

 
Oracle Manipulation Example:

contract VulnerablePriceOracle {
    function getPrice() public view returns (uint256) {
        // Vulnerable to manipulation
        return UniswapV2Pair(0x123...).getReserves();
    }
}

Mitigation:

text
Secure Oracle Patterns:

1. Multiple Oracle Sources:
contract SecureOracle {
    address[] public oracles;
    
    function getPrice() public view returns (uint256) {
        uint256 total = 0;
        for (uint256 i = 0; i < oracles.length; i++) {
            total += IOracle(oracles[i]).getPrice();
        }
        return total / oracles.length;
    }
}

2. Time-Weighted Average Price (TWAP):
// Use Uniswap V3 TWAP
function getTWAP() public view returns (uint256) {
    // Chainlink TWAP or custom implementation
}

3. Use Chainlink Price Feeds:
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";

contract ChainlinkOracle {
    AggregatorV3Interface internal priceFeed;
    
    constructor(address _priceFeed) {
        priceFeed = AggregatorV3Interface(_priceFeed);
    }
    
    function getPrice() public view returns (uint256) {
        (, int256 price, , , ) = priceFeed.latestRoundData();
        require(price > 0, "Invalid price");
        return uint256(price);
    }
}

4.4.8: Flash Loan Attacks

Understanding Flash Loans:

Flash loans allow borrowing without collateral, as long as the loan is repaid within the same transaction.

 
Flash Loan Attack Pattern:

1. Flash loan borrowed
2. Manipulate price
3. Profit from manipulation
4. Repay loan
5. Keep profit

Mitigation:

text
Flash Loan Protection:

1. Time-Weighted Average Price (TWAP)
2. Check before/after price
3. Use multiple oracles
4. Implement circuit breakers

contract FlashLoanProtection {
    uint256 private _lastPrice;
    uint256 private _priceChangeLimit = 5;  // 5% limit
    
    function executeOperation(uint256 amount, uint256 fee) external {
        uint256 currentPrice = getPrice();
        
        // Check price manipulation
        require(
            _lastPrice == 0 || 
            currentPrice <= _lastPrice * (100 + _priceChangeLimit) / 100 &&
            currentPrice >= _lastPrice * (100 - _priceChangeLimit) / 100,
            "Price manipulation detected"
        );
        
        _lastPrice = currentPrice;
        // Execute operation
    }
}

4.4.9: Security Tools and Auditing

Security Tools:

 
 
Tool Purpose Description
Slither Static Analysis Detects vulnerabilities
Mythril Security Analysis Finds security bugs
Oyente Security Analysis Checks for vulnerabilities
Securify Security Analysis Automated verification
Echidna Fuzzing Property-based testing
Foundry Testing Fuzzing, differential testing

Audit Process:

text
Smart Contract Audit Process:

1. Planning:
   - Define scope
   - Understand functionality
   - Review documentation

2. Manual Review:
   - Code review
   - Architecture review
   - Logic review

3. Automated Analysis:
   - Run security tools
   - Static analysis
   - Dynamic analysis

4. Testing:
   - Unit tests
   - Integration tests
   - Fuzzing

5. Reporting:
   - List vulnerabilities
   - Prioritize fixes
   - Provide recommendations

6. Remediation:
   - Fix vulnerabilities
   - Re-audit fixes
   - Final approval

4.4.10: Real-World Attacks

Notable Attacks:

 
 
Attack Year Loss Vulnerability
The DAO 2016 $60M Reentrancy
Parity Wallet 2017 $30M Access Control
Parity Multi-Sig 2017 $150M Library Vulnerability
bZx 2020 $1M Price Oracle
Harvest Finance 2020 $24M Flash Loan
BadgerDAO 2021 $120M Front-end Attack
Poly Network 2021 $600M Cross-chain Vulnerability
Wormhole 2022 $320M Signature Verification

Lessons Learned:

Key Lessons:

1. Always follow checks-effects-interactions
2. Use battle-tested libraries (OpenZeppelin)
3. Multiple independent audits
4. Bug bounty programs
5. Gradual deployment
6. Circuit breakers for critical functions
7. Regular security updates
8. Monitor for suspicious activity

ADDITIONAL DEEP TECHNICAL NOTES:

1. Security Checklist

text
Pre-Deployment Checklist:

☐ Code Audit (Multiple firms)
☐ Test Coverage > 95%
☐ Static Analysis (Slither, Mythril)
☐ Fuzzing (Echidna, Foundry)
☐ Formal Verification (if possible)
☐ Bug Bounty Program
☐ Documentation Complete
☐ Emergency Procedures
☐ Upgrade Path (if needed)
☐ Monitoring Setup

2. SafeMath (Pre-0.8.0)

SafeMath Implementation:

library SafeMath {
    function add(uint256 a, uint256 b) internal pure returns (uint256) {
        uint256 c = a + b;
        require(c >= a, "Addition overflow");
        return c;
    }
    
    function sub(uint256 a, uint256 b) internal pure returns (uint256) {
        require(a >= b, "Subtraction underflow");
        return a - b;
    }
    
    function mul(uint256 a, uint256 b) internal pure returns (uint256) {
        if (a == 0) return 0;
        uint256 c = a * b;
        require(c / a == b, "Multiplication overflow");
        return c;
    }
    
    function div(uint256 a, uint256 b) internal pure returns (uint256) {
        require(b > 0, "Division by zero");
        return a / b;
    }
}