- Overview
- 1. Creating a Custom Token (Coin)
- 2. Coin Operations
- 3. Balance vs Coin
- 4. NFTs on IOTA
- 5. NFT Collections with Display
- 6. DEX / AMM Patterns
- 7. Flash Loans
- 8. Oracle Patterns
- 9. Sponsored Transactions
- References
IOTA Rebased uses Move's type system to represent fungible tokens (Coin<T>), non-fungible tokens (objects with key + store), and DeFi primitives. The iota::coin module provides the standard for fungible tokens, while NFTs are simply unique objects.
Every fungible token on IOTA uses the Coin<T> standard from iota::coin. You create one using a One-Time Witness (OTW):
module examples::my_token {
use iota::coin;
use iota::url;
/// OTW — must be module name in UPPERCASE, only `drop`.
public struct MY_TOKEN has drop {}
fun init(witness: MY_TOKEN, ctx: &mut TxContext) {
let (treasury_cap, metadata) = coin::create_currency(
witness,
9, // decimals
b"MYT", // symbol
b"My Token", // name
b"A custom token on IOTA", // description
option::some(url::new_unsafe_from_bytes(
b"https://example.com/icon.png"
)), // icon URL
ctx,
);
// Freeze metadata — immutable, visible to all
transfer::public_freeze_object(metadata);
// Transfer TreasuryCap to deployer — controls minting
transfer::public_transfer(treasury_cap, tx_context::sender(ctx));
}
}module examples::my_token {
// ... (init above)
/// Mint tokens — requires TreasuryCap.
public fun mint(
treasury: &mut TreasuryCap<MY_TOKEN>,
amount: u64,
recipient: address,
ctx: &mut TxContext,
) {
let coin = coin::mint(treasury, amount, ctx);
transfer::public_transfer(coin, recipient);
}
/// Burn tokens — requires TreasuryCap.
public fun burn(
treasury: &mut TreasuryCap<MY_TOKEN>,
coin: Coin<MY_TOKEN>,
) {
coin::burn(treasury, coin);
}
}The iota::coin module provides standard operations:
use iota::coin::{Self, Coin};
use iota::iota::IOTA;
/// Split a coin — take `amount` out and return the new coin.
public fun take_payment(payment: &mut Coin<IOTA>, amount: u64, ctx: &mut TxContext): Coin<IOTA> {
coin::split(payment, amount, ctx)
}
/// Merge two coins into one.
public fun combine(coin1: &mut Coin<IOTA>, coin2: Coin<IOTA>) {
coin::join(coin1, coin2);
}
/// Check coin value.
public fun check_balance(coin: &Coin<IOTA>): u64 {
coin::value(coin)
}
/// Create a zero-value coin.
public fun empty_coin(ctx: &mut TxContext): Coin<IOTA> {
coin::zero(ctx)
}Best practice for accepting payments:
// ✅ Good — caller knows exactly what they pay
public fun buy_item(payment: Coin<IOTA>, ctx: &mut TxContext): Item {
assert!(coin::value(&payment) == ITEM_PRICE, EIncorrectPayment);
transfer::public_transfer(payment, @treasury);
Item { id: object::new(ctx) }
}
// ❌ Bad — caller must trust the function to take the right amount
public fun buy_item(payment: &mut Coin<IOTA>, amount: u64, ctx: &mut TxContext): Item {
let paid = coin::split(payment, amount, ctx);
// ...
}Coin<T> is an object (has key). Balance<T> is a raw value (has store only) — lighter weight for internal accounting.
use iota::balance::{Self, Balance};
use iota::coin::{Self, Coin};
public struct Vault has key {
id: UID,
funds: Balance<IOTA>, // Not a separate object — stored inline
}
public fun deposit(vault: &mut Vault, payment: Coin<IOTA>) {
let bal = coin::into_balance(payment); // Coin → Balance
balance::join(&mut vault.funds, bal);
}
public fun withdraw(vault: &mut Vault, amount: u64, ctx: &mut TxContext): Coin<IOTA> {
let bal = balance::split(&mut vault.funds, amount);
coin::from_balance(bal, ctx) // Balance → Coin
}
public fun total(vault: &Vault): u64 {
balance::value(&vault.funds)
}When to use which:
Coin<T>— when transferring to users, accepting payments, public-facingBalance<T>— for internal pool balances, vaults, protocol reserves
NFTs are simply unique objects. There's no special "NFT standard" — any struct with key (and optionally store) is an NFT:
module examples::basic_nft {
use std::string::String;
public struct ArtNFT has key, store {
id: UID,
name: String,
description: String,
image_url: String,
creator: address,
}
public fun mint(
name: String,
description: String,
image_url: String,
ctx: &mut TxContext,
): ArtNFT {
ArtNFT {
id: object::new(ctx),
name,
description,
image_url,
creator: tx_context::sender(ctx),
}
}
/// Burn (destroy) the NFT.
public fun burn(nft: ArtNFT) {
let ArtNFT { id, name: _, description: _, image_url: _, creator: _ } = nft;
object::delete(id);
}
}The Display standard tells wallets and explorers how to render your objects:
module examples::collectible {
use std::string::String;
use iota::package;
use iota::display;
public struct COLLECTIBLE has drop {}
public struct Card has key, store {
id: UID,
name: String,
image_url: String,
power: u64,
}
fun init(otw: COLLECTIBLE, ctx: &mut TxContext) {
let publisher = package::claim(otw, ctx);
let mut disp = display::new_with_fields<Card>(
&publisher,
vector[
b"name".to_string(),
b"image_url".to_string(),
b"description".to_string(),
],
vector[
b"{name}".to_string(),
b"{image_url}".to_string(),
b"Card with power {power}".to_string(),
],
ctx,
);
display::update_version(&mut disp);
transfer::public_transfer(publisher, tx_context::sender(ctx));
transfer::public_transfer(disp, tx_context::sender(ctx));
}
public fun mint(name: String, image_url: String, power: u64, ctx: &mut TxContext): Card {
Card { id: object::new(ctx), name, image_url, power }
}
}A simplified constant-product AMM on IOTA:
module examples::simple_amm {
use iota::balance::{Self, Balance};
use iota::coin::{Self, Coin};
const EInsufficientLiquidity: u64 = 0;
const EZeroAmount: u64 = 1;
/// LP token for this pool.
public struct LP<phantom A, phantom B> has drop {}
/// The AMM pool — shared object.
public struct Pool<phantom A, phantom B> has key {
id: UID,
reserve_a: Balance<A>,
reserve_b: Balance<B>,
lp_supply: u64,
}
/// Create a new pool with initial liquidity.
public fun create_pool<A, B>(
coin_a: Coin<A>,
coin_b: Coin<B>,
ctx: &mut TxContext,
) {
let bal_a = coin::into_balance(coin_a);
let bal_b = coin::into_balance(coin_b);
let lp_supply = balance::value(&bal_a); // simplified
let pool = Pool<A, B> {
id: object::new(ctx),
reserve_a: bal_a,
reserve_b: bal_b,
lp_supply,
};
transfer::share_object(pool);
}
/// Swap A for B using constant product formula.
public fun swap_a_for_b<A, B>(
pool: &mut Pool<A, B>,
coin_in: Coin<A>,
ctx: &mut TxContext,
): Coin<B> {
let amount_in = coin::value(&coin_in);
assert!(amount_in > 0, EZeroAmount);
let reserve_a = balance::value(&pool.reserve_a);
let reserve_b = balance::value(&pool.reserve_b);
// Constant product: (reserve_a + amount_in) * (reserve_b - amount_out) = reserve_a * reserve_b
// With 0.3% fee:
let amount_in_with_fee = amount_in * 997;
let numerator = amount_in_with_fee * reserve_b;
let denominator = (reserve_a * 1000) + amount_in_with_fee;
let amount_out = numerator / denominator;
assert!(amount_out > 0 && amount_out < reserve_b, EInsufficientLiquidity);
balance::join(&mut pool.reserve_a, coin::into_balance(coin_in));
let out_balance = balance::split(&mut pool.reserve_b, amount_out);
coin::from_balance(out_balance, ctx)
}
}Note: This is a simplified example. Production AMMs need LP token minting/burning, slippage protection, fee collection, and proper math (sqrt for initial LP).
Flash loans use the Hot Potato pattern to enforce repayment within the same transaction:
module examples::flash_lender {
use iota::balance::{Self, Balance};
use iota::coin::{Self, Coin};
const EInsufficientFunds: u64 = 0;
const ERepaymentTooLow: u64 = 1;
public struct FlashLender<phantom T> has key {
id: UID,
funds: Balance<T>,
fee_bps: u64, // Fee in basis points (100 = 1%)
}
/// Hot potato — must be consumed by `repay` in the same tx.
public struct FlashReceipt<phantom T> {
lender_id: ID,
amount: u64,
fee: u64,
}
/// Borrow funds — returns coins + a receipt (hot potato).
public fun borrow<T>(
lender: &mut FlashLender<T>,
amount: u64,
ctx: &mut TxContext,
): (Coin<T>, FlashReceipt<T>) {
assert!(balance::value(&lender.funds) >= amount, EInsufficientFunds);
let fee = (amount * lender.fee_bps) / 10000;
let loan = balance::split(&mut lender.funds, amount);
let receipt = FlashReceipt<T> {
lender_id: object::id(lender),
amount,
fee,
};
(coin::from_balance(loan, ctx), receipt)
}
/// Repay the loan — consumes the receipt (hot potato resolved).
public fun repay<T>(
lender: &mut FlashLender<T>,
receipt: FlashReceipt<T>,
repayment: Coin<T>,
) {
let FlashReceipt { lender_id, amount, fee } = receipt;
assert!(object::id(lender) == lender_id, 0);
assert!(coin::value(&repayment) >= amount + fee, ERepaymentTooLow);
balance::join(&mut lender.funds, coin::into_balance(repayment));
}
}Oracles on IOTA typically use shared objects updated by authorized feeds:
module examples::price_oracle {
const ENotAuthorized: u64 = 0;
const EStalePrice: u64 = 1;
public struct OracleAdminCap has key, store { id: UID }
public struct PriceFeed has key {
id: UID,
price: u64, // Price in smallest unit
decimals: u8,
last_updated: u64, // Epoch timestamp
}
fun init(ctx: &mut TxContext) {
transfer::transfer(
OracleAdminCap { id: object::new(ctx) },
tx_context::sender(ctx),
);
transfer::share_object(PriceFeed {
id: object::new(ctx),
price: 0,
decimals: 8,
last_updated: 0,
});
}
/// Update price — requires admin capability.
public fun update_price(
_admin: &OracleAdminCap,
feed: &mut PriceFeed,
new_price: u64,
ctx: &TxContext,
) {
feed.price = new_price;
feed.last_updated = tx_context::epoch(ctx);
}
/// Read price — consumers check staleness themselves.
public fun get_price(feed: &PriceFeed): (u64, u8, u64) {
(feed.price, feed.decimals, feed.last_updated)
}
}IOTA Rebased supports sponsored transactions where an app pays gas fees on behalf of users. This is handled at the transaction level (not in Move code) using the SDK:
// TypeScript SDK example
import { IotaClient } from '@iota/iota-sdk/client';
import { Transaction } from '@iota/iota-sdk/transactions';
const tx = new Transaction();
tx.setSender(userAddress);
tx.setGasOwner(sponsorAddress); // Sponsor pays gas
tx.setGasBudget(10_000_000);
// User signs the tx, then sponsor signs separately
const userSig = await userKeypair.signTransaction(txBytes);
const sponsorSig = await sponsorKeypair.signTransaction(txBytes);
// Execute with both signatures
await client.executeTransactionBlock({
transactionBlock: txBytes,
signature: [userSig.signature, sponsorSig.signature],
});This enables gasless UX for end users — critical for mainstream adoption.
Unlike Coin<T> which is freely transferable, Token<T> is a closed-loop token with spend policies enforced by the creator. Useful for loyalty points, in-game currency, vouchers.
module examples::loyalty {
use iota::token::{Self, Token, ActionRequest};
use iota::coin::{Self, TreasuryCap};
public struct LOYALTY has drop {}
fun init(witness: LOYALTY, ctx: &mut TxContext) {
let (treasury_cap, metadata) = coin::create_currency(
witness, 0, b"LOYALTY", b"Loyalty Points",
b"Non-transferable loyalty points", option::none(), ctx,
);
transfer::public_freeze_object(metadata);
// Create token policy — controls what actions are allowed
let (mut policy, policy_cap) = token::new_policy(&treasury_cap, ctx);
// Allow spending but not peer-to-peer transfer
token::allow(&mut policy, &policy_cap, token::spend_action(), ctx);
token::share_policy(policy);
transfer::public_transfer(policy_cap, ctx.sender());
transfer::public_transfer(treasury_cap, ctx.sender());
}
}The iota::kiosk module provides marketplace primitives — list, purchase, and enforce royalties:
use iota::kiosk::{Self, Kiosk, KioskOwnerCap};
use iota::transfer_policy::{Self, TransferPolicy};
// Create a kiosk (marketplace storefront)
public fun create_kiosk(ctx: &mut TxContext): (Kiosk, KioskOwnerCap) {
kiosk::new(ctx)
}
// List an item for sale
public fun list_item<T: key + store>(
kiosk: &mut Kiosk,
cap: &KioskOwnerCap,
item_id: ID,
price: u64,
) {
kiosk::list<T>(kiosk, cap, item_id, price);
}