Comparing ens with monad
ens
View full →Author
@0xinit
Stars
53
Repository
0xinit/cryptoskills
ENS (Ethereum Name Service)
ENS maps human-readable names (alice.eth) to Ethereum addresses, content hashes, and arbitrary metadata. It is the identity layer for Ethereum — used for wallets, dApps, and onchain profiles. The architecture separates the registry (who owns a name) from resolvers (what data a name points to).
What You Probably Got Wrong
- ENS uses
namehash, not plain strings -- The registry and resolvers never see "alice.eth" as a string. Names are normalized (UTS-46), then hashed with the recursivenamehashalgorithm (EIP-137). If you pass a raw string to a contract call, it will not work. viem handles this automatically in its ENS actions but you must usenamehash()andlabelhash()for direct contract calls. - Registry vs Resolver vs Registrar -- three different contracts -- The Registry tracks name ownership and which resolver to use. The Resolver stores records (address, text, contenthash). The Registrar handles
.ethname registration and renewal. Confusing these is the most common ENS integration bug. .ethregistrar uses commit-reveal, not a single transaction -- Registration requires two transactions separated by at least 60 seconds: firstcommit(secret), wait, thenregister(name, owner, duration, secret, ...). This prevents frontrunning. Skipping the wait or reusing a secret will revert.- Reverse resolution is opt-in -- An address only has a "primary name" if the owner explicitly set it via the Reverse Registrar. Do not assume every address has a reverse record. Always handle
nullreturns fromgetEnsName(). - Name Wrapper changes ownership semantics -- Since 2023, ENS names can be "wrapped" as ERC-1155 tokens via the Name Wrapper contract. Wrapped names have fuses that permanently restrict operations (cannot unwrap, cannot set resolver, etc.). Check
isWrappedbefore assuming standard ownership patterns. - CCIP-Read (ERC-3668) enables offchain resolution -- Resolvers can return an
OffchainLookuperror that instructs the client to fetch data from an offchain gateway and verify it onchain. This powers offchain subdomains, L2 resolution, and gasless record updates. viem handles CCIP-Read automatically. - Wildcard resolution (ENSIP-10) is real -- Resolvers can implement
resolve(bytes name, bytes data)to handle any subdomain dynamically, even ones not explicitly registered. This is how services like cb.id and lens.xyz work. - ENS names expire --
.ethnames require annual renewal. Expired names enter a 90-day grace period, then a 21-day premium auction, then become available. Do not cache resolution results indefinitely. normalize()before any ENS operation -- Names must be UTS-46 normalized before hashing. "Alice.ETH" and "alice.eth" produce different hashes. viem normalizes automatically, but if you build raw calldata you must normalize first using@adraffy/ens-normalize.
Quick Start
Installation
npm install viem
viem has built-in ENS support -- no additional packages needed for resolution.
Forward Resolution (Name to Address)
import { createPublicClient, http } from "viem";
import { mainnet } from "viem/chains";
const client = createPublicClient({
chain: mainnet,
transport: http(process.env.RPC_URL),
});
const address = await client.getEnsAddress({
name: "vitalik.eth",
});
// "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
Reverse Resolution (Address to Name)
const name = await client.getEnsName({
address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
});
// "vitalik.eth" (or null if no primary name set)
Get Avatar
const avatar = await client.getEnsAvatar({
name: "vitalik.eth",
});
// HTTPS URL to avatar image, or null
Name Resolution
Forward Resolution with Coin Types
ENS can store addresses for any blockchain, not just Ethereum. Each chain has a SLIP-44 coin type.
const ethAddress = await client.getEnsAddress({
name: "vitalik.eth",
});
// BTC address (coin type 0)
const btcAddress = await client.getEnsAddress({
name: "vitalik.eth",
coinType: 0,
});
// Solana address (coin type 501)
const solAddress = await client.getEnsAddress({
name: "vitalik.eth",
coinType: 501,
});
Text Records
ENS text records store arbitrary key-value metadata. Standard keys are defined in ENSIP-5.
const twitter = await client.getEnsText({
name: "vitalik.eth",
key: "com.twitter",
});
const github = await client.getEnsText({
name: "vitalik.eth",
key: "com.github",
});
const email = await client.getEnsText({
name: "vitalik.eth",
key: "email",
});
const url = await client.getEnsText({
name: "vitalik.eth",
key: "url",
});
const description = await client.getEnsText({
name: "vitalik.eth",
key: "description",
});
// Avatar is also a text record (ENSIP-12 supports NFT references)
const avatarRecord = await client.getEnsText({
name: "vitalik.eth",
key: "avatar",
});
// Can be HTTPS URL, IPFS URI, or NFT reference like
// "eip155:1/erc721:0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d/1234"
Standard Text Record Keys
| Key | Description |
|---|---|
email | Email address |
url | Website URL |
avatar | Avatar image (HTTPS, IPFS, or NFT reference) |
description | Short bio |
display | Display name (may differ from ENS name) |
com.twitter | Twitter/X handle |
com.github | GitHub username |
com.discord | Discord username |
org.telegram | Telegram handle |
notice | Contract notice text |
keywords | Comma-separated keywords |
header | Profile header/banner image |
Content Hash
import { createPublicClient, http, parseAbi } from "viem";
import { mainnet } from "viem/chains";
import { namehash } from "viem/ens";
const client = createPublicClient({
chain: mainnet,
transport: http(process.env.RPC_URL),
});
const RESOLVER_ABI = parseAbi([
"function contenthash(bytes32 node) view returns (bytes)",
]);
const node = namehash("vitalik.eth");
// First get the resolver address
const resolverAddress = await client.getEnsResolver({
name: "vitalik.eth",
});
const contenthash = await client.readContract({
address: resolverAddress,
abi: RESOLVER_ABI,
functionName: "contenthash",
args: [node],
});
// Encoded content hash (IPFS, Swarm, Arweave, etc.)
Batch Resolution with Multicall
import { createPublicClient, http, parseAbi } from "viem";
import { mainnet } from "viem/chains";
import { namehash } from "viem/ens";
const client = createPublicClient({
chain: mainnet,
transport: http(process.env.RPC_URL),
});
const RESOLVER_ABI = parseAbi([
"function addr(bytes32 node) view returns (address)",
"function text(bytes32 node, string key) view returns (string)",
]);
const node = namehash("vitalik.eth");
const resolverAddress = await client.getEnsResolver({
name: "vitalik.eth",
});
const results = await client.multicall({
contracts: [
{
address: resolverAddress,
abi: RESOLVER_ABI,
functionName: "addr",
args: [node],
},
{
address: resolverAddress,
abi: RESOLVER_ABI,
functionName: "text",
args: [node, "com.twitter"],
},
{
address: resolverAddress,
abi: RESOLVER_ABI,
functionName: "text",
args: [node, "com.github"],
},
{
address: resolverAddress,
abi: RESOLVER_ABI,
functionName: "text",
args: [node, "url"],
},
],
});
const [addr, twitter, github, url] = results.map((r) => r.result);
Registration
Commit-Reveal Process
ENS .eth registration uses a two-step commit-reveal to prevent frontrunning. You must wait at least 60 seconds between commit and register.
import {
createPublicClient,
createWalletClient,
http,
parseAbi,
encodePacked,
keccak256,
parseEther,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { mainnet } from "viem/chains";
const ETH_REGISTRAR_CONTROLLER =
"0x253553366Da8546fC250F225fe3d25d0C782303b" as const;
const CONTROLLER_ABI = parseAbi([
"function rentPrice(string name, uint256 duration) view returns (tuple(uint256 base, uint256 premium))",
"function available(string name) view returns (bool)",
"function makeCommitment(string name, address owner, uint256 duration, bytes32 secret, address resolver, bytes[] data, bool reverseRecord, uint16 ownerControlledFuses) pure returns (bytes32)",
"function commit(bytes32 commitment) external",
"function register(string name, address owner, uint256 duration, bytes32 secret, address resolver, bytes[] data, bool reverseRecord, uint16 ownerControlledFuses) payable",
]);
const PUBLIC_RESOLVER = "0x231b0Ee14048e9dCcD1d247744d114a4EB5E8E63" as const;
const account = privateKeyToAccount(
process.env.PRIVATE_KEY as `0x${string}`
);
const client = createPublicClient({
chain: mainnet,
transport: http(process.env.RPC_URL),
});
const walletClient = createWalletClient({
account,
chain: mainnet,
transport: http(process.env.RPC_URL),
});
async function registerName(label: string, durationSeconds: bigint) {
// 1. Check availability
const isAvailable = await client.readContract({
address: ETH_REGISTRAR_CONTROLLER,
abi: CONTROLLER_ABI,
functionName: "available",
args: [label],
});
if (!isAvailable) throw new Error(`${label}.eth is not available`);
// 2. Get price
const rentPrice = await client.readContract({
address: ETH_REGISTRAR_CONTROLLER,
abi: CONTROLLER_ABI,
functionName: "rentPrice",
args: [label, durationSeconds],
});
// Add 10% buffer for price fluctuation during commit-reveal wait
const totalPrice =
((rentPrice.base + rentPrice.premium) * 110n) / 100n;
// 3. Generate secret (random 32 bytes)
const secret = keccak256(
encodePacked(["address", "uint256"], [account.address, BigInt(Date.now())])
);
// 4. Create commitment
const commitment = await client.readContract({
address: ETH_REGISTRAR_CONTROLLER,
abi: CONTROLLER_ABI,
functionName: "makeCommitment",
args: [
label,
account.address,
durationSeconds,
secret,
PUBLIC_RESOLVER,
[], // data (encoded resolver calls to set records at registration)
true, // reverseRecord (set as primary name)
0, // ownerControlledFuses (0 = no fuses)
],
});
// 5. Submit commitment
const commitHash = await walletClient.writeContract({
address: ETH_REGISTRAR_CONTROLLER,
abi: CONTROLLER_ABI,
functionName: "commit",
args: [commitment],
});
await client.waitForTransactionReceipt({ hash: commitHash });
console.log("Commitment submitted. Waiting 60 seconds...");
// 6. Wait at least 60 seconds (minCommitmentAge)
await new Promise((resolve) => setTimeout(resolve, 65_000));
// 7. Register
const registerHash = await walletClient.writeContract({
address: ETH_REGISTRAR_CONTROLLER,
abi: CONTROLLER_ABI,
functionName: "register",
args: [
label,
account.address,
durationSeconds,
secret,
PUBLIC_RESOLVER,
[],
true,
0,
],
value: totalPrice,
});
const receipt = await client.waitForTransactionReceipt({
hash: registerHash,
});
if (receipt.status !== "success") {
throw new Error("Registration transaction reverted");
}
console.log(`Registered ${label}.eth for ${durationSeconds / 31536000n} year(s)`);
return receipt;
}
// Register for 1 year (365 days in seconds)
await registerName("myname", 31536000n);
Renewal
const CONTROLLER_ABI_RENEW = parseAbi([
"function rentPrice(string name, uint256 duration) view returns (tuple(uint256 base, uint256 premium))",
"function renew(string name, uint256 duration) payable",
]);
async function renewName(label: string, durationSeconds: bigint) {
const rentPrice = await client.readContract({
address: ETH_REGISTRAR_CONTROLLER,
abi: CONTROLLER_ABI_RENEW,
functionName: "rentPrice",
args: [label, durationSeconds],
});
// 5% buffer for price changes
const totalPrice = ((rentPrice.base + rentPrice.premium) * 105n) / 100n;
const hash = await walletClient.writeContract({
address: ETH_REGISTRAR_CONTROLLER,
abi: CONTROLLER_ABI_RENEW,
functionName: "renew",
args: [label, durationSeconds],
value: totalPrice,
});
const receipt = await client.waitForTransactionReceipt({ hash });
if (receipt.status !== "success") {
throw new Error("Renewal transaction reverted");
}
console.log(`Renewed ${label}.eth for ${durationSeconds / 31536000n} year(s)`);
return receipt;
}
Check Price Before Registering
async function getRegistrationCost(
label: string,
durationSeconds: bigint
): Promise<{ base: bigint; premium: bigint; total: bigint }> {
const rentPrice = await client.readContract({
address: ETH_REGISTRAR_CONTROLLER,
abi: CONTROLLER_ABI,
functionName: "rentPrice",
args: [label, durationSeconds],
});
return {
base: rentPrice.base,
premium: rentPrice.premium,
total: rentPrice.base + rentPrice.premium,
};
}
Working with Resolvers
Setting Text Records
import {
createPublicClient,
createWalletClient,
http,
parseAbi,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { mainnet } from "viem/chains";
import { namehash } from "viem/ens";
const PUBLIC_RESOLVER = "0x231b0Ee14048e9dCcD1d247744d114a4EB5E8E63" as const;
const RESOLVER_ABI = parseAbi([
"function setText(bytes32 node, string key, string value) external",
"function setAddr(bytes32 node, address addr) external",
"function setAddr(bytes32 node, uint256 coinType, bytes value) external",
"function setContenthash(bytes32 node, bytes hash) external",
"function multicall(bytes[] data) external returns (bytes[])",
]);
const account = privateKeyToAccount(
process.env.PRIVATE_KEY as `0x${string}`
);
const client = createPublicClient({
chain: mainnet,
transport: http(process.env.RPC_URL),
});
const walletClient = createWalletClient({
account,
chain: mainnet,
transport: http(process.env.RPC_URL),
});
const node = namehash("myname.eth");
// Set a single text record
const hash = await walletClient.writeContract({
address: PUBLIC_RESOLVER,
abi: RESOLVER_ABI,
functionName: "setText",
args: [node, "com.twitter", "myhandle"],
});
await client.waitForTransactionReceipt({ hash });
Batch Update Records with Multicall
Setting multiple records in a single transaction using the resolver's built-in multicall.
import { encodeFunctionData } from "viem";
const node = namehash("myname.eth");
const calls = [
encodeFunctionData({
abi: RESOLVER_ABI,
functionName: "setText",
args: [node, "com.twitter", "myhandle"],
}),
encodeFunctionData({
abi: RESOLVER_ABI,
functionName: "setText",
args: [node, "com.github", "mygithub"],
}),
encodeFunctionData({
abi: RESOLVER_ABI,
functionName: "setText",
args: [node, "url", "https://mysite.com"],
}),
encodeFunctionData({
abi: RESOLVER_ABI,
functionName: "setText",
args: [node, "email", "me@mysite.com"],
}),
encodeFunctionData({
abi: RESOLVER_ABI,
functionName: "setText",
args: [node, "avatar", "https://mysite.com/avatar.png"],
}),
];
const hash = await walletClient.writeContract({
address: PUBLIC_RESOLVER,
abi: RESOLVER_ABI,
functionName: "multicall",
args: [calls],
});
const receipt = await client.waitForTransactionReceipt({ hash });
if (receipt.status !== "success") {
throw new Error("Multicall record update reverted");
}
Setting the Primary Name (Reverse Record)
const REVERSE_REGISTRAR = "0xa58E81fe9b61B5c3fE2AFD33CF304c454AbFc7Cb" as const;
const REVERSE_ABI = parseAbi([
"function setName(string name) external returns (bytes32)",
]);
const hash = await walletClient.writeContract({
address: REVERSE_REGISTRAR,
abi: REVERSE_ABI,
functionName: "setName",
args: ["myname.eth"],
});
await client.waitForTransactionReceipt({ hash });
Subdomains
Creating an Onchain Subdomain
import { parseAbi } from "viem";
import { namehash, labelhash } from "viem/ens";
const ENS_REGISTRY = "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e" as const;
const REGISTRY_ABI = parseAbi([
"function setSubnodeRecord(bytes32 node, bytes32 label, address owner, address resolver, uint64 ttl) external",
"function owner(bytes32 node) view returns (address)",
"function resolver(bytes32 node) view returns (address)",
]);
const parentNode = namehash("myname.eth");
const subLabel = labelhash("sub");
const hash = await walletClient.writeContract({
address: ENS_REGISTRY,
abi: REGISTRY_ABI,
functionName: "setSubnodeRecord",
args: [
parentNode,
subLabel,
account.address, // owner of sub.myname.eth
PUBLIC_RESOLVER, // resolver
0n, // TTL
],
});
await client.waitForTransactionReceipt({ hash });
// sub.myname.eth now exists and points to PUBLIC_RESOLVER
Offchain Subdomains (CCIP-Read / ERC-3668)
Offchain subdomains let you issue unlimited subdomains without gas costs. The resolver responds with an OffchainLookup error that directs the client to a gateway URL. The gateway returns signed data that is verified onchain.
This is how services like cb.id (Coinbase), uni.eth (Uniswap), and lens.xyz work.
For offchain resolution, viem handles CCIP-Read transparently -- no client-side changes needed:
// Resolving an offchain subdomain works identically to onchain names
const address = await client.getEnsAddress({
name: "myuser.cb.id",
});
// viem automatically:
// 1. Calls resolver.resolve(...)
// 2. Catches OffchainLookup revert
// 3. Fetches from the gateway URL
// 4. Calls resolver with the gateway proof
// 5. Returns the verified address
const avatar = await client.getEnsAvatar({
name: "myuser.cb.id",
});
To build your own offchain resolver, implement ERC-3668:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {IExtendedResolver} from "@ensdomains/ens-contracts/contracts/resolvers/profiles/IExtendedResolver.sol";
/// @notice Offchain resolver that delegates lookups to a gateway
/// @dev Implements ERC-3668 (CCIP-Read) and ENSIP-10 (wildcard resolution)
contract OffchainResolver is IExtendedResolver {
string public url;
address public signer;
error OffchainLookup(
address sender,
string[] urls,
bytes callData,
bytes4 callbackFunction,
bytes extraData
);
constructor(string memory _url, address _signer) {
url = _url;
signer = _signer;
}
/// @notice ENSIP-10 wildcard resolve entry point
function resolve(
bytes calldata name,
bytes calldata data
) external view returns (bytes memory) {
string[] memory urls = new string[](1);
urls[0] = url;
revert OffchainLookup(
address(this),
urls,
data,
this.resolveWithProof.selector,
abi.encode(name, data)
);
}
/// @notice Callback that verifies the gateway signature
function resolveWithProof(
bytes calldata response,
bytes calldata extraData
) external view returns (bytes memory) {
// Verify signature from gateway matches expected signer
// Return decoded result
// Implementation depends on your signing scheme
}
}
Contract Addresses
Ethereum Mainnet. Last verified: 2025-03-01.
| Contract | Address | Purpose |
|---|---|---|
| ENS Registry | 0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e | Core registry -- maps names to owners and resolvers |
| Public Resolver | 0x231b0Ee14048e9dCcD1d247744d114a4EB5E8E63 | Default resolver for address, text, contenthash, and ABI records |
| ETH Registrar Controller | 0x253553366Da8546fC250F225fe3d25d0C782303b | Handles .eth name registration and renewal (commit-reveal) |
| Name Wrapper | 0xD4416b13d2b3a9aBae7AcD5D6C2BbDBE25686401 | Wraps names as ERC-1155 tokens with permission fuses |
| Reverse Registrar | 0xa58E81fe9b61B5c3fE2AFD33CF304c454AbFc7Cb | Manages reverse records (address-to-name mapping) |
| Base Registrar (NFT) | 0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85 | ERC-721 NFT for .eth second-level names |
| Universal Resolver | 0xce01f8eee7E30F8E3BfC1C22bCBc01faBc8680E4 | Batch resolution with CCIP-Read support |
Address Constants for TypeScript
const ENS_ADDRESSES = {
registry: "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e",
publicResolver: "0x231b0Ee14048e9dCcD1d247744d114a4EB5E8E63",
ethRegistrarController: "0x253553366Da8546fC250F225fe3d25d0C782303b",
nameWrapper: "0xD4416b13d2b3a9aBae7AcD5D6C2BbDBE25686401",
reverseRegistrar: "0xa58E81fe9b61B5c3fE2AFD33CF304c454AbFc7Cb",
baseRegistrar: "0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85",
universalResolver: "0xce01f8eee7E30F8E3BfC1C22bCBc01faBc8680E4",
} as const satisfies Record<string, `0x${string}`>;
Sepolia Testnet
Last verified: 2025-03-01.
| Contract | Address |
|---|---|
| ENS Registry | 0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e |
| Public Resolver | 0x8FADE66B79cC9f707aB26799354482EB93a5B7dD |
| ETH Registrar Controller | 0xFED6a969AaA60E4961FCD3EBF1A2e8913DeBe6c7 |
| Name Wrapper | 0x0635513f179D50A207757E05759CbD106d7dFcE8 |
| Reverse Registrar | 0xA0a1AbcDAe1a2a4A2EF8e9113Ff0e02DD81DC0C6 |
Error Handling
Common Resolution Errors
async function safeResolve(name: string) {
try {
const address = await client.getEnsAddress({ name });
if (!address) {
console.log(`${name} has no address record set`);
return null;
}
return address;
} catch (error) {
if (error instanceof Error) {
// Name does not exist or is malformed
if (error.message.includes("Could not find resolver")) {
console.log(`${name} is not registered or has no resolver`);
return null;
}
// CCIP-Read gateway failure
if (error.message.includes("OffchainLookup")) {
console.log(`Offchain resolution failed for ${name}`);
return null;
}
}
throw error;
}
}
Common Registration Errors
| Error | Cause | Fix |
|---|---|---|
CommitmentTooNew | Called register() less than 60s after commit() | Wait at least 60 seconds between commit and register |
CommitmentTooOld | Commitment expired (older than 24 hours) | Submit a new commitment |
NameNotAvailable | Name is registered or in grace period | Check available() first |
DurationTooShort | Duration under minimum (28 days) | Use at least 2419200 seconds |
InsufficientValue | Sent less ETH than rentPrice() requires | Add a 5-10% buffer to rentPrice() result |
Unauthorised | Caller is not the name owner | Verify ownership via registry before writing records |
Validating ENS Names
import { normalize } from "viem/ens";
function isValidEnsName(name: string): boolean {
try {
normalize(name);
return true;
} catch {
return false;
}
}
// normalize() throws on invalid names
// Valid: "alice.eth", "sub.alice.eth", "alice.xyz"
// Invalid: names with zero-width characters, confusable Unicode, etc.
Key Constants
| Constant | Value | Notes |
|---|---|---|
| Min commitment age | 60 seconds | Wait between commit and register |
| Max commitment age | 86400 seconds (24h) | Commitment expires after this |
| Min registration duration | 2419200 seconds (28 days) | Shortest allowed registration |
| Grace period | 90 days | After expiry, owner can still renew |
| Premium auction | 21 days | After grace period, decaying price auction |
Namehash of eth | 0x93cdeb708b7545dc668eb9280176169d1c33cfd8ed6f04690a0bcc88a93fc4ae | Used as parent node for .eth names |
References
- ENS Documentation -- official docs covering architecture, resolution, registration, and CCIP-Read
- EIP-137: ENS -- core ENS specification (registry, namehash, resolvers)
- EIP-181: Reverse Resolution -- reverse registrar and addr.reverse namespace
- EIP-2304: Multichain Address Resolution -- SLIP-44 coin type support in resolvers
- ERC-3668: CCIP-Read -- offchain data retrieval standard
- ENSIP-5: Text Records -- standardized text record keys
- ENSIP-10: Wildcard Resolution -- dynamic subdomain resolution
- ENSIP-12: Avatar Text Records -- NFT and IPFS avatar specification
- ENS Deployments -- official contract addresses per network
- viem ENS Actions -- built-in ENS resolution in viem
- @adraffy/ens-normalize -- reference UTS-46 normalization library used by viem
monad
View full →Author
@0xinit
Stars
53
Repository
0xinit/cryptoskills
Monad L1 Development
Chain Configuration
Mainnet
| Property | Value |
|---|---|
| Chain ID | 143 |
| Currency | MON (18 decimals) |
| EVM Version | Pectra fork |
| Block Time | 400ms |
| Finality | 800ms (2 slots) |
| Block Gas Limit | 200M |
| Tx Gas Limit | 30M |
| Gas Throughput | 500M gas/sec |
| Min Base Fee | 100 MON-gwei |
| Node Version | v0.12.7 / MONAD_EIGHT |
RPC Endpoints (Mainnet)
| URL | Provider | Rate Limit | Batch | Notes |
|---|---|---|---|---|
https://rpc.monad.xyz / wss://rpc.monad.xyz | QuickNode | 25 rps | 100 | Default |
https://rpc1.monad.xyz / wss://rpc1.monad.xyz | Alchemy | 15 rps | 100 | No debug/trace |
https://rpc2.monad.xyz / wss://rpc2.monad.xyz | Goldsky Edge | 300/10s | 10 | Historical state |
https://rpc3.monad.xyz / wss://rpc3.monad.xyz | Ankr | 300/10s | 10 | No debug |
https://rpc-mainnet.monadinfra.com / wss://rpc-mainnet.monadinfra.com | MF | 20 rps | 1 | Historical state |
Block Explorers
| Explorer | URL |
|---|---|
| MonadVision | https://monadvision.com |
| Monadscan | https://monadscan.com |
| Socialscan | https://monad.socialscan.io |
| Visualization | https://gmonads.com |
| Traces | Phalcon Explorer, Tenderly |
| UserOps | Jiffyscan |
Testnet
| Property | Value |
|---|---|
| Chain ID | 10143 |
| RPC | https://testnet-rpc.monad.xyz |
| WebSocket | wss://testnet-rpc.monad.xyz |
| Explorer | https://testnet.monadexplorer.com |
| Faucet | https://testnet.monad.xyz |
Key Differences from Ethereum
| Feature | Ethereum | Monad |
|---|---|---|
| Block time | 12s | 400ms |
| Finality | ~12-18 min | 800ms (2 slots) |
| Throughput | ~10 TPS | 10,000+ TPS |
| Gas charging | Gas used | Gas limit |
| Max contract size | 24.5 KB | 128 KB |
| Blob txns (EIP-4844) | Supported | Not supported |
| Global mempool | Yes | No (leader-based forwarding) |
| Account cold access | 2,600 gas | 10,100 gas |
| Storage cold access | 2,100 gas | 8,100 gas |
| Reserve balance | None | ~10 MON per account |
TIMESTAMP granularity | 1 per block | 2-3 blocks share same second |
| Precompile 0x0100 | N/A | EIP-7951 secp256r1 (P256) |
| EIP-7702 min balance | None | 10 MON for delegated EOAs |
| EIP-7702 CREATE/CREATE2 | Allowed | Banned for delegated EOAs |
| Tx types supported | 0,1,2,3,4 | 0,1,2,4 (no type 3) |
Gas Limit Charging Model
Monad charges gas_limit * price_per_gas, NOT gas_used * price_per_gas. This enables asynchronous execution — execution happens after consensus, so gas used isn't known at inclusion time.
gas_paid = gas_limit * price_per_gas
price_per_gas = min(base_price_per_gas + priority_price_per_gas, max_price_per_gas)
Set gas limits explicitly for fixed-cost operations (e.g., 21000 for transfers) to avoid overpaying.
Reserve Balance
Every account maintains a ~10 MON reserve for gas across the next 3 blocks. Transactions that would reduce balance below this threshold are rejected. This prevents DoS during asynchronous execution.
Block Lifecycle & Finality
Proposed → Voted (speculative finality, T+1) → Finalized (T+2) → Verified/state root (T+5)
| Phase | Latency | When to Use |
|---|---|---|
| Voted | 400ms | UI updates, most dApps |
| Finalized | 800ms | Conservative apps |
| Verified | ~2s | Exchanges, bridges, stablecoins |
Quick Start: viem Chain Definition
import { defineChain } from "viem";
export const monad = defineChain({
id: 143,
name: "Monad",
nativeCurrency: { name: "MON", symbol: "MON", decimals: 18 },
rpcUrls: {
default: { http: ["https://rpc.monad.xyz"], webSocket: ["wss://rpc.monad.xyz"] },
},
blockExplorers: {
default: { name: "MonadVision", url: "https://monadvision.com" },
monadscan: { name: "Monadscan", url: "https://monadscan.com" },
},
});
export const monadTestnet = defineChain({
id: 10143,
name: "Monad Testnet",
nativeCurrency: { name: "MON", symbol: "MON", decimals: 18 },
rpcUrls: {
default: { http: ["https://testnet-rpc.monad.xyz"], webSocket: ["wss://testnet-rpc.monad.xyz"] },
},
blockExplorers: {
default: { name: "Monad Explorer", url: "https://testnet.monadexplorer.com" },
},
testnet: true,
});
Quick Start: Foundry Setup
Install Monad Foundry Fork
curl -L https://raw.githubusercontent.com/category-labs/foundry/monad/foundryup/install | bash
foundryup --network monad
Project Init
forge init --template monad-developers/foundry-monad my-project
foundry.toml
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
evm_version = "prague"
[rpc_endpoints]
monad = "https://rpc.monad.xyz"
monad_testnet = "https://testnet-rpc.monad.xyz"
[etherscan]
monad = { key = "${ETHERSCAN_API_KEY}", chain = 143, url = "https://api.etherscan.io/v2/api?chainid=143" }
monad_testnet = { key = "${ETHERSCAN_API_KEY}", chain = 10143, url = "https://api.etherscan.io/v2/api?chainid=10143" }
Quick Start: Hardhat Configuration (v2)
const config: HardhatUserConfig = {
solidity: {
version: "0.8.28",
settings: {
evmVersion: "prague",
metadata: { bytecodeHash: "ipfs" },
},
},
networks: {
monadTestnet: {
url: "https://testnet-rpc.monad.xyz",
chainId: 10143,
accounts: [process.env.PRIVATE_KEY!],
},
monadMainnet: {
url: "https://rpc.monad.xyz",
chainId: 143,
accounts: [process.env.PRIVATE_KEY!],
},
},
etherscan: {
customChains: [{
network: "monadMainnet",
chainId: 143,
urls: {
apiURL: "https://api.etherscan.io/v2/api?chainid=143",
browserURL: "https://monadscan.com",
},
}],
},
sourcify: {
enabled: true,
apiUrl: "https://sourcify-api-monad.blockvision.org",
browserUrl: "https://monadvision.com",
},
};
Deployment
Foundry Deploy (Keystore)
cast wallet import monad-deployer --private-key $(cast wallet new | grep 'Private key:' | awk '{print $3}')
forge create src/MyContract.sol:MyContract \
--account monad-deployer \
--rpc-url https://rpc.monad.xyz \
--broadcast
forge create src/MyToken.sol:MyToken \
--account monad-deployer \
--rpc-url https://rpc.monad.xyz \
--constructor-args "MyToken" "MTK" 18 \
--broadcast
Foundry Deploy (Script)
forge script script/Deploy.s.sol \
--account monad-deployer \
--rpc-url https://rpc.monad.xyz \
--broadcast \
--slow
Hardhat Deploy
npx hardhat ignition deploy ignition/modules/Counter.ts --network monadMainnet
npx hardhat ignition deploy ignition/modules/Counter.ts --network monadMainnet --reset
Verification
MonadVision (Sourcify)
forge verify-contract <address> <ContractName> \
--chain 143 \
--verifier sourcify \
--verifier-url https://sourcify-api-monad.blockvision.org/
Monadscan (Etherscan)
forge verify-contract <address> <ContractName> \
--chain 143 \
--verifier etherscan \
--etherscan-api-key $ETHERSCAN_API_KEY \
--watch
Socialscan
forge verify-contract <address> <ContractName> \
--chain 143 \
--verifier etherscan \
--etherscan-api-key $SOCIALSCAN_API_KEY \
--verifier-url https://api.socialscan.io/monad-mainnet/v1/explorer/command_api/contract \
--watch
Hardhat Verify
npx hardhat verify <address> --network monadMainnet
For testnet verification, replace --chain 143 with --chain 10143 and use testnet RPC/explorer URLs.
Opcode Repricing Summary
Cold state access is ~4x more expensive on Monad than Ethereum. Warm access is identical.
| Access Type | Ethereum | Monad |
|---|---|---|
| Account (cold) | 2,600 | 10,100 |
| Storage slot (cold) | 2,100 | 8,100 |
| Account (warm) | 100 | 100 |
| Storage slot (warm) | 100 | 100 |
Selected precompile repricing:
| Precompile | Ethereum | Monad | Multiplier |
|---|---|---|---|
| ecRecover (0x01) | 3,000 | 6,000 | 2x |
| ecMul (0x07) | 6,000 | 30,000 | 5x |
| ecPairing (0x08) | 45,000 | 225,000 | 5x |
| point evaluation (0x0a) | 50,000 | 200,000 | 4x |
Monad-specific precompile: secp256r1 (P256) at 0x0100 for WebAuthn/passkey signature verification (EIP-7951).
EIP-1559 Parameters
| Parameter | Value |
|---|---|
| Block gas limit | 200M |
| Block gas target | 160M (80% of limit) |
| Per-transaction gas limit | 30M |
| Min base fee | 100 MON-gwei |
| Base fee max step size | 1/28 |
| Base fee decay factor | 0.96 |
The base fee controller increases slower and decreases faster than Ethereum's to prevent blockspace underutilization on a high-throughput chain.
Gas Optimization Tips
- Warm your storage — cold reads are 4x more expensive; use access lists (type 1/2 txns) for known slots
- Set explicit gas limits — you're charged for the limit, not usage
- Batch operations — high throughput means batching is less critical, but still saves gas limit overhead
- Avoid unnecessary cold precompile calls — ecPairing is 5x more expensive than Ethereum
- Design for parallel execution — per-user mappings over global counters where possible
- No blob transactions — use calldata for data availability
Parallel Execution
Monad executes transactions concurrently with optimistic conflict detection. No Solidity changes needed.
- Multiple virtual executors process transactions simultaneously
- Each generates "pending results" (inputs: SLOADs, outputs: SSTOREs)
- Serial commitment validates each result's inputs remain valid
- Conflict detected -> re-execute the affected transaction
- Results committed in original transaction order
Every transaction executes at most twice. Most transactions don't conflict, achieving near-linear speedup.
Parallel-Friendly Contract Design
| Pattern | Parallelizes Well | Why |
|---|---|---|
| Per-user mappings | Yes | Independent state per user |
| ERC-20 transfers between different pairs | Yes | Different storage slots |
| Global counter increment | No | All txns write same slot |
| AMM swaps on same pool | No | Same reserves storage |
| Independent NFT mints (incremental ID) | Partially | tokenId counter serializes |
Staking Precompile
Address: 0x0000000000000000000000000000000000001000
Only standard CALL is allowed. STATICCALL, DELEGATECALL, and CALLCODE are not permitted.
Core Functions
| Function | Selector | Gas Cost |
|---|---|---|
delegate(uint64) | 0x84994fec | 260,850 |
undelegate(uint64,uint256,uint8) | 0x5cf41514 | 147,750 |
compound(uint64) | 0xb34fea67 | 285,050 |
claimRewards(uint64) | 0xa76e2ca5 | 155,375 |
withdraw(uint64,uint8) | 0xaed2ee73 | 68,675 |
Delegate (Solidity)
address constant STAKING = 0x0000000000000000000000000000000000001000;
function delegateToValidator(uint64 validatorId) external payable {
(bool success,) = STAKING.call{value: msg.value}(
abi.encodeWithSelector(0x84994fec, validatorId)
);
require(success, "Delegation failed");
}
Delegate (viem)
import { encodeFunctionData } from "viem";
const STAKING_ADDRESS = "0x0000000000000000000000000000000000001000";
const hash = await walletClient.sendTransaction({
to: STAKING_ADDRESS,
value: parseEther("100"),
data: encodeFunctionData({
abi: [{ name: "delegate", type: "function", inputs: [{ name: "validatorId", type: "uint64" }], outputs: [] }],
functionName: "delegate",
args: [1n],
}),
});
EIP-7702 on Monad
Allows EOAs to gain smart contract capabilities via code delegation.
| Restriction | Detail |
|---|---|
| Minimum balance | Delegated EOAs cannot drop below 10 MON |
| CREATE/CREATE2 | Banned when delegated EOAs execute as smart contracts |
| Clearing delegation | Send type 0x04 pointing to address(0) |
import { walletClient } from "./client";
const authorization = await walletClient.signAuthorization({
account,
contractAddress: "0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2",
});
const hash = await walletClient.sendTransaction({
authorizationList: [authorization],
data: "0xdeadbeef",
to: walletClient.account.address,
});
WebSocket Subscriptions
Standard eth_subscribe plus Monad-specific extensions:
newHeads — standard new block headers
logs — standard log filtering
monadNewHeads — Monad-specific block headers with extra fields
monadLogs — Monad-specific log events
Execution Events (Advanced)
For ultra-low-latency data consumption, Monad exposes execution events via shared-memory ring buffers. Consumer runs on same host as node. ~1 microsecond latency. Supported in C, C++, and Rust only.
Use execution events when JSON-RPC can't keep up with 10,000 TPS throughput. For most dApps, standard WebSocket subscriptions are sufficient.
Canonical Contracts
| Contract | Address |
|---|---|
| Wrapped MON (WMON) | 0x3bd359C1119dA7Da1D913D1C4D2B7c461115433A |
| Staking Precompile | 0x0000000000000000000000000000000000001000 |
| Multicall3 | 0xcA11bde05977b3631167028862bE2a173976CA11 |
| USDC | 0x754704Bc059F8C67012fEd69BC8A327a5aafb603 |
| USDT0 | 0xe7cd86e13AC4309349F30B3435a9d337750fC82D |
| WETH | 0xEE8c0E9f1BFFb4Eb878d8f15f368A02a35481242 |
| WBTC | 0x0555E30da8f98308EdB960aa94C0Db47230d2B9c |
| ERC-4337 EntryPoint v0.7 | 0x0000000071727De22E5E9d8BAf0edAc6f37da032 |
| Safe | 0x69f4D1788e39c87893C980c06EdF4b7f686e2938 |
Supported Transaction Types
| Type | Name | Supported | Notes |
|---|---|---|---|
| 0 | Legacy | Yes | Pre-EIP-155 allowed but discouraged |
| 1 | EIP-2930 (access list) | Yes | |
| 2 | EIP-1559 (dynamic fee) | Yes | Recommended |
| 3 | EIP-4844 (blob) | No | Not supported on Monad |
| 4 | EIP-7702 (delegation) | Yes | With Monad-specific restrictions |
Smart Contract Tips
- Gas optimization still matters — even with cheap gas, optimize for users
- Same security model — all Solidity best practices (CEI, reentrancy guards) apply
- Parallel-friendly design — contracts with per-user mappings parallelize better than global counters
- 128 KB contract limit — larger contracts are possible but still optimize for gas
- No code changes needed for parallelism — it's at the runtime level
block.timestamp— 2-3 blocks may share the same second; don't rely on sub-second granularity- No blob transactions — EIP-4844 type 3 txns are not supported
Required Tooling Versions
| Tool | Minimum Version |
|---|---|
| Foundry | Monad fork (foundryup --network monad) |
| viem | 2.40.0+ |
| alloy-chains | 0.2.20+ |
| Hardhat Solidity | evmVersion: "prague" |
Pre-Deployment Checklist
- Using Monad Foundry fork or Hardhat with
evmVersion: "prague" - Correct chain ID (143 mainnet / 10143 testnet)
- Account funded with MON (remember ~10 MON reserve)
- Gas limit set explicitly for predictable cost (gas limit is charged, not gas used)
- Private key in env var, not hardcoded
- Contract size under 128 KB
- No EIP-4844 blob transactions (type 3 not supported)
- Verified on at least one explorer after deploy
Additional Reference
| File | Contents |
|---|---|
docs/architecture.md | MonadBFT consensus, parallel execution, deferred execution, MonadDb, JIT, RaptorCast |
docs/deployment.md | Foundry + Hardhat deploy/verify step-by-step guides |
docs/gas-and-opcodes.md | Gas pricing model, opcode repricing tables, precompile costs |
docs/staking.md | Staking precompile ABI, functions, events, epoch mechanics |
docs/ecosystem.md | Token addresses, bridges, oracles, indexers, canonical contracts |
docs/troubleshooting.md | Common issues and fixes for Monad development |
resources/contract-addresses.md | Key Monad contract addresses |
templates/deploy-monad.sh | Shell script for deploying to Monad |