Smart Contract Security Audit: Comprehensive Methodology
Executive Summary
This technical guide provides a comprehensive methodology for auditing smart contract security, covering vulnerability identification, testing techniques, and remediation strategies.
1. Audit Preparation
1.1 Scope Definition
interface AuditScope {
contracts: string[];
functions: string[];
externalDependencies: string[];
testCoverage: number;
auditTimeline: {
startDate: Date;
endDate: Date;
};
}
function defineAuditScope(
targetContracts: string[],
businessLogic: string[]
): AuditScope {
return {
contracts: targetContracts,
functions: extractFunctions(targetContracts),
externalDependencies: identifyDependencies(targetContracts),
testCoverage: calculateTestCoverage(targetContracts),
auditTimeline: {
startDate: new Date(),
endDate: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000) // 30 days
}
};
}
1.2 Tool Setup
# Install audit tools
npm install -g @openzeppelin/contracts
npm install -g hardhat
npm install -g @nomiclabs/hardhat-ethers
npm install -g @nomiclabs/hardhat-waffle
npm install -g ethereum-waffle
npm install -g chai
# Install security tools
npm install -g slither-analyzer
npm install -g mythril
npm install -g echidna
2. Static Analysis
2.1 Slither Analysis
from slither import Slither
def run_slither_analysis(contract_path: str):
slither = Slither(contract_path)
# Detect common vulnerabilities
vulnerabilities = {
'reentrancy': slither.detectors.Reentrancy,
'unchecked_calls': slither.detectors.UncheckedCall,
'arithmetic_overflow': slither.detectors.ArithmeticOverflow,
'access_control': slither.detectors.AccessControl
}
results = {}
for vuln_name, detector in vulnerabilities.items():
findings = detector.detect(slither)
results[vuln_name] = [str(finding) for finding in findings]
return results
2.2 Mythril Symbolic Execution
# Run Mythril analysis
mythril analyze contract.sol --solc-json mythril.config.json
# Configuration file
{
"remappings": ["@openzeppelin/=node_modules/@openzeppelin/"],
"optimizer": {
"enabled": true,
"runs": 200
},
"evmVersion": "london"
}
3. Common Vulnerabilities
3.1 Reentrancy Attacks
// VULNERABLE CODE
contract VulnerableBank {
mapping(address => uint256) public balances;
function withdraw(uint256 amount) public {
require(balances[msg.sender] >= amount);
// External call before state update - VULNERABLE
(bool success,) = msg.sender.call{value: amount}("");
require(success);
balances[msg.sender] -= amount;
}
}
// SECURE CODE
contract SecureBank {
mapping(address => uint256) public balances;
bool private locked;
modifier noReentrancy() {
require(!locked, "Reentrant call");
locked = true;
_;
locked = false;
}
function withdraw(uint256 amount) public noReentrancy {
require(balances[msg.sender] >= amount);
balances[msg.sender] -= amount;
// State update before external call
(bool success,) = msg.sender.call{value: amount}("");
require(success);
}
}
3.2 Integer Overflow/Underflow
// VULNERABLE CODE (Solidity < 0.8.0)
contract VulnerableMath {
mapping(address => uint256) public balances;
function transfer(address to, uint256 amount) public {
require(balances[msg.sender] >= amount);
balances[msg.sender] -= amount; // Can underflow
balances[to] += amount; // Can overflow
}
}
// SECURE CODE
contract SecureMath {
using SafeMath for uint256;
mapping(address => uint256) public balances;
function transfer(address to, uint256 amount) public {
require(balances[msg.sender] >= amount);
balances[msg.sender] = balances[msg.sender].sub(amount);
balances[to] = balances[to].add(amount);
}
}
// MODERN SECURE CODE (Solidity >= 0.8.0)
contract ModernSecureMath {
mapping(address => uint256) public balances;
function transfer(address to, uint256 amount) public {
require(balances[msg.sender] >= amount);
// Built-in overflow protection
balances[msg.sender] -= amount;
balances[to] += amount;
}
}
3.3 Access Control Issues
// VULNERABLE CODE
contract VulnerableAccess {
address public owner;
function sensitiveFunction() public {
// Anyone can call this!
// Missing access control
}
}
// SECURE CODE
contract SecureAccess {
address public owner;
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "Not authorized");
_;
}
function sensitiveFunction() public onlyOwner {
// Only owner can call
}
function transferOwnership(address newOwner) public onlyOwner {
require(newOwner != address(0), "Invalid owner");
owner = newOwner;
}
}
4. Dynamic Analysis
4.1 Fuzz Testing with Echidna
// Echidna test file
contract EchidnaTest {
Token token;
constructor() {
token = new Token();
}
// Property: Total supply should never decrease
function testTotalSupply() public {
uint256 initialSupply = token.totalSupply();
// Perform some operations
assert(token.totalSupply() >= initialSupply);
}
// Property: Balance should not exceed total supply
function testBalanceInvariant() public {
address randomUser = address(uint160(uint256(keccak256(abi.encodePacked(block.timestamp)))));
assert(token.balanceOf(randomUser) <= token.totalSupply());
}
}
# Run Echidna fuzzing
echidna-test contract.sol --contract EchidnaTest --config echidna.config.yaml
4.2 Unit Testing
import { expect } from "chai";
import { ethers } from "hardhat";
describe("Token Contract", function () {
let token: any;
let owner: any;
let addr1: any;
let addr2: any;
beforeEach(async function () {
[owner, addr1, addr2] = await ethers.getSigners();
const Token = await ethers.getContractFactory("Token");
token = await Token.deploy();
await token.deployed();
});
describe("Transfer", function () {
it("Should transfer tokens between accounts", async function () {
await token.transfer(addr1.address, 50);
expect(await token.balanceOf(addr1.address)).to.equal(50);
});
it("Should fail if sender doesn't have enough tokens", async function () {
await expect(
token.connect(addr1).transfer(owner.address, 1)
).to.be.revertedWith("Insufficient balance");
});
it("Should update balances after transfer", async function () {
const initialOwnerBalance = await token.balanceOf(owner.address);
await token.transfer(addr1.address, 100);
await token.transfer(addr2.address, 50);
expect(await token.balanceOf(owner.address)).to.equal(initialOwnerBalance.sub(150));
expect(await token.balanceOf(addr1.address)).to.equal(100);
expect(await token.balanceOf(addr2.address)).to.equal(50);
});
});
});
5. Formal Verification
5.1 Certora Formal Verification
// Certora specification
methods {
function balanceOf(address) external returns (uint256);
function transfer(address,uint256) external;
function totalSupply() external returns (uint256);
}
// Total supply should remain constant
invariant totalSupplyConstant()
totalSupply() == 1000000; // Assuming initial supply
// Balance should never be negative
rule balanceNeverNegative(address user) {
require balanceOf(user) >= 0;
// This should hold for all operations
}
// Transfer should preserve total supply
rule transferPreservesTotalSupply(address sender, address receiver, uint256 amount) {
uint256 totalBefore = totalSupply();
transfer(sender, receiver, amount);
uint256 totalAfter = totalSupply();
assert totalBefore == totalAfter;
}
5.2 Runtime Verification
contract RuntimeVerification {
mapping(bytes32 => bool) public invariants;
modifier checkInvariant(bytes32 invariantId) {
_;
require(invariants[invariantId], "Invariant violated");
}
function setInvariant(bytes32 invariantId, bool value) external {
invariants[invariantId] = value;
}
function verifyBalanceInvariant(address token) external view returns (bool) {
// Custom invariant checking logic
return true; // Placeholder
}
}
6. Gas Optimization Audit
6.1 Gas Usage Analysis
async function analyzeGasUsage(contract: any, methodName: string, ...args: any[]) {
const tx = await contract.populateTransaction[methodName](...args);
const gasEstimate = await ethers.provider.estimateGas(tx);
console.log(`Gas estimate for ${methodName}: ${gasEstimate.toString()}`);
// Check against gas limit
const blockGasLimit = await ethers.provider.getBlock("latest").then(b => b.gasLimit);
if (gasEstimate.gt(blockGasLimit)) {
console.warn(`Gas estimate exceeds block gas limit!`);
}
return gasEstimate;
}
6.2 Optimization Techniques
// OPTIMIZED CODE
contract GasOptimized {
mapping(address => uint256) public balances;
mapping(address => mapping(address => uint256)) public allowances;
// Use uint256 for storage slots
uint256 public totalSupply;
// Cache storage variables
function transferOptimized(address to, uint256 amount) external {
uint256 senderBalance = balances[msg.sender];
require(senderBalance >= amount, "Insufficient balance");
// Update balances in one transaction
balances[msg.sender] = senderBalance - amount;
balances[to] += amount;
emit Transfer(msg.sender, to, amount);
}
// Use events instead of storage for historical data
event Transfer(address indexed from, address indexed to, uint256 value);
}
7. Dependency Analysis
7.1 External Contract Risks
class DependencyAnalyzer {
async analyzeDependencies(contractAddress: string): Promise<DependencyReport> {
const bytecode = await ethers.provider.getCode(contractAddress);
const dependencies = await this.extractDependencies(bytecode);
const report: DependencyReport = {
externalCalls: dependencies.externalCalls,
libraryUsage: dependencies.libraries,
risks: []
};
// Analyze risks
for (const call of dependencies.externalCalls) {
if (this.isRiskyCall(call)) {
report.risks.push({
type: 'Risky External Call',
description: `Call to ${call.contract} may be vulnerable`,
severity: 'High'
});
}
}
return report;
}
private isRiskyCall(call: ExternalCall): boolean {
// Check if call is to known vulnerable contract
// Check if call uses deprecated patterns
return false; // Placeholder
}
}
8. Audit Report Generation
8.1 Vulnerability Classification
enum Severity {
CRITICAL = 'Critical',
HIGH = 'High',
MEDIUM = 'Medium',
LOW = 'Low',
INFO = 'Info'
}
interface Vulnerability {
id: string;
title: string;
description: string;
severity: Severity;
location: {
file: string;
line: number;
function?: string;
};
impact: string;
recommendation: string;
cwe?: string; // Common Weakness Enumeration
}
8.2 Report Generation
class AuditReportGenerator {
generateReport(findings: Vulnerability[]): AuditReport {
const report: AuditReport = {
summary: this.generateSummary(findings),
vulnerabilities: findings,
recommendations: this.generateRecommendations(findings),
codeQuality: this.assessCodeQuality(findings),
testCoverage: this.calculateTestCoverage(),
timestamp: new Date().toISOString()
};
return report;
}
private generateSummary(findings: Vulnerability[]): ReportSummary {
const severityCount = findings.reduce((acc, finding) => {
acc[finding.severity] = (acc[finding.severity] || 0) + 1;
return acc;
}, {} as Record<Severity, number>);
return {
totalFindings: findings.length,
criticalCount: severityCount[Severity.CRITICAL] || 0,
highCount: severityCount[Severity.HIGH] || 0,
mediumCount: severityCount[Severity.MEDIUM] || 0,
lowCount: severityCount[Severity.LOW] || 0,
infoCount: severityCount[Severity.INFO] || 0
};
}
}
9. Best Practices
9.1 Code Review Checklist
- All external calls use checks-effects-interactions pattern
- Integer operations use SafeMath or Solidity 0.8+ built-ins
- Access control uses OpenZeppelin’s Ownable or AccessControl
- Events emitted for all state changes
- Input validation on all public/external functions
- Reentrancy guards on functions calling external contracts
- Proper error handling and revert messages
9.2 Testing Checklist
- Unit tests for all functions
- Integration tests for contract interactions
- Fuzz testing with Echidna
- Formal verification where applicable
- Gas usage optimization
- Edge case testing
10. Conclusion
Smart contract security audits are essential for identifying and mitigating vulnerabilities before deployment. Following this comprehensive methodology ensures thorough coverage of potential security issues.
References
-
“Smart Contract Security Best Practices” - OpenZeppelin
-
“Ethereum Smart Contract Security Techniques and Tips” - ConsenSys
-
“Common Vulnerabilities in Smart Contracts” - OWASP
-
“Formal Verification of Smart Contracts” - Runtime Verification
Tools and Resources
- Slither: https://github.com/crytic/slither
- Mythril: https://github.com/ConsenSys/mythril
- Echidna: https://github.com/crytic/echidna
- Certora: https://www.certora.com/
Further Reading
- “Mastering Ethereum” by Andreas Antonopoulos
- “Smart Contract Development” by Ben Edgington
- “Blockchain Security” by Andrew Miller