Skip to main content

df_hyperliquid_core/transfer/
evm.rs

1//! HyperCore and HyperEVM, linked. A linked spot token crosses between Core and EVM spot through
2//! its system address: `0x2222...2222` for HYPE, `0x20`, zeros and the token index for any other.
3//! Core to EVM is a `spotSend` or `sendAsset` to [`Linked::system`]; EVM to Core is a HyperEVM
4//! transaction to the same address ([`Linked::to_core`]). A send to a system address leaves Core
5//! rather than reaching a person, so a transfer checks for them.
6
7use crate::client::evm::tx::Call;
8use crate::client::evm::u256::{BadAmount, NATIVE_DECIMALS, U256};
9use crate::transfer::erc20;
10use crate::utils::{Address, Dec};
11
12/// HYPE's system address, `0x2222...2222`: HYPE is the native coin on HyperEVM.
13pub const HYPE_SYSTEM: Address = Address::from_bytes([0x22; 20]);
14
15/// The address a linked spot token crosses through: `0x20`, then the token
16/// index big-endian in the low bytes. Token 200 is `0x2000...00c8`.
17#[must_use]
18pub fn system_address(index: u64) -> Address {
19    let mut bytes = [0u8; 20];
20    bytes[0] = 0x20;
21    bytes[12..].copy_from_slice(&index.to_be_bytes());
22    Address::from_bytes(bytes)
23}
24
25/// Whether a send to `address` crosses between Core and HyperEVM rather than
26/// reaching a person: `0x2222...2222`, or `0x20`, zeros and a token index.
27#[must_use]
28pub fn is_system_address(address: Address) -> bool {
29    address == HYPE_SYSTEM || system_token(address).is_some()
30}
31
32/// The token index a `0x20` system address stands for; `None` for HYPE's,
33/// which crosses natively.
34#[must_use]
35pub fn system_token(address: Address) -> Option<u64> {
36    let bytes = address.bytes();
37    if bytes[0] != 0x20 || bytes[1..12].iter().any(|byte| *byte != 0) {
38        return None;
39    }
40    let mut index = [0u8; 8];
41    index.copy_from_slice(&bytes[12..]);
42    Some(u64::from_be_bytes(index))
43}
44
45/// A spot token as it is linked to HyperEVM. Its contract can hold arbitrary code; nothing here
46/// checks its supply or what its `transfer` does.
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub struct Linked {
49    pub index: u64,
50    /// The ERC-20; `None` for HYPE, which is the chain's native coin.
51    pub contract: Option<Address>,
52    /// The decimals Core counts the token in (`spotMeta`'s `weiDecimals`).
53    pub wei_decimals: u32,
54    /// How many more the ERC-20 counts in, -2 to 18.
55    pub evm_extra_wei_decimals: i32,
56}
57
58impl Linked {
59    /// HYPE: native on HyperEVM, eighteen decimals there.
60    #[must_use]
61    pub const fn hype(wei_decimals: u32) -> Linked {
62        Linked { index: 0, contract: None, wei_decimals, evm_extra_wei_decimals: 0 }
63    }
64
65    /// A token linked to an ERC-20 (`spotMeta`'s `evmContract`).
66    #[must_use]
67    pub const fn erc20(index: u64, contract: Address, wei_decimals: u32, evm_extra_wei_decimals: i32) -> Linked {
68        Linked { index, contract: Some(contract), wei_decimals, evm_extra_wei_decimals }
69    }
70
71    /// Where the token crosses, both ways. Sent there on Core, it arrives at the sender's address
72    /// on HyperEVM.
73    #[must_use]
74    pub fn system(&self) -> Address {
75        match self.contract {
76            None => HYPE_SYSTEM,
77            Some(_) => system_address(self.index),
78        }
79    }
80
81    /// The decimals the token counts in on HyperEVM.
82    #[must_use]
83    pub fn evm_decimals(&self) -> Option<u32> {
84        match self.contract {
85            None => Some(NATIVE_DECIMALS),
86            Some(_) => u32::try_from(i64::from(self.wei_decimals) + i64::from(self.evm_extra_wei_decimals)).ok(),
87        }
88    }
89
90    /// The most places an amount crossing either way may have: the fewer of
91    /// Core's and the EVM's.
92    #[must_use]
93    pub fn core_places(&self) -> Option<u32> {
94        self.evm_decimals().map(|evm| evm.min(self.wei_decimals))
95    }
96
97    /// The HyperEVM call that sends `amount` back to Core: HYPE as a plain value transfer, any
98    /// other token as an ERC-20 `transfer` to the system address. Exact, positive, and no finer
99    /// than [`Linked::core_places`].
100    pub fn to_core(&self, amount: Dec) -> Result<Call, BadAmount> {
101        let places = self.core_places().ok_or(BadAmount::TooLarge)?;
102        let decimals = self.evm_decimals().ok_or(BadAmount::TooLarge)?;
103        if !amount.is_positive() {
104            return Err(BadAmount::NotPositive(amount));
105        }
106        if amount.places() > places {
107            return Err(BadAmount::TooFine { value: amount, decimals: places });
108        }
109        let units = U256::from_dec(amount, decimals)?;
110        Ok(match self.contract {
111            None => Call { to: Some(HYPE_SYSTEM), value: units, data: Vec::new() },
112            Some(contract) => {
113                Call { to: Some(contract), value: U256::ZERO, data: erc20::transfer(self.system(), units) }
114            }
115        })
116    }
117}
118
119#[cfg(test)]
120#[path = "tests/evm.rs"]
121mod tests;