Skip to main content

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