Skip to content

Commit

Permalink
feat: add wal_exchange contract
Browse files Browse the repository at this point in the history
  • Loading branch information
karlwuest committed Nov 18, 2024
1 parent 050dd3a commit 0c15cb8
Show file tree
Hide file tree
Showing 3 changed files with 367 additions and 0 deletions.
44 changes: 44 additions & 0 deletions contracts/wal_exchange/Move.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# @generated by Move, please check-in and do not edit manually.

[move]
version = 3
manifest_digest = "9BD464E4CF5DFF7B2A2445FBF02BC36F60218BD4A5262900323FCD6183B30723"
deps_digest = "3C4103934B1E040BB6B23F1D610B4EF9F2F1166A50A104EADCF77467C004C600"
dependencies = [
{ id = "Sui", name = "Sui" },
{ id = "WAL", name = "WAL" },
]

[[move.package]]
id = "MoveStdlib"
source = { git = "https://github.com/MystenLabs/sui.git", rev = "testnet-v1.35.0", subdir = "crates/sui-framework/packages/move-stdlib" }

[[move.package]]
id = "Sui"
source = { git = "https://github.com/MystenLabs/sui.git", rev = "testnet-v1.35.0", subdir = "crates/sui-framework/packages/sui-framework" }

dependencies = [
{ id = "MoveStdlib", name = "MoveStdlib" },
]

[[move.package]]
id = "WAL"
source = { local = "../wal" }

dependencies = [
{ id = "Sui", name = "Sui" },
]

[move.toolchain-version]
compiler-version = "1.35.0"
edition = "2024.beta"
flavor = "sui"


[env]

[env.testnet]
chain-id = "4c78adac"
original-published-id = "0x9f992cc2430a1f442ca7a5ca7638169f5d5c00e0ebc3977a65e9ac6e497fe5ef"
latest-published-id = "0x9f992cc2430a1f442ca7a5ca7638169f5d5c00e0ebc3977a65e9ac6e497fe5ef"
published-version = "1"
12 changes: 12 additions & 0 deletions contracts/wal_exchange/Move.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[package]
name = "WAL_exchange"
license = "Apache-2.0"
authors = ["Mysten Labs <[email protected]>"]
edition = "2024.beta"

[dependencies]
Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "testnet-v1.35.0" }
WAL = { local = "../wal" }

[addresses]
wal_exchange = "0x0"
311 changes: 311 additions & 0 deletions contracts/wal_exchange/sources/wal_exchange.move
Original file line number Diff line number Diff line change
@@ -0,0 +1,311 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

/// Module: wal_exchange
module wal_exchange::wal_exchange;

use sui::{balance::{Self, Balance}, coin::Coin, sui::SUI};
use wal::wal::WAL;

// Keep errors in `walrus-sui/types/move_errors.rs` up to date with changes here.
const EInsufficientFundsInExchange: u64 = 0;
const EInsufficientInputBalance: u64 = 1;
const EUnauthorizedAdminCap: u64 = 2;
const EInvalidExchangeRate: u64 = 3;

/// A public exchange that allows exchanging SUI for WAL at a fixed exchange rate.
public struct Exchange has key, store {
id: UID,
wal: Balance<WAL>,
sui: Balance<SUI>,
rate: ExchangeRate,
admin: ID,
}

/// Capability that allows the holder to modify an `Exchange`'s exchange rate and withdraw funds.
public struct AdminCap has key, store {
id: UID,
}

/// Represents an exchange rate: `wal` WAL = `sui` SUI.
public struct ExchangeRate has copy, drop, store {
wal: u64,
sui: u64,
}

// === Functions for `ExchangeRate` ===

/// Creates a new exchange rate, making sure it is valid.
public fun new_exchange_rate(wal: u64, sui: u64): ExchangeRate {
assert!(wal != 0 && sui != 0, EInvalidExchangeRate);
ExchangeRate { wal, sui }
}

fun wal_to_sui(self: &ExchangeRate, amount: u64): u64 {
amount * self.sui / self.wal
}

fun sui_to_wal(self: &ExchangeRate, amount: u64): u64 {
amount * self.wal / self.sui
}

// === Functions for `Exchange` ===

// Creation functions

/// Creates a new shared exchange with a 1:1 exchange rate and returns the associated `AdminCap`.
public fun new(ctx: &mut TxContext): AdminCap {
let admin_cap = AdminCap {
id: object::new(ctx),
};
transfer::share_object(Exchange {
id: object::new(ctx),
wal: balance::zero(),
sui: balance::zero(),
rate: ExchangeRate { wal: 1, sui: 1 },
admin: object::id(&admin_cap),
});
admin_cap
}

/// Creates a new shared exchange with a 1:1 exchange rate, funds it with WAL, and returns the
/// associated `AdminCap`.
public fun new_funded(wal: &mut Coin<WAL>, amount: u64, ctx: &mut TxContext): AdminCap {
let admin_cap = AdminCap {
id: object::new(ctx),
};
let mut exchange = Exchange {
id: object::new(ctx),
wal: balance::zero(),
sui: balance::zero(),
rate: ExchangeRate { wal: 1, sui: 1 },
admin: object::id(&admin_cap),
};
exchange.add_wal(wal, amount);

transfer::share_object(exchange);
admin_cap
}

/// Adds WAL to the balance stored in the exchange.
public fun add_wal(self: &mut Exchange, wal: &mut Coin<WAL>, amount: u64) {
self.wal.join(wal.balance_mut().split(amount));
}

/// Adds SUI to the balance stored in the exchange.
public fun add_sui(self: &mut Exchange, sui: &mut Coin<SUI>, amount: u64) {
self.sui.join(sui.balance_mut().split(amount));
}

/// Adds WAL to the balance stored in the exchange.
public fun add_all_wal(self: &mut Exchange, wal: Coin<WAL>) {
self.wal.join(wal.into_balance());
}

/// Adds SUI to the balance stored in the exchange.
public fun add_all_sui(self: &mut Exchange, sui: Coin<SUI>) {
self.sui.join(sui.into_balance());
}

// Admin functions

fun check_admin(self: &Exchange, admin_cap: &AdminCap) {
assert!(self.admin == object::id(admin_cap), EUnauthorizedAdminCap);
}

/// Withdraws WAL from the balance stored in the exchange.
public fun withdraw_wal(
self: &mut Exchange,
amount: u64,
admin_cap: &AdminCap,
ctx: &mut TxContext,
): Coin<WAL> {
self.check_admin(admin_cap);
assert!(self.wal.value() >= amount, EInsufficientFundsInExchange);
self.wal.split(amount).into_coin(ctx)
}

/// Withdraws SUI from the balance stored in the exchange.
public fun withdraw_sui(
self: &mut Exchange,
amount: u64,
admin_cap: &AdminCap,
ctx: &mut TxContext,
): Coin<SUI> {
self.check_admin(admin_cap);
assert!(self.sui.value() >= amount, EInsufficientFundsInExchange);
self.sui.split(amount).into_coin(ctx)
}

/// Sets the exchange rate of the exchange to `wal` WAL = `sui` SUI.
public fun set_exchange_rate(self: &mut Exchange, wal: u64, sui: u64, admin_cap: &AdminCap) {
self.check_admin(admin_cap);
self.rate = new_exchange_rate(wal, sui);
}

// User functions

/// Exchanges the provided SUI coin for WAL at the exchange's rate.
public fun exchange_all_for_wal(
self: &mut Exchange,
sui: Coin<SUI>,
ctx: &mut TxContext,
): Coin<WAL> {
let value_wal = self.rate.sui_to_wal(sui.value());
assert!(self.wal.value() >= value_wal, EInsufficientFundsInExchange);
self.sui.join(sui.into_balance());
self.wal.split(value_wal).into_coin(ctx)
}

/// Exchanges `amount_sui` out of the provided SUI coin for WAL at the exchange's rate.
public fun exchange_for_wal(
self: &mut Exchange,
sui: &mut Coin<SUI>,
amount_sui: u64,
ctx: &mut TxContext,
): Coin<WAL> {
assert!(sui.value() >= amount_sui, EInsufficientInputBalance);
self.exchange_all_for_wal(sui.split(amount_sui, ctx), ctx)
}

/// Exchanges the provided WAL coin for SUI at the exchange's rate.
public fun exchange_all_for_sui(
self: &mut Exchange,
wal: Coin<WAL>,
ctx: &mut TxContext,
): Coin<SUI> {
let value_sui = self.rate.wal_to_sui(wal.value());
assert!(self.sui.value() >= value_sui, EInsufficientFundsInExchange);
self.wal.join(wal.into_balance());
self.sui.split(value_sui).into_coin(ctx)
}

/// Exchanges `amount_wal` out of the provided WAL coin for SUI at the exchange's rate.
public fun exchange_for_sui(
self: &mut Exchange,
wal: &mut Coin<WAL>,
amount_wal: u64,
ctx: &mut TxContext,
): Coin<SUI> {
assert!(wal.value() >= amount_wal, EInsufficientInputBalance);
self.exchange_all_for_sui(wal.split(amount_wal, ctx), ctx)
}

// === Tests ===

#[test_only]
use sui::coin;
#[test_only]
use sui::test_utils::destroy;

#[test_only]
fun new_for_testing(wal_per_sui: u64, ctx: &mut TxContext): (Exchange, AdminCap) {
let admin_cap = AdminCap {
id: object::new(ctx),
};
(
Exchange {
id: object::new(ctx),
wal: balance::zero(),
sui: balance::zero(),
rate: ExchangeRate { wal: wal_per_sui, sui: 1 },
admin: object::id(&admin_cap),
},
admin_cap,
)
}

#[test]
fun test_standard_flow() {
let ctx = &mut tx_context::dummy();
let (mut exchange, admin_cap) = new_for_testing(1, ctx);

exchange.set_exchange_rate(4, 2, &admin_cap);
exchange.add_all_wal(coin::mint_for_testing(1_000_000, ctx));
exchange.add_all_sui(coin::mint_for_testing(1_000_000, ctx));

let mut wal_coin = exchange.exchange_all_for_wal(coin::mint_for_testing(42, ctx), ctx);
assert!(wal_coin.value() == 84);
assert!(exchange.sui.value() == 1_000_042);
assert!(exchange.wal.value() == 999_916);

let mut sui_coin = exchange.exchange_for_sui(&mut wal_coin, 9, ctx);
assert!(sui_coin.value() == 4);
assert!(wal_coin.value() == 75);

let wal_coin_2 = exchange.exchange_for_wal(&mut sui_coin, 2, ctx);
assert!(wal_coin_2.value() == 4);
assert!(sui_coin.value() == 2);

let withdraw_wal_coin = exchange.withdraw_wal(13, &admin_cap, ctx);
assert!(withdraw_wal_coin.value() == 13);

let withdraw_sui_coin = exchange.withdraw_sui(42, &admin_cap, ctx);
assert!(withdraw_sui_coin.value() == 42);

destroy(wal_coin);
destroy(wal_coin_2);
destroy(sui_coin);
destroy(withdraw_wal_coin);
destroy(withdraw_sui_coin);
destroy(exchange);
destroy(admin_cap);
}

#[test]
#[expected_failure(abort_code = EInsufficientFundsInExchange)]
fun test_insufficient_funds_in_exchange() {
let ctx = &mut tx_context::dummy();
let (mut exchange, _admin_cap) = new_for_testing(2, ctx);

exchange.add_all_sui(coin::mint_for_testing(1_000_000, ctx));
let wal_coin = exchange.exchange_all_for_wal(coin::mint_for_testing(1, ctx), ctx);

destroy(wal_coin);
destroy(exchange);
destroy(_admin_cap);
}

#[test]
#[expected_failure(abort_code = EInsufficientInputBalance)]
fun test_insufficient_coin() {
let ctx = &mut tx_context::dummy();
let (mut exchange, _admin_cap) = new_for_testing(2, ctx);

exchange.add_all_sui(coin::mint_for_testing(1_000_000, ctx));
let mut sui_coin = coin::mint_for_testing(1, ctx);
let wal_coin = exchange.exchange_for_wal(&mut sui_coin, 2, ctx);

destroy(sui_coin);
destroy(wal_coin);
destroy(exchange);
destroy(_admin_cap);
}

#[test]
#[expected_failure(abort_code = EUnauthorizedAdminCap)]
fun test_unauthorized() {
let ctx = &mut tx_context::dummy();
let (mut exchange_1, _admin_cap_1) = new_for_testing(2, ctx);
let (mut _exchange_2, admin_cap_2) = new_for_testing(2, ctx);

exchange_1.set_exchange_rate(1, 1, &admin_cap_2);

destroy(exchange_1);
destroy(_admin_cap_1);
destroy(_exchange_2);
destroy(admin_cap_2);
}

#[test]
fun test_creation() {
let ctx = &mut tx_context::dummy();
let mut coin = coin::mint_for_testing(1_000_000, ctx);
let _admin_cap_1 = new(ctx);
let _admin_cap_2 = new_funded(&mut coin, 100, ctx);
(ctx);

destroy(coin);
destroy(_admin_cap_1);
destroy(_admin_cap_2);
}

0 comments on commit 0c15cb8

Please sign in to comment.