代码见:
https://github.com/NonceGeek/Web3-dApp-Camp/tree/main/move-dapp/sui/swap
合约 liquidity 实现了一个在 sui 公链上的 DEX 模式的 swap 案例,它仅仅实现了以下几种功能:
默认在此案例中交互的两种 Coin 交换汇率为 1:1,在其中没有设置交易手续费,也没考虑其中一种 Coin 的增加或减少所带来的流动损失。
/// simple swap module in sui
module swap::liquidity{
// packages: 包
// structs: 结构体
// consts: 常量
// functions: 函数
}
包含 LP, Pool, Pocket 三个结构体:
///Liquidity provider, parameter 'X' and 'Y'
///are coins held in the pool.
struct LP<phantom X, phantom Y> has drop {}
/// Pool with exchange
struct Pool<phantom X, phantom Y> has key {
id: UID,
coin_x: Balance<X>,
coin_y: Balance<Y>,
lp_supply: Supply<LP<X, Y>>
}
///pocket to keep the Liquidity provider
///and balance of Coin X/Y
struct Pocket has key, store {
id: UID,
table: Table<ID, vector<u64>>
}
https://github.com/MystenLabs/sui/blob/testnet/crates/sui-framework/packages/sui-framework/sources/balance.move#L26
定义 sui 中 Token 的供应量,该结构体点个具备 store能力,可以作为其它 struct 字段存储。
https://github.com/MystenLabs/sui/blob/testnet/crates/sui-framework/packages/sui-framework/sources/table.move#L27
Table 是 sui 中提供的一个容器,在标注类型时,要明确其中的键 K和值 V的类型。
///create a new pool
public fun new_pool<X, Y>(ctx: &mut TxContext) {
let new_pool = Pool<X, Y> {
id: object::new(ctx),
coin_x: balance::zero(),
coin_y: balance::zero(),
lp_supply: balance::create_supply<LP<X, Y>>(LP {})
};
transfer::share_object(new_pool);
}
///entry function to generate new pool
public entry fun generate_pool<X, Y>(ctx: &mut TxContext) {
new_pool<X, Y>(ctx);
}
https://github.com/MystenLabs/sui/blob/testnet/crates/sui-framework/packages/sui-framework/sources/balance.move#L67
初始化一个Coin的余额,价值为 0。
https://github.com/MystenLabs/sui/blob/testnet/crates/sui-framework/packages/sui-framework/sources/balance.move#L47
初始化一个 Coin的供应量,价值为 0。
加上注释:
///create a new pool
public fun new_pool<X, Y>(ctx: &mut TxContext) {
// 构造新的 Pool
let new_pool = Pool<X, Y> {
id: object::new(ctx),
coin_x: balance::zero(), // 初始化 Coin X 余额,数量 0
coin_y: balance::zero(), // 初始化 Coin Y 余额,数量 0
lp_supply: balance::create_supply<LP<X, Y>>(LP {}) // 初始化供应量,0 余额
};
// 共享新构造的交易流动池
transfer::share_object(new_pool);
}
///entry function to generate new pool
public entry fun generate_pool<X, Y>(ctx: &mut TxContext) {
new_pool<X, Y>(ctx); // 调用 new_pool 函数创建新的交易流动池
}
///entry function to create new pocket
public entry fun create_pocket(ctx: &mut TxContext) {
// 构造新的 Pocket
let pocket = Pocket {
id: object::new(ctx),
table: table::new<ID, vector<u64>>(ctx) // 初始化 table 容器,存储 Coin X/Y 在流水凭证中的数额
};
// 将新的 Pocket transer 给交易发送人
transfer::public_transfer(pocket, sender(ctx));
}
https://github.com/MystenLabs/sui/blob/testnet/crates/sui-framework/packages/sui-framework/sources/table.move#L35
初始化 Table容器。
///Add liquidity into pool, exchange rate is 1 between X and Y
public fun add_liquidity<X, Y>(pool: &mut Pool<X, Y>,
coin_x: Coin<X>,
coin_y: Coin<Y>,
ctx: &mut TxContext): (Coin<LP<X, Y>>, vector<u64>) {
let coin_x_value = coin::value(&coin_x);
let coin_y_value = coin::value(&coin_y);
assert!(coin_x_value > 0 && coin_y_value > 0, ErrZeroAmount);
coin::put(&mut pool.coin_x, coin_x);
coin::put(&mut pool.coin_y, coin_y);
let lp_bal = balance::increase_supply(&mut pool.lp_supply, coin_x_value + coin_y_value);
let vec_value = vector::empty<u64>();
vector::push_back(&mut vec_value, coin_x_value);
vector::push_back(&mut vec_value, coin_y_value);
(coin::from_balance(lp_bal, ctx), vec_value)
}
///entry function to deposit total Coin X and Y to pool
public entry fun deposit_totally<X, Y>(pool: &mut Pool<X, Y>,
coin_x: Coin<X>,
coin_y: Coin<Y>,
pocket: &mut Pocket,
ctx: &mut TxContext) {
let (lp, vec) = add_liquidity(pool, coin_x, coin_y, ctx);
let lp_id = object::id(&lp);
table::add(&mut pocket.table, lp_id, vec);
transfer::public_transfer(lp, sender(ctx));
}
///entry function to deposit part of Coin X and Y to pool
public entry fun deposit_partly<X, Y>(pool: &mut Pool<X, Y>,
coin_x_vec: vector<Coin<X>>,
coin_y_vec: vector<Coin<Y>>,
coin_x_amt: u64,
coin_y_amt: u64,
pocket: &mut Pocket,
ctx: &mut TxContext) {
let coin_x_new = coin::zero<X>(ctx);
let coin_y_new = coin::zero<Y>(ctx);
pay::join_vec(&mut coin_x_new, coin_x_vec);
pay::join_vec(&mut coin_y_new, coin_y_vec);
let coin_x_in = coin::split(&mut coin_x_new, coin_x_amt, ctx);
let coin_y_in = coin::split(&mut coin_y_new, coin_y_amt, ctx);
let (lp, vec) = add_liquidity(pool, coin_x_in, coin_y_in, ctx);
let lp_id = object::id(&lp);
table::add(&mut pocket.table, lp_id, vec);
transfer::public_transfer(lp, sender(ctx));
let sender_address = sender(ctx);
transfer::public_transfer(coin_x_new, sender_address);
transfer::public_transfer(coin_y_new, sender_address);
}
https://github.com/MystenLabs/sui/blob/testnet/crates/sui-framework/packages/sui-framework/sources/coin.move#L101
获取一枚 Coin中的价值 value。
https://github.com/MystenLabs/sui/blob/testnet/crates/sui-framework/packages/sui-framework/sources/coin.move#L148
将一枚 Coin的价值增加到另一个 Balance Obj 中,然后销毁这枚 Coin。
https://github.com/MystenLabs/sui/blob/testnet/crates/sui-framework/packages/sui-framework/sources/coin.move#L180
从一枚 Coin中分出价值为 split_amount 的余额,并用这个数量的余额构造一枚新的 Coin。
https://github.com/MystenLabs/sui/blob/testnet/crates/sui-framework/packages/sui-framework/sources/balance.move#L52
增加一种 Coin的供应数量,增量为 value,并返回价值为 value的 Balance。Supply 中的字段 value是 u64 类型,所以在增加供应量时,需要注意不要超过 u64 的最大值,即18446744073709551615u64。
https://github.com/MystenLabs/sui/blob/testnet/crates/sui-framework/packages/sui-framework/sources/object.move#L133
获取一个 obj的 ID。
https://github.com/MystenLabs/sui/blob/testnet/crates/sui-framework/packages/sui-framework/sources/table.move#L45
将一对键 K和值 V添加到容器 table 内。
https://github.com/MystenLabs/sui/blob/testnet/crates/sui-framework/packages/sui-framework/sources/pay.move#L68
将一组存储在向量中的 coins的余额全部合并到另一个 Coin中,同时销毁这组 coins。
在调用这个函数时,给 coins 传值需要用到格式:
'["coin1","coin2","coin3",...]'
加上注释
///Add liquidity into pool, exchange rate is 1 between X and Y
public fun add_liquidity<X, Y>(pool: &mut Pool<X, Y>,
coin_x: Coin<X>,
coin_y: Coin<Y>,
ctx: &mut TxContext): (Coin<LP<X, Y>>, vector<u64>) {
let coin_x_value = coin::value(&coin_x);// 获取 Coin X 中的数额
let coin_y_value = coin::value(&coin_y);// 获取 Coin Y 中的数额
assert!(coin_x_value > 0 && coin_y_value > 0, ErrZeroAmount);
coin::put(&mut pool.coin_x, coin_x); // 将 Coin X 的数额回到流动池中 X 的余额中,并销毁 X
coin::put(&mut pool.coin_y, coin_y); // 将 Coin Y 的数额回到流动池中 Y 的余额中,并销毁 Y
// 增加流动池中总的供应量,增量为上面被销毁的 Coin X 和 Coin Y 的数额总和,X 和 Y 的 exchange rate 为 1:1
let lp_bal = balance::increase_supply(&mut pool.lp_supply, coin_x_value + coin_y_value);
let vec_value = vector::empty<u64>();
vector::push_back(&mut vec_value, coin_x_value);
vector::push_back(&mut vec_value, coin_y_value);
// 返回 Coin LP 和 存储 Coin X/Y 数额的向量
(coin::from_balance(lp_bal, ctx), vec_value)
}
///entry function to deposit total Coin X and Y to pool
public entry fun deposit_totally<X, Y>(pool: &mut Pool<X, Y>,
coin_x: Coin<X>,
coin_y: Coin<Y>,
pocket: &mut Pocket,
ctx: &mut TxContext) {
// 调用 add_liquidity 函数
let (lp, vec) = add_liquidity(pool, coin_x, coin_y, ctx);
// 获取返回的 LP 的 ID
let lp_id = object::id(&lp);
// 将 LP ID 和返回的向量加到容器中
table::add(&mut pocket.table, lp_id, vec);
// 传输流水凭证给发送人
transfer::public_transfer(lp, sender(ctx));
}
///entry function to deposit part of Coin X and Y to pool
public entry fun deposit_partly<X, Y>(pool: &mut Pool<X, Y>,
coin_x_vec: vector<Coin<X>>,
coin_y_vec: vector<Coin<Y>>,
coin_x_amt: u64,
coin_y_amt: u64,
pocket: &mut Pocket,
ctx: &mut TxContext) {
let coin_x_new = coin::zero<X>(ctx);// 构造 0 数额的 Coin X
let coin_y_new = coin::zero<Y>(ctx);// 构造 0 数额的 Coin Y
// 将向量中的一组 Coin X/Y 加入到新构造的 Coin X/Y 中
pay::join_vec(&mut coin_x_new, coin_x_vec);
pay::join_vec(&mut coin_y_new, coin_y_vec);
// 从整合好的 Coin X/Y 中分离一枚数额为 coin_x_amt/coin_y_amt 的新 Coin
let coin_x_in = coin::split(&mut coin_x_new, coin_x_amt, ctx);
let coin_y_in = coin::split(&mut coin_y_new, coin_y_amt, ctx);
// 调用 add_liquidity 函数
let (lp, vec) = add_liquidity(pool, coin_x_in, coin_y_in, ctx);
let lp_id = object::id(&lp);
table::add(&mut pocket.table, lp_id, vec);
transfer::public_transfer(lp, sender(ctx));
let sender_address = sender(ctx);
// 分离出 coin_x_amt/coin_y_amt 数额之后,将有剩余数额的 Coin X/Y 传输给发送人
transfer::public_transfer(coin_x_new, sender_address);
transfer::public_transfer(coin_y_new, sender_address);
}
///Remove the liquidity and balance from pool
public fun remove_liquidity<X, Y>(pool: &mut Pool<X, Y>,
lp: Coin<LP<X, Y>>,
vec: vector<u64>,
ctx: &mut TxContext): (Coin<X>, Coin<Y>) {
assert!(vector::length(&vec) == 2, ErrInvalidVecotrType);
let lp_balance_value = coin::value(&lp); // 获取流水凭证中的数额
let coin_x_out = *vector::borrow(&mut vec, 0);// 获取流水凭证中 Coin X 的数额
let coin_y_out = *vector::borrow(&mut vec, 1);// 获取流水凭证中 Coin Y 的数额
assert!(lp_balance_value == coin_x_out + coin_y_out, ErrBalanceNotMatch);
assert!(balance::value(&mut pool.coin_x) > coin_x_out, ErrNotEnoughXInPool);
assert!(balance::value(&mut pool.coin_y) > coin_y_out, ErrNotEnoughYInPool);
// 缩减供应
balance::decrease_supply(&mut pool.lp_supply, coin::into_balance(lp));
// 返回从流动池余额中移除的 Coin X/Y
(
coin::take(&mut pool.coin_x, coin_x_out, ctx),
coin::take(&mut pool.coin_y, coin_y_out, ctx)
)
}
///entry function Withdraw all balance in Liquidity provider from pool
public entry fun remove_liquidity_totally<X, Y>(pool: &mut Pool<X, Y>,
lp: Coin<LP<X, Y>>,
pocket: &mut Pocket,
ctx: &mut TxContext) {
let lp_id = object::id(&lp); // 获取 LP 的 ID
// 从钱包中获取存有 Coin X/Y 数额的向量容器
let vec = *table::borrow(&mut pocket.table, lp_id);
// 调用 remove_liquidity 函数
let (coin_x_out, coin_y_out) = remove_liquidity(pool, lp, vec, ctx);
assert!(coin::value(&coin_x_out) > 0 && coin::value(&coin_y_out) > 0, ErrRemoveFailed);
// 从钱包中移除存有 Coin X/Y 数额的向量容器
let vec_out = table::remove(&mut pocket.table, lp_id);
vector::remove(&mut vec_out, 0);
vector::remove(&mut vec_out, 0);
let sender_address = sender(ctx);
// 将流动池资产中获得的 Coin X/Y 传输给发送人
transfer::public_transfer(coin_x_out, sender_address);
transfer::public_transfer(coin_y_out, sender_address);
}
https://github.com/MystenLabs/sui/blob/testnet/crates/sui-framework/packages/sui-framework/sources/balance.move#L59
缩减 Coin的供应量。
Swap 可以理解成拿着现金去银行兑换货币,交易方用 Coin X 兑换 Coin Y 之后,流水池中 Coin X 的余额将会增加,而 Coin Y 的余额会减少;反之亦然。
///swap Coin X to Y, return Coin Y
public fun swap_x_outto_y<X, Y>(pool: &mut Pool<X, Y>,
paid_in: Coin<X>,
ctx: &mut TxContext): Coin<Y> {
let paid_value = coin::value(&paid_in);// 获取 Coin X 的数额
coin::put(&mut pool.coin_x, paid_in); // 将 Coin X 的数额增加到流动池中,并销毁这枚 coin
assert!(paid_value < balance::value(&mut pool.coin_y), ErrNotEnoughYInPool);
coin::take(&mut pool.coin_y, paid_value, ctx)// 从流动池中获取 Coin Y 并返回
}
public entry fun swap_x_to_y<X, Y>(pool: &mut Pool<X, Y>,
coin_x_vec: vector<Coin<X>>,
amount: u64,
ctx: &mut TxContext) {
let coin_x = coin::zero<X>(ctx); // 构造一个 0 数额的 Coin X: coin_x
pay::join_vec<X>(&mut coin_x, coin_x_vec); // 将一组 Coin X 合并至上面新的 coin_x
// 从 coin_x 中分离出数额为 amount 的 Coin:coin_x_in
let coin_x_in = coin::split(&mut coin_x, amount, ctx);
// 调用 swap_x_outto_y 函数,获取 Coin Y: coin_y_out
let coin_y_out = swap_x_outto_y(pool, coin_x_in, ctx);
let sender_addres = sender(ctx);
// 将 coin_x 和 coin_y_out 传输给发送人
transfer::public_transfer(coin_x, sender_addres);
transfer::public_transfer(coin_y_out, sender_addres);
}
///swap Coin Y to X, return Coin X
public fun swap_y_into_x<X, Y>(pool: &mut Pool<X, Y>,
paid_in: Coin<Y>,
ctx: &mut TxContext): Coin<X> {
let paid_value = coin::value(&paid_in);// 获取 Coin Y 的数额
coin::put(&mut pool.coin_y, paid_in);// 将 Coin Y 的数额增加到流动池中,并销毁这枚 coin
assert!(paid_value < balance::value(&mut pool.coin_x), ErrNotEnoughXInPool);
coin::take(&mut pool.coin_x, paid_value, ctx) // 从流动池中获取 Coin X 并返回
}
public entry fun swap_y_to_x<X, Y>(pool: &mut Pool<X, Y>,
coin_y_vec: vector<Coin<Y>>,
amount: u64,
ctx: &mut TxContext) {
let coin_y = coin::zero<Y>(ctx); // 构造一个 0 数额的 Coin Y: coin_y
pay::join_vec<Y>(&mut coin_y, coin_y_vec); // 将一组 Coin Y 合并至上面新的 coin_y
// 从 coin_y 中分离出数额为 amount 的 Coin:coin_y_in
let coin_y_in = coin::split(&mut coin_y, amount, ctx);
// 调用 swap_y_into_x 函数,获取 Coin X: coin_x_out
let coin_x_out = swap_y_into_x(pool, coin_y_in, ctx);
let sender_addres = sender(ctx);
// 将 coin_x_out 和 coin_y 传输给发送人
transfer::public_transfer(coin_x_out, sender_addres);
transfer::public_transfer(coin_y, sender_addres);
}
sui client publish ./ --gas-budget 30000

在浏览器中查看发布详情:
https://explorer.sui.io/transaction/6wNJmAXVjpBWwhFtShTgmsEqV29JBDHbf7mhPcbuY2HM?network=devnet

$ CNYA="0xe0d443006afd6c7b9b46b7d1b68c4e6f17f28e78::cnya::CNYA"
CNYW="0xe0d443006afd6c7b9b46b7d1b68c4e6f17f28e78::cnyw::CNYW"
CNYACAP="0x927ba1eefd56151aba1b5efa198c432b2bca61da"
CNYWCAP="0xe51e360289c484fe5fa2a4295cb073c76354ffd8"
AMMPKG="0xdda924e1dd47e34581134a7ab3b6e7e76a9962f0"
POCKETID="0xdc508f15246dcfd94d537c016c823a591b559f7f"
POOLID="0xfbcfe320eafddc81842d1ea162c13da4b06b730b"
$ sui client call --package 0xe0d443006afd6c7b9b46b7d1b68c4e6f17f28e78 \
--module cnya \
--function mint_coin \
--gas-budget 10000 \
--args $CNYACAP \"1000\"

$ sui client call --package 0xe0d443006afd6c7b9b46b7d1b68c4e6f17f28e78 \
--module cnyw \
--function mint_coin \
--gas-budget 10000 \
--args $CNYWCAP \"1000\"

首先要 mint 两枚数额较大的 CNYA 和 CNYW,然后再把这两枚币加入到流动池中。

$ sui client call --package $AMMPKG \
--module liquidity \
--function deposit_totally \
--gas-budget 10000 \
--type-args $CNYA $CNYW \
--args $POOLID \
0x08d1891ba33b72d0cef705f586d8729c2da1793c \
0x03939ad84238dacc9ea56646e735b9674338951a \
$POCKETID
在调用函数 deposit_totally 时,需要传递两种 Coin的类型,在上面我们已经将 Coin的类型设置了参数 CNYA/CNYW,在调用时,需要写清楚 type-args参数的传值:
--type-args $CNYA $CNYW

调用之后,从 sui 浏览器中就能查到调用的钱包地址下,新增了一个 LP:
LP1
流动池中的余额也增加了:

$ sui client call --package $AMMPKG \
--module liquidity \
--function remove_liquidity_totally \
--gas-budget 10000 \
--type-args $CNYA $CNYW \
--args $POOLID \
0x65282e65c9c7a26e58662efec3f1ddbf7c09d6d0 \
$POCKETID

调用之后,流动凭证0x65282e65c9c7a26e58662efec3f1ddbf7c09d6d0被销毁:

流动池中的余额减少:

新加两枚 Coin, CNYA 和 CNYW:


$ sui client call --package $AMMPKG \
--module liquidity \
--function deposit_partly \
--gas-budget 10000 \
--type-args $CNYA $CNYW \
--args $POOLID \
'["0x0d6fab6c80e36d2f827b2f3d55968c15d931a269","0x11ba673b62c81cfda7ec1cb57aad019d4d2884d6"]' \
'["0x5179f3173aae19f325631a88ba091b7dedd84a71","0x973dbc2b802f69901c84cb68bf5fe73f8e81797b"]' \
\"1500\" \"1400\" $POCKETID

调用之后,两枚价值均为 1000 的 CNYA 货币:
0x0d6fab6c80e36d2f827b2f3d55968c15d931a269
0x11ba673b62c81cfda7ec1cb57aad019d4d2884d6
被合并成了一枚:
0x34f730bb05686aaa7900e7bc99d3293e44e82423
两枚价值均为 1000 的 CNYW 货币:
0x5179f3173aae19f325631a88ba091b7dedd84a71
0x973dbc2b802f69901c84cb68bf5fe73f8e81797b
被合并成了一枚:
0xc3466267243d3ed62294e99e180919a89d8e7826
然后向流动池中存入 1500 价值的 CNYA 和 1400 价值的 CNYW,剩余 CNYA 价值为 500,CNYW 价值为 600。


一个新的流水凭证 LP传输到调用函数的钱包地址:

流水池中新加 1500 的 CNYA 余额和 1400 的 CNYW 余额,以及 2900 总供应额:

4.2.5 调用 swap_x_to_y 函数
$ sui client call --package $AMMPKG \
--module liquidity \
--function swap_x_to_y \
--gas-budget 10000 \
--type-args $CNYA $CNYW \
--args $POOLID \
'["0x7f9d80bdbfef59100c6e9ba4185d8c01b986da99"]' \
\"314\"

调用后,流动池中新增 314 价值的 CNYA 余额,减少了 314 价值的 CNYW 余额,总供应量不变:

调用函数的钱包地址中新增价值为 314 的 CNYW:

【免责声明】市场有风险,投资需谨慎。本文不构成投资建议,用户应考虑本文中的任何意见、观点或结论是否符合其特定状况。据此投资,责任自负。
