| name | security |
| description | Security for Hyperliquid applications — Solidity vulnerabilities on HyperEVM, API signing security, nonce safety, HYPE/USDC decimal traps, and the pre-deploy checklist. |
Security on Hyperliquid
What You Probably Got Wrong
"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.
Critical Vulnerabilities (HyperEVM)
1. Token Decimal Mismatch
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
2. Reentrancy
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");
}
3. Precision Loss
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
4. SafeERC20
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);
5. Oracle Safety
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:
- HyperCore mark prices via the precompile or oracle feeds
- Time-weighted average prices (TWAP) over multiple blocks
- An external oracle if available
6. Access Control
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
}
}
7. HYPE Bridge Security
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);
}
HyperCore API Security
Signing Safety
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.
def place_safe_order(exchange, coin, is_buy, sz, px):
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}"
print(f"Placing order: {coin} {'BUY' if is_buy else 'SELL'} {sz} @ {px}")
return exchange.order(coin, is_buy, sz, px,
{"limit": {"tif": "Gtc"}})
Nonce Replay Protection
HyperCore nonces protect against replay attacks. But:
- Don't expose your signed messages before submitting — they could be replayed with the same nonce
- Nonces are per-address — a compromised agent key can't replay mainnet nonces on testnet (different chainId in domain)
Rate Limits and Error Handling
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))
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"}})
Pre-Deploy Security Checklist
Run through this for EVERY HyperEVM contract before deploying to production:
HyperEVM Contracts
HyperCore API Integration
HyperEVM-Specific Notes
Cancun 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
Automated Security Tools
slither .
mythril analyze src/MyContract.sol
forge test --fuzz-runs 10000
forge test --gas-report
Run slither before any mainnet deployment. No high/medium findings unaddressed.