Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ cache/
lib/
node_modules/
.env
**/__pycache__/
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,28 @@ forge install kcolbchain/audit-checklist
When working on this repository directly, initialize submodules before running
the local verification commands:

### Prerequisites

- **Foundry** (forge + cast): Install via [foundryup](https://book.getfoundry.sh/getting-started/installation).
- ⚠️ **Linux users**: The latest forge binaries require **GLIBC >= 2.33**.
Run `ldd --version` to check. If you're on an older distro (e.g. CentOS 8,
Ubuntu 20.04 with stock glibc), use the [Foundry Docker image](https://ghcr.io/foundry-rs/foundry):
```bash
docker pull ghcr.io/foundry-rs/foundry:latest
docker run --rm -v "$PWD":/workspace -w /workspace ghcr.io/foundry-rs/foundry:latest forge test
```
- **Slither** (optional, for static analysis): `pip install slither-analyzer`
- Memory-constrained hosts can run specific detectors:
```bash
slither . --config slither.config.json --detect reentrancy-eth,uninitialized-state,unchecked-transfer
```
- Full analysis (may need 4 GB+ RAM):
```bash
slither . --config slither.config.json
```

### Quick verification

```bash
git submodule update --init --recursive
forge build
Expand Down
2 changes: 2 additions & 0 deletions src/checks/ERC777ReentrancyCheck.sol
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ abstract contract ERC777ReentrancyCheck is ChecklistBase {

/// @dev Perform a deposit as `depositor` of `amount`. Override to match
/// your target's deposit flow (approve+pull, push, etc.).
/// @notice Implementations MUST use `vm.prank(depositor)` (or `vm.startPrank`/`vm.stopPrank`)
/// before the deposit call so that the target contract sees `depositor` as `msg.sender`.
function performDeposit(address depositor, uint256 amount) internal virtual;

function test_erc777_reentrancy_on_withdraw() public {
Expand Down
14 changes: 10 additions & 4 deletions src/checks/ReentrancyCheck.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ abstract contract ReentrancyCheck is ChecklistBase {
return 1 ether;
}

/// @dev Override to perform a deposit into the target contract
/// @dev Override to perform a deposit into the target contract as `depositor`.
/// @notice Implementations MUST use `vm.prank(depositor)` (or `vm.startPrank`/`vm.stopPrank`)
/// before the deposit call so that the target contract sees `depositor` as `msg.sender`.
/// The check function does NOT call `vm.prank` before this hook to avoid prank-stacking
/// errors in Foundry >= 1.0.
function performDeposit(address depositor, uint256 amount) internal virtual;

function test_reentrancy_on_withdraw() public {
Expand All @@ -27,15 +31,17 @@ abstract contract ReentrancyCheck is ChecklistBase {
uint256 depositAmount = getDepositValue();
vm.deal(attacker, depositAmount);

// Deposit as the attacker
vm.prank(attacker);
// Deposit as the attacker.
// NOTE: performDeposit handles msg.sender internally via vm.prank.
// Do NOT add vm.prank(attacker) here — it would stack with the prank
// inside performDeposit and cause Foundry >= 1.0 to throw
// "cannot overwrite a prank until it is applied at least once".
performDeposit(attacker, depositAmount);

uint256 targetBalBefore = address(targetContract).balance;

// Trigger the attack — attacker calls withdraw, which sends ETH,
// which triggers receive(), which re-enters withdraw()
vm.prank(attacker);
ReentrantAttacker(payable(attacker)).attack();

uint256 attackerBal = address(attacker).balance;
Expand Down