Skip to main content

df_hyperliquid_core/transfer/
bridge.rs

1//! USDC in and out of Hyperliquid through the Arbitrum bridge. In: a USDC `transfer` to the bridge
2//! on Arbitrum, credited to the same address on Hyperliquid in about a minute; under
3//! [`MINIMUM_DEPOSIT`] it is lost. Out: `withdraw3` on the venue ([`crate::transfer::Withdraw3`]).
4//! The reads before a deposit are [`crate::client::evm::rpc`] queries, sent by the caller.
5
6use crate::client::Network;
7use crate::client::evm::rpc::{BlockTag, CallRequest, Query, RpcError, eth};
8use crate::client::evm::u256::NATIVE_DECIMALS;
9use crate::client::evm::u256::U256;
10use crate::transfer::erc20;
11use crate::utils::{Address, Dec, USDC_DECIMALS, hex};
12
13const fn address(text: &str) -> Address {
14    match Address::parse_const(text) {
15        Some(address) => address,
16        None => Address::ZERO,
17    }
18}
19
20/// The Hyperliquid bridge on Arbitrum One.
21pub const MAINNET_BRIDGE: Address = address("0x2df1c51e09aecf9cacb7bc98cb1742757f163df7");
22/// Testnet's bridge, on Arbitrum Sepolia.
23pub const TESTNET_BRIDGE: Address = address("0x08cfc1B6b2dCF36A1480b99353A354AA8AC56f89");
24
25/// Native USDC on Arbitrum One.
26pub const MAINNET_USDC: Address = address("0xaf88d065e77c8cC2239327C5EDb3A432268e5831");
27/// The USDC testnet's bridge takes, on Arbitrum Sepolia.
28pub const TESTNET_USDC: Address = address("0x1baAbB04529D43a73232B713C0FE471f7c7334d5");
29
30/// Below this the bridge credits nothing and the USDC is gone.
31pub const MINIMUM_DEPOSIT: Dec = Dec::from_int(5);
32
33/// The bridge a deposit on `network` goes to.
34#[must_use]
35pub const fn bridge(network: Network) -> Address {
36    match network {
37        Network::Mainnet => MAINNET_BRIDGE,
38        Network::Testnet => TESTNET_BRIDGE,
39    }
40}
41
42/// The USDC contract a deposit on `network` is a transfer of.
43#[must_use]
44pub const fn usdc(network: Network) -> Address {
45    match network {
46        Network::Mainnet => MAINNET_USDC,
47        Network::Testnet => TESTNET_USDC,
48    }
49}
50
51/// A public JSON-RPC for the Arbitrum chain the bridge is on ([`Network::arbitrum_chain_id`]).
52#[must_use]
53pub const fn rpc_url(network: Network) -> &'static str {
54    match network {
55        Network::Mainnet => "https://arb1.arbitrum.io/rpc",
56        Network::Testnet => "https://sepolia-rollup.arbitrum.io/rpc",
57    }
58}
59
60#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
61pub enum BadDeposit {
62    #[error("{0} USDC has more than six decimal places")]
63    TooFine(Dec),
64    #[error("{0} USDC is not a positive amount")]
65    NotPositive(Dec),
66    #[error("{0} USDC is below the bridge's minimum of {MINIMUM_DEPOSIT}; less is lost")]
67    BelowMinimum(Dec),
68    #[error("{0} USDC is too large")]
69    TooLarge(Dec),
70}
71
72/// `0x` calldata for USDC `transfer(to, amount)` ([`erc20::transfer`]). The amount is scaled to
73/// six decimals exactly; a seventh place is refused, never rounded.
74pub fn usdc_transfer_calldata(to: Address, amount: Dec) -> Result<String, BadDeposit> {
75    if !amount.is_positive() {
76        return Err(BadDeposit::NotPositive(amount));
77    }
78    if amount.places() > USDC_DECIMALS {
79        return Err(BadDeposit::TooFine(amount));
80    }
81    let units = U256::from_dec(amount, USDC_DECIMALS).map_err(|_| BadDeposit::TooLarge(amount))?;
82    Ok(hex::encode_prefixed(&erc20::transfer(to, units)))
83}
84
85/// [`usdc_transfer_calldata`] to the network's bridge, refusing what it would lose. The
86/// transaction goes to [`usdc`] on [`Network::arbitrum_chain_id`] with no value.
87pub fn deposit_calldata(network: Network, amount: Dec) -> Result<String, BadDeposit> {
88    if amount.is_positive() && amount < MINIMUM_DEPOSIT {
89        return Err(BadDeposit::BelowMinimum(amount));
90    }
91    usdc_transfer_calldata(bridge(network), amount)
92}
93
94/// The owner's USDC on the network's Arbitrum chain: `balanceOf` through `eth_call` at the latest
95/// block. Its answer reads with [`usdc_amount`].
96#[must_use]
97pub fn usdc_balance_request(network: Network, owner: Address) -> Query<Vec<u8>> {
98    eth::call(&CallRequest::to(usdc(network), erc20::balance_of(owner)), BlockTag::Latest)
99}
100
101/// The owner's ETH, for gas, at the latest block. Its answer reads with [`eth_amount`].
102#[must_use]
103pub fn eth_balance_request(owner: Address) -> Query<U256> {
104    eth::balance(owner, BlockTag::Latest)
105}
106
107/// A [`usdc_balance_request`] answer as USDC, exactly.
108pub fn usdc_amount(answer: &[u8]) -> Result<Dec, RpcError> {
109    let unreadable = |detail: String| RpcError::Unreadable { method: "eth_call", detail };
110    let units = erc20::read_amount(answer).map_err(|error| unreadable(error.to_string()))?;
111    units.to_dec(USDC_DECIMALS).map_err(|error| unreadable(error.to_string()))
112}
113
114/// An [`eth_balance_request`] answer as ETH, exactly.
115pub fn eth_amount(wei: U256) -> Result<Dec, RpcError> {
116    wei.to_dec(NATIVE_DECIMALS)
117        .map_err(|error| RpcError::Unreadable { method: "eth_getBalance", detail: error.to_string() })
118}
119
120#[cfg(test)]
121#[path = "tests/bridge.rs"]
122mod tests;