HyperEVM 開発 · L1 の precompile と連携 · 5/8
HyperEVM で作る
よく使う形
HyperEVM の ERC-20 トークン
// 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 コントラクトの形
// 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));
}
}
これは ERC-4626 のトークン化 vault の裏にある持ち分の計算を示しています。ただし ERC-4626 には準拠していません。また、最初の預け入れを狙う「inflation」攻撃(空の vault にトークンを贈って持ち分の価格をゆがめる攻撃)に弱いです。本番では OpenZeppelin の ERC4626 を土台にしてください。仮想の持ち分による防御が入っています。
vault で HyperCore の価格を読む
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));
}
}
開発の流れ
- Solidity でコントラクトを書く(Hardhat か Foundry)
- Hardhat / Foundry のテストの仕組みで手元でテストする
- テストネットにデプロイする — 本物の HyperCore のデータで動きを確かめます(precompile は手元のノードにはありません)
- エクスプローラーで検証する — ソースコードを公開します
- メインネットにデプロイする — 十分にテストしてから
- フロントエンドを作る — コントラクトとのやりとりには ethers.js / viem を使います
安全面で気をつけること
- リエントランシー — OpenZeppelin の ReentrancyGuard を使います
- フラッシュローン攻撃 — 価格に頼るロジックには注意します
- precompile をどこまで信じるか — HyperCore のデータは EVM ブロックが作られた時点の状態です。不正な入力で呼ぶと revert し、渡したガスをすべて使い切ります
- アップグレード — アップグレードできるコントラクトには proxy の形を検討します
- 権限の管理 — 管理用の関数には Ownable か AccessControl を使います