Skip to content
Closed
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion rs/ethereum/cketh/minter/src/ledger_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,36 @@ impl LedgerClient {
from: Account,
amount: A,
memo: BurnMemo,
) -> Result<LedgerBurnIndex, LedgerBurnError> {
self.burn_from_with_spender(from, None, amount, memo).await
}

pub async fn burn_from_own_subaccount<A: Into<Nat>>(
&self,
subaccount: [u8; 32],
amount: A,
memo: BurnMemo,
) -> Result<LedgerBurnIndex, LedgerBurnError> {
let from = Account {
owner: ic_cdk::api::canister_self(),
subaccount: Some(subaccount),
};
self.burn_from_with_spender(from, Some(subaccount), amount, memo)
.await
}

async fn burn_from_with_spender<A: Into<Nat>>(
&self,
from: Account,
spender_subaccount: Option<[u8; 32]>,
amount: A,
memo: BurnMemo,
) -> Result<LedgerBurnIndex, LedgerBurnError> {
let amount = amount.into();
match self
.client
.transfer_from(TransferFromArgs {
spender_subaccount: None,
spender_subaccount,
from,
to: ic_cdk::api::canister_self().into(),
amount: amount.clone(),
Expand Down
7 changes: 7 additions & 0 deletions rs/ethereum/cketh/minter/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,10 @@ pub const EVM_RPC_ID_PRODUCTION: Principal =
Principal::from_slice(&[0, 0, 0, 0, 2, 48, 0, 204, 1, 1]);
pub const EVM_RPC_ID_STAGING: Principal = Principal::from_slice(&[0, 0, 0, 0, 2, 48, 0, 161, 1, 1]);
pub const CKETH_LEDGER_MEMO_SIZE: u16 = 80;

pub const CKETH_FEE_SUBACCOUNT: [u8; 32] = {
let mut subaccount = [0_u8; 32];
subaccount[30] = 0x0f;
subaccount[31] = 0xee;
subaccount
};
84 changes: 84 additions & 0 deletions rs/ethereum/cketh/minter/tests/cketh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1344,3 +1344,87 @@ mod cketh_evm_rpc {
}
}
}

/// The fee subaccount is the minter's own, so burning from it is a self-spend: the ledger accepts
/// `{minter, 0fee}` as the spender without an allowance. These two tests pin that, since the
/// sweeper funding in `ledger_client` depends on it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

  1. isn't this testing more the ledger than the minter?
  2. Maybe better to combine both tests to have a single CkEthSetup

mod fee_account {
use super::*;
use ic_cketh_minter::CKETH_FEE_SUBACCOUNT;
use icrc_ledger_types::icrc2::transfer_from::{TransferFromArgs, TransferFromError};

const FUNDING_AMOUNT: u64 = 1_000_000_000_000_000_000;

fn burn_from_fee_account(
cketh: &CkEthSetup,
amount: u64,
spender_subaccount: Option<[u8; 32]>,
) -> Result<Nat, TransferFromError> {
cketh.call_ledger_transfer_from(TransferFromArgs {
spender_subaccount,
from: cketh.fee_account(),
to: cketh.minting_account(),
amount: Nat::from(amount),
fee: None,
memo: None,
created_at_time: None,
})
}

fn setup_with_funded_fee_account() -> CkEthSetup {
let cketh = CkEthSetup::default();
cketh
.call_ledger_mint(cketh.fee_account(), FUNDING_AMOUNT)
.expect("minting into the fee account must succeed");
cketh
}

#[test]
fn should_burn_from_fee_account_without_an_allowance() {
let cketh = setup_with_funded_fee_account();
assert_eq!(
cketh.balance_of(cketh.fee_account()),
Nat::from(FUNDING_AMOUNT)
);

let supply_before = cketh.total_supply();
let burn_amount = FUNDING_AMOUNT / 4;

let result = burn_from_fee_account(&cketh, burn_amount, Some(CKETH_FEE_SUBACCOUNT));

assert!(
result.is_ok(),
"burning from the fee account while naming it as the spender must need no allowance, \
got {result:?}"
);
assert_eq!(
cketh.balance_of(cketh.fee_account()),
Nat::from(FUNDING_AMOUNT - burn_amount),
"the fee account must be debited by exactly the burned amount (burns are fee-free)"
);
assert_eq!(
cketh.total_supply(),
supply_before - Nat::from(burn_amount),
"a transfer to the minting account must reduce total supply, i.e. be a real burn"
);
}

#[test]
fn should_reject_burning_from_fee_account_with_the_default_spender_subaccount() {
let cketh = setup_with_funded_fee_account();

let result = burn_from_fee_account(&cketh, FUNDING_AMOUNT / 4, None);

assert_matches!(
result,
Err(TransferFromError::InsufficientAllowance { .. }),
"spending {{minter, None}} against {{minter, 0fee}} must be rejected for want of an \
allowance"
);
assert_eq!(
cketh.balance_of(cketh.fee_account()),
Nat::from(FUNDING_AMOUNT),
"a rejected burn must not move funds"
);
}
}
83 changes: 83 additions & 0 deletions rs/ethereum/cketh/test_utils/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use ic_cketh_minter::endpoints::{
use ic_cketh_minter::lifecycle::upgrade::UpgradeArg;
use ic_cketh_minter::logs::Log;
use ic_cketh_minter::{
CKETH_FEE_SUBACCOUNT,
endpoints::{CandidBlockTag, Eip1559TransactionPrice},
lifecycle::{EthereumNetwork, MinterArg, init::InitArg as MinterInitArgs},
};
Expand All @@ -30,7 +31,9 @@ use ic_test_utilities_load_wasm::load_wasm;
use ic_types::ingress::{IngressState, IngressStatus};
use ic_types_cycles::Cycles;
use icrc_ledger_types::icrc1::account::Account;
use icrc_ledger_types::icrc1::transfer::{TransferArg, TransferError};
use icrc_ledger_types::icrc2::approve::{ApproveArgs, ApproveError};
use icrc_ledger_types::icrc2::transfer_from::{TransferFromArgs, TransferFromError};
use num_traits::cast::ToPrimitive;
use std::path::PathBuf;
use std::str::FromStr;
Expand Down Expand Up @@ -265,6 +268,34 @@ impl CkEthSetup {
.unwrap()
}

pub fn total_supply(&self) -> Nat {
Decode!(
&assert_reply(
self.env
.query(self.ledger_id, "icrc1_total_supply", Encode!().unwrap())
.expect("failed to query the total supply on the ledger")
),
Nat
)
.unwrap()
}

/// The ledger's minting account: the minter with no subaccount. Transferring there burns.
pub fn minting_account(&self) -> Account {
Account {
owner: self.minter_id.into(),
subaccount: None,
}
}

/// The minter's fee subaccount, where the ckETH taken as withdrawal fees accumulates.
pub fn fee_account(&self) -> Account {
Account {
owner: self.minter_id.into(),
subaccount: Some(CKETH_FEE_SUBACCOUNT),
}
}

pub fn eip_1559_transaction_price(
&self,
ledger_id: Option<Principal>,
Expand Down Expand Up @@ -333,6 +364,58 @@ impl CkEthSetup {
.unwrap()
}

/// Mints `amount` into `to` by transferring out of the ledger's minting account, which is the
/// minter itself. Lets a test start from a funded account without driving a whole deposit.
pub fn call_ledger_mint(
&self,
to: impl Into<Account>,
amount: u64,
) -> Result<Nat, TransferError> {
Decode!(
&assert_reply(
self.env
.execute_ingress_as(
PrincipalId::from(Principal::from(self.minter_id)),
self.ledger_id,
"icrc1_transfer",
Encode!(&TransferArg {
from_subaccount: None,
to: to.into(),
fee: None,
created_at_time: None,
memo: None,
amount: Nat::from(amount),
})
.unwrap(),
)
.expect("failed to execute a mint on the ledger")
),
Result<Nat, TransferError>
)
.unwrap()
}

/// Calls `icrc2_transfer_from` on the ckETH ledger as the minter.
pub fn call_ledger_transfer_from(
&self,
args: TransferFromArgs,
) -> Result<Nat, TransferFromError> {
Decode!(
&assert_reply(
self.env
.execute_ingress_as(
PrincipalId::from(Principal::from(self.minter_id)),
self.ledger_id,
"icrc2_transfer_from",
Encode!(&args).unwrap(),
)
.expect("failed to execute icrc2_transfer_from on the ledger")
),
Result<Nat, TransferFromError>
)
.unwrap()
}

pub fn call_ledger_approve_minter(
self,
from: Principal,
Expand Down
Loading