diff --git a/.github/workflows/sui-ci.yml b/.github/workflows/sui-ci.yml new file mode 100644 index 0000000..05f11a5 --- /dev/null +++ b/.github/workflows/sui-ci.yml @@ -0,0 +1,55 @@ +name: Sui Move CI + +on: + push: + branches: [ main, "feat/**" ] + paths: + - 'Sui/Move/**' + - '.github/workflows/sui-ci.yml' + pull_request: + branches: [ main ] + paths: + - 'Sui/Move/**' + - '.github/workflows/sui-ci.yml' + +jobs: + sui-ci: + name: Build, Test, Lint & Coverage (Sui Move) + runs-on: ubuntu-latest + + defaults: + run: + working-directory: Sui/Move/smart-contracts/escrow/contract + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Sui CLI + run: | + echo "Downloading precompiled Sui binary..." + curl -fsSL "https://github.com/MystenLabs/sui/releases/download/testnet-v1.76.1/sui-testnet-v1.76.1-ubuntu-x86_64.tgz" -o sui.tgz + tar -xzf sui.tgz + sudo find . -name "sui" -type f -exec mv {} /usr/local/bin/sui \; + sudo chmod +x /usr/local/bin/sui + + - name: Verify Sui installation + run: sui --version + + - name: 1. Build Package & Enforce Linter + run: sui move build --lint --warnings-are-errors + + - name: 2. Run Unit Tests + run: sui move test + + - name: 3. Code Coverage Summary + run: | + sui move test --coverage + sui move coverage summary + + - name: 4. Gas & Computation Units Benchmarks + run: | + echo "### 📊 Sui Move Gas & Computation Units Benchmarks" >> $GITHUB_STEP_SUMMARY + echo '```text' >> $GITHUB_STEP_SUMMARY + sui move test --statistics >> $GITHUB_STEP_SUMMARY 2>&1 + echo '```' >> $GITHUB_STEP_SUMMARY diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2df7331 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +# Agent skills +.agents/ +.claude/ +agent/ +skills-lock.json diff --git a/Sui/Move/smart-contracts/escrow/contract/.gitignore b/Sui/Move/smart-contracts/escrow/contract/.gitignore new file mode 100644 index 0000000..3f55832 --- /dev/null +++ b/Sui/Move/smart-contracts/escrow/contract/.gitignore @@ -0,0 +1,2 @@ +build/ +.coverage_map.mvcov diff --git a/Sui/Move/smart-contracts/escrow/contract/Move.lock b/Sui/Move/smart-contracts/escrow/contract/Move.lock new file mode 100644 index 0000000..d5c3024 --- /dev/null +++ b/Sui/Move/smart-contracts/escrow/contract/Move.lock @@ -0,0 +1,41 @@ +# Generated by move; do not edit +# This file should be checked in. + +[move] +version = 4 + +[pinned.local.MoveStdlib] +source = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/move-stdlib", rev = "718ae563a42fb4ba0d055588f81c704dcef58c25" } +use_environment = "local" +manifest_digest = "C4FE4C91DE74CBF223B2E380AE40F592177D21870DC2D7EB6227D2D694E05363" +deps = {} + +[pinned.local.Sui] +source = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "718ae563a42fb4ba0d055588f81c704dcef58c25" } +use_environment = "local" +manifest_digest = "98EF265044695258DF77084171BC5CCC94248AEEC2463AF6274326433A1512BA" +deps = { MoveStdlib = "MoveStdlib" } + +[pinned.local.escrow] +source = { root = true } +use_environment = "local" +manifest_digest = "5F75228E403340DD70556AFD567899C8C2E4BB556771AF4C11F408D875984A08" +deps = { std = "MoveStdlib", sui = "Sui" } + +[pinned.testnet.MoveStdlib] +source = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/move-stdlib", rev = "718ae563a42fb4ba0d055588f81c704dcef58c25" } +use_environment = "testnet" +manifest_digest = "C4FE4C91DE74CBF223B2E380AE40F592177D21870DC2D7EB6227D2D694E05363" +deps = {} + +[pinned.testnet.Sui] +source = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "718ae563a42fb4ba0d055588f81c704dcef58c25" } +use_environment = "testnet" +manifest_digest = "7AFB66695545775FBFBB2D3078ADFD084244D5002392E837FDE21D9EA1C6D01C" +deps = { MoveStdlib = "MoveStdlib" } + +[pinned.testnet.escrow] +source = { root = true } +use_environment = "testnet" +manifest_digest = "5745706258F61D6CE210904B3E6AE87A73CE9D31A6F93BE4718C442529332A87" +deps = { std = "MoveStdlib", sui = "Sui" } diff --git a/Sui/Move/smart-contracts/escrow/contract/Move.toml b/Sui/Move/smart-contracts/escrow/contract/Move.toml new file mode 100644 index 0000000..2cfdc6c --- /dev/null +++ b/Sui/Move/smart-contracts/escrow/contract/Move.toml @@ -0,0 +1,8 @@ +[package] +name = "escrow" +edition = "2024.beta" + +[dependencies] + +[environments] +local = "786400e0" diff --git a/Sui/Move/smart-contracts/escrow/contract/Published.toml b/Sui/Move/smart-contracts/escrow/contract/Published.toml new file mode 100644 index 0000000..8f41d8a --- /dev/null +++ b/Sui/Move/smart-contracts/escrow/contract/Published.toml @@ -0,0 +1,12 @@ +# Generated by Move +# This file contains metadata about published versions of this package in different environments +# This file SHOULD be committed to source control + +[published.local] +chain-id = "786400e0" +published-at = "0xf155a09874b07527d5bd7e8f919f26e09c9e4a324166425c9c4b4c0221e79f4e" +original-id = "0xf155a09874b07527d5bd7e8f919f26e09c9e4a324166425c9c4b4c0221e79f4e" +version = 1 +toolchain-version = "1.73.0" +build-config = { flavor = "sui", edition = "2024" } +upgrade-capability = "0xb5e86fdba15a266be9a659635ace8af7ac1e698e7087389b6f9f6777ae4f5c4c" diff --git a/Sui/Move/smart-contracts/escrow/contract/README.md b/Sui/Move/smart-contracts/escrow/contract/README.md index c879c8c..a241642 100644 --- a/Sui/Move/smart-contracts/escrow/contract/README.md +++ b/Sui/Move/smart-contracts/escrow/contract/README.md @@ -1,6 +1,267 @@ -# Escrow Contract Source (Move) +# Escrow Contract Source (Sui Move) -The contract implementation goes here. Keep the code heavily annotated to explain each -account, instruction, and state transition. +This directory contains the Sui Move smart contracts implementing asset locking, single-owner custodied swaps, and peer-to-peer shared object exchanges. -_This folder is intentionally empty for now._ +The package is modularly structured into three core modules: +1. **`lock`**: Low-level asset locking primitive using dynamic object fields (`Locked` and `Key`). +2. **`owned`**: Custodied / single-owner escrow pattern managed by a trusted third party. +3. **`shared`**: Peer-to-peer trustless escrow pattern built on Sui shared objects. + +--- + +## Architecture Overview + +```mermaid +flowchart TD + SubA["Asset A (key + store)"] -->|"lock::lock"| LockedA["Locked(T) + Key A"] + SubB["Asset B (key + store)"] -->|"lock::lock"| LockedB["Locked(U) + Key B"] + + subgraph EscrowModels ["Escrow Models"] + direction TB + LockedA -->|"Option 1: owned::create"| Custody["Owned Escrow (Custodian)"] + LockedB -->|"Option 1: owned::create"| Custody + + LockedA -->|"Option 2: shared::create"| SharedObj["Shared Object Escrow (P2P)"] + end + + Custody -->|"owned::swap"| Exchange1["Atomic Swap via Custodian"] + SharedObj -->|"shared::swap"| Exchange2["Atomic Direct P2P Swap"] +``` + +--- + +## 1. Asset Lock Primitive (`lock.move`) + +Before an asset can be placed in escrow, it is locked using `lock::lock`. This attaches the asset to a `Locked` container via **Dynamic Object Fields (DOF)** and produces a matching `Key`. + +### Sequence Diagram: Lock & Unlock + +```mermaid +sequenceDiagram + autonumber + actor Alice + participant LockModule as "lock::lock" + participant LockedObj as "Locked Container" + participant KeyObj as "Key Object" + + Alice->>LockModule: lock(item, ctx) + LockModule->>LockedObj: Create container & attach item via DOF + LockModule->>KeyObj: Mint Key (for: object::id(&key)) + LockModule-->>Alice: Return (Locked, Key) + + Note over Alice, KeyObj: Key ID must match locked.key + Alice->>LockModule: unlock(locked, key) + LockModule->>Alice: Extract item & destroy Locked container +``` + +### Actual Code Snippet (`lock.move`) + +```move +/// Lock an object into a `Locked` container and receive a `Key`. +public fun lock(obj: T, ctx: &mut TxContext): (Locked, Key) { + let key = Key { id: object::new(ctx) }; + let mut lock = Locked { + id: object::new(ctx), + key: object::id(&key), + }; + + // Attach object via Dynamic Object Field (DOF) + dof::add(&mut lock.id, LockedObjectKey {}, obj); + + (lock, key) +} +``` + +--- + +## 2. Custodied / Single-Owner Escrow (`owned.move`) + +In the single-owner model, both parties lock their assets and transfer their `Escrow` objects to a designated **Custodian** (trusted third-party). The custodian executes `owned::swap` to complete the trade. + +### Sequence Diagram: Custodied Swap Flow + +```mermaid +sequenceDiagram + autonumber + actor Alice + actor Bob + actor Custodian + participant OwnedModule as "owned::swap" + + Alice->>Custodian: create escrow (Escrow A for Bob's Key B) + Bob->>Custodian: create escrow (Escrow B for Alice's Key A) + + Custodian->>OwnedModule: swap(Escrow A, Escrow B) + Note over OwnedModule: Verify senders, recipients & exchange keys match + OwnedModule->>Bob: Transfer Asset A + OwnedModule->>Alice: Transfer Asset B +``` + +### Actual Code Snippet (`owned.move`) + +```move +/// Function for custodian (trusted third-party) to perform a swap between two parties. +public fun swap(obj1: Escrow, obj2: Escrow) { + let Escrow { + id: id1, + sender: sender1, + recipient: recipient1, + exchange_key: exchange_key1, + escrowed_key: escrowed_key1, + escrowed: escrowed1, + } = obj1; + + let Escrow { + id: id2, + sender: sender2, + recipient: recipient2, + exchange_key: exchange_key2, + escrowed_key: escrowed_key2, + escrowed: escrowed2, + } = obj2; + + // Validate reciprocity + assert!(recipient1 == sender2, EMismatchedSenderRecipient); + assert!(recipient2 == sender1, EMismatchedSenderRecipient); + assert!(exchange_key1 == escrowed_key2, EMismatchedExchangeObject); + assert!(exchange_key2 == escrowed_key1, EMismatchedExchangeObject); + + // Atomic execution & object deletion (claims storage rebates) + id1.delete(); + id2.delete(); + + transfer::public_transfer(escrowed1, recipient1); + transfer::public_transfer(escrowed2, recipient2); +} +``` + +--- + +## 3. Shared Object Peer-to-Peer Escrow (`shared.move`) + +In the P2P shared object model, Alice creates a shared `Escrow` object onchain. Anyone can view the offer, but **only the designated recipient with the matching `Locked` + `Key`** can execute `shared::swap`. + +### Sequence Diagram: Direct P2P Swap + +```mermaid +sequenceDiagram + autonumber + actor Alice + actor Bob + participant SharedEscrow as "Shared Escrow Object" + participant SharedModule as "shared::swap" + + Alice->>SharedEscrow: shared::create(Locked A, Key A, recipient: Bob, exchange_key: Key B) + Note over SharedEscrow: Escrow object is shared onchain + + Bob->>SharedModule: swap(Escrow, Key B, Locked B, ctx) + Note over SharedModule: Verify Bob is recipient & Key B matches exchange_key + SharedModule->>Bob: Return Escrowed Asset A + SharedModule->>Alice: Transfer Bob's Asset B (unlocks Locked B with Key B) + SharedModule->>SharedEscrow: Delete Escrow Object & Claim Storage Rebate +``` + +### Actual Code Snippet (`shared.move`) + +```move +/// Recipient swaps their Locked asset for the shared escrowed item. +public fun swap( + mut escrow: Escrow, + key: Key, + locked: Locked, + ctx: &mut TxContext, +): T { + // 1. Remove escrowed item from Dynamic Object Field + let escrowed = dof::remove(&mut escrow.id, EscrowedObjectKey {}); + + let Escrow { + id, + sender, + recipient, + exchange_key, + } = escrow; + + // 2. Enforce access control and key matching + assert!(recipient == ctx.sender(), EMismatchedSenderRecipient); + assert!(exchange_key == object::id(&key), EMismatchedExchangeObject); + + // 3. Unlock and transfer recipient's asset to sender + transfer::public_transfer(locked.unlock(key), sender); + + // 4. Emit event and delete Escrow ID (storage gas refund) + event::emit(EscrowSwapped { escrow_id: id.to_inner() }); + id.delete(); + + // 5. Return escrowed item to recipient + escrowed +} +``` + +--- + +## Localnet Setup & Package Deployment Guide + +Follow these step-by-step instructions to set up a local network node, fund your local wallet, build/test, and publish the Move escrow package. + +### Step 1: Start Sui Localnet +In your first terminal window, start a clean local validator node: + +```bash +sui start --with-faucet --force-regenesis +``` +*Leave this terminal window running as your local network server.* + +### Step 2: Switch CLI Environment & Fund Address +In a **second terminal window**: + +```bash +# 1. Switch active environment to local +sui client switch --env local + +# 2. Get local SUI gas tokens from the faucet +sui client faucet + +# 3. Check your active wallet balance +sui client balance +``` + +### Step 3: Build & Test Move Package +Navigate to the contract package directory: + +```bash +cd Sui/Move/smart-contracts/escrow/contract + +# Build package with strict linter checks +sui move build --lint --warnings-are-errors + +# Run unit test suite (13 passing tests) +sui move test +``` + +### Step 4: Publish Package to Localnet +Deploy the package to your running local node: + +```bash +sui client publish +``` + +Sui CLI automatically deploys `lock`, `owned`, and `shared` modules, creates your `UpgradeCap`, and records the deployment details in `Published.toml`. + +### Step 5: Inspect Deployed Package & Interact +```bash +# Query the published package details on localnet +sui client object + +# Query your Upgrade Capability +sui client object +``` + +--- + +## How Each Piece Comes Together + +1. **Locking Mechanism**: The `lock` module decouples an asset's storage footprint from its trade rights by issuing a `Key`. The asset remains safe in `Locked` until the `Key` is presented (`locked.key == object::id(&key)`). +2. **Flexible Execution Models**: + - **Custodian Flow (`owned.move`)**: Best for structured/brokered trades where a trusted entity verifies offchain conditions before executing swaps atomically. + - **Direct P2P Flow (`shared.move`)**: Completely trustless onchain exchange without intermediaries. +3. **Storage Gas Optimization**: Unpacking structs (`let Escrow { id, ... } = escrow;`) and calling `id.delete()` destroys the object's UID, **reclaiming storage rebates** on Sui and providing net negative storage gas costs upon swap completion. diff --git a/Sui/Move/smart-contracts/escrow/contract/sources/lock.move b/Sui/Move/smart-contracts/escrow/contract/sources/lock.move new file mode 100644 index 0000000..38b2438 --- /dev/null +++ b/Sui/Move/smart-contracts/escrow/contract/sources/lock.move @@ -0,0 +1,88 @@ +/// The `lock` module offers an API for wrapping any object that has +/// `store` and protecting it with a single-use `Key`. +/// +/// This is used to commit to swapping a particular object in a +/// particular, fixed state during escrow. +module escrow::lock; + +use sui::dynamic_object_field as dof; +use sui::event; + +/// The `name` of the DOF that holds the Locked object. +/// Allows better discoverability for the locked object. +public struct LockedObjectKey has copy, drop, store {} + +/// A wrapper that protects access to `obj` by requiring access to a `Key`. +/// +/// Used to ensure an object is not modified if it might be involved in a +/// swap. +/// +/// Object is added as a Dynamic Object Field so that it can still be looked-up. +public struct Locked has key, store { + id: UID, + key: ID, +} + +/// Key to open a locked object (consuming the `Key`) +public struct Key has key, store { id: UID } + +// === Error codes === + +/// The key does not match this lock. +const ELockKeyMismatch: u64 = 0; + +// === Public Functions === + +/// Lock `obj` and get a key that can be used to unlock it. +public fun lock(obj: T, ctx: &mut TxContext): (Locked, Key) { + let key = Key { id: object::new(ctx) }; + let mut lock = Locked { + id: object::new(ctx), + key: object::id(&key), + }; + + event::emit(LockCreated { + lock_id: object::id(&lock), + key_id: object::id(&key), + creator: ctx.sender(), + item_id: object::id(&obj), + }); + + // Adds the `object` as a DOF for the `lock` object + dof::add(&mut lock.id, LockedObjectKey {}, obj); + + (lock, key) +} + +/// Unlock the object in `locked`, consuming the `key`. Fails if the wrong +/// `key` is passed in for the locked object. +public fun unlock(mut locked: Locked, key: Key): T { + assert!(locked.key == object::id(&key), ELockKeyMismatch); + let Key { id } = key; + id.delete(); + + let obj = dof::remove(&mut locked.id, LockedObjectKey {}); + + event::emit(LockDestroyed { lock_id: object::id(&locked) }); + + let Locked { id, key: _ } = locked; + id.delete(); + obj +} + +// === Events === +public struct LockCreated has copy, drop { + /// The ID of the `Locked` object. + lock_id: ID, + /// The ID of the key that unlocks a locked object in a `Locked`. + key_id: ID, + /// The creator of the locked object. + creator: address, + /// The ID of the item that is locked. + item_id: ID, +} + +public struct LockDestroyed has copy, drop { + /// The ID of the `Locked` object. + lock_id: ID, +} \ No newline at end of file diff --git a/Sui/Move/smart-contracts/escrow/contract/sources/owned.move b/Sui/Move/smart-contracts/escrow/contract/sources/owned.move new file mode 100644 index 0000000..f911967 --- /dev/null +++ b/Sui/Move/smart-contracts/escrow/contract/sources/owned.move @@ -0,0 +1,151 @@ +/// An escrow for atomic swap of objects using single-owner transactions that +/// trusts a third party for liveness, but not safety. +/// +/// Swap via Escrow proceeds in three phases: +/// +/// 1. Both parties `lock` their objects, getting the `Locked` object and a +/// `Key`. Each party can `unlock` their object, to preserve liveness if the +/// other party stalls before completing the second stage. +/// +/// 2. Both parties register an `Escrow` object with the custodian, this +/// requires passing the locked object and its key. The key is consumed to +/// unlock the object, but its ID is remembered so the custodian can ensure +/// the right objects being swapped. The custodian is trusted to preserve +/// liveness. +/// +/// 3. The custodian swaps the locked objects as long as all conditions are met: +/// +/// - The sender of one Escrow is the recipient of the other and vice versa. +/// If this is not true, the custodian has incorrectly paired together this +/// swap. +/// +/// - The key of the desired object (`exchange_key`) matches the key the +/// other object was locked with (`escrowed_key`) and vice versa. + +/// If this is not true, it means the wrong objects are being swapped, +/// either because the custodian paired the wrong escrows together, or +/// because one of the parties tampered with their object after locking it. +/// +/// The key in question is the ID of the `Key` object that unlocked the +/// `Locked` object that the respective objects resided in immediately +/// before being sent to the custodian. +module escrow::owned; + +use escrow::lock::{Locked, Key}; + +/// An object held in escrow +public struct Escrow has key { + id: UID, + /// Owner of `escrowed` + sender: address, + /// Intended recipient + recipient: address, + /// The ID of the key that opens the lock on the object sender wants + /// from recipient. + exchange_key: ID, + /// The ID of the key that locked the escrowed object, before it was + /// escrowed. + escrowed_key: ID, + /// The escrowed object. + escrowed: T, +} + +// === Error codes === + +/// The `sender` and `recipient` of the two escrowed objects do not match +const EMismatchedSenderRecipient: u64 = 0; + +/// The `exchange_key` fields of the two escrowed objects do not match +const EMismatchedExchangeObject: u64 = 1; + +// === Public Functions === + +/// `ctx.sender()` requests a swap with `recipient` of a locked +/// object `locked` in exchange for an object referred to by `exchange_key`. +/// The swap is performed by a third-party, `custodian`, that is trusted to +/// maintain liveness, but not safety (the only actions they can perform are +/// to successfully progress the swap). +/// +/// `locked` will be unlocked with its corresponding `key` before being sent +/// to the custodian, but the underlying object is still not accessible +/// until after the swap has executed successfully, or the custodian returns +/// the object. +/// +/// `exchange_key` is the ID of a `Key` that unlocks the sender's desired +/// object. Gating the swap on the key ensures that it will not succeed if +/// the desired object is tampered with after the sender's object is held in +/// escrow, because the recipient would have to consume the key to tamper +/// with the object, and if they re-locked the object it would be protected +/// by a different, incompatible key. +public fun create( + key: Key, + locked: Locked, + exchange_key: ID, + recipient: address, + custodian: address, + ctx: &mut TxContext, +) { + let escrow = Escrow { + id: object::new(ctx), + sender: ctx.sender(), + recipient, + exchange_key, + escrowed_key: object::id(&key), + escrowed: locked.unlock(key), + }; + + transfer::transfer(escrow, custodian); +} + +/// Function for custodian (trusted third-party) to perform a swap between +/// two parties. Fails if their senders and recipients do not match, or if +/// their respective desired objects do not match. +public fun swap(obj1: Escrow, obj2: Escrow) { + let Escrow { + id: id1, + sender: sender1, + recipient: recipient1, + exchange_key: exchange_key1, + escrowed_key: escrowed_key1, + escrowed: escrowed1, + } = obj1; + + let Escrow { + id: id2, + sender: sender2, + recipient: recipient2, + exchange_key: exchange_key2, + escrowed_key: escrowed_key2, + escrowed: escrowed2, + } = obj2; + id1.delete(); + id2.delete(); + + // Make sure the sender and recipient match each other + assert!(sender1 == recipient2, EMismatchedSenderRecipient); + assert!(sender2 == recipient1, EMismatchedSenderRecipient); + + // Make sure the objects match each other and haven't been modified + // (they remain locked). + assert!(escrowed_key1 == exchange_key2, EMismatchedExchangeObject); + assert!(escrowed_key2 == exchange_key1, EMismatchedExchangeObject); + + // Do the actual swap + transfer::public_transfer(escrowed1, recipient1); + transfer::public_transfer(escrowed2, recipient2); +} + +/// The custodian can always return an escrowed object to its original +/// owner. +public fun return_to_sender(obj: Escrow) { + let Escrow { + id, + sender, + recipient: _, + exchange_key: _, + escrowed_key: _, + escrowed, + } = obj; + id.delete(); + transfer::public_transfer(escrowed, sender); +} diff --git a/Sui/Move/smart-contracts/escrow/contract/sources/shared.move b/Sui/Move/smart-contracts/escrow/contract/sources/shared.move new file mode 100644 index 0000000..1376f74 --- /dev/null +++ b/Sui/Move/smart-contracts/escrow/contract/sources/shared.move @@ -0,0 +1,157 @@ +/// An escrow for atomic swap of objects using shared objects without a trusted +/// third party. +/// +/// The protocol consists of three phases: +/// +/// 1. One party `lock`s their object, getting a `Locked` object and its `Key`. +/// This party can `unlock` their object to preserve livness if the other +/// party stalls before completing the second stage. +/// +/// 2. The other party registers a publicly accessible, shared `Escrow` object. +/// This effectively locks their object at a particular version as well, +/// waiting for the first party to complete the swap. The second party is +/// able to request their object is returned to them, to preserve liveness as +/// well. +/// +/// 3. The first party sends their locked object and its key to the shared +/// `Escrow` object. This completes the swap, as long as all conditions are +/// met: +/// +/// - The sender of the swap transaction is the recipient of the `Escrow`. +/// +/// - The key of the desired object (`exchange_key`) in the escrow matches +/// the key supplied in the swap. +/// +/// - The key supplied in the swap unlocks the `Locked`. +module escrow::shared; + +use escrow::lock::{Locked, Key}; +use sui::dynamic_object_field as dof; +use sui::event; + +/// The `name` of the DOF that holds the Escrowed object. +/// Allows easy discoverability for the escrowed object. +public struct EscrowedObjectKey has copy, drop, store {} + +/// An object held in escrow +/// +/// The escrowed object is added as a Dynamic Object Field so it can still be looked-up. +public struct Escrow has key, store { + id: UID, + /// Owner of `escrowed` + sender: address, + /// Intended recipient + recipient: address, + /// ID of the key that opens the lock on the object sender wants from + /// recipient. + exchange_key: ID, +} + +// === Error codes === + +/// The `sender` and `recipient` of the two escrowed objects do not match +const EMismatchedSenderRecipient: u64 = 0; + +/// The `exchange_for` fields of the two escrowed objects do not match +const EMismatchedExchangeObject: u64 = 1; + +// === Public Functions === + +public fun create( + escrowed: T, + exchange_key: ID, + recipient: address, + ctx: &mut TxContext, +) { + let mut escrow = Escrow { + id: object::new(ctx), + sender: ctx.sender(), + recipient, + exchange_key, + }; + + event::emit(EscrowCreated { + escrow_id: object::id(&escrow), + key_id: exchange_key, + sender: escrow.sender, + recipient, + item_id: object::id(&escrowed), + }); + + dof::add(&mut escrow.id, EscrowedObjectKey {}, escrowed); + + transfer::public_share_object(escrow); +} + +/// The `recipient` of the escrow can exchange `obj` with the escrowed item +public fun swap( + mut escrow: Escrow, + key: Key, + locked: Locked, + ctx: &mut TxContext, +): T { + let escrowed = dof::remove(&mut escrow.id, EscrowedObjectKey {}); + + let Escrow { + id, + sender, + recipient, + exchange_key, + } = escrow; + + assert!(recipient == ctx.sender(), EMismatchedSenderRecipient); + assert!(exchange_key == object::id(&key), EMismatchedExchangeObject); + + // Do the actual swap + transfer::public_transfer(locked.unlock(key), sender); + + event::emit(EscrowSwapped { + escrow_id: id.to_inner(), + }); + + id.delete(); + + escrowed +} + +/// The `creator` can cancel the escrow and get back the escrowed item +public fun return_to_sender(mut escrow: Escrow, ctx: &mut TxContext): T { + event::emit(EscrowCancelled { + escrow_id: object::id(&escrow), + }); + + let escrowed = dof::remove(&mut escrow.id, EscrowedObjectKey {}); + + let Escrow { + id, + sender, + recipient: _, + exchange_key: _, + } = escrow; + + assert!(sender == ctx.sender(), EMismatchedSenderRecipient); + id.delete(); + escrowed +} + +// === Events === +public struct EscrowCreated has copy, drop { + /// the ID of the escrow that was created + escrow_id: ID, + /// The ID of the `Key` that unlocks the requested object. + key_id: ID, + /// The id of the sender who'll receive `T` upon swap + sender: address, + /// The (original) recipient of the escrowed object + recipient: address, + /// The ID of the escrowed item + item_id: ID, +} + +public struct EscrowSwapped has copy, drop { + escrow_id: ID, +} + +public struct EscrowCancelled has copy, drop { + escrow_id: ID, +} diff --git a/Sui/Move/smart-contracts/escrow/contract/tests/lock_tests.move b/Sui/Move/smart-contracts/escrow/contract/tests/lock_tests.move new file mode 100644 index 0000000..3ce2bab --- /dev/null +++ b/Sui/Move/smart-contracts/escrow/contract/tests/lock_tests.move @@ -0,0 +1,37 @@ +#[test_only] +module escrow::lock_tests; + +use sui::coin::{Self, Coin}; +use sui::sui::SUI; +use sui::test_scenario::{Self as ts, Scenario}; + +use escrow::lock::{Self, ELockKeyMismatch}; + +fun test_coin(ts: &mut Scenario): Coin { + coin::mint_for_testing(42, ts.ctx()) +} + +#[test] +fun test_lock_unlock() { + let mut ts = ts::begin(@0xA); + let coin = test_coin(&mut ts); + + let (lock, key) = lock::lock(coin, ts.ctx()); + let coin = lock.unlock(key); + + coin.burn_for_testing(); + ts.end(); +} + +#[test] +#[expected_failure(abort_code = ELockKeyMismatch, location = lock)] +fun test_lock_key_mismatch() { + let mut ts = ts::begin(@0xA); + let coin = test_coin(&mut ts); + let another_coin = test_coin(&mut ts); + let (l, _k) = lock::lock(coin, ts.ctx()); + let (_l, k) = lock::lock(another_coin, ts.ctx()); + + let _key = l.unlock(k); + abort 1337 +} diff --git a/Sui/Move/smart-contracts/escrow/contract/tests/owned_tests.move b/Sui/Move/smart-contracts/escrow/contract/tests/owned_tests.move new file mode 100644 index 0000000..fa87f4e --- /dev/null +++ b/Sui/Move/smart-contracts/escrow/contract/tests/owned_tests.move @@ -0,0 +1,287 @@ +#[test_only] +module escrow::owned_tests; + +use sui::coin::{Self, Coin}; +use sui::sui::SUI; +use sui::test_scenario::{Self as ts, Scenario}; + +use escrow::lock::{Self, Locked, Key}; +use escrow::owned::{Self, EMismatchedSenderRecipient, EMismatchedExchangeObject}; + +const ALICE: address = @0xA; +const BOB: address = @0xB; +const CUSTODIAN: address = @0xC; +const DIANE: address = @0xD; + +fun test_coin(ts: &mut Scenario): Coin { + coin::mint_for_testing(42, ts::ctx(ts)) +} + +#[test] +fun test_successful_swap() { + let mut ts = ts::begin(@0x0); + + // Alice locks the object they want to trade + let (i1, ik1) = { + ts.next_tx(ALICE); + let c = test_coin(&mut ts); + let cid = object::id(&c); + let (l, k) = lock::lock(c, ts.ctx()); + let kid = object::id(&k); + transfer::public_transfer(l, ALICE); + transfer::public_transfer(k, ALICE); + (cid, kid) + }; + + // Bob locks their object as well. + let (i2, ik2) = { + ts.next_tx(BOB); + let c = test_coin(&mut ts); + let cid = object::id(&c); + let (l, k) = lock::lock(c, ts.ctx()); + let kid = object::id(&k); + transfer::public_transfer(l, BOB); + transfer::public_transfer(k, BOB); + (cid, kid) + }; + + // Alice gives the custodian their object to hold in escrow. + { + ts.next_tx(ALICE); + let k1: Key = ts.take_from_sender(); + let l1: Locked> = ts.take_from_sender(); + owned::create(k1, l1, ik2, BOB, CUSTODIAN, ts.ctx()); + }; + + // Bob does the same. + { + ts.next_tx(BOB); + let k2: Key = ts.take_from_sender(); + let l2: Locked> = ts.take_from_sender(); + owned::create(k2, l2, ik1, ALICE, CUSTODIAN, ts.ctx()); + }; + + // The custodian makes the swap + { + ts.next_tx(CUSTODIAN); + owned::swap, Coin>( + ts.take_from_sender(), + ts.take_from_sender(), + ); + }; + + // Commit effects from the swap + ts.next_tx(@0x0); + + // Alice gets the object from Bob + { + let c: Coin = ts.take_from_address_by_id(ALICE, i2); + ts::return_to_address(ALICE, c); + }; + + // Bob gets the object from Alice + { + let c: Coin = ts.take_from_address_by_id(BOB, i1); + ts::return_to_address(BOB, c); + }; + + ts.end(); +} + +#[test] +#[expected_failure(abort_code = EMismatchedSenderRecipient, location = owned)] +fun test_mismatch_sender() { + let mut ts = ts::begin(@0x0); + + let ik1 = { + ts.next_tx(ALICE); + let c = test_coin(&mut ts); + let (l, k) = lock::lock(c, ts.ctx()); + let kid = object::id(&k); + transfer::public_transfer(l, ALICE); + transfer::public_transfer(k, ALICE); + kid + }; + + let ik2 = { + ts.next_tx(BOB); + let c = test_coin(&mut ts); + let (l, k) = lock::lock(c, ts.ctx()); + let kid = object::id(&k); + transfer::public_transfer(l, BOB); + transfer::public_transfer(k, BOB); + kid + }; + + // Alice wants to trade with Bob. + { + ts.next_tx(ALICE); + let k1: Key = ts.take_from_sender(); + let l1: Locked> = ts.take_from_sender(); + owned::create(k1, l1, ik2, BOB, CUSTODIAN, ts.ctx()); + }; + + // But Bob wants to trade with Diane. + { + ts.next_tx(BOB); + let k2: Key = ts.take_from_sender(); + let l2: Locked> = ts.take_from_sender(); + owned::create(k2, l2, ik1, DIANE, CUSTODIAN, ts.ctx()); + }; + + // When the custodian tries to match up the swap, it will fail. + { + ts.next_tx(CUSTODIAN); + owned::swap, Coin>( + ts.take_from_sender(), + ts.take_from_sender(), + ); + }; + + abort 1337 +} + +#[test] +#[expected_failure(abort_code = EMismatchedExchangeObject, location = owned)] +fun test_mismatch_object() { + let mut ts = ts::begin(@0x0); + + let ik1 = { + ts.next_tx(ALICE); + let c = test_coin(&mut ts); + let (l, k) = lock::lock(c, ts.ctx()); + let kid = object::id(&k); + transfer::public_transfer(l, ALICE); + transfer::public_transfer(k, ALICE); + kid + }; + + { + ts.next_tx(BOB); + let c = test_coin(&mut ts); + let (l, k) = lock::lock(c, ts.ctx()); + transfer::public_transfer(l, BOB); + transfer::public_transfer(k, BOB); + }; + + // Alice wants to trade with Bob, but Alice has asked for an + // object (via its `exchange_key`) that Bob has not put up for + // the swap. + { + ts.next_tx(ALICE); + let k1: Key = ts.take_from_sender(); + let l1: Locked> = ts.take_from_sender(); + owned::create(k1, l1, ik1, BOB, CUSTODIAN, ts.ctx()); + }; + + { + ts.next_tx(BOB); + let k2: Key = ts.take_from_sender(); + let l2: Locked> = ts.take_from_sender(); + owned::create(k2, l2, ik1, ALICE, CUSTODIAN, ts.ctx()); + }; + + // When the custodian tries to match up the swap, it will fail. + { + ts.next_tx(CUSTODIAN); + owned::swap, Coin>( + ts.take_from_sender(), + ts.take_from_sender(), + ); + }; + + abort 1337 +} + +#[test] +#[expected_failure(abort_code = EMismatchedExchangeObject, location = owned)] +fun test_object_tamper() { + let mut ts = ts::begin(@0x0); + + // Alice locks the object they want to trade + let ik1 = { + ts.next_tx(ALICE); + let c = test_coin(&mut ts); + let (l, k) = lock::lock(c, ts.ctx()); + let kid = object::id(&k); + transfer::public_transfer(l, ALICE); + transfer::public_transfer(k, ALICE); + kid + }; + + // Bob locks their object as well. + let ik2 = { + ts.next_tx(BOB); + let c = test_coin(&mut ts); + let (l, k) = lock::lock(c, ts.ctx()); + let kid = object::id(&k); + transfer::public_transfer(l, BOB); + transfer::public_transfer(k, BOB); + kid + }; + + // Alice gives the custodian their object to hold in escrow. + { + ts.next_tx(ALICE); + let k1: Key = ts.take_from_sender(); + let l1: Locked> = ts.take_from_sender(); + owned::create(k1, l1, ik2, BOB, CUSTODIAN, ts.ctx()); + }; + + // Bob has a change of heart, so they unlock the object and tamper + // with it. + { + ts.next_tx(BOB); + let k: Key = ts.take_from_sender(); + let l: Locked> = ts.take_from_sender(); + let mut c = lock::unlock(l, k); + + let _dust = coin::split(&mut c, 1, ts.ctx()); + let (l, k) = lock::lock(c, ts.ctx()); + owned::create(k, l, ik1, ALICE, CUSTODIAN, ts.ctx()); + }; + + // When the Custodian makes the swap, it detects Bob's nefarious + // behaviour. + { + ts.next_tx(CUSTODIAN); + owned::swap, Coin>( + ts.take_from_sender(), + ts.take_from_sender(), + ); + }; + + abort 1337 +} + +#[test] +fun test_return_to_sender() { + let mut ts = ts::begin(@0x0); + + // Alice locks the object they want to trade + let cid = { + ts.next_tx(ALICE); + let c = test_coin(&mut ts); + let cid = object::id(&c); + let (l, k) = lock::lock(c, ts.ctx()); + let i = object::id_from_address(@0x0); + owned::create(k, l, i, BOB, CUSTODIAN, ts.ctx()); + cid + }; + + // Custodian sends it back + { + ts.next_tx(CUSTODIAN); + owned::return_to_sender>(ts.take_from_sender()); + }; + + ts.next_tx(@0x0); + + // Alice can then access it. + { + let c: Coin = ts.take_from_address_by_id(ALICE, cid); + ts::return_to_address(ALICE, c) + }; + + ts.end(); +} diff --git a/Sui/Move/smart-contracts/escrow/contract/tests/shared_tests.move b/Sui/Move/smart-contracts/escrow/contract/tests/shared_tests.move new file mode 100644 index 0000000..325dd78 --- /dev/null +++ b/Sui/Move/smart-contracts/escrow/contract/tests/shared_tests.move @@ -0,0 +1,269 @@ +#[test_only] +module escrow::shared_tests; + +use sui::coin::{Self, Coin}; +use sui::sui::SUI; +use sui::test_scenario::{Self as ts, Scenario}; + +use escrow::lock::{Self, Locked, Key}; +use escrow::shared::{Self, Escrow, EMismatchedSenderRecipient, EMismatchedExchangeObject}; + +const ALICE: address = @0xA; +const BOB: address = @0xB; +const DIANE: address = @0xD; + +fun test_coin(ts: &mut Scenario): Coin { + coin::mint_for_testing(42, ts.ctx()) +} + +#[test] +fun test_successful_swap() { + let mut ts = ts::begin(@0x0); + + // Bob locks the object they want to trade. + let (i2, ik2) = { + ts.next_tx(BOB); + let c = test_coin(&mut ts); + let cid = object::id(&c); + let (l, k) = lock::lock(c, ts.ctx()); + let kid = object::id(&k); + transfer::public_transfer(l, BOB); + transfer::public_transfer(k, BOB); + (cid, kid) + }; + + // Alice creates a public Escrow holding the object they are willing to + // share, and the object they want from Bob + let i1 = { + ts.next_tx(ALICE); + let c = test_coin(&mut ts); + let cid = object::id(&c); + shared::create(c, ik2, BOB, ts.ctx()); + cid + }; + + // Bob responds by offering their object, and gets Alice's object in + // return. + { + ts.next_tx(BOB); + let escrow: Escrow> = ts.take_shared(); + let k2: Key = ts.take_from_sender(); + let l2: Locked> = ts.take_from_sender(); + let c = escrow.swap(k2, l2, ts.ctx()); + + transfer::public_transfer(c, BOB); + }; + + // Commit effects from the swap + ts.next_tx(@0x0); + + // Alice gets the object from Bob + { + let c: Coin = ts.take_from_address_by_id(ALICE, i2); + ts::return_to_address(ALICE, c); + }; + + // Bob gets the object from Alice + { + let c: Coin = ts.take_from_address_by_id(BOB, i1); + ts::return_to_address(BOB, c); + }; + + ts::end(ts); +} + +#[test] +#[expected_failure(abort_code = EMismatchedSenderRecipient, location = shared)] +fun test_mismatch_sender() { + let mut ts = ts::begin(@0x0); + + let ik2 = { + ts.next_tx(DIANE); + let c = test_coin(&mut ts); + let (l, k) = lock::lock(c, ts.ctx()); + let kid = object::id(&k); + transfer::public_transfer(l, DIANE); + transfer::public_transfer(k, DIANE); + kid + }; + + // Alice wants to trade with Bob. + { + ts.next_tx(ALICE); + let c = test_coin(&mut ts); + shared::create(c, ik2, BOB, ts.ctx()); + }; + + // But Diane is the one who attempts the swap + { + ts.next_tx(DIANE); + let escrow: Escrow> = ts.take_shared(); + let k2: Key = ts.take_from_sender(); + let l2: Locked> = ts.take_from_sender(); + let c = escrow.swap(k2, l2, ts.ctx()); + + transfer::public_transfer(c, DIANE); + }; + + abort 1337 +} + +#[test] +#[expected_failure(abort_code = EMismatchedExchangeObject, location = shared)] +fun test_mismatch_object() { + let mut ts = ts::begin(@0x0); + + { + ts.next_tx(BOB); + let c = test_coin(&mut ts); + let (l, k) = lock::lock(c, ts.ctx()); + transfer::public_transfer(l, BOB); + transfer::public_transfer(k, BOB); + }; + + // Alice wants to trade with Bob, but Alice has asked for an object (via + // its `exchange_key`) that Bob has not put up for the swap. + { + ts.next_tx(ALICE); + let c = test_coin(&mut ts); + let cid = object::id(&c); + shared::create(c, cid, BOB, ts.ctx()); + }; + + // When Bob tries to complete the swap, it will fail, because they + // cannot meet Alice's requirements. + { + ts.next_tx(BOB); + let escrow: Escrow> = ts.take_shared(); + let k2: Key = ts.take_from_sender(); + let l2: Locked> = ts.take_from_sender(); + let c = escrow.swap(k2, l2, ts.ctx()); + + transfer::public_transfer(c, BOB); + }; + + abort 1337 +} + +#[test] +#[expected_failure(abort_code = EMismatchedExchangeObject, location = shared)] +fun test_object_tamper() { + let mut ts = ts::begin(@0x0); + + // Bob locks their object. + let ik2 = { + ts.next_tx(BOB); + let c = test_coin(&mut ts); + let (l, k) = lock::lock(c, ts.ctx()); + let kid = object::id(&k); + transfer::public_transfer(l, BOB); + transfer::public_transfer(k, BOB); + kid + }; + + // Alice sets up the escrow + { + ts.next_tx(ALICE); + let c = test_coin(&mut ts); + shared::create(c, ik2, BOB, ts.ctx()); + }; + + // Bob has a change of heart, so they unlock the object and tamper with + // it before initiating the swap, but it won't be possible for Bob to + // hide their tampering. + { + ts.next_tx(BOB); + let k: Key = ts.take_from_sender(); + let l: Locked> = ts.take_from_sender(); + let mut c = lock::unlock(l, k); + + let _dust = c.split(1, ts.ctx()); + let (l, k) = lock::lock(c, ts.ctx()); + let escrow: Escrow> = ts.take_shared(); + let c = escrow.swap(k, l, ts.ctx()); + + transfer::public_transfer(c, BOB); + }; + + abort 1337 +} + +#[test] +fun test_return_to_sender() { + let mut ts = ts::begin(@0x0); + + // Alice puts up the object they want to trade + let cid = { + ts.next_tx(ALICE); + let c = test_coin(&mut ts); + let cid = object::id(&c); + let i = object::id_from_address(@0x0); + shared::create(c, i, BOB, ts.ctx()); + cid + }; + + // ...but has a change of heart and takes it back + { + ts.next_tx(ALICE); + let escrow: Escrow> = ts.take_shared(); + let c = escrow.return_to_sender(ts.ctx()); + + transfer::public_transfer(c, ALICE); + }; + + ts.next_tx(@0x0); + + // Alice can then access it. + { + let c: Coin = ts.take_from_address_by_id(ALICE, cid); + ts::return_to_address(ALICE, c) + }; + + ts::end(ts); +} + +#[test] +#[expected_failure] +fun test_return_to_sender_failed_swap() { + let mut ts = ts::begin(@0x0); + + // Bob locks their object. + let ik2 = { + ts.next_tx(BOB); + let c = test_coin(&mut ts); + let (l, k) = lock::lock(c, ts.ctx()); + let kid = object::id(&k); + transfer::public_transfer(l, BOB); + transfer::public_transfer(k, BOB); + kid + }; + + // Alice creates a public Escrow holding the object they are willing to + // share, and the object they want from Bob + { + ts.next_tx(ALICE); + let c = test_coin(&mut ts); + shared::create(c, ik2, BOB, ts.ctx()); + }; + + // ...but then has a change of heart + { + ts.next_tx(ALICE); + let escrow: Escrow> = ts.take_shared(); + let c = escrow.return_to_sender(ts.ctx()); + transfer::public_transfer(c, ALICE); + }; + + // Bob's attempt to complete the swap will now fail. + { + ts.next_tx(BOB); + let escrow: Escrow> = ts.take_shared(); + let k2: Key = ts.take_from_sender(); + let l2: Locked> = ts.take_from_sender(); + let c = escrow.swap(k2, l2, ts.ctx()); + + transfer::public_transfer(c, BOB); + }; + + abort 1337 +} diff --git a/Sui/Move/smart-contracts/escrow/performance/README.md b/Sui/Move/smart-contracts/escrow/performance/README.md index 5710a26..60cb256 100644 --- a/Sui/Move/smart-contracts/escrow/performance/README.md +++ b/Sui/Move/smart-contracts/escrow/performance/README.md @@ -1,6 +1,100 @@ -# Escrow Performance (Move) +# Escrow Performance Benchmarks (Sui Move) -## What should go here -- Benchmarks: compute units (Solana) / gas (EVM) per instruction. -- Comparisons against equivalent contracts in other frameworks. -- Optimization notes and before/after numbers. +Automated gas profile benchmark report generated on **2026-07-29 14:18:43 UTC**. + +## Environment & Tooling +* **Compiler / Toolchain**: `sui 1.73.0-homebrew` +* **Benchmarking Command**: `sui move test --statistics` +* **Target Environment**: Move 2024 Edition / Sui Move VM + +--- + +## Gas & Computation Profile Output + +```text +INCLUDING DEPENDENCY MoveStdlib +INCLUDING DEPENDENCY Sui +BUILDING escrow +Running Move unit tests +[ PASS ] escrow::lock_tests::test_lock_key_mismatch +[ PASS ] escrow::lock_tests::test_lock_unlock +[ PASS ] escrow::shared_tests::test_mismatch_object +[ PASS ] escrow::shared_tests::test_mismatch_sender +[ PASS ] escrow::shared_tests::test_object_tamper +[ PASS ] escrow::owned_tests::test_mismatch_object +[ PASS ] escrow::shared_tests::test_return_to_sender +[ PASS ] escrow::owned_tests::test_mismatch_sender +[ PASS ] escrow::shared_tests::test_return_to_sender_failed_swap +[ PASS ] escrow::shared_tests::test_successful_swap +[ PASS ] escrow::owned_tests::test_object_tamper +[ PASS ] escrow::owned_tests::test_return_to_sender +[ PASS ] escrow::owned_tests::test_successful_swap + +Test Statistics: + +┌─────────────────────────────────────────────────────────┬────────────┬───────────────────────────┐ +│ Test Name │ Time │ Gas Used │ +├─────────────────────────────────────────────────────────┼────────────┼───────────────────────────┤ +│ escrow::lock_tests::test_lock_key_mismatch │ 0.252 │ 49999995000022 │ +├─────────────────────────────────────────────────────────┼────────────┼───────────────────────────┤ +│ escrow::lock_tests::test_lock_unlock │ 0.000 │ 49999995000016 │ +├─────────────────────────────────────────────────────────┼────────────┼───────────────────────────┤ +│ escrow::owned_tests::test_mismatch_object │ 0.257 │ 49999995000046 │ +├─────────────────────────────────────────────────────────┼────────────┼───────────────────────────┤ +│ escrow::owned_tests::test_mismatch_sender │ 0.001 │ 49999995000046 │ +├─────────────────────────────────────────────────────────┼────────────┼───────────────────────────┤ +│ escrow::owned_tests::test_object_tamper │ 0.001 │ 49999995000060 │ +├─────────────────────────────────────────────────────────┼────────────┼───────────────────────────┤ +│ escrow::owned_tests::test_return_to_sender │ 0.000 │ 49999995000025 │ +├─────────────────────────────────────────────────────────┼────────────┼───────────────────────────┤ +│ escrow::owned_tests::test_successful_swap │ 0.001 │ 49999995000050 │ +├─────────────────────────────────────────────────────────┼────────────┼───────────────────────────┤ +│ escrow::shared_tests::test_mismatch_object │ 0.256 │ 49999995000033 │ +├─────────────────────────────────────────────────────────┼────────────┼───────────────────────────┤ +│ escrow::shared_tests::test_mismatch_sender │ 0.000 │ 49999995000032 │ +├─────────────────────────────────────────────────────────┼────────────┼───────────────────────────┤ +│ escrow::shared_tests::test_object_tamper │ 0.000 │ 49999995000046 │ +├─────────────────────────────────────────────────────────┼────────────┼───────────────────────────┤ +│ escrow::shared_tests::test_return_to_sender │ 0.000 │ 49999995000024 │ +├─────────────────────────────────────────────────────────┼────────────┼───────────────────────────┤ +│ escrow::shared_tests::test_return_to_sender_failed_swap │ 0.000 │ 49999995000035 │ +├─────────────────────────────────────────────────────────┼────────────┼───────────────────────────┤ +│ escrow::shared_tests::test_successful_swap │ 0.000 │ 49999995000042 │ +└─────────────────────────────────────────────────────────┴────────────┴───────────────────────────┘ + +Test result: OK. Total tests: 13; passed: 13; failed: 0 +``` + +--- + +## Sui Move Storage & Gas Notes + +1. **Storage Rebates**: + * Calling `id.delete()` when unpacking `Escrow` structs reclaims storage gas rebates, resulting in a gas refund upon swap completion or cancellation. +2. **Dynamic Object Fields (DOF)**: + * Holding escrowed objects via `sui::dynamic_object_field` prevents unnecessary object wrapping penalties while preserving global object discoverability. + +--- + +## How to Verify and Re-run Benchmarks + +Anyone can verify these numbers or update the benchmark report locally using any of the following 3 commands: + +### Option 1: From the Package Directory (`contract/`) +```bash +bash ../performance/benchmark.sh +# or: ../performance/benchmark.sh +``` + +### Option 2: From the Performance Directory (`performance/`) +```bash +cd Sui/Move/smart-contracts/escrow/performance +./benchmark.sh +# or: bash benchmark.sh +``` + +### Option 3: From the Repository Root (`casc_lab/`) +```bash +bash Sui/Move/smart-contracts/escrow/performance/benchmark.sh +# or: ./Sui/Move/smart-contracts/escrow/performance/benchmark.sh +``` diff --git a/Sui/Move/smart-contracts/escrow/performance/benchmark.sh b/Sui/Move/smart-contracts/escrow/performance/benchmark.sh new file mode 100755 index 0000000..ef6290e --- /dev/null +++ b/Sui/Move/smart-contracts/escrow/performance/benchmark.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash + +# Directories +PERF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PACKAGE_DIR="${PERF_DIR}/../contract" +PERF_README="${PERF_DIR}/README.md" + +echo "==================================================" +echo " Running Sui Move Gas Profiling & Benchmarks..." +echo "==================================================" + +cd "${PACKAGE_DIR}" + +# Run test gas statistics +TEST_OUTPUT=$(sui move test --statistics 2>&1) +SUI_VERSION=$(sui --version 2>&1 || echo "sui-cli") +TIMESTAMP=$(date -u +"%Y-%m-%d %H:%M:%S UTC") + +# Write updated performance/README.md +cat < "${PERF_README}" +# Escrow Performance Benchmarks (Sui Move) + +Automated gas profile benchmark report generated on **${TIMESTAMP}**. + +## Environment & Tooling +* **Compiler / Toolchain**: \`${SUI_VERSION}\` +* **Benchmarking Command**: \`sui move test --statistics\` +* **Target Environment**: Move 2024 Edition / Sui Move VM + +--- + +## Gas & Computation Profile Output + +\`\`\`text +${TEST_OUTPUT} +\`\`\` + +--- + +## Sui Move Storage & Gas Notes + +1. **Storage Rebates**: + * Calling \`id.delete()\` when unpacking \`Escrow\` structs reclaims storage gas rebates, resulting in a gas refund upon swap completion or cancellation. +2. **Dynamic Object Fields (DOF)**: + * Holding escrowed objects via \`sui::dynamic_object_field\` prevents unnecessary object wrapping penalties while preserving global object discoverability. + +--- + +## How to Verify and Re-run Benchmarks + +Anyone can verify these numbers or update the benchmark report locally using any of the following 3 commands: + +### Option 1: From the Package Directory (\`contract/\`) +\`\`\`bash +bash ../performance/benchmark.sh +# or: ../performance/benchmark.sh +\`\`\` + +### Option 2: From the Performance Directory (\`performance/\`) +\`\`\`bash +cd Sui/Move/smart-contracts/escrow/performance +./benchmark.sh +# or: bash benchmark.sh +\`\`\` + +### Option 3: From the Repository Root (\`casc_lab/\`) +\`\`\`bash +bash Sui/Move/smart-contracts/escrow/performance/benchmark.sh +# or: ./Sui/Move/smart-contracts/escrow/performance/benchmark.sh +\`\`\` +EOF + +echo "==================================================" +echo " ✅ Benchmarks recorded in performance/README.md" +echo "==================================================" diff --git a/Sui/Move/smart-contracts/escrow/tests/README.md b/Sui/Move/smart-contracts/escrow/tests/README.md deleted file mode 100644 index 0939359..0000000 --- a/Sui/Move/smart-contracts/escrow/tests/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# Escrow Tests (Move) - -## What should go here -- Unit + integration tests for the escrow contract. -- A GitHub Actions workflow that runs this suite on push / PR. - -Add the workflow under the repo-level `.github/workflows/` and reference it here.