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