Skip to main content
Smart contracts are immutable once deployed and control real funds. A single bug can lead to irreversible loss. This page covers the most common vulnerability classes and the tooling used to find them.

Common vulnerability classes

Reentrancy

A contract calls an external address before updating its own state. The external address calls back into the original contract before the state update completes, draining funds.
// VULNERABLE: updates balance AFTER transferring ETH
function withdraw(uint256 amount) external {
    require(balances[msg.sender] >= amount);
    (bool success, ) = msg.sender.call{value: amount}("");  // re-entry here
    require(success);
    balances[msg.sender] -= amount;  // too late
}

// FIXED: update state BEFORE transferring
function withdraw(uint256 amount) external {
    require(balances[msg.sender] >= amount);
    balances[msg.sender] -= amount;  // update first
    (bool success, ) = msg.sender.call{value: amount}("");
    require(success);
}
Mitigation: Checks-Effects-Interactions pattern, ReentrancyGuard from OpenZeppelin.

Integer overflow / underflow

Prior to Solidity 0.8.0, arithmetic wrapped silently. A balance of 0 decremented by 1 becomes 2^256 - 1.
// Solidity < 0.8.0 — VULNERABLE
uint256 balance = 0;
balance -= 1;  // wraps to 2**256 - 1

// Solidity >= 0.8.0: arithmetic reverts on overflow/underflow by default
// For older code, use SafeMath or upgrade the compiler

Access control

Missing or incorrect access control allows unauthorised users to call privileged functions.
// VULNERABLE: no access control on mint
function mint(address to, uint256 amount) external {
    _mint(to, amount);  // anyone can mint unlimited tokens
}

// FIXED:
function mint(address to, uint256 amount) external onlyOwner {
    _mint(to, amount);
}
Common patterns: missing onlyOwner, unprotected initialize() in proxy contracts (can be called by anyone before the owner does), tx.origin instead of msg.sender.

Oracle manipulation / flash loan attacks

On-chain price oracles that use spot reserves (e.g., Uniswap V2 getAmounts) can be manipulated in a single transaction using a flash loan:
1

Borrow a large amount

Borrow millions of tokens from a flash loan provider (Aave, Uniswap V3) at no upfront cost.
2

Manipulate the oracle

Dump the borrowed tokens into the pool, crashing the price in the pool’s reserves.
3

Exploit the protocol

The vulnerable protocol reads the manipulated price as collateral value and allows overcollateralised borrowing or liquidation.
4

Repay and profit

Repay the flash loan in the same transaction. The profit is the difference between the manipulated trade and market price.
Mitigation: Use time-weighted average price (TWAP) oracles, require multiple block confirmation, or use Chainlink price feeds.

Delegatecall proxy vulnerabilities

delegatecall executes code from another contract in the caller’s storage context. Proxy patterns rely on this for upgradeability, but storage collisions can allow an attacker to overwrite the implementation address.
// Storage layout collision: if proxy and implementation both have
// a variable at slot 0, writing to one overwrites the other
contract Proxy {
    address public implementation;  // slot 0
}

contract Implementation {
    address public owner;  // slot 0 in delegatecall context = overwrites proxy.implementation
}
Mitigation: EIP-1967 unstructured storage slots, OpenZeppelin TransparentProxy or UUPS.

Auditing methodology

1

Static analysis

Run Slither to find common patterns automatically:
slither ./src/contracts
2

Manual code review

Read every function with external calls. Check: who can call this? What state changes happen before external calls? What is the trust model?
3

Unit and fuzz testing

Write tests that cover boundary conditions — zero amounts, maximum uint256, reentrancy callers, etc. Use Foundry’s vm.fuzz for property-based testing.
4

Mutation testing

Use slither-mutate to verify your tests actually catch behavioural changes (see below).
5

Economic analysis

Model flash loan attack scenarios. Identify price oracle dependencies. Check for MEV (Miner Extractable Value) risks.

Mutation testing with slither-mutate

Mutation testing “tests your tests” by inserting small, semantics-changing edits (mutants) into the source and re-running the test suite. If the tests still pass after a mutation, there is a blind spot.
# Install Slither v0.10.2+
pip install slither-analyzer

# Run mutation campaign (Foundry)
slither-mutate ./src/contracts --test-cmd="forge test" &> >(tee mutation.results)

# List available mutators
slither-mutate --list-mutators

Common mutation operators

OperatorExample change
Operator replacement>=>, +-
Constant replacement1 ether0
Condition negationif (x > 0)if (!(x > 0))
Comment replacement (CR)Entire line → //comment
Revert replacementStatement → revert()

Understanding the output

INFO:Slither-Mutate:[CR] Line 33: 'balances[msg.sender] -= amount' ==> '//balances[msg.sender] -= amount' --> UNCAUGHT
UNCAUGHT means the tests passed even with that line commented out — a missing assertion.

Triage workflow

1

Inspect surviving mutants

Apply the mutant locally and run a focused test to understand what is not being asserted.
2

Add state assertions

Do not only check return values — verify post-state: balances, total supply, authorisation effects, emitted events.
3

Improve mocks

Replace overly permissive mocks with realistic behaviour that enforces transfer amounts and failure paths.
4

Add fuzz invariants

Conservation of value, non-negative balances, monotonic supply — these catch entire classes of mutations.

Case study: Arkis DeFi protocol

INFO:Slither-Mutate:[CR] Line 33: 
  'cmdsToExecute.last().value = _cmd.value' 
  ==> '//cmdsToExecute.last().value = _cmd.value' --> UNCAUGHT
Commenting out the assignment didn’t break any tests. Root cause: the code trusted a user-controlled _cmd.value instead of validating actual token transfers. An attacker could desynchronise expected vs actual transfers to drain funds. Severity: High.

Solidity security checklist

  • All external calls are made after state updates (Checks-Effects-Interactions)
  • ReentrancyGuard on functions that handle ETH or tokens
  • All initializer functions are protected against re-initialisation
  • No tx.origin for authentication — use msg.sender
  • Price oracles use TWAP, not spot reserves
  • No unprotected selfdestruct or delegatecall to user-supplied addresses
  • Integer arithmetic uses Solidity 0.8+ or SafeMath
  • Storage layout is documented and consistent across proxy/implementation
  • Events are emitted for all state-changing functions
  • Access control roles are documented and tested

Build docs developers (and LLMs) love