Comparing debridge with monad

Author

@0xinit

Stars

53

Repository

0xinit/cryptoskills

skills/debridge/SKILL.md

deBridge Solana SDK Development Guide

A comprehensive guide for building Solana programs with the deBridge Solana SDK - enabling decentralized cross-chain transfers of arbitrary messages and value between blockchains.

Overview

deBridge is a cross-chain infrastructure protocol enabling:

  • Cross-Chain Transfers: Bridge assets between Solana and 20+ EVM chains
  • Message Passing: Send arbitrary messages across blockchains
  • External Calls: Execute smart contract calls on destination chains
  • Sub-Second Settlement: ~2 second median settlement time
  • Capital Efficiency: Intent-based architecture with 4bps lowest spreads

Key Features

  • 26+ security audits (Halborn, Zokyo, Ackee Blockchain)
  • $200K bug bounty on Immunefi
  • 100% uptime since launch
  • Zero security incidents

Quick Start

Installation

Add the SDK to your Anchor/Solana program:

cargo add --git ssh://git@github.com/debridge-finance/debridge-solana-sdk.git debridge-solana-sdk

Or add to Cargo.toml:

[dependencies]
debridge-solana-sdk = { git = "ssh://git@github.com/debridge-finance/debridge-solana-sdk.git" }

Basic Setup (Anchor)

use anchor_lang::prelude::*;
use debridge_solana_sdk::prelude::*;

declare_id!("YourProgramId11111111111111111111111111111");

#[program]
pub mod my_bridge_program {
    use super::*;

    pub fn send_cross_chain(
        ctx: Context<SendCrossChain>,
        target_chain_id: [u8; 32],
        receiver: Vec<u8>,
        amount: u64,
    ) -> Result<()> {
        // Invoke deBridge send
        debridge_sending::invoke_debridge_send(
            debridge_sending::SendIx {
                target_chain_id,
                receiver,
                is_use_asset_fee: false,  // Use native SOL for fees
                amount,
                submission_params: None,
                referral_code: None,
            },
            ctx.remaining_accounts,
        )?;

        Ok(())
    }
}

#[derive(Accounts)]
pub struct SendCrossChain<'info> {
    #[account(mut)]
    pub sender: Signer<'info>,
    // Additional accounts passed via remaining_accounts
}

Core Concepts

1. Chain IDs

deBridge uses 32-byte chain identifiers for all supported networks:

use debridge_solana_sdk::chain_ids::*;

// Solana
let solana = SOLANA_CHAIN_ID;  // Solana mainnet

// EVM Chains
let ethereum = ETHEREUM_CHAIN_ID;     // Chain ID: 1
let polygon = POLYGON_CHAIN_ID;       // Chain ID: 137
let bnb = BNB_CHAIN_CHAIN_ID;         // Chain ID: 56
let arbitrum = ARBITRUM_CHAIN_ID;     // Chain ID: 42161
let avalanche = AVALANCHE_CHAIN_ID;   // Chain ID: 43114
let fantom = FANTOM_CHAIN_ID;         // Chain ID: 250
let heco = HECO_CHAIN_ID;             // Chain ID: 128

2. Program IDs

use debridge_solana_sdk::{DEBRIDGE_ID, SETTINGS_ID};

// Main deBridge program for sending/claiming
let debridge_program = DEBRIDGE_ID;

// Settings and confirmation storage program
let settings_program = SETTINGS_ID;

3. Fee Structure

deBridge supports multiple fee payment methods:

// Native Fee (SOL)
is_use_asset_fee: false  // Pay fees in SOL

// Asset Fee
is_use_asset_fee: true   // Pay fees in the bridged token

// Fee Constants
const BPS_DENOMINATOR: u64 = 10000;  // Basis points divisor

4. Flags

Control transfer behavior with flags:

use debridge_solana_sdk::flags::*;

// Available flags (bit positions)
const UNWRAP_ETH: u8 = 0;              // Unwrap to native ETH on destination
const REVERT_IF_EXTERNAL_FAIL: u8 = 1; // Revert if external call fails
const PROXY_WITH_SENDER: u8 = 2;       // Include sender in proxy call
const SEND_HASHED_DATA: u8 = 3;        // Send data as hash
const DIRECT_WALLET_FLOW: u8 = 31;     // Use direct wallet flow

// Setting flags on submission params
let mut flags = [0u8; 32];
flags.set_reserved_flag(UNWRAP_ETH);
flags.set_reserved_flag(REVERT_IF_EXTERNAL_FAIL);

Sending Cross-Chain Transfers

Basic Token Transfer

use debridge_solana_sdk::prelude::*;

pub fn send_tokens(
    ctx: Context<SendTokens>,
    amount: u64,
) -> Result<()> {
    debridge_sending::invoke_debridge_send(
        debridge_sending::SendIx {
            target_chain_id: chain_ids::ETHEREUM_CHAIN_ID,
            receiver: recipient_eth_address.to_vec(),
            is_use_asset_fee: false,
            amount,
            submission_params: None,
            referral_code: Some(12345),  // Optional referral
        },
        ctx.remaining_accounts,
    )?;

    Ok(())
}

Transfer with Fixed Native Fee

pub fn send_with_native_fee(
    ctx: Context<Send>,
    target_chain_id: [u8; 32],
    receiver: Vec<u8>,
    amount: u64,
) -> Result<()> {
    // Get the fixed fee for the target chain
    let fee = debridge_sending::get_chain_native_fix_fee(
        &target_chain_id,
        ctx.remaining_accounts,
    )?;

    debridge_sending::invoke_debridge_send(
        debridge_sending::SendIx {
            target_chain_id,
            receiver,
            is_use_asset_fee: false,
            amount,
            submission_params: None,
            referral_code: None,
        },
        ctx.remaining_accounts,
    )?;

    Ok(())
}

Transfer with Asset Fee

pub fn send_with_asset_fee(
    ctx: Context<Send>,
    target_chain_id: [u8; 32],
    receiver: Vec<u8>,
    amount: u64,
) -> Result<()> {
    // Check if asset fee is available for this chain
    let is_available = debridge_sending::is_asset_fee_available(
        &target_chain_id,
        ctx.remaining_accounts,
    )?;

    if !is_available {
        return Err(error!(ErrorCode::AssetFeeNotAvailable));
    }

    debridge_sending::invoke_debridge_send(
        debridge_sending::SendIx {
            target_chain_id,
            receiver,
            is_use_asset_fee: true,  // Use asset for fees
            amount,
            submission_params: None,
            referral_code: None,
        },
        ctx.remaining_accounts,
    )?;

    Ok(())
}

Transfer with Exact Amount

pub fn send_exact_amount(
    ctx: Context<Send>,
    target_chain_id: [u8; 32],
    receiver: Vec<u8>,
    exact_receive_amount: u64,
) -> Result<()> {
    // Calculate total amount including fees
    let total_with_fees = debridge_sending::add_all_fees(
        exact_receive_amount,
        &target_chain_id,
        ctx.remaining_accounts,
    )?;

    debridge_sending::invoke_debridge_send(
        debridge_sending::SendIx {
            target_chain_id,
            receiver,
            is_use_asset_fee: true,
            amount: total_with_fees,
            submission_params: None,
            referral_code: None,
        },
        ctx.remaining_accounts,
    )?;

    Ok(())
}

Transfer from PDA (Signed)

pub fn send_from_pda(
    ctx: Context<SendFromPda>,
    target_chain_id: [u8; 32],
    receiver: Vec<u8>,
    amount: u64,
    pda_seeds: Vec<Vec<u8>>,
) -> Result<()> {
    // Use signed variant for PDA-owned tokens
    debridge_sending::invoke_debridge_send_signed(
        debridge_sending::SendIx {
            target_chain_id,
            receiver,
            is_use_asset_fee: false,
            amount,
            submission_params: None,
            referral_code: None,
        },
        ctx.remaining_accounts,
        &pda_seeds,
    )?;

    Ok(())
}

Message Passing

Send messages without token transfers:

use debridge_solana_sdk::prelude::*;

pub fn send_message(
    ctx: Context<SendMessage>,
    target_chain_id: [u8; 32],
    receiver: Vec<u8>,
    message_data: Vec<u8>,
) -> Result<()> {
    // Create submission params with message
    let submission_params = debridge_sending::SendSubmissionParamsInput {
        execution_fee: 0,
        flags: [0u8; 32],
        fallback_address: receiver.clone(),
        external_call_shortcut: compute_keccak256(&message_data),
    };

    // Send message (zero amount)
    debridge_sending::invoke_send_message(
        debridge_sending::SendIx {
            target_chain_id,
            receiver,
            is_use_asset_fee: false,
            amount: 0,  // No token transfer
            submission_params: Some(submission_params),
            referral_code: None,
        },
        ctx.remaining_accounts,
    )?;

    Ok(())
}

External Calls

Execute smart contract calls on destination chains:

Initialize External Call Buffer

pub fn init_external_call(
    ctx: Context<InitExternalCall>,
    target_chain_id: [u8; 32],
    external_call_data: Vec<u8>,
) -> Result<()> {
    let shortcut = compute_keccak256(&external_call_data);

    debridge_sending::invoke_init_external_call(
        debridge_sending::InitExternalCallIx {
            external_call_len: external_call_data.len() as u32,
            chain_id: target_chain_id,
            external_call_shortcut: shortcut,
            external_call: external_call_data,
        },
        ctx.remaining_accounts,
    )?;

    Ok(())
}

Send with External Call

pub fn send_with_external_call(
    ctx: Context<SendWithExternalCall>,
    target_chain_id: [u8; 32],
    receiver: Vec<u8>,  // Target contract address
    amount: u64,
    external_call_data: Vec<u8>,
    execution_fee: u64,  // Fee for executor on destination
) -> Result<()> {
    let shortcut = compute_keccak256(&external_call_data);

    // Set flags for external call behavior
    let mut flags = [0u8; 32];
    flags.set_reserved_flag(flags::REVERT_IF_EXTERNAL_FAIL);

    let submission_params = debridge_sending::SendSubmissionParamsInput {
        execution_fee,
        flags,
        fallback_address: ctx.accounts.fallback.key().to_bytes().to_vec(),
        external_call_shortcut: shortcut,
    };

    debridge_sending::invoke_debridge_send(
        debridge_sending::SendIx {
            target_chain_id,
            receiver,
            is_use_asset_fee: false,
            amount,
            submission_params: Some(submission_params),
            referral_code: None,
        },
        ctx.remaining_accounts,
    )?;

    Ok(())
}

Claim Verification

Verify claims on the receiving side:

Validate Incoming Claims

use debridge_solana_sdk::check_claiming::*;

pub fn receive_tokens(ctx: Context<ReceiveTokens>) -> Result<()> {
    // Get and validate the parent claim instruction
    let claim_ix = ValidatedExecuteExtCallIx::try_from_current_ix()?;

    // Validate submission details
    let validation = SubmissionAccountValidation {
        receiver_validation: Some(ctx.accounts.receiver.key()),
        token_mint_validation: Some(ctx.accounts.token_mint.key()),
        source_chain_id_validation: Some(chain_ids::ETHEREUM_CHAIN_ID),
        ..Default::default()
    };

    claim_ix.validate_submission_account(
        &ctx.accounts.submission_account,
        &validation,
    )?;

    // Proceed with claim logic
    Ok(())
}

Get Submission Key

pub fn get_claim_info(ctx: Context<ClaimInfo>) -> Result<Pubkey> {
    let claim_ix = ValidatedExecuteExtCallIx::try_from_current_ix()?;
    let submission_key = claim_ix.get_submission_key()?;
    Ok(submission_key)
}

Fee Queries

Get Transfer Fees

// Get base transfer fee (in BPS)
let transfer_fee = debridge_sending::get_transfer_fee(
    ctx.remaining_accounts,
)?;

// Get transfer fee for specific chain
let chain_fee = debridge_sending::get_transfer_fee_for_chain(
    &target_chain_id,
    ctx.remaining_accounts,
)?;

// Get default native fix fee
let default_fee = debridge_sending::get_default_native_fix_fee(
    ctx.remaining_accounts,
)?;

// Get chain-specific native fix fee
let native_fee = debridge_sending::get_chain_native_fix_fee(
    &target_chain_id,
    ctx.remaining_accounts,
)?;

// Get asset fix fee for chain
let asset_fee = debridge_sending::try_get_chain_asset_fix_fee(
    &target_chain_id,
    ctx.remaining_accounts,
)?;

Calculate Total Amount with Fees

// Add transfer fee to amount
let with_transfer_fee = debridge_sending::add_transfer_fee(
    amount,
    ctx.remaining_accounts,
)?;

// Add all fees (transfer + execution + asset fees)
let total_amount = debridge_sending::add_all_fees(
    amount,
    &target_chain_id,
    ctx.remaining_accounts,
)?;

Chain Support Queries

// Check if chain is supported
let is_supported = debridge_sending::is_chain_supported(
    &target_chain_id,
    ctx.remaining_accounts,
)?;

// Get chain support info
let chain_info = debridge_sending::get_chain_support_info(
    &target_chain_id,
    ctx.remaining_accounts,
)?;

// Check if asset fee is available
let asset_fee_available = debridge_sending::is_asset_fee_available(
    &target_chain_id,
    ctx.remaining_accounts,
)?;

PDA Derivation

Bridge Account

use debridge_solana_sdk::keys::*;

// Find bridge PDA for a token mint
let (bridge_address, bump) = BridgePubkey::find_bridge_address(&token_mint);

// Create with known bump
let bridge_address = BridgePubkey::create_bridge_address(&token_mint, bump)?;

Chain Support Info

// Find chain support info PDA
let (chain_support_info, bump) = ChainSupportInfoPubkey::find_chain_support_info_address(
    &target_chain_id,
);

Asset Fee Info

// Find asset fee info PDA
let (asset_fee_info, bump) = AssetFeeInfoPubkey::find_asset_fee_info_address(
    &bridge_pubkey,
    &target_chain_id,
);

// Get default bridge fee address
let default_fee = AssetFeeInfoPubkey::default_bridge_fee_address();

External Call Storage

// Find external call storage PDA
let (storage, bump) = ExternalCallStoragePubkey::find_external_call_storage_address(
    &shortcut,
    &owner,
);

// Find external call meta PDA
let (meta, bump) = ExternalCallMetaPubkey::find_external_call_meta_address(
    &storage_account,
);

Required Accounts

The SDK requires specific accounts passed via remaining_accounts. The account order is important:

IndexAccountSignerWritableDescription
0BridgeNoYesBridge account for token
1Token MintNoNoSPL Token mint
2Staking WalletNoYesStaking rewards wallet
3Mint AuthorityNoNoToken mint authority
4Chain Support InfoNoNoTarget chain config
5Settings ProgramNoNodeBridge settings
6SPL Token ProgramNoNoToken program
7StateNoNoProtocol state
8deBridge ProgramNoNoMain deBridge program
...Additional accounts--Varies by operation

TypeScript Client Integration

Setup

import { Connection, Keypair, PublicKey, Transaction } from '@solana/web3.js';
import { Program, AnchorProvider, Wallet } from '@coral-xyz/anchor';

const connection = new Connection('https://api.mainnet-beta.solana.com');
const wallet = new Wallet(keypair);
const provider = new AnchorProvider(connection, wallet, {});

// deBridge Program IDs
const DEBRIDGE_PROGRAM_ID = new PublicKey('DEbrdGj3HsRsAzx6uH4MKyREKxVAfBydijLUF3ygsFfh');
const SETTINGS_PROGRAM_ID = new PublicKey('DeSetTwWhjZq6Pz9Kfdo1KoS5NqtsM6G8ERbX4SSCSft');

Build Send Transaction

import {
  TOKEN_PROGRAM_ID,
  getAssociatedTokenAddress
} from '@solana/spl-token';

async function buildSendTransaction(
  tokenMint: PublicKey,
  amount: bigint,
  targetChainId: Uint8Array,
  receiver: Uint8Array,
): Promise<Transaction> {
  // Derive required PDAs
  const [bridge] = PublicKey.findProgramAddressSync(
    [Buffer.from('BRIDGE'), tokenMint.toBuffer()],
    DEBRIDGE_PROGRAM_ID
  );

  const [chainSupportInfo] = PublicKey.findProgramAddressSync(
    [Buffer.from('CHAIN_SUPPORT_INFO'), targetChainId],
    SETTINGS_PROGRAM_ID
  );

  const [state] = PublicKey.findProgramAddressSync(
    [Buffer.from('STATE')],
    DEBRIDGE_PROGRAM_ID
  );

  // Build instruction with remaining accounts
  const instruction = await program.methods
    .sendViaDebridge(
      Array.from(targetChainId),
      Array.from(receiver),
      new BN(amount.toString()),
    )
    .remainingAccounts([
      { pubkey: bridge, isSigner: false, isWritable: true },
      { pubkey: tokenMint, isSigner: false, isWritable: false },
      // ... additional required accounts
    ])
    .instruction();

  return new Transaction().add(instruction);
}

Build External Call Data

import { ethers } from 'ethers';
import { keccak256 } from '@ethersproject/keccak256';

function buildExternalCallData(
  targetContract: string,
  functionSig: string,
  params: any[]
): { data: Uint8Array; shortcut: Uint8Array } {
  const iface = new ethers.Interface([functionSig]);
  const calldata = iface.encodeFunctionData(
    functionSig.split('(')[0].replace('function ', ''),
    params
  );

  const data = ethers.getBytes(calldata);
  const shortcut = ethers.getBytes(keccak256(data));

  return { data, shortcut };
}

// Example: ERC20 approve call
const { data, shortcut } = buildExternalCallData(
  '0xTargetContract...',
  'function approve(address spender, uint256 amount)',
  ['0xSpenderAddress...', ethers.parseEther('1000')]
);

Testing

Anchor Test Setup

# Anchor.toml
[provider]
cluster = "mainnet"  # Use mainnet for testing with real deBridge

[programs.mainnet]
my_program = "YourProgramId..."

Run Tests

# Full build and test
cd example_program && anchor build && anchor test

# Test only (skip rebuild)
anchor test --skip-build --skip-deploy

Local Testing Tips

  1. Use Mainnet Fork: deBridge infrastructure is on mainnet
  2. Mock Remaining Accounts: Create mock accounts for unit tests
  3. Test Fee Calculations: Verify fee amounts before sending

Build Features

The SDK supports different environments via Cargo features:

# Production (default) - uses hardcoded program IDs
debridge-solana-sdk = { git = "..." }

# Custom environment - uses env vars
debridge-solana-sdk = { git = "...", features = ["env"] }

Environment variables for custom networks:

  • DEBRIDGE_PROGRAM_PUBKEY: Custom deBridge program ID
  • DEBRIDGE_SETTINGS_PROGRAM_PUBKEY: Custom settings program ID

Resources

Skill Structure

debridge/
├── SKILL.md                          # This file
├── resources/
│   ├── sdk-api-reference.md          # Complete SDK API reference
│   ├── chain-ids.md                  # Supported chain identifiers
│   ├── program-ids.md                # Program IDs and PDAs
│   └── error-codes.md                # Error types and handling
├── examples/
│   ├── basic-transfer/               # Simple cross-chain transfer
│   ├── external-calls/               # External call execution
│   ├── message-passing/              # Message-only transfers
│   └── fee-configurations/           # Fee payment options
└── docs/
    └── troubleshooting.md            # Common issues and solutions

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.