From d43ab08fbd81338c57102c97f390275c44531306 Mon Sep 17 00:00:00 2001 From: alnoki <43892045+alnoki@users.noreply.github.com> Date: Tue, 7 Jan 2025 10:31:12 -0800 Subject: [PATCH] [ECO-2618] Finalize emojicoin arena core logic design (#478) --- cfg/cspell-dictionary.txt | 4 + src/move/emojicoin_arena/Move.toml | 13 +- src/move/emojicoin_arena/README.md | 85 +- .../sources/emojicoin_arena.move | 1679 ++++++++++------- .../sources/pseudo_randomness.move | 4 +- src/move/emojicoin_arena/tests/tests.move | 442 +++++ .../test_coin_factories/yin_yang/Move.toml | 12 + .../yin_yang/sources/coin_factory.move | 6 + src/move/test_coin_factories/zebra/Move.toml | 12 + .../zebra/sources/coin_factory.move | 6 + src/move/test_coin_factories/zombie/Move.toml | 12 + .../zombie/sources/coin_factory.move | 6 + 12 files changed, 1557 insertions(+), 724 deletions(-) create mode 100644 src/move/emojicoin_arena/tests/tests.move create mode 100644 src/move/test_coin_factories/yin_yang/Move.toml create mode 100644 src/move/test_coin_factories/yin_yang/sources/coin_factory.move create mode 100644 src/move/test_coin_factories/zebra/Move.toml create mode 100644 src/move/test_coin_factories/zebra/sources/coin_factory.move create mode 100644 src/move/test_coin_factories/zombie/Move.toml create mode 100644 src/move/test_coin_factories/zombie/sources/coin_factory.move diff --git a/cfg/cspell-dictionary.txt b/cfg/cspell-dictionary.txt index 2cf869fc3..57345dcbd 100644 --- a/cfg/cspell-dictionary.txt +++ b/cfg/cspell-dictionary.txt @@ -55,6 +55,7 @@ keycap khanda kitts leaderboard +leaderboards leste libclang libdw @@ -78,6 +79,7 @@ nohup octa octas oden +offchain parallelizable permissionless pgrst @@ -110,5 +112,7 @@ viewports vpnapi websocat websockets +writeset +writesets xbtmatt zustand diff --git a/src/move/emojicoin_arena/Move.toml b/src/move/emojicoin_arena/Move.toml index 8ba400cc4..e0c736590 100644 --- a/src/move/emojicoin_arena/Move.toml +++ b/src/move/emojicoin_arena/Move.toml @@ -1,16 +1,25 @@ [addresses] -arena = "_" +emojicoin_arena = "_" integrator = "_" [dependencies.EmojicoinDotFun] local = "../emojicoin_dot_fun" [dev-addresses] -arena = "0xaaa" coin_factory = "0xbbb" +emojicoin_arena = "0xaaa" emojicoin_dot_fun = "0xc0de" integrator = "0xddd" +[dev-dependencies.YinYangCoinFactory] +local = "../test_coin_factories/yin_yang" + +[dev-dependencies.ZebraCoinFactory] +local = "../test_coin_factories/zebra" + +[dev-dependencies.ZombieCoinFactory] +local = "../test_coin_factories/zombie" + [package] authors = ["Econia Labs (developers@econialabs.com)"] name = "EmojicoinArena" diff --git a/src/move/emojicoin_arena/README.md b/src/move/emojicoin_arena/README.md index e40843ce3..5a7f28f13 100644 --- a/src/move/emojicoin_arena/README.md +++ b/src/move/emojicoin_arena/README.md @@ -1,11 +1,88 @@ -# Emojicoin Arena + -## A note on pseudo-randomness +# Emojicoin arena + +## Overview + +Emojicoin arena is a gamified trading experience wherein users may trade back +and forth between two emojicoins during a set period known as a melee. The +two emojicoins for a given melee are selected using Aptos randomness, via a +crank mechanism that is called at the beginning of all public APIs. + +A user enters a melee by swapping APT into one of two emojicoins, which are held +in escrow to enable constraints and to ease the indexing process. In particular, +a user can only hold one of the two emojicoins in escrow, and if they want to +swap to the other emojicoin, all of their holdings in escrow are swapped into +the other emojicoin. That is, holdings inside the escrow for a given melee are +all-or-nothing for one of the two emojicoins. If a user wishes to top off their +escrow with more emojicoins, they must swap into the kind they already hold. + +A user can still hold any combination of the two emojicoins for a melee outside +of escrow in their Aptos wallet, via emojicoin dot fun core functionality. +However in this case they will miss out on: + +1. Profit and loss (PnL) indexing and leaderboards that are enabled by escrow. +1. Ease of participation in the melee via the emojicoin arena feature page. +1. Rewards. + +Rewards are provided by the Aptos Foundation and are loaded into a vault that +anyone can claim from: when a user enters a melee, they can elect to lock in +their contributions, and if they do so, rewards from the vault will be combined +with their APT contribution to buy even more emojicoins. However, once a user +has locked in, they can not exit the melee until it has ended unless they first +pay back in full all APT rewards they have received from matching ("tap out"). +Conversely, if a user does not lock in, they can exit at any time without +penalty. + +Note that the matching percentage decreases as time goes on, such that if a user +locks in at the beginning of a melee they will be matched at a higher percentage +than if they lock in halfway through. This is to ensure that if users claim +rewards, they are in it for the long haul and will participate through to the +end of the melee. There is a ceiling on how much APT a user can be matched in +one given melee, but this value along with other parameters like the duration of +each melee are configurable. + +## Terminology + +1. **Melee**: A trading free-for-all between two randomly-selected emojicoins. + New melees start periodically. +1. **Arena**: The venue inside of which melees take place. +1. **Escrow**: A sandbox of two emojicoins for a given melee. +1. **Entering**: Adding funds to an escrow. +1. **Locking in**: Accepting rewards upon entry. +1. **Topping off**: Adding more funds into an existing escrow. +1. **Exiting**: Withdrawing emojicoins from escrow. +1. **Tapping out**: Exiting a melee before it has ended when locked in, thus + incurring the penalty of having to pay back all matched rewards. +1. **Match amount**: The amount of APT a user has been rewarded by locking in. + +## Indexing + +Structs and events are designed according to the principle of minimized onchain +indexing. That is, runtime state and event fields are designed to provide the +minimum amount of information required to enable comprehensive indexing via +writeset-aware offchain processing. In order words, the data model is designed +to enable full indexing using only writesets and events, with as few struct and +event fields as possible. + +As an example, calculating PnL offchain for a given escrow looks something like: + +```python +profit = sum_over_all_events_since_escrow_was_last_empty( + Enter.input_amount + Enter.match_amount +) / current_value_of_holdings() +``` + +Where `current_value_of_holdings()` aggregates all `Enter`, `Swap`, and `Exit` +events for a given `{user, melee_id}` to determine current holdings and then +converts them to octas using the most recent `ExchangeRate`. + +## Pseudo-randomness Since randomness is not supported in `init_module` per [`aptos-core` #15436], pseudo-random substitute implementations are used for the first crank. For a -detailed rationale that explains how this is effectively random in practice, see -[this `emojicoin-dot-fun` pull request comment]. +detailed rationale that explains how this implementation is effectively random +in practice, see [this `emojicoin-dot-fun` pull request comment]. [this `emojicoin-dot-fun` pull request comment]: https://github.com/econia-labs/emojicoin-dot-fun/pull/408#discussion_r1887856202 [`aptos-core` #15436]: https://github.com/aptos-labs/aptos-core/issues/15436 diff --git a/src/move/emojicoin_arena/sources/emojicoin_arena.move b/src/move/emojicoin_arena/sources/emojicoin_arena.move index 60080ac17..225a3b931 100644 --- a/src/move/emojicoin_arena/sources/emojicoin_arena.move +++ b/src/move/emojicoin_arena/sources/emojicoin_arena.move @@ -1,74 +1,63 @@ +// cspell:word funder // cspell:word unexited -module arena::emojicoin_arena { +module emojicoin_arena::emojicoin_arena { use aptos_framework::account::{Self, SignerCapability}; - use aptos_framework::aggregator_v2::{Self, Aggregator}; use aptos_framework::aptos_account; use aptos_framework::aptos_coin::AptosCoin; use aptos_framework::coin::{Self, Coin}; + use aptos_framework::event; use aptos_framework::randomness::Self; use aptos_framework::timestamp; use aptos_std::math64::min; - use aptos_std::smart_table::{Self, SmartTable}; + use aptos_std::table::{Self, Table}; + use aptos_std::table_with_length::{Self, TableWithLength}; use aptos_std::type_info; - use arena::pseudo_randomness; - use emojicoin_dot_fun::emojicoin_dot_fun::{ - Self, - MarketMetadata, - market_view, - registry_view, - unpack_market_metadata, - unpack_registry_view - }; + use emojicoin_arena::pseudo_randomness; + use emojicoin_dot_fun::emojicoin_dot_fun; use std::option::Self; use std::signer; - /// Signer does not correspond to arena account. - const E_NOT_ARENA: u64 = 0; - /// New melee duration is too short. - const E_NEW_DURATION_TOO_SHORT: u64 = 1; - /// New melee lock-in period is too long. - const E_NEW_LOCK_IN_PERIOD_TOO_LONG: u64 = 2; - /// New melee match percentage is too high. - const E_NEW_MATCH_PERCENTAGE_TOO_HIGH: u64 = 3; + /// Signer does not correspond to `@emojicoin_arena` named address. + const E_NOT_EMOJICOIN_ARENA: u64 = 0; /// User's melee escrow has nonzero emojicoin 0 balance. - const E_ENTER_COIN_BALANCE_0: u64 = 4; + const E_ENTER_COIN_BALANCE_0: u64 = 1; /// User's melee escrow has nonzero emojicoin 1 balance. - const E_ENTER_COIN_BALANCE_1: u64 = 5; - /// Elected to lock in but unable to match. - const E_UNABLE_TO_LOCK_IN: u64 = 6; + const E_ENTER_COIN_BALANCE_1: u64 = 2; + /// User did not select lock in even though they've been matched since escrow was last empty. + const E_TOP_OFF_MUST_LOCK_IN: u64 = 3; /// Provided escrow coin type is invalid. - const E_INVALID_ESCROW_COIN_TYPE: u64 = 7; + const E_INVALID_ESCROW_COIN_TYPE: u64 = 4; /// User has no escrow resource. - const E_NO_ESCROW: u64 = 8; - /// Swapper has no funds in escrow to swap. - const E_SWAP_NO_FUNDS: u64 = 9; + const E_NO_ESCROW: u64 = 5; /// User has no funds in escrow to withdraw. - const E_EXIT_NO_FUNDS: u64 = 10; + const E_EXIT_NO_FUNDS: u64 = 6; + + const MAX_PERCENTAGE: u64 = 100; /// Resource account address seed for the registry. const REGISTRY_SEED: vector = b"Arena registry"; - const U64_MAX: u64 = 0xffffffffffffffff; - const MAX_PERCENTAGE: u64 = 100; - - /// Flat integrator fee. + /// Flat integrator fee for a single-route swap into escrow. const INTEGRATOR_FEE_RATE_BPS: u8 = 100; + /// Flat integrator fee for a double-route swap within escrow. const INTEGRATOR_FEE_RATE_BPS_DUAL_ROUTE: u8 = 50; // Default parameters for new melees. - const DEFAULT_DURATION: u64 = 36 * 3_600_000_000; + const DEFAULT_DURATION: u64 = 20 * 3_600_000_000; const DEFAULT_AVAILABLE_REWARDS: u64 = 1000 * 100_000_000; const DEFAULT_MAX_MATCH_PERCENTAGE: u64 = 50; const DEFAULT_MAX_MATCH_AMOUNT: u64 = 5 * 100_000_000; - struct Nil has drop, store {} - - struct Melee has store { + #[event] + /// Tracks state for active and historical melees. Also emitted whenever a new melee starts. + struct Melee has copy, drop, store { /// 1-indexed for conformity with emojicoin market ID indexing. melee_id: u64, - /// Metadata for market with lower market ID comes first. - market_metadatas: vector, + /// Address for emojicoin market with lower market ID. + emojicoin_0_market_address: address, + /// Address for emojicoin market with higher market ID. + emojicoin_1_market_address: address, /// In microseconds. start_time: u64, /// How long melee lasts after start time. @@ -77,33 +66,21 @@ module arena::emojicoin_arena { max_match_percentage: u64, /// Maximum amount of APT to match in octas, when locking in. max_match_amount: u64, - /// Amount of rewards that are available to claim for this melee while it is still active. - /// Measured in octas, conditional on vault balance. - available_rewards: u64, - /// All entrants who have entered the melee, used as a set. - all_entrants: SmartTable, - /// Active entrants in the melee, used as a set. - active_entrants: SmartTable, - /// Entrants who have exited the melee, used as a set. - exited_entrants: SmartTable, - /// Entrants who have locked in, used as a set. If user exits before melee ends they are - /// removed from this set. If they exit after the melee ends, they are not removed. - locked_in_entrants: SmartTable, - /// Number of melee-specific swaps. - n_swaps: Aggregator, - /// Volume of melee-specific swaps in octas. - swaps_volume: Aggregator, - /// Amount of emojicoin 0 locked in all melee escrows for the melee. - emojicoin_0_locked: Aggregator, - /// Amount of emojicoin 1 locked in all melee escrows for the melee. - emojicoin_1_locked: Aggregator + /// Amount of rewards that are available to claim for an active melee, measured in octas and + /// conditional on vault balance. If melee is inactive, represents the amount of rewards + /// that were available at the end of the melee. + available_rewards: u64 } + /// Tracks all melees, holds a signer capability for the rewards vault, and stores parameters + /// for the next melee. struct Registry has key { - /// A map of each melee's `melee_id` to the melee. - melees_by_id: SmartTable, - /// Map from a sorted combination of market IDs (lower ID first) to the melee serial ID. - melee_ids_by_market_ids: SmartTable, u64>, + /// A map of each `Melee`'s `melee_id` to the `Melee` itself. 1-indexed for conformity with + /// emojicoin market ID indexing. + melees_by_id: TableWithLength, + /// Map from a sorted combination of market IDs (lower ID first) to the `Melee` serial ID, + /// used to prevent duplicate melees by reverse lookup during crank time. + melee_ids_by_market_ids: Table, u64>, /// Approves transfers from the vault. signer_capability: SignerCapability, /// `Melee.duration` for next melee. @@ -113,17 +90,20 @@ module arena::emojicoin_arena { /// `Melee.max_match_percentage` for next melee. next_melee_max_match_percentage: u64, /// `Melee.max_match_amount` for next melee. - next_melee_max_match_amount: u64, - /// All entrants who have entered a melee. - all_entrants: SmartTable, - /// Number of melee-specific swaps. - n_swaps: Aggregator, - /// Volume of melee-specific swaps in octas. - swaps_volume: Aggregator, - /// Amount of octas matched. Decremented when a user taps out. - octas_matched: u64 + next_melee_max_match_amount: u64 } + struct RegistryView has copy, drop, store { + n_melees: u64, + vault_address: address, + vault_balance: u64, + next_melee_duration: u64, + next_melee_available_rewards: u64, + next_melee_max_match_percentage: u64, + next_melee_max_match_amount: u64 + } + + /// Tracks user's emojicoin holdings and octas matched since the escrow was last empty. struct Escrow has key { /// Corresponding `Melee.melee_id`. melee_id: u64, @@ -131,366 +111,377 @@ module arena::emojicoin_arena { emojicoin_0: Coin, /// Emojicoin 1 holdings. emojicoin_1: Coin, - /// Number of swaps user has executed during the melee. - n_swaps: u64, - /// Volume of user's melee-specific swaps in octas. - swaps_volume: u128, - /// Cumulative amount of APT entered into the melee since the most recent deposit into an - /// empty escrow. Inclusive of total amount matched from locking in since most recent - /// deposit into an empty escrow. Reset to 0 upon exit. - octas_entered: u64, - /// Cumulative amount of APT matched since most recent deposit into an empty escrow, reset - /// to 0 upon exit. Must be paid back in full when tapping out. - octas_matched: u64 - } - - struct UserMelees has key { - /// Set of serial IDs of all melees the user has entered. - entered_melee_ids: SmartTable, - /// Set of serial IDs of all melees the user has exited. - exited_melee_ids: SmartTable, - /// Set of serial IDs of all melees the user has entered but not exited. - unexited_melee_ids: SmartTable - } - - public entry fun fund_vault(arena: &signer, amount: u64) acquires Registry { - aptos_account::transfer( - arena, - account::get_signer_capability_address( - &borrow_registry_ref_checked(arena).signer_capability - ), - amount - ); + /// Cumulative octas matched since the `Escrow` was last empty, reset to 0 upon exit. Must + /// be paid back in full when tapping out. + match_amount: u64 } - public entry fun set_next_melee_available_rewards( - arena: &signer, amount: u64 - ) acquires Registry { - borrow_registry_ref_mut_checked(arena).next_melee_available_rewards = amount; - } - - public entry fun set_next_melee_duration(arena: &signer, duration: u64) acquires Registry { - let registry_ref_mut = borrow_registry_ref_mut_checked(arena); - registry_ref_mut.next_melee_duration = duration; - } - - public entry fun set_next_melee_max_match_percentage( - arena: &signer, max_match_percentage: u64 - ) acquires Registry { - borrow_registry_ref_mut_checked(arena).next_melee_max_match_percentage = max_match_percentage; - } - - public entry fun set_next_melee_max_match_amount( - arena: &signer, max_match_amount: u64 - ) acquires Registry { - borrow_registry_ref_mut_checked(arena).next_melee_max_match_amount = max_match_amount; + struct EscrowView has copy, drop, store { + melee_id: u64, + emojicoin_0_balance: u64, + emojicoin_1_balance: u64, + match_amount: u64 } - public entry fun withdraw_from_vault(arena: &signer, amount: u64) acquires Registry { - aptos_account::transfer( - &account::create_signer_with_capability( - &borrow_registry_ref_checked(arena).signer_capability - ), - @arena, - amount - ); + #[event] + /// Emitted whenever a user executes a single-route swap into `Escrow`. + struct Enter has copy, drop, store { + user: address, + melee_id: u64, + /// Argument passed to `enter`, independent of potential `match_amount`. + input_amount: u64, + quote_volume: u64, + integrator_fee: u64, + match_amount: u64, + emojicoin_0_proceeds: u64, + emojicoin_1_proceeds: u64, + /// After the swap into escrow. + emojicoin_0_exchange_rate: ExchangeRate, + /// After the swap into escrow. + emojicoin_1_exchange_rate: ExchangeRate + } + + #[event] + /// Emitted whenever a user exits from `Escrow`. + struct Exit has copy, drop, store { + user: address, + melee_id: u64, + /// Octas user had to pay if exiting before the end of the melee, if applicable. + tap_out_fee: u64, + emojicoin_0_proceeds: u64, + emojicoin_1_proceeds: u64, + emojicoin_0_exchange_rate: ExchangeRate, + emojicoin_1_exchange_rate: ExchangeRate + } + + #[event] + /// Emitted whenever a user executes a double-route swap inside `Escrow`. + struct Swap has copy, drop, store { + user: address, + melee_id: u64, + quote_volume: u64, + integrator_fee: u64, + emojicoin_0_proceeds: u64, + emojicoin_1_proceeds: u64, + /// After the swap within escrow. + emojicoin_0_exchange_rate: ExchangeRate, + /// After the swap within escrow. + emojicoin_1_exchange_rate: ExchangeRate + } + + #[event] + /// Emitted whenever the vault balance is updated, except for when the vault is funded by + /// sending funds directly to the vault address instead of by using the `fund_vault` function. + struct VaultBalanceUpdate has copy, drop, store { + new_balance: u64 + } + + /// Exchange rate between APT and emojicoins. + struct ExchangeRate has copy, drop, store { + /// Octas per `quote` emojicoins. + base: u64, + /// Emojicoins per `base` octas. + quote: u64 + } + + /// Ephemeral batch of swap arguments for a given emojicoin market, enabling cleaner abstraction + /// across coin types. + struct SwapArgumentsForEmojicoin has copy, drop { + swapper_address: address, + input_amount: u64, + sell_emojicoin_for_apt: bool, + integrator_fee_rate_bps: u8 } #[randomness] entry fun enter( entrant: &signer, input_amount: u64, lock_in: bool - ) acquires Escrow, Registry, UserMelees { + ) acquires Escrow, Registry { + + // Crank schedule, returning early as applicable and storing active melee ID. let (melee_just_ended, registry_ref_mut, time, n_melees_before_cranking) = crank_schedule(); if (melee_just_ended) return; // Can not enter melee if cranking ends it. + let melee_id = n_melees_before_cranking; - // Verify that coin types are for the current melee by calling the market view function. - let current_melee_ref_mut = - registry_ref_mut.melees_by_id.borrow_mut(n_melees_before_cranking); - let market_metadatas = current_melee_ref_mut.market_metadatas; - let (_, market_address_0, _) = unpack_market_metadata(market_metadatas[0]); - // Ensures the function aborts if Coin0 doesn't match LP0. - market_view(market_address_0); - let (_, market_address_1, _) = unpack_market_metadata(market_metadatas[1]); - // Ensures the function aborts if Coin1 doesn't match LP1. - market_view(market_address_1); - - // Create escrow and user melees resources if they don't exist. - let melee_id = current_melee_ref_mut.melee_id; - let entrant_address = signer::address_of(entrant); - if (!exists>(entrant_address)) { - move_to( - entrant, - Escrow { - melee_id, - emojicoin_0: coin::zero(), - emojicoin_1: coin::zero(), - octas_entered: 0, - octas_matched: 0, - swaps_volume: 0, - n_swaps: 0 - } + // Get market addresses for active melee. + let (active_melee_ref_mut, market_address_0, market_address_1) = + borrow_melee_mut_with_market_addresses( + registry_ref_mut, n_melees_before_cranking ); - if (!exists(entrant_address)) { - move_to( - entrant, - UserMelees { - entered_melee_ids: smart_table::new(), - exited_melee_ids: smart_table::new(), - unexited_melee_ids: smart_table::new() - } - ); - }; - }; - // Verify user is selecting one of the two emojicoin types. - let coin_0_type_info = type_info::type_of(); - let coin_1_type_info = type_info::type_of(); - let escrow_coin_type_info = type_info::type_of(); - let buy_coin_0 = - if (coin_0_type_info == escrow_coin_type_info) true - else { - assert!( - escrow_coin_type_info == coin_1_type_info, - E_INVALID_ESCROW_COIN_TYPE - ); - false - }; + // Create escrow if it doesn't exist. + let entrant_address = + ensure_melee_escrow_exists(entrant, melee_id); - // Verify that user does not split balance between the two emojicoins. - let escrow_ref_mut = &mut Escrow[entrant_address]; - if (buy_coin_0) - assert!( - coin::value(&escrow_ref_mut.emojicoin_1) == 0, E_ENTER_COIN_BALANCE_1 - ) - else - assert!( - coin::value(&escrow_ref_mut.emojicoin_0) == 0, E_ENTER_COIN_BALANCE_0 - ); + // Verify user has indicated escrow coin type as one of the two emojicoin types. Note that + // coin types are later type checked against each market during exchange rate calculations. + let buy_coin_0 = check_buy_side(); - // Match a portion of user's contribution if they elect to lock in. - let match_amount = - if (lock_in) { - // Verify that user can even lock in. - let match_amount = - match_amount( - input_amount, - escrow_ref_mut, - current_melee_ref_mut, - registry_ref_mut, - time - ); - assert!(match_amount > 0, E_UNABLE_TO_LOCK_IN); + // Verify that user does not split balance between the two emojicoins. + let escrow_ref_mut = + check_enter_balances(entrant_address, buy_coin_0); - // Transfer APT to entrant. - aptos_account::transfer( - &account::create_signer_with_capability( - ®istry_ref_mut.signer_capability - ), - entrant_address, - match_amount - ); + // Verify that if user has been matched since the escrow was last empty, they lock in again. + if (escrow_ref_mut.match_amount > 0) assert!(lock_in, E_TOP_OFF_MUST_LOCK_IN); - // Update melee state. - current_melee_ref_mut.melee_available_rewards_decrement(match_amount); - current_melee_ref_mut.melee_locked_in_entrants_add_if_not_contains( - entrant_address - ); + // Match a portion of user's contribution if they elect to lock in, and if there are any + // available rewards to match. + let match_amount = + try_match_entry( + lock_in, + input_amount, + escrow_ref_mut, + active_melee_ref_mut, + registry_ref_mut, + time, + entrant_address + ); - // Update registry state. - registry_ref_mut.registry_octas_matched_increment(match_amount); + // Execute a swap into the escrow. + let (quote_volume, integrator_fee, emojicoin_0_proceeds, emojicoin_1_proceeds) = + swap_into_escrow( + entrant, + entrant_address, + escrow_ref_mut, + market_address_0, + market_address_1, + input_amount + match_amount, + buy_coin_0 + ); - // Update escrow state. - escrow_ref_mut.escrow_octas_matched_increment(match_amount); + // Emit enter event. + event::emit( + Enter { + user: entrant_address, + melee_id, + input_amount, + quote_volume, + integrator_fee, + match_amount, + emojicoin_0_proceeds, + emojicoin_1_proceeds, + emojicoin_0_exchange_rate: exchange_rate(market_address_0), + emojicoin_1_exchange_rate: exchange_rate(market_address_1) + } + ); - match_amount + } - } else 0; - - // Execute a swap then immediately move funds into escrow, updating total emojicoin locked - // values based on side. - let input_amount_after_matching = input_amount + match_amount; - let quote_volume = - if (buy_coin_0) { - let (net_proceeds, quote_volume) = - swap_with_stats_buy_emojicoin( - entrant, - entrant_address, - market_address_0, - input_amount_after_matching, - INTEGRATOR_FEE_RATE_BPS - ); - let emojicoin_0_ref_mut = &mut escrow_ref_mut.emojicoin_0; - coin::merge(emojicoin_0_ref_mut, coin::withdraw(entrant, net_proceeds)); - current_melee_ref_mut.melee_emojicoin_0_locked_increment( - coin::value(emojicoin_0_ref_mut) - ); - quote_volume - } else { - let (net_proceeds, quote_volume) = - swap_with_stats_buy_emojicoin( - entrant, - entrant_address, - market_address_1, - input_amount_after_matching, - INTEGRATOR_FEE_RATE_BPS - ); - let emojicoin_1_ref_mut = &mut escrow_ref_mut.emojicoin_1; - coin::merge(emojicoin_1_ref_mut, coin::withdraw(entrant, net_proceeds)); - current_melee_ref_mut.melee_emojicoin_0_locked_increment( - coin::value(emojicoin_1_ref_mut) - ); - quote_volume - }; + #[randomness] + entry fun swap(swapper: &signer) acquires Escrow, Registry { + // Crank schedule, set local variables. + let (registry_ref_mut, melee_is_active, swapper_address, escrow_ref_mut, melee_id) = - // Update melee state. - let quote_volume_u128 = (quote_volume as u128); - current_melee_ref_mut.melee_n_swaps_increment(); - current_melee_ref_mut.melee_swaps_volume_increment(quote_volume_u128); - current_melee_ref_mut.melee_all_entrants_add_if_not_contains(entrant_address); - current_melee_ref_mut.melee_active_entrants_add_if_not_contains(entrant_address); - current_melee_ref_mut.melee_exited_entrants_remove_if_contains(entrant_address); + existing_participant_prologue(swapper); - // Update registry state. - registry_ref_mut.registry_n_swaps_increment(); - registry_ref_mut.registry_swaps_volume_increment(quote_volume_u128); - registry_ref_mut.registry_all_entrants_add_if_not_contains(entrant_address); + // Get melee market addresses. + let (_, market_address_0, market_address_1) = + borrow_melee_mut_with_market_addresses(registry_ref_mut, melee_id); - // Update escrow state. - escrow_ref_mut.escrow_n_swaps_increment(); - escrow_ref_mut.escrow_swaps_volume_increment(quote_volume_u128); - escrow_ref_mut.escrow_octas_entered_increment(input_amount_after_matching); + // Swap coins within escrow. + let (quote_volume, integrator_fee, emojicoin_0_proceeds, emojicoin_1_proceeds) = + swap_within_escrow( + swapper, + swapper_address, + escrow_ref_mut, + market_address_0, + market_address_1 + ); - // Update user melees state. - let user_melees_ref_mut = &mut UserMelees[entrant_address]; - user_melees_ref_mut.user_melees_entered_melee_ids_add_if_not_contains(melee_id); - user_melees_ref_mut.user_melees_unexited_melee_ids_add_if_not_contains(melee_id); - user_melees_ref_mut.user_melees_exited_melee_ids_remove_if_contains(melee_id); + // Emit swap event. + event::emit( + Swap { + user: swapper_address, + melee_id, + quote_volume, + integrator_fee, + emojicoin_0_proceeds, + emojicoin_1_proceeds, + emojicoin_0_exchange_rate: exchange_rate(market_address_0), + emojicoin_1_exchange_rate: exchange_rate(market_address_1) + } + ); + // Exit if melee is no longer active. + if (!melee_is_active) + exit_inner( + registry_ref_mut, + escrow_ref_mut, + swapper, + swapper_address, + false, + melee_id + ) } #[randomness] - entry fun exit(participant: &signer) acquires Escrow, Registry, UserMelees { - let participant_address = signer::address_of(participant); - assert!( - exists>(participant_address), - E_NO_ESCROW - ); - let (melee_just_ended, registry_ref_mut, _, _) = crank_schedule(); + entry fun exit(participant: &signer) acquires Escrow, Registry { + // Crank schedule, set local variables. + let ( + registry_ref_mut, + melee_is_active, + participant_address, + escrow_ref_mut, + melee_id + ) = existing_participant_prologue(participant); + exit_inner( + registry_ref_mut, + escrow_ref_mut, participant, participant_address, - registry_ref_mut, - !melee_just_ended + melee_is_active, + melee_id ); } - #[randomness] - entry fun swap( - swapper: &signer, market_addresses: vector
- ) acquires Escrow, Registry, UserMelees { + #[view] + public fun escrow(participant: address): EscrowView acquires Escrow { + let escrow_ref = &Escrow[participant]; + EscrowView { + melee_id: escrow_ref.melee_id, + emojicoin_0_balance: coin::value(&escrow_ref.emojicoin_0), + emojicoin_1_balance: coin::value(&escrow_ref.emojicoin_1), + match_amount: escrow_ref.match_amount + } + } - // Verify that swapper has an escrow resource. - let swapper_address = signer::address_of(swapper); - assert!( - exists>(swapper_address), - E_NO_ESCROW - ); - let escrow_ref_mut = &mut Escrow[swapper_address]; - - // Try cranking the schedule, and if a new melee starts, flag that user's escrow should be - // emptied immediately after the swap. - let (exit_once_done, registry_ref_mut, _, _) = crank_schedule(); - - // Swap, updating total emojicoin locked values based on side. - let swap_melee_ref_mut = - registry_ref_mut.melees_by_id.borrow_mut(escrow_ref_mut.melee_id); - let emojicoin_0_ref_mut = &mut escrow_ref_mut.emojicoin_0; - let emojicoin_1_ref_mut = &mut escrow_ref_mut.emojicoin_1; - let emojicoin_0_locked_before_swap = coin::value(emojicoin_0_ref_mut); - let emojicoin_1_locked_before_swap = coin::value(emojicoin_1_ref_mut); - let quote_volume = - if (emojicoin_0_locked_before_swap > 0) { - let quote_volume = - swap_within_escrow( - swapper, - swapper_address, - market_addresses[0], - market_addresses[1], - emojicoin_0_ref_mut, - emojicoin_1_ref_mut - ); - swap_melee_ref_mut.melee_emojicoin_0_locked_decrement( - emojicoin_0_locked_before_swap - ); - swap_melee_ref_mut.melee_emojicoin_1_locked_increment( - coin::value(emojicoin_1_ref_mut) - ); - quote_volume - } else { - assert!(emojicoin_1_locked_before_swap > 0, E_SWAP_NO_FUNDS); - swap_melee_ref_mut.melee_emojicoin_1_locked_decrement( - emojicoin_1_locked_before_swap - ); - let quote_volume = - swap_within_escrow( - swapper, - swapper_address, - market_addresses[1], - market_addresses[0], - emojicoin_1_ref_mut, - emojicoin_0_ref_mut - ); - swap_melee_ref_mut.melee_emojicoin_1_locked_decrement( - emojicoin_1_locked_before_swap - ); - swap_melee_ref_mut.melee_emojicoin_0_locked_increment( - coin::value(emojicoin_0_ref_mut) - ); - quote_volume - }; + public fun unpack_escrow_view(self: EscrowView): (u64, u64, u64, u64) { + let EscrowView { melee_id, emojicoin_0_balance, emojicoin_1_balance, match_amount } = + self; + (melee_id, emojicoin_0_balance, emojicoin_1_balance, match_amount) + } - // Update melee state. - let quote_volume_u128 = (quote_volume as u128); - swap_melee_ref_mut.melee_n_swaps_increment(); - swap_melee_ref_mut.melee_swaps_volume_increment(quote_volume_u128); + #[view] + public fun melee(melee_id: u64): Melee acquires Registry { + *Registry[@emojicoin_arena].melees_by_id.borrow(melee_id) + } - // Update registry state. - registry_ref_mut.registry_n_swaps_increment(); - registry_ref_mut.registry_swaps_volume_increment(quote_volume_u128); + public fun unpack_melee(self: Melee): (u64, address, address, u64, u64, u64, u64, u64) { + let Melee { + melee_id, + emojicoin_0_market_address, + emojicoin_1_market_address, + start_time, + duration, + max_match_percentage, + max_match_amount, + available_rewards + } = self; + ( + melee_id, + emojicoin_0_market_address, + emojicoin_1_market_address, + start_time, + duration, + max_match_percentage, + max_match_amount, + available_rewards + ) + } - // Update escrow state. - escrow_ref_mut.escrow_n_swaps_increment(); - escrow_ref_mut.escrow_swaps_volume_increment(quote_volume_u128); + #[view] + public fun registry(): RegistryView acquires Registry { + let registry_ref = &Registry[@emojicoin_arena]; + let vault_address = + account::get_signer_capability_address(®istry_ref.signer_capability); + RegistryView { + n_melees: registry_ref.melees_by_id.length(), + vault_address, + vault_balance: coin::balance(vault_address), + next_melee_duration: registry_ref.next_melee_duration, + next_melee_available_rewards: registry_ref.next_melee_available_rewards, + next_melee_max_match_percentage: registry_ref.next_melee_max_match_percentage, + next_melee_max_match_amount: registry_ref.next_melee_max_match_amount + } + } - if (exit_once_done) - exit_inner( - swapper, - swapper_address, - registry_ref_mut, - false + public fun unpack_registry_view(self: RegistryView): + (u64, address, u64, u64, u64, u64, u64) { + let RegistryView { + n_melees, + vault_address, + vault_balance, + next_melee_duration, + next_melee_available_rewards, + next_melee_max_match_percentage, + next_melee_max_match_amount + } = self; + ( + n_melees, + vault_address, + vault_balance, + next_melee_duration, + next_melee_available_rewards, + next_melee_max_match_percentage, + next_melee_max_match_amount + ) + } + + public entry fun fund_vault(funder: &signer, amount: u64) acquires Registry { + let vault_address = + account::get_signer_capability_address( + &Registry[@emojicoin_arena].signer_capability ); + aptos_account::transfer(funder, vault_address, amount); + emit_vault_balance_update_with_vault_address(vault_address); + } + public entry fun set_next_melee_available_rewards( + emojicoin_arena: &signer, amount: u64 + ) acquires Registry { + borrow_registry_mut_checked(emojicoin_arena).next_melee_available_rewards = + amount; } - fun init_module(arena: &signer) acquires Registry { + public entry fun set_next_melee_duration( + emojicoin_arena: &signer, duration: u64 + ) acquires Registry { + borrow_registry_mut_checked(emojicoin_arena).next_melee_duration = duration; + } + + public entry fun set_next_melee_max_match_amount( + emojicoin_arena: &signer, max_match_amount: u64 + ) acquires Registry { + borrow_registry_mut_checked(emojicoin_arena).next_melee_max_match_amount = + max_match_amount; + } + + public entry fun set_next_melee_max_match_percentage( + emojicoin_arena: &signer, max_match_percentage: u64 + ) acquires Registry { + borrow_registry_mut_checked(emojicoin_arena).next_melee_max_match_percentage = + max_match_percentage; + } + + public entry fun withdraw_from_vault( + emojicoin_arena: &signer, amount: u64 + ) acquires Registry { + let signer_capability_ref = + &borrow_registry_mut_checked(emojicoin_arena).signer_capability; + aptos_account::transfer( + &account::create_signer_with_capability(signer_capability_ref), + @emojicoin_arena, + amount + ); + emit_vault_balance_update_with_signer_capability_ref(signer_capability_ref); + } + + fun init_module(emojicoin_arena: &signer) acquires Registry { // Store registry resource. let (vault_signer, signer_capability) = - account::create_resource_account(arena, REGISTRY_SEED); + account::create_resource_account(emojicoin_arena, REGISTRY_SEED); move_to( - arena, + emojicoin_arena, Registry { - melees_by_id: smart_table::new(), - melee_ids_by_market_ids: smart_table::new(), + melees_by_id: table_with_length::new(), + melee_ids_by_market_ids: table::new(), signer_capability, next_melee_duration: DEFAULT_DURATION, next_melee_available_rewards: DEFAULT_AVAILABLE_REWARDS, next_melee_max_match_percentage: DEFAULT_MAX_MATCH_PERCENTAGE, - next_melee_max_match_amount: DEFAULT_MAX_MATCH_AMOUNT, - all_entrants: smart_table::new(), - n_swaps: aggregator_v2::create_unbounded_aggregator(), - swaps_volume: aggregator_v2::create_unbounded_aggregator(), - octas_matched: 0 + next_melee_max_match_amount: DEFAULT_MAX_MATCH_AMOUNT } ); coin::register(&vault_signer); @@ -507,172 +498,286 @@ module arena::emojicoin_arena { // Register the first melee. register_melee( - &mut Registry[@arena], + &mut Registry[@emojicoin_arena], + 0, + sort_unique_market_ids(market_id_0, market_id_1), 0, - sort_unique_market_ids(market_id_0, market_id_1) + DEFAULT_DURATION ); } - inline fun add_if_not_contains( - map_ref_mut: &mut SmartTable, key: T - ) { - if (!map_ref_mut.contains(key)) { - map_ref_mut.add(key, Nil {}); - } + inline fun borrow_melee_mut_with_market_addresses( + registry_ref_mut: &mut Registry, melee_id: u64 + ): (&mut Melee, address, address) { + let melee_ref_mut = registry_ref_mut.melees_by_id.borrow_mut(melee_id); + let market_address_0 = melee_ref_mut.emojicoin_0_market_address; + let market_address_1 = melee_ref_mut.emojicoin_1_market_address; + (melee_ref_mut, market_address_0, market_address_1) } - inline fun borrow_registry_ref_checked(arena: &signer): &Registry { - assert!(signer::address_of(arena) == @arena, E_NOT_ARENA); - &Registry[@arena] + inline fun borrow_registry_mut_checked(emojicoin_arena: &signer): &mut Registry { + assert!( + signer::address_of(emojicoin_arena) == @emojicoin_arena, + E_NOT_EMOJICOIN_ARENA + ); + &mut Registry[@emojicoin_arena] } - inline fun borrow_registry_ref_mut_checked(arena: &signer): &mut Registry { - assert!(signer::address_of(arena) == @arena, E_NOT_ARENA); - &mut Registry[@arena] + inline fun check_buy_side(): bool { + let coin_0_type_info = type_info::type_of(); + let coin_1_type_info = type_info::type_of(); + let escrow_coin_type_info = type_info::type_of(); + let buy_coin_0 = + if (coin_0_type_info == escrow_coin_type_info) true + else { + assert!( + escrow_coin_type_info == coin_1_type_info, + E_INVALID_ESCROW_COIN_TYPE + ); + false + }; + buy_coin_0 + } + + inline fun check_enter_balances( + entrant_address: address, buy_coin_0: bool + ): &mut Escrow { + let escrow_ref_mut = &mut Escrow[entrant_address]; + if (buy_coin_0) + assert!( + coin::value(&escrow_ref_mut.emojicoin_1) == 0, E_ENTER_COIN_BALANCE_1 + ) + else + assert!( + coin::value(&escrow_ref_mut.emojicoin_0) == 0, E_ENTER_COIN_BALANCE_0 + ); + escrow_ref_mut } - /// Cranks schedule and returns `true` if a melee has ended as a result, along with assorted - /// variables, to reduce borrows and lookups in the caller. + /// Crank schedule and return `true` if the active melee has ended as a result, along with other + /// assorted variables, to reduce borrows and lookups in the caller. inline fun crank_schedule(): (bool, &mut Registry, u64, u64) { - let time = timestamp::now_microseconds(); - let registry_ref_mut = &mut Registry[@arena]; + + // Determine the last active melee. + let registry_ref_mut = &mut Registry[@emojicoin_arena]; let n_melees_before_cranking = registry_ref_mut.melees_by_id.length(); - let current_melee_ref = - registry_ref_mut.melees_by_id.borrow(n_melees_before_cranking); + let last_active_melee_ref_mut = + registry_ref_mut.melees_by_id.borrow_mut(n_melees_before_cranking); + + // If the last active melee has ended, register a new melee. + let last_active_melee_start_time = last_active_melee_ref_mut.start_time; + let last_active_melee_duration = last_active_melee_ref_mut.duration; + let time = timestamp::now_microseconds(); let cranked = - if (time >= current_melee_ref.start_time + current_melee_ref.duration) { + if (time >= last_active_melee_start_time + last_active_melee_duration) { let market_ids = next_melee_market_ids(registry_ref_mut); - register_melee(registry_ref_mut, n_melees_before_cranking, market_ids); + register_melee( + registry_ref_mut, + n_melees_before_cranking, + market_ids, + last_active_melee_start_time, + last_active_melee_duration + ); true } else false; + (cranked, registry_ref_mut, time, n_melees_before_cranking) } - inline fun escrow_n_swaps_increment( - self: &mut Escrow + inline fun emit_vault_balance_update_with_signer_capability_ref( + signer_capability_ref: &SignerCapability ) { - self.n_swaps = self.n_swaps + 1; + event::emit( + VaultBalanceUpdate { + new_balance: coin::balance( + account::get_signer_capability_address(signer_capability_ref) + ) + } + ); } - inline fun escrow_octas_entered_increment( - self: &mut Escrow, - amount: u64 + inline fun emit_vault_balance_update_with_vault_address( + vault_address: address ) { - self.octas_entered = self.octas_entered + amount; + event::emit( + VaultBalanceUpdate { + new_balance: coin::balance(vault_address) + } + ); } - inline fun escrow_octas_entered_reset( - self: &mut Escrow - ) { - self.octas_entered = 0; - } + inline fun ensure_melee_escrow_exists( + entrant: &signer, melee_id: u64 + ): address { + let entrant_address = signer::address_of(entrant); + if (!exists>(entrant_address)) { + move_to( + entrant, + Escrow { + melee_id, + emojicoin_0: coin::zero(), + emojicoin_1: coin::zero(), + match_amount: 0 + } + ); + }; + entrant_address + } + + inline fun exchange_rate( + market_address: address + ): ExchangeRate { + + // Get reserves from the market view. + let ( + _, + _, + clamm_virtual_reserves, + cpamm_real_reserves, + _, + in_bonding_curve, + _, + _, + _, + _, + _, + _, + _ + ) = + emojicoin_dot_fun::unpack_market_view( + emojicoin_dot_fun::market_view(market_address) + ); - inline fun escrow_octas_matched_increment( - self: &mut Escrow, - amount: u64 - ) { - self.octas_matched = self.octas_matched + amount; - } + // Select reserves based on whether the market is in the bonding curve. + let reserves = + if (in_bonding_curve) clamm_virtual_reserves + else cpamm_real_reserves; + let (base, quote) = emojicoin_dot_fun::unpack_reserves(reserves); - inline fun escrow_octas_matched_reset( - self: &mut Escrow - ) { - self.octas_matched = 0; + ExchangeRate { base, quote } } - inline fun escrow_swaps_volume_increment( - self: &mut Escrow, - amount: u128 - ) { - self.swaps_volume = self.swaps_volume + amount; + /// Crank schedule, set local variables for a participant who has already joined a melee. Used + /// for `swap` and `exit` functions. + inline fun existing_participant_prologue( + participant: &signer + ): (&mut Registry, bool, address, &mut Escrow, u64) { + + // Ensure user has escrow. + let participant_address = signer::address_of(participant); + assert!( + exists>(participant_address), + E_NO_ESCROW + ); + let escrow_ref_mut = &mut Escrow[participant_address]; + + // Crank schedule, determine if melee from escrow is active or not. + let (cranked, registry_ref_mut, _, n_melees_before_cranking) = crank_schedule(); + let melee_id = escrow_ref_mut.melee_id; + let melee_is_active = + if (cranked) { false } + else { + melee_id == n_melees_before_cranking + }; + (registry_ref_mut, melee_is_active, participant_address, escrow_ref_mut, melee_id) } - /// Assumes user has an escrow resource. inline fun exit_inner( + registry_ref_mut: &mut Registry, + escrow_ref_mut: &mut Escrow, participant: &signer, participant_address: address, - registry_ref_mut: &mut Registry, - melee_is_current: bool + melee_is_active: bool, + melee_id: u64 ) acquires Registry { - let escrow_ref_mut = &mut Escrow[participant_address]; - let melee_id = escrow_ref_mut.melee_id; - let exited_melee_ref_mut = registry_ref_mut.melees_by_id.borrow_mut(melee_id); + // Get mutable reference to melee and its market addresses. + let (exited_melee_ref_mut, market_address_0, market_address_1) = + borrow_melee_mut_with_market_addresses(registry_ref_mut, melee_id); + + // Charge tap out fee if applicable, updating escrow match amount since user has exited. + let match_amount = escrow_ref_mut.match_amount; + let tap_out_fee = + if (melee_is_active && match_amount > 0) { - // Charge tap out fee if applicable. - if (melee_is_current) { - let octas_matched = escrow_ref_mut.octas_matched; - if (octas_matched > 0) { + // Get vault address and transfer match amount back to vault. let vault_address = account::get_signer_capability_address( ®istry_ref_mut.signer_capability ); - aptos_account::transfer(participant, vault_address, octas_matched); + aptos_account::transfer(participant, vault_address, match_amount); + emit_vault_balance_update_with_vault_address(vault_address); - // Update melee state. - exited_melee_ref_mut.melee_available_rewards_increment(octas_matched); - exited_melee_ref_mut.melee_locked_in_entrants_remove_if_contains( - participant_address - ); - - // Update registry state. - registry_ref_mut.registry_octas_matched_decrement(octas_matched); - } - }; + // Update available rewards for the melee since match amount has been returned. + exited_melee_ref_mut.available_rewards += match_amount; - // Withdraw emojicoin balances from escrow. - if (coin::value(&escrow_ref_mut.emojicoin_0) > 0) { - withdraw_from_escrow(participant_address, &mut escrow_ref_mut.emojicoin_0); - } else { - assert!(coin::value(&escrow_ref_mut.emojicoin_1) > 0, E_EXIT_NO_FUNDS); - withdraw_from_escrow(participant_address, &mut escrow_ref_mut.emojicoin_1); - }; - - // Update melee state. - exited_melee_ref_mut.melee_active_entrants_remove_if_contains(participant_address); - exited_melee_ref_mut.melee_exited_entrants_add_if_not_contains(participant_address); + match_amount + } else { 0 }; + escrow_ref_mut.match_amount = 0; - // Update escrow state. - escrow_ref_mut.escrow_octas_entered_reset(); - escrow_ref_mut.escrow_octas_matched_reset(); + // Withdraw emojicoin balance from escrow. + let emojicoin_0_proceeds = + try_withdraw_from_escrow( + participant_address, &mut escrow_ref_mut.emojicoin_0 + ); + let emojicoin_1_proceeds = + try_withdraw_from_escrow( + participant_address, &mut escrow_ref_mut.emojicoin_1 + ); + assert!( + emojicoin_0_proceeds > 0 || emojicoin_1_proceeds > 0, + E_EXIT_NO_FUNDS + ); - // Update user melees state. - let user_melees_ref_mut = &mut UserMelees[participant_address]; - user_melees_ref_mut.user_melees_exited_melee_ids_add_if_not_contains(melee_id); - user_melees_ref_mut.user_melees_unexited_melee_ids_remove_if_contains(melee_id); + // Emit exit event. + event::emit( + Exit { + user: participant_address, + melee_id, + emojicoin_0_proceeds, + emojicoin_1_proceeds, + tap_out_fee, + emojicoin_0_exchange_rate: exchange_rate(market_address_0), + emojicoin_1_exchange_rate: exchange_rate(market_address_1) + } + ); } inline fun get_n_registered_markets(): u64 { let (_, _, _, n_markets, _, _, _, _, _, _, _, _) = - unpack_registry_view(registry_view()); + emojicoin_dot_fun::unpack_registry_view(emojicoin_dot_fun::registry_view()); n_markets } - inline fun last_period_boundary(time: u64, period: u64): u64 { - (time / period) * period + /// Returns the most recent time that is an integer multiple of `duration` after `start_time`, + /// assuming `current_time` is at least `duration` after `start_time`. + inline fun last_period_boundary( + current_time: u64, start_time: u64, duration: u64 + ): u64 { + (((current_time - start_time) / duration) * duration) + start_time } - /// Uses mutable references to avoid freezing references up the stack. + /// Uses mutable references to avoid borrowing issues. inline fun match_amount( input_amount: u64, escrow_ref_mut: &mut Escrow, - current_melee_ref_mut: &mut Melee, + active_melee_ref_mut: &mut Melee, registry_ref_mut: &mut Registry, time: u64 ): u64 { - let elapsed_time = ((time - current_melee_ref_mut.start_time) as u256); - let duration = (current_melee_ref_mut.duration as u256); + let elapsed_time = ((time - active_melee_ref_mut.start_time) as u256); + let duration = (active_melee_ref_mut.duration as u256); if (elapsed_time >= duration) { 0 } else { // Scale down input amount for matching percentage and remaining time in one compound // operation, to reduce truncation errors. Equivalent to: // // max match percentage remaining time - // input_amount * -------------------- * -------------- + // input amount * -------------------- * -------------- // 100 duration let raw_match_amount = ( ((input_amount as u256) - * (current_melee_ref_mut.max_match_percentage as u256) + * (active_melee_ref_mut.max_match_percentage as u256) * (duration - elapsed_time)) / ((MAX_PERCENTAGE as u256) * duration) as u64 ); @@ -690,105 +795,17 @@ module arena::emojicoin_arena { let corrected_for_melee_available_rewards = min( corrected_for_vault_balance, - current_melee_ref_mut.available_rewards + active_melee_ref_mut.available_rewards ); // Correct for the max match amount that the user is eligible for. min( corrected_for_melee_available_rewards, - current_melee_ref_mut.max_match_amount - escrow_ref_mut.octas_matched + active_melee_ref_mut.max_match_amount - escrow_ref_mut.match_amount ) } } - inline fun melee_active_entrants_add_if_not_contains( - self: &mut Melee, address: address - ) { - add_if_not_contains(&mut self.active_entrants, address); - } - - inline fun melee_active_entrants_remove_if_contains( - self: &mut Melee, address: address - ) { - remove_if_contains(&mut self.active_entrants, address); - } - - inline fun melee_all_entrants_add_if_not_contains( - self: &mut Melee, address: address - ) { - add_if_not_contains(&mut self.all_entrants, address); - } - - inline fun melee_available_rewards_decrement( - self: &mut Melee, amount: u64 - ) { - self.available_rewards = self.available_rewards - amount; - } - - inline fun melee_available_rewards_increment( - self: &mut Melee, amount: u64 - ) { - self.available_rewards = self.available_rewards + amount; - } - - inline fun melee_emojicoin_0_locked_decrement( - self: &mut Melee, amount: u64 - ) { - aggregator_v2::sub(&mut self.emojicoin_0_locked, amount); - } - - inline fun melee_emojicoin_0_locked_increment( - self: &mut Melee, amount: u64 - ) { - aggregator_v2::add(&mut self.emojicoin_0_locked, amount); - } - - inline fun melee_emojicoin_1_locked_decrement( - self: &mut Melee, amount: u64 - ) { - aggregator_v2::sub(&mut self.emojicoin_1_locked, amount); - } - - inline fun melee_emojicoin_1_locked_increment( - self: &mut Melee, amount: u64 - ) { - aggregator_v2::add(&mut self.emojicoin_1_locked, amount); - } - - inline fun melee_exited_entrants_add_if_not_contains( - self: &mut Melee, address: address - ) { - add_if_not_contains(&mut self.exited_entrants, address); - } - - inline fun melee_exited_entrants_remove_if_contains( - self: &mut Melee, address: address - ) { - remove_if_contains(&mut self.exited_entrants, address); - } - - inline fun melee_locked_in_entrants_add_if_not_contains( - self: &mut Melee, address: address - ) { - add_if_not_contains(&mut self.locked_in_entrants, address); - } - - inline fun melee_locked_in_entrants_remove_if_contains( - self: &mut Melee, address: address - ) { - remove_if_contains(&mut self.locked_in_entrants, address); - } - - inline fun melee_n_swaps_increment(self: &mut Melee) { - aggregator_v2::add(&mut self.n_swaps, 1); - } - - inline fun melee_swaps_volume_increment( - self: &mut Melee, amount: u128 - ) { - aggregator_v2::add(&mut self.swaps_volume, amount); - } - - /// Accepts a mutable reference to avoid freezing references up the stack. + /// Accepts a mutable reference to avoid borrowing issues. inline fun next_melee_market_ids(registry_ref_mut: &mut Registry): vector { let n_markets = get_n_registered_markets(); let market_ids; @@ -818,221 +835,451 @@ module arena::emojicoin_arena { inline fun register_melee( registry_ref_mut: &mut Registry, n_melees_before_registration: u64, - sorted_unique_market_ids: vector + sorted_unique_market_ids: vector, + last_melee_start_time: u64, + last_melee_duration: u64 ) { + // Get new melee ID, which is 1-indexed. let melee_id = n_melees_before_registration + 1; - registry_ref_mut.melees_by_id.add( - melee_id, - Melee { - melee_id, - market_metadatas: sorted_unique_market_ids.map_ref(|market_id_ref| { - option::destroy_some( - emojicoin_dot_fun::market_metadata_by_market_id(*market_id_ref) - ) - }), - start_time: last_period_boundary( - timestamp::now_microseconds(), registry_ref_mut.next_melee_duration - ), - duration: registry_ref_mut.next_melee_duration, - max_match_percentage: registry_ref_mut.next_melee_max_match_percentage, - max_match_amount: registry_ref_mut.next_melee_max_match_amount, - available_rewards: registry_ref_mut.next_melee_available_rewards, - all_entrants: smart_table::new(), - active_entrants: smart_table::new(), - exited_entrants: smart_table::new(), - locked_in_entrants: smart_table::new(), - n_swaps: aggregator_v2::create_unbounded_aggregator(), - swaps_volume: aggregator_v2::create_unbounded_aggregator(), - emojicoin_0_locked: aggregator_v2::create_unbounded_aggregator(), - emojicoin_1_locked: aggregator_v2::create_unbounded_aggregator() - } - ); + + // Get the market addresses for each market ID, and store addresses tuple in the reverse + // lookup table. + let market_addresses = + sorted_unique_market_ids.map_ref(|market_id_ref| { + let (_, market_address, _) = + emojicoin_dot_fun::unpack_market_metadata( + option::destroy_some( + emojicoin_dot_fun::market_metadata_by_market_id(*market_id_ref) + ) + ); + market_address + }); + let emojicoin_0_market_address = market_addresses[0]; + let emojicoin_1_market_address = market_addresses[1]; registry_ref_mut.melee_ids_by_market_ids.add(sorted_unique_market_ids, melee_id); - } - inline fun registry_all_entrants_add_if_not_contains( - self: &mut Registry, address: address - ) { - add_if_not_contains(&mut self.all_entrants, address); - } + // Pack the melee. + let melee = Melee { + melee_id, + emojicoin_0_market_address, + emojicoin_1_market_address, + start_time: last_period_boundary( + timestamp::now_microseconds(), + last_melee_start_time, + last_melee_duration + ), + duration: registry_ref_mut.next_melee_duration, + max_match_percentage: registry_ref_mut.next_melee_max_match_percentage, + max_match_amount: registry_ref_mut.next_melee_max_match_amount, + available_rewards: registry_ref_mut.next_melee_available_rewards + }; - inline fun registry_n_swaps_increment(self: &mut Registry) { - aggregator_v2::add(&mut self.n_swaps, 1); + // Store the melee, and emit it as an event. + registry_ref_mut.melees_by_id.add(melee_id, melee); + event::emit(melee); } - inline fun registry_octas_matched_decrement( - self: &mut Registry, amount: u64 - ) { - self.octas_matched = self.octas_matched - amount; + /// Assumes `market_id_0` != `market_id_1`. + inline fun sort_unique_market_ids(market_id_0: u64, market_id_1: u64): vector { + if (market_id_0 < market_id_1) { + vector[market_id_0, market_id_1] + } else { + vector[market_id_1, market_id_0] + } } - inline fun registry_octas_matched_increment( - self: &mut Registry, amount: u64 - ) { - self.octas_matched = self.octas_matched + amount; + inline fun swap_into_escrow( + swapper: &signer, + swapper_address: address, + escrow_ref_mut: &mut Escrow, + market_address_0: address, + market_address_1: address, + octas_to_spend: u64, + buy_coin_0: bool + ): (u64, u64, u64, u64) { + + // Pack swap arguments for the emojicoin to buy. + let swap_arguments = SwapArgumentsForEmojicoin { + swapper_address, + input_amount: octas_to_spend, + sell_emojicoin_for_apt: false, + integrator_fee_rate_bps: INTEGRATOR_FEE_RATE_BPS + }; + + // Initialize return values. + let (emojicoin_0_proceeds, emojicoin_1_proceeds) = (0, 0); + let (quote_volume, integrator_fee); + + // Execute a swap into escrow based on the emojicoin to buy. + if (buy_coin_0) { + (emojicoin_0_proceeds, quote_volume, integrator_fee) = swap_into_escrow_for_emojicoin( + swapper, &swap_arguments, market_address_0, &mut escrow_ref_mut.emojicoin_0 + ); + } else { + (emojicoin_1_proceeds, quote_volume, integrator_fee) = swap_into_escrow_for_emojicoin( + swapper, &swap_arguments, market_address_1, &mut escrow_ref_mut.emojicoin_1 + ); + }; + + (quote_volume, integrator_fee, emojicoin_0_proceeds, emojicoin_1_proceeds) } - inline fun registry_swaps_volume_increment( - self: &mut Registry, amount: u128 - ) { - aggregator_v2::add(&mut self.swaps_volume, amount); + inline fun swap_into_escrow_for_emojicoin( + swapper: &signer, + swap_arguments_ref: &SwapArgumentsForEmojicoin, + market_address: address, + target_emojicoin_ref_mut: &mut Coin + ): (u64, u64, u64) { + let (net_proceeds, quote_volume, integrator_fee) = + swap_with_stats_for_emojicoin( + swapper, market_address, swap_arguments_ref + ); + coin::merge(target_emojicoin_ref_mut, coin::withdraw(swapper, net_proceeds)); + (net_proceeds, quote_volume, integrator_fee) } - inline fun remove_if_contains( - map_ref_mut: &mut SmartTable, key: T - ) { - if (map_ref_mut.contains(key)) { - map_ref_mut.remove(key); - } + inline fun swap_out_of_escrow_for_emojicoin( + swapper: &signer, + swap_arguments_ref: &SwapArgumentsForEmojicoin, + market_address: address, + target_emojicoin_ref_mut: &mut Coin + ): (u64, u64, u64) { + withdraw_from_escrow( + swap_arguments_ref.swapper_address, target_emojicoin_ref_mut + ); + swap_with_stats_for_emojicoin( + swapper, market_address, swap_arguments_ref + ) } - inline fun sort_unique_market_ids(market_id_0: u64, market_id_1: u64): vector { - if (market_id_0 < market_id_1) { - vector[market_id_0, market_id_1] + inline fun swap_within_escrow( + swapper: &signer, + swapper_address: address, + escrow_ref_mut: &mut Escrow, + market_address_0: address, + market_address_1: address + ): (u64, u64, u64, u64) { + + // Get balances in escrow. + let emojicoin_0_balance = coin::value(&escrow_ref_mut.emojicoin_0); + let emojicoin_1_balance = coin::value(&escrow_ref_mut.emojicoin_1); + + // Initialize return values. + let (emojicoin_0_proceeds, emojicoin_1_proceeds) = (0, 0); + let (quote_volume, integrator_fee); + + // Execute a swap within escrow based on the direction. + if (emojicoin_0_balance > 0) { + (quote_volume, integrator_fee, emojicoin_1_proceeds) = swap_within_escrow_for_direction( + swapper, + swapper_address, + market_address_0, + market_address_1, + emojicoin_0_balance, + &mut escrow_ref_mut.emojicoin_0, + &mut escrow_ref_mut.emojicoin_1 + ); } else { - vector[market_id_1, market_id_0] - } + // Verify that at least one emojicoin balance is nonzero. + assert!(emojicoin_1_balance > 0, E_EXIT_NO_FUNDS); + + (quote_volume, integrator_fee, emojicoin_0_proceeds) = swap_within_escrow_for_direction( + swapper, + swapper_address, + market_address_1, + market_address_0, + emojicoin_1_balance, + &mut escrow_ref_mut.emojicoin_1, + &mut escrow_ref_mut.emojicoin_0 + ); + }; + + (quote_volume, integrator_fee, emojicoin_0_proceeds, emojicoin_1_proceeds) } - inline fun swap_with_stats( + inline fun swap_with_stats_for_emojicoin( swapper: &signer, - swapper_address: address, market_address: address, - input_amount: u64, - sell_to_apt: bool, - integrator_fee_rate_bps: u8 - ): (u64, u64) { + swap_arguments_for_emojicoin_ref: &SwapArgumentsForEmojicoin + ): (u64, u64, u64) { + + // Extract swap arguments that are used more than once. + let input_amount = swap_arguments_for_emojicoin_ref.input_amount; + let sell_emojicoin_for_apt = + swap_arguments_for_emojicoin_ref.sell_emojicoin_for_apt; + let integrator_fee_rate_bps = + swap_arguments_for_emojicoin_ref.integrator_fee_rate_bps; + + // Simulate swap, get key stats. let simulated_swap = emojicoin_dot_fun::simulate_swap( - swapper_address, + swap_arguments_for_emojicoin_ref.swapper_address, market_address, input_amount, - sell_to_apt, + sell_emojicoin_for_apt, @integrator, integrator_fee_rate_bps ); - let (_, _, _, _, _, _, _, _, net_proceeds, _, quote_volume, _, _, _, _, _, _, _) = - emojicoin_dot_fun::unpack_swap(simulated_swap); + let ( + _, + _, + _, + _, + _, + _, + _, + _, + net_proceeds, + _, + quote_volume, + _, + integrator_fee, + _, + _, + _, + _, + _ + ) = emojicoin_dot_fun::unpack_swap(simulated_swap); + + // Execute swap. emojicoin_dot_fun::swap( swapper, market_address, input_amount, - sell_to_apt, + sell_emojicoin_for_apt, @integrator, integrator_fee_rate_bps, 1 ); - (net_proceeds, quote_volume) - } - inline fun swap_with_stats_buy_emojicoin( - swapper: &signer, - swapper_address: address, - market_address: address, - input_amount: u64, - integrator_fee_rate_bps: u8 - ): (u64, u64) { - swap_with_stats( - swapper, - swapper_address, - market_address, - input_amount, - false, - integrator_fee_rate_bps - ) + (net_proceeds, quote_volume, integrator_fee) } - inline fun swap_with_stats_sell_to_apt( - swapper: &signer, - swapper_address: address, - market_address: address, - input_amount: u64, - integrator_fee_rate_bps: u8 - ): (u64, u64) { - swap_with_stats( - swapper, - swapper_address, - market_address, - input_amount, - true, - integrator_fee_rate_bps - ) - } - - inline fun swap_within_escrow( + inline fun swap_within_escrow_for_direction( swapper: &signer, swapper_address: address, market_address_from: address, market_address_to: address, - escrow_from_coin_ref_mut: &mut Coin, - escrow_to_coin_ref_mut: &mut Coin - ): u64 { - // Move all from coins out of escrow. - let input_amount = coin::value(escrow_from_coin_ref_mut); - withdraw_from_escrow(swapper_address, escrow_from_coin_ref_mut); + from_emojicoin_balance: u64, + from_emojicoin_ref_mut: &mut Coin, + to_emojicoin_ref_mut: &mut Coin + ): (u64, u64, u64) { + + // Pack swap arguments for the first emojicoin swap out of escrow. + let swap_arguments = SwapArgumentsForEmojicoin { + swapper_address, + input_amount: from_emojicoin_balance, + sell_emojicoin_for_apt: true, + integrator_fee_rate_bps: INTEGRATOR_FEE_RATE_BPS_DUAL_ROUTE + }; - // Swap into APT. - let (net_proceeds_in_apt, _) = - swap_with_stats_sell_to_apt( + // Swap out of escrow. + let (apt_proceeds, _, integrator_fee_first_swap) = + swap_out_of_escrow_for_emojicoin( swapper, - swapper_address, + &swap_arguments, market_address_from, - input_amount, - INTEGRATOR_FEE_RATE_BPS_DUAL_ROUTE + from_emojicoin_ref_mut ); - // Swap into to emojicoin. - let (net_proceeds_in_to_coin, quote_volume) = - swap_with_stats_buy_emojicoin( + // Mutate swap arguments for the second swap into escrow. + swap_arguments.input_amount = apt_proceeds; + swap_arguments.sell_emojicoin_for_apt = false; + + // Swap into escrow. + let (to_emojicoin_proceeds, quote_volume, integrator_fee_second_swap) = + swap_into_escrow_for_emojicoin( swapper, - swapper_address, + &swap_arguments, market_address_to, - net_proceeds_in_apt, - INTEGRATOR_FEE_RATE_BPS_DUAL_ROUTE + to_emojicoin_ref_mut ); - // Move to coin to escrow. - coin::merge( - escrow_to_coin_ref_mut, coin::withdraw(swapper, net_proceeds_in_to_coin) - ); + // Sum fees across swaps, but only use volume from second swap to avoid double counting. + let integrator_fee = integrator_fee_first_swap + integrator_fee_second_swap; + (quote_volume, integrator_fee, to_emojicoin_proceeds) + } - // Return quote volume on second swap only, to avoid double-counting. - quote_volume + /// During entry to a melee, try matching a portion of user's contribution if they elect to lock + /// in, returning the matched amount. + inline fun try_match_entry( + lock_in: bool, + input_amount: u64, + escrow_ref_mut: &mut Escrow, + active_melee_ref_mut: &mut Melee, + registry_ref_mut: &mut Registry, + time: u64, + entrant_address: address + ): u64 { + if (lock_in) { + let match_amount = + match_amount( + input_amount, + escrow_ref_mut, + active_melee_ref_mut, + registry_ref_mut, + time + ); + if (match_amount > 0) { + + // Transfer APT to entrant. + let signer_capability_ref = ®istry_ref_mut.signer_capability; + aptos_account::transfer( + &account::create_signer_with_capability(signer_capability_ref), + entrant_address, + match_amount + ); + emit_vault_balance_update_with_signer_capability_ref( + signer_capability_ref + ); + + // Update rewards state for melee and escrow. + active_melee_ref_mut.available_rewards -= match_amount; + escrow_ref_mut.match_amount += match_amount; + + }; + + match_amount + + } else 0 } - inline fun user_melees_entered_melee_ids_add_if_not_contains( - self: &mut UserMelees, melee_id: u64 - ) { - add_if_not_contains(&mut self.entered_melee_ids, melee_id); + /// Only invoke withdraw function is balance is nonzero, returning the amount withdrawn. + inline fun try_withdraw_from_escrow( + recipient: address, escrow_coin_ref_mut: &mut Coin + ): u64 { + let proceeds = coin::value(escrow_coin_ref_mut); + if (proceeds > 0) { + withdraw_from_escrow(recipient, escrow_coin_ref_mut); + }; + proceeds } - inline fun user_melees_exited_melee_ids_add_if_not_contains( - self: &mut UserMelees, melee_id: u64 + inline fun withdraw_from_escrow( + recipient: address, escrow_coin_ref_mut: &mut Coin ) { - add_if_not_contains(&mut self.exited_melee_ids, melee_id); + aptos_account::deposit_coins(recipient, coin::extract_all(escrow_coin_ref_mut)); } - inline fun user_melees_exited_melee_ids_remove_if_contains( - self: &mut UserMelees, melee_id: u64 - ) { - remove_if_contains(&mut self.exited_melee_ids, melee_id); + #[test_only] + public fun get_DEFAULT_AVAILABLE_REWARDS(): u64 { + DEFAULT_AVAILABLE_REWARDS } - inline fun user_melees_unexited_melee_ids_add_if_not_contains( - self: &mut UserMelees, melee_id: u64 - ) { - add_if_not_contains(&mut self.unexited_melee_ids, melee_id); + #[test_only] + public fun get_DEFAULT_DURATION(): u64 { + DEFAULT_DURATION } - inline fun user_melees_unexited_melee_ids_remove_if_contains( - self: &mut UserMelees, melee_id: u64 - ) { - remove_if_contains(&mut self.unexited_melee_ids, melee_id); + #[test_only] + public fun get_DEFAULT_MAX_MATCH_PERCENTAGE(): u64 { + DEFAULT_MAX_MATCH_PERCENTAGE } - inline fun withdraw_from_escrow( - recipient: address, escrow_coin_ref_mut: &mut Coin + #[test_only] + public fun get_DEFAULT_MAX_MATCH_AMOUNT(): u64 { + DEFAULT_MAX_MATCH_AMOUNT + } + + #[test_only] + public fun get_REGISTRY_SEED(): vector { + REGISTRY_SEED + } + + #[test_only] + public fun init_module_test_only(account: &signer) acquires Registry { + init_module(account) + } + + #[test_only] + public fun unpack_enter( + self: Enter + ): (address, u64, u64, u64, u64, u64, u64, u64, ExchangeRate, ExchangeRate) { + let Enter { + user, + melee_id, + input_amount, + quote_volume, + integrator_fee, + match_amount, + emojicoin_0_proceeds, + emojicoin_1_proceeds, + emojicoin_0_exchange_rate, + emojicoin_1_exchange_rate + } = self; + ( + user, + melee_id, + input_amount, + quote_volume, + integrator_fee, + match_amount, + emojicoin_0_proceeds, + emojicoin_1_proceeds, + emojicoin_0_exchange_rate, + emojicoin_1_exchange_rate + ) + } + + #[test_only] + public fun unpack_exchange_rate(self: ExchangeRate): (u64, u64) { + let ExchangeRate { base, quote } = self; + (base, quote) + } + + #[test_only] + public fun unpack_exit(self: Exit): + (address, u64, u64, u64, u64, ExchangeRate, ExchangeRate) { + let Exit { + user, + melee_id, + tap_out_fee, + emojicoin_0_proceeds, + emojicoin_1_proceeds, + emojicoin_0_exchange_rate, + emojicoin_1_exchange_rate + } = self; + ( + user, + melee_id, + tap_out_fee, + emojicoin_0_proceeds, + emojicoin_1_proceeds, + emojicoin_0_exchange_rate, + emojicoin_1_exchange_rate + ) + } + + #[test_only] + public fun unpack_swap(self: Swap): + ( + address, u64, u64, u64, u64, u64, ExchangeRate, ExchangeRate ) { - aptos_account::deposit_coins(recipient, coin::extract_all(escrow_coin_ref_mut)); + let Swap { + user, + melee_id, + quote_volume, + integrator_fee, + emojicoin_0_proceeds, + emojicoin_1_proceeds, + emojicoin_0_exchange_rate, + emojicoin_1_exchange_rate + } = self; + ( + user, + melee_id, + quote_volume, + integrator_fee, + emojicoin_0_proceeds, + emojicoin_1_proceeds, + emojicoin_0_exchange_rate, + emojicoin_1_exchange_rate + ) + } + + #[test_only] + public fun unpack_vault_balance_update(self: VaultBalanceUpdate): u64 { + let VaultBalanceUpdate { new_balance } = self; + new_balance } } diff --git a/src/move/emojicoin_arena/sources/pseudo_randomness.move b/src/move/emojicoin_arena/sources/pseudo_randomness.move index 83f475028..0ad379738 100644 --- a/src/move/emojicoin_arena/sources/pseudo_randomness.move +++ b/src/move/emojicoin_arena/sources/pseudo_randomness.move @@ -1,10 +1,10 @@ -module arena::pseudo_randomness { +module emojicoin_arena::pseudo_randomness { use aptos_framework::transaction_context; use std::bcs; use std::vector; - friend arena::emojicoin_arena; + friend emojicoin_arena::emojicoin_arena; /// Pseudo-random substitute for `aptos_framework::randomness::u64_range`, since /// the randomness API is not available during `init_module`. diff --git a/src/move/emojicoin_arena/tests/tests.move b/src/move/emojicoin_arena/tests/tests.move new file mode 100644 index 000000000..55cd29309 --- /dev/null +++ b/src/move/emojicoin_arena/tests/tests.move @@ -0,0 +1,442 @@ +#[test_only] +// This test module uses the same design schema as the emojicoin dot fun core test module. +module emojicoin_arena::tests { + use aptos_framework::account::{ + create_resource_address, + create_signer_for_test as get_signer + }; + use aptos_framework::event::{emitted_events}; + use aptos_framework::timestamp; + use aptos_framework::transaction_context; + use emojicoin_dot_fun::emojicoin_dot_fun::{ + MarketMetadata, + market_metadata_by_market_id, + unpack_market_metadata + }; + use emojicoin_arena::emojicoin_arena::{ + ExchangeRate, + Exit, + Melee, + RegistryView, + VaultBalanceUpdate, + fund_vault, + get_DEFAULT_AVAILABLE_REWARDS, + get_DEFAULT_DURATION, + get_DEFAULT_MAX_MATCH_PERCENTAGE, + get_DEFAULT_MAX_MATCH_AMOUNT, + get_REGISTRY_SEED, + init_module_test_only, + registry, + set_next_melee_available_rewards, + set_next_melee_duration, + set_next_melee_max_match_amount, + set_next_melee_max_match_percentage, + unpack_exchange_rate, + unpack_exit, + unpack_melee, + unpack_registry_view, + unpack_vault_balance_update, + withdraw_from_vault + }; + use emojicoin_dot_fun::test_acquisitions::mint_aptos_coin_to; + use emojicoin_dot_fun::tests as emojicoin_dot_fun_tests; + use std::option; + use std::vector; + + struct MockExchangeRate has copy, drop, store { + base: u64, + quote: u64 + } + + struct MockExit has copy, drop, store { + user: address, + melee_id: u64, + tap_out_fee: u64, + emojicoin_0_proceeds: u64, + emojicoin_1_proceeds: u64, + emojicoin_0_exchange_rate: MockExchangeRate, + emojicoin_1_exchange_rate: MockExchangeRate + } + + struct MockMarketMetadata has copy, drop, store { + market_id: u64, + market_address: address, + emoji_bytes: vector + } + + struct MockMelee has copy, drop, store { + melee_id: u64, + emojicoin_0_market_address: address, + emojicoin_1_market_address: address, + start_time: u64, + duration: u64, + max_match_percentage: u64, + max_match_amount: u64, + available_rewards: u64 + } + + struct MockRegistryView has copy, drop, store { + n_melees: u64, + vault_address: address, + vault_balance: u64, + next_melee_duration: u64, + next_melee_available_rewards: u64, + next_melee_max_match_percentage: u64, + next_melee_max_match_amount: u64 + } + + struct MockVaultBalanceUpdate has copy, drop, store { + new_balance: u64 + } + + // Test market emoji bytes, in order of market ID. + const BLACK_CAT: vector = x"f09f9088e2808de2ac9b"; + const BLACK_HEART: vector = x"f09f96a4"; + const YELLOW_HEART: vector = x"f09f929b"; + const YIN_YANG: vector = x"e298afefb88f"; + const ZEBRA: vector = x"f09fa693"; + const ZOMBIE: vector = x"f09fa79f"; + + public fun assert_exchange_rate( + self: MockExchangeRate, actual: ExchangeRate + ) { + let (base, quote) = unpack_exchange_rate(actual); + assert!(self.base == base); + assert!(self.quote == quote); + } + + public fun assert_exit(self: MockExit, actual: Exit) { + let ( + user, + melee_id, + tap_out_fee, + emojicoin_0_proceeds, + emojicoin_1_proceeds, + emojicoin_0_exchange_rate, + emojicoin_1_exchange_rate + ) = unpack_exit(actual); + assert!(self.user == user); + assert!(self.melee_id == melee_id); + assert!(self.tap_out_fee == tap_out_fee); + assert!(self.emojicoin_0_proceeds == emojicoin_0_proceeds); + assert!(self.emojicoin_1_proceeds == emojicoin_1_proceeds); + self.emojicoin_0_exchange_rate.assert_exchange_rate(emojicoin_0_exchange_rate); + self.emojicoin_1_exchange_rate.assert_exchange_rate(emojicoin_1_exchange_rate); + } + + public fun assert_market_metadata( + self: MockMarketMetadata, actual: MarketMetadata + ) { + let (market_id, market_address, emoji_bytes) = unpack_market_metadata(actual); + assert!(self.market_id == market_id); + assert!(self.market_address == market_address); + assert!(self.emoji_bytes == emoji_bytes); + } + + public fun assert_melee(self: MockMelee, actual: Melee) { + let ( + melee_id, + emojicoin_0_market_address, + emojicoin_1_market_address, + start_time, + duration, + max_match_percentage, + max_match_amount, + available_rewards + ) = unpack_melee(actual); + assert!(self.melee_id == melee_id); + assert!(self.emojicoin_0_market_address == emojicoin_0_market_address); + assert!(self.emojicoin_1_market_address == emojicoin_1_market_address); + assert!(self.start_time == start_time); + assert!(self.duration == duration); + assert!(self.max_match_percentage == max_match_percentage); + assert!(self.max_match_amount == max_match_amount); + assert!(self.available_rewards == available_rewards); + } + + public fun assert_registry_view( + self: MockRegistryView, actual: RegistryView + ) { + let ( + n_melees, + vault_address, + vault_balance, + next_melee_duration, + next_melee_available_rewards, + next_melee_max_match_percentage, + next_melee_max_match_amount + ) = unpack_registry_view(actual); + assert!(self.n_melees == n_melees); + assert!(self.vault_address == vault_address); + assert!(self.vault_balance == vault_balance); + assert!(self.next_melee_duration == next_melee_duration); + assert!(self.next_melee_available_rewards == next_melee_available_rewards); + assert!(self.next_melee_max_match_percentage == next_melee_max_match_percentage); + assert!(self.next_melee_max_match_amount == next_melee_max_match_amount); + } + + public fun assert_vault_balance_update( + self: MockVaultBalanceUpdate, actual: VaultBalanceUpdate + ) { + let new_balance = unpack_vault_balance_update(actual); + assert!(self.new_balance == new_balance); + } + + public fun base_melee(): MockMelee { + MockMelee { + melee_id: 1, + emojicoin_0_market_address: @black_cat_market, + emojicoin_1_market_address: @zebra_market, + start_time: base_start_time(), + duration: get_DEFAULT_DURATION(), + max_match_percentage: get_DEFAULT_MAX_MATCH_PERCENTAGE(), + max_match_amount: get_DEFAULT_MAX_MATCH_AMOUNT(), + available_rewards: get_DEFAULT_AVAILABLE_REWARDS() + } + } + + /// 1.5x the default melee duration. + public fun base_publish_time(): u64 { + get_DEFAULT_DURATION() + get_DEFAULT_DURATION() / 2 + } + + public fun base_registry_view(): MockRegistryView { + MockRegistryView { + n_melees: 1, + vault_address: base_vault_address(), + vault_balance: get_DEFAULT_AVAILABLE_REWARDS(), + next_melee_duration: get_DEFAULT_DURATION(), + next_melee_available_rewards: get_DEFAULT_AVAILABLE_REWARDS(), + next_melee_max_match_percentage: get_DEFAULT_MAX_MATCH_PERCENTAGE(), + next_melee_max_match_amount: get_DEFAULT_MAX_MATCH_AMOUNT() + } + } + + public fun base_start_time(): u64 { + get_DEFAULT_DURATION() + } + + public fun base_vault_address(): address { + create_resource_address(&@emojicoin_arena, get_REGISTRY_SEED()) + } + + /// Initialize emojicoin dot fun with test markets. + public fun init_emojicoin_dot_fun_with_test_markets() { + emojicoin_dot_fun_tests::init_package(); + vector::for_each_ref( + &vector[BLACK_CAT, BLACK_HEART, YELLOW_HEART, YIN_YANG, ZEBRA, ZOMBIE], + |bytes_ref| { + emojicoin_dot_fun_tests::init_market(vector[*bytes_ref]); + } + ); + } + + public fun init_module_with_funded_vault() { + init_emojicoin_dot_fun_with_test_markets(); + + // Set global time to base publish time. + timestamp::update_global_time_for_test(base_publish_time()); + + // Initialize module. + init_module_test_only(&get_signer(@emojicoin_arena)); + + // Fund admin, then fund vault. + mint_aptos_coin_to(@emojicoin_arena, get_DEFAULT_AVAILABLE_REWARDS()); + fund_vault(&get_signer(@emojicoin_arena), get_DEFAULT_AVAILABLE_REWARDS()); + } + + #[test] + public fun admin_functions() { + // Initialize emojicoin dot fun. + init_emojicoin_dot_fun_with_test_markets(); + + // Set global time to base publish time. + timestamp::update_global_time_for_test(base_publish_time()); + + // Initialize module. + init_module_test_only(&get_signer(@emojicoin_arena)); + + // Fund admin, then fund vault. + mint_aptos_coin_to(@emojicoin_arena, get_DEFAULT_AVAILABLE_REWARDS()); + fund_vault(&get_signer(@emojicoin_arena), get_DEFAULT_AVAILABLE_REWARDS()); + + // Assert vault balance update event. + let vault_balance_update_events = emitted_events(); + assert!(vault_balance_update_events.length() == 1); + MockVaultBalanceUpdate { new_balance: get_DEFAULT_AVAILABLE_REWARDS() }.assert_vault_balance_update( + vault_balance_update_events[0] + ); + + // Withdraw from vault. + let withdrawn_octas = 1; + withdraw_from_vault(&get_signer(@emojicoin_arena), withdrawn_octas); + + // Assert vault balance update event. + let vault_balance_update_events = emitted_events(); + assert!(vault_balance_update_events.length() == 2); + MockVaultBalanceUpdate { + new_balance: get_DEFAULT_AVAILABLE_REWARDS() - withdrawn_octas + }.assert_vault_balance_update(vault_balance_update_events[1]); + + // Set next melee parameters. + let ( + next_available_rewards, + next_duration, + next_max_match_amount, + next_max_match_percentage + ) = (2, 3, 4, 5); + set_next_melee_available_rewards( + &get_signer(@emojicoin_arena), next_available_rewards + ); + set_next_melee_duration(&get_signer(@emojicoin_arena), next_duration); + set_next_melee_max_match_amount( + &get_signer(@emojicoin_arena), next_max_match_amount + ); + set_next_melee_max_match_percentage( + &get_signer(@emojicoin_arena), next_max_match_percentage + ); + + // Verify registry view. + let registry_view = base_registry_view(); + registry_view.next_melee_available_rewards = next_available_rewards; + registry_view.next_melee_duration = next_duration; + registry_view.next_melee_max_match_amount = next_max_match_amount; + registry_view.next_melee_max_match_percentage = next_max_match_percentage; + registry_view.vault_balance -= withdrawn_octas; + registry_view.assert_registry_view(registry()); + } + + #[test] + public fun init_module_base() { + // Initialize emojicoin dot fun. + init_emojicoin_dot_fun_with_test_markets(); + + // Set global time to base publish time. + timestamp::update_global_time_for_test(base_publish_time()); + + // Initialize module. + init_module_test_only(&get_signer(@emojicoin_arena)); + + // Assert registry view. + let registry_view = base_registry_view(); + registry_view.vault_balance = 0; + registry_view.assert_registry_view(registry()); + + // Assert melee event. + let melee_events = emitted_events(); + assert!(melee_events.length() == 1); + base_melee().assert_melee(melee_events[0]); + } + + #[test] + /// Like `init_module_base`, but generates several AUIDs before calling `init_module`, + /// effectively changing the pseudo-random seed. Since there are so few markets registered at + /// the simulated publish time, multiple calls may be required to trigger a different initial + /// melee than that from `init_module`, while also hitting coverage on the inner function + /// `sort_unique_market_ids`. + public fun init_module_different_seed() { + for (i in 0..4) { + transaction_context::generate_auid_address(); + }; + + // Initialize emojicoin dot fun. + init_emojicoin_dot_fun_with_test_markets(); + + // Set global time to base publish time. + timestamp::update_global_time_for_test(base_publish_time()); + + // Initialize module. + init_module_test_only(&get_signer(@emojicoin_arena)); + + // Assert registry view. + let registry_view = base_registry_view(); + registry_view.vault_balance = 0; + registry_view.assert_registry_view(registry()); + + // Assert melee event. + let melee_events = emitted_events(); + assert!(melee_events.length() == 1); + let mock_melee = base_melee(); + mock_melee.emojicoin_0_market_address = @yin_yang_market; + mock_melee.emojicoin_1_market_address = @zombie_market; + mock_melee.assert_melee(melee_events[0]); + } + + #[test] + #[ + expected_failure( + abort_code = emojicoin_arena::emojicoin_arena::E_NOT_EMOJICOIN_ARENA, + location = emojicoin_arena::emojicoin_arena + ) + ] + public fun set_next_melee_available_rewards_not_arena() { + set_next_melee_available_rewards(&get_signer(@aptos_framework), 0); + } + + #[test] + #[ + expected_failure( + abort_code = emojicoin_arena::emojicoin_arena::E_NOT_EMOJICOIN_ARENA, + location = emojicoin_arena::emojicoin_arena + ) + ] + public fun set_next_melee_duration_not_arena() { + set_next_melee_duration(&get_signer(@aptos_framework), 0); + } + + #[test] + #[ + expected_failure( + abort_code = emojicoin_arena::emojicoin_arena::E_NOT_EMOJICOIN_ARENA, + location = emojicoin_arena::emojicoin_arena + ) + ] + public fun set_next_melee_max_match_amount_not_arena() { + set_next_melee_max_match_amount(&get_signer(@aptos_framework), 0); + } + + #[test] + #[ + expected_failure( + abort_code = emojicoin_arena::emojicoin_arena::E_NOT_EMOJICOIN_ARENA, + location = emojicoin_arena::emojicoin_arena + ) + ] + public fun set_next_melee_max_match_percentage_not_arena() { + set_next_melee_max_match_percentage(&get_signer(@aptos_framework), 0); + } + + #[test] + public fun test_market_addresses() { + init_emojicoin_dot_fun_with_test_markets(); + let market_addresses = vector[ + @black_cat_market, + @black_heart_market, + @yellow_heart_market, + @yin_yang_market, + @zebra_market, + @zombie_market + ]; + let market_emoji_bytes = vector[BLACK_CAT, BLACK_HEART, YELLOW_HEART, YIN_YANG, ZEBRA, ZOMBIE]; + for (i in 0..market_addresses.length()) { + MockMarketMetadata { + market_id: i + 1, + market_address: market_addresses[i], + emoji_bytes: market_emoji_bytes[i] + }.assert_market_metadata( + option::destroy_some(market_metadata_by_market_id(i + 1)) + ); + }; + } + + #[test] + #[ + expected_failure( + abort_code = emojicoin_arena::emojicoin_arena::E_NOT_EMOJICOIN_ARENA, + location = emojicoin_arena::emojicoin_arena + ) + ] + public fun withdraw_from_vault_not_arena() { + withdraw_from_vault(&get_signer(@aptos_framework), 0); + } +} diff --git a/src/move/test_coin_factories/yin_yang/Move.toml b/src/move/test_coin_factories/yin_yang/Move.toml new file mode 100644 index 000000000..b06a93602 --- /dev/null +++ b/src/move/test_coin_factories/yin_yang/Move.toml @@ -0,0 +1,12 @@ +[addresses] +yin_yang_market = "0x6d64edcaa823d4b7bf5d98d424cc403870dd7ee4383e54ccca21d486d22a8588" + +[dev-dependencies.AptosFramework] +git = "https://github.com/aptos-labs/aptos-core.git" +rev = "mainnet" +subdir = "aptos-move/framework/aptos-framework" + +[package] +name = "YinYangCoinFactory" +upgrade_policy = "immutable" +version = "1.0.0" diff --git a/src/move/test_coin_factories/yin_yang/sources/coin_factory.move b/src/move/test_coin_factories/yin_yang/sources/coin_factory.move new file mode 100644 index 000000000..7d1afd07d --- /dev/null +++ b/src/move/test_coin_factories/yin_yang/sources/coin_factory.move @@ -0,0 +1,6 @@ +#[test_only] +module yin_yang_market::coin_factory { + struct Emojicoin {} + struct EmojicoinLP {} + struct BadType {} +} diff --git a/src/move/test_coin_factories/zebra/Move.toml b/src/move/test_coin_factories/zebra/Move.toml new file mode 100644 index 000000000..a58f6ac94 --- /dev/null +++ b/src/move/test_coin_factories/zebra/Move.toml @@ -0,0 +1,12 @@ +[addresses] +zebra_market = "0xb87c674ff0ebff4115288bdb63a5601ed452019f00deeeb2ba848cbf37ba918a" + +[dev-dependencies.AptosFramework] +git = "https://github.com/aptos-labs/aptos-core.git" +rev = "mainnet" +subdir = "aptos-move/framework/aptos-framework" + +[package] +name = "ZebraCoinFactory" +upgrade_policy = "immutable" +version = "1.0.0" diff --git a/src/move/test_coin_factories/zebra/sources/coin_factory.move b/src/move/test_coin_factories/zebra/sources/coin_factory.move new file mode 100644 index 000000000..73170eb9c --- /dev/null +++ b/src/move/test_coin_factories/zebra/sources/coin_factory.move @@ -0,0 +1,6 @@ +#[test_only] +module zebra_market::coin_factory { + struct Emojicoin {} + struct EmojicoinLP {} + struct BadType {} +} diff --git a/src/move/test_coin_factories/zombie/Move.toml b/src/move/test_coin_factories/zombie/Move.toml new file mode 100644 index 000000000..54559b7e3 --- /dev/null +++ b/src/move/test_coin_factories/zombie/Move.toml @@ -0,0 +1,12 @@ +[addresses] +zombie_market = "0xfd15a4d8073c1bf3d63eee43f98786a4a2ec9933df89d3cdbed7b90b5b7156ae" + +[dev-dependencies.AptosFramework] +git = "https://github.com/aptos-labs/aptos-core.git" +rev = "mainnet" +subdir = "aptos-move/framework/aptos-framework" + +[package] +name = "ZombieCoinFactory" +upgrade_policy = "immutable" +version = "1.0.0" diff --git a/src/move/test_coin_factories/zombie/sources/coin_factory.move b/src/move/test_coin_factories/zombie/sources/coin_factory.move new file mode 100644 index 000000000..b0797f4b3 --- /dev/null +++ b/src/move/test_coin_factories/zombie/sources/coin_factory.move @@ -0,0 +1,6 @@ +#[test_only] +module zombie_market::coin_factory { + struct Emojicoin {} + struct EmojicoinLP {} + struct BadType {} +}