hyperliquid.evm_escrow

Documentation for eth_defi.hyperliquid.evm_escrow Python module.

EVM escrow lifecycle for HyperEVM-to-HyperCore bridging.

When USDC is bridged from HyperEVM to HyperCore via CoreDepositWallet.deposit(), the funds do not appear in the user’s HyperCore spot account immediately. Instead they pass through an intermediate EVM escrow stage:

  1. EVM transaction landsCoreDepositWallet.deposit(amount, SPOT_DEX) burns USDC on HyperEVM and queues a bridge action.

  2. EVM escrow – the deposited amount appears in the evmEscrows field of the user’s spotClearinghouseState. The funds are locked here while HyperCore’s L1 processes the bridge.

  3. HyperCore processing – within a few seconds (typically 2-4 s on mainnet, up to ~10 s on testnet under load) HyperCore picks up the queued action. The escrow entry disappears and the amount is credited to the user’s HyperCore spot balance.

Contract activation prerequisite

Smart contract addresses (such as Safe multisigs) must be activated on HyperCore before CoreDepositWallet.deposit() bridge actions will clear the EVM escrow. Without activation, deposited USDC gets permanently stuck in the evmEscrows field and never reaches the spot account.

Activation is performed via CoreDepositWallet.depositFor(safe, amount, dex) which bridges USDC to the Safe’s HyperCore spot and creates the account. New HyperCore accounts incur a 1 USDC account creation fee, so the minimum activation amount must be >1 USDC (deposits ≤1 USDC to new accounts fail silently). The default activation amount is 2 USDC.

Note

CoreWriter.sendRawAction(spotSend) does not work for activation: HyperCore silently drops spotSend actions targeting non-existent addresses (the EVM transaction succeeds but no USDC is transferred).

Use is_account_activated() to check activation status and activate_account() to perform the activation before depositing.

Why this matters for multi-step deposit flows

The old 4-step single-multicall deposit batched all actions into one EVM block:

  1. approve USDC to CoreDepositWallet

  2. CoreDepositWallet.deposit() – bridge USDC to HyperCore spot

  3. CoreWriter.sendRawAction(transferUsdClass) – move from spot to perp

  4. CoreWriter.sendRawAction(vaultTransfer) – deposit into vault

Steps 3-4 depend on the USDC having cleared the EVM escrow and being available in the spot account. Because all four steps land in the same EVM block, HyperCore processes them in a single batch. In practice this works most of the time, but under heavy load the bridge step can take longer, causing steps 3-4 to silently fail on HyperCore while the EVM transaction succeeds.

The fix is to split the deposit into two phases with an escrow wait:

  • Phase 1: approve + CoreDepositWallet.deposit()

  • Wait: poll spotClearinghouseState until the evmEscrows entry for USDC disappears (meaning funds arrived in spot).

  • Phase 2: transferUsdClass + vaultTransfer

Checking escrow status

Use fetch_spot_clearinghouse_state() from eth_defi.hyperliquid.api:

from eth_defi.hyperliquid.api import fetch_spot_clearinghouse_state
from eth_defi.hyperliquid.session import create_hyperliquid_session

session = create_hyperliquid_session()
state = fetch_spot_clearinghouse_state(session, user="0xAbc...")

if state.evm_escrows:
    for e in state.evm_escrows:
        print(f"{e.coin}: {e.total} in escrow")
else:
    print("No EVM escrows -- funds have cleared")

Expected latencies

  • Mainnet: 2-4 seconds typical, up to 10 seconds under load.

  • Testnet: 2-10 seconds typical, can be slower during congestion.

  • Maximum observed: ~30 seconds in extreme cases.

The wait_for_evm_escrow_clear() helper uses conservative defaults (60 s timeout, 2 s poll interval) to handle worst-case scenarios.

Known issues

  • depositFor on HyperEVM testnet does not create HyperCore accounts for contract addresses (e.g. Safe multisigs). The EVM transaction succeeds but the account is never created and USDC is lost. See hyperliquid-dex/node#138.

Module Attributes

CORE_USER_EXISTS_ADDRESS

coreUserExists precompile address on HyperEVM.

DEFAULT_ACTIVATION_AMOUNT

Default USDC amount (raw, 6 decimals) for account activation.

Functions

activate_account(web3, lagoon_vault, deployer)

Activate a Safe's HyperCore account via depositFor.

activate_account_sponsored(web3, ...[, ...])

Activate a HyperCore account via deployer-sponsored depositFor.

is_account_activated(web3, user)

Check if an address is activated on HyperCore.

wait_for_evm_escrow_clear(session, user[, ...])

Wait until the user's EVM escrow is empty (all bridged funds have cleared).

CORE_USER_EXISTS_ADDRESS = '0x0000000000000000000000000000000000000810'

coreUserExists precompile address on HyperEVM. Returns whether an address exists on HyperCore. See PrecompileLib.sol in hyper-evm-lib.

DEFAULT_ACTIVATION_AMOUNT = 2000000

Default USDC amount (raw, 6 decimals) for account activation. New HyperCore accounts incur a ~1 USDC fee, so the minimum activation deposit must comfortably exceed that.

is_account_activated(web3, user)

Check if an address is activated on HyperCore.

Uses the coreUserExists precompile at CORE_USER_EXISTS_ADDRESS to definitively check whether the address exists on HyperCore.

Smart contracts (like Safe multisigs) must be activated before CoreDepositWallet.deposit() bridge actions will clear the EVM escrow. See activate_account().

Note

P12: Precompile reads return stale data within the same block as a CoreWriter action. This should not affect activation checks in practice because activation (depositFor) and deposit (CDW.deposit) are in separate transactions, and the trade executor waits for activation receipt before proceeding.

Example:

from eth_defi.hyperliquid.evm_escrow import is_account_activated
from eth_defi.provider.multi_provider import create_multi_provider_web3

web3 = create_multi_provider_web3("https://rpc.hyperliquid.xyz/evm")
if is_account_activated(web3, user="0xAbc..."):
    print("Account is activated")
else:
    print("Account needs activation")
Parameters
  • web3 (web3.main.Web3) – Web3 connection to HyperEVM.

  • user (str) – On-chain address to check.

Returns

True if the address exists on HyperCore.

Return type

bool

activate_account(web3, lagoon_vault, deployer, session=None, activation_amount=2000000, timeout=60.0, poll_interval=2.0)

Activate a Safe’s HyperCore account via depositFor.

Smart contracts (like Safe multisigs) must be activated on HyperCore before CoreDepositWallet.deposit() bridge actions will work. Without activation, deposited USDC gets permanently stuck in the evmEscrows field.

The activation flow uses transact_via_trading_strategy_module to call CoreDepositWallet.depositFor(safe, amount, SPOT_DEX) through the Safe’s trading strategy module. This bridges USDC from the Safe’s EVM balance to the Safe’s HyperCore spot account, creating the account in the process.

Note

New HyperCore accounts incur a 1 USDC account creation fee. Deposits ≤1 USDC to new accounts fail silently. The default activation_amount of 2 USDC comfortably exceeds the fee.

Warning

The Safe must hold sufficient EVM USDC for the activation amount. The guard must have depositFor whitelisted via whitelistCoreWriter() (included since guard v0.x).

Example:

from eth_defi.hyperliquid.evm_escrow import activate_account

activate_account(
    web3=web3,
    lagoon_vault=lagoon_vault,
    deployer=deployer_wallet,
)
Parameters
  • web3 (Web3) – Web3 connection to HyperEVM.

  • lagoon_vault (LagoonVault) – Lagoon vault instance with trading_strategy_module_address configured. The Safe associated with this vault will be activated.

  • deployer (HotWallet) – Hot wallet for the asset manager / deployer EOA.

  • session (HyperliquidSession | None) – Optional Hyperliquid API session. If provided, the function checks that the Safe has no existing EVM escrow entries before attempting activation. Stuck escrow entries from prior failed deposits will prevent activation from succeeding.

  • activation_amount (int) – USDC amount in raw units (6 decimals) to deposit for activation. Defaults to 2 USDC (DEFAULT_ACTIVATION_AMOUNT). Must comfortably exceed the ~1 USDC account creation fee.

  • timeout (float) – Maximum seconds to wait for activation verification. Defaults to 60 seconds.

  • poll_interval (float) – Seconds between precompile polls. Defaults to 2 seconds.

Raises

TimeoutError – If the activation does not complete within the timeout period.

Return type

None

activate_account_sponsored(web3, target_address, deployer, usdc_address, session=None, activation_amount=2000000, timeout=60.0, poll_interval=2.0)

Activate a HyperCore account via deployer-sponsored depositFor.

Unlike activate_account() which routes through the Safe’s trading strategy module, this function has the deployer EOA call CoreDepositWallet.depositFor(target, amount, SPOT_DEX) directly. The USDC comes from the deployer’s own EVM balance, not the Safe’s.

This is a workaround for the testnet issue where depositFor called through a Smart contract (Safe multisig) does not create HyperCore accounts. See hyperliquid-dex/node#138.

Note

New HyperCore accounts incur a 1 USDC account creation fee. Deposits ≤1 USDC to new accounts fail silently. The default activation_amount of 2 USDC comfortably exceeds the fee.

Example:

from eth_defi.hyperliquid.evm_escrow import activate_account_sponsored

activate_account_sponsored(
    web3=web3,
    target_address="0xAbc...",
    deployer=deployer_wallet,
    usdc_address="0xDef...",
)
Parameters
  • web3 (web3.main.Web3) – Web3 connection to HyperEVM.

  • target_address (Union[eth_typing.evm.HexAddress, str]) – The address to activate on HyperCore (e.g. a Safe multisig).

  • deployer (eth_defi.hotwallet.HotWallet) – Hot wallet for the deployer EOA that will pay for the activation. Must hold sufficient EVM USDC.

  • usdc_address (Union[eth_typing.evm.HexAddress, str]) – USDC token contract address on HyperEVM.

  • session (eth_defi.hyperliquid.session.HyperliquidSession | None) – Optional Hyperliquid API session. If provided, checks that the target has no existing EVM escrow entries before attempting activation.

  • activation_amount (int) – USDC amount in raw units (6 decimals) to deposit for activation. Defaults to 2 USDC (DEFAULT_ACTIVATION_AMOUNT).

  • timeout (float) – Maximum seconds to wait for activation verification. Defaults to 60 seconds.

  • poll_interval (float) – Seconds between precompile polls. Defaults to 2 seconds.

Raises

TimeoutError – If the activation does not complete within the timeout period.

Return type

None

wait_for_evm_escrow_clear(session, user, timeout=60.0, poll_interval=2.0, expected_usdc=None)

Wait until the user’s EVM escrow is empty (all bridged funds have cleared).

Waits one poll_interval before the first check to give HyperCore time to register the escrow entry (the API can lag behind the EVM tx). Then polls spotClearinghouseState until evmEscrows is empty, indicating that all CoreDepositWallet.deposit() actions have been processed and funds are available in the user’s spot account.

P15: When expected_usdc is provided, also verifies that the user’s HyperCore spot USDC balance increased by at least the expected amount. This provides a dual check: escrow empty and USDC arrived in spot. Without this, a race condition where the escrow entry disappears but USDC is lost (e.g. burned by a failed bridge action) would go undetected.

Example:

from eth_defi.hyperliquid.evm_escrow import wait_for_evm_escrow_clear
from eth_defi.hyperliquid.session import create_hyperliquid_session

session = create_hyperliquid_session()
wait_for_evm_escrow_clear(session, user="0xAbc...", expected_usdc=Decimal("100"))
# Now safe to proceed with CoreWriter actions that need spot balance
Parameters
  • session (HyperliquidSession) – Session from create_hyperliquid_session().

  • user (str) – On-chain address whose escrow to monitor (the Safe address for Lagoon vaults).

  • timeout (float) – Maximum seconds to wait before raising TimeoutError. Defaults to 60 seconds which is conservative for typical 2-10 s latency.

  • poll_interval (float) – Seconds between API polls. Defaults to 2 seconds.

  • expected_usdc (Decimal | None) – Optional expected USDC increase in the spot balance (human units, e.g. Decimal("50") for 50 USDC). When provided, the function captures the pre-existing USDC spot balance and verifies that the balance increased by at least this amount after escrows clear.

Raises

TimeoutError – If the escrow does not clear within the timeout period.

Return type

None