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};
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 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 },
27 timelock::{
28 TimelockOperationParams, TimelockOperationPayload, TimelockOperationType,
29 derive_timelock_address_from_contract_type, perform_timelock_operation,
30 },
31 },
32};
33
34#[derive(Builder, Clone)]
69#[builder(setter(strip_option))]
70pub struct DeployerArgs<P: Provider + WalletProvider> {
71 deployer: P,
72 rpc_url: Url,
73 #[builder(default)]
74 token_recipient: Option<Address>,
75 #[builder(default)]
76 mock_light_client: bool,
77 #[builder(default)]
78 use_multisig: bool,
79 #[builder(default)]
80 genesis_lc_state: Option<LightClientStateSol>,
81 #[builder(default)]
82 genesis_st_state: Option<StakeTableStateSol>,
83 #[builder(default)]
84 permissioned_prover: Option<Address>,
85 #[builder(default)]
86 blocks_per_epoch: Option<u64>,
87 #[builder(default)]
88 epoch_start_block: Option<u64>,
89 #[builder(default)]
90 exit_escrow_period: Option<U256>,
91 #[builder(default)]
92 multisig: Option<Address>,
93 #[builder(default)]
94 multisig_pauser: Option<Address>,
95 #[builder(default)]
96 initial_token_supply: Option<U256>,
97 #[builder(default)]
98 token_name: Option<String>,
99 #[builder(default)]
100 token_symbol: Option<String>,
101 #[builder(default)]
102 ops_timelock_admin: Option<Address>,
103 #[builder(default)]
104 ops_timelock_delay: Option<U256>,
105 #[builder(default)]
106 ops_timelock_executors: Option<Vec<Address>>,
107 #[builder(default)]
108 ops_timelock_proposers: Option<Vec<Address>>,
109 #[builder(default)]
110 safe_exit_timelock_admin: Option<Address>,
111 #[builder(default)]
112 safe_exit_timelock_delay: Option<U256>,
113 #[builder(default)]
114 safe_exit_timelock_executors: Option<Vec<Address>>,
115 #[builder(default)]
116 safe_exit_timelock_proposers: Option<Vec<Address>>,
117 #[builder(default)]
118 timelock_operation_type: Option<TimelockOperationType>,
119 #[builder(default)]
120 target_contract: Option<OwnableContract>,
121 #[builder(default)]
122 timelock_operation_value: Option<U256>,
123 #[builder(default)]
124 timelock_operation_delay: Option<U256>,
125 #[builder(default)]
126 timelock_operation_function_signature: Option<String>,
127 #[builder(default)]
128 timelock_operation_function_values: Option<Vec<String>>,
129 #[builder(default)]
130 timelock_operation_salt: Option<String>,
131 #[builder(default)]
132 use_timelock_owner: Option<bool>,
133 #[builder(default)]
134 transfer_ownership_from_eoa: Option<bool>,
135 #[builder(default)]
136 transfer_ownership_new_owner: Option<Address>,
137 #[builder(default)]
138 timelock_operation_id: Option<String>,
139 #[builder(default)]
140 multisig_transaction_target: Option<Address>,
141 #[builder(default)]
142 multisig_transaction_function_signature: Option<String>,
143 #[builder(default)]
144 multisig_transaction_function_args: Option<Vec<String>>,
145 #[builder(default)]
146 multisig_transaction_value: Option<String>,
147 #[builder(default)]
148 output_path: Option<PathBuf>,
149 #[builder(default)]
150 chain_id: u64,
151}
152
153impl<P: Provider + WalletProvider> DeployerArgs<P> {
154 pub async fn deploy(&self, contracts: &mut Contracts, target: Contract) -> Result<()> {
156 let provider = &self.deployer;
157 let admin = provider.default_signer_address();
158 match target {
159 Contract::FeeContractProxy => {
160 if contracts.address(Contract::FeeContractProxy).is_some() {
161 let use_multisig = self.use_multisig;
163
164 tracing::info!(?use_multisig, "Upgrading FeeContract to V1.0.1");
165 if use_multisig {
166 let calldata = upgrade_fee_contract_multisig_owner(
167 provider,
168 contracts,
169 MultisigOwnerCheck::RequireContract,
170 )
171 .await?
172 .with_description("Upgrade FeeContract to V1.0.1".to_string());
173 output_safe_tx_builder(
174 &calldata,
175 self.output_path.as_deref(),
176 self.chain_id,
177 )?;
178 } else {
179 crate::upgrade_fee_v1(provider, contracts).await?;
180 }
181 } else {
182 let addr = crate::deploy_fee_contract_proxy(provider, contracts, admin).await?;
184
185 if let Some(use_timelock_owner) = self.use_timelock_owner {
186 tracing::info!(
191 "Transferring ownership to OpsTimelock: {:?}",
192 use_timelock_owner
193 );
194 if use_timelock_owner {
196 let timelock_addr = derive_timelock_address_from_contract_type(
197 OwnableContract::FeeContractProxy,
198 contracts,
199 )?;
200 crate::transfer_ownership(
201 provider,
202 Contract::FeeContractProxy,
203 addr,
204 timelock_addr,
205 )
206 .await?;
207 }
208 } else if let Some(multisig) = self.multisig {
209 tracing::info!("Transferring ownership to multisig: {:?}", multisig);
210 crate::transfer_ownership(
211 provider,
212 Contract::FeeContractProxy,
213 addr,
214 multisig,
215 )
216 .await?;
217 }
218 }
219 },
220 Contract::EspTokenProxy => {
221 let token_recipient = self.token_recipient.unwrap_or(admin);
222 let token_name = self
223 .token_name
224 .clone()
225 .context("Token name must be set when deploying esp token")?;
226 let token_symbol = self
227 .token_symbol
228 .clone()
229 .context("Token symbol must be set when deploying esp token")?;
230 let initial_supply = self
231 .initial_token_supply
232 .context("Initial token supply must be set when deploying esp token")?;
233 crate::deploy_token_proxy(
234 provider,
235 contracts,
236 admin,
237 token_recipient,
238 initial_supply,
239 &token_name,
240 &token_symbol,
241 )
242 .await?;
243
244 },
246 Contract::EspTokenV2 => {
247 let use_multisig = self.use_multisig;
248
249 if use_multisig {
250 let calldata = upgrade_esp_token_v2_multisig_owner(
251 provider,
252 contracts,
253 MultisigOwnerCheck::RequireContract,
254 )
255 .await?
256 .with_description("Upgrade EspToken to V2".to_string());
257 output_safe_tx_builder(&calldata, self.output_path.as_deref(), self.chain_id)?;
258 } else {
259 crate::upgrade_esp_token_v2(provider, contracts).await?;
260 let addr = contracts
261 .address(Contract::EspTokenProxy)
262 .expect("fail to get EspTokenProxy address");
263
264 if let Some(use_timelock_owner) = self.use_timelock_owner {
265 if use_timelock_owner {
267 tracing::info!("Transferring ownership to SafeExitTimelock");
272 let timelock_addr = derive_timelock_address_from_contract_type(
273 OwnableContract::EspTokenProxy,
274 contracts,
275 )?;
276 crate::transfer_ownership(
277 provider,
278 Contract::EspTokenProxy,
279 addr,
280 timelock_addr,
281 )
282 .await?;
283 }
284 } else if let Some(multisig) = self.multisig {
285 let token_proxy = contracts
286 .address(Contract::EspTokenProxy)
287 .expect("fail to get EspTokenProxy address");
288 crate::transfer_ownership(
289 provider,
290 Contract::EspTokenProxy,
291 token_proxy,
292 multisig,
293 )
294 .await?;
295 }
296 }
297 },
298 Contract::LightClientProxy => {
299 assert!(
300 self.genesis_lc_state.is_some(),
301 "forget to specify genesis_lc_state()"
302 );
303 assert!(
304 self.genesis_st_state.is_some(),
305 "forget to specify genesis_st_state()"
306 );
307 crate::deploy_light_client_proxy(
308 provider,
309 contracts,
310 self.mock_light_client,
311 self.genesis_lc_state.clone().unwrap(),
312 self.genesis_st_state.clone().unwrap(),
313 admin,
314 self.permissioned_prover,
315 )
316 .await?;
317 },
319 Contract::LightClientV2 => {
320 assert!(
321 self.blocks_per_epoch.is_some(),
322 "forgot to specify blocks_per_epoch()"
323 );
324 assert!(
325 self.epoch_start_block.is_some(),
326 "forgot to specify epoch_start_block()"
327 );
328
329 let use_mock = self.mock_light_client;
330 let use_multisig = self.use_multisig;
331 let mut blocks_per_epoch = self.blocks_per_epoch.unwrap();
332 let epoch_start_block = self.epoch_start_block.unwrap();
333
334 if use_mock && blocks_per_epoch == 0 {
339 blocks_per_epoch = u64::MAX;
340 }
341 tracing::info!(%blocks_per_epoch, ?use_multisig, "Upgrading LightClientV2 with ");
342 if use_multisig {
343 let calldata = upgrade_light_client_v2_multisig_owner(
344 provider,
345 contracts,
346 LightClientV2UpgradeParams {
347 blocks_per_epoch,
348 epoch_start_block,
349 },
350 use_mock,
351 MultisigOwnerCheck::RequireContract,
352 )
353 .await?
354 .with_description("Upgrade LightClient to V2".to_string());
355 output_safe_tx_builder(&calldata, self.output_path.as_deref(), self.chain_id)?;
356 } else {
357 crate::upgrade_light_client_v2(
358 provider,
359 contracts,
360 use_mock,
361 blocks_per_epoch,
362 epoch_start_block,
363 )
364 .await?;
365 }
367 },
368 Contract::LightClientV3 => {
369 let use_mock = self.mock_light_client;
370 let use_multisig = self.use_multisig;
371
372 tracing::info!(?use_multisig, "Upgrading LightClientV3 with ");
373 if use_multisig {
374 let calldata = upgrade_light_client_v3_multisig_owner(
375 provider,
376 contracts,
377 use_mock,
378 MultisigOwnerCheck::RequireContract,
379 )
380 .await?
381 .with_description("Upgrade LightClient to V3".to_string());
382 output_safe_tx_builder(&calldata, self.output_path.as_deref(), self.chain_id)?;
383 } else {
384 crate::upgrade_light_client_v3(provider, contracts, use_mock).await?;
385
386 let addr = contracts
388 .address(Contract::LightClientProxy)
389 .expect("fail to get LightClientProxy address");
390
391 if let Some(use_timelock_owner) = self.use_timelock_owner {
392 tracing::info!("Transferring ownership to OpsTimelock");
397 if use_timelock_owner {
399 let timelock_addr = derive_timelock_address_from_contract_type(
400 OwnableContract::LightClientProxy,
401 contracts,
402 )?;
403 crate::transfer_ownership(
404 provider,
405 Contract::LightClientProxy,
406 addr,
407 timelock_addr,
408 )
409 .await?;
410 }
411 } else if let Some(multisig) = self.multisig {
412 crate::transfer_ownership(
413 provider,
414 Contract::LightClientProxy,
415 addr,
416 multisig,
417 )
418 .await?;
419 }
420 }
421 },
422 Contract::StakeTableProxy => {
423 let token_addr = contracts
424 .address(Contract::EspTokenProxy)
425 .context("no ESP token proxy address")?;
426 let lc_addr = contracts
427 .address(Contract::LightClientProxy)
428 .context("no LightClient proxy address")?;
429 let escrow_period = self
430 .exit_escrow_period
431 .unwrap_or(U256::from(crate::DEFAULT_EXIT_ESCROW_PERIOD_SECONDS));
432 crate::deploy_stake_table_proxy(
433 provider,
434 contracts,
435 token_addr,
436 lc_addr,
437 escrow_period,
438 admin,
439 )
440 .await?;
441
442 },
444 Contract::StakeTableV2 => {
445 let use_multisig = self.use_multisig;
446 let multisig_pauser = self.multisig_pauser.unwrap_or(admin);
448 let l1_client = L1Client::new(vec![self.rpc_url.clone()])?;
449 tracing::info!(?use_multisig, "Upgrading to StakeTableV2 with ");
450 if use_multisig {
451 let calldata = upgrade_stake_table_v2_multisig_owner(
452 provider,
453 l1_client,
454 contracts,
455 StakeTableV2UpgradeParams {
456 multisig_address: self.multisig.context(
457 "Multisig address must be set when upgrading to --use-multisig \
458 flag is present",
459 )?,
460 pauser: multisig_pauser,
461 },
462 MultisigOwnerCheck::RequireContract,
463 )
464 .await?
465 .with_description("Upgrade StakeTable to V2".to_string());
466 output_safe_tx_builder(&calldata, self.output_path.as_deref(), self.chain_id)?;
467 } else {
468 let admin = match self.use_timelock_owner {
471 Some(true) => derive_timelock_address_from_contract_type(
472 OwnableContract::StakeTableProxy,
473 contracts,
474 )?,
475 Some(false) => admin, None => {
477 if let Some(multisig) = self.multisig {
478 multisig
479 } else {
480 admin }
482 },
483 };
484
485 tracing::info!("Upgrading StakeTableV2 with admin: {:?}", admin);
486 crate::upgrade_stake_table_v2(
487 provider,
488 l1_client,
489 contracts,
490 multisig_pauser,
491 admin,
492 )
493 .await?;
494
495 }
497 },
498 Contract::OpsTimelock => {
499 let ops_timelock_delay = self
500 .ops_timelock_delay
501 .context("Ops Timelock delay must be set when deploying Ops Timelock")?;
502 let ops_timelock_proposers = self
503 .ops_timelock_proposers
504 .clone()
505 .context("Ops Timelock proposers must be set when deploying Ops Timelock")?;
506 let ops_timelock_executors = self
507 .ops_timelock_executors
508 .clone()
509 .context("Ops Timelock executors must be set when deploying Ops Timelock")?;
510 let ops_timelock_admin = self
511 .ops_timelock_admin
512 .context("Ops Timelock admin must be set when deploying Ops Timelock")?;
513 crate::deploy_ops_timelock(
514 provider,
515 contracts,
516 ops_timelock_delay,
517 ops_timelock_proposers,
518 ops_timelock_executors,
519 ops_timelock_admin,
520 )
521 .await?;
522 },
523 Contract::SafeExitTimelock => {
524 let safe_exit_timelock_delay = self.safe_exit_timelock_delay.context(
525 "SafeExitTimelock delay must be set when deploying SafeExitTimelock",
526 )?;
527 let safe_exit_timelock_proposers =
528 self.safe_exit_timelock_proposers.clone().context(
529 "SafeExitTimelock proposers must be set when deploying SafeExitTimelock",
530 )?;
531 let safe_exit_timelock_executors =
532 self.safe_exit_timelock_executors.clone().context(
533 "SafeExitTimelock executors must be set when deploying SafeExitTimelock",
534 )?;
535 let safe_exit_timelock_admin = self.safe_exit_timelock_admin.context(
536 "SafeExitTimelock admin must be set when deploying SafeExitTimelock",
537 )?;
538 crate::deploy_safe_exit_timelock(
539 provider,
540 contracts,
541 safe_exit_timelock_delay,
542 safe_exit_timelock_proposers,
543 safe_exit_timelock_executors,
544 safe_exit_timelock_admin,
545 )
546 .await?;
547 },
548 Contract::RewardClaimProxy => {
549 let token_addr = contracts
550 .address(Contract::EspTokenProxy)
551 .context("no ESP token proxy address")?;
552 let lc_addr = contracts
553 .address(Contract::LightClientProxy)
554 .context("no LightClient proxy address")?;
555 let deployer_addr = provider.default_signer_address();
558 let pauser = self.multisig_pauser.unwrap_or(deployer_addr);
559
560 let admin = match self.use_timelock_owner {
563 Some(true) => derive_timelock_address_from_contract_type(
564 OwnableContract::RewardClaimProxy,
565 contracts,
566 )?,
567 Some(false) => admin, None => {
569 if let Some(multisig) = self.multisig {
570 multisig
571 } else {
572 admin }
574 },
575 };
576
577 tracing::info!("Deploying RewardClaimProxy with admin: {:?}", admin);
578 crate::deploy_reward_claim_proxy(
579 provider, contracts, token_addr, lc_addr, admin, pauser,
580 )
581 .await?;
582
583 },
586 _ => {
587 panic!("Deploying {target} not supported.");
588 },
589 }
590 Ok(())
591 }
592
593 pub async fn deploy_to_stake_table_v1(&self, contracts: &mut Contracts) -> Result<()> {
595 self.deploy(contracts, Contract::OpsTimelock).await?;
597 self.deploy(contracts, Contract::SafeExitTimelock).await?;
598
599 self.deploy(contracts, Contract::FeeContractProxy).await?;
601 self.deploy(contracts, Contract::EspTokenProxy).await?;
602 self.deploy(contracts, Contract::LightClientProxy).await?;
603 self.deploy(contracts, Contract::LightClientV2).await?;
604 self.deploy(contracts, Contract::StakeTableProxy).await?;
605 Ok(())
606 }
607
608 pub async fn deploy_all(&self, contracts: &mut Contracts) -> Result<()> {
610 self.deploy_to_stake_table_v1(contracts).await?;
611 self.deploy(contracts, Contract::StakeTableV2).await?;
612 self.deploy(contracts, Contract::LightClientV3).await?;
613 self.deploy(contracts, Contract::RewardClaimProxy).await?;
614 self.deploy(contracts, Contract::EspTokenV2).await?;
615 Ok(())
616 }
617
618 pub async fn propose_timelock_operation_for_contract(
628 &self,
629 contracts: &mut Contracts,
630 ) -> Result<()> {
631 let timelock_operation_type = self
632 .timelock_operation_type
633 .context("Timelock operation type not found")?;
634 let target_contract = self.target_contract.context("Timelock target not found")?;
635 let contract_type: Contract = target_contract.into();
636 let target_addr = contracts
637 .address(contract_type)
638 .context(format!("{:?} address not found", contract_type))?;
639
640 let (timelock_operation_data, operation_id) = if timelock_operation_type
641 == TimelockOperationType::Cancel
642 && self.timelock_operation_id.is_some()
643 {
644 let op_id_str = self
646 .timelock_operation_id
647 .as_ref()
648 .context("Operation ID not found")?;
649 let op_id = if let Some(stripped) = op_id_str.strip_prefix("0x") {
650 B256::from_hex(stripped).context("Invalid operation ID hex format")?
651 } else {
652 B256::from_hex(op_id_str).context("Invalid operation ID hex format")?
653 };
654
655 let minimal_payload = TimelockOperationPayload {
656 target: target_addr,
657 value: U256::ZERO,
658 data: Bytes::new(),
659 predecessor: B256::ZERO,
660 salt: B256::ZERO,
661 delay: U256::ZERO,
662 };
663 (minimal_payload, Some(op_id))
664 } else {
665 let value = self
667 .timelock_operation_value
668 .context("Timelock operation value not found")?;
669 let function_signature = self
670 .timelock_operation_function_signature
671 .as_ref()
672 .context("Timelock operation function signature not found")?;
673 let function_values = self
674 .timelock_operation_function_values
675 .clone()
676 .context("Timelock operation function values not found")?;
677 let salt = self
678 .timelock_operation_salt
679 .clone()
680 .context("Timelock operation salt not found")?;
681 let delay = self
682 .timelock_operation_delay
683 .context("Timelock operation delay not found")?;
684
685 let function_calldata =
686 encode_function_call(function_signature, function_values.clone())
687 .context("Failed to encode function data")?;
688
689 let salt_trimmed = salt.trim();
691 let salt_bytes = if salt_trimmed.is_empty() || salt_trimmed == "0x" {
692 B256::ZERO
693 } else {
694 let hex_str = salt_trimmed.strip_prefix("0x").unwrap_or(salt_trimmed);
695 B256::from_hex(hex_str).context("Invalid salt hex format")?
696 };
697
698 let operation = TimelockOperationPayload {
699 target: target_addr,
700 value,
701 data: function_calldata,
702 predecessor: B256::ZERO, salt: salt_bytes,
704 delay,
705 };
706 (operation, None)
707 };
708
709 let params = if let Some(multisig_proposer) = self.multisig {
710 TimelockOperationParams {
712 multisig_proposer: Some(multisig_proposer),
713 operation_id,
714 dry_run: false,
715 }
716 } else {
717 TimelockOperationParams {
719 multisig_proposer: None,
720 operation_id,
721 dry_run: false,
722 }
723 };
724
725 perform_timelock_operation(
726 &self.deployer,
727 contract_type,
728 timelock_operation_data,
729 timelock_operation_type,
730 params,
731 )
732 .await?;
733
734 Ok(())
735 }
736
737 pub async fn encode_transfer_ownership_to_timelock(
739 &self,
740 contracts: &mut Contracts,
741 ) -> Result<()> {
742 let _multisig = self.multisig.expect(
744 "Multisig address must be set when proposing ownership transfer. Use \
745 --multisig-address or ESPRESSO_SEQUENCER_ETH_MULTISIG_ADDRESS",
746 );
747 let ownable_contract = self.target_contract.ok_or_else(|| {
748 anyhow::anyhow!(
749 "Must provide target_contract when using \
750 --propose-transfer-ownership-to-timelock. Use --target-contract or \
751 ESPRESSO_TARGET_CONTRACT"
752 )
753 })?;
754
755 let timelock_address =
756 derive_timelock_address_from_contract_type(ownable_contract, contracts)?;
757
758 if !crate::is_contract(&self.deployer, timelock_address).await? {
759 anyhow::bail!(
760 "Timelock address is not a contract (expected timelock at {timelock_address:#x})"
761 );
762 }
763
764 let contract: Contract = ownable_contract.into();
765 tracing::info!(
766 "Encoding transfer of ownership from multisig to timelock for {:?} (timelock: {:?})",
767 contract,
768 timelock_address
769 );
770 let calldata = transfer_ownership_from_multisig_to_timelock(
771 contracts,
772 contract,
773 TransferOwnershipParams {
774 new_owner: timelock_address,
775 },
776 )?
777 .with_description(format!(
778 "Transfer {} ownership to timelock {timelock_address}",
779 contract
780 ));
781 output_safe_tx_builder(&calldata, self.output_path.as_deref(), self.chain_id)?;
782 tracing::info!("Successfully encoded ownership transfer for {}", contract);
783 Ok(())
784 }
785
786 pub async fn transfer_ownership_from_eoa(&self, contracts: &mut Contracts) -> Result<()> {
788 let transfer_ownership_from_eoa = self
789 .transfer_ownership_from_eoa
790 .ok_or_else(|| anyhow::anyhow!("transfer_ownership_from_eoa flag not set"))?;
791
792 if !transfer_ownership_from_eoa {
793 return Ok(());
794 }
795
796 let ownable_contract = self.target_contract.ok_or_else(|| {
797 anyhow::anyhow!("Must provide target_contract when using transfer_ownership_from_eoa")
798 })?;
799 let new_owner = self.transfer_ownership_new_owner.ok_or_else(|| {
800 anyhow::anyhow!(
801 "Must provide transfer_ownership_new_owner when using transfer_ownership_from_eoa"
802 )
803 })?;
804
805 let contract_type: Contract = ownable_contract.into();
806 let contract_address = contracts.address(contract_type).ok_or_else(|| {
807 anyhow::anyhow!(
808 "Contract {:?} not found in deployed contracts",
809 contract_type
810 )
811 })?;
812
813 let receipt = if contract_type == Contract::RewardClaimProxy {
816 tracing::info!(
817 "Granting DEFAULT_ADMIN_ROLE for {:?} to {} (RewardClaim uses AccessControl, not \
818 Ownable)",
819 contract_type,
820 new_owner
821 );
822 crate::grant_admin_role(&self.deployer, contract_type, contract_address, new_owner)
823 .await?
824 } else {
825 tracing::info!(
826 "Transferring ownership of {:?} from EOA to {}",
827 contract_type,
828 new_owner
829 );
830 crate::transfer_ownership(&self.deployer, contract_type, contract_address, new_owner)
831 .await?
832 };
833
834 tracing::info!(
835 "Successfully transferred admin control of {:?} to {}. Transaction: {}",
836 contract_type,
837 new_owner,
838 receipt.transaction_hash
839 );
840
841 Ok(())
842 }
843
844 pub async fn encode_multisig_transaction(&self) -> Result<()> {
846 let target = self
847 .multisig_transaction_target
848 .context("Multisig transaction target address not found")?;
849 let function_signature = self
850 .multisig_transaction_function_signature
851 .as_ref()
852 .context("Multisig transaction function signature not found")?;
853 let function_args = self
854 .multisig_transaction_function_args
855 .clone()
856 .unwrap_or_default();
857 let value: U256 = self
858 .multisig_transaction_value
859 .as_deref()
860 .unwrap_or("0")
861 .parse()
862 .context("Failed to parse multisig transaction value as U256")?;
863
864 let calldata = encode_generic_calldata(target, function_signature, function_args, value)?
865 .with_description(format!("Call {} on {target}", function_signature));
866 output_safe_tx_builder(&calldata, self.output_path.as_deref(), self.chain_id)?;
867
868 Ok(())
869 }
870}