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 / AirlockReceiver.t.sol
test/AirlockReceiver.t.sol567 lines · 24400 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 {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol";
7import {ERC1967Utils} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Utils.sol";
8import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
9import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.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 {EvilDrainImpl} from "../src/attack/EvilDrainImpl.sol";
17import {AirlockReceiver} from "../src/cre/AirlockReceiver.sol";
18import {IReceiver} from "../src/cre/IReceiver.sol";
19
20contract AirlockReceiverTest is Test {
21 uint64 constant DELAY = 30;
22 uint8 constant VETO = 0;
23 uint8 constant CLEAR = 1;
24 bytes32 constant R1 = keccak256("GUARD/R1_TOTAL_SUPPLY");
25 bytes32 constant EVIDENCE = keccak256("evidence-json");
26
27 address deployer = makeAddr("deployer");
28 address admin = makeAddr("admin");
29 address emergency = makeAddr("emergencyGuardian");
30 address approver = makeAddr("approver");
31 address forwarder = makeAddr("forwarder");
32 address h1 = makeAddr("holder1");
33 Vm.Wallet attester;
34 Vm.Wallet impostor;
35
36 DemoBTC v1;
37 DemoBTCV2 v2;
38 EvilMintImpl evilMint;
39 PausedImpl pausedImpl;
40 address proxy;
41 ProxyAdmin proxyAdmin;
42 AirlockAdmin aa;
43 AirlockReceiver rx;
44
45 /// One signed verdict, as the workflow builds it (cre/veto/report.ts).
46 struct V {
47 uint8 action;
48 uint256 id;
49 bytes32 digest;
50 bytes32 rule;
51 bytes32 evidence;
52 uint64 until;
53 }
54
55 function setUp() public {
56 vm.roll(100);
57 vm.warp(1_800_000_000);
58 attester = vm.createWallet("attester");
59 impostor = vm.createWallet("impostor");
60
61 vm.startPrank(deployer);
62 v1 = new DemoBTC();
63 address[] memory holders = new address[](1);
64 holders[0] = h1;
65 uint256[] memory amounts = new uint256[](1);
66 amounts[0] = 100e8;
67 proxy = address(
68 new TransparentUpgradeableProxy(
69 address(v1), admin, abi.encodeCall(DemoBTC.initialize, (admin, holders, amounts))
70 )
71 );
72 proxyAdmin = ProxyAdmin(address(uint160(uint256(vm.load(proxy, ERC1967Utils.ADMIN_SLOT)))));
73 v2 = new DemoBTCV2();
74 evilMint = new EvilMintImpl();
75 pausedImpl = new PausedImpl();
76 // The receiver needs AirlockAdmin's address and AirlockAdmin needs the receiver (clearance oracle with code,
77 // and a guardian): deploy the receiver first against AirlockAdmin's predicted address, exactly as
78 // script/DeployEth.s.sol does.
79 address predicted = vm.computeCreateAddress(deployer, vm.getNonce(deployer) + 1);
80 rx = new AirlockReceiver(predicted, forwarder, attester.addr, bytes32(0), address(0));
81 address[] memory guardians = new address[](2);
82 guardians[0] = address(rx);
83 guardians[1] = emergency;
84 aa = new AirlockAdmin(
85 address(proxyAdmin), proxy, admin, guardians, approver, address(pausedImpl), DELAY, address(rx)
86 );
87 vm.stopPrank();
88 assertEq(address(aa), predicted, "predicted AirlockAdmin address");
89
90 vm.startPrank(admin);
91 proxyAdmin.transferOwnership(address(aa));
92 DemoBTC(proxy).transferOwnership(address(aa));
93 vm.stopPrank();
94 }
95
96 // ------------------------------------------------------------------ helpers
97
98 function _propose(address impl) internal returns (uint256 id) {
99 vm.prank(admin);
100 id = aa.proposeUpgrade(impl, "");
101 }
102
103 function _verdict(uint8 action, uint256 id) internal view returns (V memory v) {
104 v.action = action;
105 v.id = id;
106 v.digest = aa.proposalDigest(id);
107 v.rule = action == VETO ? R1 : bytes32(0);
108 v.evidence = EVIDENCE;
109 v.until = uint64(vm.getBlockNumber() + 50);
110 }
111
112 function _signWith(uint256 pk, AirlockReceiver r, V memory v) internal view returns (bytes memory) {
113 bytes32 h = r.verdictDigest(v.action, v.id, v.digest, v.rule, v.evidence, v.until);
114 (uint8 sv, bytes32 sr, bytes32 ss) = vm.sign(pk, h);
115 return abi.encodePacked(sr, ss, sv);
116 }
117
118 function _report(V memory v, bytes memory sig) internal pure returns (bytes memory) {
119 return abi.encode(v.action, v.id, v.digest, v.rule, v.evidence, v.until, sig);
120 }
121
122 function _signed(V memory v) internal view returns (bytes memory) {
123 return _report(v, _signWith(attester.privateKey, rx, v));
124 }
125
126 function _deliver(bytes memory rep) internal {
127 vm.prank(forwarder);
128 rx.onReport("", rep);
129 }
130
131 function _metadata(bytes32 workflowId, address owner) internal pure returns (bytes memory) {
132 return abi.encodePacked(workflowId, bytes10("airlock-wf"), owner, bytes2(0x0001));
133 }
134
135 function _status(uint256 id) internal view returns (AirlockAdmin.Status) {
136 return aa.getProposal(id).status;
137 }
138
139 // ------------------------------------------------------------------ EIP-712 shape (for the off-chain signer)
140
141 function test_typehashAndDomain() public view {
142 assertEq(
143 rx.VERDICT_TYPEHASH(),
144 keccak256(
145 "Verdict(address airlockAdmin,uint8 action,uint256 proposalId,bytes32 proposalDigest,bytes32 ruleHash,bytes32 evidenceHash,uint64 validUntilBlock)"
146 )
147 );
148 bytes32 domain = keccak256(
149 abi.encode(
150 keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
151 keccak256("AirlockReceiver"),
152 keccak256("1"),
153 block.chainid,
154 address(rx)
155 )
156 );
157 assertEq(rx.domainSeparator(), domain);
158 bytes32 structHash = keccak256(
159 abi.encode(
160 rx.VERDICT_TYPEHASH(),
161 address(aa),
162 uint8(1),
163 uint256(7),
164 bytes32(uint256(0xd1)),
165 bytes32(0),
166 EVIDENCE,
167 uint64(1234)
168 )
169 );
170 assertEq(
171 rx.verdictDigest(1, 7, bytes32(uint256(0xd1)), bytes32(0), EVIDENCE, 1234),
172 keccak256(abi.encodePacked(hex"1901", domain, structHash))
173 );
174 assertEq(address(rx.airlockAdmin()), address(aa));
175 assertEq(rx.attester(), attester.addr);
176 assertEq(rx.forwarder(), forwarder);
177 assertEq(aa.clearanceOracle(), address(rx));
178 assertTrue(aa.isGuardian(address(rx)));
179 }
180
181 // ------------------------------------------------------------------ veto
182
183 function test_vetoReport_vetoesAndAttachesEvidence() public {
184 uint256 id = _propose(address(evilMint));
185 V memory v = _verdict(VETO, id);
186 bytes memory rep = _signed(v);
187
188 vm.expectEmit(true, true, false, true, address(rx));
189 emit AirlockReceiver.Verdict(id, VETO, R1, EVIDENCE, bytes32(uint256(7)));
190 vm.expectEmit(true, true, false, true, address(aa));
191 emit AirlockAdmin.Vetoed(id, address(rx), R1, 1);
192 vm.expectEmit(true, false, false, true, address(aa));
193 emit AirlockAdmin.ReasonAttached(id, address(rx), EVIDENCE);
194 vm.prank(forwarder);
195 rx.onReport(_metadata(bytes32(uint256(7)), address(0xaaaa)), rep);
196
197 assertEq(uint8(_status(id)), uint8(AirlockAdmin.Status.Vetoed));
198 assertTrue(aa.hasVetoed(id, address(rx)));
199 assertEq(aa.clearanceOf(id), bytes32(0));
200 assertTrue(aa.oracleJudged(id), "a CRE veto is the oracle's verdict");
201
202 vm.roll(vm.getBlockNumber() + DELAY);
203 vm.expectRevert(abi.encodeWithSelector(AirlockAdmin.ProposalVetoed.selector, id));
204 aa.execute(id);
205 assertEq(DemoBTC(proxy).totalSupply(), 100e8, "malicious implementation never went live");
206 }
207
208 // ------------------------------------------------------------------ clear
209
210 function test_clearReport_clearsThenExecutesAfterCooldown() public {
211 uint256 id = _propose(address(v2));
212 V memory v = _verdict(CLEAR, id);
213
214 vm.expectEmit(true, true, false, true, address(rx));
215 emit AirlockReceiver.Verdict(id, CLEAR, bytes32(0), EVIDENCE, bytes32(0));
216 vm.expectEmit(true, true, false, true, address(aa));
217 emit AirlockAdmin.Cleared(id, address(rx), EVIDENCE);
218 assertFalse(aa.oracleJudged(id));
219 _deliver(_signed(v));
220 assertEq(aa.clearanceOf(id), EVIDENCE);
221 assertTrue(aa.oracleJudged(id), "a CRE clear is the oracle's verdict");
222 assertEq(uint8(_status(id)), uint8(AirlockAdmin.Status.Pending));
223
224 vm.roll(vm.getBlockNumber() + DELAY - 1);
225 vm.expectRevert(); // TooEarly: a clearance never shortens the cooldown
226 aa.execute(id);
227 vm.roll(vm.getBlockNumber() + 1);
228 aa.execute(id);
229 assertEq(DemoBTCV2(proxy).version(), "2");
230 }
231
232 function test_withoutClearReport_honestUpgradeWaits() public {
233 uint256 id = _propose(address(v2));
234 vm.roll(vm.getBlockNumber() + DELAY * 10);
235 vm.expectRevert(abi.encodeWithSelector(AirlockAdmin.NotCleared.selector, id));
236 aa.execute(id);
237 }
238
239 /// The emergency guardian's veto is not the oracle's verdict.
240 function test_emergencyVetoLeavesOracleJudgedUnset() public {
241 uint256 id = _propose(address(evilMint));
242 vm.prank(emergency);
243 aa.veto(id, keccak256("manual"));
244 assertEq(uint8(_status(id)), uint8(AirlockAdmin.Status.Vetoed));
245 assertFalse(aa.oracleJudged(id));
246 }
247
248 /// What the sweeper relies on: a proposal the approver cleared by hand is still Pending with oracleJudged false,
249 /// so CRE judges it; a CRE Veto then wins over the manual clearance.
250 function test_approverClearedThenCreVeto_vetoWins() public {
251 EvilDrainImpl drain = new EvilDrainImpl();
252 uint256 id = _propose(address(drain));
253 bytes32 manual = keccak256("approver-review");
254 vm.prank(approver); // a mistaken manual clearance
255 aa.clear(id, manual);
256 assertEq(aa.clearanceOf(id), manual);
257 assertEq(uint8(_status(id)), uint8(AirlockAdmin.Status.Pending));
258 assertFalse(aa.oracleJudged(id), "still to be judged by CRE");
259
260 _deliver(_signed(_verdict(VETO, id)));
261 assertTrue(aa.oracleJudged(id));
262 assertEq(uint8(_status(id)), uint8(AirlockAdmin.Status.Vetoed));
263 vm.roll(vm.getBlockNumber() + DELAY);
264 vm.expectRevert(abi.encodeWithSelector(AirlockAdmin.ProposalVetoed.selector, id));
265 aa.execute(id);
266 }
267
268 /// ...and a CRE Clear on an approver-cleared proposal marks it judged and keeps the first evidence hash.
269 function test_approverClearedThenCreClear_judgedFirstEvidenceKept() public {
270 uint256 id = _propose(address(v2));
271 bytes32 manual = keccak256("approver-review");
272 vm.prank(approver);
273 aa.clear(id, manual);
274 _deliver(_signed(_verdict(CLEAR, id)));
275 assertTrue(aa.oracleJudged(id));
276 assertEq(aa.clearanceOf(id), manual);
277 vm.roll(vm.getBlockNumber() + DELAY);
278 aa.execute(id);
279 assertEq(DemoBTCV2(proxy).version(), "2");
280 }
281
282 /// The emergency guardian's veto beats the oracle's clearance.
283 function test_guardianVetoBeatsOracleClearance() public {
284 uint256 id = _propose(address(v2));
285 _deliver(_signed(_verdict(CLEAR, id)));
286 vm.prank(emergency);
287 aa.veto(id, keccak256("manual"));
288 vm.roll(vm.getBlockNumber() + DELAY);
289 vm.expectRevert(abi.encodeWithSelector(AirlockAdmin.ProposalVetoed.selector, id));
290 aa.execute(id);
291 }
292
293 /// A veto report landing after a clear report still wins (and a later clear report is refused).
294 function test_vetoReportAfterClearReport() public {
295 uint256 id = _propose(address(evilMint));
296 bytes memory clearRep = _signed(_verdict(CLEAR, id));
297 _deliver(clearRep);
298 _deliver(_signed(_verdict(VETO, id)));
299 assertEq(uint8(_status(id)), uint8(AirlockAdmin.Status.Vetoed));
300 vm.expectRevert(abi.encodeWithSelector(AirlockAdmin.ProposalVetoed.selector, id));
301 _deliver(clearRep);
302 vm.roll(vm.getBlockNumber() + DELAY);
303 vm.expectRevert(abi.encodeWithSelector(AirlockAdmin.ProposalVetoed.selector, id));
304 aa.execute(id);
305 }
306
307 function test_clearReportWithRuleHashRejected() public {
308 uint256 id = _propose(address(v2));
309 V memory v = _verdict(CLEAR, id);
310 v.rule = R1;
311 bytes memory rep = _signed(v);
312 vm.expectRevert(abi.encodeWithSelector(AirlockReceiver.RuleOnClear.selector, R1));
313 _deliver(rep);
314 assertEq(aa.clearanceOf(id), bytes32(0));
315 }
316
317 function test_unknownActionRejected() public {
318 uint256 id = _propose(address(v2));
319 V memory v = _verdict(CLEAR, id);
320 v.action = 2;
321 bytes memory rep = _signed(v);
322 vm.expectRevert(abi.encodeWithSelector(AirlockReceiver.InvalidAction.selector, uint8(2)));
323 _deliver(rep);
324 }
325
326 function test_clearWithEmptyEvidenceRejected() public {
327 uint256 id = _propose(address(v2));
328 V memory v = _verdict(CLEAR, id);
329 v.evidence = bytes32(0);
330 bytes memory rep = _signed(v);
331 vm.expectRevert(AirlockAdmin.EmptyEvidence.selector);
332 _deliver(rep);
333 }
334
335 // ------------------------------------------------------------------ proposalDigest binding
336
337 /// A verdict about other content is rejected even when the attester signed it: here a clearance computed for
338 /// an honest v2 proposal is pointed at a malicious proposal's id.
339 function test_digestBinding_otherContentRejected() public {
340 uint256 honest = _propose(address(v2));
341 uint256 evil = _propose(address(evilMint));
342 V memory v = _verdict(CLEAR, honest);
343 v.id = evil; // signed for (evil id, honest content)
344 bytes memory rep = _signed(v);
345 vm.expectRevert(AirlockReceiver.ProposalMismatch.selector);
346 _deliver(rep);
347 assertEq(aa.clearanceOf(evil), bytes32(0));
348 assertEq(aa.clearanceOf(honest), bytes32(0));
349 }
350
351 /// The same id on another chain history (a reorg, a fork, a redeployment) carries other content.
352 function test_digestBinding_sameIdOtherHistoryRejected() public {
353 uint256 id = _propose(address(evilMint));
354 V memory v = _verdict(CLEAR, id);
355 v.digest = keccak256(
356 abi.encode(
357 uint8(0), address(v2), uint256(0), address(v2).codehash, keccak256(""), uint64(vm.getBlockNumber())
358 )
359 );
360 bytes memory rep = _signed(v);
361 vm.expectRevert(AirlockReceiver.ProposalMismatch.selector);
362 _deliver(rep);
363 v.digest = keccak256(
364 abi.encode(
365 uint8(0),
366 address(evilMint),
367 uint256(0),
368 address(evilMint).codehash,
369 keccak256(""),
370 uint64(vm.getBlockNumber() - 1)
371 )
372 );
373 rep = _signed(v);
374 vm.expectRevert(AirlockReceiver.ProposalMismatch.selector);
375 _deliver(rep);
376 assertEq(uint8(_status(id)), uint8(AirlockAdmin.Status.Pending));
377 }
378
379 function test_digestBinding_unknownProposalRejected() public {
380 V memory v = V(CLEAR, 42, keccak256("x"), bytes32(0), EVIDENCE, uint64(vm.getBlockNumber() + 50));
381 bytes memory rep = _signed(v);
382 vm.expectRevert(abi.encodeWithSelector(AirlockAdmin.UnknownProposal.selector, 42));
383 _deliver(rep);
384 }
385
386 function testFuzz_digestBinding(bytes32 wrongDigest) public {
387 uint256 id = _propose(address(v2));
388 vm.assume(wrongDigest != aa.proposalDigest(id));
389 V memory v = _verdict(CLEAR, id);
390 v.digest = wrongDigest;
391 bytes memory rep = _signed(v);
392 vm.expectRevert(AirlockReceiver.ProposalMismatch.selector);
393 _deliver(rep);
394 }
395
396 // ------------------------------------------------------------------ validUntilBlock
397
398 function test_validUntilBlock_inclusive_thenExpired() public {
399 uint256 id = _propose(address(v2));
400 V memory v = _verdict(CLEAR, id);
401 v.until = uint64(vm.getBlockNumber() + 5);
402 bytes memory rep = _signed(v);
403
404 vm.roll(v.until + 1);
405 vm.expectRevert(abi.encodeWithSelector(AirlockReceiver.ReportExpired.selector, v.until, uint256(v.until) + 1));
406 _deliver(rep);
407
408 vm.roll(v.until); // the last valid block
409 _deliver(rep);
410 assertEq(aa.clearanceOf(id), EVIDENCE);
411 }
412
413 function test_validUntilBlock_isSigned() public {
414 uint256 id = _propose(address(evilMint));
415 V memory v = _verdict(VETO, id);
416 v.until = uint64(vm.getBlockNumber() + 5);
417 bytes memory sig = _signWith(attester.privateKey, rx, v);
418 v.until = uint64(vm.getBlockNumber() + 500); // stretched after signing
419 vm.prank(forwarder);
420 vm.expectRevert(); // recovers some other address
421 rx.onReport("", _report(v, sig));
422 assertEq(uint8(_status(id)), uint8(AirlockAdmin.Status.Pending));
423 }
424
425 // ------------------------------------------------------------------ replay
426
427 function test_replay_vetoIsHarmless() public {
428 uint256 id = _propose(address(evilMint));
429 bytes memory rep = _signed(_verdict(VETO, id));
430 _deliver(rep);
431 _deliver(rep); // same proposal, already vetoed: still vetoed, counted once
432 assertEq(aa.getProposal(id).vetoCount, 1);
433 }
434
435 function test_replay_clearIsIdempotent_thenRefusedOnceExecuted() public {
436 uint256 id = _propose(address(v2));
437 bytes memory rep = _signed(_verdict(CLEAR, id));
438 _deliver(rep);
439 _deliver(rep);
440 assertEq(aa.clearanceOf(id), EVIDENCE);
441 vm.roll(vm.getBlockNumber() + DELAY);
442 aa.execute(id);
443 vm.expectRevert(abi.encodeWithSelector(AirlockAdmin.AlreadyExecuted.selector, id));
444 _deliver(rep);
445 }
446
447 /// A report for one proposal never applies to another, even one with identical content in the same block.
448 function test_replay_otherIdSameContentRejected() public {
449 uint256 a = _propose(address(v2));
450 uint256 b = _propose(address(v2));
451 assertEq(aa.proposalDigest(a), aa.proposalDigest(b));
452 V memory v = _verdict(CLEAR, a);
453 bytes memory sig = _signWith(attester.privateKey, rx, v);
454 v.id = b;
455 vm.prank(forwarder);
456 vm.expectRevert(); // the id is signed: recovers some other address
457 rx.onReport("", _report(v, sig));
458 assertEq(aa.clearanceOf(b), bytes32(0));
459 }
460
461 // ------------------------------------------------------------------ authentication
462
463 function test_rejectsWrongForwarder() public {
464 uint256 id = _propose(address(v2));
465 bytes memory rep = _signed(_verdict(CLEAR, id));
466 vm.expectRevert(abi.encodeWithSelector(AirlockReceiver.InvalidSender.selector, address(this), forwarder));
467 rx.onReport("", rep);
468 vm.prank(admin);
469 vm.expectRevert(abi.encodeWithSelector(AirlockReceiver.InvalidSender.selector, admin, forwarder));
470 rx.onReport("", rep);
471 assertEq(aa.clearanceOf(id), bytes32(0));
472 }
473
474 function test_rejectsImpostorSignature() public {
475 uint256 id = _propose(address(v2));
476 V memory v = _verdict(CLEAR, id);
477 bytes memory rep = _report(v, _signWith(impostor.privateKey, rx, v));
478 vm.expectRevert(abi.encodeWithSelector(AirlockReceiver.InvalidAttestation.selector, impostor.addr));
479 _deliver(rep);
480 }
481
482 function test_signatureIsBoundToAction() public {
483 uint256 id = _propose(address(v2));
484 V memory v = _verdict(VETO, id);
485 v.rule = bytes32(0);
486 bytes memory sig = _signWith(attester.privateKey, rx, v); // signed as a veto
487 v.action = CLEAR;
488 vm.prank(forwarder);
489 vm.expectRevert(); // recovers some other address
490 rx.onReport("", _report(v, sig));
491 assertEq(aa.clearanceOf(id), bytes32(0));
492 }
493
494 function test_signatureIsBoundToReceiver() public {
495 AirlockReceiver rx2 = new AirlockReceiver(address(aa), forwarder, attester.addr, bytes32(0), address(0));
496 uint256 id = _propose(address(v2));
497 V memory v = _verdict(CLEAR, id);
498 bytes memory rep = _report(v, _signWith(attester.privateKey, rx2, v));
499 vm.prank(forwarder);
500 vm.expectRevert(); // domain separator differs
501 rx.onReport("", rep);
502 }
503
504 function test_malformedSignatureRejected() public {
505 uint256 id = _propose(address(v2));
506 V memory v = _verdict(CLEAR, id);
507 vm.prank(forwarder);
508 vm.expectRevert(abi.encodeWithSelector(ECDSA.ECDSAInvalidSignatureLength.selector, uint256(3)));
509 rx.onReport("", _report(v, hex"010203"));
510 }
511
512 function test_metadataCheckedWhenConfigured() public {
513 bytes32 wfId = keccak256("workflow");
514 address wfOwner = makeAddr("workflowOwner");
515 AirlockReceiver strict = new AirlockReceiver(address(aa), forwarder, attester.addr, wfId, wfOwner);
516 uint256 id = _propose(address(evilMint));
517 V memory v = _verdict(VETO, id);
518 bytes memory rep = _report(v, _signWith(attester.privateKey, strict, v));
519
520 vm.startPrank(forwarder);
521 vm.expectRevert(AirlockReceiver.InvalidMetadata.selector);
522 strict.onReport("", rep);
523 vm.expectRevert(abi.encodeWithSelector(AirlockReceiver.InvalidWorkflowId.selector, bytes32(uint256(1)), wfId));
524 strict.onReport(_metadata(bytes32(uint256(1)), wfOwner), rep);
525 vm.expectRevert(abi.encodeWithSelector(AirlockReceiver.InvalidWorkflowOwner.selector, address(0xbeef), wfOwner));
526 strict.onReport(_metadata(wfId, address(0xbeef)), rep);
527 vm.stopPrank();
528 // `strict` is not a guardian of `aa`, so a fully valid report gets as far as AirlockAdmin and stops there.
529 vm.expectRevert(AirlockAdmin.NotGuardian.selector);
530 vm.prank(forwarder);
531 strict.onReport(_metadata(wfId, wfOwner), rep);
532 // nor is it the clearance oracle
533 V memory c = _verdict(CLEAR, id);
534 bytes memory clearRep = _report(c, _signWith(attester.privateKey, strict, c));
535 vm.expectRevert(AirlockAdmin.NotClearer.selector);
536 vm.prank(forwarder);
537 strict.onReport(_metadata(wfId, wfOwner), clearRep);
538 }
539
540 function test_supportsInterface() public view {
541 assertTrue(rx.supportsInterface(type(IReceiver).interfaceId));
542 assertTrue(rx.supportsInterface(type(IERC165).interfaceId));
543 assertFalse(rx.supportsInterface(0xffffffff));
544 }
545
546 function test_constructorRejectsZero() public {
547 vm.expectRevert(AirlockReceiver.ZeroAddress.selector);
548 new AirlockReceiver(address(0), forwarder, attester.addr, bytes32(0), address(0));
549 vm.expectRevert(AirlockReceiver.ZeroAddress.selector);
550 new AirlockReceiver(address(aa), address(0), attester.addr, bytes32(0), address(0));
551 vm.expectRevert(AirlockReceiver.ZeroAddress.selector);
552 new AirlockReceiver(address(aa), forwarder, address(0), bytes32(0), address(0));
553 }
554
555 /// Only the attester's signatures carry a verdict, for either action.
556 function testFuzz_impostorSignatureRejected(uint256 pk, bool clear) public {
557 pk = bound(pk, 1, 115792089237316195423570985008687907852837564279074904382605163141518161494336);
558 vm.assume(pk != attester.privateKey);
559 uint256 id = _propose(address(evilMint));
560 V memory v = _verdict(clear ? CLEAR : VETO, id);
561 bytes memory rep = _report(v, _signWith(pk, rx, v));
562 vm.expectRevert(abi.encodeWithSelector(AirlockReceiver.InvalidAttestation.selector, vm.addr(pk)));
563 _deliver(rep);
564 assertEq(uint8(_status(id)), uint8(AirlockAdmin.Status.Pending));
565 assertEq(aa.clearanceOf(id), bytes32(0));
566 }
567}