Skip to content

Commit 8bdf2d7

Browse files
committed
feat(op-deployer): integrate safe deployment/provisioning
This adds safe contracts once L1 is ready, then provisions the safe wallets based on provided specification, and finally integrate these wallets into the op-deployer workflow.
1 parent 2b18d37 commit 8bdf2d7

5 files changed

Lines changed: 243 additions & 80 deletions

File tree

main.star

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -99,12 +99,13 @@ def run(plan, args={}):
9999
wait_for_sync.wait_for_startup(plan, l1_config_env_vars)
100100

101101
deployment_output = contract_deployer.deploy_contracts(
102-
plan,
103-
l1_priv_key,
104-
l1_config_env_vars,
105-
optimism_args,
106-
l1_network,
107-
altda_deploy_config,
102+
plan=plan,
103+
priv_key=l1_priv_key,
104+
l1_config_env_vars=l1_config_env_vars,
105+
optimism_args=optimism_args,
106+
l1_network=l1_network,
107+
altda_args=altda_deploy_config,
108+
registry=registry,
108109
)
109110

110111
jwt_file = plan.upload_files(

src/contracts/contract_deployer.star

Lines changed: 156 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ FACTORY_DEPLOYER_CODE = "0xf8a58085174876e800830186a08080b853604580600e600039806
66

77
FUND_SCRIPT_FILEPATH = "../../static_files/scripts"
88

9-
utils = import_module("../util.star")
9+
_utils = import_module("../util.star")
1010
_filter = import_module("../util/filter.star")
11+
_registry = import_module("/src/package_io/registry.star")
12+
_file = import_module("/src/util/file.star")
1113

1214
ethereum_package_genesis_constants = import_module(
1315
"github.com/ethpandaops/ethereum-package/src/prelaunch_data_generator/genesis_constants/genesis_constants.star"
@@ -72,7 +74,7 @@ def _normalize_artifacts_locators(plan, l1_locator, l2_locator):
7274

7375

7476
def deploy_contracts(
75-
plan, priv_key, l1_config_env_vars, optimism_args, l1_network, altda_args
77+
plan, priv_key, l1_config_env_vars, optimism_args, l1_network, altda_args, registry
7678
):
7779
l2_chain_ids_list = [
7880
str(chain.network_params.network_id) for chain in optimism_args.chains
@@ -116,10 +118,11 @@ def deploy_contracts(
116118
name="op-deployer-fund-script",
117119
)
118120

121+
# fund wallets
119122
plan.run_sh(
120123
name="op-deployer-fund",
121124
description="Collect keys, and fund addresses",
122-
image=utils.DEPLOYMENT_UTILS_IMAGE,
125+
image=_utils.DEPLOYMENT_UTILS_IMAGE,
123126
env_vars={
124127
"DEPLOYER_PRIVATE_KEY": priv_key,
125128
"FUND_PRIVATE_KEY": ethereum_package_genesis_constants.PRE_FUNDED_ACCOUNTS[
@@ -142,6 +145,101 @@ def deploy_contracts(
142145
run='bash /fund-script/fund.sh "{0}"'.format(l2_chain_ids),
143146
)
144147

148+
apply_files = {
149+
"/network-data": op_deployer_init.files_artifacts[0],
150+
}
151+
152+
# deploy safes if necessary
153+
safes_params = optimism_args.op_contract_deployer_params.safes
154+
155+
if safes_params.enabled:
156+
roles = {}
157+
for name, spec in safes_params.roles.items():
158+
parts = spec.split(":")
159+
threshold = int(parts[0])
160+
num_owners = int(parts[1])
161+
owners = [
162+
ethereum_package_genesis_constants.PRE_FUNDED_ACCOUNTS[i].address
163+
for i in range(num_owners)
164+
]
165+
roles[name] = {
166+
"threshold": threshold,
167+
"owners": owners,
168+
}
169+
170+
safe_utils_image = safes_params.image
171+
pk = priv_key
172+
node_url = l1_config_env_vars["L1_RPC_URL"]
173+
mount_point = "/safes-data"
174+
contracts_path = "{0}/contracts.json".format(mount_point)
175+
safes_path = "{0}/safes.json".format(mount_point)
176+
roles_path = "/roles/roles.json"
177+
178+
# first deploy the contracts if need be
179+
safe = plan.run_sh(
180+
name="op-deployer-deploy-safe-contracts",
181+
description="Deploy safe contracts",
182+
image=safe_utils_image,
183+
store=[
184+
StoreSpec(
185+
src=mount_point,
186+
name="safe-utils-data",
187+
)
188+
],
189+
env_vars={
190+
"PK": pk,
191+
"NODE_URL": node_url,
192+
"OUTPUT": contracts_path,
193+
},
194+
run=" && ".join(
195+
[
196+
"mkdir -p {0}".format(mount_point),
197+
"/deploy_contracts.sh",
198+
]
199+
),
200+
)
201+
202+
# then create the roles spec file
203+
roles_spec = _file.from_string(
204+
plan,
205+
"/roles.json",
206+
json.encode(roles),
207+
"op-deployer-roles-spec",
208+
"Rendered roles spec",
209+
)
210+
211+
# then create the safes
212+
plan.run_sh(
213+
name="op-deployer-deploy-safes",
214+
description="Deploy safes",
215+
image=safe_utils_image,
216+
env_vars={
217+
"PK": pk,
218+
"NODE_URL": node_url,
219+
"CONTRACTS_JSON": contracts_path,
220+
"ROLES_JSON": roles_path,
221+
"OUTPUT": safes_path,
222+
},
223+
files={
224+
mount_point: safe.files_artifacts[0],
225+
"/roles": roles_spec,
226+
},
227+
store=[
228+
StoreSpec(
229+
src=mount_point,
230+
name="safe-utils-data",
231+
)
232+
],
233+
run=" && ".join(
234+
[
235+
"mkdir -p {0}".format(mount_point),
236+
"/provision_wallets.sh",
237+
]
238+
),
239+
)
240+
241+
apply_files[mount_point] = safe.files_artifacts[0]
242+
145243
hardfork_schedule = []
146244
for index, chain in enumerate(optimism_args.chains):
147245
np = chain.network_params
@@ -168,12 +266,20 @@ def deploy_contracts(
168266
"l1ContractsLocator": l1_artifacts_locator,
169267
"l2ContractsLocator": l2_artifacts_locator,
170268
"superchainRoles": {
171-
"superchainGuardian": read_chain_cmd("l1ProxyAdmin", l2_chain_ids_list[0]),
172-
"protocolVersionsOwner": read_chain_cmd(
173-
"l1ProxyAdmin", l2_chain_ids_list[0]
269+
"superchainGuardian": _read_chain_cmd(
270+
role="superchainGuardian",
271+
fallback="l1ProxyAdmin",
272+
chain_id=l2_chain_ids_list[0],
273+
),
274+
"protocolVersionsOwner": _read_chain_cmd(
275+
role="protocolVersionsOwner",
276+
fallback="l1ProxyAdmin",
277+
chain_id=l2_chain_ids_list[0],
174278
),
175-
"superchainProxyAdminOwner": read_chain_cmd(
176-
"l1ProxyAdmin", l2_chain_ids_list[0]
279+
"superchainProxyAdminOwner": _read_chain_cmd(
280+
role="superchainProxyAdminOwner",
281+
fallback="l1ProxyAdmin",
282+
chain_id=l2_chain_ids_list[0],
177283
),
178284
},
179285
"chains": [],
@@ -206,21 +312,37 @@ def deploy_contracts(
206312
True if chain.network_params.fund_dev_accounts else False
207313
),
208314
},
209-
"baseFeeVaultRecipient": read_chain_cmd(
210-
"baseFeeVaultRecipient", chain_id
315+
"baseFeeVaultRecipient": _read_chain_cmd(
316+
role="baseFeeVaultRecipient", chain_id=chain_id
211317
),
212-
"l1FeeVaultRecipient": read_chain_cmd("l1FeeVaultRecipient", chain_id),
213-
"sequencerFeeVaultRecipient": read_chain_cmd(
214-
"sequencerFeeVaultRecipient", chain_id
318+
"l1FeeVaultRecipient": _read_chain_cmd(
319+
role="l1FeeVaultRecipient", chain_id=chain_id
320+
),
321+
"sequencerFeeVaultRecipient": _read_chain_cmd(
322+
role="sequencerFeeVaultRecipient", chain_id=chain_id
215323
),
216324
"roles": {
217-
"batcher": read_chain_cmd("batcher", chain_id),
218-
"challenger": read_chain_cmd("challenger", chain_id),
219-
"l1ProxyAdminOwner": read_chain_cmd("l1ProxyAdmin", chain_id),
220-
"l2ProxyAdminOwner": read_chain_cmd("l2ProxyAdmin", chain_id),
221-
"proposer": read_chain_cmd("proposer", chain_id),
222-
"systemConfigOwner": read_chain_cmd("systemConfigOwner", chain_id),
223-
"unsafeBlockSigner": read_chain_cmd("sequencer", chain_id),
325+
"batcher": _read_chain_cmd(role="batcher", chain_id=chain_id),
326+
"challenger": _read_chain_cmd(role="challenger", chain_id=chain_id),
327+
"l1ProxyAdminOwner": _read_chain_cmd(
328+
role="l1ProxyAdminOwner",
329+
fallback="l1ProxyAdmin",
330+
chain_id=chain_id,
331+
),
332+
"l2ProxyAdminOwner": _read_chain_cmd(
333+
role="l2ProxyAdminOwner",
334+
fallback="l2ProxyAdmin",
335+
chain_id=chain_id,
336+
),
337+
"proposer": _read_chain_cmd(role="proposer", chain_id=chain_id),
338+
"systemConfigOwner": _read_chain_cmd(
339+
role="systemConfigOwner", chain_id=chain_id
340+
),
341+
"unsafeBlockSigner": _read_chain_cmd(
342+
role="unsafeBlockSigner",
343+
fallback="sequencer",
344+
chain_id=chain_id,
345+
),
224346
},
225347
"dangerousAdditionalDisputeGames": [
226348
{
@@ -252,22 +374,23 @@ def deploy_contracts(
252374
intent["chains"].append(intent_chain)
253375

254376
intent_json = json.encode(intent)
255-
intent_json_artifact = utils.write_to_file(plan, intent_json, "/tmp", "intent.json")
377+
intent_json_artifact = _utils.write_to_file(
378+
plan, intent_json, "/tmp", "intent.json"
379+
)
380+
381+
apply_files["/tmp"] = intent_json_artifact
256382

257383
op_deployer_configure = plan.run_sh(
258384
name="op-deployer-configure",
259385
description="Configure L2 contract deployments",
260-
image=utils.DEPLOYMENT_UTILS_IMAGE,
386+
image=_utils.DEPLOYMENT_UTILS_IMAGE,
261387
store=[
262388
StoreSpec(
263389
src="/network-data",
264390
name="op-deployer-configs",
265391
)
266392
],
267-
files={
268-
"/network-data": op_deployer_init.files_artifacts[0],
269-
"/tmp": intent_json_artifact,
270-
},
393+
files=apply_files,
271394
run=" && ".join(
272395
[
273396
# zhwrd: this mess is temporary until we implement json reading for op-deployer intent file
@@ -327,7 +450,7 @@ def deploy_contracts(
327450
plan.run_sh(
328451
name="op-deployer-generate-chainspec",
329452
description="Generate chainspec",
330-
image=utils.DEPLOYMENT_UTILS_IMAGE,
453+
image=_utils.DEPLOYMENT_UTILS_IMAGE,
331454
env_vars={"CHAIN_ID": str(chain.network_params.network_id)},
332455
store=[
333456
StoreSpec(
@@ -349,5 +472,9 @@ def chain_key(index, key):
349472
return "chains.[{0}].{1}".format(index, key)
350473

351474

352-
def read_chain_cmd(filename, l2_chain_id):
353-
return "`jq -r .address /network-data/{0}-{1}.json`".format(filename, l2_chain_id)
475+
def _read_chain_cmd(role, chain_id, fallback=None):
476+
# if the role is in the safes.json file, use it
477+
# otherwise, read the fallback from the network-data file
478+
return "`jq -e -r .{0} /safes-data/safes.json 2>/dev/null | grep -v null || jq -e -r .address /network-data/{1}-{2}.json`".format(
479+
role, fallback or role, chain_id
480+
)

src/contracts/input_parser.star

Lines changed: 65 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,25 @@ _net = import_module("/src/util/net.star")
33
_id = import_module("/src/util/id.star")
44
_registry = import_module("/src/package_io/registry.star")
55

6+
_DEFAULT_SAFES_ARGS = {
7+
"enabled": False,
8+
"image": None,
9+
"roles": {},
10+
}
11+
12+
613
_DEFAULT_ARGS = {
714
"image": None,
815
"l1_artifacts_locator": None,
916
"l2_artifacts_locator": None,
1017
"overrides": {},
11-
"multisig": {},
18+
"safes": _DEFAULT_SAFES_ARGS,
1219
}
1320

1421

1522
def parse(args, registry):
1623
# Any extra attributes will cause an error
17-
extra_keys = _filter.assert_keys(
24+
_filter.assert_keys(
1825
args or {},
1926
_DEFAULT_ARGS.keys(),
2027
"Invalid attributes in op-deployer configuration: {}",
@@ -26,24 +33,67 @@ def parse(args, registry):
2633
_registry.OP_DEPLOYER
2734
)
2835

29-
op_deployer_params["l1_artifacts_locator"] = op_deployer_params["l1_artifacts_locator"] or registry.get(
30-
_registry.OP_CONTRACTS
31-
)
36+
op_deployer_params["l1_artifacts_locator"] = op_deployer_params[
37+
"l1_artifacts_locator"
38+
] or registry.get(_registry.OP_CONTRACTS)
3239

33-
op_deployer_params["l2_artifacts_locator"] = op_deployer_params["l2_artifacts_locator"] or registry.get(
34-
_registry.OP_CONTRACTS
35-
)
40+
op_deployer_params["l2_artifacts_locator"] = op_deployer_params[
41+
"l2_artifacts_locator"
42+
] or registry.get(_registry.OP_CONTRACTS)
43+
44+
_validate_overrides(op_deployer_params["overrides"])
3645

37-
_validate_string_map("overrides", op_deployer_params["overrides"])
38-
_validate_string_map("multisig", op_deployer_params["multisig"])
46+
op_deployer_params["safes"] = _parse_safes(op_deployer_params["safes"], registry)
3947

4048
return struct(**op_deployer_params)
4149

4250

43-
def _validate_string_map(name, string_map):
44-
if type(string_map) != "dict":
45-
fail("{} must be a dict, got {}".format(name, type(string_map)))
51+
def _validate_overrides(overrides):
52+
if type(overrides) != "dict":
53+
fail("overrides must be a dict, got {}".format(type(overrides)))
4654

47-
for key, value in string_map.items():
55+
for key, value in overrides.items():
4856
if type(value) != "string":
49-
fail("{} must be a dict of strings, got {}".format(name, type(value)))
57+
fail("overrides must be a dict of strings, got {}".format(type(overrides)))
58+
59+
60+
def _parse_safes(safes, registry):
61+
safes = _validate_safes(safes)
62+
63+
if safes["enabled"]:
64+
safes["image"] = safes["image"] or registry.get(_registry.SAFE_UTILS)
65+
66+
return struct(**safes)
67+
68+
69+
def _validate_safes(safes):
70+
_filter.assert_keys(
71+
safes or {},
72+
_DEFAULT_SAFES_ARGS.keys(),
73+
"Invalid attributes in safes configuration: {}",
74+
)
75+
76+
safes = _DEFAULT_SAFES_ARGS | _filter.remove_none(safes or {})
77+
78+
roles = safes["roles"]
79+
if type(roles) != "dict":
80+
fail("safes.roles must be a dict, got {}".format(type(roles)))
81+
82+
for name, spec in roles.items():
83+
if type(spec) != "string":
84+
fail("safes.roles must be a dict of strings, got {}".format(type(roles)))
85+
86+
parts = spec.split(":")
87+
if len(parts) != 2:
88+
fail("Invalid safes.roles spec for {}: {}".format(name, spec))
89+
90+
threshold = int(parts[0])
91+
num_owners = int(parts[1])
92+
if num_owners < threshold:
93+
fail(
94+
"safes.roles must have at least as many owners as the threshold for {}".format(
95+
name
96+
)
97+
)
98+
99+
return safes

0 commit comments

Comments
 (0)