Skip to content

Commit

Permalink
feat: mechanism to revoke withdraw and whitelist cap, and pause depos…
Browse files Browse the repository at this point in the history
…it (#25)

* add new cap object

* implement cap issuance

* failure test cases

* fix case

* readme

* fix integration test

* pause mechanism
  • Loading branch information
lumtis authored Feb 7, 2025
1 parent e53cfd3 commit ef23e96
Show file tree
Hide file tree
Showing 4 changed files with 492 additions and 82 deletions.
10 changes: 4 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@

This repository hosts the smart contract deployed on the SUI network to enable ZetaChain's cross-chain functionality.

## Prerequisites
install SUI toolchain: https://github.com/MystenLabs/sui

Expand All @@ -12,15 +14,11 @@ sui move test

First compile and run the validator
```
./localhost/run-sui.sh
./localtest/run-sui.sh
```
Then run the test program
```
cd localhost && go run main.go
cd localtest && go run main.go
```

If successful you will not see any panic.

## TODO
- [ ] cryptography: native multisig
- [ ] call user specified contract
20 changes: 10 additions & 10 deletions localtest/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,16 +123,16 @@ func main() {
}

{ // register vault2 from signer2;
// first need to transfer the adminCap from signer1 to signer2
// first need to transfer the whitelistCap from signer1 to signer2
// typeName := fmt.Sprintf("%s::gateway::WithdrawCap", moduleId)
typeName := fmt.Sprintf("%s::gateway::AdminCap", moduleId)
adminCapId, err := filterOwnedObject(cli, signerAccount.Address, typeName)
typeName := fmt.Sprintf("%s::gateway::WhitelistCap", moduleId)
whitelistCapId, err := filterOwnedObject(cli, signerAccount.Address, typeName)
if err != nil {
panic(err)
}
fmt.Printf("adminCapId id %s\n", adminCapId)
if adminCapId == "" {
panic("failed to find WithdrawCap object")
fmt.Printf("whitelistCapId id %s\n", whitelistCapId)
if whitelistCapId == "" {
panic("failed to find whitelistCapId object")
}

s2 := signer2.NewSignerSecp256k1Random()
Expand All @@ -142,7 +142,7 @@ func main() {
{
tx, err := cli.TransferObject(ctx, models.TransferObjectRequest{
Signer: signerAccount.Address,
ObjectId: adminCapId,
ObjectId: whitelistCapId,
Recipient: string(s2.Address()),
GasBudget: "5000000000",
})
Expand All @@ -163,9 +163,9 @@ func main() {
}

if resp.Effects.Status.Status != "success" {
panic("failed to transfer AdminCap")
panic("failed to transfer whitelistCapId")
}
fmt.Printf("AdminCap transferred to signer2\n")
fmt.Printf("whitelistCapId transferred to signer2\n")
}

tx, err := cli.MoveCall(ctx, models.MoveCallRequest{
Expand All @@ -174,7 +174,7 @@ func main() {
Module: "gateway",
Function: "whitelist",
TypeArguments: []interface{}{"0x2::sui::SUI"},
Arguments: []interface{}{gatewayObjectId, adminCapId},
Arguments: []interface{}{gatewayObjectId, whitelistCapId},
GasBudget: "5000000000",
})
if err != nil {
Expand Down
192 changes: 148 additions & 44 deletions sources/gateway.move
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ const EInvalidReceiverAddress: u64 = 1;
const ENotWhitelisted: u64 = 2;
const ENonceMismatch: u64 = 3;
const EPayloadTooLong: u64 = 4;
const EInactiveWithdrawCap: u64 = 5;
const EInactiveWhitelistCap: u64 = 6;
const EDepositPaused: u64 = 7;

const ReceiverAddressLength: u64 = 42;
const PayloadMaxLength: u64 = 1024;
Expand All @@ -30,14 +33,22 @@ public struct Gateway has key {
id: UID,
vaults: Bag,
nonce: u64,
active_withdraw_cap: ID,
active_whitelist_cap: ID,
deposit_paused: bool,
}

// WithdrawCap is a capability object that allows the caller to withdraw tokens from the gateway
public struct WithdrawCap has key, store {
id: UID,
}

// AdminCap is a capability object that allows the caller to whitelist a new vault
// WhitelistCap is a capability object that allows the caller to whitelist a new vault
public struct WhitelistCap has key, store {
id: UID,
}

// AdminCap is a capability object that allows to issue new capabilities
public struct AdminCap has key, store {
id: UID,
}
Expand All @@ -64,27 +75,38 @@ public struct DepositAndCallEvent has copy, drop {
// === Initialization ===

fun init(ctx: &mut TxContext) {
let gateway = Gateway {
// to withdraw tokens from the gateway, the caller must have the WithdrawCap
let withdraw_cap = WithdrawCap {
id: object::new(ctx),
vaults: bag::new(ctx),
nonce: 0,
};
transfer::share_object(gateway);

// to withdraw tokens from the gateway, the caller must have the WithdrawCap
let withdraw_cap = WithdrawCap {
// to whitelist a new vault, the caller must have the WhitelistCap
let whitelist_cap = WhitelistCap {
id: object::new(ctx),
};
transfer::transfer(withdraw_cap, tx_context::sender(ctx));

// to whitelist a new vault, the caller must have the AdminCap
let admin_cap = AdminCap {
id: object::new(ctx),
};

// create and share the gateway object
let gateway = Gateway {
id: object::new(ctx),
vaults: bag::new(ctx),
nonce: 0,
active_withdraw_cap: object::id(&withdraw_cap),
active_whitelist_cap: object::id(&whitelist_cap),
deposit_paused: false,
};

transfer::transfer(withdraw_cap, tx_context::sender(ctx));
transfer::transfer(whitelist_cap, tx_context::sender(ctx));
transfer::transfer(admin_cap, tx_context::sender(ctx));
transfer::share_object(gateway);
}

// === Deposit Functions ===
// === Entrypoints ===

// deposit allows the user to deposit tokens into the gateway
entry fun deposit<T>(gateway: &mut Gateway, coin: Coin<T>, receiver: String, ctx: &mut TxContext) {
Expand All @@ -102,14 +124,55 @@ entry fun deposit_and_call<T>(
deposit_and_call_impl(gateway, coin, receiver, payload, ctx)
}

// withdraw allows the TSS to withdraw tokens from the gateway
entry fun withdraw<T>(
gateway: &mut Gateway,
amount: u64,
nonce: u64,
receiver: address,
cap: &WithdrawCap,
ctx: &mut TxContext,
) {
let coin = withdraw_impl<T>(gateway, amount, nonce, cap, ctx);
transfer::public_transfer(coin, receiver);
}

// whitelist whitelists a new coin by creating a new vault for the coin type
entry fun whitelist<T>(gateway: &mut Gateway, _cap: &WhitelistCap) {
whitelist_impl<T>(gateway, _cap)
}

// issue_withdraw_and_whitelist_cap issues a new WithdrawCap and WhitelistCap and revokes the old ones
entry fun issue_withdraw_and_whitelist_cap(
gateway: &mut Gateway,
_cap: &AdminCap,
ctx: &mut TxContext,
) {
let (withdraw_cap, whitelist_cap) = issue_withdraw_and_whitelist_cap_impl(gateway, _cap, ctx);
transfer::transfer(withdraw_cap, tx_context::sender(ctx));
transfer::transfer(whitelist_cap, tx_context::sender(ctx));
}

// pause pauses the deposit functionality
entry fun pause(gateway: &mut Gateway, cap: &AdminCap) {
pause_impl(gateway, cap)
}

// unpause unpauses the deposit functionality
entry fun unpause(gateway: &mut Gateway, cap: &AdminCap) {
unpause_impl(gateway, cap)
}

// === Deposit Functions ===

public fun deposit_impl<T>(
gateway: &mut Gateway,
coin: Coin<T>,
receiver: String,
ctx: &mut TxContext,
) {
let amount = coin.value();
let coin_name = get_coin_name<T>();
let coin_name = coin_name<T>();

check_receiver_and_deposit_to_vault(gateway, coin, receiver);

Expand All @@ -132,7 +195,7 @@ public fun deposit_and_call_impl<T>(
assert!(payload.length() <= PayloadMaxLength, EPayloadTooLong);

let amount = coin.value();
let coin_name = get_coin_name<T>();
let coin_name = coin_name<T>();

check_receiver_and_deposit_to_vault(gateway, coin, receiver);

Expand All @@ -150,87 +213,128 @@ public fun deposit_and_call_impl<T>(
fun check_receiver_and_deposit_to_vault<T>(gateway: &mut Gateway, coin: Coin<T>, receiver: String) {
assert!(receiver.length() == ReceiverAddressLength, EInvalidReceiverAddress);
assert!(is_whitelisted<T>(gateway), ENotWhitelisted);
assert!(!gateway.deposit_paused, EDepositPaused);

// Deposit the coin into the vault
let coin_name = get_coin_name<T>();
let coin_name = coin_name<T>();
let vault = bag::borrow_mut<String, Vault<T>>(&mut gateway.vaults, coin_name);
balance::join(&mut vault.balance, coin.into_balance());
}

// === Withdraw Functions ===

// withdraw allows the TSS to withdraw tokens from the gateway
entry fun withdraw<T>(
gateway: &mut Gateway,
amount: u64,
nonce: u64,
recipient: address,
cap: &WithdrawCap,
ctx: &mut TxContext,
) {
let coin = withdraw_impl<T>(gateway, amount, nonce, cap, ctx);
transfer::public_transfer(coin, recipient);
}

public fun withdraw_impl<T>(
gateway: &mut Gateway,
amount: u64,
nonce: u64,
_cap: &WithdrawCap,
cap: &WithdrawCap,
ctx: &mut TxContext,
): Coin<T> {
assert!(gateway.active_withdraw_cap == object::id(cap), EInactiveWithdrawCap);
assert!(is_whitelisted<T>(gateway), ENotWhitelisted);
assert!(nonce == gateway.nonce, ENonceMismatch); // prevent replay
gateway.nonce = nonce + 1;

// Withdraw the coin from the vault
let coin_name = get_coin_name<T>();
let coin_name = coin_name<T>();
let vault = bag::borrow_mut<String, Vault<T>>(&mut gateway.vaults, coin_name);
let coin_out = coin::take(&mut vault.balance, amount, ctx);
coin_out
}

// === Admin Functions ===

public fun whitelist_impl<T>(gateway: &mut Gateway, cap: &WhitelistCap) {
assert!(gateway.active_whitelist_cap == object::id(cap), EInactiveWhitelistCap);
assert!(is_whitelisted<T>(gateway) == false, EAlreadyWhitelisted);
let vault_name = coin_name<T>();
let vault = Vault<T> {
balance: balance::zero<T>(),
};
bag::add(&mut gateway.vaults, vault_name, vault);
}

public fun issue_withdraw_and_whitelist_cap_impl(
gateway: &mut Gateway,
_cap: &AdminCap,
ctx: &mut TxContext,
): (WithdrawCap, WhitelistCap) {
let withdraw_cap = WithdrawCap {
id: object::new(ctx),
};
let whitelist_cap = WhitelistCap {
id: object::new(ctx),
};
gateway.active_withdraw_cap = object::id(&withdraw_cap);
gateway.active_whitelist_cap = object::id(&whitelist_cap);
(withdraw_cap, whitelist_cap)
}

public fun pause_impl(gateway: &mut Gateway, _cap: &AdminCap) {
gateway.deposit_paused = true;
}

public fun unpause_impl(gateway: &mut Gateway, _cap: &AdminCap) {
gateway.deposit_paused = false;
}

// === View Functions ===

public fun nonce(gateway: &Gateway): u64 {
gateway.nonce
}

public fun get_vault_balance<T>(gateway: &Gateway): u64 {
public fun active_withdraw_cap(gateway: &Gateway): ID {
gateway.active_withdraw_cap
}

public fun active_whitelist_cap(gateway: &Gateway): ID {
gateway.active_whitelist_cap
}

public fun vault_balance<T>(gateway: &Gateway): u64 {
if (!is_whitelisted<T>(gateway)) {
return 0
};
let coin_name = get_coin_name<T>();
let coin_name = coin_name<T>();
let vault = bag::borrow<String, Vault<T>>(&gateway.vaults, coin_name);
balance::value(&vault.balance)
}

public fun is_paused(gateway: &Gateway): bool {
gateway.deposit_paused
}

// is_whitelisted returns true if a given coin type is whitelisted
public fun is_whitelisted<T>(gateway: &Gateway): bool {
let vault_name = get_coin_name<T>();
let vault_name = coin_name<T>();
bag::contains_with_type<String, Vault<T>>(&gateway.vaults, vault_name)
}

// === Admin Functions ===

// whitelist whitelists a new coin by creating a new vault for the coin type
public fun whitelist<T>(gateway: &mut Gateway, _cap: &AdminCap) {
assert!(is_whitelisted<T>(gateway) == false, EAlreadyWhitelisted);
let vault_name = get_coin_name<T>();
let vault = Vault<T> {
balance: balance::zero<T>(),
};
bag::add(&mut gateway.vaults, vault_name, vault);
}

// === Helpers ===

// get_coin_name returns the name of the coin type to index the vault
fun get_coin_name<T>(): String {
// coin_name returns the name of the coin type to index the vault
fun coin_name<T>(): String {
into_string(get<T>())
}

// === Test Helpers ===

#[test_only]
public fun init_for_testing(ctx: &mut TxContext) {
init(ctx)
}

#[test_only]
public fun create_test_withdraw_cap(ctx: &mut TxContext): WithdrawCap {
WithdrawCap {
id: object::new(ctx),
}
}

#[test_only]
public fun create_test_whitelist_cap(ctx: &mut TxContext): WhitelistCap {
WhitelistCap {
id: object::new(ctx),
}
}
Loading

0 comments on commit ef23e96

Please sign in to comment.