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 / script / DeployEth.s.sol
script/DeployEth.s.sol202 lines · 11124 bytes
1// SPDX-License-Identifier: MIT
2pragma solidity 0.8.37;
3
4import {Script} from "forge-std/Script.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";
8
9import {AirlockAdmin} from "../src/AirlockAdmin.sol";
10import {DemoBTC} from "../src/DemoBTC.sol";
11import {DemoBTCV2} from "../src/DemoBTCV2.sol";
12import {PausedImpl} from "../src/PausedImpl.sol";
13import {EvilMintImpl} from "../src/attack/EvilMintImpl.sol";
14import {EvilOwnerImpl} from "../src/attack/EvilOwnerImpl.sol";
15import {EvilFeeImpl} from "../src/attack/EvilFeeImpl.sol";
16import {EvilBaseFeeImpl} from "../src/attack/EvilBaseFeeImpl.sol";
17import {EvilDrainImpl} from "../src/attack/EvilDrainImpl.sol";
18import {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).
40contract 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}