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.Integer overflow / underflow
Prior to Solidity 0.8.0, arithmetic wrapped silently. A balance of0 decremented by 1 becomes 2^256 - 1.
Access control
Missing or incorrect access control allows unauthorised users to call privileged functions.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 V2getAmounts) can be manipulated in a single transaction using a flash loan:
Borrow a large amount
Borrow millions of tokens from a flash loan provider (Aave, Uniswap V3) at no upfront cost.
Manipulate the oracle
Dump the borrowed tokens into the pool, crashing the price in the pool’s reserves.
Exploit the protocol
The vulnerable protocol reads the manipulated price as collateral value and allows overcollateralised borrowing or liquidation.
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.
Auditing methodology
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?
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.Mutation testing
Use
slither-mutate to verify your tests actually catch behavioural changes (see below).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.Common mutation operators
| Operator | Example change |
|---|---|
| Operator replacement | >= → >, + → - |
| Constant replacement | 1 ether → 0 |
| Condition negation | if (x > 0) → if (!(x > 0)) |
| Comment replacement (CR) | Entire line → //comment |
| Revert replacement | Statement → revert() |
Understanding the output
UNCAUGHT means the tests passed even with that line commented out — a missing assertion.
Triage workflow
Inspect surviving mutants
Apply the mutant locally and run a focused test to understand what is not being asserted.
Add state assertions
Do not only check return values — verify post-state: balances, total supply, authorisation effects, emitted events.
Improve mocks
Replace overly permissive mocks with realistic behaviour that enforces transfer amounts and failure paths.
Case study: Arkis DeFi protocol
_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
externalcalls are made after state updates (Checks-Effects-Interactions) -
ReentrancyGuardon functions that handle ETH or tokens - All
initializerfunctions are protected against re-initialisation - No
tx.originfor authentication — usemsg.sender - Price oracles use TWAP, not spot reserves
- No unprotected
selfdestructordelegatecallto 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