17 min read
Overview
Hyperliquid's dual architecture combines a high-performance trading engine (HyperCore) with EVM compatibility (HyperEVM), creating unique opportunities for DeFi developers. While possibilities are endless, one of the most exciting applications is the ability to access HyperCore's real-time price feeds directly from your HyperEVM dApps, without the need for a separate price oracle.
This unique architecture makes it possible to access real-time price data—like perpetual futures prices—directly within your smart contracts, without relying on external oracles. It’s a game-changer for DeFi builders who want native access to HyperCore’s financial primitives from an EVM context.
In this guide, you’ll build a HyperEVM smart contract that reads live prices from HyperCore. Along the way, you’ll explore how to use Hyperliquid’s L1Read.sol interface and precompile contracts to fetch and convert oracle prices, laying the foundation for more advanced dApps like trading bots, lending protocols, and on-chain analytics.
What You Will Learn
- How to set up a Foundry project for HyperEVM
- How to deploy a smart contract on HyperEVM to fetch HyperCore oracle prices by leveraging HyperEVM's precompiles
- How to query HyperCore price data using direct scripts and smart contracts
What You Will Need
- Foundry installed on your machine
- Basic Solidity knowledge
- HYPE tokens for gas (from testnet faucet or mainnet purchase)
- A wallet for contract deployment (e.g., MetaMask, Rabby, or brand new wallet created in Foundry)
- A free QuickNode account (optional, but recommended) to access dedicated Hyperliquid RPC endpoint (for mainnet)
Hyperliquid's Architecture: HyperCore and HyperEVM
Hyperliquid is a Layer 1 blockchain optimized for high-performance DeFi use cases. Its core engine, HyperCore, handles a fully onchain order book with sub-second finality. This performance is made possible by HyperBFT, Hyperliquid’s consensus mechanism, which ensures one-block finality and supports up to 200,000 orders per second. On the other hand, HyperEVM integrates Ethereum-compatible smart contracts into this ecosystem, letting developers build on top of HyperCore's order book data.
HyperCore exposes its functionality through precompiles - special contracts at predefined addresses that allow HyperEVM contracts to query data like oracle prices, asset metadata, and user positions. These can be accessed directly in Solidity using static calls, thanks to the L1Read.sol
contract provided by Hyperliquid. To ensure you're using the correct precompile addresses, always refer to the latest list in Hyperliquid’s official developer documentation.
In this guide, we'll use following functions that are available in the L1Read.sol
contract:
oraclePx(uint32 index)
: Returns the oracle price of a perpetual futures contract at a given index. Utilizes theORACLE_PX_PRECOMPILE_ADDRESS
precompile.perpAssetInfo(uint32 index)
: Returns the metadata of a perpetual futures contract at a given index. Utilizes thePERP_ASSET_INFO_PRECOMPILE_ADDRESS
precompile.
Hyperliquid's architecture allows both reading and writing to the HyperCore within the HyperEVM. However, their write system contracts are not yet available on the HyperEVM mainnet. So, in this guide, we'll focus on reading from the HyperCore.
Prerequisites
Before we start, make sure you do the following:
Install Foundry
To get started, make sure you have Foundry, a toolkit for EVM smart contract development, installed on your machine. If not, install it by running the following command in your terminal:
curl -L https://foundry.paradigm.xyz | bash
foundryup
If you're new to Foundry, check out our Foundry tutorial to get started.
Get Your QuickNode Endpoint
To interact with the HyperEVM, you must connect to the Hyperliquid blockchain using an RPC endpoint. While you can use public RPCs, we recommend using a dedicated endpoint for production use. To get your endpoint, sign up for a free QuickNode account and create an endpoint for Hyperliquid (Mainnet).
Set Up Your Wallet
You can create a new wallet or use an existing one to deploy your smart contract through Foundry.
While importing your wallet, we suggest encrypting your private key for security, instead of using it directly. To learn more about why it's important, check out the guide on How to Secure Your Private Keys in Hardhat and Foundry guide.
Run the following command in your terminal and enter your wallet's private key and a password to encrypt it:
cast wallet import your-wallet-name --interactive
Replace your-wallet-name
with the name of your wallet. You'll be prompted to enter your private key and set a password for encryption. The --interactive
flag ensures the private key isn't saved in your shell history for security.
For a new wallet, you can use the following command to create a new wallet:
cast wallet new
Get HYPE Tokens
HYPE is the native token on Hyperliquid, and you'll need some HYPE tokens to pay as gas fees for our smart contract deployment.
For the testnet, you can get some test USDC from Hyperliquid's Testnet Faucet and use it to buy some HYPE. If you want to use the mainnet, you can purchase HYPE tokens on the Hyperliquid Mainnet. Whether you're using testnet or mainnet, you need to transfer HYPE from HyperCore perps to HyperCore spot and then to HyperEVM, all through their UI as shown below:
Watch this video to learn how to get HYPE tokens on HyperEVM:
In this video, learn everything about Hyperliquid HyperEVM and how to get started with HyperEVM. We cover the differences between HyperCore and HyperEVM, and how to get/transfer HYPE tokens into the HyperEVM wallet.
How to Interact with the HyperCore from HyperEVM
In this section, we will show you how to interact with the HyperCore Oracle Prices in your HyperEVM dApp by leveraging HyperEVM's precompiles. We will cover the following methods:
- Using direct queries with Foundry's
Cast
- Building a simple Solidity contract to fetch HyperCore Oracle Prices
First, let's create a Foundry project to work with.
Set Up a Foundry Project
Step 1: Create a Foundry Project
forge init hyperevm-oracle-prices
cd hyperevm-oracle-prices
This command will create a new directory called hyperevm-oracle-prices
and initialize a Foundry project inside it, with some initial files (i.e., src/Counter.sol
) and configurations (i.e., foundry.toml
).
├── README.md
├── foundry.toml // Foundry configuration file
├── lib
├── script
│ └── Counter.s.sol // Foundry deployment script
├── src
│ └── Counter.sol // Main contract file
└── test
└── Counter.t.sol // Test contract file
Step 2: Update the File Structure
Rename the deployment script and main contract files to match the project name, and remove the test file since we won't be using it in this guide.
mv script/Counter.s.sol script/DeployPriceOracleReader.s.sol
mv src/Counter.sol src/PriceOracleReader.sol
rm test/Counter.t.sol
Step 3: Configure Environment Variables
Create a .env
file in the root directory to store endpoint URLs. You can use the QuickNode Hyperliquid Mainnet RPC endpoint, or use public endpoints.
TESTNET_RPC_URL=https://rpc.hyperliquid-testnet.xyz/evm
MAINNET_RPC_URL=https://rpc.hyperliquid.xyz/evm # or your own QuickNode Hyperliquid RPC endpoint
Then, load the variables into your terminal environment:
source .env
Step 4: Update Foundry Config File
Update the foundry.toml
file to include the Hyperliquid RPC URLs:
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
[rpc_endpoints]
hyperliquid_testnet = "${TESTNET_RPC_URL}"
hyperliquid_mainnet = "${MAINNET_RPC_URL}"
Get Token Metadata
To get the metadata of a token, we'll make an API call to the Hyperliquid's API endpoint. The endpoint returns the token's name, size decimals (szDecimals), and other metadata along with the token's index.
curl --location 'https://api.hyperliquid.xyz/info' \
--header 'Content-Type: application/json' \
--data '{"type": "meta"}'
Token indexes are different for the mainnet and testnet. So, you need to update the API endpoint to
https://api.hyperliquid-testnet.xyz/info
if you're using the testnet.
Once you retrieve the token index (e.g., 5), you’ll need to format it correctly for smart contract calls. This involves:
- Converting the index to hexadecimal
- Prefixing it with
0x
- Padding it to 32 bytes (64 hex characters) with leading zeros
For example, if the token index is 5, the conversion would look like this:
Decimal: 5
Hex: 0x0000000000000000000000000000000000000000000000000000000000000005
szDecimals
is the number of significant digits in the price. For example, for BTC, theszDecimals
is 5.
Now, you are ready to go! Let's understand how to interact with the HyperCore from HyperEVM by directly querying HyperCore's precompiles or building a simple Solidity contract to fetch HyperCore Oracle Prices.
Method 1: Direct Queries with Foundry's Cast
This method allows you to quickly interact with HyperCore's precompiles without the need for a deployed contract. You can use the cast
command to perform these queries directly from your terminal.
Step 1: Identify the Precompile Address
As explained in the Hyperliquid's Architecture section, there are different types of precompiles available for interacting with HyperCore. In this example, we'll focus on the ORACLE_PX_PRECOMPILE_ADDRESS
, which is the address of the HyperCore Oracle Prices precompile and takes one parameter - the token index.
Always refer to the Hyperliquid’s official developer documentation to ensure you're using the correct precompile addresses.
ORACLE_PX_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000807
Step 2: Query the Price with Cast
The following command template can be used to query the price of any token:
cast call <precompile_address> <input_parameter_token_index> --rpc-url <rpc_url>
For example, to query the price of BTC (token index 3 on the testnet):
cast call 0x0000000000000000000000000000000000000807 0x0000000000000000000000000000000000000000000000000000000000000003 --rpc-url $TESTNET_RPC_URL
Direct queries use raw 32-byte inputs.
The output will be the price of BTC in USD, with up to 5 significant digits and 6 - szDecimals
decimals, where szDecimals
varies depending on the token.
You should see the value as hexadecimal, for example:
0x00000000000000000000000000000000000000000000000000000000000ea768
Convert the output to a decimal number:
cast --to-dec 0x00000000000000000000000000000000000000000000000000000000000ea768
For example, the output of this command is 960360, which represents the price of BTC in USD (96036.0) at the time of writing this guide.
While direct queries are useful for quick testing, a smart contract provides an onchain solution for dApps. Let's build our PriceOracleReader
contract.
Method 2: Query the Price with a Smart Contract
In this section, we'll build a smart contract that queries HyperCore's oracle prices and returns the price of any token.
Step 1: Import the L1Read Contract
First, we need to add the L1Read.sol
contract provided by Hyperliquid to our project.
Note: The
L1Read.sol
contract is not part of the official Foundry package, so we need to add it manually.
Note2: We modified the
L1Read.sol
contract's all external functions to public, so that we can call them from our smart contract.
Create a new file called L1Read.sol
under the src
directory and paste the following code:
Click to expand
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract L1Read {
struct Position {
int64 szi;
uint64 entryNtl;
int64 isolatedRawUsd;
uint32 leverage;
bool isIsolated;
}
struct SpotBalance {
uint64 total;
uint64 hold;
uint64 entryNtl;
}
struct UserVaultEquity {
uint64 equity;
uint64 lockedUntilTimestamp;
}
struct Withdrawable {
uint64 withdrawable;
}
struct Delegation {
address validator;
uint64 amount;
uint64 lockedUntilTimestamp;
}
struct DelegatorSummary {
uint64 delegated;
uint64 undelegated;
uint64 totalPendingWithdrawal;
uint64 nPendingWithdrawals;
}
struct PerpAssetInfo {
string coin;
uint32 marginTableId;
uint8 szDecimals;
uint8 maxLeverage;
bool onlyIsolated;
}
struct SpotInfo {
string name;
uint64[2] tokens;
}
struct TokenInfo {
string name;
uint64[] spots;
uint64 deployerTradingFeeShare;
address deployer;
address evmContract;
uint8 szDecimals;
uint8 weiDecimals;
int8 evmExtraWeiDecimals;
}
address constant POSITION_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000800;
address constant SPOT_BALANCE_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000801;
address constant VAULT_EQUITY_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000802;
address constant WITHDRAWABLE_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000803;
address constant DELEGATIONS_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000804;
address constant DELEGATOR_SUMMARY_PRECOMPILE_ADDRESS =
0x0000000000000000000000000000000000000805;
address constant MARK_PX_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000806;
address constant ORACLE_PX_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000807;
address constant SPOT_PX_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000808;
address constant L1_BLOCK_NUMBER_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000809;
address constant PERP_ASSET_INFO_PRECOMPILE_ADDRESS = 0x000000000000000000000000000000000000080a;
address constant SPOT_INFO_PRECOMPILE_ADDRESS = 0x000000000000000000000000000000000000080b;
address constant TOKEN_INFO_PRECOMPILE_ADDRESS = 0x000000000000000000000000000000000000080C;
function position(address user, uint16 perp) public view returns (Position memory) {
bool success;
bytes memory result;
(success, result) = POSITION_PRECOMPILE_ADDRESS.staticcall(abi.encode(user, perp));
require(success, "Position precompile call failed");
return abi.decode(result, (Position));
}
function spotBalance(address user, uint64 token) public view returns (SpotBalance memory) {
bool success;
bytes memory result;
(success, result) = SPOT_BALANCE_PRECOMPILE_ADDRESS.staticcall(abi.encode(user, token));
require(success, "SpotBalance precompile call failed");
return abi.decode(result, (SpotBalance));
}
function userVaultEquity(
address user,
address vault
) public view returns (UserVaultEquity memory) {
bool success;
bytes memory result;
(success, result) = VAULT_EQUITY_PRECOMPILE_ADDRESS.staticcall(abi.encode(user, vault));
require(success, "VaultEquity precompile call failed");
return abi.decode(result, (UserVaultEquity));
}
function withdrawable(address user) public view returns (Withdrawable memory) {
bool success;
bytes memory result;
(success, result) = WITHDRAWABLE_PRECOMPILE_ADDRESS.staticcall(abi.encode(user));
require(success, "Withdrawable precompile call failed");
return abi.decode(result, (Withdrawable));
}
function delegations(address user) public view returns (Delegation[] memory) {
bool success;
bytes memory result;
(success, result) = DELEGATIONS_PRECOMPILE_ADDRESS.staticcall(abi.encode(user));
require(success, "Delegations precompile call failed");
return abi.decode(result, (Delegation[]));
}
function delegatorSummary(address user) public view returns (DelegatorSummary memory) {
bool success;
bytes memory result;
(success, result) = DELEGATOR_SUMMARY_PRECOMPILE_ADDRESS.staticcall(abi.encode(user));
require(success, "DelegatorySummary precompile call failed");
return abi.decode(result, (DelegatorSummary));
}
function markPx(uint32 index) public view returns (uint64) {
bool success;
bytes memory result;
(success, result) = MARK_PX_PRECOMPILE_ADDRESS.staticcall(abi.encode(index));
require(success, "MarkPx precompile call failed");
return abi.decode(result, (uint64));
}
function oraclePx(uint32 index) public view returns (uint64) {
bool success;
bytes memory result;
(success, result) = ORACLE_PX_PRECOMPILE_ADDRESS.staticcall(abi.encode(index));
require(success, "OraclePx precompile call failed");
return abi.decode(result, (uint64));
}
function spotPx(uint32 index) public view returns (uint64) {
bool success;
bytes memory result;
(success, result) = SPOT_PX_PRECOMPILE_ADDRESS.staticcall(abi.encode(index));
require(success, "SpotPx precompile call failed");
return abi.decode(result, (uint64));
}
function l1BlockNumber() public view returns (uint64) {
bool success;
bytes memory result;
(success, result) = L1_BLOCK_NUMBER_PRECOMPILE_ADDRESS.staticcall(abi.encode());
require(success, "L1BlockNumber precompile call failed");
return abi.decode(result, (uint64));
}
function perpAssetInfo(uint32 perp) public view returns (PerpAssetInfo memory) {
bool success;
bytes memory result;
(success, result) = PERP_ASSET_INFO_PRECOMPILE_ADDRESS.staticcall(abi.encode(perp));
require(success, "PerpAssetInfo precompile call failed");
return abi.decode(result, (PerpAssetInfo));
}
function spotInfo(uint32 spot) public view returns (SpotInfo memory) {
bool success;
bytes memory result;
(success, result) = SPOT_INFO_PRECOMPILE_ADDRESS.staticcall(abi.encode(spot));
require(success, "SpotInfo precompile call failed");
return abi.decode(result, (SpotInfo));
}
function tokenInfo(uint32 token) public view returns (TokenInfo memory) {
bool success;
bytes memory result;
(success, result) = TOKEN_INFO_PRECOMPILE_ADDRESS.staticcall(abi.encode(token));
require(success, "TokenInfo precompile call failed");
return abi.decode(result, (TokenInfo));
}
}
This file includes functions that uses Solidity's staticcall
to interact with precompiles at their predefined addresses.
Step 2: Create a PriceOracleReader Contract
Open the PriceOracleReader.sol
file and paste the following code:
Click to expand
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
// Import the L1Read contract
import "./L1Read.sol";
contract PriceOracleReader is L1Read {
// Mapping to store the latest price for each perp asset index
mapping(uint32 => uint256) public latestPrices;
// Mapping to store asset names
mapping(uint32 => string) public assetNames;
// Event for price updates
event PriceUpdated(uint32 indexed perpIndex, uint256 price);
/**
* @dev Update the price for a perp asset
* @param perpIndex The index of the perp asset
* @return The converted price with 18 decimals
*/
function updatePrice(uint32 perpIndex) public returns (uint256) {
// Get the raw oracle price using the inherited function
uint64 rawPrice = oraclePx(perpIndex);
// Get the asset info using the inherited function
PerpAssetInfo memory assetInfo = perpAssetInfo(perpIndex);
uint8 szDecimals = assetInfo.szDecimals;
// Store the asset name
assetNames[perpIndex] = assetInfo.coin;
// Convert the price: price / 10^(6 - szDecimals) * 10^18
// This converts from raw price to human-readable price with 18 decimals
uint256 divisor = 10 ** (6 - szDecimals);
uint256 convertedPrice = (uint256(rawPrice) * 1e18) / divisor;
// Store the converted price
latestPrices[perpIndex] = convertedPrice;
// Emit event
emit PriceUpdated(perpIndex, convertedPrice);
return convertedPrice;
}
/**
* @dev Get the latest price for a perp asset
* @param perpIndex The index of the perp asset
* @return The latest converted price with 18 decimals
*/
function getLatestPrice(uint32 perpIndex) public view returns (uint256) {
return latestPrices[perpIndex];
}
}
This contract;
- inherits from the local
L1Read
contract we just added - includes an
updatePrice
function that fetches the latest price and converts it to 18 decimals using the inheritedoraclePx
andperpAssetInfo
functions - includes a
getLatestPrice
function that returns the latest price for a given perp asset index - includes a
PriceUpdated
event to notify when the price is updated.
The updatePrice
function converts the raw price to a human-readable price with 18 decimals. The conversion formula is:
uint256 convertedPrice = (uint256(rawPrice) * 1e18) / divisor;
where rawPrice
is the raw price returned by the oraclePx
function in hexadecimal format, divisor
is calculated as 10 ** (6 - szDecimals)
, and szDecimals
is the number of significant digits in the price.
Step 3: Deploy the PriceOracleReader Contract
Now that we have the PriceOracleReader
contract, we can deploy it to the testnet or mainnet.
Step 4: Create a Deployment Script
Open the script/DeployPriceOracleReader.s.sol
file and paste the following code:
Click to expand
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Script.sol";
import "../src/PriceOracleReader.sol";
contract DeployPriceOracleReader is Script {
function run() external {
// Start broadcast to record and send transactions
vm.startBroadcast();
// Deploy the PriceOracleReader contract
PriceOracleReader reader = new PriceOracleReader();
console.log("PriceOracleReader deployed at:", address(reader));
// End broadcast
vm.stopBroadcast();
}
}
This script deploys the PriceOracleReader
contract and logs the address.
Step 5: Run the Deployment Script
Run the following command to compile and deploy the contract:
forge build
forge script script/DeployPriceOracleReader.s.sol:DeployPriceOracleReader --rpc-url $TESTNET_RPC_URL --account your-wallet-name --broadcast
Note: The
--account
flag specifies the wallet that will be used to deploy the contract. You can use the same wallet you imported before. Usecast wallet list
to list your wallets if you're not sure which one to use.
The deployment script will start the broadcast process and deploy the contract. Once the transaction is confirmed, the contract will be deployed and the address will be logged.
Step 6: Interact with Your Contract on HyperEVM
Now that the contract is deployed, we can call the updatePrice
function to fetch the latest price for a given perp asset index. The following command can be used to query the price of the perp asset with index 3 on the testnet:
cast call <your_contract_address> "updatePrice(uint32)(uint256)" 3 --rpc-url $TESTNET_RPC_URL
Note: The
updatePrice
function takes auint32
parameter, which is the index of the perp asset. Since the index is encoded within theoraclePx
function itself, we don't need to pass it as encoded data.
This command will return the latest price of the perp asset with index 3 in wei (18 decimals), such as 96036000000000000000000
to represent $96036.
Conclusion
Congratulations! You’ve now learned how to access real-time oracle prices from HyperCore using both scripts and Solidity smart contracts via HyperEVM. This integration unlocks DeFi possibilities like lending platforms, synthetic assets, or arbitrage bots—all within a high-performance, EVM-compatible ecosystem of Hyperliquid.
You can interact with your HyperEVM smart contract using Ethereum libraries like ethers.js or Viem. Check out our ethers.js guides and Viem guides to learn more about these libraries.
Next Steps
- Explore other read precompiles: HyperCore exposes a wide range of precompiles that can be used to query data from the HyperCore order book. Explore the precompiles and their capabilities to see how they can be used in your HyperEVM dApps.
- Explore Hyperliquid’s write precompiles: Hyperliquid's write precompiles allow you to place trades onchain, enabling advanced trading strategies and advanced order management.
- Expand your HyperEVM dApp: By combining HyperCore with HyperEVM, you can create a dApp that leverages HyperCore's onchain order book data to provide a seamless user experience, such as trading bots, lending platforms, or synthetic assets.
Further Resources
- Hyperliquid Documentation
- QuickNode Hyperliquid Guides
- QuickNode Hyperliquid Docs
- Foundry Book
- Video: What is Hyperliquid HyperEVM and How to Get Started
If you are stuck or have questions, drop them in our Discord. Stay up to date with the latest by following us on Twitter (@QuickNode) or our Telegram announcement channel.
We ❤️ Feedback!
Let us know if you have any feedback or requests for new topics. We'd love to hear from you.