Complete Projects

Key ideas: Supply<LP<X, Y>> mints LP shares without a separate coin module; rounding always favors the pool; min_out provides slippage protection; functions return coins so they compose in PTBs (swap → add liquidity → stake in one transaction).

42.1 Constant-product AMM (Sui)

Key ideas: Supply<LP<X, Y>> mints LP shares without a separate coin module; rounding always favors the pool; min_out provides slippage protection; functions return coins so they compose in PTBs (swap → add liquidity → stake in one transaction). Production AMMs also add a minimum-liquidity lock, protocol fees, and TWAP observations.

module amm::pool;

use sui::balance::{Self, Balance, Supply};
use sui::coin::{Self, Coin};
use sui::event;

const EZeroAmount: u64 = 0;
const ESlippage: u64 = 1;
const EFeeTooHigh: u64 = 2;
const MAX_FEE_BPS: u64 = 1_000; // 10%

/// LP token type for pool X/Y
public struct LP<phantom X, phantom Y> has drop {}

public struct Pool<phantom X, phantom Y> has key {
    id: UID,
    reserve_x: Balance<X>,
    reserve_y: Balance<Y>,
    lp_supply: Supply<LP<X, Y>>,
    fee_bps: u64,
}

public struct Swapped has copy, drop { pool: ID, x_to_y: bool, amount_in: u64, amount_out: u64 }

/// Create and share a pool; returns initial LP coins to the creator.
public fun create<X, Y>(
    x: Coin<X>, y: Coin<Y>, fee_bps: u64, ctx: &mut TxContext,
): Coin<LP<X, Y>> {
    assert!(x.value() > 0 && y.value() > 0, EZeroAmount);
    assert!(fee_bps <= MAX_FEE_BPS, EFeeTooHigh);
    let shares = std::u128::sqrt((x.value() as u128) * (y.value() as u128)) as u64;
    let mut lp_supply = balance::create_supply(LP<X, Y> {});
    let lp = lp_supply.increase_supply(shares);
    transfer::share_object(Pool {
        id: object::new(ctx),
        reserve_x: x.into_balance(),
        reserve_y: y.into_balance(),

…

42.2 Staking vault with time-based rewards (Aptos, Fungible Asset)

Borrow note: writing accrue(pool, s) (passing &Pool while s is a mutable borrow into pool.stakes) would be rejected by the borrow checker as aliasing. That is why the code copies pool.reward_rate into a local first and passes only the rate — a classic Move lesson: restructure data access rather than fight the checker. The vault must be pre-funded with reward tokens, and production code should use a global "reward per share" accumulator (as in MasterChef-style contracts) and cap total rewards.

module my_addr::staking {
    use std::signer;
    use aptos_framework::object::{Self, Object, ExtendRef};
    use aptos_framework::fungible_asset::Metadata;
    use aptos_framework::primary_fungible_store;
    use aptos_framework::timestamp;
    use aptos_std::table::{Self, Table};

    const E_NOT_ADMIN: u64 = 1;
    const E_ZERO: u64 = 2;
    const E_NO_STAKE: u64 = 3;
    const E_INSUFFICIENT: u64 = 4;
    const PRECISION: u128 = 1_000_000_000;   // reward rate scaling

    struct Pool has key {
        vault_ref: ExtendRef,               // controls the vault object holding tokens
        token: Object<Metadata>,
        reward_rate: u128,                  // reward per staked unit per second, * PRECISION
        stakes: Table<address, Stake>,
    }

    struct Stake has store, drop {
        amount: u64,
        pending: u64,
        last_update: u64,
    }

    fun vault_address(pool: &Pool): address {
        object::address_from_extend_ref(&pool.vault_ref)
    }

    public entry fun initialize(admin: &signer, token: Object<Metadata>, reward_rate: u128) {
        assert!(signer::address_of(admin) == @my_addr, E_NOT_ADMIN);
        let cref = object::create_named_object(adm
…

42.3 Escrowed sale (Sui)

Shared objects can be taken by value to delete them, which is exactly what purchase and cancel do. For trustless object-for-object swaps without shared objects, Mysten's escrow examples use a two-phase design with an owned locked object plus a key, or a third-party custodian.

module escrow::sale;

use sui::coin::Coin;
use sui::sui::SUI;

const EWrongPrice: u64 = 0;
const ENotSeller: u64 = 1;
const ENotBuyer: u64 = 2;

/// Shared escrow holding an item for a specific buyer at a fixed price.
public struct Escrow<T: key + store> has key {
    id: UID,
    seller: address,
    buyer: address,
    price: u64,
    item: T,
}

public fun create<T: key + store>(item: T, buyer: address, price: u64, ctx: &mut TxContext) {
    transfer::share_object(Escrow { id: object::new(ctx), seller: ctx.sender(), buyer, price, item });
}

public fun purchase<T: key + store>(escrow: Escrow<T>, payment: Coin<SUI>, ctx: &TxContext): T {
    assert!(ctx.sender() == escrow.buyer, ENotBuyer);
    assert!(payment.value() == escrow.price, EWrongPrice);
    let Escrow { id, seller, buyer: _, price: _, item } = escrow;
    transfer::public_transfer(payment, seller);
    id.delete();
    item
}

public fun cancel<T: key + store>(escrow: Escrow<T>, ctx: &TxContext): T {
    assert!(ctx.sender() == escrow.seller, ENotSeller);
    let Escrow { id, seller: _, buyer: _, price: _, item } = escrow;
    id.delete();
    item
}

42.4 NFT marketplace (Sui, generic)

For collections that require creator royalties, build on Kiosk and TransferPolicy instead (Chapter 34): marketplaces then route purchases through the seller's kiosk so the creator's rules are always enforced. On Aptos, the equivalent is a marketplace module listing Object<Token> items using objects and FA payments.

module market::marketplace;

use sui::balance::{Self, Balance};
use sui::coin::{Self, Coin};
use sui::dynamic_object_field as dof;
use sui::event;
use sui::sui::SUI;

const EWrongAmount: u64 = 0;
const ENotSeller: u64 = 1;

public struct Marketplace has key { id: UID, fee_bps: u64, fees: Balance<SUI> }
public struct MarketCap has key, store { id: UID, market: ID }

/// A listing is an object; the item is attached to it as a dynamic object field.
public struct Listing has key, store { id: UID, seller: address, price: u64 }

public struct ItemKey has copy, drop, store {}

public struct Listed has copy, drop { listing: ID, price: u64, seller: address }
public struct Sold has copy, drop { listing: ID, buyer: address }

fun init(ctx: &mut TxContext) {
    let market = Marketplace { id: object::new(ctx), fee_bps: 250, fees: balance::zero() };
    transfer::transfer(MarketCap { id: object::new(ctx), market: object::id(&market) }, ctx.sender());
    transfer::share_object(market);
}

public fun list<T: key + store>(m: &mut Marketplace, item: T, price: u64, ctx: &mut TxContext): ID {
    let mut listing = Listing { id: object::new(ctx), seller: ctx.sender(), price };
    dof::add(&mut listi
…

42.5 Token-weighted DAO (Sui)

The Approval hot potato lets other modules (treasury, parameter config, upgrade manager holding the UpgradeCap) require a passed proposal in the same PTB where the action happens. Extensions: delegation, vote escrow (lock duration multiplies weight), timelocks between approval and execution, and proposal types encoded as enums.

module dao::governance;

use sui::balance::{Self, Balance};
use sui::clock::Clock;
use sui::coin::{Self, Coin};
use sui::table::{Self, Table};

const EVotingClosed: u64 = 0;
const EVotingOpen: u64 = 1;
const EAlreadyVoted: u64 = 2;
const ENotPassed: u64 = 3;
const EAlreadyExecuted: u64 = 4;

public struct Proposal<phantom GOV> has key {
    id: UID,
    description: vector<u8>,
    end_ms: u64,
    yes: u64,
    no: u64,
    quorum: u64,
    executed: bool,
    locked: Table<address, Balance<GOV>>,   // voters' locked tokens
}

/// Hot potato: proof that a proposal passed; consumed by the module that acts on it.
public struct Approval { proposal_id: ID }

public fun propose<GOV>(description: vector<u8>, duration_ms: u64, quorum: u64, clock: &Clock, ctx: &mut TxContext) {
    transfer::share_object(Proposal<GOV> {
        id: object::new(ctx),
        description,
        end_ms: clock.timestamp_ms() + duration_ms,
        yes: 0, no: 0, quorum,
        executed: false,
        locked: table::new(ctx),
    });
}

public fun vote<GOV>(p: &mut Proposal<GOV>, tokens: Coin<GOV>, support: bool, clock: &Clock, ctx: &TxContext) {
    assert!(clock.timestamp_ms() < p.end_ms, EVotingClosed);
…