diff --git a/rs/ethereum/cketh/minter/cketh_minter.did b/rs/ethereum/cketh/minter/cketh_minter.did index 95c12594059d..c984ac2a8401 100644 --- a/rs/ethereum/cketh/minter/cketh_minter.did +++ b/rs/ethereum/cketh/minter/cketh_minter.did @@ -109,6 +109,9 @@ type InitArg = record { // with the Ethereum blockchain. If not specified, uses the production or // staging EVM RPC canister based on the ethereum_network field. evm_rpc_id : opt principal; + + // Address of the sweeper smart contract. + ethereum_sweeper_contract_address : opt text; }; type UpgradeArg = record { @@ -143,6 +146,9 @@ type UpgradeArg = record { // Change the last scraped block number of the deposit with subaccount helper smart contract. last_deposit_with_subaccount_scraped_block_number : opt nat; + + // Change the sweeper smart contract address. + ethereum_sweeper_contract_address : opt text; }; type MinterArg = variant { UpgradeArg : UpgradeArg; InitArg : InitArg }; @@ -202,6 +208,9 @@ type MinterInfo = record { // Address of the ETH or ERC20 deposit with subaccount helper smart contract. deposit_with_subaccount_helper_contract_address: opt text; + // Address of the sweeper smart contract. + sweeper_contract_address: opt text; + // Information of supported ERC20 tokens. supported_ckerc20_tokens: opt vec CkErc20Token; diff --git a/rs/ethereum/cketh/minter/src/dashboard.rs b/rs/ethereum/cketh/minter/src/dashboard.rs index 9e4908e037ca..d64afd0c3903 100644 --- a/rs/ethereum/cketh/minter/src/dashboard.rs +++ b/rs/ethereum/cketh/minter/src/dashboard.rs @@ -279,6 +279,7 @@ pub struct DashboardTemplate { pub ethereum_network: EthereumNetwork, pub ecdsa_key_name: String, pub minter_address: String, + pub sweeper_contract_address: Option
, pub log_scrapings: LogScrapings, pub next_transaction_nonce: TransactionNonce, pub minimum_withdrawal_amount: Wei, @@ -482,6 +483,7 @@ impl DashboardTemplate { .minter_address() .map(|addr| addr.to_string()) .unwrap_or_default(), + sweeper_contract_address: state.sweeper_contract_address, log_scrapings: state.log_scrapings.clone(), cketh_ledger_id: state.cketh_ledger_id, next_transaction_nonce: state.eth_transactions.next_transaction_nonce(), diff --git a/rs/ethereum/cketh/minter/src/dashboard/tests.rs b/rs/ethereum/cketh/minter/src/dashboard/tests.rs index c2dd88646e51..b6adf2332b7e 100644 --- a/rs/ethereum/cketh/minter/src/dashboard/tests.rs +++ b/rs/ethereum/cketh/minter/src/dashboard/tests.rs @@ -47,7 +47,18 @@ fn should_display_metadata() { .has_minimum_withdrawal_amount("10_000_000_000_000_000") .has_eth_balance("0") .has_total_effective_tx_fees("0") - .has_total_unspent_tx_fees("0"); + .has_total_unspent_tx_fees("0") + .has_no_elements_matching("#sweeper-contract-address"); + + let dashboard = DashboardTemplate { + sweeper_contract_address: Some( + Address::from_str("0x2D39863d30716aaf2B7fFFd85Dd03Dda2BFC2E38").unwrap(), + ), + ..dashboard + }; + + DashboardAssert::assert_that(dashboard) + .has_sweeper_contract_address("0x2D39863d30716aaf2B7fFFd85Dd03Dda2BFC2E38"); } #[test] @@ -147,8 +158,7 @@ fn should_display_helper_smart_contracts() { ) { dashboard .log_scrapings - .set_contract_address(id, contract_address.parse().unwrap()) - .unwrap(); + .set_contract_address(id, contract_address.parse().unwrap()); dashboard .log_scrapings .set_last_scraped_block_number(id, BlockNumber::from(last_scraped_block_number)); @@ -1146,6 +1156,7 @@ fn initial_state() -> State { next_transaction_nonce: TransactionNonce::ZERO.into(), last_scraped_block_number: candid::Nat::from(INITIAL_LAST_SCRAPED_BLOCK_NUMBER), evm_rpc_id: None, + ethereum_sweeper_contract_address: None, }) .expect("valid init args") } @@ -1611,6 +1622,14 @@ mod assertions { ) } + pub fn has_sweeper_contract_address(&self, expected_address: &str) -> &Self { + self.has_string_value( + "#sweeper-contract-address > td", + expected_address, + "wrong sweeper contract address", + ) + } + pub fn has_helper_contract( &self, id: LogScrapingId, diff --git a/rs/ethereum/cketh/minter/src/endpoints.rs b/rs/ethereum/cketh/minter/src/endpoints.rs index 98cf64b54aa3..9358f32320bb 100644 --- a/rs/ethereum/cketh/minter/src/endpoints.rs +++ b/rs/ethereum/cketh/minter/src/endpoints.rs @@ -71,6 +71,7 @@ pub struct MinterInfo { pub eth_helper_contract_address: Option, pub erc20_helper_contract_address: Option, pub deposit_with_subaccount_helper_contract_address: Option, + pub sweeper_contract_address: Option, pub supported_ckerc20_tokens: Option>, pub minimum_withdrawal_amount: Option, pub ethereum_block_height: Option, diff --git a/rs/ethereum/cketh/minter/src/eth_logs/tests.rs b/rs/ethereum/cketh/minter/src/eth_logs/tests.rs index 1671f64591d8..cee585f137b5 100644 --- a/rs/ethereum/cketh/minter/src/eth_logs/tests.rs +++ b/rs/ethereum/cketh/minter/src/eth_logs/tests.rs @@ -277,13 +277,10 @@ mod scraping { let last_scraped_block_number = BlockNumber::from(6_970_446_u32); let state = { let mut state = initial_state(); - state - .log_scrapings - .set_contract_address( - LogScrapingId::EthOrErc20DepositWithSubaccount, - CONTRACT_ADDRESS, - ) - .unwrap(); + state.log_scrapings.set_contract_address( + LogScrapingId::EthOrErc20DepositWithSubaccount, + CONTRACT_ADDRESS, + ); state.log_scrapings.set_last_scraped_block_number( LogScrapingId::EthOrErc20DepositWithSubaccount, last_scraped_block_number, diff --git a/rs/ethereum/cketh/minter/src/lifecycle/init.rs b/rs/ethereum/cketh/minter/src/lifecycle/init.rs index 43dae5e6fa0b..1d2b319d3a87 100644 --- a/rs/ethereum/cketh/minter/src/lifecycle/init.rs +++ b/rs/ethereum/cketh/minter/src/lifecycle/init.rs @@ -32,6 +32,8 @@ pub struct InitArg { pub last_scraped_block_number: Nat, #[cbor(n(9), with = "icrc_cbor::principal::option")] pub evm_rpc_id: Option, + #[n(10)] + pub ethereum_sweeper_contract_address: Option, } impl TryFrom for State { @@ -47,6 +49,7 @@ impl TryFrom for State { next_transaction_nonce, last_scraped_block_number, evm_rpc_id, + ethereum_sweeper_contract_address, }: InitArg, ) -> Result { use std::str::FromStr; @@ -59,9 +62,11 @@ impl TryFrom for State { let eth_helper_contract_address = ethereum_contract_address .map(|a| Address::from_str(&a)) .transpose() - .map_err(|e| { - InvalidStateError::InvalidEthereumContractAddress(format!("ERROR: {e}")) - })?; + .map_err(|e| InvalidStateError::InvalidContractAddress(format!("ERROR: {e}")))?; + let sweeper_contract_address = ethereum_sweeper_contract_address + .map(|a| Address::from_str(&a)) + .transpose() + .map_err(|e| InvalidStateError::InvalidContractAddress(format!("ERROR: {e}")))?; let last_scraped_block_number = BlockNumber::try_from(last_scraped_block_number) .map_err(|e| InvalidStateError::InvalidLastScrapedBlockNumber(format!("ERROR: {e}")))?; let first_scraped_block_number = @@ -79,10 +84,7 @@ impl TryFrom for State { let mut log_scrapings = LogScrapings::new(last_scraped_block_number); if let Some(contract_address) = eth_helper_contract_address { log_scrapings - .set_contract_address(LogScrapingId::EthDepositWithoutSubaccount, contract_address) - .map_err(|e| { - InvalidStateError::InvalidEthereumContractAddress(format!("ERROR: {e:?}")) - })?; + .set_contract_address(LogScrapingId::EthDepositWithoutSubaccount, contract_address); } let state = Self { ethereum_network, @@ -110,6 +112,7 @@ impl TryFrom for State { erc20_balances: Default::default(), log_scrapings, automatic_deposits: AutomaticDeposits::default(), + sweeper_contract_address, }; state.validate_config()?; Ok(state) diff --git a/rs/ethereum/cketh/minter/src/lifecycle/tests.rs b/rs/ethereum/cketh/minter/src/lifecycle/tests.rs index bec6fcdf7fe9..efe827cdc68d 100644 --- a/rs/ethereum/cketh/minter/src/lifecycle/tests.rs +++ b/rs/ethereum/cketh/minter/src/lifecycle/tests.rs @@ -6,7 +6,9 @@ mod init { use crate::test_fixtures::valid_init_arg; use assert_matches::assert_matches; use candid::{Nat, Principal}; + use ic_ethereum_types::Address; use num_bigint::BigUint; + use std::str::FromStr; #[test] fn should_fail_when_init_args_invalid() { @@ -31,7 +33,7 @@ mod init { ethereum_contract_address: Some("invalid".to_string()), ..valid_init_arg() }), - Err(InvalidStateError::InvalidEthereumContractAddress(_)) + Err(InvalidStateError::InvalidContractAddress(_)) ); assert_matches!( @@ -41,7 +43,38 @@ mod init { ), ..valid_init_arg() }), - Err(InvalidStateError::InvalidEthereumContractAddress(_)) + Err(InvalidStateError::InvalidContractAddress(_)) + ); + + assert_matches!( + State::try_from(InitArg { + ethereum_sweeper_contract_address: Some("invalid".to_string()), + ..valid_init_arg() + }), + Err(InvalidStateError::InvalidContractAddress(_)) + ); + + assert_matches!( + State::try_from(InitArg { + ethereum_sweeper_contract_address: Some( + "0x0000000000000000000000000000000000000000".to_string(), + ), + ..valid_init_arg() + }), + Err(InvalidStateError::InvalidContractAddress(_)) + ); + + assert_matches!( + State::try_from(InitArg { + ethereum_contract_address: Some( + "0xb44B5e756A894775FC32EDdf3314Bb1B1944dC34".to_string(), + ), + ethereum_sweeper_contract_address: Some( + "0xb44B5e756A894775FC32EDdf3314Bb1B1944dC34".to_string(), + ), + ..valid_init_arg() + }), + Err(InvalidStateError::InvalidContractAddress(_)) ); assert_matches!( @@ -75,7 +108,12 @@ mod init { #[test] fn should_succeed() { - let init_arg = valid_init_arg(); + let init_arg = InitArg { + ethereum_sweeper_contract_address: Some( + "0xb44B5e756A894775FC32EDdf3314Bb1B1944dC34".to_string(), + ), + ..valid_init_arg() + }; let state = State::try_from(init_arg.clone()).expect("valid init args"); @@ -96,5 +134,9 @@ mod init { state.eth_transactions.next_transaction_nonce(), TransactionNonce::ZERO ); + assert_eq!( + state.sweeper_contract_address, + Some(Address::from_str("0xb44B5e756A894775FC32EDdf3314Bb1B1944dC34").unwrap()) + ); } } diff --git a/rs/ethereum/cketh/minter/src/lifecycle/upgrade.rs b/rs/ethereum/cketh/minter/src/lifecycle/upgrade.rs index 609f555ee125..20b0e7daf35c 100644 --- a/rs/ethereum/cketh/minter/src/lifecycle/upgrade.rs +++ b/rs/ethereum/cketh/minter/src/lifecycle/upgrade.rs @@ -30,6 +30,8 @@ pub struct UpgradeArg { pub deposit_with_subaccount_helper_contract_address: Option, #[cbor(n(9), with = "icrc_cbor::nat::option")] pub last_deposit_with_subaccount_scraped_block_number: Option, + #[n(10)] + pub ethereum_sweeper_contract_address: Option, } pub fn post_upgrade(upgrade_args: Option) { diff --git a/rs/ethereum/cketh/minter/src/main.rs b/rs/ethereum/cketh/minter/src/main.rs index 90d848c130f3..d2ec5769c55c 100644 --- a/rs/ethereum/cketh/minter/src/main.rs +++ b/rs/ethereum/cketh/minter/src/main.rs @@ -290,6 +290,7 @@ async fn get_minter_info() -> MinterInfo { eth_helper_contract_address, erc20_helper_contract_address, deposit_with_subaccount_helper_contract_address, + sweeper_contract_address: s.sweeper_contract_address.map(|a| a.to_string()), supported_ckerc20_tokens, minimum_withdrawal_amount: Some(s.cketh_minimum_withdrawal_amount.into()), ethereum_block_height: Some(s.ethereum_block_height.clone()), diff --git a/rs/ethereum/cketh/minter/src/state.rs b/rs/ethereum/cketh/minter/src/state.rs index e43c17681a94..77cb54d238b5 100644 --- a/rs/ethereum/cketh/minter/src/state.rs +++ b/rs/ethereum/cketh/minter/src/state.rs @@ -113,6 +113,10 @@ pub struct State { /// ckERC20 deposit addresses registered via `deposit_erc20`, individually /// derived for each user and watched for incoming deposits. pub automatic_deposits: AutomaticDeposits, + + /// Address of the sweeper smart contract on Ethereum, which the minter + /// delegates to when sweeping funded deposit addresses. + pub sweeper_contract_address: Option
, } #[derive(Eq, PartialEq, Debug)] @@ -120,8 +124,7 @@ pub enum InvalidStateError { InvalidTransactionNonce(String), InvalidEcdsaKeyName(String), InvalidLedgerId(String), - InvalidEthereumContractAddress(String), - InvalidErc20HelperContractAddress(String), + InvalidContractAddress(String), InvalidMinimumWithdrawalAmount(String), InvalidLastScrapedBlockNumber(String), InvalidLastErc20ScrapedBlockNumber(String), @@ -182,6 +185,25 @@ impl State { .to_string(), )); } + + let mut seen_contract_addresses = BTreeSet::new(); + for address in self + .log_scrapings + .iter() + .filter_map(|(_id, scraping)| scraping.contract_address()) + .chain(self.sweeper_contract_address.as_ref()) + { + if address == &Address::ZERO { + return Err(InvalidStateError::InvalidContractAddress( + "contract address must not be zero".to_string(), + )); + } + if !seen_contract_addresses.insert(address) { + return Err(InvalidStateError::InvalidContractAddress(format!( + "contract address {address} is used by more than one contract" + ))); + } + } Ok(()) } @@ -474,6 +496,7 @@ impl State { evm_rpc_id, deposit_with_subaccount_helper_contract_address, last_deposit_with_subaccount_scraped_block_number, + ethereum_sweeper_contract_address, } = upgrade_args; if let Some(nonce) = next_transaction_nonce { let nonce = TransactionNonce::try_from(nonce) @@ -487,30 +510,20 @@ impl State { self.cketh_minimum_withdrawal_amount = minimum_withdrawal_amount; } if let Some(address) = ethereum_contract_address { - let eth_helper_contract_address = Address::from_str(&address).map_err(|e| { - InvalidStateError::InvalidEthereumContractAddress(format!("ERROR: {e}")) - })?; - self.log_scrapings - .set_contract_address( - LogScrapingId::EthDepositWithoutSubaccount, - eth_helper_contract_address, - ) - .map_err(|e| { - InvalidStateError::InvalidEthereumContractAddress(format!("ERROR: {e:?}")) - })?; + let eth_helper_contract_address = Address::from_str(&address) + .map_err(|e| InvalidStateError::InvalidContractAddress(format!("ERROR: {e}")))?; + self.log_scrapings.set_contract_address( + LogScrapingId::EthDepositWithoutSubaccount, + eth_helper_contract_address, + ); } if let Some(address) = erc20_helper_contract_address { - let erc20_helper_contract_address = Address::from_str(&address).map_err(|e| { - InvalidStateError::InvalidErc20HelperContractAddress(format!("ERROR: {e}")) - })?; - self.log_scrapings - .set_contract_address( - LogScrapingId::Erc20DepositWithoutSubaccount, - erc20_helper_contract_address, - ) - .map_err(|e| { - InvalidStateError::InvalidEthereumContractAddress(format!("ERROR: {e:?}")) - })?; + let erc20_helper_contract_address = Address::from_str(&address) + .map_err(|e| InvalidStateError::InvalidContractAddress(format!("ERROR: {e}")))?; + self.log_scrapings.set_contract_address( + LogScrapingId::Erc20DepositWithoutSubaccount, + erc20_helper_contract_address, + ); } if let Some(block_number) = last_erc20_scraped_block_number { self.log_scrapings.set_last_scraped_block_number( @@ -521,14 +534,10 @@ impl State { ); } if let Some(address) = deposit_with_subaccount_helper_contract_address { - let address = Address::from_str(&address).map_err(|e| { - InvalidStateError::InvalidErc20HelperContractAddress(format!("ERROR: {e}")) - })?; + let address = Address::from_str(&address) + .map_err(|e| InvalidStateError::InvalidContractAddress(format!("ERROR: {e}")))?; self.log_scrapings - .set_contract_address(LogScrapingId::EthOrErc20DepositWithSubaccount, address) - .map_err(|e| { - InvalidStateError::InvalidEthereumContractAddress(format!("ERROR: {e:?}")) - })?; + .set_contract_address(LogScrapingId::EthOrErc20DepositWithSubaccount, address); } if let Some(block_number) = last_deposit_with_subaccount_scraped_block_number { self.log_scrapings.set_last_scraped_block_number( @@ -547,6 +556,11 @@ impl State { if let Some(evm_id) = evm_rpc_id { self.evm_rpc_id = evm_id; } + if let Some(address) = ethereum_sweeper_contract_address { + let address = Address::from_str(&address) + .map_err(|e| InvalidStateError::InvalidContractAddress(format!("ERROR: {e}")))?; + self.sweeper_contract_address = Some(address); + } self.validate_config() } @@ -583,6 +597,10 @@ impl State { ); ensure_eq!(self.ckerc20_tokens, other.ckerc20_tokens); ensure_eq!(self.automatic_deposits, other.automatic_deposits); + ensure_eq!( + self.sweeper_contract_address, + other.sweeper_contract_address + ); self.eth_transactions .is_equivalent_to(&other.eth_transactions) diff --git a/rs/ethereum/cketh/minter/src/state/eth_logs_scraping/mod.rs b/rs/ethereum/cketh/minter/src/state/eth_logs_scraping/mod.rs index cfc017acfd7f..6ea7cb2949a0 100644 --- a/rs/ethereum/cketh/minter/src/state/eth_logs_scraping/mod.rs +++ b/rs/ethereum/cketh/minter/src/state/eth_logs_scraping/mod.rs @@ -31,11 +31,7 @@ impl LogScrapings { self.scrapings.iter() } - pub fn set_contract_address( - &mut self, - id: LogScrapingId, - contract_address: Address, - ) -> Result<(), LogScrapingStateError> { + pub fn set_contract_address(&mut self, id: LogScrapingId, contract_address: Address) { self.get_mut(id).set_contract_address(contract_address) } @@ -118,11 +114,6 @@ impl Display for LogScrapingId { } } -#[derive(Clone, PartialEq, Debug)] -pub enum LogScrapingStateError { - InvalidContractAddress(String), -} - #[derive(Clone, Copy, PartialEq, Debug)] #[repr(u8)] pub enum LogScrapingStatus { @@ -159,17 +150,8 @@ impl LogScrapingState { } } - pub fn set_contract_address( - &mut self, - contract_address: Address, - ) -> Result<(), LogScrapingStateError> { - if contract_address == Address::ZERO { - return Err(LogScrapingStateError::InvalidContractAddress( - "contract address must not be zero".to_string(), - )); - } + pub fn set_contract_address(&mut self, contract_address: Address) { self.contract_address = Some(contract_address); - Ok(()) } pub fn set_last_scraped_block_number(&mut self, block_number: BlockNumber) { diff --git a/rs/ethereum/cketh/minter/src/state/eth_logs_scraping/tests.rs b/rs/ethereum/cketh/minter/src/state/eth_logs_scraping/tests.rs index 4eeec72e5b28..0ecdf8a1065c 100644 --- a/rs/ethereum/cketh/minter/src/state/eth_logs_scraping/tests.rs +++ b/rs/ethereum/cketh/minter/src/state/eth_logs_scraping/tests.rs @@ -22,18 +22,14 @@ fn should_not_change_other_field_when_deposit_with_subaccount_present() { } ); - scrapings - .set_contract_address( - LogScrapingId::EthDepositWithoutSubaccount, - ETH_HELPER_SMART_CONTRACT.parse().unwrap(), - ) - .unwrap(); - scrapings - .set_contract_address( - LogScrapingId::Erc20DepositWithoutSubaccount, - ERC20_HELPER_SMART_CONTRACT.parse().unwrap(), - ) - .unwrap(); + scrapings.set_contract_address( + LogScrapingId::EthDepositWithoutSubaccount, + ETH_HELPER_SMART_CONTRACT.parse().unwrap(), + ); + scrapings.set_contract_address( + LogScrapingId::Erc20DepositWithoutSubaccount, + ERC20_HELPER_SMART_CONTRACT.parse().unwrap(), + ); let info_before = scrapings.info(); assert_eq!( @@ -50,14 +46,12 @@ fn should_not_change_other_field_when_deposit_with_subaccount_present() { } ); - scrapings - .set_contract_address( - LogScrapingId::EthOrErc20DepositWithSubaccount, - DEPOSIT_WITH_SUBACCOUNT_HELPER_SMART_CONTRACT - .parse() - .unwrap(), - ) - .unwrap(); + scrapings.set_contract_address( + LogScrapingId::EthOrErc20DepositWithSubaccount, + DEPOSIT_WITH_SUBACCOUNT_HELPER_SMART_CONTRACT + .parse() + .unwrap(), + ); scrapings.set_last_scraped_block_number( LogScrapingId::EthOrErc20DepositWithSubaccount, (LAST_SCRAPED_BLOCK_NUMBER + 1).into(), diff --git a/rs/ethereum/cketh/minter/src/state/tests.rs b/rs/ethereum/cketh/minter/src/state/tests.rs index b5b7d2de7a2d..d23788e56571 100644 --- a/rs/ethereum/cketh/minter/src/state/tests.rs +++ b/rs/ethereum/cketh/minter/src/state/tests.rs @@ -344,7 +344,7 @@ mod upgrade { ethereum_contract_address: Some("invalid".to_string()), ..Default::default() }), - Err(InvalidStateError::InvalidEthereumContractAddress(_)) + Err(InvalidStateError::InvalidContractAddress(_)) ); let mut state = initial_state(); @@ -355,7 +355,61 @@ mod upgrade { ), ..Default::default() }), - Err(InvalidStateError::InvalidEthereumContractAddress(_)) + Err(InvalidStateError::InvalidContractAddress(_)) + ); + + let mut state = initial_state(); + assert_matches!( + state.upgrade(UpgradeArg { + ethereum_sweeper_contract_address: Some("invalid".to_string()), + ..Default::default() + }), + Err(InvalidStateError::InvalidContractAddress(_)) + ); + + let mut state = initial_state(); + assert_matches!( + state.upgrade(UpgradeArg { + ethereum_sweeper_contract_address: Some( + "0x0000000000000000000000000000000000000000".to_string(), + ), + ..Default::default() + }), + Err(InvalidStateError::InvalidContractAddress(_)) + ); + + let mut state = initial_state(); + assert_matches!( + state.upgrade(UpgradeArg { + erc20_helper_contract_address: Some( + "0xb44B5e756A894775FC32EDdf3314Bb1B1944dC34".to_string(), + ), + ethereum_sweeper_contract_address: Some( + "0xb44B5e756A894775FC32EDdf3314Bb1B1944dC34".to_string(), + ), + ..Default::default() + }), + Err(InvalidStateError::InvalidContractAddress(_)) + ); + + let mut state = initial_state(); + state + .upgrade(UpgradeArg { + ethereum_contract_address: Some( + "0xb44B5e756A894775FC32EDdf3314Bb1B1944dC34".to_string(), + ), + ..Default::default() + }) + .expect("valid upgrade args"); + assert_matches!( + state.upgrade(UpgradeArg { + erc20_helper_contract_address: Some( + "0xb44B5e756A894775FC32EDdf3314Bb1B1944dC34".to_string(), + ), + ..Default::default() + }), + Err(InvalidStateError::InvalidContractAddress(_)), + "a contract address already set by an earlier upgrade must stay distinct" ); } @@ -370,6 +424,9 @@ mod upgrade { "0xb44B5e756A894775FC32EDdf3314Bb1B1944dC34".to_string(), ), ethereum_block_height: Some(CandidBlockTag::Safe), + ethereum_sweeper_contract_address: Some( + "0x2D39863d30716aaf2B7fFFd85Dd03Dda2BFC2E38".to_string(), + ), ..Default::default() }; @@ -390,6 +447,10 @@ mod upgrade { Some(&Address::from_str("0xb44B5e756A894775FC32EDdf3314Bb1B1944dC34").unwrap()) ); assert_eq!(state.ethereum_block_height, CandidBlockTag::Safe); + assert_eq!( + state.sweeper_contract_address, + Some(Address::from_str("0x2D39863d30716aaf2B7fFFd85Dd03Dda2BFC2E38").unwrap()) + ); } } @@ -576,6 +637,7 @@ prop_compose! { ecdsa_key_name in "[a-z_]*", last_scraped_block_number in arb_nat(), evm_rpc_id in proptest::option::of(arb_principal()), + sweeper_contract_address in proptest::option::of(arb_address()), ) -> InitArg { InitArg { ethereum_network: EthereumNetwork::Sepolia, @@ -587,6 +649,7 @@ prop_compose! { next_transaction_nonce, last_scraped_block_number, evm_rpc_id, + ethereum_sweeper_contract_address: sweeper_contract_address.map(|addr| addr.to_string()), } } } @@ -603,6 +666,7 @@ prop_compose! { evm_rpc_id in proptest::option::of(arb_principal()), deposit_with_subaccount_helper_contract_address in proptest::option::of(arb_address()), last_deposit_with_subaccount_scraped_block_number in proptest::option::of(arb_nat()), + sweeper_contract_address in proptest::option::of(arb_address()), ) -> UpgradeArg { UpgradeArg { ethereum_contract_address: contract_address.map(|addr| addr.to_string()), @@ -614,7 +678,8 @@ prop_compose! { last_erc20_scraped_block_number, evm_rpc_id, deposit_with_subaccount_helper_contract_address: deposit_with_subaccount_helper_contract_address.map(|addr| addr.to_string()), - last_deposit_with_subaccount_scraped_block_number + last_deposit_with_subaccount_scraped_block_number, + ethereum_sweeper_contract_address: sweeper_contract_address.map(|addr| addr.to_string()), } } } @@ -1076,6 +1141,11 @@ fn state_equivalence() { evm_rpc_id: EVM_RPC_ID_PRODUCTION, ckerc20_tokens, automatic_deposits, + sweeper_contract_address: Some( + "0x2D39863d30716aaf2B7fFFd85Dd03Dda2BFC2E38" + .parse() + .unwrap(), + ), }; assert_eq!( @@ -1169,6 +1239,15 @@ fn state_equivalence() { "changing essential fields should break equivalence", ); + assert_ne!( + Ok(()), + state.is_equivalent_to(&State { + sweeper_contract_address: None, + ..state.clone() + }), + "changing essential fields should break equivalence", + ); + assert_eq!( Ok(()), state.is_equivalent_to(&State { diff --git a/rs/ethereum/cketh/minter/src/test_fixtures.rs b/rs/ethereum/cketh/minter/src/test_fixtures.rs index c9d77c9c7318..4a47f86790f1 100644 --- a/rs/ethereum/cketh/minter/src/test_fixtures.rs +++ b/rs/ethereum/cketh/minter/src/test_fixtures.rs @@ -48,6 +48,7 @@ pub fn valid_init_arg() -> InitArg { next_transaction_nonce: Default::default(), last_scraped_block_number: Default::default(), evm_rpc_id: Some(EVM_RPC_ID_STAGING), + ethereum_sweeper_contract_address: None, } } diff --git a/rs/ethereum/cketh/minter/templates/dashboard.html b/rs/ethereum/cketh/minter/templates/dashboard.html index ca6d37d9e7f4..6514ff926fa7 100644 --- a/rs/ethereum/cketh/minter/templates/dashboard.html +++ b/rs/ethereum/cketh/minter/templates/dashboard.html @@ -111,6 +111,12 @@

Metadata

Minter address {% call etherscan_address_link(minter_address) %}{% endcall %} + {% if sweeper_contract_address.is_some() -%} + + Sweeper contract address + {% call etherscan_address_link(sweeper_contract_address.unwrap()) %}{% endcall %} + + {%- endif %} ckETH ledger canister ID {{ cketh_ledger_id }} diff --git a/rs/ethereum/cketh/minter/tests/ckerc20.rs b/rs/ethereum/cketh/minter/tests/ckerc20.rs index 39d4fab975d1..32a6ea3f7c22 100644 --- a/rs/ethereum/cketh/minter/tests/ckerc20.rs +++ b/rs/ethereum/cketh/minter/tests/ckerc20.rs @@ -2058,6 +2058,7 @@ fn should_retrieve_minter_info() { ERC20_HELPER_CONTRACT_ADDRESS )), deposit_with_subaccount_helper_contract_address: None, + sweeper_contract_address: None, supported_ckerc20_tokens: Some(supported_ckerc20_tokens), minimum_withdrawal_amount: Some(Nat::from(CKETH_MINIMUM_WITHDRAWAL_AMOUNT)), ethereum_block_height: Some(Finalized), diff --git a/rs/ethereum/cketh/minter/tests/cketh.rs b/rs/ethereum/cketh/minter/tests/cketh.rs index 68d569fb3c9e..2fbc9ce804fb 100644 --- a/rs/ethereum/cketh/minter/tests/cketh.rs +++ b/rs/ethereum/cketh/minter/tests/cketh.rs @@ -1211,6 +1211,7 @@ fn should_retrieve_minter_info() { )), erc20_helper_contract_address: None, deposit_with_subaccount_helper_contract_address: None, + sweeper_contract_address: None, supported_ckerc20_tokens: None, minimum_withdrawal_amount: Some(Nat::from(CKETH_MINIMUM_WITHDRAWAL_AMOUNT)), ethereum_block_height: Some(Finalized), @@ -1270,6 +1271,24 @@ fn should_retrieve_minter_info() { ); } +#[test] +fn should_retrieve_sweeper_contract_address_after_upgrade() { + const SWEEPER_CONTRACT_ADDRESS: &str = "0x2D39863d30716aaf2B7fFFd85Dd03Dda2BFC2E38"; + + let cketh = CkEthSetup::default(); + assert_eq!(cketh.get_minter_info().sweeper_contract_address, None); + + let cketh = cketh.check_audit_logs_and_upgrade(UpgradeArg { + ethereum_sweeper_contract_address: Some(SWEEPER_CONTRACT_ADDRESS.to_string()), + ..Default::default() + }); + + assert_eq!( + cketh.get_minter_info().sweeper_contract_address, + Some(format_ethereum_address_to_eip_55(SWEEPER_CONTRACT_ADDRESS)) + ); +} + fn format_ethereum_address_to_eip_55(address: &str) -> String { Address::from_str(address).unwrap().to_string() } diff --git a/rs/ethereum/cketh/test_utils/src/lib.rs b/rs/ethereum/cketh/test_utils/src/lib.rs index 37248e4c6f30..fa0fe5ac1912 100644 --- a/rs/ethereum/cketh/test_utils/src/lib.rs +++ b/rs/ethereum/cketh/test_utils/src/lib.rs @@ -807,6 +807,7 @@ fn install_minter( minimum_withdrawal_amount: CKETH_MINIMUM_WITHDRAWAL_AMOUNT.into(), last_scraped_block_number: LAST_SCRAPED_BLOCK_NUMBER_AT_INSTALL.into(), evm_rpc_id: Some(evm_rpc_id), + ethereum_sweeper_contract_address: None, }; let minter_arg = MinterArg::InitArg(args); env.install_canister( diff --git a/rs/ethereum/cketh/test_utils/src/live_scan.rs b/rs/ethereum/cketh/test_utils/src/live_scan.rs index bde3c936c3ef..29d2f04fc752 100644 --- a/rs/ethereum/cketh/test_utils/src/live_scan.rs +++ b/rs/ethereum/cketh/test_utils/src/live_scan.rs @@ -263,6 +263,7 @@ fn install_minter( next_transaction_nonce: Nat::from(0_u8), last_scraped_block_number: Nat::from(0_u8), evm_rpc_id: Some(evm_rpc_id), + ethereum_sweeper_contract_address: None, }; env.install_canister( minter_id, diff --git a/rs/tests/cross_chain/ic_xc_cketh_test.rs b/rs/tests/cross_chain/ic_xc_cketh_test.rs index 7aa65febe906..182af4c7f819 100644 --- a/rs/tests/cross_chain/ic_xc_cketh_test.rs +++ b/rs/tests/cross_chain/ic_xc_cketh_test.rs @@ -362,6 +362,7 @@ fn minter_init_args(ecdsa_key_id: &EcdsaKeyId, cketh_ledger: Principal) -> Minte next_transaction_nonce: Nat::from(0_u8), last_scraped_block_number: Nat::from(0_u8), evm_rpc_id: None, + ethereum_sweeper_contract_address: None, } }