1use std::path::PathBuf;
4
5use alloy::{
6 hex::FromHex,
7 primitives::{Address, B256, Bytes, U256},
8 providers::{Provider, WalletProvider},
9};
10use anyhow::{Context, Result, ensure};
11use derive_builder::Builder;
12use espresso_types::v0_1::L1Client;
13use hotshot_contract_adapter::sol_types::{LightClientStateSol, StakeTableStateSol};
14use url::Url;
15
16use crate::{
17 Contract, Contracts, OwnableContract, encode_function_call,
18 output::output_safe_tx_builder,
19 proposals::{
20 multisig::{
21 LightClientV2UpgradeParams, MultisigOwnerCheck, StakeTableV2UpgradeParams,
22 StakeTableV3UpgradeParams, TransferOwnershipParams, encode_generic_calldata,
23 transfer_ownership_from_multisig_to_timelock, upgrade_esp_token_v2_multisig_owner,
24 upgrade_fee_contract_multisig_owner, upgrade_light_client_v2_multisig_owner,
25 upgrade_light_client_v3_multisig_owner, upgrade_stake_table_v2_multisig_owner,
26 upgrade_stake_table_v3_multisig_owner,
27 },
28 timelock::{
29 StakeTableV3TimelockProposalParams, TimelockOperationParams, TimelockOperationPayload,
30 TimelockOperationType, derive_timelock_address_from_contract_type,
31 perform_timelock_operation, upgrade_stake_table_v3_timelock_proposal,
32 },
33 write::{WriteProposalParams, resolve_network, write_stake_table_v3_proposal_dir},
34 },
35};
36
37#[derive(Builder, Clone)]
72#[builder(setter(strip_option))]
73pub struct DeployerArgs<P: Provider + WalletProvider> {
74 deployer: P,
75 rpc_url: Url,
76 #[builder(default)]
77 token_recipient: Option<Address>,
78 #[builder(default)]
79 mock_light_client: bool,
80 #[builder(default)]
81 use_multisig: bool,
82 #[builder(default)]
83 genesis_lc_state: Option<LightClientStateSol>,
84 #[builder(default)]
85 genesis_st_state: Option<StakeTableStateSol>,
86 #[builder(default)]
87 permissioned_prover: Option<Address>,
88 #[builder(default)]
89 blocks_per_epoch: Option<u64>,
90 #[builder(default)]
91 epoch_start_block: Option<u64>,
92 #[builder(default)]
93 exit_escrow_period: Option<U256>,
94 #[builder(default)]
95 multisig: Option<Address>,
96 #[builder(default)]
97 multisig_pauser: Option<Address>,
98 #[builder(default)]
99 initial_token_supply: Option<U256>,
100 #[builder(default)]
101 token_name: Option<String>,
102 #[builder(default)]
103 token_symbol: Option<String>,
104 #[builder(default)]
105 ops_timelock_admin: Option<Address>,
106 #[builder(default)]
107 ops_timelock_delay: Option<U256>,
108 #[builder(default)]
109 ops_timelock_executors: Option<Vec<Address>>,
110 #[builder(default)]
111 ops_timelock_proposers: Option<Vec<Address>>,
112 #[builder(default)]
113 safe_exit_timelock_admin: Option<Address>,
114 #[builder(default)]
115 safe_exit_timelock_delay: Option<U256>,
116 #[builder(default)]
117 safe_exit_timelock_executors: Option<Vec<Address>>,
118 #[builder(default)]
119 safe_exit_timelock_proposers: Option<Vec<Address>>,
120 #[builder(default)]
121 timelock_operation_type: Option<TimelockOperationType>,
122 #[builder(default)]
123 target_contract: Option<OwnableContract>,
124 #[builder(default)]
125 timelock_operation_value: Option<U256>,
126 #[builder(default)]
127 timelock_operation_delay: Option<U256>,
128 #[builder(default)]
129 timelock_operation_function_signature: Option<String>,
130 #[builder(default)]
131 timelock_operation_function_values: Option<Vec<String>>,
132 #[builder(default)]
133 timelock_operation_salt: Option<String>,
134 #[builder(default)]
135 use_timelock_owner: Option<bool>,
136 #[builder(default)]
137 transfer_ownership_from_eoa: Option<bool>,
138 #[builder(default)]
139 transfer_ownership_new_owner: Option<Address>,
140 #[builder(default)]
141 timelock_operation_id: Option<String>,
142 #[builder(default)]
143 multisig_transaction_target: Option<Address>,
144 #[builder(default)]
145 multisig_transaction_function_signature: Option<String>,
146 #[builder(default)]
147 multisig_transaction_function_args: Option<Vec<String>>,
148 #[builder(default)]
149 multisig_transaction_value: Option<String>,
150 #[builder(default)]
151 output_path: Option<PathBuf>,
152 #[builder(default)]
153 output_dir: Option<PathBuf>,
154 #[builder(default)]
155 chain_id: u64,
156 #[builder(default)]
158 network: Option<String>,
159 #[builder(default)]
161 proposal_slug: Option<String>,
162 #[builder(default)]
164 proposals_root: Option<PathBuf>,
165}
166
167impl<P: Provider + WalletProvider> DeployerArgs<P> {
168 pub async fn deploy(&self, contracts: &mut Contracts, target: Contract) -> Result<()> {
170 let provider = &self.deployer;
171 let admin = provider.default_signer_address();
172 match target {
173 Contract::FeeContractProxy => {
174 if contracts.address(Contract::FeeContractProxy).is_some() {
175 let use_multisig = self.use_multisig;
177
178 tracing::info!(?use_multisig, "Upgrading FeeContract to V1.0.1");
179 if use_multisig {
180 let calldata = upgrade_fee_contract_multisig_owner(
181 provider,
182 contracts,
183 MultisigOwnerCheck::RequireContract,
184 )
185 .await?
186 .with_description("Upgrade FeeContract to V1.0.1".to_string());
187 output_safe_tx_builder(
188 &calldata,
189 self.output_path.as_deref(),
190 self.chain_id,
191 )?;
192 } else {
193 crate::upgrade_fee_v1(provider, contracts).await?;
194 }
195 } else {
196 let addr = crate::deploy_fee_contract_proxy(provider, contracts, admin).await?;
198
199 if let Some(use_timelock_owner) = self.use_timelock_owner {
200 tracing::info!(
205 "Transferring ownership to OpsTimelock: {:?}",
206 use_timelock_owner
207 );
208 if use_timelock_owner {
210 let timelock_addr = derive_timelock_address_from_contract_type(
211 OwnableContract::FeeContractProxy,
212 contracts,
213 )?;
214 crate::transfer_ownership(
215 provider,
216 Contract::FeeContractProxy,
217 addr,
218 timelock_addr,
219 )
220 .await?;
221 }
222 } else if let Some(multisig) = self.multisig {
223 tracing::info!("Transferring ownership to multisig: {:?}", multisig);
224 crate::transfer_ownership(
225 provider,
226 Contract::FeeContractProxy,
227 addr,
228 multisig,
229 )
230 .await?;
231 }
232 }
233 },
234 Contract::EspTokenProxy => {
235 let token_recipient = self.token_recipient.unwrap_or(admin);
236 let token_name = self
237 .token_name
238 .clone()
239 .context("Token name must be set when deploying esp token")?;
240 let token_symbol = self
241 .token_symbol
242 .clone()
243 .context("Token symbol must be set when deploying esp token")?;
244 let initial_supply = self
245 .initial_token_supply
246 .context("Initial token supply must be set when deploying esp token")?;
247 crate::deploy_token_proxy(
248 provider,
249 contracts,
250 admin,
251 token_recipient,
252 initial_supply,
253 &token_name,
254 &token_symbol,
255 )
256 .await?;
257
258 },
260 Contract::EspTokenV2 => {
261 let use_multisig = self.use_multisig;
262
263 if use_multisig {
264 let calldata = upgrade_esp_token_v2_multisig_owner(
265 provider,
266 contracts,
267 MultisigOwnerCheck::RequireContract,
268 )
269 .await?
270 .with_description("Upgrade EspToken to V2".to_string());
271 output_safe_tx_builder(&calldata, self.output_path.as_deref(), self.chain_id)?;
272 } else {
273 crate::upgrade_esp_token_v2(provider, contracts).await?;
274 let addr = contracts
275 .address(Contract::EspTokenProxy)
276 .expect("fail to get EspTokenProxy address");
277
278 if let Some(use_timelock_owner) = self.use_timelock_owner {
279 if use_timelock_owner {
281 tracing::info!("Transferring ownership to SafeExitTimelock");
286 let timelock_addr = derive_timelock_address_from_contract_type(
287 OwnableContract::EspTokenProxy,
288 contracts,
289 )?;
290 crate::transfer_ownership(
291 provider,
292 Contract::EspTokenProxy,
293 addr,
294 timelock_addr,
295 )
296 .await?;
297 }
298 } else if let Some(multisig) = self.multisig {
299 let token_proxy = contracts
300 .address(Contract::EspTokenProxy)
301 .expect("fail to get EspTokenProxy address");
302 crate::transfer_ownership(
303 provider,
304 Contract::EspTokenProxy,
305 token_proxy,
306 multisig,
307 )
308 .await?;
309 }
310 }
311 },
312 Contract::LightClientProxy => {
313 assert!(
314 self.genesis_lc_state.is_some(),
315 "forget to specify genesis_lc_state()"
316 );
317 assert!(
318 self.genesis_st_state.is_some(),
319 "forget to specify genesis_st_state()"
320 );
321 crate::deploy_light_client_proxy(
322 provider,
323 contracts,
324 self.mock_light_client,
325 self.genesis_lc_state.clone().unwrap(),
326 self.genesis_st_state.clone().unwrap(),
327 admin,
328 self.permissioned_prover,
329 )
330 .await?;
331 },
333 Contract::LightClientV2 => {
334 assert!(
335 self.blocks_per_epoch.is_some(),
336 "forgot to specify blocks_per_epoch()"
337 );
338 assert!(
339 self.epoch_start_block.is_some(),
340 "forgot to specify epoch_start_block()"
341 );
342
343 let use_mock = self.mock_light_client;
344 let use_multisig = self.use_multisig;
345 let mut blocks_per_epoch = self.blocks_per_epoch.unwrap();
346 let epoch_start_block = self.epoch_start_block.unwrap();
347
348 if use_mock && blocks_per_epoch == 0 {
353 blocks_per_epoch = u64::MAX;
354 }
355 tracing::info!(%blocks_per_epoch, ?use_multisig, "Upgrading LightClientV2 with ");
356 if use_multisig {
357 let calldata = upgrade_light_client_v2_multisig_owner(
358 provider,
359 contracts,
360 LightClientV2UpgradeParams {
361 blocks_per_epoch,
362 epoch_start_block,
363 },
364 use_mock,
365 MultisigOwnerCheck::RequireContract,
366 )
367 .await?
368 .with_description("Upgrade LightClient to V2".to_string());
369 output_safe_tx_builder(&calldata, self.output_path.as_deref(), self.chain_id)?;
370 } else {
371 crate::upgrade_light_client_v2(
372 provider,
373 contracts,
374 use_mock,
375 blocks_per_epoch,
376 epoch_start_block,
377 )
378 .await?;
379 }
381 },
382 Contract::LightClientV3 => {
383 let use_mock = self.mock_light_client;
384 let use_multisig = self.use_multisig;
385
386 tracing::info!(?use_multisig, "Upgrading LightClientV3 with ");
387 if use_multisig {
388 let calldata = upgrade_light_client_v3_multisig_owner(
389 provider,
390 contracts,
391 use_mock,
392 MultisigOwnerCheck::RequireContract,
393 )
394 .await?
395 .with_description("Upgrade LightClient to V3".to_string());
396 output_safe_tx_builder(&calldata, self.output_path.as_deref(), self.chain_id)?;
397 } else {
398 crate::upgrade_light_client_v3(provider, contracts, use_mock).await?;
399
400 let addr = contracts
402 .address(Contract::LightClientProxy)
403 .expect("fail to get LightClientProxy address");
404
405 if let Some(use_timelock_owner) = self.use_timelock_owner {
406 tracing::info!("Transferring ownership to OpsTimelock");
411 if use_timelock_owner {
413 let timelock_addr = derive_timelock_address_from_contract_type(
414 OwnableContract::LightClientProxy,
415 contracts,
416 )?;
417 crate::transfer_ownership(
418 provider,
419 Contract::LightClientProxy,
420 addr,
421 timelock_addr,
422 )
423 .await?;
424 }
425 } else if let Some(multisig) = self.multisig {
426 crate::transfer_ownership(
427 provider,
428 Contract::LightClientProxy,
429 addr,
430 multisig,
431 )
432 .await?;
433 }
434 }
435 },
436 Contract::StakeTableProxy => {
437 let token_addr = contracts
438 .address(Contract::EspTokenProxy)
439 .context("no ESP token proxy address")?;
440 let lc_addr = contracts
441 .address(Contract::LightClientProxy)
442 .context("no LightClient proxy address")?;
443 let escrow_period = self
444 .exit_escrow_period
445 .unwrap_or(U256::from(crate::DEFAULT_EXIT_ESCROW_PERIOD_SECONDS));
446 crate::deploy_stake_table_proxy(
447 provider,
448 contracts,
449 token_addr,
450 lc_addr,
451 escrow_period,
452 admin,
453 )
454 .await?;
455
456 },
458 Contract::StakeTableV2 => {
459 let use_multisig = self.use_multisig;
460 let multisig_pauser = self.multisig_pauser.unwrap_or(admin);
462 let l1_client = L1Client::new(vec![self.rpc_url.clone()])?;
463 tracing::info!(?use_multisig, "Upgrading to StakeTableV2 with ");
464 if use_multisig {
465 let calldata = upgrade_stake_table_v2_multisig_owner(
466 provider,
467 l1_client,
468 contracts,
469 StakeTableV2UpgradeParams {
470 multisig_address: self.multisig.context(
471 "Multisig address must be set when upgrading to --use-multisig \
472 flag is present",
473 )?,
474 pauser: multisig_pauser,
475 },
476 MultisigOwnerCheck::RequireContract,
477 )
478 .await?
479 .with_description("Upgrade StakeTable to V2".to_string());
480 output_safe_tx_builder(&calldata, self.output_path.as_deref(), self.chain_id)?;
481 } else {
482 let admin = match self.use_timelock_owner {
485 Some(true) => derive_timelock_address_from_contract_type(
486 OwnableContract::StakeTableProxy,
487 contracts,
488 )?,
489 Some(false) => admin, None => {
491 if let Some(multisig) = self.multisig {
492 multisig
493 } else {
494 admin }
496 },
497 };
498
499 tracing::info!("Upgrading StakeTableV2 with admin: {:?}", admin);
500 crate::upgrade_stake_table_v2(
501 provider,
502 l1_client,
503 contracts,
504 multisig_pauser,
505 admin,
506 )
507 .await?;
508
509 }
511 },
512 Contract::StakeTableV3 => {
513 let use_multisig = self.use_multisig;
514 let use_timelock_owner = self.use_timelock_owner.unwrap_or(false);
515 tracing::info!(
516 ?use_multisig,
517 ?use_timelock_owner,
518 "Upgrading to StakeTableV3"
519 );
520 if use_timelock_owner {
521 let salt_str = self.timelock_operation_salt.clone().context(
526 "timelock_operation_salt must be set for StakeTableV3 upgrade with \
527 --use-timelock-owner",
528 )?;
529 let salt_trimmed = salt_str.trim();
530 let hex_str = salt_trimmed.strip_prefix("0x").unwrap_or(salt_trimmed);
531 let salt = B256::from_hex(hex_str).context("Invalid salt hex format")?;
532 ensure!(
533 salt != B256::ZERO,
534 "timelock_operation_salt must be non-zero"
535 );
536 let delay = self.timelock_operation_delay.context(
537 "timelock_operation_delay must be set for StakeTableV3 upgrade with \
538 --use-timelock-owner",
539 )?;
540
541 let proposal = upgrade_stake_table_v3_timelock_proposal(
542 provider,
543 contracts,
544 StakeTableV3TimelockProposalParams { salt, delay },
545 )
546 .await?;
547
548 let network = resolve_network(self.chain_id, self.network.clone())?;
549 let slug = self
550 .proposal_slug
551 .clone()
552 .unwrap_or_else(|| "stake-table-v3".to_owned());
553 let proposals_root = self
555 .proposals_root
556 .clone()
557 .or_else(|| self.output_dir.clone())
558 .unwrap_or_else(|| PathBuf::from("contracts/deployments/proposals"));
559 let proposal_dir = write_stake_table_v3_proposal_dir(
560 WriteProposalParams {
561 proposals_root,
562 network,
563 slug,
564 chain_id: self.chain_id,
565 proxy: proposal.proxy_addr,
566 new_impl: proposal.v3_impl_addr,
567 timelock: proposal.timelock_addr,
568 salt,
569 delay,
570 schedule_calldata: proposal.schedule.data.clone(),
571 execute_calldata: proposal.execute.data.clone(),
572 safe_override: None,
573 },
574 provider,
575 )
576 .await?;
577 output_safe_tx_builder(
578 &proposal.schedule,
579 Some(&proposal_dir.join("schedule.json")),
580 self.chain_id,
581 )?;
582 output_safe_tx_builder(
583 &proposal.execute,
584 Some(&proposal_dir.join("execute.json")),
585 self.chain_id,
586 )?;
587 tracing::info!(
588 path = %proposal_dir.display(),
589 "wrote StakeTableV3 timelock proposal"
590 );
591 } else if use_multisig {
592 let calldata = upgrade_stake_table_v3_multisig_owner(
593 provider,
594 contracts,
595 StakeTableV3UpgradeParams {
596 multisig_address: self.multisig.context(
597 "Multisig address required for StakeTableV3 upgrade with \
598 --use-multisig",
599 )?,
600 },
601 MultisigOwnerCheck::RequireContract,
602 )
603 .await?
604 .with_description("Upgrade StakeTable to V3".to_string());
605 output_safe_tx_builder(&calldata, self.output_path.as_deref(), self.chain_id)?;
606 } else {
607 crate::upgrade_stake_table_v3(provider, contracts).await?;
608 }
609 },
610 Contract::OpsTimelock => {
611 let ops_timelock_delay = self
612 .ops_timelock_delay
613 .context("Ops Timelock delay must be set when deploying Ops Timelock")?;
614 let ops_timelock_proposers = self
615 .ops_timelock_proposers
616 .clone()
617 .context("Ops Timelock proposers must be set when deploying Ops Timelock")?;
618 let ops_timelock_executors = self
619 .ops_timelock_executors
620 .clone()
621 .context("Ops Timelock executors must be set when deploying Ops Timelock")?;
622 let ops_timelock_admin = self
623 .ops_timelock_admin
624 .context("Ops Timelock admin must be set when deploying Ops Timelock")?;
625 crate::deploy_ops_timelock(
626 provider,
627 contracts,
628 ops_timelock_delay,
629 ops_timelock_proposers,
630 ops_timelock_executors,
631 ops_timelock_admin,
632 )
633 .await?;
634 },
635 Contract::SafeExitTimelock => {
636 let safe_exit_timelock_delay = self.safe_exit_timelock_delay.context(
637 "SafeExitTimelock delay must be set when deploying SafeExitTimelock",
638 )?;
639 let safe_exit_timelock_proposers =
640 self.safe_exit_timelock_proposers.clone().context(
641 "SafeExitTimelock proposers must be set when deploying SafeExitTimelock",
642 )?;
643 let safe_exit_timelock_executors =
644 self.safe_exit_timelock_executors.clone().context(
645 "SafeExitTimelock executors must be set when deploying SafeExitTimelock",
646 )?;
647 let safe_exit_timelock_admin = self.safe_exit_timelock_admin.context(
648 "SafeExitTimelock admin must be set when deploying SafeExitTimelock",
649 )?;
650 crate::deploy_safe_exit_timelock(
651 provider,
652 contracts,
653 safe_exit_timelock_delay,
654 safe_exit_timelock_proposers,
655 safe_exit_timelock_executors,
656 safe_exit_timelock_admin,
657 )
658 .await?;
659 },
660 Contract::RewardClaimProxy => {
661 let token_addr = contracts
662 .address(Contract::EspTokenProxy)
663 .context("no ESP token proxy address")?;
664 let lc_addr = contracts
665 .address(Contract::LightClientProxy)
666 .context("no LightClient proxy address")?;
667 let deployer_addr = provider.default_signer_address();
670 let pauser = self.multisig_pauser.unwrap_or(deployer_addr);
671
672 let admin = match self.use_timelock_owner {
675 Some(true) => derive_timelock_address_from_contract_type(
676 OwnableContract::RewardClaimProxy,
677 contracts,
678 )?,
679 Some(false) => admin, None => {
681 if let Some(multisig) = self.multisig {
682 multisig
683 } else {
684 admin }
686 },
687 };
688
689 tracing::info!("Deploying RewardClaimProxy with admin: {:?}", admin);
690 crate::deploy_reward_claim_proxy(
691 provider, contracts, token_addr, lc_addr, admin, pauser,
692 )
693 .await?;
694
695 },
698 _ => {
699 panic!("Deploying {target} not supported.");
700 },
701 }
702 Ok(())
703 }
704
705 pub async fn deploy_to_stake_table_v1(&self, contracts: &mut Contracts) -> Result<()> {
707 self.deploy(contracts, Contract::OpsTimelock).await?;
709 self.deploy(contracts, Contract::SafeExitTimelock).await?;
710
711 self.deploy(contracts, Contract::FeeContractProxy).await?;
713 self.deploy(contracts, Contract::EspTokenProxy).await?;
714 self.deploy(contracts, Contract::LightClientProxy).await?;
715 self.deploy(contracts, Contract::LightClientV2).await?;
716 self.deploy(contracts, Contract::StakeTableProxy).await?;
717 Ok(())
718 }
719
720 pub async fn deploy_to_stake_table_v2(&self, contracts: &mut Contracts) -> Result<()> {
722 self.deploy_to_stake_table_v1(contracts).await?;
723 self.deploy(contracts, Contract::StakeTableV2).await?;
724 self.deploy(contracts, Contract::LightClientV3).await?;
725 self.deploy(contracts, Contract::RewardClaimProxy).await?;
726 self.deploy(contracts, Contract::EspTokenV2).await?;
727 Ok(())
728 }
729
730 pub async fn deploy_to_stake_table_v3(&self, contracts: &mut Contracts) -> Result<()> {
732 self.deploy_to_stake_table_v2(contracts).await?;
733 self.deploy(contracts, Contract::StakeTableV3).await?;
734 Ok(())
735 }
736
737 pub async fn propose_timelock_operation_for_contract(
747 &self,
748 contracts: &mut Contracts,
749 ) -> Result<()> {
750 let timelock_operation_type = self
751 .timelock_operation_type
752 .context("Timelock operation type not found")?;
753 let target_contract = self.target_contract.context("Timelock target not found")?;
754 let contract_type: Contract = target_contract.into();
755 let target_addr = contracts
756 .address(contract_type)
757 .context(format!("{:?} address not found", contract_type))?;
758
759 let (timelock_operation_data, operation_id) = if timelock_operation_type
760 == TimelockOperationType::Cancel
761 && self.timelock_operation_id.is_some()
762 {
763 let op_id_str = self
765 .timelock_operation_id
766 .as_ref()
767 .context("Operation ID not found")?;
768 let op_id = if let Some(stripped) = op_id_str.strip_prefix("0x") {
769 B256::from_hex(stripped).context("Invalid operation ID hex format")?
770 } else {
771 B256::from_hex(op_id_str).context("Invalid operation ID hex format")?
772 };
773
774 let minimal_payload = TimelockOperationPayload {
775 target: target_addr,
776 value: U256::ZERO,
777 data: Bytes::new(),
778 predecessor: B256::ZERO,
779 salt: B256::ZERO,
780 delay: U256::ZERO,
781 };
782 (minimal_payload, Some(op_id))
783 } else {
784 let value = self
786 .timelock_operation_value
787 .context("Timelock operation value not found")?;
788 let function_signature = self
789 .timelock_operation_function_signature
790 .as_ref()
791 .context("Timelock operation function signature not found")?;
792 let function_values = self
793 .timelock_operation_function_values
794 .clone()
795 .context("Timelock operation function values not found")?;
796 let salt = self
797 .timelock_operation_salt
798 .clone()
799 .context("Timelock operation salt not found")?;
800 let delay = self
801 .timelock_operation_delay
802 .context("Timelock operation delay not found")?;
803
804 let function_calldata =
805 encode_function_call(function_signature, function_values.clone())
806 .context("Failed to encode function data")?;
807
808 let salt_trimmed = salt.trim();
809 let hex_str = salt_trimmed.strip_prefix("0x").unwrap_or(salt_trimmed);
810 let salt_bytes = B256::from_hex(hex_str).context("Invalid salt hex format")?;
811 ensure!(
812 salt_bytes != B256::ZERO,
813 "timelock_operation_salt must be non-zero"
814 );
815
816 let operation = TimelockOperationPayload {
817 target: target_addr,
818 value,
819 data: function_calldata,
820 predecessor: B256::ZERO, salt: salt_bytes,
822 delay,
823 };
824 (operation, None)
825 };
826
827 let params = if let Some(multisig_proposer) = self.multisig {
828 TimelockOperationParams {
830 multisig_proposer: Some(multisig_proposer),
831 operation_id,
832 dry_run: false,
833 }
834 } else {
835 TimelockOperationParams {
837 multisig_proposer: None,
838 operation_id,
839 dry_run: false,
840 }
841 };
842
843 perform_timelock_operation(
844 &self.deployer,
845 contract_type,
846 timelock_operation_data,
847 timelock_operation_type,
848 params,
849 )
850 .await?;
851
852 Ok(())
853 }
854
855 pub async fn encode_transfer_ownership_to_timelock(
857 &self,
858 contracts: &mut Contracts,
859 ) -> Result<()> {
860 let _multisig = self.multisig.expect(
862 "Multisig address must be set when proposing ownership transfer. Use \
863 --multisig-address or ESPRESSO_ETH_MULTISIG_ADDRESS",
864 );
865 let ownable_contract = self.target_contract.ok_or_else(|| {
866 anyhow::anyhow!(
867 "Must provide target_contract when using \
868 --propose-transfer-ownership-to-timelock. Use --target-contract or \
869 ESPRESSO_TARGET_CONTRACT"
870 )
871 })?;
872
873 let timelock_address =
874 derive_timelock_address_from_contract_type(ownable_contract, contracts)?;
875
876 if !crate::is_contract(&self.deployer, timelock_address).await? {
877 anyhow::bail!(
878 "Timelock address is not a contract (expected timelock at {timelock_address:#x})"
879 );
880 }
881
882 let contract: Contract = ownable_contract.into();
883 tracing::info!(
884 "Encoding transfer of ownership from multisig to timelock for {:?} (timelock: {:?})",
885 contract,
886 timelock_address
887 );
888 let calldata = transfer_ownership_from_multisig_to_timelock(
889 contracts,
890 contract,
891 TransferOwnershipParams {
892 new_owner: timelock_address,
893 },
894 )?
895 .with_description(format!(
896 "Transfer {} ownership to timelock {timelock_address}",
897 contract
898 ));
899 output_safe_tx_builder(&calldata, self.output_path.as_deref(), self.chain_id)?;
900 tracing::info!("Successfully encoded ownership transfer for {}", contract);
901 Ok(())
902 }
903
904 pub async fn transfer_ownership_from_eoa(&self, contracts: &mut Contracts) -> Result<()> {
906 let transfer_ownership_from_eoa = self
907 .transfer_ownership_from_eoa
908 .ok_or_else(|| anyhow::anyhow!("transfer_ownership_from_eoa flag not set"))?;
909
910 if !transfer_ownership_from_eoa {
911 return Ok(());
912 }
913
914 let ownable_contract = self.target_contract.ok_or_else(|| {
915 anyhow::anyhow!("Must provide target_contract when using transfer_ownership_from_eoa")
916 })?;
917 let new_owner = self.transfer_ownership_new_owner.ok_or_else(|| {
918 anyhow::anyhow!(
919 "Must provide transfer_ownership_new_owner when using transfer_ownership_from_eoa"
920 )
921 })?;
922
923 let contract_type: Contract = ownable_contract.into();
924 let contract_address = contracts.address(contract_type).ok_or_else(|| {
925 anyhow::anyhow!(
926 "Contract {:?} not found in deployed contracts",
927 contract_type
928 )
929 })?;
930
931 let receipt = if contract_type == Contract::RewardClaimProxy {
934 tracing::info!(
935 "Granting DEFAULT_ADMIN_ROLE for {:?} to {} (RewardClaim uses AccessControl, not \
936 Ownable)",
937 contract_type,
938 new_owner
939 );
940 crate::grant_admin_role(&self.deployer, contract_type, contract_address, new_owner)
941 .await?
942 } else {
943 tracing::info!(
944 "Transferring ownership of {:?} from EOA to {}",
945 contract_type,
946 new_owner
947 );
948 crate::transfer_ownership(&self.deployer, contract_type, contract_address, new_owner)
949 .await?
950 };
951
952 tracing::info!(
953 "Successfully transferred admin control of {:?} to {}. Transaction: {}",
954 contract_type,
955 new_owner,
956 receipt.transaction_hash
957 );
958
959 Ok(())
960 }
961
962 pub async fn encode_multisig_transaction(&self) -> Result<()> {
964 let target = self
965 .multisig_transaction_target
966 .context("Multisig transaction target address not found")?;
967 let function_signature = self
968 .multisig_transaction_function_signature
969 .as_ref()
970 .context("Multisig transaction function signature not found")?;
971 let function_args = self
972 .multisig_transaction_function_args
973 .clone()
974 .unwrap_or_default();
975 let value: U256 = self
976 .multisig_transaction_value
977 .as_deref()
978 .unwrap_or("0")
979 .parse()
980 .context("Failed to parse multisig transaction value as U256")?;
981
982 let calldata = encode_generic_calldata(target, function_signature, function_args, value)?
983 .with_description(format!("Call {} on {target}", function_signature));
984 output_safe_tx_builder(&calldata, self.output_path.as_deref(), self.chain_id)?;
985
986 Ok(())
987 }
988}