Skip to content
Closed
Show file tree
Hide file tree
Changes from 11 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
};
49 changes: 49 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,52 @@ mod cketh_evm_rpc {
}
}
}

/// Burning from the minter's own fee subaccount, end to end: a self-spend needing no allowance,
/// paying no fee, and reducing total supply because the destination is the minting account. That
/// combination is what the sweeper funding in `ledger_client` relies on.
///
/// The underlying ledger rule — that a spender may spend from an account only when it names the
/// account's own subaccount — belongs to every ledger, and is covered generically in the ledger
/// state machine tests (DEFI-2978) rather than here.
#[test]
fn should_burn_from_fee_account_without_an_allowance() {
use ic_cketh_minter::CKETH_FEE_SUBACCOUNT;
use icrc_ledger_types::icrc2::transfer_from::TransferFromArgs;

const FUNDING_AMOUNT: u64 = 1_000_000_000_000_000_000;

let cketh = CkEthSetup::default();
cketh
.call_ledger_mint(cketh.fee_account(), FUNDING_AMOUNT)
.expect("minting into the fee account must succeed");

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

let result = cketh.call_ledger_transfer_from(TransferFromArgs {
spender_subaccount: Some(CKETH_FEE_SUBACCOUNT),
from: cketh.fee_account(),
to: cketh.minting_account(),
amount: Nat::from(burn_amount),
fee: None,
memo: None,
created_at_time: None,
});

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"
);
}
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