espresso_contract_deployer/
lib.rs

1use std::{collections::HashMap, io::Write, time::Duration};
2
3use alloy::{
4    contract::RawCallBuilder,
5    dyn_abi::{DynSolType, DynSolValue, JsonAbiExt},
6    hex::{FromHex, ToHexExt},
7    json_abi::Function,
8    network::{Ethereum, EthereumWallet, TransactionBuilder},
9    primitives::{Address, B256, Bytes, U256},
10    providers::{
11        Provider, ProviderBuilder, RootProvider,
12        fillers::{FillProvider, JoinFill, WalletFiller},
13        utils::JoinedRecommendedFillers,
14    },
15    rpc::{client::RpcClient, types::TransactionReceipt},
16    signers::{
17        ledger::LedgerSigner,
18        local::{MnemonicBuilder, PrivateKeySigner, coins_bip39::English},
19    },
20    transports::http::reqwest::Url,
21};
22use anyhow::{Context, Result, anyhow};
23use clap::{Parser, ValueEnum, builder::OsStr};
24use derive_more::Display;
25use espresso_types::{v0_1::L1Client, v0_3::Fetcher};
26use hotshot_contract_adapter::sol_types::*;
27
28pub mod builder;
29pub mod impersonate_filler;
30pub mod network_config;
31pub mod output;
32pub mod proposals;
33pub mod provider;
34
35/// Type alias that connects to providers with recommended fillers and wallet
36/// use `<HttpProviderWithWallet as WalletProvider>::wallet()` to access internal wallet
37/// use `<HttpProviderWithWallet as WalletProvider>::default_signer_address(&provider)` to get wallet address
38pub type HttpProviderWithWallet = FillProvider<
39    JoinFill<JoinedRecommendedFillers, WalletFiller<EthereumWallet>>,
40    RootProvider,
41    Ethereum,
42>;
43
44/// a handy thin wrapper around wallet builder and provider builder that directly
45/// returns an instantiated `Provider` with default fillers with wallet, ready to send tx
46pub fn build_provider(
47    mnemonic: impl AsRef<str>,
48    account_index: u32,
49    url: Url,
50    poll_interval: Option<Duration>,
51) -> HttpProviderWithWallet {
52    let signer = build_signer(mnemonic.as_ref(), account_index);
53    let wallet = EthereumWallet::from(signer);
54
55    // alloy sets the polling interval automatically. It tries to guess if an RPC is local, but this
56    // guess is wrong when the RPC is running inside docker. This results to 7 second polling
57    // intervals on a chain with 1s block time. Therefore, allow overriding the polling interval
58    // with a custom value.
59    if let Some(interval) = poll_interval {
60        tracing::info!("Using custom L1 poll interval: {interval:?}");
61        let client = RpcClient::new_http(url.clone()).with_poll_interval(interval);
62        ProviderBuilder::new().wallet(wallet).connect_client(client)
63    } else {
64        tracing::info!("Using default L1 poll interval");
65        ProviderBuilder::new().wallet(wallet).connect_http(url)
66    }
67}
68
69// TODO: tech-debt: provider creation logic should be refactored to handle mnemonic and
70// ledger signers and consolidated with similar code in staking-cli
71pub fn build_provider_ledger(
72    signer: LedgerSigner,
73    url: Url,
74    poll_interval: Option<Duration>,
75) -> HttpProviderWithWallet {
76    let wallet = EthereumWallet::from(signer);
77
78    // alloy sets the polling interval automatically. It tries to guess if an RPC is local, but this
79    // guess is wrong when the RPC is running inside docker. This results to 7 second polling
80    // intervals on a chain with 1s block time. Therefore, allow overriding the polling interval
81    // with a custom value.
82    if let Some(interval) = poll_interval {
83        tracing::info!("Using custom L1 poll interval: {interval:?}");
84        let client = RpcClient::new_http(url.clone()).with_poll_interval(interval);
85        ProviderBuilder::new().wallet(wallet).connect_client(client)
86    } else {
87        tracing::info!("Using default L1 poll interval");
88        ProviderBuilder::new().wallet(wallet).connect_http(url)
89    }
90}
91
92pub fn build_signer(mnemonic: impl AsRef<str>, account_index: u32) -> PrivateKeySigner {
93    MnemonicBuilder::<English>::default()
94        .phrase(mnemonic.as_ref())
95        .index(account_index)
96        .expect("wrong mnemonic or index")
97        .build()
98        .expect("fail to build signer")
99}
100
101/// similar to [`build_provider()`] but using a random wallet
102pub fn build_random_provider(url: Url) -> HttpProviderWithWallet {
103    let signer = MnemonicBuilder::<English>::default()
104        .build_random()
105        .expect("fail to build signer");
106    let wallet = EthereumWallet::from(signer);
107    ProviderBuilder::new().wallet(wallet).connect_http(url)
108}
109
110// We pass this during `forge bind --libraries` as a placeholder for the actual deployed library address
111const LIBRARY_PLACEHOLDER_ADDRESS: &str = "ffffffffffffffffffffffffffffffffffffffff";
112/// `stateHistoryRetentionPeriod` in LightClient.sol as the maximum retention period in seconds
113pub const MAX_HISTORY_RETENTION_SECONDS: u32 = 864000;
114/// Default exit escrow period for stake table (2 days in seconds)
115pub const DEFAULT_EXIT_ESCROW_PERIOD_SECONDS: u64 = 172800;
116/// Maximum number of retries for state checks after transactions
117pub const MAX_RETRY_ATTEMPTS: u32 = 5;
118/// Initial delay in milliseconds for retry exponential backoff
119pub const RETRY_INITIAL_DELAY_MS: u64 = 500;
120
121/// Set of predeployed contracts.
122#[derive(Clone, Debug, Parser)]
123pub struct DeployedContracts {
124    /// Use an already-deployed PlonkVerifier.sol instead of deploying a new one.
125    #[clap(long, env = Contract::PlonkVerifier)]
126    plonk_verifier: Option<Address>,
127
128    /// OpsTimelock.sol
129    #[clap(long, env = Contract::OpsTimelock)]
130    ops_timelock: Option<Address>,
131
132    /// SafeExitTimelock.sol
133    #[clap(long, env = Contract::SafeExitTimelock)]
134    safe_exit_timelock: Option<Address>,
135
136    /// PlonkVerifierV2.sol
137    #[clap(long, env = Contract::PlonkVerifierV2)]
138    plonk_verifier_v2: Option<Address>,
139
140    /// PlonkVerifierV3.sol
141    #[clap(long, env = Contract::PlonkVerifierV3)]
142    plonk_verifier_v3: Option<Address>,
143
144    /// Use an already-deployed LightClient.sol instead of deploying a new one.
145    #[clap(long, env = Contract::LightClient)]
146    light_client: Option<Address>,
147
148    /// LightClientV2.sol
149    #[clap(long, env = Contract::LightClientV2)]
150    light_client_v2: Option<Address>,
151
152    /// LightClientV3.sol
153    #[clap(long, env = Contract::LightClientV3)]
154    light_client_v3: Option<Address>,
155
156    /// Use an already-deployed LightClient.sol proxy instead of deploying a new one.
157    #[clap(long, env = Contract::LightClientProxy)]
158    light_client_proxy: Option<Address>,
159
160    /// Use an already-deployed FeeContract.sol instead of deploying a new one.
161    #[clap(long, env = Contract::FeeContract)]
162    fee_contract: Option<Address>,
163
164    /// Use an already-deployed FeeContract.sol proxy instead of deploying a new one.
165    #[clap(long, env = Contract::FeeContractProxy)]
166    fee_contract_proxy: Option<Address>,
167
168    /// Use an already-deployed EspToken.sol instead of deploying a new one.
169    #[clap(long, env = Contract::EspToken)]
170    esp_token: Option<Address>,
171
172    /// Use an already-deployed EspTokenV2.sol instead of deploying a new one.
173    #[clap(long, env = Contract::EspTokenV2)]
174    esp_token_v2: Option<Address>,
175
176    /// Use an already-deployed EspToken.sol proxy instead of deploying a new one.
177    #[clap(long, env = Contract::EspTokenProxy)]
178    esp_token_proxy: Option<Address>,
179
180    /// Use an already-deployed StakeTable.sol instead of deploying a new one.
181    #[clap(long, env = Contract::StakeTable)]
182    stake_table: Option<Address>,
183
184    /// Use an already-deployed StakeTableV2.sol instead of deploying a new one.
185    #[clap(long, env = Contract::StakeTableV2)]
186    stake_table_v2: Option<Address>,
187
188    /// Use an already-deployed StakeTable.sol proxy instead of deploying a new one.
189    #[clap(long, env = Contract::StakeTableProxy)]
190    stake_table_proxy: Option<Address>,
191
192    /// RewardClaim.sol
193    #[clap(long, env = Contract::RewardClaim)]
194    reward_claim: Option<Address>,
195
196    /// Use an already-deployed RewardClaim.sol proxy instead of deploying a new one.
197    #[clap(long, env = Contract::RewardClaimProxy)]
198    reward_claim_proxy: Option<Address>,
199}
200
201/// An identifier for a particular contract.
202#[derive(Clone, Copy, Debug, Display, PartialEq, Eq, Hash)]
203pub enum Contract {
204    #[display("ESPRESSO_SEQUENCER_PLONK_VERIFIER_ADDRESS")]
205    PlonkVerifier,
206    #[display("ESPRESSO_SEQUENCER_OPS_TIMELOCK_ADDRESS")]
207    OpsTimelock,
208    #[display("ESPRESSO_SEQUENCER_SAFE_EXIT_TIMELOCK_ADDRESS")]
209    SafeExitTimelock,
210    #[display("ESPRESSO_SEQUENCER_PLONK_VERIFIER_V2_ADDRESS")]
211    PlonkVerifierV2,
212    #[display("ESPRESSO_SEQUENCER_PLONK_VERIFIER_V3_ADDRESS")]
213    PlonkVerifierV3,
214    #[display("ESPRESSO_SEQUENCER_LIGHT_CLIENT_ADDRESS")]
215    LightClient,
216    #[display("ESPRESSO_SEQUENCER_LIGHT_CLIENT_V2_ADDRESS")]
217    LightClientV2,
218    #[display("ESPRESSO_SEQUENCER_LIGHT_CLIENT_V3_ADDRESS")]
219    LightClientV3,
220    #[display("ESPRESSO_SEQUENCER_LIGHT_CLIENT_PROXY_ADDRESS")]
221    LightClientProxy,
222    #[display("ESPRESSO_SEQUENCER_FEE_CONTRACT_ADDRESS")]
223    FeeContract,
224    #[display("ESPRESSO_SEQUENCER_FEE_CONTRACT_PROXY_ADDRESS")]
225    FeeContractProxy,
226    #[display("ESPRESSO_SEQUENCER_ESP_TOKEN_ADDRESS")]
227    EspToken,
228    #[display("ESPRESSO_SEQUENCER_ESP_TOKEN_V2_ADDRESS")]
229    EspTokenV2,
230    #[display("ESPRESSO_SEQUENCER_ESP_TOKEN_PROXY_ADDRESS")]
231    EspTokenProxy,
232    #[display("ESPRESSO_SEQUENCER_STAKE_TABLE_ADDRESS")]
233    StakeTable,
234    #[display("ESPRESSO_SEQUENCER_STAKE_TABLE_V2_ADDRESS")]
235    StakeTableV2,
236    #[display("ESPRESSO_SEQUENCER_STAKE_TABLE_PROXY_ADDRESS")]
237    StakeTableProxy,
238    #[display("ESPRESSO_SEQUENCER_REWARD_CLAIM_ADDRESS")]
239    RewardClaim,
240    #[display("ESPRESSO_SEQUENCER_REWARD_CLAIM_PROXY_ADDRESS")]
241    RewardClaimProxy,
242}
243
244#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, ValueEnum)]
245#[clap(rename_all = "kebab-case")]
246pub enum OwnableContract {
247    #[value(alias = "feecontract", alias = "FeeContract")]
248    FeeContractProxy,
249    #[value(alias = "lightclient", alias = "LightClient")]
250    LightClientProxy,
251    #[value(alias = "staketable", alias = "StakeTable")]
252    StakeTableProxy,
253    #[value(alias = "esptoken", alias = "EspToken")]
254    EspTokenProxy,
255    #[value(alias = "rewardclaim", alias = "RewardClaim")]
256    RewardClaimProxy,
257}
258
259impl From<OwnableContract> for Contract {
260    fn from(c: OwnableContract) -> Contract {
261        match c {
262            OwnableContract::FeeContractProxy => Contract::FeeContractProxy,
263            OwnableContract::LightClientProxy => Contract::LightClientProxy,
264            OwnableContract::StakeTableProxy => Contract::StakeTableProxy,
265            OwnableContract::EspTokenProxy => Contract::EspTokenProxy,
266            OwnableContract::RewardClaimProxy => Contract::RewardClaimProxy,
267        }
268    }
269}
270
271impl From<Contract> for OsStr {
272    fn from(c: Contract) -> OsStr {
273        c.to_string().into()
274    }
275}
276
277/// Cache of contracts predeployed or deployed during this current run.
278#[derive(Debug, Clone)]
279pub struct Contracts {
280    addresses: HashMap<Contract, Address>,
281    // TODO: having the cooldown field here is a bit hacky but we postpone a better solution to
282    // avoid a large refactor.
283    deploy_cooldown: Duration,
284}
285
286impl Default for Contracts {
287    fn default() -> Self {
288        Self {
289            addresses: HashMap::new(),
290            deploy_cooldown: Duration::ZERO,
291        }
292    }
293}
294
295impl From<DeployedContracts> for Contracts {
296    fn from(deployed: DeployedContracts) -> Self {
297        let mut m = HashMap::new();
298        if let Some(addr) = deployed.plonk_verifier {
299            m.insert(Contract::PlonkVerifier, addr);
300        }
301        if let Some(addr) = deployed.plonk_verifier_v2 {
302            m.insert(Contract::PlonkVerifierV2, addr);
303        }
304        if let Some(addr) = deployed.plonk_verifier_v3 {
305            m.insert(Contract::PlonkVerifierV3, addr);
306        }
307        if let Some(addr) = deployed.safe_exit_timelock {
308            m.insert(Contract::SafeExitTimelock, addr);
309        }
310        if let Some(addr) = deployed.ops_timelock {
311            m.insert(Contract::OpsTimelock, addr);
312        }
313        if let Some(addr) = deployed.light_client {
314            m.insert(Contract::LightClient, addr);
315        }
316        if let Some(addr) = deployed.light_client_v2 {
317            m.insert(Contract::LightClientV2, addr);
318        }
319        if let Some(addr) = deployed.light_client_v3 {
320            m.insert(Contract::LightClientV3, addr);
321        }
322        if let Some(addr) = deployed.light_client_proxy {
323            m.insert(Contract::LightClientProxy, addr);
324        }
325        if let Some(addr) = deployed.fee_contract {
326            m.insert(Contract::FeeContract, addr);
327        }
328        if let Some(addr) = deployed.fee_contract_proxy {
329            m.insert(Contract::FeeContractProxy, addr);
330        }
331        if let Some(addr) = deployed.esp_token {
332            m.insert(Contract::EspToken, addr);
333        }
334        if let Some(addr) = deployed.esp_token_v2 {
335            m.insert(Contract::EspTokenV2, addr);
336        }
337        if let Some(addr) = deployed.esp_token_proxy {
338            m.insert(Contract::EspTokenProxy, addr);
339        }
340        if let Some(addr) = deployed.stake_table {
341            m.insert(Contract::StakeTable, addr);
342        }
343        if let Some(addr) = deployed.stake_table_v2 {
344            m.insert(Contract::StakeTableV2, addr);
345        }
346        if let Some(addr) = deployed.stake_table_proxy {
347            m.insert(Contract::StakeTableProxy, addr);
348        }
349        if let Some(addr) = deployed.reward_claim {
350            m.insert(Contract::RewardClaim, addr);
351        }
352        if let Some(addr) = deployed.reward_claim_proxy {
353            m.insert(Contract::RewardClaimProxy, addr);
354        }
355        Self {
356            addresses: m,
357            deploy_cooldown: Duration::ZERO,
358        }
359    }
360}
361
362impl Contracts {
363    pub fn new() -> Self {
364        Self::default()
365    }
366
367    pub fn with_cooldown(cooldown: Duration) -> Self {
368        Self {
369            addresses: HashMap::new(),
370            deploy_cooldown: cooldown,
371        }
372    }
373
374    pub fn set_cooldown(&mut self, cooldown: Duration) {
375        self.deploy_cooldown = cooldown;
376    }
377
378    pub fn address(&self, contract: Contract) -> Option<Address> {
379        self.addresses.get(&contract).copied()
380    }
381
382    pub fn get(&self, contract: &Contract) -> Option<&Address> {
383        self.addresses.get(contract)
384    }
385
386    pub fn iter(&self) -> impl Iterator<Item = (&Contract, &Address)> {
387        self.addresses.iter()
388    }
389
390    pub fn remove(&mut self, contract: &Contract) -> Option<Address> {
391        self.addresses.remove(contract)
392    }
393
394    /// Deploy a contract (with logging and cached deployments)
395    ///
396    /// The deployment `tx` will be sent only if contract `name` is not already deployed;
397    /// otherwise this function will just return the predeployed address.
398    pub async fn deploy<P>(&mut self, name: Contract, tx: RawCallBuilder<P>) -> Result<Address>
399    where
400        P: Provider,
401    {
402        if let Some(addr) = self.addresses.get(&name) {
403            tracing::info!("skipping deployment of {name}, already deployed at {addr:#x}");
404            return Ok(*addr);
405        }
406        tracing::info!("deploying {name}");
407        let pending_tx = tx.send().await?;
408        let tx_hash = *pending_tx.tx_hash();
409        tracing::info!(%tx_hash, "waiting for tx to be mined");
410        let receipt = pending_tx.get_receipt().await?;
411        if !receipt.inner.is_success() {
412            anyhow::bail!("Deployment transaction failed: {:?}", receipt);
413        }
414        tracing::info!(%receipt.gas_used, %tx_hash, "tx mined");
415        let addr = receipt
416            .contract_address
417            .ok_or(alloy::contract::Error::ContractNotDeployed)?;
418
419        tracing::info!("deployed {name} at {addr:#x}");
420
421        if !self.deploy_cooldown.is_zero() {
422            tokio::time::sleep(self.deploy_cooldown).await;
423            tracing::info!("cooldown {:?} after deployment", self.deploy_cooldown);
424        }
425
426        self.addresses.insert(name, addr);
427        Ok(addr)
428    }
429
430    /// Write a .env file.
431    pub fn write(&self, mut w: impl Write) -> Result<()> {
432        for (contract, address) in &self.addresses {
433            writeln!(w, "{contract}={address:#x}")?;
434        }
435        Ok(())
436    }
437}
438
439/// Default deployment function `LightClient.sol` or `LightClientMock.sol` with `mock: true`.
440///
441/// # NOTE:
442/// In most cases, you only need to use [`deploy_light_client_proxy()`]
443///
444/// # NOTE:
445/// currently, `LightClient.sol` follows upgradable contract, thus a follow-up
446/// call to `.initialize()` with proper genesis block (and other constructor args)
447/// are expected to be *delegatecall-ed through the proxy contract*.
448pub(crate) async fn deploy_light_client_contract(
449    provider: impl Provider,
450    contracts: &mut Contracts,
451    mock: bool,
452) -> Result<Address> {
453    // Deploy library contracts.
454    let plonk_verifier_addr = contracts
455        .deploy(
456            Contract::PlonkVerifier,
457            PlonkVerifier::deploy_builder(&provider),
458        )
459        .await?;
460
461    assert!(is_contract(&provider, plonk_verifier_addr).await?);
462
463    // when generate alloy's bindings, we supply a placeholder address, now we modify the actual
464    // bytecode with deployed address of the library.
465    let target_lc_bytecode = if mock {
466        LightClientMock::BYTECODE.encode_hex()
467    } else {
468        LightClient::BYTECODE.encode_hex()
469    };
470    let lc_linked_bytecode = {
471        match target_lc_bytecode
472            .matches(LIBRARY_PLACEHOLDER_ADDRESS)
473            .count()
474        {
475            0 => return Err(anyhow!("lib placeholder not found")),
476            1 => Bytes::from_hex(target_lc_bytecode.replacen(
477                LIBRARY_PLACEHOLDER_ADDRESS,
478                &plonk_verifier_addr.encode_hex(),
479                1,
480            ))?,
481            _ => {
482                return Err(anyhow!(
483                    "more than one lib placeholder found, consider using a different value"
484                ));
485            },
486        }
487    };
488
489    // Deploy the light client
490    let light_client_addr = if mock {
491        // for mock, we don't populate the `contracts` since it only track production-ready deployments
492        let addr = LightClientMock::deploy_builder(&provider)
493            .map(|req| req.with_deploy_code(lc_linked_bytecode))
494            .deploy()
495            .await?;
496        tracing::info!("deployed LightClientMock at {addr:#x}");
497        addr
498    } else {
499        contracts
500            .deploy(
501                Contract::LightClient,
502                LightClient::deploy_builder(&provider)
503                    .map(|req| req.with_deploy_code(lc_linked_bytecode)),
504            )
505            .await?
506    };
507    Ok(light_client_addr)
508}
509
510/// The primary logic for deploying and initializing an upgradable light client contract.
511///
512/// Deploy the upgradable proxy contract, point to an already deployed light client contract as its implementation, and invoke `initialize()` on it.
513/// This is run after `deploy_light_client_contract()`, returns the proxy address.
514/// This works for both mock and production light client proxy.
515pub async fn deploy_light_client_proxy(
516    provider: impl Provider,
517    contracts: &mut Contracts,
518    mock: bool,
519    genesis_state: LightClientStateSol,
520    genesis_stake: StakeTableStateSol,
521    admin: Address,
522    prover: Option<Address>,
523) -> Result<Address> {
524    // deploy the light client implementation contract
525    let impl_addr = deploy_light_client_contract(&provider, contracts, mock).await?;
526    let lc = LightClient::new(impl_addr, &provider);
527
528    // prepare the input arg for `initialize()`
529    let init_data = lc
530        .initialize(
531            genesis_state,
532            genesis_stake,
533            MAX_HISTORY_RETENTION_SECONDS,
534            admin,
535        )
536        .calldata()
537        .to_owned();
538    // deploy proxy and initialize
539    let lc_proxy_addr = contracts
540        .deploy(
541            Contract::LightClientProxy,
542            ERC1967Proxy::deploy_builder(&provider, impl_addr, init_data),
543        )
544        .await?;
545
546    // sanity check
547    if !is_proxy_contract(&provider, lc_proxy_addr).await? {
548        panic!("LightClientProxy detected not as a proxy, report error!");
549    }
550
551    // instantiate a proxy instance, cast as LightClient's ABI interface
552    let lc_proxy = LightClient::new(lc_proxy_addr, &provider);
553
554    // set permissioned prover
555    if let Some(prover) = prover {
556        tracing::info!(%lc_proxy_addr, %prover, "Set permissioned prover ");
557        lc_proxy
558            .setPermissionedProver(prover)
559            .send()
560            .await?
561            .get_receipt()
562            .await?;
563    }
564
565    // post deploy verification checks
566    assert_eq!(lc_proxy.getVersion().call().await?.majorVersion, 1);
567    assert_eq!(lc_proxy.owner().call().await?, admin);
568    if let Some(prover) = prover {
569        assert_eq!(lc_proxy.permissionedProver().call().await?, prover);
570    }
571    assert_eq!(lc_proxy.stateHistoryRetentionPeriod().call().await?, 864000);
572    assert_eq!(
573        lc_proxy.currentBlockNumber().call().await?,
574        U256::from(provider.get_block_number().await?)
575    );
576
577    Ok(lc_proxy_addr)
578}
579
580/// Upgrade the light client proxy to use LightClientV2.
581/// Internally, first detect existence of proxy, then deploy LCV2, then upgrade and initializeV2.
582/// Internal to "deploy LCV2", we deploy PlonkVerifierV2 whose address will be used at LCV2 init time.
583/// Assumes:
584/// - the proxy is already deployed.
585/// - the proxy is owned by a regular EOA, not a multisig.
586/// - the proxy is not yet initialized for V2
587///
588/// Returns the receipt of the upgrade transaction.
589pub async fn upgrade_light_client_v2(
590    provider: impl Provider,
591    contracts: &mut Contracts,
592    is_mock: bool,
593    blocks_per_epoch: u64,
594    epoch_start_block: u64,
595) -> Result<TransactionReceipt> {
596    match contracts.address(Contract::LightClientProxy) {
597        // check if proxy already exists
598        None => Err(anyhow!("LightClientProxy not found, can't upgrade")),
599        Some(proxy_addr) => {
600            let proxy = LightClient::new(proxy_addr, &provider);
601
602            let curr_version = proxy.getVersion().call().await?;
603            if curr_version.majorVersion > 2 {
604                anyhow::bail!(
605                    "Expected LightClient V1 or V2 for upgrade to V2, found V{}",
606                    curr_version.majorVersion
607                );
608            }
609            // Log a warning if this is a patch upgrade (re-applying same version)
610            if curr_version.majorVersion == 2 {
611                tracing::warn!(
612                    "Re-applying LightClient V2 (patch upgrade). This will deploy a fresh \
613                     implementation."
614                );
615            }
616            let state_history_retention_period = proxy.stateHistoryRetentionPeriod().call().await?;
617            // first deploy PlonkVerifierV2.sol
618            let pv2_addr = contracts
619                .deploy(
620                    Contract::PlonkVerifierV2,
621                    PlonkVerifierV2::deploy_builder(&provider),
622                )
623                .await?;
624
625            assert!(is_contract(&provider, pv2_addr).await?);
626
627            // then deploy LightClientV2.sol
628            let target_lcv2_bytecode = if is_mock {
629                LightClientV2Mock::BYTECODE.encode_hex()
630            } else {
631                LightClientV2::BYTECODE.encode_hex()
632            };
633            let lcv2_linked_bytecode = {
634                match target_lcv2_bytecode
635                    .matches(LIBRARY_PLACEHOLDER_ADDRESS)
636                    .count()
637                {
638                    0 => return Err(anyhow!("lib placeholder not found")),
639                    1 => Bytes::from_hex(target_lcv2_bytecode.replacen(
640                        LIBRARY_PLACEHOLDER_ADDRESS,
641                        &pv2_addr.encode_hex(),
642                        1,
643                    ))?,
644                    _ => {
645                        return Err(anyhow!(
646                            "more than one lib placeholder found, consider using a different value"
647                        ));
648                    },
649                }
650            };
651            let lcv2_addr = if is_mock {
652                let addr = LightClientV2Mock::deploy_builder(&provider)
653                    .map(|req| req.with_deploy_code(lcv2_linked_bytecode))
654                    .deploy()
655                    .await?;
656                tracing::info!("deployed LightClientV2Mock at {addr:#x}");
657                addr
658            } else {
659                // Only check/remove from cache if this is a patch upgrade (V2 -> V2)
660                let is_patch_upgrade = curr_version.majorVersion == 2;
661                if is_patch_upgrade {
662                    let cached_lcv2_addr = contracts.address(Contract::LightClientV2);
663                    // For patch upgrades, we need to deploy a fresh implementation contract.
664                    // If LightClientV2 is already in the cache, the caller must unset it first
665                    // to make the redeployment requirement explicit.
666                    if let Some(addr) = cached_lcv2_addr {
667                        anyhow::bail!(
668                            "LightClientV2 implementation address is already set in cache \
669                             ({:#x}). For patch upgrades, the implementation must be redeployed. \
670                             Please unset ESPRESSO_SEQUENCER_LIGHT_CLIENT_V2_ADDRESS or remove it \
671                             from the cache first.",
672                            addr
673                        );
674                    }
675                }
676
677                contracts
678                    .deploy(
679                        Contract::LightClientV2,
680                        LightClientV2::deploy_builder(&provider)
681                            .map(|req| req.with_deploy_code(lcv2_linked_bytecode)),
682                    )
683                    .await?
684            };
685
686            // get owner of proxy
687            let owner = proxy.owner().call().await?;
688            let owner_addr = owner;
689            tracing::info!("Proxy owner: {owner_addr:#x}");
690
691            // prepare initial function call calldata (checks if already initialized)
692            // you cannot initialize a proxy that is already initialized
693            // so if one wanted to use this function to upgrade a proxy to v2, that's already v2
694            // then we shouldn't call the initialize function
695            let lcv2 = LightClientV2::new(lcv2_addr, &provider);
696            let init_data = if already_initialized(&provider, proxy_addr, 2).await? {
697                vec![].into()
698            } else {
699                lcv2.initializeV2(blocks_per_epoch, epoch_start_block)
700                    .calldata()
701                    .to_owned()
702            };
703            // invoke upgrade on proxy
704            let receipt = proxy
705                .upgradeToAndCall(lcv2_addr, init_data)
706                .send()
707                .await?
708                .get_receipt()
709                .await?;
710            let proxy_as_v2 = LightClientV2::new(proxy_addr, &provider);
711
712            if receipt.inner.is_success() {
713                // check that the upgrade is complete (with retry for RPC timing)
714                let is_complete = retry_until_true("LightClientProxy V2 version check", || async {
715                    Ok(proxy_as_v2.getVersion().call().await?.majorVersion == 2)
716                })
717                .await?;
718
719                if !is_complete {
720                    anyhow::bail!(
721                        "LightClientProxy version check failed after retries: expected V2"
722                    );
723                }
724
725                // post deploy verification checks
726                assert_eq!(proxy_as_v2.blocksPerEpoch().call().await?, blocks_per_epoch);
727                assert_eq!(
728                    proxy_as_v2.epochStartBlock().call().await?,
729                    epoch_start_block
730                );
731                assert_eq!(
732                    proxy_as_v2.stateHistoryRetentionPeriod().call().await?,
733                    state_history_retention_period
734                );
735                assert_eq!(
736                    proxy_as_v2.currentBlockNumber().call().await?,
737                    U256::from(provider.get_block_number().await?)
738                );
739
740                tracing::info!(%lcv2_addr, "LightClientProxy successfully upgraded to V2");
741                tracing::info!(
742                    "blocksPerEpoch: {}",
743                    proxy_as_v2.blocksPerEpoch().call().await?
744                );
745                tracing::info!(
746                    "epochStartBlock: {}",
747                    proxy_as_v2.epochStartBlock().call().await?
748                );
749            } else {
750                tracing::error!("LightClientProxy upgrade failed: {:?}", receipt);
751            }
752
753            Ok(receipt)
754        },
755    }
756}
757
758/// Upgrade the light client proxy to use LightClientV3.
759/// Internally, first detect existence of proxy, then deploy LCV3, then upgrade and initializeV3.
760/// Internal to "deploy LCV3", we deploy PlonkVerifierV3 whose address will be used at LCV3 init time.
761/// Assumes:
762/// - the proxy is already deployed.
763/// - the proxy is owned by a regular EOA, not a multisig.
764/// - the proxy is not yet initialized for V3
765pub async fn upgrade_light_client_v3(
766    provider: impl Provider,
767    contracts: &mut Contracts,
768    is_mock: bool,
769) -> Result<TransactionReceipt> {
770    match contracts.address(Contract::LightClientProxy) {
771        // check if proxy already exists
772        None => Err(anyhow!("LightClientProxy not found, can't upgrade")),
773        Some(proxy_addr) => {
774            let proxy = LightClient::new(proxy_addr, &provider);
775
776            // Check proxy version, V3 requires at least V2 as a prerequisite
777            // This ensures we don't try to upgrade from V1 directly to V3
778            // V1 -> V2 -> V3 is the correct upgrade path
779            let version = proxy.getVersion().call().await?;
780            if version.majorVersion < 2 {
781                anyhow::bail!(
782                    "LightClientProxy is V{}, can't upgrade to V3. Must upgrade to V2 first.",
783                    version.majorVersion
784                );
785            }
786
787            // first deploy PlonkVerifierV3.sol
788            let pv3_addr = contracts
789                .deploy(
790                    Contract::PlonkVerifierV3,
791                    PlonkVerifierV3::deploy_builder(&provider),
792                )
793                .await?;
794            assert!(is_contract(&provider, pv3_addr).await?);
795
796            // then deploy LightClientV3.sol
797            let target_lcv3_bytecode = if is_mock {
798                LightClientV3Mock::BYTECODE.encode_hex()
799            } else {
800                LightClientV3::BYTECODE.encode_hex()
801            };
802            let lcv3_linked_bytecode = {
803                match target_lcv3_bytecode
804                    .matches(LIBRARY_PLACEHOLDER_ADDRESS)
805                    .count()
806                {
807                    0 => return Err(anyhow!("lib placeholder not found")),
808                    1 => Bytes::from_hex(target_lcv3_bytecode.replacen(
809                        LIBRARY_PLACEHOLDER_ADDRESS,
810                        &pv3_addr.encode_hex(),
811                        1,
812                    ))?,
813                    _ => {
814                        return Err(anyhow!(
815                            "more than one lib placeholder found, consider using a different value"
816                        ));
817                    },
818                }
819            };
820            let lcv3_addr = if is_mock {
821                let addr = LightClientV3Mock::deploy_builder(&provider)
822                    .map(|req| req.with_deploy_code(lcv3_linked_bytecode))
823                    .deploy()
824                    .await?;
825                tracing::info!("deployed LightClientV3Mock at {addr:#x}");
826                addr
827            } else {
828                contracts
829                    .deploy(
830                        Contract::LightClientV3,
831                        LightClientV3::deploy_builder(&provider)
832                            .map(|req| req.with_deploy_code(lcv3_linked_bytecode)),
833                    )
834                    .await?
835            };
836
837            // get owner of proxy
838            let owner = proxy.owner().call().await?;
839            let owner_addr = owner;
840            tracing::info!("Proxy owner: {owner_addr:#x}");
841
842            let lcv3 = LightClientV3::new(lcv3_addr, &provider);
843
844            // prepare initial function call calldata (checks if already initialized)
845            // you cannot initialize a proxy that is already initialized
846            // so if one wanted to use this function to upgrade a proxy to v3, that's already v3
847            // then we shouldn't call the initialize function
848            let init_data = if already_initialized(&provider, proxy_addr, 3).await? {
849                vec![].into()
850            } else {
851                lcv3.initializeV3().calldata().to_owned()
852            };
853
854            // invoke upgrade on proxy
855            let receipt = proxy
856                .upgradeToAndCall(lcv3_addr, init_data)
857                .send()
858                .await?
859                .get_receipt()
860                .await?;
861
862            let proxy_as_v3 = LightClientV3::new(proxy_addr, &provider);
863
864            if receipt.inner.is_success() {
865                // check that the upgrade is complete (with retry for RPC timing)
866                let version_is_v3 =
867                    retry_until_true("LightClientProxy V3 version check", || async {
868                        Ok(proxy_as_v3.getVersion().call().await?.majorVersion == 3)
869                    })
870                    .await?;
871
872                if !version_is_v3 {
873                    anyhow::bail!(
874                        "LightClientProxy version check failed after retries: expected V3"
875                    );
876                }
877
878                tracing::info!(%lcv3_addr, "LightClientProxy successfully upgraded to V3");
879            } else {
880                tracing::error!("LightClientProxy upgrade failed: {:?}", receipt);
881            }
882
883            Ok(receipt)
884        },
885    }
886}
887
888async fn already_initialized(
889    provider: impl Provider,
890    proxy_addr: Address,
891    expected_major_version: u8,
892) -> Result<bool> {
893    let initialized = get_proxy_initialized_version(&provider, proxy_addr).await?;
894    tracing::info!("Initialized version: {}", initialized);
895
896    // since all upgradable contracts have a getVersion() function, we can use it to get the major version
897    let contract_proxy = LightClientV2::new(proxy_addr, &provider);
898    let contract_major_version = contract_proxy.getVersion().call().await?.majorVersion;
899
900    Ok(initialized == contract_major_version && contract_major_version == expected_major_version)
901}
902
903/// The primary logic for deploying and initializing an upgradable fee contract.
904///
905/// Deploy the upgradable proxy contract, point to a deployed fee contract as its implementation, and invoke `initialize()` on it.
906/// - `admin`: is the new owner (e.g. a multisig address) of the proxy contract
907///
908/// Return the proxy address.
909pub async fn deploy_fee_contract_proxy(
910    provider: impl Provider,
911    contracts: &mut Contracts,
912    admin: Address,
913) -> Result<Address> {
914    // deploy the fee implementation contract
915    let fee_addr = contracts
916        .deploy(
917            Contract::FeeContract,
918            FeeContract::deploy_builder(&provider),
919        )
920        .await?;
921    let fee = FeeContract::new(fee_addr, &provider);
922
923    // prepare the input arg for `initialize()`
924    let init_data = fee.initialize(admin).calldata().to_owned();
925    // deploy proxy and initialize
926    let fee_proxy_addr = contracts
927        .deploy(
928            Contract::FeeContractProxy,
929            ERC1967Proxy::deploy_builder(&provider, fee_addr, init_data),
930        )
931        .await?;
932    // sanity check
933    if !is_proxy_contract(&provider, fee_proxy_addr).await? {
934        panic!("FeeContractProxy detected not as a proxy, report error!");
935    }
936
937    // post deploy verification checks
938    let fee_proxy = FeeContract::new(fee_proxy_addr, &provider);
939    assert_eq!(fee_proxy.getVersion().call().await?.majorVersion, 1);
940    assert_eq!(fee_proxy.owner().call().await?, admin);
941
942    Ok(fee_proxy_addr)
943}
944
945/// The primary logic for deploying and initializing an upgradable Espresso Token contract.
946pub async fn deploy_token_proxy(
947    provider: impl Provider,
948    contracts: &mut Contracts,
949    owner: Address,
950    init_grant_recipient: Address,
951    initial_supply: U256,
952    name: &str,
953    symbol: &str,
954) -> Result<Address> {
955    let token_addr = contracts
956        .deploy(Contract::EspToken, EspToken::deploy_builder(&provider))
957        .await?;
958    let token = EspToken::new(token_addr, &provider);
959
960    let init_data = token
961        .initialize(
962            owner,
963            init_grant_recipient,
964            initial_supply,
965            name.to_string(),
966            symbol.to_string(),
967        )
968        .calldata()
969        .to_owned();
970
971    let token_proxy_addr = contracts
972        .deploy(
973            Contract::EspTokenProxy,
974            ERC1967Proxy::deploy_builder(&provider, token_addr, init_data),
975        )
976        .await?;
977
978    if !is_proxy_contract(&provider, token_proxy_addr).await? {
979        panic!("EspTokenProxy detected not as a proxy, report error!");
980    }
981
982    // post deploy verification checks
983    let token_proxy = EspToken::new(token_proxy_addr, &provider);
984    assert_eq!(token_proxy.getVersion().call().await?.majorVersion, 1);
985    assert_eq!(token_proxy.owner().call().await?, owner);
986    assert_eq!(token_proxy.symbol().call().await?, symbol);
987    assert_eq!(token_proxy.decimals().call().await?, 18);
988    assert_eq!(token_proxy.name().call().await?, name);
989    let total_supply = token_proxy.totalSupply().call().await?;
990    assert_eq!(
991        token_proxy.balanceOf(init_grant_recipient).call().await?,
992        total_supply
993    );
994
995    Ok(token_proxy_addr)
996}
997
998/// Upgrade the esp token proxy to use EspTokenV2.
999pub async fn upgrade_esp_token_v2(
1000    provider: impl Provider,
1001    contracts: &mut Contracts,
1002) -> Result<TransactionReceipt> {
1003    let Some(proxy_addr) = contracts.address(Contract::EspTokenProxy) else {
1004        anyhow::bail!("EspTokenProxy not found, can't upgrade")
1005    };
1006
1007    let proxy = EspToken::new(proxy_addr, &provider);
1008    // Deploy the new implementation
1009    let v2_addr = contracts
1010        .deploy(Contract::EspTokenV2, EspTokenV2::deploy_builder(&provider))
1011        .await?;
1012
1013    assert!(is_contract(&provider, v2_addr).await?);
1014
1015    // prepare init calldata for V2
1016    let reward_claim_addr = contracts
1017        .address(Contract::RewardClaimProxy)
1018        .ok_or_else(|| anyhow!("RewardClaimProxy not found"))?;
1019    let proxy_as_v2 = EspTokenV2::new(proxy_addr, &provider);
1020    let init_data = proxy_as_v2
1021        .initializeV2(reward_claim_addr)
1022        .calldata()
1023        .to_owned();
1024
1025    // invoke upgrade on proxy with initializeV2 call
1026    let receipt = proxy
1027        .upgradeToAndCall(v2_addr, init_data)
1028        .send()
1029        .await?
1030        .get_receipt()
1031        .await?;
1032
1033    if receipt.inner.is_success() {
1034        // check that the upgrade is complete (with retry for RPC timing)
1035        let version_is_v2 = retry_until_true("EspTokenProxy V2 version check", || async {
1036            Ok(proxy_as_v2.getVersion().call().await?.majorVersion == 2)
1037        })
1038        .await?;
1039
1040        if !version_is_v2 {
1041            anyhow::bail!("EspTokenProxy version check failed after retries: expected V2");
1042        }
1043
1044        // post deploy verification checks
1045        assert_eq!(proxy_as_v2.name().call().await?, "Espresso");
1046        assert_eq!(proxy_as_v2.rewardClaim().call().await?, reward_claim_addr);
1047        tracing::info!(%v2_addr, "EspToken successfully upgraded to");
1048    } else {
1049        anyhow::bail!("EspToken upgrade failed: {:?}", receipt);
1050    }
1051
1052    Ok(receipt)
1053}
1054
1055/// The primary logic for deploying and initializing an upgradable permissionless StakeTable contract.
1056pub async fn deploy_stake_table_proxy(
1057    provider: impl Provider,
1058    contracts: &mut Contracts,
1059    token_addr: Address,
1060    light_client_addr: Address,
1061    exit_escrow_period: U256,
1062    owner: Address,
1063) -> Result<Address> {
1064    let stake_table_addr = contracts
1065        .deploy(Contract::StakeTable, StakeTable::deploy_builder(&provider))
1066        .await?;
1067    let stake_table = StakeTable::new(stake_table_addr, &provider);
1068
1069    // TODO: verify the light client address contains a contract
1070    // See #3163, it's a cyclic dependency in the demo environment
1071    // assert!(is_contract(&provider, light_client_addr).await?);
1072
1073    // verify the token address contains a contract
1074    if !is_contract(&provider, token_addr).await? {
1075        anyhow::bail!("Token address is not a contract, can't deploy StakeTableProxy");
1076    }
1077
1078    let init_data = stake_table
1079        .initialize(token_addr, light_client_addr, exit_escrow_period, owner)
1080        .calldata()
1081        .to_owned();
1082
1083    let st_proxy_addr = contracts
1084        .deploy(
1085            Contract::StakeTableProxy,
1086            ERC1967Proxy::deploy_builder(&provider, stake_table_addr, init_data),
1087        )
1088        .await?;
1089
1090    if !is_proxy_contract(&provider, st_proxy_addr).await? {
1091        panic!("StakeTableProxy detected not as a proxy, report error!");
1092    }
1093
1094    let st_proxy = StakeTable::new(st_proxy_addr, &provider);
1095    assert_eq!(st_proxy.getVersion().call().await?.majorVersion, 1);
1096    assert_eq!(st_proxy.owner().call().await?, owner);
1097    assert_eq!(st_proxy.token().call().await?, token_addr);
1098    assert_eq!(st_proxy.lightClient().call().await?, light_client_addr);
1099    assert_eq!(
1100        st_proxy.exitEscrowPeriod().call().await?,
1101        exit_escrow_period
1102    );
1103
1104    Ok(st_proxy_addr)
1105}
1106
1107/// Deploy and initialize the RewardClaim contract behind a proxy
1108pub async fn deploy_reward_claim_proxy(
1109    provider: impl Provider,
1110    contracts: &mut Contracts,
1111    esp_token_addr: Address,
1112    light_client_addr: Address,
1113    admin: Address,
1114    pauser: Address,
1115) -> Result<Address> {
1116    let reward_claim_addr = contracts
1117        .deploy(
1118            Contract::RewardClaim,
1119            RewardClaim::deploy_builder(&provider),
1120        )
1121        .await?;
1122    let reward_claim = RewardClaim::new(reward_claim_addr, &provider);
1123
1124    // verify the esp token address contains a contract
1125    if !is_contract(&provider, esp_token_addr).await? {
1126        anyhow::bail!("EspToken address is not a contract, can't deploy RewardClaimProxy");
1127    }
1128
1129    // verify the light client address contains a contract
1130    if !is_contract(&provider, light_client_addr).await? {
1131        anyhow::bail!("LightClient address is not a contract, can't deploy RewardClaimProxy");
1132    }
1133
1134    let init_data = reward_claim
1135        .initialize(admin, esp_token_addr, light_client_addr, pauser)
1136        .calldata()
1137        .to_owned();
1138    let reward_claim_proxy_addr = contracts
1139        .deploy(
1140            Contract::RewardClaimProxy,
1141            ERC1967Proxy::deploy_builder(&provider, reward_claim_addr, init_data),
1142        )
1143        .await?;
1144
1145    if !is_proxy_contract(&provider, reward_claim_proxy_addr).await? {
1146        panic!("RewardClaimProxy detected not as a proxy, report error!");
1147    }
1148
1149    let reward_claim_proxy = RewardClaim::new(reward_claim_proxy_addr, &provider);
1150    assert_eq!(
1151        reward_claim_proxy.getVersion().call().await?,
1152        (1, 0, 0).into()
1153    );
1154    // Verify admin has DEFAULT_ADMIN_ROLE
1155    let admin_role = reward_claim_proxy.DEFAULT_ADMIN_ROLE().call().await?;
1156    assert!(
1157        reward_claim_proxy.hasRole(admin_role, admin).call().await?,
1158        "admin should have DEFAULT_ADMIN_ROLE"
1159    );
1160    assert_eq!(reward_claim_proxy.espToken().call().await?, esp_token_addr);
1161    assert_eq!(
1162        reward_claim_proxy.lightClient().call().await?,
1163        light_client_addr
1164    );
1165
1166    Ok(reward_claim_proxy_addr)
1167}
1168
1169/// Read stake table data from L1 StakeTable V1 events for V2 migration
1170///
1171/// Returns both the active stake and commissions needed for StakeTable V2 initialization.
1172/// Assumes an infura RPC is used, otherwise it may hit other rate limits.
1173pub async fn fetch_stake_table_for_stake_table_storage_migration(
1174    l1_client: L1Client,
1175    stake_table_address: Address,
1176) -> Result<(U256, Vec<StakeTableV2::InitialCommission>)> {
1177    let stake_table = StakeTable::new(stake_table_address, &l1_client);
1178
1179    // Verify this is a V1 contract
1180    let version = stake_table.getVersion().call().await?;
1181    if version.majorVersion != 1 {
1182        anyhow::bail!(
1183            "Expected StakeTable V1 for migration, found V{}",
1184            version.majorVersion
1185        );
1186    }
1187
1188    tracing::info!("Fetching all validators from StakeTable V1 contract");
1189
1190    // Get the latest L1 block to query up to
1191    let latest_block = l1_client.provider.get_block_number().await?;
1192
1193    // Use Fetcher to get all validators from contract events
1194    let (validators, _stake_table_hash) =
1195        Fetcher::fetch_all_validators_from_contract(l1_client, stake_table_address, latest_block)
1196            .await
1197            .context("Failed to fetch validators from V1 contract")?;
1198
1199    // Sum up total active stake from all validators
1200    let active_stake = validators.values().map(|v| v.stake).sum::<U256>();
1201
1202    // Extract commissions from validators
1203    let commissions = validators
1204        .values()
1205        .map(|v| StakeTableV2::InitialCommission {
1206            validator: v.account,
1207            commission: v.commission,
1208        })
1209        .collect::<Vec<_>>();
1210
1211    tracing::info!(
1212        "Found {} active stake and {} commissions from {} validators to migrate from V1",
1213        active_stake,
1214        commissions.len(),
1215        validators.len()
1216    );
1217
1218    Ok((active_stake, commissions))
1219}
1220
1221/// Prepare the upgrade data for StakeTable V2, checking version and fetching commissions if needed.
1222///
1223/// Returns:
1224/// - The initialization commissions (maybe used for post deployment verification)
1225/// - The initialization calldata (if initialization is needed)
1226pub async fn prepare_stake_table_v2_upgrade(
1227    l1_client: L1Client,
1228    proxy_addr: Address,
1229    pauser: Address,
1230    admin: Address,
1231) -> Result<(
1232    Option<Vec<StakeTableV2::InitialCommission>>,
1233    Option<U256>,
1234    Option<Bytes>,
1235)> {
1236    let proxy = StakeTable::new(proxy_addr, &l1_client);
1237
1238    let current_version = proxy.getVersion().call().await?;
1239    let target_version = 2;
1240    if current_version.majorVersion > target_version {
1241        anyhow::bail!(
1242            "Expected StakeTable V1 or V2, found V{}",
1243            current_version.majorVersion
1244        );
1245    }
1246
1247    // For a non-major version upgrade the proxy storage must already be initialized.
1248    let needs_initialization =
1249        !already_initialized(&l1_client.provider, proxy_addr, target_version).await?;
1250    assert_eq!(
1251        needs_initialization,
1252        current_version.majorVersion < target_version,
1253        "unexpected version initialized"
1254    );
1255
1256    if needs_initialization {
1257        tracing::info!("Fetching stake table data from V1 contract for migration");
1258        let (active_stake, commissions) =
1259            fetch_stake_table_for_stake_table_storage_migration(l1_client.clone(), proxy_addr)
1260                .await?;
1261
1262        tracing::info!(
1263            %pauser,
1264            %admin,
1265            active_stake = %format!("{:?} wei", active_stake),
1266            commission_count = commissions.len(),
1267            "Init Data to be signed. Function: initializeV2",
1268        );
1269
1270        // We can use any address here since we're just building calldata
1271        let data = StakeTableV2::new(Address::ZERO, &l1_client)
1272            .initializeV2(pauser, admin, active_stake, commissions.clone())
1273            .calldata()
1274            .to_owned();
1275
1276        Ok((Some(commissions), Some(active_stake), Some(data)))
1277    } else {
1278        tracing::info!(
1279            "Proxy was already initialized for version {}",
1280            target_version
1281        );
1282        Ok((None, None, None))
1283    }
1284}
1285
1286/// Upgrade the stake table proxy from V1 to V2, or patch V2
1287pub async fn upgrade_stake_table_v2(
1288    provider: impl Provider,
1289    l1_client: L1Client,
1290    contracts: &mut Contracts,
1291    pauser: Address,
1292    admin: Address,
1293) -> Result<TransactionReceipt> {
1294    tracing::info!("Upgrading StakeTableProxy to StakeTableV2 with EOA admin");
1295    let Some(proxy_addr) = contracts.address(Contract::StakeTableProxy) else {
1296        anyhow::bail!("StakeTableProxy not found, can't upgrade")
1297    };
1298
1299    // First prepare upgrade data (including fetching commissions if needed)
1300    let (init_commissions, init_active_stake, init_data) =
1301        prepare_stake_table_v2_upgrade(l1_client.clone(), proxy_addr, pauser, admin).await?;
1302
1303    // Then deploy the new implementation
1304    let v2_addr = contracts
1305        .deploy(
1306            Contract::StakeTableV2,
1307            StakeTableV2::deploy_builder(&provider),
1308        )
1309        .await?;
1310
1311    let proxy = StakeTable::new(proxy_addr, &provider);
1312
1313    let receipt = proxy
1314        .upgradeToAndCall(v2_addr, init_data.unwrap_or_default())
1315        .send()
1316        .await?
1317        .get_receipt()
1318        .await?;
1319
1320    let proxy_as_v2 = StakeTableV2::new(proxy_addr, &provider);
1321
1322    if receipt.inner.is_success() {
1323        //TODO: check event emission instead as it's more reliable
1324        // check that the upgrade is complete (with retry for RPC timing)
1325        let version_is_v2 = retry_until_true("StakeTableProxy V2 version check", || async {
1326            Ok(proxy_as_v2.getVersion().call().await?.majorVersion == 2)
1327        })
1328        .await?;
1329        if !version_is_v2 {
1330            anyhow::bail!("StakeTableProxy version check failed after retries: expected V2");
1331        }
1332
1333        // post deploy verification checks
1334        let pauser_role = proxy_as_v2.PAUSER_ROLE().call().await?;
1335        assert!(
1336            proxy_as_v2.hasRole(pauser_role, pauser).call().await?,
1337            "pauser should have PAUSER_ROLE"
1338        );
1339
1340        let admin_role = proxy_as_v2.DEFAULT_ADMIN_ROLE().call().await?;
1341        assert!(proxy_as_v2.hasRole(admin_role, admin).call().await?,);
1342
1343        if let Some(migrated) = init_commissions {
1344            tracing::info!("Verifying migrated commissions, may take a minute");
1345            for init_comm in migrated {
1346                let tracking = proxy_as_v2
1347                    .commissionTracking(init_comm.validator)
1348                    .call()
1349                    .await?;
1350                assert_eq!(tracking.commission, init_comm.commission);
1351            }
1352        }
1353
1354        tracing::info!("Verifying migrated active stake");
1355        assert_eq!(
1356            proxy_as_v2.activeStake().call().await?,
1357            init_active_stake.unwrap_or_default(),
1358            "migrated active stake does not match"
1359        );
1360
1361        tracing::info!(%v2_addr, "StakeTable successfully upgraded to");
1362    } else {
1363        anyhow::bail!("StakeTable upgrade failed: {:?}", receipt);
1364    }
1365
1366    Ok(receipt)
1367}
1368
1369/// Upgrade the fee contract from V1.x.x to V1.0.1
1370/// This is for fee contracts owned by an EOA
1371pub async fn upgrade_fee_v1(
1372    provider: impl Provider,
1373    contracts: &mut Contracts,
1374) -> Result<TransactionReceipt> {
1375    tracing::info!("Upgrading FeeContract to FeeContractV1.0.1 with EOA admin");
1376    let Some(fee_contract_proxy_addr) = contracts.address(Contract::FeeContractProxy) else {
1377        anyhow::bail!("FeeContractProxy not found, can't upgrade")
1378    };
1379
1380    let fee_contract_proxy = FeeContract::new(fee_contract_proxy_addr, &provider);
1381
1382    let owner = fee_contract_proxy.owner().call().await?;
1383    if is_contract(&provider, owner).await? {
1384        anyhow::bail!(
1385            "FeeContract owner ({:#x}) is not an EOA, can't upgrade",
1386            owner
1387        );
1388    }
1389
1390    let curr_version = fee_contract_proxy.getVersion().call().await?;
1391    if curr_version.majorVersion != 1 {
1392        anyhow::bail!(
1393            "Expected FeeContract V1 for upgrade, found V{}.{}.{}",
1394            curr_version.majorVersion,
1395            curr_version.minorVersion,
1396            curr_version.patchVersion
1397        );
1398    }
1399
1400    let cached_fee_contract_addr = contracts.address(Contract::FeeContract);
1401
1402    // For patch upgrades, we need to deploy a fresh implementation contract.
1403    // If FeeContract is already in the cache, the caller must unset it first
1404    // to make the redeployment requirement explicit.
1405    if let Some(cached_fee_contract_addr) = cached_fee_contract_addr {
1406        anyhow::bail!(
1407            "FeeContract implementation address is already set in cache ({:#x}). For patch \
1408             upgrades, the implementation must be redeployed. Please unset \
1409             ESPRESSO_FEE_CONTRACT_ADDRESS or remove it from the cache first.",
1410            cached_fee_contract_addr
1411        );
1412    }
1413
1414    // now deploy the new implementation contract
1415    let new_fee_contract_addr = contracts
1416        .deploy(
1417            Contract::FeeContract,
1418            FeeContract::deploy_builder(&provider),
1419        )
1420        .await?;
1421
1422    let receipt = fee_contract_proxy
1423        .upgradeToAndCall(new_fee_contract_addr, vec![].into())
1424        .send()
1425        .await?
1426        .get_receipt()
1427        .await
1428        .context("Failed to get upgrade transaction receipt")?;
1429
1430    if receipt.inner.is_success() {
1431        let new_version = fee_contract_proxy.getVersion().call().await?;
1432        if new_version != (1, 0, 1).into() {
1433            anyhow::bail!(
1434                "Upgrade transaction succeeded but version is incorrect: V{}.{}.{} (expected \
1435                 V1.0.1). Proxy: {fee_contract_proxy_addr:#x}, New impl: \
1436                 {new_fee_contract_addr:#x}",
1437                new_version.majorVersion,
1438                new_version.minorVersion,
1439                new_version.patchVersion
1440            );
1441        }
1442        tracing::info!(
1443            proxy = %fee_contract_proxy_addr,
1444            impl = %new_fee_contract_addr,
1445            "FeeContract successfully upgraded to v1.0.1"
1446        );
1447    } else {
1448        anyhow::bail!("FeeContract upgrade failed: {:?}", receipt);
1449    }
1450
1451    Ok(receipt)
1452}
1453
1454/// Common logic for any Ownable contract to transfer ownership
1455pub async fn transfer_ownership(
1456    provider: impl Provider,
1457    target_contract: Contract,
1458    target_address: Address,
1459    new_owner: Address,
1460) -> Result<TransactionReceipt> {
1461    // Use OwnableUpgradeable interface for all Ownable contracts
1462    // This is more generic and maintainable than matching on each contract type
1463    let ownable = OwnableUpgradeable::new(target_address, &provider);
1464
1465    // Verify the contract is actually Ownable by checking if we can read the owner
1466    let current_owner = ownable.owner().call().await.context(format!(
1467        "Contract at {target_address:#x} does not implement Ownable interface"
1468    ))?;
1469
1470    tracing::info!(%target_contract, %target_address, current_owner = %current_owner, new_owner = %new_owner, "Transferring ownership of {target_contract}");
1471
1472    let receipt = ownable
1473        .transferOwnership(new_owner)
1474        .send()
1475        .await?
1476        .get_receipt()
1477        .await?;
1478
1479    let tx_hash = receipt.transaction_hash;
1480    tracing::info!(%receipt.gas_used, %tx_hash, "ownership transferred");
1481    Ok(receipt)
1482}
1483
1484/// Grant DEFAULT_ADMIN_ROLE to a new admin for AccessControl-based contracts
1485/// This handles contracts like RewardClaim that use AccessControl instead of Ownable
1486pub async fn grant_admin_role(
1487    provider: impl Provider,
1488    target_contract: Contract,
1489    target_address: Address,
1490    new_admin: Address,
1491) -> Result<TransactionReceipt> {
1492    // Use AccessControlUpgradeable interface
1493    let access_control = AccessControlUpgradeable::new(target_address, &provider);
1494
1495    // Verify the contract is actually AccessControl by checking if we can read roles
1496    let admin_role = access_control
1497        .DEFAULT_ADMIN_ROLE()
1498        .call()
1499        .await
1500        .context(format!(
1501            "Contract at {target_address:#x} does not implement AccessControl interface"
1502        ))?;
1503
1504    // Check if new_admin already has the role (for logging purposes)
1505    let already_has_role = access_control.hasRole(admin_role, new_admin).call().await?;
1506
1507    tracing::info!(
1508        %target_contract,
1509        %target_address,
1510        new_admin = %new_admin,
1511        already_has_role = %already_has_role,
1512        "Granting DEFAULT_ADMIN_ROLE for {target_contract}"
1513    );
1514
1515    // For RewardClaim, grantRole handles the revoke of the previous admin internally
1516    let receipt = access_control
1517        .grantRole(admin_role, new_admin)
1518        .send()
1519        .await?
1520        .get_receipt()
1521        .await?;
1522
1523    let tx_hash = receipt.transaction_hash;
1524    tracing::info!(%receipt.gas_used, %tx_hash, "admin role granted");
1525    Ok(receipt)
1526}
1527
1528/// helper function to decide if the contract at given address `addr` is a proxy contract
1529pub async fn is_proxy_contract(provider: impl Provider, addr: Address) -> Result<bool> {
1530    // when the implementation address is not equal to zero, it's a proxy
1531    Ok(read_proxy_impl(provider, addr).await? != Address::default())
1532}
1533
1534pub async fn read_proxy_impl(provider: impl Provider, addr: Address) -> Result<Address> {
1535    // confirm that the proxy_address is a proxy
1536    // using the implementation slot, 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc, which is the keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1
1537    let impl_slot = U256::from_str_radix(
1538        "360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc",
1539        16,
1540    )?;
1541
1542    // Use retry_until_true to verify storage is readable (non-zero address)
1543    let is_readable = retry_until_true("Read proxy implementation", || async {
1544        match provider.get_storage_at(addr, impl_slot).await {
1545            Ok(_) => {
1546                // storage is readable so return true
1547                Ok(true)
1548            },
1549            Err(e) => {
1550                tracing::debug!("Storage read failed (will retry): {}", e);
1551                Ok(false)
1552            },
1553        }
1554    })
1555    .await?;
1556
1557    if !is_readable {
1558        anyhow::bail!(
1559            "Proxy implementation storage is not readable after retries at address {addr:#x}"
1560        );
1561    }
1562
1563    // Final read to get the actual address (we know it's readable now)
1564    let storage = provider.get_storage_at(addr, impl_slot).await?;
1565    let impl_addr = Address::from_slice(&storage.to_be_bytes_vec()[12..]);
1566
1567    Ok(impl_addr)
1568}
1569
1570pub async fn is_contract(provider: impl Provider, address: Address) -> Result<bool> {
1571    if address == Address::ZERO {
1572        return Ok(false);
1573    }
1574    Ok(!provider.get_code_at(address).await?.is_empty())
1575}
1576
1577pub async fn get_proxy_initialized_version(
1578    provider: impl Provider,
1579    proxy_addr: Address,
1580) -> Result<u8> {
1581    // From openzeppelin Initializable.sol, the initialized version slot is keccak256("openzeppelin.storage.Initializable");
1582    let slot: B256 = "0xf0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00"
1583        .parse()
1584        .context("Failed to parse OpenZeppelin Initializable slot")?;
1585    let value = provider.get_storage_at(proxy_addr, slot.into()).await?;
1586    let initialized = value.as_le_bytes()[0]; // `_initialized` is u8 stored in the last byte
1587    Ok(initialized)
1588}
1589
1590/// Deploy and initialize the Ops Timelock contract
1591///
1592/// Parameters:
1593/// - `min_delay`: The minimum delay for operations
1594/// - `proposers`: The list of addresses that can propose
1595/// - `executors`: The list of addresses that can execute
1596/// - `admin`: The address that can perform admin actions
1597pub async fn deploy_ops_timelock(
1598    provider: impl Provider,
1599    contracts: &mut Contracts,
1600    min_delay: U256,
1601    proposers: Vec<Address>,
1602    executors: Vec<Address>,
1603    admin: Address,
1604) -> Result<Address> {
1605    tracing::info!(
1606        "OpsTimelock will be deployed with the following parameters: min_delay: {:?}, proposers: \
1607         {:?}, executors: {:?}, admin: {:?}",
1608        min_delay,
1609        proposers,
1610        executors,
1611        admin
1612    );
1613    let timelock_addr = contracts
1614        .deploy(
1615            Contract::OpsTimelock,
1616            OpsTimelock::deploy_builder(
1617                &provider,
1618                min_delay,
1619                proposers.clone(),
1620                executors.clone(),
1621                admin,
1622            ),
1623        )
1624        .await?;
1625
1626    // Verify deployment
1627    let timelock = OpsTimelock::new(timelock_addr, &provider);
1628
1629    // Verify initialization parameters
1630    assert_eq!(timelock.getMinDelay().call().await?, min_delay);
1631    assert!(
1632        timelock
1633            .hasRole(timelock.PROPOSER_ROLE().call().await?, proposers[0])
1634            .call()
1635            .await?
1636    );
1637    assert!(
1638        timelock
1639            .hasRole(timelock.EXECUTOR_ROLE().call().await?, executors[0])
1640            .call()
1641            .await?
1642    );
1643
1644    // test that the admin is in the default admin role where DEFAULT_ADMIN_ROLE = 0x00
1645    let default_admin_role = U256::ZERO;
1646    assert!(
1647        timelock
1648            .hasRole(default_admin_role.into(), admin)
1649            .call()
1650            .await?
1651    );
1652
1653    Ok(timelock_addr)
1654}
1655
1656/// Deploy and initialize the Safe Exit Timelock contract
1657///
1658/// Parameters:
1659/// - `min_delay`: The minimum delay for operations
1660/// - `proposers`: The list of addresses that can propose
1661/// - `executors`: The list of addresses that can execute
1662/// - `admin`: The address that can perform admin actions
1663pub async fn deploy_safe_exit_timelock(
1664    provider: impl Provider,
1665    contracts: &mut Contracts,
1666    min_delay: U256,
1667    proposers: Vec<Address>,
1668    executors: Vec<Address>,
1669    admin: Address,
1670) -> Result<Address> {
1671    tracing::info!(
1672        "SafeExitTimelock will be deployed with the following parameters: min_delay: {:?}, \
1673         proposers: {:?}, executors: {:?}, admin: {:?}",
1674        min_delay,
1675        proposers,
1676        executors,
1677        admin
1678    );
1679    let timelock_addr = contracts
1680        .deploy(
1681            Contract::SafeExitTimelock,
1682            SafeExitTimelock::deploy_builder(
1683                &provider,
1684                min_delay,
1685                proposers.clone(),
1686                executors.clone(),
1687                admin,
1688            ),
1689        )
1690        .await?;
1691
1692    // Verify deployment
1693    let timelock = SafeExitTimelock::new(timelock_addr, &provider);
1694
1695    // Verify initialization parameters
1696    assert_eq!(timelock.getMinDelay().call().await?, min_delay);
1697    assert!(
1698        timelock
1699            .hasRole(timelock.PROPOSER_ROLE().call().await?, proposers[0])
1700            .call()
1701            .await?
1702    );
1703    assert!(
1704        timelock
1705            .hasRole(timelock.EXECUTOR_ROLE().call().await?, executors[0])
1706            .call()
1707            .await?
1708    );
1709
1710    // test that the admin is in the default admin role where DEFAULT_ADMIN_ROLE = 0x00
1711    let default_admin_role = U256::ZERO;
1712    assert!(
1713        timelock
1714            .hasRole(default_admin_role.into(), admin)
1715            .call()
1716            .await?
1717    );
1718
1719    Ok(timelock_addr)
1720}
1721
1722/// Encode a function call with the given signature and arguments
1723///
1724/// Parameters:
1725/// - `signature`: e.g. `"transfer(address,uint256)"`
1726/// - `args`: Solidity typed arguments as `Vec<&str>`
1727///
1728/// Returns:
1729/// - Full calldata: selector + encoded arguments
1730pub fn encode_function_call(signature: &str, args: Vec<String>) -> Result<Bytes> {
1731    let func = Function::parse(signature)?;
1732
1733    // Check if argument count matches the function signature
1734    if args.len() != func.inputs.len() {
1735        anyhow::bail!(
1736            "Mismatch between argument count ({}) and parameter count ({})",
1737            args.len(),
1738            func.inputs.len()
1739        );
1740    }
1741
1742    // Parse argument values using the function's parameter types directly
1743    let arg_values: Vec<DynSolValue> =
1744        func.inputs
1745            .iter()
1746            .enumerate()
1747            .map(|(i, param)| {
1748                let arg_str = &args[i];
1749                let dyn_type: DynSolType =
1750                    param.ty.to_string().parse().map_err(|e| {
1751                        anyhow!("Failed to parse parameter type '{}': {}", param.ty, e)
1752                    })?;
1753                dyn_type.coerce_str(arg_str).map_err(|e| {
1754                    anyhow!(
1755                        "Failed to coerce argument '{}' to type '{}': {}",
1756                        arg_str,
1757                        param.ty,
1758                        e
1759                    )
1760                })
1761            })
1762            .collect::<Result<Vec<_>>>()?;
1763
1764    let encoded_input = func.abi_encode_input(&arg_values)?;
1765    let data = Bytes::from(encoded_input);
1766    Ok(data)
1767}
1768
1769/// retry helper for checking state after transactions
1770/// Retries up to 5 times with exponential backoff (500ms, 1s, 2s, 4s, 8s)
1771/// Parameters:
1772/// - `check_name`: the name of the check
1773/// - `check_fn`: the function to check, must return `Result<bool>`
1774///
1775/// Returns:
1776/// - `Ok(true)` if the check passed, `Ok(false)`
1777pub async fn retry_until_true<F, Fut>(check_name: &str, mut check_fn: F) -> Result<bool>
1778where
1779    F: FnMut() -> Fut,
1780    Fut: std::future::Future<Output = Result<bool>>,
1781{
1782    for attempt in 0..MAX_RETRY_ATTEMPTS {
1783        match check_fn().await {
1784            Ok(true) => return Ok(true),
1785            Ok(false) | Err(_) if attempt < MAX_RETRY_ATTEMPTS - 1 => {
1786                let delay_ms = RETRY_INITIAL_DELAY_MS * (1 << attempt);
1787                tracing::warn!("{} not ready, retrying in {}ms...", check_name, delay_ms);
1788                tokio::time::sleep(Duration::from_millis(delay_ms)).await;
1789            },
1790            Ok(false) => {
1791                tracing::error!(
1792                    "{} not ready after {} attempts, returning false",
1793                    check_name,
1794                    MAX_RETRY_ATTEMPTS
1795                );
1796                return Ok(false);
1797            },
1798            Err(e) => {
1799                tracing::error!(
1800                    "{} not ready after {} attempts,  (treating as not ready): {e:#}",
1801                    check_name,
1802                    MAX_RETRY_ATTEMPTS
1803                );
1804                return Ok(false);
1805            },
1806        }
1807    }
1808    // should never reach here, but defensive fallback to return false
1809    Ok(false)
1810}
1811
1812#[cfg(test)]
1813mod tests {
1814    use std::sync::Arc;
1815
1816    use alloy::{
1817        node_bindings::{Anvil, AnvilInstance},
1818        primitives::utils::parse_ether,
1819        providers::{ProviderBuilder, ext::AnvilApi, layers::AnvilProvider},
1820        sol_types::SolValue,
1821    };
1822    use espresso_types::testing::TestValidator;
1823    use hotshot_contract_adapter::sol_types::{FeeContract, StakeTableV2};
1824
1825    use super::*;
1826    use crate::{
1827        Contracts,
1828        builder::DeployerArgsBuilder,
1829        impersonate_filler::ImpersonateFiller,
1830        proposals::{
1831            multisig::{
1832                LightClientV2UpgradeParams, MultisigOwnerCheck, StakeTableV2UpgradeParams,
1833                TransferOwnershipParams, transfer_ownership_from_multisig_to_timelock,
1834                upgrade_esp_token_v2_multisig_owner, upgrade_fee_contract_multisig_owner,
1835                upgrade_light_client_v2_multisig_owner, upgrade_stake_table_v2_multisig_owner,
1836            },
1837            timelock::{
1838                TimelockOperationParams, TimelockOperationPayload, TimelockOperationType,
1839                derive_timelock_address_from_contract_type, perform_timelock_operation,
1840            },
1841        },
1842    };
1843
1844    trait ProviderBuilderExt: Sized {
1845        fn connect_anvil_with_l1_client(
1846            self,
1847        ) -> Result<(AnvilInstance, HttpProviderWithWallet, L1Client)> {
1848            let anvil = Anvil::new().spawn();
1849            let wallet = anvil.wallet().unwrap();
1850            let provider = ProviderBuilder::new()
1851                .wallet(wallet)
1852                .connect_http(anvil.endpoint_url());
1853            let l1_client = L1Client::anvil(&anvil)?;
1854            Ok((anvil, provider, l1_client))
1855        }
1856    }
1857
1858    impl<L, F, N> ProviderBuilderExt for ProviderBuilder<L, F, N> {}
1859
1860    #[test_log::test(tokio::test)]
1861    async fn test_is_contract() -> Result<(), anyhow::Error> {
1862        let provider = ProviderBuilder::new().connect_anvil_with_wallet();
1863
1864        // test with zero address returns false
1865        let zero_address = Address::ZERO;
1866        assert!(!is_contract(&provider, zero_address).await?);
1867
1868        // Test with a non-contract address (e.g., a random address)
1869        let random_address = Address::random();
1870        assert!(!is_contract(&provider, random_address).await?);
1871
1872        // Deploy a contract and test with its address
1873        let fee_contract = FeeContract::deploy(&provider).await?;
1874        let contract_address = *fee_contract.address();
1875        assert!(is_contract(&provider, contract_address).await?);
1876
1877        Ok(())
1878    }
1879
1880    #[test_log::test(tokio::test)]
1881    async fn test_is_proxy_contract() -> Result<()> {
1882        let provider = ProviderBuilder::new().connect_anvil_with_wallet();
1883        let deployer = provider.get_accounts().await?[0];
1884
1885        let fee_contract = FeeContract::deploy(&provider).await?;
1886        let init_data = fee_contract.initialize(deployer).calldata().clone();
1887        let proxy =
1888            ERC1967Proxy::deploy(&provider, *fee_contract.address(), init_data.clone()).await?;
1889
1890        assert!(is_proxy_contract(&provider, *proxy.address()).await?);
1891        assert!(!is_proxy_contract(&provider, *fee_contract.address()).await?);
1892        Ok(())
1893    }
1894
1895    #[test_log::test(tokio::test)]
1896    async fn test_deploy_light_client() -> Result<()> {
1897        let provider = ProviderBuilder::new().connect_anvil_with_wallet();
1898        let mut contracts = Contracts::new();
1899
1900        // first test if LightClientMock can be deployed
1901        let mock_lc_addr = deploy_light_client_contract(&provider, &mut contracts, true).await?;
1902        let pv_addr = contracts.address(Contract::PlonkVerifier).unwrap();
1903
1904        // then deploy the actual LightClient
1905        let lc_addr = deploy_light_client_contract(&provider, &mut contracts, false).await?;
1906        assert_ne!(mock_lc_addr, lc_addr);
1907        // check that we didn't redeploy PlonkVerifier again, instead use existing ones
1908        assert_eq!(contracts.address(Contract::PlonkVerifier).unwrap(), pv_addr);
1909        Ok(())
1910    }
1911
1912    #[test_log::test(tokio::test)]
1913    async fn test_deploy_mock_light_client_proxy() -> Result<()> {
1914        let provider = ProviderBuilder::new().connect_anvil_with_wallet();
1915        let mut contracts = Contracts::new();
1916
1917        // prepare `initialize()` input
1918        let genesis_state = LightClientStateSol::dummy_genesis();
1919        let genesis_stake = StakeTableStateSol::dummy_genesis();
1920        let admin = provider.get_accounts().await?[0];
1921        let prover = admin;
1922
1923        let lc_proxy_addr = deploy_light_client_proxy(
1924            &provider,
1925            &mut contracts,
1926            true, // is_mock = true
1927            genesis_state.clone(),
1928            genesis_stake.clone(),
1929            admin,
1930            Some(prover),
1931        )
1932        .await?;
1933
1934        // check initialization is correct
1935        let lc = LightClientMock::new(lc_proxy_addr, &provider);
1936        let finalized_state: LightClientStateSol = lc.finalizedState().call().await?.into();
1937        assert_eq!(
1938            genesis_state.abi_encode_params(),
1939            finalized_state.abi_encode_params()
1940        );
1941        // mock set the state
1942        let new_state = LightClientStateSol {
1943            viewNum: 10,
1944            blockHeight: 10,
1945            blockCommRoot: U256::from(42),
1946        };
1947        lc.setFinalizedState(new_state.clone().into())
1948            .send()
1949            .await?
1950            .watch()
1951            .await?;
1952        let finalized_state: LightClientStateSol = lc.finalizedState().call().await?.into();
1953        assert_eq!(
1954            new_state.abi_encode_params(),
1955            finalized_state.abi_encode_params()
1956        );
1957
1958        Ok(())
1959    }
1960
1961    #[test_log::test(tokio::test)]
1962    async fn test_deploy_light_client_proxy() -> Result<()> {
1963        let provider = ProviderBuilder::new().connect_anvil_with_wallet();
1964        let mut contracts = Contracts::new();
1965
1966        // prepare `initialize()` input
1967        let genesis_state = LightClientStateSol::dummy_genesis();
1968        let genesis_stake = StakeTableStateSol::dummy_genesis();
1969        let admin = provider.get_accounts().await?[0];
1970        let prover = Address::random();
1971
1972        let lc_proxy_addr = deploy_light_client_proxy(
1973            &provider,
1974            &mut contracts,
1975            false,
1976            genesis_state.clone(),
1977            genesis_stake.clone(),
1978            admin,
1979            Some(prover),
1980        )
1981        .await?;
1982
1983        // check initialization is correct
1984        let lc = LightClient::new(lc_proxy_addr, &provider);
1985        let finalized_state = lc.finalizedState().call().await?;
1986        assert_eq!(finalized_state.viewNum, genesis_state.viewNum);
1987        assert_eq!(finalized_state.blockHeight, genesis_state.blockHeight);
1988        assert_eq!(&finalized_state.blockCommRoot, &genesis_state.blockCommRoot);
1989
1990        let fetched_stake = lc.genesisStakeTableState().call().await?;
1991        assert_eq!(fetched_stake.blsKeyComm, genesis_stake.blsKeyComm);
1992        assert_eq!(fetched_stake.schnorrKeyComm, genesis_stake.schnorrKeyComm);
1993        assert_eq!(fetched_stake.amountComm, genesis_stake.amountComm);
1994        assert_eq!(fetched_stake.threshold, genesis_stake.threshold);
1995
1996        let fetched_prover = lc.permissionedProver().call().await?;
1997        assert_eq!(fetched_prover, prover);
1998
1999        // test transfer ownership to multisig
2000        let multisig = Address::random();
2001        let _receipt = transfer_ownership(
2002            &provider,
2003            Contract::LightClientProxy,
2004            lc_proxy_addr,
2005            multisig,
2006        )
2007        .await?;
2008        assert_eq!(lc.owner().call().await?, multisig);
2009
2010        Ok(())
2011    }
2012
2013    #[test_log::test(tokio::test)]
2014    async fn test_deploy_fee_contract_proxy() -> Result<()> {
2015        let provider = ProviderBuilder::new().connect_anvil_with_wallet();
2016        let mut contracts = Contracts::new();
2017        let admin = provider.get_accounts().await?[0];
2018        let alice = Address::random();
2019
2020        let fee_proxy_addr = deploy_fee_contract_proxy(&provider, &mut contracts, alice).await?;
2021
2022        // check initialization is correct
2023        let fee = FeeContract::new(fee_proxy_addr, &provider);
2024        let fetched_owner = fee.owner().call().await?;
2025        assert_eq!(fetched_owner, alice);
2026
2027        // redeploy new fee with admin being the owner
2028        contracts = Contracts::new();
2029        let fee_proxy_addr = deploy_fee_contract_proxy(&provider, &mut contracts, admin).await?;
2030        let fee = FeeContract::new(fee_proxy_addr, &provider);
2031
2032        // test transfer ownership to multisig
2033        let multisig = Address::random();
2034        let _receipt = transfer_ownership(
2035            &provider,
2036            Contract::FeeContractProxy,
2037            fee_proxy_addr,
2038            multisig,
2039        )
2040        .await?;
2041        assert_eq!(fee.owner().call().await?, multisig);
2042
2043        Ok(())
2044    }
2045
2046    async fn test_upgrade_light_client_to_v2_helper(is_mock: bool) -> Result<()> {
2047        let provider = ProviderBuilder::new().connect_anvil_with_wallet();
2048        let mut contracts = Contracts::new();
2049        let blocks_per_epoch = 10; // for test
2050        let epoch_start_block = 22;
2051
2052        // prepare `initialize()` input
2053        let genesis_state = LightClientStateSol::dummy_genesis();
2054        let genesis_stake = StakeTableStateSol::dummy_genesis();
2055        let admin = provider.get_accounts().await?[0];
2056        let prover = Address::random();
2057
2058        // deploy proxy and V1
2059        let lc_proxy_addr = deploy_light_client_proxy(
2060            &provider,
2061            &mut contracts,
2062            false,
2063            genesis_state.clone(),
2064            genesis_stake.clone(),
2065            admin,
2066            Some(prover),
2067        )
2068        .await?;
2069
2070        let state_history_retention_period = LightClient::new(lc_proxy_addr, &provider)
2071            .stateHistoryRetentionPeriod()
2072            .call()
2073            .await?;
2074
2075        // then upgrade to v2
2076        upgrade_light_client_v2(
2077            &provider,
2078            &mut contracts,
2079            is_mock,
2080            blocks_per_epoch,
2081            epoch_start_block,
2082        )
2083        .await?;
2084
2085        // test correct v1 state persistence
2086        let lc = LightClientV2::new(lc_proxy_addr, &provider);
2087        let finalized_state: LightClientStateSol = lc.finalizedState().call().await?.into();
2088        assert_eq!(
2089            genesis_state.abi_encode_params(),
2090            finalized_state.abi_encode_params()
2091        );
2092        // test new v2 state
2093        let next_stake: StakeTableStateSol = lc.votingStakeTableState().call().await?.into();
2094        assert_eq!(
2095            genesis_stake.abi_encode_params(),
2096            next_stake.abi_encode_params()
2097        );
2098        assert_eq!(lc.getVersion().call().await?.majorVersion, 2);
2099        assert_eq!(lc.blocksPerEpoch().call().await?, blocks_per_epoch);
2100        assert_eq!(lc.epochStartBlock().call().await?, epoch_start_block);
2101        assert_eq!(
2102            lc.stateHistoryRetentionPeriod().call().await?,
2103            state_history_retention_period
2104        );
2105
2106        // test mock-specific functions
2107        if is_mock {
2108            // recast to mock
2109            let lc_mock = LightClientV2Mock::new(lc_proxy_addr, &provider);
2110            let new_blocks_per_epoch = blocks_per_epoch + 10;
2111            lc_mock
2112                .setBlocksPerEpoch(new_blocks_per_epoch)
2113                .send()
2114                .await?
2115                .watch()
2116                .await?;
2117            assert_eq!(new_blocks_per_epoch, lc_mock.blocksPerEpoch().call().await?);
2118        }
2119        Ok(())
2120    }
2121
2122    #[test_log::test(tokio::test)]
2123    async fn test_upgrade_light_client_to_v2() -> Result<()> {
2124        test_upgrade_light_client_to_v2_helper(false).await
2125    }
2126
2127    #[test_log::test(tokio::test)]
2128    async fn test_upgrade_mock_light_client_v2() -> Result<()> {
2129        test_upgrade_light_client_to_v2_helper(true).await
2130    }
2131
2132    #[test_log::test(tokio::test)]
2133    async fn test_fetch_stake_table_for_stake_table_storage_migration() -> Result<()> {
2134        let (_anvil, provider, l1_client) =
2135            ProviderBuilder::new().connect_anvil_with_l1_client()?;
2136        let mut contracts = Contracts::new();
2137        let owner = provider.get_accounts().await?[0];
2138
2139        let token_addr = deploy_token_proxy(
2140            &provider,
2141            &mut contracts,
2142            owner,
2143            owner,
2144            U256::from(10_000_000u64),
2145            "Test Token",
2146            "TEST",
2147        )
2148        .await?;
2149        let lc_addr = deploy_light_client_contract(&provider, &mut contracts, false).await?;
2150        let exit_escrow_period = U256::from(DEFAULT_EXIT_ESCROW_PERIOD_SECONDS);
2151
2152        let stake_table_proxy_addr = deploy_stake_table_proxy(
2153            &provider,
2154            &mut contracts,
2155            token_addr,
2156            lc_addr,
2157            exit_escrow_period,
2158            owner,
2159        )
2160        .await?;
2161
2162        // Use V2 interface even for V1 contract (V2 ABI is a superset of V1)
2163        let stake_table = StakeTableV2::new(stake_table_proxy_addr, &provider);
2164
2165        let accounts = provider.get_accounts().await?;
2166        let validators = [
2167            TestValidator::random_update_keys(accounts[4], 1234),
2168            TestValidator::random_update_keys(accounts[5], 4567),
2169        ];
2170
2171        for validator in validators.iter() {
2172            let receipt = stake_table
2173                .registerValidator(
2174                    validator.bls_vk,
2175                    validator.schnorr_vk,
2176                    validator.bls_sig.into(),
2177                    validator.commission,
2178                )
2179                .from(validator.account)
2180                .send()
2181                .await?
2182                .get_receipt()
2183                .await?;
2184            assert!(receipt.status());
2185        }
2186
2187        let delegators = [
2188            (
2189                accounts[0],
2190                validators[0].account,
2191                parse_ether("10").unwrap(),
2192            ),
2193            (
2194                accounts[1],
2195                validators[1].account,
2196                parse_ether("10").unwrap(),
2197            ),
2198            (
2199                accounts[2],
2200                validators[0].account,
2201                parse_ether("100").unwrap(),
2202            ),
2203        ];
2204
2205        let token = EspToken::new(token_addr, &provider);
2206
2207        for (delegator, validator, amount) in delegators.iter() {
2208            let receipt = token
2209                .transfer(*delegator, *amount)
2210                .from(Address::from(*owner))
2211                .send()
2212                .await?
2213                .get_receipt()
2214                .await?;
2215            assert!(receipt.status());
2216
2217            let receipt = token
2218                .approve(stake_table_proxy_addr, *amount)
2219                .from(*delegator)
2220                .send()
2221                .await?
2222                .get_receipt()
2223                .await?;
2224            assert!(receipt.status());
2225
2226            let receipt = stake_table
2227                .delegate(*validator, *amount)
2228                .from(*delegator)
2229                .send()
2230                .await?
2231                .get_receipt()
2232                .await?;
2233            assert!(receipt.status());
2234        }
2235
2236        let receipt = stake_table
2237            .deregisterValidator()
2238            .from(validators[1].account)
2239            .send()
2240            .await?
2241            .get_receipt()
2242            .await?;
2243        assert!(receipt.status());
2244
2245        let undelegations = [
2246            (
2247                accounts[0],
2248                validators[0].account,
2249                parse_ether("5").unwrap(),
2250            ),
2251            (
2252                accounts[2],
2253                validators[0].account,
2254                parse_ether("3").unwrap(),
2255            ),
2256        ];
2257
2258        for (delegator_addr, validator_addr, amount) in undelegations.iter() {
2259            let receipt = stake_table
2260                .undelegate(*validator_addr, *amount)
2261                .from(*delegator_addr)
2262                .send()
2263                .await?
2264                .get_receipt()
2265                .await?;
2266            assert!(receipt.status());
2267        }
2268
2269        let (fetched_active_stake, fetched_commissions) =
2270            fetch_stake_table_for_stake_table_storage_migration(
2271                l1_client.clone(),
2272                stake_table_proxy_addr,
2273            )
2274            .await?;
2275
2276        let expected_active_stake: U256 = delegators
2277            .iter()
2278            .map(|(_, _, amount)| *amount)
2279            .sum::<U256>()
2280            - delegators
2281                .iter()
2282                .filter(|(_, validator, _)| *validator == validators[1].account)
2283                .map(|(_, _, amount)| *amount)
2284                .sum::<U256>()
2285            - undelegations
2286                .iter()
2287                .map(|(_, _, amount)| *amount)
2288                .sum::<U256>();
2289
2290        assert_eq!(fetched_active_stake, expected_active_stake);
2291        assert!(fetched_active_stake <= token.balanceOf(stake_table_proxy_addr).call().await?);
2292
2293        assert_eq!(fetched_commissions.len(), 1);
2294        assert_eq!(fetched_commissions[0].validator, validators[0].account);
2295        assert_eq!(fetched_commissions[0].commission, validators[0].commission);
2296
2297        // Migration only applies to V1 contract
2298        let stake_table_v2 = StakeTableV2::deploy(&provider).await?;
2299        let err = fetch_stake_table_for_stake_table_storage_migration(
2300            l1_client.clone(),
2301            *stake_table_v2.address(),
2302        )
2303        .await
2304        .unwrap_err();
2305        assert!(err.to_string().contains("Expected StakeTable V1"));
2306
2307        Ok(())
2308    }
2309
2310    /// Check that we can fetch stake table data on sepolia for the migration.
2311    ///
2312    /// Assumes an infura RPC is used, otherwise fetching may hit other rate limits.
2313    ///
2314    /// env RPC_URL=... cargo test -p espresso-contract-deployer -- --ignored test_fetch_stake_table_sepolia
2315    #[ignore]
2316    #[test_log::test(tokio::test)]
2317    async fn test_fetch_stake_table_sepolia() -> Result<()> {
2318        let rpc_url: Url = std::env::var("RPC_URL")
2319            .expect("RPC_URL environment variable not set")
2320            .parse()?;
2321        let provider = ProviderBuilder::new().connect_http(rpc_url.clone());
2322        let l1_client = L1Client::new(vec![rpc_url])?;
2323
2324        // Decaf / sepolia stake table address
2325        let stake_table_address: Address = "0x40304FbE94D5E7D1492Dd90c53a2D63E8506a037".parse()?;
2326        let (fetched_active_stake, fetched_commissions) =
2327            fetch_stake_table_for_stake_table_storage_migration(l1_client, stake_table_address)
2328                .await?;
2329
2330        assert!(!fetched_commissions.is_empty());
2331        assert!(!fetched_active_stake.is_zero());
2332
2333        println!(
2334            "Fetched {} commissions from Sepolia StakeTable",
2335            fetched_commissions.len()
2336        );
2337        for commission in &fetched_commissions {
2338            println!(
2339                "  Validator: {}, Commission: {}",
2340                commission.validator, commission.commission
2341            );
2342        }
2343        println!("Fetched active stake: {}", fetched_active_stake);
2344
2345        let pauser = Address::random();
2346        let admin = Address::random();
2347        let init_v2_calldata = StakeTableV2::new(stake_table_address, &provider)
2348            .initializeV2(pauser, admin, fetched_active_stake, fetched_commissions)
2349            .calldata()
2350            .clone();
2351        println!("Calldata size: {} bytes", init_v2_calldata.len());
2352
2353        // The max calldata size is 128 kB per tx, but at the time of writing we
2354        // only need about 7 kB therefore applying a stricter limit of 32 kB
2355        assert!(init_v2_calldata.len() < 32 * 1024);
2356        Ok(())
2357    }
2358
2359    impl Contracts {
2360        fn insert(&mut self, name: Contract, address: Address) -> Option<Address> {
2361            self.addresses.insert(name, address)
2362        }
2363    }
2364
2365    /// Fork test to test if we can upgrade the decaf stake table from V1 to V2
2366    /// This test forks Sepolia (where decaf runs) using anvil, fetches existing commissions,
2367    /// impersonates the proxy owner, and performs the upgrade.
2368    ///
2369    /// Assumes an infura RPC is used, otherwise fetching commissions may hit other rate limits.
2370    ///
2371    /// env RPC_URL=... cargo test -p espresso-contract-deployer -- --ignored test_upgrade_decaf_stake_table_fork
2372    #[ignore]
2373    #[test_log::test(tokio::test)]
2374    async fn test_upgrade_decaf_stake_table_fork() -> Result<()> {
2375        let rpc_url = std::env::var("RPC_URL").expect("RPC_URL environment variable not set");
2376
2377        // Decaf / sepolia stake table address
2378        let stake_table_address: Address = "0x40304FbE94D5E7D1492Dd90c53a2D63E8506a037".parse()?;
2379        let anvil = Anvil::new()
2380            .fork(rpc_url)
2381            .arg("--retries")
2382            .arg("20")
2383            .spawn();
2384
2385        let provider = ProviderBuilder::new().connect_http(anvil.endpoint().parse()?);
2386        let proxy = StakeTable::new(stake_table_address, &provider);
2387        let proxy_owner = proxy.owner().call().await?;
2388        tracing::info!("Proxy owner address: {proxy_owner:#x}");
2389
2390        // Enable impersonation for the proxy owner
2391        let provider = ProviderBuilder::new()
2392            .filler(ImpersonateFiller::new(proxy_owner))
2393            .connect_http(anvil.endpoint().parse()?);
2394        let l1_client = L1Client::anvil(&anvil)?;
2395        let anvil_arc = Arc::new(anvil);
2396        let anvil_provider = AnvilProvider::new(provider.clone(), anvil_arc);
2397        anvil_provider.anvil_auto_impersonate_account(true).await?;
2398        anvil_provider
2399            .anvil_set_balance(proxy_owner, parse_ether("100")?)
2400            .await?;
2401
2402        // We need a Contracts instance with proxy deployed
2403        let mut contracts = Contracts::new();
2404        contracts.insert(Contract::StakeTableProxy, stake_table_address);
2405        let pauser = Address::random();
2406        let admin = proxy_owner;
2407
2408        upgrade_stake_table_v2(&provider, l1_client, &mut contracts, pauser, admin).await?;
2409        Ok(())
2410    }
2411
2412    async fn test_upgrade_light_client_to_v3_helper(options: UpgradeTestOptions) -> Result<()> {
2413        let provider = ProviderBuilder::new().connect_anvil_with_wallet();
2414        let mut contracts = Contracts::new();
2415        let blocks_per_epoch = 10; // for test
2416        let epoch_start_block = 22;
2417
2418        // prepare `initialize()` input
2419        let genesis_state = LightClientStateSol::dummy_genesis();
2420        let genesis_stake = StakeTableStateSol::dummy_genesis();
2421        let admin = provider.get_accounts().await?[0];
2422        let prover = Address::random();
2423
2424        // deploy proxy and V1
2425        let lc_proxy_addr = deploy_light_client_proxy(
2426            &provider,
2427            &mut contracts,
2428            false,
2429            genesis_state.clone(),
2430            genesis_stake.clone(),
2431            admin,
2432            Some(prover),
2433        )
2434        .await?;
2435
2436        // first upgrade to v2
2437        upgrade_light_client_v2(
2438            &provider,
2439            &mut contracts,
2440            options.is_mock,
2441            blocks_per_epoch,
2442            epoch_start_block,
2443        )
2444        .await?;
2445
2446        // then upgrade to v3
2447        upgrade_light_client_v3(&provider, &mut contracts, options.is_mock).await?;
2448
2449        // test correct v1 and v2 state persistence
2450        let lc = LightClientV3::new(lc_proxy_addr, &provider);
2451        let finalized_state: LightClientStateSol = lc.finalizedState().call().await?.into();
2452        assert_eq!(
2453            genesis_state.abi_encode_params(),
2454            finalized_state.abi_encode_params()
2455        );
2456
2457        // test v2 state persistence
2458        let next_stake: StakeTableStateSol = lc.votingStakeTableState().call().await?.into();
2459        assert_eq!(
2460            genesis_stake.abi_encode_params(),
2461            next_stake.abi_encode_params()
2462        );
2463
2464        // test v3 specific properties
2465        assert_eq!(lc.getVersion().call().await?.majorVersion, 3);
2466
2467        // V3 inherits blocks_per_epoch and epoch_start_block from V2
2468        let lc_as_v2 = LightClientV2::new(lc_proxy_addr, &provider);
2469        assert_eq!(lc_as_v2.blocksPerEpoch().call().await?, blocks_per_epoch);
2470        assert_eq!(lc_as_v2.epochStartBlock().call().await?, epoch_start_block);
2471
2472        // test mock-specific functions
2473        if options.is_mock {
2474            // recast to mock
2475            let lc_mock = LightClientV3Mock::new(lc_proxy_addr, &provider);
2476            // Test that mock-specific functions work
2477            let new_blocks_per_epoch = blocks_per_epoch + 10;
2478            lc_mock
2479                .setBlocksPerEpoch(new_blocks_per_epoch)
2480                .send()
2481                .await?
2482                .watch()
2483                .await?;
2484            assert_eq!(new_blocks_per_epoch, lc_mock.blocksPerEpoch().call().await?);
2485        }
2486        Ok(())
2487    }
2488
2489    #[test_log::test(tokio::test)]
2490    async fn test_upgrade_light_client_to_v3() -> Result<()> {
2491        test_upgrade_light_client_to_v3_helper(UpgradeTestOptions {
2492            is_mock: false,
2493            upgrade_count: UpgradeCount::Once,
2494        })
2495        .await
2496    }
2497
2498    #[test_log::test(tokio::test)]
2499    async fn test_upgrade_mock_light_client_v3() -> Result<()> {
2500        test_upgrade_light_client_to_v3_helper(UpgradeTestOptions {
2501            is_mock: true,
2502            upgrade_count: UpgradeCount::Once,
2503        })
2504        .await
2505    }
2506
2507    #[derive(Debug, Clone, Copy)]
2508    pub enum UpgradeCount {
2509        Once,
2510        Twice,
2511    }
2512
2513    #[derive(Debug, Clone, Copy)]
2514    pub struct UpgradeTestOptions {
2515        pub is_mock: bool,
2516        pub upgrade_count: UpgradeCount,
2517    }
2518    // This test is used to test the upgrade of the LightClientProxy via the multisig wallet
2519    // It only tests the upgrade proposal via the typescript script and thus requires the upgrade proposal to be sent to a real network
2520    // However, the contracts are deployed on anvil, so the test will pass even if the upgrade proposal is not executed
2521    // The test assumes that there is a file .env.deployer.rs.test in the root directory:
2522    // Ensure that the private key has proposal rights on the Safe Multisig Wallet and the SDK supports the network
2523    async fn test_upgrade_light_client_to_v2_multisig_owner_helper(
2524        options: UpgradeTestOptions,
2525    ) -> Result<()> {
2526        let multisig_admin = Address::random();
2527        let mnemonic = "test test test test test test test test test test test junk".to_string();
2528        let account_index = 0u32;
2529        let anvil = Anvil::default().spawn();
2530
2531        let mut contracts = Contracts::new();
2532        let blocks_per_epoch = 10; // for test
2533        let epoch_start_block = 22;
2534        let admin_signer = MnemonicBuilder::<English>::default()
2535            .phrase(mnemonic)
2536            .index(account_index)
2537            .expect("wrong mnemonic or index")
2538            .build()?;
2539        let admin = admin_signer.address();
2540        let provider = ProviderBuilder::new()
2541            .wallet(admin_signer)
2542            .connect(&anvil.endpoint())
2543            .await?;
2544
2545        // prepare `initialize()` input
2546        let genesis_state = LightClientStateSol::dummy_genesis();
2547        let genesis_stake = StakeTableStateSol::dummy_genesis();
2548
2549        let prover = Address::random();
2550
2551        // deploy proxy and V1
2552        let lc_proxy_addr = deploy_light_client_proxy(
2553            &provider,
2554            &mut contracts,
2555            false,
2556            genesis_state.clone(),
2557            genesis_stake.clone(),
2558            admin,
2559            Some(prover),
2560        )
2561        .await?;
2562        if matches!(options.upgrade_count, UpgradeCount::Twice) {
2563            // upgrade to v2
2564            upgrade_light_client_v2(
2565                &provider,
2566                &mut contracts,
2567                options.is_mock,
2568                blocks_per_epoch,
2569                epoch_start_block,
2570            )
2571            .await?;
2572        }
2573
2574        // transfer ownership to multisig
2575        let _receipt = transfer_ownership(
2576            &provider,
2577            Contract::LightClientProxy,
2578            lc_proxy_addr,
2579            multisig_admin,
2580        )
2581        .await?;
2582        let lc = LightClient::new(lc_proxy_addr, &provider);
2583        assert_eq!(lc.owner().call().await?, multisig_admin);
2584
2585        // then encode upgrade calldata for the multisig wallet
2586        let calldata = upgrade_light_client_v2_multisig_owner(
2587            &provider,
2588            &mut contracts,
2589            LightClientV2UpgradeParams {
2590                blocks_per_epoch,
2591                epoch_start_block,
2592            },
2593            options.is_mock,
2594            MultisigOwnerCheck::Skip,
2595        )
2596        .await?;
2597        tracing::info!(
2598            "Encoded calldata for LightClientProxy upgrade: to={:#x}, data={}",
2599            calldata.to,
2600            calldata.data
2601        );
2602        // Calldata target is the proxy
2603        assert_eq!(calldata.to, lc_proxy_addr);
2604        // Calldata is non-empty (encodes upgradeToAndCall)
2605        assert!(!calldata.data.is_empty());
2606
2607        Ok(())
2608    }
2609
2610    #[test_log::test(tokio::test)]
2611    async fn test_upgrade_light_client_to_v2_multisig_owner() -> Result<()> {
2612        test_upgrade_light_client_to_v2_multisig_owner_helper(UpgradeTestOptions {
2613            is_mock: false,
2614            upgrade_count: UpgradeCount::Once,
2615        })
2616        .await
2617    }
2618
2619    // We expect no init data for the second upgrade because the proxy was already initialized
2620    #[test_log::test(tokio::test)]
2621    async fn test_upgrade_light_client_to_v2_twice_multisig_owner() -> Result<()> {
2622        test_upgrade_light_client_to_v2_multisig_owner_helper(UpgradeTestOptions {
2623            is_mock: false,
2624            upgrade_count: UpgradeCount::Twice,
2625        })
2626        .await
2627    }
2628
2629    #[test_log::test(tokio::test)]
2630    async fn test_deploy_token_proxy() -> Result<()> {
2631        let provider = ProviderBuilder::new().connect_anvil_with_wallet();
2632        let mut contracts = Contracts::new();
2633
2634        let init_recipient = provider.get_accounts().await?[0];
2635        let rand_owner = Address::random();
2636        let initial_supply = U256::from(3590000000u64);
2637        let name = "Espresso";
2638        let symbol = "ESP";
2639
2640        let addr = deploy_token_proxy(
2641            &provider,
2642            &mut contracts,
2643            rand_owner,
2644            init_recipient,
2645            initial_supply,
2646            name,
2647            symbol,
2648        )
2649        .await?;
2650        let token = EspToken::new(addr, &provider);
2651
2652        assert_eq!(token.owner().call().await?, rand_owner);
2653        let total_supply = token.totalSupply().call().await?;
2654        assert_eq!(
2655            total_supply,
2656            parse_ether(&initial_supply.to_string()).unwrap()
2657        );
2658        assert_eq!(token.balanceOf(init_recipient).call().await?, total_supply);
2659        assert_eq!(token.name().call().await?, name);
2660        assert_eq!(token.symbol().call().await?, symbol);
2661
2662        Ok(())
2663    }
2664
2665    #[test_log::test(tokio::test)]
2666    async fn test_deploy_stake_table_proxy() -> Result<()> {
2667        let provider = ProviderBuilder::new().connect_anvil_with_wallet();
2668        let mut contracts = Contracts::new();
2669
2670        // deploy token
2671        let init_recipient = provider.get_accounts().await?[0];
2672        let token_owner = Address::random();
2673        let token_name = "Espresso";
2674        let token_symbol = "ESP";
2675        let initial_supply = U256::from(3590000000u64);
2676        let token_addr = deploy_token_proxy(
2677            &provider,
2678            &mut contracts,
2679            token_owner,
2680            init_recipient,
2681            initial_supply,
2682            token_name,
2683            token_symbol,
2684        )
2685        .await?;
2686
2687        // deploy light client proxy
2688        let lc_proxy_addr = deploy_light_client_proxy(
2689            &provider,
2690            &mut contracts,
2691            false,
2692            LightClientStateSol::dummy_genesis(),
2693            StakeTableStateSol::dummy_genesis(),
2694            init_recipient,
2695            Some(init_recipient),
2696        )
2697        .await?;
2698
2699        // deploy stake table
2700        let exit_escrow_period = U256::from(DEFAULT_EXIT_ESCROW_PERIOD_SECONDS);
2701        let owner = init_recipient;
2702        let stake_table_addr = deploy_stake_table_proxy(
2703            &provider,
2704            &mut contracts,
2705            token_addr,
2706            lc_proxy_addr,
2707            exit_escrow_period,
2708            owner,
2709        )
2710        .await?;
2711        let stake_table = StakeTable::new(stake_table_addr, &provider);
2712
2713        assert_eq!(
2714            stake_table.exitEscrowPeriod().call().await?,
2715            exit_escrow_period
2716        );
2717        assert_eq!(stake_table.owner().call().await?, owner);
2718        assert_eq!(stake_table.token().call().await?, token_addr);
2719        assert_eq!(stake_table.lightClient().call().await?, lc_proxy_addr);
2720        Ok(())
2721    }
2722
2723    #[test_log::test(tokio::test)]
2724    async fn test_upgrade_stake_table_v2() -> Result<()> {
2725        let (_anvil, provider, l1_client) =
2726            ProviderBuilder::new().connect_anvil_with_l1_client()?;
2727        let mut contracts = Contracts::new();
2728
2729        // deploy token
2730        let init_recipient = provider.get_accounts().await?[0];
2731        let token_owner = Address::random();
2732        let token_name = "Espresso";
2733        let token_symbol = "ESP";
2734        let initial_supply = U256::from(3590000000u64);
2735        let token_addr = deploy_token_proxy(
2736            &provider,
2737            &mut contracts,
2738            token_owner,
2739            init_recipient,
2740            initial_supply,
2741            token_name,
2742            token_symbol,
2743        )
2744        .await?;
2745
2746        // deploy light client proxy
2747        let lc_proxy_addr = deploy_light_client_proxy(
2748            &provider,
2749            &mut contracts,
2750            false,
2751            LightClientStateSol::dummy_genesis(),
2752            StakeTableStateSol::dummy_genesis(),
2753            init_recipient,
2754            Some(init_recipient),
2755        )
2756        .await?;
2757
2758        // deploy stake table
2759        let exit_escrow_period = U256::from(DEFAULT_EXIT_ESCROW_PERIOD_SECONDS);
2760        let owner = init_recipient;
2761        let stake_table_addr = deploy_stake_table_proxy(
2762            &provider,
2763            &mut contracts,
2764            token_addr,
2765            lc_proxy_addr,
2766            exit_escrow_period,
2767            owner,
2768        )
2769        .await?;
2770        let stake_table_v1 = StakeTable::new(stake_table_addr, &provider);
2771        assert_eq!(stake_table_v1.getVersion().call().await?, (1, 0, 0).into());
2772
2773        // snapshot for later patch upgrade, the upgrade will skip the v2
2774        // deployment if contracts already contains a v2
2775        let mut contracts_v1 = contracts.clone();
2776
2777        // upgrade to v2
2778        let pauser = Address::random();
2779        upgrade_stake_table_v2(&provider, l1_client.clone(), &mut contracts, pauser, owner).await?;
2780
2781        let stake_table_v2 = StakeTableV2::new(stake_table_addr, &provider);
2782
2783        assert_eq!(stake_table_v2.getVersion().call().await?, (2, 0, 0).into());
2784        assert_eq!(stake_table_v2.owner().call().await?, owner);
2785        assert_eq!(stake_table_v2.token().call().await?, token_addr);
2786        assert_eq!(stake_table_v2.lightClient().call().await?, lc_proxy_addr);
2787
2788        // get pauser role
2789        let pauser_role = stake_table_v2.PAUSER_ROLE().call().await?;
2790        assert!(
2791            stake_table_v2.hasRole(pauser_role, pauser).call().await?,
2792            "pauser should have PAUSER_ROLE"
2793        );
2794
2795        // get admin role
2796        let admin_role = stake_table_v2.DEFAULT_ADMIN_ROLE().call().await?;
2797        assert!(stake_table_v2.hasRole(admin_role, owner).call().await?,);
2798
2799        // ensure we can upgrade (again) to a V2 patch version
2800        let current_impl = read_proxy_impl(&provider, stake_table_addr).await?;
2801        upgrade_stake_table_v2(&provider, l1_client, &mut contracts_v1, pauser, owner).await?;
2802        assert_ne!(
2803            read_proxy_impl(&provider, stake_table_addr).await?,
2804            current_impl
2805        );
2806
2807        Ok(())
2808    }
2809
2810    // This test is used to test the upgrade of the StakeTableProxy via the multisig wallet
2811    // It only tests the upgrade proposal via the typescript script and thus requires the upgrade proposal to be sent to a real network
2812    // However, the contracts are deployed on anvil, so the test will pass even if the upgrade proposal is not executed
2813    // The test assumes that there is a file .env.deployer.rs.test in the root directory with the following variables:
2814    // RPC_URL=
2815    // SAFE_MULTISIG_ADDRESS=0x0000000000000000000000000000000000000000
2816    // SAFE_ORCHESTRATOR_PRIVATE_KEY=0x0000000000000000000000000000000000000000000000000000000000000000
2817    // Ensure that the private key has proposal rights on the Safe Multisig Wallet and the SDK supports the network
2818    async fn test_upgrade_stake_table_to_v2_multisig_owner_helper() -> Result<()> {
2819        let multisig_admin = Address::random();
2820        let (_anvil, provider, l1_client) =
2821            ProviderBuilder::new().connect_anvil_with_l1_client()?;
2822        let mut contracts = Contracts::new();
2823        let init_recipient = provider.get_accounts().await?[0];
2824        let token_owner = Address::random();
2825        let initial_supply = U256::from(3590000000u64);
2826
2827        // deploy proxy and V1
2828        let token_addr = deploy_token_proxy(
2829            &provider,
2830            &mut contracts,
2831            token_owner,
2832            init_recipient,
2833            initial_supply,
2834            "Espresso",
2835            "ESP",
2836        )
2837        .await?;
2838        // deploy light client proxy
2839        let lc_proxy_addr = deploy_light_client_proxy(
2840            &provider,
2841            &mut contracts,
2842            false,
2843            LightClientStateSol::dummy_genesis(),
2844            StakeTableStateSol::dummy_genesis(),
2845            init_recipient,
2846            Some(init_recipient),
2847        )
2848        .await?;
2849
2850        let exit_escrow_period = U256::from(DEFAULT_EXIT_ESCROW_PERIOD_SECONDS);
2851        let owner = init_recipient;
2852        let stake_table_proxy_addr = deploy_stake_table_proxy(
2853            &provider,
2854            &mut contracts,
2855            token_addr,
2856            lc_proxy_addr,
2857            exit_escrow_period,
2858            owner,
2859        )
2860        .await?;
2861        // transfer ownership to multisig
2862        let _receipt = transfer_ownership(
2863            &provider,
2864            Contract::StakeTableProxy,
2865            stake_table_proxy_addr,
2866            multisig_admin,
2867        )
2868        .await?;
2869        let stake_table = StakeTable::new(stake_table_proxy_addr, &provider);
2870        assert_eq!(stake_table.owner().call().await?, multisig_admin);
2871        // then encode upgrade calldata for the multisig wallet
2872        let pauser = Address::random();
2873        let calldata = upgrade_stake_table_v2_multisig_owner(
2874            &provider,
2875            l1_client,
2876            &mut contracts,
2877            StakeTableV2UpgradeParams {
2878                multisig_address: multisig_admin,
2879                pauser,
2880            },
2881            MultisigOwnerCheck::Skip,
2882        )
2883        .await?;
2884        assert_eq!(calldata.to, stake_table_proxy_addr);
2885
2886        Ok(())
2887    }
2888
2889    #[test_log::test(tokio::test)]
2890    async fn test_upgrade_stake_table_to_v2_multisig_owner() -> Result<()> {
2891        test_upgrade_stake_table_to_v2_multisig_owner_helper().await
2892    }
2893
2894    #[test_log::test(tokio::test)]
2895    async fn test_deploy_ops_timelock() -> Result<()> {
2896        let provider = ProviderBuilder::new().connect_anvil_with_wallet();
2897        let mut contracts = Contracts::new();
2898
2899        // Setup test parameters
2900        let min_delay = U256::from(86400); // 1 day in seconds
2901        let admin = provider.get_accounts().await?[0];
2902        let proposers = vec![Address::random()];
2903        let executors = vec![Address::random()];
2904
2905        let timelock_addr = deploy_ops_timelock(
2906            &provider,
2907            &mut contracts,
2908            min_delay,
2909            proposers.clone(),
2910            executors.clone(),
2911            admin,
2912        )
2913        .await?;
2914
2915        // Verify deployment
2916        let timelock = OpsTimelock::new(timelock_addr, &provider);
2917        assert_eq!(timelock.getMinDelay().call().await?, min_delay);
2918
2919        // Verify initialization parameters
2920        assert_eq!(timelock.getMinDelay().call().await?, min_delay);
2921        assert!(
2922            timelock
2923                .hasRole(timelock.PROPOSER_ROLE().call().await?, proposers[0])
2924                .call()
2925                .await?
2926        );
2927        assert!(
2928            timelock
2929                .hasRole(timelock.EXECUTOR_ROLE().call().await?, executors[0])
2930                .call()
2931                .await?
2932        );
2933
2934        // test that the admin is in the default admin role where DEFAULT_ADMIN_ROLE = 0x00
2935        let default_admin_role = U256::ZERO;
2936        assert!(
2937            timelock
2938                .hasRole(default_admin_role.into(), admin)
2939                .call()
2940                .await?
2941        );
2942        Ok(())
2943    }
2944
2945    #[test_log::test(tokio::test)]
2946    async fn test_deploy_safe_exit_timelock() -> Result<()> {
2947        let provider = ProviderBuilder::new().connect_anvil_with_wallet();
2948        let mut contracts = Contracts::new();
2949
2950        // Setup test parameters
2951        let min_delay = U256::from(86400); // 1 day in seconds
2952        let admin = provider.get_accounts().await?[0];
2953        let proposers = vec![Address::random()];
2954        let executors = vec![Address::random()];
2955
2956        let timelock_addr = deploy_safe_exit_timelock(
2957            &provider,
2958            &mut contracts,
2959            min_delay,
2960            proposers.clone(),
2961            executors.clone(),
2962            admin,
2963        )
2964        .await?;
2965
2966        // Verify deployment
2967        let timelock = SafeExitTimelock::new(timelock_addr, &provider);
2968        assert_eq!(timelock.getMinDelay().call().await?, min_delay);
2969
2970        // Verify initialization parameters
2971        assert_eq!(timelock.getMinDelay().call().await?, min_delay);
2972        assert!(
2973            timelock
2974                .hasRole(timelock.PROPOSER_ROLE().call().await?, proposers[0])
2975                .call()
2976                .await?
2977        );
2978        assert!(
2979            timelock
2980                .hasRole(timelock.EXECUTOR_ROLE().call().await?, executors[0])
2981                .call()
2982                .await?
2983        );
2984
2985        // test that the admin is in the default admin role where DEFAULT_ADMIN_ROLE = 0x00
2986        let default_admin_role = U256::ZERO;
2987        assert!(
2988            timelock
2989                .hasRole(default_admin_role.into(), admin)
2990                .call()
2991                .await?
2992        );
2993        Ok(())
2994    }
2995
2996    #[test_log::test(tokio::test)]
2997    async fn test_upgrade_esp_token_v2() -> Result<()> {
2998        let provider = ProviderBuilder::new().connect_anvil_with_wallet();
2999        let mut contracts = Contracts::new();
3000
3001        // deploy token
3002        let init_recipient = provider.get_accounts().await?[1];
3003        let token_owner = provider.get_accounts().await?[0];
3004        let token_name = "Espresso";
3005        let token_symbol = "ESP";
3006        let initial_supply = U256::from(3590000000u64);
3007        let token_proxy_addr = deploy_token_proxy(
3008            &provider,
3009            &mut contracts,
3010            token_owner,
3011            init_recipient,
3012            initial_supply,
3013            token_name,
3014            token_symbol,
3015        )
3016        .await?;
3017        let esp_token = EspToken::new(token_proxy_addr, &provider);
3018        assert_eq!(esp_token.name().call().await?, token_name);
3019
3020        let fake_reward_claim = Address::random();
3021        contracts.insert(Contract::RewardClaimProxy, fake_reward_claim);
3022
3023        // upgrade to v2
3024        upgrade_esp_token_v2(&provider, &mut contracts).await?;
3025
3026        let esp_token_v2 = EspTokenV2::new(token_proxy_addr, &provider);
3027
3028        assert_eq!(esp_token_v2.getVersion().call().await?, (2, 0, 0).into());
3029        assert_eq!(esp_token_v2.owner().call().await?, token_owner);
3030
3031        // name is hardcoded in the EspTokenV2 contract
3032        assert_eq!(esp_token_v2.name().call().await?, "Espresso");
3033        assert_eq!(esp_token_v2.symbol().call().await?, "ESP");
3034        assert_eq!(esp_token_v2.decimals().call().await?, 18);
3035
3036        let initial_supply_in_wei = parse_ether(&initial_supply.to_string()).unwrap();
3037        assert_eq!(
3038            esp_token_v2.totalSupply().call().await?,
3039            initial_supply_in_wei
3040        );
3041        assert_eq!(
3042            esp_token_v2.balanceOf(init_recipient).call().await?,
3043            initial_supply_in_wei
3044        );
3045        assert_eq!(
3046            esp_token_v2.balanceOf(token_owner).call().await?,
3047            U256::ZERO
3048        );
3049
3050        assert_eq!(esp_token_v2.rewardClaim().call().await?, fake_reward_claim);
3051
3052        Ok(())
3053    }
3054
3055    // We expect no init data for the upgrade because there is no reinitializer for v2
3056    #[test_log::test(tokio::test)]
3057    async fn test_upgrade_esp_token_v2_multisig_owner() -> Result<()> {
3058        test_upgrade_esp_token_v2_multisig_owner_helper(UpgradeTestOptions {
3059            is_mock: false,
3060            upgrade_count: UpgradeCount::Once,
3061        })
3062        .await
3063    }
3064
3065    // This test is used to test the upgrade of the EspTokenProxy via the multisig wallet
3066    // It only tests the upgrade proposal via the typescript script and thus requires the upgrade proposal to be sent to a real network
3067    // However, the contracts are deployed on anvil, so the test will pass even if the upgrade proposal is not executed
3068    // The test assumes that there is a file .env.deployer.rs.test in the root directory:
3069    // Ensure that the private key has proposal rights on the Safe Multisig Wallet and the SDK supports the network
3070    async fn test_upgrade_esp_token_v2_multisig_owner_helper(
3071        options: UpgradeTestOptions,
3072    ) -> Result<()> {
3073        let multisig_admin = Address::random();
3074        let mnemonic = "test test test test test test test test test test test junk".to_string();
3075        let account_index = 0u32;
3076        let anvil = Anvil::default().spawn();
3077
3078        let mut contracts = Contracts::new();
3079        let admin_signer = MnemonicBuilder::<English>::default()
3080            .phrase(mnemonic)
3081            .index(account_index)
3082            .expect("wrong mnemonic or index")
3083            .build()?;
3084        let admin = admin_signer.address();
3085        let provider = ProviderBuilder::new()
3086            .wallet(admin_signer)
3087            .connect(&anvil.endpoint())
3088            .await?;
3089        let init_recipient = provider.get_accounts().await?[0];
3090        let initial_supply = U256::from(3590000000u64);
3091        let token_name = "Espresso";
3092        let token_symbol = "ESP";
3093
3094        // deploy proxy and V1
3095        let esp_token_proxy_addr = deploy_token_proxy(
3096            &provider,
3097            &mut contracts,
3098            admin,
3099            init_recipient,
3100            initial_supply,
3101            token_name,
3102            token_symbol,
3103        )
3104        .await?;
3105        if matches!(options.upgrade_count, UpgradeCount::Twice) {
3106            // upgrade to v2
3107            upgrade_esp_token_v2(&provider, &mut contracts).await?;
3108        }
3109
3110        // transfer ownership to multisig
3111        let _receipt = transfer_ownership(
3112            &provider,
3113            Contract::EspTokenProxy,
3114            esp_token_proxy_addr,
3115            multisig_admin,
3116        )
3117        .await?;
3118        let esp_token = EspToken::new(esp_token_proxy_addr, &provider);
3119        assert_eq!(esp_token.owner().call().await?, multisig_admin);
3120
3121        let fake_reward_claim = Address::random();
3122        contracts.insert(Contract::RewardClaimProxy, fake_reward_claim);
3123
3124        // then encode upgrade calldata for the multisig wallet
3125        let calldata = upgrade_esp_token_v2_multisig_owner(
3126            &provider,
3127            &mut contracts,
3128            MultisigOwnerCheck::Skip,
3129        )
3130        .await?;
3131        tracing::info!(
3132            "Encoded calldata for EspTokenProxy upgrade: to={:#x}, data={}",
3133            calldata.to,
3134            calldata.data
3135        );
3136        assert_eq!(calldata.to, esp_token_proxy_addr);
3137        assert!(!calldata.data.is_empty());
3138
3139        Ok(())
3140    }
3141
3142    #[test_log::test(tokio::test)]
3143    async fn test_schedule_and_execute_timelock_operation() -> Result<()> {
3144        let provider = ProviderBuilder::new().connect_anvil_with_wallet();
3145        let mut contracts = Contracts::new();
3146        let delay = U256::from(0);
3147
3148        // Get the provider's wallet address (the one actually sending transactions)
3149        let provider_wallet = provider.get_accounts().await?[0];
3150
3151        let proposers = vec![provider_wallet];
3152        let executors = vec![provider_wallet];
3153
3154        let timelock_addr = deploy_ops_timelock(
3155            &provider,
3156            &mut contracts,
3157            delay,
3158            proposers,
3159            executors,
3160            provider_wallet, // Use provider wallet as admin too
3161        )
3162        .await?;
3163
3164        // deploy fee contract and set the timelock as the admin
3165        let fee_contract_proxy_addr =
3166            deploy_fee_contract_proxy(&provider, &mut contracts, timelock_addr).await?;
3167
3168        let proxy = FeeContract::new(fee_contract_proxy_addr, &provider);
3169        let upgrade_data = proxy
3170            .transferOwnership(provider_wallet)
3171            .calldata()
3172            .to_owned();
3173
3174        // propose a timelock operation
3175        let mut operation = TimelockOperationPayload {
3176            target: fee_contract_proxy_addr,
3177            value: U256::ZERO,
3178            data: upgrade_data,
3179            predecessor: B256::ZERO,
3180            salt: B256::ZERO,
3181            delay,
3182        };
3183        let operation_id = perform_timelock_operation(
3184            &provider,
3185            Contract::FeeContractProxy,
3186            operation.clone(),
3187            TimelockOperationType::Schedule,
3188            TimelockOperationParams::default(),
3189        )
3190        .await?;
3191
3192        // check that the tx is scheduled
3193        let timelock = OpsTimelock::new(timelock_addr, &provider);
3194        assert!(timelock.isOperationPending(operation_id).call().await?);
3195        assert!(timelock.isOperationReady(operation_id).call().await?);
3196        assert!(!timelock.isOperationDone(operation_id).call().await?);
3197        assert!(timelock.getTimestamp(operation_id).call().await? > U256::ZERO);
3198
3199        // execute the tx since the delay is 0
3200        perform_timelock_operation(
3201            &provider,
3202            Contract::FeeContractProxy,
3203            operation.clone(),
3204            TimelockOperationType::Execute,
3205            TimelockOperationParams::default(),
3206        )
3207        .await?;
3208
3209        // check that the tx is executed
3210        assert!(timelock.isOperationDone(operation_id).call().await?);
3211        assert!(!timelock.isOperationPending(operation_id).call().await?);
3212        assert!(!timelock.isOperationReady(operation_id).call().await?);
3213        // check that the new owner is the provider_wallet
3214        let fee_contract = FeeContract::new(operation.target, &provider);
3215        assert_eq!(fee_contract.owner().call().await?, provider_wallet);
3216
3217        operation.value = U256::from(1);
3218        //transfer ownership back to the timelock
3219        let tx_receipt = fee_contract
3220            .transferOwnership(timelock_addr)
3221            .send()
3222            .await?
3223            .get_receipt()
3224            .await?;
3225        assert!(tx_receipt.inner.is_success());
3226
3227        let operation_id = perform_timelock_operation(
3228            &provider,
3229            Contract::FeeContractProxy,
3230            operation.clone(),
3231            TimelockOperationType::Schedule,
3232            TimelockOperationParams::default(),
3233        )
3234        .await?;
3235
3236        perform_timelock_operation(
3237            &provider,
3238            Contract::FeeContractProxy,
3239            operation.clone(),
3240            TimelockOperationType::Cancel,
3241            TimelockOperationParams::default(),
3242        )
3243        .await?;
3244
3245        // check that the tx is cancelled
3246        timelock
3247            .hashOperation(
3248                operation.target,
3249                operation.value,
3250                operation.data.clone(),
3251                operation.predecessor,
3252                operation.salt,
3253            )
3254            .call()
3255            .await?;
3256        assert!(timelock.getTimestamp(operation_id).call().await? == U256::ZERO);
3257
3258        let operation_id = perform_timelock_operation(
3259            &provider,
3260            Contract::FeeContractProxy,
3261            operation.clone(),
3262            TimelockOperationType::Schedule,
3263            TimelockOperationParams::default(),
3264        )
3265        .await?;
3266
3267        // Test canceling with only the operation_id (operation payload not needed)
3268        perform_timelock_operation(
3269            &provider,
3270            Contract::FeeContractProxy,
3271            // Use a minimal/empty operation payload since operation_id is provided
3272            TimelockOperationPayload {
3273                target: fee_contract_proxy_addr, // Not used when operation_id is provided
3274                value: U256::ZERO,
3275                data: Bytes::new(),
3276                predecessor: B256::ZERO,
3277                salt: B256::ZERO,
3278                delay: U256::ZERO,
3279            },
3280            TimelockOperationType::Cancel,
3281            TimelockOperationParams {
3282                operation_id: Some(operation_id), // Only operation_id is needed
3283                ..Default::default()
3284            },
3285        )
3286        .await?;
3287        assert!(timelock.getTimestamp(operation_id).call().await? == U256::ZERO);
3288        Ok(())
3289    }
3290
3291    async fn test_transfer_ownership_helper(contract_type: Contract) -> Result<()> {
3292        let multisig_admin = Address::random();
3293        let timelock = Address::random();
3294        let mnemonic = "test test test test test test test test test test test junk".to_string();
3295        let account_index = 0u32;
3296        let anvil = Anvil::default().spawn();
3297
3298        let mut contracts = Contracts::new();
3299        let admin_signer = MnemonicBuilder::<English>::default()
3300            .phrase(mnemonic)
3301            .index(account_index)
3302            .expect("wrong mnemonic or index")
3303            .build()?;
3304        let admin = admin_signer.address();
3305        let provider = ProviderBuilder::new()
3306            .wallet(admin_signer)
3307            .connect_http(anvil.endpoint_url());
3308
3309        // prepare `initialize()` input
3310        let genesis_state = LightClientStateSol::dummy_genesis();
3311        let genesis_stake = StakeTableStateSol::dummy_genesis();
3312
3313        let prover = Address::random();
3314
3315        // deploy proxy and V1
3316        let proxy_addr = match contract_type {
3317            Contract::LightClientProxy => {
3318                deploy_light_client_proxy(
3319                    &provider,
3320                    &mut contracts,
3321                    false,
3322                    genesis_state.clone(),
3323                    genesis_stake.clone(),
3324                    admin,
3325                    Some(prover),
3326                )
3327                .await?
3328            },
3329            Contract::FeeContractProxy => {
3330                deploy_fee_contract_proxy(&provider, &mut contracts, admin).await?
3331            },
3332            Contract::EspTokenProxy => {
3333                deploy_token_proxy(
3334                    &provider,
3335                    &mut contracts,
3336                    admin,
3337                    multisig_admin,
3338                    U256::from(0u64),
3339                    "Test",
3340                    "TEST",
3341                )
3342                .await?
3343            },
3344            Contract::StakeTableProxy => {
3345                let token_addr = deploy_token_proxy(
3346                    &provider,
3347                    &mut contracts,
3348                    admin,
3349                    admin,
3350                    U256::from(0u64),
3351                    "Test",
3352                    "TEST",
3353                )
3354                .await?;
3355                let initial_admin = provider.get_accounts().await?[0];
3356                // deploy light client proxy
3357                let lc_proxy_addr = deploy_light_client_proxy(
3358                    &provider,
3359                    &mut contracts,
3360                    false,
3361                    LightClientStateSol::dummy_genesis(),
3362                    StakeTableStateSol::dummy_genesis(),
3363                    initial_admin,
3364                    Some(prover),
3365                )
3366                .await?;
3367                // upgrade to v2
3368                let blocks_per_epoch = 50;
3369                let epoch_start_block = 50;
3370                upgrade_light_client_v2(
3371                    &provider,
3372                    &mut contracts,
3373                    false,
3374                    blocks_per_epoch,
3375                    epoch_start_block,
3376                )
3377                .await?;
3378                let lc_v2 = LightClientV2::new(lc_proxy_addr, &provider);
3379                assert_eq!(lc_v2.getVersion().call().await?.majorVersion, 2);
3380                assert_eq!(lc_v2.blocksPerEpoch().call().await?, blocks_per_epoch);
3381                assert_eq!(lc_v2.epochStartBlock().call().await?, epoch_start_block);
3382
3383                deploy_stake_table_proxy(
3384                    &provider,
3385                    &mut contracts,
3386                    token_addr,
3387                    lc_proxy_addr,
3388                    U256::from(DEFAULT_EXIT_ESCROW_PERIOD_SECONDS),
3389                    admin,
3390                )
3391                .await?
3392            },
3393            _ => anyhow::bail!("Not a proxy contract, can't transfer ownership"),
3394        };
3395
3396        // transfer ownership to multisig
3397        let _receipt =
3398            transfer_ownership(&provider, contract_type, proxy_addr, multisig_admin).await?;
3399        assert_eq!(
3400            OwnableUpgradeable::new(proxy_addr, &provider)
3401                .owner()
3402                .call()
3403                .await?,
3404            multisig_admin
3405        );
3406
3407        // encode transfer ownership calldata for the multisig wallet
3408        let calldata = transfer_ownership_from_multisig_to_timelock(
3409            &mut contracts,
3410            contract_type,
3411            TransferOwnershipParams {
3412                new_owner: timelock,
3413            },
3414        )?;
3415        tracing::info!(
3416            "Transfer ownership calldata: to={:#x}, data={}",
3417            calldata.to,
3418            calldata.data
3419        );
3420        assert_eq!(calldata.to, proxy_addr);
3421
3422        let expected_calldata =
3423            crate::encode_function_call("transferOwnership(address)", vec![timelock.to_string()])
3424                .unwrap();
3425        assert_eq!(calldata.data, expected_calldata);
3426
3427        // Execute the calldata on Anvil as multisig_admin to verify it works
3428        let impersonation_provider = ProviderBuilder::new()
3429            .filler(ImpersonateFiller::new(multisig_admin))
3430            .connect_http(anvil.endpoint_url());
3431        let anvil_provider = AnvilProvider::new(impersonation_provider.clone(), Arc::new(anvil));
3432        anvil_provider.anvil_auto_impersonate_account(true).await?;
3433        anvil_provider
3434            .anvil_set_balance(multisig_admin, parse_ether("100")?)
3435            .await?;
3436
3437        let tx = alloy::rpc::types::TransactionRequest::default()
3438            .to(calldata.to)
3439            .input(calldata.data.into());
3440        impersonation_provider
3441            .send_transaction(tx)
3442            .await?
3443            .get_receipt()
3444            .await?;
3445
3446        assert_eq!(
3447            OwnableUpgradeable::new(proxy_addr, &impersonation_provider)
3448                .owner()
3449                .call()
3450                .await?,
3451            timelock,
3452            "ownership should have transferred to timelock"
3453        );
3454
3455        Ok(())
3456    }
3457
3458    #[test_log::test(tokio::test)]
3459    async fn test_encode_function_call() -> Result<()> {
3460        let function_signature = "transfer(address,uint256)".to_string();
3461        let values = vec![
3462            "0x000000000000000000000000000000000000dead".to_string(),
3463            "1000".to_string(),
3464        ];
3465        let expected = "0xa9059cbb000000000000000000000000000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000003e8".parse::<Bytes>()?;
3466        let encoded = encode_function_call(&function_signature, values).expect("encoding failed");
3467
3468        assert_eq!(encoded, expected);
3469        Ok(())
3470    }
3471
3472    #[test_log::test(tokio::test)]
3473    async fn test_encode_function_call_upgrade_to_and_call() -> Result<()> {
3474        let function_signature = "upgradeToAndCall(address,bytes)".to_string();
3475        let values = vec![
3476            "0xe1f131b07550a689d6a11f21d9e9238a5c466996".to_string(),
3477            "0x".to_string(),
3478        ];
3479        let expected = "0x4f1ef286000000000000000000000000e1f131b07550a689d6a11f21d9e9238a5c46699600000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000".parse::<Bytes>()?;
3480        let encoded = encode_function_call(&function_signature, values).expect("encoding failed");
3481
3482        assert_eq!(encoded, expected);
3483        Ok(())
3484    }
3485
3486    #[test_log::test(tokio::test)]
3487    async fn test_encode_function_call_with_bytes32() -> Result<()> {
3488        let function_signature = "setHash(bytes32)".to_string();
3489        let values =
3490            vec!["0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string()];
3491        let expected = "0x0c4c42850123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
3492            .parse::<Bytes>()?;
3493        let encoded = encode_function_call(&function_signature, values).expect("encoding failed");
3494
3495        assert_eq!(encoded, expected);
3496        Ok(())
3497    }
3498
3499    #[test_log::test(tokio::test)]
3500    async fn test_encode_function_call_with_bytes() -> Result<()> {
3501        let function_signature = "emitData(bytes)".to_string();
3502        let values = vec!["0xdeadbeef".to_string()];
3503        let expected = "0xd836083e00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000004deadbeef00000000000000000000000000000000000000000000000000000000".parse::<Bytes>()?;
3504        let encoded = encode_function_call(&function_signature, values).expect("encoding failed");
3505
3506        assert_eq!(encoded, expected);
3507        Ok(())
3508    }
3509
3510    #[test_log::test(tokio::test)]
3511    async fn test_encode_function_call_with_bool() -> Result<()> {
3512        let function_signature = "setFlag(bool)".to_string();
3513        let mut values = vec!["true".to_string()];
3514        let mut expected =
3515            "0x3927f6af0000000000000000000000000000000000000000000000000000000000000001"
3516                .parse::<Bytes>()?;
3517        let mut encoded =
3518            encode_function_call(&function_signature, values).expect("encoding failed");
3519
3520        assert_eq!(encoded, expected);
3521
3522        values = vec!["false".to_string()];
3523        expected = "0x3927f6af0000000000000000000000000000000000000000000000000000000000000000"
3524            .parse::<Bytes>()?;
3525        encoded = encode_function_call(&function_signature, values).expect("encoding failed");
3526
3527        assert_eq!(encoded, expected);
3528        Ok(())
3529    }
3530
3531    #[test_log::test(tokio::test)]
3532    async fn test_encode_function_call_with_string() -> Result<()> {
3533        let function_signature = "logMessage(string)".to_string();
3534        let values = vec!["Hello, world!".to_string()];
3535        let expected = "0x7c9900520000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000d48656c6c6f2c20776f726c642100000000000000000000000000000000000000".parse::<Bytes>()?;
3536        let encoded = encode_function_call(&function_signature, values).expect("encoding failed");
3537
3538        assert_eq!(encoded, expected);
3539        Ok(())
3540    }
3541
3542    #[test_log::test(tokio::test)]
3543    async fn test_transfer_ownership_light_client_proxy() -> Result<()> {
3544        test_transfer_ownership_helper(Contract::LightClientProxy).await
3545    }
3546
3547    #[test_log::test(tokio::test)]
3548    async fn test_transfer_ownership_fee_contract_proxy() -> Result<()> {
3549        test_transfer_ownership_helper(Contract::FeeContractProxy).await
3550    }
3551
3552    #[test_log::test(tokio::test)]
3553    async fn test_transfer_ownership_esp_token_proxy() -> Result<()> {
3554        test_transfer_ownership_helper(Contract::EspTokenProxy).await
3555    }
3556
3557    #[test_log::test(tokio::test)]
3558    async fn test_transfer_ownership_stake_table_proxy() -> Result<()> {
3559        test_transfer_ownership_helper(Contract::StakeTableProxy).await
3560    }
3561
3562    #[test_log::test(tokio::test)]
3563    async fn test_get_proxy_initialized_version_initialized() -> Result<()> {
3564        let provider = ProviderBuilder::new().connect_anvil_with_wallet();
3565        let mut contracts = Contracts::new();
3566        let owner = provider.get_accounts().await?[0];
3567
3568        let token_addr = deploy_token_proxy(
3569            &provider,
3570            &mut contracts,
3571            owner,
3572            owner,
3573            U256::from(10_000_000u64),
3574            "Test Token",
3575            "TEST",
3576        )
3577        .await?;
3578
3579        let lc_addr = deploy_light_client_contract(&provider, &mut contracts, false).await?;
3580        let exit_escrow_period = U256::from(DEFAULT_EXIT_ESCROW_PERIOD_SECONDS);
3581
3582        let stake_table_proxy_addr = deploy_stake_table_proxy(
3583            &provider,
3584            &mut contracts,
3585            token_addr,
3586            lc_addr,
3587            exit_escrow_period,
3588            owner,
3589        )
3590        .await?;
3591
3592        let version = get_proxy_initialized_version(&provider, stake_table_proxy_addr).await?;
3593        assert_eq!(version, 1, "Initialized proxy should return version 1");
3594
3595        Ok(())
3596    }
3597
3598    #[test_log::test(tokio::test)]
3599    async fn test_get_proxy_initialized_version_reinitialized() -> Result<()> {
3600        let (_anvil, provider, l1_client) =
3601            ProviderBuilder::new().connect_anvil_with_l1_client()?;
3602        let mut contracts = Contracts::new();
3603        let owner = l1_client.get_accounts().await?[0];
3604
3605        let token_addr = deploy_token_proxy(
3606            &provider,
3607            &mut contracts,
3608            owner,
3609            owner,
3610            U256::from(10_000_000u64),
3611            "Test Token",
3612            "TEST",
3613        )
3614        .await?;
3615
3616        let lc_addr = deploy_light_client_contract(&provider, &mut contracts, false).await?;
3617        let exit_escrow_period = U256::from(DEFAULT_EXIT_ESCROW_PERIOD_SECONDS);
3618
3619        let stake_table_proxy_addr = deploy_stake_table_proxy(
3620            &provider,
3621            &mut contracts,
3622            token_addr,
3623            lc_addr,
3624            exit_escrow_period,
3625            owner,
3626        )
3627        .await?;
3628
3629        let pauser = Address::random();
3630        let admin = Address::random();
3631        upgrade_stake_table_v2(&provider, l1_client.clone(), &mut contracts, pauser, admin).await?;
3632
3633        let version = get_proxy_initialized_version(&l1_client, stake_table_proxy_addr).await?;
3634        assert_eq!(version, 2, "Reinitialized proxy should return version 2");
3635
3636        Ok(())
3637    }
3638
3639    #[test_log::test(tokio::test)]
3640    async fn test_grant_admin_role_reward_claim() -> Result<()> {
3641        let (_anvil, provider, l1_client) =
3642            ProviderBuilder::new().connect_anvil_with_l1_client()?;
3643
3644        let mut contracts = Contracts::new();
3645        let deployer = l1_client.get_accounts().await?[0];
3646        let new_admin = Address::random();
3647
3648        // Deploy RewardClaim
3649        let esp_token_addr = deploy_token_proxy(
3650            &provider,
3651            &mut contracts,
3652            deployer,
3653            deployer,
3654            U256::from(10_000_000u64),
3655            "Test Token",
3656            "TEST",
3657        )
3658        .await?;
3659        let lc_addr = deploy_light_client_contract(&provider, &mut contracts, false).await?;
3660        let reward_claim_addr = deploy_reward_claim_proxy(
3661            &provider,
3662            &mut contracts,
3663            esp_token_addr,
3664            lc_addr,
3665            deployer,
3666            deployer, // pauser
3667        )
3668        .await?;
3669
3670        // Verify initial admin
3671        let reward_claim = RewardClaim::new(reward_claim_addr, &provider);
3672        let admin_role = reward_claim.DEFAULT_ADMIN_ROLE().call().await?;
3673        assert!(reward_claim.hasRole(admin_role, deployer).call().await?);
3674        assert!(!reward_claim.hasRole(admin_role, new_admin).call().await?);
3675
3676        // Grant admin role
3677        let receipt = grant_admin_role(
3678            &provider,
3679            Contract::RewardClaimProxy,
3680            reward_claim_addr,
3681            new_admin,
3682        )
3683        .await?;
3684
3685        assert!(receipt.inner.is_success());
3686
3687        // Verify new admin has role and old admin doesn't
3688        assert!(reward_claim.hasRole(admin_role, new_admin).call().await?);
3689        assert!(!reward_claim.hasRole(admin_role, deployer).call().await?);
3690
3691        Ok(())
3692    }
3693
3694    #[test_log::test(tokio::test)]
3695    async fn test_transfer_ownership_from_eoa_reward_claim_routes_to_grant_role() -> Result<()> {
3696        let (anvil, provider, l1_client) = ProviderBuilder::new().connect_anvil_with_l1_client()?;
3697
3698        let mut contracts = Contracts::new();
3699        let deployer = l1_client.get_accounts().await?[0];
3700        let new_admin = Address::random();
3701
3702        // Deploy RewardClaim
3703        let esp_token_addr = deploy_token_proxy(
3704            &provider,
3705            &mut contracts,
3706            deployer,
3707            deployer,
3708            U256::from(10_000_000u64),
3709            "Test Token",
3710            "TEST",
3711        )
3712        .await?;
3713        let lc_addr = deploy_light_client_contract(&provider, &mut contracts, false).await?;
3714        let reward_claim_addr = deploy_reward_claim_proxy(
3715            &provider,
3716            &mut contracts,
3717            esp_token_addr,
3718            lc_addr,
3719            deployer,
3720            deployer, // pauser
3721        )
3722        .await?;
3723
3724        // Verify initial admin
3725        let reward_claim = RewardClaim::new(reward_claim_addr, &provider);
3726        let admin_role = reward_claim.DEFAULT_ADMIN_ROLE().call().await?;
3727        assert!(reward_claim.hasRole(admin_role, deployer).call().await?);
3728        assert!(!reward_claim.hasRole(admin_role, new_admin).call().await?);
3729
3730        use builder::DeployerArgsBuilder;
3731
3732        let mut args_builder = DeployerArgsBuilder::default();
3733        args_builder
3734            .deployer(provider.clone())
3735            .rpc_url(anvil.endpoint_url())
3736            .transfer_ownership_from_eoa(true)
3737            .target_contract(OwnableContract::RewardClaimProxy)
3738            .transfer_ownership_new_owner(new_admin);
3739        let args = args_builder.build()?;
3740
3741        args.transfer_ownership_from_eoa(&mut contracts).await?;
3742
3743        // Verify new admin has role and old admin doesn't
3744        assert!(reward_claim.hasRole(admin_role, new_admin).call().await?);
3745        assert!(!reward_claim.hasRole(admin_role, deployer).call().await?);
3746
3747        Ok(())
3748    }
3749
3750    #[test_log::test(tokio::test)]
3751    async fn test_perform_timelock_operation_reward_claim() -> Result<()> {
3752        let (anvil, provider, _l1_client) =
3753            ProviderBuilder::new().connect_anvil_with_l1_client()?;
3754        let mut contracts = Contracts::new();
3755        let delay = U256::from(0);
3756        let provider_wallet = provider.get_accounts().await?[0];
3757
3758        // Deploy SafeExitTimelock
3759        let timelock_addr = deploy_safe_exit_timelock(
3760            &provider,
3761            &mut contracts,
3762            delay,
3763            vec![provider_wallet],
3764            vec![provider_wallet],
3765            provider_wallet,
3766        )
3767        .await?;
3768
3769        // Deploy dependencies
3770        let token_addr = deploy_token_proxy(
3771            &provider,
3772            &mut contracts,
3773            provider_wallet,
3774            provider_wallet,
3775            U256::from(10_000_000u64),
3776            "Test Token",
3777            "TEST",
3778        )
3779        .await?;
3780        let lc_addr = deploy_light_client_contract(&provider, &mut contracts, false).await?;
3781
3782        // Deploy RewardClaim with timelock as admin
3783        let reward_claim_addr = deploy_reward_claim_proxy(
3784            &provider,
3785            &mut contracts,
3786            token_addr,
3787            lc_addr,
3788            timelock_addr,
3789            provider_wallet,
3790        )
3791        .await?;
3792
3793        let reward_claim = RewardClaim::new(reward_claim_addr, &provider);
3794        let pauser_role = reward_claim.PAUSER_ROLE().call().await?;
3795        let new_pauser = Address::random();
3796
3797        // Verify new_pauser doesn't have the role yet
3798        assert!(!reward_claim.hasRole(pauser_role, new_pauser).call().await?);
3799
3800        // Use DeployerArgsBuilder to test propose_timelock_operation_for_contract
3801        use builder::DeployerArgsBuilder;
3802        use proposals::timelock::TimelockOperationType;
3803
3804        let mut args_builder = DeployerArgsBuilder::default();
3805        args_builder
3806            .deployer(provider.clone())
3807            .rpc_url(anvil.endpoint_url())
3808            .timelock_operation_type(TimelockOperationType::Schedule)
3809            .target_contract(OwnableContract::RewardClaimProxy)
3810            .timelock_operation_value(U256::ZERO)
3811            .timelock_operation_function_signature("grantRole(bytes32,address)".to_string())
3812            .timelock_operation_function_values(vec![
3813                format!("{:#x}", pauser_role),
3814                format!("{:#x}", new_pauser),
3815            ])
3816            .timelock_operation_salt(
3817                "0x0000000000000000000000000000000000000000000000000000000000000001".to_string(),
3818            )
3819            .timelock_operation_delay(delay);
3820
3821        let args = args_builder.build()?;
3822
3823        // Schedule the operation using the high-level function
3824        args.propose_timelock_operation_for_contract(&mut contracts)
3825            .await?;
3826
3827        // Now execute it
3828        let mut args_builder = DeployerArgsBuilder::default();
3829        args_builder
3830            .deployer(provider.clone())
3831            .rpc_url(anvil.endpoint_url())
3832            .timelock_operation_type(TimelockOperationType::Execute)
3833            .target_contract(OwnableContract::RewardClaimProxy)
3834            .timelock_operation_value(U256::ZERO)
3835            .timelock_operation_function_signature("grantRole(bytes32,address)".to_string())
3836            .timelock_operation_function_values(vec![
3837                format!("{:#x}", pauser_role),
3838                format!("{:#x}", new_pauser),
3839            ])
3840            .timelock_operation_salt(
3841                "0x0000000000000000000000000000000000000000000000000000000000000001".to_string(),
3842            )
3843            .timelock_operation_delay(delay);
3844
3845        let args = args_builder.build()?;
3846        args.propose_timelock_operation_for_contract(&mut contracts)
3847            .await?;
3848
3849        // Verify the function was actually called
3850        assert!(reward_claim.hasRole(pauser_role, new_pauser).call().await?);
3851
3852        Ok(())
3853    }
3854
3855    #[test_log::test(tokio::test)]
3856    async fn test_derive_timelock_address_from_contracts() -> Result<()> {
3857        use builder::DeployerArgsBuilder;
3858        use proposals::timelock::{TimelockContract, get_timelock_for_contract};
3859
3860        let (anvil, provider, _l1_client) =
3861            ProviderBuilder::new().connect_anvil_with_l1_client()?;
3862        let mut contracts = Contracts::new();
3863        let delay = U256::from(0);
3864        let provider_wallet = provider.get_accounts().await?[0];
3865        let proposers = vec![provider_wallet];
3866        let executors = vec![provider_wallet];
3867        let rpc_url = anvil.endpoint_url();
3868
3869        // Deploy both timelocks first
3870        let ops_timelock_addr = deploy_ops_timelock(
3871            &provider,
3872            &mut contracts,
3873            delay,
3874            proposers.clone(),
3875            executors.clone(),
3876            provider_wallet,
3877        )
3878        .await?;
3879
3880        let safe_exit_timelock_addr = deploy_safe_exit_timelock(
3881            &provider,
3882            &mut contracts,
3883            delay,
3884            proposers,
3885            executors,
3886            provider_wallet,
3887        )
3888        .await?;
3889
3890        //  Use DeployerArgsBuilder to deploy FeeContractProxy with use_timelock_owner
3891        // This tests the actual deployment code path and verifies it chooses OpsTimelock
3892        let mut args_builder = DeployerArgsBuilder::default();
3893        args_builder
3894            .deployer(provider.clone())
3895            .use_timelock_owner(true)
3896            .rpc_url(rpc_url.clone());
3897        let args = args_builder.build()?;
3898
3899        // Deploy FeeContractProxy - it should automatically use OpsTimelock
3900        args.deploy(&mut contracts, Contract::FeeContractProxy)
3901            .await?;
3902
3903        // Verify derivation function returns correct timelock
3904        let fee_contract_derived_timelock = derive_timelock_address_from_contract_type(
3905            OwnableContract::FeeContractProxy,
3906            &contracts,
3907        )?;
3908        assert_eq!(fee_contract_derived_timelock, ops_timelock_addr);
3909
3910        // Verify on-chain that FeeContractProxy has OpsTimelock as owner
3911        let fee_contract_addr = contracts
3912            .address(Contract::FeeContractProxy)
3913            .expect("FeeContractProxy should be deployed");
3914        let fee_contract = FeeContract::new(fee_contract_addr, &provider);
3915        let actual_owner = fee_contract.owner().call().await?;
3916        assert_eq!(
3917            actual_owner, ops_timelock_addr,
3918            "FeeContractProxy should have OpsTimelock as owner"
3919        );
3920
3921        let queried_timelock =
3922            get_timelock_for_contract(&provider, Contract::FeeContractProxy, fee_contract_addr)
3923                .await?;
3924
3925        match queried_timelock {
3926            TimelockContract::OpsTimelock(addr) => {
3927                assert_eq!(
3928                    addr, ops_timelock_addr,
3929                    "Queried timelock should match deployed OpsTimelock"
3930                );
3931                assert_eq!(
3932                    addr, fee_contract_derived_timelock,
3933                    "Queried timelock should match derived timelock"
3934                );
3935            },
3936            _ => panic!(
3937                "FeeContractProxy should use OpsTimelock, got: {:?}",
3938                queried_timelock
3939            ),
3940        }
3941
3942        //  Deploy RewardsClaimProxy - it should automatically use SafeExitTimelock
3943        let _esp_token_addr = deploy_token_proxy(
3944            &provider,
3945            &mut contracts,
3946            provider_wallet,
3947            provider_wallet,
3948            U256::from(10_000_000u64),
3949            "Test Token",
3950            "TEST",
3951        )
3952        .await?;
3953
3954        // prepare `initialize()` input
3955        let genesis_state = LightClientStateSol::dummy_genesis();
3956        let genesis_stake = StakeTableStateSol::dummy_genesis();
3957        let admin = provider.get_accounts().await?[0];
3958        let prover = admin;
3959
3960        let _lc_proxy_addr = deploy_light_client_proxy(
3961            &provider,
3962            &mut contracts,
3963            true, // is_mock = true
3964            genesis_state.clone(),
3965            genesis_stake.clone(),
3966            admin,
3967            Some(prover),
3968        )
3969        .await?;
3970
3971        let mut args_builder2 = DeployerArgsBuilder::default();
3972        args_builder2
3973            .deployer(provider.clone())
3974            .use_timelock_owner(true)
3975            .rpc_url(rpc_url.clone());
3976        let args2 = args_builder2.build()?;
3977
3978        args2
3979            .deploy(&mut contracts, Contract::RewardClaimProxy)
3980            .await?;
3981
3982        // Verify derivation
3983        let reward_claim_derived_timelock = derive_timelock_address_from_contract_type(
3984            OwnableContract::RewardClaimProxy,
3985            &contracts,
3986        )?;
3987        assert_eq!(reward_claim_derived_timelock, safe_exit_timelock_addr);
3988
3989        // Verify on-chain
3990        let reward_claim_addr = contracts
3991            .address(Contract::RewardClaimProxy)
3992            .expect("RewardClaimProxy should be deployed");
3993        let reward_claim = RewardClaim::new(reward_claim_addr, &provider);
3994        let actual_owner = reward_claim.currentAdmin().call().await?;
3995        assert_eq!(
3996            actual_owner, safe_exit_timelock_addr,
3997            "RewardClaimProxy should have SafeExitTimelock as owner"
3998        );
3999
4000        let queried_timelock =
4001            get_timelock_for_contract(&provider, Contract::RewardClaimProxy, reward_claim_addr)
4002                .await?;
4003
4004        match queried_timelock {
4005            TimelockContract::SafeExitTimelock(addr) => {
4006                assert_eq!(
4007                    addr, safe_exit_timelock_addr,
4008                    "Queried timelock should match deployed SafeExitTimelock"
4009                );
4010                assert_eq!(
4011                    addr, reward_claim_derived_timelock,
4012                    "Queried timelock should match derived timelock"
4013                );
4014            },
4015            _ => panic!(
4016                "RewardClaimProxy should use SafeExitTimelock, got: {:?}",
4017                queried_timelock
4018            ),
4019        }
4020
4021        // Error case - missing timelock
4022        let empty_contracts = Contracts::new();
4023        let result = derive_timelock_address_from_contract_type(
4024            OwnableContract::FeeContractProxy,
4025            &empty_contracts,
4026        );
4027        assert!(
4028            result.is_err(),
4029            "Should error when timelock is missing from contracts map"
4030        );
4031        let error_msg = result.unwrap_err().to_string();
4032        assert!(
4033            error_msg.contains("not found") || error_msg.contains("OpsTimelock"),
4034            "Error should mention missing timelock, got: {}",
4035            error_msg
4036        );
4037
4038        // NOTE: Invalid contract type test removed - OwnableContract enum now provides
4039        // compile-time safety so invalid types can't be passed to the function.
4040
4041        Ok(())
4042    }
4043
4044    #[test_log::test(tokio::test)]
4045    async fn test_upgrade_esp_token_v2_with_timelock_owner() -> Result<()> {
4046        use builder::DeployerArgsBuilder;
4047
4048        let (anvil, provider, _l1_client) =
4049            ProviderBuilder::new().connect_anvil_with_l1_client()?;
4050        let mut contracts = Contracts::new();
4051        let delay = U256::from(0);
4052        let provider_wallet = provider.get_accounts().await?[0];
4053        let proposers = vec![provider_wallet];
4054        let executors = vec![provider_wallet];
4055        let rpc_url = anvil.endpoint_url();
4056
4057        let safe_exit_timelock_addr = deploy_safe_exit_timelock(
4058            &provider,
4059            &mut contracts,
4060            delay,
4061            proposers,
4062            executors,
4063            provider_wallet,
4064        )
4065        .await?;
4066
4067        let token_owner = provider_wallet;
4068        let init_recipient = provider_wallet;
4069        let initial_supply = U256::from(10_000_000u64);
4070        let token_name = "Espresso";
4071        let token_symbol = "ESP";
4072
4073        let token_proxy_addr = deploy_token_proxy(
4074            &provider,
4075            &mut contracts,
4076            token_owner,
4077            init_recipient,
4078            initial_supply,
4079            token_name,
4080            token_symbol,
4081        )
4082        .await?;
4083
4084        let fake_reward_claim = Address::random();
4085        contracts.insert(Contract::RewardClaimProxy, fake_reward_claim);
4086
4087        let mut args_builder = DeployerArgsBuilder::default();
4088        args_builder
4089            .deployer(provider.clone())
4090            .use_timelock_owner(true)
4091            .rpc_url(rpc_url);
4092        let args = args_builder.build()?;
4093
4094        args.deploy(&mut contracts, Contract::EspTokenV2).await?;
4095
4096        let esp_token_v2 = EspTokenV2::new(token_proxy_addr, &provider);
4097        assert_eq!(esp_token_v2.getVersion().call().await?, (2, 0, 0).into());
4098        assert_eq!(
4099            esp_token_v2.owner().call().await?,
4100            safe_exit_timelock_addr,
4101            "EspTokenProxy should have SafeExitTimelock as owner after V2 upgrade"
4102        );
4103
4104        Ok(())
4105    }
4106
4107    #[test_log::test(tokio::test)]
4108    async fn test_upgrade_light_client_v3_with_timelock_owner() -> Result<()> {
4109        use builder::DeployerArgsBuilder;
4110
4111        let (anvil, provider, _l1_client) =
4112            ProviderBuilder::new().connect_anvil_with_l1_client()?;
4113        let mut contracts = Contracts::new();
4114        let delay = U256::from(0);
4115        let provider_wallet = provider.get_accounts().await?[0];
4116        let proposers = vec![provider_wallet];
4117        let executors = vec![provider_wallet];
4118        let rpc_url = anvil.endpoint_url();
4119
4120        let ops_timelock_addr = deploy_ops_timelock(
4121            &provider,
4122            &mut contracts,
4123            delay,
4124            proposers,
4125            executors,
4126            provider_wallet,
4127        )
4128        .await?;
4129
4130        let genesis_state = LightClientStateSol::dummy_genesis();
4131        let genesis_stake = StakeTableStateSol::dummy_genesis();
4132
4133        let lc_proxy_addr = deploy_light_client_proxy(
4134            &provider,
4135            &mut contracts,
4136            false,
4137            genesis_state,
4138            genesis_stake,
4139            provider_wallet,
4140            Some(provider_wallet),
4141        )
4142        .await?;
4143
4144        upgrade_light_client_v2(&provider, &mut contracts, false, 10, 22).await?;
4145
4146        let mut args_builder = DeployerArgsBuilder::default();
4147        args_builder
4148            .deployer(provider.clone())
4149            .use_timelock_owner(true)
4150            .rpc_url(rpc_url);
4151        let args = args_builder.build()?;
4152
4153        args.deploy(&mut contracts, Contract::LightClientV3).await?;
4154
4155        let lc_v3 = LightClientV3::new(lc_proxy_addr, &provider);
4156        assert_eq!(lc_v3.getVersion().call().await?.majorVersion, 3);
4157        assert_eq!(
4158            lc_v3.owner().call().await?,
4159            ops_timelock_addr,
4160            "LightClientProxy should have OpsTimelock as owner after V3 upgrade"
4161        );
4162
4163        Ok(())
4164    }
4165
4166    #[test_log::test(tokio::test)]
4167    async fn test_upgrade_stake_table_v2_with_timelock_owner() -> Result<()> {
4168        use builder::DeployerArgsBuilder;
4169
4170        let (anvil, provider, _l1_client) =
4171            ProviderBuilder::new().connect_anvil_with_l1_client()?;
4172        let mut contracts = Contracts::new();
4173        let delay = U256::from(0);
4174        let provider_wallet = provider.get_accounts().await?[0];
4175        let proposers = vec![provider_wallet];
4176        let executors = vec![provider_wallet];
4177        let rpc_url = anvil.endpoint_url();
4178
4179        let ops_timelock_addr = deploy_ops_timelock(
4180            &provider,
4181            &mut contracts,
4182            delay,
4183            proposers,
4184            executors,
4185            provider_wallet,
4186        )
4187        .await?;
4188
4189        let token_proxy_addr = deploy_token_proxy(
4190            &provider,
4191            &mut contracts,
4192            provider_wallet,
4193            provider_wallet,
4194            U256::from(10_000_000u64),
4195            "Espresso",
4196            "ESP",
4197        )
4198        .await?;
4199
4200        let genesis_state = LightClientStateSol::dummy_genesis();
4201        let genesis_stake = StakeTableStateSol::dummy_genesis();
4202
4203        let lc_proxy_addr = deploy_light_client_proxy(
4204            &provider,
4205            &mut contracts,
4206            false,
4207            genesis_state,
4208            genesis_stake,
4209            provider_wallet,
4210            Some(provider_wallet),
4211        )
4212        .await?;
4213
4214        let escrow_period = U256::from(crate::DEFAULT_EXIT_ESCROW_PERIOD_SECONDS);
4215        let st_proxy_addr = deploy_stake_table_proxy(
4216            &provider,
4217            &mut contracts,
4218            token_proxy_addr,
4219            lc_proxy_addr,
4220            escrow_period,
4221            provider_wallet,
4222        )
4223        .await?;
4224
4225        let mut args_builder = DeployerArgsBuilder::default();
4226        args_builder
4227            .deployer(provider.clone())
4228            .use_timelock_owner(true)
4229            .rpc_url(rpc_url);
4230        let args = args_builder.build()?;
4231
4232        args.deploy(&mut contracts, Contract::StakeTableV2).await?;
4233
4234        let st_v2 = StakeTableV2::new(st_proxy_addr, &provider);
4235        assert_eq!(st_v2.getVersion().call().await?, (2, 0, 0).into());
4236        assert_eq!(
4237            st_v2.owner().call().await?,
4238            ops_timelock_addr,
4239            "StakeTableProxy should have OpsTimelock as owner after V2 upgrade"
4240        );
4241
4242        Ok(())
4243    }
4244
4245    #[test_log::test(tokio::test)]
4246    async fn test_perform_timelock_operation_unified() -> Result<()> {
4247        use crate::proposals::timelock::{
4248            TimelockOperationParams, TimelockOperationPayload, TimelockOperationType,
4249            perform_timelock_operation,
4250        };
4251
4252        let provider = ProviderBuilder::new().connect_anvil_with_wallet();
4253        let mut contracts = Contracts::new();
4254        let delay = U256::from(0);
4255        let provider_wallet = provider.get_accounts().await?[0];
4256        let another_wallet = provider.get_accounts().await?[1];
4257        let proposers = vec![provider_wallet];
4258        let executors = vec![provider_wallet];
4259
4260        let timelock_addr = deploy_ops_timelock(
4261            &provider,
4262            &mut contracts,
4263            delay,
4264            proposers,
4265            executors,
4266            provider_wallet,
4267        )
4268        .await?;
4269        let timelock = OpsTimelock::new(timelock_addr, &provider);
4270
4271        let fee_contract_proxy_addr =
4272            deploy_fee_contract_proxy(&provider, &mut contracts, timelock_addr).await?;
4273        let proxy = FeeContract::new(fee_contract_proxy_addr, &provider);
4274
4275        let transfer_ownership_data = proxy
4276            .transferOwnership(another_wallet)
4277            .calldata()
4278            .to_owned();
4279
4280        assert_eq!(proxy.owner().call().await?, timelock_addr);
4281
4282        let operation = TimelockOperationPayload {
4283            target: fee_contract_proxy_addr,
4284            value: U256::ZERO,
4285            data: transfer_ownership_data.clone(),
4286            predecessor: B256::ZERO,
4287            salt: B256::ZERO,
4288            delay,
4289        };
4290
4291        // Test Cancel via unified function (schedule and cancel a new operation)
4292        let mut cancel_operation = operation.clone();
4293        cancel_operation.value = U256::from(1);
4294        let cancel_params = TimelockOperationParams::default();
4295        let cancel_operation_id = perform_timelock_operation(
4296            &provider,
4297            Contract::FeeContractProxy,
4298            cancel_operation.clone(),
4299            TimelockOperationType::Schedule,
4300            cancel_params.clone(),
4301        )
4302        .await?;
4303
4304        perform_timelock_operation(
4305            &provider,
4306            Contract::FeeContractProxy,
4307            cancel_operation,
4308            TimelockOperationType::Cancel,
4309            cancel_params,
4310        )
4311        .await?;
4312
4313        assert!(timelock.getTimestamp(cancel_operation_id).call().await? == U256::ZERO);
4314
4315        // Test schedule transfer ownership operation
4316        let params = TimelockOperationParams::default();
4317        let operation_id = perform_timelock_operation(
4318            &provider,
4319            Contract::FeeContractProxy,
4320            operation.clone(),
4321            TimelockOperationType::Schedule,
4322            params,
4323        )
4324        .await?;
4325
4326        assert!(timelock.isOperationPending(operation_id).call().await?);
4327
4328        let params = TimelockOperationParams::default();
4329        perform_timelock_operation(
4330            &provider,
4331            Contract::FeeContractProxy,
4332            operation.clone(),
4333            TimelockOperationType::Execute,
4334            params,
4335        )
4336        .await?;
4337
4338        assert!(timelock.isOperationDone(operation_id).call().await?);
4339
4340        // confirm that the operation occurred on the fee contract
4341        assert_eq!(proxy.owner().call().await?, another_wallet);
4342        Ok(())
4343    }
4344    #[test_log::test(tokio::test)]
4345    async fn test_upgrade_fee_contract_multisig_owner() -> Result<()> {
4346        let multisig_admin = Address::random();
4347        let (anvil, provider, _l1_client) =
4348            ProviderBuilder::new().connect_anvil_with_l1_client()?;
4349        let mut contracts = Contracts::new();
4350        let admin = provider.get_accounts().await?[0];
4351
4352        // Deploy FeeContract proxy
4353        let fee_contract_proxy_addr =
4354            deploy_fee_contract_proxy(&provider, &mut contracts, admin).await?;
4355
4356        // transfer ownership to multisig
4357        let _receipt = transfer_ownership(
4358            &provider,
4359            Contract::FeeContractProxy,
4360            fee_contract_proxy_addr,
4361            multisig_admin,
4362        )
4363        .await?;
4364
4365        // For patch upgrades the implementation must be redeployed; clear the cached address
4366        contracts.remove(&Contract::FeeContract);
4367
4368        // Encode upgrade calldata for the multisig wallet
4369        let calldata = upgrade_fee_contract_multisig_owner(
4370            &provider,
4371            &mut contracts,
4372            MultisigOwnerCheck::Skip,
4373        )
4374        .await?;
4375
4376        tracing::info!(
4377            "Encoded calldata for FeeContractProxy upgrade: to={:#x}, data={}",
4378            calldata.to,
4379            calldata.data
4380        );
4381
4382        assert_eq!(calldata.to, fee_contract_proxy_addr);
4383        // Verify it encodes upgradeToAndCall with empty init data
4384        let fee_impl_addr = contracts.address(Contract::FeeContract).unwrap_or_default();
4385        let expected = crate::encode_function_call(
4386            "upgradeToAndCall(address,bytes)",
4387            vec![fee_impl_addr.to_string(), "0x".to_string()],
4388        )
4389        .unwrap();
4390        assert_eq!(calldata.data, expected);
4391
4392        // Execute the calldata on Anvil as multisig_admin to verify it works
4393        let impersonation_provider = ProviderBuilder::new()
4394            .filler(ImpersonateFiller::new(multisig_admin))
4395            .connect_http(anvil.endpoint_url());
4396        let anvil_provider = AnvilProvider::new(impersonation_provider.clone(), Arc::new(anvil));
4397        anvil_provider.anvil_auto_impersonate_account(true).await?;
4398        anvil_provider
4399            .anvil_set_balance(multisig_admin, parse_ether("100")?)
4400            .await?;
4401
4402        let tx = alloy::rpc::types::TransactionRequest::default()
4403            .to(calldata.to)
4404            .input(calldata.data.into());
4405        let receipt = impersonation_provider
4406            .send_transaction(tx)
4407            .await?
4408            .get_receipt()
4409            .await?;
4410        assert!(receipt.inner.is_success(), "upgrade tx failed");
4411
4412        // Verify the proxy now points to the new implementation
4413        let new_impl = read_proxy_impl(&impersonation_provider, fee_contract_proxy_addr).await?;
4414        assert_eq!(new_impl, fee_impl_addr, "proxy should point to new impl");
4415
4416        Ok(())
4417    }
4418
4419    #[test_log::test(tokio::test)]
4420    async fn test_upgrade_fee_contract_v1_0_1_eoa() -> Result<()> {
4421        let (_anvil, provider, _l1_client) =
4422            ProviderBuilder::new().connect_anvil_with_l1_client()?;
4423        let mut contracts = Contracts::new();
4424        let admin = provider.get_accounts().await?[0];
4425
4426        let fee_contract_proxy_addr =
4427            deploy_fee_contract_proxy(&provider, &mut contracts, admin).await?;
4428        let fee_contract_proxy = FeeContract::new(fee_contract_proxy_addr, &provider);
4429        let curr_version = fee_contract_proxy.getVersion().call().await?;
4430        assert_eq!(curr_version, (1, 0, 1).into()); // since the current version of the contract is 1.0.1 as needed for the patch
4431
4432        let cached_impl_addr = contracts.address(Contract::FeeContract);
4433
4434        // For patch upgrades, we need to clear the cache to allow redeployment
4435        contracts.remove(&Contract::FeeContract);
4436
4437        // Test the upgrade function directly
4438        let receipt = upgrade_fee_v1(&provider, &mut contracts).await?;
4439
4440        assert!(receipt.inner.is_success());
4441
4442        // Verify a new implementation was deployed (if old one existed)
4443        let new_impl_addr = contracts.address(Contract::FeeContract);
4444        if let Some(old_addr) = cached_impl_addr {
4445            assert_ne!(
4446                old_addr,
4447                new_impl_addr.expect("New implementation should be deployed"),
4448                "New implementation should have a different address"
4449            );
4450        }
4451
4452        // Verify the proxy now points to the new implementation
4453        let new_proxy_impl = read_proxy_impl(&provider, fee_contract_proxy_addr).await?;
4454        assert_eq!(
4455            new_proxy_impl,
4456            new_impl_addr.expect("New implementation should be deployed"),
4457            "Proxy should point to the new implementation address"
4458        );
4459
4460        // Verify version is correct (this is already checked in upgrade_fee_v1, but explicit here)
4461        let new_version = fee_contract_proxy.getVersion().call().await?;
4462        assert_eq!(new_version, (1, 0, 1).into());
4463
4464        Ok(())
4465    }
4466
4467    #[test_log::test(tokio::test)]
4468    async fn test_upgrade_light_client_v2_twice_checks_impl_address() -> Result<()> {
4469        let provider = ProviderBuilder::new().connect_anvil_with_wallet();
4470        let mut contracts = Contracts::new();
4471        let blocks_per_epoch = 10;
4472        let epoch_start_block = 22;
4473
4474        // Prepare initialization inputs
4475        let genesis_state = LightClientStateSol::dummy_genesis();
4476        let genesis_stake = StakeTableStateSol::dummy_genesis();
4477        let admin = provider.get_accounts().await?[0];
4478        let prover = Address::random();
4479
4480        // Deploy proxy and V1
4481        let lc_proxy_addr = deploy_light_client_proxy(
4482            &provider,
4483            &mut contracts,
4484            false,
4485            genesis_state.clone(),
4486            genesis_stake.clone(),
4487            admin,
4488            Some(prover),
4489        )
4490        .await?;
4491
4492        // First upgrade to V2
4493        upgrade_light_client_v2(
4494            &provider,
4495            &mut contracts,
4496            false, // is_mock
4497            blocks_per_epoch,
4498            epoch_start_block,
4499        )
4500        .await?;
4501
4502        // Capture the implementation address after first upgrade
4503        let first_impl_addr = read_proxy_impl(&provider, lc_proxy_addr).await?;
4504
4505        // Also capture what's in the cache
4506        let cached_impl_addr = contracts.address(Contract::LightClientV2);
4507        assert_eq!(
4508            first_impl_addr,
4509            cached_impl_addr.expect("LightClientV2 should be in cache"),
4510            "First upgrade: proxy should point to cached implementation"
4511        );
4512
4513        // Second upgrade to V2 (re-applying same version)
4514        // For patch upgrades, we need to clear the cache to allow redeployment
4515        contracts.remove(&Contract::LightClientV2);
4516
4517        // Second upgrade to V2 (re-applying same version)
4518        upgrade_light_client_v2(
4519            &provider,
4520            &mut contracts,
4521            false, // is_mock
4522            blocks_per_epoch,
4523            epoch_start_block,
4524        )
4525        .await?;
4526
4527        // Check if implementation address changed
4528        let second_impl_addr = read_proxy_impl(&provider, lc_proxy_addr).await?;
4529
4530        let cached_impl_addr_after = contracts.address(Contract::LightClientV2);
4531        assert_ne!(
4532            first_impl_addr, second_impl_addr,
4533            "LightClientV2 should have been deployed again"
4534        );
4535        assert_eq!(
4536            second_impl_addr,
4537            cached_impl_addr_after.expect("LightClientV2 should still be in cache"),
4538            "Second upgrade: proxy should point to cached implementation"
4539        );
4540
4541        Ok(())
4542    }
4543
4544    #[test_log::test(tokio::test)]
4545    async fn test_encode_multisig_transaction() -> Result<()> {
4546        let (anvil, provider, _l1_client) =
4547            ProviderBuilder::new().connect_anvil_with_l1_client()?;
4548        let mut contracts = Contracts::new();
4549        let provider_wallet = provider.get_accounts().await?[0];
4550
4551        let fee_contract_proxy_addr =
4552            deploy_fee_contract_proxy(&provider, &mut contracts, provider_wallet).await?;
4553        let new_owner = Address::random();
4554
4555        // Use DeployerArgsBuilder to test encode_multisig_transaction
4556        use builder::DeployerArgsBuilder;
4557
4558        let mut args_builder = DeployerArgsBuilder::default();
4559        args_builder
4560            .deployer(provider.clone())
4561            .rpc_url(anvil.endpoint_url())
4562            .multisig(provider_wallet)
4563            .multisig_transaction_target(fee_contract_proxy_addr)
4564            .multisig_transaction_function_signature("transferOwnership(address)".to_string())
4565            .multisig_transaction_function_args(vec![new_owner.to_string()])
4566            .multisig_transaction_value("0".to_string());
4567
4568        let args = args_builder.build()?;
4569
4570        let result = args.encode_multisig_transaction().await;
4571
4572        match result {
4573            Ok(_) => {
4574                tracing::info!("Multisig transaction proposal succeeded in dry_run mode");
4575                tracing::info!("Result: {:?}", result);
4576            },
4577            Err(e) => {
4578                tracing::info!("Multisig transaction proposal failed: {}", e);
4579            },
4580        }
4581
4582        Ok(())
4583    }
4584
4585    #[test_log::test(tokio::test)]
4586    async fn transfer_ownership_to_timelock_target_eoa_fails() -> Result<()> {
4587        let provider = ProviderBuilder::new().connect_anvil_with_wallet();
4588        let multisig = Address::random();
4589        let eoa_address = Address::random();
4590
4591        // Set up contracts with an EOA address registered as the OpsTimelock
4592        let mut contracts = Contracts::new();
4593        contracts.insert(Contract::OpsTimelock, eoa_address);
4594
4595        // Deploy a FeeContractProxy so there's a target contract
4596        let admin = provider.get_accounts().await?[0];
4597        deploy_fee_contract_proxy(&provider, &mut contracts, admin).await?;
4598
4599        let mut args_builder = DeployerArgsBuilder::default();
4600        args_builder
4601            .deployer(provider.clone())
4602            .rpc_url(Url::parse("http://localhost:8545")?)
4603            .multisig(multisig)
4604            .target_contract(OwnableContract::FeeContractProxy);
4605
4606        let args = args_builder.build()?;
4607        assert!(
4608            args.encode_transfer_ownership_to_timelock(&mut contracts)
4609                .await
4610                .unwrap_err()
4611                .to_string()
4612                .contains("Timelock address is not a contract")
4613        );
4614
4615        Ok(())
4616    }
4617}