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..bfd32f60 --- /dev/null +++ b/sessions/13-04-26-test/security/README.md @@ -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. 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..64546f37 --- /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 { + 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); + } +} + +