From c1ed7cc451119432d32d8eacf471f314e0ce5275 Mon Sep 17 00:00:00 2001 From: Sam Blackshear Date: Thu, 18 Nov 2021 15:19:32 -0800 Subject: [PATCH] [RFC] Example-driven proposal for Move-based programmability --- .gitattributes | 2 + .gitignore | 4 + fastx_programmability/Move.toml | 16 ++ fastx_programmability/README.md | 21 ++ .../examples/CombinableObjects.move | 59 ++++ .../examples/CustomObjectTemplate.move | 107 ++++++++ fastx_programmability/examples/EconMod.move | 92 +++++++ fastx_programmability/examples/Hero.move | 254 ++++++++++++++++++ fastx_programmability/examples/HeroMod.move | 126 +++++++++ .../examples/TrustedCoin.move | 22 ++ .../sources/Authenticator.move | 40 +++ fastx_programmability/sources/Coin.move | 120 +++++++++ fastx_programmability/sources/Escrow.move | 88 ++++++ fastx_programmability/sources/ID.move | 46 ++++ fastx_programmability/sources/Immutable.move | 23 ++ fastx_programmability/sources/Math.move | 21 ++ fastx_programmability/sources/Object.move | 33 +++ fastx_programmability/sources/Transfer.move | 11 + fastx_programmability/sources/TxContext.move | 41 +++ 19 files changed, 1126 insertions(+) create mode 100644 .gitattributes create mode 100644 fastx_programmability/Move.toml create mode 100644 fastx_programmability/README.md create mode 100644 fastx_programmability/examples/CombinableObjects.move create mode 100644 fastx_programmability/examples/CustomObjectTemplate.move create mode 100644 fastx_programmability/examples/EconMod.move create mode 100644 fastx_programmability/examples/Hero.move create mode 100644 fastx_programmability/examples/HeroMod.move create mode 100644 fastx_programmability/examples/TrustedCoin.move create mode 100644 fastx_programmability/sources/Authenticator.move create mode 100644 fastx_programmability/sources/Coin.move create mode 100644 fastx_programmability/sources/Escrow.move create mode 100644 fastx_programmability/sources/ID.move create mode 100644 fastx_programmability/sources/Immutable.move create mode 100644 fastx_programmability/sources/Math.move create mode 100644 fastx_programmability/sources/Object.move create mode 100644 fastx_programmability/sources/Transfer.move create mode 100644 fastx_programmability/sources/TxContext.move diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000000..a80e5d093862f --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Best-effort syntax highlighting for Move: just use Rust +*.move linguist-language=Rust diff --git a/.gitignore b/.gitignore index 477968a2ff236..b03f6552fc369 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,9 @@ +# Rust build directory /target +# Move build directory +build + .DS_Store # Thumbnails diff --git a/fastx_programmability/Move.toml b/fastx_programmability/Move.toml new file mode 100644 index 0000000000000..a3d1e5368d7e5 --- /dev/null +++ b/fastx_programmability/Move.toml @@ -0,0 +1,16 @@ +[package] +name = "FastX" +version = "0.0.1" + +[dependencies] +MoveStdlib = { git = "https://github.com/diem/diem.git", subdir="language/move-stdlib", rev = "56ab033cc403b489e891424a629e76f643d4fb6b" } + +[addresses] +Std = "0x1" +FastX = "0x2" +Examples = "0x3" + +[dev-addresses] +Std = "0x1" +FastX = "0x2" +Examples = "0x3" diff --git a/fastx_programmability/README.md b/fastx_programmability/README.md new file mode 100644 index 0000000000000..12dc2c735b412 --- /dev/null +++ b/fastx_programmability/README.md @@ -0,0 +1,21 @@ +# FastX Programmability with Move + +This is a proof-of-concept Move standard library for FastX (`sources/`), along with several examples of programs that FastX users might want to write (`examples`). `CustomObjectTemplate.move` is a good starting point for understanding the proposed model. + +### Setup + +``` +# install Move CLI +cargo install --git https://github.com/diem/diem move-cli --branch main +# put it in your PATH +export PATH="$PATH:~/.cargo/bin" +``` + +For reading/editing Move, your best bet is vscode + this [plugin](https://marketplace.visualstudio.com/items?itemName=move.move-analyzer). + +### Building + +``` +# Inside the fastx_programmability/ dir +move package -d build +``` diff --git a/fastx_programmability/examples/CombinableObjects.move b/fastx_programmability/examples/CombinableObjects.move new file mode 100644 index 0000000000000..f54bfea50db3a --- /dev/null +++ b/fastx_programmability/examples/CombinableObjects.move @@ -0,0 +1,59 @@ +/// Example of objects that can be combined to create +/// new objects +module Examples::CombinableObjects { + use Examples::TrustedCoin::EXAMPLE; + use FastX::Authenticator::{Self, Authenticator}; + use FastX::Coin::{Self, Coin}; + use FastX::ID::ID; + use FastX::Transfer; + use FastX::TxContext::{Self, TxContext}; + + struct Ham has key { + id: ID + } + + struct Bread has key { + id: ID + } + + struct Sandwich has key { + id: ID + } + + /// Address selling ham, bread, etc + const GROCERY: vector = b""; + /// Price for ham + const HAM_PRICE: u64 = 10; + /// Price for bread + const BREAD_PRICE: u64 = 2; + + /// Not enough funds to pay for the good in question + const EINSUFFICIENT_FUNDS: u64 = 0; + + /// Exchange `c` for some ham + public fun buy_ham(c: Coin, ctx: &mut TxContext): Ham { + assert!(Coin::value(&c) == HAM_PRICE, EINSUFFICIENT_FUNDS); + Transfer::transfer(c, admin()); + Ham { id: TxContext::new_id(ctx) } + } + + /// Exchange `c` for some bread + public fun buy_bread(c: Coin, ctx: &mut TxContext): Bread { + assert!(Coin::value(&c) == BREAD_PRICE, EINSUFFICIENT_FUNDS); + Transfer::transfer(c, admin()); + Bread { id: TxContext::new_id(ctx) } + } + + /// Combine the `ham` and `bread` into a delicious sandwich + public fun make_sandwich( + ham: Ham, bread: Bread, ctx: &mut TxContext + ): Sandwich { + let Ham { id: _ } = ham; + let Bread { id: _ } = bread; + Sandwich { id: TxContext::new_id(ctx) } + } + + fun admin(): Authenticator { + Authenticator::new(GROCERY) + } +} diff --git a/fastx_programmability/examples/CustomObjectTemplate.move b/fastx_programmability/examples/CustomObjectTemplate.move new file mode 100644 index 0000000000000..78ce92f1cd628 --- /dev/null +++ b/fastx_programmability/examples/CustomObjectTemplate.move @@ -0,0 +1,107 @@ +/// An example of a custom object with comments explaining the relevant bits +module Examples::CustomObjectTemplate { + use FastX::Authenticator::{Self, Authenticator}; + use FastX::ID::ID; + use FastX::Transfer; + use FastX::TxContext::{Self, TxContext}; + + /// A custom fastX object. Every object must have the `key` attribute + /// (indicating that it is allowed to be a key in the fastX global object + /// pool), and must have a field `id: ID` corresponding to its fastX ObjId. + /// Other object attributes present at the protocol level (authenticator, + /// sequence number, TxDigest, ...) are intentionally not exposed here. + struct Object has key { + id: ID, + /// Custom objects can have fields of arbitrary type... + custom_field: u64, + /// ... including other objects + child_obj: ChildObject, + /// ... and other global objects + nested_obj: AnotherObject, + } + + /// An object that can be stored inside global objects or other child + /// objects, but cannot be placed in the global object pool on its own. + /// Note that it doesn't need an ID field + struct ChildObject has store { + a_field: bool, + } + + /// An object that can live either in the global object pool or as a nested + /// object. + struct AnotherObject has key, store { + id: ID, + } + + /// Example of updating an object. All Move fields are private, so the + /// fields of `Object` can only be (directly) updated by code in this + /// module. + public fun write_field(o: &mut Object, v: u64) { + if (some_conditional_logic()) { + o.custom_field = v + } + } + + /// Example of transferring an object to a a new owner. A struct can only + /// be transferred by the module that declares it. + public fun transfer(o: Object, recipient: Authenticator) { + assert!(some_conditional_logic(), 0); + Transfer::transfer(o, recipient) + } + + /// Simple getter + public fun read_field(o: &Object): u64 { + o.custom_field + } + + /// Example of creating a object by deriving a unique ID from the current + /// transaction and returning it to the caller (who may call functions + /// from this module to read/write it, package it into another object, ...) + public fun create(tx: &mut TxContext): Object { + Object { + id: TxContext::new_id(tx), + custom_field: 0, + child_obj: ChildObject { a_field: false }, + nested_obj: AnotherObject { id: TxContext::new_id(tx) } + } + } + + /// Example of an entrypoint function to be embedded in a FastX + /// transaction. The first argument of an entrypoint is always a + /// special `TxContext` created by the runtime that is useful for deriving + /// new id's or determining the sender of the transaction. + /// After the `TxContext`, entrypoints can take struct types with the `key` + /// attribute as input, as well as primitive types like ints, bools, ... + /// + /// A FastX transaction must declare the ID's of each object it will + /// access + any primitive inputs. The runtime that processes the + /// transaction fetches the values associated with the ID's, type-checks + /// the values + primitive inputs against the function signature of the + /// `main`, then calls the function with these values. + /// + /// If the script terminates successfully, the runtime collects changes to + /// input objects + created objects + emitted events, increments the + /// sequence number each object, creates a hash that commits to the + /// outputs, etc. + public fun main( + ctx: &mut TxContext, + to_read: &Object, + to_write: &mut Object, + to_consume: Object, + // ... end objects, begin primitive type inputs + int_input: u64, + bytes_input: vector + ) { + let v = read_field(to_read); + write_field(to_write, v + int_input); + transfer(to_consume, Authenticator::new(bytes_input)); + // demonstrate creating a new object for the sender + let sender = TxContext::get_authenticator(ctx); + Transfer::transfer(create(ctx), sender) + } + + fun some_conditional_logic(): bool { + // placeholder for checks implemented in arbitrary Move code + true + } +} diff --git a/fastx_programmability/examples/EconMod.move b/fastx_programmability/examples/EconMod.move new file mode 100644 index 0000000000000..7fc82cd36c169 --- /dev/null +++ b/fastx_programmability/examples/EconMod.move @@ -0,0 +1,92 @@ +/// Mod of the economics of the SeaScape game. In the game, a `Hero` can onAly +/// slay a `SeaMonster` if they have sufficient strength. This mod allows a +/// player with a weak `Hero` to ask a player with a stronger `Hero` to slay +/// the monster for them in exchange for some of the reward. +module Examples::EconMod { + use Examples::HeroMod::{Self, SeaMonster, RUM}; + use Examples::Hero::Hero; + use FastX::Authenticator::Authenticator; + use FastX::Coin::{Self, Coin}; + use FastX::ID::ID; + use FastX::Transfer; + use FastX::TxContext::{Self, TxContext}; + + /// Created by `monster_owner`, a player with a monster that's too strong + /// for them to slay + transferred to a player who can slay the monster. + /// The two players split the reward for slaying the monster according to + /// the `helper_reward` parameter. + struct HelpMeSlayThisMonster has key { + id: ID, + /// Monster to be slay by the owner of this object + monster: SeaMonster, + /// Identity of the user that originally owned the monster + monster_owner: Authenticator, + /// Number of tokens that will go to the helper. The owner will get + /// the `monster` reward - `helper_reward` tokens + helper_reward: u64, + } + + // TODO: proper error codes + /// The specified helper reward is too large + const EINVALID_HELPER_REWARD: u64 = 0; + + /// Create an offer for `helper` to slay the monster in exchange for + /// some of the reward + public fun create( + monster: SeaMonster, + helper_reward: u64, + helper: Authenticator, + ctx: &mut TxContext, + ) { + // make sure the advertised reward is not too large + that the owner + // gets a nonzero reward + assert!( + HeroMod::monster_reward(&monster) > helper_reward, + EINVALID_HELPER_REWARD + ); + Transfer::transfer( + HelpMeSlayThisMonster { + id: TxContext::new_id(ctx), + monster, + monster_owner: TxContext::get_authenticator(ctx), + helper_reward + }, + helper + ) + } + + /// Helper should call this if they are willing to help out and slay the + /// monster. + public fun slay( + hero: &Hero, wrapper: HelpMeSlayThisMonster, ctx: &mut TxContext, + ): Coin { + let HelpMeSlayThisMonster { + id: _, + monster, + monster_owner, + helper_reward + } = wrapper; + let owner_reward = HeroMod::slay(hero, monster); + let helper_reward = Coin::withdraw(&mut owner_reward, helper_reward, ctx); + Transfer::transfer(owner_reward, monster_owner); + helper_reward + } + + /// Helper can call this if they can't help slay the monster or don't want + /// to, and are willing to kindly return the monster to its owner. + public fun return_to_owner(wrapper: HelpMeSlayThisMonster) { + let HelpMeSlayThisMonster { + id: _, + monster, + monster_owner, + helper_reward: _ + } = wrapper; + HeroMod::transfer_monster(monster, monster_owner) + } + + /// Return the number of coins that `wrapper.owner` will earn if the + /// the helper slays the monster in `wrapper. + public fun owner_reward(wrapper: &HelpMeSlayThisMonster): u64 { + HeroMod::monster_reward(&wrapper.monster) - wrapper.helper_reward + } +} diff --git a/fastx_programmability/examples/Hero.move b/fastx_programmability/examples/Hero.move new file mode 100644 index 0000000000000..797df19dbeb68 --- /dev/null +++ b/fastx_programmability/examples/Hero.move @@ -0,0 +1,254 @@ +/// Example of a game character with basic attributes, inventory, and +/// associated logic. +module Examples::Hero { + use Examples::TrustedCoin::EXAMPLE; + use FastX::Authenticator::{Self, Authenticator}; + use FastX::Coin::{Self, Coin}; + use FastX::ID::ID; + use FastX::Math; + use FastX::Transfer; + use FastX::TxContext::{Self, TxContext}; + use Std::Option::{Self, Option}; + + /// Our hero! + struct Hero has key, store { + id: ID, + /// Hit points. If they go to zero, the hero can't do anything + hp: u64, + /// Experience of the hero. Begins at zero + experience: u64, + /// The hero's minimal inventory + sword: Option, + } + + /// The hero's trusty sword + struct Sword has key, store { + id: ID, + /// Constant set at creation. Acts as a multiplier on sword's strength. + /// Swords with high magic are rarer (because they cost more). + magic: u64, + /// Sword grows in strength as we use it + strength: u64, + } + + /// For healing wounded heroes + struct Potion has key, store { + id: ID, + /// Effectivenss of the potion + potency: u64 + } + + /// A creature that the hero can slay to level up + struct Boar has key { + id: ID, + /// Hit points before the boar is slain + hp: u64, + /// Strength of this particular boar + strength: u64 + } + + /// Capability conveying the authority to create boars and potions + struct GameAdmin has key { + id: ID, + /// Total number of boars the admin has created + boars_created: u64, + /// Total number of potions the admin has created + potions_created: u64 + } + + /// Authenticator of the admin account that receives payment for swords + const ADMIN: vector = b"some_authenticator_here"; + /// Upper bound on player's HP + const MAX_HP: u64 = 1000; + /// Upper bound on how magical a sword can be + const MAX_MAGIC: u64 = 10; + /// Minimum amount you can pay for a sword + const MIN_SWORD_COST: u64 = 100; + + // TODO: proper error codes + /// The boar won the battle + const EBOAR_WON: u64 = 0; + /// The hero is too tired to fight + const EHERO_TIRED: u64 = 1; + /// Trying to initialize from a non-admin account + const ENOT_ADMIN: u64 = 2; + /// Not enough money to purchase the given item + const EINSUFFICIENT_FUNDS: u64 = 3; + /// Trying to remove a sword, but the hero does not have one + const ENO_SWORD: u64 = 4; + + // --- Initialization + + /// Create the `GameAdmin` capability and hand it off to the admin + /// authenticator + fun init(ctx: &mut TxContext) { + let admin = admin(); + // ensure this is being initialized by the expected admin authenticator + assert!(&TxContext::get_authenticator(ctx) == &admin, ENOT_ADMIN); + Transfer::transfer( + GameAdmin { + id: TxContext::new_id(ctx), + boars_created: 0, + potions_created: 0 + }, + admin + ) + } + + // --- Gameplay --- + + /// Slay the `boar` with the `hero`'s sword, get experience. + /// Aborts if the hero has 0 HP or is not strong enough to slay the boar + public fun slay(hero: &mut Hero, boar: Boar) { + let Boar { id: _, strength: boar_strength, hp } = boar; + let hero_strength = hero_strength(hero); + let boar_hp = hp; + let hero_hp = hero.hp; + // attack the boar with the sword until its HP goes to zero + while (boar_hp > hero_strength) { + // first, the hero attacks + boar_hp = boar_hp - hero_strength; + // then, the boar gets a turn to attack. if the boar would kill + // the hero, abort--we can't let the boar win! + assert!(hero_hp >= boar_strength , EBOAR_WON); + hero_hp = hero_hp - boar_strength; + + }; + // hero takes their licks + hero.hp = hero_hp; + // hero gains experience proportional to the boar, sword grows in + // strength by one (if hero is using a sword) + hero.experience = hero.experience + hp; + if (Option::is_some(&hero.sword)) { + level_up_sword(Option::borrow_mut(&mut hero.sword), 1) + }; + } + + /// Strength of the hero when attacking + public fun hero_strength(hero: &Hero): u64 { + // a hero with zero HP is too tired to fight + if (hero.hp == 0) { + return 0 + }; + + let sword_strength = if (Option::is_some(&hero.sword)) { + sword_strength(Option::borrow(&hero.sword)) + } else { + // hero can fight without a sword, but will not be very strong + 0 + }; + // hero is weaker if he has lower HP + (hero.experience * hero.hp) + sword_strength + } + + fun level_up_sword(sword: &mut Sword, amount: u64) { + sword.strength = sword.strength + amount + } + + /// Strength of a sword when attacking + public fun sword_strength(sword: &Sword): u64 { + sword.magic + sword.strength + } + + // --- Inventory --- + + /// Heal the weary hero with a potion + public fun heal(hero: &mut Hero, potion: Potion) { + let Potion { id: _, potency } = potion; + let new_hp = hero.hp + potency; + // cap hero's HP at MAX_HP to avoid int overflows + hero.hp = Math::min(new_hp, MAX_HP) + } + + /// Add `new_sword` to the hero's inventory and return the old sword + /// (if any) + public fun equip_sword(hero: &mut Hero, new_sword: Sword): Option { + // TODO: we should add a Option::swap_opt(&mut Option, T): Option + // that does this to the Move stdlib + if (Option::is_some(&hero.sword)) { + Option::some(Option::swap(&mut hero.sword, new_sword)) + } else { + Option::fill(&mut hero.sword, new_sword); + Option::none() + } + } + + /// Disarm the hero by returning their sword. + /// Aborts if the hero does not have a sword. + public fun remove_sword(hero: &mut Hero): Sword { + assert!(Option::is_some(&hero.sword), ENO_SWORD); + Option::extract(&mut hero.sword) + } + + // --- Object creation --- + + /// It all starts with the sword. Anyone can buy a sword, and proceeds go + /// to the admin. Amount of magic in the sword depends on how much you pay + /// for it. + public fun create_sword( + payment: Coin, + ctx: &mut TxContext + ): Sword { + let value = Coin::value(&payment); + // ensure the user pays enough for the sword + assert!(value >= MIN_SWORD_COST, EINSUFFICIENT_FUNDS); + // pay the admin for ths sword + Transfer::transfer(payment, admin()); + + // magic of the sword is proportional to the amount you paid, up to + // a max. one can only imbue a sword with so much magic + let magic = (value - MIN_SWORD_COST) / MIN_SWORD_COST; + Sword { + id: TxContext::new_id(ctx), + magic: Math::min(magic, MAX_MAGIC), + strength: 1 + } + } + + /// Anyone can create a hero if they have a sword. All heros start with the + /// same attributes. + public fun create_hero(sword: Sword, ctx: &mut TxContext): Hero { + Hero { + id: TxContext::new_id(ctx), + hp: 100, + experience: 0, + sword: Option::some(sword), + } + } + + /// Admin can create a potion with the given `potency` for `recipient` + public fun send_potion( + potency: u64, + player: Authenticator, + admin: &mut GameAdmin, + ctx: &mut TxContext + ) { + admin.potions_created = admin.potions_created + 1; + // send potion to the designated player + Transfer::transfer( + Potion { id: TxContext::new_id(ctx), potency }, + player + ) + } + + /// Admin can create a boar with the given attributes for `recipient` + public fun send_boar( + hp: u64, + strength: u64, + player: Authenticator, + admin: &mut GameAdmin, + ctx: &mut TxContext + ) { + admin.boars_created = admin.boars_created + 1; + // send boars to the designated player + Transfer::transfer( + Boar { id: TxContext::new_id(ctx), hp, strength }, + player + ) + } + + fun admin(): Authenticator { + Authenticator::new(ADMIN) + } + +} diff --git a/fastx_programmability/examples/HeroMod.move b/fastx_programmability/examples/HeroMod.move new file mode 100644 index 0000000000000..dad34544bccfa --- /dev/null +++ b/fastx_programmability/examples/HeroMod.move @@ -0,0 +1,126 @@ +/// Example of a game mod or different game that uses objects from the Hero +/// game. +/// This mod introduces sea monsters that can also be slain with the hero's +/// sword. Instead of boosting the hero's experience, slaying sea monsters +/// earns RUM tokens for hero's owner. +/// Note that this mod does not require special permissions from `Hero` module; +/// anyone is free to create a mod like this. +module Examples::HeroMod { + use Examples::Hero::{Self, Hero}; + use FastX::Authenticator::Authenticator; + use FastX::ID::ID; + use FastX::Coin::{Self, Coin, TreasuryCap }; + use FastX::Transfer; + use FastX::TxContext::{Self, TxContext}; + + /// A new kind of monster for the hero to fight + struct SeaMonster has key, store { + id: ID, + /// Tokens that the user will earn for slaying this monster + reward: Coin + } + + /// Admin capability granting permission to mint RUM tokens and + /// create monsters + struct SeaScapeAdmin has key { + id: ID, + /// Permission to mint RUM + treasury_cap: TreasuryCap, + /// Total number of monsters created so far + monsters_created: u64, + /// cap on the supply of RUM + token_supply_max: u64, + /// cap on the number of monsters that can be created + monster_max: u64 + } + + /// Type of the sea game token + struct RUM has drop {} + + // TODO: proper error codes + /// Hero is not strong enough to defeat the monster. Try healing with a + /// potion, fighting boars to gain more experience, or getting a better + /// sword + const EHERO_NOT_STRONG_ENOUGH: u64 = 0; + /// Too few tokens issued + const EINVALID_TOKEN_SUPPLY: u64 = 1; + /// Too few monsters created + const EINVALID_MONSTER_SUPPLY: u64 = 2; + + // --- Initialization --- + + /// Get a treasury cap for the coin and give it to the admin + // TODO: this leverages Move module initializers + fun init(ctx: &mut TxContext, token_supply_max: u64, monster_max: u64) { + // a game with no tokens and/or no monsters is no fun + assert!(token_supply_max > 0, EINVALID_TOKEN_SUPPLY); + assert!(monster_max > 0, EINVALID_MONSTER_SUPPLY); + + Transfer::transfer( + SeaScapeAdmin { + id: TxContext::new_id(ctx), + treasury_cap: Coin::create_currency(RUM{}, ctx), + monsters_created: 0, + token_supply_max, + monster_max, + }, + TxContext::get_authenticator(ctx) + ) + } + + // --- Gameplay --- + + /// Slay the `monster` with the `hero`'s sword, earn RUM tokens in + /// exchange. + /// Aborts if the hero is not strong enough to slay the monster + public fun slay(hero: &Hero, monster: SeaMonster): Coin { + let SeaMonster { id: _, reward } = monster; + // Hero needs strength greater than the reward value to defeat the + // monster + assert!( + Hero::hero_strength(hero) >= Coin::value(&reward), + EHERO_NOT_STRONG_ENOUGH + ); + + reward + } + + // --- Object and coin creation --- + + /// Game admin can reate a monster wrapping a coin worth `reward` and send + /// it to `recipient` + public fun create_monster( + admin: &mut SeaScapeAdmin, + reward_amount: u64, + recipient: Authenticator, + ctx: &mut TxContext + ) { + let current_coin_supply = Coin::total_supply(&admin.treasury_cap); + let token_supply_max = admin.token_supply_max; + // TODO: create error codes + // ensure token supply cap is respected + assert!(reward_amount < token_supply_max, 0); + assert!(token_supply_max - reward_amount >= current_coin_supply, 1); + // ensure monster supply cap is respected + assert!(admin.monster_max - 1 >= admin.monsters_created, 2); + + let monster = SeaMonster { + id: TxContext::new_id(ctx), + reward: Coin::mint(reward_amount, &mut admin.treasury_cap, ctx) + }; + admin.monsters_created = admin.monsters_created + 1; + Transfer::transfer(monster, recipient); + } + + /// Send `monster` to `recipient` + public fun transfer_monster( + monster: SeaMonster, recipient: Authenticator + ) { + Transfer::transfer(monster, recipient) + } + + /// Reward a hero will reap from slaying this monster + public fun monster_reward(monster: &SeaMonster): u64 { + Coin::value(&monster.reward) + } +} diff --git a/fastx_programmability/examples/TrustedCoin.move b/fastx_programmability/examples/TrustedCoin.move new file mode 100644 index 0000000000000..786636f80e487 --- /dev/null +++ b/fastx_programmability/examples/TrustedCoin.move @@ -0,0 +1,22 @@ +/// Example coin with a trusted owner responsible for minting/burning (e.g., a stablecoin) +module Examples::TrustedCoin { + use FastX::Coin; + use FastX::Transfer; + use FastX::TxContext::{Self, TxContext}; + + /// Name of the coin + struct EXAMPLE has drop {} + + /// Register the trusted currency to acquire its `TreasuryCap`. Because + /// this is a module initializer, it ensures the currency only gets + /// registered once. + // TODO: this uses a module initializer, which doesn't exist in Move. + // However, we can (and I think should) choose to support this in the FastX + // adapter to enable us -cases that require at-most-once semantics + fun init(ctx: &mut TxContext) { + // Get a treasury cap for the coin and give it to the transaction + // sender + let treasury_cap = Coin::create_currency(EXAMPLE{}, ctx); + Transfer::transfer(treasury_cap, TxContext::get_authenticator(ctx)) + } +} diff --git a/fastx_programmability/sources/Authenticator.move b/fastx_programmability/sources/Authenticator.move new file mode 100644 index 0000000000000..41f6cd0eec8fa --- /dev/null +++ b/fastx_programmability/sources/Authenticator.move @@ -0,0 +1,40 @@ +module FastX::Authenticator { + /// Authenticator for an end-user (e.g., a public key) + // TODO: ideally, we would use the Move `address`` type here, + // but Move forces `address`'s to be 16 bytes + struct Authenticator has copy, drop, store { + bytes: vector + } + + /// Unforgeable token representing the authority of a particular + /// `Authenticator`. Can only be created by the VM + struct Signer has drop { + inner: Authenticator, + } + + // TODO: validation of bytes once we settle on authenticator format + public fun new(bytes: vector): Authenticator { + Authenticator { bytes } + } + + /// Derive an `Authenticator` from a `Signer` + public fun get(self: &Signer): &Authenticator { + &self.inner + } + + /// Get the raw bytes associated with `self` + public fun bytes(self: &Authenticator): &vector { + &self.bytes + } + + /// Get the raw bytes associated with `self` + public fun signer_bytes(self: &Signer): &vector { + &self.inner.bytes + } + + + /// Return true if `a` is the underlying authenticator of `s` + public fun is_signer(a: &Authenticator, s: &Signer): bool { + get(s) == a + } +} diff --git a/fastx_programmability/sources/Coin.move b/fastx_programmability/sources/Coin.move new file mode 100644 index 0000000000000..ecf3b8876a23c --- /dev/null +++ b/fastx_programmability/sources/Coin.move @@ -0,0 +1,120 @@ +module FastX::Coin { + use FastX::Authenticator::Authenticator; + use FastX::ID::ID; + use FastX::Transfer; + use FastX::TxContext::{Self, TxContext}; + use Std::Errors; + use Std::Vector; + + /// A coin of type `T` worth `value`. Transferrable + struct Coin has key, store { + id: ID, + value: u64 + } + + /// Capability allowing the bearer to mint and burn + /// coins of type `T`. Transferrable + struct TreasuryCap has key, store { + id: ID, + total_supply: u64 + } + + /// Trying to withdraw N from a coin with value < N + const EVALUE: u64 = 0; + /// Trying to destroy a coin with a nonzero value + const ENONZERO: u64 = 0; + + // === Functionality for Coin holders === + + /// Send `c` to `recipient` + public fun transfer(c: Coin, recipient: Authenticator) { + Transfer::transfer(c, recipient) + } + + /// Consume the coin `c` and add its value to `self`. + /// Aborts if `c.value + self.value > U64_MAX` + public fun join(self: &mut Coin, c: Coin) { + let Coin { id: _, value } = c; + self.value = self.value + value + } + + /// Join everything in `coins` with `self` + public fun join_vec(self: &mut Coin, coins: vector>) { + let i = 0; + let len = Vector::length(&coins); + while (i < len) { + let coin = Vector::remove(&mut coins, i); + join(self, coin); + i = i + 1 + }; + // safe because we've drained the vector + Vector::destroy_empty(coins) + } + + /// Subtract `value` from `self` and create a new coin + /// worth `value` with ID `id`. + /// Aborts if `value > self.value` + public fun withdraw( + self: &mut Coin, value: u64, ctx: &mut TxContext, + ): Coin { + assert!( + self.value >= value, + Errors::limit_exceeded(EVALUE) + ); + self.value = self.value - value; + Coin { id: TxContext::new_id(ctx), value } + } + + /// Public getter for the coin's value + public fun value(self: &Coin): u64 { + self.value + } + + /// Destroy a coin with value zero + public fun destroy_zero(c: Coin) { + let Coin { id: _, value } = c; + assert!(value == 0, Errors::invalid_argument(ENONZERO)) + } + + // === Registering new coin types and managing the coin supply === + + /// Create a new currency type `T` as and return the `TreasuryCap` + /// for `T` to the caller. + /// NOTE: It is the caller's responsibility to ensure that + /// `create_currency` can only be invoked once (e.g., by calling it from a + /// module initializer with a `witness` object that can only be created + /// in the initializer). + public fun create_currency( + _witness: T, + ctx: &mut TxContext + ): TreasuryCap { + TreasuryCap { id: TxContext::new_id(ctx), total_supply: 0 } + } + + /// Create a coin worth `value`. and increase the total supply + /// in `cap` accordingly + /// Aborts if `value` + `cap.total_supply` >= U64_MAX + public fun mint( + value: u64, cap: &mut TreasuryCap, ctx: &mut TxContext, + ): Coin { + cap.total_supply = cap.total_supply + value; + Coin { id: TxContext::new_id(ctx), value } + } + + /// Destroy the coin `c` and decrease the total supply in `cap` + /// accordingly. + public fun burn(c: Coin, cap: &mut TreasuryCap) { + let Coin { id: _, value } = c; + cap.total_supply = cap.total_supply - value + } + + /// Return the total number of `T`'s in circulation + public fun total_supply(cap: &TreasuryCap): u64 { + cap.total_supply + } + + /// Give away the treasury cap to `recipient` + public fun transfer_cap(c: TreasuryCap, recipient: Authenticator) { + Transfer::transfer(c, recipient) + } +} diff --git a/fastx_programmability/sources/Escrow.move b/fastx_programmability/sources/Escrow.move new file mode 100644 index 0000000000000..23b254a85d0ae --- /dev/null +++ b/fastx_programmability/sources/Escrow.move @@ -0,0 +1,88 @@ +/// An escrow for atomic swap of objects that +/// trusts a third party for liveness, but not +/// safety. +module FastX::Escrow { + use FastX::Authenticator::Authenticator; + use FastX::ID::{Self, IDBytes, ID}; + use FastX::Transfer; + use FastX::TxContext::{Self, TxContext}; + + /// An object held in escrow + struct EscrowedObj has key, store { + id: ID, + /// owner of the escrowed object + sender: Authenticator, + /// intended recipient of the escrowed object + recipient: Authenticator, + /// ID of the object `sender` wants in exchange + // TODO: this is probably a bad idea if the object is mutable. + // that can be fixed by asking for an additional approval + // from `sender`, but let's keep it simple for now. + exchange_for: IDBytes, + /// the escrowed object + escrowed: T, + } + + // TODO: proper error codes + const ETODO: u64 = 0; + + /// Create an escrow for exchanging goods with + /// `counterparty`, mediated by a `third_party` + /// that is trusted for liveness + public fun create( + recipient: Authenticator, + third_party: Authenticator, + exchange_for: IDBytes, + escrowed: T, + ctx: &mut TxContext + ) { + let sender = TxContext::get_authenticator(ctx); + let id = TxContext::new_id(ctx); + // escrow the object with the trusted third party + Transfer::transfer( + EscrowedObj { + id, sender, recipient, exchange_for, escrowed + }, + third_party + ); + } + + /// Trusted third party can swap compatible objects + public fun swap( + obj1: EscrowedObj, obj2: EscrowedObj + ) { + let EscrowedObj { + id: _, + sender: sender1, + recipient: recipient1, + exchange_for: exchange_for1, + escrowed: escrowed1, + } = obj1; + let EscrowedObj { + id: _, + sender: sender2, + recipient: recipient2, + exchange_for: exchange_for2, + escrowed: escrowed2, + } = obj2; + // check sender/recipient compatibility + assert!(&sender1 == &recipient2, ETODO); + assert!(&sender2 == &recipient1, ETODO); + // check object ID compatibility + assert!(ID::get_id_bytes(&escrowed1) == &exchange_for2, ETODO); + assert!(ID::get_id_bytes(&escrowed2) == &exchange_for1, ETODO); + // everything matches. do the swap! + Transfer::transfer(escrowed1, sender2); + Transfer::transfer(escrowed2, sender1) + } + + /// Trusted third party can always return an escrowed object to its original owner + public fun return_to_sender( + obj: EscrowedObj, + ) { + let EscrowedObj { + id: _, sender, recipient: _, exchange_for: _, escrowed + } = obj; + Transfer::transfer(escrowed, sender) + } +} diff --git a/fastx_programmability/sources/ID.move b/fastx_programmability/sources/ID.move new file mode 100644 index 0000000000000..9f1b9db24cc58 --- /dev/null +++ b/fastx_programmability/sources/ID.move @@ -0,0 +1,46 @@ +/// FastX object identifiers +module FastX::ID { + friend FastX::TxContext; + + /// Globally unique identifier of an object. This is a privileged type + /// that can only be derived from a `TxContext` + struct ID has store, drop { + id: IDBytes + } + + /// Underlying representation of an ID. + /// Unlike ID, not a privileged type--can be freely copied and created + struct IDBytes has store, drop, copy { + bytes: vector + } + + /// Create a new ID. Only callable by Lib + public(friend) fun new(bytes: vector): ID { + ID { id: IDBytes { bytes } } + } + + /// Create a new ID bytes for comparison with existing ID's + public fun new_bytes(bytes: vector): IDBytes { + IDBytes { bytes } + } + + /// Get the underyling `IDBytes` of `id` + public fun get_inner(id: &ID): &IDBytes { + &id.id + } + + /// Get the `IDBytes` of `obj` + public fun get_id_bytes(obj: &T): &IDBytes { + &get_id(obj).id + } + + /// Get the raw bytes of `i` + public fun get_bytes(i: &IDBytes): &vector { + &i.bytes + } + + /// Get the ID for `obj`. Safe because fastX has an extra + /// bytecode verifier pass that forces every struct with + /// the `key` ability to have a distinguished `ID` field. + public native fun get_id(obj: &T): &ID; +} diff --git a/fastx_programmability/sources/Immutable.move b/fastx_programmability/sources/Immutable.move new file mode 100644 index 0000000000000..b43491f55f01f --- /dev/null +++ b/fastx_programmability/sources/Immutable.move @@ -0,0 +1,23 @@ +module FastX::Immutable { + use FastX::ID::ID; + use FastX::Transfer; + use FastX::TxContext::{Self, TxContext}; + + /// An immutable, non-transferrable object. + /// Unlike mutable objects, immutable objects can read by *any* + /// transaction, not only one signed by the object's owner. + /// The authenticator associated with an immutable object is + /// always its creator + struct Immutable has key { + id: ID, + /// Abritrary immutable data associated with the object + data: T + } + + /// Create an immutable object wrapping `data` owned by `ctx.signer` + public fun create(data: T, ctx: &mut TxContext) { + let id = TxContext::new_id(ctx); + let obj = Immutable { id, data }; + Transfer::transfer(obj, TxContext::get_authenticator(ctx)) + } +} diff --git a/fastx_programmability/sources/Math.move b/fastx_programmability/sources/Math.move new file mode 100644 index 0000000000000..7a19a22d353e5 --- /dev/null +++ b/fastx_programmability/sources/Math.move @@ -0,0 +1,21 @@ +/// Basic math for nicer programmability +module FastX::Math { + + /// Return the larger of `x` and `y` + public fun max(x: u64, y: u64): u64 { + if (x > y) { + x + } else { + y + } + } + + /// Return the smaller of `x` and `y` + public fun min(x: u64, y: u64): u64 { + if (x < y) { + x + } else { + y + } + } +} diff --git a/fastx_programmability/sources/Object.move b/fastx_programmability/sources/Object.move new file mode 100644 index 0000000000000..a9ff1e0c67db7 --- /dev/null +++ b/fastx_programmability/sources/Object.move @@ -0,0 +1,33 @@ +module FastX::Object { + use FastX::Authenticator::Authenticator; + use FastX::ID::ID; + use FastX::Transfer; + + /// Wrapper object for storing arbitrary mutable `data` in the global + /// object pool. + struct Object has key, store { + id: ID, + /// Abritrary data associated with the object + data: T + } + + /// Create a new object wrapping `data` with id `ID` + public fun new(data: T, id: ID): Object { + Object { id, data } + } + + /// Transfer object `o` to `recipient` + public fun transfer(o: Object, recipient: Authenticator) { + Transfer::transfer(o, recipient) + } + + /// Get a mutable reference to the data embedded in `self` + public fun data_mut(self: &mut Object): &mut T { + &mut self.data + } + + /// Get an immutable reference to the data embedded in `self` + public fun data(self: &Object): &T { + &self.data + } +} diff --git a/fastx_programmability/sources/Transfer.move b/fastx_programmability/sources/Transfer.move new file mode 100644 index 0000000000000..fadf2afedcef7 --- /dev/null +++ b/fastx_programmability/sources/Transfer.move @@ -0,0 +1,11 @@ +module FastX::Transfer { + use FastX::Authenticator::Authenticator; + + /// Transfer ownership of `obj` to `recipient`. `obj` must have the + /// `key` attribute, which (in turn) ensures that `obj` has a globally + /// unique ID. + // TODO: add bytecode verifier pass to ensure that `T` is a struct declared + // in the calling module. This will allow modules to define custom transfer + // logic for their structs that cannot be subverted by other modules + public native fun transfer(obj: T, recipient: Authenticator); +} diff --git a/fastx_programmability/sources/TxContext.move b/fastx_programmability/sources/TxContext.move new file mode 100644 index 0000000000000..3c43259e19f92 --- /dev/null +++ b/fastx_programmability/sources/TxContext.move @@ -0,0 +1,41 @@ +module FastX::TxContext { + use FastX::ID::{Self, ID}; + use FastX::Authenticator::{Self, Authenticator, Signer}; + use Std::BCS; + use Std::Hash; + use Std::Vector; + + /// Information about the transaction currently being executed + struct TxContext { + /// The signer of the current transaction + // TODO: use vector if we want to support multi-agent + signer: Signer, + /// Hash of all the input objects to this transaction + inputs_hash: vector, + /// Counter recording the number of objects created while executing + /// this transaction + objects_created: u64 + } + + /// Generate a new primary key + // TODO: can make this native for better perf + public fun new_id(ctx: &mut TxContext): ID { + let msg = *&ctx.inputs_hash; + let next_object_num = ctx.objects_created; + ctx.objects_created = next_object_num + 1; + + Vector::append(&mut msg, BCS::to_bytes(&next_object_num)); + ID::new(Hash::sha3_256(msg)) + } + + /// Return the signer of the current transaction + public fun get_signer(self: &TxContext): &Signer { + &self.signer + } + + /// Return the authenticator of the user that signed the current + /// transaction + public fun get_authenticator(self: &TxContext): Authenticator { + *Authenticator::get(&self.signer) + } +}