test/AirlockAdmin.t.sol1314 lines · 55703 bytes
| 1 | // SPDX-License-Identifier: MIT |
| 2 | pragma solidity 0.8.37; |
| 3 | |
| 4 | import {Test, Vm} from "forge-std/Test.sol"; |
| 5 | import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; |
| 6 | import {ITransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; |
| 7 | import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; |
| 8 | import {ERC1967Utils} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Utils.sol"; |
| 9 | import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; |
| 10 | |
| 11 | import {AirlockAdmin} from "../src/AirlockAdmin.sol"; |
| 12 | import {DemoBTC} from "../src/DemoBTC.sol"; |
| 13 | import {DemoBTCV2} from "../src/DemoBTCV2.sol"; |
| 14 | import {PausedImpl} from "../src/PausedImpl.sol"; |
| 15 | import {EvilMintImpl} from "../src/attack/EvilMintImpl.sol"; |
| 16 | import {EvilOwnerImpl} from "../src/attack/EvilOwnerImpl.sol"; |
| 17 | import {EvilFeeImpl} from "../src/attack/EvilFeeImpl.sol"; |
| 18 | import {EvilBaseFeeImpl} from "../src/attack/EvilBaseFeeImpl.sol"; |
| 19 | import {EvilDrainImpl} from "../src/attack/EvilDrainImpl.sol"; |
| 20 | |
| 21 | contract 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 | } |