Sui 是一個創新的區塊鏈平台,相比於大家熟悉的 EVM 兼容鏈,最大特色在以 Object(物件)為核心的設計、全新的智能合約語言 Sui Move。本文聚焦在 Sui 的其中一項關鍵創新:可編程交易塊 PTB(Programmable Transaction Block),探索如何透過 PTB
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
本文章奖励通过 Dorahacks 发放:
https\://dorahacks.io/zh/daobounty/318
Sui 是一個創新的區塊鏈平台,相比於大家熟悉的 EVM 兼容鏈,最大特色在以 Object(物件)為核心的設計、全新的智能合約語言 Sui Move。本文聚焦在 Sui 的其中一項關鍵創新:可編程交易塊 PTB(Programmable Transaction Block),探索如何透過 PTB 達成多元靈活的運用場景。
一句話概括可編程交易塊的概念:
可編程交易塊為一個異構且可組合的交易序列,且具有原子性。
簡單來說可以把可編程交易塊看作一個交易,在這個交易內可以調用多個智能合約的函數,或是轉移多個物件,在一個可編程交易塊(PTB)中最多可以打包 1024 個交易,這些交易是異構的,不一定是同類型的操作,可以跟 defi 交互、mint NFT或是轉帳。
在某些區塊鏈上,交易是基本的原子執行單位,若需要達成一系列操作需要智能合約來達成,例如:在同一個交易內批量發送代幣到不同地址。而在 Sui 上,基本的原子執行單位為一個可編程交易塊(PTB),以上面這個例子來說,使用 PTB 就可以在不寫智能合約的情況下達成批量發送代幣。
可編程交易塊有以下特性:
npm install @mysten/sui.js
// 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 });
PTB 支援將以下不同類型的交易打包、鏈接在一起,創建一個適合應用程序需求的自定義原子交易塊。
txb.splitCoins(coin, amounts)
- 從提供的 coin 分割出指定數量的 cointxb.splitCoins(txb.gas, [txb.pure(100), txb.pure(200)])
txb.mergeCoins(destinationCoin, sourceCoins)
- 合併 cointxb.transferObjects(objects, address)
- 轉移物件所有權到指定地址txb.moveCall({ target, arguments, typeArguments })
- 執行一個 Move calltxb.makeMoveVec({ type, objects })
- 構造一個 vector 包含 object,用來作為 moveCall 的輸入txb.publish(modules, dependencies)
- 部署 Move package可編程交易的輸入主要有兩種:object 或 value。分別用以下兩種方法構建:
txb.object(objectId)
txb.pure(rawValue)
在瞭解了可編程交易塊的概念及特性之後,就來看看一些範例與實操吧!
有時候有些相同類型的處理你不會特別想部署一個合約處理,例如大規模鑄造 NFT 或是向多方發送代幣,這時候將交易打包到可編程交易塊顯然是一個很好的選擇。以下提供兩種作法:
Scallop Tools 是一款 UI 工具,可以將多個交易打包到一個可編程交易塊,目前支援 transfer objects/coins、merge/split coin。對於不會使用 TS SDK 的用戶也能輕鬆構建一個可編程交易塊。
定義要傳送給哪個地址 (recipients)、多少數量的 coin (amounts)
let recipients = ["0x123", "0x456"];
let amounts = [100000000, 100000000];
分割出不同數量的 coin,用於接下來的傳送
const tx = new TransactionBlock();
const coins = tx.splitCoins(
tx.gas,
amounts.map((amount) => tx.pure(amount))
);
遍歷 recipients,傳送 sui coin 到指定地址
recipients.forEach((recipient, index) => {
tx.transferObjects([coins[index]], tx.pure(recipient));
});
執行
signer.signAndExecuteTransactionBlock({ transactionBlock: tx });
完整源碼請見 github repo。
Bucket Protocol 是 SUI 上的 CDP 協議,可超額抵押 SUI/ETH/USDC/USDT 並借出原生穩定幣 BUCK。Bucket 也提供閃電貸服務,可借出 BUCK/SUI 等代幣,收取萬分之五的手續費。
閃電貸是一種無需抵押品便可以借到錢的方式,借款金額基本上無上限,取決於協議池子提供多少代幣可供借款。唯一的限制是必須要在同一個交易內將貸款及手續費還回去,如果借款人在交易結束前沒有還款,該交易會 abort。對協議方來說閃電貸基本上是零風險。
提到閃電貸就會提到一個 Sui 特有的 pattern 稱為 Hot Potato,通常會運用 Hot Potato 的特性來實作閃電貸。Hot Potato 是一個没有任何能力修飾符的 struct,因此它只能在其模塊中被打包和解包。如果函數 A 返回這樣的一個結構,則必須要有另一個函數消耗它。
以 Bucket protocol 中閃電貸函數 flash_borrow_buck
來舉例:
public flash_borrow_buck<Ty0>(Arg0: &mut BucketProtocol, Arg1: u64): Balance<BUCK> * FlashReceipt<BUCK, Ty0> {
...
}
flash_borrow_buck
返回了一個 Balance,也就是你借出來的 BUCK,加上一個 FlashReceipt
是你的借條,可以看到 FlashReceipt
沒有任何修飾符:
struct FlashReceipt<phantom Ty0> {
amount: u64,
fee: u64
}
閃電貸還款函數flash_repay_buck
需傳入 BucketProtocol
物件、BUCK、FlashReceipt
作為輸入。FlashReceipt
在此函數被消耗掉。
public flash_repay_buck<Ty0>(Arg0: &mut BucketProtocol, Arg1: Balance<BUCK>, Arg2: FlashReceipt<BUCK, Ty0>) {
...
}
在 sui explorer 上查看 buck 模組:https\://suiexplorer.com/object/0xce7ff77a83ea0cb6fd39bd8748e2ec89a3f41e8efdc3f4eb123e0ca37b184db2?module=buck\&network=mainnet
可以知道,以上操作是無法透過 cli 去執行,但是可以透過可編程交易塊或寫智能合約來達成。
假如現在有 1 sui,想要做循環借貸,假設 BUCK/SUI = 1,LTV = 69%。
重複以上過程。
通過以上流程,最後你會有:
不過還是要注意 bucket 的倉位是至少要借出 10 BUCK,一次性的 borrow fee 會在 0.5%\~5% 之間浮動,MCR = 110%,如果倉位 CR<110% 會被清算。
Bucket 很貼心的提供了閃電貸,循環借貸可以通過閃電貸來操作。注意會有閃電貸手續費以及 DEX 的 swap 手續費。
這邊簡單整理在 bucket 用閃電貸開槓桿的操作,假設我們最初有 10 SUI:
這邊使用了tx.moveCall
調用了閃電貸函數,主要確定有填好 target 以及參數。
target
的格式為 {package}::{module}::{function}
,以下面這個 target 來說:
FLASH_BORROW_BUCK_TARGET = "0x9e3dab13212b27f5434416939db5dec6a319d15b89a84fd074d03ece6350d3df::buck::flash_borrow_buck"
typeArguments
即為 generic,填入 sui 是因爲選擇在 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],
});
借出 BUCK 後我選擇在 Cetus 的 BUCK-SUI pool,將 BUCK swap 為 SUI,實際操作時還是要注意一下滑點:
tx.moveCall({
target: CETUS_SWAP_TARGET,
typeArguments: [BUCK_TYPE, "0x2::sui::SUI"],
arguments: [
tx.object(CETUS_GLOBAL_CONFIG),
tx.object(CETUS_BUCK_SUI_POOL),
vec,
tx.pure(true),
buck_balance_value,
tx.pure(0),
tx.pure(4295048016),
tx.object("0x6"),
],
});
在 bucket protocol 開一個倉位,這邊會借出 BUCK:
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([]),
],
});
最後償還閃電貸:
tx.moveCall({
target: REPAY_FLASH_BORROW_TARGET,
typeArguments: ["0x2::sui::SUI"],
arguments: [tx.object(PROTOCOL_OBJECT), buck_output_balance, flash_receipt],
});
如果交易不是真的要上鏈的話,可以使用 dryRunTransactionBlock
查看結果:
const response = await signer.dryRunTransactionBlock({
transactionBlock: tx,
});
完整源碼請見 github repo:
PTB 強大的結構可以將一系列交易打包在一起,創建一個自定義原子交易塊,滿足各種應用程序的需求。moveCall 可以調用任何鏈上的公共函數,極大的增強了 Sui Move 的靈活度及通用性。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!