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 @@
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,
}
}