Smart contract auditing — 10 bug classes, Foundry PoC, DeFi patterns
| Tool | Purpose | Install |
|---|---|---|
| forge | Build, test, deploy contracts | curl -L https://foundry.paradigm.xyz | bash && foundryup |
| cast | Interact with contracts/chains | (installed with Foundry) |
| anvil | Local testnet fork | (installed with Foundry) |
| slither | Static analysis | pip3 install slither-analyzer |
| solc | Solidity compiler | brew install solidity |
Skip a contract if:
public/external functionsonlyOwner, onlyRole, custom modifierstransfer, transferFrom, approve, fee-on-transfer tokensslither . --print human-summary
slither . --detect all
slither . --print contract-summary
Write to output/<target>/findings/web3_<vuln_name>.md
What: Internal accounting (state variables) drifts from actual token balances.
Detection:
balanceOf(address(this)) with internal tracking variablesdeposit(amount) assume amount tokens actually arrived?Grep patterns:
balanceOf
_totalSupply
_balances\[
shares\[
Example: Protocol assumes transferFrom(user, address(this), amount) delivers exactly amount, but token has 1% fee. Internal accounting credits amount but only amount * 0.99 arrived.
What: Missing or incorrect authorization checks on sensitive functions.
Detection:
onlyOwner/onlyRole/custom modifier that modify critical stateinitialize() callable by anyone (unprotected initializer in proxy)tx.origin used instead of msg.sender for authGrep patterns:
function.*external(?!.*only)
function.*public(?!.*only)
initialize
tx\.origin
What: Missing branches, unchecked returns, silent failures.
Detection:
else in if/else chains that should cover all casestransfer(), call(), send()try/catch that silently swallows errorsrevert on critical failure pathsGrep patterns:
\.call\{
\.send\(
\.transfer\(
try.*catch
What: Boundary errors in loops, indexing, and comparisons.
Detection:
< vs <= in loops and time checksblock.timestamp comparisons at exact boundariesGrep patterns:
for.*\.length
block\.timestamp
<= |>=
/ [0-9]
What: Price oracles that can be manipulated within a single transaction.
Detection:
getReserves())updatedAt)Grep patterns:
getReserves
latestRoundData
price
oracle
twap
Red flag: Any function that reads a price and makes a financial decision in the same transaction.
What: Vault share calculation issues, especially for first depositor.
Detection:
deposit should round down shares (favor vault), withdraw should round up shares (favor vault)totalSupply == 0Grep patterns:
totalAssets
convertToShares
convertToAssets
previewDeposit
previewMint
What: External call allows attacker to re-enter the contract before state is updated.
Detection:
.call, .transfer, .send) before state updates (violates CEI pattern)Grep patterns:
\.call\{value
\.call\(
\.transfer\(
\.send\(
nonReentrant
Check: Is there a nonReentrant modifier? If yes, is it on ALL external-facing functions?
What: Using flash loans to manipulate state within a single transaction.
Detection:
Grep patterns:
flashLoan
flashMint
balanceOf.*msg\.sender
getReserves
What: Signed messages that can be reused.
Detection:
ecrecover returning address(0) not checkedGrep patterns:
ecrecover
ECDSA
permit
signature
nonce
DOMAIN_SEPARATOR
What: Storage collision, uninitialized proxies, dangerous upgrades.
Detection:
selfdestruct in implementation contractdelegatecall to untrusted address_disableInitializers() in implementation constructorGrep patterns:
delegatecall
upgradeTo
implementation
initializer
selfdestruct
IMPLEMENTATION_SLOT
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Test.sol";
interface ITarget {
// Define target contract interface
}
contract ExploitTest is Test {
ITarget target;
address attacker = makeAddr("attacker");
address victim = makeAddr("victim");
function setUp() public {
// Fork mainnet at specific block
vm.createSelectFork("mainnet", 18_000_000);
// Set up contract references
target = ITarget(0x1234...);
// Fund accounts
deal(address(token), attacker, 1000e18);
deal(address(token), victim, 1000e18);
}
function testExploit() public {
// Record state before
uint256 attackerBalBefore = token.balanceOf(attacker);
vm.startPrank(attacker);
// ... exploit steps ...
vm.stopPrank();
// Record state after
uint256 attackerBalAfter = token.balanceOf(attacker);
// Assert the impact
assertGt(attackerBalAfter, attackerBalBefore, "Attacker should profit");
console.log("Profit:", attackerBalAfter - attackerBalBefore);
}
}
Run:
forge test --match-test testExploit -vvvv --fork-url https://eth-mainnet.g.alchemy.com/v2/<KEY>
Useful Foundry cheatcodes:
vm.prank(address) — impersonate for next callvm.startPrank(address) — impersonate until vm.stopPrank()deal(token, who, amount) — set token balancevm.warp(timestamp) — set block.timestampvm.roll(blockNumber) — set block.numbervm.expectRevert() — expect next call to revertmakeAddr("label") — create labeled address