Skip to content

Commit

Permalink
[move] new fungible token example: synthetic currency backed by baske…
Browse files Browse the repository at this point in the history
…t of other currencies

Useful to show how to represent a "reserve" via a singleton shared object. A variant of this approach can also be used to issue a token that is backed by a basket of NFT's.
  • Loading branch information
sblackshear committed Apr 6, 2022
1 parent 3d6c70a commit 0ccce8e
Show file tree
Hide file tree
Showing 4 changed files with 139 additions and 3 deletions.
1 change: 1 addition & 0 deletions sui_programmability/examples/fungible_tokens/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Fungible Tokens

* MANAGED: a token managed by a treasurer trusted for minting and burning. This is how (e.g.) a fiat-backed stablecoin would work.
* BASKET: a synthetic token backed by a basket of other assets. This how (e.g.) a [SDR](https://www.imf.org/en/About/Factsheets/Sheets/2016/08/01/14/51/Special-Drawing-Right-SDR)-like asset would work.
* FIXED: a token with a fixed supply (coming soon).
* ALGO: a token with an algorithmic issuance policy (coming soon).
92 changes: 92 additions & 0 deletions sui_programmability/examples/fungible_tokens/sources/BASKET.move
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// Copyright (c) 2022, Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

/// A synthetic fungible token backed by a basket of other tokens.
/// Here, we use a basket that is 1:1 SUI and MANAGED,
/// but this approach would work for a basket with arbitrary assets/ratios.
/// E.g., [SDR](https://www.imf.org/en/About/Factsheets/Sheets/2016/08/01/14/51/Special-Drawing-Right-SDR)
/// could be implemented this way.
module FungibleTokens::BASKET {
use FungibleTokens::MANAGED::MANAGED;
use Sui::Coin::{Self, Coin, TreasuryCap};
use Sui::ID::VersionedID;
use Sui::SUI::SUI;
use Sui::Transfer;
use Sui::TxContext::{Self, TxContext};

/// Name of the coin. By convention, this type has the same name as its parent module
/// and has no fields. The full type of the coin defined by this module will be `COIN<BASKET>`.
struct BASKET has drop { }

/// Singleton shared object holding the reserve assets and the capability.
struct Reserve has key {
id: VersionedID,
/// capability allowing the reserve to mint and burn BASKET
treasury_cap: TreasuryCap<BASKET>,
/// SUI coins held in the reserve
sui: Coin<SUI>,
/// MANAGED coins held in the reserve
managed: Coin<MANAGED>,
}

/// Needed to deposit a 1:1 ratio of SUI and MANAGED for minting, but deposited a different ratio
const EBAD_DEPOSIT_RATIO: u64 = 0;

fun init(ctx: &mut TxContext) {
// Get a treasury cap for the coin put it in the reserve
let treasury_cap = Coin::create_currency<BASKET>(BASKET{}, ctx);
Transfer::share_object(Reserve {
id: TxContext::new_id(ctx),
treasury_cap,
sui: Coin::zero<SUI>(ctx),
managed: Coin::zero<MANAGED>(ctx),
})
}

/// === Writes ===
/// Mint BASKET coins by accepting an equal number of SUI and MANAGED coins
public fun mint(
reserve: &mut Reserve, sui: Coin<SUI>, managed: Coin<MANAGED>, ctx: &mut TxContext
): Coin<BASKET> {
let num_sui = Coin::value(&sui);
assert!(num_sui == Coin::value(&managed), EBAD_DEPOSIT_RATIO);

Coin::join(&mut reserve.sui, sui);
Coin::join(&mut reserve.managed, managed);
Coin::mint(num_sui, &mut reserve.treasury_cap, ctx)
}

/// Burn BASKET coins and return the underlying reserve assets
public fun burn(
reserve: &mut Reserve, basket: Coin<BASKET>, ctx: &mut TxContext
): (Coin<SUI>, Coin<MANAGED>) {
let num_basket = Coin::value(&basket);
Coin::burn(basket, &mut reserve.treasury_cap);
let sui = Coin::withdraw(&mut reserve.sui, num_basket, ctx);
let managed = Coin::withdraw(&mut reserve.managed, num_basket, ctx);
(sui, managed)
}

// === Reads ===

/// Return the number of `MANAGED` coins in circulation
public fun total_supply(reserve: &Reserve): u64 {
Coin::total_supply(&reserve.treasury_cap)
}

/// Return the number of SUI in the reserve
public fun sui_supply(reserve: &Reserve): u64 {
Coin::value(&reserve.sui)
}

/// Return the number of MANAGED in the reserve
public fun managed_supply(reserve: &Reserve): u64 {
Coin::value(&reserve.managed)
}

#[test_only]
public fun init_for_testing(ctx: &mut TxContext) {
init(ctx)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,11 @@ module FungibleTokens::MANAGED {
/// and has no fields. The full type of the coin defined by this module will be `COIN<MANAGED>`.
struct MANAGED has drop {}

/// Register the trusted currency to acquire its `TreasuryCap`. Because
/// Register the managed currency to acquire its `TreasuryCap`. Because
/// this is a module initializer, it ensures the currency only gets
/// registered once.
fun init(ctx: &mut TxContext) {
// Get a treasury cap for the coin and give it to the transaction
// sender
// Get a treasury cap for the coin and give it to the transaction sender
let treasury_cap = Coin::create_currency<MANAGED>(MANAGED{}, ctx);
Transfer::transfer(treasury_cap, TxContext::sender(ctx))
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright (c) 2022, Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

#[test_only]
module FungibleTokens::BASKETTests {
use FungibleTokens::BASKET::{Self, Reserve};
use FungibleTokens::MANAGED::MANAGED;
use Sui::Coin;
use Sui::SUI::SUI;
use Sui::TestScenario;

#[test]
public fun test_mint_burn() {
let user = @0xA;

let scenario = &mut TestScenario::begin(&user);
{
let ctx = TestScenario::ctx(scenario);
BASKET::init_for_testing(ctx);
};
TestScenario::next_tx(scenario, &user);
{
let reserve = TestScenario::remove_object<Reserve>(scenario);
let ctx = TestScenario::ctx(scenario);
assert!(BASKET::total_supply(&reserve) == 0, 0);

let num_coins = 10;
let sui = Coin::mint_for_testing<SUI>(num_coins, ctx);
let managed = Coin::mint_for_testing<MANAGED>(num_coins, ctx);
let basket = BASKET::mint(&mut reserve, sui, managed, ctx);
assert!(Coin::value(&basket) == num_coins, 1);
assert!(BASKET::total_supply(&reserve) == num_coins, 2);

let (sui, managed) = BASKET::burn(&mut reserve, basket, ctx);
assert!(Coin::value(&sui) == num_coins, 3);
assert!(Coin::value(&managed) == num_coins, 4);

Coin::keep(sui, ctx);
Coin::keep(managed, ctx);
TestScenario::return_object(scenario, reserve);
}
}

}

0 comments on commit 0ccce8e

Please sign in to comment.