Skip to content

conversions.vy

This module converts amounts between the decimal systems of HyperEVM and HyperCore, and it finds network-specific addresses. A wrong unit can lose funds, so this page has many examples.

If you know Python, think of the module as a set of unit-conversion functions, like cents_to_dollars. The difference is that Vyper has no floats: every amount is an integer, and every division rounds down.

The module docstring

"""
@title HyperEVM Amount Conversions
@notice Converts token amounts between the decimal systems of HyperEVM and
        HyperCore. A wrong unit is a common bridging error. A raw EVM amount in
        a CoreWriter action can be wrong by many orders of magnitude. Then Core
        moves a very small amount, or the action fails silently.

        Decimal systems:
        - EVM amount:  the ERC-20 amount (for example 18 decimals)
        - Core wei:    the HyperCore token unit (wei_decimals of the token)
        - Core sz:     the order size unit (sz_decimals of the token)
        - Perp USD:    USDC in the perp balance (6 decimals, while spot USDC has 8)
@dev All functions are internal. Import the module:

        from hyper_vyper import conversions

        core_amount: uint64 = conversions.evm_to_wei(token_index, evm_amount)

     Each token-index conversion reads the decimals from the token_info
     precompile. If your contract converts often, read the metadata once with
     checked_token_info and use evm_to_wei_with_info.
"""

The library works with four units:

Unit Meaning USDC example: 1 USDC
EVM amount The ERC-20 amount on HyperEVM 1_000_000 (6 decimals)
Core wei The token amount on HyperCore 100_000_000 (8 decimals)
Core sz The order size unit depends on sz_decimals
Perp USD USDC in the perp balance 1_000_000 (6 decimals)

The network

@internal
@view
def _require_network():
    assert chain.id == constants.MAINNET_CHAIN_ID or chain.id == constants.TESTNET_CHAIN_ID, "Unsupported chain"


@internal
@view
def hype_token_index() -> uint64:
    """HYPE's HyperCore token index for the current chain."""
    self._require_network()
    if chain.id == constants.TESTNET_CHAIN_ID:
        return constants.HYPE_TOKEN_INDEX_TESTNET
    return constants.HYPE_TOKEN_INDEX_MAINNET

chain.id is the chain ID of the current network, and _require_network reverts on every chain except 998 (testnet) and 999 (mainnet). The Solidity library falls back to mainnet values on unknown chains. This library refuses, because a wrong address on an unknown chain could send funds to the wrong place.

hype_token_index, usdc_address and core_deposit_wallet return the value for the current network.

System addresses

@internal
@view
def system_address(token: uint64) -> address:
    """The system address for a token. Transferring ERC-20 tokens to this address
    bridges them EVM -> Core, and a send_asset to it returns them Core -> EVM."""
    if self.is_hype(token):
        return constants.HYPE_SYSTEM_ADDRESS
    return convert(
        convert(constants.BASE_SYSTEM_ADDRESS, uint160) + convert(token, uint160),
        address,
    )

An address is a 160-bit number. The code converts the base address 0x2000...0000 to a number, adds the token index, and converts the result back to an address. For token 197 (UBTC) the result is 0x2000...00c5, because 197 is 0xc5 in hexadecimal. HYPE is the exception and uses 0x2222...2222.

From EVM to Core

@internal
@view
def checked_token_info(token: uint64) -> precompiles.TokenInfo:
    """token_info for a token index that fits in uint32.
    The Solidity library narrows it to uint32 and can read the wrong token. Reverts instead."""
    assert token <= convert(max_value(uint32), uint64), "Token index too large"
    return precompiles.token_info(token)


@internal
@view
def evm_to_wei(token: uint64, amount_evm: uint256) -> uint64:
    """Convert an EVM ERC-20 amount to a Core wei amount.
    Reverts if the token has no linked EVM contract (unless HYPE).
    WARNING: truncates. If amount_evm is smaller than one Core wei unit the
    result is 0, so callers bridging funds must reject zero results."""
    return self.evm_to_wei_with_info(token, self.checked_token_info(token), amount_evm)


@internal
@view
def evm_to_wei_with_info(token: uint64, info: precompiles.TokenInfo, amount_evm: uint256) -> uint64:
    """evm_to_wei with metadata that the caller already read with checked_token_info.
    Use it to avoid a second token_info read."""
    if info.evm_contract == empty(address):
        assert self.is_hype(token), "Token not linked to EVM"
        return convert(amount_evm // 10 ** constants.HYPE_EVM_EXTRA_DECIMALS, uint64)

    if info.evm_extra_wei_decimals == 0:
        return convert(amount_evm, uint64)
    if info.evm_extra_wei_decimals > 0:
        return convert(
            amount_evm // 10 ** convert(info.evm_extra_wei_decimals, uint256),
            uint64,
        )
    return convert(
        amount_evm * 10 ** convert(-info.evm_extra_wei_decimals, uint256),
        uint64,
    )

checked_token_info reads the token metadata and rejects an index above max_value(uint32). The Solidity library truncates such an index to 32 bits, so it can read the metadata of a different token.

evm_to_wei_with_info uses evm_extra_wei_decimals, which is the number of decimals that the EVM token has in addition to the Core token.

Token EVM decimals Core decimals Extra Rule Example
USDC 6 8 -2 multiply by 100 1_000_000 EVM is 100_000_000 Core
UETH 18 9 9 divide by 10**9 3 * 10**9 + 1 EVM is 3 Core, remainder 1
HYPE 18 8 not linked divide by 10**10 10**18 EVM (1 HYPE) is 10**8 Core

Three Vyper details matter here:

  • // is integer division and rounds down, so the UETH example loses the remainder. The bridge functions (bridge.vy) reject such amounts.
  • convert(x, uint64) reverts if x does not fit in 64 bits, so a wrap-around is not possible.
  • -info.evm_extra_wei_decimals changes the sign of an int8. For the value -128 this overflows, so the code reverts instead of giving a wrong result.

HYPE has no linked EVM contract (evm_contract is the zero address), so the code uses a fixed rule for HYPE. Any other unlinked token causes the revert "Token not linked to EVM".

From Core to EVM

@internal
@view
def wei_to_evm(token: uint64, amount_wei: uint64) -> uint256:
    """Convert a Core wei amount to an EVM ERC-20 amount.
    Reverts if the token has no linked EVM contract (unless HYPE).
    Truncates when EVM has fewer decimals than Core (for example USDC: 8 -> 6)."""
    info: precompiles.TokenInfo = self.checked_token_info(token)
    if info.evm_contract == empty(address):
        assert self.is_hype(token), "Token not linked to EVM"
        return convert(amount_wei, uint256) * 10 ** constants.HYPE_EVM_EXTRA_DECIMALS

    if info.evm_extra_wei_decimals == 0:
        return convert(amount_wei, uint256)
    if info.evm_extra_wei_decimals > 0:
        return convert(amount_wei, uint256) * 10 ** convert(info.evm_extra_wei_decimals, uint256)
    return convert(amount_wei, uint256) // 10 ** convert(-info.evm_extra_wei_decimals, uint256)

This is the reverse direction, and it truncates when EVM has fewer decimals than Core. For USDC, 199 Core wei is 1 EVM unit, and the conversion drops the remaining 99 Core wei.

Order sizes

@internal
@view
def sz_to_wei(token: uint64, sz: uint64) -> uint64:
    """Convert an order size (sz_decimals) to a Core wei amount (wei_decimals)."""
    info: precompiles.TokenInfo = self.checked_token_info(token)
    return sz * convert(
        10 ** convert(info.wei_decimals - info.sz_decimals, uint256),
        uint64,
    )


@internal
@view
def wei_to_sz(token: uint64, amount_wei: uint64) -> uint64:
    """Convert a Core wei amount (wei_decimals) to an order size (sz_decimals). Truncates."""
    info: precompiles.TokenInfo = self.checked_token_info(token)
    return amount_wei // convert(
        10 ** convert(info.wei_decimals - info.sz_decimals, uint256),
        uint64,
    )

An order size uses sz_decimals, and a Core amount uses wei_decimals. HYPE has wei_decimals = 8 and sz_decimals = 2, so the factor is 10**6:

  • sz_to_wei(HYPE, 25) is 25_000_000 Core wei (0.25 HYPE).
  • wei_to_sz(HYPE, 999_999) is 0, because the division rounds down.

The subtraction wei_decimals - sz_decimals uses uint8, so it would revert for a token whose sz_decimals is larger than its wei_decimals.

USDC between spot and perp, and asset IDs

# --- USDC spot wei (8 decimals) <-> perp USD (6 decimals) ---

@internal
@pure
def wei_to_perp(amount_wei: uint64) -> uint64:
    """Convert USDC spot wei (8 decimals) to perp USD (6 decimals). Truncates."""
    return amount_wei // 100


@internal
@pure
def perp_to_wei(perp_amount: uint64) -> uint64:
    """Convert perp USD (6 decimals) to USDC spot wei (8 decimals)."""
    return perp_amount * 100


# --- Spot market index <-> order asset ID ---

@internal
@pure
def spot_to_asset_id(spot_index: uint64) -> uint32:
    """Asset ID used in limit orders for a spot market (spot_index + 10000)."""
    return convert(spot_index + constants.SPOT_ASSET_OFFSET, uint32)


@internal
@pure
def asset_to_spot_id(asset: uint64) -> uint64:
    """Spot market index from an order asset ID.
    Assumes a spot asset ID (spot index + 10000). For other asset IDs the result
    has no meaning."""
    return asset - constants.SPOT_ASSET_OFFSET
  • Spot USDC has 8 decimals and perp USD has 6, so the factor is 100. wei_to_perp(199) is 1, and the division loses 99.
  • spot_to_asset_id(1035) is 11035, the asset ID of a spot order on market
  • asset_to_spot_id is the reverse, and it only makes sense for spot asset IDs.

From a token address to a token index

@internal
@view
def token_index(token_address: address) -> uint64:
    """HyperCore token index for a linked EVM contract address.
    The chain's USDC ERC-20 maps to index 0. Other addresses are looked up in the
    TokenRegistry, which reverts with TokenNotFound(address) if unregistered.
    Anyone can write to the registry, so the result is checked against token_info.
    Index 0 is only returned for the USDC token itself. The registry also maps the
    CoreDepositWallet to 0, because Core links USDC to it, but it is not a token."""
    if token_address == self.usdc_address():
        return constants.USDC_TOKEN_INDEX
    index: uint64 = convert(
        staticcall TokenRegistry(constants.TOKEN_REGISTRY).getTokenIndex(token_address), uint64
    )
    assert index != constants.USDC_TOKEN_INDEX, "Registry mismatch"
    assert self.checked_token_info(index).evm_contract == token_address, "Registry mismatch"
    return index

token_index finds the Core index of an EVM token address:

  1. The network's USDC token is index 0, so no registry call is necessary.
  2. For any other address, the function asks the TokenRegistry contract with staticcall, a call that cannot change state.
  3. The function rejects index 0, because index 0 is USDC and step 1 already handles the USDC token. The registry also maps the Circle deposit wallet to 0, but the wallet is not a token.
  4. The function checks that the token metadata links back to the same address, because anyone can write to the registry and its answer can be stale.

In a contract that holds funds, use fixed token indices instead of token_index.

Check your understanding

  1. Convert 2.5 USDC from EVM units to Core wei, and from Core wei to perp USD.
  2. Why does evm_to_wei revert for a token without a linked EVM contract, except for HYPE?
  3. What is the result of wei_to_sz(HYPE, 1_999_999), and what happens to the rest?
  4. Why does the library check token_index answers against token_info?
  5. Which function would you use to avoid two token_info reads in one transaction?

Try it

uv run --locked pytest tests/test_reference.py -q -k "conversion or wei or usdc"

Change perp_to_wei to multiply by 10 instead of 100, run the command again and read the failures. Then restore the file with git checkout -- src/.

Next: bridge.vy.