From c48049d39d6af7ab5211ec3b82c6950ef1a79540 Mon Sep 17 00:00:00 2001 From: Yusrah Mohammed Date: Mon, 13 Apr 2026 16:02:46 +0100 Subject: [PATCH 1/3] adding the tests --- .../security/.github/workflows/test.yml | 38 +++++++++++ sessions/13-04-26-test/security/.gitignore | 14 ++++ sessions/13-04-26-test/security/.gitmodules | 3 + sessions/13-04-26-test/security/README.md | 66 +++++++++++++++++++ sessions/13-04-26-test/security/foundry.lock | 8 +++ sessions/13-04-26-test/security/foundry.toml | 6 ++ .../13-04-26-test/security/src/Attackk.sol | 23 +++++++ .../13-04-26-test/security/src/secureBank.sol | 27 ++++++++ .../13-04-26-test/security/src/security.sol | 21 ++++++ .../13-04-26-test/security/test/attackk.t.sol | 64 ++++++++++++++++++ 10 files changed, 270 insertions(+) create mode 100644 sessions/13-04-26-test/security/.github/workflows/test.yml create mode 100644 sessions/13-04-26-test/security/.gitignore create mode 100644 sessions/13-04-26-test/security/.gitmodules create mode 100644 sessions/13-04-26-test/security/README.md create mode 100644 sessions/13-04-26-test/security/foundry.lock create mode 100644 sessions/13-04-26-test/security/foundry.toml create mode 100644 sessions/13-04-26-test/security/src/Attackk.sol create mode 100644 sessions/13-04-26-test/security/src/secureBank.sol create mode 100644 sessions/13-04-26-test/security/src/security.sol create mode 100644 sessions/13-04-26-test/security/test/attackk.t.sol diff --git a/sessions/13-04-26-test/security/.github/workflows/test.yml b/sessions/13-04-26-test/security/.github/workflows/test.yml new file mode 100644 index 00000000..b79c8d4f --- /dev/null +++ b/sessions/13-04-26-test/security/.github/workflows/test.yml @@ -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 diff --git a/sessions/13-04-26-test/security/.gitignore b/sessions/13-04-26-test/security/.gitignore new file mode 100644 index 00000000..85198aaa --- /dev/null +++ b/sessions/13-04-26-test/security/.gitignore @@ -0,0 +1,14 @@ +# Compiler files +cache/ +out/ + +# Ignores development broadcast logs +!/broadcast +/broadcast/*/31337/ +/broadcast/**/dry-run/ + +# Docs +docs/ + +# Dotenv file +.env diff --git a/sessions/13-04-26-test/security/.gitmodules b/sessions/13-04-26-test/security/.gitmodules new file mode 100644 index 00000000..888d42dc --- /dev/null +++ b/sessions/13-04-26-test/security/.gitmodules @@ -0,0 +1,3 @@ +[submodule "lib/forge-std"] + path = lib/forge-std + url = https://github.com/foundry-rs/forge-std diff --git a/sessions/13-04-26-test/security/README.md b/sessions/13-04-26-test/security/README.md new file mode 100644 index 00000000..8817d6ab --- /dev/null +++ b/sessions/13-04-26-test/security/README.md @@ -0,0 +1,66 @@ +## Foundry + +**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.** + +Foundry consists of: + +- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools). +- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data. +- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network. +- **Chisel**: Fast, utilitarian, and verbose solidity REPL. + +## Documentation + +https://book.getfoundry.sh/ + +## Usage + +### Build + +```shell +$ forge build +``` + +### Test + +```shell +$ forge test +``` + +### Format + +```shell +$ forge fmt +``` + +### Gas Snapshots + +```shell +$ forge snapshot +``` + +### Anvil + +```shell +$ anvil +``` + +### Deploy + +```shell +$ forge script script/Counter.s.sol:CounterScript --rpc-url --private-key +``` + +### Cast + +```shell +$ cast +``` + +### Help + +```shell +$ forge --help +$ anvil --help +$ cast --help +``` diff --git a/sessions/13-04-26-test/security/foundry.lock b/sessions/13-04-26-test/security/foundry.lock new file mode 100644 index 00000000..bc06b89b --- /dev/null +++ b/sessions/13-04-26-test/security/foundry.lock @@ -0,0 +1,8 @@ +{ + "lib/forge-std": { + "tag": { + "name": "v1.15.0", + "rev": "0844d7e1fc5e60d77b68e469bff60265f236c398" + } + } +} \ No newline at end of file diff --git a/sessions/13-04-26-test/security/foundry.toml b/sessions/13-04-26-test/security/foundry.toml new file mode 100644 index 00000000..25b918f9 --- /dev/null +++ b/sessions/13-04-26-test/security/foundry.toml @@ -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 diff --git a/sessions/13-04-26-test/security/src/Attackk.sol b/sessions/13-04-26-test/security/src/Attackk.sol new file mode 100644 index 00000000..f5124df8 --- /dev/null +++ b/sessions/13-04-26-test/security/src/Attackk.sol @@ -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); + } + } +} diff --git a/sessions/13-04-26-test/security/src/secureBank.sol b/sessions/13-04-26-test/security/src/secureBank.sol new file mode 100644 index 00000000..8f8a1217 --- /dev/null +++ b/sessions/13-04-26-test/security/src/secureBank.sol @@ -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"); + } +} + diff --git a/sessions/13-04-26-test/security/src/security.sol b/sessions/13-04-26-test/security/src/security.sol new file mode 100644 index 00000000..85c9d358 --- /dev/null +++ b/sessions/13-04-26-test/security/src/security.sol @@ -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"); +// } + +// } + diff --git a/sessions/13-04-26-test/security/test/attackk.t.sol b/sessions/13-04-26-test/security/test/attackk.t.sol new file mode 100644 index 00000000..074ad3fa --- /dev/null +++ b/sessions/13-04-26-test/security/test/attackk.t.sol @@ -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 { + uint256 attackerBefore = attacker.balance; + vm.deal(attacker, 1 ether); + vm.prank(attacker); + try attack.attack{value: 1 ether}() {} catch {} + + uint256 attackerAfter = attacker.balance; + + assertEq(attackerAfter, attackerBefore); + } +} + + From 4baba894d7873ad85a26969c28f5386e27d55a94 Mon Sep 17 00:00:00 2001 From: Yusrah Mohammed Date: Mon, 13 Apr 2026 16:17:47 +0100 Subject: [PATCH 2/3] add: README --- sessions/13-04-26-test/security/README.md | 70 +++++------------------ 1 file changed, 14 insertions(+), 56 deletions(-) diff --git a/sessions/13-04-26-test/security/README.md b/sessions/13-04-26-test/security/README.md index 8817d6ab..bfd32f60 100644 --- a/sessions/13-04-26-test/security/README.md +++ b/sessions/13-04-26-test/security/README.md @@ -1,66 +1,24 @@ -## Foundry +# Reentrancy Attack Demo -**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.** +This project demonstrates a reentrancy attack on a simple bank contract and how to prevent it using a reentrancy guard. -Foundry consists of: +## Files -- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools). -- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data. -- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network. -- **Chisel**: Fast, utilitarian, and verbose solidity REPL. +- `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. -## Documentation +## Running Tests -https://book.getfoundry.sh/ - -## Usage - -### Build - -```shell -$ forge build -``` - -### Test - -```shell -$ forge test -``` - -### Format - -```shell -$ forge fmt -``` - -### Gas Snapshots - -```shell -$ forge snapshot -``` - -### Anvil - -```shell -$ anvil +```bash +forge test ``` -### Deploy +## Test Status -```shell -$ forge script script/Counter.s.sol:CounterScript --rpc-url --private-key -``` - -### Cast +- 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. -```shell -$ cast -``` +## Further Work -### Help - -```shell -$ forge --help -$ anvil --help -$ cast --help -``` +Fix `testDrain` in `attackk.t.sol` to correctly demonstrate that the attack is blocked by the reentrancy guard. From be231d93496830b585bf2ac1567653b08d7e52b6 Mon Sep 17 00:00:00 2001 From: Yusrah Mohammed Date: Mon, 13 Apr 2026 20:33:13 +0100 Subject: [PATCH 3/3] fix: test and update README --- sessions/13-04-26-test/security/test/attackk.t.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sessions/13-04-26-test/security/test/attackk.t.sol b/sessions/13-04-26-test/security/test/attackk.t.sol index 074ad3fa..64546f37 100644 --- a/sessions/13-04-26-test/security/test/attackk.t.sol +++ b/sessions/13-04-26-test/security/test/attackk.t.sol @@ -50,14 +50,14 @@ contract AttackContractTest is Test { } function testDrain() public { - uint256 attackerBefore = attacker.balance; - vm.deal(attacker, 1 ether); + vm.deal(attacker, 1 ether); vm.prank(attacker); try attack.attack{value: 1 ether}() {} catch {} uint256 attackerAfter = attacker.balance; - - assertEq(attackerAfter, attackerBefore); + assertEq(attackerAfter, 1 ether); + + assertEq(address(bank).balance, 10 ether); } }