Liquidium

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

Type

Prefix

Purpose

Inflow Deposit

0x1

User deposits

Inflow Repay

0x2

Debt repayments

ETH Outflow

0x4

Ethereum address withdrawals

Native Outflow

0x5

IC Principal transfers

Fee

Internal

Gas 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:

rust
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

Parameter

Value

DEX

Configurable

Swap Direction

Pool Token → ckETH

Trigger

Balance > min_threshold

Interval

Every 300 seconds

Slippage

Configurable per pool

Minimum Burn Amount

Each pool defines a min_burn_amount to prevent uneconomical withdrawals:

rust
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:

Task

Interval

Purpose

check_for_new_subaccount_inflows

60s

Scan for deposits/repayments

process_treasury_movements

60s

Detect confirmed deposits

process_event_queue

60s

Notify lending canister

swap_fees

300s

Convert fees to ckETH

sync_ledger_fee

15s

Update fee cache

Pool Metadata

Each ERC Pool maintains configuration:

rust
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:

Token

Ledger

Minter

Fee Token

ckETH

ckETH Ledger

ckETH Minter

ckETH

ckUSDT

ckUSDT Ledger

ckETH Minter

ckETH

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