security/SKILL.md
Security for Hyperliquid applications — Solidity vulnerabilities on HyperEVM, API signing security, nonce safety, HYPE/USDC decimal traps, and the pre-deploy checklist.
npx skillsauth add cloudzombie/liquidskills securityInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
"HYPE has 18 decimals so math is the same as ETH." HYPE has 18 decimals on HyperEVM, but USDC has 6. Mix them up and you're off by 1e12 — sending 1 trillion USDC instead of 1. This is the #1 bug in Hyperliquid dApps.
"HyperEVM is just like Ethereum security-wise." Almost. Same reentrancy, same precision loss, same access control issues. But also: Cancun opcodes without blobs, no MEV on HyperCore (so flash loan concerns are different), and HYPE as gas.
"Signing an API request is always safe." No. Always verify what you're signing. A malicious UI can get you to sign a different action than what's displayed. Verify the action payload before submitting.
USDC is 6 decimals. HYPE is 18 decimals. Getting this wrong transfers absurd amounts.
// ❌ WRONG — assumes 18 decimals for USDC
uint256 oneHundredUSDC = 100e18; // Actually 100 trillion USDC
// ✅ CORRECT — check decimals dynamically
uint256 oneHundredUSDC = 100 * 10 ** IERC20Metadata(usdc).decimals();
// or if you know USDC is 6:
uint256 oneHundredUSDC = 100e6; // $100 USDC
// Decimal reference for HyperEVM
// HYPE: 18 decimals (native, like ETH)
// USDC: 6 decimals (always — same as Ethereum)
// WETH: 18 decimals
// HIP-1 tokens: varies (usually 6 or 8) — always check dynamically
When mixing HYPE and USDC in calculations:
// Normalize to same decimal basis first
uint256 usdcNormalized = usdcAmount * 1e12; // 6 → 18 decimals
uint256 total = hypeAmount + usdcNormalized; // Now both 18 decimals
Same as Ethereum. CEI pattern + ReentrancyGuard:
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
function withdraw(uint256 amount) external nonReentrant {
uint256 bal = balances[msg.sender];
require(bal >= amount, "Insufficient balance");
balances[msg.sender] = bal - amount; // Effect BEFORE interaction
(bool success,) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
Solidity has no floats. Division truncates.
// ❌ WRONG — truncates to 0
uint256 fivePercent = 5 / 100;
// ✅ CORRECT — basis points
uint256 FEE_BPS = 500; // 5%
uint256 fee = (amount * FEE_BPS) / 10_000;
// ✅ ALWAYS multiply before dividing
uint256 result = (a * c) / b; // Not: a / b * c
Always use SafeERC20 for token operations:
import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
using SafeERC20 for IERC20;
// ✅ CORRECT
token.safeTransfer(to, amount);
token.safeTransferFrom(from, to, amount);
token.safeApprove(spender, amount);
// ❌ WRONG — some tokens don't return bool
token.transfer(to, amount);
Don't use HyperSwap V2 spot prices as oracles — they can be manipulated in a single block.
For HyperEVM applications that need prices:
// ❌ DANGEROUS — manipulable via a single large swap
function getPrice() internal view returns (uint256) {
(uint112 reserve0, uint112 reserve1,) = hyperSwapPair.getReserves();
return (reserve1 * 1e18) / reserve0;
}
For price data, prefer:
Every state-changing function needs explicit access control:
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
// For simple ownership
contract MyVault is Ownable {
function setFee(uint256 newFee) external onlyOwner {
fee = newFee;
}
}
// For complex roles (admin, operator, keeper)
contract ComplexVault is AccessControl {
bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE");
function rebalance() external onlyRole(OPERATOR_ROLE) {
// Only operators can rebalance
}
}
Interactions with 0x2222222222222222222222222222222222222222:
// Sending HYPE to the bridge address withdraws to HyperCore
// This is a state change — treat it like any external call
// ✅ SAFE pattern
function withdrawToHyperCore(address payable user, uint256 amount) external {
require(msg.sender == user, "Not authorized");
require(amount <= userBalances[user], "Insufficient balance");
userBalances[user] -= amount; // Update state BEFORE send
(bool success,) = BRIDGE_ADDRESS.call{value: amount}("");
require(success, "Bridge transfer failed");
emit WithdrawnToHyperCore(user, amount);
}
Every HyperCore exchange action is an EIP-712 signed message. Security rules:
Always verify what you're signing. The SDK builds the message — review the action parameters before confirming.
Verify nonces are correct. Wrong nonce = order might be replayed or dropped.
Use agent wallets for bots. Never sign HyperCore actions with your main key in automated code.
Don't expose signing keys. Agent keys should have limited withdrawal permissions.
# ✅ CORRECT: Verify order parameters before submitting
def place_safe_order(exchange, coin, is_buy, sz, px):
# Sanity checks before signing
assert sz > 0, "Zero size"
assert px > 0, "Zero price"
assert coin in APPROVED_COINS, f"Unknown coin: {coin}"
assert sz <= MAX_ORDER_SIZE, f"Order too large: {sz}"
# Log what we're about to sign
print(f"Placing order: {coin} {'BUY' if is_buy else 'SELL'} {sz} @ {px}")
return exchange.order(coin, is_buy, sz, px,
{"limit": {"tif": "Gtc"}})
HyperCore nonces protect against replay attacks. But:
import time
from functools import wraps
def with_retry(max_retries=3, delay=1.0):
def decorator(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
for attempt in range(max_retries):
try:
return fn(*args, **kwargs)
except Exception as e:
if "rate limit" in str(e).lower() and attempt < max_retries - 1:
time.sleep(delay * (2 ** attempt)) # Exponential backoff
else:
raise
return wrapper
return decorator
@with_retry(max_retries=3)
def place_order_with_retry(exchange, coin, is_buy, sz, px):
return exchange.order(coin, is_buy, sz, px, {"limit": {"tif": "Gtc"}})
Run through this for EVERY HyperEVM contract before deploying to production:
1e18 for USDC (6 decimals) or unknown tokensnonReentrant on all external-calling functionsCancun opcodes available: TSTORE, TLOAD (transient storage) work on HyperEVM.
No blobs: BLOBHASH, BLOBBASEFEE opcodes are NOT available despite Cancun compatibility. Don't use them.
Priority fees are burned: You can't use priority fee manipulation for MEV. Bots don't bid gas wars on HyperEVM like they do on Ethereum.
Block time ~1-2s: Contracts relying on block.timestamp should account for faster block production. Minimum deadlines should be longer than 1 block.
// ✅ CORRECT: Use timestamps in seconds, not block numbers
// 5 minute deadline
uint256 deadline = block.timestamp + 5 * 60;
// ❌ RISKY: Block number arithmetic depends on block time
// "100 blocks" is only ~100-200 seconds on HyperEVM, not 20 minutes
# Static analysis
slither . # Common vulnerability detection
mythril analyze src/MyContract.sol # Symbolic execution
# Foundry fuzzing
forge test --fuzz-runs 10000 # Fuzz all parameterized tests
# Gas optimization
forge test --gas-report
Run slither before any mainnet deployment. No high/medium findings unaddressed.
development
Why build on Hyperliquid. HyperBFT consensus, native orderbook, speed, AI agent angle, honest tradeoffs. Use when someone asks "should I build on Hyperliquid?", "why not Ethereum?", or when an agent needs to understand what makes Hyperliquid unique.
development
Wallets on Hyperliquid — MetaMask + chain ID 999 setup, HyperCore API wallets, agent wallet patterns, EIP-712 signing for exchange actions. Essential for any agent that needs to interact with Hyperliquid.
tools
Development tools for Hyperliquid — Foundry, Hardhat, viem, wagmi for HyperEVM; Python SDK and TypeScript SDK for HyperCore API. What works, what to use, how to set up.
testing
Smart contract testing for HyperEVM with Foundry/Hardhat — unit tests, fuzz testing, testnet fork testing. What to test, what not to test, and what LLMs get wrong on Hyperliquid.