Liquidium LogoLiquidium
TechnicalArchitecture

ERC Pool Canister

Ethereum asset custody with gas fee fronting and DEX integration

Responsibilities

  • Manage ckERC-20 deposits from users
  • Process withdrawals with gas fee fronting
  • Convert accumulated fees back to ckETH via DEX
  • Handle multi-token support with unified interface

Architecture

Subaccount Architecture

Similar to the BTC Pool, but with Ethereum-specific outflow encoding:

Subaccount Types

TypePrefixPurpose
Inflow Deposit0x1User deposits
Inflow Repay0x2Debt repayments
ETH Outflow0x4Ethereum address withdrawals
Native Outflow0x5IC Principal transfers
FeeInternalGas fee management

Ethereum Outflow Subaccounts

Ethereum addresses are exactly 20 bytes, fitting directly in the subaccount:

[0x4, <11 zero bytes>, <20 bytes of Ethereum address>]

Example for 0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb1:

[0x4, 0x0, ..., 0x74, 0x2d, 0x35, 0xCc, ..., 0xbE, 0xb1]

FEE_SUBACCOUNT

[0x0, 0x2, <30 zero bytes>]

Holds:

  • ckETH: For fronting Ethereum gas fees during withdrawals
  • Pool tokens (e.g., ckUSDT): Accumulated fees from user withdrawals awaiting swap to ckETH to replenish gas reserves

Gas Fee Fronting

Problem: Burning ckERC-20 tokens requires paying Ethereum gas fees in ETH.

Solution: Protocol fronts ckETH for gas, deducts equivalent amount in pool token from withdrawal.

Fee Calculation Flow

Withdrawal with Fee Fronting

Fee Flow Summary:

User requests: 1000 ckUSDC withdrawal
Gas fee (ETH): 0.001 ckETH
Gas fee (USDC): 2.5 ckUSDC
Ledger fees: ~0.5 ckUSDC

Net withdrawal: 997.5 ckUSDC
Fee to FEE_SUBACCOUNT: 2.5 ckUSDC
ckETH fronted: 0.001 ckETH

Automated Fee Recovery (KongSwap)

The pool must replenish its ckETH reserves used for gas fronting.

Fee Swap Flow

Implementation

Every 5 minutes, the pool checks if accumulated fees should be swapped:

async fn swap_fee() {
    // Check FEE_SUBACCOUNT balance
    let balance = erc_ledger.balance_of(FEE_SUBACCOUNT).await;
    
    // Only swap if above threshold
    if balance <= min_threshold {
        return;
    }
    
    // Approve DEX
    erc_ledger.approve(dex, balance).await;
    
    // Execute swap
    dex.swap(SwapArgs {
        pay_token: "ckUSDT",
        receive_token: "ckETH",
        pay_amount: balance,
        max_slippage: slippage_tolerance,
    }).await;
    
    // ckETH now in FEE_SUBACCOUNT for future gas fronting
}

DEX Integration

ParameterValue
DEXConfigurable
Swap DirectionPool Token → ckETH
TriggerBalance > min_threshold
IntervalEvery 300 seconds
SlippageConfigurable per pool

Minimum Burn Amount

Each pool defines a min_burn_amount to prevent uneconomical withdrawals:

if net_withdrawal <= metadata.min_burn_amount {
    return Err("burn amount is too low");
}

This ensures:

  • Withdrawal value > gas fees + protocol fees
  • Users aren't surprised by high fee ratios
  • Pool doesn't process dust withdrawals

Inflow/Outflow Detection

The ERC Pool uses the same timer-based detection as the BTC Pool:

TaskIntervalPurpose
check_for_new_subaccount_inflows60sScan for deposits/repayments
process_treasury_movements60sDetect confirmed deposits
process_event_queue60sNotify lending canister
swap_fees300sConvert fees to ckETH
sync_ledger_fee15sUpdate fee cache

Pool Metadata

Each ERC Pool maintains configuration:

PoolMetadata {
    ticker: String,              // "USDC", "USDT", "ETH"
    decimals: u8,                // Token decimals
    min_burn_amount: Nat,        // Minimum withdrawal
    cketh_ledger: Principal,     // ckETH ledger for gas
    
    // DEX configuration
    dex_swap_enabled: bool,
    dex_swap_ticker: String,
    dex_swap_min_threshold: Nat,
    dex_swap_slippage_tolerance: f64,
}

Multi-Token Support

The ERC Pool architecture supports multiple tokens with a unified interface:

TokenLedgerMinterFee Token
ckETHckETH LedgerckETH MinterckETH
ckUSDTckUSDT LedgerckETH MinterckETH

All ERC-20 tokens use the same ckETH minter for withdrawals, with gas paid in ckETH.