Smart Contract Development Guide

Overview

MoltChain smart contracts are compiled to WebAssembly (WASM) and executed inside a Wasmer sandbox. Contracts are written in Rust using #![no_std] and export extern "C" functions that the runtime invokes. The moltchain-sdk crate provides all host functions for storage, events, logging, cross-contract calls, and more.

Key Facts
  • Compile target: wasm32-unknown-unknown
  • Runtime: Wasmer sandbox with deterministic execution
  • Storage: Key-value byte store per contract
  • Entry points: #[no_mangle] pub extern "C" fn
  • Return codes: 0 = success, non-zero = error

Project Setup

Create a new contract project with the following Cargo.toml:

Cargo.toml
[package]
name = "my-contract"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
moltchain-sdk = { path = "../../sdk" }

Your src/lib.rs must declare #![no_std] and export entry points as extern "C" functions:

src/lib.rs — Minimal Contract
#![no_std]
#![cfg_attr(target_arch = "wasm32", no_main)]

extern crate alloc;
use alloc::vec::Vec;

use moltchain_sdk::{
    storage, contract, event, log,
    Address, ContractResult, ContractError,
};

/// Initialize the contract. Called once after deployment.
#[no_mangle]
pub extern "C" fn initialize(admin_ptr: *const u8) -> u32 {
    let admin = unsafe { core::slice::from_raw_parts(admin_ptr, 32) };
    storage::set(b"admin", admin);
    log::info("Contract initialized");
    0 // success
}

/// Example: store a value.
#[no_mangle]
pub extern "C" fn set_value() -> u32 {
    let args = contract::args();
    if args.len() < 8 {
        return 1; // invalid input
    }
    storage::set(b"value", &args[..8]);
    event::emit(r#"{"name":"ValueSet","data":"..."}"#);
    0
}

/// Example: read a value.
#[no_mangle]
pub extern "C" fn get_value() -> u32 {
    match storage::get(b"value") {
        Some(data) => {
            contract::set_return(&data);
            0
        }
        None => 1,
    }
}

SDK Host Functions

The moltchain-sdk crate exposes the following host functions that the WASM runtime provides to your contract.

Storage

storage::get(key: &[u8]) → Option<Vec<u8>> READ

Read a value from the contract's key-value storage. Returns None if the key does not exist. Buffer size: up to 64 KB.

ParameterTypeDescription
key&[u8]Storage key bytes

Returns: Option<Vec<u8>> — the stored value or None.

storage::set(key: &[u8], value: &[u8]) → bool WRITE

Write a value to storage. Overwrites any existing value at the key.

ParameterTypeDescription
key&[u8]Storage key bytes
value&[u8]Value bytes to store

Returns: booltrue on success.

storage::remove(key: &[u8]) → bool DELETE

Remove a key from storage. Internally writes an empty value.

ParameterTypeDescription
key&[u8]Storage key to delete

Contract Context

contract::args() → Vec<u8> READ

Get the arguments passed to this contract call. The raw byte payload from the transaction.

Returns: Vec<u8> — argument bytes (empty if none).

contract::set_return(data: &[u8]) → bool WRITE

Set the return data for this contract execution. Callers (including cross-contract callers) will receive this data.

ParameterTypeDescription
data&[u8]Return data bytes

Environment

get_caller() → [u8; 32] READ

Get the 32-byte public key of the account that invoked this contract call. Used for authorization checks.

get_value() → u64 READ

Get the amount of MOLT (in shells) transferred along with this contract call. Used for payable functions (e.g., .molt name registration).

get_timestamp() → u64 READ

Get the current block timestamp (Unix seconds). Useful for time-based logic like expiry checks and vesting schedules.

get_slot() → u64 READ

Get the current slot number. MoltChain produces ~2 slots/second. Used for slot-based timing (e.g., name expiry at ~63M slots/year).

Events & Logging

event::emit(json_data: &str) → bool EMIT

Emit a structured event as a JSON string. Events are indexed and queryable via RPC. Example:

event::emit(r#"{"name":"Transfer","from":"...","to":"...","amount":1000}"#);
log::info(msg: &str) LOG

Log a message for debugging. Logs are visible in contract execution output and via molt contract logs <address>.

Storage Model

Each contract has its own isolated key-value byte store. Both keys and values are arbitrary byte arrays. Common conventions used across MoltChain contracts:

Key Naming Conventions

PatternExampleUsage
adminb"admin"32-byte admin/owner address
id:{hex}b"id:a1b2c3..."Identity record keyed by hex-encoded address
balance:{addr}b"balance:" + addr_bytesToken/NFT balance for an address
name:{lowercase}b"name:tradingbot".molt name forward lookup
name_rev:{hex}b"name_rev:a1b2...".molt name reverse lookup
allowance:{owner}:{spender}b"allowance:" + owner + ":" + spenderToken spending allowance

Value Packing

  • Integers: Stored as little-endian u64 (8 bytes). Use u64_to_bytes() and bytes_to_u64() helpers from the SDK.
  • Addresses: Raw 32-byte public keys.
  • Fixed records: Packed structs at known byte offsets (e.g., MoltyID identity = 127 bytes with owner at 0..32, reputation at 99..107).
  • Strings: UTF-8 bytes, often with a length prefix.
Storage Pattern Example
use moltchain_sdk::{storage, u64_to_bytes, bytes_to_u64, Address};

// Store a counter
fn increment_counter() {
    let current = storage::get(b"counter")
        .map(|d| bytes_to_u64(&d))
        .unwrap_or(0);
    storage::set(b"counter", &u64_to_bytes(current + 1));
}

// Store per-account data with composite key
fn set_balance(account: Address, amount: u64) {
    let mut key = b"balance:".to_vec();
    key.extend_from_slice(account.to_bytes());
    storage::set(&key, &u64_to_bytes(amount));
}

Cross-Contract Calls

Contracts can invoke functions on other deployed contracts using the crosscall module. This enables composability — for example, checking a caller's MoltyID reputation before granting access.

call_contract(call: CrossCall) → CallResult<Vec<u8>> CALL

Execute a cross-contract call. The CrossCall struct specifies the target address, function name, arguments, and optional value transfer.

use moltchain_sdk::crosscall::{CrossCall, call_contract};
use moltchain_sdk::Address;

let target = Address::new(contract_addr_bytes);
let call = CrossCall::new(target, "get_identity", caller_pubkey.to_vec())
    .with_value(0); // optional MOLT transfer

match call_contract(call) {
    Ok(data) => { /* process return data */ }
    Err(e) => { /* handle error */ }
}

Helper Functions

call_token_transfer(token, from, to, amount) → CallResult<bool>

Transfer tokens on a token contract. Packs from/to addresses and amount into the args automatically.

call_token_balance(token, account) → CallResult<u64>

Query the token balance of an account. Calls balance_of on the target token contract and decodes the u64 result.

call_nft_transfer(nft, from, to, token_id) → CallResult<bool>

Transfer an NFT by token ID between addresses.

call_nft_owner(nft, token_id) → CallResult<Address>

Query the current owner of an NFT by token ID.

MoltyID Integration Pattern

Identity Gate Example
use moltchain_sdk::crosscall::{CrossCall, call_contract};
use moltchain_sdk::{Address, bytes_to_u64};

const MOLTYID_ADDRESS: [u8; 32] = [/* well-known address */];
const MIN_REPUTATION: u64 = 500;

fn require_reputation(caller: &[u8]) -> bool {
    let moltyid = Address::new(MOLTYID_ADDRESS);
    let call = CrossCall::new(moltyid, "get_reputation", caller.to_vec());
    match call_contract(call) {
        Ok(data) if data.len() >= 8 => {
            bytes_to_u64(&data) >= MIN_REPUTATION
        }
        _ => false,
    }
}

Token Module (MT-20)

The moltchain_sdk::token::Token struct implements the MT-20 token standard (similar to ERC-20). It provides mint, burn, transfer, and allowance functionality backed by contract storage.

Token::new(name, symbol, decimals) → Token

Create a new token definition. Call initialize() afterwards to set initial supply.

initialize(&mut self, initial_supply, owner) → ContractResult<()>

Set total supply and mint all tokens to the owner address.

balance_of(&self, account) → u64

Get the token balance for an account. Storage key: balance:{address_bytes}.

transfer(&self, from, to, amount) → ContractResult<()>

Transfer tokens between addresses. Returns InsufficientFunds if balance is too low.

mint(&mut self, to, amount, caller, owner) → ContractResult<()>

Mint new tokens. Only the token owner can mint. Increases total_supply.

burn(&mut self, from, amount) → ContractResult<()>

Burn tokens from an address. Decreases total_supply.

approve(&self, owner, spender, amount) → ContractResult<()>

Approve a spender to transfer up to amount from the owner's balance. Storage key: allowance:{owner}:{spender}.

transfer_from(&self, caller, from, to, amount) → ContractResult<()>

Transfer using an approved allowance. Decrements the allowance by amount.

allowance(&self, owner, spender) → u64

Get the current spending allowance for a spender on an owner's tokens.

get_total_supply(&self) → u64

Read total supply from persistent storage.

NFT Module (MT-721)

The moltchain_sdk::nft::NFT struct implements the MT-721 NFT standard (similar to ERC-721). Each token has a unique ID, owner, and optional metadata URI.

NFT::new(name, symbol) → NFT

Create a new NFT collection.

initialize(&mut self, minter) → NftResult<()>

Initialize the NFT collection, setting the authorized minter address.

mint(&mut self, to, token_id, metadata_uri) → NftResult<()>

Mint a new NFT with the given token ID and metadata URI. Fails if the ID already exists.

transfer(&self, from, to, token_id) → NftResult<()>

Transfer an NFT. Only the current owner can transfer.

owner_of(&self, token_id) → NftResult<Address>

Get the current owner of a token.

balance_of(&self, owner) → u64

Get the number of NFTs owned by an address.

approve(&self, owner, spender, token_id) → NftResult<()>

Approve a specific spender for a single token.

set_approval_for_all(&self, owner, operator, approved) → NftResult<()>

Approve or revoke an operator for all of the owner's tokens.

transfer_from(&self, caller, from, to, token_id) → NftResult<()>

Transfer using an approval (single token or operator). Clears the single-token approval after transfer.

burn(&mut self, owner, token_id) → NftResult<()>

Burn an NFT. Clears owner, metadata, and approval entries.

get_total_minted(&self) → u64

Get the total number of NFTs ever minted in this collection.

DEX Module (AMM)

The moltchain_sdk::dex::Pool implements a constant-product AMM (x × y = k) with a configurable fee (default 0.3%). Used by the MoltSwap contract.

Pool::new(token_a, token_b) → Pool

Create a new liquidity pool for a token pair. Default fee: 3/1000 (0.3%).

initialize(&mut self, token_a, token_b) → DexResult<()>

Initialize the pool and persist to storage.

add_liquidity(&mut self, provider, amount_a, amount_b, min_liquidity) → DexResult<u64>

Add liquidity to the pool. First provider: LP tokens = √(amount_a × amount_b). Subsequent: proportional to reserves.

Returns: Number of LP tokens minted.

remove_liquidity(&mut self, provider, liquidity, min_a, min_b) → DexResult<(u64, u64)>

Remove liquidity by burning LP tokens. Returns (amount_a, amount_b) withdrawn.

swap_a_for_b(&mut self, amount_a_in, min_b_out) → DexResult<u64>

Swap token A for token B using the constant product formula with fee deduction. Returns the amount of token B received.

swap_b_for_a(&mut self, amount_b_in, min_a_out) → DexResult<u64>

Swap token B for token A. Symmetric to swap_a_for_b.

get_amount_out(&self, amount_in, reserve_in, reserve_out) → u64

Calculate the output amount for a given input, accounting for the fee. Pure function (no state change).

get_liquidity_balance(&self, provider) → u64

Get the LP token balance for a liquidity provider.

Constant Product Formula

Output = (amount_in × (1 − fee) × reserve_out) / (reserve_in + amount_in × (1 − fee))

With default 0.3% fee: fee_numerator = 3, fee_denominator = 1000.

Testing

The SDK includes a test_mock module that provides thread-local mock storage, allowing you to unit-test contracts on your host machine without compiling to WASM.

Mock Functions

FunctionDescription
test_mock::reset()Clear all mock state (storage, caller, args, events, logs, timestamp, value, slot)
test_mock::set_caller(addr)Set the mock caller address (32 bytes)
test_mock::set_args(data)Set the mock contract arguments
test_mock::set_value(val)Set the mock MOLT value sent with the call
test_mock::set_timestamp(ts)Set the mock block timestamp
test_mock::set_slot(s)Set the mock slot number
test_mock::get_return_data()Get the data set via contract::set_return()
test_mock::get_events()Get all emitted events
test_mock::get_storage(key)Read directly from mock storage
test_mock::get_logs()Get all logged messages
Full Test Example
#[cfg(test)]
mod tests {
    use super::*;
    use moltchain_sdk::test_mock;

    #[test]
    fn test_initialize_and_set_value() {
        test_mock::reset();

        // Set up mock caller
        let admin = [1u8; 32];
        test_mock::set_caller(admin);

        // Initialize contract
        let result = initialize(admin.as_ptr());
        assert_eq!(result, 0, "initialize should succeed");

        // Verify admin was stored
        let stored = test_mock::get_storage(b"admin");
        assert_eq!(stored, Some(admin.to_vec()));

        // Set a value
        let value_bytes = 42u64.to_le_bytes();
        test_mock::set_args(&value_bytes);
        let result = set_value();
        assert_eq!(result, 0);

        // Read it back
        let result = get_value();
        assert_eq!(result, 0);
        let returned = test_mock::get_return_data();
        assert_eq!(returned, value_bytes.to_vec());

        // Check logs
        let logs = test_mock::get_logs();
        assert!(logs.iter().any(|l| l.contains("initialized")));
    }
}

Build & Deploy

Build

Compile your contract to WASM:

Terminal
# Install the WASM target (one-time)
rustup target add wasm32-unknown-unknown

# Build in release mode
cargo build --target wasm32-unknown-unknown --release

# Output location:
# target/wasm32-unknown-unknown/release/my_contract.wasm

Optimize (Optional)

Terminal
# Reduce WASM size with wasm-opt
wasm-opt -Oz -o optimized.wasm \
  target/wasm32-unknown-unknown/release/my_contract.wasm

Deploy

Terminal
# Deploy to the network
molt deploy target/wasm32-unknown-unknown/release/my_contract.wasm

# Output:
#  Deploying contract: my_contract.wasm
#  Size: 45 KB
#  Contract address: 7xK9...mN2q
#  Contract deployed!
#  Signature: 3hF8...pQ7r

Call

Terminal
# Call a function on the deployed contract
molt call 7xK9...mN2q initialize '[]'

# Call with arguments
molt call 7xK9...mN2q set_value --args '[42]'

Security Best Practices

1. Reentrancy Guards

Cross-contract calls can be re-entered. Use a storage-based mutex pattern:

Reentrancy Guard
fn enter_guard() -> bool {
    if storage::get(b"_locked").is_some() {
        return false; // already locked
    }
    storage::set(b"_locked", &[1]);
    true
}

fn exit_guard() {
    storage::remove(b"_locked");
}

#[no_mangle]
pub extern "C" fn sensitive_operation() -> u32 {
    if !enter_guard() { return 99; } // reentrancy blocked

    // ... do work, cross-contract calls, etc.

    exit_guard();
    0
}

2. Overflow Protection

Always use saturating_add and saturating_sub for arithmetic. MoltChain contracts run in #![no_std] where overflow panics abort the WASM instance.

Safe Arithmetic
//  BAD: can overflow/underflow
let new_balance = balance + amount;
let new_balance = balance - amount;

//  GOOD: saturating arithmetic
let new_balance = balance.saturating_add(amount);
let new_balance = balance.saturating_sub(amount);

//  GOOD: explicit bounds check
if balance < amount {
    return Err(ContractError::InsufficientFunds);
}
let new_balance = balance - amount;

3. Admin Authentication

Always verify the caller is the authorized admin before state-changing operations:

Auth Pattern
fn require_admin(caller: &[u8]) -> bool {
    match storage::get(b"admin") {
        Some(admin) => caller == admin.as_slice(),
        None => false,
    }
}

#[no_mangle]
pub extern "C" fn admin_only_function(caller_ptr: *const u8) -> u32 {
    let caller = unsafe { core::slice::from_raw_parts(caller_ptr, 32) };
    if !require_admin(caller) {
        log::info("Unauthorized");
        return 2;
    }
    // ... proceed
    0
}

4. Input Validation

  • Always check argument lengths before reading from raw pointers.
  • Validate string lengths against maximums (e.g., name ≤ 64 bytes, URL ≤ 256 bytes).
  • Check enum/type values are within valid ranges.
  • Verify addresses are not zero ([0u8; 32]) when that's meaningful.

5. Storage Key Isolation

  • Use unique prefixes for different data types to avoid collisions (e.g., balance:, allowance:, owner:).
  • Include separators (:) between key components to prevent ambiguity.
  • Hex-encode addresses in keys for consistent length and readability.