back to all skills

smart-contract-auditor

web3v2.0.0

Comprehensive smart contract security auditing skill. Covers static analysis tooling (Slither, Mythril, Aderyn), fuzz testing (Foundry, Echidna), vulnerability detection with Solidity code examples, proxy/upgrade safety, DeFi-specific audit patterns, gas optimization, and structured audit report generation.

copied ✓
openclawclaude-codecursorcodex
0 installsVirusTotal: cleanSource code

Smart Contract Auditor v2.0

1. Tooling Setup

Slither (Static Analysis)

pip3 install slither-analyzer
slither . --filter-paths "node_modules|lib"
slither . --print human-summary
slither . --detect reentrancy-eth,reentrancy-no-eth,arbitrary-send-erc20
slither . --print contract-summary  # function visibility overview

Mythril (Symbolic Execution)

pip3 install mythril
myth analyze contracts/Vault.sol --solv 0.8.20 --execution-timeout 300
myth analyze contracts/Vault.sol --max-depth 30 -o jsonv2

Aderyn (Rust-based Analyzer)

cargo install aderyn
aderyn .  # outputs report.md by default
aderyn . --output aderyn-report.json

Foundry Fuzzing

forge test --fuzz-runs 10000
forge test --fuzz-runs 50000 --match-test testFuzz
forge test --fuzz-seed 42 --fuzz-runs 10000  # reproducible

Foundry fuzz test example:

function testFuzz_withdraw(uint256 amount) public {
    amount = bound(amount, 1, address(vault).balance);
    vault.deposit{value: amount}();
    uint256 pre = address(this).balance;
    vault.withdraw(amount);
    assertEq(address(this).balance, pre + amount);
}

Echidna (Property-Based Fuzzing)

brew install echidna  # or download binary
echidna . --contract VaultEchidna --test-mode assertion --test-limit 50000

Echidna invariant example:

contract VaultEchidna is Vault {
    function echidna_total_balance_matches() public view returns (bool) {
        return address(this).balance >= totalDeposited;
    }
}

2. Vulnerability Checklist

2.1 Reentrancy

Vulnerable:

function withdraw(uint256 amount) external {
    require(balances[msg.sender] >= amount);
    (bool ok, ) = msg.sender.call{value: amount}("");
    require(ok);
    balances[msg.sender] -= amount; // STATE AFTER CALL — reentrancy
}

Fixed (CEI Pattern):

function withdraw(uint256 amount) external nonReentrant {
    require(balances[msg.sender] >= amount);
    balances[msg.sender] -= amount;       // EFFECTS first
    (bool ok, ) = msg.sender.call{value: amount}(""); // INTERACTION last
    require(ok);
}

Cross-function reentrancy: check if any two functions share state and one has an external call before state update.

2.2 Oracle Manipulation / Price Feed Attacks

Vulnerable (spot price):

function getPrice() public view returns (uint256) {
    (uint112 r0, uint112 r1, ) = pair.getReserves();
    return (uint256(r1) * 1e18) / uint256(r0); // manipulable in same tx
}

Fixed (Chainlink + staleness check):

function getPrice() public view returns (uint256) {
    (, int256 answer, , uint256 updatedAt, ) = priceFeed.latestRoundData();
    require(answer > 0, "invalid price");
    require(block.timestamp - updatedAt < 3600, "stale price");
    return uint256(answer);
}

Also consider TWAP for on-chain pricing:

// Uniswap V3 TWAP — use OracleLibrary.consult(pool, twapInterval)

2.3 Flash Loan Attack Vectors

Audit checks:

  • Can any single-tx deposit + action + withdraw exploit state?
  • Are governance votes protected by minimum holding periods?
  • Are liquidity-based calculations snapshottable in one block?

Guard pattern:

mapping(address => uint256) public lastDepositBlock;

function deposit() external {
    lastDepositBlock[msg.sender] = block.number;
    // ...
}

function vote() external {
    require(block.number > lastDepositBlock[msg.sender], "same block");
    // ...
}

2.4 Storage Collisions in Proxies

Problem: Proxy and implementation share storage. Misaligned slots corrupt data.

// Implementation V1
contract V1 {
    uint256 public value;    // slot 0
    address public owner;    // slot 1
}

// Implementation V2 — WRONG: inserted variable shifts slots
contract V2 {
    uint256 public value;    // slot 0
    uint256 public newVar;   // slot 1 — COLLISION with owner!
    address public owner;    // slot 2
}

// Implementation V2 — CORRECT: append only
contract V2 {
    uint256 public value;    // slot 0
    address public owner;    // slot 1
    uint256 public newVar;   // slot 2 — safe, appended
}

Use forge inspect ContractName storage-layout to verify slot alignment between versions.

2.5 Front-Running / Sandwich Attacks / MEV

Vulnerable swap:

function swap(uint256 amountIn) external {
    router.swapExactTokensForTokens(amountIn, 0, path, msg.sender, block.timestamp);
    // amountOutMin = 0 allows sandwich
}

Fixed:

function swap(uint256 amountIn, uint256 minOut, uint256 deadline) external {
    require(block.timestamp <= deadline, "expired");
    router.swapExactTokensForTokens(amountIn, minOut, path, msg.sender, deadline);
}

For sensitive operations, use commit-reveal:

mapping(bytes32 => uint256) public commits;

function commit(bytes32 hash) external { commits[hash] = block.number; }

function reveal(uint256 value, bytes32 salt) external {
    bytes32 h = keccak256(abi.encodePacked(value, salt, msg.sender));
    require(commits[h] > 0 && block.number > commits[h] + 1, "too early");
    delete commits[h];
    _execute(value);
}

2.6 Access Control Issues

Vulnerable (tx.origin):

function withdraw() external {
    require(tx.origin == owner); // phishing attack via malicious contract
}

Fixed:

function withdraw() external {
    require(msg.sender == owner); // or use OpenZeppelin Ownable/AccessControl
}

Check for:

  • Missing access modifiers on admin functions
  • Single-step ownership transfer (use Ownable2Step)
  • DEFAULT_ADMIN_ROLE granted too broadly
  • Functions that should be onlyOwner but are public

2.7 Integer Overflow/Underflow

Pre-0.8.0 (vulnerable):

// Solidity <0.8.0
uint8 balance = 255;
balance += 1; // wraps to 0 silently

// Fix: use SafeMath
balance = balance.add(1); // reverts on overflow

Post-0.8.0: Built-in overflow checks. But unchecked {} blocks bypass them:

unchecked {
    uint8 x = 255;
    x += 1; // wraps to 0 — intentional? Audit this.
}

Audit every unchecked block. Verify the math genuinely cannot overflow.

2.8 Unchecked External Calls

Vulnerable:

payable(to).send(amount); // return value ignored — funds may not arrive
token.transfer(to, amount); // non-standard tokens may return false

Fixed:

(bool ok, ) = payable(to).call{value: amount}("");
require(ok, "ETH transfer failed");

// For ERC20:
SafeERC20.safeTransfer(token, to, amount);

Also check: delegatecall return values, low-level call without length check.

2.9 Denial of Service Patterns

Unbounded loop (gas griefing):

// VULNERABLE: attacker adds thousands of entries
function distributeRewards() external {
    for (uint i = 0; i < recipients.length; i++) {
        token.transfer(recipients[i], rewards[i]); // OOG if array is huge
    }
}

Fixed (pull pattern):

mapping(address => uint256) public pendingRewards;

function claimReward() external {
    uint256 amount = pendingRewards[msg.sender];
    pendingRewards[msg.sender] = 0;
    token.safeTransfer(msg.sender, amount);
}

Other DoS vectors:

  • External call in loop (one revert blocks all)
  • Block gas limit reached via large array iteration
  • Griefing via forced revert in receive() / fallback()

3. Proxy / Upgrade Safety

UUPS vs Transparent Proxy

AspectUUPSTransparent
Upgrade logicIn implementationIn proxy
Gas (user calls)LowerHigher (admin check)
RiskForgetting _authorizeUpgrade = brickedMore complex proxy
RecommendedYes (OpenZeppelin default)Legacy

Initializer Pattern

contract VaultV1 is Initializable, UUPSUpgradeable, OwnableUpgradeable {
    uint256 public fee;

    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() { _disableInitializers(); }

    function initialize(uint256 _fee) external initializer {
        __Ownable_init(msg.sender);
        __UUPSUpgradeable_init();
        fee = _fee;
    }

    function _authorizeUpgrade(address) internal override onlyOwner {}
}

Storage Layout Rules

  1. Never reorder or remove existing storage variables
  2. Never change variable types (uint128 → uint256 changes slot)
  3. Always append new variables after existing ones
  4. Use storage gaps for future-proofing:
uint256[50] private __gap; // reserve 50 slots for future vars
  1. Run forge inspect V1 storage-layout vs forge inspect V2 storage-layout and diff

4. DeFi-Specific Audit

AMM Invariants

  • Constant product: k = reserveA * reserveB must hold after every swap
  • Check for rounding manipulation on small liquidity pools
  • Verify fee calculations don't break invariant
  • LP token mint/burn must be proportional to liquidity added/removed

Lending Protocol Checks

  • Collateral factor bounds (can't be set to manipulative values)
  • Liquidation threshold < collateral factor
  • Interest rate model edge cases (100% utilization)
  • Bad debt socialization mechanism exists
  • Oracle failure handling (pause markets, fallback feeds)
  • Borrow cap and supply cap enforcement

Flash Loan Guards

modifier noFlashLoan() {
    require(lastActionBlock[msg.sender] < block.number, "same block");
    _;
    lastActionBlock[msg.sender] = block.number;
}

Check: Can a flash loan be used to manipulate governance, oracle prices, or collateral ratios within a single transaction?


5. Gas Optimization Patterns

Storage Packing

// BEFORE: 3 slots (96 bytes)
uint256 amount;     // slot 0
uint128 timestamp;  // slot 1
bool active;        // slot 2

// AFTER: 2 slots (64 bytes)
uint128 timestamp;  // slot 0 (16 bytes)
bool active;        // slot 0 (1 byte) — packed!
uint256 amount;     // slot 1

calldata vs memory

// BEFORE: copies array to memory (~expensive)
function process(uint256[] memory ids) external { ... }

// AFTER: reads directly from calldata (~cheap, read-only)
function process(uint256[] calldata ids) external { ... }

Unchecked Arithmetic

// BEFORE
for (uint256 i = 0; i < len; i++) { ... } // overflow check on i each iteration

// AFTER
for (uint256 i = 0; i < len; ) {
    ...
    unchecked { ++i; } // safe: i < len guarantees no overflow
}

Custom Errors vs Require Strings

// BEFORE: stores string in bytecode
require(amount > 0, "Amount must be greater than zero"); // ~24 bytes

// AFTER: 4-byte selector only
error ZeroAmount();
if (amount == 0) revert ZeroAmount(); // 4 bytes

Cache Storage Reads

// BEFORE: 3 SLOAD operations
function calc() external view returns (uint256) {
    return baseRate + baseRate * multiplier / baseRate;
}

// AFTER: 1 SLOAD
function calc() external view returns (uint256) {
    uint256 _baseRate = baseRate;
    return _baseRate + _baseRate * multiplier / _baseRate;
}

Short-Circuit Conditionals

// Put cheap check first
require(amount > 0 && balances[msg.sender] >= amount); // SLOAD only if amount > 0

6. Audit Report Template

Severity Levels

SeverityDefinition
CriticalDirect loss of funds or permanent contract bricking. Exploit requires no special permissions.
HighIndirect fund loss, significant protocol disruption, or privilege escalation.
MediumLimited fund risk, griefing potential, or state inconsistency under specific conditions.
LowBest practice violation, informational, minor gas inefficiency.
GasGas optimization opportunity with no functional impact.

Finding Format

### [S-01] Title of Finding

**Severity:** Critical / High / Medium / Low / Gas
**Status:** Open / Acknowledged / Fixed
**File:** src/Vault.sol#L42-L58

**Description:**
One paragraph explaining the vulnerability and root cause.

**Impact:**
What can go wrong. Quantify if possible (e.g., "attacker drains all ETH in contract").

**Proof of Concept:**
```solidity
// Foundry test demonstrating the exploit
function test_exploit() public {
    // setup
    // attack
    // assert funds stolen
}

Recommendation: Specific code fix with diff or replacement code.

Team Response: (filled by the audited team)


### Report Structure
1. Executive Summary (scope, duration, findings count by severity)
2. Scope (contracts, commit hash, lines of code)
3. Methodology (tools used, manual review areas)
4. Findings (ordered by severity)
5. Gas Optimizations
6. Informational / Best Practices
7. Appendix (tool output, coverage report)

---

## 7. Tool Commands Reference

```bash
# Static analysis
slither .
slither . --detect reentrancy-eth,unprotected-upgrade
slither . --print human-summary

# Symbolic execution
myth analyze src/Contract.sol --solv 0.8.24 --execution-timeout 600

# Aderyn
aderyn . --output report.md

# Foundry
forge test --fuzz-runs 10000
forge test --fuzz-runs 50000 -vvvv --match-test testFuzz
forge coverage --report lcov
forge inspect Contract storage-layout
forge selectors list

# Echidna
echidna . --contract TestContract --test-mode assertion --test-limit 100000

# Coverage
forge coverage --report summary
forge coverage --report lcov && genhtml lcov.info -o coverage/

8. Test Coverage & Fuzzing Strategy

Coverage Assessment

forge coverage --report summary
# Target: >95% line coverage, >90% branch coverage
# Critical paths (withdraw, liquidate, upgrade): 100% branch coverage

What to Fuzz

Priority targets for fuzz testing:

  1. Math functions — arithmetic with user-supplied inputs
  2. Token amounts — deposits, withdrawals, swaps, fees
  3. Access boundaries — role transitions, timelocks
  4. Edge values — 0, 1, type(uint256).max, empty arrays

Invariant Testing

Define protocol invariants that must always hold:

function invariant_totalSupplyMatchesBalances() public view {
    uint256 sum = 0;
    for (uint i = 0; i < holders.length; i++) {
        sum += token.balanceOf(holders[i]);
    }
    assert(sum == token.totalSupply());
}

function invariant_vaultSolvent() public view {
    assert(address(vault).balance >= vault.totalDeposited());
}

Fuzzing Strategies

  • Random: Default — good for broad coverage
  • Guided: Use bound() to constrain inputs to realistic ranges
  • Stateful (invariant testing): Foundry calls random sequences of functions, checks invariants after each
  • Corpus-based: Echidna saves interesting inputs, replays and mutates them

CI Integration

# .github/workflows/audit.yml
- run: forge test --fuzz-runs 10000
- run: forge coverage --report summary
- run: slither . --sarif output.sarif
- run: aderyn .