Sui Programmable Transaction Block (PTBs) | Learning Move 0x03

NonceGeekDAO
7 min readAug 2, 2023

--

Author Introduction:

Ashley is a blockchain security researcher with passion for exploring new challenges and staying up-to-date with the latest technological advancements. In addition, she is also a Solidity and Move developer with experience in building DeFi protocols and conducting vulnerability mining.

Twitter: https://twitter.com/ashleyhsu_eth

Move Learning Camp Link:

https://github.com/NonceGeek/Web3-dApp-Camp

The bounty has been published on Dorahacks:

https://dorahacks.io/zh/daobounty/318

Sui is an innovative blockchain platform that sets itself apart from the traditional EVM-compatible chains. Its primary characteristics include an object-centric design and the introduction of a novel smart contract language called Sui Move. In this article, we delve into one of Sui’s major breakthroughs: the Programmable Transaction Block (PTB), exploring how PTB enables diverse and flexible use cases.

In summary, a Programmable Transaction Block (PTB) is a heterogeneous and composable sequence of transactions that maintains atomicity.

A PTB can be seen as a transaction that can invoke functions of multiple smart contracts or transfer multiple objects. In a PTB, up to 1024 transactions can be bundled together, and these transactions can be heterogeneous, meaning they are not limited to a specific type of operation. This allows for interactions with decentralized finance (DeFi), minting NFTs, or transferring assets.

For conventional blockchains, transactions are the basic atomic execution unit. To perform a series of operations, smart contracts are often employed. For instance, to accomplish batch token transfers to various addresses within a single transaction.

However, Sui adopts a different approach where the PTB serves as the basic atomic execution unit. With PTBs, tasks like batch token transfers can be achieved without the necessity of writing a dedicated smart contract. PTBs provide a more efficient and straightforward way to bundle multiple transactions into a cohesive unit, allowing for complex operations without the need for explicit contract development.

In summary, a Programmable Transaction Block (PTB) has the following characteristics:

  • It has access to all public functions on the blockchain, regardless of whether they are entry functions.
  • Atomicity ensures that all operations within the PTB are executed entirely or fully rolled back. If any errors occur during the PTB’s execution, it is reverted to the state before the transaction began, as if the transaction never occurred.
  • Previous transaction outputs can be utilized as inputs for subsequent transactions, enabling more intricate operations.
  • A PTB can bundle up to 1024 transactions together.
  • Gas smashing simplifies token management by automatically combining gas coins, with any remaining gas coins merged before the transaction’s execution results are returned.

Sui Programmable Transaction Blocks with the TS SDK

Install Sui TS SDK

npm install @mysten/sui.js

Overview

// import ts sdk
import { TransactionBlock } from "@mysten/sui.js";

const txb = new TransactionBlock();

// Transfer the object to a specific address.
txb.transferObjects([tx.object(objectId)], txb.pure("0xSuiAddress"));
// other operations
// ...
// execute
signer.signAndExecuteTransactionBlock({ transactionBlock: txb });

Available transactions

The PTB supports the bundling and chaining of various types of transactions together, enabling the creation of a customized atomic transaction block that suits the requirements of an application.

  • txb.splitCoins(coin, amounts) - Creates new coins with the defined amounts, split from the provided coin.
  • txb.mergeCoins(destinationCoin, sourceCoins) - Merges the sourceCoins into the destinationCoin.
  • txb.transferObjects(objects, address) - Transfers a list of objects to the specified address.
  • txb.moveCall({ target, arguments, typeArguments }) - Executes a Move call.
  • txb.makeMoveVec({ type, objects }) - Constructs a vector of objects that can be passed into a moveCall.
  • txb.publish(modules, dependencies) - Publishes a Move package.

Constructing inputs

The inputs for programmable transactions primarily consist of two types: objects and values. They can be constructed using the following two methods:

  • object:txb.object(objectId)
  • value:txb.pure(rawValue)

After understanding the concept and characteristics of programmable transaction blocks, let’s take a look at some examples and practical implementations!

Example1 — Batch SUI coin transfer

Suppose you have a scenario where you need to transfer coins to multiple addresses in a single transaction. With PTBs, you can achieve this without writing a complex smart contract. You can create a PTB and add multiple token transfer transactions within it, specifying the recipient addresses and the respective token amounts. Upon executing the PTB, all the token transfers will be processed atomically.
Here are two approaches to accomplish this:

Scallop Tools

Scallop Tools is a UI tool that allows users to package multiple transactions into a programmable transaction block. It currently supports features such as transferring objects/coins and merging/splitting coins. Scallop Tools is designed to provide an easy way for users who may not be familiar with the TS SDK to construct a programmable transaction block effortlessly.

TS SDK

To construct transactions, import the TransactionBlock class, and construct it:

import { TransactionBlock } from "@mysten/sui.js";
const tx = new TransactionBlock();

Define the recipients and the respective amounts of coins to be sent.

let recipients = ["0x123", "0x456"];
let amounts = [100000000, 100000000];

Split the coins into different amounts for subsequent transfers.

const coins = tx.splitCoins(
tx.gas,
amounts.map((amount) => tx.pure(amount))
);

Iterate through the recipients and send Sui coins to the specified addresses.

recipients.forEach((recipient, index) => {
tx.transferObjects([coins[index]], tx.pure(recipient));
});

Execute.

signer.signAndExecuteTransactionBlock({ transactionBlock: tx });

The complete source code can be found in the GitHub repository.

Example2 — Using flash loan to create leveraged positions on the Bucket Protocol

Background

Bucket Protocol is a CDP protocol on SUI that allows users to deposit SUI/ETH/USDC/USDT and borrow the native stablecoin BUCK. Additionally, Bucket offers a Flash Loan service, enabling users to borrow coins such as BUCK and SUI, with a 0.05% fee charged on the loan amount.

Flash loan on Bucket Protocol

A flash loan is a type of uncollateralized loan that lets a user borrow assets with no upfront collateral as long as the borrowed assets are paid back within the same blockchain transaction. If the borrower fails to repay before the transaction ends, the transaction will be aborted. Flash loans are considered almost risk-free for the protocol.

When talking about flash loans, the term Hot Potato is often mentioned, which is a unique pattern specific to Sui and is commonly used to implement flash loans. Hot Potato is a struct without any abilities, meaning it can only be packed and unpacked within its module. If a function returns such a structure, another function must consume it to complete the process.

Let’s take the example of the flash_borrow_buck function in the Bucket Protocol:

public flash_borrow_buck<Ty0>(Arg0: &mut BucketProtocol, Arg1: u64): Balance<BUCK> * FlashReceipt<BUCK, Ty0> {
...
}

The flash_borrow_buck function returns a Balance, which represents the BUCK you borrowed, along with a FlashReceipt, which serves as your flash loan receipt. It’s important to note that the FlashReceipt has no abilities:

struct FlashReceipt<phantom Ty0> {
amount: u64,
fee: u64
}

The flash loan repayment function, flash_repay_buck, requires the input of a BucketProtocol object, BUCK, and the FlashReceipt. The FlashReceipt is consumed within this function:

public flash_repay_buck<Ty0>(Arg0: &mut BucketProtocol, Arg1: Balance<BUCK>, Arg2: FlashReceipt<BUCK, Ty0>) {
...
}

To check the buck module on the Sui Explorer: https://suiexplorer.com/object/0xce7ff77a83ea0cb6fd39bd8748e2ec89a3f41e8efdc3f4eb123e0ca37b184db2?module=buck&network=mainnet

As you can see, the above operations cannot be executed directly through the command-line interface (CLI). However, they can be achieved using programmable transaction blocks or by writing smart contracts.

Process

Suppose you have 1 SUI and want to engage in cyclical deposits and borrows. Assuming BUCK/SUI = 1 and LTV (Loan-to-Value) is 69%.

  1. Deposit 1 SUI into the Bucket Protocol and borrow 0.69 BUCK.
  2. Swap 0.69 BUCK for 0.69 SUI.

Repeat the above process.

By following the steps above, you will end up with:

  • Asset = 1 + 0.69 + 0.69 * 0.69 + … = 1 / (1–0.69) = 3.2
  • Debt = 0.69 / (1–0.69) = 2.2
  • Leverage = 3.2 / 1 = 3.2

Please note that the Bucket Protocol requires a minimum borrowing of 10 BUCK, and the one-time borrow fee fluctuates between 0.5% to 5%. The Minimum Collateral Ratio (MCR) is set at 110%, and if the Collateral Ratio (CR) falls below 110%, the position will be liquidated. Keep in mind that there will be flash loan fees and swap fees to consider.

Here is a simplified outline of leveraging in the Bucket Protocol using flash loan, assuming you initially have 10 SUI:

  1. Flash loan 20 SUI worth of BUCK
  2. Swap the borrowed BUCK for SUI on a DEX
  3. Deposit SUI into the Bucket Protocol and borrow more BUCK
  4. Repay the flash loan

Implementation

In this case, tx.moveCall is used to invoke the flash loan function, ensuring that the target and parameters are properly filled.

The format of target is {package}::{module}::{function}. Taking the following target as an example:

FLASH_BORROW_BUCK_TARGET = "0x9e3dab13212b27f5434416939db5dec6a319d15b89a84fd074d03ece6350d3df::buck::flash_borrow_buck"

typeArguments refers to generics, and in this case, 0x2::sui::SUI is filled in as it indicates the choice to borrow coins from the “sui” tank.

const [buck_balance, flash_receipt] = tx.moveCall({
target: FLASH_BORROW_BUCK_TARGET,
typeArguments: ["0x2::sui::SUI"],
arguments: [tx.object(PROTOCOL_OBJECT), buck_amount],
})

After borrowing BUCK, I choose to swap BUCK for SUI on the Cetus BUCK-SUI pool. However, during the operation, it is essential to be mindful of slippage.

const [buck_coin_out, sui_coin_out] = tx.moveCall({
target: CETUS_ROUTER_SWAP_TARGET,
typeArguments: [BUCK_TYPE, "0x2::sui::SUI"],
arguments: [
tx.object(CETUS_GLOBAL_CONFIG),
tx.object(CETUS_BUCK_SUI_POOL),
buck_coin,
zero_coin,
tx.pure(true),
tx.pure(true),
buck_balance_value,
tx.pure(0),
tx.pure(4295048016),
tx.object("0x6"),
],
});

Opening a position in the Bucket Protocol:

const buck_output_balance = tx.moveCall({
target: BORROW_TARGET,
typeArguments: ["0x2::sui::SUI"],
arguments: [
tx.object(PROTOCOL_OBJECT),
tx.object(ORACLE_OBJECT),
tx.object(CLOCK_OBJECT),
sui_balance,
borrow_buck_amount,
tx.pure([]),
],
});

Repay the flash loan:

tx.moveCall({
target: REPAY_FLASH_BORROW_TARGET,
typeArguments: ["0x2::sui::SUI"],
arguments: [tx.object(PROTOCOL_OBJECT), buck_output_balance, flash_receipt],
});

If the transaction doesn’t need to be executed on-chain, you can use dryRunTransactionBlock to preview the result.

const response = await signer.dryRunTransactionBlock({
transactionBlock: tx,
});

Or, sign and execute the transaction:

const response = await signer.signAndExecuteTransactionBlock({ 
transactionBlock: tx
});

The complete source code can be found in the GitHub repository.

Summary

The powerful structure of PTB allows chaining a series of transactions together, creating a custom atomic transaction block that meets the needs of various applications. The moveCall function enables the invocation of any public function on the blockchain, significantly enhancing the flexibility and versatility of Sui Move.

Reference

--

--