Move.

Post

Share your knowledge.

Satoshi .
Sep 01, 2025
Expert Q&A

How to implement escrow in Move for peer-to-peer trades?

What’s the simplest way to build an escrow smart contract in Move for secure peer-to-peer trades?

  • Move
  • Smart Contract
0
2
Share
Comments
.

Answers

2
shamueely.
Sep 1 2025, 12:58

You build a simple, safe escrow in Move by modeling the deal as a single Escrow resource (the state machine) and gating sensitive actions with capabilities, so you deposit assets from both parties, release atomically when conditions are met, and allow a refund path on timeout; in practice you define an Escrow object keyed by the two participants, store the coins (or object IDs) inside, emit events on each step for your UI, and accept a ReleaseCap only when both deposits are present—this avoids reentrancy, prevents double-spend, and keeps everything all-or-nothing in one transaction; here’s a compact pattern you can copy:

module p2p::escrow {
    use sui::coin::{Self, Coin};
    use sui::sui::SUI;
    use sui::tx_context::{Self, TxContext};
    use sui::clock;

    struct Escrow has key {
        a: address,
        b: address,
        a_paid: bool,
        b_paid: bool,
        coin_a: option::Option<Coin<SUI>>,
        coin_b: option::Option<Coin<SUI>>,
        deadline_ms: u64,
    }

    struct ReleaseCap has key {} // authority to finalize

    public entry fun open(a: &signer, b: address, deadline_ms: u64, ctx: &mut TxContext) {
        tx_context::transfer(Escrow {
            a: signer::address_of(a), b, a_paid: false, b_paid: false,
            coin_a: option::none(), coin_b: option::none(), deadline_ms
        }, signer::address_of(a));
        tx_context::transfer(ReleaseCap {}, signer::address_of(a));
    }

    public entry fun deposit_for_a(a: &signer, mut e: Escrow, c: Coin<SUI>, ctx: &mut TxContext) {
        assert!(signer::address_of(a) == e.a, 0);
        assert!(!e.a_paid, 1);
        e.coin_a = option::some(c);
        e.a_paid = true;
        tx_context::transfer(e, signer::address_of(a));
    }

    public entry fun deposit_for_b(b: &signer, mut e: Escrow, c: Coin<SUI>, ctx: &mut TxContext) {
        assert!(signer::address_of(b) == e.b, 2);
        assert!(!e.b_paid, 3);
        e.coin_b = option::some(c);
        e.b_paid = true;
        tx_context::transfer(e, signer::address_of(b));
    }

    public entry fun release(mut e: Escrow, _cap: &ReleaseCap, ctx: &mut TxContext) {
        assert!(e.a_paid && e.b_paid, 4);
        let ca = option::extract(&mut e.coin_a);
        let cb = option::extract(&mut e.coin_b);
        coin::transfer(ca, e.b);
        coin::transfer(cb, e.a);
        // e is consumed; atomic swap complete
    }

    public entry fun refund(mut e: Escrow, now: &clock::Clock, ctx: &mut TxContext) {
        assert!(clock::now_millis(now) >= e.deadline_ms, 5);
        if (e.a_paid) coin::transfer(option::extract(&mut e.coin_a), e.a);
        if (e.b_paid) coin::transfer(option::extract(&mut e.coin_b), e.b);
        // e is consumed; both parties refunded after timeout
    }
}
// transaction block: both deposit then release (TypeScript, @mysten/sui.js/transactions)
import { Transaction } from '@mysten/sui.js/transactions';
const tx = new Transaction();
const escrowId = '0x...';            // the Escrow object ID
const capId = '0x...';               // ReleaseCap object ID held by opener
const coinA = tx.splitCoins(tx.gas, [tx.pure.u64(1_000_000)]); // example SUI split
tx.moveCall({ target: 'p2p::escrow::deposit_for_a', arguments: [tx.object(escrowId), coinA] });
const coinB = tx.splitCoins(tx.gas, [tx.pure.u64(1_000_000)]);
tx.moveCall({ target: 'p2p::escrow::deposit_for_b', arguments: [tx.object(escrowId), coinB] });
tx.moveCall({ target: 'p2p::escrow::release', arguments: [tx.object(escrowId), tx.object(capId)] });
// sign + execute: client.signAndExecuteTransaction({ transaction: tx });
0
Comments
.
NakaMoto. SUI.
Sep 1 2025, 18:42

Here’s the simplest pattern for building a peer-to-peer escrow in Move on Sui, with resource safety and ownership transfer enforced by the type system:

Core Principles

  1. Funds as Owned Objects: The seller deposits their asset (SUI or custom coin) into an escrow object that is owned by the smart contract until release.

  2. Capability Tokens: Grant the buyer and seller each an owned capability so they can call specific functions (e.g., cancel or complete).

  3. No Global State: Each escrow is its own object to avoid contention and enable parallel execution.

  4. Finalize via Explicit Action: The Move module enforces the release conditions.


Example: Simple Escrow Move Module

module p2p::escrow { use sui::object::{Self, UID}; use sui::transfer; use sui::tx_context::TxContext; use sui::coin::{Coin, SUI};

/// Represents the escrow state
struct Escrow<phantom T> has key {
    id: UID,
    seller: address,
    buyer: address,
    asset: Coin<T>,  // Locked asset
}

/// Capability for buyer to claim
struct BuyerCap has key { id: UID }

/// Capability for seller to cancel
struct SellerCap has key { id: UID }

/// Initialize an escrow with the seller's asset
public entry fun create<T>(
    buyer: address,
    asset: Coin<T>,
    ctx: &mut TxContext
) {
    let escrow = Escrow<T> {
        id: object::new(ctx),
        seller: tx_context::sender(ctx),
        buyer,
        asset,
    };
    let buyer_cap = BuyerCap { id: object::new(ctx) };
    let seller_cap = SellerCap { id: object::new(ctx) };

    transfer::public_transfer(buyer_cap, buyer);
    transfer::public_transfer(seller_cap, tx_context::sender(ctx));
    transfer::share_object(escrow);
}

/// Buyer completes the trade by claiming the asset
public entry fun claim<T>(
    escrow: &mut Escrow<T>,
    _cap: BuyerCap,
    ctx: &mut TxContext
) {
    assert!(tx_context::sender(ctx) == escrow.buyer, 0);
    let asset = escrow.asset;
    escrow.asset = Coin::zero<T>(); // Clear
    transfer::public_transfer(asset, tx_context::sender(ctx));
}

/// Seller cancels the trade if buyer doesn't complete
public entry fun cancel<T>(
    escrow: &mut Escrow<T>,
    _cap: SellerCap,
    ctx: &mut TxContext
) {
    assert!(tx_context::sender(ctx) == escrow.seller, 0);
    let asset = escrow.asset;
    escrow.asset = Coin::zero<T>();
    transfer::public_transfer(asset, tx_context::sender(ctx));
}

}


Key Features

Resource Safety: Assets are locked in Escrow (a resource) and cannot be duplicated or lost.

No Double Spend: Buyer and seller actions require their respective capability.

Parallelizable: Each escrow is an independent object, so multiple escrows run in parallel.

Best Practices

Add timeouts (e.g., block height checks) before cancel is allowed.

Use events to log escrow creation, claim, and cancel for off-chain analytics.

Support arbitrary assets by using a generic type parameter for the coin type.

0
Comments
.

Do you know the answer?

Please log in and share it.

Move is an executable bytecode language used to implement custom transactions and smart contracts.

249Posts553Answers
Sui.X.Peera.

Earn Your Share of 1000 Sui

Gain Reputation Points & Get Rewards for Helping the Sui Community Grow.

Reward CampaignSeptember