Source mirror · commit 369512f · 2026-10-26 · MIT · canonical repo: gitlab.com/jiapunk/upgrade-airlock
git clone https://upgrade-airlock.pages.dev/source/upgrade-airlock.git
upgrade-airlock / test / fixtures / DrainVariants.sol
test/fixtures/DrainVariants.sol90 lines · 4165 bytes
1// SPDX-License-Identifier: MIT
2pragma solidity 0.8.37;
3
4import {DemoBTC} from "../../src/DemoBTC.sol";
5
6// Test-only attack fixtures for the CRE clearance workflow's fork E2E. Each inherits DemoBTC, so it keeps v1's
7// storage layout and every v1 selector intact (mint stays owner-only, transfers conserve, the owner stays put,
8// no dangerous opcodes): R1, R2, R3, R4 and R9 all pass, and only R3B can catch the drain. These are the seven
9// variants a reviewer used to slip past an R3B that only matched the exact rescue(from, to, amount) shape; the
10// hardened simulation must veto every one of them R3B.
11//
12// Deliberately malicious demo code. Never deploy outside a demo.
13
14/// @notice A: rescue with the amount word between the two addresses — rescue(from, amount, to).
15contract VariantA_RescueReorder is DemoBTC {
16 function rescue(address from, uint256 amount, address to) external {
17 _transfer(from, to, amount);
18 }
19}
20
21/// @notice B: a sweep whose first word is the amount — sweep(amount, from, to).
22contract VariantB_SweepReorder is DemoBTC {
23 function sweep(uint256 amount, address from, address to) external {
24 _transfer(from, to, amount);
25 }
26}
27
28/// @notice C: a four-argument rescueTokens(token, from, to, amount) that insists the token is this contract. The
29/// probe that fills the remaining address slots with the proxy address passes the guard.
30contract VariantC_RescueTokensGuard is DemoBTC {
31 error WrongToken();
32
33 function rescueTokens(address token, address from, address to, uint256 amount) external {
34 if (token != address(this)) revert WrongToken();
35 _transfer(from, to, amount);
36 }
37}
38
39/// @notice D: an open rescue(from, to, amount) gated on the caller already holding tokens. A stranger with no
40/// balance cannot reach it — unless the simulation funds the stranger first.
41contract VariantD_RescueRequireTokens is DemoBTC {
42 error NoTokens();
43
44 function rescue(address from, address to, uint256 amount) external {
45 if (balanceOf(msg.sender) == 0) revert NoTokens();
46 _transfer(from, to, amount);
47 }
48}
49
50/// @notice E: a rescue(from, to, amount) that moves balances with raw storage writes and emits NO Transfer event,
51/// so a probe out and a later probe back net to zero with no logs. The post-probe balance read catches it.
52contract VariantE_RescueNoLog is DemoBTC {
53 // ERC20Upgradeable ERC-7201 storage: _balances is the first field of the struct at this slot.
54 // keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.ERC20")) - 1)) & ~bytes32(uint256(0xff))
55 bytes32 private constant ERC20_STORAGE = 0x52c63247e1f47db19d5ce0460030c497f067ca4cebf71ba98eeadabe20bace00;
56
57 function rescue(address from, address to, uint256 amount) external {
58 bytes32 fromSlot = keccak256(abi.encode(from, ERC20_STORAGE));
59 bytes32 toSlot = keccak256(abi.encode(to, ERC20_STORAGE));
60 assembly {
61 let fb := sload(fromSlot)
62 if lt(fb, amount) { revert(0, 0) }
63 sstore(fromSlot, sub(fb, amount))
64 sstore(toSlot, add(sload(toSlot), amount))
65 }
66 }
67}
68
69/// @notice F: an open rescue(from, to, amount) gated on the caller holding ETH. A stranger with no ETH cannot reach
70/// it — unless the simulation gives the stranger an ETH balance override.
71contract VariantF_RescueRequireEth is DemoBTC {
72 error NoEth();
73
74 function rescue(address from, address to, uint256 amount) external {
75 if (msg.sender.balance == 0) revert NoEth();
76 _transfer(from, to, amount);
77 }
78}
79
80/// @notice G: no new selector. transferFrom waives the allowance check whenever the caller already holds tokens, so
81/// a token-holding stranger can pull any holder's balance. The R3B transferFrom runs from the funded stranger.
82contract VariantG_TransferFromAllowanceSkip is DemoBTC {
83 function transferFrom(address from, address to, uint256 value) public override returns (bool) {
84 if (balanceOf(msg.sender) != 0) {
85 _transfer(from, to, value);
86 return true;
87 }
88 return super.transferFrom(from, to, value);
89 }
90}