HyperEVM Development · L1 Precompiles & Interop · 5/8
Building on HyperEVM
Common Patterns
ERC-20 Tokens on HyperEVM
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract HypeToken is ERC20 {
constructor(uint256 initialSupply) ERC20("HypeToken", "HTKN") {
_mint(msg.sender, initialSupply * 10 ** decimals());
}
}
Vault Contract Pattern
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
contract SimpleVault is ERC20, Ownable {
using SafeERC20 for IERC20;
IERC20 public immutable asset; // e.g. a USDC token on HyperEVM
constructor(address _asset)
ERC20("Vault Share", "vUSDC")
Ownable(msg.sender)
{
asset = IERC20(_asset);
}
function deposit(uint256 amount) external returns (uint256 shares) {
uint256 assetsBefore = asset.balanceOf(address(this));
uint256 totalShares = totalSupply();
if (totalShares == 0) {
shares = amount;
} else {
shares = (amount * totalShares) / assetsBefore;
}
require(shares > 0, "zero shares");
asset.safeTransferFrom(msg.sender, address(this), amount);
_mint(msg.sender, shares);
}
function withdraw(uint256 shares) external returns (uint256 amount) {
uint256 assetsHeld = asset.balanceOf(address(this));
uint256 totalShares = totalSupply();
amount = (shares * assetsHeld) / totalShares;
_burn(msg.sender, shares);
asset.safeTransfer(msg.sender, amount);
}
function totalAssets() public view returns (uint256) {
return asset.balanceOf(address(this));
}
}
This shows the share accounting behind ERC-4626 tokenized vaults, but it is not ERC-4626 compliant and is vulnerable to the first-depositor "inflation" attack (someone donates tokens to the empty vault to skew the share price). For production, build on OpenZeppelin's ERC4626, which includes virtual-share protection.
Reading HyperCore Prices in a Vault
contract PriceAwareVault is SimpleVault {
// Mark price read precompile (see the previous lesson)
address constant MARK_PX = 0x0000000000000000000000000000000000000806;
constructor(address _asset) SimpleVault(_asset) {}
// Raw price: divide by 10^(6 - szDecimals) for a human-readable value
function getMarkPx(uint32 perpIndex) public view returns (uint64) {
(bool ok, bytes memory res) = MARK_PX.staticcall(abi.encode(perpIndex));
require(ok, "markPx failed");
return abi.decode(res, (uint64));
}
}
Development Workflow
- Write contracts in Solidity (Hardhat or Foundry)
- Test locally with Hardhat/Foundry test framework
- Deploy to testnet — verify behavior with real HyperCore data (precompiles do not exist on a local node)
- Verify on explorer — publish source code
- Deploy to mainnet — after thorough testing
- Build frontend — ethers.js / viem for contract interaction
Security Considerations
- Reentrancy — Use OpenZeppelin's ReentrancyGuard
- Flash Loan Attacks — Be careful with price-dependent logic
- Precompile Trust — HyperCore data reflects state when the EVM block was built; a call with an invalid input reverts and burns all gas passed to it
- Upgradability — Consider proxy patterns for upgradeable contracts
- Access Control — Use Ownable or AccessControl for admin functions