script/DeployEth.s.sol202 lines · 11124 bytes
| 1 | // SPDX-License-Identifier: MIT |
| 2 | pragma solidity 0.8.37; |
| 3 | |
| 4 | import {Script} from "forge-std/Script.sol"; |
| 5 | import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; |
| 6 | import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; |
| 7 | import {ERC1967Utils} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Utils.sol"; |
| 8 | |
| 9 | import {AirlockAdmin} from "../src/AirlockAdmin.sol"; |
| 10 | import {DemoBTC} from "../src/DemoBTC.sol"; |
| 11 | import {DemoBTCV2} from "../src/DemoBTCV2.sol"; |
| 12 | import {PausedImpl} from "../src/PausedImpl.sol"; |
| 13 | import {EvilMintImpl} from "../src/attack/EvilMintImpl.sol"; |
| 14 | import {EvilOwnerImpl} from "../src/attack/EvilOwnerImpl.sol"; |
| 15 | import {EvilFeeImpl} from "../src/attack/EvilFeeImpl.sol"; |
| 16 | import {EvilBaseFeeImpl} from "../src/attack/EvilBaseFeeImpl.sol"; |
| 17 | import {EvilDrainImpl} from "../src/attack/EvilDrainImpl.sol"; |
| 18 | import {AirlockReceiver} from "../src/cre/AirlockReceiver.sol"; |
| 19 | |
| 20 | /// @notice Ethereum (Sepolia) deployment of Upgrade Airlock with Chainlink CRE as the clearance oracle: |
| 21 | /// DemoBTC v1 -> TransparentUpgradeableProxy -> demo impls (v2, three attacks, the basefee-evasion attack, the |
| 22 | /// third-party drain attack, the paused impl) -> AirlockReceiver (against AirlockAdmin's predicted address) -> |
| 23 | /// AirlockAdmin(guardians = [AirlockReceiver, emergency guardian], clearanceOracle = AirlockReceiver) -> adoption |
| 24 | /// (ProxyAdmin and the token's Ownable handed to AirlockAdmin). 13 transactions, all from the deployer. |
| 25 | /// The deployer key is the demo owner: it can only propose. The approver (manual clearance, codehash registry), the |
| 26 | /// emergency guardian (veto, breakGlass) and the attester (signs the CRE verdicts) are separate keys; the run |
| 27 | /// refuses to deploy when one is missing or when any two of owner, approver, emergency guardian and attester are |
| 28 | /// the same address. |
| 29 | /// The Sepolia MockKeystoneForwarder (what `cre workflow simulate` writes through) is permissionless and forwards |
| 30 | /// whatever metadata its caller supplies, so behind it the attester's signature alone authenticates a verdict. The |
| 31 | /// run refuses it unless ALLOW_MOCK_FORWARDER=1 says so explicitly, or WORKFLOW_ID and WORKFLOW_OWNER are both set. |
| 32 | /// |
| 33 | /// Env: DEPLOYER_PK (required), ATTESTER_ADDR (required, the CRE workflow's attestation key), |
| 34 | /// APPROVER_ADDR (required), EMERGENCY_GUARDIAN_ADDR (required), |
| 35 | /// FORWARDER (default: Sepolia MockKeystoneForwarder used by `cre workflow simulate`), DELAY_BLOCKS (default 25), |
| 36 | /// WORKFLOW_ID / WORKFLOW_OWNER (default 0 = not checked; set them for a DON deployment), |
| 37 | /// ALLOW_MOCK_FORWARDER ("1" or "true" accepts the mock forwarder without WORKFLOW_ID and WORKFLOW_OWNER), |
| 38 | /// OUT_JSON (default cache/deployment-eth.dryrun.json; must be under deployments/ or cache/). |
| 39 | /// Sepolia runs the Amsterdam rules: run with `--skip-simulation` (on Sepolia and on an anvil fork of it). |
| 40 | contract DeployEth is Script { |
| 41 | address internal constant SEPOLIA_MOCK_FORWARDER = 0x15fC6ae953E024d975e77382eEeC56A9101f9F88; |
| 42 | uint64 internal constant DEFAULT_DELAY = 25; |
| 43 | |
| 44 | struct Out { |
| 45 | address v1; |
| 46 | address proxy; |
| 47 | address proxyAdmin; |
| 48 | address v2; |
| 49 | address evilMint; |
| 50 | address evilOwner; |
| 51 | address evilFee; |
| 52 | address evilBaseFee; |
| 53 | address evilDrain; |
| 54 | address paused; |
| 55 | address receiver; |
| 56 | address airlockAdmin; |
| 57 | } |
| 58 | |
| 59 | struct Cfg { |
| 60 | uint256 pk; |
| 61 | address owner; |
| 62 | address attester; |
| 63 | address forwarder; |
| 64 | address approver; |
| 65 | address emergency; |
| 66 | uint64 delay; |
| 67 | bytes32 workflowId; |
| 68 | address workflowOwner; |
| 69 | bool allowMockForwarder; |
| 70 | address[] holders; |
| 71 | uint256[] amounts; |
| 72 | } |
| 73 | |
| 74 | function run() external { |
| 75 | Cfg memory c = _config(); |
| 76 | uint256 startBlock = block.number; |
| 77 | Out memory o = deploy(c); |
| 78 | _write(c, o, startBlock); |
| 79 | } |
| 80 | |
| 81 | /// @notice Refuses missing or overlapping roles. The owner proposes; the approver clears by hand, the emergency |
| 82 | /// guardian vetoes and the attester signs the verdicts the receiver turns into clearances, so none of the |
| 83 | /// four may share a key. |
| 84 | function checkRoles(address owner, address approver, address emergency, address attester) public pure { |
| 85 | require(approver != address(0), "APPROVER_ADDR is required"); |
| 86 | require(emergency != address(0), "EMERGENCY_GUARDIAN_ADDR is required"); |
| 87 | require(attester != address(0), "ATTESTER_ADDR is required"); |
| 88 | require(approver != owner, "APPROVER_ADDR must differ from the owner (deployer)"); |
| 89 | require(emergency != owner, "EMERGENCY_GUARDIAN_ADDR must differ from the owner (deployer)"); |
| 90 | require(approver != emergency, "APPROVER_ADDR and EMERGENCY_GUARDIAN_ADDR must differ"); |
| 91 | require(attester != owner, "ATTESTER_ADDR must differ from the owner (deployer)"); |
| 92 | require(attester != approver, "ATTESTER_ADDR must differ from APPROVER_ADDR"); |
| 93 | require(attester != emergency, "ATTESTER_ADDR must differ from EMERGENCY_GUARDIAN_ADDR"); |
| 94 | } |
| 95 | |
| 96 | /// @notice Refuses the permissionless Sepolia MockKeystoneForwarder unless the caller opted in explicitly |
| 97 | /// (ALLOW_MOCK_FORWARDER=1) or set both WORKFLOW_ID and WORKFLOW_OWNER. Any other forwarder passes. |
| 98 | function checkForwarder(address forwarder, bytes32 workflowId, address workflowOwner, bool allowMock) public pure { |
| 99 | if (forwarder != SEPOLIA_MOCK_FORWARDER || allowMock) return; |
| 100 | require( |
| 101 | workflowId != bytes32(0) && workflowOwner != address(0), |
| 102 | "mock forwarder refused: set ALLOW_MOCK_FORWARDER=1, or WORKFLOW_ID and WORKFLOW_OWNER" |
| 103 | ); |
| 104 | } |
| 105 | |
| 106 | /// @notice The demo holders: keyless addresses (each is a hash, nobody holds a key for it). The attack |
| 107 | /// simulation does not need signatures (eth_simulateV1 takes `from` as given). |
| 108 | function demoHolders() public pure returns (address[] memory holders, uint256[] memory amounts) { |
| 109 | holders = new address[](3); |
| 110 | amounts = new uint256[](3); |
| 111 | for (uint256 i = 0; i < 3; i++) { |
| 112 | holders[i] = address(uint160(uint256(keccak256(abi.encodePacked("upgrade-airlock/demo-holder/", i + 1))))); |
| 113 | } |
| 114 | amounts[0] = 100e8; // dBTC has 8 decimals |
| 115 | amounts[1] = 50e8; |
| 116 | amounts[2] = 25e8; |
| 117 | } |
| 118 | |
| 119 | function deploy(Cfg memory c) public returns (Out memory o) { |
| 120 | checkRoles(c.owner, c.approver, c.emergency, c.attester); |
| 121 | checkForwarder(c.forwarder, c.workflowId, c.workflowOwner, c.allowMockForwarder); |
| 122 | require(c.forwarder.code.length > 0, "forwarder has no code on this chain"); |
| 123 | |
| 124 | vm.startBroadcast(c.pk); |
| 125 | o.v1 = address(new DemoBTC()); |
| 126 | o.proxy = address( |
| 127 | new TransparentUpgradeableProxy( |
| 128 | o.v1, c.owner, abi.encodeCall(DemoBTC.initialize, (c.owner, c.holders, c.amounts)) |
| 129 | ) |
| 130 | ); |
| 131 | o.proxyAdmin = address(uint160(uint256(vm.load(o.proxy, ERC1967Utils.ADMIN_SLOT)))); |
| 132 | o.v2 = address(new DemoBTCV2()); |
| 133 | o.evilMint = address(new EvilMintImpl()); |
| 134 | o.evilOwner = address(new EvilOwnerImpl()); |
| 135 | o.evilFee = address(new EvilFeeImpl()); |
| 136 | o.evilBaseFee = address(new EvilBaseFeeImpl()); |
| 137 | o.evilDrain = address(new EvilDrainImpl()); |
| 138 | o.paused = address(new PausedImpl()); |
| 139 | // The receiver is deployed first, against AirlockAdmin's address (the next contract this key creates): |
| 140 | // AirlockAdmin requires its clearance oracle to have code already. |
| 141 | address predicted = vm.computeCreateAddress(c.owner, vm.getNonce(c.owner) + 1); |
| 142 | o.receiver = address(new AirlockReceiver(predicted, c.forwarder, c.attester, c.workflowId, c.workflowOwner)); |
| 143 | address[] memory guardians = new address[](2); |
| 144 | guardians[0] = o.receiver; |
| 145 | guardians[1] = c.emergency; |
| 146 | o.airlockAdmin = address( |
| 147 | new AirlockAdmin(o.proxyAdmin, o.proxy, c.owner, guardians, c.approver, o.paused, c.delay, o.receiver) |
| 148 | ); |
| 149 | require(o.airlockAdmin == predicted, "AirlockAdmin address prediction failed"); |
| 150 | ProxyAdmin(o.proxyAdmin).transferOwnership(o.airlockAdmin); |
| 151 | DemoBTC(o.proxy).transferOwnership(o.airlockAdmin); |
| 152 | vm.stopBroadcast(); |
| 153 | |
| 154 | AirlockAdmin a = AirlockAdmin(o.airlockAdmin); |
| 155 | require(a.isWired(), "ProxyAdmin not handed to AirlockAdmin"); |
| 156 | require(DemoBTC(o.proxy).owner() == o.airlockAdmin, "token owner is not AirlockAdmin"); |
| 157 | require(a.isGuardian(o.receiver) && a.isGuardian(c.emergency), "guardians not set"); |
| 158 | require(a.clearanceOracle() == o.receiver, "receiver is not the clearance oracle"); |
| 159 | require(address(AirlockReceiver(o.receiver).airlockAdmin()) == o.airlockAdmin, "receiver wired elsewhere"); |
| 160 | } |
| 161 | |
| 162 | function _config() internal view returns (Cfg memory c) { |
| 163 | c.pk = vm.envUint("DEPLOYER_PK"); |
| 164 | c.owner = vm.addr(c.pk); |
| 165 | c.attester = vm.envOr("ATTESTER_ADDR", address(0)); |
| 166 | c.forwarder = vm.envOr("FORWARDER", SEPOLIA_MOCK_FORWARDER); |
| 167 | c.approver = vm.envOr("APPROVER_ADDR", address(0)); |
| 168 | c.emergency = vm.envOr("EMERGENCY_GUARDIAN_ADDR", address(0)); |
| 169 | c.delay = uint64(vm.envOr("DELAY_BLOCKS", uint256(DEFAULT_DELAY))); |
| 170 | c.workflowId = vm.envOr("WORKFLOW_ID", bytes32(0)); |
| 171 | c.workflowOwner = vm.envOr("WORKFLOW_OWNER", address(0)); |
| 172 | bytes32 allow = keccak256(bytes(vm.envOr("ALLOW_MOCK_FORWARDER", string("")))); |
| 173 | c.allowMockForwarder = allow == keccak256("1") || allow == keccak256("true"); |
| 174 | (c.holders, c.amounts) = demoHolders(); |
| 175 | } |
| 176 | |
| 177 | function _write(Cfg memory c, Out memory o, uint256 startBlock) internal { |
| 178 | string memory j = "deployment"; |
| 179 | vm.serializeUint(j, "chainId", block.chainid); |
| 180 | vm.serializeUint(j, "startBlock", startBlock); |
| 181 | vm.serializeAddress(j, "owner", c.owner); |
| 182 | vm.serializeAddress(j, "approver", c.approver); |
| 183 | vm.serializeAddress(j, "emergencyGuardian", c.emergency); |
| 184 | vm.serializeAddress(j, "attester", c.attester); |
| 185 | vm.serializeAddress(j, "forwarder", c.forwarder); |
| 186 | vm.serializeUint(j, "delayBlocks", c.delay); |
| 187 | vm.serializeAddress(j, "holders", c.holders); |
| 188 | vm.serializeAddress(j, "proxy", o.proxy); |
| 189 | vm.serializeAddress(j, "proxyAdmin", o.proxyAdmin); |
| 190 | vm.serializeAddress(j, "receiver", o.receiver); |
| 191 | vm.serializeAddress(j, "airlockAdmin", o.airlockAdmin); |
| 192 | vm.serializeAddress(j, "v1", o.v1); |
| 193 | vm.serializeAddress(j, "v2", o.v2); |
| 194 | vm.serializeAddress(j, "evilMint", o.evilMint); |
| 195 | vm.serializeAddress(j, "evilOwner", o.evilOwner); |
| 196 | vm.serializeAddress(j, "evilFee", o.evilFee); |
| 197 | vm.serializeAddress(j, "evilBaseFee", o.evilBaseFee); |
| 198 | vm.serializeAddress(j, "evilDrain", o.evilDrain); |
| 199 | string memory out = vm.serializeAddress(j, "paused", o.paused); |
| 200 | vm.writeJson(out, vm.envOr("OUT_JSON", string("cache/deployment-eth.dryrun.json"))); |
| 201 | } |
| 202 | } |