Skip to content

Commit d346d33

Browse files
gregorydemayclaude
andcommitted
fix(cketh): report the ETH minimum deposit in get_minter_info
The live harness read the ETH scan minimum through deposit_eth, which registers the caller's deposit address as a side effect. The minter now reports the minimum as a backward-compatible minimum_eth_deposit_amount field in get_minter_info, and the harness reads both asset minimums through one side-effect-free method generic over Into<Asset>. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EaB5zAqurYgyPNDyYkcqoD
1 parent 49c0d40 commit d346d33

7 files changed

Lines changed: 57 additions & 42 deletions

File tree

rs/ethereum/cketh/minter/cketh_minter.did

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,10 @@ type MinterInfo = record {
254254
// balance scan to treat it as a deposit candidate. A smaller balance is never detected.
255255
minimum_deposit_amounts : opt vec record { erc20_contract_address: text; minimum_deposit_amount: nat};
256256

257+
// Minimum balance, in wei, that a deposit address must hold for the
258+
// balance scan to treat it as an ETH deposit candidate. A smaller balance is never detected.
259+
minimum_eth_deposit_amount : opt nat;
260+
257261
// Last scraped block number for logs of the ETH helper contract.
258262
last_eth_scraped_block_number: opt nat;
259263

rs/ethereum/cketh/minter/src/endpoints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ pub struct MinterInfo {
8787
pub last_gas_fee_estimate: Option<GasFeeEstimate>,
8888
pub erc20_balances: Option<Vec<Erc20Balance>>,
8989
pub minimum_deposit_amounts: Option<Vec<Erc20MinimumDeposit>>,
90+
pub minimum_eth_deposit_amount: Option<Nat>,
9091
pub last_eth_scraped_block_number: Option<Nat>,
9192
pub last_erc20_scraped_block_number: Option<Nat>,
9293
pub last_deposit_with_subaccount_scraped_block_number: Option<Nat>,

rs/ethereum/cketh/minter/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,7 @@ async fn get_minter_info() -> MinterInfo {
411411
),
412412
erc20_balances,
413413
minimum_deposit_amounts,
414+
minimum_eth_deposit_amount: Some(min_deposit(&Asset::Eth).into()),
414415
last_eth_scraped_block_number,
415416
last_erc20_scraped_block_number,
416417
last_deposit_with_subaccount_scraped_block_number,

rs/ethereum/cketh/minter/tests/ckerc20.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2365,6 +2365,7 @@ fn should_retrieve_minter_info() {
23652365
.collect();
23662366

23672367
const USD_STABLECOIN_MINIMUM_DEPOSIT: u64 = 10_000_000;
2368+
const MINIMUM_ETH_DEPOSIT_WEI: u64 = 5_000_000_000_000_000;
23682369
let minimum_deposit_amounts = supported_ckerc20_tokens
23692370
.iter()
23702371
.map(|token| Erc20MinimumDeposit {
@@ -2398,6 +2399,7 @@ fn should_retrieve_minter_info() {
23982399
last_gas_fee_estimate: None,
23992400
erc20_balances: Some(erc20_balances),
24002401
minimum_deposit_amounts: Some(minimum_deposit_amounts),
2402+
minimum_eth_deposit_amount: Some(Nat::from(MINIMUM_ETH_DEPOSIT_WEI)),
24012403
last_eth_scraped_block_number: Some(LAST_SCRAPED_BLOCK_NUMBER_AT_INSTALL.into()),
24022404
last_erc20_scraped_block_number: Some(LAST_SCRAPED_BLOCK_NUMBER_AT_INSTALL.into()),
24032405
last_deposit_with_subaccount_scraped_block_number: Some(

rs/ethereum/cketh/minter/tests/cketh.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1192,6 +1192,8 @@ fn should_derive_minter_address() {
11921192
#[allow(deprecated)]
11931193
#[test]
11941194
fn should_retrieve_minter_info() {
1195+
const MINIMUM_ETH_DEPOSIT_WEI: u64 = 5_000_000_000_000_000;
1196+
11951197
let cketh = CkEthSetup::default();
11961198
let max_eth_logs_block_range = cketh.max_logs_block_range();
11971199
let caller: Principal = cketh.caller.into();
@@ -1223,6 +1225,7 @@ fn should_retrieve_minter_info() {
12231225
last_gas_fee_estimate: None,
12241226
erc20_balances: None,
12251227
minimum_deposit_amounts: None,
1228+
minimum_eth_deposit_amount: Some(Nat::from(MINIMUM_ETH_DEPOSIT_WEI)),
12261229
last_eth_scraped_block_number: Some(LAST_SCRAPED_BLOCK_NUMBER_AT_INSTALL.into()),
12271230
last_erc20_scraped_block_number: Some(LAST_SCRAPED_BLOCK_NUMBER_AT_INSTALL.into()),
12281231
last_deposit_with_subaccount_scraped_block_number: Some(

rs/ethereum/cketh/minter/tests/deposit_from_cex.rs

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
//! principal and subaccount.
99
1010
use assert_matches::assert_matches;
11+
use ic_cketh_minter::asset::Asset;
1112
use ic_cketh_minter::balance_scan::batcher::{
1213
BalanceOfCall, MAX_CALLS_PER_BATCH, decode_balance_batch, encode_balance_batch,
1314
encode_eth_balance_batch,
@@ -21,7 +22,7 @@ use ic_cketh_test_utils::anvil::{
2122
};
2223
use ic_cketh_test_utils::ckerc20::{CkErc20Setup, Erc20Token};
2324
use ic_cketh_test_utils::live::{
24-
CexDeposit, DepositPlan, EthCexDeposit, EthDepositPlan, LiveSetup,
25+
CexDeposit, DepositPlan, EthCexDeposit, EthDepositPlan, LiveSetup, contract_address,
2526
};
2627
use ic_cketh_test_utils::{CkEthSetup, SWEEPER_ADDRESS};
2728
use ic_ethereum_types::Address;
@@ -305,9 +306,9 @@ fn should_flag_only_erc20_deposits_at_or_above_the_per_token_minimum() {
305306
.supported_erc20_tokens_owned()
306307
.try_into()
307308
.expect("expected exactly 2 supported tokens");
308-
let usdt_minimum = setup.minimum_deposit_amount(&usdt);
309+
let usdt_minimum = setup.minimum_deposit_amount(contract_address(&usdt));
309310
let usdt_above_minimum = 2 * usdt_minimum;
310-
let usdc_at_minimum = setup.minimum_deposit_amount(&usdc);
311+
let usdc_at_minimum = setup.minimum_deposit_amount(contract_address(&usdc));
311312
let usdt_below_minimum = usdt_minimum / 10;
312313
let plans = [
313314
(setup.depositor(1), usdt.clone(), usdt_above_minimum),
@@ -372,9 +373,9 @@ fn should_credit_mixed_erc20_and_eth_deposits_through_one_sweep_per_asset() {
372373
let erc20_only = (setup.depositor(1), [1_u8; 32]);
373374
let both_assets = (setup.depositor(2), [2_u8; 32]);
374375
let eth_only = (setup.depositor(3), [3_u8; 32]);
375-
let usdc_amount = 3 * setup.minimum_deposit_amount(&usdc);
376-
let usdt_amount = 5 * setup.minimum_deposit_amount(&usdt);
377-
let eth_minimum = setup.minimum_eth_deposit_amount(both_assets.0, both_assets.1);
376+
let usdc_amount = 3 * setup.minimum_deposit_amount(contract_address(&usdc));
377+
let usdt_amount = 5 * setup.minimum_deposit_amount(contract_address(&usdt));
378+
let eth_minimum = setup.minimum_deposit_amount(Asset::Eth);
378379
let both_eth_amount = 4 * eth_minimum;
379380
let eth_only_amount = 7 * eth_minimum;
380381

@@ -463,7 +464,7 @@ fn should_flag_only_eth_deposits_at_or_above_the_minimum() {
463464
const DEPOSIT_SUBACCOUNT: [u8; 32] = [42; 32];
464465

465466
let setup = LiveSetup::<CkErc20Setup>::new();
466-
let minimum = setup.minimum_eth_deposit_amount(setup.depositor(1), DEPOSIT_SUBACCOUNT);
467+
let minimum = setup.minimum_deposit_amount(Asset::Eth);
467468
let above_minimum = 2 * minimum;
468469
let at_minimum = minimum;
469470
let below_minimum = minimum / 10;
@@ -543,8 +544,8 @@ fn should_credit_twenty_erc20_deposits_through_one_sweep_per_token() {
543544
.supported_erc20_tokens_owned()
544545
.try_into()
545546
.expect("expected exactly 2 supported tokens");
546-
let usdc_deposit = 10 * setup.minimum_deposit_amount(&usdc);
547-
let usdt_deposit = 15 * setup.minimum_deposit_amount(&usdt);
547+
let usdc_deposit = 10 * setup.minimum_deposit_amount(contract_address(&usdc));
548+
let usdt_deposit = 15 * setup.minimum_deposit_amount(contract_address(&usdt));
548549

549550
// Every depositor gets a distinct principal and a distinct subaccount, so no two share a
550551
// deposit address and each attestation binds a different account.
@@ -607,7 +608,7 @@ fn should_credit_twenty_eth_deposits_through_ten_deposit_sweeps() {
607608
let funded_gas = setup.anvil_eth_balance(&sweeper);
608609
let delegate = setup.sweep_contracts().delegate;
609610
let minter_eth_before = setup.minter_eth_balance();
610-
let eth_minimum = setup.minimum_eth_deposit_amount(setup.depositor(0), [0; 32]);
611+
let eth_minimum = setup.minimum_deposit_amount(Asset::Eth);
611612

612613
let plans: Vec<EthDepositPlan> = (0..DEPOSITORS)
613614
.map(|index| EthDepositPlan {
@@ -660,7 +661,7 @@ fn should_sweep_a_second_eth_deposit_despite_resending_a_stale_authorization() {
660661
let mints_before = setup
661662
.minter_count_events(|event| matches!(event.payload, EventPayload::MintedCkEth { .. }));
662663
let owner = setup.depositor(1);
663-
let eth_minimum = setup.minimum_eth_deposit_amount(owner, DEPOSIT_SUBACCOUNT);
664+
let eth_minimum = setup.minimum_deposit_amount(Asset::Eth);
664665

665666
let (setup, first_deposits) = setup
666667
.call_minter_deposit_eth([EthDepositPlan {
@@ -759,7 +760,7 @@ fn should_sweep_a_second_erc20_deposit_despite_resending_a_stale_authorization()
759760
.supported_erc20_tokens_owned()
760761
.try_into()
761762
.expect("expected exactly 2 supported tokens");
762-
let usdc_minimum = setup.minimum_deposit_amount(&usdc);
763+
let usdc_minimum = setup.minimum_deposit_amount(contract_address(&usdc));
763764
let owner = setup.depositor(1);
764765

765766
let (setup, first_deposits) = setup

rs/ethereum/cketh/test_utils/src/live.rs

Lines changed: 33 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
5151
use candid::{Decode, Encode, Nat, Principal};
5252
use ic_base_types::PrincipalId;
53+
use ic_cketh_minter::asset::Asset;
5354
use ic_cketh_minter::endpoints::events::{
5455
Asset as EventAsset, Event, EventPayload, TransactionStatus,
5556
};
@@ -253,36 +254,38 @@ impl LiveSetup<CkErc20Setup> {
253254
self.fixture.supported_erc20_tokens.clone()
254255
}
255256

256-
pub fn minimum_deposit_amount(&self, token: &Erc20Token) -> u128 {
257-
let minimum = self
258-
.get_minter_info()
259-
.minimum_deposit_amounts
260-
.expect("BUG: the minter reports no minimum deposit amounts")
261-
.into_iter()
262-
.find(|minimum| {
263-
Address::from_str(&minimum.erc20_contract_address)
264-
.expect("BUG: the minter reported an invalid token address")
265-
== contract_address(token)
266-
})
267-
.unwrap_or_else(|| {
268-
panic!(
269-
"BUG: the minter reports no minimum deposit amount for {}",
270-
token.contract.address
271-
)
272-
});
273-
let no_minimum_sentinel: Nat = Erc20Value::MAX.into();
274-
assert_ne!(
275-
minimum.minimum_deposit_amount, no_minimum_sentinel,
276-
"the minter reports no real minimum deposit amount for {}",
277-
token.contract.address
278-
);
279-
nat_to_u128(minimum.minimum_deposit_amount)
280-
}
281-
282-
/// The ETH minimum as `deposit_eth` reports it. Read through `caller`'s own registration,
283-
/// which `deposit_eth` makes idempotently, so pass an account the test deposits for anyway.
284-
pub fn minimum_eth_deposit_amount(&self, caller: Principal, subaccount: [u8; 32]) -> u128 {
285-
nat_to_u128(self.deposit_eth(caller, subaccount).minimum_deposit_amount)
257+
/// The minimum balance `asset`'s deposit address must hold for the balance scan to flag it,
258+
/// as `get_minter_info` reports it.
259+
pub fn minimum_deposit_amount(&self, asset: impl Into<Asset>) -> u128 {
260+
let info = self.get_minter_info();
261+
let minimum = match asset.into() {
262+
Asset::Eth => info
263+
.minimum_eth_deposit_amount
264+
.expect("BUG: the minter reports no ETH minimum deposit amount"),
265+
Asset::Erc20(token_address) => {
266+
let minimum = info
267+
.minimum_deposit_amounts
268+
.expect("BUG: the minter reports no minimum deposit amounts")
269+
.into_iter()
270+
.find(|minimum| {
271+
Address::from_str(&minimum.erc20_contract_address)
272+
.expect("BUG: the minter reported an invalid token address")
273+
== token_address
274+
})
275+
.unwrap_or_else(|| {
276+
panic!(
277+
"BUG: the minter reports no minimum deposit amount for {token_address}"
278+
)
279+
});
280+
let no_minimum_sentinel: Nat = Erc20Value::MAX.into();
281+
assert_ne!(
282+
minimum.minimum_deposit_amount, no_minimum_sentinel,
283+
"the minter reports no real minimum deposit amount for {token_address}"
284+
);
285+
minimum.minimum_deposit_amount
286+
}
287+
};
288+
nat_to_u128(minimum)
286289
}
287290

288291
/// Calls `deposit_eth` as `caller`, which registers (idempotently) that user's

0 commit comments

Comments
 (0)