Comparing aptos with monad
aptos
View full →Author
@0xinit
Stars
53
Repository
0xinit/cryptoskills
Aptos Move L1 Development
Aptos is a Layer 1 blockchain built on Move, the language originally developed for Meta's Diem project. It achieves high throughput via Block-STM, a parallel execution engine that processes transactions optimistically and re-executes on conflicts. Smart contracts are called modules, and data is stored as resources at account addresses in a global storage model.
What You Probably Got Wrong
AI agents trained on Sui Move or Solidity make critical errors when generating Aptos Move code. Fix these first.
-
Aptos Move uses global storage, NOT Sui's object model — Resources are stored at addresses using
move_to,move_from,borrow_global, andborrow_global_mut. There is noobject::ObjectIDorsui::object::UID. When you want to store data, youmove_to<T>(signer, resource)to place it at the signer's address. To read it, youborrow_global<T>(address). -
Resource accounts are NOT regular accounts — A resource account is a special account with no private key, controlled by its creating module. You create one with
account::create_resource_account(origin, seed). The module publishes to the resource account's address. This is how protocols deploy immutable, admin-less contracts. -
Token V1 is deprecated — use Token V2 (Digital Assets) — The
aptos_tokenmodule (V1) is legacy. Useaptos_token_objects(V2), which uses the Move Object model. V2 tokens are stored as objects at their own addresses, not in a creator's TokenStore. Collections and tokens are first-class objects. -
@aptos-labs/ts-sdkreplaces the oldaptospackage — The npm packageaptosis deprecated. Use@aptos-labs/ts-sdk. The entry point isnew Aptos(new AptosConfig({ network: Network.MAINNET })). Do not import fromaptos. -
Coin standard is NOT ERC-20 — Aptos uses
aptos_framework::coinwith generics. A coin type isCoin<CoinType>whereCoinTypeis a phantom type parameter defined by the deploying module. There is no approval/allowance pattern — coins are moved directly. -
signeris notmsg.sender— In Aptos Move, thesigneris passed as a function parameter. A function must explicitly accept&signerto access the caller's address and perform operations on their account. Usesigner::address_of(account)to get the address. -
View functions are explicit — You must annotate functions with
#[view]to make them callable off-chain without a transaction. They cannot modify state. They are called via the/viewAPI endpoint, not through transaction submission. -
u256exists butu64is standard for amounts — Unlike Solidity'suint256default, Aptos usesu64for coin amounts and most counters.u256exists but is rarely used. APT has 8 decimals (not 18). 1 APT = 100,000,000 octas.
Chain Configuration
Mainnet
| Property | Value |
|---|---|
| Chain ID | 1 |
| Currency | APT (8 decimals) |
| Block Time | ~100-300ms (sub-second) |
| Finality | ~900ms |
| Max Gas Unit | 2,000,000 |
| Gas Unit Price | Min 100 octas |
| VM | Move VM with Block-STM |
| Consensus | AptosBFT (DiemBFT v4) |
RPC Endpoints
| URL | Provider | Notes |
|---|---|---|
https://fullnode.mainnet.aptoslabs.com/v1 | Aptos Labs | Default REST API |
https://mainnet.aptoslabs.com/v1 | Aptos Labs | Alternative |
https://aptos-mainnet.nodereal.io/v1 | NodeReal | Rate-limited |
Block Explorers
| Explorer | URL |
|---|---|
| Aptos Explorer | https://explorer.aptoslabs.com |
| Aptscan | https://aptscan.ai |
Testnet
| Property | Value |
|---|---|
| Chain ID | 2 |
| RPC | https://fullnode.testnet.aptoslabs.com/v1 |
| Faucet | https://faucet.testnet.aptoslabs.com |
| Explorer | https://explorer.aptoslabs.com/?network=testnet |
Devnet
| Property | Value |
|---|---|
| Chain ID | varies (resets frequently) |
| RPC | https://fullnode.devnet.aptoslabs.com/v1 |
| Faucet | https://faucet.devnet.aptoslabs.com |
Quick Start
Install Aptos CLI
# macOS
brew install aptos
# Linux / manual
curl -fsSL "https://aptos.dev/scripts/install_cli.py" | python3
# Verify
aptos --version
Create a New Move Project
# Initialize a new Move package
aptos move init --name my_module
# Project structure:
# my_module/
# ├── Move.toml
# └── sources/
# └── my_module.move
Move.toml Configuration
[package]
name = "my_module"
version = "0.1.0"
[addresses]
my_addr = "_"
[dependencies]
AptosFramework = { git = "https://github.com/aptos-labs/aptos-core.git", subdir = "aptos-move/framework/aptos-framework", rev = "mainnet" }
AptosTokenObjects = { git = "https://github.com/aptos-labs/aptos-core.git", subdir = "aptos-move/framework/aptos-token-objects", rev = "mainnet" }
TypeScript SDK Setup
npm install @aptos-labs/ts-sdk
import { Aptos, AptosConfig, Network } from "@aptos-labs/ts-sdk";
const config = new AptosConfig({ network: Network.MAINNET });
const aptos = new Aptos(config);
Move Module Development
Module Structure
module my_addr::counter {
use std::signer;
struct Counter has key {
value: u64,
}
/// Initialize a counter resource at the signer's address
public entry fun initialize(account: &signer) {
let counter = Counter { value: 0 };
move_to(account, counter);
}
/// Increment the counter stored at the signer's address
public entry fun increment(account: &signer) acquires Counter {
let addr = signer::address_of(account);
let counter = borrow_global_mut<Counter>(addr);
counter.value = counter.value + 1;
}
/// Read the counter value at any address
#[view]
public fun get_count(addr: address): u64 acquires Counter {
borrow_global<Counter>(addr).value
}
}
Key Move Concepts
Global Storage Operations
// Store a resource at signer's address (signer must not already have one)
move_to<T>(signer, resource);
// Remove and return a resource from an address
let resource = move_from<T>(addr);
// Immutable reference to resource at address
let ref = borrow_global<T>(addr);
// Mutable reference to resource at address
let ref_mut = borrow_global_mut<T>(addr);
// Check if a resource exists at address
let exists = exists<T>(addr);
Abilities
// has copy — value can be copied
// has drop — value can be dropped (destroyed implicitly)
// has store — value can be stored inside another struct
// has key — value can be stored as a top-level resource in global storage
struct Coin has store {
value: u64,
}
struct CoinStore has key {
coin: Coin,
}
Access Control Pattern
module my_addr::admin {
use std::signer;
struct AdminConfig has key {
admin: address,
}
const E_NOT_ADMIN: u64 = 1;
const E_ALREADY_INITIALIZED: u64 = 2;
public entry fun initialize(account: &signer) {
let addr = signer::address_of(account);
assert!(!exists<AdminConfig>(addr), E_ALREADY_INITIALIZED);
move_to(account, AdminConfig { admin: addr });
}
public entry fun admin_only_action(account: &signer, config_addr: address) acquires AdminConfig {
let config = borrow_global<AdminConfig>(config_addr);
assert!(signer::address_of(account) == config.admin, E_NOT_ADMIN);
// perform privileged action
}
}
Events
module my_addr::events_example {
use aptos_framework::event;
#[event]
struct TransferEvent has drop, store {
from: address,
to: address,
amount: u64,
}
public entry fun transfer(from: &signer, to: address, amount: u64) {
// ... transfer logic ...
event::emit(TransferEvent {
from: signer::address_of(from),
to,
amount,
});
}
}
Resource Accounts
module my_addr::resource_account_example {
use std::signer;
use aptos_framework::account;
use aptos_framework::resource_account;
struct ModuleData has key {
resource_signer_cap: account::SignerCapability,
}
/// Called once during module publication to a resource account.
/// The resource account's signer capability is stored for later use.
fun init_module(resource_signer: &signer) {
let resource_signer_cap = resource_account::retrieve_resource_account_cap(
resource_signer,
@source_addr
);
move_to(resource_signer, ModuleData {
resource_signer_cap,
});
}
/// Use the stored signer capability to act as the resource account
public entry fun do_something(caller: &signer) acquires ModuleData {
let module_data = borrow_global<ModuleData>(@my_addr);
let resource_signer = account::create_signer_with_capability(
&module_data.resource_signer_cap
);
// resource_signer can now sign transactions on behalf of the resource account
}
}
Coin Standard
Creating a Custom Coin
module my_addr::my_coin {
use std::signer;
use std::string;
use aptos_framework::coin;
/// Phantom type marker for the coin — defines the coin type globally
struct MyCoin {}
struct CoinCapabilities has key {
burn_cap: coin::BurnCapability<MyCoin>,
freeze_cap: coin::FreezeCapability<MyCoin>,
mint_cap: coin::MintCapability<MyCoin>,
}
const E_NOT_ADMIN: u64 = 1;
public entry fun initialize(account: &signer) {
let (burn_cap, freeze_cap, mint_cap) = coin::initialize<MyCoin>(
account,
string::utf8(b"My Coin"),
string::utf8(b"MYC"),
8, // decimals
true, // monitor_supply
);
move_to(account, CoinCapabilities {
burn_cap,
freeze_cap,
mint_cap,
});
}
public entry fun mint(
account: &signer,
to: address,
amount: u64,
) acquires CoinCapabilities {
let addr = signer::address_of(account);
let caps = borrow_global<CoinCapabilities>(addr);
let coins = coin::mint(amount, &caps.mint_cap);
coin::deposit(to, coins);
}
public entry fun burn(
account: &signer,
amount: u64,
) acquires CoinCapabilities {
let addr = signer::address_of(account);
let caps = borrow_global<CoinCapabilities>(addr);
let coins = coin::withdraw<MyCoin>(account, amount);
coin::burn(coins, &caps.burn_cap);
}
}
Registering for a Coin
// Before receiving any coin type, an account must register for it
public entry fun register_coin<CoinType>(account: &signer) {
coin::register<CoinType>(account);
}
Token V2 — Digital Assets
Creating a Collection and Token
module my_addr::nft {
use std::signer;
use std::string::{Self, String};
use std::option;
use aptos_token_objects::collection;
use aptos_token_objects::token;
struct TokenRefs has key {
burn_ref: token::BurnRef,
transfer_ref: option::Option<object::TransferRef>,
mutator_ref: token::MutatorRef,
}
public entry fun create_collection(creator: &signer) {
collection::create_unlimited_collection(
creator,
string::utf8(b"Collection description"),
string::utf8(b"My Collection"),
option::none(), // no royalty
string::utf8(b"https://example.com/collection"),
);
}
public entry fun mint_token(creator: &signer) {
let constructor_ref = token::create_named_token(
creator,
string::utf8(b"My Collection"),
string::utf8(b"Token description"),
string::utf8(b"Token #1"),
option::none(), // no royalty
string::utf8(b"https://example.com/token/1"),
);
let token_signer = object::generate_signer(&constructor_ref);
let burn_ref = token::generate_burn_ref(&constructor_ref);
let mutator_ref = token::generate_mutator_ref(&constructor_ref);
move_to(&token_signer, TokenRefs {
burn_ref,
transfer_ref: option::none(),
mutator_ref,
});
}
}
TypeScript SDK (@aptos-labs/ts-sdk)
Client Initialization
import {
Aptos,
AptosConfig,
Network,
Account,
Ed25519PrivateKey,
AccountAddress,
} from "@aptos-labs/ts-sdk";
// Mainnet
const aptos = new Aptos(new AptosConfig({ network: Network.MAINNET }));
// Testnet
const aptosTestnet = new Aptos(new AptosConfig({ network: Network.TESTNET }));
// Custom node
const aptosCustom = new Aptos(
new AptosConfig({
fullnode: "https://my-node.example.com/v1",
indexer: "https://my-indexer.example.com/v1/graphql",
})
);
Account Management
// Generate a new account
const account = Account.generate();
console.log("Address:", account.accountAddress.toString());
console.log("Private key:", account.privateKey.toString());
// From existing private key
const privateKey = new Ed25519PrivateKey("0x...");
const existingAccount = Account.fromPrivateKey({ privateKey });
// Fund on testnet
const aptosTestnet = new Aptos(new AptosConfig({ network: Network.TESTNET }));
await aptosTestnet.fundAccount({
accountAddress: account.accountAddress,
amount: 100_000_000, // 1 APT = 100,000,000 octas
});
Transfer APT
async function transferAPT(
aptos: Aptos,
sender: Account,
recipientAddress: string,
amountOctas: number
): Promise<string> {
const transaction = await aptos.transaction.build.simple({
sender: sender.accountAddress,
data: {
function: "0x1::aptos_account::transfer",
functionArguments: [AccountAddress.from(recipientAddress), amountOctas],
},
});
const pendingTx = await aptos.signAndSubmitTransaction({
signer: sender,
transaction,
});
const committedTx = await aptos.waitForTransaction({
transactionHash: pendingTx.hash,
});
return committedTx.hash;
}
View Functions
async function getBalance(aptos: Aptos, address: string): Promise<bigint> {
const result = await aptos.view({
payload: {
function: "0x1::coin::balance",
typeArguments: ["0x1::aptos_coin::AptosCoin"],
functionArguments: [AccountAddress.from(address)],
},
});
return BigInt(result[0] as string);
}
Read Account Resources
async function getCoinStore(aptos: Aptos, address: string) {
return aptos.getAccountResource({
accountAddress: AccountAddress.from(address),
resourceType: "0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>",
});
}
Multi-Agent Transactions
// Multi-agent: multiple signers for one transaction
async function multiAgentTransfer(
aptos: Aptos,
sender: Account,
secondSigner: Account
) {
const transaction = await aptos.transaction.build.multiAgent({
sender: sender.accountAddress,
secondarySignerAddresses: [secondSigner.accountAddress],
data: {
function: "0xmodule::my_module::multi_signer_action",
functionArguments: [],
},
});
const senderAuth = aptos.transaction.sign({
signer: sender,
transaction,
});
const secondAuth = aptos.transaction.sign({
signer: secondSigner,
transaction,
});
const pendingTx = await aptos.transaction.submit.multiAgent({
transaction,
senderAuthenticator: senderAuth,
additionalSignersAuthenticators: [secondAuth],
});
return aptos.waitForTransaction({ transactionHash: pendingTx.hash });
}
Gas Estimation
async function estimateGas(aptos: Aptos, sender: Account) {
const transaction = await aptos.transaction.build.simple({
sender: sender.accountAddress,
data: {
function: "0x1::aptos_account::transfer",
functionArguments: [
AccountAddress.from("0xrecipient"),
100_000_000,
],
},
});
// Simulate to get gas estimate
const simulation = await aptos.transaction.simulate.simple({
signerPublicKey: sender.publicKey,
transaction,
});
const gasUsed = BigInt(simulation[0].gas_used);
const gasUnitPrice = BigInt(simulation[0].gas_unit_price);
const totalCost = gasUsed * gasUnitPrice;
return { gasUsed, gasUnitPrice, totalCost };
}
Compile and Deploy
Compile Module
# Compile
aptos move compile --named-addresses my_addr=default
# Run tests
aptos move test --named-addresses my_addr=default
# Publish to testnet (requires funded account)
aptos move publish --named-addresses my_addr=default --profile testnet
CLI Account Setup
# Initialize a new profile (generates keypair, funds on devnet/testnet)
aptos init --profile testnet --network testnet
# Initialize with existing private key
aptos init --profile mainnet --private-key 0x... --network mainnet
# Check account balance
aptos account balance --profile testnet
See examples/deploy-module/ for full SDK deployment code.
Testing Move Modules
#[test_only]
module my_addr::counter_tests {
use std::signer;
use my_addr::counter;
#[test(account = @0x1)]
fun test_initialize(account: &signer) {
counter::initialize(account);
let addr = signer::address_of(account);
assert!(counter::get_count(addr) == 0, 0);
}
#[test(account = @0x1)]
fun test_increment(account: &signer) {
counter::initialize(account);
counter::increment(account);
let addr = signer::address_of(account);
assert!(counter::get_count(addr) == 1, 0);
}
#[test(account = @0x1)]
#[expected_failure(abort_code = 0x60001, location = aptos_framework::account)]
fun test_double_initialize(account: &signer) {
counter::initialize(account);
counter::initialize(account); // should fail: resource already exists
}
}
Block-STM Parallel Execution
Aptos uses Block-STM for optimistic parallel execution. Transactions within a block execute concurrently. If two transactions conflict (read/write to the same resource), one is re-executed.
What This Means for Developers
- Independent transactions run in parallel — Transactions touching different accounts or resources execute simultaneously.
- Contention on hot resources serializes execution — If your contract uses a single global counter that every transaction increments, Block-STM will detect the conflict and serialize those transactions. Performance degrades to sequential.
- Design for parallelism — Use per-user resources instead of global state when possible. Example: instead of a global
TotalDepositscounter, track deposits per-user and aggregate off-chain.
Anti-Pattern: Global Hot Resource
// BAD: Every deposit transaction conflicts on the same resource
struct GlobalState has key {
total_deposits: u64,
}
public entry fun deposit(account: &signer, amount: u64) acquires GlobalState {
let state = borrow_global_mut<GlobalState>(@module_addr);
state.total_deposits = state.total_deposits + amount;
// every deposit serializes here
}
Pattern: Per-User State
// GOOD: Each user's deposit is independent — parallel-friendly
struct UserDeposit has key {
amount: u64,
}
public entry fun deposit(account: &signer, amount: u64) acquires UserDeposit {
let addr = signer::address_of(account);
if (exists<UserDeposit>(addr)) {
let deposit = borrow_global_mut<UserDeposit>(addr);
deposit.amount = deposit.amount + amount;
} else {
move_to(account, UserDeposit { amount });
};
}
Move Object Model
The Move Object model (used by Token V2) creates objects at deterministic addresses. Objects are distinct from resources stored at user addresses.
module my_addr::object_example {
use aptos_framework::object::{Self, Object, ConstructorRef};
use std::signer;
struct MyObject has key {
value: u64,
}
/// Create a named object at a deterministic address
public entry fun create(creator: &signer) {
let constructor_ref = object::create_named_object(
creator,
b"my_object_seed",
);
let object_signer = object::generate_signer(&constructor_ref);
move_to(&object_signer, MyObject { value: 42 });
}
/// Transfer ownership of an object
public entry fun transfer_object(
owner: &signer,
obj: Object<MyObject>,
to: address,
) {
object::transfer(owner, obj, to);
}
#[view]
public fun get_value(obj: Object<MyObject>): u64 acquires MyObject {
let obj_addr = object::object_address(&obj);
borrow_global<MyObject>(obj_addr).value
}
}
Common Patterns
Table Storage (Key-Value Map)
use aptos_std::table::{Self, Table};
struct Registry has key {
entries: Table<address, u64>,
}
public entry fun add_entry(account: &signer, key: address, value: u64) acquires Registry {
let registry = borrow_global_mut<Registry>(signer::address_of(account));
table::upsert(&mut registry.entries, key, value);
}
#[view]
public fun get_entry(registry_addr: address, key: address): u64 acquires Registry {
let registry = borrow_global<Registry>(registry_addr);
*table::borrow(®istry.entries, key)
}
Timestamp
use aptos_framework::timestamp;
public fun is_expired(deadline: u64): bool {
timestamp::now_seconds() > deadline
}
Indexer and GraphQL
Aptos provides a GraphQL indexer for querying historical data, events, and token ownership.
| Network | Indexer URL |
|---|---|
| Mainnet | https://indexer.mainnet.aptoslabs.com/v1/graphql |
| Testnet | https://indexer.testnet.aptoslabs.com/v1/graphql |
Key tables: current_token_ownerships_v2 (NFT ownership), current_token_datas_v2 (token metadata), coin_activities (transfer history), account_transactions (transaction history).
See examples/read-resources/ for full GraphQL query patterns.
Reference Links
- Official Docs: https://aptos.dev
- Move Language Reference: https://aptos.dev/en/build/smart-contracts/book
- TypeScript SDK: https://github.com/aptos-labs/aptos-ts-sdk
- Framework Source: https://github.com/aptos-labs/aptos-core/tree/main/aptos-move/framework
- Token V2 Standard: https://aptos.dev/en/build/smart-contracts/digital-asset
- Move Prover: https://aptos.dev/en/build/smart-contracts/prover
Last verified: 2025-12-01
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 |