Comparing aave with monad

Author

@0xinit

Stars

53

Repository

0xinit/cryptoskills

skills/aave/SKILL.md

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 LendingPool with deposit(). V3 uses Pool with supply(). The function signatures, events, and return types differ. If you see ILendingPool or deposit(), you are writing V2 code. Stop.
  • aTokens rebase — balance changes every blockaToken.balanceOf(user) increases each block as interest accrues. This is not a transfer. Do not try to track balances with Transfer events alone. Use balanceOf() at read time or scaledBalanceOf() 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 = 2 for the interestRateMode parameter. Passing STABLE_RATE = 1 will revert on markets where it is disabled.
  • Health factor is 18-decimal fixed point, not a percentagegetUserAccountData() returns healthFactor as a uint256 with 18 decimals. A health factor of 1e18 means liquidation threshold. Below 1e18 = 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_TOTAL on 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 check getReserveData() for supplyCap before 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)

ChainAddress
Ethereum0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2
Arbitrum0x794a61358D6845594F94dc1DB02A252b5b4814aD
Optimism0x794a61358D6845594F94dc1DB02A252b5b4814aD
Polygon0x794a61358D6845594F94dc1DB02A252b5b4814aD
Base0xA238Dd80C259a72e81d7e4664a9801593F98d1c5

PoolAddressesProvider

ChainAddress
Ethereum0x2f39d218133AFaB8F2B819B1066c7E434Ad94E9e
Arbitrum0xa97684ead0e402dC232d5A977953DF7ECBaB3CDb
Optimism0xa97684ead0e402dC232d5A977953DF7ECBaB3CDb
Polygon0xa97684ead0e402dC232d5A977953DF7ECBaB3CDb
Base0xe20fCBdBfFC4Dd138cE8b2E6FBb6CB49777ad64D

Aave Oracle

ChainAddress
Ethereum0x54586bE62E3c3580375aE3723C145253060Ca0C2
Arbitrum0xb56c2F0B653B2e0b10C9b928C8580Ac5Df02C7C7
Optimism0xD81eb3728a631871a7eBBaD631b5f424909f0c77
Polygon0xb023e699F5a33916Ea823A16485e259257cA8Bd1
Base0x2Cc0Fc26eD4563A5ce5e8bdcfe1A2878676Ae156

Common Token Addresses (Ethereum Mainnet)

TokenAddress
WETH0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
USDC0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
USDT0xdAC17F958D2ee523a2206206994597C13D831ec7
DAI0x6B175474E89094C44Da98b954EedeAC495271d0F
WBTC0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599

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

IDLabelTypical LTVUse Case
0None (default)Varies per assetGeneral lending
1Stablecoins97%Borrow USDT against USDC
2ETH correlated93%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 CodeNameCauseFix
1CALLER_NOT_POOL_ADMINNon-admin calling admin functionUse correct admin account
26COLLATERAL_CANNOT_COVER_NEW_BORROWInsufficient collateral for borrowSupply more collateral or borrow less
27COLLATERAL_SAME_AS_BORROWING_CURRENCYCannot use same asset as collateral and borrow in isolation modeUse a different collateral
28AMOUNT_BIGGER_THAN_MAX_LOAN_SIZE_STABLEStable rate borrow exceeds limitUse variable rate (interestRateMode = 2)
29NO_DEBT_OF_SELECTED_TYPERepaying debt type that does not existCheck interestRateMode matches your debt
30NO_EXPLICIT_AMOUNT_TO_REPAY_ON_BEHALFRepaying on behalf with type(uint256).maxSpecify exact repay amount when paying for another user
35HEALTH_FACTOR_LOWER_THAN_LIQUIDATION_THRESHOLDAction would make position liquidatableReduce borrow amount or add collateral
36INCONSISTENT_EMODE_CATEGORYBorrowing asset outside active E-Mode categorySwitch E-Mode or borrow a compatible asset
50SUPPLY_CAP_EXCEEDEDAsset supply cap reachedWait for withdrawals or use a different market
51BORROW_CAP_EXCEEDEDAsset borrow cap reachedWait for repayments or use a different market

Full error code list: contracts/protocol/libraries/helpers/Errors.sol in 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

  1. Simulate before executing — Always call simulateContract before writeContract to catch reverts without spending gas.
  2. Check receipt.status — A confirmed transaction can still revert. Always verify receipt.status === "success".
  3. Monitor health factor off-chain — Set up alerts when HF drops below 2.0. Automate repayment or collateral addition below 1.5.
  4. 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.
  5. Approve exact amounts — Avoid type(uint256).max approvals in production. Approve only what is needed per transaction.

References

Author

@0xinit

Stars

53

Repository

0xinit/cryptoskills

skills/monad/SKILL.md

Monad L1 Development

Chain Configuration

Mainnet

PropertyValue
Chain ID143
CurrencyMON (18 decimals)
EVM VersionPectra fork
Block Time400ms
Finality800ms (2 slots)
Block Gas Limit200M
Tx Gas Limit30M
Gas Throughput500M gas/sec
Min Base Fee100 MON-gwei
Node Versionv0.12.7 / MONAD_EIGHT

RPC Endpoints (Mainnet)

URLProviderRate LimitBatchNotes
https://rpc.monad.xyz / wss://rpc.monad.xyzQuickNode25 rps100Default
https://rpc1.monad.xyz / wss://rpc1.monad.xyzAlchemy15 rps100No debug/trace
https://rpc2.monad.xyz / wss://rpc2.monad.xyzGoldsky Edge300/10s10Historical state
https://rpc3.monad.xyz / wss://rpc3.monad.xyzAnkr300/10s10No debug
https://rpc-mainnet.monadinfra.com / wss://rpc-mainnet.monadinfra.comMF20 rps1Historical state

Block Explorers

ExplorerURL
MonadVisionhttps://monadvision.com
Monadscanhttps://monadscan.com
Socialscanhttps://monad.socialscan.io
Visualizationhttps://gmonads.com
TracesPhalcon Explorer, Tenderly
UserOpsJiffyscan

Testnet

PropertyValue
Chain ID10143
RPChttps://testnet-rpc.monad.xyz
WebSocketwss://testnet-rpc.monad.xyz
Explorerhttps://testnet.monadexplorer.com
Faucethttps://testnet.monad.xyz

Key Differences from Ethereum

FeatureEthereumMonad
Block time12s400ms
Finality~12-18 min800ms (2 slots)
Throughput~10 TPS10,000+ TPS
Gas chargingGas usedGas limit
Max contract size24.5 KB128 KB
Blob txns (EIP-4844)SupportedNot supported
Global mempoolYesNo (leader-based forwarding)
Account cold access2,600 gas10,100 gas
Storage cold access2,100 gas8,100 gas
Reserve balanceNone~10 MON per account
TIMESTAMP granularity1 per block2-3 blocks share same second
Precompile 0x0100N/AEIP-7951 secp256r1 (P256)
EIP-7702 min balanceNone10 MON for delegated EOAs
EIP-7702 CREATE/CREATE2AllowedBanned for delegated EOAs
Tx types supported0,1,2,3,40,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)
PhaseLatencyWhen to Use
Voted400msUI updates, most dApps
Finalized800msConservative apps
Verified~2sExchanges, 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 TypeEthereumMonad
Account (cold)2,60010,100
Storage slot (cold)2,1008,100
Account (warm)100100
Storage slot (warm)100100

Selected precompile repricing:

PrecompileEthereumMonadMultiplier
ecRecover (0x01)3,0006,0002x
ecMul (0x07)6,00030,0005x
ecPairing (0x08)45,000225,0005x
point evaluation (0x0a)50,000200,0004x

Monad-specific precompile: secp256r1 (P256) at 0x0100 for WebAuthn/passkey signature verification (EIP-7951).

EIP-1559 Parameters

ParameterValue
Block gas limit200M
Block gas target160M (80% of limit)
Per-transaction gas limit30M
Min base fee100 MON-gwei
Base fee max step size1/28
Base fee decay factor0.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

  1. Warm your storage — cold reads are 4x more expensive; use access lists (type 1/2 txns) for known slots
  2. Set explicit gas limits — you're charged for the limit, not usage
  3. Batch operations — high throughput means batching is less critical, but still saves gas limit overhead
  4. Avoid unnecessary cold precompile calls — ecPairing is 5x more expensive than Ethereum
  5. Design for parallel execution — per-user mappings over global counters where possible
  6. No blob transactions — use calldata for data availability

Parallel Execution

Monad executes transactions concurrently with optimistic conflict detection. No Solidity changes needed.

  1. Multiple virtual executors process transactions simultaneously
  2. Each generates "pending results" (inputs: SLOADs, outputs: SSTOREs)
  3. Serial commitment validates each result's inputs remain valid
  4. Conflict detected -> re-execute the affected transaction
  5. 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

PatternParallelizes WellWhy
Per-user mappingsYesIndependent state per user
ERC-20 transfers between different pairsYesDifferent storage slots
Global counter incrementNoAll txns write same slot
AMM swaps on same poolNoSame reserves storage
Independent NFT mints (incremental ID)PartiallytokenId counter serializes

Staking Precompile

Address: 0x0000000000000000000000000000000000001000

Only standard CALL is allowed. STATICCALL, DELEGATECALL, and CALLCODE are not permitted.

Core Functions

FunctionSelectorGas Cost
delegate(uint64)0x84994fec260,850
undelegate(uint64,uint256,uint8)0x5cf41514147,750
compound(uint64)0xb34fea67285,050
claimRewards(uint64)0xa76e2ca5155,375
withdraw(uint64,uint8)0xaed2ee7368,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.

RestrictionDetail
Minimum balanceDelegated EOAs cannot drop below 10 MON
CREATE/CREATE2Banned when delegated EOAs execute as smart contracts
Clearing delegationSend 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

ContractAddress
Wrapped MON (WMON)0x3bd359C1119dA7Da1D913D1C4D2B7c461115433A
Staking Precompile0x0000000000000000000000000000000000001000
Multicall30xcA11bde05977b3631167028862bE2a173976CA11
USDC0x754704Bc059F8C67012fEd69BC8A327a5aafb603
USDT00xe7cd86e13AC4309349F30B3435a9d337750fC82D
WETH0xEE8c0E9f1BFFb4Eb878d8f15f368A02a35481242
WBTC0x0555E30da8f98308EdB960aa94C0Db47230d2B9c
ERC-4337 EntryPoint v0.70x0000000071727De22E5E9d8BAf0edAc6f37da032
Safe0x69f4D1788e39c87893C980c06EdF4b7f686e2938

Supported Transaction Types

TypeNameSupportedNotes
0LegacyYesPre-EIP-155 allowed but discouraged
1EIP-2930 (access list)Yes
2EIP-1559 (dynamic fee)YesRecommended
3EIP-4844 (blob)NoNot supported on Monad
4EIP-7702 (delegation)YesWith 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

ToolMinimum Version
FoundryMonad fork (foundryup --network monad)
viem2.40.0+
alloy-chains0.2.20+
Hardhat SolidityevmVersion: "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

FileContents
docs/architecture.mdMonadBFT consensus, parallel execution, deferred execution, MonadDb, JIT, RaptorCast
docs/deployment.mdFoundry + Hardhat deploy/verify step-by-step guides
docs/gas-and-opcodes.mdGas pricing model, opcode repricing tables, precompile costs
docs/staking.mdStaking precompile ABI, functions, events, epoch mechanics
docs/ecosystem.mdToken addresses, bridges, oracles, indexers, canonical contracts
docs/troubleshooting.mdCommon issues and fixes for Monad development
resources/contract-addresses.mdKey Monad contract addresses
templates/deploy-monad.shShell script for deploying to Monad

AI Skill Finder

Ask me what skills you need

What are you building?

Tell me what you're working on and I'll find the best agent skills for you.