Comparing aave with monad
aave
View full →Author
@0xinit
Stars
53
Repository
0xinit/cryptoskills
Aave V3
Aave V3 is the dominant on-chain lending protocol. Users supply assets to earn yield and borrow against collateral. The protocol runs on Ethereum, Arbitrum, Optimism, Base, Polygon, and other EVM chains with identical interfaces. All interaction goes through the IPool contract.
What You Probably Got Wrong
LLMs confuse V2 and V3 constantly. V3 has different interfaces, different addresses, and different behavior. These corrections are non-negotiable.
- V3 is not V2 — different interfaces everywhere — V2 uses
LendingPoolwithdeposit(). V3 usesPoolwithsupply(). The function signatures, events, and return types differ. If you seeILendingPoolordeposit(), you are writing V2 code. Stop. - aTokens rebase — balance changes every block —
aToken.balanceOf(user)increases each block as interest accrues. This is not a transfer. Do not try to track balances with Transfer events alone. UsebalanceOf()at read time orscaledBalanceOf()for the underlying non-rebasing amount. - Stable rate borrowing is deprecated on most markets — Aave governance disabled stable rate borrows on Ethereum mainnet and most L2 deployments. Use
VARIABLE_RATE = 2for theinterestRateModeparameter. PassingSTABLE_RATE = 1will revert on markets where it is disabled. - Health factor is 18-decimal fixed point, not a percentage —
getUserAccountData()returnshealthFactoras auint256with 18 decimals. A health factor of1e18means liquidation threshold. Below1e18= liquidatable. Do not divide by 100. - Flash loan fee is 0.05% on V3, not 0.09% — V3 reduced the default flash loan premium from 0.09% (V2) to 0.05%. The exact fee is configurable per market via governance. Check
FLASHLOAN_PREMIUM_TOTALon the Pool contract. - E-Mode changes collateral/borrow parameters — Enabling E-Mode (Efficiency Mode) overrides LTV, liquidation threshold, and liquidation bonus for assets in the same category (e.g., stablecoins). It does NOT change the underlying asset. Forgetting this causes incorrect health factor calculations.
- Supply caps exist in V3 — V3 introduced per-asset supply and borrow caps.
supply()will revert if the cap is reached. Always checkgetReserveData()forsupplyCapbefore large deposits. - V3 addresses are different per chain AND per market — Ethereum has a "Main" market and a "Lido" market with completely different Pool addresses. Always verify you are targeting the correct market.
Quick Start
Installation
npm install @aave/aave-v3-core viem
For TypeScript projects, the @aave/aave-v3-core package provides Solidity interfaces. For frontend/backend interaction, use viem directly with the ABIs below.
Minimal ABI Fragments
const poolAbi = [
{
name: "supply",
type: "function",
stateMutability: "nonpayable",
inputs: [
{ name: "asset", type: "address" },
{ name: "amount", type: "uint256" },
{ name: "onBehalfOf", type: "address" },
{ name: "referralCode", type: "uint16" },
],
outputs: [],
},
{
name: "borrow",
type: "function",
stateMutability: "nonpayable",
inputs: [
{ name: "asset", type: "address" },
{ name: "amount", type: "uint256" },
{ name: "interestRateMode", type: "uint256" },
{ name: "referralCode", type: "uint16" },
{ name: "onBehalfOf", type: "address" },
],
outputs: [],
},
{
name: "repay",
type: "function",
stateMutability: "nonpayable",
inputs: [
{ name: "asset", type: "address" },
{ name: "amount", type: "uint256" },
{ name: "interestRateMode", type: "uint256" },
{ name: "onBehalfOf", type: "address" },
],
outputs: [{ name: "", type: "uint256" }],
},
{
name: "withdraw",
type: "function",
stateMutability: "nonpayable",
inputs: [
{ name: "asset", type: "address" },
{ name: "amount", type: "uint256" },
{ name: "to", type: "address" },
],
outputs: [{ name: "", type: "uint256" }],
},
{
name: "getUserAccountData",
type: "function",
stateMutability: "view",
inputs: [{ name: "user", type: "address" }],
outputs: [
{ name: "totalCollateralBase", type: "uint256" },
{ name: "totalDebtBase", type: "uint256" },
{ name: "availableBorrowsBase", type: "uint256" },
{ name: "currentLiquidationThreshold", type: "uint256" },
{ name: "ltv", type: "uint256" },
{ name: "healthFactor", type: "uint256" },
],
},
{
name: "flashLoanSimple",
type: "function",
stateMutability: "nonpayable",
inputs: [
{ name: "receiverAddress", type: "address" },
{ name: "asset", type: "address" },
{ name: "amount", type: "uint256" },
{ name: "params", type: "bytes" },
{ name: "referralCode", type: "uint16" },
],
outputs: [],
},
{
name: "setUserEMode",
type: "function",
stateMutability: "nonpayable",
inputs: [{ name: "categoryId", type: "uint8" }],
outputs: [],
},
{
name: "getReserveData",
type: "function",
stateMutability: "view",
inputs: [{ name: "asset", type: "address" }],
outputs: [
{
name: "",
type: "tuple",
components: [
{ name: "configuration", type: "uint256" },
{ name: "liquidityIndex", type: "uint128" },
{ name: "currentLiquidityRate", type: "uint128" },
{ name: "variableBorrowIndex", type: "uint128" },
{ name: "currentVariableBorrowRate", type: "uint128" },
{ name: "currentStableBorrowRate", type: "uint128" },
{ name: "lastUpdateTimestamp", type: "uint40" },
{ name: "id", type: "uint16" },
{ name: "aTokenAddress", type: "address" },
{ name: "stableDebtTokenAddress", type: "address" },
{ name: "variableDebtTokenAddress", type: "address" },
{ name: "interestRateStrategyAddress", type: "address" },
{ name: "accruedToTreasury", type: "uint128" },
{ name: "unbacked", type: "uint128" },
{ name: "isolationModeTotalDebt", type: "uint128" },
],
},
],
},
] as const;
const erc20Abi = [
{
name: "approve",
type: "function",
stateMutability: "nonpayable",
inputs: [
{ name: "spender", type: "address" },
{ name: "amount", type: "uint256" },
],
outputs: [{ name: "", type: "bool" }],
},
{
name: "balanceOf",
type: "function",
stateMutability: "view",
inputs: [{ name: "account", type: "address" }],
outputs: [{ name: "", type: "uint256" }],
},
] as const;
Basic Supply (TypeScript)
import { createPublicClient, createWalletClient, http, parseUnits } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { mainnet } from "viem/chains";
const POOL = "0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2" as const;
const USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" as const;
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
const publicClient = createPublicClient({
chain: mainnet,
transport: http(process.env.RPC_URL),
});
const walletClient = createWalletClient({
account,
chain: mainnet,
transport: http(process.env.RPC_URL),
});
const amount = parseUnits("1000", 6); // 1000 USDC
// Approve Pool to spend USDC
const approveHash = await walletClient.writeContract({
address: USDC,
abi: erc20Abi,
functionName: "approve",
args: [POOL, amount],
});
await publicClient.waitForTransactionReceipt({ hash: approveHash });
// Supply USDC to Aave
const supplyHash = await walletClient.writeContract({
address: POOL,
abi: poolAbi,
functionName: "supply",
args: [USDC, amount, account.address, 0],
});
const receipt = await publicClient.waitForTransactionReceipt({ hash: supplyHash });
if (receipt.status !== "success") {
throw new Error("Supply transaction reverted");
}
Core Operations
Supply (Solidity)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {IPool} from "@aave/aave-v3-core/contracts/interfaces/IPool.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract AaveSupplier {
IPool public immutable pool;
constructor(address _pool) {
pool = IPool(_pool);
}
/// @notice Supply asset to Aave V3 on behalf of msg.sender
/// @param asset ERC20 token to supply
/// @param amount Amount in token's native decimals
function supplyToAave(address asset, uint256 amount) external {
IERC20(asset).transferFrom(msg.sender, address(this), amount);
IERC20(asset).approve(address(pool), amount);
pool.supply(asset, amount, msg.sender, 0);
}
}
Borrow (TypeScript)
// Variable rate = 2. Stable rate (1) is deprecated on most markets.
const VARIABLE_RATE = 2n;
const borrowHash = await walletClient.writeContract({
address: POOL,
abi: poolAbi,
functionName: "borrow",
args: [USDC, parseUnits("500", 6), VARIABLE_RATE, 0, account.address],
});
const borrowReceipt = await publicClient.waitForTransactionReceipt({ hash: borrowHash });
if (borrowReceipt.status !== "success") {
throw new Error("Borrow transaction reverted");
}
Repay (TypeScript)
const repayAmount = parseUnits("500", 6);
// Approve Pool to pull repayment
const repayApproveHash = await walletClient.writeContract({
address: USDC,
abi: erc20Abi,
functionName: "approve",
args: [POOL, repayAmount],
});
await publicClient.waitForTransactionReceipt({ hash: repayApproveHash });
// type(uint256).max to repay entire debt
const repayHash = await walletClient.writeContract({
address: POOL,
abi: poolAbi,
functionName: "repay",
args: [USDC, repayAmount, 2n, account.address],
});
const repayReceipt = await publicClient.waitForTransactionReceipt({ hash: repayHash });
if (repayReceipt.status !== "success") {
throw new Error("Repay transaction reverted");
}
Withdraw (TypeScript)
// type(uint256).max withdraws entire balance
const maxUint256 = 2n ** 256n - 1n;
const withdrawHash = await walletClient.writeContract({
address: POOL,
abi: poolAbi,
functionName: "withdraw",
args: [USDC, maxUint256, account.address],
});
const withdrawReceipt = await publicClient.waitForTransactionReceipt({
hash: withdrawHash,
});
if (withdrawReceipt.status !== "success") {
throw new Error("Withdraw transaction reverted");
}
Repay and Withdraw (Solidity)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {IPool} from "@aave/aave-v3-core/contracts/interfaces/IPool.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract AavePositionManager {
IPool public immutable pool;
constructor(address _pool) {
pool = IPool(_pool);
}
/// @notice Repay variable-rate debt on behalf of msg.sender
function repayDebt(address asset, uint256 amount) external {
IERC20(asset).transferFrom(msg.sender, address(this), amount);
IERC20(asset).approve(address(pool), amount);
// interestRateMode 2 = variable rate
pool.repay(asset, amount, 2, msg.sender);
}
/// @notice Withdraw supplied asset back to msg.sender
/// @param amount Use type(uint256).max to withdraw entire balance
function withdrawFromAave(address asset, uint256 amount) external {
pool.withdraw(asset, amount, msg.sender);
}
}
Flash Loans
V3 provides flashLoanSimple for single-asset flash loans (simpler interface, lower gas) and flashLoan for multi-asset. The fee is 0.05% by default.
Flash Loan Receiver (Solidity)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {IPool} from "@aave/aave-v3-core/contracts/interfaces/IPool.sol";
import {IFlashLoanSimpleReceiver} from
"@aave/aave-v3-core/contracts/flashloan/base/FlashLoanSimpleReceiver.sol";
import {IPoolAddressesProvider} from
"@aave/aave-v3-core/contracts/interfaces/IPoolAddressesProvider.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract SimpleFlashLoan is IFlashLoanSimpleReceiver {
IPoolAddressesProvider public immutable override ADDRESSES_PROVIDER;
IPool public immutable override POOL;
constructor(address provider) {
ADDRESSES_PROVIDER = IPoolAddressesProvider(provider);
POOL = IPool(IPoolAddressesProvider(provider).getPool());
}
/// @notice Called by Aave Pool after flash loan funds are transferred
/// @dev Must approve Pool to pull back (amount + premium) before returning
function executeOperation(
address asset,
uint256 amount,
uint256 premium,
address initiator,
bytes calldata /* params */
) external override returns (bool) {
if (msg.sender != address(POOL)) revert("Caller not Pool");
if (initiator != address(this)) revert("Initiator not this contract");
// --- Custom logic here ---
// You have `amount` of `asset` available in this contract.
// Do arbitrage, liquidation, collateral swap, etc.
// Repay flash loan: approve Pool to pull amount + fee
uint256 amountOwed = amount + premium;
IERC20(asset).approve(address(POOL), amountOwed);
return true;
}
/// @notice Trigger a flash loan
/// @param asset Token to borrow
/// @param amount Amount to flash borrow
function requestFlashLoan(address asset, uint256 amount) external {
POOL.flashLoanSimple(address(this), asset, amount, "", 0);
}
}
Trigger Flash Loan (TypeScript)
const flashLoanContractAddress = "0x...YOUR_DEPLOYED_CONTRACT..." as `0x${string}`;
const flashLoanAbi = [
{
name: "requestFlashLoan",
type: "function",
stateMutability: "nonpayable",
inputs: [
{ name: "asset", type: "address" },
{ name: "amount", type: "uint256" },
],
outputs: [],
},
] as const;
// Flash borrow 1M USDC
const txHash = await walletClient.writeContract({
address: flashLoanContractAddress,
abi: flashLoanAbi,
functionName: "requestFlashLoan",
args: [USDC, parseUnits("1000000", 6)],
});
const flashReceipt = await publicClient.waitForTransactionReceipt({ hash: txHash });
if (flashReceipt.status !== "success") {
throw new Error("Flash loan reverted");
}
Reading Protocol State
Get User Account Data
const [
totalCollateralBase,
totalDebtBase,
availableBorrowsBase,
currentLiquidationThreshold,
ltv,
healthFactor,
] = await publicClient.readContract({
address: POOL,
abi: poolAbi,
functionName: "getUserAccountData",
args: [account.address],
});
// All "Base" values are in USD with 8 decimals (Aave oracle base currency)
const collateralUsd = Number(totalCollateralBase) / 1e8;
const debtUsd = Number(totalDebtBase) / 1e8;
// healthFactor has 18 decimals. Below 1e18 = liquidatable.
const hf = Number(healthFactor) / 1e18;
console.log(`Health Factor: ${hf}`);
console.log(`Collateral: $${collateralUsd}, Debt: $${debtUsd}`);
Get Reserve Data
const reserveData = await publicClient.readContract({
address: POOL,
abi: poolAbi,
functionName: "getReserveData",
args: [USDC],
});
// Supply APY: currentLiquidityRate is a ray (27 decimals)
const supplyRateRay = reserveData.currentLiquidityRate;
const supplyAPY = Number(supplyRateRay) / 1e27;
console.log(`USDC Supply APY: ${(supplyAPY * 100).toFixed(2)}%`);
// aToken address for this reserve
const aTokenAddress = reserveData.aTokenAddress;
Track aToken Balance
// aToken balance includes accrued interest (rebases every block)
const aUsdcBalance = await publicClient.readContract({
address: reserveData.aTokenAddress,
abi: erc20Abi,
functionName: "balanceOf",
args: [account.address],
});
// Balance in human-readable format (USDC has 6 decimals)
const balanceFormatted = Number(aUsdcBalance) / 1e6;
console.log(`aUSDC balance: ${balanceFormatted}`);
Contract Addresses
Last verified: 2025-05-01
All Aave V3 deployments share the same interface. Addresses sourced from @bgd-labs/aave-address-book and official Aave governance.
Pool (main entry point for all operations)
| Chain | Address |
|---|---|
| Ethereum | 0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2 |
| Arbitrum | 0x794a61358D6845594F94dc1DB02A252b5b4814aD |
| Optimism | 0x794a61358D6845594F94dc1DB02A252b5b4814aD |
| Polygon | 0x794a61358D6845594F94dc1DB02A252b5b4814aD |
| Base | 0xA238Dd80C259a72e81d7e4664a9801593F98d1c5 |
PoolAddressesProvider
| Chain | Address |
|---|---|
| Ethereum | 0x2f39d218133AFaB8F2B819B1066c7E434Ad94E9e |
| Arbitrum | 0xa97684ead0e402dC232d5A977953DF7ECBaB3CDb |
| Optimism | 0xa97684ead0e402dC232d5A977953DF7ECBaB3CDb |
| Polygon | 0xa97684ead0e402dC232d5A977953DF7ECBaB3CDb |
| Base | 0xe20fCBdBfFC4Dd138cE8b2E6FBb6CB49777ad64D |
Aave Oracle
| Chain | Address |
|---|---|
| Ethereum | 0x54586bE62E3c3580375aE3723C145253060Ca0C2 |
| Arbitrum | 0xb56c2F0B653B2e0b10C9b928C8580Ac5Df02C7C7 |
| Optimism | 0xD81eb3728a631871a7eBBaD631b5f424909f0c77 |
| Polygon | 0xb023e699F5a33916Ea823A16485e259257cA8Bd1 |
| Base | 0x2Cc0Fc26eD4563A5ce5e8bdcfe1A2878676Ae156 |
Common Token Addresses (Ethereum Mainnet)
| Token | Address |
|---|---|
| WETH | 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 |
| USDC | 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 |
| USDT | 0xdAC17F958D2ee523a2206206994597C13D831ec7 |
| DAI | 0x6B175474E89094C44Da98b954EedeAC495271d0F |
| WBTC | 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599 |
Verify any address before mainnet use:
cast code <address> --rpc-url $RPC_URL
E-Mode (Efficiency Mode)
E-Mode lets users achieve higher capital efficiency when borrowing and supplying correlated assets (e.g., stablecoins against stablecoins, ETH against stETH).
How It Works
Each E-Mode category defines:
- Higher LTV (e.g., 97% for stablecoins vs 75% default)
- Higher liquidation threshold (e.g., 97.5%)
- Lower liquidation bonus (e.g., 1% vs 5%)
- Optional oracle override for the category
Common E-Mode Categories
| ID | Label | Typical LTV | Use Case |
|---|---|---|---|
| 0 | None (default) | Varies per asset | General lending |
| 1 | Stablecoins | 97% | Borrow USDT against USDC |
| 2 | ETH correlated | 93% | Borrow WETH against wstETH |
Category IDs and parameters vary by chain and market. Query on-chain.
Enable E-Mode (TypeScript)
// Enable stablecoin E-Mode (category 1)
const emodeHash = await walletClient.writeContract({
address: POOL,
abi: poolAbi,
functionName: "setUserEMode",
args: [1],
});
await publicClient.waitForTransactionReceipt({ hash: emodeHash });
Enable E-Mode (Solidity)
// Enable E-Mode before supplying/borrowing for higher LTV
pool.setUserEMode(1); // 1 = stablecoins category
// To disable, set back to 0
// Reverts if current position would be undercollateralized without E-Mode
pool.setUserEMode(0);
E-Mode Constraint
You can only borrow assets that belong to the active E-Mode category. Supplying is unrestricted. Setting E-Mode to 0 reverts if your position would become unhealthy at default LTV/threshold.
Error Handling
| Error Code | Name | Cause | Fix |
|---|---|---|---|
| 1 | CALLER_NOT_POOL_ADMIN | Non-admin calling admin function | Use correct admin account |
| 26 | COLLATERAL_CANNOT_COVER_NEW_BORROW | Insufficient collateral for borrow | Supply more collateral or borrow less |
| 27 | COLLATERAL_SAME_AS_BORROWING_CURRENCY | Cannot use same asset as collateral and borrow in isolation mode | Use a different collateral |
| 28 | AMOUNT_BIGGER_THAN_MAX_LOAN_SIZE_STABLE | Stable rate borrow exceeds limit | Use variable rate (interestRateMode = 2) |
| 29 | NO_DEBT_OF_SELECTED_TYPE | Repaying debt type that does not exist | Check interestRateMode matches your debt |
| 30 | NO_EXPLICIT_AMOUNT_TO_REPAY_ON_BEHALF | Repaying on behalf with type(uint256).max | Specify exact repay amount when paying for another user |
| 35 | HEALTH_FACTOR_LOWER_THAN_LIQUIDATION_THRESHOLD | Action would make position liquidatable | Reduce borrow amount or add collateral |
| 36 | INCONSISTENT_EMODE_CATEGORY | Borrowing asset outside active E-Mode category | Switch E-Mode or borrow a compatible asset |
| 50 | SUPPLY_CAP_EXCEEDED | Asset supply cap reached | Wait for withdrawals or use a different market |
| 51 | BORROW_CAP_EXCEEDED | Asset borrow cap reached | Wait for repayments or use a different market |
Full error code list:
contracts/protocol/libraries/helpers/Errors.solin aave-v3-core
Handling Reverts in TypeScript
import { BaseError, ContractFunctionRevertedError } from "viem";
try {
await publicClient.simulateContract({
address: POOL,
abi: poolAbi,
functionName: "borrow",
args: [USDC, parseUnits("500", 6), 2n, 0, account.address],
account: account.address,
});
} catch (err) {
if (err instanceof BaseError) {
const revertError = err.walk(
(e) => e instanceof ContractFunctionRevertedError
);
if (revertError instanceof ContractFunctionRevertedError) {
const errorName = revertError.data?.errorName;
console.error(`Aave revert: ${errorName}`);
}
}
}
Security
Health Factor Monitoring
A health factor below 1.0 means the position is liquidatable. Third-party liquidators actively monitor the mempool. Always maintain a buffer.
async function checkHealthFactor(
userAddress: `0x${string}`
): Promise<{ safe: boolean; healthFactor: number }> {
const [, , , , , healthFactor] = await publicClient.readContract({
address: POOL,
abi: poolAbi,
functionName: "getUserAccountData",
args: [userAddress],
});
const hf = Number(healthFactor) / 1e18;
// 1.5 is a conservative safety buffer
return { safe: hf > 1.5, healthFactor: hf };
}
Liquidation Risk Factors
- Oracle price movement — If collateral price drops or debt price increases, health factor drops. Aave uses Chainlink oracles; check feed freshness.
- Accruing interest — Variable borrow rates compound. A 50% APY borrow accumulates debt faster than most users expect.
- E-Mode exit — Disabling E-Mode instantly applies lower LTV/thresholds. A safe E-Mode position may become liquidatable at default parameters.
- Supply cap filling — If you need to add emergency collateral and the supply cap is full, you cannot. Diversify collateral types.
Best Practices
- Simulate before executing — Always call
simulateContractbeforewriteContractto catch reverts without spending gas. - Check
receipt.status— A confirmed transaction can still revert. Always verifyreceipt.status === "success". - Monitor health factor off-chain — Set up alerts when HF drops below 2.0. Automate repayment or collateral addition below 1.5.
- Never hardcode gas limits for Aave calls — Pool operations have variable gas costs depending on reserves touched, E-Mode state, and isolation mode. Let the node estimate.
- Approve exact amounts — Avoid
type(uint256).maxapprovals in production. Approve only what is needed per transaction.
References
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 |