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 / AirlockAdmin.t.sol
test/AirlockAdmin.t.sol1314 lines · 55703 bytes
1// SPDX-License-Identifier: MIT
2pragma solidity 0.8.37;
3
4import {Test, Vm} from "forge-std/Test.sol";
5import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
6import {ITransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
7import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol";
8import {ERC1967Utils} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Utils.sol";
9import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
10
11import {AirlockAdmin} from "../src/AirlockAdmin.sol";
12import {DemoBTC} from "../src/DemoBTC.sol";
13import {DemoBTCV2} from "../src/DemoBTCV2.sol";
14import {PausedImpl} from "../src/PausedImpl.sol";
15import {EvilMintImpl} from "../src/attack/EvilMintImpl.sol";
16import {EvilOwnerImpl} from "../src/attack/EvilOwnerImpl.sol";
17import {EvilFeeImpl} from "../src/attack/EvilFeeImpl.sol";
18import {EvilBaseFeeImpl} from "../src/attack/EvilBaseFeeImpl.sol";
19import {EvilDrainImpl} from "../src/attack/EvilDrainImpl.sol";
20
21contract AirlockAdminTest is Test {
22 uint64 constant DELAY = 30;
23 bytes32 constant R1 = keccak256("GUARD/R1_TOTAL_SUPPLY");
24 bytes32 constant R3 = keccak256("GUARD/R3_TRANSFER_CONSERVATION");
25 bytes32 constant R7 = keccak256("GUARD/R7_CONFIG_GUARD");
26 bytes32 constant EVIDENCE = keccak256("evidence-json");
27
28 address deployer = makeAddr("deployer");
29 address admin = makeAddr("admin");
30 address s1 = makeAddr("guardian1");
31 address s2 = makeAddr("guardian2");
32 address s3 = makeAddr("guardian3");
33 address approver = makeAddr("approver");
34 address oracle = makeAddr("clearanceOracle");
35 address h1 = makeAddr("holder1");
36 address h2 = makeAddr("holder2");
37 address h3 = makeAddr("holder3");
38 address stranger = makeAddr("stranger");
39
40 DemoBTC v1;
41 DemoBTCV2 v2;
42 EvilMintImpl evilMint;
43 EvilOwnerImpl evilOwner;
44 EvilFeeImpl evilFee;
45 EvilBaseFeeImpl evilBaseFee;
46 EvilDrainImpl evilDrain;
47 PausedImpl pausedImpl;
48 address proxy;
49 ProxyAdmin proxyAdmin;
50 AirlockAdmin aa;
51 DemoBTC token; // the proxy, seen through the v1 ABI
52
53 function setUp() public {
54 vm.roll(100);
55 vm.etch(oracle, hex"00"); // the clearance oracle must be a contract (in production: AirlockReceiver)
56 vm.startPrank(deployer);
57 v1 = new DemoBTC();
58 address[] memory holders = new address[](3);
59 holders[0] = h1;
60 holders[1] = h2;
61 holders[2] = h3;
62 uint256[] memory amounts = new uint256[](3);
63 amounts[0] = 100e8;
64 amounts[1] = 50e8;
65 amounts[2] = 25e8;
66 proxy = address(
67 new TransparentUpgradeableProxy(
68 address(v1), admin, abi.encodeCall(DemoBTC.initialize, (admin, holders, amounts))
69 )
70 );
71 proxyAdmin = ProxyAdmin(address(uint160(uint256(vm.load(proxy, ERC1967Utils.ADMIN_SLOT)))));
72 v2 = new DemoBTCV2();
73 evilMint = new EvilMintImpl();
74 evilOwner = new EvilOwnerImpl();
75 evilFee = new EvilFeeImpl();
76 evilBaseFee = new EvilBaseFeeImpl();
77 evilDrain = new EvilDrainImpl();
78 pausedImpl = new PausedImpl();
79 aa = _newAdmin(admin, _guardians(), approver, address(pausedImpl), DELAY, oracle);
80 vm.stopPrank();
81
82 // Adoption, exactly as script/DeployEth.s.sol: the owner hands both ProxyAdmin and the token's Ownable to
83 // AirlockAdmin.
84 token = DemoBTC(proxy);
85 vm.startPrank(admin);
86 proxyAdmin.transferOwnership(address(aa));
87 token.transferOwnership(address(aa));
88 vm.stopPrank();
89 }
90
91 // ------------------------------------------------------------------ helpers
92
93 function _newAdmin(
94 address owner_,
95 address[] memory guardians_,
96 address approver_,
97 address paused_,
98 uint64 delay_,
99 address oracle_
100 ) internal returns (AirlockAdmin) {
101 return new AirlockAdmin(address(proxyAdmin), proxy, owner_, guardians_, approver_, paused_, delay_, oracle_);
102 }
103
104 function _guardians() internal view returns (address[] memory g) {
105 g = new address[](3);
106 g[0] = s1;
107 g[1] = s2;
108 g[2] = s3;
109 }
110
111 function _advance(uint256 n) internal {
112 vm.roll(vm.getBlockNumber() + n);
113 }
114
115 function _impl() internal view returns (address) {
116 return address(uint160(uint256(vm.load(proxy, ERC1967Utils.IMPLEMENTATION_SLOT))));
117 }
118
119 function _proposeUpgrade(address impl) internal returns (uint256 id) {
120 vm.prank(admin);
121 id = aa.proposeUpgrade(impl, "");
122 }
123
124 function _clear(uint256 id) internal {
125 vm.prank(oracle);
126 aa.clear(id, EVIDENCE);
127 }
128
129 function _status(uint256 id) internal view returns (AirlockAdmin.Status) {
130 return aa.getProposal(id).status;
131 }
132
133 // ------------------------------------------------------------------ wiring (T6)
134
135 function test_wiring() public view {
136 assertTrue(aa.isWired());
137 assertEq(proxyAdmin.owner(), address(aa));
138 assertEq(aa.proxy(), proxy);
139 assertEq(aa.proxyAdmin(), address(proxyAdmin));
140 assertEq(aa.owner(), admin);
141 assertEq(aa.approver(), approver);
142 assertEq(aa.clearanceOracle(), oracle);
143 assertEq(aa.pausedImpl(), address(pausedImpl));
144 assertEq(aa.delayBlocks(), DELAY);
145 assertEq(aa.guardianCount(), 3);
146 assertTrue(aa.isGuardian(s1) && aa.isGuardian(s2) && aa.isGuardian(s3));
147 assertFalse(aa.isGuardian(admin));
148 assertEq(aa.nextId(), 1);
149 assertFalse(aa.paused());
150 assertEq(_impl(), address(v1));
151 assertEq(token.symbol(), "dBTC");
152 assertEq(token.balanceOf(h1), 100e8);
153 assertEq(token.owner(), address(aa));
154 }
155
156 function test_T6_ownerCannotBypassAirlock() public {
157 vm.prank(admin);
158 vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, admin));
159 proxyAdmin.upgradeAndCall(ITransparentUpgradeableProxy(proxy), address(evilMint), "");
160 assertEq(_impl(), address(v1));
161 assertTrue(aa.isWired());
162 }
163
164 /// A leaked owner key must not be able to mint (or grab the token) without going through a proposal: the
165 /// token's Ownable belongs to AirlockAdmin after adoption.
166 function test_T6_ownerCannotMintDirectly() public {
167 uint256 sup = token.totalSupply();
168 vm.startPrank(admin);
169 vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, admin));
170 token.mint(admin, 1_000_000e8);
171 vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, admin));
172 token.transferOwnership(admin);
173 vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, admin));
174 token.renounceOwnership();
175 vm.stopPrank();
176 assertEq(token.totalSupply(), sup);
177 assertEq(token.owner(), address(aa));
178 }
179
180 /// AirlockAdmin owns the token but has no generic call path, so nobody can reach mint through it.
181 function test_T6_tokenOwnerIsAirlockAdmin_noOneMints() public {
182 vm.prank(stranger);
183 vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, stranger));
184 token.mint(stranger, 1);
185 vm.prank(s1);
186 vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, s1));
187 token.mint(s1, 1);
188 assertEq(token.totalSupply(), 175e8);
189 }
190
191 // ------------------------------------------------------------------ T1: EvilMint vetoed
192
193 function test_T1_evilMintVetoed_executeRevertsProposalVetoed() public {
194 uint256 id = _proposeUpgrade(address(evilMint));
195 _advance(2);
196 vm.prank(s1);
197 aa.veto(id, R1);
198 assertEq(uint8(_status(id)), uint8(AirlockAdmin.Status.Vetoed));
199
200 _advance(DELAY); // well past readyBlock
201 vm.expectRevert(abi.encodeWithSelector(AirlockAdmin.ProposalVetoed.selector, id));
202 aa.execute(id);
203 assertEq(_impl(), address(v1));
204 }
205
206 // ------------------------------------------------------------------ v2: 29 blocks TooEarly, 30 ok (when cleared)
207
208 function test_v2_tooEarlyAt29_executesAt30() public {
209 uint256 b0 = vm.getBlockNumber();
210 uint256 id = _proposeUpgrade(address(v2));
211 AirlockAdmin.Proposal memory p = aa.getProposal(id);
212 assertEq(id, 1);
213 assertEq(p.readyBlock, b0 + DELAY);
214 assertEq(p.proposedBlock, b0);
215 assertEq(p.codehash, address(v2).codehash);
216 assertEq(p.dataHash, keccak256(""));
217 _clear(id);
218
219 uint256 sup = token.totalSupply();
220 uint256 bal1 = token.balanceOf(h1);
221 uint256 bal2 = token.balanceOf(h2);
222 uint256 bal3 = token.balanceOf(h3);
223
224 vm.roll(b0 + 29);
225 vm.expectRevert(abi.encodeWithSelector(AirlockAdmin.TooEarly.selector, id, uint64(b0 + 30), uint64(b0 + 29)));
226 aa.execute(id);
227
228 vm.roll(b0 + 30);
229 vm.expectEmit(true, false, false, true, address(aa));
230 emit AirlockAdmin.Executed(id, stranger);
231 vm.prank(stranger); // anyone may execute
232 aa.execute(id);
233
234 assertEq(_impl(), address(v2));
235 assertEq(DemoBTCV2(proxy).version(), "2");
236 assertEq(token.totalSupply(), sup);
237 assertEq(token.balanceOf(h1), bal1);
238 assertEq(token.balanceOf(h2), bal2);
239 assertEq(token.balanceOf(h3), bal3);
240 assertEq(token.owner(), address(aa));
241 assertEq(uint8(_status(id)), uint8(AirlockAdmin.Status.Executed));
242
243 vm.expectRevert(abi.encodeWithSelector(AirlockAdmin.AlreadyExecuted.selector, id));
244 aa.execute(id);
245 }
246
247 function test_proposedEvent() public {
248 uint256 ready = vm.getBlockNumber() + DELAY;
249 vm.expectEmit(true, true, true, true, address(aa));
250 emit AirlockAdmin.Proposed(1, 0, address(v2), 0, hex"", address(v2).codehash, uint64(ready));
251 _proposeUpgrade(address(v2));
252 assertEq(aa.nextId(), 2);
253 }
254
255 function test_upgradeWithInitData() public {
256 bytes memory data = abi.encodeCall(DemoBTC.mint, (h1, 1)); // runs as the proxy itself
257 vm.prank(admin);
258 uint256 id = aa.proposeUpgrade(address(v2), data);
259 assertEq(aa.getProposal(id).dataHash, keccak256(data));
260 assertEq(aa.getProposalData(id), data);
261 _clear(id);
262 _advance(DELAY);
263 // mint is onlyOwner; inside upgradeToAndCall msg.sender is the ProxyAdmin -> whole execute reverts
264 vm.expectRevert();
265 aa.execute(id);
266 assertEq(uint8(_status(id)), uint8(AirlockAdmin.Status.Pending));
267 assertEq(_impl(), address(v1));
268 }
269
270 // ------------------------------------------------------------------ clearance: the second door
271
272 /// Every proposal kind waits for a clearance, even long after its readyBlock.
273 function test_clearanceRequired_everyKind() public {
274 address s4 = makeAddr("guardian4");
275 vm.startPrank(admin);
276 uint256[4] memory ids = [
277 aa.proposeUpgrade(address(v2), ""),
278 aa.proposeSetGuardian(s4, true),
279 aa.proposeSetDelay(40),
280 aa.proposeSetPausedImpl(address(v2))
281 ];
282 vm.stopPrank();
283 _advance(DELAY * 100);
284 for (uint256 i = 0; i < 4; i++) {
285 assertEq(uint8(aa.getProposal(ids[i]).kind), i);
286 vm.expectRevert(abi.encodeWithSelector(AirlockAdmin.NotCleared.selector, ids[i]));
287 aa.execute(ids[i]);
288 assertEq(uint8(_status(ids[i])), uint8(AirlockAdmin.Status.Pending));
289 }
290 assertEq(_impl(), address(v1));
291 assertFalse(aa.isGuardian(s4));
292 assertEq(aa.delayBlocks(), DELAY);
293 assertEq(aa.pausedImpl(), address(pausedImpl));
294
295 for (uint256 i = 0; i < 4; i++) {
296 _clear(ids[i]);
297 aa.execute(ids[i]);
298 assertEq(uint8(_status(ids[i])), uint8(AirlockAdmin.Status.Executed));
299 }
300 assertEq(_impl(), address(v2));
301 assertTrue(aa.isGuardian(s4));
302 assertEq(aa.delayBlocks(), 40);
303 assertEq(aa.pausedImpl(), address(v2));
304 }
305
306 /// Time is checked first: a proposal that is neither ready nor cleared reports TooEarly; a cleared one still
307 /// waits the full cooldown.
308 function test_clearanceDoesNotShortenTheCooldown() public {
309 uint256 b0 = vm.getBlockNumber();
310 uint256 id = _proposeUpgrade(address(v2));
311 vm.expectRevert(abi.encodeWithSelector(AirlockAdmin.TooEarly.selector, id, uint64(b0 + DELAY), uint64(b0)));
312 aa.execute(id);
313 _clear(id);
314 vm.roll(b0 + DELAY - 1);
315 vm.expectRevert(
316 abi.encodeWithSelector(AirlockAdmin.TooEarly.selector, id, uint64(b0 + DELAY), uint64(b0 + DELAY - 1))
317 );
318 aa.execute(id);
319 vm.roll(b0 + DELAY);
320 aa.execute(id);
321 assertEq(_impl(), address(v2));
322 }
323
324 /// A guardian veto beats a clearance, whichever comes first.
325 function test_vetoBeatsClearance() public {
326 uint256 id = _proposeUpgrade(address(evilMint));
327 _clear(id);
328 assertEq(aa.clearanceOf(id), EVIDENCE);
329 vm.prank(s2);
330 aa.veto(id, R1);
331 assertEq(uint8(_status(id)), uint8(AirlockAdmin.Status.Vetoed));
332 _advance(DELAY);
333 vm.expectRevert(abi.encodeWithSelector(AirlockAdmin.ProposalVetoed.selector, id));
334 aa.execute(id);
335
336 // and a clearance after the veto is refused
337 uint256 id2 = _proposeUpgrade(address(evilMint));
338 vm.prank(s1);
339 aa.veto(id2, R1);
340 vm.prank(oracle);
341 vm.expectRevert(abi.encodeWithSelector(AirlockAdmin.ProposalVetoed.selector, id2));
342 aa.clear(id2, EVIDENCE);
343 vm.prank(approver);
344 vm.expectRevert(abi.encodeWithSelector(AirlockAdmin.ProposalVetoed.selector, id2));
345 aa.clear(id2, EVIDENCE);
346 assertEq(aa.clearanceOf(id2), bytes32(0));
347 assertEq(_impl(), address(v1));
348 }
349
350 function test_clearByOracle_event() public {
351 uint256 id = _proposeUpgrade(address(v2));
352 vm.expectEmit(true, true, false, true, address(aa));
353 emit AirlockAdmin.Cleared(id, oracle, EVIDENCE);
354 _clear(id);
355 assertEq(aa.clearanceOf(id), EVIDENCE);
356 }
357
358 /// The approver is the manual second door (e.g. while the oracle is down).
359 function test_clearByApprover() public {
360 uint256 id = _proposeUpgrade(address(v2));
361 bytes32 ev = keccak256("approver-review");
362 vm.expectEmit(true, true, false, true, address(aa));
363 emit AirlockAdmin.Cleared(id, approver, ev);
364 vm.prank(approver);
365 aa.clear(id, ev);
366 assertEq(aa.clearanceOf(id), ev);
367 _advance(DELAY);
368 aa.execute(id);
369 assertEq(_impl(), address(v2));
370 }
371
372 function test_onlyClearerRolesClear() public {
373 uint256 id = _proposeUpgrade(address(v2));
374 address[5] memory others = [stranger, admin, s1, s2, s3];
375 for (uint256 i = 0; i < others.length; i++) {
376 vm.prank(others[i]);
377 vm.expectRevert(AirlockAdmin.NotClearer.selector);
378 aa.clear(id, EVIDENCE);
379 }
380 assertEq(aa.clearanceOf(id), bytes32(0));
381 }
382
383 /// A second clearance (by anyone allowed) keeps the first evidence hash; it is still observable as an event.
384 function test_clearIdempotent_keepsFirstHash() public {
385 uint256 id = _proposeUpgrade(address(v2));
386 bytes32 ev2 = keccak256("second");
387 _clear(id);
388 vm.expectEmit(true, true, false, true, address(aa));
389 emit AirlockAdmin.Cleared(id, approver, ev2);
390 vm.prank(approver);
391 aa.clear(id, ev2);
392 assertEq(aa.clearanceOf(id), EVIDENCE);
393 _clear(id);
394 assertEq(aa.clearanceOf(id), EVIDENCE);
395 }
396
397 function test_clear_requiresPendingAndEvidence() public {
398 vm.startPrank(oracle);
399 vm.expectRevert(abi.encodeWithSelector(AirlockAdmin.UnknownProposal.selector, 0));
400 aa.clear(0, EVIDENCE);
401 vm.expectRevert(abi.encodeWithSelector(AirlockAdmin.UnknownProposal.selector, 5));
402 aa.clear(5, EVIDENCE);
403 vm.stopPrank();
404
405 uint256 id = _proposeUpgrade(address(v2));
406 vm.prank(oracle);
407 vm.expectRevert(AirlockAdmin.EmptyEvidence.selector);
408 aa.clear(id, bytes32(0));
409
410 _clear(id);
411 _advance(DELAY);
412 aa.execute(id);
413 vm.prank(oracle);
414 vm.expectRevert(abi.encodeWithSelector(AirlockAdmin.AlreadyExecuted.selector, id));
415 aa.clear(id, EVIDENCE);
416 }
417
418 // ------------------------------------------------------------------ oracleJudged: the oracle's own verdict
419
420 /// An AirlockAdmin whose clearance oracle is also a guardian, as in the deployment (AirlockReceiver is both).
421 /// Not wired to the proxy: only proposals, clearances and vetoes are exercised.
422 function _adminWithOracleGuardian() internal returns (AirlockAdmin a) {
423 address[] memory g = new address[](2);
424 g[0] = oracle;
425 g[1] = s1;
426 a = _newAdmin(admin, g, approver, address(pausedImpl), DELAY, oracle);
427 }
428
429 function test_oracleJudged_onlyTheOraclesClearSetsIt() public {
430 uint256 id = _proposeUpgrade(address(v2));
431 assertFalse(aa.oracleJudged(id));
432 bytes32 manual = keccak256("approver-review");
433 vm.prank(approver);
434 aa.clear(id, manual);
435 assertFalse(aa.oracleJudged(id), "a manual clearance is not an oracle verdict");
436 assertEq(aa.clearanceOf(id), manual);
437
438 // the oracle judges the approver-cleared proposal anyway: first evidence kept, oracleJudged set
439 vm.expectEmit(true, true, false, true, address(aa));
440 emit AirlockAdmin.Cleared(id, oracle, EVIDENCE);
441 _clear(id);
442 assertTrue(aa.oracleJudged(id));
443 assertEq(aa.clearanceOf(id), manual);
444
445 uint256 id2 = _proposeUpgrade(address(v2));
446 _clear(id2);
447 assertTrue(aa.oracleJudged(id2));
448 assertFalse(aa.oracleJudged(id2 + 1)); // unknown ids read false
449 assertFalse(aa.oracleJudged(0));
450 }
451
452 /// A veto by the oracle (it is also a guardian) sets oracleJudged; a veto by another guardian does not.
453 function test_oracleJudged_setByTheOraclesVetoOnly() public {
454 AirlockAdmin a = _adminWithOracleGuardian();
455 vm.startPrank(admin);
456 uint256 byHuman = a.proposeUpgrade(address(evilMint), "");
457 uint256 byOracle = a.proposeUpgrade(address(evilMint), "");
458 vm.stopPrank();
459
460 vm.prank(s1);
461 a.veto(byHuman, R1);
462 assertEq(uint8(a.getProposal(byHuman).status), uint8(AirlockAdmin.Status.Vetoed));
463 assertFalse(a.oracleJudged(byHuman));
464 vm.prank(oracle); // the oracle may still record its own veto on a vetoed proposal
465 a.veto(byHuman, R1);
466 assertTrue(a.oracleJudged(byHuman));
467
468 vm.prank(oracle);
469 a.veto(byOracle, R1);
470 assertEq(uint8(a.getProposal(byOracle).status), uint8(AirlockAdmin.Status.Vetoed));
471 assertTrue(a.oracleJudged(byOracle));
472 }
473
474 /// The case the sweeper exists for: the approver cleared by hand, the oracle never judged (oracleJudged false,
475 /// still Pending), the oracle then judges and vetoes. The veto wins over the manual clearance.
476 function test_oracleJudged_approverClearedThenOracleVetoes_vetoWins() public {
477 AirlockAdmin a = _adminWithOracleGuardian();
478 vm.prank(admin);
479 uint256 id = a.proposeUpgrade(address(evilDrain), "");
480 vm.prank(approver); // a mistaken manual clearance
481 a.clear(id, keccak256("approver-review"));
482 assertEq(uint8(a.getProposal(id).status), uint8(AirlockAdmin.Status.Pending));
483 assertFalse(a.oracleJudged(id), "still to be judged by the oracle");
484
485 vm.prank(oracle);
486 a.veto(id, R3);
487 assertTrue(a.oracleJudged(id));
488 assertEq(uint8(a.getProposal(id).status), uint8(AirlockAdmin.Status.Vetoed));
489 _advance(DELAY);
490 vm.expectRevert(abi.encodeWithSelector(AirlockAdmin.ProposalVetoed.selector, id));
491 a.execute(id);
492 }
493
494 function testFuzz_oracleJudged_onlyTheOracle(address who, bool clearFirst) public {
495 vm.assume(who != oracle);
496 AirlockAdmin a = _adminWithOracleGuardian();
497 vm.prank(admin);
498 uint256 id = a.proposeUpgrade(address(v2), "");
499 if (clearFirst) {
500 vm.prank(approver);
501 a.clear(id, EVIDENCE);
502 }
503 vm.prank(who);
504 try a.clear(id, EVIDENCE) {} catch {}
505 vm.prank(who);
506 try a.veto(id, R1) {} catch {}
507 assertFalse(a.oracleJudged(id));
508 }
509
510 // ------------------------------------------------------------------ proposalDigest
511
512 function _expectedDigest(uint256 id) internal view returns (bytes32) {
513 AirlockAdmin.Proposal memory p = aa.getProposal(id);
514 return keccak256(abi.encode(uint8(p.kind), p.target, p.value, p.codehash, p.dataHash, uint64(p.proposedBlock)));
515 }
516
517 function test_proposalDigest_encoding() public {
518 bytes memory data = abi.encodeCall(DemoBTCV2.version, ());
519 vm.prank(admin);
520 uint256 id = aa.proposeUpgrade(address(v2), data);
521 bytes32 want = keccak256(
522 abi.encode(
523 uint8(0), address(v2), uint256(0), address(v2).codehash, keccak256(data), uint64(vm.getBlockNumber())
524 )
525 );
526 assertEq(aa.proposalDigest(id), want);
527 assertEq(aa.proposalDigest(id), _expectedDigest(id));
528
529 vm.prank(admin);
530 uint256 g = aa.proposeSetGuardian(s1, false);
531 assertEq(
532 aa.proposalDigest(g),
533 keccak256(abi.encode(uint8(1), s1, uint256(0), bytes32(0), keccak256(""), uint64(vm.getBlockNumber())))
534 );
535 }
536
537 /// The digest binds the content: another implementation, other init data or another block gives another digest.
538 function test_proposalDigest_bindsContent() public {
539 uint256 a = _proposeUpgrade(address(v2));
540 uint256 b = _proposeUpgrade(address(v2)); // same content, same block
541 uint256 c = _proposeUpgrade(address(evilMint));
542 vm.prank(admin);
543 uint256 d = aa.proposeUpgrade(address(v2), hex"01");
544 _advance(1);
545 uint256 e = _proposeUpgrade(address(v2));
546 assertEq(aa.proposalDigest(a), aa.proposalDigest(b)); // the id is bound by the verdict signature
547 assertTrue(aa.proposalDigest(a) != aa.proposalDigest(c));
548 assertTrue(aa.proposalDigest(a) != aa.proposalDigest(d));
549 assertTrue(aa.proposalDigest(a) != aa.proposalDigest(e));
550 }
551
552 function test_proposalDigest_unknownReverts() public {
553 vm.expectRevert(abi.encodeWithSelector(AirlockAdmin.UnknownProposal.selector, 0));
554 aa.proposalDigest(0);
555 vm.expectRevert(abi.encodeWithSelector(AirlockAdmin.UnknownProposal.selector, 1));
556 aa.proposalDigest(1);
557 }
558
559 /// The digest stays readable after a veto or an execution (evidence can be checked later).
560 function test_proposalDigest_stableAcrossStatus() public {
561 uint256 id = _proposeUpgrade(address(v2));
562 bytes32 d0 = aa.proposalDigest(id);
563 _clear(id);
564 _advance(DELAY);
565 aa.execute(id);
566 assertEq(aa.proposalDigest(id), d0);
567 }
568
569 // ------------------------------------------------------------------ EIP-7702 and codehash pinning
570
571 function _designator(address delegate) internal pure returns (bytes memory) {
572 return abi.encodePacked(hex"ef0100", delegate);
573 }
574
575 /// A real EIP-7702 delegation (vm.signAndAttachDelegation): the EOA runs v2's code, but its code is the
576 /// 23-byte designator and its key can re-point it at any time. It is refused as an upgrade target.
577 function test_7702_delegatedEoaRefusedAsUpgradeTarget() public {
578 Vm.Wallet memory w = vm.createWallet("delegatingEoa");
579 vm.signAndAttachDelegation(address(v2), w.privateKey);
580 assertEq(DemoBTCV2(w.addr).version(), "2"); // the delegation is live: the EOA runs v2
581 assertEq(w.addr.code, _designator(address(v2)));
582 assertEq(w.addr.codehash, keccak256(_designator(address(v2))));
583
584 vm.prank(admin);
585 vm.expectRevert(abi.encodeWithSelector(AirlockAdmin.DelegatedAccount.selector, w.addr));
586 aa.proposeUpgrade(w.addr, "");
587 assertEq(aa.nextId(), 1, "nothing proposed");
588
589 // why: re-delegating changes the code that runs (and the codehash) with one signature, no deployment
590 vm.signAndAttachDelegation(address(evilMint), w.privateKey);
591 DemoBTC(w.addr).decimals();
592 assertEq(w.addr.code, _designator(address(evilMint)));
593 }
594
595 function test_7702_delegatedEoaRefusedAsPausedImpl() public {
596 address eoa = makeAddr("delegatedEoa");
597 vm.etch(eoa, _designator(address(pausedImpl)));
598 vm.prank(admin);
599 vm.expectRevert(abi.encodeWithSelector(AirlockAdmin.DelegatedAccount.selector, eoa));
600 aa.proposeSetPausedImpl(eoa);
601 vm.prank(admin);
602 vm.expectRevert(abi.encodeWithSelector(AirlockAdmin.DelegatedAccount.selector, eoa));
603 aa.proposeUpgrade(eoa, "");
604 }
605
606 /// Whatever it delegates to (a contract, an EOA, a precompile), a designator is refused.
607 function testFuzz_7702_anyDesignatorRefused(address delegate, bool asPausedImpl) public {
608 address eoa = makeAddr("designated");
609 vm.etch(eoa, _designator(delegate));
610 vm.prank(admin);
611 vm.expectRevert(abi.encodeWithSelector(AirlockAdmin.DelegatedAccount.selector, eoa));
612 if (asPausedImpl) aa.proposeSetPausedImpl(eoa);
613 else aa.proposeUpgrade(eoa, "");
614 }
615
616 /// Ordinary short code that merely contains 0xef01 later on is not a designator.
617 function test_7702_shortOrdinaryCodeAccepted() public {
618 address tiny = makeAddr("tiny");
619 vm.etch(tiny, hex"00ef01");
620 vm.prank(admin);
621 uint256 id = aa.proposeUpgrade(tiny, "");
622 assertEq(aa.getProposal(id).codehash, keccak256(hex"00ef01"));
623 }
624
625 function test_7702_constructorRefusesDelegatedPausedImplOrOracle() public {
626 address eoa = makeAddr("delegatedEoa");
627 vm.etch(eoa, _designator(address(pausedImpl)));
628 vm.expectRevert(abi.encodeWithSelector(AirlockAdmin.DelegatedAccount.selector, eoa));
629 _newAdmin(admin, _guardians(), approver, eoa, DELAY, oracle);
630
631 address eoaOracle = makeAddr("delegatedOracle");
632 vm.etch(eoaOracle, _designator(oracle));
633 vm.expectRevert(abi.encodeWithSelector(AirlockAdmin.DelegatedAccount.selector, eoaOracle));
634 _newAdmin(admin, _guardians(), approver, address(pausedImpl), DELAY, eoaOracle);
635 }
636
637 /// A cleared upgrade whose target's code changed after it was proposed (a re-created CREATE2 address, or a
638 /// designator: modelled with vm.etch) cannot execute: CodehashChanged, the proposal stays Pending, and it
639 /// runs once the judged code is back.
640 function test_codehashChanged_upgradeRefused() public {
641 DemoBTCV2 cand = new DemoBTCV2();
642 bytes memory judged = address(cand).code;
643 bytes32 judgedHash = address(cand).codehash;
644 uint256 id = _proposeUpgrade(address(cand));
645 assertEq(aa.getProposal(id).codehash, judgedHash);
646 _clear(id);
647 _advance(DELAY);
648
649 bytes[3] memory swaps = [address(evilMint).code, _designator(address(evilMint)), bytes("")];
650 for (uint256 i = 0; i < swaps.length; i++) {
651 vm.etch(address(cand), swaps[i]);
652 vm.expectRevert(
653 abi.encodeWithSelector(AirlockAdmin.CodehashChanged.selector, id, judgedHash, address(cand).codehash)
654 );
655 vm.prank(stranger);
656 aa.execute(id);
657 assertEq(uint8(_status(id)), uint8(AirlockAdmin.Status.Pending));
658 assertEq(_impl(), address(v1));
659 }
660
661 vm.etch(address(cand), judged);
662 aa.execute(id);
663 assertEq(_impl(), address(cand));
664 assertEq(DemoBTCV2(proxy).version(), "2");
665 }
666
667 /// The same check guards the break-glass target: breakGlass would switch to whatever code is there.
668 function test_codehashChanged_setPausedImplRefused() public {
669 PausedImpl cand = new PausedImpl();
670 bytes32 judgedHash = address(cand).codehash;
671 vm.prank(admin);
672 uint256 id = aa.proposeSetPausedImpl(address(cand));
673 _clear(id);
674 _advance(DELAY);
675 vm.etch(address(cand), address(evilMint).code);
676 vm.expectRevert(
677 abi.encodeWithSelector(AirlockAdmin.CodehashChanged.selector, id, judgedHash, address(evilMint).codehash)
678 );
679 aa.execute(id);
680 assertEq(aa.pausedImpl(), address(pausedImpl));
681
682 vm.etch(address(cand), _designator(address(evilMint))); // a designator is refused the same way
683 vm.expectRevert(
684 abi.encodeWithSelector(
685 AirlockAdmin.CodehashChanged.selector, id, judgedHash, keccak256(_designator(address(evilMint)))
686 )
687 );
688 aa.execute(id);
689 assertEq(aa.pausedImpl(), address(pausedImpl));
690 }
691
692 function testFuzz_codehashChanged(bytes calldata other) public {
693 vm.assume(other.length > 0 && other.length < 2_000 && other[0] != 0xef); // 0xef: designator / EOF, above
694 DemoBTCV2 cand = new DemoBTCV2();
695 bytes32 judgedHash = address(cand).codehash;
696 vm.assume(keccak256(other) != judgedHash);
697 uint256 id = _proposeUpgrade(address(cand));
698 _clear(id);
699 _advance(DELAY);
700 vm.etch(address(cand), other);
701 vm.expectRevert(abi.encodeWithSelector(AirlockAdmin.CodehashChanged.selector, id, judgedHash, keccak256(other)));
702 aa.execute(id);
703 }
704
705 /// Guardian and delay changes carry no code: their codehash is zero and nothing is rechecked.
706 function test_codehashCheck_onlyForCodeKinds() public {
707 vm.startPrank(admin);
708 uint256 g = aa.proposeSetGuardian(makeAddr("guardian4"), true);
709 uint256 d = aa.proposeSetDelay(40);
710 vm.stopPrank();
711 assertEq(aa.getProposal(g).codehash, bytes32(0));
712 assertEq(aa.getProposal(d).codehash, bytes32(0));
713 _clear(g);
714 _clear(d);
715 _advance(DELAY);
716 aa.execute(g);
717 aa.execute(d);
718 assertEq(aa.delayBlocks(), 40);
719 }
720
721 // ------------------------------------------------------------------ roles never overlap
722
723 function test_roleOverlap_constructor() public {
724 address[] memory g = _guardians();
725 // approver == owner
726 vm.expectRevert(AirlockAdmin.RoleOverlap.selector);
727 _newAdmin(admin, g, admin, address(pausedImpl), DELAY, oracle);
728 // owner listed as a guardian
729 address[] memory withOwner = new address[](2);
730 withOwner[0] = s1;
731 withOwner[1] = admin;
732 vm.expectRevert(AirlockAdmin.RoleOverlap.selector);
733 _newAdmin(admin, withOwner, approver, address(pausedImpl), DELAY, oracle);
734 // clearance oracle == owner (an owner contract cannot clear its own proposals)
735 vm.expectRevert(AirlockAdmin.RoleOverlap.selector);
736 _newAdmin(oracle, g, approver, address(pausedImpl), DELAY, oracle);
737 // clearance oracle missing or without code
738 vm.expectRevert(AirlockAdmin.ZeroAddress.selector);
739 _newAdmin(admin, g, approver, address(pausedImpl), DELAY, address(0));
740 vm.expectRevert(abi.encodeWithSelector(AirlockAdmin.NoCode.selector, stranger));
741 _newAdmin(admin, g, approver, address(pausedImpl), DELAY, stranger);
742 // the oracle may also be a guardian (AirlockReceiver is both)
743 address[] memory withOracle = new address[](2);
744 withOracle[0] = oracle;
745 withOracle[1] = s1;
746 AirlockAdmin ok = _newAdmin(admin, withOracle, approver, address(pausedImpl), DELAY, oracle);
747 assertTrue(ok.isGuardian(oracle));
748 }
749
750 function test_roleOverlap_guardianChange() public {
751 vm.prank(admin);
752 vm.expectRevert(AirlockAdmin.RoleOverlap.selector);
753 aa.proposeSetGuardian(admin, true);
754 // removing the owner (never a guardian) is a harmless no-op proposal
755 vm.prank(admin);
756 uint256 id = aa.proposeSetGuardian(admin, false);
757 _clear(id);
758 _advance(DELAY);
759 aa.execute(id);
760 assertFalse(aa.isGuardian(admin));
761 assertEq(aa.guardianCount(), 3);
762 }
763
764 // ------------------------------------------------------------------ T4 / T5: config changes
765
766 function test_T4_removeGuardian_vetoable_notInstant() public {
767 vm.prank(admin);
768 uint256 id = aa.proposeSetGuardian(s1, false);
769 AirlockAdmin.Proposal memory p = aa.getProposal(id);
770 assertEq(uint8(p.kind), uint8(AirlockAdmin.Kind.SetGuardian));
771 assertEq(p.target, s1);
772 assertEq(p.value, 0);
773 assertTrue(aa.isGuardian(s1)); // not removed yet
774
775 vm.expectRevert(
776 abi.encodeWithSelector(AirlockAdmin.TooEarly.selector, id, p.readyBlock, uint64(vm.getBlockNumber()))
777 );
778 aa.execute(id);
779
780 vm.prank(s1); // the guardian being removed can still veto
781 aa.veto(id, R7);
782 _advance(DELAY);
783 vm.expectRevert(abi.encodeWithSelector(AirlockAdmin.ProposalVetoed.selector, id));
784 aa.execute(id);
785 assertTrue(aa.isGuardian(s1));
786 assertEq(aa.guardianCount(), 3);
787 }
788
789 function test_T5_shortenDelay_vetoable_notInstant() public {
790 vm.prank(admin);
791 uint256 id = aa.proposeSetDelay(10);
792 assertEq(aa.getProposal(id).value, 10);
793 assertEq(aa.delayBlocks(), DELAY);
794
795 vm.expectRevert();
796 aa.execute(id); // TooEarly
797
798 vm.prank(s2);
799 aa.veto(id, R7);
800 _advance(DELAY);
801 vm.expectRevert(abi.encodeWithSelector(AirlockAdmin.ProposalVetoed.selector, id));
802 aa.execute(id);
803 assertEq(aa.delayBlocks(), DELAY);
804 }
805
806 function test_T5_cannotProposeDelayBelowMinimum() public {
807 vm.prank(admin);
808 vm.expectRevert(abi.encodeWithSelector(AirlockAdmin.InvalidDelay.selector, uint64(0)));
809 aa.proposeSetDelay(0);
810 }
811
812 function test_configChanges_executeAfterDelayIfClearedAndNotVetoed() public {
813 address s4 = makeAddr("guardian4");
814 vm.startPrank(admin);
815 uint256 add = aa.proposeSetGuardian(s4, true);
816 uint256 rm = aa.proposeSetGuardian(s1, false);
817 uint256 dl = aa.proposeSetDelay(40);
818 uint256 pi = aa.proposeSetPausedImpl(address(v2));
819 vm.stopPrank();
820 _clear(add);
821 _clear(rm);
822 _clear(dl);
823 _clear(pi);
824 _advance(DELAY);
825 aa.execute(add);
826 aa.execute(rm);
827 aa.execute(dl);
828 aa.execute(pi);
829 assertTrue(aa.isGuardian(s4));
830 assertFalse(aa.isGuardian(s1));
831 assertEq(aa.guardianCount(), 3);
832 assertEq(aa.delayBlocks(), 40);
833 assertEq(aa.pausedImpl(), address(v2));
834
835 // the new delay applies to the next proposal
836 uint256 b = vm.getBlockNumber();
837 uint256 id = _proposeUpgrade(address(v2));
838 assertEq(aa.getProposal(id).readyBlock, b + 40);
839
840 vm.prank(s1);
841 vm.expectRevert(AirlockAdmin.NotGuardian.selector);
842 aa.veto(id, R1);
843 }
844
845 function test_cannotRemoveLastGuardian() public {
846 vm.startPrank(admin);
847 uint256 a = aa.proposeSetGuardian(s1, false);
848 uint256 b = aa.proposeSetGuardian(s2, false);
849 uint256 c = aa.proposeSetGuardian(s3, false);
850 vm.stopPrank();
851 _clear(a);
852 _clear(b);
853 _clear(c);
854 _advance(DELAY);
855 aa.execute(a);
856 aa.execute(b);
857 vm.expectRevert(abi.encodeWithSelector(AirlockAdmin.GuardianBounds.selector, 0));
858 aa.execute(c);
859 assertTrue(aa.isGuardian(s3));
860 }
861
862 function test_proposeSetPausedImpl_requiresCode() public {
863 vm.prank(admin);
864 vm.expectRevert(abi.encodeWithSelector(AirlockAdmin.NoCode.selector, stranger));
865 aa.proposeSetPausedImpl(stranger);
866 vm.prank(admin);
867 vm.expectRevert(abi.encodeWithSelector(AirlockAdmin.NoCode.selector, stranger));
868 aa.proposeUpgrade(stranger, "");
869 }
870
871 // ------------------------------------------------------------------ breakGlass
872
873 function test_breakGlass_immediate_thenUnpausePassesBothDoors() public {
874 vm.expectEmit(true, false, false, true, address(aa));
875 emit AirlockAdmin.BrokeGlass(s3, address(pausedImpl));
876 vm.prank(s3);
877 aa.breakGlass();
878 assertTrue(aa.paused());
879 assertEq(_impl(), address(pausedImpl));
880
881 // views still work, transfers and mints revert
882 assertEq(token.balanceOf(h1), 100e8);
883 assertEq(token.totalSupply(), 175e8);
884 assertEq(token.owner(), address(aa));
885 vm.prank(h1);
886 vm.expectRevert(PausedImpl.TokenPaused.selector);
887 token.transfer(h2, 1);
888 vm.prank(address(aa)); // even the token owner cannot mint while paused
889 vm.expectRevert(PausedImpl.TokenPaused.selector);
890 token.mint(address(aa), 1);
891
892 vm.prank(admin);
893 vm.expectRevert(AirlockAdmin.AlreadyPaused.selector);
894 aa.breakGlass();
895
896 // unpause = upgrade back, which waits the full delay and needs a clearance
897 uint256 b0 = vm.getBlockNumber();
898 uint256 id = _proposeUpgrade(address(v2));
899 vm.roll(b0 + DELAY - 1);
900 vm.expectRevert(
901 abi.encodeWithSelector(AirlockAdmin.TooEarly.selector, id, uint64(b0 + DELAY), uint64(b0 + DELAY - 1))
902 );
903 aa.execute(id);
904 vm.roll(b0 + DELAY);
905 vm.expectRevert(abi.encodeWithSelector(AirlockAdmin.NotCleared.selector, id));
906 aa.execute(id);
907 assertTrue(aa.paused());
908
909 _clear(id);
910 aa.execute(id);
911 assertFalse(aa.paused());
912 assertEq(_impl(), address(v2));
913 vm.prank(h1);
914 assertTrue(token.transfer(h2, 1e8));
915 assertEq(token.balanceOf(h2), 51e8);
916 }
917
918 function test_breakGlass_byOwner() public {
919 vm.prank(admin);
920 aa.breakGlass();
921 assertTrue(aa.paused());
922 }
923
924 function test_breakGlass_onlyOwnerOrGuardian() public {
925 address[3] memory others = [stranger, approver, oracle];
926 for (uint256 i = 0; i < others.length; i++) {
927 vm.prank(others[i]);
928 vm.expectRevert(AirlockAdmin.NotOwnerOrGuardian.selector);
929 aa.breakGlass();
930 }
931 }
932
933 // ------------------------------------------------------------------ access control
934
935 function test_onlyOwnerProposes() public {
936 address[4] memory others = [stranger, s1, approver, oracle];
937 for (uint256 i = 0; i < others.length; i++) {
938 vm.startPrank(others[i]);
939 vm.expectRevert(AirlockAdmin.NotOwner.selector);
940 aa.proposeUpgrade(address(v2), "");
941 vm.expectRevert(AirlockAdmin.NotOwner.selector);
942 aa.proposeSetGuardian(stranger, true);
943 vm.expectRevert(AirlockAdmin.NotOwner.selector);
944 aa.proposeSetDelay(40);
945 vm.expectRevert(AirlockAdmin.NotOwner.selector);
946 aa.proposeSetPausedImpl(address(v2));
947 vm.stopPrank();
948 }
949 }
950
951 function test_onlyGuardianVetoesAndAttaches() public {
952 uint256 id = _proposeUpgrade(address(evilMint));
953 address[4] memory others = [stranger, admin, approver, oracle];
954 for (uint256 i = 0; i < others.length; i++) {
955 vm.prank(others[i]);
956 vm.expectRevert(AirlockAdmin.NotGuardian.selector);
957 aa.veto(id, R1);
958 vm.prank(others[i]);
959 vm.expectRevert(AirlockAdmin.NotGuardian.selector);
960 aa.attachReason(id, bytes32(uint256(1)));
961 }
962 assertEq(uint8(_status(id)), uint8(AirlockAdmin.Status.Pending));
963
964 bytes32 report = keccak256(unicode"报告");
965 vm.expectEmit(true, false, false, true, address(aa));
966 emit AirlockAdmin.ReasonAttached(id, s1, report);
967 vm.prank(s1);
968 aa.attachReason(id, report);
969
970 vm.prank(s1);
971 vm.expectRevert(abi.encodeWithSelector(AirlockAdmin.UnknownProposal.selector, 99));
972 aa.attachReason(99, report);
973 }
974
975 function test_onlyApproverApprovesCodehash() public {
976 bytes32 h = address(v2).codehash;
977 address[4] memory others = [stranger, admin, s1, oracle];
978 for (uint256 i = 0; i < others.length; i++) {
979 vm.prank(others[i]);
980 vm.expectRevert(AirlockAdmin.NotApprover.selector);
981 aa.approveCodehash(h);
982 }
983 assertFalse(aa.isApprovedCodehash(h));
984 vm.expectEmit(true, false, false, true, address(aa));
985 emit AirlockAdmin.CodehashApproved(h, approver);
986 vm.prank(approver);
987 aa.approveCodehash(h);
988 assertTrue(aa.isApprovedCodehash(h));
989 }
990
991 // ------------------------------------------------------------------ veto idempotency
992
993 function test_vetoIdempotent_threeGuardians() public {
994 uint256 id = _proposeUpgrade(address(evilFee));
995 address[3] memory g = [s1, s2, s3];
996 vm.recordLogs();
997 for (uint256 i = 0; i < 3; i++) {
998 vm.expectEmit(true, true, false, true, address(aa));
999 emit AirlockAdmin.Vetoed(id, g[i], R3, i + 1);
1000 vm.prank(g[i]);
1001 aa.veto(id, R3);
1002 assertEq(uint8(_status(id)), uint8(AirlockAdmin.Status.Vetoed));
1003 }
1004 Vm.Log[] memory logs = vm.getRecordedLogs();
1005 uint256 n;
1006 for (uint256 i = 0; i < logs.length; i++) {
1007 if (logs[i].emitter == address(aa) && logs[i].topics[0] == AirlockAdmin.Vetoed.selector) n++;
1008 }
1009 assertEq(n, 3);
1010 assertEq(aa.getProposal(id).vetoCount, 3);
1011
1012 // the same guardian again: still Vetoed, counted once, event emitted again
1013 vm.expectEmit(true, true, false, true, address(aa));
1014 emit AirlockAdmin.Vetoed(id, s1, R3, 3);
1015 vm.prank(s1);
1016 aa.veto(id, R3);
1017 assertEq(aa.getProposal(id).vetoCount, 3);
1018 }
1019
1020 function test_vetoExecutedReverts_unknownReverts() public {
1021 uint256 id = _proposeUpgrade(address(v2));
1022 _clear(id);
1023 _advance(DELAY);
1024 aa.execute(id);
1025 vm.prank(s1);
1026 vm.expectRevert(abi.encodeWithSelector(AirlockAdmin.AlreadyExecuted.selector, id));
1027 aa.veto(id, R1);
1028
1029 vm.prank(s1);
1030 vm.expectRevert(abi.encodeWithSelector(AirlockAdmin.UnknownProposal.selector, 0));
1031 aa.veto(0, R1);
1032 vm.expectRevert(abi.encodeWithSelector(AirlockAdmin.UnknownProposal.selector, 7));
1033 aa.execute(7);
1034 }
1035
1036 // ------------------------------------------------------------------ attack impls really misbehave
1037
1038 /// Same trick as the workflow's state override: point the live proxy's implementation slot at the candidate
1039 /// and act as a stranger. The proxy's storage (balances, owner) is untouched.
1040 function _overrideImpl(address impl) internal {
1041 vm.store(proxy, ERC1967Utils.IMPLEMENTATION_SLOT, bytes32(uint256(uint160(impl))));
1042 }
1043
1044 function test_attack_evilMint_strangerMints() public {
1045 uint256 sup = token.totalSupply();
1046 _overrideImpl(address(evilMint));
1047 vm.prank(stranger);
1048 token.mint(stranger, 1_000_000e8);
1049 assertEq(token.totalSupply(), sup + 1_000_000e8);
1050 assertEq(token.balanceOf(stranger), 1_000_000e8);
1051 }
1052
1053 function test_attack_v1_strangerCannotMint() public {
1054 vm.prank(stranger);
1055 vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, stranger));
1056 token.mint(stranger, 1);
1057 }
1058
1059 function test_attack_evilOwner_strangerTakesOwnership() public {
1060 _overrideImpl(address(evilOwner));
1061 vm.prank(stranger);
1062 EvilOwnerImpl(proxy).claimOwner();
1063 assertEq(token.owner(), stranger);
1064 }
1065
1066 function test_attack_evilFee_transferNotConserved() public {
1067 _overrideImpl(address(evilFee));
1068 uint256 sup = token.totalSupply();
1069 uint256 ownerBefore = token.balanceOf(address(aa));
1070 vm.prank(h1);
1071 token.transfer(stranger, 10e8);
1072 assertEq(token.balanceOf(h1), 90e8); // holder lost X
1073 assertEq(token.balanceOf(stranger), 9e8); // receiver got only 0.9 X
1074 // 10% skimmed to owner(), which is AirlockAdmin: the R3 invariant (receiver +X, everyone else on the
1075 // watch list unchanged) is still violated, so the oracle still vetoes.
1076 assertEq(token.balanceOf(address(aa)), ownerBefore + 1e8);
1077 assertEq(token.balanceOf(admin), 0);
1078 assertEq(token.totalSupply(), sup);
1079 }
1080
1081 /// R3 scenario end to end with AirlockAdmin as token owner: the EvilFee skim lands in AirlockAdmin (stuck
1082 /// there, no call path), the transfer is still not conserved for a fresh receiver, and the R3 veto keeps the
1083 /// proposal from ever executing, even with a clearance on record.
1084 function test_T3_evilFee_skimToAirlockAdmin_R3StillHolds_andVetoed() public {
1085 address receiver = makeAddr("freshReceiver");
1086 uint256 x = 10e8;
1087 uint256 snap = vm.snapshotState();
1088 _overrideImpl(address(evilFee)); // what the workflow's state override sees
1089 uint256 h1Before = token.balanceOf(h1);
1090 uint256 aaBefore = token.balanceOf(address(aa));
1091 vm.prank(h1);
1092 token.transfer(receiver, x);
1093 int256 dHolder = int256(token.balanceOf(h1)) - int256(h1Before);
1094 int256 dReceiver = int256(token.balanceOf(receiver));
1095 int256 dOwner = int256(token.balanceOf(address(aa))) - int256(aaBefore);
1096 assertEq(dHolder, -int256(x));
1097 assertTrue(dReceiver != int256(x)); // R3 violated
1098 assertEq(dOwner, int256(x / 10)); // skimmed to AirlockAdmin
1099 vm.revertToState(snap);
1100
1101 uint256 id = _proposeUpgrade(address(evilFee));
1102 vm.prank(approver); // a mistaken manual clearance
1103 aa.clear(id, EVIDENCE);
1104 vm.prank(s2);
1105 aa.veto(id, R3);
1106 _advance(DELAY);
1107 vm.expectRevert(abi.encodeWithSelector(AirlockAdmin.ProposalVetoed.selector, id));
1108 aa.execute(id);
1109 assertEq(_impl(), address(v1));
1110 vm.prank(h1);
1111 token.transfer(receiver, x);
1112 assertEq(token.balanceOf(receiver), x); // v1 still conserves
1113 }
1114
1115 /// EvilBaseFee behaves exactly like v1 while block.basefee is zero (what eth_simulateV1 without validation sees).
1116 function test_attack_evilBaseFee_likeV1AtZeroBasefee() public {
1117 _overrideImpl(address(evilBaseFee));
1118 vm.fee(0);
1119 uint256 sup = token.totalSupply();
1120 vm.prank(stranger);
1121 vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, stranger));
1122 token.mint(stranger, 1_000_000e8);
1123 // the owner path is unchanged too
1124 vm.prank(address(aa));
1125 token.mint(h1, 1);
1126 assertEq(token.totalSupply(), sup + 1);
1127 vm.prank(h1);
1128 token.transfer(h2, 10e8);
1129 assertEq(token.balanceOf(h2), 60e8); // transfers conserve
1130 }
1131
1132 /// ...and lets anyone mint on a real block (any base fee above zero).
1133 function testFuzz_attack_evilBaseFee_strangerMintsAtPositiveBasefee(uint256 fee) public {
1134 fee = bound(fee, 1, 1_000_000 gwei);
1135 _overrideImpl(address(evilBaseFee));
1136 vm.fee(fee);
1137 uint256 sup = token.totalSupply();
1138 vm.prank(stranger);
1139 token.mint(stranger, 1_000_000e8);
1140 assertEq(token.totalSupply(), sup + 1_000_000e8);
1141 assertEq(token.balanceOf(stranger), 1_000_000e8);
1142 }
1143
1144 /// EvilDrain: anyone moves any holder's tokens through `rescue`, without an allowance.
1145 function test_attack_evilDrain_strangerMovesAHoldersTokens() public {
1146 _overrideImpl(address(evilDrain));
1147 uint256 sup = token.totalSupply();
1148 vm.prank(stranger);
1149 EvilDrainImpl(proxy).rescue(h1, stranger, 100e8);
1150 assertEq(token.balanceOf(h1), 0);
1151 assertEq(token.balanceOf(stranger), 100e8);
1152 assertEq(token.allowance(h1, stranger), 0);
1153 assertEq(token.totalSupply(), sup); // R1 sees nothing
1154 assertEq(token.owner(), address(aa)); // R2 sees nothing
1155 }
1156
1157 /// ...while every v1 path behaves exactly like v1: only a probe of the new selector shows the drain.
1158 function test_attack_evilDrain_v1PathsUnchanged() public {
1159 _overrideImpl(address(evilDrain));
1160 vm.prank(stranger);
1161 vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, stranger));
1162 token.mint(stranger, 1);
1163 vm.prank(h1);
1164 assertTrue(token.transfer(h2, 10e8));
1165 assertEq(token.balanceOf(h1), 90e8);
1166 assertEq(token.balanceOf(h2), 60e8); // transfers conserve (R3 sees nothing)
1167 }
1168
1169 function test_attack_v1_hasNoRescue() public {
1170 vm.prank(stranger);
1171 vm.expectRevert();
1172 EvilDrainImpl(proxy).rescue(h1, stranger, 1);
1173 assertEq(token.balanceOf(h1), 100e8);
1174 }
1175
1176 // ------------------------------------------------------------------ bytecode (what R9 sees)
1177
1178 /// True when `op` appears as an opcode in `code` (PUSH data and the CBOR metadata tail skipped), the same walk
1179 /// as the workflow's static check.
1180 function _hasOpcode(bytes memory code, uint8 op) internal pure returns (bool) {
1181 uint256 end = code.length;
1182 if (end >= 2) {
1183 uint256 metaLen = (uint256(uint8(code[end - 2])) << 8) | uint8(code[end - 1]);
1184 if (metaLen + 2 <= end && uint8(code[end - metaLen - 2]) == 0xa2) end = end - metaLen - 2;
1185 }
1186 for (uint256 pc = 0; pc < end; pc++) {
1187 uint8 b = uint8(code[pc]);
1188 if (b == op) return true;
1189 if (b >= 0x60 && b <= 0x7f) pc += b - 0x5f;
1190 }
1191 return false;
1192 }
1193
1194 function test_bytecode_evilBaseFeeCarriesBasefee_honestImplsCarryNoEnvOpcodes() public view {
1195 assertTrue(_hasOpcode(address(evilBaseFee).code, 0x48), "EvilBaseFee: BASEFEE");
1196 uint8[5] memory env = [0x48, 0x3a, 0x4a, 0x49, 0x45]; // BASEFEE GASPRICE BLOBBASEFEE BLOBHASH GASLIMIT
1197 address[3] memory honest = [address(v1), address(v2), address(pausedImpl)];
1198 for (uint256 i = 0; i < honest.length; i++) {
1199 for (uint256 j = 0; j < env.length; j++) {
1200 assertFalse(_hasOpcode(honest[i].code, env[j]), "honest impl carries an environment opcode");
1201 }
1202 }
1203 }
1204
1205 /// EvilDrain adds no opcode v1 lacks (no environment opcode, call, create or selfdestruct): R9 cannot see it.
1206 function test_bytecode_evilDrainAddsNoOpcode() public view {
1207 bool[256] memory inV1 = _opcodeSet(address(v1).code);
1208 bool[256] memory inDrain = _opcodeSet(address(evilDrain).code);
1209 for (uint256 op = 0; op < 256; op++) {
1210 if (inDrain[op]) assertTrue(inV1[op], "EvilDrain adds an opcode v1 lacks");
1211 }
1212 }
1213
1214 function _opcodeSet(bytes memory code) internal pure returns (bool[256] memory set) {
1215 uint256 end = code.length;
1216 if (end >= 2) {
1217 uint256 metaLen = (uint256(uint8(code[end - 2])) << 8) | uint8(code[end - 1]);
1218 if (metaLen + 2 <= end && uint8(code[end - metaLen - 2]) == 0xa2) end = end - metaLen - 2;
1219 }
1220 for (uint256 pc = 0; pc < end; pc++) {
1221 uint8 b = uint8(code[pc]);
1222 set[b] = true;
1223 if (b >= 0x60 && b <= 0x7f) pc += b - 0x5f;
1224 }
1225 }
1226
1227 function test_attack_selectorsUnchanged() public view {
1228 // EvilMint, EvilFee and EvilBaseFee expose the same mint/transfer selectors as v1, so a selector diff misses
1229 // them.
1230 assertGt(address(evilMint).code.length, 0);
1231 assertGt(address(evilFee).code.length, 0);
1232 assertGt(address(evilBaseFee).code.length, 0);
1233 assertTrue(address(evilMint).codehash != address(v1).codehash);
1234 assertTrue(address(evilFee).codehash != address(v1).codehash);
1235 assertTrue(address(evilBaseFee).codehash != address(v1).codehash);
1236 assertTrue(address(evilDrain).codehash != address(v1).codehash);
1237 }
1238
1239 function test_implsCannotBeInitialized() public {
1240 address[] memory hs = new address[](0);
1241 uint256[] memory am = new uint256[](0);
1242 vm.expectRevert();
1243 v1.initialize(stranger, hs, am);
1244 vm.expectRevert();
1245 evilMint.initialize(stranger, hs, am);
1246 vm.expectRevert();
1247 evilBaseFee.initialize(stranger, hs, am);
1248 vm.expectRevert();
1249 evilDrain.initialize(stranger, hs, am);
1250 }
1251
1252 // ------------------------------------------------------------------ constructor validation
1253
1254 function test_constructorValidation() public {
1255 address[] memory g = _guardians();
1256 vm.expectRevert(AirlockAdmin.ZeroAddress.selector);
1257 new AirlockAdmin(address(0), proxy, admin, g, approver, address(pausedImpl), DELAY, oracle);
1258 vm.expectRevert(AirlockAdmin.ZeroAddress.selector);
1259 _newAdmin(admin, g, address(0), address(pausedImpl), DELAY, oracle);
1260 vm.expectRevert(abi.encodeWithSelector(AirlockAdmin.NoCode.selector, stranger));
1261 _newAdmin(admin, g, approver, stranger, DELAY, oracle);
1262 vm.expectRevert(abi.encodeWithSelector(AirlockAdmin.GuardianBounds.selector, 0));
1263 _newAdmin(admin, new address[](0), approver, address(pausedImpl), DELAY, oracle);
1264 address[] memory nine = new address[](9);
1265 for (uint256 i = 0; i < 9; i++) {
1266 nine[i] = address(uint160(0x1000 + i));
1267 }
1268 vm.expectRevert(abi.encodeWithSelector(AirlockAdmin.GuardianBounds.selector, 9));
1269 _newAdmin(admin, nine, approver, address(pausedImpl), DELAY, oracle);
1270 g[1] = address(0);
1271 vm.expectRevert(AirlockAdmin.ZeroAddress.selector);
1272 _newAdmin(admin, g, approver, address(pausedImpl), DELAY, oracle);
1273 }
1274
1275 // ------------------------------------------------------------------ fuzz
1276
1277 function testFuzz_proposeSetDelay_outOfBounds(uint64 d) public {
1278 vm.assume(d < aa.MIN_DELAY_BLOCKS() || d > aa.MAX_DELAY_BLOCKS());
1279 vm.prank(admin);
1280 vm.expectRevert(abi.encodeWithSelector(AirlockAdmin.InvalidDelay.selector, d));
1281 aa.proposeSetDelay(d);
1282 }
1283
1284 function testFuzz_constructor_delayOutOfBounds(uint64 d) public {
1285 vm.assume(d < 10 || d > 50_400);
1286 vm.expectRevert(abi.encodeWithSelector(AirlockAdmin.InvalidDelay.selector, d));
1287 _newAdmin(admin, _guardians(), approver, address(pausedImpl), d, oracle);
1288 }
1289
1290 function testFuzz_proposeSetDelay_inBounds(uint64 d) public {
1291 d = uint64(bound(d, 10, 50_400));
1292 vm.prank(admin);
1293 uint256 id = aa.proposeSetDelay(d);
1294 assertEq(aa.getProposal(id).value, d);
1295 }
1296
1297 /// Only the clearance oracle and the approver can clear.
1298 function testFuzz_onlyClearerRoles(address who) public {
1299 vm.assume(who != oracle && who != approver);
1300 uint256 id = _proposeUpgrade(address(v2));
1301 vm.prank(who);
1302 vm.expectRevert(AirlockAdmin.NotClearer.selector);
1303 aa.clear(id, EVIDENCE);
1304 }
1305
1306 /// Without a clearance nothing executes, however long it waits.
1307 function testFuzz_noClearanceNoExecute(uint256 wait) public {
1308 wait = bound(wait, DELAY, 10_000_000);
1309 uint256 id = _proposeUpgrade(address(v2));
1310 _advance(wait);
1311 vm.expectRevert(abi.encodeWithSelector(AirlockAdmin.NotCleared.selector, id));
1312 aa.execute(id);
1313 }
1314}