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
38 changes: 38 additions & 0 deletions sessions/13-04-26-test/security/.github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: CI

permissions: {}

on:
push:
pull_request:
workflow_dispatch:

env:
FOUNDRY_PROFILE: ci

jobs:
check:
name: Foundry project
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v5
with:
persist-credentials: false
submodules: recursive

- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1

- name: Show Forge version
run: forge --version

- name: Run Forge fmt
run: forge fmt --check

- name: Run Forge build
run: forge build --sizes

- name: Run Forge tests
run: forge test -vvv
14 changes: 14 additions & 0 deletions sessions/13-04-26-test/security/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Compiler files
cache/
out/

# Ignores development broadcast logs
!/broadcast
/broadcast/*/31337/
/broadcast/**/dry-run/

# Docs
docs/

# Dotenv file
.env
3 changes: 3 additions & 0 deletions sessions/13-04-26-test/security/.gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "lib/forge-std"]
path = lib/forge-std
url = https://github.com/foundry-rs/forge-std
24 changes: 24 additions & 0 deletions sessions/13-04-26-test/security/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Reentrancy Attack Demo

This project demonstrates a reentrancy attack on a simple bank contract and how to prevent it using a reentrancy guard.

## Files

- `src/secureBank.sol`: The secure bank contract with deposit/withdraw functions protected by a `noReentrancy` modifier.
- `src/Attackk.sol`: The attack contract that attempts to exploit reentrancy.
- `test/attackk.t.sol`: Test suite including tests for the attack and security measures.

## Running Tests

```bash
forge test
```

## Test Status

- Most tests pass, confirming the reentrancy guard works.
- `testDrain` is currently failing - this test attempts to verify that an attacker can drain the contract's funds with just 1 ETH. Since the guard prevents this, the test needs adjustment to properly assert the prevention.

## Further Work

Fix `testDrain` in `attackk.t.sol` to correctly demonstrate that the attack is blocked by the reentrancy guard.
8 changes: 8 additions & 0 deletions sessions/13-04-26-test/security/foundry.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"lib/forge-std": {
"tag": {
"name": "v1.15.0",
"rev": "0844d7e1fc5e60d77b68e469bff60265f236c398"
}
}
}
6 changes: 6 additions & 0 deletions sessions/13-04-26-test/security/foundry.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[profile.default]
src = "src"
out = "out"
libs = ["lib"]

# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
23 changes: 23 additions & 0 deletions sessions/13-04-26-test/security/src/Attackk.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {SecureBank} from "./secureBank.sol";

contract AttackContract {
SecureBank public bank;

constructor(address _bank) {
bank = SecureBank(_bank);
}

function attack() public payable {
bank.deposit{value: msg.value}();
bank.withdraw(msg.value);
}

receive() external payable {
if (address(bank).balance >= msg.value) {
bank.withdraw(msg.value);
}
}
}
27 changes: 27 additions & 0 deletions sessions/13-04-26-test/security/src/secureBank.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

contract SecureBank {
mapping(address => uint256) public balances;

bool private locked;

modifier noReentrancy() {
require(!locked, "No reentrancy");
locked = true;
_;
locked = false;
}

function deposit() public payable {
balances[msg.sender] += msg.value;
}

function withdraw(uint256 amount) public noReentrancy {
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
(bool success,) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
}

21 changes: 21 additions & 0 deletions sessions/13-04-26-test/security/src/security.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {SecureBank} from "./secureBank.sol";

// contract SecureBank {
// mapping(address => uint256) public balances;

// function deposit() public payable {
// balances[msg.sender] += msg.value;
// }

// function withdraw(uint256 amount) public noReentrancy {
// require(balances[msg.sender] >= amount, "Insufficient balance");
// balances[msg.sender] -= amount;
// (bool success,) = msg.sender.call{value: amount}("");
// require(success, "Transfer failed");
// }

// }

64 changes: 64 additions & 0 deletions sessions/13-04-26-test/security/test/attackk.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Test} from "forge-std/Test.sol";
import {SecureBank} from "../src/secureBank.sol";
import {AttackContract} from "../src/Attackk.sol";

contract AttackContractTest is Test {
SecureBank bank;
AttackContract attack;
address user1;
address attacker;

function setUp() public {
user1 = address(0x1);
attacker = address(0x2);

bank = new SecureBank();
attack = new AttackContract(address(bank));

vm.deal(user1, 10 ether);
vm.prank(user1);
bank.deposit{value: 10 ether}();
}

function testAttackReverts() public {
vm.deal(attacker, 1 ether);
vm.prank(attacker);
vm.expectRevert("Transfer failed");
attack.attack{value: 1 ether}();
}

function testFundsSafe() public {
vm.deal(attacker, 1 ether);
vm.prank(attacker);
try attack.attack{value: 1 ether}() {} catch {}

uint256 bankBalance = address(bank).balance;
assertGe(bankBalance, 10 ether);
}

function testWithdrawWorks() public {
uint256 beforeBalance = user1.balance;
vm.prank(user1);
bank.withdraw(5 ether);
uint256 afterBalance = user1.balance;

assertGt(afterBalance, beforeBalance);
assertEq(bank.balances(user1), 5 ether);
}

function testDrain() public {
vm.deal(attacker, 1 ether);
vm.prank(attacker);
try attack.attack{value: 1 ether}() {} catch {}

uint256 attackerAfter = attacker.balance;
assertEq(attackerAfter, 1 ether);

assertEq(address(bank).balance, 10 ether);
}
}