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 ifxdoes not fit in 64 bits, so a wrap-around is not possible.-info.evm_extra_wei_decimalschanges the sign of anint8. For the value-128this 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)is25_000_000Core wei (0.25 HYPE).wei_to_sz(HYPE, 999_999)is0, 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)is1, and the division loses 99. spot_to_asset_id(1035)is11035, the asset ID of a spot order on marketasset_to_spot_idis 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:
- The network's USDC token is index 0, so no registry call is necessary.
- For any other address, the function asks the TokenRegistry contract with
staticcall, a call that cannot change state. - 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.
- 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
- Convert 2.5 USDC from EVM units to Core wei, and from Core wei to perp USD.
- Why does
evm_to_weirevert for a token without a linked EVM contract, except for HYPE? - What is the result of
wei_to_sz(HYPE, 1_999_999), and what happens to the rest? - Why does the library check
token_indexanswers againsttoken_info? - Which function would you use to avoid two
token_inforeads in one transaction?
Try it
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.