├── remappings.txt ├── src ├── interfaces │ ├── ICoreWriter.sol │ ├── ITokenRegistry.sol │ └── ICoreDepositWallet.sol ├── registry │ ├── README.md │ └── TokenRegistry.sol ├── examples │ ├── README.md │ ├── StakingExample.sol │ ├── TradingExample.sol │ ├── VaultExample.sol │ └── BridgingExample.sol ├── common │ ├── HLConversions.sol │ └── HLConstants.sol ├── CoreWriterLib.sol └── PrecompileLib.sol ├── foundry.lock ├── .gitmodules ├── .gitignore ├── foundry.toml ├── test ├── unit-tests │ ├── StakingTest.t.sol │ ├── BridgingTest.t.sol │ ├── FeeSimulationTest.t.sol │ └── OfflineTest.t.sol ├── BaseSimulatorTest.sol ├── utils │ ├── PrecompileSimulator.sol │ ├── HypeTradingContract.sol │ ├── L1Read.sol │ └── RealL1Read.sol ├── simulation │ ├── HyperCore.sol │ ├── CoreWriterSim.sol │ ├── PrecompileSim.sol │ ├── hyper-core │ │ ├── CoreView.sol │ │ ├── CoreState.sol │ │ └── CoreExecution.sol │ └── CoreSimulatorLib.sol └── PrecompileLibTests.t.sol ├── script └── PrecompileScript.sol ├── LICENSE └── README.md /remappings.txt: -------------------------------------------------------------------------------- 1 | @openzeppelin/contracts=lib/openzeppelin-contracts/contracts 2 | @hyper-evm-lib/=./ -------------------------------------------------------------------------------- /src/interfaces/ICoreWriter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | interface ICoreWriter { 5 | function sendRawAction(bytes calldata data) external; 6 | } 7 | -------------------------------------------------------------------------------- /foundry.lock: -------------------------------------------------------------------------------- 1 | { 2 | "lib/forge-std": { 3 | "rev": "8bbcf6e3f8f62f419e5429a0bd89331c85c37824" 4 | }, 5 | "lib/openzeppelin-contracts": { 6 | "rev": "c64a1edb67b6e3f4a15cca8909c9482ad33a02b0" 7 | } 8 | } -------------------------------------------------------------------------------- /src/interfaces/ITokenRegistry.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | interface ITokenRegistry { 5 | function getTokenIndex(address evmContract) external view returns (uint32 index); 6 | } 7 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | [submodule "lib/openzeppelin-contracts"] 5 | path = lib/openzeppelin-contracts 6 | url = https://github.com/openzeppelin/openzeppelin-contracts 7 | -------------------------------------------------------------------------------- /src/interfaces/ICoreDepositWallet.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | interface ICoreDepositWallet { 5 | function deposit(uint256 amount, uint32 destinationDex) external; 6 | function depositFor(address recipient, uint256 amount, uint32 destinationDex) external; 7 | } 8 | 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiler files 2 | cache/ 3 | out/ 4 | 5 | # Ignores development broadcast logs 6 | !/broadcast 7 | /broadcast/*/31337/ 8 | /broadcast/**/dry-run/ 9 | 10 | # Docs 11 | docs/ 12 | 13 | # Dotenv file 14 | .env 15 | 16 | # misc 17 | lcov.info 18 | ignored/ 19 | 20 | # misc 21 | lcov.info 22 | ignored/ 23 | broadcast/ -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = "src" 3 | out = "out" 4 | libs = ["lib"] 5 | 6 | # Optimizer settings 7 | optimizer = true 8 | optimizer_runs = 200 9 | 10 | [rpc_endpoints] 11 | hyperliquid = "https://rpc.hyperliquid.xyz/evm" 12 | testnet = "https://rpc.hyperliquid-testnet.xyz/evm" 13 | 14 | [fmt] 15 | 16 | 17 | # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options 18 | -------------------------------------------------------------------------------- /test/unit-tests/StakingTest.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.28; 3 | 4 | import {Test} from "forge-std/Test.sol"; 5 | import {PrecompileLib} from "../../src/PrecompileLib.sol"; 6 | import {CoreSimulatorLib} from "../simulation/CoreSimulatorLib.sol"; 7 | 8 | contract StakingBalanceTest is Test { 9 | function setUp() public { 10 | vm.createSelectFork(vm.envString("ALCHEMY_RPC")); 11 | CoreSimulatorLib.init(); 12 | } 13 | 14 | function test_liveStakingBalance() public { 15 | address stakingAddress = 0x77C3Ea550D2Da44B120e55071f57a108f8dd5E45; 16 | 17 | PrecompileLib.DelegatorSummary memory summary = PrecompileLib.delegatorSummary(stakingAddress); 18 | uint256 totalStaking = uint256(summary.delegated); 19 | 20 | emit log_named_uint("total staking balance (core units)", totalStaking); 21 | 22 | assertGt(totalStaking, 0); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /script/PrecompileScript.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import {TokenRegistry} from "../src/registry/TokenRegistry.sol"; 5 | import {console} from "forge-std/console.sol"; 6 | import {PrecompileLib} from "../src/PrecompileLib.sol"; 7 | import {Script, VmSafe} from "forge-std/Script.sol"; 8 | import {PrecompileSimulator} from "../test/utils/PrecompileSimulator.sol"; 9 | 10 | // In order for the script to work, run `forge script` with the `--skip-simulation` flag 11 | contract PrecompileScript is Script { 12 | function run() public { 13 | vm.startBroadcast(); 14 | PrecompileSimulator.init(); // script works because of this 15 | 16 | Tester tester = new Tester(); 17 | tester.logValues(); 18 | 19 | vm.stopBroadcast(); 20 | } 21 | } 22 | 23 | contract Tester { 24 | function logValues() public { 25 | console.log("msg.sender", msg.sender); 26 | console.log("tx.origin", tx.origin); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/registry/README.md: -------------------------------------------------------------------------------- 1 | # TokenRegistry 2 | 3 | ## Description 4 | Precompiles like `spotBalance`, `spotPx` and more, all require either a token index (for `spotBalance`) or a spot market index (for `spotPx`) as an input parameter. 5 | 6 | Natively, there is no way to derive the token index given a token's contract address, requiring projects to store it manually, or pass it in as a parameter whenever needed. 7 | 8 | TokenRegistry solves this by providing a deployed-onchain mapping from EVM contract addresses to their HyperCore token indices, populated trustlessly using precompile lookups for each index. 9 | 10 | ## Usage 11 | The `PrecompileLib` exposes a [function](https://github.com/hyperliquid-dev/hyper-evm-lib/blob/b347756c392934712af9c27b92028a00b93cb68c/src/PrecompileLib.sol#L61-L66) to read from the `TokenRegistry`, and can be used instead of directly interacting with the `TokenRegistry` contract. 12 | 13 | For reference, the `TokenRegistry` is deployed on mainnet at [0x0b51d1a9098cf8a72c325003f44c194d41d7a85b](https://hyperevmscan.io/address/0x0b51d1a9098cf8a72c325003f44c194d41d7a85b) 14 | 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Obsidian Audits 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | This directory contains practical examples demonstrating how to use `evm-lib` to interact with HyperEVM and HyperCore. Each example showcases different aspects of the library's functionality. 4 | 5 | --- 6 | 7 | ## Core Concepts 8 | 9 | ### Function Overloading in Bridging 10 | The library provides two ways to bridge tokens: 11 | 1. **By Token Address**: [bridgeToCore(address tokenAddress, uint256 evmAmount)](https://github.com/hyperliquid-dev/evm-lib/blob/f27ed9ebcba8c61c6cbfbe4727c52e50d0c2759b/src/CoreWriterLib.sol#L38-L41) 12 | 2. **By Token ID**: [bridgeToCore(uint64 token, uint256 evmAmount)](https://github.com/hyperliquid-dev/evm-lib/blob/f27ed9ebcba8c61c6cbfbe4727c52e50d0c2759b/src/CoreWriterLib.sol#L43-L53) 13 | 14 | The address version uses the [TokenRegistry](https://github.com/hyperliquid-dev/evm-lib/blob/main/src/registry/TokenRegistry.sol) to resolve the token ID, removing the need for developers to store the token ID for each linked evm token address. 15 | ### Decimal Conversions 16 | Tokens are represented using differing amounts of precision depending on where they're used: 17 | - **EVM** 18 | - **Spot** 19 | - **Perps** 20 | 21 | The library provides conversion functions to handle these differences. 22 | 23 | ### TokenRegistry Usage 24 | The [TokenRegistry](https://github.com/hyperliquid-dev/hyper-evm-lib/blob/main/src/registry/TokenRegistry.sol) eliminates the need to manually track token IDs by providing an onchain mapping from EVM contract addresses to HyperCore token indices. This is populated trustlessly using the precompiles 25 | 26 | --- 27 | -------------------------------------------------------------------------------- /test/BaseSimulatorTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import {Test, console} from "forge-std/Test.sol"; 5 | import {PrecompileLib} from "../src/PrecompileLib.sol"; 6 | import {CoreWriterLib} from "../src/CoreWriterLib.sol"; 7 | import {HLConversions} from "../src/common/HLConversions.sol"; 8 | import {HLConstants} from "../src/common/HLConstants.sol"; 9 | import {HyperCore} from "./simulation/HyperCore.sol"; 10 | import {CoreSimulatorLib} from "./simulation/CoreSimulatorLib.sol"; 11 | 12 | /** 13 | * @title BaseSimulatorTest 14 | * @notice Base test contract that sets up the HyperCore simulation 15 | */ 16 | abstract contract BaseSimulatorTest is Test { 17 | using PrecompileLib for address; 18 | using HLConversions for *; 19 | 20 | HyperCore public hyperCore; 21 | 22 | // Common token addresses 23 | address public constant USDT0 = 0xB8CE59FC3717ada4C02eaDF9682A9e934F625ebb; 24 | address public constant uBTC = 0x9FDBdA0A5e284c32744D2f17Ee5c74B284993463; 25 | address public constant uETH = 0xBe6727B535545C67d5cAa73dEa54865B92CF7907; 26 | address public constant uSOL = 0x068f321Fa8Fb9f0D135f290Ef6a3e2813e1c8A29; 27 | 28 | // Common token indices 29 | uint64 public constant USDC_TOKEN = 0; 30 | uint64 public constant HYPE_TOKEN = 150; 31 | 32 | address user = makeAddr("user"); 33 | 34 | function setUp() public virtual { 35 | string memory hyperliquidRpc = "https://rpc.hyperliquid.xyz/evm"; 36 | vm.createSelectFork(hyperliquidRpc); 37 | 38 | hyperCore = CoreSimulatorLib.init(); 39 | 40 | hyperCore.forceAccountActivation(user); 41 | hyperCore.forceSpotBalance(user, USDC_TOKEN, 1000e8); 42 | hyperCore.forcePerpBalance(user, 1000e6); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /test/utils/PrecompileSimulator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import {Vm} from "forge-std/Vm.sol"; 5 | 6 | Vm constant vm = Vm(address(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D)); 7 | 8 | /** 9 | * @title PrecompileSimulator 10 | * @dev A library used to etch precompiles into their addresses, for usage in foundry scripts and fork tests 11 | * Note: When using this library for scripts, call `forge script` with the `--skip-simulation` flag to avoid reverting during simulation 12 | * @notice modified from: https://github.com/sprites0/hyperevm-project-template/blob/main/src/MoreRealisticL1Precompiles.sol 13 | */ 14 | library PrecompileSimulator { 15 | uint256 constant NUM_PRECOMPILES = 17; 16 | 17 | function init() internal { 18 | // Etch all the precompiles to their respective addresses 19 | for (uint160 i = 0; i < NUM_PRECOMPILES; i++) { 20 | address precompileAddress = address(uint160(0x0000000000000000000000000000000000000800) + i); 21 | vm.etch(precompileAddress, type(MockPrecompile).runtimeCode); 22 | vm.allowCheatcodes(precompileAddress); 23 | } 24 | } 25 | } 26 | 27 | contract MockPrecompile { 28 | fallback() external payable { 29 | vm.pauseGasMetering(); 30 | bytes memory response = _makeRpcCall(address(this), msg.data); 31 | vm.resumeGasMetering(); 32 | assembly { 33 | return(add(response, 32), mload(response)) 34 | } 35 | } 36 | 37 | function _makeRpcCall(address target, bytes memory params) internal returns (bytes memory) { 38 | // Construct the JSON-RPC payload 39 | string memory jsonPayload = 40 | string.concat('[{"to":"', vm.toString(target), '","data":"', vm.toString(params), '"},"latest"]'); 41 | 42 | // Make the RPC call 43 | return vm.rpc("eth_call", jsonPayload); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/examples/StakingExample.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import {CoreWriterLib, HLConstants, HLConversions} from "@hyper-evm-lib/src/CoreWriterLib.sol"; 5 | /** 6 | * @title StakingExample 7 | * @dev This contract demonstrates CoreWriterLib staking functionality. 8 | */ 9 | 10 | contract StakingExample { 11 | using CoreWriterLib for *; 12 | 13 | error NoHypeBalance(); 14 | 15 | /** 16 | * @notice Transfers HYPE tokens to core, stakes them, and delegates to a validator 17 | */ 18 | function bridgeHypeAndStake(uint256 evmAmount, address validator) external payable { 19 | // Transfer HYPE tokens to core 20 | uint64 hypeTokenIndex = HLConstants.hypeTokenIndex(); 21 | hypeTokenIndex.bridgeToCore(evmAmount); 22 | 23 | // Using data from the `TokenInfo` precompile, convert EVM amount to core decimals for staking operations 24 | uint64 coreAmount = HLConversions.evmToWei(hypeTokenIndex, evmAmount); 25 | 26 | // Transfer tokens to staking account 27 | CoreWriterLib.depositStake(coreAmount); 28 | 29 | // Delegate the tokens to a validator 30 | CoreWriterLib.delegateToken(validator, coreAmount, false); 31 | } 32 | 33 | /** 34 | * @notice Undelegates tokens from a validator 35 | */ 36 | function undelegateTokens(address validator, uint64 coreAmount) external { 37 | // Undelegate tokens by setting the bool `undelegate` parameter to true 38 | CoreWriterLib.delegateToken(validator, coreAmount, true); 39 | } 40 | 41 | /** 42 | * @notice Undelegates tokens from a validator and withdraws them to the spot balance 43 | */ 44 | function undelegateAndWithdrawStake(address validator, uint64 coreAmount) external { 45 | // Undelegate tokens from the validator 46 | CoreWriterLib.delegateToken(validator, coreAmount, true); 47 | 48 | // Withdraw the tokens from staking 49 | CoreWriterLib.withdrawStake(coreAmount); 50 | } 51 | 52 | /** 53 | * @notice Withdraws tokens from the staking balance 54 | */ 55 | function withdrawStake(uint64 coreAmount) external { 56 | // Withdraw the tokens from the staking balance 57 | CoreWriterLib.withdrawStake(coreAmount); 58 | } 59 | 60 | /** 61 | * @notice Transfers all HYPE balance to the sender 62 | */ 63 | function transferAllHypeToSender() external { 64 | uint256 balance = address(this).balance; 65 | if (balance == 0) revert NoHypeBalance(); 66 | payable(msg.sender).transfer(balance); 67 | } 68 | 69 | receive() external payable {} 70 | } 71 | -------------------------------------------------------------------------------- /src/examples/TradingExample.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import {CoreWriterLib, HLConstants, HLConversions} from "@hyper-evm-lib/src/CoreWriterLib.sol"; 5 | 6 | /** 7 | * @title TradingExample 8 | * @dev This contract demonstrates CoreWriterLib trading functionality. 9 | */ 10 | contract TradingExample { 11 | using CoreWriterLib for *; 12 | 13 | uint64 public constant USDC_TOKEN_ID = 0; 14 | 15 | /** 16 | * @notice Places a limit order 17 | * @param asset Asset ID to trade 18 | * @param isBuy True for buy order, false for sell order 19 | * @param limitPx Limit price for the order 20 | * @param sz Size/quantity of the order 21 | * @param reduceOnly True if order should only reduce position 22 | * @param encodedTif Time in force encoding (1=ALO, 2=GTC, 3=IOC) 23 | * @param cloid Client order ID for tracking 24 | */ 25 | function placeLimitOrder( 26 | uint32 asset, 27 | bool isBuy, 28 | uint64 limitPx, 29 | uint64 sz, 30 | bool reduceOnly, 31 | uint8 encodedTif, 32 | uint128 cloid 33 | ) external { 34 | CoreWriterLib.placeLimitOrder(asset, isBuy, limitPx, sz, reduceOnly, encodedTif, cloid); 35 | } 36 | 37 | /** 38 | * @notice Cancels an order by client order ID 39 | * @param asset Asset ID of the order to cancel 40 | * @param cloid Client order ID of the order to cancel 41 | */ 42 | function cancelOrderByCloid(uint32 asset, uint128 cloid) external { 43 | CoreWriterLib.cancelOrderByCloid(asset, cloid); 44 | } 45 | 46 | /** 47 | * @notice Transfers USDC tokens to another address 48 | * @param to Address of the recipient 49 | * @param coreAmount Amount of USDC to transfer 50 | */ 51 | function transferUsdc(address to, uint64 coreAmount) external { 52 | CoreWriterLib.spotSend(to, USDC_TOKEN_ID, coreAmount); 53 | } 54 | 55 | /** 56 | * @notice Transfers USDC between spot and perp trading accounts 57 | * @param coreAmount Amount to transfer 58 | * @param toPerp If true, transfers from spot to perp; if false, transfers from perp to spot 59 | */ 60 | function transferUsdcBetweenSpotAndPerp(uint64 coreAmount, bool toPerp) external { 61 | uint64 usdcPerpAmount = HLConversions.weiToPerp(coreAmount); 62 | CoreWriterLib.transferUsdClass(usdcPerpAmount, toPerp); 63 | } 64 | 65 | /** 66 | * @notice Withdraws tokens from staking balance and bridges them back to EVM 67 | * @param evmAmount Amount of tokens to bridge back to EVM 68 | */ 69 | function bridgeHypeBackToEvm(uint64 evmAmount) external { 70 | // Bridge tokens back to EVM 71 | CoreWriterLib.bridgeToEvm(HLConstants.hypeTokenIndex(), evmAmount, true); 72 | } 73 | 74 | receive() external payable {} 75 | } 76 | -------------------------------------------------------------------------------- /test/simulation/HyperCore.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.28; 3 | 4 | import {CoreExecution} from "./hyper-core/CoreExecution.sol"; 5 | import {DoubleEndedQueue} from "@openzeppelin/contracts/utils/structs/DoubleEndedQueue.sol"; 6 | import {HLConstants} from "../../src/PrecompileLib.sol"; 7 | 8 | contract HyperCore is CoreExecution { 9 | using DoubleEndedQueue for DoubleEndedQueue.Bytes32Deque; 10 | 11 | function executeRawAction(address sender, uint24 kind, bytes calldata data) public payable { 12 | if (kind == HLConstants.LIMIT_ORDER_ACTION) { 13 | LimitOrderAction memory action = abi.decode(data, (LimitOrderAction)); 14 | 15 | // for perps (check that the ID is not a spot asset ID) 16 | if (action.asset < 1e4 || action.asset >= 1e5) { 17 | executePerpLimitOrder(sender, action); 18 | } else { 19 | executeSpotLimitOrder(sender, action); 20 | } 21 | return; 22 | } 23 | 24 | if (kind == HLConstants.VAULT_TRANSFER_ACTION) { 25 | executeVaultTransfer(sender, abi.decode(data, (VaultTransferAction))); 26 | return; 27 | } 28 | 29 | if (kind == HLConstants.TOKEN_DELEGATE_ACTION) { 30 | executeTokenDelegate(sender, abi.decode(data, (TokenDelegateAction))); 31 | return; 32 | } 33 | 34 | if (kind == HLConstants.STAKING_DEPOSIT_ACTION) { 35 | executeStakingDeposit(sender, abi.decode(data, (StakingDepositAction))); 36 | return; 37 | } 38 | 39 | if (kind == HLConstants.STAKING_WITHDRAW_ACTION) { 40 | executeStakingWithdraw(sender, abi.decode(data, (StakingWithdrawAction))); 41 | return; 42 | } 43 | 44 | if (kind == HLConstants.SPOT_SEND_ACTION) { 45 | executeSpotSend(sender, abi.decode(data, (SpotSendAction))); 46 | return; 47 | } 48 | 49 | if (kind == HLConstants.USD_CLASS_TRANSFER_ACTION) { 50 | executeUsdClassTransfer(sender, abi.decode(data, (UsdClassTransferAction))); 51 | return; 52 | } 53 | } 54 | 55 | /// @dev unstaking takes 7 days and after which it will automatically appear in the users 56 | /// spot balance so we need to check this at the end of each operation to simulate that. 57 | function processStakingWithdrawals() public { 58 | while (_withdrawQueue.length() > 0) { 59 | WithdrawRequest memory request = deserializeWithdrawRequest(_withdrawQueue.front()); 60 | 61 | if (request.lockedUntilTimestamp > block.timestamp) { 62 | break; 63 | } 64 | 65 | _withdrawQueue.popFront(); 66 | 67 | _accounts[request.account].spot[HLConstants.hypeTokenIndex()] += request.amount; 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /test/unit-tests/BridgingTest.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.28; 3 | 4 | import {Test} from "forge-std/Test.sol"; 5 | import {PrecompileLib} from "../../src/PrecompileLib.sol"; 6 | import {CoreSimulatorLib} from "../simulation/CoreSimulatorLib.sol"; 7 | 8 | import {CoreWriterLib, HLConversions, HLConstants} from "../../src/CoreWriterLib.sol"; 9 | import {IERC20} from "forge-std/interfaces/IERC20.sol"; 10 | contract BridgingTest is Test { 11 | function setUp() public { 12 | vm.createSelectFork("https://rpc.hyperliquid.xyz/evm"); 13 | CoreSimulatorLib.init(); 14 | } 15 | 16 | function test_bridgeUSDCToCore() public { 17 | IERC20 USDC = IERC20(0xb88339CB7199b77E23DB6E890353E22632Ba630f); 18 | 19 | address user = makeAddr("user"); 20 | deal(address(USDC), user, 1000e6); 21 | vm.startPrank(user); 22 | CoreWriterLib.bridgeToCore(address(USDC), 1000e6); 23 | vm.stopPrank(); 24 | 25 | uint64 activationFee = !PrecompileLib.coreUserExists(user) ? 1e8 : 0; 26 | 27 | CoreSimulatorLib.nextBlock(); 28 | 29 | assertEq(PrecompileLib.spotBalance(address(user), 0).total, HLConversions.evmToWei(0, 1000e6) - activationFee); 30 | } 31 | 32 | // TODO: To be able to bridge to perp dexes directly, SendAsset action needs to be implemented. Requires refactor of CoreState to support multiple perp dexes (HIP-3) 33 | function test_bridgeUSDCToCoreForRecipient() public { 34 | IERC20 USDC = IERC20(0xb88339CB7199b77E23DB6E890353E22632Ba630f); 35 | address recipient = makeAddr("recipient"); 36 | address user = makeAddr("user"); 37 | deal(address(USDC), user, 1000e6); 38 | vm.startPrank(user); 39 | CoreWriterLib.bridgeUsdcToCoreFor(recipient, 1000e6, HLConstants.SPOT_DEX); 40 | vm.stopPrank(); 41 | 42 | uint64 activationFee = !PrecompileLib.coreUserExists(recipient) ? 1e8 : 0; 43 | 44 | CoreSimulatorLib.nextBlock(); 45 | 46 | assertEq(PrecompileLib.spotBalance(address(recipient), 0).total, HLConversions.evmToWei(0, 1000e6) - activationFee); 47 | } 48 | 49 | function test_bridgeCoreToEvm() public { 50 | IERC20 USDC = IERC20(0xb88339CB7199b77E23DB6E890353E22632Ba630f); 51 | address user = makeAddr("user"); 52 | // Give the user some USDC, then bridge it to Core 53 | deal(address(USDC), user, 5000e6); 54 | vm.startPrank(user); 55 | CoreWriterLib.bridgeToCore(address(USDC), 1000e6); 56 | vm.stopPrank(); 57 | 58 | // Move to next block to process bridge 59 | CoreSimulatorLib.nextBlock(); 60 | PrecompileLib.SpotBalance memory spotBalance = PrecompileLib.spotBalance(address(user), 0); 61 | 62 | assertEq(spotBalance.total, HLConversions.evmToWei(0, 1000e6) - 1e8); 63 | 64 | // Bridge from Core back to EVM (simulate core withdrawal) 65 | vm.startPrank(user); 66 | CoreWriterLib.bridgeToEvm(address(USDC), 500e6); 67 | vm.stopPrank(); 68 | 69 | // Move to next block to process withdrawal 70 | CoreSimulatorLib.nextBlock(); 71 | 72 | // Expect user's EVM USDC balance increased by 500e6 73 | // (This assumes the CoreSimulatorLib processes withdrawal immediately.) 74 | assertEq(USDC.balanceOf(user), 4500e6); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/common/HLConversions.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import {HLConstants} from "./HLConstants.sol"; 5 | import {PrecompileLib} from "../PrecompileLib.sol"; 6 | import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; 7 | 8 | library HLConversions { 9 | error HLConversions__InvalidToken(uint64 token); 10 | 11 | /** 12 | * @dev Converts an EVM amount to a Core (wei) amount, handling both positive and negative extra decimals 13 | * Note: If evmExtraWeiDecimals > 0, and evmAmount < 10**evmExtraWeiDecimals, the result will be 0 14 | */ 15 | function evmToWei(uint64 token, uint256 evmAmount) internal view returns (uint64) { 16 | PrecompileLib.TokenInfo memory info = PrecompileLib.tokenInfo(uint32(token)); 17 | 18 | if (info.evmContract != address(0)) { 19 | if (info.evmExtraWeiDecimals > 0) { 20 | uint256 amount = evmAmount / (10 ** uint8(info.evmExtraWeiDecimals)); 21 | return SafeCast.toUint64(amount); 22 | } else if (info.evmExtraWeiDecimals < 0) { 23 | uint256 amount = evmAmount * (10 ** uint8(-info.evmExtraWeiDecimals)); 24 | return SafeCast.toUint64(amount); 25 | } 26 | } else if (HLConstants.isHype(token)) { 27 | return SafeCast.toUint64(evmAmount / (10 ** HLConstants.HYPE_EVM_EXTRA_DECIMALS)); 28 | } 29 | 30 | revert HLConversions__InvalidToken(token); 31 | } 32 | 33 | function evmToWei(address token, uint256 evmAmount) internal view returns (uint64) { 34 | return evmToWei(PrecompileLib.getTokenIndex(token), evmAmount); 35 | } 36 | 37 | function weiToEvm(uint64 token, uint64 amountWei) internal view returns (uint256) { 38 | PrecompileLib.TokenInfo memory info = PrecompileLib.tokenInfo(uint32(token)); 39 | if (info.evmContract != address(0)) { 40 | if (info.evmExtraWeiDecimals > 0) { 41 | return (uint256(amountWei) * (10 ** uint8(info.evmExtraWeiDecimals))); 42 | } else if (info.evmExtraWeiDecimals < 0) { 43 | return amountWei / (10 ** uint8(-info.evmExtraWeiDecimals)); 44 | } 45 | } else if (HLConstants.isHype(token)) { 46 | return (uint256(amountWei) * (10 ** HLConstants.HYPE_EVM_EXTRA_DECIMALS)); 47 | } 48 | 49 | revert HLConversions__InvalidToken(token); 50 | } 51 | 52 | function weiToEvm(address token, uint64 amountWei) internal view returns (uint256) { 53 | return weiToEvm(PrecompileLib.getTokenIndex(token), amountWei); 54 | } 55 | 56 | function szToWei(uint64 token, uint64 sz) internal view returns (uint64) { 57 | PrecompileLib.TokenInfo memory info = PrecompileLib.tokenInfo(uint32(token)); 58 | return sz * uint64(10 ** (info.weiDecimals - info.szDecimals)); 59 | } 60 | 61 | function szToWei(address token, uint64 sz) internal view returns (uint64) { 62 | return szToWei(PrecompileLib.getTokenIndex(token), sz); 63 | } 64 | 65 | function weiToSz(uint64 token, uint64 amountWei) internal view returns (uint64) { 66 | PrecompileLib.TokenInfo memory info = PrecompileLib.tokenInfo(uint32(token)); 67 | return amountWei / uint64(10 ** (info.weiDecimals - info.szDecimals)); 68 | } 69 | 70 | function weiToSz(address token, uint64 amountWei) internal view returns (uint64) { 71 | return weiToSz(PrecompileLib.getTokenIndex(token), amountWei); 72 | } 73 | 74 | // for USDC between spot and perp 75 | function weiToPerp(uint64 amountWei) internal pure returns (uint64) { 76 | return amountWei / 10 ** 2; 77 | } 78 | 79 | function perpToWei(uint64 perpAmount) internal pure returns (uint64) { 80 | return perpAmount * 10 ** 2; 81 | } 82 | 83 | function spotToAssetId(uint64 spot) internal pure returns (uint32) { 84 | return SafeCast.toUint32(spot + 10000); 85 | } 86 | 87 | function assetToSpotId(uint64 asset) internal pure returns (uint64) { 88 | return asset - 10000; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/registry/TokenRegistry.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | /** 5 | * @title TokenRegistry v1.0 6 | * @author Obsidian (https://x.com/ObsidianAudits) 7 | * @notice A trustless, onchain record of Hyperliquid token indices for each linked evm contract 8 | * @dev Data is sourced solely from the respective precompiles 9 | */ 10 | contract TokenRegistry { 11 | address constant TOKEN_INFO_PRECOMPILE_ADDRESS = 0x000000000000000000000000000000000000080C; 12 | 13 | /// @notice Maps evm contract addresses to their HyperCore token index 14 | mapping(address => TokenData) internal addressToIndex; 15 | 16 | /** 17 | * @notice Get the index of a token by passing in the evm contract address 18 | * @param evmContract The evm contract address of the token 19 | * @return index The index of the token 20 | * @dev Reverts with TokenNotFound if the contract address is not registered 21 | */ 22 | function getTokenIndex(address evmContract) external view returns (uint32 index) { 23 | TokenData memory data = addressToIndex[evmContract]; 24 | 25 | if (!data.isSet) { 26 | revert TokenNotFound(evmContract); 27 | } 28 | 29 | return data.index; 30 | } 31 | 32 | /** 33 | * @notice Register a token by passing in its index 34 | * @param tokenIndex The index of the token to register 35 | * @dev Calls the token info precompile and stores the mapping 36 | */ 37 | function setTokenInfo(uint32 tokenIndex) public { 38 | // call the precompile 39 | address evmContract = getTokenAddress(tokenIndex); 40 | 41 | if (evmContract == address(0)) { 42 | revert NoEvmContract(tokenIndex); 43 | } 44 | 45 | addressToIndex[evmContract] = TokenData({index: tokenIndex, isSet: true}); 46 | } 47 | 48 | /** 49 | * @notice Register a batch of tokens by passing in their indices 50 | * @param startIndex The index of the first token to register 51 | * @param endIndex The index of the last token to register 52 | * @dev Enable big blocks before calling this function 53 | */ 54 | function batchSetTokenInfo(uint32 startIndex, uint32 endIndex) external { 55 | for (uint32 i = startIndex; i <= endIndex; i++) { 56 | if (getTokenAddress(i) != address(0)) { 57 | setTokenInfo(i); 58 | } 59 | } 60 | } 61 | 62 | /** 63 | * @notice Check if a token is registered in the registry 64 | * @param evmContract The evm contract address of the token 65 | * @return bool True if the token is registered, false otherwise 66 | */ 67 | function hasTokenIndex(address evmContract) external view returns (bool) { 68 | TokenData memory data = addressToIndex[evmContract]; 69 | return data.isSet; 70 | } 71 | 72 | /** 73 | * @notice Get the evm contract address of a token by passing in the index 74 | * @param index The index of the token 75 | * @return evmContract The evm contract address of the token 76 | */ 77 | function getTokenAddress(uint32 index) public view returns (address) { 78 | (bool success, bytes memory result) = TOKEN_INFO_PRECOMPILE_ADDRESS.staticcall(abi.encode(index)); 79 | if (!success) revert PrecompileCallFailed(); 80 | TokenInfo memory info = abi.decode(result, (TokenInfo)); 81 | return info.evmContract; 82 | } 83 | 84 | struct TokenInfo { 85 | string name; 86 | uint64[] spots; 87 | uint64 deployerTradingFeeShare; 88 | address deployer; 89 | address evmContract; 90 | uint8 szDecimals; 91 | uint8 weiDecimals; 92 | int8 evmExtraWeiDecimals; 93 | } 94 | 95 | struct TokenData { 96 | uint32 index; 97 | bool isSet; // needed since index is 0 for uninitialized tokens, but is also a valid index 98 | } 99 | 100 | error TokenNotFound(address evmContract); 101 | error NoEvmContract(uint32 index); 102 | error PrecompileCallFailed(); 103 | } -------------------------------------------------------------------------------- /test/simulation/CoreWriterSim.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.28; 3 | 4 | import {Heap} from "@openzeppelin/contracts/utils/structs/Heap.sol"; 5 | import {Address} from "@openzeppelin/contracts/utils/Address.sol"; 6 | import {HyperCore, CoreExecution} from "./HyperCore.sol"; 7 | 8 | contract CoreWriterSim { 9 | using Address for address; 10 | using Heap for Heap.Uint256Heap; 11 | 12 | uint128 private _sequence; 13 | 14 | Heap.Uint256Heap private _actionQueue; 15 | 16 | struct Action { 17 | uint256 timestamp; 18 | bytes data; 19 | uint256 value; 20 | } 21 | 22 | mapping(uint256 id => Action) _actions; 23 | 24 | event RawAction(address indexed user, bytes data); 25 | 26 | HyperCore constant _hyperCore = HyperCore(payable(0x9999999999999999999999999999999999999999)); 27 | 28 | /////// testing config 29 | ///////////////////////// 30 | bool public revertOnFailure; 31 | 32 | function setRevertOnFailure(bool _revertOnFailure) public { 33 | revertOnFailure = _revertOnFailure; 34 | } 35 | 36 | function enqueueAction(bytes memory data, uint256 value) public { 37 | enqueueAction(block.timestamp, data, value); 38 | } 39 | 40 | function enqueueAction(uint256 timestamp, bytes memory data, uint256 value) public { 41 | uint256 uniqueId = (uint256(timestamp) << 128) | uint256(_sequence++); 42 | 43 | _actions[uniqueId] = Action(timestamp, data, value); 44 | _actionQueue.insert(uniqueId); 45 | } 46 | 47 | function executeQueuedActions(bool expectRevert) external { 48 | bool atLeastOneFail; 49 | while (_actionQueue.length() > 0) { 50 | Action memory action = _actions[_actionQueue.peek()]; 51 | 52 | // the action queue is a priority queue so the timestamp takes precedence in the 53 | // ordering which means we can safely stop processing if the actions are delayed 54 | if (action.timestamp > block.timestamp) { 55 | break; 56 | } 57 | 58 | (bool success,) = address(_hyperCore).call{value: action.value}(action.data); 59 | 60 | if (!success) { 61 | atLeastOneFail = true; 62 | } 63 | 64 | if (revertOnFailure && !success && !expectRevert) { 65 | revert("CoreWriter action failed: Reverting due to revertOnFailure flag"); 66 | } 67 | 68 | _actionQueue.pop(); 69 | } 70 | 71 | if (expectRevert && !atLeastOneFail) { 72 | revert("Expected revert, but action succeeded"); 73 | } 74 | 75 | _hyperCore.processStakingWithdrawals(); 76 | } 77 | 78 | function tokenTransferCallback(uint64 token, address from, uint256 value) public { 79 | // there's a special case when transferring to the L1 via the system address which 80 | // is that the balance isn't reflected on the L1 until after the EVM block has finished 81 | // and the subsequent EVM block has been processed, this means that the balance can be 82 | // in limbo for the user 83 | tokenTransferCallback(msg.sender, token, from, value); 84 | } 85 | 86 | function tokenTransferCallback(address sender, uint64 token, address from, uint256 value) public { 87 | enqueueAction(abi.encodeCall(CoreExecution.executeTokenTransfer, (sender, token, from, value)), 0); 88 | } 89 | 90 | function nativeTransferCallback(address sender, address from, uint256 value) public payable { 91 | enqueueAction(abi.encodeCall(CoreExecution.executeNativeTransfer, (sender, from, value)), value); 92 | } 93 | 94 | function sendRawAction(bytes calldata data) external { 95 | uint8 version = uint8(data[0]); 96 | require(version == 1); 97 | 98 | uint24 kind = (uint24(uint8(data[1])) << 16) | (uint24(uint8(data[2])) << 8) | (uint24(uint8(data[3]))); 99 | 100 | bytes memory call = abi.encodeCall(HyperCore.executeRawAction, (msg.sender, kind, data[4:])); 101 | 102 | enqueueAction(block.timestamp, call, 0); 103 | 104 | emit RawAction(msg.sender, data); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/examples/VaultExample.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import {CoreWriterLib, HLConversions} from "@hyper-evm-lib/src/CoreWriterLib.sol"; 5 | import {PrecompileLib} from "@hyper-evm-lib/src/PrecompileLib.sol"; 6 | 7 | /** 8 | * @title VaultExample 9 | * @dev This contract demonstrates CoreWriterLib vault functionality. 10 | */ 11 | contract VaultExample { 12 | using CoreWriterLib for *; 13 | 14 | uint64 public constant USDC_TOKEN_ID = 0; 15 | 16 | /*////////////////////////////////////////////////////////////// 17 | Basic Vault Operations 18 | //////////////////////////////////////////////////////////////*/ 19 | 20 | /** 21 | * @notice Deposits USDC to a specified vault 22 | * @param vault Address of the vault to deposit to 23 | * @param usdcAmount Amount of USDC to deposit 24 | */ 25 | function depositToVault(address vault, uint64 usdcAmount) external { 26 | CoreWriterLib.vaultTransfer(vault, true, usdcAmount); 27 | } 28 | 29 | /** 30 | * @notice Withdraws USDC from a specified vault 31 | * @param vault Address of the vault to withdraw from 32 | * @param usdcAmount Amount of USDC to withdraw 33 | */ 34 | function withdrawFromVault(address vault, uint64 usdcAmount) external { 35 | CoreWriterLib.vaultTransfer(vault, false, usdcAmount); 36 | } 37 | 38 | /*////////////////////////////////////////////////////////////// 39 | Advanced Vault Operations 40 | //////////////////////////////////////////////////////////////*/ 41 | 42 | /** 43 | * @notice Withdraws USDC from vault and sends to recipient 44 | * @dev vaultTransfer checks if funds are withdrawable and reverts if locked 45 | * @param vault Address of the vault to withdraw from 46 | * @param recipient Address to send the withdrawn USDC to 47 | * @param coreAmount Amount of USDC to withdraw and send 48 | */ 49 | function withdrawFromVaultAndSend(address vault, address recipient, uint64 coreAmount) external { 50 | uint64 usdcPerpAmount = HLConversions.weiToPerp(coreAmount); 51 | 52 | CoreWriterLib.vaultTransfer(vault, false, usdcPerpAmount); 53 | 54 | CoreWriterLib.transferUsdClass(usdcPerpAmount, false); 55 | 56 | CoreWriterLib.spotSend(recipient, USDC_TOKEN_ID, coreAmount); 57 | } 58 | 59 | /** 60 | * @notice Transfers USDC from spot to perp and deposits to vault 61 | * @param vault Address of the vault to deposit USDC to 62 | * @param coreAmount Amount of USDC to transfer and deposit to vault 63 | */ 64 | function transferUsdcToPerpAndDepositToVault(address vault, uint64 coreAmount) external { 65 | uint64 usdcPerpAmount = HLConversions.weiToPerp(coreAmount); 66 | 67 | CoreWriterLib.transferUsdClass(usdcPerpAmount, true); 68 | 69 | CoreWriterLib.vaultTransfer(vault, true, usdcPerpAmount); 70 | } 71 | 72 | /*////////////////////////////////////////////////////////////// 73 | Vault Information 74 | //////////////////////////////////////////////////////////////*/ 75 | 76 | /** 77 | * @notice Gets the vault equity for a user in a specific vault 78 | * @param user Address of the user to check 79 | * @param vault Address of the vault to check 80 | * @return equity Amount of equity the user has in the vault 81 | * @return lockedUntilTimestamp Timestamp until which the equity is locked 82 | */ 83 | function getVaultEquity(address user, address vault) 84 | public 85 | view 86 | returns (uint64 equity, uint64 lockedUntilTimestamp) 87 | { 88 | PrecompileLib.UserVaultEquity memory vaultEquity = PrecompileLib.userVaultEquity(user, vault); 89 | return (vaultEquity.equity, vaultEquity.lockedUntilTimestamp); 90 | } 91 | 92 | /** 93 | * @notice Checks if funds are withdrawable from a vault for a user 94 | * @param user Address of the user to check 95 | * @param vault Address of the vault to check 96 | * @return withdrawable True if funds can be withdrawn, false if locked 97 | */ 98 | function isWithdrawable(address user, address vault) public view returns (bool withdrawable) { 99 | PrecompileLib.UserVaultEquity memory vaultEquity = PrecompileLib.userVaultEquity(user, vault); 100 | return CoreWriterLib.toMilliseconds(uint64(block.timestamp)) >= vaultEquity.lockedUntilTimestamp; 101 | } 102 | 103 | receive() external payable {} 104 | } 105 | -------------------------------------------------------------------------------- /test/PrecompileLibTests.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import {Test, console} from "forge-std/Test.sol"; 5 | import {PrecompileLib} from "../src/PrecompileLib.sol"; 6 | import {HLConstants} from "../src/common/HLConstants.sol"; 7 | import {PrecompileSimulator} from "./utils/PrecompileSimulator.sol"; 8 | 9 | contract PrecompileLibTests is Test { 10 | using PrecompileLib for address; 11 | 12 | address public constant USDT0 = 0xB8CE59FC3717ada4C02eaDF9682A9e934F625ebb; 13 | address public constant uBTC = 0x9FDBdA0A5e284c32744D2f17Ee5c74B284993463; 14 | address public constant uETH = 0xBe6727B535545C67d5cAa73dEa54865B92CF7907; 15 | address public constant uSOL = 0x068f321Fa8Fb9f0D135f290Ef6a3e2813e1c8A29; 16 | 17 | function setUp() public { 18 | vm.createSelectFork("https://rpc.hyperliquid.xyz/evm"); 19 | PrecompileSimulator.init(); 20 | } 21 | 22 | function test_tokenInfo() public { 23 | PrecompileLib.TokenInfo memory info = USDT0.tokenInfo(); 24 | assertEq(info.name, "USDT0"); 25 | } 26 | 27 | function test_spotInfo() public { 28 | PrecompileLib.SpotInfo memory info = USDT0.spotInfo(); 29 | } 30 | 31 | function test_spotPx() public { 32 | uint64 px = USDT0.spotPx(); 33 | console.log("px: %e", px); 34 | } 35 | 36 | function test_normalizedSpotPrice() public { 37 | uint64 tokenIndex = USDT0.getTokenIndex(); 38 | uint64 spotIndex = PrecompileLib.getSpotIndex(tokenIndex); 39 | 40 | uint64 spotIndex_alt = PrecompileLib.getSpotIndex(tokenIndex, 0); 41 | assertEq(spotIndex, spotIndex_alt); 42 | 43 | uint256 spotIndex_alt2 = USDT0.getSpotIndex(); 44 | assertEq(spotIndex, spotIndex_alt2); 45 | 46 | uint256 price = PrecompileLib.normalizedSpotPx(spotIndex); 47 | 48 | console.log("price: %e", price); 49 | assertApproxEqAbs(price, 1e8, 1e5); 50 | } 51 | 52 | function test_normalizedMarkPx() public { 53 | uint256 price = PrecompileLib.normalizedMarkPx(0); 54 | console.log("BTC price: %e", price); 55 | assertApproxEqAbs(price, 114000e6, 40000e6); 56 | 57 | price = PrecompileLib.normalizedMarkPx(1); 58 | console.log("ETH price: %e", price); 59 | assertApproxEqAbs(price, 4000e6, 2000e6); 60 | } 61 | 62 | function test_normalizedOraclePrice() public { 63 | uint256 price = PrecompileLib.normalizedOraclePx(0); 64 | console.log("BTC price: %e", price); 65 | assertApproxEqAbs(price, 114000e6, 40000e6); 66 | 67 | price = PrecompileLib.normalizedOraclePx(1); 68 | console.log("ETH price: %e", price); 69 | assertApproxEqAbs(price, 4000e6, 2000e6); 70 | } 71 | 72 | function test_spotBalance() public { 73 | PrecompileLib.SpotBalance memory balance = 74 | PrecompileLib.spotBalance(0xF036a5261406a394bd63Eb4dF49C464634a66155, 150); 75 | console.log("balance: %e", balance.total); 76 | } 77 | 78 | function test_bbo() public { 79 | uint64 tokenIndex = uBTC.getTokenIndex(); 80 | uint64 spotIndex = PrecompileLib.getSpotIndex(tokenIndex); 81 | uint64 asset = spotIndex + 10000; 82 | PrecompileLib.Bbo memory bbo = PrecompileLib.bbo(asset); 83 | console.log("bid: %e", bbo.bid); 84 | console.log("ask: %e", bbo.ask); 85 | } 86 | 87 | function test_accountMarginSummary() public { 88 | address whale = 0x2Ba553d9F990a3B66b03b2dC0D030dfC1c061036; 89 | PrecompileLib.AccountMarginSummary memory summary = PrecompileLib.accountMarginSummary(0, whale); 90 | 91 | assertGt(summary.marginUsed, 0); 92 | assertGt(summary.ntlPos, 0); 93 | 94 | console.log("accountValue: %e", summary.accountValue); 95 | console.log("marginUsed: %e", summary.marginUsed); 96 | console.log("ntlPos: %e", summary.ntlPos); 97 | console.log("rawUsd: %e", summary.rawUsd); 98 | } 99 | 100 | function test_coreUserExists() public { 101 | address whale = 0x2Ba553d9F990a3B66b03b2dC0D030dfC1c061036; 102 | address whale2 = 0x751140B83d289353B3B6dA2c7e8659b3a0642F11; 103 | 104 | bool exists = PrecompileLib.coreUserExists(whale); 105 | bool exists2 = PrecompileLib.coreUserExists(whale2); 106 | 107 | assertEq(exists, true); 108 | assertEq(exists2, false); 109 | } 110 | 111 | function test_l1BlockNumber() public { 112 | uint64 blockNumber = PrecompileLib.l1BlockNumber(); 113 | console.log("L1 block number:", blockNumber); 114 | 115 | console.log("EVM block number:", block.number); 116 | 117 | assertGt(blockNumber, block.number); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hyper-evm-lib 2 | ![License](https://img.shields.io/github/license/hyperliquid-dev/hyper-evm-lib) 3 | ![Solidity](https://img.shields.io/badge/solidity-%3E%3D0.8.0-blue) 4 | 5 | Untitled design (2) 6 | 7 | ## The all-in-one toolkit to seamlessly build smart contracts on HyperEVM 8 | 9 | This library makes it easy to build on HyperEVM. It provides a unified interface for: 10 | 11 | * Bridging assets between HyperEVM and Core, abstracting away the complexity of decimal conversions 12 | * Performing all `CoreWriter` actions 13 | * Accessing data from native precompiles without needing a token index 14 | * Retrieving token indexes, and spot market indexes based on their linked evm contract address 15 | 16 | The library securely abstracts away the low-level mechanics of Hyperliquid's EVM ↔ Core interactions so you can focus on building your protocol's core business logic. 17 | 18 | The testing framework provides a robust simulation engine for HyperCore interactions, enabling local foundry testing of precompile calls, CoreWriter actions, and EVM⇄Core token bridging. This allows developers to test their contracts in a local environment, within seconds, without needing to spend hours deploying and testing on testnet. 19 | 20 | --- 21 | 22 | ## Key Components 23 | 24 | ### CoreWriterLib 25 | 26 | Includes functions to call `CoreWriter` actions, and also has helpers to: 27 | 28 | * Bridge tokens to/from Core 29 | * Convert spot token amount representation between EVM and Core (wei) decimals 30 | 31 | ### PrecompileLib 32 | 33 | Includes functionality to query the native read precompiles. 34 | 35 | PrecompileLib includes additional functions to query data using EVM token addresses, removing the need to store or pass in the token/spot index. 36 | 37 | ### TokenRegistry 38 | 39 | Precompiles like `spotBalance`, `spotPx` and more, all require either a token index (for `spotBalance`) or a spot market index (for `spotPx`) as an input parameter. 40 | 41 | Natively, there is no way to derive the token index given a token's contract address, requiring projects to store it manually, or pass it in as a parameter whenever needed. 42 | 43 | [TokenRegistry](https://github.com/hyperliquid-dev/hyper-evm-lib/blob/main/src/registry/TokenRegistry.sol) solves this by providing a deployed-onchain mapping from EVM contract addresses to their HyperCore token indices, populated trustlessly using precompile lookups for each index. 44 | 45 | ### Testing Framework 46 | 47 | A robust and flexible test engine for HyperCore interactions, enabling local foundry testing of precompile calls, CoreWriter actions, and EVM⇄Core token bridging. This allows developers to test their contracts in a local environment, within seconds, without needing to spend hours deploying and testing on testnet. 48 | 49 | For more information on usage and how it works, see the [docs](https://hyperlib.dev/testing/overview). 50 | 51 | --- 52 | 53 | ## Installation 54 | 55 | Install with **Foundry**: 56 | 57 | ```sh 58 | forge install hyperliquid-dev/hyper-evm-lib 59 | echo "@hyper-evm-lib=lib/hyper-evm-lib" >> remappings.txt 60 | ``` 61 | --- 62 | 63 | ## Usage Examples 64 | 65 | See the [examples](./src/examples/) directory for examples of how the libraries can be used in practice. 66 | 67 | To see how the testing framework can be used, refer to [`CoreSimulatorTest.t.sol`](./test/CoreSimulatorTest.t.sol) and the testing framework docs at [https://hyperlib.dev](https://hyperlib.dev/). 68 | 69 | --- 70 | 71 | ## Security Considerations 72 | 73 | * `bridgeToEvm()` for non-HYPE tokens requires the contract to hold HYPE on HyperCore for gas; otherwise, the `spotSend` will fail. 74 | * Be aware of potential precision loss in `evmToWei()` when the EVM token decimals exceed Core decimals, due to integer division during downscaling. 75 | * Ensure that contracts are deployed with complete functionality to prevent stuck assets in Core 76 | * For example, implementing `bridgeToCore` but not `bridgeToEvm` can lead to stuck, unretrievable assets on HyperCore 77 | * Note that precompiles return data from the start of the block, so CoreWriter actions will not be reflected in precompile data until next call. 78 | 79 | --- 80 | 81 | ## Contributing 82 | This toolkit is developed and maintained by the team at [Obsidian Audits](https://github.com/ObsidianAudits): 83 | 84 | - [0xjuaan](https://github.com/0xjuaan) 85 | - [0xSpearmint](https://github.com/0xspearmint) 86 | 87 | For support, bug reports, or integration questions, open an [issue](https://github.com/hyperliquid-dev/hyper-evm-lib/issues) or reach out on [TG](https://t.me/juan_sec) 88 | 89 | The library and testing framework are under active development, and contributions are welcome. 90 | 91 | Want to improve or extend functionality? Feel free to create a PR. 92 | 93 | Help us make building on Hyperliquid as smooth and secure as possible. 94 | -------------------------------------------------------------------------------- /src/examples/BridgingExample.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import {CoreWriterLib, HLConstants, HLConversions} from "@hyper-evm-lib/src/CoreWriterLib.sol"; 5 | import {PrecompileLib} from "@hyper-evm-lib/src/PrecompileLib.sol"; 6 | 7 | /** 8 | * @title BridgingExample 9 | * @dev This contract demonstrates CoreWriterLib bridging functionality. 10 | * Provides examples for bridging tokens between EVM and Core using both 11 | * token IDs and token addresses. 12 | */ 13 | contract BridgingExample { 14 | using CoreWriterLib for *; 15 | 16 | /*////////////////////////////////////////////////////////////// 17 | EVM to Core Bridging 18 | //////////////////////////////////////////////////////////////*/ 19 | 20 | /** 21 | * @notice Bridges tokens from EVM to Core using token ID 22 | */ 23 | function bridgeToCoreById(uint64 tokenId, uint256 evmAmount) external payable { 24 | // Bridge tokens to core using token ID 25 | CoreWriterLib.bridgeToCore(tokenId, evmAmount); 26 | } 27 | 28 | /** 29 | * @notice Bridges tokens from EVM to Core using token address 30 | */ 31 | function bridgeToCoreByAddress(address tokenAddress, uint256 evmAmount) external payable { 32 | // Bridge tokens to core using token address 33 | CoreWriterLib.bridgeToCore(tokenAddress, evmAmount); 34 | } 35 | 36 | /** 37 | * @notice Bridges USDC from EVM to Core for a specific recipient 38 | * @param recipient The address that will receive the USDC on Core 39 | * @param evmAmount The amount of USDC to bridge (in EVM decimals) 40 | */ 41 | function bridgeUsdcToCoreFor(address recipient, uint256 evmAmount) external payable { 42 | // Bridge USDC to core for a specific recipient 43 | CoreWriterLib.bridgeUsdcToCoreFor(recipient, evmAmount, HLConstants.SPOT_DEX); 44 | } 45 | 46 | /*////////////////////////////////////////////////////////////// 47 | Core to EVM Bridging 48 | //////////////////////////////////////////////////////////////*/ 49 | 50 | /** 51 | * @notice Bridges tokens from Core to EVM using token ID 52 | */ 53 | function bridgeToEvmById(uint64 tokenId, uint256 evmAmount) external { 54 | // Bridge tokens from core to EVM using token ID 55 | // Note: For non-HYPE tokens, the contract must hold some HYPE on core for gas 56 | CoreWriterLib.bridgeToEvm(tokenId, evmAmount, true); 57 | } 58 | 59 | /** 60 | * @notice Bridges tokens from Core to EVM using token address 61 | */ 62 | function bridgeToEvmByAddress(address tokenAddress, uint256 evmAmount) external { 63 | // Bridge tokens from core to EVM using token address 64 | // Note: For non-HYPE tokens, the contract must hold some HYPE on core for gas 65 | CoreWriterLib.bridgeToEvm(tokenAddress, evmAmount); 66 | } 67 | 68 | /** 69 | * @notice Bridges HYPE tokens from Core to EVM 70 | * @param evmAmount Amount of HYPE tokens to bridge (in EVM decimals) 71 | */ 72 | function bridgeHypeToEvm(uint256 evmAmount) external { 73 | // Bridge HYPE tokens from core to EVM 74 | CoreWriterLib.bridgeToEvm(HLConstants.hypeTokenIndex(), evmAmount, true); 75 | } 76 | 77 | /*////////////////////////////////////////////////////////////// 78 | Advanced Bridging Example 79 | //////////////////////////////////////////////////////////////*/ 80 | 81 | /** 82 | * @notice Bridges tokens to core and then sends them to another address 83 | */ 84 | function bridgeToCoreAndSend(address tokenAddress, uint256 evmAmount, address recipient) external payable { 85 | // Get token ID from address 86 | uint64 tokenId = PrecompileLib.getTokenIndex(tokenAddress); 87 | 88 | // Bridge tokens to core 89 | CoreWriterLib.bridgeToCore(tokenAddress, evmAmount); 90 | 91 | // Convert EVM amount to core amount 92 | uint64 coreAmount = HLConversions.evmToWei(tokenId, evmAmount); 93 | 94 | // Send tokens to recipient on core 95 | CoreWriterLib.spotSend(recipient, tokenId, coreAmount); 96 | } 97 | 98 | function bridgeToCoreAndSendHype(uint256 evmAmount, address recipient) external payable { 99 | // Bridge tokens to core 100 | CoreWriterLib.bridgeToCore(HLConstants.hypeTokenIndex(), evmAmount); 101 | 102 | // Convert EVM amount to core amount 103 | uint64 coreAmount = HLConversions.evmToWei(HLConstants.hypeTokenIndex(), evmAmount); 104 | 105 | // Send tokens to recipient on core 106 | CoreWriterLib.spotSend(recipient, HLConstants.hypeTokenIndex(), coreAmount); 107 | } 108 | 109 | /*////////////////////////////////////////////////////////////// 110 | Utility Functions 111 | //////////////////////////////////////////////////////////////*/ 112 | 113 | /** 114 | * @notice Gets the token index for a given token address 115 | */ 116 | function getTokenIndex(address tokenAddress) external view returns (uint64 tokenId) { 117 | return PrecompileLib.getTokenIndex(tokenAddress); 118 | } 119 | 120 | receive() external payable {} 121 | } 122 | -------------------------------------------------------------------------------- /src/common/HLConstants.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import {ICoreWriter} from "../interfaces/ICoreWriter.sol"; 5 | 6 | library HLConstants { 7 | /*////////////////////////////////////////////////////////////// 8 | Addresses 9 | //////////////////////////////////////////////////////////////*/ 10 | 11 | address constant POSITION_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000800; 12 | address constant SPOT_BALANCE_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000801; 13 | address constant VAULT_EQUITY_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000802; 14 | address constant WITHDRAWABLE_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000803; 15 | address constant DELEGATIONS_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000804; 16 | address constant DELEGATOR_SUMMARY_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000805; 17 | address constant MARK_PX_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000806; 18 | address constant ORACLE_PX_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000807; 19 | address constant SPOT_PX_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000808; 20 | address constant L1_BLOCK_NUMBER_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000809; 21 | address constant PERP_ASSET_INFO_PRECOMPILE_ADDRESS = 0x000000000000000000000000000000000000080a; 22 | address constant SPOT_INFO_PRECOMPILE_ADDRESS = 0x000000000000000000000000000000000000080b; 23 | address constant TOKEN_INFO_PRECOMPILE_ADDRESS = 0x000000000000000000000000000000000000080C; 24 | address constant TOKEN_SUPPLY_PRECOMPILE_ADDRESS = 0x000000000000000000000000000000000000080D; 25 | address constant BBO_PRECOMPILE_ADDRESS = 0x000000000000000000000000000000000000080e; 26 | address constant ACCOUNT_MARGIN_SUMMARY_PRECOMPILE_ADDRESS = 0x000000000000000000000000000000000000080F; 27 | address constant CORE_USER_EXISTS_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000810; 28 | 29 | uint160 constant BASE_SYSTEM_ADDRESS = uint160(0x2000000000000000000000000000000000000000); 30 | address constant HYPE_SYSTEM_ADDRESS = 0x2222222222222222222222222222222222222222; 31 | 32 | address constant USDC_EVM_CONTRACT = 0xb88339CB7199b77E23DB6E890353E22632Ba630f; 33 | address constant TESTNET_USDC_CONTRACT = 0x2B3370eE501B4a559b57D449569354196457D8Ab; 34 | 35 | address constant CORE_DEPOSIT_WALLET = 0x6B9E773128f453f5c2C60935Ee2DE2CBc5390A24; 36 | address constant TESTNET_CORE_DEPOSIT_WALLET = 0x0B80659a4076E9E93C7DbE0f10675A16a3e5C206; 37 | 38 | uint64 constant USDC_TOKEN_INDEX = 0; 39 | uint8 constant HYPE_EVM_EXTRA_DECIMALS = 10; 40 | 41 | /*////////////////////////////////////////////////////////////// 42 | HYPE Utils 43 | //////////////////////////////////////////////////////////////*/ 44 | function hypeTokenIndex() internal view returns (uint64) { 45 | return block.chainid == 998 ? 1105 : 150; 46 | } 47 | 48 | function isHype(uint64 index) internal view returns (bool) { 49 | return index == hypeTokenIndex(); 50 | } 51 | 52 | /*////////////////////////////////////////////////////////////// 53 | USDC Utils 54 | //////////////////////////////////////////////////////////////*/ 55 | function isUsdc(uint64 index) internal pure returns (bool) { 56 | return index == USDC_TOKEN_INDEX; 57 | } 58 | 59 | function usdc() internal view returns (address) { 60 | return block.chainid == 998 ? TESTNET_USDC_CONTRACT : USDC_EVM_CONTRACT; 61 | } 62 | 63 | function coreDepositWallet() internal view returns (address) { 64 | return block.chainid == 998 ? TESTNET_CORE_DEPOSIT_WALLET : CORE_DEPOSIT_WALLET; 65 | } 66 | 67 | /*////////////////////////////////////////////////////////////// 68 | CoreWriter Actions 69 | //////////////////////////////////////////////////////////////*/ 70 | 71 | uint24 constant LIMIT_ORDER_ACTION = 1; 72 | uint24 constant VAULT_TRANSFER_ACTION = 2; 73 | 74 | uint24 constant TOKEN_DELEGATE_ACTION = 3; 75 | uint24 constant STAKING_DEPOSIT_ACTION = 4; 76 | uint24 constant STAKING_WITHDRAW_ACTION = 5; 77 | 78 | uint24 constant SPOT_SEND_ACTION = 6; 79 | uint24 constant USD_CLASS_TRANSFER_ACTION = 7; 80 | 81 | uint24 constant FINALIZE_EVM_CONTRACT_ACTION = 8; 82 | uint24 constant ADD_API_WALLET_ACTION = 9; 83 | uint24 constant CANCEL_ORDER_BY_OID_ACTION = 10; 84 | uint24 constant CANCEL_ORDER_BY_CLOID_ACTION = 11; 85 | uint24 constant APPROVE_BUILDER_FEE_ACTION = 12; 86 | 87 | /*////////////////////////////////////////////////////////////// 88 | Limit Order Time in Force 89 | //////////////////////////////////////////////////////////////*/ 90 | 91 | uint8 public constant LIMIT_ORDER_TIF_ALO = 1; 92 | uint8 public constant LIMIT_ORDER_TIF_GTC = 2; 93 | uint8 public constant LIMIT_ORDER_TIF_IOC = 3; 94 | 95 | 96 | /*////////////////////////////////////////////////////////////// 97 | Dex Constants 98 | //////////////////////////////////////////////////////////////*/ 99 | uint32 constant DEFAULT_PERP_DEX = 0; 100 | uint32 constant SPOT_DEX = type(uint32).max; 101 | 102 | 103 | 104 | } 105 | 106 | -------------------------------------------------------------------------------- /test/utils/HypeTradingContract.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import {CoreWriterLib} from "../../src/CoreWriterLib.sol"; 5 | import {PrecompileLib} from "../../src/PrecompileLib.sol"; 6 | import {HLConstants} from "../../src/common/HLConstants.sol"; 7 | 8 | /** 9 | * @title HypeTradingContract 10 | * @dev A simple contract to place HYPE limit orders and view position data 11 | */ 12 | contract HypeTradingContract { 13 | using CoreWriterLib for *; 14 | 15 | address public owner; 16 | 17 | constructor(address _owner) { 18 | owner = _owner; 19 | } 20 | 21 | modifier onlyAuthorized() { 22 | require(msg.sender == owner, "Only authorized user can call this function"); 23 | _; 24 | } 25 | 26 | /** 27 | * @notice Get the HYPE token index based on current chain 28 | * @return HYPE token index (1105 for mainnet, 150 for testnet) 29 | */ 30 | function getHypeTokenIndex() public view returns (uint64) { 31 | return HLConstants.hypeTokenIndex(); 32 | } 33 | 34 | /** 35 | * @notice Places a limit order for any perp asset (can be used for both buy and sell orders) 36 | * @param perpId Perpetual asset ID to trade 37 | * @param isBuy True for buy/long position, false for sell/short position 38 | * @param limitPx Limit price for the order (set very high for market buy, very low for market sell) 39 | * @param sz Size/quantity of the order 40 | * @param reduceOnly True if order should only reduce existing position, false to open new position 41 | * @param cloid Client order ID for tracking 42 | */ 43 | function createLimitOrder(uint32 perpId, bool isBuy, uint64 limitPx, uint64 sz, bool reduceOnly, uint128 cloid) 44 | external 45 | onlyAuthorized 46 | { 47 | CoreWriterLib.placeLimitOrder( 48 | perpId, 49 | isBuy, 50 | limitPx, 51 | sz, 52 | reduceOnly, 53 | HLConstants.LIMIT_ORDER_TIF_IOC, // IOC for immediate execution (market-like behavior) 54 | cloid 55 | ); 56 | } 57 | 58 | /** 59 | * @notice View function to get position data from the position precompile 60 | * @param user Address of the user to get position for 61 | * @param perpIndex Perp index (use HYPE_ASSET_ID or other perp asset) 62 | * @return position Position data including size, entry price, PnL, leverage, and isolation status 63 | */ 64 | function getPosition(address user, uint16 perpIndex) 65 | external 66 | view 67 | returns (PrecompileLib.Position memory position) 68 | { 69 | return PrecompileLib.position(user, perpIndex); 70 | } 71 | 72 | /** 73 | * @notice Get position for a user for any perp asset 74 | * @param user Address of the user 75 | * @param perpIndex Perp index to get position for 76 | * @return position Position data for the specified perp 77 | */ 78 | function getUserPosition(address user, uint16 perpIndex) 79 | external 80 | view 81 | returns (PrecompileLib.Position memory position) 82 | { 83 | return PrecompileLib.position(user, perpIndex); 84 | } 85 | 86 | /** 87 | * @notice Cancel an order by client order ID 88 | * @param perpId Perpetual asset ID of the order to cancel 89 | * @param cloid Client order ID to cancel 90 | */ 91 | function cancelOrder(uint32 perpId, uint128 cloid) external onlyAuthorized { 92 | CoreWriterLib.cancelOrderByCloid(perpId, cloid); 93 | } 94 | 95 | /** 96 | * @notice Get account margin summary for trading 97 | * @param user Address of the user 98 | * @return marginSummary Account margin data including account value, margin used, etc. 99 | */ 100 | function getAccountMarginSummary(address user) 101 | external 102 | view 103 | returns (PrecompileLib.AccountMarginSummary memory marginSummary) 104 | { 105 | // Use perpDexIndex = 0 for main perp dex 106 | return PrecompileLib.accountMarginSummary(0, user); 107 | } 108 | 109 | /** 110 | * @notice Transfer USDC between spot and perp trading accounts 111 | * @param ntl Amount to transfer (in core decimals) 112 | * @param toPerp If true, transfers from spot to perp; if false, transfers from perp to spot 113 | */ 114 | function transferUsdClass(uint64 ntl, bool toPerp) external onlyAuthorized { 115 | CoreWriterLib.transferUsdClass(ntl, toPerp); 116 | } 117 | 118 | /** 119 | * @notice Send USDC to another address via spot transfer 120 | * @param to Address of the recipient 121 | * @param coreAmount Amount of USDC to transfer (in core decimals) 122 | */ 123 | function spotSendUsdc(address to, uint64 coreAmount) external onlyAuthorized { 124 | uint64 usdcTokenId = 0; // USDC token ID is 0 125 | CoreWriterLib.spotSend(to, usdcTokenId, coreAmount); 126 | } 127 | 128 | /** 129 | * @notice Send any token to another address via spot transfer 130 | * @param to Address of the recipient 131 | * @param token Token ID to transfer 132 | * @param coreAmount Amount to transfer (in core decimals) 133 | */ 134 | function spotSend(address to, uint64 token, uint64 coreAmount) external onlyAuthorized { 135 | CoreWriterLib.spotSend(to, token, coreAmount); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /test/simulation/PrecompileSim.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.28; 3 | 4 | import {HyperCore} from "./HyperCore.sol"; 5 | 6 | import {Vm} from "forge-std/Vm.sol"; 7 | 8 | /// @dev this contract is deployed for each different precompile address such that the fallback can be executed for each 9 | contract PrecompileSim { 10 | Vm internal constant vm = Vm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); 11 | HyperCore constant _hyperCore = HyperCore(payable(0x9999999999999999999999999999999999999999)); 12 | 13 | address constant POSITION_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000800; 14 | address constant SPOT_BALANCE_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000801; 15 | address constant VAULT_EQUITY_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000802; 16 | address constant WITHDRAWABLE_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000803; 17 | address constant DELEGATIONS_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000804; 18 | address constant DELEGATOR_SUMMARY_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000805; 19 | address constant MARK_PX_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000806; 20 | address constant ORACLE_PX_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000807; 21 | address constant SPOT_PX_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000808; 22 | address constant L1_BLOCK_NUMBER_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000809; 23 | address constant PERP_ASSET_INFO_PRECOMPILE_ADDRESS = 0x000000000000000000000000000000000000080a; 24 | address constant SPOT_INFO_PRECOMPILE_ADDRESS = 0x000000000000000000000000000000000000080b; 25 | address constant TOKEN_INFO_PRECOMPILE_ADDRESS = 0x000000000000000000000000000000000000080C; 26 | address constant TOKEN_SUPPLY_PRECOMPILE_ADDRESS = 0x000000000000000000000000000000000000080D; 27 | address constant BBO_PRECOMPILE_ADDRESS = 0x000000000000000000000000000000000000080e; 28 | address constant ACCOUNT_MARGIN_SUMMARY_PRECOMPILE_ADDRESS = 0x000000000000000000000000000000000000080F; 29 | address constant CORE_USER_EXISTS_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000810; 30 | 31 | receive() external payable {} 32 | 33 | fallback(bytes calldata data) external returns (bytes memory) { 34 | if (address(this) == SPOT_BALANCE_PRECOMPILE_ADDRESS) { 35 | (address user, uint64 token) = abi.decode(data, (address, uint64)); 36 | return abi.encode(_hyperCore.readSpotBalance(user, token)); 37 | } 38 | 39 | if (address(this) == VAULT_EQUITY_PRECOMPILE_ADDRESS) { 40 | (address user, address vault) = abi.decode(data, (address, address)); 41 | return abi.encode(_hyperCore.readUserVaultEquity(user, vault)); 42 | } 43 | 44 | if (address(this) == WITHDRAWABLE_PRECOMPILE_ADDRESS) { 45 | address user = abi.decode(data, (address)); 46 | return abi.encode(_hyperCore.readWithdrawable(user)); 47 | } 48 | 49 | if (address(this) == DELEGATIONS_PRECOMPILE_ADDRESS) { 50 | address user = abi.decode(data, (address)); 51 | return abi.encode(_hyperCore.readDelegations(user)); 52 | } 53 | 54 | if (address(this) == DELEGATOR_SUMMARY_PRECOMPILE_ADDRESS) { 55 | address user = abi.decode(data, (address)); 56 | return abi.encode(_hyperCore.readDelegatorSummary(user)); 57 | } 58 | 59 | if (address(this) == POSITION_PRECOMPILE_ADDRESS) { 60 | (address user, uint16 perp) = abi.decode(data, (address, uint16)); 61 | return abi.encode(_hyperCore.readPosition(user, perp)); 62 | } 63 | 64 | if (address(this) == CORE_USER_EXISTS_PRECOMPILE_ADDRESS) { 65 | address user = abi.decode(data, (address)); 66 | return abi.encode(_hyperCore.coreUserExists(user)); 67 | } 68 | 69 | if (address(this) == MARK_PX_PRECOMPILE_ADDRESS) { 70 | uint32 perp = abi.decode(data, (uint32)); 71 | return abi.encode(_hyperCore.readMarkPx(perp)); 72 | } 73 | 74 | if (address(this) == ORACLE_PX_PRECOMPILE_ADDRESS) { 75 | uint32 perp = abi.decode(data, (uint32)); 76 | return abi.encode(_hyperCore.readOraclePx(perp)); 77 | } 78 | 79 | if (address(this) == SPOT_PX_PRECOMPILE_ADDRESS) { 80 | uint64 spotIndex = abi.decode(data, (uint64)); 81 | return abi.encode(_hyperCore.readSpotPx(uint32(spotIndex))); 82 | } 83 | if (address(this) == PERP_ASSET_INFO_PRECOMPILE_ADDRESS) { 84 | uint32 perp = abi.decode(data, (uint32)); 85 | return abi.encode(_hyperCore.readPerpAssetInfo(perp)); 86 | } 87 | 88 | if (address(this) == SPOT_INFO_PRECOMPILE_ADDRESS) { 89 | uint32 spotMarketId = abi.decode(data, (uint32)); 90 | return abi.encode(_hyperCore.readSpotInfo(spotMarketId)); 91 | } 92 | 93 | if (address(this) == TOKEN_INFO_PRECOMPILE_ADDRESS) { 94 | uint32 token = abi.decode(data, (uint32)); 95 | return abi.encode(_hyperCore.readTokenInfo(token)); 96 | } 97 | 98 | if (address(this) == ACCOUNT_MARGIN_SUMMARY_PRECOMPILE_ADDRESS) { 99 | (uint16 perp_dex_index, address user) = abi.decode(data, (uint16, address)); 100 | return abi.encode(_hyperCore.readAccountMarginSummary(perp_dex_index, user)); 101 | } 102 | 103 | return _makeRpcCall(address(this), data); 104 | } 105 | 106 | function _makeRpcCall(address target, bytes memory params) internal returns (bytes memory) { 107 | // Construct the JSON-RPC payload 108 | string memory jsonPayload = 109 | string.concat('[{"to":"', vm.toString(target), '","data":"', vm.toString(params), '"},"latest"]'); 110 | 111 | // Make the RPC call 112 | return vm.rpc("eth_call", jsonPayload); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /test/unit-tests/FeeSimulationTest.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import {Test} from "forge-std/Test.sol"; 5 | import {HyperCore} from "../simulation/HyperCore.sol"; 6 | import {CoreSimulatorLib} from "../simulation/CoreSimulatorLib.sol"; 7 | import {PrecompileLib} from "../../src/PrecompileLib.sol"; 8 | import {CoreWriterLib} from "../../src/CoreWriterLib.sol"; 9 | import {HLConstants} from "../../src/common/HLConstants.sol"; 10 | import {HLConversions} from "../../src/common/HLConversions.sol"; 11 | 12 | contract FeeSimulationTest is Test { 13 | HyperCore hyperCore; 14 | address user = makeAddr("user"); 15 | 16 | uint64 constant USDC = 0; 17 | uint64 constant HYPE = 150; 18 | uint32 constant HYPE_SPOT = 107; 19 | uint16 constant HYPE_PERP = 159; 20 | 21 | function setUp() public { 22 | vm.createSelectFork("https://rpc.hyperliquid.xyz/evm"); 23 | hyperCore = CoreSimulatorLib.init(); 24 | 25 | CoreSimulatorLib.forceAccountActivation(user); 26 | CoreSimulatorLib.forceSpotBalance(user, USDC, 10_000e8); 27 | CoreSimulatorLib.forceSpotBalance(user, HYPE, 0); 28 | CoreSimulatorLib.forcePerpBalance(user, 10_000e6); 29 | CoreSimulatorLib.forcePerpLeverage(user, HYPE_PERP, 10); 30 | } 31 | 32 | function test_spotFee_onBuy() public { 33 | uint64 baseSz = 10e8; // 10 HYPE (in sz=8 decimals notation) 34 | uint64 usdcBefore = PrecompileLib.spotBalance(user, USDC).total; 35 | uint64 hypeBefore = PrecompileLib.spotBalance(user, HYPE).total; 36 | CoreSimulatorLib.setSpotPx(HYPE_SPOT, PrecompileLib.spotPx(HYPE_SPOT)); 37 | 38 | uint32 assetId = HLConversions.spotToAssetId(HYPE_SPOT); 39 | uint64 limitPx = uint64(PrecompileLib.normalizedSpotPx(HYPE_SPOT)); 40 | 41 | vm.startPrank(user); 42 | CoreWriterLib.placeLimitOrder(assetId, true, limitPx, baseSz, false, HLConstants.LIMIT_ORDER_TIF_IOC, 1); 43 | vm.stopPrank(); 44 | 45 | CoreSimulatorLib.nextBlock(); 46 | 47 | uint64 usdcAfter = PrecompileLib.spotBalance(user, USDC).total; 48 | uint64 hypeAfter = PrecompileLib.spotBalance(user, HYPE).total; 49 | 50 | PrecompileLib.TokenInfo memory hypeInfo = PrecompileLib.tokenInfo(HYPE); 51 | uint64 spotPxRaw = hyperCore.readSpotPx(HYPE_SPOT) * uint64(10 ** hypeInfo.szDecimals); 52 | uint64 amountIn = uint64((uint256(baseSz) * uint256(spotPxRaw)) / 1e8); 53 | uint64 fee = uint64(uint256(amountIn) * 400 / 1e6); 54 | 55 | assertEq(usdcBefore - usdcAfter, amountIn + fee, "USDC debit should equal notional plus maker fee"); 56 | assertEq(hypeAfter - hypeBefore, baseSz, "Exact base amount should be received"); 57 | } 58 | 59 | function test_spotFee_onSell() public { 60 | CoreSimulatorLib.setSpotMakerFee(280); 61 | CoreSimulatorLib.setSpotPx(HYPE_SPOT, PrecompileLib.spotPx(HYPE_SPOT)); 62 | 63 | // Provide inventory for selling 64 | CoreSimulatorLib.forceSpotBalance(user, HYPE, 20e8); 65 | CoreSimulatorLib.forceSpotBalance(user, USDC, 0); 66 | 67 | uint64 baseSz = 5e8; // 5 HYPE 68 | uint64 usdcBefore = PrecompileLib.spotBalance(user, USDC).total; 69 | uint64 hypeBefore = PrecompileLib.spotBalance(user, HYPE).total; 70 | 71 | uint32 assetId = HLConversions.spotToAssetId(HYPE_SPOT); 72 | uint64 limitPx = uint64(PrecompileLib.normalizedSpotPx(HYPE_SPOT)); 73 | 74 | vm.startPrank(user); 75 | CoreWriterLib.placeLimitOrder(assetId, false, limitPx, baseSz, false, HLConstants.LIMIT_ORDER_TIF_IOC, 1); 76 | vm.stopPrank(); 77 | 78 | CoreSimulatorLib.nextBlock(); 79 | 80 | uint64 usdcAfter = PrecompileLib.spotBalance(user, USDC).total; 81 | uint64 hypeAfter = PrecompileLib.spotBalance(user, HYPE).total; 82 | 83 | PrecompileLib.TokenInfo memory hypeInfo = PrecompileLib.tokenInfo(HYPE); 84 | uint64 spotPxRaw = hyperCore.readSpotPx(HYPE_SPOT) * uint64(10 ** hypeInfo.szDecimals); 85 | uint64 amountOut = uint64((uint256(baseSz) * uint256(spotPxRaw)) / 1e8); 86 | uint64 fee = uint64(uint256(amountOut) * 280 / 1e6); 87 | uint64 netProceeds = amountOut - fee; 88 | 89 | assertEq(usdcAfter - usdcBefore, netProceeds, "Quote proceeds should net out fee"); 90 | assertEq(hypeBefore - hypeAfter, baseSz, "Base balance should decrease by sell size"); 91 | } 92 | 93 | function test_perpFee_onLong() public { 94 | uint64 sz = 1e8; 95 | uint64 perpBalanceBefore = hyperCore.readPerpBalance(user); 96 | 97 | CoreSimulatorLib.setMarkPx(HYPE_PERP, PrecompileLib.markPx(HYPE_PERP)); 98 | 99 | uint256 startingPrice = PrecompileLib.markPx(HYPE_PERP); 100 | 101 | vm.startPrank(user); 102 | CoreWriterLib.placeLimitOrder(HYPE_PERP, true, type(uint64).max, sz, false, HLConstants.LIMIT_ORDER_TIF_IOC, 1); 103 | vm.stopPrank(); 104 | 105 | CoreSimulatorLib.nextBlock(); 106 | 107 | uint64 perpBalanceAfter = hyperCore.readPerpBalance(user); 108 | 109 | uint64 scaledSz = _scalePerpSz(sz); 110 | uint256 notional = uint256(scaledSz) * uint256(startingPrice); 111 | uint64 expectedFee = uint64(notional * 150 / 1e6); 112 | 113 | assertEq(perpBalanceBefore - perpBalanceAfter, expectedFee, "Perp balance should be debited by maker fee"); 114 | } 115 | 116 | function test_perpFee_onShort() public { 117 | CoreSimulatorLib.setMarkPx(HYPE_PERP, PrecompileLib.markPx(HYPE_PERP)); 118 | 119 | uint256 startingPrice = PrecompileLib.markPx(HYPE_PERP); 120 | 121 | uint64 sz = 2e8; 122 | uint64 perpBalanceBefore = hyperCore.readPerpBalance(user); 123 | 124 | vm.startPrank(user); 125 | CoreWriterLib.placeLimitOrder(HYPE_PERP, false, 0, sz, false, HLConstants.LIMIT_ORDER_TIF_IOC, 2); 126 | vm.stopPrank(); 127 | 128 | CoreSimulatorLib.nextBlock(); 129 | 130 | uint64 perpBalanceAfter = hyperCore.readPerpBalance(user); 131 | 132 | uint64 scaledSz = _scalePerpSz(sz); 133 | uint256 notional = uint256(scaledSz) * uint256(startingPrice); 134 | uint64 expectedFee = uint64(notional * 150 / 1e6); 135 | 136 | assertApproxEqAbs( 137 | perpBalanceBefore - perpBalanceAfter, expectedFee, 2, "Short orders should also pay maker fees on notional" 138 | ); 139 | } 140 | 141 | function _scalePerpSz(uint64 amount) internal returns (uint64) { 142 | uint8 perpSzDecimals = PrecompileLib.perpAssetInfo(HYPE_PERP).szDecimals; 143 | if (perpSzDecimals == 8) { 144 | return amount; 145 | } else if (perpSzDecimals < 8) { 146 | return amount / (uint64(10) ** (8 - perpSzDecimals)); 147 | } else { 148 | return amount * (uint64(10) ** (perpSzDecimals - 8)); 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /test/utils/L1Read.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | contract L1Read { 5 | struct Position { 6 | int64 szi; 7 | uint64 entryNtl; 8 | int64 isolatedRawUsd; 9 | uint32 leverage; 10 | bool isIsolated; 11 | } 12 | 13 | struct SpotBalance { 14 | uint64 total; 15 | uint64 hold; 16 | uint64 entryNtl; 17 | } 18 | 19 | struct UserVaultEquity { 20 | uint64 equity; 21 | uint64 lockedUntilTimestamp; 22 | } 23 | 24 | struct Withdrawable { 25 | uint64 withdrawable; 26 | } 27 | 28 | struct Delegation { 29 | address validator; 30 | uint64 amount; 31 | uint64 lockedUntilTimestamp; 32 | } 33 | 34 | struct DelegatorSummary { 35 | uint64 delegated; 36 | uint64 undelegated; 37 | uint64 totalPendingWithdrawal; 38 | uint64 nPendingWithdrawals; 39 | } 40 | 41 | struct PerpAssetInfo { 42 | string coin; 43 | uint32 marginTableId; 44 | uint8 szDecimals; 45 | uint8 maxLeverage; 46 | bool onlyIsolated; 47 | } 48 | 49 | struct SpotInfo { 50 | string name; 51 | uint64[2] tokens; 52 | } 53 | 54 | struct TokenInfo { 55 | string name; 56 | uint64[] spots; 57 | uint64 deployerTradingFeeShare; 58 | address deployer; 59 | address evmContract; 60 | uint8 szDecimals; 61 | uint8 weiDecimals; 62 | int8 evmExtraWeiDecimals; 63 | } 64 | 65 | struct UserBalance { 66 | address user; 67 | uint64 balance; 68 | } 69 | 70 | struct TokenSupply { 71 | uint64 maxSupply; 72 | uint64 totalSupply; 73 | uint64 circulatingSupply; 74 | uint64 futureEmissions; 75 | UserBalance[] nonCirculatingUserBalances; 76 | } 77 | 78 | struct Bbo { 79 | uint64 bid; 80 | uint64 ask; 81 | } 82 | 83 | struct AccountMarginSummary { 84 | int64 accountValue; 85 | uint64 marginUsed; 86 | uint64 ntlPos; 87 | int64 rawUsd; 88 | } 89 | 90 | struct CoreUserExists { 91 | bool exists; 92 | } 93 | 94 | address constant POSITION_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000800; 95 | address constant SPOT_BALANCE_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000801; 96 | address constant VAULT_EQUITY_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000802; 97 | address constant WITHDRAWABLE_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000803; 98 | address constant DELEGATIONS_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000804; 99 | address constant DELEGATOR_SUMMARY_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000805; 100 | address constant MARK_PX_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000806; 101 | address constant ORACLE_PX_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000807; 102 | address constant SPOT_PX_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000808; 103 | address constant L1_BLOCK_NUMBER_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000809; 104 | address constant PERP_ASSET_INFO_PRECOMPILE_ADDRESS = 0x000000000000000000000000000000000000080a; 105 | address constant SPOT_INFO_PRECOMPILE_ADDRESS = 0x000000000000000000000000000000000000080b; 106 | address constant TOKEN_INFO_PRECOMPILE_ADDRESS = 0x000000000000000000000000000000000000080C; 107 | address constant TOKEN_SUPPLY_PRECOMPILE_ADDRESS = 0x000000000000000000000000000000000000080D; 108 | address constant BBO_PRECOMPILE_ADDRESS = 0x000000000000000000000000000000000000080e; 109 | address constant ACCOUNT_MARGIN_SUMMARY_PRECOMPILE_ADDRESS = 0x000000000000000000000000000000000000080F; 110 | address constant CORE_USER_EXISTS_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000810; 111 | 112 | function position(address user, uint16 perp) external view returns (Position memory) { 113 | bool success; 114 | bytes memory result; 115 | (success, result) = POSITION_PRECOMPILE_ADDRESS.staticcall(abi.encode(user, perp)); 116 | require(success, "Position precompile call failed"); 117 | return abi.decode(result, (Position)); 118 | } 119 | 120 | function spotBalance(address user, uint64 token) external view returns (SpotBalance memory) { 121 | bool success; 122 | bytes memory result; 123 | (success, result) = SPOT_BALANCE_PRECOMPILE_ADDRESS.staticcall(abi.encode(user, token)); 124 | require(success, "SpotBalance precompile call failed"); 125 | return abi.decode(result, (SpotBalance)); 126 | } 127 | 128 | function userVaultEquity(address user, address vault) external view returns (UserVaultEquity memory) { 129 | bool success; 130 | bytes memory result; 131 | (success, result) = VAULT_EQUITY_PRECOMPILE_ADDRESS.staticcall(abi.encode(user, vault)); 132 | require(success, "VaultEquity precompile call failed"); 133 | return abi.decode(result, (UserVaultEquity)); 134 | } 135 | 136 | function withdrawable(address user) external view returns (Withdrawable memory) { 137 | bool success; 138 | bytes memory result; 139 | (success, result) = WITHDRAWABLE_PRECOMPILE_ADDRESS.staticcall(abi.encode(user)); 140 | require(success, "Withdrawable precompile call failed"); 141 | return abi.decode(result, (Withdrawable)); 142 | } 143 | 144 | function delegations(address user) external view returns (Delegation[] memory) { 145 | bool success; 146 | bytes memory result; 147 | (success, result) = DELEGATIONS_PRECOMPILE_ADDRESS.staticcall(abi.encode(user)); 148 | require(success, "Delegations precompile call failed"); 149 | return abi.decode(result, (Delegation[])); 150 | } 151 | 152 | function delegatorSummary(address user) external view returns (DelegatorSummary memory) { 153 | bool success; 154 | bytes memory result; 155 | (success, result) = DELEGATOR_SUMMARY_PRECOMPILE_ADDRESS.staticcall(abi.encode(user)); 156 | require(success, "DelegatorySummary precompile call failed"); 157 | return abi.decode(result, (DelegatorSummary)); 158 | } 159 | 160 | function markPx(uint32 index) external view returns (uint64) { 161 | bool success; 162 | bytes memory result; 163 | (success, result) = MARK_PX_PRECOMPILE_ADDRESS.staticcall(abi.encode(index)); 164 | require(success, "MarkPx precompile call failed"); 165 | return abi.decode(result, (uint64)); 166 | } 167 | 168 | function oraclePx(uint32 index) external view returns (uint64) { 169 | bool success; 170 | bytes memory result; 171 | (success, result) = ORACLE_PX_PRECOMPILE_ADDRESS.staticcall(abi.encode(index)); 172 | require(success, "OraclePx precompile call failed"); 173 | return abi.decode(result, (uint64)); 174 | } 175 | 176 | function spotPx(uint32 index) external view returns (uint64) { 177 | bool success; 178 | bytes memory result; 179 | (success, result) = SPOT_PX_PRECOMPILE_ADDRESS.staticcall(abi.encode(index)); 180 | require(success, "SpotPx precompile call failed"); 181 | return abi.decode(result, (uint64)); 182 | } 183 | 184 | function l1BlockNumber() external view returns (uint64) { 185 | bool success; 186 | bytes memory result; 187 | (success, result) = L1_BLOCK_NUMBER_PRECOMPILE_ADDRESS.staticcall(abi.encode()); 188 | require(success, "L1BlockNumber precompile call failed"); 189 | return abi.decode(result, (uint64)); 190 | } 191 | 192 | function perpAssetInfo(uint32 perp) external view returns (PerpAssetInfo memory) { 193 | bool success; 194 | bytes memory result; 195 | (success, result) = PERP_ASSET_INFO_PRECOMPILE_ADDRESS.staticcall(abi.encode(perp)); 196 | require(success, "PerpAssetInfo precompile call failed"); 197 | return abi.decode(result, (PerpAssetInfo)); 198 | } 199 | 200 | function spotInfo(uint32 spot) external view returns (SpotInfo memory) { 201 | bool success; 202 | bytes memory result; 203 | (success, result) = SPOT_INFO_PRECOMPILE_ADDRESS.staticcall(abi.encode(spot)); 204 | require(success, "SpotInfo precompile call failed"); 205 | return abi.decode(result, (SpotInfo)); 206 | } 207 | 208 | function tokenInfo(uint32 token) external view returns (TokenInfo memory) { 209 | bool success; 210 | bytes memory result; 211 | (success, result) = TOKEN_INFO_PRECOMPILE_ADDRESS.staticcall(abi.encode(token)); 212 | require(success, "TokenInfo precompile call failed"); 213 | return abi.decode(result, (TokenInfo)); 214 | } 215 | 216 | function tokenSupply(uint32 token) external view returns (TokenSupply memory) { 217 | bool success; 218 | bytes memory result; 219 | (success, result) = TOKEN_SUPPLY_PRECOMPILE_ADDRESS.staticcall(abi.encode(token)); 220 | require(success, "TokenSupply precompile call failed"); 221 | return abi.decode(result, (TokenSupply)); 222 | } 223 | 224 | function bbo(uint32 asset) external view returns (Bbo memory) { 225 | bool success; 226 | bytes memory result; 227 | (success, result) = BBO_PRECOMPILE_ADDRESS.staticcall(abi.encode(asset)); 228 | require(success, "Bbo precompile call failed"); 229 | return abi.decode(result, (Bbo)); 230 | } 231 | 232 | function accountMarginSummary(uint32 perp_dex_index, address user) 233 | external 234 | view 235 | returns (AccountMarginSummary memory) 236 | { 237 | bool success; 238 | bytes memory result; 239 | (success, result) = ACCOUNT_MARGIN_SUMMARY_PRECOMPILE_ADDRESS.staticcall(abi.encode(perp_dex_index, user)); 240 | require(success, "Account margin summary precompile call failed"); 241 | return abi.decode(result, (AccountMarginSummary)); 242 | } 243 | 244 | function coreUserExists(address user) external view returns (CoreUserExists memory) { 245 | bool success; 246 | bytes memory result; 247 | (success, result) = CORE_USER_EXISTS_PRECOMPILE_ADDRESS.staticcall(abi.encode(user)); 248 | require(success, "Core user exists precompile call failed"); 249 | return abi.decode(result, (CoreUserExists)); 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /src/CoreWriterLib.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 6 | 7 | import {PrecompileLib} from "./PrecompileLib.sol"; 8 | import {HLConstants} from "./common/HLConstants.sol"; 9 | import {HLConversions} from "./common/HLConversions.sol"; 10 | 11 | import {ICoreWriter} from "./interfaces/ICoreWriter.sol"; 12 | import {ICoreDepositWallet} from "./interfaces/ICoreDepositWallet.sol"; 13 | 14 | /** 15 | * @title CoreWriterLib v1.1 16 | * @author Obsidian (https://x.com/ObsidianAudits) 17 | * @notice A library for interacting with HyperEVM's CoreWriter 18 | * 19 | * @dev Additional functionality for: 20 | * - Bridging assets between EVM and HyperCore 21 | * - Converting decimal representations between EVM and HyperCore amounts 22 | * - Security checks before sending actions to CoreWriter 23 | */ 24 | library CoreWriterLib { 25 | using SafeERC20 for IERC20; 26 | 27 | ICoreWriter constant coreWriter = ICoreWriter(0x3333333333333333333333333333333333333333); 28 | 29 | error CoreWriterLib__StillLockedUntilTimestamp(uint64 lockedUntilTimestamp); 30 | error CoreWriterLib__CannotSelfTransfer(); 31 | error CoreWriterLib__HypeTransferFailed(); 32 | error CoreWriterLib__CoreAmountTooLarge(uint256 amount); 33 | error CoreWriterLib__EvmAmountTooSmall(uint256 amount); 34 | 35 | /*////////////////////////////////////////////////////////////// 36 | EVM <---> Core Bridging 37 | //////////////////////////////////////////////////////////////*/ 38 | 39 | function bridgeToCore(address tokenAddress, uint256 evmAmount) internal { 40 | uint64 tokenIndex = PrecompileLib.getTokenIndex(tokenAddress); 41 | bridgeToCore(tokenIndex, evmAmount); 42 | } 43 | 44 | /** 45 | * @dev All tokens (including USDC) will be bridged to the spot dex 46 | */ 47 | function bridgeToCore(uint64 token, uint256 evmAmount) internal { 48 | ICoreDepositWallet coreDepositWallet = ICoreDepositWallet(HLConstants.coreDepositWallet()); 49 | 50 | // Check if amount would be 0 after conversion to prevent token loss 51 | uint64 coreAmount = HLConversions.evmToWei(token, evmAmount); 52 | if (coreAmount == 0) revert CoreWriterLib__EvmAmountTooSmall(evmAmount); 53 | address systemAddress = getSystemAddress(token); 54 | if (HLConstants.isUsdc(token)) { 55 | IERC20(HLConstants.usdc()).approve(address(coreDepositWallet), evmAmount); 56 | coreDepositWallet.deposit(evmAmount, uint32(type(uint32).max)); 57 | } else if (isHype(token)) { 58 | (bool success,) = systemAddress.call{value: evmAmount}(""); 59 | if (!success) revert CoreWriterLib__HypeTransferFailed(); 60 | } else { 61 | PrecompileLib.TokenInfo memory info = PrecompileLib.tokenInfo(uint32(token)); 62 | address tokenAddress = info.evmContract; 63 | IERC20(tokenAddress).safeTransfer(systemAddress, evmAmount); 64 | } 65 | } 66 | 67 | /** 68 | * @notice Bridges USDC from EVM to Core to a specific recipient 69 | * @param recipient The address that will receive the USDC on Core 70 | * @param evmAmount The amount of USDC to bridge (in EVM decimals) 71 | * @param destinationDex The dex to send the USDC to on Core (type(uint32).max for spot, 0 for default perp dex) 72 | */ 73 | function bridgeUsdcToCoreFor(address recipient, uint256 evmAmount, uint32 destinationDex) internal { 74 | ICoreDepositWallet coreDepositWallet = ICoreDepositWallet(HLConstants.coreDepositWallet()); 75 | 76 | // Check if amount would be 0 after conversion to prevent token loss 77 | uint64 coreAmount = HLConversions.evmToWei(HLConstants.USDC_TOKEN_INDEX, evmAmount); 78 | if (coreAmount == 0) revert CoreWriterLib__EvmAmountTooSmall(evmAmount); 79 | 80 | IERC20(HLConstants.usdc()).approve(address(coreDepositWallet), evmAmount); 81 | coreDepositWallet.depositFor(recipient, evmAmount, destinationDex); 82 | } 83 | 84 | function bridgeToEvm(address tokenAddress, uint256 evmAmount) internal { 85 | uint64 tokenIndex = PrecompileLib.getTokenIndex(tokenAddress); 86 | bridgeToEvm(tokenIndex, evmAmount, true); 87 | } 88 | 89 | // NOTE: For bridging non-HYPE tokens, the contract must hold some HYPE on core (enough to cover the transfer gas), otherwise spotSend will fail 90 | function bridgeToEvm(uint64 token, uint256 amount, bool isEvmAmount) internal { 91 | address systemAddress = getSystemAddress(token); 92 | 93 | uint64 coreAmount; 94 | if (isEvmAmount) { 95 | coreAmount = HLConversions.evmToWei(token, amount); 96 | if (coreAmount == 0) revert CoreWriterLib__EvmAmountTooSmall(amount); 97 | } else { 98 | if (amount > type(uint64).max) revert CoreWriterLib__CoreAmountTooLarge(amount); 99 | coreAmount = uint64(amount); 100 | } 101 | 102 | spotSend(systemAddress, token, coreAmount); 103 | } 104 | 105 | function spotSend(address to, uint64 token, uint64 amountWei) internal { 106 | // Self-transfers will always fail, so reverting here 107 | if (to == address(this)) revert CoreWriterLib__CannotSelfTransfer(); 108 | 109 | coreWriter.sendRawAction( 110 | abi.encodePacked(uint8(1), HLConstants.SPOT_SEND_ACTION, abi.encode(to, token, amountWei)) 111 | ); 112 | } 113 | 114 | /*////////////////////////////////////////////////////////////// 115 | Bridging Utils 116 | //////////////////////////////////////////////////////////////*/ 117 | 118 | function getSystemAddress(uint64 index) internal view returns (address) { 119 | if (index == HLConstants.hypeTokenIndex()) { 120 | return HLConstants.HYPE_SYSTEM_ADDRESS; 121 | } 122 | return address(HLConstants.BASE_SYSTEM_ADDRESS + index); 123 | } 124 | 125 | function isHype(uint64 index) internal view returns (bool) { 126 | return index == HLConstants.hypeTokenIndex(); 127 | } 128 | 129 | /*////////////////////////////////////////////////////////////// 130 | Staking 131 | //////////////////////////////////////////////////////////////*/ 132 | function delegateToken(address validator, uint64 amountWei, bool undelegate) internal { 133 | coreWriter.sendRawAction( 134 | abi.encodePacked(uint8(1), HLConstants.TOKEN_DELEGATE_ACTION, abi.encode(validator, amountWei, undelegate)) 135 | ); 136 | } 137 | 138 | function depositStake(uint64 amountWei) internal { 139 | coreWriter.sendRawAction(abi.encodePacked(uint8(1), HLConstants.STAKING_DEPOSIT_ACTION, abi.encode(amountWei))); 140 | } 141 | 142 | function withdrawStake(uint64 amountWei) internal { 143 | coreWriter.sendRawAction(abi.encodePacked(uint8(1), HLConstants.STAKING_WITHDRAW_ACTION, abi.encode(amountWei))); 144 | } 145 | 146 | /*////////////////////////////////////////////////////////////// 147 | Trading 148 | //////////////////////////////////////////////////////////////*/ 149 | 150 | function toMilliseconds(uint64 timestamp) internal pure returns (uint64) { 151 | return timestamp * 1000; 152 | } 153 | 154 | function _canWithdrawFromVault(address vault) internal view returns (bool, uint64) { 155 | PrecompileLib.UserVaultEquity memory vaultEquity = PrecompileLib.userVaultEquity(address(this), vault); 156 | 157 | return 158 | ( 159 | toMilliseconds(uint64(block.timestamp)) > vaultEquity.lockedUntilTimestamp, 160 | vaultEquity.lockedUntilTimestamp 161 | ); 162 | } 163 | 164 | function vaultTransfer(address vault, bool isDeposit, uint64 usdAmount) internal { 165 | if (!isDeposit) { 166 | (bool canWithdraw, uint64 lockedUntilTimestamp) = _canWithdrawFromVault(vault); 167 | 168 | if (!canWithdraw) revert CoreWriterLib__StillLockedUntilTimestamp(lockedUntilTimestamp); 169 | } 170 | 171 | coreWriter.sendRawAction( 172 | abi.encodePacked(uint8(1), HLConstants.VAULT_TRANSFER_ACTION, abi.encode(vault, isDeposit, usdAmount)) 173 | ); 174 | } 175 | 176 | function transferUsdClass(uint64 ntl, bool toPerp) internal { 177 | coreWriter.sendRawAction( 178 | abi.encodePacked(uint8(1), HLConstants.USD_CLASS_TRANSFER_ACTION, abi.encode(ntl, toPerp)) 179 | ); 180 | } 181 | 182 | function placeLimitOrder( 183 | uint32 asset, 184 | bool isBuy, 185 | uint64 limitPx, 186 | uint64 sz, 187 | bool reduceOnly, 188 | uint8 encodedTif, 189 | uint128 cloid 190 | ) internal { 191 | coreWriter.sendRawAction( 192 | abi.encodePacked( 193 | uint8(1), 194 | HLConstants.LIMIT_ORDER_ACTION, 195 | abi.encode(asset, isBuy, limitPx, sz, reduceOnly, encodedTif, cloid) 196 | ) 197 | ); 198 | } 199 | 200 | function addApiWallet(address wallet, string memory name) internal { 201 | coreWriter.sendRawAction( 202 | abi.encodePacked(uint8(1), HLConstants.ADD_API_WALLET_ACTION, abi.encode(wallet, name)) 203 | ); 204 | } 205 | 206 | function cancelOrderByOrderId(uint32 asset, uint64 orderId) internal { 207 | coreWriter.sendRawAction( 208 | abi.encodePacked(uint8(1), HLConstants.CANCEL_ORDER_BY_OID_ACTION, abi.encode(asset, orderId)) 209 | ); 210 | } 211 | 212 | function cancelOrderByCloid(uint32 asset, uint128 cloid) internal { 213 | coreWriter.sendRawAction( 214 | abi.encodePacked(uint8(1), HLConstants.CANCEL_ORDER_BY_CLOID_ACTION, abi.encode(asset, cloid)) 215 | ); 216 | } 217 | 218 | function finalizeEvmContract(uint64 token, uint8 encodedVariant, uint64 createNonce) internal { 219 | coreWriter.sendRawAction( 220 | abi.encodePacked( 221 | uint8(1), HLConstants.FINALIZE_EVM_CONTRACT_ACTION, abi.encode(token, encodedVariant, createNonce) 222 | ) 223 | ); 224 | } 225 | 226 | function approveBuilderFee(uint64 maxFeeRate, address builder) internal { 227 | coreWriter.sendRawAction( 228 | abi.encodePacked(uint8(1), HLConstants.APPROVE_BUILDER_FEE_ACTION, abi.encode(maxFeeRate, builder)) 229 | ); 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /test/unit-tests/OfflineTest.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import {Test, console} from "forge-std/Test.sol"; 5 | import {PrecompileLib} from "../../src/PrecompileLib.sol"; 6 | import {HLConversions} from "../../src/common/HLConversions.sol"; 7 | import {HLConstants} from "../../src/common/HLConstants.sol"; 8 | import {BridgingExample} from "../../src/examples/BridgingExample.sol"; 9 | import {HyperCore} from "../simulation/HyperCore.sol"; 10 | import {L1Read} from "../utils/L1Read.sol"; 11 | import {HypeTradingContract} from "../utils/HypeTradingContract.sol"; 12 | import {CoreSimulatorLib} from "../simulation/CoreSimulatorLib.sol"; 13 | import {RealL1Read} from "../utils/RealL1Read.sol"; 14 | import {CoreWriterLib} from "../../src/CoreWriterLib.sol"; 15 | import {VaultExample} from "../../src/examples/VaultExample.sol"; 16 | import {StakingExample} from "../../src/examples/StakingExample.sol"; 17 | import {IERC20} from "forge-std/interfaces/IERC20.sol"; 18 | 19 | contract OfflineTest is Test { 20 | using PrecompileLib for address; 21 | using HLConversions for *; 22 | 23 | address public constant USDT0 = 0xB8CE59FC3717ada4C02eaDF9682A9e934F625ebb; 24 | 25 | HyperCore public hyperCore; 26 | address public user = makeAddr("user"); 27 | 28 | BridgingExample public bridgingExample; 29 | 30 | L1Read l1Read; 31 | 32 | function setUp() public { 33 | // set up the HyperCore simulation 34 | hyperCore = CoreSimulatorLib.init(); 35 | 36 | hyperCore.setUseRealL1Read(false); 37 | 38 | bridgingExample = new BridgingExample(); 39 | 40 | CoreSimulatorLib.forceAccountActivation(user); 41 | CoreSimulatorLib.forceAccountActivation(address(bridgingExample)); 42 | 43 | assertEq(PrecompileLib.coreUserExists(user), true); 44 | assertEq(PrecompileLib.coreUserExists(address(bridgingExample)), true); 45 | 46 | l1Read = new L1Read(); 47 | } 48 | 49 | function test_offline_bridgeHypeToCore() public { 50 | deal(address(user), 10000e18); 51 | 52 | vm.startPrank(user); 53 | bridgingExample.bridgeToCoreById{value: 1e18}(150, 1e18); 54 | 55 | (uint64 total, uint64 hold, uint64 entryNtl) = 56 | abi.decode(abi.encode(PrecompileLib.spotBalance(address(bridgingExample), 150)), (uint64, uint64, uint64)); 57 | assertEq(total, 0); 58 | 59 | CoreSimulatorLib.nextBlock(); 60 | 61 | (total, hold, entryNtl) = 62 | abi.decode(abi.encode(PrecompileLib.spotBalance(address(bridgingExample), 150)), (uint64, uint64, uint64)); 63 | assertEq(total, 1e8); 64 | } 65 | 66 | function test_offline_bridgeToCoreAndSend() public { 67 | deal(address(user), 10000e18); 68 | 69 | vm.startPrank(user); 70 | bridgingExample.bridgeToCoreAndSendHype{value: 1e18}(1e18, address(user)); 71 | 72 | (uint64 total, uint64 hold, uint64 entryNtl) = 73 | abi.decode(abi.encode(PrecompileLib.spotBalance(address(user), 150)), (uint64, uint64, uint64)); 74 | assertEq(total, 0); 75 | 76 | CoreSimulatorLib.nextBlock(); 77 | 78 | (total, hold, entryNtl) = 79 | abi.decode(abi.encode(PrecompileLib.spotBalance(address(user), 150)), (uint64, uint64, uint64)); 80 | assertEq(total, 1e8); 81 | } 82 | 83 | function test_offline_spotPrice() public { 84 | uint64 px = PrecompileLib.spotPx(107); 85 | assertEq(px, 0); 86 | 87 | CoreSimulatorLib.setSpotPx(107, 40e6); 88 | 89 | px = hyperCore.readSpotPx(107); 90 | assertEq(px, 40e6); 91 | } 92 | 93 | function test_offline_spotTrading() public { 94 | vm.startPrank(user); 95 | SpotTrader spotTrader = new SpotTrader(); 96 | CoreSimulatorLib.forceAccountActivation(address(spotTrader)); 97 | CoreSimulatorLib.forceAccountActivation(address(user)); 98 | CoreSimulatorLib.forceSpotBalance(address(spotTrader), 0, 1e18); 99 | CoreSimulatorLib.forceSpotBalance(address(spotTrader), 254, 1e18); 100 | 101 | CoreSimulatorLib.setRevertOnFailure(true); 102 | CoreSimulatorLib.setSpotPx(107, 40e6); // this represents a price of 40 USD per HYPE (represented with 8-szDecimals decimals) 103 | 104 | uint64 baseAmt = 100e8; // 100 HYPE 105 | uint64 quoteAmt = 10000e8; // 10k USDC (or quote token) 106 | 107 | // Store balances BEFORE order 108 | uint256 balanceAsset150Before = PrecompileLib.spotBalance(address(spotTrader), 150).total; 109 | uint256 balanceAsset0Before = PrecompileLib.spotBalance(address(spotTrader), 0).total; 110 | 111 | console.log("=== BEFORE ORDER ==="); 112 | console.log("Asset 150 balance:", balanceAsset150Before); 113 | console.log("Asset 0 balance:", balanceAsset0Before); 114 | console.log("Order: BUY", baseAmt, "base at price", quoteAmt); 115 | 116 | spotTrader.placeLimitOrder(10000 + 107, true, quoteAmt, baseAmt, false, 1); 117 | 118 | CoreSimulatorLib.nextBlock(); 119 | 120 | // Store balances AFTER order execution 121 | uint256 balanceAsset150After = PrecompileLib.spotBalance(address(spotTrader), 150).total; 122 | uint256 balanceAsset0After = PrecompileLib.spotBalance(address(spotTrader), 0).total; 123 | 124 | console.log("\n=== AFTER ORDER ==="); 125 | console.log("Asset 150 balance:", balanceAsset150After); 126 | console.log("Asset 0 balance:", balanceAsset0After); 127 | 128 | console.log("\n=== CHANGES ==="); 129 | console.log("Asset 150 change:", int256(balanceAsset150After) - int256(balanceAsset150Before)); 130 | console.log("Asset 0 change:", int256(balanceAsset0After) - int256(balanceAsset0Before)); 131 | 132 | // Verify the swap occurred as expected 133 | // For a BUY order: asset 0 (quote) should decrease, asset 150 (base) should increase 134 | assertLt(balanceAsset0After, balanceAsset0Before, "Quote asset should decrease"); 135 | assertGt(balanceAsset150After, balanceAsset150Before, "Base asset should increase"); 136 | } 137 | 138 | function test_offline_spot_limitOrder() public { 139 | vm.startPrank(user); 140 | SpotTrader spotTrader = new SpotTrader(); 141 | CoreSimulatorLib.forceAccountActivation(address(spotTrader)); 142 | CoreSimulatorLib.forceAccountActivation(address(user)); 143 | CoreSimulatorLib.forceSpotBalance(address(spotTrader), 0, 1e18); 144 | CoreSimulatorLib.forceSpotBalance(address(spotTrader), 150, 1e18); 145 | 146 | CoreSimulatorLib.setSpotPx(107, 40e6); 147 | 148 | // Log the current spot price before placing order 149 | uint32 spotMarketId = 107; 150 | uint64 currentSpotPx = uint64(PrecompileLib.normalizedSpotPx(spotMarketId)); 151 | 152 | console.log("currentSpotPx", currentSpotPx); 153 | console.log("=== INITIAL STATE ==="); 154 | console.log("Current spot price for market 107:", currentSpotPx); 155 | 156 | // Place a buy order with limit price below current spot price (won't execute immediately) 157 | uint64 limitPx = currentSpotPx / 2; // Set limit price below current price 158 | uint64 baseAmt = 1e8; // 1 HYPE 159 | 160 | console.log("Placing buy order:"); 161 | console.log(" Limit price:", limitPx); 162 | console.log(" Base amount:", baseAmt); 163 | console.log(" Expected executeNow:", limitPx >= currentSpotPx ? "true" : "false"); 164 | 165 | // Store balances BEFORE order placement 166 | uint256 balanceAsset150Before = PrecompileLib.spotBalance(address(spotTrader), 150).total; 167 | uint256 balanceAsset0Before = PrecompileLib.spotBalance(address(spotTrader), 0).total; 168 | 169 | console.log("\n=== BEFORE ORDER PLACEMENT ==="); 170 | console.log("Asset 150 balance:", balanceAsset150Before); 171 | console.log("Asset 0 balance:", balanceAsset0Before); 172 | 173 | spotTrader.placeLimitOrder(10000 + spotMarketId, true, limitPx, baseAmt, false, 1); 174 | 175 | CoreSimulatorLib.nextBlock(); 176 | 177 | // Store balances AFTER first block (order pending) 178 | uint256 balanceAsset150AfterBlock1 = PrecompileLib.spotBalance(address(spotTrader), 150).total; 179 | uint256 balanceAsset0AfterBlock1 = PrecompileLib.spotBalance(address(spotTrader), 0).total; 180 | 181 | console.log("\n=== AFTER FIRST BLOCK (Order Pending) ==="); 182 | console.log("Asset 150 balance:", balanceAsset150AfterBlock1); 183 | console.log("Asset 0 balance:", balanceAsset0AfterBlock1); 184 | console.log("Asset 150 change:", int256(balanceAsset150AfterBlock1) - int256(balanceAsset150Before)); 185 | console.log("Asset 0 change:", int256(balanceAsset0AfterBlock1) - int256(balanceAsset0Before)); 186 | 187 | // Now update the price to match the order's limit price 188 | console.log("\n=== UPDATING PRICE ==="); 189 | console.log("Setting spot price to:", limitPx / 100); 190 | CoreSimulatorLib.setSpotPx(spotMarketId, limitPx / 100); 191 | 192 | CoreSimulatorLib.nextBlock(); 193 | 194 | // Store balances AFTER price update (order executed) 195 | uint256 balanceAsset150AfterExecution = PrecompileLib.spotBalance(address(spotTrader), 150).total; 196 | uint256 balanceAsset0AfterExecution = PrecompileLib.spotBalance(address(spotTrader), 0).total; 197 | 198 | console.log("\n=== AFTER PRICE UPDATE (Order Executed) ==="); 199 | console.log("Asset 150 balance:", balanceAsset150AfterExecution); 200 | console.log("Asset 0 balance:", balanceAsset0AfterExecution); 201 | console.log( 202 | "Asset 150 change from pending:", int256(balanceAsset150AfterExecution) - int256(balanceAsset150AfterBlock1) 203 | ); 204 | console.log( 205 | "Asset 0 change from pending:", int256(balanceAsset0AfterExecution) - int256(balanceAsset0AfterBlock1) 206 | ); 207 | 208 | console.log("\n=== TOTAL CHANGES (from start) ==="); 209 | console.log("Asset 150 total change:", int256(balanceAsset150AfterExecution) - int256(balanceAsset150Before)); 210 | console.log("Asset 0 total change:", int256(balanceAsset0AfterExecution) - int256(balanceAsset0Before)); 211 | 212 | // Verify the limit order executed after price update 213 | // For a BUY order: asset 0 (quote) should decrease, asset 150 (base) should increase 214 | assertLt(balanceAsset0AfterExecution, balanceAsset0Before, "Quote asset should decrease after execution"); 215 | assertGt(balanceAsset150AfterExecution, balanceAsset150Before, "Base asset should increase after execution"); 216 | } 217 | } 218 | 219 | contract SpotTrader { 220 | function placeLimitOrder(uint32 asset, bool isBuy, uint64 limitPx, uint64 sz, bool reduceOnly, uint128 cloid) 221 | public 222 | { 223 | CoreWriterLib.placeLimitOrder(asset, isBuy, limitPx, sz, reduceOnly, HLConstants.LIMIT_ORDER_TIF_IOC, cloid); 224 | } 225 | 226 | function placeLimitOrderGTC(uint32 asset, bool isBuy, uint64 limitPx, uint64 sz, bool reduceOnly, uint128 cloid) 227 | public 228 | { 229 | CoreWriterLib.placeLimitOrder(asset, isBuy, limitPx, sz, reduceOnly, HLConstants.LIMIT_ORDER_TIF_GTC, cloid); 230 | } 231 | 232 | function bridgeToCore(address asset, uint64 amount) public { 233 | CoreWriterLib.bridgeToCore(asset, amount); 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /test/simulation/hyper-core/CoreView.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.28; 3 | 4 | import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; 5 | import {DoubleEndedQueue} from "@openzeppelin/contracts/utils/structs/DoubleEndedQueue.sol"; 6 | import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; 7 | import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; 8 | import {PrecompileLib} from "../../../src/PrecompileLib.sol"; 9 | import {RealL1Read} from "../../utils/RealL1Read.sol"; 10 | import {CoreState} from "./CoreState.sol"; 11 | 12 | /// Modified from https://github.com/ambitlabsxyz/hypercore 13 | contract CoreView is CoreState { 14 | using EnumerableSet for EnumerableSet.AddressSet; 15 | using DoubleEndedQueue for DoubleEndedQueue.Bytes32Deque; 16 | using EnumerableSet for EnumerableSet.UintSet; 17 | 18 | using SafeCast for uint256; 19 | 20 | function tokenExists(uint32 token) public view returns (bool) { 21 | return bytes(_tokens[token].name).length > 0; 22 | } 23 | 24 | function readTokenInfo(uint32 token) public returns (PrecompileLib.TokenInfo memory) { 25 | bool notStoredToken = _tokens[token].szDecimals == 0 && _tokens[token].deployer == address(0); 26 | 27 | if (notStoredToken && useRealL1Read) { 28 | return RealL1Read.tokenInfo(token); 29 | } 30 | return _tokens[token]; 31 | } 32 | 33 | function readSpotInfo(uint32 spotMarketId) public returns (PrecompileLib.SpotInfo memory) { 34 | if (bytes(_spotInfo[spotMarketId].name).length == 0 && useRealL1Read) { 35 | return RealL1Read.spotInfo(spotMarketId); 36 | } 37 | return _spotInfo[spotMarketId]; 38 | } 39 | 40 | function readPerpAssetInfo(uint32 perp) public returns (PrecompileLib.PerpAssetInfo memory) { 41 | if (bytes(_perpAssetInfo[perp].coin).length == 0 && useRealL1Read) { 42 | return RealL1Read.perpAssetInfo(perp); 43 | } 44 | return _perpAssetInfo[perp]; 45 | } 46 | 47 | function readMarkPx(uint32 perp) public returns (uint64) { 48 | if (_perpMarkPrice[perp] == 0 && useRealL1Read) { 49 | return RealL1Read.markPx(perp); 50 | } 51 | 52 | return _perpMarkPrice[perp]; 53 | } 54 | 55 | function readOraclePx(uint32 perp) public returns (uint64) { 56 | if (_perpOraclePrice[perp] == 0 && useRealL1Read) { 57 | return RealL1Read.oraclePx(perp); 58 | } 59 | 60 | return _perpOraclePrice[perp]; 61 | } 62 | 63 | function readSpotPx(uint32 spotMarketId) public returns (uint64) { 64 | if (_spotPrice[spotMarketId] == 0 && useRealL1Read) { 65 | return RealL1Read.spotPx(spotMarketId); 66 | } 67 | 68 | return _spotPrice[spotMarketId]; 69 | } 70 | 71 | function readSpotBalance(address account, uint64 token) public returns (PrecompileLib.SpotBalance memory) { 72 | if (_initializedSpotBalance[account][token] == false && useRealL1Read) { 73 | return RealL1Read.spotBalance(account, token); 74 | } 75 | 76 | return PrecompileLib.SpotBalance({total: _accounts[account].spot[token], entryNtl: 0, hold: 0}); 77 | } 78 | 79 | // Even if the HyperCore account is not created, the precompile returns 0 (it does not revert) 80 | function readWithdrawable(address account) public returns (PrecompileLib.Withdrawable memory) { 81 | if (_accounts[account].activated == false && useRealL1Read) { 82 | return PrecompileLib.Withdrawable({withdrawable: RealL1Read.withdrawable(account)}); 83 | } 84 | 85 | return _previewWithdrawable(account); 86 | } 87 | 88 | function readPerpBalance(address account) public returns (uint64) { 89 | if (_accounts[account].activated == false && useRealL1Read) { 90 | return RealL1Read.withdrawable(account); 91 | } 92 | 93 | return _accounts[account].perpBalance; 94 | } 95 | 96 | function readUserVaultEquity(address user, address vault) 97 | public 98 | view 99 | returns (PrecompileLib.UserVaultEquity memory) 100 | { 101 | PrecompileLib.UserVaultEquity memory equity = _accounts[user].vaultEquity[vault]; 102 | 103 | uint256 multiplier = _vaultMultiplier[vault] == 0 ? 1e18 : _vaultMultiplier[vault]; 104 | uint256 lastMultiplier = _userVaultMultiplier[user][vault] == 0 ? 1e18 : _userVaultMultiplier[user][vault]; 105 | if (multiplier != 0) equity.equity = uint64((uint256(equity.equity) * multiplier) / lastMultiplier); 106 | return equity; 107 | } 108 | 109 | function _getDelegationAmount(address user, address validator) internal view returns (uint64) { 110 | uint256 multiplier = _stakingYieldIndex; 111 | uint256 userLastMultiplier = 112 | _userStakingYieldIndex[user][validator] == 0 ? 1e18 : _userStakingYieldIndex[user][validator]; 113 | 114 | return 115 | SafeCast.toUint64(uint256(_accounts[user].delegations[validator].amount) * multiplier / userLastMultiplier); 116 | } 117 | 118 | function readDelegations(address user) public returns (PrecompileLib.Delegation[] memory userDelegations) { 119 | if (_accounts[user].activated == false && useRealL1Read) { 120 | return RealL1Read.delegations(user); 121 | } 122 | 123 | address[] memory validators = _accounts[user].delegatedValidators.values(); 124 | 125 | userDelegations = new PrecompileLib.Delegation[](validators.length); 126 | for (uint256 i; i < userDelegations.length; i++) { 127 | userDelegations[i].validator = validators[i]; 128 | userDelegations[i].amount = _getDelegationAmount(user, validators[i]); 129 | userDelegations[i].lockedUntilTimestamp = _accounts[user].delegations[validators[i]].lockedUntilTimestamp; 130 | } 131 | } 132 | 133 | function readDelegatorSummary(address user) public returns (PrecompileLib.DelegatorSummary memory summary) { 134 | if (_accounts[user].activated == false && useRealL1Read) { 135 | return RealL1Read.delegatorSummary(user); 136 | } 137 | 138 | address[] memory validators = _accounts[user].delegatedValidators.values(); 139 | 140 | for (uint256 i; i < validators.length; i++) { 141 | summary.delegated += _getDelegationAmount(user, validators[i]); 142 | } 143 | summary.undelegated = _accounts[user].staking; 144 | 145 | for (uint256 i; i < _withdrawQueue.length(); i++) { 146 | WithdrawRequest memory request = deserializeWithdrawRequest(_withdrawQueue.at(i)); 147 | if (request.account == user) { 148 | summary.nPendingWithdrawals++; 149 | summary.totalPendingWithdrawal += request.amount; 150 | } 151 | } 152 | } 153 | 154 | function readPosition(address user, uint16 perp) public returns (PrecompileLib.Position memory) { 155 | if (_accounts[user].activated == false && useRealL1Read) { 156 | return RealL1Read.position(user, perp); 157 | } 158 | 159 | return _accounts[user].positions[perp]; 160 | } 161 | 162 | function coreUserExists(address account) public returns (bool) { 163 | if (_accounts[account].activated == false && useRealL1Read) { 164 | return RealL1Read.coreUserExists(account); 165 | } 166 | 167 | return _accounts[account].activated; 168 | } 169 | 170 | function readAccountMarginSummary(uint16 perp_dex_index, address user) 171 | public 172 | returns (PrecompileLib.AccountMarginSummary memory) 173 | { 174 | // 1. maintain an enumerable set for the perps that a user is in 175 | // 2. iterate over their positions and calculate position value, add them up (value = abs(sz * markPx)) 176 | return _previewAccountMarginSummary(user); 177 | } 178 | 179 | function _previewAccountMarginSummary(address sender) internal returns (PrecompileLib.AccountMarginSummary memory) { 180 | uint64 totalNtlPos = 0; 181 | uint64 totalMarginUsed = 0; 182 | 183 | uint64 entryNtlByLeverage = 0; 184 | 185 | uint64 totalLongNtlPos = 0; 186 | uint64 totalShortNtlPos = 0; 187 | 188 | for (uint256 i = 0; i < _userPerpPositions[sender].length(); i++) { 189 | uint16 perpIndex = uint16(_userPerpPositions[sender].at(i)); 190 | 191 | PrecompileLib.Position memory position = _accounts[sender].positions[perpIndex]; 192 | 193 | uint32 leverage = position.leverage; 194 | uint64 markPx = readMarkPx(perpIndex); 195 | 196 | entryNtlByLeverage += position.entryNtl / leverage; 197 | 198 | int64 szi = position.szi; 199 | 200 | if (szi > 0) { 201 | uint64 ntlPos = uint64(szi) * markPx; 202 | totalNtlPos += ntlPos; 203 | totalMarginUsed += ntlPos / leverage; 204 | 205 | totalLongNtlPos += ntlPos; 206 | } else if (szi < 0) { 207 | uint64 ntlPos = uint64(-szi) * markPx; 208 | totalNtlPos += ntlPos; 209 | totalMarginUsed += ntlPos / leverage; 210 | 211 | totalShortNtlPos += ntlPos; 212 | } 213 | } 214 | 215 | int64 totalAccountValue = int64(_accounts[sender].perpBalance - entryNtlByLeverage + totalMarginUsed); 216 | int64 totalRawUsd = totalAccountValue - int64(totalLongNtlPos) + int64(totalShortNtlPos); 217 | 218 | return PrecompileLib.AccountMarginSummary({ 219 | accountValue: totalAccountValue, marginUsed: totalMarginUsed, ntlPos: totalNtlPos, rawUsd: totalRawUsd 220 | }); 221 | } 222 | 223 | function _previewWithdrawable(address account) internal returns (PrecompileLib.Withdrawable memory) { 224 | PrecompileLib.AccountMarginSummary memory summary = _previewAccountMarginSummary(account); 225 | 226 | uint64 transferMarginRequirement = 0; 227 | 228 | for (uint256 i = 0; i < _userPerpPositions[account].length(); i++) { 229 | uint16 perpIndex = uint16(_userPerpPositions[account].at(i)); 230 | PrecompileLib.Position memory position = _accounts[account].positions[perpIndex]; 231 | uint64 markPx = readMarkPx(perpIndex); 232 | 233 | uint64 ntlPos = 0; 234 | 235 | if (position.szi > 0) { 236 | ntlPos = uint64(position.szi) * markPx; 237 | } else if (position.szi < 0) { 238 | ntlPos = uint64(-position.szi) * markPx; 239 | } 240 | 241 | uint64 initialMargin = position.entryNtl / position.leverage; 242 | 243 | transferMarginRequirement += max(ntlPos / 10, initialMargin); 244 | } 245 | 246 | int64 withdrawable = summary.accountValue - int64(transferMarginRequirement); 247 | 248 | uint64 withdrawableAmount = withdrawable > 0 ? uint64(withdrawable) : 0; 249 | 250 | return PrecompileLib.Withdrawable({withdrawable: withdrawableAmount}); 251 | } 252 | 253 | function max(uint64 a, uint64 b) internal pure returns (uint64) { 254 | return a > b ? a : b; 255 | } 256 | 257 | function getTokenIndexFromSystemAddress(address systemAddr) internal pure returns (uint64) { 258 | if (systemAddr == address(0x2222222222222222222222222222222222222222)) { 259 | return 150; // HYPE token index 260 | } 261 | 262 | if (uint160(systemAddr) < uint160(0x2000000000000000000000000000000000000000)) return type(uint64).max; 263 | 264 | return uint64(uint160(systemAddr) - uint160(0x2000000000000000000000000000000000000000)); 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /test/utils/RealL1Read.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import {Vm} from "forge-std/Vm.sol"; 5 | import {PrecompileLib} from "../../src/PrecompileLib.sol"; 6 | import {HyperCore} from "../simulation/HyperCore.sol"; 7 | 8 | HyperCore constant hyperCore = HyperCore(payable(0x9999999999999999999999999999999999999999)); 9 | 10 | // Makes RPC calls to get real precompile data (independent of the test environment) 11 | // During offline mode, this will call the local precompile to return local data 12 | library RealL1Read { 13 | Vm constant vm = Vm(address(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D)); 14 | 15 | address constant POSITION_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000800; 16 | address constant SPOT_BALANCE_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000801; 17 | address constant VAULT_EQUITY_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000802; 18 | address constant WITHDRAWABLE_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000803; 19 | address constant DELEGATIONS_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000804; 20 | address constant DELEGATOR_SUMMARY_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000805; 21 | address constant MARK_PX_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000806; 22 | address constant ORACLE_PX_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000807; 23 | address constant SPOT_PX_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000808; 24 | address constant L1_BLOCK_NUMBER_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000809; 25 | address constant PERP_ASSET_INFO_PRECOMPILE_ADDRESS = 0x000000000000000000000000000000000000080a; 26 | address constant SPOT_INFO_PRECOMPILE_ADDRESS = 0x000000000000000000000000000000000000080b; 27 | address constant TOKEN_INFO_PRECOMPILE_ADDRESS = 0x000000000000000000000000000000000000080C; 28 | address constant TOKEN_SUPPLY_PRECOMPILE_ADDRESS = 0x000000000000000000000000000000000000080D; 29 | address constant BBO_PRECOMPILE_ADDRESS = 0x000000000000000000000000000000000000080e; 30 | address constant ACCOUNT_MARGIN_SUMMARY_PRECOMPILE_ADDRESS = 0x000000000000000000000000000000000000080F; 31 | address constant CORE_USER_EXISTS_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000810; 32 | 33 | address constant INVALID_ADDRESS = address(1); 34 | 35 | function _makeRpcCall(address target, bytes memory params) internal returns (bytes memory) { 36 | // Construct the JSON-RPC payload 37 | string memory jsonPayload = 38 | string.concat('[{"to":"', vm.toString(target), '","data":"', vm.toString(params), '"},"latest"]'); 39 | 40 | bool useArchivedBlockNumber = false; 41 | 42 | if (useArchivedBlockNumber) { 43 | string memory blockNumberHex = string.concat("0x", toHexString(block.number)); 44 | 45 | jsonPayload = string.concat( 46 | '[{"to":"', vm.toString(target), '","data":"', vm.toString(params), '"},"', blockNumberHex, '"]' 47 | ); 48 | } 49 | 50 | // Make the RPC call 51 | try vm.rpc("eth_call", jsonPayload) returns (bytes memory data) { 52 | return data; 53 | } catch { 54 | return ""; 55 | } 56 | } 57 | 58 | function toHexString(uint256 a) internal pure returns (string memory) { 59 | uint256 count = 0; 60 | uint256 b = a; 61 | while (b != 0) { 62 | count++; 63 | b /= 16; 64 | } 65 | bytes memory res = new bytes(count); 66 | for (uint256 i = 0; i < count; ++i) { 67 | b = a % 16; 68 | res[count - i - 1] = toHexDigit(uint8(b)); 69 | a /= 16; 70 | } 71 | return string(res); 72 | } 73 | 74 | function toHexDigit(uint8 d) internal pure returns (bytes1) { 75 | if (0 <= d && d <= 9) { 76 | return bytes1(uint8(bytes1("0")) + d); 77 | } else if (10 <= uint8(d) && uint8(d) <= 15) { 78 | return bytes1(uint8(bytes1("a")) + d - 10); 79 | } 80 | // revert("Invalid hex digit"); 81 | revert(); 82 | } 83 | 84 | function isOfflineMode() internal view returns (bool) { 85 | return !isForkActive() && !hyperCore.useRealL1Read(); 86 | } 87 | 88 | function isForkActive() internal view returns (bool) { 89 | try vm.activeFork() returns (uint256) { 90 | return true; // Fork is active 91 | } catch { 92 | return false; // No fork active 93 | } 94 | } 95 | 96 | function position(address user, uint16 perp) internal returns (PrecompileLib.Position memory) { 97 | if (isOfflineMode()) { 98 | return PrecompileLib.position(user, perp); 99 | } 100 | 101 | bytes memory result = _makeRpcCall(POSITION_PRECOMPILE_ADDRESS, abi.encode(user, perp)); 102 | 103 | if (result.length == 0) { 104 | return PrecompileLib.Position({szi: 0, entryNtl: 0, isolatedRawUsd: 0, leverage: 0, isIsolated: false}); 105 | } 106 | return abi.decode(result, (PrecompileLib.Position)); 107 | } 108 | 109 | function spotBalance(address user, uint64 token) internal returns (PrecompileLib.SpotBalance memory) { 110 | if (isOfflineMode()) { 111 | return PrecompileLib.spotBalance(user, token); 112 | } 113 | 114 | bytes memory result = _makeRpcCall(SPOT_BALANCE_PRECOMPILE_ADDRESS, abi.encode(user, token)); 115 | if (result.length == 0) { 116 | return PrecompileLib.SpotBalance({total: 0, hold: 0, entryNtl: 0}); 117 | } 118 | return abi.decode(result, (PrecompileLib.SpotBalance)); 119 | } 120 | 121 | function userVaultEquity(address user, address vault) internal returns (PrecompileLib.UserVaultEquity memory) { 122 | if (isOfflineMode()) { 123 | return PrecompileLib.userVaultEquity(user, vault); 124 | } 125 | 126 | bytes memory result = _makeRpcCall(VAULT_EQUITY_PRECOMPILE_ADDRESS, abi.encode(user, vault)); 127 | if (result.length == 0) { 128 | return PrecompileLib.UserVaultEquity({equity: 0, lockedUntilTimestamp: 0}); 129 | } 130 | return abi.decode(result, (PrecompileLib.UserVaultEquity)); 131 | } 132 | 133 | function withdrawable(address user) internal returns (uint64) { 134 | if (isOfflineMode()) { 135 | return PrecompileLib.withdrawable(user); 136 | } 137 | 138 | bytes memory result = _makeRpcCall(WITHDRAWABLE_PRECOMPILE_ADDRESS, abi.encode(user)); 139 | if (result.length == 0) { 140 | return 0; 141 | } 142 | return abi.decode(result, (PrecompileLib.Withdrawable)).withdrawable; 143 | } 144 | 145 | function delegations(address user) internal returns (PrecompileLib.Delegation[] memory) { 146 | if (isOfflineMode()) { 147 | return PrecompileLib.delegations(user); 148 | } 149 | 150 | bytes memory result = _makeRpcCall(DELEGATIONS_PRECOMPILE_ADDRESS, abi.encode(user)); 151 | if (result.length == 0) { 152 | return new PrecompileLib.Delegation[](0); 153 | } 154 | return abi.decode(result, (PrecompileLib.Delegation[])); 155 | } 156 | 157 | function delegatorSummary(address user) internal returns (PrecompileLib.DelegatorSummary memory) { 158 | if (isOfflineMode()) { 159 | return PrecompileLib.delegatorSummary(user); 160 | } 161 | 162 | bytes memory result = _makeRpcCall(DELEGATOR_SUMMARY_PRECOMPILE_ADDRESS, abi.encode(user)); 163 | return abi.decode(result, (PrecompileLib.DelegatorSummary)); 164 | } 165 | 166 | function markPx(uint32 index) internal returns (uint64) { 167 | if (isOfflineMode()) { 168 | return PrecompileLib.markPx(index); 169 | } 170 | 171 | bytes memory result = _makeRpcCall(MARK_PX_PRECOMPILE_ADDRESS, abi.encode(index)); 172 | return abi.decode(result, (uint64)); 173 | } 174 | 175 | function oraclePx(uint32 index) internal returns (uint64) { 176 | if (isOfflineMode()) { 177 | return PrecompileLib.oraclePx(index); 178 | } 179 | 180 | bytes memory result = _makeRpcCall(ORACLE_PX_PRECOMPILE_ADDRESS, abi.encode(index)); 181 | return abi.decode(result, (uint64)); 182 | } 183 | 184 | function spotPx(uint32 index) internal returns (uint64) { 185 | if (isOfflineMode()) { 186 | return PrecompileLib.spotPx(index); 187 | } 188 | 189 | bytes memory result = _makeRpcCall(SPOT_PX_PRECOMPILE_ADDRESS, abi.encode(index)); 190 | return abi.decode(result, (uint64)); 191 | } 192 | 193 | function l1BlockNumber() internal returns (uint64) { 194 | if (isOfflineMode()) { 195 | return PrecompileLib.l1BlockNumber(); 196 | } 197 | 198 | bytes memory result = _makeRpcCall(L1_BLOCK_NUMBER_PRECOMPILE_ADDRESS, abi.encode()); 199 | return abi.decode(result, (uint64)); 200 | } 201 | 202 | function perpAssetInfo(uint32 perp) internal returns (PrecompileLib.PerpAssetInfo memory) { 203 | if (isOfflineMode()) { 204 | return PrecompileLib.perpAssetInfo(perp); 205 | } 206 | 207 | bytes memory result = _makeRpcCall(PERP_ASSET_INFO_PRECOMPILE_ADDRESS, abi.encode(perp)); 208 | return abi.decode(result, (PrecompileLib.PerpAssetInfo)); 209 | } 210 | 211 | function spotInfo(uint32 spot) internal returns (PrecompileLib.SpotInfo memory) { 212 | if (isOfflineMode()) { 213 | return PrecompileLib.spotInfo(spot); 214 | } 215 | 216 | bytes memory result = _makeRpcCall(SPOT_INFO_PRECOMPILE_ADDRESS, abi.encode(spot)); 217 | return abi.decode(result, (PrecompileLib.SpotInfo)); 218 | } 219 | 220 | function tokenInfo(uint32 token) internal returns (PrecompileLib.TokenInfo memory) { 221 | if (isOfflineMode()) { 222 | return PrecompileLib.tokenInfo(token); 223 | } 224 | 225 | bytes memory result = _makeRpcCall(TOKEN_INFO_PRECOMPILE_ADDRESS, abi.encode(token)); 226 | if (result.length == 0) { 227 | return PrecompileLib.TokenInfo({ 228 | name: "", 229 | spots: new uint64[](0), 230 | deployerTradingFeeShare: 0, 231 | deployer: INVALID_ADDRESS, 232 | evmContract: INVALID_ADDRESS, 233 | szDecimals: 0, 234 | weiDecimals: 0, 235 | evmExtraWeiDecimals: 0 236 | }); 237 | } 238 | return abi.decode(result, (PrecompileLib.TokenInfo)); 239 | } 240 | 241 | function tokenSupply(uint32 token) internal returns (PrecompileLib.TokenSupply memory) { 242 | if (isOfflineMode()) { 243 | return PrecompileLib.tokenSupply(token); 244 | } 245 | 246 | bytes memory result = _makeRpcCall(TOKEN_SUPPLY_PRECOMPILE_ADDRESS, abi.encode(token)); 247 | return abi.decode(result, (PrecompileLib.TokenSupply)); 248 | } 249 | 250 | function bbo(uint32 asset) internal returns (PrecompileLib.Bbo memory) { 251 | if (isOfflineMode()) { 252 | return PrecompileLib.bbo(asset); 253 | } 254 | 255 | bytes memory result = _makeRpcCall(BBO_PRECOMPILE_ADDRESS, abi.encode(asset)); 256 | return abi.decode(result, (PrecompileLib.Bbo)); 257 | } 258 | 259 | function accountMarginSummary(uint32 perp_dex_index, address user) 260 | internal 261 | returns (PrecompileLib.AccountMarginSummary memory) 262 | { 263 | if (isOfflineMode()) { 264 | return PrecompileLib.accountMarginSummary(perp_dex_index, user); 265 | } 266 | 267 | bytes memory result = _makeRpcCall(ACCOUNT_MARGIN_SUMMARY_PRECOMPILE_ADDRESS, abi.encode(perp_dex_index, user)); 268 | return abi.decode(result, (PrecompileLib.AccountMarginSummary)); 269 | } 270 | 271 | function coreUserExists(address user) internal returns (bool) { 272 | if (isOfflineMode()) { 273 | return PrecompileLib.coreUserExists(user); 274 | } 275 | 276 | bytes memory result = _makeRpcCall(CORE_USER_EXISTS_PRECOMPILE_ADDRESS, abi.encode(user)); 277 | return abi.decode(result, (bool)); 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /test/simulation/CoreSimulatorLib.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import {Vm} from "forge-std/Vm.sol"; 5 | import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 6 | import {HyperCore} from "./HyperCore.sol"; 7 | import {CoreWriterSim} from "./CoreWriterSim.sol"; 8 | import {PrecompileSim} from "./PrecompileSim.sol"; 9 | 10 | import {PrecompileLib, HLConstants} from "../../src/PrecompileLib.sol"; 11 | import {TokenRegistry} from "../../src/registry/TokenRegistry.sol"; 12 | 13 | Vm constant vm = Vm(address(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D)); 14 | CoreWriterSim constant coreWriter = CoreWriterSim(0x3333333333333333333333333333333333333333); 15 | 16 | contract HypeSystemContract { 17 | receive() external payable { 18 | coreWriter.nativeTransferCallback{value: msg.value}(msg.sender, msg.sender, msg.value); 19 | } 20 | } 21 | 22 | /** 23 | * @title CoreSimulatorLib 24 | * @dev A library used to simulate HyperCore functionality in foundry tests 25 | */ 26 | library CoreSimulatorLib { 27 | uint256 constant NUM_PRECOMPILES = 17; 28 | 29 | HyperCore constant hyperCore = HyperCore(payable(0x9999999999999999999999999999999999999999)); 30 | 31 | // ERC20 Transfer event signature 32 | bytes32 constant TRANSFER_EVENT_SIG = keccak256("Transfer(address,address,uint256)"); 33 | 34 | function init() internal returns (HyperCore) { 35 | vm.pauseGasMetering(); 36 | 37 | HyperCore coreImpl = new HyperCore(); 38 | 39 | vm.etch(address(hyperCore), address(coreImpl).code); 40 | 41 | // Setting storage variables at the etched address 42 | hyperCore.setStakingYieldIndex(1e18); 43 | hyperCore.setUseRealL1Read(true); 44 | hyperCore.setSpotMakerFee(400); 45 | hyperCore.setPerpMakerFee(150); 46 | 47 | vm.etch(address(coreWriter), type(CoreWriterSim).runtimeCode); 48 | 49 | // Initialize precompiles 50 | for (uint160 i = 0; i < NUM_PRECOMPILES; i++) { 51 | address precompileAddress = address(uint160(0x0000000000000000000000000000000000000800) + i); 52 | vm.etch(precompileAddress, type(PrecompileSim).runtimeCode); 53 | vm.allowCheatcodes(precompileAddress); 54 | } 55 | 56 | // System addresses 57 | address hypeSystemAddress = address(0x2222222222222222222222222222222222222222); 58 | vm.etch(hypeSystemAddress, type(HypeSystemContract).runtimeCode); 59 | 60 | // Start recording logs for token transfer tracking 61 | vm.recordLogs(); 62 | 63 | vm.allowCheatcodes(address(hyperCore)); 64 | vm.allowCheatcodes(address(coreWriter)); 65 | 66 | // if offline mode, deploy the TokenRegistry and register main tokens 67 | if (!isForkActive()) { 68 | _deployTokenRegistryAndCoreTokens(); 69 | } 70 | 71 | vm.resumeGasMetering(); 72 | 73 | return hyperCore; 74 | } 75 | 76 | function nextBlock(bool expectRevert) internal { 77 | // Get all recorded logs 78 | Vm.Log[] memory entries = vm.getRecordedLogs(); 79 | 80 | // Process any ERC20 transfers to system addresses (EVM->Core transfers are processed before CoreWriter actions) 81 | for (uint256 i = 0; i < entries.length; i++) { 82 | Vm.Log memory entry = entries[i]; 83 | 84 | // Check if it's a Transfer event 85 | if (entry.topics[0] == TRANSFER_EVENT_SIG) { 86 | address from = address(uint160(uint256(entry.topics[1]))); 87 | address to = address(uint160(uint256(entry.topics[2]))); 88 | uint256 amount = abi.decode(entry.data, (uint256)); 89 | 90 | // Check if destination is a system address 91 | if (isSystemAddress(to)) { 92 | uint64 tokenIndex = getTokenIndexFromSystemAddress(to); 93 | 94 | // Call tokenTransferCallback on HyperCoreWrite 95 | hyperCore.executeTokenTransfer(address(0), tokenIndex, from, amount); 96 | } 97 | } 98 | } 99 | 100 | // Clear recorded logs for next block 101 | vm.recordLogs(); 102 | 103 | // Advance block 104 | vm.roll(block.number + 1); 105 | vm.warp(block.timestamp + 1); 106 | 107 | // liquidate any positions that are liquidatable 108 | hyperCore.liquidatePositions(); 109 | 110 | // Process any pending actions 111 | coreWriter.executeQueuedActions(expectRevert); 112 | 113 | // Process pending orders 114 | hyperCore.processPendingOrders(); 115 | } 116 | 117 | function nextBlock() internal { 118 | nextBlock(false); 119 | } 120 | 121 | ////// Testing Config Setters ///////// 122 | 123 | function setRevertOnFailure(bool _revertOnFailure) internal { 124 | coreWriter.setRevertOnFailure(_revertOnFailure); 125 | } 126 | 127 | // cheatcodes // 128 | function forceAccountActivation(address account) internal { 129 | hyperCore.forceAccountActivation(account); 130 | } 131 | 132 | function setOfflineMode(bool isOffline) internal { 133 | hyperCore.setUseRealL1Read(!isOffline); 134 | vm.warp(vm.unixTime() / 1e3); 135 | } 136 | 137 | function forceSpotBalance(address account, uint64 token, uint64 _wei) internal { 138 | hyperCore.forceSpotBalance(account, token, _wei); 139 | } 140 | 141 | function forcePerpBalance(address account, uint64 usd) internal { 142 | hyperCore.forcePerpBalance(account, usd); 143 | } 144 | 145 | function forceStakingBalance(address account, uint64 _wei) internal { 146 | hyperCore.forceStakingBalance(account, _wei); 147 | } 148 | 149 | function forceDelegation(address account, address validator, uint64 amount, uint64 lockedUntilTimestamp) internal { 150 | hyperCore.forceDelegation(account, validator, amount, lockedUntilTimestamp); 151 | } 152 | 153 | function forceVaultEquity(address account, address vault, uint64 usd, uint64 lockedUntilTimestamp) internal { 154 | hyperCore.forceVaultEquity(account, vault, usd, lockedUntilTimestamp); 155 | } 156 | 157 | function setMarkPx(uint32 perp, uint64 markPx) internal { 158 | hyperCore.setMarkPx(perp, markPx); 159 | } 160 | 161 | function setMarkPx(uint32 perp, uint64 priceDiffBps, bool isIncrease) internal { 162 | hyperCore.setMarkPx(perp, priceDiffBps, isIncrease); 163 | } 164 | 165 | function setSpotPx(uint32 spotMarketId, uint64 spotPx) internal { 166 | hyperCore.setSpotPx(spotMarketId, spotPx); 167 | } 168 | 169 | function setSpotPx(uint32 spotMarketId, uint64 priceDiffBps, bool isIncrease) internal { 170 | hyperCore.setSpotPx(spotMarketId, priceDiffBps, isIncrease); 171 | } 172 | 173 | function setVaultMultiplier(address vault, uint64 multiplier) internal { 174 | hyperCore.setVaultMultiplier(vault, multiplier); 175 | } 176 | 177 | function setStakingYieldIndex(uint64 multiplier) internal { 178 | hyperCore.setStakingYieldIndex(multiplier); 179 | } 180 | 181 | function setSpotMakerFee(uint16 bps) internal { 182 | hyperCore.setSpotMakerFee(bps); 183 | } 184 | 185 | function setPerpMakerFee(uint16 bps) internal { 186 | hyperCore.setPerpMakerFee(bps); 187 | } 188 | 189 | function forcePerpLeverage(address account, uint16 perp, uint32 leverage) internal { 190 | hyperCore.forcePerpPositionLeverage(account, perp, leverage); 191 | } 192 | 193 | ///// Private Functions ///// 194 | function _deployTokenRegistryAndCoreTokens() private { 195 | TokenRegistry registry = TokenRegistry(0x0b51d1A9098cf8a72C325003F44C194D41d7A85B); 196 | vm.etch(address(registry), type(TokenRegistry).runtimeCode); 197 | 198 | // register HYPE in hyperCore 199 | uint64[] memory hypeSpots = new uint64[](3); 200 | hypeSpots[0] = 107; 201 | hypeSpots[1] = 207; 202 | hypeSpots[2] = 232; 203 | PrecompileLib.TokenInfo memory hypeTokenInfo = PrecompileLib.TokenInfo({ 204 | name: "HYPE", 205 | spots: hypeSpots, 206 | deployerTradingFeeShare: 0, 207 | deployer: address(0), 208 | evmContract: address(0), 209 | szDecimals: 2, 210 | weiDecimals: 8, 211 | evmExtraWeiDecimals: 0 212 | }); 213 | hyperCore.registerTokenInfo(150, hypeTokenInfo); 214 | 215 | // register USDC in hyperCore 216 | uint64[] memory usdcSpots = new uint64[](0); 217 | PrecompileLib.TokenInfo memory usdcTokenInfo = PrecompileLib.TokenInfo({ 218 | name: "USDC", 219 | spots: usdcSpots, 220 | deployerTradingFeeShare: 0, 221 | deployer: address(0), 222 | evmContract: address(0), 223 | szDecimals: 8, 224 | weiDecimals: 8, 225 | evmExtraWeiDecimals: 0 226 | }); 227 | hyperCore.registerTokenInfo(0, usdcTokenInfo); 228 | 229 | // register USDT in hyperCore 230 | address usdt0 = 0xB8CE59FC3717ada4C02eaDF9682A9e934F625ebb; 231 | 232 | Token usdt0Token = new Token(); 233 | vm.etch(usdt0, address(usdt0Token).code); 234 | 235 | uint64[] memory usdt0Spots = new uint64[](1); 236 | usdt0Spots[0] = 166; 237 | PrecompileLib.TokenInfo memory usdtTokenInfo = PrecompileLib.TokenInfo({ 238 | name: "USDT0", 239 | spots: usdt0Spots, 240 | deployerTradingFeeShare: 0, 241 | deployer: 0x1a6362AD64ccFF5902D46D875B36e8798267d154, 242 | evmContract: usdt0, 243 | szDecimals: 2, 244 | weiDecimals: 8, 245 | evmExtraWeiDecimals: -2 246 | }); 247 | hyperCore.registerTokenInfo(268, usdtTokenInfo); 248 | registry.setTokenInfo(268); 249 | 250 | // register spot markets 251 | PrecompileLib.SpotInfo memory hypeSpotInfo = 252 | PrecompileLib.SpotInfo({name: "@107", tokens: [uint64(150), uint64(0)]}); 253 | hyperCore.registerSpotInfo(107, hypeSpotInfo); 254 | 255 | PrecompileLib.SpotInfo memory usdt0SpotInfo = 256 | PrecompileLib.SpotInfo({name: "@166", tokens: [uint64(268), uint64(0)]}); 257 | hyperCore.registerSpotInfo(166, usdt0SpotInfo); 258 | 259 | // register HYPE perp info 260 | PrecompileLib.PerpAssetInfo memory hypePerpAssetInfo = PrecompileLib.PerpAssetInfo({ 261 | coin: "HYPE", marginTableId: 52, szDecimals: 2, maxLeverage: 10, onlyIsolated: false 262 | }); 263 | hyperCore.registerPerpAssetInfo(150, hypePerpAssetInfo); 264 | } 265 | 266 | ///// VIEW AND PURE ///////// 267 | 268 | function isSystemAddress(address addr) internal view returns (bool) { 269 | // Check if it's the HYPE system address 270 | if (addr == address(0x2222222222222222222222222222222222222222)) { 271 | return true; 272 | } 273 | 274 | // Check if it's a token system address (0x2000...0000 + index) 275 | uint160 baseAddr = uint160(0x2000000000000000000000000000000000000000); 276 | uint160 addrInt = uint160(addr); 277 | 278 | if (addrInt >= baseAddr && addrInt < baseAddr + 10000) { 279 | uint64 tokenIndex = uint64(addrInt - baseAddr); 280 | 281 | if (tokenExists(tokenIndex)) { 282 | return true; 283 | } else { 284 | revert("Bridging failed: Corresponding token not found on HyperCore"); 285 | } 286 | } 287 | 288 | return false; 289 | } 290 | 291 | function getTokenIndexFromSystemAddress(address systemAddr) internal pure returns (uint64) { 292 | if (systemAddr == address(0x2222222222222222222222222222222222222222)) { 293 | return 150; // HYPE token index 294 | } 295 | 296 | if (uint160(systemAddr) < uint160(0x2000000000000000000000000000000000000000)) return type(uint64).max; 297 | 298 | return uint64(uint160(systemAddr) - uint160(0x2000000000000000000000000000000000000000)); 299 | } 300 | 301 | function tokenExists(uint64 token) internal view returns (bool) { 302 | (bool success,) = HLConstants.TOKEN_INFO_PRECOMPILE_ADDRESS.staticcall(abi.encode(token)); 303 | return success; 304 | } 305 | 306 | /// @dev Make an address persistent to prevent RPC storage calls 307 | /// Call this for any test addresses you create/etch to prevent RPC calls 308 | function makeAddressPersistent(address addr) internal { 309 | vm.makePersistent(addr); 310 | vm.deal(addr, 1 wei); // Ensure it "exists" in the fork 311 | } 312 | 313 | function isForkActive() internal view returns (bool) { 314 | try vm.activeFork() returns (uint256) { 315 | return true; // Fork is active 316 | } catch { 317 | return false; // No fork active 318 | } 319 | } 320 | } 321 | 322 | contract Token is ERC20 { 323 | constructor() ERC20("USDT0", "USDT0") {} 324 | } 325 | -------------------------------------------------------------------------------- /src/PrecompileLib.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import {ITokenRegistry} from "./interfaces/ITokenRegistry.sol"; 5 | import {HLConstants} from "./common/HLConstants.sol"; 6 | 7 | /** 8 | * @title PrecompileLib v1.0 9 | * @author Obsidian (https://x.com/ObsidianAudits) 10 | * @notice A library with helper functions for interacting with HyperEVM's precompiles 11 | */ 12 | library PrecompileLib { 13 | // Onchain record of token indices for each linked evm contract 14 | ITokenRegistry constant REGISTRY = ITokenRegistry(0x0b51d1A9098cf8a72C325003F44C194D41d7A85B); 15 | 16 | /*////////////////////////////////////////////////////////////// 17 | Custom Utility Functions 18 | (Overloads accepting token address instead of index) 19 | //////////////////////////////////////////////////////////////*/ 20 | 21 | /** 22 | * @notice Gets TokenInfo for a given token address by looking up its index and fetching from the precompile. 23 | * @dev Overload of tokenInfo(uint64 token) 24 | */ 25 | function tokenInfo(address tokenAddress) internal view returns (TokenInfo memory) { 26 | uint64 index = getTokenIndex(tokenAddress); 27 | return tokenInfo(index); 28 | } 29 | 30 | /** 31 | * @notice Gets SpotInfo for the token/USDC market using the token address. 32 | * @dev Overload of spotInfo(uint64 tokenIndex) 33 | * Finds the spot market where USDC (index 0) is the quote. 34 | */ 35 | function spotInfo(address tokenAddress) internal view returns (SpotInfo memory) { 36 | uint64 tokenIndex = getTokenIndex(tokenAddress); 37 | uint64 spotIndex = getSpotIndex(tokenIndex); 38 | return spotInfo(spotIndex); 39 | } 40 | 41 | /** 42 | * @notice Gets the spot price for the token/USDC market using the token address. 43 | * @dev Overload of spotPx(uint64 spotIndex) 44 | */ 45 | function spotPx(address tokenAddress) internal view returns (uint64) { 46 | uint64 tokenIndex = getTokenIndex(tokenAddress); 47 | uint64 spotIndex = getSpotIndex(tokenIndex); 48 | return spotPx(spotIndex); 49 | } 50 | 51 | /** 52 | * @notice Gets a user's spot balance for a given token address. 53 | * @dev Overload of spotBalance(address user, uint64 token) 54 | */ 55 | function spotBalance(address user, address tokenAddress) internal view returns (SpotBalance memory) { 56 | uint64 tokenIndex = getTokenIndex(tokenAddress); 57 | return spotBalance(user, tokenIndex); 58 | } 59 | 60 | /** 61 | * @notice Gets the index of a token from its address. Reverts if token is not linked to HyperCore. 62 | */ 63 | function getTokenIndex(address tokenAddress) internal view returns (uint64) { 64 | if (tokenAddress == HLConstants.usdc()) { 65 | return HLConstants.USDC_TOKEN_INDEX; 66 | } 67 | return REGISTRY.getTokenIndex(tokenAddress); 68 | } 69 | 70 | /** 71 | * @notice Gets the spot market index for the token/USDC pair for a token using its address. 72 | * @dev Overload of getSpotIndex(uint64 tokenIndex) 73 | */ 74 | function getSpotIndex(address tokenAddress) internal view returns (uint64) { 75 | uint64 tokenIndex = getTokenIndex(tokenAddress); 76 | return getSpotIndex(tokenIndex); 77 | } 78 | 79 | /** 80 | * @notice Gets the spot market index for a token. 81 | * @dev If only one spot market exists, returns it. Otherwise, finds the spot market with USDC as the quote token. 82 | */ 83 | function getSpotIndex(uint64 tokenIndex) internal view returns (uint64) { 84 | uint64[] memory spots = tokenInfo(tokenIndex).spots; 85 | 86 | if (spots.length == 1) return spots[0]; 87 | 88 | for (uint256 idx = 0; idx < spots.length; idx++) { 89 | SpotInfo memory spot = spotInfo(spots[idx]); 90 | if (spot.tokens[1] == 0) { 91 | // index 0 = USDC 92 | return spots[idx]; 93 | } 94 | } 95 | revert PrecompileLib__SpotIndexNotFound(); 96 | } 97 | 98 | /*////////////////////////////////////////////////////////////// 99 | Using Alternate Quote Token (non USDC) 100 | //////////////////////////////////////////////////////////////*/ 101 | 102 | /** 103 | * @notice Gets the spot market index for a token/quote pair. 104 | * Iterates all spot markets for the token and matches the quote token index. 105 | * @dev Overload of getSpotIndex(uint64 tokenIndex) 106 | */ 107 | function getSpotIndex(uint64 tokenIndex, uint64 quoteTokenIndex) internal view returns (uint64) { 108 | uint64[] memory spots = tokenInfo(tokenIndex).spots; 109 | 110 | for (uint256 idx = 0; idx < spots.length; idx++) { 111 | SpotInfo memory spot = spotInfo(spots[idx]); 112 | if (spot.tokens[1] == quoteTokenIndex) { 113 | return spots[idx]; 114 | } 115 | } 116 | revert PrecompileLib__SpotIndexNotFound(); 117 | } 118 | 119 | /** 120 | * @notice Gets SpotInfo for a token/quote pair using token addresses. 121 | * Looks up both token and quote indices, then finds the spot market. 122 | * @dev Overload of spotInfo(uint64 spotIndex) 123 | */ 124 | function spotInfo(address token, address quoteToken) internal view returns (SpotInfo memory) { 125 | uint64 tokenIndex = getTokenIndex(token); 126 | uint64 quoteTokenIndex = getTokenIndex(quoteToken); 127 | uint64 spotIndex = getSpotIndex(tokenIndex, quoteTokenIndex); 128 | return spotInfo(spotIndex); 129 | } 130 | 131 | /** 132 | * @notice Gets the spot price for a token/quote pair using token addresses. 133 | * Looks up both token and quote indices, then finds the spot market. 134 | * @dev Overload of spotPx(uint64 spotIndex) 135 | */ 136 | function spotPx(address token, address quoteToken) internal view returns (uint64) { 137 | uint64 tokenIndex = getTokenIndex(token); 138 | uint64 quoteTokenIndex = getTokenIndex(quoteToken); 139 | uint64 spotIndex = getSpotIndex(tokenIndex, quoteTokenIndex); 140 | return spotPx(spotIndex); 141 | } 142 | 143 | /*////////////////////////////////////////////////////////////// 144 | Price decimals normalization 145 | //////////////////////////////////////////////////////////////*/ 146 | 147 | // returns spot price as a fixed-point integer with 8 decimals 148 | function normalizedSpotPx(uint64 spotIndex) internal view returns (uint256) { 149 | SpotInfo memory info = spotInfo(spotIndex); 150 | uint8 baseSzDecimals = tokenInfo(info.tokens[0]).szDecimals; 151 | return spotPx(spotIndex) * 10 ** baseSzDecimals; 152 | } 153 | 154 | // returns mark price as a fixed-point integer with 6 decimals 155 | function normalizedMarkPx(uint32 perpIndex) internal view returns (uint256) { 156 | PerpAssetInfo memory info = perpAssetInfo(perpIndex); 157 | return markPx(perpIndex) * 10 ** info.szDecimals; 158 | } 159 | 160 | // returns perp oracle price as a fixed-point integer with 6 decimals 161 | function normalizedOraclePx(uint32 perpIndex) internal view returns (uint256) { 162 | PerpAssetInfo memory info = perpAssetInfo(perpIndex); 163 | return oraclePx(perpIndex) * 10 ** info.szDecimals; 164 | } 165 | 166 | /*////////////////////////////////////////////////////////////// 167 | Precompile Calls 168 | //////////////////////////////////////////////////////////////*/ 169 | 170 | function position(address user, uint16 perp) internal view returns (Position memory) { 171 | (bool success, bytes memory result) = HLConstants.POSITION_PRECOMPILE_ADDRESS.staticcall(abi.encode(user, perp)); 172 | if (!success) revert PrecompileLib__PositionPrecompileFailed(); 173 | return abi.decode(result, (Position)); 174 | } 175 | 176 | function spotBalance(address user, uint64 token) internal view returns (SpotBalance memory) { 177 | (bool success, bytes memory result) = 178 | HLConstants.SPOT_BALANCE_PRECOMPILE_ADDRESS.staticcall(abi.encode(user, token)); 179 | if (!success) revert PrecompileLib__SpotBalancePrecompileFailed(); 180 | return abi.decode(result, (SpotBalance)); 181 | } 182 | 183 | function userVaultEquity(address user, address vault) internal view returns (UserVaultEquity memory) { 184 | (bool success, bytes memory result) = 185 | HLConstants.VAULT_EQUITY_PRECOMPILE_ADDRESS.staticcall(abi.encode(user, vault)); 186 | if (!success) revert PrecompileLib__VaultEquityPrecompileFailed(); 187 | return abi.decode(result, (UserVaultEquity)); 188 | } 189 | 190 | function withdrawable(address user) internal view returns (uint64) { 191 | (bool success, bytes memory result) = HLConstants.WITHDRAWABLE_PRECOMPILE_ADDRESS.staticcall(abi.encode(user)); 192 | if (!success) revert PrecompileLib__WithdrawablePrecompileFailed(); 193 | return abi.decode(result, (Withdrawable)).withdrawable; 194 | } 195 | 196 | function delegations(address user) internal view returns (Delegation[] memory) { 197 | (bool success, bytes memory result) = HLConstants.DELEGATIONS_PRECOMPILE_ADDRESS.staticcall(abi.encode(user)); 198 | if (!success) revert PrecompileLib__DelegationsPrecompileFailed(); 199 | return abi.decode(result, (Delegation[])); 200 | } 201 | 202 | function delegatorSummary(address user) internal view returns (DelegatorSummary memory) { 203 | (bool success, bytes memory result) = 204 | HLConstants.DELEGATOR_SUMMARY_PRECOMPILE_ADDRESS.staticcall(abi.encode(user)); 205 | if (!success) revert PrecompileLib__DelegatorSummaryPrecompileFailed(); 206 | return abi.decode(result, (DelegatorSummary)); 207 | } 208 | 209 | function markPx(uint32 perpIndex) internal view returns (uint64) { 210 | (bool success, bytes memory result) = HLConstants.MARK_PX_PRECOMPILE_ADDRESS.staticcall(abi.encode(perpIndex)); 211 | if (!success) revert PrecompileLib__MarkPxPrecompileFailed(); 212 | return abi.decode(result, (uint64)); 213 | } 214 | 215 | function oraclePx(uint32 perpIndex) internal view returns (uint64) { 216 | (bool success, bytes memory result) = HLConstants.ORACLE_PX_PRECOMPILE_ADDRESS.staticcall(abi.encode(perpIndex)); 217 | if (!success) revert PrecompileLib__OraclePxPrecompileFailed(); 218 | return abi.decode(result, (uint64)); 219 | } 220 | 221 | function spotPx(uint64 spotIndex) internal view returns (uint64) { 222 | (bool success, bytes memory result) = HLConstants.SPOT_PX_PRECOMPILE_ADDRESS.staticcall(abi.encode(spotIndex)); 223 | if (!success) revert PrecompileLib__SpotPxPrecompileFailed(); 224 | return abi.decode(result, (uint64)); 225 | } 226 | 227 | function perpAssetInfo(uint32 perp) internal view returns (PerpAssetInfo memory) { 228 | (bool success, bytes memory result) = 229 | HLConstants.PERP_ASSET_INFO_PRECOMPILE_ADDRESS.staticcall(abi.encode(perp)); 230 | if (!success) revert PrecompileLib__PerpAssetInfoPrecompileFailed(); 231 | return abi.decode(result, (PerpAssetInfo)); 232 | } 233 | 234 | function spotInfo(uint64 spotIndex) internal view returns (SpotInfo memory) { 235 | (bool success, bytes memory result) = HLConstants.SPOT_INFO_PRECOMPILE_ADDRESS.staticcall(abi.encode(spotIndex)); 236 | if (!success) revert PrecompileLib__SpotInfoPrecompileFailed(); 237 | return abi.decode(result, (SpotInfo)); 238 | } 239 | 240 | function tokenInfo(uint64 token) internal view returns (TokenInfo memory) { 241 | (bool success, bytes memory result) = HLConstants.TOKEN_INFO_PRECOMPILE_ADDRESS.staticcall(abi.encode(token)); 242 | if (!success) revert PrecompileLib__TokenInfoPrecompileFailed(); 243 | return abi.decode(result, (TokenInfo)); 244 | } 245 | 246 | function tokenSupply(uint64 token) internal view returns (TokenSupply memory) { 247 | (bool success, bytes memory result) = HLConstants.TOKEN_SUPPLY_PRECOMPILE_ADDRESS.staticcall(abi.encode(token)); 248 | if (!success) revert PrecompileLib__TokenSupplyPrecompileFailed(); 249 | return abi.decode(result, (TokenSupply)); 250 | } 251 | 252 | function l1BlockNumber() internal view returns (uint64) { 253 | (bool success, bytes memory result) = HLConstants.L1_BLOCK_NUMBER_PRECOMPILE_ADDRESS.staticcall(abi.encode()); 254 | if (!success) revert PrecompileLib__L1BlockNumberPrecompileFailed(); 255 | return abi.decode(result, (uint64)); 256 | } 257 | 258 | function bbo(uint64 asset) internal view returns (Bbo memory) { 259 | (bool success, bytes memory result) = HLConstants.BBO_PRECOMPILE_ADDRESS.staticcall(abi.encode(asset)); 260 | if (!success) revert PrecompileLib__BboPrecompileFailed(); 261 | return abi.decode(result, (Bbo)); 262 | } 263 | 264 | function accountMarginSummary(uint32 perpDexIndex, address user) 265 | internal 266 | view 267 | returns (AccountMarginSummary memory) 268 | { 269 | (bool success, bytes memory result) = HLConstants.ACCOUNT_MARGIN_SUMMARY_PRECOMPILE_ADDRESS 270 | .staticcall(abi.encode(perpDexIndex, user)); 271 | if (!success) revert PrecompileLib__AccountMarginSummaryPrecompileFailed(); 272 | return abi.decode(result, (AccountMarginSummary)); 273 | } 274 | 275 | function coreUserExists(address user) internal view returns (bool) { 276 | (bool success, bytes memory result) = 277 | HLConstants.CORE_USER_EXISTS_PRECOMPILE_ADDRESS.staticcall(abi.encode(user)); 278 | if (!success) revert PrecompileLib__CoreUserExistsPrecompileFailed(); 279 | return abi.decode(result, (CoreUserExists)).exists; 280 | } 281 | 282 | /*////////////////////////////////////////////////////////////// 283 | Structs 284 | //////////////////////////////////////////////////////////////*/ 285 | struct Position { 286 | int64 szi; 287 | uint64 entryNtl; 288 | int64 isolatedRawUsd; 289 | uint32 leverage; 290 | bool isIsolated; 291 | } 292 | 293 | struct SpotBalance { 294 | uint64 total; 295 | uint64 hold; 296 | uint64 entryNtl; 297 | } 298 | 299 | struct UserVaultEquity { 300 | uint64 equity; 301 | uint64 lockedUntilTimestamp; 302 | } 303 | 304 | struct Withdrawable { 305 | uint64 withdrawable; 306 | } 307 | 308 | struct Delegation { 309 | address validator; 310 | uint64 amount; 311 | uint64 lockedUntilTimestamp; 312 | } 313 | 314 | struct DelegatorSummary { 315 | uint64 delegated; 316 | uint64 undelegated; 317 | uint64 totalPendingWithdrawal; 318 | uint64 nPendingWithdrawals; 319 | } 320 | 321 | struct PerpAssetInfo { 322 | string coin; 323 | uint32 marginTableId; 324 | uint8 szDecimals; 325 | uint8 maxLeverage; 326 | bool onlyIsolated; 327 | } 328 | 329 | struct SpotInfo { 330 | string name; 331 | uint64[2] tokens; 332 | } 333 | 334 | struct TokenInfo { 335 | string name; 336 | uint64[] spots; 337 | uint64 deployerTradingFeeShare; 338 | address deployer; 339 | address evmContract; 340 | uint8 szDecimals; 341 | uint8 weiDecimals; 342 | int8 evmExtraWeiDecimals; 343 | } 344 | 345 | struct UserBalance { 346 | address user; 347 | uint64 balance; 348 | } 349 | 350 | struct TokenSupply { 351 | uint64 maxSupply; 352 | uint64 totalSupply; 353 | uint64 circulatingSupply; 354 | uint64 futureEmissions; 355 | UserBalance[] nonCirculatingUserBalances; 356 | } 357 | 358 | struct Bbo { 359 | uint64 bid; 360 | uint64 ask; 361 | } 362 | 363 | struct AccountMarginSummary { 364 | int64 accountValue; 365 | uint64 marginUsed; 366 | uint64 ntlPos; 367 | int64 rawUsd; 368 | } 369 | 370 | struct CoreUserExists { 371 | bool exists; 372 | } 373 | 374 | error PrecompileLib__PositionPrecompileFailed(); 375 | error PrecompileLib__SpotBalancePrecompileFailed(); 376 | error PrecompileLib__VaultEquityPrecompileFailed(); 377 | error PrecompileLib__WithdrawablePrecompileFailed(); 378 | error PrecompileLib__DelegationsPrecompileFailed(); 379 | error PrecompileLib__DelegatorSummaryPrecompileFailed(); 380 | error PrecompileLib__MarkPxPrecompileFailed(); 381 | error PrecompileLib__OraclePxPrecompileFailed(); 382 | error PrecompileLib__SpotPxPrecompileFailed(); 383 | error PrecompileLib__PerpAssetInfoPrecompileFailed(); 384 | error PrecompileLib__SpotInfoPrecompileFailed(); 385 | error PrecompileLib__TokenInfoPrecompileFailed(); 386 | error PrecompileLib__TokenSupplyPrecompileFailed(); 387 | error PrecompileLib__L1BlockNumberPrecompileFailed(); 388 | error PrecompileLib__BboPrecompileFailed(); 389 | error PrecompileLib__AccountMarginSummaryPrecompileFailed(); 390 | error PrecompileLib__CoreUserExistsPrecompileFailed(); 391 | error PrecompileLib__SpotIndexNotFound(); 392 | } 393 | -------------------------------------------------------------------------------- /test/simulation/hyper-core/CoreState.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.28; 3 | 4 | import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; 5 | import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; 6 | import {DoubleEndedQueue} from "@openzeppelin/contracts/utils/structs/DoubleEndedQueue.sol"; 7 | import {Heap} from "@openzeppelin/contracts/utils/structs/Heap.sol"; 8 | 9 | import {PrecompileLib} from "../../../src/PrecompileLib.sol"; 10 | import {HLConstants} from "../../../src/CoreWriterLib.sol"; 11 | 12 | import {RealL1Read} from "../../utils/RealL1Read.sol"; 13 | import {StdCheats, Vm} from "forge-std/StdCheats.sol"; 14 | 15 | /// Modified from https://github.com/ambitlabsxyz/hypercore 16 | contract CoreState is StdCheats { 17 | using SafeCast for uint256; 18 | using EnumerableSet for EnumerableSet.AddressSet; 19 | using DoubleEndedQueue for DoubleEndedQueue.Bytes32Deque; 20 | using Heap for Heap.Uint256Heap; 21 | 22 | using RealL1Read for *; 23 | 24 | Vm internal constant vm = Vm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); 25 | 26 | uint64 public immutable HYPE_TOKEN_INDEX; 27 | uint64 public constant USDC_TOKEN_INDEX = 0; 28 | uint256 public constant FEE_DENOMINATOR = 1e6; 29 | 30 | // Default taker fees for simulator-controlled spot/perp trades (100% = 1e6) 31 | uint16 public spotMakerFee; 32 | uint16 public perpMakerFee; 33 | 34 | constructor() { 35 | HYPE_TOKEN_INDEX = HLConstants.hypeTokenIndex(); 36 | } 37 | 38 | struct WithdrawRequest { 39 | address account; 40 | uint64 amount; 41 | uint32 lockedUntilTimestamp; 42 | } 43 | 44 | struct AccountData { 45 | bool activated; 46 | mapping(uint64 token => uint64 balance) spot; 47 | mapping(address vault => PrecompileLib.UserVaultEquity) vaultEquity; 48 | uint64 staking; // undelegated staking balance 49 | EnumerableSet.AddressSet delegatedValidators; 50 | mapping(address validator => PrecompileLib.Delegation) delegations; 51 | uint64 perpBalance; 52 | mapping(uint16 perpIndex => PrecompileLib.Position) positions; 53 | mapping(uint16 perpIndex => uint64 margin) margin; 54 | mapping(uint16 perpIndex => PrecompileLib.AccountMarginSummary) marginSummary; 55 | } 56 | 57 | struct PendingOrder { 58 | address sender; 59 | LimitOrderAction action; 60 | } 61 | 62 | // Whether to use real L1 read or not 63 | bool public useRealL1Read; 64 | 65 | // registered token info 66 | mapping(uint64 token => PrecompileLib.TokenInfo) internal _tokens; 67 | mapping(uint32 perp => PrecompileLib.PerpAssetInfo) internal _perpAssetInfo; 68 | mapping(uint32 spot => PrecompileLib.SpotInfo) internal _spotInfo; 69 | 70 | mapping(address account => AccountData) internal _accounts; 71 | 72 | mapping(address account => bool initialized) internal _initializedAccounts; 73 | mapping(address account => mapping(uint64 token => bool initialized)) internal _initializedSpotBalance; 74 | mapping(address account => mapping(address vault => bool initialized)) internal _initializedVaults; 75 | 76 | mapping(address account => mapping(uint32 perpIndex => bool initialized)) internal _initializedPerpPosition; 77 | 78 | mapping(address account => mapping(uint64 token => uint64 latentBalance)) internal _latentSpotBalance; 79 | 80 | mapping(uint32 perpIndex => uint64 markPrice) internal _perpMarkPrice; 81 | mapping(uint32 perpIndex => uint64 oraclePrice) internal _perpOraclePrice; 82 | mapping(uint32 spotMarketId => uint64 spotPrice) internal _spotPrice; 83 | 84 | mapping(address vault => uint64) internal _vaultEquity; 85 | 86 | DoubleEndedQueue.Bytes32Deque internal _withdrawQueue; 87 | 88 | PendingOrder[] internal _pendingOrders; 89 | 90 | EnumerableSet.AddressSet internal _validators; 91 | 92 | mapping(address user => mapping(address vault => uint256 userVaultMultiplier)) internal _userVaultMultiplier; 93 | mapping(address vault => uint256 multiplier) internal _vaultMultiplier; 94 | 95 | mapping(address user => mapping(address validator => uint256 userStakingYieldIndex)) internal 96 | _userStakingYieldIndex; 97 | uint256 internal _stakingYieldIndex; // assumes same yield for all validators TODO: account for differences due to commissions 98 | 99 | EnumerableSet.Bytes32Set internal _openPerpPositions; 100 | 101 | // Maps user address to a set of perp indices they have active positions in 102 | mapping(address => EnumerableSet.UintSet) internal _userPerpPositions; 103 | 104 | mapping(uint64 token => bool isQuoteToken) internal _isQuoteToken; 105 | 106 | ///////////////////////// 107 | /// STATE INITIALIZERS/// 108 | ///////////////////////// 109 | 110 | modifier initAccountWithToken(address _account, uint64 token) { 111 | if (!_initializedSpotBalance[_account][token]) { 112 | registerTokenInfo(token); 113 | _initializeAccountWithToken(_account, token); 114 | } 115 | _; 116 | } 117 | 118 | modifier initAccountWithSpotMarket(address _account, uint32 spotMarketId) { 119 | uint64 baseToken = PrecompileLib.spotInfo(spotMarketId).tokens[0]; 120 | uint64 quoteToken = PrecompileLib.spotInfo(spotMarketId).tokens[1]; 121 | 122 | if (!_initializedSpotBalance[_account][baseToken]) { 123 | registerTokenInfo(baseToken); 124 | _initializeAccountWithToken(_account, baseToken); 125 | } 126 | 127 | if (!_initializedSpotBalance[_account][quoteToken]) { 128 | registerTokenInfo(quoteToken); 129 | _initializeAccountWithToken(_account, quoteToken); 130 | } 131 | 132 | _; 133 | } 134 | 135 | modifier initAccountWithVault(address _account, address _vault) { 136 | if (!_initializedVaults[_account][_vault]) { 137 | _initializeAccount(_account); 138 | _initializeAccountWithVault(_account, _vault); 139 | } 140 | _; 141 | } 142 | 143 | modifier initAccountWithPerp(address _account, uint16 perp) { 144 | if (_perpAssetInfo[perp].maxLeverage == 0) { 145 | registerPerpAssetInfo(perp, RealL1Read.perpAssetInfo(perp)); 146 | } 147 | 148 | if (_initializedPerpPosition[_account][perp] == false) { 149 | _initializeAccount(_account); 150 | _initializeAccountWithPerp(_account, perp); 151 | } 152 | _; 153 | } 154 | 155 | modifier initAccount(address _account) { 156 | if (!_initializedAccounts[_account]) { 157 | _initializeAccount(_account); 158 | } 159 | _; 160 | } 161 | 162 | function setUseRealL1Read(bool _useRealL1Read) public { 163 | useRealL1Read = _useRealL1Read; 164 | } 165 | 166 | function setSpotMakerFee(uint16 bps) public { 167 | require(bps <= FEE_DENOMINATOR, "fee too high"); 168 | spotMakerFee = bps; 169 | } 170 | 171 | function setPerpMakerFee(uint16 bps) public { 172 | require(bps <= FEE_DENOMINATOR, "fee too high"); 173 | perpMakerFee = bps; 174 | } 175 | 176 | function _initializeAccountWithToken(address _account, uint64 token) internal { 177 | _initializeAccount(_account); 178 | 179 | if (_accounts[_account].activated == false) { 180 | return; 181 | } 182 | 183 | _initializedSpotBalance[_account][token] = true; 184 | _accounts[_account].spot[token] = RealL1Read.spotBalance(_account, token).total; 185 | } 186 | 187 | function _initializeAccountWithVault(address _account, address _vault) internal { 188 | _initializedVaults[_account][_vault] = true; 189 | _accounts[_account].vaultEquity[_vault] = RealL1Read.userVaultEquity(_account, _vault); 190 | } 191 | 192 | function _initializeAccountWithPerp(address _account, uint16 perp) internal { 193 | _initializedPerpPosition[_account][perp] = true; 194 | _accounts[_account].positions[perp] = RealL1Read.position(_account, perp); 195 | } 196 | 197 | function _initializeAccount(address _account) internal { 198 | _initializeAccount(_account, false); 199 | } 200 | 201 | function _initializeAccount(address _account, bool force) internal { 202 | bool initialized = _initializedAccounts[_account]; 203 | 204 | if (initialized) { 205 | return; 206 | } 207 | 208 | AccountData storage account = _accounts[_account]; 209 | 210 | // check if the acc is created on Core 211 | bool coreUserExists = RealL1Read.coreUserExists(_account); 212 | if (!coreUserExists && !force) { 213 | return; 214 | } 215 | 216 | _initializedAccounts[_account] = true; 217 | account.activated = true; 218 | 219 | // setting perp balance 220 | account.perpBalance = RealL1Read.withdrawable(_account); 221 | 222 | // setting staking balance 223 | PrecompileLib.DelegatorSummary memory summary; 224 | summary = RealL1Read.delegatorSummary(_account); 225 | account.staking = summary.undelegated; 226 | 227 | // assume each pending withdrawal is of equal size 228 | uint64 pendingWithdrawals = summary.nPendingWithdrawals; 229 | 230 | // when handling existing pending withdrawals, we don't have access to granular details on each one 231 | // so we assume equal size and expiry after 7 days 232 | if (pendingWithdrawals > 0) { 233 | // assume that they all expire after 7 days 234 | uint32 pendingWithdrawalTime = uint32(block.timestamp + 7 days); 235 | 236 | for (uint256 i = 0; i < pendingWithdrawals; i++) { 237 | uint256 pendingWithdrawalAmount; 238 | 239 | bool last = i == pendingWithdrawals - 1; 240 | 241 | if (!last) { 242 | pendingWithdrawalAmount = summary.totalPendingWithdrawal / pendingWithdrawals; 243 | } else { 244 | // ensure that sum(withdrawalAmount) = totalPendingWithdrawal (accounting for precision loss during division) 245 | pendingWithdrawalAmount = 246 | summary.totalPendingWithdrawal - (summary.totalPendingWithdrawal / pendingWithdrawals) * i; 247 | } 248 | 249 | // add to withdrawal queue 250 | _withdrawQueue.pushBack( 251 | serializeWithdrawRequest( 252 | WithdrawRequest({ 253 | account: _account, 254 | amount: uint64(pendingWithdrawalAmount), 255 | lockedUntilTimestamp: pendingWithdrawalTime 256 | }) 257 | ) 258 | ); 259 | } 260 | } 261 | 262 | // set delegations 263 | PrecompileLib.Delegation[] memory delegations; 264 | delegations = RealL1Read.delegations(_account); 265 | for (uint256 i = 0; i < delegations.length; i++) { 266 | account.delegations[delegations[i].validator] = delegations[i]; 267 | account.delegatedValidators.add(delegations[i].validator); 268 | } 269 | 270 | _accounts[_account].marginSummary[0] = RealL1Read.accountMarginSummary(0, _account); 271 | } 272 | 273 | modifier whenActivated(address sender) { 274 | if (_accounts[sender].activated == false) { 275 | return; 276 | } 277 | _; 278 | } 279 | 280 | function registerTokenInfo(uint64 index) public { 281 | // if the token is already registered, return early 282 | if (bytes(_tokens[index].name).length > 0) { 283 | return; 284 | } 285 | 286 | PrecompileLib.TokenInfo memory tokenInfo; 287 | tokenInfo = RealL1Read.tokenInfo(uint32(index)); 288 | 289 | // this means that the precompile call failed 290 | if (tokenInfo.evmContract == RealL1Read.INVALID_ADDRESS) return; 291 | _tokens[index] = tokenInfo; 292 | 293 | // quote token status for USDC, USDT0, USDH respectively 294 | if (index == 0 || index == 268 || index == 360) { 295 | _isQuoteToken[index] = true; 296 | } 297 | } 298 | 299 | function registerTokenInfo(uint64 index, PrecompileLib.TokenInfo memory tokenInfo) public { 300 | _tokens[index] = tokenInfo; 301 | } 302 | 303 | function registerSpotInfo(uint32 spotIndex, PrecompileLib.SpotInfo memory spotInfo) public { 304 | _spotInfo[spotIndex] = spotInfo; 305 | } 306 | 307 | function registerPerpAssetInfo(uint16 perpIndex, PrecompileLib.PerpAssetInfo memory perpAssetInfo) public { 308 | _perpAssetInfo[perpIndex] = perpAssetInfo; 309 | } 310 | 311 | // @dev if this set has len > 0, only validators within the set can be delegated to 312 | function registerValidator(address validator) public { 313 | _validators.add(validator); 314 | } 315 | 316 | /// @dev account creation can be forced when there isnt a reliance on testing that workflow. 317 | function forceAccountActivation(address account) public { 318 | // force initialize the account 319 | _initializeAccount(account, true); 320 | _accounts[account].activated = true; 321 | } 322 | 323 | function forceSpotBalance(address account, uint64 token, uint64 _wei) public payable { 324 | if (_accounts[account].activated == false) { 325 | forceAccountActivation(account); 326 | } 327 | 328 | if (_initializedSpotBalance[account][token] == false) { 329 | registerTokenInfo(token); 330 | _initializeAccountWithToken(account, token); 331 | } 332 | 333 | _accounts[account].spot[token] = _wei; 334 | } 335 | 336 | function forcePerpBalance(address account, uint64 usd) public payable { 337 | if (_accounts[account].activated == false) { 338 | forceAccountActivation(account); 339 | } 340 | if (_initializedAccounts[account] == false) { 341 | _initializeAccount(account); 342 | } 343 | 344 | _accounts[account].perpBalance = usd; 345 | } 346 | 347 | function forcePerpPositionLeverage(address account, uint16 perp, uint32 leverage) public payable { 348 | if (_accounts[account].activated == false) { 349 | forceAccountActivation(account); 350 | } 351 | if (_initializedPerpPosition[account][perp] == false) { 352 | _initializeAccountWithPerp(account, perp); 353 | } 354 | 355 | _accounts[account].positions[perp].leverage = leverage; 356 | } 357 | 358 | function forceStakingBalance(address account, uint64 _wei) public payable { 359 | forceAccountActivation(account); 360 | _accounts[account].staking = _wei; 361 | } 362 | 363 | function forceDelegation(address account, address validator, uint64 amount, uint64 lockedUntilTimestamp) public { 364 | forceAccountActivation(account); 365 | _accounts[account].delegations[validator] = PrecompileLib.Delegation({ 366 | validator: validator, amount: amount, lockedUntilTimestamp: lockedUntilTimestamp 367 | }); 368 | } 369 | 370 | function forceVaultEquity(address account, address vault, uint64 usd, uint64 lockedUntilTimestamp) public payable { 371 | forceAccountActivation(account); 372 | 373 | _vaultEquity[vault] -= _accounts[account].vaultEquity[vault].equity; 374 | _vaultEquity[vault] += usd; 375 | 376 | _accounts[account].vaultEquity[vault].equity = usd; 377 | _accounts[account].vaultEquity[vault].lockedUntilTimestamp = 378 | lockedUntilTimestamp > 0 ? lockedUntilTimestamp : uint64((block.timestamp + 3600) * 1000); 379 | } 380 | 381 | //////// conversions //////// 382 | 383 | function toWei(uint256 amount, int8 evmExtraWeiDecimals) internal pure returns (uint64) { 384 | uint256 _wei = evmExtraWeiDecimals == 0 385 | ? amount 386 | : evmExtraWeiDecimals > 0 387 | ? amount / 10 ** uint8(evmExtraWeiDecimals) 388 | : amount * 10 ** uint8(-evmExtraWeiDecimals); 389 | 390 | return _wei.toUint64(); 391 | } 392 | 393 | function fromWei(uint64 _wei, int8 evmExtraWeiDecimals) internal pure returns (uint256) { 394 | return evmExtraWeiDecimals == 0 395 | ? _wei 396 | : evmExtraWeiDecimals > 0 397 | ? _wei * 10 ** uint8(evmExtraWeiDecimals) 398 | : _wei / 10 ** uint8(-evmExtraWeiDecimals); 399 | } 400 | 401 | function fromPerp(uint64 usd) internal pure returns (uint64) { 402 | return usd * 1e2; 403 | } 404 | 405 | // converting a withdraw request into a bytes32 406 | function serializeWithdrawRequest(CoreState.WithdrawRequest memory request) internal pure returns (bytes32) { 407 | return bytes32( 408 | (uint256(uint160(request.account)) << 96) | (uint256(request.amount) << 32) 409 | | uint40(request.lockedUntilTimestamp) 410 | ); 411 | } 412 | 413 | function deserializeWithdrawRequest(bytes32 data) internal pure returns (CoreState.WithdrawRequest memory request) { 414 | request.account = address(uint160(uint256(data) >> 96)); 415 | request.amount = uint64(uint256(data) >> 32); 416 | request.lockedUntilTimestamp = uint32(uint256(data)); 417 | } 418 | 419 | struct LimitOrderAction { 420 | uint32 asset; 421 | bool isBuy; 422 | uint64 limitPx; 423 | uint64 sz; 424 | bool reduceOnly; 425 | uint8 encodedTif; 426 | uint128 cloid; 427 | } 428 | 429 | struct VaultTransferAction { 430 | address vault; 431 | bool isDeposit; 432 | uint64 usd; 433 | } 434 | 435 | struct TokenDelegateAction { 436 | address validator; 437 | uint64 _wei; 438 | bool isUndelegate; 439 | } 440 | 441 | struct StakingDepositAction { 442 | uint64 _wei; 443 | } 444 | 445 | struct StakingWithdrawAction { 446 | uint64 _wei; 447 | } 448 | 449 | struct SpotSendAction { 450 | address destination; 451 | uint64 token; 452 | uint64 _wei; 453 | } 454 | 455 | struct UsdClassTransferAction { 456 | uint64 ntl; 457 | bool toPerp; 458 | } 459 | } 460 | -------------------------------------------------------------------------------- /test/simulation/hyper-core/CoreExecution.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.28; 3 | 4 | import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; 5 | import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; 6 | import {DoubleEndedQueue} from "@openzeppelin/contracts/utils/structs/DoubleEndedQueue.sol"; 7 | import {Heap} from "@openzeppelin/contracts/utils/structs/Heap.sol"; 8 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 9 | import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 10 | import {PrecompileLib} from "../../../src/PrecompileLib.sol"; 11 | import {CoreWriterLib, HLConstants} from "../../../src/CoreWriterLib.sol"; 12 | import {HLConversions} from "../../../src/common/HLConversions.sol"; 13 | import {RealL1Read} from "../../utils/RealL1Read.sol"; 14 | import {CoreView} from "./CoreView.sol"; 15 | 16 | contract CoreExecution is CoreView { 17 | using SafeCast for uint256; 18 | using EnumerableSet for EnumerableSet.AddressSet; 19 | using EnumerableSet for EnumerableSet.Bytes32Set; 20 | using DoubleEndedQueue for DoubleEndedQueue.Bytes32Deque; 21 | using Heap for Heap.Uint256Heap; 22 | using SafeERC20 for IERC20; 23 | using RealL1Read for *; 24 | 25 | using EnumerableSet for EnumerableSet.UintSet; 26 | 27 | function _getKey(address user, uint16 perpIndex) internal pure returns (bytes32) { 28 | return bytes32((uint256(uint160(user)) << 16) | uint256(perpIndex)); 29 | } 30 | 31 | function executeTokenTransfer(address, uint64 token, address from, uint256 value) 32 | public 33 | payable 34 | initAccountWithToken(from, token) 35 | { 36 | if (_accounts[from].activated) { 37 | _accounts[from].spot[token] += toWei(value, _tokens[token].evmExtraWeiDecimals); 38 | } 39 | else { 40 | if (_isQuoteToken[token]) { 41 | uint64 amount = toWei(value, _tokens[token].evmExtraWeiDecimals); 42 | uint64 activationFee = 1e8; 43 | if (amount < activationFee) { 44 | revert("insufficient amount bridged for activation fee"); 45 | } 46 | else { 47 | _accounts[from].spot[token] += (amount - activationFee); 48 | _accounts[from].activated = true; 49 | _initializedSpotBalance[from][token] = true; 50 | } 51 | } 52 | else { 53 | _latentSpotBalance[from][token] += toWei(value, _tokens[token].evmExtraWeiDecimals); 54 | } 55 | } 56 | } 57 | 58 | function executeNativeTransfer(address, address from, uint256 value) 59 | public 60 | payable 61 | initAccountWithToken(from, HYPE_TOKEN_INDEX) 62 | { 63 | if (_accounts[from].activated) { 64 | _accounts[from].spot[HYPE_TOKEN_INDEX] += (value / 1e10).toUint64(); 65 | } else { 66 | _latentSpotBalance[from][HYPE_TOKEN_INDEX] += (value / 1e10).toUint64(); 67 | } 68 | } 69 | 70 | function executePerpLimitOrder(address sender, LimitOrderAction memory action) 71 | public 72 | initAccountWithPerp(sender, uint16(action.asset)) 73 | { 74 | uint16 perpIndex = uint16(action.asset); 75 | PrecompileLib.Position memory position = _accounts[sender].positions[perpIndex]; 76 | 77 | bool isolated = position.isIsolated; 78 | 79 | uint256 markPx = readMarkPx(perpIndex); 80 | uint256 normalizedMarkPx = PrecompileLib.normalizedMarkPx(perpIndex) * 100; 81 | 82 | PrecompileLib.PerpAssetInfo memory perpInfo = PrecompileLib.perpAssetInfo(perpIndex); 83 | action.sz = scale(action.sz, 8, perpInfo.szDecimals); 84 | 85 | if (!isolated) { 86 | if (action.isBuy) { 87 | if (normalizedMarkPx <= action.limitPx) { 88 | _updateMarginSummary(sender); 89 | _executePerpLong(sender, action, markPx); 90 | _updateMarginSummary(sender); 91 | } 92 | } else { 93 | if (normalizedMarkPx >= action.limitPx) { 94 | _updateMarginSummary(sender); 95 | _executePerpShort(sender, action, markPx); 96 | _updateMarginSummary(sender); 97 | } 98 | } 99 | } 100 | } 101 | 102 | function _executePerpLong(address sender, LimitOrderAction memory action, uint256 markPx) internal { 103 | uint16 perpIndex = uint16(action.asset); 104 | int64 szi = _accounts[sender].positions[perpIndex].szi; 105 | uint32 leverage = _accounts[sender].positions[perpIndex].leverage; 106 | 107 | uint64 _markPx = markPx.toUint64(); 108 | 109 | // Add require checks for safety (e.g., leverage > 0, action.sz > 0, etc.) 110 | require(leverage > 0, "Invalid leverage"); 111 | require(action.sz > 0, "Invalid size"); 112 | require(markPx > 0, "Invalid price"); 113 | 114 | if (perpMakerFee > 0) { 115 | uint256 notional = uint256(action.sz) * uint256(_markPx); 116 | uint64 fee = SafeCast.toUint64((notional * uint256(perpMakerFee)) / FEE_DENOMINATOR); 117 | require(_accounts[sender].perpBalance >= fee, "insufficient perp balance for fee"); 118 | _accounts[sender].perpBalance -= fee; 119 | } 120 | 121 | int64 newSzi = szi + int64(action.sz); 122 | 123 | if (szi >= 0) { 124 | // No PnL realization for same-direction increase 125 | // Update position size (more positive for long) 126 | _accounts[sender].positions[perpIndex].szi += int64(action.sz); 127 | 128 | // Additive update to entryNtl to preserve weighted average 129 | // New entryNtl = old_entryNtl + (action.sz * markPx) 130 | _accounts[sender].positions[perpIndex].entryNtl += uint64(action.sz) * uint64(markPx); 131 | } else { 132 | if (newSzi <= 0) { 133 | uint64 avgEntryPrice = _accounts[sender].positions[perpIndex].entryNtl / uint64(-szi); 134 | int64 pnl = int64(action.sz) * (int64(avgEntryPrice) - int64(_markPx)); 135 | 136 | uint64 closedMargin = 137 | (uint64(action.sz) * _accounts[sender].positions[perpIndex].entryNtl / uint64(-szi)) / leverage; 138 | 139 | _accounts[sender].perpBalance = pnl > 0 140 | ? _accounts[sender].perpBalance + uint64(pnl) 141 | : _accounts[sender].perpBalance - uint64(-pnl); 142 | 143 | _accounts[sender].positions[perpIndex].szi = newSzi; 144 | _accounts[sender].positions[perpIndex].entryNtl = uint64(-newSzi) * avgEntryPrice; 145 | } else { 146 | uint64 avgEntryPrice = _accounts[sender].positions[perpIndex].entryNtl / uint64(-szi); 147 | int64 pnl = int64(-szi) * (int64(avgEntryPrice) - int64(_markPx)); 148 | _accounts[sender].perpBalance = pnl > 0 149 | ? _accounts[sender].perpBalance + uint64(pnl) 150 | : _accounts[sender].perpBalance - uint64(-pnl); 151 | 152 | uint64 newLongSize = uint64(newSzi); 153 | uint64 newMargin = newLongSize * _markPx / leverage; 154 | 155 | _accounts[sender].positions[perpIndex].szi = newSzi; 156 | _accounts[sender].positions[perpIndex].entryNtl = newLongSize * _markPx; 157 | } 158 | } 159 | 160 | bytes32 key = _getKey(sender, perpIndex); 161 | if (szi == 0 && newSzi != 0) { 162 | _openPerpPositions.add(key); 163 | _userPerpPositions[sender].add(perpIndex); 164 | } else if (szi != 0 && newSzi == 0) { 165 | _openPerpPositions.remove(key); 166 | _userPerpPositions[sender].remove(perpIndex); 167 | } 168 | } 169 | 170 | function _executePerpShort(address sender, LimitOrderAction memory action, uint256 markPx) internal { 171 | uint16 perpIndex = uint16(action.asset); 172 | int64 szi = _accounts[sender].positions[perpIndex].szi; 173 | uint32 leverage = _accounts[sender].positions[perpIndex].leverage; 174 | 175 | uint64 _markPx = markPx.toUint64(); 176 | 177 | // Add require checks for safety (e.g., leverage > 0, action.sz > 0, etc.) 178 | require(leverage > 0, "Invalid leverage"); 179 | require(action.sz > 0, "Invalid size"); 180 | require(markPx > 0, "Invalid price"); 181 | 182 | if (perpMakerFee > 0) { 183 | uint256 notional = uint256(action.sz) * uint256(_markPx); 184 | uint64 fee = SafeCast.toUint64((notional * uint256(perpMakerFee)) / FEE_DENOMINATOR); 185 | require(_accounts[sender].perpBalance >= fee, "insufficient perp balance for fee"); 186 | _accounts[sender].perpBalance -= fee; 187 | } 188 | 189 | int64 newSzi = szi - int64(action.sz); 190 | 191 | if (szi <= 0) { 192 | // No PnL realization for same-direction increase 193 | // Update position size (more negative for short) 194 | _accounts[sender].positions[perpIndex].szi -= int64(action.sz); 195 | 196 | // Additive update to entryNtl to preserve weighted average 197 | // New entryNtl = old_entryNtl + (action.sz * markPx) 198 | _accounts[sender].positions[perpIndex].entryNtl += uint64(action.sz) * uint64(markPx); 199 | } else { 200 | if (newSzi >= 0) { 201 | uint64 avgEntryPrice = _accounts[sender].positions[perpIndex].entryNtl / uint64(szi); 202 | int64 pnl = int64(action.sz) * (int64(_markPx) - int64(avgEntryPrice)); 203 | uint64 closedMargin = 204 | (uint64(action.sz) * _accounts[sender].positions[perpIndex].entryNtl / uint64(szi)) / leverage; 205 | 206 | _accounts[sender].perpBalance = pnl > 0 207 | ? _accounts[sender].perpBalance + uint64(pnl) 208 | : _accounts[sender].perpBalance - uint64(-pnl); 209 | 210 | _accounts[sender].positions[perpIndex].szi = newSzi; 211 | _accounts[sender].positions[perpIndex].entryNtl = uint64(newSzi) * avgEntryPrice; 212 | } else { 213 | uint64 avgEntryPrice = _accounts[sender].positions[perpIndex].entryNtl / uint64(szi); 214 | int64 pnl = int64(szi) * (int64(_markPx) - int64(avgEntryPrice)); 215 | _accounts[sender].perpBalance = pnl > 0 216 | ? _accounts[sender].perpBalance + uint64(pnl) 217 | : _accounts[sender].perpBalance - uint64(-pnl); 218 | 219 | uint64 newShortSize = uint64(-newSzi); 220 | uint64 newMargin = newShortSize * _markPx / leverage; 221 | 222 | _accounts[sender].positions[perpIndex].szi = newSzi; 223 | _accounts[sender].positions[perpIndex].entryNtl = newShortSize * _markPx; 224 | } 225 | } 226 | 227 | bytes32 key = _getKey(sender, perpIndex); 228 | if (szi == 0 && newSzi != 0) { 229 | _openPerpPositions.add(key); 230 | _userPerpPositions[sender].add(perpIndex); 231 | } else if (szi != 0 && newSzi == 0) { 232 | _openPerpPositions.remove(key); 233 | _userPerpPositions[sender].remove(perpIndex); 234 | } 235 | } 236 | 237 | function _updateMarginSummary(address sender) internal { 238 | uint64 totalNtlPos = 0; 239 | uint64 totalMarginUsed = 0; 240 | 241 | uint64 entryNtlByLeverage = 0; 242 | 243 | uint64 totalLongNtlPos = 0; 244 | uint64 totalShortNtlPos = 0; 245 | 246 | for (uint256 i = 0; i < _userPerpPositions[sender].length(); i++) { 247 | uint16 perpIndex = uint16(_userPerpPositions[sender].at(i)); 248 | 249 | PrecompileLib.Position memory position = _accounts[sender].positions[perpIndex]; 250 | 251 | uint32 leverage = position.leverage; 252 | uint64 markPx = readMarkPx(perpIndex); 253 | 254 | entryNtlByLeverage += position.entryNtl / leverage; 255 | 256 | int64 szi = position.szi; 257 | 258 | if (szi > 0) { 259 | uint64 ntlPos = uint64(szi) * markPx; 260 | totalNtlPos += ntlPos; 261 | totalMarginUsed += ntlPos / leverage; 262 | 263 | totalLongNtlPos += ntlPos; 264 | } else if (szi < 0) { 265 | uint64 ntlPos = uint64(-szi) * markPx; 266 | totalNtlPos += ntlPos; 267 | totalMarginUsed += ntlPos / leverage; 268 | 269 | totalShortNtlPos += ntlPos; 270 | } 271 | } 272 | 273 | int64 totalAccountValue = int64(_accounts[sender].perpBalance - entryNtlByLeverage + totalMarginUsed); 274 | int64 totalRawUsd = totalAccountValue - int64(totalLongNtlPos) + int64(totalShortNtlPos); 275 | 276 | _accounts[sender].marginSummary[0] = PrecompileLib.AccountMarginSummary({ 277 | accountValue: totalAccountValue, marginUsed: totalMarginUsed, ntlPos: totalNtlPos, rawUsd: totalRawUsd 278 | }); 279 | } 280 | 281 | // basic simulation of spot trading, not accounting for orderbook depth, or fees 282 | function executeSpotLimitOrder(address sender, LimitOrderAction memory action) 283 | public 284 | initAccountWithSpotMarket(sender, uint32(HLConversions.assetToSpotId(action.asset))) 285 | { 286 | uint32 spotMarketId = uint32(HLConversions.assetToSpotId(action.asset)); 287 | 288 | PrecompileLib.SpotInfo memory spotInfo = RealL1Read.spotInfo(spotMarketId); 289 | uint64 baseToken = spotInfo.tokens[0]; 290 | uint64 quoteToken = spotInfo.tokens[1]; 291 | 292 | uint8 baseSzDecimals = _tokens[baseToken].szDecimals; 293 | uint8 baseWeiDecimals = _tokens[baseToken].weiDecimals; 294 | 295 | uint64 spotPx = readSpotPx(spotMarketId) * SafeCast.toUint64(10 ** baseSzDecimals); 296 | 297 | if (spotPx == 0 && !useRealL1Read) { 298 | // in offline mode, if price is not set, we revert 299 | revert("Offline mode: spot price has not been set. Use CoreSimulatorLib.setSpotPx()"); 300 | } 301 | 302 | if (isActionExecutable(action, spotPx)) { 303 | uint64 orderSz = action.sz; 304 | if (action.isBuy) { 305 | _executeSpotBuy(sender, baseToken, quoteToken, baseWeiDecimals, spotPx, orderSz); 306 | } else { 307 | _executeSpotSell(sender, baseToken, quoteToken, baseWeiDecimals, spotPx, orderSz); 308 | } 309 | } else { 310 | _pendingOrders.push(PendingOrder({sender: sender, action: action})); 311 | } 312 | } 313 | 314 | function _executeSpotBuy( 315 | address sender, 316 | uint64 baseToken, 317 | uint64 quoteToken, 318 | uint8 baseWeiDecimals, 319 | uint64 spotPx, 320 | uint64 orderSz 321 | ) internal { 322 | uint64 amountIn = SafeCast.toUint64(uint256(orderSz) * uint256(spotPx) / 1e8); 323 | uint64 amountOut = scale(orderSz, 8, baseWeiDecimals); 324 | 325 | uint64 totalDebit = amountIn; 326 | if (spotMakerFee > 0) { 327 | totalDebit = 328 | SafeCast.toUint64(uint256(amountIn) + ((uint256(amountIn) * uint256(spotMakerFee)) / FEE_DENOMINATOR)); 329 | } 330 | 331 | if (_accounts[sender].spot[quoteToken] < totalDebit) { 332 | revert("insufficient balance"); 333 | } 334 | 335 | _accounts[sender].spot[quoteToken] -= totalDebit; 336 | _accounts[sender].spot[baseToken] += amountOut; 337 | } 338 | 339 | function _executeSpotSell( 340 | address sender, 341 | uint64 baseToken, 342 | uint64 quoteToken, 343 | uint8 baseWeiDecimals, 344 | uint64 spotPx, 345 | uint64 orderSz 346 | ) internal { 347 | uint64 amountIn = scale(orderSz, 8, baseWeiDecimals); 348 | uint64 amountOut = SafeCast.toUint64(uint256(orderSz) * uint256(spotPx) / 1e8); 349 | 350 | if (_accounts[sender].spot[baseToken] < amountIn) { 351 | revert("insufficient balance"); 352 | } 353 | 354 | uint64 netProceeds = amountOut; 355 | if (spotMakerFee > 0) { 356 | uint64 fee = SafeCast.toUint64((uint256(amountOut) * uint256(spotMakerFee)) / FEE_DENOMINATOR); 357 | 358 | require(netProceeds > fee, "fee exceeds proceeds"); 359 | netProceeds -= fee; 360 | } 361 | 362 | _accounts[sender].spot[baseToken] -= amountIn; 363 | _accounts[sender].spot[quoteToken] += netProceeds; 364 | } 365 | 366 | function scale(uint64 amount, uint8 fromDecimals, uint8 toDecimals) internal pure returns (uint64) { 367 | if (fromDecimals == toDecimals) { 368 | return amount; 369 | } else if (fromDecimals < toDecimals) { 370 | uint8 diff = toDecimals - fromDecimals; 371 | return amount * uint64(10) ** diff; 372 | } else { 373 | uint8 diff = fromDecimals - toDecimals; 374 | return amount / (uint64(10) ** diff); 375 | } 376 | } 377 | 378 | function executeSpotSend(address sender, SpotSendAction memory action) 379 | public 380 | initAccountWithToken(sender, action.token) 381 | whenActivated(sender) 382 | initAccountWithToken(action.destination, action.token) 383 | { 384 | if (action._wei > _accounts[sender].spot[action.token]) { 385 | revert("insufficient balance"); 386 | } 387 | 388 | // handle account activation case, skip activation for system addresses 389 | if ( 390 | _accounts[action.destination].activated == false 391 | && getTokenIndexFromSystemAddress(action.destination) > 10000 392 | ) { 393 | _chargeUSDCFee(sender); 394 | 395 | _accounts[action.destination].activated = true; 396 | 397 | _accounts[sender].spot[action.token] -= action._wei; 398 | _accounts[action.destination].spot[action.token] += _latentSpotBalance[sender][action.token] + action._wei; 399 | 400 | // this will no longer be needed 401 | _latentSpotBalance[sender][action.token] = 0; 402 | 403 | // officially init the destination account 404 | _initializedAccounts[action.destination] = true; 405 | _initializedSpotBalance[action.destination][action.token] = true; 406 | return; 407 | } 408 | 409 | address systemAddress = CoreWriterLib.getSystemAddress(action.token); 410 | 411 | _accounts[sender].spot[action.token] -= action._wei; 412 | 413 | if (action.destination != systemAddress) { 414 | _accounts[action.destination].spot[action.token] += action._wei; 415 | } else { 416 | uint256 transferAmount; 417 | if (action.token == HLConstants.hypeTokenIndex()) { 418 | transferAmount = uint256(action._wei) * 1e10; 419 | deal(systemAddress, systemAddress.balance + transferAmount); 420 | vm.startPrank(systemAddress); 421 | (bool success,) = address(sender).call{value: transferAmount, gas: 30000}(""); 422 | if (!success) { 423 | revert("transfer failed"); 424 | } 425 | return; 426 | } 427 | address evmContract = _tokens[action.token].evmContract; 428 | transferAmount = fromWei(action._wei, _tokens[action.token].evmExtraWeiDecimals); 429 | 430 | if (evmContract != address(HLConstants.coreDepositWallet())) { 431 | deal(evmContract, systemAddress, IERC20(evmContract).balanceOf(systemAddress) + transferAmount); 432 | } 433 | 434 | vm.startPrank(systemAddress); 435 | IERC20(evmContract).safeTransfer(sender, transferAmount); 436 | } 437 | } 438 | 439 | function _chargeUSDCFee(address sender) internal { 440 | if (_accounts[sender].spot[USDC_TOKEN_INDEX] >= 1e8) { 441 | _accounts[sender].spot[USDC_TOKEN_INDEX] -= 1e8; 442 | } else if (_accounts[sender].perpBalance >= 1e8) { 443 | _accounts[sender].perpBalance -= 1e8; 444 | } else { 445 | revert("insufficient USDC balance for fee"); 446 | } 447 | } 448 | 449 | function executeUsdClassTransfer(address sender, UsdClassTransferAction memory action) 450 | public 451 | initAccountWithToken(sender, USDC_TOKEN_INDEX) 452 | whenActivated(sender) 453 | { 454 | if (action.toPerp) { 455 | if (fromPerp(action.ntl) <= _accounts[sender].spot[USDC_TOKEN_INDEX]) { 456 | _accounts[sender].perpBalance += action.ntl; 457 | _accounts[sender].spot[USDC_TOKEN_INDEX] -= fromPerp(action.ntl); 458 | } 459 | } else { 460 | if (action.ntl <= _accounts[sender].perpBalance) { 461 | _accounts[sender].perpBalance -= action.ntl; 462 | _accounts[sender].spot[USDC_TOKEN_INDEX] += fromPerp(action.ntl); 463 | } 464 | } 465 | } 466 | 467 | function executeVaultTransfer(address sender, VaultTransferAction memory action) 468 | public 469 | initAccountWithVault(sender, action.vault) 470 | whenActivated(sender) 471 | { 472 | // first update their vault equity 473 | _accounts[sender].vaultEquity[action.vault].equity = readUserVaultEquity(sender, action.vault).equity; 474 | _userVaultMultiplier[sender][action.vault] = _vaultMultiplier[action.vault]; 475 | 476 | if (action.isDeposit) { 477 | if (action.usd <= _accounts[sender].perpBalance) { 478 | _accounts[sender].vaultEquity[action.vault].equity += action.usd; 479 | _accounts[sender].vaultEquity[action.vault].lockedUntilTimestamp = 480 | uint64((block.timestamp + 86400) * 1000); 481 | _accounts[sender].perpBalance -= action.usd; 482 | _vaultEquity[action.vault] += action.usd; 483 | } else { 484 | revert("insufficient balance"); 485 | } 486 | } else { 487 | PrecompileLib.UserVaultEquity storage userVaultEquity = _accounts[sender].vaultEquity[action.vault]; 488 | 489 | // a zero amount means withdraw the entire amount 490 | action.usd = action.usd == 0 ? userVaultEquity.equity : action.usd; 491 | 492 | // the vaults have a minimum withdraw of 1 / 100,000,000 493 | if (action.usd < _vaultEquity[action.vault] / 1e8) { 494 | revert("does not meet minimum withdraw"); 495 | } 496 | 497 | if (action.usd <= userVaultEquity.equity && userVaultEquity.lockedUntilTimestamp / 1000 <= block.timestamp) 498 | { 499 | userVaultEquity.equity -= action.usd; 500 | _accounts[sender].perpBalance += action.usd; 501 | } else { 502 | revert("equity too low, or locked"); 503 | } 504 | } 505 | } 506 | 507 | function executeStakingDeposit(address sender, StakingDepositAction memory action) 508 | public 509 | initAccountWithToken(sender, HYPE_TOKEN_INDEX) 510 | whenActivated(sender) 511 | { 512 | if (action._wei <= _accounts[sender].spot[HYPE_TOKEN_INDEX]) { 513 | _accounts[sender].spot[HYPE_TOKEN_INDEX] -= action._wei; 514 | _accounts[sender].staking += action._wei; 515 | } 516 | } 517 | 518 | function executeStakingWithdraw(address sender, StakingWithdrawAction memory action) 519 | public 520 | initAccountWithToken(sender, HYPE_TOKEN_INDEX) 521 | whenActivated(sender) 522 | { 523 | PrecompileLib.DelegatorSummary memory summary = readDelegatorSummary(sender); 524 | 525 | if (summary.nPendingWithdrawals >= 5) { 526 | revert("maximum of 5 pending withdrawals per account"); 527 | } 528 | 529 | if (action._wei <= _accounts[sender].staking) { 530 | _accounts[sender].staking -= action._wei; 531 | 532 | WithdrawRequest memory withrawRequest = WithdrawRequest({ 533 | account: sender, amount: action._wei, lockedUntilTimestamp: uint32(block.timestamp + 7 days) 534 | }); 535 | 536 | _withdrawQueue.pushBack(serializeWithdrawRequest(withrawRequest)); 537 | } 538 | } 539 | 540 | function executeTokenDelegate(address sender, TokenDelegateAction memory action) 541 | public 542 | initAccountWithToken(sender, HYPE_TOKEN_INDEX) 543 | whenActivated(sender) 544 | { 545 | if (_validators.length() != 0) { 546 | require(_validators.contains(action.validator)); 547 | } 548 | 549 | // first update their delegation amount based on staking yield 550 | PrecompileLib.Delegation storage delegation = _accounts[sender].delegations[action.validator]; 551 | delegation.amount = _getDelegationAmount(sender, action.validator); 552 | _userStakingYieldIndex[sender][action.validator] = _stakingYieldIndex; 553 | 554 | _accounts[sender].delegatedValidators.add(action.validator); 555 | 556 | if (action.isUndelegate) { 557 | if (action._wei <= delegation.amount && block.timestamp * 1000 > delegation.lockedUntilTimestamp) { 558 | _accounts[sender].staking += action._wei; 559 | delegation.amount -= action._wei; 560 | 561 | if (delegation.amount == 0) { 562 | _accounts[sender].delegatedValidators.remove(action.validator); 563 | } 564 | } else { 565 | revert("Insufficient delegation amount OR Delegation is locked"); 566 | } 567 | } else { 568 | if (action._wei <= _accounts[sender].staking) { 569 | _accounts[sender].staking -= action._wei; 570 | _accounts[sender].delegations[action.validator].amount += action._wei; 571 | 572 | _accounts[sender].delegations[action.validator].lockedUntilTimestamp = 573 | ((block.timestamp + 86400) * 1000).toUint64(); 574 | } else { 575 | revert("Insufficient staking balance"); 576 | } 577 | } 578 | } 579 | 580 | function setMarkPx(uint32 perp, uint64 priceDiffBps, bool isIncrease) public { 581 | uint64 basePrice = readMarkPx(perp); 582 | if (isIncrease) { 583 | _perpMarkPrice[perp] = basePrice * (10000 + priceDiffBps) / 10000; 584 | } else { 585 | _perpMarkPrice[perp] = basePrice * (10000 - priceDiffBps) / 10000; 586 | } 587 | } 588 | 589 | function setMarkPx(uint32 perp, uint64 markPx) public { 590 | _perpMarkPrice[perp] = markPx; 591 | } 592 | 593 | function setSpotPx(uint32 spotMarketId, uint64 priceDiffBps, bool isIncrease) public { 594 | uint64 basePrice = readSpotPx(spotMarketId); 595 | if (isIncrease) { 596 | _spotPrice[spotMarketId] = basePrice * (10000 + priceDiffBps) / 10000; 597 | } else { 598 | _spotPrice[spotMarketId] = basePrice * (10000 - priceDiffBps) / 10000; 599 | } 600 | } 601 | 602 | function setSpotPx(uint32 spotMarketId, uint64 spotPx) public { 603 | _spotPrice[spotMarketId] = spotPx; 604 | } 605 | 606 | function isActionExecutable(LimitOrderAction memory action, uint64 px) internal pure returns (bool) { 607 | bool executable = action.isBuy ? action.limitPx >= px : action.limitPx <= px; 608 | return executable; 609 | } 610 | 611 | function setVaultMultiplier(address vault, uint256 multiplier) public { 612 | _vaultMultiplier[vault] = multiplier; 613 | } 614 | 615 | function setStakingYieldIndex(uint256 multiplier) public { 616 | _stakingYieldIndex = multiplier; 617 | } 618 | 619 | function processPendingOrders() public { 620 | for (uint256 i = _pendingOrders.length; i > 0; i--) { 621 | PendingOrder memory order = _pendingOrders[i - 1]; 622 | uint32 spotMarketId = uint32(HLConversions.assetToSpotId(order.action.asset)); 623 | PrecompileLib.SpotInfo memory spotInfo = PrecompileLib.spotInfo(spotMarketId); 624 | PrecompileLib.TokenInfo memory baseToken = _tokens[spotInfo.tokens[0]]; 625 | uint64 spotPx = readSpotPx(spotMarketId) * SafeCast.toUint64(10 ** baseToken.szDecimals); 626 | 627 | if (isActionExecutable(order.action, spotPx)) { 628 | executeSpotLimitOrder(order.sender, order.action); 629 | 630 | // Remove executed order by swapping with last and popping 631 | _pendingOrders[i - 1] = _pendingOrders[_pendingOrders.length - 1]; 632 | _pendingOrders.pop(); 633 | } 634 | } 635 | } 636 | 637 | ////////// PERP LIQUIDATIONS //////////////////// 638 | function isLiquidatable(address user) public returns (bool) { 639 | uint64 totalNotional = 0; 640 | int64 totalUPnL = 0; 641 | uint64 totalLocked = 0; 642 | uint64 mmReq = 0; 643 | 644 | uint256 len = _userPerpPositions[user].length(); 645 | 646 | for (uint256 i = len; i > 0; i--) { 647 | uint16 perpIndex = uint16(_userPerpPositions[user].at(i - 1)); 648 | PrecompileLib.Position memory pos = _accounts[user].positions[perpIndex]; 649 | if (pos.szi != 0) { 650 | uint64 markPx = readMarkPx(perpIndex); 651 | int64 szi = pos.szi; 652 | uint64 avgEntry = pos.entryNtl / abs(szi); 653 | int64 uPnL = szi * (int64(markPx) - int64(avgEntry)); 654 | totalUPnL += uPnL; 655 | totalLocked += _accounts[user].margin[perpIndex]; 656 | 657 | uint64 positionNotional = abs(szi) * markPx; 658 | totalNotional += positionNotional; 659 | 660 | // Per-perp maintenance margin requirement based on max leverage 661 | uint32 maxLev = _getMaxLeverage(perpIndex); 662 | uint64 mmBps = 5000 / maxLev; // 5000 / maxLev gives bps for mm_fraction = 0.5 / maxLev 663 | mmReq += (positionNotional * mmBps) / 10000; 664 | } 665 | } 666 | 667 | if (totalNotional == 0) { 668 | return false; 669 | } 670 | 671 | int64 equity = int64(_accounts[user].perpBalance) + int64(totalLocked) + totalUPnL; 672 | 673 | return equity < int64(mmReq); 674 | } 675 | 676 | function abs(int64 value) internal pure returns (uint64) { 677 | return value > 0 ? uint64(value) : uint64(-value); 678 | } 679 | 680 | function _getMaxLeverage(uint16 perpIndex) public view returns (uint32) { 681 | return _perpAssetInfo[perpIndex].maxLeverage; 682 | } 683 | 684 | // simplified liquidation, nukes all positions and resets the perp balance 685 | // for future: make this more realistic 686 | function _liquidateUser(address user) public { 687 | uint256 len = _userPerpPositions[user].length(); 688 | for (uint256 i = len; i > 0; i--) { 689 | uint16 perpIndex = uint16(_userPerpPositions[user].at(i - 1)); 690 | 691 | bytes32 key = _getKey(user, perpIndex); 692 | _openPerpPositions.remove(key); 693 | _accounts[user].positions[perpIndex].szi = 0; 694 | _accounts[user].positions[perpIndex].entryNtl = 0; 695 | _accounts[user].margin[perpIndex] = 0; 696 | _userPerpPositions[user].remove(perpIndex); 697 | } 698 | 699 | _accounts[user].perpBalance = 0; 700 | } 701 | 702 | function liquidatePositions() public { 703 | uint256 len = _openPerpPositions.length(); 704 | 705 | if (len == 0) return; 706 | 707 | for (uint256 i = len; i > 0; i--) { 708 | bytes32 key = _openPerpPositions.at(i - 1); 709 | address user = address(uint160(uint256(key) >> 16)); 710 | if (isLiquidatable(user)) { 711 | _liquidateUser(user); 712 | } 713 | } 714 | } 715 | } 716 | --------------------------------------------------------------------------------