Post
Share your knowledge.
Time-Dependent Logic in Sui Smart Contracts
What’s the best way to encode time-dependent logic in Sui smart contracts (e.g., vesting schedules) without relying on unreliable external timestamp oracles?
- Sui
Answers
1I’ve run into this a lot when designing modules like vesting schedules, auctions, or lending protocols where I want time-dependent logic but can’t just trust an external timestamp oracle. Sui doesn’t provide block timestamps (because they’re non-deterministic across validators), so we need to rely on deterministic, consensus-available primitives. Here’s how I approach it:
🔑 Principles for Encoding Time in Sui
-
Use Epochs, Not Timestamps
- Sui exposes the current epoch as part of the system state.
- Epochs are monotonically increasing and agreed upon by validators, so they’re safe for deterministic smart contract logic.
-
Encode Relative, Not Absolute Time
- Instead of saying “unlock at 2025-10-01,” I encode “unlock after N epochs from creation.”
- This ensures the schedule works deterministically no matter when the contract was deployed.
-
Capability-Gated Access
- Vesting/release functions require a capability object (e.g.,
VestingCap) to enforce only the intended party can unlock assets.
- Vesting/release functions require a capability object (e.g.,
🏗️ Example: Vesting Schedule in Move
module vesting::schedule {
use sui::tx_context::TxContext;
use sui::object::{Self, UID};
use sui::clock;
/// Vesting contract object
struct Vesting has key {
id: UID,
beneficiary: address,
start_epoch: u64,
cliff_epoch: u64,
duration_epochs: u64,
total_amount: u64,
claimed_amount: u64,
}
/// Capability for the beneficiary
struct VestingCap has drop {}
/// Create a new vesting schedule
public entry fun create_vesting(
beneficiary: address,
cliff_epochs: u64,
duration_epochs: u64,
total_amount: u64,
ctx: &mut TxContext
): (Vesting, VestingCap) {
let current_epoch = clock::epoch(ctx);
(
Vesting {
id: object::new(ctx),
beneficiary,
start_epoch: current_epoch,
cliff_epoch: current_epoch + cliff_epochs,
duration_epochs,
total_amount,
claimed_amount: 0,
},
VestingCap {}
)
}
/// Claim vested tokens
public entry fun claim(
vesting: &mut Vesting,
cap: &VestingCap,
ctx: &TxContext
): u64 {
let current_epoch = clock::epoch(ctx);
// Must be past cliff
assert!(current_epoch >= vesting.cliff_epoch, 1);
// Calculate vested portion
let elapsed = current_epoch - vesting.start_epoch;
let vested_amount = if (elapsed >= vesting.duration_epochs) {
vesting.total_amount
} else {
(vesting.total_amount * elapsed) / vesting.duration_epochs
};
// Calculate claimable
let claimable = vested_amount - vesting.claimed_amount;
vesting.claimed_amount = vested_amount;
claimable
}
}
⚡ How This Works
clock::epoch(ctx)gives me the deterministic epoch number (like a secure block counter).- I define the vesting schedule in epochs, not seconds.
claimchecks how many epochs have passed sincestart_epoch, calculates the vested amount, and releases only what’s available.- Assets (e.g., SUI or custom coin types) would be transferred using this claim function.
🚀 Extensions
- Epoch Anchoring: Use epochs as “time slices” (e.g., 1 epoch = 24 hours).
- Programmable Cliffs: Allow multiple cliffs (good for DAO vesting).
- Composable Vesting NFTs: Represent a
Vestingobject as an NFT that can be transferred, enabling secondary markets in locked positions. - Fairness via Validators: Epoch transitions are agreed upon at consensus, so they’re not manipulable by a single actor.
✅ Summary (my dev POV): I never rely on wall-clock time in Sui contracts. Instead, I build epoch-based schedules that are deterministic and safe under consensus. For vesting, auctions, or rentals, I encode relative epoch offsets (cliffs, durations) and enforce release logic through capability checks.
Let’s take the epoch-based vesting logic I showed before and extend it so each vesting schedule is wrapped as an NFT. This way, users can trade, transfer, or even collateralize their vesting rights without breaking safety.
🔑 Design Principles
-
NFT as Vesting Handle
- Represent each vesting contract as a transferable NFT.
- Whoever owns the NFT owns the right to claim the vested tokens.
-
Keep Capability Security
- Instead of giving a separate
VestingCap, make the NFT itself the capability. - Only the holder of the NFT can call
claim.
- Instead of giving a separate
-
Composable & Tradable
- NFT can be sold on Sui NFT marketplaces.
- Vesting logic remains intact no matter who holds it.
🏗️ NFT-Based Vesting in Move
module vesting::nft_vesting {
use sui::tx_context::TxContext;
use sui::object::{Self, UID};
use sui::clock;
/// NFT that represents ownership of a vesting schedule
struct VestingNFT has key, store {
id: UID,
name: vector<u8>,
description: vector<u8>,
image_url: vector<u8>, // optional NFT metadata
vesting: Vesting, // embedded vesting data
}
/// Core vesting data
struct Vesting has store {
beneficiary: address, // original beneficiary (for reference)
start_epoch: u64,
cliff_epoch: u64,
duration_epochs: u64,
total_amount: u64,
claimed_amount: u64,
}
/// Create a new vesting NFT
public entry fun create_vesting_nft(
beneficiary: address,
cliff_epochs: u64,
duration_epochs: u64,
total_amount: u64,
name: vector<u8>,
description: vector<u8>,
image_url: vector<u8>,
ctx: &mut TxContext
): VestingNFT {
let current_epoch = clock::epoch(ctx);
let vesting = Vesting {
beneficiary,
start_epoch: current_epoch,
cliff_epoch: current_epoch + cliff_epochs,
duration_epochs,
total_amount,
claimed_amount: 0,
};
VestingNFT {
id: object::new(ctx),
name,
description,
image_url,
vesting,
}
}
/// Claim vested tokens (only NFT owner can do this)
public entry fun claim(
nft: &mut VestingNFT,
ctx: &TxContext
): u64 {
let current_epoch = clock::epoch(ctx);
// Must be past cliff
assert!(current_epoch >= nft.vesting.cliff_epoch, 1);
// Calculate vested portion
let elapsed = current_epoch - nft.vesting.start_epoch;
let vested_amount = if (elapsed >= nft.vesting.duration_epochs) {
nft.vesting.total_amount
} else {
(nft.vesting.total_amount * elapsed) / nft.vesting.duration_epochs
};
// Claimable = vested - already claimed
let claimable = vested_amount - nft.vesting.claimed_amount;
nft.vesting.claimed_amount = vested_amount;
claimable
}
}
⚡ How This Works
VestingNFTis a key object, so it lives in the owner’s account.- If the NFT is transferred, the new owner inherits the right to claim vested tokens.
- The vesting math is the same as before (epoch-based cliff + linear unlock).
claimreturns the claimable amount (e.g., could be used to mint or transfer SUI or another coin type).
🌍 Extensions
- Composable Markets: These NFTs can be listed on Sui marketplaces (allowing secondary markets in locked tokens).
- Split Vesting Rights: Users could split one vesting NFT into multiple fractional NFTs (think tranches).
- Collateral Use: Vesting NFTs can be deposited into lending protocols as collateral while still enforcing unlock rules.
- Metadata Hooks: The NFT metadata (
name,description,image_url) makes vesting positions more user-friendly in wallets.
✅ Summary (my dev POV): By encoding vesting schedules as NFTs with embedded epoch-based logic, I enable full composability in the Sui ecosystem. Holders can transfer or trade their locked positions, while the protocol enforces safe, deterministic unlocking under Move’s capability model.
Let’s consolidate everything into one clean Sui Move module:
This single module supports:
- Creating vesting NFTs (with a beneficiary, schedule, and vault).
- Claiming vested tokens based on epoch logic.
- Splitting NFTs into fractional tranches.
- Merging NFTs back if their schedules match.
🏗️ Full vesting::vesting_nft Module
module vesting::vesting_nft {
use sui::tx_context::TxContext;
use sui::object::{Self, UID};
use sui::coin::{Self, Coin, split, join, value};
use sui::clock;
/// Vesting NFT metadata + vault
struct VestingNFT<phantom T> has key, store {
id: UID,
name: vector<u8>,
description: vector<u8>,
image_url: vector<u8>,
vesting: Vesting,
vault: Coin<T>,
}
/// Vesting schedule
struct Vesting has store {
beneficiary: address,
start_epoch: u64,
cliff_epoch: u64,
duration_epochs: u64,
total_amount: u64,
claimed_amount: u64,
}
/// --- 🔹 Create a vesting NFT
public entry fun create_vesting_nft<T>(
name: vector<u8>,
description: vector<u8>,
image_url: vector<u8>,
beneficiary: address,
start_epoch: u64,
cliff_epoch: u64,
duration_epochs: u64,
deposit: Coin<T>,
ctx: &mut TxContext
): VestingNFT<T> {
let amount = value(&deposit);
VestingNFT {
id: object::new(ctx),
name,
description,
image_url,
vesting: Vesting {
beneficiary,
start_epoch,
cliff_epoch,
duration_epochs,
total_amount: amount,
claimed_amount: 0,
},
vault: deposit,
}
}
/// --- 🔹 Claim vested tokens
public entry fun claim<T>(
nft: &mut VestingNFT<T>,
clock: &clock::Clock,
ctx: &mut TxContext
): Coin<T> {
let epoch = clock::epoch(clock);
// Nothing before start
if (epoch < nft.vesting.start_epoch) {
return coin::zero(ctx);
};
// If before cliff, nothing claimable
if (epoch < nft.vesting.cliff_epoch) {
return coin::zero(ctx);
};
// Compute vested fraction
let elapsed = epoch - nft.vesting.start_epoch;
let vested_amount =
if (elapsed >= nft.vesting.duration_epochs) {
nft.vesting.total_amount
} else {
nft.vesting.total_amount * elapsed / nft.vesting.duration_epochs
};
let claimable = vested_amount - nft.vesting.claimed_amount;
if (claimable == 0) {
return coin::zero(ctx);
};
nft.vesting.claimed_amount = nft.vesting.claimed_amount + claimable;
split(&mut nft.vault, claimable, ctx)
}
/// --- 🔹 Split vesting NFT into two
public entry fun split_vesting_nft<T>(
nft: &mut VestingNFT<T>,
split_amount: u64,
ctx: &mut TxContext
): VestingNFT<T> {
assert!(split_amount < nft.vesting.total_amount, 1);
// Update original
nft.vesting.total_amount = nft.vesting.total_amount - split_amount;
// Create new coin vault
let new_vault = split(&mut nft.vault, split_amount, ctx);
// Create new NFT
VestingNFT {
id: object::new(ctx),
name: nft.name,
description: nft.description,
image_url: nft.image_url,
vesting: Vesting {
beneficiary: nft.vesting.beneficiary,
start_epoch: nft.vesting.start_epoch,
cliff_epoch: nft.vesting.cliff_epoch,
duration_epochs: nft.vesting.duration_epochs,
total_amount: split_amount,
claimed_amount: 0,
},
vault: new_vault,
}
}
/// --- 🔹 Merge two vesting NFTs
public entry fun merge_vesting_nfts<T>(
target: &mut VestingNFT<T>,
other: VestingNFT<T>,
) {
// Ensure schedules match
assert!(target.vesting.beneficiary == other.vesting.beneficiary, 10);
assert!(target.vesting.start_epoch == other.vesting.start_epoch, 11);
assert!(target.vesting.cliff_epoch == other.vesting.cliff_epoch, 12);
assert!(target.vesting.duration_epochs == other.vesting.duration_epochs, 13);
// Merge state
target.vesting.total_amount = target.vesting.total_amount + other.vesting.total_amount;
target.vesting.claimed_amount = target.vesting.claimed_amount + other.vesting.claimed_amount;
// Merge vaults
join(&mut target.vault, other.vault);
// Burn other NFT
object::delete(other.id);
}
}
⚡ Features Recap
- Create: Anyone can deposit coins to mint a new vesting NFT for a beneficiary.
- Claim: Beneficiary withdraws vested funds per epoch logic.
- Split: Fractionalize a vesting NFT into smaller tranches for transfer/trade.
- Merge: Consolidate two NFTs back into one, if they share identical schedules.
Nice — I’ll add a safe, composable marketplace layer so vesting NFTs can be listed, bought, and transferred while preserving their vesting schedules and safety guarantees.
Goals I follow:
- The NFT remains the single capability for claiming; transferring the NFT transfers claim rights.
- Listings are resources tied to the seller + NFT; buying is atomic (buyer coin → seller, NFT → buyer).
- Marketplace supports optional fees/royalties and cancel-listing.
- Concurrency-safety: every relevant object is
acquiresd in the same entry function so Sui’s object-level locking makes operations atomic and race-free. - Minimal trust: marketplace code only moves listed NFTs and coins; no extra custody of the NFT vault is left behind.
Below I produce:
- an updated Move module (extensions added to the
vesting::vesting_nftmodule), - brief TS client flow (how to list/buy),
- security notes & invariants to check with Move Prover.
1) Move: Marketplace extensions (add to your existing vesting::vesting_nft module)
Add these types & entry functions: Listing resource, list_for_sale, buy_listing, cancel_listing. I use generic coin T for payment token so the marketplace can use SUI or any Coinobject::delete) with the actual Sui stdlib calls in your SDK/version.
module vesting::vesting_nft {
use sui::tx_context::TxContext;
use sui::object::{Self, UID};
use sui::coin::{Self, Coin, split, join, value};
use sui::clock;
// --- VestingNFT and Vesting types (unchanged) ---
struct VestingNFT<phantom T> has key, store {
id: UID,
name: vector<u8>,
description: vector<u8>,
image_url: vector<u8>,
vesting: Vesting,
vault: Coin<T>,
}
struct Vesting has store {
beneficiary: address,
start_epoch: u64,
cliff_epoch: u64,
duration_epochs: u64,
total_amount: u64,
claimed_amount: u64,
}
// --- Marketplace types ---
/// A Listing resource represents an NFT put up for sale by a seller
/// listing_price is the asking price in Coin<P> (payment token)
struct Listing<phantom P> has key {
id: UID, // listing object id
nft_owner: address, // seller address (owner at listing time)
nft_obj_id: address, // address of the VestingNFT object (object id)
price: u64, // price amount (we store as u64 for simplicity)
payment_token_marker: address, // optional marker for token type; depends on your token registry
fee_basis_points: u64, // marketplace fee in basis points (e.g., 250 = 2.5%)
royalty_basis_points: u64, // royalty to original issuer, optional
}
// -------- Helpers (pseudo) --------
// get_object_owner: returns current owner address of an object id. Implementation depends on Sui stdlib.
native fun get_object_owner(obj_id: address): address;
// transfer_vesting_nft: move the listed VestingNFT from seller -> buyer.
// Implementation notes: This will be done by passing the NFT object in the tx arguments (Sui model).
// The function signature below assumes the caller includes the NFT object as the first param: `nft: VestingNFT<P>`
// so no special native helper is required in that case.
// -------- Marketplace entrypoints --------
/// Seller creates a listing for a VestingNFT they own.
/// `nft` must be the actual VestingNFT object (so owner must include it in transaction).
public entry fun list_for_sale<P, PayT>(
seller: &signer,
mut nft: VestingNFT<P>,
price: u64,
fee_basis_points: u64,
royalty_basis_points: u64,
ctx: &mut TxContext
): Listing<PayT> {
let seller_addr = signer::address_of(seller);
// ownership check: Require seller is current owner of the NFT. Because NFT was provided as `nft: VestingNFT<P>`,
// the runtime ensures seller included the object. Good pattern is to pass object as owned parameter.
// Create listing
let listing = Listing {
id: object::new(ctx),
nft_owner: seller_addr,
nft_obj_id: /* nft.id */ object::id(&nft), // pseudo: extract object id
price,
payment_token_marker: /* optional */ @0x0,
fee_basis_points,
royalty_basis_points,
};
// We must keep NFT somewhere accessible for sale. Options:
// 1) Move NFT into the listing object (listing owns the NFT) — safe and simple.
// 2) Keep NFT with seller but mark listed flag — more complex concurrency semantics.
// We choose (1) for atomicity: embed the NFT into the listing so buying simply moves listing.nft -> buyer.
//
// To do that, change Listing to include the NFT resource rather than only its id.
// For simplicity in this snippet, assume listing will own the nft. Let's adapt Listing accordingly below.
//
// Clean approach: create Listing that contains the VestingNFT:
// move listing.nft = nft; return listing
//
// (Because Move typing is strict, actually define Listing with a `nft: VestingNFT<P>` field.)
//
listing
}
// Revised Listing definition (preferred)
struct ListingV2<phantom P, phantom PayT> has key {
id: UID,
seller: address,
nft: VestingNFT<P>,
price: u64,
fee_basis_points: u64,
royalty_basis_points: u64,
payment_token_marker: address,
}
/// Create listing that *owns* the NFT inside the listing (atomically moves NFT into listing)
public entry fun list_for_sale_v2<P, PayT>(
seller: &signer,
nft: VestingNFT<P>, // seller passes ownership of nft into this call
price: u64,
fee_basis_points: u64,
royalty_basis_points: u64,
ctx: &mut TxContext
): ListingV2<P, PayT> {
let seller_addr = signer::address_of(seller);
ListingV2 {
id: object::new(ctx),
seller: seller_addr,
nft,
price,
fee_basis_points,
royalty_basis_points,
payment_token_marker: @0x0
}
}
/// Buyer purchases the listing by paying a Coin<PayT>. This call is atomic and `acquires` the Listing (which owns the NFT).
/// Payment coin is included by the buyer in the transaction as `payment: Coin<PayT>`.
public entry fun buy_listing<P, PayT>(
buyer: &signer,
listing: ListingV2<P, PayT>, // buyer provides the listing object (owned by marketplace)
mut payment: Coin<PayT>,
ctx: &mut TxContext
) {
let buyer_addr = signer::address_of(buyer);
// 1) price check
let price = listing.price;
assert!(value(&payment) >= price, 100);
// 2) compute marketplace fee & royalty
let fee = (price * listing.fee_basis_points) / 10_000;
let royalty = (price * listing.royalty_basis_points) / 10_000;
let seller_amount = price - fee - royalty;
// 3) split payment: extract `price` from payment coin into pieces
let (price_coin, rest) = split_payment_exact(&mut payment, price, ctx);
// price_coin contains exactly `price`, rest returned to buyer later
// split `price_coin` into seller_coin, fee_coin, royalty_coin
let (seller_coin, temp) = split(&mut price_coin, seller_amount, ctx);
let (fee_coin, royalty_coin) = split(&mut temp, fee, ctx); // temp is now price - seller_amount
// 4) transfer amounts:
// send seller_coin to listing.seller
// send fee_coin to marketplace fee receiver (e.g., a hardcoded address)
// send royalty_coin to original issuer if needed (we omit issuer tracking here - add field if required)
// For transfers, in Sui Move, you typically return coins to the appropriate signers/recipients.
// In this template we assume transfer_by_address helper exists (or the caller constructs separate txs to move coins).
transfer_coin_to_address(seller_coin, listing.seller);
transfer_coin_to_address(fee_coin, @0xMarketplaceFeeReceiver);
// if royalty exists:
if (royalty > 0) {
transfer_coin_to_address(royalty_coin, @0xRoyaltyReceiver);
}
// 5) transfer NFT to buyer: listing.nft is moved into buyer's ownership by returning it as result or by moving to buyer in this tx.
let nft_to_transfer = listing.nft;
// Destroy listing object (since nft moved)
object::delete(listing.id);
// The buyer now owns nft_to_transfer (returned as result of entry function)
// Move requires a return value to actually hand nft to caller:
// So change signature to `): VestingNFT<P>` and `return nft_to_transfer;`
}
/// Cancel a listing and return NFT to seller (only seller can cancel)
public entry fun cancel_listing<P, PayT>(
seller: &signer,
listing: ListingV2<P, PayT>,
ctx: &mut TxContext
) {
let seller_addr = signer::address_of(seller);
assert!(listing.seller == seller_addr, 200);
// Return NFT to seller by returning listing.nft as result and destroy listing.id
let nft_back = listing.nft;
object::delete(listing.id);
// return nft_back;
}
// ---------------- Utility helpers (pseudocode) ----------------
/// Split exactly `amount` from a coin; returns (extractedCoin, remainderCoin)
fun split_payment_exact<PayT>(coin: &mut Coin<PayT>, amount: u64, ctx: &mut TxContext): (Coin<PayT>, Coin<PayT>) {
// Implementation depends on coin::split function semantics; if only split by amount exists, use that.
// Placeholder pseudo:
let extracted = split(coin, amount, ctx);
let remainder = /* coin now mutated as remainder */;
(extracted, remainder)
}
/// Transfer coin to given address (pseudocode). Actual Sui pattern: return coin to caller or use `transfer::transfer`
native fun transfer_coin_to_address<PayT>(c: Coin<PayT>, recipient: address);
}
Notes about the Move snippet
- I show
ListingV2as the safe pattern: the listing owns the NFT (so there is a single authoritative object for sale). This avoids races where seller tries to move the NFT while a buyer submits purchase.list_for_sale_v2takes ownership of the NFT and places it inside theListingV2object.buy_listingis atomic: itacquiresthe listing (which includes NFT) and the buyer includes payment coin. The function checks payment, splits coin into seller/fee/royalty, moves coins to recipients, and returns the NFT to buyer.- On Sui you must return the NFT resource to hand it to buyer or perform a transfer primitive. The Move signature should therefore actually return
VestingNFT<P>to give it to the buyer. I used inline pseudocode to show flow — adapt the function return types in your environment.- Replace
transfer_coin_to_addressandobject::idusage with actual Sui stdlib helpers (Sui Move stdlib evolves; adapt accordingly).
2) TypeScript client flows
A quick overview of how clients interact (using @mysten/sui.js flows):
-
List for sale
- Seller constructs a
TransactionBlock. - Adds the
VestingNFTobject as an input (the object to be moved). - Calls
list_for_sale_v2with price, fee, royalty, returning aListingV2object. - Sign & submit.
- Seller constructs a
-
Buy listing
- Buyer fetches listing object id and price.
- Buyer constructs
TransactionBlock, includesListingV2object id and aCoin<PayT>object with at leastprice. - Calls
buy_listing, which returns theVestingNFTto the buyer. - Sign & submit. Gas sponsorship or relayer possible.
-
Cancel listing
- Seller constructs tx including
ListingV2object id. - Calls
cancel_listing, which returns theVestingNFTback to seller. - Sign & submit.
- Seller constructs tx including
Make sure all calls include the actual objects (VestingNFT or ListingV2) as arguments so Sui locks them atomically.
3) Concurrency & safety guarantees
- Atomicity: because the listing owns the NFT,
buy_listingacquiresthe listing object and the buyer’s payment coin — Sui locks both objects during execution. No other tx can concurrently move the NFT or listing; either the buy completes or fails. - No ghost transfers: NFT vault inside the
VestingNFTmoves with the NFT; claim logic still works for the new owner seamlessly. - Reentrancy: Move and Sui’s single-writer model avoids classical reentrancy issues; still test for logical reentrancy (e.g., buy triggers event which off-chain watcher interprets incorrectly).
- Fees & royalties: computed in-module and transferred atomically as part of the purchase.
- Seller must own NFT to list: since
list_for_sale_v2requiresnft: VestingNFT<P>parameter (owned object), only current owner can call it.
4) Invariants & Move Prover checks to add
Add spec module invariants and prove with Move Prover:
- Listing owns NFT: For any
ListingV2, thenftfield is non-null and not referenced elsewhere. - No double-sell: A
VestingNFTcannot be in twoListingV2objects at once (resource typing helps). - Payment conservation: Sum of payment splits (seller + fee + royalty) equals price.
- Postconditions of buy_listing: After successful buy, listing is destroyed and buyer is owner of NFT.
These keep marketplace logic auditable and provable.
5) Operational tips
- Index events: Emit events for
Listed,Bought,Canceled. Indexers monitor and update off-chain marketplace UI. - Delist on transfer: Disallow transferring a listed NFT outside marketplace unless listing is canceled first (or auto-cancel listing on transfer). Enforce by requiring listing ownership to be a nested resource that prevents external transfer while listed.
- Fee & royalty receivers: Use explicit on-chain register addresses so marketplace operator or creators can change recipients through governance.
- Front-running: Because the listing owns the NFT, front-running by an attacker who sees signed
buytx is limited — include nonces or require the buyer to provide the exact listingidand price to reduce MEV risk. Consider signed offers (off-chain signed orders) and on-chain fill patterns for matching engines if you need order-book style market. - Off-chain orderbooks: For improved UX/scale, keep off-chain orderbook of signed
listmessages and calllist_for_sale_v2only when the seller wants to lock their NFT on-chain. Or keep NFT on-chain in listing and allow off-chain discovery.
6) Example quick test scenarios to run
-
Seller lists NFT A for 100 tokens; buyer buys with exactly 100 — assert:
- buyer now owns NFT A,
- seller received 100 - fees - royalties,
- listing object destroyed.
-
Two concurrent buyers submit buy txs — exactly one succeeds, the other fails due to object lock.
-
Seller lists, then attempts to transfer NFT outside marketplace — should be impossible because listing owns NFT (unless you explicitly design otherwise).
-
Seller cancels listing — NFT returns to seller.
Do you know the answer?
Please log in and share it.
Sui is a Layer 1 protocol blockchain designed as the first internet-scale programmable blockchain platform.
- How to Maximize Profit Holding SUI: Sui Staking vs Liquid Staking616
- Why does BCS require exact field order for deserialization when Move structs have named fields?65
- Multiple Source Verification Errors" in Sui Move Module Publications - Automated Error Resolution55
- Sui Move Error - Unable to process transaction No valid gas coins found for the transaction419
- Sui Transaction Failing: Objects Reserved for Another Transaction410