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:
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:
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):
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+):
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:
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:
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:
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
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;
}
}