forked from ArbitrumFoundation/governance
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathproposalCreator.ts
344 lines (315 loc) · 11 KB
/
proposalCreator.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
import { getL1Network, getL2Network } from "@arbitrum/sdk";
import { ArbSys__factory } from "@arbitrum/sdk/dist/lib/abi/factories/ArbSys__factory";
import { ARB_SYS_ADDRESS } from "@arbitrum/sdk/dist/lib/dataEntities/constants";
import { defaultAbiCoder } from "@ethersproject/abi";
import { JsonRpcProvider } from "@ethersproject/providers";
import { BigNumber, constants, utils } from "ethers";
import { id, keccak256 } from "ethers/lib/utils";
import {
L1ArbitrumTimelock__factory,
L2ArbitrumGovernor__factory,
ArbitrumTimelock__factory,
UpgradeExecutor__factory,
} from "../typechain-types";
/**
* Config network where the governor is located.
*/
export interface L2GovConfig {
/**
* Address of the governor where the proposal will be sent
*/
readonly governorAddr: string;
/**
* Provider for the network where the governor is located
*/
readonly provider: JsonRpcProvider;
}
/**
* Config for the L1 network on which this l2 networks are based
*/
export interface L1GovConfig {
/**
* The address of the timelock on L1
*/
readonly timelockAddr: string;
/**
* Provider for the L1 network
*/
readonly provider: JsonRpcProvider;
}
/**
* Config for the network where the upgrade will actually take place - for a mainnet upgrade, it could be ArbOne, L1, or ArbNova.
*/
export interface UpgradeConfig {
/**
* Address of the upgrade executor that will execute the upgrade
*/
readonly upgradeExecutorAddr: string;
/**
* Provider for the network where the upgrade will take place
*/
readonly provider: JsonRpcProvider;
}
export interface UpgradePathConfig {}
/**
* A governance proposal
*/
export class Proposal {
public constructor(
public readonly target: string,
public readonly value: BigNumber,
public readonly callData: string,
public readonly description: string
) {}
/**
* Form a proposal from the call data sent to the propose method
* @param data
* @returns
*/
public static fromData(data: string) {
const arbGovInterface = L2ArbitrumGovernor__factory.createInterface();
const parts = arbGovInterface.decodeFunctionData("propose", data) as [
string[],
BigNumber[],
string[],
string
];
return new Proposal(parts[0][0], parts[1][0], parts[2][0], parts[3]);
}
/**
* Encode the proposal parameters as call data to be sent to the governor
*/
public encode() {
const arbGovInterface = L2ArbitrumGovernor__factory.createInterface();
return arbGovInterface.encodeFunctionData("propose", [
[this.target],
[this.value],
[this.callData],
this.description,
]);
}
/**
* The id of this proposal
*/
public id() {
const descriptionHash = id(this.description);
return keccak256(
defaultAbiCoder.encode(
["address[]", "uint256[]", "bytes[]", "bytes32"],
[[this.target], [this.value], [this.callData], descriptionHash]
)
);
}
}
/**
* Creates proposals that originate on an L2, are then withdrawn to an L1 and go through a timelock
* there, and are then finally executed on another L2 or L1.
*/
export class RoundTripProposalCreator {
/**
* A proposal creator for a specific round trip config
* @param l1Config Config for the L1 network on which this l2 networks are based
* @param targetNetworkConfigs Configs for the network where the upgrades will actually take place - could be ArbOne, L1, or ArbNova (for mainnet).
*/
constructor(
public readonly l1Config: L1GovConfig,
public readonly targetNetworkConfigs: UpgradeConfig[]
) {}
/**
* Creates calldata for roundtrio path; data to be used either in a proposal or directly in timelock.schedule
*/
public async createRoundTripCallData(
upgradeAddrs: string[],
upgradeValues: BigNumber[],
upgradeDatas: string[],
proposalDescription: string
) {
const { l1TimelockTo, l1TimelockScheduleCallData } =
await this.createRoundTripCallDataForArbSysCall(
upgradeAddrs,
upgradeValues,
upgradeDatas,
proposalDescription
);
const iArbSys = ArbSys__factory.createInterface();
return iArbSys.encodeFunctionData("sendTxToL1", [l1TimelockTo, l1TimelockScheduleCallData]);
}
/**
* Generates arguments for ArbSys.sendTxToL1 for a constitutional proposal. Can be used to submit a proposal in e.g. the Tally UI.
*/
public async createRoundTripCallDataForArbSysCall(
upgradeAddrs: string[],
upgradeValues: BigNumber[],
upgradeDatas: string[],
proposalDescription: string,
useSchedule = false // defaults to scheduleBatch in L1 timelock. If true, will use schedule. Can only be used if only one action is included in proposal
) {
if (
new Set([
upgradeAddrs.length,
upgradeValues.length,
upgradeDatas.length,
this.targetNetworkConfigs.length,
]).size > 1
)
throw new Error("Inputs array size mismatch");
const descriptionHash = id(proposalDescription);
// the l1 timelock
const l1TimelockTo = this.l1Config.timelockAddr;
const l1Timelock = L1ArbitrumTimelock__factory.connect(l1TimelockTo, this.l1Config.provider);
const minDelay = await l1Timelock.getMinDelay();
const l1Targets: string[] = [];
const l1Values: BigNumber[] = [];
const l1CallDatas: string[] = [];
for (let i = 0; i < upgradeAddrs.length; i++) {
const upgradeAddr = upgradeAddrs[i];
const upgradeValue = upgradeValues[i];
const upgradeData = upgradeDatas[i];
const targetNetworkConfig = this.targetNetworkConfigs[i];
// indices of the target network configs should correspond to the indices of the upgradeAddrs (and upgradeValues and upgradeDatas)
// we include this sanity check to help catch a misconfiguration:
if ((await targetNetworkConfig.provider.getCode(upgradeAddr)).length == 2)
throw new Error("Action contract not found on configured network");
// the upgrade executor
const iUpgradeExecutor = UpgradeExecutor__factory.createInterface();
const upgradeExecutorCallData = iUpgradeExecutor.encodeFunctionData("execute", [
upgradeAddr,
upgradeData,
]);
const upgradeExecutorTo = targetNetworkConfig.upgradeExecutorAddr;
const upgradeExecutorValue = upgradeValue;
const inbox = await (async () => {
targetNetworkConfig.provider;
try {
const l2Network = await getL2Network(targetNetworkConfig.provider);
return l2Network.ethBridge.inbox;
} catch (err) {
// just check this is an expected l1 chain id and throw if not
await getL1Network(targetNetworkConfig.provider);
return null;
}
})();
if (inbox) {
l1Targets.push(await l1Timelock.RETRYABLE_TICKET_MAGIC());
l1CallDatas.push(
defaultAbiCoder.encode(
["address", "address", "uint256", "uint256", "uint256", "bytes"],
[inbox, upgradeExecutorTo, upgradeExecutorValue, 0, 0, upgradeExecutorCallData]
)
);
// this value gets ignored from xchain upgrades
l1Values.push(BigNumber.from(0));
} else {
l1Targets.push(upgradeExecutorTo);
l1CallDatas.push(upgradeExecutorCallData);
l1Values.push(upgradeExecutorValue);
}
}
const l1TimelockScheduleCallData = (() => {
if (useSchedule) {
if (upgradeAddrs.length > 1)
throw new Error("Must use schedule batch for multiple messages");
return l1Timelock.interface.encodeFunctionData("schedule", [
l1Targets[0],
l1Values[0],
l1CallDatas[0],
constants.HashZero,
descriptionHash,
minDelay,
]);
} else {
return l1Timelock.interface.encodeFunctionData("scheduleBatch", [
l1Targets,
l1Values,
l1CallDatas,
constants.HashZero,
descriptionHash,
minDelay,
]);
}
})();
return {
l1TimelockTo,
l1TimelockScheduleCallData,
};
}
/**
* Create a a new proposal
* @param upgradeAddr The address of the upgrade contract that will be called by an UpgradeExecutor
* @param upgradeValue Value sent to the upgrade contract
* @param upgradeData Call data sent to the upgrade contract
* @param proposalDescription The proposal description
* @returns
*/
public async create(
upgradeAddrs: string[],
upgradeValues: BigNumber[],
upgradeDatas: string[],
proposalDescription: string
): Promise<Proposal> {
const proposalCallData = await this.createRoundTripCallData(
upgradeAddrs,
upgradeValues,
upgradeDatas,
proposalDescription
);
return new Proposal(ARB_SYS_ADDRESS, BigNumber.from(0), proposalCallData, proposalDescription);
}
/**
* Outputs the arguments to be passed in to to the Core Governor Timelock's schedule method. This can be called by the 7 of 12 security council (non-critical delayed upgrade)
* @param l2GovConfig config for network where governance is located
* @param upgradeAddr address of Governance Action contract (to be eventually passed into UpgradeExecutor.execute)
* @param description The proposal description
* @returns Object with Timelock.schedule params
*/
public async createTimelockScheduleArgs(
l2GovConfig: L2GovConfig,
upgradeAddrs: string[],
description: string,
options: {
upgradeValue?: BigNumber;
_upgradeParams?: {
upgradeABI: string;
upgradeArgs: any[];
};
_delay?: BigNumber;
predecessor?: string;
} = {}
) {
// default upgrade value and predecessor values
const { upgradeValue = constants.Zero, predecessor = "0x" } = options;
const l2Gov = await L2ArbitrumGovernor__factory.connect(
l2GovConfig.governorAddr,
l2GovConfig.provider
);
const l2TimelockAddress = await l2Gov.timelock();
const l2Timelock = await ArbitrumTimelock__factory.connect(
l2TimelockAddress,
l2GovConfig.provider
);
const minDelay = await l2Timelock.getMinDelay();
const delay = options?._delay || minDelay; // default to min delay
if (delay.lt(minDelay)) throw new Error("Timelock delay below minimum delay");
let ABI = options?._upgradeParams
? [options?._upgradeParams.upgradeABI]
: ["function perform() external"]; // default to perform with no params
let upgradeArgs = options?._upgradeParams ? options?._upgradeParams.upgradeArgs : []; // default to empty array / no values
let actionIface = new utils.Interface(ABI);
const upgradeData = actionIface.encodeFunctionData("perform", upgradeArgs);
const proposalCallData = await this.createRoundTripCallData(
upgradeAddrs,
upgradeAddrs.map(() => upgradeValue),
upgradeAddrs.map(() => upgradeData),
description
);
const salt = keccak256(defaultAbiCoder.encode(["string"], [description]));
return {
target: ARB_SYS_ADDRESS,
value: upgradeValue.toNumber(),
data: proposalCallData,
predecessor,
salt,
delay: delay.toNumber(),
};
}
}