Skip to main content

Trustless Swap Backend

Multi-Page Guide

This is the first in a three-part guide on how to build a trustless atomic swap on Sui.

This particular protocol consists of three phases:

  1. One party locks their object, obtaining a Locked object and its Key. This party can unlock their object to preserve liveness 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, and the key supplied in the swap unlocks the Locked<U>.

Let's create a Sui project using the terminal command sui move new escrow. Create a file in the sources directory named shared.move, and let's go through the code together.

shared.move

info

You can view the complete source code for this app example in the Sui repository.

Let's go through the code line by line:

Structs

Escrow<T>

This struct represents an object held in escrow. It contains the following fields:

  • id: Unique identifier for the escrow object.
  • sender: Address of the owner of the escrowed object.
  • recipient: Intended recipient of the escrowed object.
  • exchange_key: ID of the key that opens the lock on the object sender wants from the recipient.
  • escrowed: The actual object held in escrow.
struct Escrow<T: key + store> has key, store {
id: UID,
sender: address,
recipient: address,
exchange_key: ID,
escrowed: T,
}

The ID of the key is used as the exchange_key, rather than the ID of the object, to ensure that the object is not modified after a trade has been initiated.

Error codes

Two constants are defined to represent potential errors during the execution of the swap:

  • EMismatchedSenderRecipient: The sender and recipient of the two escrowed objects do not match.
  • EMismatchedExchangeObject: The exchange_for fields of the two escrowed objects do not match.
const EMismatchedSenderRecipient: u64 = 0;
const EMismatchedExchangeObject: u64 = 1;

Public functions

create

This function is used to create a new escrow object. It takes four arguments: the object to be escrowed, the ID of the key that opens the lock on the object the sender wants from the recipient, the intended recipient, and the transaction context.

public fun create<T: key + store>(
escrowed: T,
exchange_key: ID,
recipient: address,
ctx: &mut TxContext
) {
let mut escrow = Escrow<T> {
id: object::new(ctx),
sender: ctx.sender(),
recipient,
exchange_key,
};

dof::add(&mut escrow.id, EscrowedObjectKey {}, escrowed);

transfer::public_share_object(escrow);
}

swap

This function is used to perform the swap operation. It takes four arguments: the escrow object, the key, the locked object, and the transaction context.

public fun swap<T: key + store, U: key + store>(
mut escrow: Escrow<T>,
key: Key,
locked: Locked<U>,
ctx: &TxContext,
): T {
let escrowed = dof::remove<EscrowedObjectKey, T>(&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);
id.delete();

escrowed
}

The id.delete() function call is used to delete the shared Escrow object. Previously, Move supported only the deletion of owned objects, but shared-object deletion has since been enabled.

return_to_sender

This function is used to cancel the escrow and return the escrowed item to the sender. It takes two arguments: the escrow object and the transaction context.

public fun return_to_sender<T: key + store>(
mut escrow: Escrow<T>,
ctx: &TxContext
): T {
let escrowed = dof::remove<EscrowedObjectKey, T>(&mut escrow.id, EscrowedObjectKey {});

let Escrow {
id,
sender,
recipient: _,
exchange_key: _,
} = escrow;

assert!(sender == ctx.sender(), EMismatchedSenderRecipient);
id.delete();
escrowed
}

Once again, the shared Escrow object is deleted after the escrowed item is returned to the sender.

Tests

The code includes several tests to ensure the correct functioning of the atomic swap process. These tests cover successful swaps, mismatches in sender or recipient, mismatches in the exchange object, tampering with the object, and returning the object to the sender.

In conclusion, this code provides a robust and secure way to perform atomic swaps of objects in a decentralized system, without the need for a trusted third party. It uses shared objects and a series of checks to ensure that the swap only occurs if all conditions are met.

Deployment

info

See Publish a Package for a more detailed guide on publishing packages or Sui Client CLI for a complete reference of client commands in the Sui CLI.

Before publishing your code, you must first initialize the Sui Client CLI, if you haven't already. To do so, in a terminal or console at the root directory of the project enter sui client. If you receive the following response, complete the remaining instructions:

Config file ["<FILE-PATH>/.sui/sui_config/client.yaml"] doesn't exist, do you want to connect to a Sui Full node server [y/N]?

Enter y to proceed. You receive the following response:

Sui Full node server URL (Defaults to Sui Devnet if not specified) :

Leave this blank (press Enter). You receive the following response:

Select key scheme to generate keypair (0 for ed25519, 1 for secp256k1, 2: for secp256r1):

Select 0. Now you should have a Sui address set up.

Before being able to publish your package to Testnet, you need Testnet SUI tokens. To get some, join the Sui Discord, complete the verification steps, enter the #testnet-faucet channel and type !faucet <WALLET ADDRESS>. For other ways to get SUI in your Testnet account, see Get SUI Tokens.

Now that you have an account with some Testnet SUI, you can deploy your contracts. To publish your package, use the following command in the same terminal or console:

sui client publish --gas-budget <GAS-BUDGET>

For the gas budget, use a standard value such as 20000000.

Next steps

You have written and deployed the Move package. To turn this into a complete dApp with frontend, you need to create a frontend. For the frontend to be updated, create an indexer that listens to the blockchain as escrows are made and swaps are fulfilled.

For the next step, you create the indexing service.