From 6c0169039989c82e11899fca4d49ee5fbbbf0125 Mon Sep 17 00:00:00 2001 From: pisuthd <pisuth.dae@gmail.com> Date: Mon, 3 Jun 2024 13:05:27 +0900 Subject: [PATCH 1/4] init aptos project --- package.json | 3 ++- packages/aptos/.gitignore | 2 ++ packages/aptos/Move.toml | 17 +++++++++++++++++ packages/aptos/package.json | 14 ++++++++++++++ packages/aptos/sources/vault.move | 11 +++++++++++ 5 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 packages/aptos/.gitignore create mode 100644 packages/aptos/Move.toml create mode 100644 packages/aptos/package.json create mode 100644 packages/aptos/sources/vault.move diff --git a/package.json b/package.json index fa8b324..06dea15 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "bootstrap": "npm install; lerna bootstrap;", "start": "run-p --print-label package:*", "build": "lerna run --parallel build", - "test": "lerna run test --concurrency 1 --stream --scope sui" + "test": "lerna run test --concurrency 1 --stream --scope sui", + "test-aptos": "lerna run test --concurrency 1 --stream --scope aptos" }, "devDependencies": { "lerna": "^6.6.2", diff --git a/packages/aptos/.gitignore b/packages/aptos/.gitignore new file mode 100644 index 0000000..243f545 --- /dev/null +++ b/packages/aptos/.gitignore @@ -0,0 +1,2 @@ +.aptos/ +build/ \ No newline at end of file diff --git a/packages/aptos/Move.toml b/packages/aptos/Move.toml new file mode 100644 index 0000000..27e36fe --- /dev/null +++ b/packages/aptos/Move.toml @@ -0,0 +1,17 @@ +[package] +name = "legato" +version = "0.1.0" +authors = [] + +[addresses] +legato_addr = "f586bbdd187df4e3bd7cd5b8af8a7018d8b8b5f3ec24a95cd5ac79d94ebca981" +aptos_framework = "0x1" + +[dev-addresses] + +[dependencies.AptosFramework] +git = "https://github.com/aptos-labs/aptos-core.git" +rev = "mainnet" +subdir = "aptos-move/framework/aptos-framework" + +[dev-dependencies] diff --git a/packages/aptos/package.json b/packages/aptos/package.json new file mode 100644 index 0000000..f763524 --- /dev/null +++ b/packages/aptos/package.json @@ -0,0 +1,14 @@ +{ + "name": "aptos", + "version": "0.1.0", + "description": "", + "main": "index.js", + "directories": { + "test": "tests" + }, + "scripts": { + "test": "aptos move test" + }, + "author": "", + "license": "MIT" +} diff --git a/packages/aptos/sources/vault.move b/packages/aptos/sources/vault.move new file mode 100644 index 0000000..a47d4b6 --- /dev/null +++ b/packages/aptos/sources/vault.move @@ -0,0 +1,11 @@ +// Copyright (c) Tamago Blockchain Labs, Inc. +// SPDX-License-Identifier: MIT + +// A timelock vault to convert APT into future value including yield tokenization for general purposes. +// Each vault maintains its own tokens and adheres to a quarterly expiration schedule. + +module legato_addr::vault { + + + +} \ No newline at end of file From 767d4addf6bf31c0ef98eb8e2db026c6a1f80bdf Mon Sep 17 00:00:00 2001 From: pisuthd <pisuth.dae@gmail.com> Date: Mon, 3 Jun 2024 19:15:40 +0900 Subject: [PATCH 2/4] add base fa and mock tokens --- .../sources/utils/base_fungible_asset.move | 185 ++++++++++++++++++ .../aptos/sources/utils/mock_legato..move | 70 +++++++ packages/aptos/sources/utils/mock_usdc.move | 47 +++++ 3 files changed, 302 insertions(+) create mode 100644 packages/aptos/sources/utils/base_fungible_asset.move create mode 100644 packages/aptos/sources/utils/mock_legato..move create mode 100644 packages/aptos/sources/utils/mock_usdc.move diff --git a/packages/aptos/sources/utils/base_fungible_asset.move b/packages/aptos/sources/utils/base_fungible_asset.move new file mode 100644 index 0000000..9162ced --- /dev/null +++ b/packages/aptos/sources/utils/base_fungible_asset.move @@ -0,0 +1,185 @@ +// A base fungible asset module that allows anyone to mint and burn coins + +module legato_addr::base_fungible_asset { + + use aptos_framework::fungible_asset::{Self, MintRef, TransferRef, BurnRef, Metadata, FungibleStore, FungibleAsset}; + use aptos_framework::object::{Self, Object, ConstructorRef}; + use aptos_framework::primary_fungible_store; + use std::error; + use std::signer; + use std::string::{String, utf8}; + use std::option; + use std::vector; + + /// Only fungible asset metadata owner can make changes. + const ERR_NOT_OWNER: u64 = 1; + /// The length of ref_flags is not 3. + const ERR_INVALID_REF_FLAGS_LENGTH: u64 = 2; + /// The lengths of two vector do not equal. + const ERR_VECTORS_LENGTH_MISMATCH: u64 = 3; + /// MintRef error. + const ERR_MINT_REF: u64 = 4; + /// TransferRef error. + const ERR_TRANSFER_REF: u64 = 5; + /// BurnRef error. + const ERR_BURN_REF: u64 = 6; + + #[resource_group_member(group = aptos_framework::object::ObjectGroup)] + /// Hold refs to control the minting, transfer and burning of fungible assets. + struct ManagingRefs has key { + mint_ref: MintRef, + transfer_ref: TransferRef, + burn_ref: BurnRef, + } + + /// Initialize metadata object + public fun initialize( + constructor_ref: &ConstructorRef, + maximum_supply: u128, + name: String, + symbol: String, + decimals: u8, + icon_uri: String, + project_uri: String + ) { + let supply = if (maximum_supply != 0) { + option::some(maximum_supply) + } else { + option::none() + }; + primary_fungible_store::create_primary_store_enabled_fungible_asset( + constructor_ref, + supply, + name, + symbol, + decimals, + icon_uri, + project_uri, + ); + let metadata_object_signer = object::generate_signer(constructor_ref); + move_to( + &metadata_object_signer, + ManagingRefs { + mint_ref: fungible_asset::generate_mint_ref(constructor_ref), + transfer_ref: fungible_asset::generate_transfer_ref(constructor_ref), + burn_ref: fungible_asset::generate_burn_ref(constructor_ref) + } + ) + + } + + /// Mint to the primary fungible stores of the accounts with amounts of FAs. + public entry fun mint_to_primary_stores( + asset: Object<Metadata>, + to: vector<address>, + amounts: vector<u64> + ) acquires ManagingRefs { + let receiver_primary_stores = vector::map( + to, + |addr| primary_fungible_store::ensure_primary_store_exists(addr, asset) + ); + mint( asset, receiver_primary_stores, amounts); + } + + /// Mint to multiple fungible stores with amounts of FAs. + public entry fun mint( + asset: Object<Metadata>, + stores: vector<Object<FungibleStore>>, + amounts: vector<u64>, + ) acquires ManagingRefs { + let length = vector::length(&stores); + assert!(length == vector::length(&amounts), error::invalid_argument(ERR_VECTORS_LENGTH_MISMATCH)); + let mint_ref = borrow_mint_ref(asset); + let i = 0; + while (i < length) { + fungible_asset::mint_to(mint_ref, *vector::borrow(&stores, i), *vector::borrow(&amounts, i)); + i = i + 1; + } + } + + /// Burn fungible assets from the primary stores of accounts. + public entry fun burn_from_primary_stores( + asset: Object<Metadata>, + from: vector<address>, + amounts: vector<u64> + ) acquires ManagingRefs { + let primary_stores = vector::map( + from, + |addr| primary_fungible_store::primary_store(addr, asset) + ); + burn( asset, primary_stores, amounts); + } + + + /// Burn fungible assets from fungible stores. + public entry fun burn( + asset: Object<Metadata>, + stores: vector<Object<FungibleStore>>, + amounts: vector<u64> + ) acquires ManagingRefs { + let length = vector::length(&stores); + assert!(length == vector::length(&amounts), error::invalid_argument(ERR_VECTORS_LENGTH_MISMATCH)); + let burn_ref = borrow_burn_ref( asset); + let i = 0; + while (i < length) { + fungible_asset::burn_from(burn_ref, *vector::borrow(&stores, i), *vector::borrow(&amounts, i)); + i = i + 1; + }; + } + + inline fun borrow_mint_ref( + asset: Object<Metadata>, + ): &MintRef acquires ManagingRefs { + let refs = borrow_global<ManagingRefs>(object::object_address(&asset)); + &refs.mint_ref + } + + inline fun borrow_burn_ref( + asset: Object<Metadata>, + ): &BurnRef acquires ManagingRefs { + let refs = borrow_global<ManagingRefs>(object::object_address(&asset)); + &refs.burn_ref + } + + #[test_only] + use aptos_framework::object::object_from_constructor_ref; + + #[test_only] + fun create_test_mfa(creator: &signer): Object<Metadata> { + let constructor_ref = &object::create_named_object(creator, b"APT"); + initialize( + constructor_ref, + 0, + utf8(b"Aptos Token"), /* name */ + utf8(b"APT"), /* symbol */ + 8, /* decimals */ + utf8(b"http://example.com/favicon.ico"), /* icon */ + utf8(b"http://example.com"), /* project */ + ); + object_from_constructor_ref<Metadata>(constructor_ref) + } + + #[test(creator = @legato_addr, alice = @0xface)] + fun test_basic_flow( + creator: &signer, + alice: &signer + ) acquires ManagingRefs { + let metadata = create_test_mfa(creator); + let creator_address = signer::address_of(creator); + let alice_address = signer::address_of(alice); + + mint_to_primary_stores( metadata, vector[creator_address, alice_address], vector[100, 50]); + assert!(primary_fungible_store::balance(creator_address, metadata) == 100, 1); + assert!(primary_fungible_store::balance(alice_address, metadata) == 50, 2); + + primary_fungible_store::transfer(creator, metadata, alice_address, 5); + + assert!(primary_fungible_store::balance(creator_address, metadata) == 95, 3); + assert!(primary_fungible_store::balance(alice_address, metadata) == 55, 4); + + burn_from_primary_stores( metadata, vector[creator_address, alice_address], vector[95, 55]); + assert!(primary_fungible_store::balance(creator_address, metadata) == 0, 5); + assert!(primary_fungible_store::balance(alice_address, metadata) == 0, 6); + } + +} \ No newline at end of file diff --git a/packages/aptos/sources/utils/mock_legato..move b/packages/aptos/sources/utils/mock_legato..move new file mode 100644 index 0000000..dc308d9 --- /dev/null +++ b/packages/aptos/sources/utils/mock_legato..move @@ -0,0 +1,70 @@ + +// Mock Legato coins in fungible asset + +module legato_addr::mock_legato { + + use aptos_framework::object; + use aptos_framework::fungible_asset::{Self, Metadata, FungibleAsset}; + use aptos_framework::object::Object; + use legato_addr::base_fungible_asset; + use std::string::utf8; + + const ASSET_SYMBOL: vector<u8> = b"LEGATO"; + + /// Initialize metadata object and store the refs. + fun init_module(admin: &signer) { + let constructor_ref = &object::create_named_object(admin, ASSET_SYMBOL); + base_fungible_asset::initialize( + constructor_ref, + 0, /* maximum_supply. 0 means no maximum */ + utf8(b"Mock Legato Tokens"), /* name */ + utf8(ASSET_SYMBOL), /* symbol */ + 8, /* decimals */ + utf8(b"http://example.com/favicon.ico"), /* icon */ + utf8(b"http://example.com"), /* project */ + ); + } + + #[view] + /// Return the address of the metadata that's created when this module is deployed. + public fun get_metadata(): Object<Metadata> { + let metadata_address = object::create_object_address(&@legato_addr, ASSET_SYMBOL); + object::address_to_object<Metadata>(metadata_address) + } + + /// Mint as the owner of metadata object. + public entry fun mint( to: address, amount: u64) { + base_fungible_asset::mint_to_primary_stores( get_metadata(), vector[to], vector[amount]); + } + + + /// Burn fungible assets as the owner of metadata object. + public entry fun burn( from: address, amount: u64) { + base_fungible_asset::burn_from_primary_stores( get_metadata(), vector[from], vector[amount]); + } + + #[test_only] + use aptos_framework::primary_fungible_store; + #[test_only] + use std::signer; + + #[test(creator = @legato_addr, alice = @0xface)] + fun test_basic_flow(creator: &signer, alice: &signer) { + init_module(creator); + let creator_address = signer::address_of(creator); + let alice_address = signer::address_of(alice); + + mint( creator_address, 100); + let metadata = get_metadata(); + assert!(primary_fungible_store::balance(creator_address, metadata) == 100, 1); + + primary_fungible_store::transfer(creator, metadata, alice_address, 5); + assert!(primary_fungible_store::balance(alice_address, metadata) == 5, 2); + + burn( creator_address, 95); + burn( alice_address, 5); + assert!(primary_fungible_store::balance(creator_address, metadata) == 0, 3); + assert!(primary_fungible_store::balance(alice_address, metadata) == 0, 4); + } + +} \ No newline at end of file diff --git a/packages/aptos/sources/utils/mock_usdc.move b/packages/aptos/sources/utils/mock_usdc.move new file mode 100644 index 0000000..7e7cdb6 --- /dev/null +++ b/packages/aptos/sources/utils/mock_usdc.move @@ -0,0 +1,47 @@ + + +// Mock USDC coins in fungible asset + +module legato_addr::mock_usdc { + + use aptos_framework::object; + use aptos_framework::fungible_asset::{Self, Metadata, FungibleAsset}; + use aptos_framework::object::Object; + use legato_addr::base_fungible_asset; + use std::string::utf8; + + const ASSET_SYMBOL: vector<u8> = b"USDC"; + + /// Initialize metadata object and store the refs. + fun init_module(admin: &signer) { + let constructor_ref = &object::create_named_object(admin, ASSET_SYMBOL); + base_fungible_asset::initialize( + constructor_ref, + 0, /* maximum_supply. 0 means no maximum */ + utf8(b"Mock USDC Tokens"), /* name */ + utf8(ASSET_SYMBOL), /* symbol */ + 6, /* decimals */ + utf8(b"http://example.com/favicon.ico"), /* icon */ + utf8(b"http://example.com"), /* project */ + ); + } + + #[view] + /// Return the address of the metadata that's created when this module is deployed. + public fun get_metadata(): Object<Metadata> { + let metadata_address = object::create_object_address(&@legato_addr, ASSET_SYMBOL); + object::address_to_object<Metadata>(metadata_address) + } + + /// Mint as the owner of metadata object. + public entry fun mint( to: address, amount: u64) { + base_fungible_asset::mint_to_primary_stores( get_metadata(), vector[to], vector[amount]); + } + + + /// Burn fungible assets as the owner of metadata object. + public entry fun burn( from: address, amount: u64) { + base_fungible_asset::burn_from_primary_stores( get_metadata(), vector[from], vector[amount]); + } + +} \ No newline at end of file From 63fdecb082f93c22896ac39b1ec2282840a4d40c Mon Sep 17 00:00:00 2001 From: pisuthd <pisuth.dae@gmail.com> Date: Wed, 5 Jun 2024 21:30:09 +0900 Subject: [PATCH 3/4] add legacy mock coins / base contracts for amm & lbp --- .../sources/utils/base_fungible_asset.move | 2 +- packages/aptos/sources/utils/mock_legato.move | 74 ++++ ...{mock_legato..move => mock_legato_fa.move} | 11 +- packages/aptos/sources/utils/mock_usdc.move | 80 ++-- .../aptos/sources/utils/mock_usdc_fa.move | 54 +++ packages/aptos/sources/utils/stable_math.move | 66 +++ .../aptos/sources/utils/vault_token_name.move | 44 ++ packages/aptos/sources/utils/weight_math.move | 385 ++++++++++++++++++ packages/sui/sources/amm.move | 4 +- 9 files changed, 682 insertions(+), 38 deletions(-) create mode 100644 packages/aptos/sources/utils/mock_legato.move rename packages/aptos/sources/utils/{mock_legato..move => mock_legato_fa.move} (90%) create mode 100644 packages/aptos/sources/utils/mock_usdc_fa.move create mode 100644 packages/aptos/sources/utils/stable_math.move create mode 100644 packages/aptos/sources/utils/vault_token_name.move create mode 100644 packages/aptos/sources/utils/weight_math.move diff --git a/packages/aptos/sources/utils/base_fungible_asset.move b/packages/aptos/sources/utils/base_fungible_asset.move index 9162ced..cc245ce 100644 --- a/packages/aptos/sources/utils/base_fungible_asset.move +++ b/packages/aptos/sources/utils/base_fungible_asset.move @@ -2,7 +2,7 @@ module legato_addr::base_fungible_asset { - use aptos_framework::fungible_asset::{Self, MintRef, TransferRef, BurnRef, Metadata, FungibleStore, FungibleAsset}; + use aptos_framework::fungible_asset::{Self, MintRef, TransferRef, BurnRef, Metadata, FungibleStore}; use aptos_framework::object::{Self, Object, ConstructorRef}; use aptos_framework::primary_fungible_store; use std::error; diff --git a/packages/aptos/sources/utils/mock_legato.move b/packages/aptos/sources/utils/mock_legato.move new file mode 100644 index 0000000..a07227c --- /dev/null +++ b/packages/aptos/sources/utils/mock_legato.move @@ -0,0 +1,74 @@ + +// Mock Legato coins + +module legato_addr::mock_legato { + + use std::signer; + use std::string::{Self, String }; + + use aptos_framework::account; + use aptos_framework::coin::{Self, Coin, MintCapability, BurnCapability}; + + const TOKEN_NAME: vector<u8> = b"Legato Token"; + + struct LEGATO_TOKEN has drop, store {} + + struct MockManager has key { + mint_cap: MintCapability<LEGATO_TOKEN>, + burn_cap: BurnCapability<LEGATO_TOKEN> + } + + fun init_module(sender: &signer) { + + let token_name = string::utf8(b"Legato Token"); + let token_symbol = string::utf8(b"LEGATO"); + + let (burn_cap, freeze_cap, mint_cap) = coin::initialize<LEGATO_TOKEN>(sender, token_name, token_symbol, 8, true); + coin::destroy_freeze_cap(freeze_cap); + + move_to( sender, MockManager { + mint_cap, + burn_cap + }); + } + + public entry fun mint(sender: &signer , amount: u64) acquires MockManager { + let mock_manager = borrow_global_mut<MockManager>(@legato_addr); + let coins = coin::mint<LEGATO_TOKEN>(amount, &mock_manager.mint_cap ); + + let sender_address = signer::address_of(sender); + + if (!coin::is_account_registered<LEGATO_TOKEN>(sender_address)) { + coin::register<LEGATO_TOKEN>(sender); + }; + coin::deposit(sender_address, coins); + } + + public entry fun burn(sender: &signer, amount: u64) acquires MockManager { + let mock_manager = borrow_global_mut<MockManager>(@legato_addr); + + let burn_coin = coin::withdraw<LEGATO_TOKEN>(sender, amount); + coin::burn(burn_coin, &mock_manager.burn_cap); + } + + // ======== Test-related Functions ========= + + #[test_only] + public fun init_module_for_testing(sender: &signer) { + init_module(sender) + } + + // #[test(deployer = @legato_addr, alice = @0x1234)] + // public fun test_basic_flow(deployer: &signer, alice: &signer) { + // init_module(deployer); + + // let alice_address = signer::address_of(alice); + + // account::create_account_for_test(alice_address); + + // mint(alice, 1000); + // assert!(coin::balance<LEGATO_TOKEN>(alice_address) == 1000 , 0); + + // } + +} \ No newline at end of file diff --git a/packages/aptos/sources/utils/mock_legato..move b/packages/aptos/sources/utils/mock_legato_fa.move similarity index 90% rename from packages/aptos/sources/utils/mock_legato..move rename to packages/aptos/sources/utils/mock_legato_fa.move index dc308d9..84f42fc 100644 --- a/packages/aptos/sources/utils/mock_legato..move +++ b/packages/aptos/sources/utils/mock_legato_fa.move @@ -1,10 +1,10 @@ // Mock Legato coins in fungible asset -module legato_addr::mock_legato { +module legato_addr::mock_legato_fa { use aptos_framework::object; - use aptos_framework::fungible_asset::{Self, Metadata, FungibleAsset}; + use aptos_framework::fungible_asset::{ Metadata}; use aptos_framework::object::Object; use legato_addr::base_fungible_asset; use std::string::utf8; @@ -67,4 +67,11 @@ module legato_addr::mock_legato { assert!(primary_fungible_store::balance(alice_address, metadata) == 0, 4); } + // ======== Test-related Functions ========= + + #[test_only] + public fun init_module_for_testing(deployer: &signer) { + init_module(deployer) + } + } \ No newline at end of file diff --git a/packages/aptos/sources/utils/mock_usdc.move b/packages/aptos/sources/utils/mock_usdc.move index 7e7cdb6..49d6711 100644 --- a/packages/aptos/sources/utils/mock_usdc.move +++ b/packages/aptos/sources/utils/mock_usdc.move @@ -1,47 +1,61 @@ - -// Mock USDC coins in fungible asset +// Mock Legato coins module legato_addr::mock_usdc { - use aptos_framework::object; - use aptos_framework::fungible_asset::{Self, Metadata, FungibleAsset}; - use aptos_framework::object::Object; - use legato_addr::base_fungible_asset; - use std::string::utf8; - - const ASSET_SYMBOL: vector<u8> = b"USDC"; - - /// Initialize metadata object and store the refs. - fun init_module(admin: &signer) { - let constructor_ref = &object::create_named_object(admin, ASSET_SYMBOL); - base_fungible_asset::initialize( - constructor_ref, - 0, /* maximum_supply. 0 means no maximum */ - utf8(b"Mock USDC Tokens"), /* name */ - utf8(ASSET_SYMBOL), /* symbol */ - 6, /* decimals */ - utf8(b"http://example.com/favicon.ico"), /* icon */ - utf8(b"http://example.com"), /* project */ - ); + use std::signer; + use std::string::{Self, String }; + + use aptos_framework::account; + use aptos_framework::coin::{Self, Coin, MintCapability, BurnCapability}; + + const TOKEN_NAME: vector<u8> = b"USDC Token"; + + struct USDC_TOKEN has drop, store {} + + struct MockManager has key { + mint_cap: MintCapability<USDC_TOKEN>, + burn_cap: BurnCapability<USDC_TOKEN> } - #[view] - /// Return the address of the metadata that's created when this module is deployed. - public fun get_metadata(): Object<Metadata> { - let metadata_address = object::create_object_address(&@legato_addr, ASSET_SYMBOL); - object::address_to_object<Metadata>(metadata_address) + fun init_module(sender: &signer) { + + let token_name = string::utf8(b"USDC Token"); + let token_symbol = string::utf8(b"USDC"); + + let (burn_cap, freeze_cap, mint_cap) = coin::initialize<USDC_TOKEN>(sender, token_name, token_symbol, 6, true); + coin::destroy_freeze_cap(freeze_cap); + + move_to( sender, MockManager { + mint_cap, + burn_cap + }); + } + + public entry fun mint(sender: &signer , amount: u64) acquires MockManager { + let mock_manager = borrow_global_mut<MockManager>(@legato_addr); + let coins = coin::mint<USDC_TOKEN>(amount, &mock_manager.mint_cap ); + + let sender_address = signer::address_of(sender); + + if (!coin::is_account_registered<USDC_TOKEN>(sender_address)) { + coin::register<USDC_TOKEN>(sender); + }; + coin::deposit(sender_address, coins); } - /// Mint as the owner of metadata object. - public entry fun mint( to: address, amount: u64) { - base_fungible_asset::mint_to_primary_stores( get_metadata(), vector[to], vector[amount]); + public entry fun burn(sender: &signer, amount: u64) acquires MockManager { + let mock_manager = borrow_global_mut<MockManager>(@legato_addr); + + let burn_coin = coin::withdraw<USDC_TOKEN>(sender, amount); + coin::burn(burn_coin, &mock_manager.burn_cap); } + // ======== Test-related Functions ========= - /// Burn fungible assets as the owner of metadata object. - public entry fun burn( from: address, amount: u64) { - base_fungible_asset::burn_from_primary_stores( get_metadata(), vector[from], vector[amount]); + #[test_only] + public fun init_module_for_testing(sender: &signer) { + init_module(sender) } } \ No newline at end of file diff --git a/packages/aptos/sources/utils/mock_usdc_fa.move b/packages/aptos/sources/utils/mock_usdc_fa.move new file mode 100644 index 0000000..0060e19 --- /dev/null +++ b/packages/aptos/sources/utils/mock_usdc_fa.move @@ -0,0 +1,54 @@ + + +// Mock USDC coins in fungible asset + +module legato_addr::mock_usdc_fa { + + use aptos_framework::object; + use aptos_framework::fungible_asset::{Self, Metadata}; + use aptos_framework::object::Object; + use legato_addr::base_fungible_asset; + use std::string::utf8; + + const ASSET_SYMBOL: vector<u8> = b"USDC"; + + /// Initialize metadata object and store the refs. + fun init_module(admin: &signer) { + let constructor_ref = &object::create_named_object(admin, ASSET_SYMBOL); + base_fungible_asset::initialize( + constructor_ref, + 0, /* maximum_supply. 0 means no maximum */ + utf8(b"Mock USDC Tokens"), /* name */ + utf8(ASSET_SYMBOL), /* symbol */ + 6, /* decimals */ + utf8(b"http://example.com/favicon.ico"), /* icon */ + utf8(b"http://example.com"), /* project */ + ); + } + + #[view] + /// Return the address of the metadata that's created when this module is deployed. + public fun get_metadata(): Object<Metadata> { + let metadata_address = object::create_object_address(&@legato_addr, ASSET_SYMBOL); + object::address_to_object<Metadata>(metadata_address) + } + + /// Mint as the owner of metadata object. + public entry fun mint( to: address, amount: u64) { + base_fungible_asset::mint_to_primary_stores( get_metadata(), vector[to], vector[amount]); + } + + + /// Burn fungible assets as the owner of metadata object. + public entry fun burn( from: address, amount: u64) { + base_fungible_asset::burn_from_primary_stores( get_metadata(), vector[from], vector[amount]); + } + + // ======== Test-related Functions ========= + + #[test_only] + public fun init_module_for_testing(deployer: &signer) { + init_module(deployer) + } + +} \ No newline at end of file diff --git a/packages/aptos/sources/utils/stable_math.move b/packages/aptos/sources/utils/stable_math.move new file mode 100644 index 0000000..536f7a2 --- /dev/null +++ b/packages/aptos/sources/utils/stable_math.move @@ -0,0 +1,66 @@ +/// Necessary library for setting up and calculating a stablecoin pool. +/// The pool must have equal weights and relies on the formula k = x^3 * y + x * y^3, borrowed from +/// https://github.com/aptos-labs/aptos-core/blob/main/aptos-move/move-examples/swap/sources/liquidity_pool.move + + +module legato_addr::stable_math { + + use aptos_std::math128; + + // Computes initial LP amount using the formula - total_share = sqrt( amount_x * amount_y ) + public fun compute_initial_lp( + amount_x: u64, + amount_y: u64 + ): u64 { + ( math128::sqrt(( amount_x as u128) * ( amount_y as u128) ) as u64 ) + } + + // Calculate the output amount using k = x^3 * y + x * y^3 + public fun get_amount_out( + amount_in: u64, + reserve_in: u64, + reserve_out: u64, + ) : u64 { + let k = calculate_constant_k((reserve_in as u256), (reserve_out as u256)); + (((reserve_out as u256) - get_y(((amount_in + reserve_in) as u256), k, (reserve_out as u256))) as u64) + } + + fun calculate_constant_k(r1: u256, r2: u256): u256 { + (r1 * r1 * r1 * r2 + r2 * r2 * r2 * r1) + } + + fun get_y(x0: u256, xy: u256, y: u256): u256 { + let i = 0; + while (i < 255) { + let y_prev = y; + let k = f(x0, y); + if (k < xy) { + let dy = (xy - k) / d(x0, y); + y = y + dy; + } else { + let dy = (k - xy) / d(x0, y); + y = y - dy; + }; + if (y > y_prev) { + if (y - y_prev <= 1) { + return y + } + } else { + if (y_prev - y <= 1) { + return y + } + }; + i = i + 1; + }; + y + } + + fun f(x0: u256, y: u256): u256 { + x0 * (y * y * y) + (x0 * x0 * x0) * y + } + + fun d(x0: u256, y: u256): u256 { + 3 * x0 * (y * y) + (x0 * x0 * x0) + } + +} \ No newline at end of file diff --git a/packages/aptos/sources/utils/vault_token_name.move b/packages/aptos/sources/utils/vault_token_name.move new file mode 100644 index 0000000..cd6eb11 --- /dev/null +++ b/packages/aptos/sources/utils/vault_token_name.move @@ -0,0 +1,44 @@ + + + +// Each vault has it owned tokens and follows a quarterly expiration + +module legato_addr::vault_token_name { + + // Define structs for March, June, September, and December from 2024 to 2030 + struct MAR_2024 has drop {} // March 2024 + struct JUN_2024 has drop {} // June 2024 + struct SEP_2024 has drop {} // September 2024 + struct DEC_2024 has drop {} // December 2024 + + struct MAR_2025 has drop {} // March 2025 + struct JUN_2025 has drop {} // June 2025 + struct SEP_2025 has drop {} // September 2025 + struct DEC_2025 has drop {} // December 2025 + + struct MAR_2026 has drop {} // March 2026 + struct JUN_2026 has drop {} // June 2026 + struct SEP_2026 has drop {} // September 2026 + struct DEC_2026 has drop {} // December 2026 + + struct MAR_2027 has drop {} // March 2027 + struct JUN_2027 has drop {} // June 2027 + struct SEP_2027 has drop {} // September 2027 + struct DEC_2027 has drop {} // December 2027 + + struct MAR_2028 has drop {} // March 2028 + struct JUN_2028 has drop {} // June 2028 + struct SEP_2028 has drop {} // September 2028 + struct DEC_2028 has drop {} // December 2028 + + struct MAR_2029 has drop {} // March 2029 + struct JUN_2029 has drop {} // June 2029 + struct SEP_2029 has drop {} // September 2029 + struct DEC_2029 has drop {} // December 2029 + + struct MAR_2030 has drop {} // March 2030 + struct JUN_2030 has drop {} // June 2030 + struct SEP_2030 has drop {} // September 2030 + struct DEC_2030 has drop {} // December 2030 + +} \ No newline at end of file diff --git a/packages/aptos/sources/utils/weight_math.move b/packages/aptos/sources/utils/weight_math.move new file mode 100644 index 0000000..a781902 --- /dev/null +++ b/packages/aptos/sources/utils/weight_math.move @@ -0,0 +1,385 @@ + + +// Module for weighted math operations, the formulas are borrowed from Balancer V2 Lite project. +// https://github.com/icmoore/balancer-v2-lite + + +module legato_addr::weighted_math { + + use aptos_std::fixed_point64::{Self, FixedPoint64}; + use aptos_std::math_fixed64; + + const WEIGHT_SCALE: u64 = 10000; + + const LOG_2_E: u128 = 26613026195707766742; + + // Maximum values for u64 and u128 + const MAX_U64: u128 = 18446744073709551615; + const MAX_U128: u256 = 340282366920938463463374607431768211455; + + const ERR_INCORRECT_SWAP: u64 = 1; + + // Computes the optimal value for adding liquidity + public fun compute_optimal_value( + amount_out: u64, + reserve_in: u64, + weight_in: u64, + reserve_out: u64, + weight_out: u64, + ) : u64 { + + get_amount_in( + amount_out, + reserve_in, + weight_in, + reserve_out, + weight_out + ) + } + + // Calculate the output amount according to the pool weight + // - amountOut = balanceOut * (1 - ((balanceIn / (balanceIn + amountIn)) ^ (wI / wO))) + public fun get_amount_out( + amount_in: u64, + reserve_in: u64, + weight_in: u64, + reserve_out: u64, + weight_out: u64, + ) : u64 { + + // Scale the amount to adjust for the provided scaling factor of the asset + let amount_in_after_scaled = (amount_in as u128); + let reserve_in_after_scaled = (reserve_in as u128); + let reserve_out_after_scaled = (reserve_out as u128); + + if (weight_in == weight_out) { + let denominator = reserve_in_after_scaled+amount_in_after_scaled; + let base = fixed_point64::create_from_rational(reserve_in_after_scaled , denominator); + let amount_out = fixed_point64::multiply_u128( reserve_out_after_scaled, fixed_point64::sub(fixed_point64::create_from_u128(1), base ) ); + + (amount_out as u64) + } else { + + let denominator = reserve_in_after_scaled+amount_in_after_scaled; + let base = fixed_point64::create_from_rational(reserve_in_after_scaled , denominator); + let exponent = fixed_point64::create_from_rational((weight_in as u128), (weight_out as u128)); + + let power = power(base, exponent); + let amount_out = fixed_point64::multiply_u128( reserve_out_after_scaled , fixed_point64::sub( fixed_point64::create_from_u128(1), power ) ); + + (amount_out as u64) + } + + } + + // Calculate the output amount according to the pool weight + // - amountIn = balanceIn * ((( balanceOut / (balanceOut - amountOut) ) ^ (wO/wI))-1) + public fun get_amount_in( + amount_out: u64, + reserve_in: u64, + weight_in: u64, + reserve_out: u64, + weight_out: u64, + ) : u64 { + + let amount_out_after_scaled = (amount_out as u128); + let reserve_in_after_scaled = (reserve_in as u128); + let reserve_out_after_scaled = (reserve_out as u128); + + if (weight_in == weight_out) { + let denominator = reserve_out_after_scaled-amount_out_after_scaled; + let base = fixed_point64::create_from_rational(reserve_out_after_scaled , denominator); + let amount_out = fixed_point64::multiply_u128( reserve_in_after_scaled, fixed_point64::sub(base, fixed_point64::create_from_u128(1)) ); + + (amount_out as u64) + } else { + + let denominator = reserve_out_after_scaled-amount_out_after_scaled; + let base = fixed_point64::create_from_rational(reserve_out_after_scaled , denominator); + let exponent = fixed_point64::create_from_rational((weight_out as u128), (weight_in as u128)); + + let power = power(base, exponent); + let amount_out = fixed_point64::multiply_u128( reserve_in_after_scaled, fixed_point64::sub(power, fixed_point64::create_from_u128(1)) ); + + (amount_out as u64) + } + + } + + // Computes initial LP amount using the formula - total_share = (amount_x^weight_x) * (amount_y^weight_y) + public fun compute_initial_lp( + weight_x: u64, + weight_y: u64, + amount_x: u64, + amount_y: u64 + ): u64 { + + let amount_x_after_weighted = power( fixed_point64::create_from_u128( (amount_x as u128)), fixed_point64::create_from_rational( (weight_x as u128), (WEIGHT_SCALE as u128) )); + let amount_y_after_weighted = power( fixed_point64::create_from_u128( (amount_y as u128)), fixed_point64::create_from_rational( (weight_y as u128), (WEIGHT_SCALE as u128) )); + + let sum = math_fixed64::mul_div( amount_x_after_weighted, amount_y_after_weighted, fixed_point64::create_from_u128(1) ); + + (fixed_point64::round( sum ) as u64) + } + + // Computes LP when it's set + public fun compute_derive_lp( + amount_x: u64, + amount_y: u64, + weight_x: u64, + weight_y: u64, + reserve_x: u64, + reserve_y: u64, + lp_supply: u64 + ): (u64) { + + let for_x = token_for_lp(amount_x, reserve_x, weight_x, lp_supply ); + let for_y = token_for_lp(amount_y, reserve_y, weight_y, lp_supply ); + + (fixed_point64::round( fixed_point64::add(for_x, for_y ) ) as u64) + } + + // Computes coins to be sent out when withdrawing liquidity + public fun compute_withdrawn_coins( + lp_amount: u64, + lp_supply: u64, + reserve_x: u64, + reserve_y: u64, + weight_x: u64, + weight_y: u64 + ): (u64, u64) { + + let amount_x = lp_for_token( (lp_amount/2 as u128) , lp_supply, reserve_x, weight_x ); + let amount_y = lp_for_token( (lp_amount/2 as u128) , lp_supply, reserve_y, weight_y ); + + ((amount_x as u64),(amount_y as u64)) + } + + // Calculates the amount of output coins to receive from a given LP amount + // - output_amount = reserve * (1 - ((lp_supply - lp_amount) / lp_supply) ^ (1 / weight)) + public fun lp_for_token( + lp_amount: u128, + lp_supply: u64, + reserve: u64, + weight: u64 + ) : u64 { + + let base = fixed_point64::create_from_rational( ((lp_supply-(lp_amount as u64)) as u128), (lp_supply as u128) ); + let power = power(base, fixed_point64::create_from_rational( (WEIGHT_SCALE as u128), (weight as u128) ) ); + + ( (fixed_point64::multiply_u128( (reserve as u128) , fixed_point64::sub( fixed_point64::create_from_u128(1), power ) )) as u64) + } + + // Calculates the amount of LP tokens to receive from a given coins. + // - lp_out = lp_supply * ((reserve + amount) / reserve) ^ (weight / WEIGHT_SCALE) - 1 + public fun token_for_lp( + amount: u64, + reserve: u64, + weight: u64, + lp_supply: u64 + ) : FixedPoint64 { + + let base = fixed_point64::create_from_rational( ( (reserve+amount) as u128 ), (reserve as u128) ); + let power = power(base, fixed_point64::create_from_rational( (weight as u128), (WEIGHT_SCALE as u128) ) ); + + // fixed_point64::multiply_u128( (lp_supply as u128) , fixed_point64::sub( power, fixed_point64::create_from_u128(1) ) ) + math_fixed64::mul_div( fixed_point64::create_from_u128( (lp_supply as u128) ), fixed_point64::sub( power, fixed_point64::create_from_u128(1) ), fixed_point64::create_from_u128(1) ) + } + + // Helper function to calculate the power of a FixedPoint64 number to a FixedPoint64 exponent + // - When `n` is > 1, it uses the formula `exp(y * ln(x))` instead of `x^y`. + // - When `n` is < 1, it employs the Newton-Raphson method. + public fun power(n: FixedPoint64, e: FixedPoint64) : FixedPoint64 { + // Check if the exponent is 0, return 1 if it is + if (fixed_point64::equal(e, fixed_point64::create_from_u128(0)) ) { + fixed_point64::create_from_u128(1) + } else if (fixed_point64::equal(e, fixed_point64::create_from_u128(1))) { + // If the exponent is 1, return the base value n + n + } else if (fixed_point64::less(n, fixed_point64::create_from_u128(1))) { + + // Split the exponent into integer and fractional parts + let integerPart = fixed_point64::floor( e ); + let fractionalPart = fixed_point64::sub(e, fixed_point64::create_from_u128(integerPart)); + + // Calculate the integer power using math_fixed64 power function + let result = math_fixed64::pow( n, (integerPart as u64) ); + + if ( fixed_point64::equal( fractionalPart, fixed_point64::create_from_u128(0) ) ) { + // If the fractional part is zero, return the integer result + result + } else { + // Calculate the fractional using internal nth root function + let nth = math_fixed64::mul_div( fixed_point64::create_from_u128(1), fixed_point64::create_from_u128(1), fractionalPart ); + + let nth_rounded = fixed_point64::round(nth); + + let fractionalResult = nth_root( n , (nth_rounded as u64) ); + + // Combine the integer and fractional powers using multiplication + math_fixed64::mul_div( result, fractionalResult, fixed_point64::create_from_u128(1) ) + } + + } else { + + // Calculate ln(n) times e + let ln_x_times_y = math_fixed64::mul_div( e , ln(n), fixed_point64::create_from_u128(1) ); + // Compute exp(ln(x) * y) to get the result of x^y + math_fixed64::exp(ln_x_times_y) + } + + } + + // Helper function to approximate the n-th root of a number using the Newton-Raphson method when x < 1. + public fun nth_root( x: FixedPoint64, n: u64): FixedPoint64 { + if ( n == 0 ) { + fixed_point64::create_from_u128(1) + } else { + + // Initialize guess + let guess = fixed_point64::create_from_rational(1, 2); + + // Define the epsilon value for determining convergence + let epsilon = fixed_point64::create_from_rational( 1, 1000 ); + + let delta = fixed_point64::create_from_rational( MAX_U64, 1 ); + + // Perform Newton-Raphson iterations until convergence + while ( fixed_point64::greater( delta , epsilon )) { + + let xn = pow_raw( guess, n); + let derivative = math_fixed64::mul_div( fixed_point64::create_from_u128( (n as u128)), pow_raw( guess, n-1), fixed_point64::create_from_u128(1) ); + + if (fixed_point64::greater_or_equal(xn, x)) { + delta = math_fixed64::mul_div( fixed_point64::sub(xn, x) , fixed_point64::create_from_u128(1), derivative); + guess = fixed_point64::sub(guess, delta); + } else { + delta = math_fixed64::mul_div( fixed_point64::sub(x, xn) , fixed_point64::create_from_u128(1), derivative); + guess = fixed_point64::add(guess, delta); + }; + + }; + // Return the final approximation of the n-th root + guess + } + } + + // Function to calculate the power of a FixedPoint64 number + public fun pow_raw(x: FixedPoint64, n: u64): FixedPoint64 { + // Get the raw value of x as a 256-bit unsigned integer + let raw_value = (fixed_point64::get_raw_value(x) as u256); + + let res: u256 = 1 << 64; + + // Perform exponentiation using bitwise operations + while (n != 0) { + if (n & 1 != 0) { + res = (res * raw_value) >> 64; + }; + n = n >> 1; + if ( raw_value <= MAX_U128 ) { + raw_value = (raw_value * raw_value) >> 64; + } else { + raw_value = (raw_value >> 32) * (raw_value >> 32); + }; + }; + + fixed_point64::create_from_raw_value((res as u128)) + } + + // Calculate the natural logarithm of the input using FixedPoint64 + public fun ln(input : FixedPoint64) : FixedPoint64 { + // Define the constant log_2(e) + let log_2_e = fixed_point64::create_from_raw_value( LOG_2_E ); + + // Calculate the base-2 logarithm of the input + let after_log2 = (math_fixed64::log2_plus_64( input )); + + let fixed_2 = fixed_point64::create_from_u128(64); + + // Subtract 64 to adjust the result back + let (after_subtracted, _) = absolute( after_log2, fixed_2 ); + math_fixed64::mul_div( after_subtracted, fixed_point64::create_from_u128(1) , log_2_e) + } + + fun absolute( a: FixedPoint64, b: FixedPoint64 ) : (FixedPoint64, bool) { + if (fixed_point64::greater_or_equal(a, b)) { + (fixed_point64::sub(a, b), false) + } else { + (fixed_point64::sub(b, a), true) + } + } + + // calculate fee to treasury + public fun get_fee_to_treasury(current_fee: FixedPoint64, input: u64): (u64,u64) { + let fee = (fixed_point64::multiply_u128( (input as u128) , current_fee) as u64); + return ( input-fee,fee) + } + + #[test] + public fun test_ln() { + + let output_1 = ln( fixed_point64::create_from_u128(10) ); + assert!( fixed_point64::almost_equal( output_1, fixed_point64::create_from_rational( 230258509299, 100000000000 ), fixed_point64::create_from_u128(1)) , 0 ); // 2.30258509299 + + let output_2 = ln( fixed_point64::create_from_u128(100) ); + assert!( fixed_point64::almost_equal( output_2, fixed_point64::create_from_rational( 460517018599 , 100000000000 ), fixed_point64::create_from_u128(1)) , 1 ); // 4.60517018599 + + let output_3 = ln( fixed_point64::create_from_u128(500) ); + assert!( fixed_point64::almost_equal( output_3, fixed_point64::create_from_rational( 621460809842 , 100000000000 ), fixed_point64::create_from_u128(1)) , 2 ); // 6.21460809842 + + // return absolute value when input < 1 + let output_4 = ln( fixed_point64::create_from_rational(1, 2) ); + assert!( fixed_point64::almost_equal( output_4, fixed_point64::create_from_rational( 693147181 , 1000000000 ), fixed_point64::create_from_u128(1)) , 2 ); // 0.693147181 + + } + + #[test] + public fun test_power() { + + // Asserts that 2^3 = 8 + let output_1 = power( fixed_point64::create_from_u128(2), fixed_point64::create_from_u128(3) ); + assert!( fixed_point64::round(output_1) == 8, 0 ); + + // Asserts that 200^3 = 8000000 + let output_2 = power( fixed_point64::create_from_u128(200), fixed_point64::create_from_u128(3) ); + assert!( fixed_point64::round(output_2) == 8000000, 1 ); + + // Asserts that 30^5 = 24300000 + let output_3 = power( fixed_point64::create_from_u128(30), fixed_point64::create_from_u128(5) ); + assert!( fixed_point64::round(output_3) == 24300000, 2 ); // 30^5 = 24300000 + + // tests for nth-root calculations + + // Asserts that the square root of 16 is approximately 4. + let n_output_1 = power( fixed_point64::create_from_u128(16), fixed_point64::create_from_rational(1, 2 ) ); + assert!( fixed_point64::almost_equal( n_output_1, fixed_point64::create_from_rational( 4, 1 ), fixed_point64::create_from_u128(1)) , 3 ); + // Asserts that the fifth root of 625 is approximately 3.623. + let n_output_2 = power( fixed_point64::create_from_u128(625), fixed_point64::create_from_rational(1, 5 ) ); + assert!( fixed_point64::almost_equal( n_output_2, fixed_point64::create_from_rational( 3623, 1000 ), fixed_point64::create_from_u128(1)) , 4 ); + // Asserts that the cube root of 1000 is approximately 9.999999977. + let n_output_3 = power( fixed_point64::create_from_u128(1000), fixed_point64::create_from_rational(1, 3 ) ); + assert!( fixed_point64::almost_equal( n_output_3, fixed_point64::create_from_rational( 9999, 1000 ), fixed_point64::create_from_u128(1)) , 5 ); + // Asserts that the cube root of 729 is approximately 8.99999998. + let n_output_4 = power( fixed_point64::create_from_u128(729), fixed_point64::create_from_rational(1, 3 ) ); + assert!( fixed_point64::almost_equal( n_output_4, fixed_point64::create_from_rational( 8999, 1000 ), fixed_point64::create_from_u128(1)) , 6 ); + + // Asserts that the fourth root of 9/16 is approximately 0.866025404. + let n_output_5 = power( fixed_point64::create_from_rational( 9, 16 ), fixed_point64::create_from_rational( 1, 4 ) ); + assert!( fixed_point64::almost_equal( n_output_5, fixed_point64::create_from_rational( 866025404, 1000000000 ), fixed_point64::create_from_u128(1)) , 7 ); // 0.866025404 + + // Asserts that the tenth root of 1/2 is approximately 0.420448208. + let n_output_6 = power( fixed_point64::create_from_rational( 1, 2 ), fixed_point64::create_from_rational( 10, 8 ) ); + assert!( fixed_point64::almost_equal( n_output_6, fixed_point64::create_from_rational( 420448208, 1000000000 ), fixed_point64::create_from_u128(1)) , 8 ); // 0.420448208 + + // Asserts that the fifth root of 2/5 is approximately 0.01024. + let n_output_7 = power( fixed_point64::create_from_rational( 2, 5 ), fixed_point64::create_from_rational( 5, 1 ) ); + assert!( fixed_point64::almost_equal( n_output_7, fixed_point64::create_from_rational( 1024, 100000 ), fixed_point64::create_from_u128(1)) , 9 ); // 0.01024 + + // Asserts that the ninth root of 3/5 is approximately 0.566896603. + let n_output_8 = power( fixed_point64::create_from_rational( 3, 5 ), fixed_point64::create_from_rational( 10, 9 ) ); + assert!( fixed_point64::almost_equal( n_output_8, fixed_point64::create_from_rational( 566896603, 1000000000 ), fixed_point64::create_from_u128(1)) , 10 ); // 0.566896603 + + } + +} \ No newline at end of file diff --git a/packages/sui/sources/amm.move b/packages/sui/sources/amm.move index bf95e57..e8e2777 100644 --- a/packages/sui/sources/amm.move +++ b/packages/sui/sources/amm.move @@ -470,7 +470,7 @@ module legato::amm { proj_on_x: bool, // Indicates whether the project token is on the X or Y side start_weight: u64, // Initial weight of the project token. final_weight: u64, // The weight when the pool is stabilized. - is_vault: bool, // Determines the quota unit: false - by settlement collected, true - by staking rewards. + is_vault: bool, // false - only common coins, true - coins+staking rewards. target_amount: u64, // The target amount required to fully shift the weight. ctx: &mut TxContext ) { @@ -1161,7 +1161,7 @@ module legato::amm { lbp::set_new_target_amount( params, new_target_amount ); } - // Set a new target amount for LBP + // Enable/Disable buy with pair or with vault tokens public entry fun lbp_enable_buy_with_pair_and_vault<Y>(global: &mut AMMGlobal, _manager_cap: &mut ManagerCap, enable_pair: bool, enable_vault: bool) { assert!( has_registered<SUI, Y>(global) , ERR_NOT_REGISTERED); From 87f72ef0df53b59e62fddd545ef0271526b4196a Mon Sep 17 00:00:00 2001 From: pisuthd <pisuth.dae@gmail.com> Date: Thu, 6 Jun 2024 14:36:54 +0900 Subject: [PATCH 4/4] Legato Aptos main contracts --- packages/aptos/.gitignore | 3 +- packages/aptos/sources/amm.move | 1018 +++++++++++++++++ packages/aptos/sources/lbp.move | 177 +++ packages/aptos/sources/utils/mock_legato.move | 7 +- .../aptos/sources/utils/mock_legato_fa.move | 1 + packages/aptos/sources/utils/mock_usdc.move | 5 +- .../aptos/sources/utils/mock_usdc_fa.move | 2 +- packages/aptos/sources/vault.move | 547 +++++++++ packages/aptos/tests/amm_tests.move | 187 +++ .../aptos_system/delegation_pool_tests.move | 205 ++++ packages/aptos/tests/lbp_usdc_tests.move | 108 ++ packages/aptos/tests/vault_tests.move | 296 +++++ 12 files changed, 2547 insertions(+), 9 deletions(-) create mode 100644 packages/aptos/sources/amm.move create mode 100644 packages/aptos/sources/lbp.move create mode 100644 packages/aptos/tests/amm_tests.move create mode 100644 packages/aptos/tests/aptos_system/delegation_pool_tests.move create mode 100644 packages/aptos/tests/lbp_usdc_tests.move create mode 100644 packages/aptos/tests/vault_tests.move diff --git a/packages/aptos/.gitignore b/packages/aptos/.gitignore index 243f545..599abe9 100644 --- a/packages/aptos/.gitignore +++ b/packages/aptos/.gitignore @@ -1,2 +1,3 @@ .aptos/ -build/ \ No newline at end of file +build/ +old/ \ No newline at end of file diff --git a/packages/aptos/sources/amm.move b/packages/aptos/sources/amm.move new file mode 100644 index 0000000..d1c0832 --- /dev/null +++ b/packages/aptos/sources/amm.move @@ -0,0 +1,1018 @@ +// Copyright (c) Tamago Blockchain Labs, Inc. +// SPDX-License-Identifier: MIT + +// A custom weight DEX for trading tokens to tokens including vault tokens +// Allows setup of various types of pools - weighted pool, stable pool and LBP pool +// Forked from OmniBTC AMM Swap and improved with math from Balancer V2 Lite +// Supports only legacy coins for now + +module legato_addr::amm { + + use std::option::{Self, Option}; + use std::signer; + use std::vector; + use std::bcs; + use std::string::{Self, String }; + + use aptos_framework::event; + use aptos_framework::coin::{Self, Coin, MintCapability, BurnCapability}; + use aptos_framework::object::{Self, ExtendRef, Object, ConstructorRef }; + use aptos_std::smart_vector::{Self, SmartVector}; + use aptos_std::comparator::{Self, Result}; + use aptos_std::type_info; + use aptos_std::fixed_point64::{Self, FixedPoint64}; + + use legato_addr::weighted_math; + use legato_addr::stable_math; + use legato_addr::lbp::{Self, LBPParams}; + + // ======== Constants ======== + + // Default swap fee of 0.5% in fixed-point + const DEFAULT_FEE: u128 = 92233720368547758; + // 0.25% for LBP + const LBP_FEE: u128 = 46116860184273879; + // 0.1% for stable pools + const STABLE_FEE: u128 = 18446744073709551; + // Minimal liquidity. + const MINIMAL_LIQUIDITY: u64 = 1000; + // Max u64 value. + const U64_MAX: u64 = 18446744073709551615; + + const WEIGHT_SCALE: u64 = 10000; + + const SYMBOL_PREFIX_LENGTH: u64 = 4; + + const LP_TOKEN_DECIMALS: u8 = 8; + // The max value that can be held in one of the Balances of + /// a Pool. U64 MAX / WEIGHT_SCALE + const MAX_POOL_VALUE : u64 = 18446744073709551615; + + // ======== Errors ======== + + const ERR_NOT_COIN: u64 = 101; + const ERR_THE_SAME_COIN: u64 = 102; + const ERR_WEIGHTS_SUM: u64 = 103; + const ERR_UNAUTHORIZED: u64 = 104; + const ERR_MUST_BE_ORDER: u64 = 105; + const ERR_POOL_HAS_REGISTERED: u64 = 106; + const ERR_INVALID_ADDRESS: u64 = 107; + const ERR_POOL_NOT_REGISTER: u64 = 108; + const ERR_NOT_LBP: u64 = 109; + const ERR_PAUSED: u64 = 110; + const ERR_POOL_EXISTS: u64 = 111; + const ERR_COIN_OUT_NUM_LESS_THAN_EXPECTED_MINIMUM: u64 = 112; + const ERR_INSUFFICIENT_COIN_X: u64 = 113; + const ERR_INSUFFICIENT_COIN_Y: u64 = 114; + const ERR_OVERLIMIT: u64 = 115; + const ERR_U64_OVERFLOW: u64 = 116; + const ERR_LIQUID_NOT_ENOUGH: u64 = 117; + const ERR_POOL_FULL: u64 = 118; + + + // ======== Structs ========= + /// The Pool token that will be used to mark the pool share + /// of a liquidity provider. The parameter `X` and `Y` is for the + /// coin held in the pool. + struct LP<phantom X, phantom Y> has drop, store {} + + // Liquidity pool with custom weighting + struct Pool<phantom X, phantom Y> has key, store { + coin_x: Coin<X>, + coin_y: Coin<Y>, + weight_x: u64, // 50% using 5000 + weight_y: u64, // 50% using 5000 + min_liquidity: Coin<LP<X, Y>>, + swap_fee: FixedPoint64, + lp_mint: MintCapability<LP<X, Y>>, + lp_burn: BurnCapability<LP<X, Y>>, + lbp_params: Option<LBPParams>, // Params for a LBP pool + has_paused: bool, + is_stable: bool, // Indicates if the pool is a stable pool + is_lbp: bool, // Indicates if the pool is a LBP + } + + // Represents the global state of the AMM. + #[resource_group_member(group = aptos_framework::object::ObjectGroup)] + struct AMMManager has key { + pool_list: SmartVector<String>, // all pools in the system + whitelist: SmartVector<address>, // who can setup a new pool + extend_ref: ExtendRef, + enable_whitelist: bool, + treasury_address: address // where all fees from all pools will be sent for further LP staking + } + + #[event] + /// Event emitted when a pool is created. + struct RegisterPool has drop, store { + pool_name: String, + coin_x: String, + coin_y: String, + weight_x: u64, + weight_y: u64, + is_stable: bool, + is_lbp: bool + } + + #[event] + struct Swapped has drop, store { + pool_name: String, + coin_in: String, + coin_out: String, + amount_in: u64, + amount_out: u64 + } + + #[event] + struct AddedLiquidity has drop, store { + pool_name: String, + coin_x: String, + coin_y: String, + coin_x_in: u64, + coin_y_in: u64, + lp_out: u64 + } + + #[event] + struct RemovedLiquidity has drop, store { + pool_name: String, + coin_x: String, + coin_y: String, + lp_in: u64, + coin_x_out: u64, + coin_y_out: u64 + } + + // Constructor for this module. + fun init_module(sender: &signer) { + + let constructor_ref = object::create_object(signer::address_of(sender)); + let extend_ref = object::generate_extend_ref(&constructor_ref); + + let whitelist = smart_vector::new(); + smart_vector::push_back(&mut whitelist, signer::address_of(sender)); + + move_to(sender, AMMManager { + whitelist, + pool_list: smart_vector::new(), + extend_ref, + enable_whitelist: true, + treasury_address: signer::address_of(sender) + }); + } + + // ======== Entry Points ========= + + /// Entry point for the `swap` method. + /// Sends swapped Coin to the sender. + public entry fun swap<X,Y>(sender: &signer, coin_in: u64, coin_out_min: u64) acquires AMMManager, Pool { + let is_order = is_order<X, Y>(); + assert!(coin::is_coin_initialized<X>(), ERR_NOT_COIN); + assert!(coin::is_coin_initialized<Y>(), ERR_NOT_COIN); + + if (is_order) { + let (reserve_x, reserve_y) = get_reserves_size<X, Y>(); + swap_out_y<X, Y>(sender, coin_in, coin_out_min, reserve_x, reserve_y); + } else { + let (reserve_y, reserve_x) = get_reserves_size<Y, X>(); + swap_out_x<Y, X>(sender, coin_in, coin_out_min, reserve_x, reserve_y); + }; + } + + // Register a new liquidity pool with custom weights + public entry fun register_pool<X, Y>( + sender: &signer, + weight_x: u64, + weight_y: u64 + ) acquires AMMManager { + let is_order = is_order<X, Y>(); + if (!is_order) { + register_pool<Y,X>(sender, weight_y, weight_x); + } else { + assert!(coin::is_coin_initialized<X>(), ERR_NOT_COIN); + assert!(coin::is_coin_initialized<Y>(), ERR_NOT_COIN); + + let config = borrow_global_mut<AMMManager>(@legato_addr); + let config_object_signer = object::generate_signer_for_extending(&config.extend_ref); + if (config.enable_whitelist) { + // Ensure that the caller is on the whitelist + assert!( smart_vector::contains(&config.whitelist, &(signer::address_of(sender))) , ERR_UNAUTHORIZED); + }; + + let (lp_name, lp_symbol) = generate_lp_name_and_symbol<X, Y>(); + + assert!( !smart_vector::contains(&config.pool_list, &lp_name) , ERR_POOL_HAS_REGISTERED); + + let (lp_burn_cap, lp_freeze_cap, lp_mint_cap) = coin::initialize<LP<X, Y>>(sender, lp_name, lp_symbol, 8, true); + coin::destroy_freeze_cap(lp_freeze_cap); + + // Registers X and Y if not already registered + if (!coin::is_account_registered<X>(signer::address_of(&config_object_signer))) { + coin::register<X>(&config_object_signer) + }; + + if (!coin::is_account_registered<Y>(signer::address_of(&config_object_signer))) { + coin::register<Y>(&config_object_signer) + }; + + let pool = init_pool_params<X,Y>(weight_x, weight_y, fixed_point64::create_from_raw_value( DEFAULT_FEE ), lp_mint_cap, lp_burn_cap, false, false, option::none() ); + + move_to(&config_object_signer, pool); + + smart_vector::push_back(&mut config.pool_list, lp_name); + + // Emit an event + event::emit(RegisterPool { pool_name: lp_name, coin_x : coin::symbol<X>(), coin_y : coin::symbol<Y>(), weight_x, weight_y, is_stable: false, is_lbp: false }); + }; + } + + // Register a stable pool, weights are fixed at 50/50 + public entry fun register_stable_pool<X,Y>(sender: &signer) acquires AMMManager { + let is_order = is_order<X, Y>(); + if (!is_order) { + register_stable_pool<Y,X>(sender ); + } else { + assert!(coin::is_coin_initialized<X>(), ERR_NOT_COIN); + assert!(coin::is_coin_initialized<Y>(), ERR_NOT_COIN); + + let config = borrow_global_mut<AMMManager>(@legato_addr); + let config_object_signer = object::generate_signer_for_extending(&config.extend_ref); + if (config.enable_whitelist) { + // Ensure that the caller is on the whitelist + assert!( smart_vector::contains(&config.whitelist, &(signer::address_of(sender))) , ERR_UNAUTHORIZED); + }; + + let (lp_name, lp_symbol) = generate_lp_name_and_symbol<X, Y>(); + + assert!( !smart_vector::contains(&config.pool_list, &lp_name) , ERR_POOL_HAS_REGISTERED); + + let (lp_burn_cap, lp_freeze_cap, lp_mint_cap) = coin::initialize<LP<X, Y>>(sender, lp_name, lp_symbol, 8, true); + coin::destroy_freeze_cap(lp_freeze_cap); + + // Registers X and Y if not already registered + if (!coin::is_account_registered<X>(signer::address_of(&config_object_signer))) { + coin::register<X>(&config_object_signer) + }; + + if (!coin::is_account_registered<Y>(signer::address_of(&config_object_signer))) { + coin::register<Y>(&config_object_signer) + }; + + let pool = init_pool_params<X,Y>(5000, 5000, fixed_point64::create_from_raw_value( STABLE_FEE ), lp_mint_cap, lp_burn_cap, true, false, option::none() ); + + move_to(&config_object_signer, pool); + + smart_vector::push_back(&mut config.pool_list, lp_name); + + // Emit an event + event::emit(RegisterPool { pool_name: lp_name, coin_x : coin::symbol<X>(), coin_y : coin::symbol<Y>(), weight_x: 5000, weight_y: 5000, is_stable: true, is_lbp: false }); + }; + + } + + // Register an LBP pool, project token weights must be greater than 50%. + // is_vault specifies if staking rewards from Legato Vault are accepted + public entry fun register_lbp_pool<X,Y>( + sender: &signer, + proj_on_x: bool, // Indicates whether the project token is on the X or Y side + start_weight: u64, // Initial weight of the project token. + final_weight: u64, // The weight when the pool is stabilized. + is_vault: bool, // false - only common coins, true - coins+staking rewards. + target_amount: u64, // The target amount required to fully shift the weight. + ) acquires AMMManager { + + let is_order = is_order<X, Y>(); + assert!(is_order, ERR_MUST_BE_ORDER); + + assert!(coin::is_coin_initialized<X>(), ERR_NOT_COIN); + assert!(coin::is_coin_initialized<Y>(), ERR_NOT_COIN); + + let config = borrow_global_mut<AMMManager>(@legato_addr); + let config_object_signer = object::generate_signer_for_extending(&config.extend_ref); + if (config.enable_whitelist) { + // Ensure that the caller is on the whitelist + assert!( smart_vector::contains(&config.whitelist, &(signer::address_of(sender))) , ERR_UNAUTHORIZED); + }; + + let (lp_name, lp_symbol) = generate_lp_name_and_symbol<X, Y>(); + + assert!( !smart_vector::contains(&config.pool_list, &lp_name) , ERR_POOL_HAS_REGISTERED); + + let (lp_burn_cap, lp_freeze_cap, lp_mint_cap) = coin::initialize<LP<X, Y>>(sender, lp_name, lp_symbol, 8, true); + coin::destroy_freeze_cap(lp_freeze_cap); + + // Registers X and Y if not already registered + if (!coin::is_account_registered<X>(signer::address_of(&config_object_signer))) { + coin::register<X>(&config_object_signer) + }; + + if (!coin::is_account_registered<Y>(signer::address_of(&config_object_signer))) { + coin::register<Y>(&config_object_signer) + }; + + let params = lbp::construct_init_params( + proj_on_x, + start_weight, + final_weight, + is_vault, + target_amount + ); + + let pool = init_pool_params<X,Y>(0, 0, fixed_point64::create_from_raw_value( LBP_FEE ), lp_mint_cap, lp_burn_cap, false, true, option::some<LBPParams>(params) ); + + move_to(&config_object_signer, pool); + + smart_vector::push_back(&mut config.pool_list, lp_name); + + // Emit an event + event::emit(RegisterPool { pool_name: lp_name, coin_x : coin::symbol<X>(), coin_y : coin::symbol<Y>(), weight_x: start_weight, weight_y: final_weight, is_stable: false, is_lbp: true }); + + } + + /// Entrypoint for the `add_liquidity` method. + /// Sends `LP<X,Y>` to the transaction sender. + public entry fun add_liquidity<X,Y>( + lp_provider: &signer, + coin_x_amount: u64, + coin_x_min: u64, + coin_y_amount: u64, + coin_y_min: u64 + ) acquires AMMManager, Pool { + let is_order = is_order<X, Y>(); + assert!(is_order, ERR_MUST_BE_ORDER); + + assert!(coin::is_coin_initialized<X>(), ERR_NOT_COIN); + assert!(coin::is_coin_initialized<Y>(), ERR_NOT_COIN); + + let config = borrow_global_mut<AMMManager>(@legato_addr); + let (lp_name, _) = generate_lp_name_and_symbol<X, Y>(); + assert!( smart_vector::contains(&config.pool_list, &lp_name) , ERR_POOL_EXISTS); + + let (optimal_x, optimal_y, _) = calc_optimal_coin_values<X, Y>( + coin_x_amount, + coin_y_amount, + coin_x_min, + coin_y_min + ); + + let (reserves_x, reserves_y) = get_reserves_size<X, Y>(); + + assert!(optimal_x >= coin_x_min, ERR_INSUFFICIENT_COIN_X); + assert!(optimal_y >= coin_y_min, ERR_INSUFFICIENT_COIN_Y); + + let coin_x_opt = coin::withdraw<X>(lp_provider, optimal_x); + let coin_y_opt = coin::withdraw<Y>(lp_provider, optimal_y); + + let lp_coins = mint_lp<X, Y>( + coin_x_opt, + coin_y_opt, + optimal_x, + optimal_y, + reserves_x, + reserves_y + ); + + let lp_amount = coin::value(&lp_coins); + + let lp_provider_address = signer::address_of(lp_provider); + if (!coin::is_account_registered<LP<X, Y>>(lp_provider_address)) { + coin::register<LP<X, Y>>(lp_provider); + }; + coin::deposit(lp_provider_address, lp_coins); + + // Emit an event + event::emit(AddedLiquidity { pool_name: lp_name, coin_x : coin::symbol<X>(), coin_y : coin::symbol<Y>(), coin_x_in:optimal_x, coin_y_in: optimal_y , lp_out: lp_amount }); + } + + /// Entrypoint for the `remove_liquidity` method. + /// Transfers Coin<X> and Coin<Y> to the sender. + public entry fun remove_liquidity<X, Y>( + lp_provider: &signer, + lp_amount: u64 + ) acquires AMMManager, Pool { + let is_order = is_order<X, Y>(); + assert!(is_order, ERR_MUST_BE_ORDER); + + assert!(coin::is_coin_initialized<LP<X,Y>>(), ERR_NOT_COIN); + + let config = borrow_global_mut<AMMManager>(@legato_addr); + + let (lp_name, _) = generate_lp_name_and_symbol<X, Y>(); + assert!( smart_vector::contains(&config.pool_list, &lp_name) , ERR_POOL_NOT_REGISTER); + + let config_object_signer = object::generate_signer_for_extending(&config.extend_ref); + let pool_address = signer::address_of(&config_object_signer); + assert!(exists<Pool<X, Y>>(pool_address), ERR_POOL_EXISTS); + + let (reserves_x, reserves_y) = get_reserves_size<X, Y>(); + let lp_coins_total = option::extract(&mut coin::supply<LP<X, Y>>()); + + let pool = borrow_global_mut<Pool<X, Y>>(pool_address); + assert!(!pool.has_paused , ERR_PAUSED ); + + let coin_x_out = 0; + let coin_y_out = 0; + + if (!pool.is_stable) { + + let (weight_x, weight_y ) = pool_current_weight<X,Y>(pool); + + (coin_x_out, coin_y_out) = weighted_math::compute_withdrawn_coins( + lp_amount, + (lp_coins_total as u64), + reserves_x, + reserves_y, + weight_x, + weight_y + ); + + let coin_x = coin::extract(&mut pool.coin_x, coin_x_out); + coin::deposit(signer::address_of(lp_provider), coin_x); + + let coin_y = coin::extract(&mut pool.coin_y, coin_y_out); + coin::deposit(signer::address_of(lp_provider), coin_y); + + } else { + + let multiplier = fixed_point64::create_from_rational( (lp_amount as u128), (lp_coins_total as u128) ); + + coin_x_out = (fixed_point64::multiply_u128( (reserves_x as u128), multiplier ) as u64); + coin_y_out = (fixed_point64::multiply_u128( (reserves_y as u128), multiplier ) as u64); + + let coin_x = coin::extract(&mut pool.coin_x, (coin_x_out)); + coin::deposit(signer::address_of(lp_provider), coin_x); + + let coin_y = coin::extract(&mut pool.coin_y, (coin_y_out)); + coin::deposit(signer::address_of(lp_provider), coin_y); + + }; + + let burn_coin = coin::withdraw<LP<X, Y>>(lp_provider, lp_amount); + coin::burn(burn_coin, &pool.lp_burn); + + // Emit an event + event::emit(RemovedLiquidity { pool_name: lp_name, coin_x : coin::symbol<X>(), coin_y : coin::symbol<Y>(), lp_in: lp_amount, coin_x_out, coin_y_out }); + } + + #[view] + public fun is_order<X, Y>(): bool { + let comp = compare<X, Y>(); + assert!(!comparator::is_equal(&comp), ERR_THE_SAME_COIN); + + if (comparator::is_smaller_than(&comp)) { + true + } else { + false + } + } + + #[view] + public fun get_config_object_address() : address acquires AMMManager { + let config = borrow_global<AMMManager>(@legato_addr); + let config_object_signer = object::generate_signer_for_extending(&config.extend_ref); + signer::address_of(&config_object_signer) + } + + #[view] + public fun get_treasury_address(): address acquires AMMManager { + let config = borrow_global<AMMManager>(@legato_addr); + config.treasury_address + } + + // Retrieves information about the LBP pool + #[view] + public fun lbp_info<X,Y>() : (u64, u64, u64, u64) acquires AMMManager, Pool { + let is_order = is_order<X, Y>(); + let config = borrow_global_mut<AMMManager>(@legato_addr); + let config_object_signer = object::generate_signer_for_extending(&config.extend_ref); + let pool_address = signer::address_of(&config_object_signer); + + if (is_order) { + assert!(exists<Pool<X, Y>>(pool_address), ERR_POOL_EXISTS); + let pool = borrow_global_mut<Pool<X, Y>>(pool_address); + + assert!( pool.is_lbp == true , ERR_NOT_LBP); + + let ( weight_x, weight_y ) = pool_current_weight(pool); + let params = option::borrow(&pool.lbp_params); + + (weight_x, weight_y, lbp::total_amount_collected(params), lbp::total_target_amount(params)) + } else { + assert!(exists<Pool<Y, X>>(pool_address), ERR_POOL_EXISTS); + let pool = borrow_global_mut<Pool<Y, X>>(pool_address); + + assert!( pool.is_lbp == true , ERR_NOT_LBP); + + let ( weight_y, weight_x ) = pool_current_weight(pool); + let params = option::borrow(&pool.lbp_params); + + ( weight_x, weight_y, lbp::total_amount_collected(params), lbp::total_target_amount(params)) + } + + } + + /// Calculate amounts needed for adding new liquidity for both `X` and `Y`. + /// * `x_desired` - desired value of coins `X`. + /// * `y_desired` - desired value of coins `Y`. + /// Returns both `X` and `Y` coins amounts. + public fun calc_optimal_coin_values<X, Y>( + x_desired: u64, + y_desired: u64, + coin_x_min: u64, + coin_y_min: u64 + ): (u64, u64, bool) acquires Pool, AMMManager { + let (reserves_x, reserves_y) = get_reserves_size<X, Y>(); + + let config = borrow_global<AMMManager>(@legato_addr); + let config_object_signer = object::generate_signer_for_extending(&config.extend_ref); + let pool_address = signer::address_of(&config_object_signer); + let pool = borrow_global<Pool<X, Y>>(pool_address); + + if (reserves_x == 0 && reserves_y == 0) { + return (x_desired, y_desired, true) + } else { + + // For non-stable pools, use weighted math to compute optimal values. + if (!pool.is_stable) { + + let (weight_x, weight_y ) = pool_current_weight<X,Y>(pool); + + let y_needed = weighted_math::compute_optimal_value(x_desired, reserves_y, weight_y, reserves_x, weight_x ); + + if (y_needed <= y_desired) { + assert!(y_needed >= coin_y_min, ERR_INSUFFICIENT_COIN_Y); + return (x_desired, y_needed, false) + } else { + let x_needed = weighted_math::compute_optimal_value(y_desired, reserves_x, weight_x, reserves_y, weight_y); + assert!(x_needed <= x_desired, ERR_OVERLIMIT); + assert!(x_needed >= coin_x_min, ERR_INSUFFICIENT_COIN_X); + return (x_needed, y_desired, false) + } + + } else { + + // For stable pools, use stable math to compute the optimal values. + let coin_y_returned = stable_math::get_amount_out( + x_desired, + reserves_x, + reserves_y + ); + + if (coin_y_returned <= y_desired) { + assert!(coin_y_returned >= coin_y_min, ERR_INSUFFICIENT_COIN_Y); + return (x_desired, coin_y_returned, false) + } else { + let coin_x_returned = stable_math::get_amount_out( + y_desired, + reserves_y, + reserves_x + ); + + assert!(coin_x_returned <= x_desired, ERR_OVERLIMIT); + assert!(coin_x_returned >= coin_x_min, ERR_INSUFFICIENT_COIN_X); + return (coin_x_returned, y_desired, false) + } + + } + + } + } + + public fun get_reserves_size<X, Y>(): (u64, u64) acquires Pool, AMMManager { + let config = borrow_global_mut<AMMManager>(@legato_addr); + let config_object_signer = object::generate_signer_for_extending(&config.extend_ref); + let pool_address = signer::address_of(&config_object_signer); + + assert!(exists<Pool<X, Y>>(pool_address), ERR_POOL_NOT_REGISTER); + + let pool = borrow_global<Pool<X, Y>>(pool_address); + + let x_reserve = coin::value(&pool.coin_x); + let y_reserve = coin::value(&pool.coin_y); + + (x_reserve, y_reserve) + } + + // Retrieve the current weights of the pool + public fun pool_current_weight<X,Y>(pool: &Pool<X, Y> ): (u64, u64) { + + if (!pool.is_lbp) { + ( pool.weight_x, pool.weight_y ) + } else { + let params = option::borrow(&pool.lbp_params); + lbp::current_weight( params ) + } + + } + + /// Calculates the provided liquidity based on the current LP supply and reserves. + /// If the LP supply is zero, it computes the initial liquidity and increases the supply. + public fun calculate_provided_liq<X,Y>(pool: &mut Pool<X, Y>, lp_supply: u64, coin_x_reserve: u64, coin_y_reserve: u64, optimal_coin_x: u64, optimal_coin_y: u64 ) : u64 { + if (!pool.is_stable) { + + // Obtain the current weights of the pool + let (weight_x, weight_y ) = pool_current_weight<X,Y>(pool); + + if (0 == lp_supply) { + + let initial_liq = weighted_math::compute_initial_lp( weight_x, weight_y , optimal_coin_x , optimal_coin_y ); + assert!(initial_liq > MINIMAL_LIQUIDITY, ERR_LIQUID_NOT_ENOUGH); + + coin::merge(&mut pool.min_liquidity, coin::mint<LP<X, Y>>(MINIMAL_LIQUIDITY, &pool.lp_mint) ); + + initial_liq - MINIMAL_LIQUIDITY + } else { + weighted_math::compute_derive_lp( optimal_coin_x, optimal_coin_y, weight_x, weight_y, coin_x_reserve, coin_y_reserve, lp_supply ) + } + } else { + if (0 == lp_supply) { + + let initial_liq = stable_math::compute_initial_lp( optimal_coin_x , optimal_coin_y ); + assert!(initial_liq > MINIMAL_LIQUIDITY, ERR_LIQUID_NOT_ENOUGH); + + coin::merge(&mut pool.min_liquidity, coin::mint<LP<X, Y>>(MINIMAL_LIQUIDITY, &pool.lp_mint) ); + + initial_liq - MINIMAL_LIQUIDITY + } else { + let x_liq = (lp_supply as u128) * (optimal_coin_x as u128) / (coin_x_reserve as u128); + let y_liq = (lp_supply as u128) * (optimal_coin_y as u128) / (coin_y_reserve as u128); + if (x_liq < y_liq) { + assert!(x_liq < (U64_MAX as u128), ERR_U64_OVERFLOW); + (x_liq as u64) + } else { + assert!(y_liq < (U64_MAX as u128), ERR_U64_OVERFLOW); + (y_liq as u64) + } + } + } + } + + // ======== Internal Functions ========= + + fun coin_symbol_prefix<CoinType>(): String { + let symbol = coin::symbol<CoinType>(); + let prefix_length = SYMBOL_PREFIX_LENGTH; + if (string::length(&symbol) < SYMBOL_PREFIX_LENGTH) { + prefix_length = string::length(&symbol); + }; + string::sub_string(&symbol, 0, prefix_length) + } + + /// Compare two coins, 'X' and 'Y'. + fun compare<X, Y>(): Result { + let x_info = type_info::type_of<X>(); + let x_compare = &mut type_info::struct_name(&x_info); + vector::append(x_compare, type_info::module_name(&x_info)); + + let y_info = type_info::type_of<Y>(); + let y_compare = &mut type_info::struct_name(&y_info); + vector::append(y_compare, type_info::module_name(&y_info)); + + let comp = comparator::compare(x_compare, y_compare); + if (!comparator::is_equal(&comp)) return comp; + + let x_address = type_info::account_address(&x_info); + let y_address = type_info::account_address(&y_info); + comparator::compare(&x_address, &y_address) + } + + /// Generate LP coin name and symbol for pair `X`/`Y`. + /// ``` + /// name = "LP-" + symbol<X>() + "-" + symbol<Y>(); + /// symbol = symbol<X>()[0:4] + "-" + symbol<Y>()[0:4]; + /// ``` + /// For example, for `LP<BTC, USDT>`, + /// the result will be `(b"LP-BTC-USDT", b"BTC-USDT")` + public fun generate_lp_name_and_symbol<X, Y>(): (String, String) { + let lp_name = string::utf8(b""); + string::append_utf8(&mut lp_name, b"LP-"); + string::append(&mut lp_name, coin::symbol<X>()); + string::append_utf8(&mut lp_name, b"-"); + string::append(&mut lp_name, coin::symbol<Y>()); + + let lp_symbol = string::utf8(b""); + string::append(&mut lp_symbol, coin_symbol_prefix<X>()); + string::append_utf8(&mut lp_symbol, b"-"); + string::append(&mut lp_symbol, coin_symbol_prefix<Y>()); + + (lp_name, lp_symbol) + } + + fun init_pool_params<X,Y>(weight_x: u64, weight_y: u64, swap_fee: FixedPoint64, lp_mint: MintCapability<LP<X,Y>>, lp_burn: BurnCapability<LP<X,Y>>, is_stable: bool, is_lbp: bool, lbp_params: Option<LBPParams> ) : Pool<X, Y> { + // Ensure that the normalized weights sum up to 100% + if (!is_lbp) { + assert!( weight_x+weight_y == 10000, ERR_WEIGHTS_SUM); + }; + + Pool<X, Y> { + coin_x: coin::zero<X>(), + coin_y: coin::zero<Y>(), + weight_x, + weight_y, + swap_fee, + lp_mint, + lp_burn, + min_liquidity: coin::zero<LP<X,Y>>(), + has_paused: false, + is_stable, + is_lbp, + lbp_params + } + } + + fun swap_out_y<X, Y>( + sender: &signer, + coin_in_value: u64, + coin_out_min_value: u64, + reserve_in: u64, + reserve_out: u64, + ) acquires Pool, AMMManager { + + let config = borrow_global_mut<AMMManager>(@legato_addr); + let (lp_name, _) = generate_lp_name_and_symbol<X, Y>(); + assert!( smart_vector::contains(&config.pool_list, &lp_name) , ERR_POOL_NOT_REGISTER); + + let config_object_signer = object::generate_signer_for_extending(&config.extend_ref); + let pool_address = signer::address_of(&config_object_signer); + + let pool = borrow_global_mut<Pool<X, Y>>(pool_address); + + assert!(!pool.has_paused , ERR_PAUSED ); + + let (coin_x_after_fees, coin_x_fee) = weighted_math::get_fee_to_treasury( pool.swap_fee , coin_in_value); + + let (weight_x, weight_y ) = pool_current_weight<X,Y>(pool); + + let coin_y_out = get_amount_out( + pool.is_stable, + coin_x_after_fees, + reserve_in, + weight_x, + reserve_out, + weight_y + ); + + assert!( + coin_y_out >= coin_out_min_value, + ERR_COIN_OUT_NUM_LESS_THAN_EXPECTED_MINIMUM + ); + + if (pool.is_lbp) { + let params = option::borrow_mut(&mut pool.lbp_params); + let is_buy = lbp::is_buy(params); + lbp::verify_and_adjust_amount(params, is_buy, coin_in_value, coin_y_out, false ); + }; + + let coin_in = coin::withdraw<X>(sender, coin_in_value); + let fee_in = coin::extract(&mut coin_in, coin_x_fee); + + coin::deposit( config.treasury_address, fee_in); + + coin::merge(&mut pool.coin_x, coin_in); + + let out_swapped = coin::extract(&mut pool.coin_y, coin_y_out); + + if (!coin::is_account_registered<Y>(signer::address_of(sender))) { + coin::register<Y>(sender); + }; + + coin::deposit(signer::address_of(sender), out_swapped); + + // Emit an event + event::emit(Swapped { pool_name: lp_name, coin_in : coin::symbol<X>(), coin_out : coin::symbol<Y>(), amount_in: coin_in_value, amount_out: coin_y_out }); + } + + fun swap_out_x<X,Y>( + sender: &signer, + coin_in_value: u64, + coin_out_min_value: u64, + reserve_in: u64, + reserve_out: u64 + ) acquires Pool, AMMManager { + + let config = borrow_global_mut<AMMManager>(@legato_addr); + let (lp_name , _) = generate_lp_name_and_symbol<X, Y>(); + assert!( smart_vector::contains(&config.pool_list, &lp_name), ERR_POOL_NOT_REGISTER); + + let config_object_signer = object::generate_signer_for_extending(&config.extend_ref); + let pool_address = signer::address_of(&config_object_signer); + + let pool = borrow_global_mut<Pool<X, Y>>(pool_address); + + assert!(!pool.has_paused , ERR_PAUSED ); + + let (coin_y_after_fees, coin_y_fee) = weighted_math::get_fee_to_treasury( pool.swap_fee , coin_in_value); + + let (weight_x, weight_y ) = pool_current_weight(pool); + + let coin_x_out = weighted_math::get_amount_out( + coin_y_after_fees, + reserve_in, + weight_y, + reserve_out, + weight_x + ); + + assert!( + coin_x_out >= coin_out_min_value, + ERR_COIN_OUT_NUM_LESS_THAN_EXPECTED_MINIMUM + ); + + if (pool.is_lbp) { + let params = option::borrow_mut(&mut pool.lbp_params); + let is_buy = lbp::is_buy(params); + lbp::verify_and_adjust_amount(params, !is_buy, coin_in_value, coin_x_out, false); + }; + + let coin_in = coin::withdraw<Y>(sender, coin_in_value); + let fee_in = coin::extract(&mut coin_in, coin_y_fee); + + coin::deposit( config.treasury_address, fee_in); + + coin::merge(&mut pool.coin_y, coin_in); + + let out_swapped = coin::extract(&mut pool.coin_x, coin_x_out); + + if (!coin::is_account_registered<X>(signer::address_of(sender))) { + coin::register<X>(sender); + }; + + coin::deposit(signer::address_of(sender), out_swapped); + + // Emit an event + event::emit(Swapped { pool_name: lp_name, coin_in : coin::symbol<Y>(), coin_out : coin::symbol<X>(), amount_in: coin_in_value, amount_out: coin_x_out }); + } + + fun get_amount_out(is_stable: bool, coin_in: u64, reserve_in: u64, weight_in: u64, reserve_out: u64, weight_out: u64) : u64 { + if (!is_stable) { + weighted_math::get_amount_out( + coin_in, + reserve_in, + weight_in, + reserve_out, + weight_out, + ) + } else { + stable_math::get_amount_out( + coin_in, + reserve_in, + reserve_out + ) + } + } + + // mint LP tokens + fun mint_lp<X, Y>( + coin_x: Coin<X>, + coin_y: Coin<Y>, + optimal_x: u64, + optimal_y: u64, + coin_x_reserve: u64, + coin_y_reserve: u64 + ): Coin<LP<X, Y>> acquires Pool, AMMManager { + let config = borrow_global_mut<AMMManager>(@legato_addr); + let config_object_signer = object::generate_signer_for_extending(&config.extend_ref); + let pool_address = signer::address_of(&config_object_signer); + + assert!(exists<Pool<X, Y>>(pool_address), ERR_POOL_EXISTS); + + let pool = borrow_global_mut<Pool<X, Y>>(pool_address); + assert!(!pool.has_paused , ERR_PAUSED ); + + // let x_provided_val = coin::value<X>(&coin_x); + // let y_provided_val = coin::value<Y>(&coin_y); + + // Retrieves total LP coins supply + let lp_coins_total = option::extract(&mut coin::supply<LP<X, Y>>()); + + // Computes provided liquidity + let provided_liq = calculate_provided_liq<X,Y>(pool, (lp_coins_total as u64), coin_x_reserve, coin_y_reserve, optimal_x, optimal_y ); + + // Merges provided coins into pool + coin::merge(&mut pool.coin_x, coin_x); + coin::merge(&mut pool.coin_y, coin_y); + + assert!(coin::value(&pool.coin_x) < MAX_POOL_VALUE, ERR_POOL_FULL); + assert!(coin::value(&pool.coin_y) < MAX_POOL_VALUE, ERR_POOL_FULL); + + // Mints LP tokens + coin::mint<LP<X, Y>>(provided_liq, &pool.lp_mint) + } + + // ======== Only Governance ========= + + // Adds a user to the whitelist + public entry fun add_whitelist(sender: &signer, whitelist_address: address) acquires AMMManager { + assert!( signer::address_of(sender) == @legato_addr , ERR_UNAUTHORIZED); + let config = borrow_global_mut<AMMManager>(@legato_addr); + assert!( !smart_vector::contains(&config.whitelist, &whitelist_address) , ERR_INVALID_ADDRESS); + smart_vector::push_back(&mut config.whitelist, whitelist_address); + } + + // Removes a user from the whitelist + public entry fun remove_whitelist(sender: &signer, whitelist_address: address) acquires AMMManager { + assert!( signer::address_of(sender) == @legato_addr , ERR_UNAUTHORIZED); + let config = borrow_global_mut<AMMManager>(@legato_addr); + let (found, idx) = smart_vector::index_of<address>(&config.whitelist, &whitelist_address); + assert!( found , ERR_INVALID_ADDRESS); + smart_vector::swap_remove<address>(&mut config.whitelist, idx ); + } + + // Update treasury address + public entry fun update_treasury_address(sender: &signer, new_address: address) acquires AMMManager { + assert!( signer::address_of(sender) == @legato_addr , ERR_UNAUTHORIZED); + let config = borrow_global_mut<AMMManager>(@legato_addr); + config.treasury_address = new_address; + } + + // Enable or disable whitelist requirement + public entry fun enable_whitelist(sender: &signer, is_enable: bool) acquires AMMManager { + assert!( signer::address_of(sender) == @legato_addr , ERR_UNAUTHORIZED); + let config = borrow_global_mut<AMMManager>(@legato_addr); + config.enable_whitelist = is_enable; + } + + // Updates the swap fee for the specified pool + public entry fun update_pool_fee<X,Y>(sender: &signer, fee_numerator: u128, fee_denominator: u128) acquires AMMManager, Pool { + let is_order = is_order<X, Y>(); + assert!(is_order, ERR_MUST_BE_ORDER); + assert!( signer::address_of(sender) == @legato_addr , ERR_UNAUTHORIZED); + + let config = borrow_global_mut<AMMManager>(@legato_addr); + let (lp_name, _) = generate_lp_name_and_symbol<X, Y>(); + assert!( smart_vector::contains(&config.pool_list, &lp_name), ERR_POOL_NOT_REGISTER); + + let config_object_signer = object::generate_signer_for_extending(&config.extend_ref); + let pool_address = signer::address_of(&config_object_signer); + + let pool_config = borrow_global_mut<Pool<X, Y>>(pool_address); + pool_config.swap_fee = fixed_point64::create_from_rational( fee_numerator, fee_denominator ); + + } + + // Pause/Unpause the LP pool + public entry fun pause<X,Y>(sender: &signer, is_pause: bool) acquires AMMManager, Pool { + let is_order = is_order<X, Y>(); + assert!(is_order, ERR_MUST_BE_ORDER); + assert!( signer::address_of(sender) == @legato_addr , ERR_UNAUTHORIZED); + + let config = borrow_global_mut<AMMManager>(@legato_addr); + let (lp_name, _) = generate_lp_name_and_symbol<X, Y>(); + assert!( smart_vector::contains(&config.pool_list, &lp_name), ERR_POOL_NOT_REGISTER); + + let config_object_signer = object::generate_signer_for_extending(&config.extend_ref); + let pool_address = signer::address_of(&config_object_signer); + + let pool_config = borrow_global_mut<Pool<X, Y>>(pool_address); + pool_config.has_paused = is_pause + } + + // Set a new target amount for LBP + public entry fun lbp_set_target_amount<X,Y>(sender: &signer, new_target_amount: u64) acquires AMMManager, Pool { + let is_order = is_order<X, Y>(); + assert!(is_order, ERR_MUST_BE_ORDER); + assert!( signer::address_of(sender) == @legato_addr , ERR_UNAUTHORIZED); + + let config = borrow_global_mut<AMMManager>(@legato_addr); + let (lp_name, _) = generate_lp_name_and_symbol<X, Y>(); + assert!( smart_vector::contains(&config.pool_list, &lp_name) , ERR_POOL_NOT_REGISTER); + + let config_object_signer = object::generate_signer_for_extending(&config.extend_ref); + let pool_address = signer::address_of(&config_object_signer); + let pool_config = borrow_global_mut<Pool<X, Y>>(pool_address); + + let params = option::borrow_mut(&mut pool_config.lbp_params); + lbp::set_new_target_amount( params, new_target_amount ); + } + + // Enable/Disable buy with pair or with vault tokens + public entry fun lbp_enable_buy_with_pair_and_vault<X,Y>(sender: &signer, enable_pair: bool, enable_vault: bool) acquires AMMManager, Pool { + let is_order = is_order<X, Y>(); + assert!(is_order, ERR_MUST_BE_ORDER); + assert!( signer::address_of(sender) == @legato_addr , ERR_UNAUTHORIZED); + + let config = borrow_global_mut<AMMManager>(@legato_addr); + let (lp_name, _) = generate_lp_name_and_symbol<X, Y>(); + assert!( smart_vector::contains(&config.pool_list, &lp_name) , ERR_POOL_NOT_REGISTER); + + let config_object_signer = object::generate_signer_for_extending(&config.extend_ref); + let pool_address = signer::address_of(&config_object_signer); + let pool_config = borrow_global_mut<Pool<X, Y>>(pool_address); + + assert!( pool_config.is_lbp , ERR_NOT_LBP); + + let params = option::borrow_mut(&mut pool_config.lbp_params); + + lbp::enable_buy_with_pair( params, enable_pair ); + lbp::enable_buy_with_vault( params, enable_vault ); + } + + + // ======== Test-related Functions ========= + + #[test_only] + public fun init_module_for_testing(deployer: &signer) { + init_module(deployer) + } + +} \ No newline at end of file diff --git a/packages/aptos/sources/lbp.move b/packages/aptos/sources/lbp.move new file mode 100644 index 0000000..e64cbee --- /dev/null +++ b/packages/aptos/sources/lbp.move @@ -0,0 +1,177 @@ +// Copyright (c) Tamago Blockchain Labs, Inc. +// SPDX-License-Identifier: MIT + +// An extension to AMM for LBP pools allows automatic weight shifting triggered by liquidity inflow +// The pool can be paired with any tokens such as APT or USDC and even with staking rewards earned through Legato Vault + +module legato_addr::lbp { + + use aptos_std::fixed_point64::{Self, FixedPoint64}; + use legato_addr::weighted_math::{power}; + + + friend legato_addr::amm; + + + /// The integer scaling setting for weights + const WEIGHT_SCALE: u64 = 10000; + + // ======== Errors ======== + + const ERR_INVALID_WEIGHT: u64 = 301; + const ERR_INVALID_SENDER: u64 = 302; + const ERR_EMPTY: u64 = 303; + const ERR_INVALID_POOL: u64 = 304; + const ERR_INSUFFICIENT_AMOUNT : u64 = 305; + const ERR_INVALID_AMOUNT: u64 = 306; + const ERR_TOO_LOW_AMOUNT: u64 = 307; + const ERR_BUY_DISABLED_WITH_TOKEN: u64 = 308; + const ERR_BUY_DISABLED_WITH_VAULT: u64 = 309; + + // ======== Structs ========= + + // Defines the settings for the LBP pool. + struct LBPParams has store { + is_proj_on_x: bool, // Indicates if the project token is on the X side of the pool. + start_weight: u64, // Initial weight of the project token. + final_weight: u64, // The weight when the pool is stabilized. + is_vault: bool, // Accepts vault tokens + target_amount: u64, // The target amount required to fully shift the weight. + total_amount_collected: u64, // Total amount accumulated in the pool. + enable_buy_with_pair: bool, // Enable/Disable buy with pair tokens (SUI or USDC). + enable_buy_with_vault: bool // Enable/Disable buy with vault tokens (PT). + } + + // Constructs initialization parameters for an Lpending_amountBP + public(friend) fun construct_init_params( + proj_on_x: bool, // Indicates whether the project token is on the X or Y side + start_weight: u64, + final_weight: u64, + is_vault: bool, // Determines if accept vault tokens. + target_amount: u64 + ) : LBPParams { + + // Check if weights and trigger amount are within valid ranges. + assert!( start_weight >= 5000 && start_weight < WEIGHT_SCALE, ERR_INVALID_WEIGHT ); + assert!( final_weight >= 5000 && final_weight < WEIGHT_SCALE, ERR_INVALID_WEIGHT ); + assert!( start_weight > final_weight, ERR_INVALID_WEIGHT ); + + LBPParams { + is_proj_on_x: proj_on_x, + start_weight, + final_weight, + is_vault, + target_amount, + total_amount_collected: 0, + enable_buy_with_pair: true, + enable_buy_with_vault: true + } + } + + // Calculates the current weight of the project token. + // - decline_ratio = (total_collected / target_amount)^(stablized_weight / start_weight) + public(friend) fun current_weight(params: &LBPParams ) : (u64, u64) { + + // Check if fully shifted + let weight_base = if ( params.total_amount_collected >= params.target_amount ) { + // Use final weight if the target amount is reached + params.final_weight + } else if (10000 > params.total_amount_collected ) { + // Return the start weight is value is less than 10000 + params.start_weight + } else { + + // Calculate the weight difference + let weight_diff = if (params.start_weight > params.final_weight) { + params.start_weight-params.final_weight + } else { + 0 + }; + + assert!( weight_diff > 0 , ERR_INVALID_WEIGHT); + + // Ensure the accumulated amount does not exceed the target amount + let accumulated_amount = if (params.target_amount > params.total_amount_collected) { + (params.total_amount_collected as u128) + } else { + (params.target_amount as u128) + }; + let total_target_amount = (params.target_amount as u128); + + // Calculate the decline ratio for weight adjustment + let decline_ratio = power( fixed_point64::create_from_rational(accumulated_amount, total_target_amount), fixed_point64::create_from_rational( (params.final_weight as u128), (params.start_weight as u128) )); + + // Adjust the start weight by the decline ratio to get the current weight + params.start_weight-(fixed_point64::multiply_u128((weight_diff as u128), decline_ratio) as u64) + }; + + + let weight_pair = WEIGHT_SCALE-weight_base; + + if ( params.is_proj_on_x ) { + (weight_base, weight_pair) + } else { + (weight_pair, weight_base) + } + } + + // Only admin can set a new target amount + public(friend) fun set_new_target_amount(params: &mut LBPParams, new_target_amount: u64) { + assert!( new_target_amount > params.total_amount_collected, ERR_TOO_LOW_AMOUNT ); + params.target_amount = new_target_amount; + } + + public(friend) fun enable_buy_with_pair(params: &mut LBPParams, is_enable: bool) { + params.enable_buy_with_pair = is_enable; + } + + public(friend) fun enable_buy_with_vault(params: &mut LBPParams, is_enable: bool) { + params.enable_buy_with_vault = is_enable; + } + + public (friend) fun is_buy(params: &LBPParams) : bool { + // X -> Y + if ( params.is_proj_on_x ) { + false + } else { + // Y -> X + true + } + } + + // Verifies and adjusts the amount for weight calculation + public(friend) fun verify_and_adjust_amount(params: &mut LBPParams, is_buy: bool, amount_in: u64, _amount_out: u64, is_vault: bool ) { + // Works when the weight is not stabilized + if ( params.target_amount > params.total_amount_collected) { + // Considered only buy transactions + if (is_buy) { + assert!( params.target_amount > amount_in, ERR_INVALID_AMOUNT ); + if (is_vault) { + assert!( params.enable_buy_with_vault, ERR_BUY_DISABLED_WITH_VAULT ); + } else { + assert!( params.enable_buy_with_pair, ERR_BUY_DISABLED_WITH_TOKEN ); + }; + + // Update the total amount collected + params.total_amount_collected = params.total_amount_collected+amount_in; + }; + }; + } + + public fun is_vault(params: &LBPParams) : bool { + params.is_vault + } + + public fun total_amount_collected(params: &LBPParams) : u64 { + params.total_amount_collected + } + + public fun total_target_amount(params: &LBPParams) : u64 { + params.target_amount + } + + public fun proj_on_x(params: &LBPParams) : bool { + params.is_proj_on_x + } + +} \ No newline at end of file diff --git a/packages/aptos/sources/utils/mock_legato.move b/packages/aptos/sources/utils/mock_legato.move index a07227c..8d26306 100644 --- a/packages/aptos/sources/utils/mock_legato.move +++ b/packages/aptos/sources/utils/mock_legato.move @@ -4,10 +4,9 @@ module legato_addr::mock_legato { use std::signer; - use std::string::{Self, String }; - - use aptos_framework::account; - use aptos_framework::coin::{Self, Coin, MintCapability, BurnCapability}; + use std::string::{Self }; + + use aptos_framework::coin::{Self, MintCapability, BurnCapability}; const TOKEN_NAME: vector<u8> = b"Legato Token"; diff --git a/packages/aptos/sources/utils/mock_legato_fa.move b/packages/aptos/sources/utils/mock_legato_fa.move index 84f42fc..e3fccb8 100644 --- a/packages/aptos/sources/utils/mock_legato_fa.move +++ b/packages/aptos/sources/utils/mock_legato_fa.move @@ -6,6 +6,7 @@ module legato_addr::mock_legato_fa { use aptos_framework::object; use aptos_framework::fungible_asset::{ Metadata}; use aptos_framework::object::Object; + use legato_addr::base_fungible_asset; use std::string::utf8; diff --git a/packages/aptos/sources/utils/mock_usdc.move b/packages/aptos/sources/utils/mock_usdc.move index 49d6711..0351912 100644 --- a/packages/aptos/sources/utils/mock_usdc.move +++ b/packages/aptos/sources/utils/mock_usdc.move @@ -4,10 +4,9 @@ module legato_addr::mock_usdc { use std::signer; - use std::string::{Self, String }; + use std::string::{Self }; - use aptos_framework::account; - use aptos_framework::coin::{Self, Coin, MintCapability, BurnCapability}; + use aptos_framework::coin::{Self, MintCapability, BurnCapability}; const TOKEN_NAME: vector<u8> = b"USDC Token"; diff --git a/packages/aptos/sources/utils/mock_usdc_fa.move b/packages/aptos/sources/utils/mock_usdc_fa.move index 0060e19..731c625 100644 --- a/packages/aptos/sources/utils/mock_usdc_fa.move +++ b/packages/aptos/sources/utils/mock_usdc_fa.move @@ -5,7 +5,7 @@ module legato_addr::mock_usdc_fa { use aptos_framework::object; - use aptos_framework::fungible_asset::{Self, Metadata}; + use aptos_framework::fungible_asset::{ Metadata}; use aptos_framework::object::Object; use legato_addr::base_fungible_asset; use std::string::utf8; diff --git a/packages/aptos/sources/vault.move b/packages/aptos/sources/vault.move index a47d4b6..b5b67b0 100644 --- a/packages/aptos/sources/vault.move +++ b/packages/aptos/sources/vault.move @@ -6,6 +6,553 @@ module legato_addr::vault { + use std::signer; + use aptos_framework::timestamp; + use std::string::{Self, String, utf8}; + use aptos_framework::event; + use aptos_framework::aptos_coin::AptosCoin; + use aptos_framework::object::{Self, ExtendRef}; + use aptos_framework::delegation_pool as dp; + use aptos_framework::coin::{Self, MintCapability, BurnCapability}; + use aptos_std::smart_vector::{Self, SmartVector}; + use aptos_std::type_info; + use aptos_std::fixed_point64::{Self, FixedPoint64}; + use aptos_std::math_fixed64::{Self}; + use aptos_std::table::{Self, Table}; + + // ======== Constants ======== + + const MIN_APT_TO_STAKE: u64 = 100000000; // 1 APT + const DEFAULT_EXIT_FEE: u128 = 553402322211286548; // 3% in fixed-point + const DEFAULT_BATCH_AMOUNT: u64 = 1500000000; // 15 APT + + // ======== Errors ======== + + const ERR_UNAUTHORIZED: u64 = 101; + const ERR_INVALID_MATURITY: u64 = 102; + const ERR_VAULT_EXISTS: u64 = 103; + const ERR_VAULT_MATURED: u64 = 104; + const ERR_MIN_THRESHOLD: u64 = 105; + const ERR_INVALID_VAULT: u64 = 106; + const ERR_VAULT_NOT_MATURED: u64 = 107; + const ERR_INVALID_ADDRESS: u64 = 108; + const ERR_INVALID_TYPE: u64 = 109; + + // ======== Structs ========= + + // represent the future value at maturity date + struct PT_TOKEN<phantom P> has drop {} + + struct VaultReserve<phantom P> has key { + pt_mint: MintCapability<PT_TOKEN<P>>, + pt_burn: BurnCapability<PT_TOKEN<P>>, + pt_total_supply: u64, // Total supply of PT tokens + } + + struct VaultConfig has store { + maturity_time: u64, + vault_apy: FixedPoint64, // Vault's APY based on the average rate + enable_mint: bool, + enable_exit: bool, + enable_redeem: bool + } + + struct VaultManager has key { + delegator_pools: SmartVector<address>, // Supported delegator pools + vault_list: SmartVector<String>, // List of all vaults in the system + vault_config: Table<String, VaultConfig>, + extend_ref: ExtendRef, // self-sovereight identity + pending_stake: u64, // Pending stake + pending_unstake: Table<address, u64>, + unlocked_amount: u64, + pending_withdrawal: Table<address, u64>, + requesters: SmartVector<address>, + batch_amount: u64, + exit_fee: FixedPoint64 + } + + + #[event] + struct NewVault has drop, store { + vault_name: String, + maturity_time: u64, + vault_apy: u128 // fixed-point raw value + } + + #[event] + struct MintEvent has drop, store { + vault_name: String, + vault_apy: u128, + maturity_time: u64, + current_time: u64, + apt_in: u64, + pt_out: u64, + sender: address + } + + #[event] + struct RequestRedeem has drop, store { + vault_name: String, + current_time: u64, + pt_amount: u64, + sender: address + } + + #[event] + struct RequestExit has drop, store { + vault_name: String, + current_time: u64, + pt_amount: u64, + exit_amount: u64, + sender: address + } + + #[event] + struct Withdrawn has drop, store { + withdraw_amount: u64, + requester: address + } + + // constructor + fun init_module(sender: &signer) { + + let constructor_ref = object::create_object(signer::address_of(sender)); + let extend_ref = object::generate_extend_ref(&constructor_ref); + + move_to(sender, VaultManager { + delegator_pools: smart_vector::new<address>(), + vault_list: smart_vector::new<String>(), + vault_config: table::new<String, VaultConfig>(), + extend_ref, + pending_stake: 0, + unlocked_amount: 0, + pending_unstake: table::new<address, u64>(), + pending_withdrawal: table::new<address, u64>(), + requesters: smart_vector::new<address>(), + batch_amount: DEFAULT_BATCH_AMOUNT, + exit_fee: fixed_point64::create_from_raw_value(DEFAULT_EXIT_FEE) + }); + } + + // ======== Public Functions ========= + + // Convert APT into future PT tokens equivalent to the value at the maturity date. + public entry fun mint<P>(sender: &signer, input_amount: u64) acquires VaultReserve, VaultManager { + assert!(exists<VaultReserve<P>>(@legato_addr), ERR_INVALID_VAULT); + assert!(coin::balance<AptosCoin>(signer::address_of(sender)) >= MIN_APT_TO_STAKE, ERR_MIN_THRESHOLD); + + let type_name = type_info::type_name<P>(); + + let config = borrow_global_mut<VaultManager>(@legato_addr); + let config_object_signer = object::generate_signer_for_extending(&config.extend_ref); + + let vault_config = table::borrow_mut( &mut config.vault_config, type_name ); + let vault_reserve = borrow_global_mut<VaultReserve<P>>(@legato_addr); + + // Ensure that the vault has not yet matured + assert!(vault_config.maturity_time > timestamp::now_seconds() , ERR_VAULT_MATURED); + + // attaches to object + let input_coin = coin::withdraw<AptosCoin>(sender, input_amount); + if (!coin::is_account_registered<AptosCoin>(signer::address_of(&config_object_signer))) { + coin::register<AptosCoin>(&config_object_signer); + }; + + coin::deposit(signer::address_of(&config_object_signer), input_coin); + + // Update the pending stake amount + config.pending_stake = config.pending_stake+input_amount; + + let vault_apy: FixedPoint64 = vault_config.vault_apy; + let maturity_time: u64 = vault_config.maturity_time; + + // Calculate the amount of PT tokens to be sent out + let pt_amount = calculate_pt_debt_amount( vault_apy, timestamp::now_seconds(), maturity_time, input_amount ); + + // Mint PT tokens and deposit them into the sender's account + let pt_coin = coin::mint<PT_TOKEN<P>>(pt_amount, &vault_reserve.pt_mint); + if (!coin::is_account_registered<PT_TOKEN<P>>(signer::address_of(sender))) { + coin::register<PT_TOKEN<P>>(sender); + }; + coin::deposit(signer::address_of(sender), pt_coin); + + // Update + vault_reserve.pt_total_supply = vault_reserve.pt_total_supply+pt_amount; + + transfer_stake(); + + // Emit an event + event::emit( + MintEvent { + vault_name: type_name, + vault_apy: fixed_point64::get_raw_value(vault_apy), + maturity_time, + current_time: timestamp::now_seconds(), + apt_in: input_amount, + pt_out: pt_amount, + sender: signer::address_of(sender) + } + ) + } + + // Check if the APT in pending_stake meets the BATCH_AMOUNT, then use it to stake on a randomly supported validator. + public entry fun transfer_stake() acquires VaultManager { + let config = borrow_global_mut<VaultManager>(@legato_addr); + if (config.pending_stake >= config.batch_amount) { + + let config_object_signer = object::generate_signer_for_extending(&config.extend_ref); + + let validator_index = timestamp::now_seconds() % smart_vector::length( &config.delegator_pools ); + let validator_address = *smart_vector::borrow( &config.delegator_pools, validator_index ); + let pool_address = dp::get_owned_pool_address(validator_address); + dp::add_stake(&config_object_signer, pool_address, config.pending_stake); + + config.pending_stake = 0; + + }; + } + + // request redeem when the vault reaches its maturity date + public entry fun request_redeem<P>( sender: &signer, amount: u64 ) acquires VaultReserve, VaultManager { + assert!(exists<VaultReserve<P>>(@legato_addr), ERR_INVALID_VAULT); + + let type_name = type_info::type_name<P>(); + + let config = borrow_global_mut<VaultManager>(@legato_addr); + + let vault_config = table::borrow_mut( &mut config.vault_config, type_name ); + let vault_reserve = borrow_global_mut<VaultReserve<P>>(@legato_addr); + + // Check if the vault has matured + assert!(timestamp::now_seconds() > vault_config.maturity_time, ERR_VAULT_NOT_MATURED); + + // Burn PT tokens on the sender's account + let pt_coin = coin::withdraw<PT_TOKEN<P>>(sender, amount); + coin::burn(pt_coin, &vault_reserve.pt_burn); + + // Add the request to the withdrawal list + table::add( + &mut config.pending_unstake, + signer::address_of(sender), + amount + ); + + if ( !smart_vector::contains(&config.requesters, &(signer::address_of(sender))) ) { + smart_vector::push_back(&mut config.requesters, signer::address_of(sender)); + }; + + // Update + vault_reserve.pt_total_supply = vault_reserve.pt_total_supply-amount; + + // Emit an event + event::emit( + RequestRedeem { + vault_name: type_name, + current_time: timestamp::now_seconds(), + pt_amount: amount, + sender: signer::address_of(sender) + } + ) + } + + // request exit when the vault is not matured, the amount returned + // - APT = PT / e^(rt) - exit fee% + public entry fun request_exit<P>(sender: &signer, amount: u64 ) acquires VaultReserve, VaultManager { + assert!(exists<VaultReserve<P>>(@legato_addr), ERR_INVALID_VAULT); + + let type_name = type_info::type_name<P>(); + + let config = borrow_global_mut<VaultManager>(@legato_addr); + + let vault_config = table::borrow_mut( &mut config.vault_config, type_name ); + let vault_reserve = borrow_global_mut<VaultReserve<P>>(@legato_addr); + + assert!(vault_config.maturity_time > timestamp::now_seconds() , ERR_VAULT_MATURED); + + // Burn PT tokens on the sender's account + let pt_coin = coin::withdraw<PT_TOKEN<P>>(sender, amount); + coin::burn(pt_coin, &vault_reserve.pt_burn); + + let adjusted_amount = calculate_exit_amount(vault_config.vault_apy, timestamp::now_seconds(), vault_config.maturity_time, amount); + + // Add the request to the withdrawal list + table::add( + &mut config.pending_unstake, + signer::address_of(sender), + adjusted_amount + ); + + if ( !smart_vector::contains(&config.requesters, &(signer::address_of(sender))) ) { + smart_vector::push_back(&mut config.requesters, signer::address_of(sender)); + }; + + // Update + vault_reserve.pt_total_supply = vault_reserve.pt_total_supply-amount; + + // Emit an event + event::emit( + RequestExit { + vault_name: type_name, + current_time: timestamp::now_seconds(), + pt_amount: amount, + exit_amount: adjusted_amount, + sender: signer::address_of(sender) + } + ) + } + + // Calculate the amount of PT debt to be sent out using the formula P = S * e^(rt) + public fun calculate_pt_debt_amount(apy: FixedPoint64, from_timestamp: u64, to_timestamp: u64, input_amount: u64): u64 { + + // Calculate time duration in years + let time = fixed_point64::create_from_rational( ((to_timestamp-from_timestamp) as u128), 31556926 ); + + // Calculate rt (rate * time) + let rt = math_fixed64::mul_div( apy, time, fixed_point64::create_from_u128(1)); + let multiplier = math_fixed64::exp(rt); + + // the final PT debt amount + ( fixed_point64::multiply_u128( (input_amount as u128), multiplier ) as u64 ) + } + + // Calculate the amount when exiting using the formula S = P / e^(rt + public fun calculate_exit_amount(apy: FixedPoint64, from_timestamp: u64, to_timestamp: u64, output_amount: u64) : u64 { + + let time = fixed_point64::create_from_rational( ((to_timestamp-from_timestamp) as u128), 31556926 ); + + let rt = math_fixed64::mul_div( apy, time, fixed_point64::create_from_u128(1)); + let denominator = math_fixed64::exp(rt); + + ( fixed_point64::divide_u128( (output_amount as u128), denominator ) as u64 ) + } + + #[view] + // get PT balance from the given account + public fun get_pt_balance<P>(account: address): u64 { + coin::balance<PT_TOKEN<P>>(account) + } + + #[view] + public fun get_config_object_address() : address acquires VaultManager { + let config = borrow_global_mut<VaultManager>(@legato_addr); + let config_object_signer = object::generate_signer_for_extending(&config.extend_ref); + signer::address_of(&config_object_signer) + } + + // ======== Only Governance ========= + + // Create a new vault + public entry fun new_vault<P>( + sender: &signer, + maturity_time: u64, + apy_numerator: u128, + apy_denominator: u128 + ) acquires VaultManager { + assert!( signer::address_of(sender) == @legato_addr , ERR_UNAUTHORIZED); + assert!( signer::address_of(sender) == @legato_addr , ERR_UNAUTHORIZED); + // Check if the vault already exists + assert!(!vault_exist<P>(@legato_addr), ERR_VAULT_EXISTS); + // The maturity date should not have passed. + assert!(maturity_time > timestamp::now_seconds()+86400, ERR_INVALID_MATURITY); + // Ensure the current maturity date is greater than that of the previous vault. + assert_maturity_time<P>(@legato_addr, maturity_time); + + let config = borrow_global_mut<VaultManager>(@legato_addr); + + // Construct the name and symbol of the PT token + let type_name = type_info::type_name<P>(); + let index = string::index_of(&type_name, &utf8(b"vault_token_name::")); + assert!(index != string::length(&type_name) , ERR_INVALID_TYPE ); + let token_name = string::utf8(b"PT-"); + let token_symbol = string::sub_string( &type_name, index+18, string::length(&type_name)); + string::append( &mut token_name, token_symbol); + + // Initialize vault's PT token + let (pt_burn, freeze_cap, pt_mint) = coin::initialize<PT_TOKEN<P>>( + sender, + token_name, + token_symbol, + 8, // Number of decimal places + true, // token is fungible + ); + + coin::destroy_freeze_cap(freeze_cap); + + let vault_apy = fixed_point64::create_from_rational( apy_numerator, apy_denominator ); + + let vault_config = VaultConfig { + maturity_time, + vault_apy, + enable_mint: true, + enable_redeem: true, + enable_exit: true + }; + + move_to( + sender, + VaultReserve<P> { + pt_mint, + pt_burn, + pt_total_supply: 0 + }, + ); + + // Add the vault name to the list. + smart_vector::push_back(&mut config.vault_list, type_name); + + // Add the configuration to the config table. + table::add( + &mut config.vault_config, + type_name, + vault_config + ); + + // Emit an event + event::emit( + NewVault { + vault_name: type_name, + maturity_time, + vault_apy: fixed_point64::get_raw_value(vault_apy) + } + ) + + } + + // Update the batch amount for staking. + public entry fun update_batch_amount(sender: &signer, new_amount: u64) acquires VaultManager { + assert!( signer::address_of(sender) == @legato_addr , ERR_UNAUTHORIZED); + let config = borrow_global_mut<VaultManager>(@legato_addr); + config.batch_amount = new_amount; + } + + // Add a validator to the whitelist. + public entry fun add_whitelist(sender: &signer, whitelist_address: address) acquires VaultManager { + assert!( signer::address_of(sender) == @legato_addr , ERR_UNAUTHORIZED); + let config = borrow_global_mut<VaultManager>(@legato_addr); + smart_vector::push_back(&mut config.delegator_pools, whitelist_address); + } + + // Remove a validator from the whitelist. + public entry fun remove_whitelist(sender: &signer, whitelist_address: address) acquires VaultManager { + assert!( signer::address_of(sender) == @legato_addr , ERR_UNAUTHORIZED); + let config = borrow_global_mut<VaultManager>(@legato_addr); + let (found, idx) = smart_vector::index_of<address>(&config.delegator_pools, &whitelist_address); + assert!( found , ERR_INVALID_ADDRESS); + smart_vector::swap_remove<address>(&mut config.delegator_pools, idx ); + } + + // Admin proceeds to unlock APT for further withdrawal according to the request table. + public entry fun admin_proceed_unstake(sender: &signer, validator_address: address) acquires VaultManager { + assert!( signer::address_of(sender) == @legato_addr , ERR_UNAUTHORIZED); + + let config = borrow_global_mut<VaultManager>(@legato_addr); + let config_object_signer = object::generate_signer_for_extending(&config.extend_ref); + let total_requester = smart_vector::length( &config.requesters ); + + if (total_requester > 0) { + + let requester_count = 0; + let total_unlock = 0; + + while ( requester_count < total_requester) { + let requester_address = *smart_vector::borrow( &config.requesters, requester_count ); + let unlock_amount = table::remove( &mut config.pending_unstake, requester_address); + + if (unlock_amount > 0) { + table::add( + &mut config.pending_withdrawal, + requester_address, + unlock_amount + ); + + total_unlock = total_unlock+unlock_amount; + }; + + requester_count = requester_count+1; + }; + + let pool_address = dp::get_owned_pool_address(validator_address); + dp::unlock(&config_object_signer, pool_address, total_unlock); + + config.unlocked_amount = config.unlocked_amount + total_unlock; + }; + + } + + // Admin proceeds with withdrawal of unlocked APT tokens. + public entry fun admin_proceed_withdrawal(sender: &signer, validator_address: address) acquires VaultManager { + assert!( signer::address_of(sender) == @legato_addr , ERR_UNAUTHORIZED); + + let config = borrow_global_mut<VaultManager>(@legato_addr); + let config_object_signer = object::generate_signer_for_extending(&config.extend_ref); + + // Proceed with withdrawal if there are unlocked tokens. + if ( config.unlocked_amount > 0) { + + // Withdraw the unlocked APT tokens from the delegation pool. + let pool_address = dp::get_owned_pool_address(validator_address); + dp::withdraw(&config_object_signer, pool_address, config.unlocked_amount); + + // Retrieve the total withdrawn APT tokens. + let total_apt_withdrawn = coin::balance<AptosCoin>( signer::address_of(&config_object_signer) ); + + let total_requester = smart_vector::length( &config.requesters ); + let requester_count = 0; + + // Loop through each requester and process their withdrawal. + while ( requester_count < total_requester) { + let requester_address = *smart_vector::borrow( &config.requesters, requester_count ); + let withdraw_amount = table::remove( &mut config.pending_withdrawal, requester_address); + + if (withdraw_amount > 0) { + // Ensure withdrawal amount does not exceed the available balance. + if (withdraw_amount > total_apt_withdrawn) { + withdraw_amount = total_apt_withdrawn; + }; + let apt_coin = coin::withdraw<AptosCoin>(&config_object_signer, withdraw_amount); + coin::deposit(requester_address, apt_coin); + + // Emit an event + event::emit( + Withdrawn { + withdraw_amount, + requester: requester_address + } + ); + + }; + + requester_count = requester_count+1; + }; + + // Reset the unlocked amount after processing withdrawals. + config.unlocked_amount = 0; + }; + + } + + + // ======== Internal Functions ========= + + fun vault_exist<P>(addr: address): bool { + exists<VaultReserve<P>>(addr) + } + + fun assert_maturity_time<P>(addr: address, maturity_time: u64) acquires VaultManager { + + let global_config = borrow_global_mut<VaultManager>(addr); + if ( smart_vector::length( &global_config.vault_list ) > 0) { + let recent_vault_name = *smart_vector::borrow( &global_config.vault_list, smart_vector::length( &global_config.vault_list )-1 ); + let recent_vault_config = table::borrow_mut( &mut global_config.vault_config, recent_vault_name ); + assert!( maturity_time > recent_vault_config.maturity_time, ERR_INVALID_MATURITY ); + }; + + } + + #[test_only] + public fun init_module_for_testing(deployer: &signer) { + init_module(deployer) + } } \ No newline at end of file diff --git a/packages/aptos/tests/amm_tests.move b/packages/aptos/tests/amm_tests.move new file mode 100644 index 0000000..62bc24a --- /dev/null +++ b/packages/aptos/tests/amm_tests.move @@ -0,0 +1,187 @@ + + +#[test_only] +module legato_addr::amm_tests { + + use std::string::utf8; + use std::signer; + + use aptos_framework::account; + use aptos_framework::coin::{Self, MintCapability}; + + use legato_addr::mock_usdc::{Self, USDC_TOKEN}; + use legato_addr::mock_legato::{Self, LEGATO_TOKEN}; + use legato_addr::amm::{Self, LP}; + + // When setting up a 90/10 pool of ~$100k + // Initial allocation at 1 XBTC = 50,000 USDC + const XBTC_AMOUNT: u64 = 180_000_000; // 90% at 1.8 BTC + const USDC_AMOUNT_90_10: u64 = 10000_000000; // 10% at 10,000 USDC + + // When setting up a 50/50 pool of ~$100k + // Initial allocation at 1 LEGATO = 0.001 USDC + const LEGATO_AMOUNT: u64 = 50000000_00000000; // 50,000,000 LEGATO + const USDC_AMOUNT_50_50: u64 = 50000_000000; // 50,000 USDC + + // test coins + + struct XBTC_TOKEN {} + + // Registering pools + #[test(deployer = @legato_addr, lp_provider = @0xdead, user = @0xbeef )] + fun test_register_pools(deployer: &signer, lp_provider: &signer, user: &signer) { + register_pools(deployer, lp_provider, user); + } + + // Swapping tokens + #[test(deployer = @legato_addr, lp_provider = @0xdead, user = @0xbeef )] + fun test_swap_usdc_for_xbtc(deployer: &signer, lp_provider: &signer, user: &signer) { + register_pools(deployer, lp_provider, user); + + mock_usdc::mint( user , 100_000000 ); // 100 USDC + amm::swap<USDC_TOKEN, XBTC_TOKEN>(user, 100_000000, 0); // 100 USDC + + assert!(coin::balance<XBTC_TOKEN>(signer::address_of(user)) == 197_906, 1); // 0.00197906 XBTC at a rate of 1 BTC = 52405 USDT + } + + #[test(deployer = @legato_addr, lp_provider = @0xdead, user = @0xbeef )] + fun test_swap_xbtc_for_usdc(deployer: &signer, lp_provider: &signer, user: &signer) { + register_pools(deployer, lp_provider, user); + + amm::swap<XBTC_TOKEN, USDC_TOKEN>(lp_provider, 100000, 0); // 0.001 XBTC + + assert!(coin::balance<USDC_TOKEN>(signer::address_of(lp_provider)) == 49_613272, 1); // 49.613272 USDC at a rate of 1 BTC = 51465 USDT + } + + #[test(deployer = @legato_addr, lp_provider = @0xdead, user = @0xbeef )] + fun test_swap_usdc_for_legato(deployer: &signer, lp_provider: &signer, user: &signer) { + register_pools(deployer, lp_provider, user); + + mock_usdc::mint( user , 250_000000 ); // 250 USDC + amm::swap<USDC_TOKEN, LEGATO_TOKEN>(user, 250_000000, 0); // 250 USDC + + assert!(coin::balance<LEGATO_TOKEN>(signer::address_of(user)) == 247518_59598004, 1); // 247,518 LEGATO at a rate of 0.001010028 LEGATO/USDC + } + + #[test(deployer = @legato_addr, lp_provider = @0xdead, user = @0xbeef )] + fun test_swap_legato_for_usdc(deployer: &signer, lp_provider: &signer, user: &signer) { + register_pools(deployer, lp_provider, user); + + mock_legato::mint( user, 100000_00000000 ); // 100,000 LEGATO + amm::swap<LEGATO_TOKEN, USDC_TOKEN>(user, 100000_00000000, 0); // 100,000 LEGATO + + assert!(coin::balance<USDC_TOKEN>(signer::address_of(user)) == 99_302388, 1); // 99.302 USDC at a rate of 0.00099302 LEGATO/USDC + } + + #[test(deployer = @legato_addr, lp_provider = @0xdead, user = @0xbeef )] + fun test_remove_liquidity(deployer: &signer, lp_provider: &signer, user: &signer) { + register_pools(deployer, lp_provider, user); + + mock_usdc::mint(lp_provider, 5000_000000); + + amm::add_liquidity<USDC_TOKEN, XBTC_TOKEN>( + lp_provider, + 5000_000000, // 5000 USDC + 1, + 15000000, // 0.15 XBTC + 1 + ); + + let lp_balance = coin::balance<LP<USDC_TOKEN, XBTC_TOKEN>>( signer::address_of(lp_provider) ); + + amm::remove_liquidity<USDC_TOKEN, XBTC_TOKEN>( + lp_provider, + lp_balance + ); + + + } + + #[test_only] + public fun register_pools(deployer: &signer, lp_provider: &signer, user: &signer) { + + amm::init_module_for_testing(deployer); + mock_usdc::init_module_for_testing(deployer); + mock_legato::init_module_for_testing(deployer); + + let deployer_address = signer::address_of(deployer); + let lp_provider_address = signer::address_of(lp_provider); + let user_address = signer::address_of(user); + + account::create_account_for_test(lp_provider_address); + account::create_account_for_test(deployer_address); + account::create_account_for_test(user_address); + account::create_account_for_test( amm::get_config_object_address() ); + + // USDC + mock_usdc::mint( deployer , USDC_AMOUNT_50_50+USDC_AMOUNT_90_10 ); + assert!( (coin::balance<USDC_TOKEN>( deployer_address )) == USDC_AMOUNT_50_50+USDC_AMOUNT_90_10, 0 ); + + // LEGATO + mock_legato::mint( deployer , LEGATO_AMOUNT ); + assert!( (coin::balance<LEGATO_TOKEN>( deployer_address )) == LEGATO_AMOUNT, 0 ); + + // XBTC + coin::register<XBTC_TOKEN>(deployer); + coin::register<XBTC_TOKEN>(lp_provider); + coin::register<XBTC_TOKEN>(user); + let xbtc_mint_cap = register_coin<XBTC_TOKEN>(deployer, b"BTC", b"BTC", 8); + coin::deposit(deployer_address, coin::mint<XBTC_TOKEN>(XBTC_AMOUNT, &xbtc_mint_cap)); + coin::deposit(lp_provider_address, coin::mint<XBTC_TOKEN>(XBTC_AMOUNT, &xbtc_mint_cap)); + coin::destroy_mint_cap(xbtc_mint_cap); + assert!(coin::balance<XBTC_TOKEN>(deployer_address) == XBTC_AMOUNT, 1); + + // Setup a 10/90 pool + amm::register_pool<USDC_TOKEN, XBTC_TOKEN>(deployer, 1000, 9000); + + amm::add_liquidity<USDC_TOKEN, XBTC_TOKEN>( + deployer, + USDC_AMOUNT_90_10, + 1, + XBTC_AMOUNT, + 1 + ); + + assert!(coin::balance<USDC_TOKEN>(deployer_address) == USDC_AMOUNT_50_50, 2); + assert!(coin::balance<XBTC_TOKEN>(deployer_address) == 0, 3); + + assert!(coin::balance<LP<USDC_TOKEN, XBTC_TOKEN>>(deployer_address) == 2_68994649, 4); + + // Setup a 50/50 pool + amm::register_pool<USDC_TOKEN, LEGATO_TOKEN>(deployer, 5000, 5000); + + amm::add_liquidity<USDC_TOKEN, LEGATO_TOKEN>( + deployer, + USDC_AMOUNT_50_50, + 1, + LEGATO_AMOUNT, + 1 + ); + + assert!(coin::balance<USDC_TOKEN>(deployer_address) == 0, 5); + assert!(coin::balance<LEGATO_TOKEN>(deployer_address) == 0, 6); + + } + + + #[test_only] + fun register_coin<CoinType>( + coin_admin: &signer, + name: vector<u8>, + symbol: vector<u8>, + decimals: u8 + ): MintCapability<CoinType> { + let (burn_cap, freeze_cap, mint_cap) = + coin::initialize<CoinType>( + coin_admin, + utf8(name), + utf8(symbol), + decimals, + true); + coin::destroy_freeze_cap(freeze_cap); + coin::destroy_burn_cap(burn_cap); + + mint_cap + } + +} \ No newline at end of file diff --git a/packages/aptos/tests/aptos_system/delegation_pool_tests.move b/packages/aptos/tests/aptos_system/delegation_pool_tests.move new file mode 100644 index 0000000..ec08419 --- /dev/null +++ b/packages/aptos/tests/aptos_system/delegation_pool_tests.move @@ -0,0 +1,205 @@ + +#[test_only] +module legato_addr::delegation_pool_tests { + + // use std::features; + use std::signer; + + use aptos_std::bls12381; + use aptos_std::stake; + use aptos_std::vector; + + use aptos_framework::account; + use aptos_framework::aptos_coin::AptosCoin; + use aptos_framework::coin; + use aptos_framework::reconfiguration; + use aptos_framework::delegation_pool as dp; + use aptos_framework::timestamp; + + #[test_only] + const EPOCH_DURATION: u64 = 60; + + #[test_only] + const LOCKUP_CYCLE_SECONDS: u64 = 3600; + + #[test_only] + const MODULE_EVENT: u64 = 26; + + #[test_only] + const DELEGATION_POOLS: u64 = 11; + + #[test_only] + const OPERATOR_BENEFICIARY_CHANGE: u64 = 39; + + #[test_only] + const COMMISSION_CHANGE_DELEGATION_POOL: u64 = 42; + + #[test_only] + const ONE_APT: u64 = 100000000; // 1x10**8 + + #[test_only] + const VALIDATOR_STATUS_PENDING_ACTIVE: u64 = 1; + const VALIDATOR_STATUS_ACTIVE: u64 = 2; + const VALIDATOR_STATUS_PENDING_INACTIVE: u64 = 3; + const VALIDATOR_STATUS_INACTIVE: u64 = 4; + + + #[test(aptos_framework = @aptos_framework, validator = @0x123)] + public entry fun test_validator_staking( + aptos_framework: &signer, + validator: &signer, + ) { + initialize_for_test(aptos_framework); + let (_sk, pk, pop) = generate_identity(); + initialize_test_validator(&pk, &pop, validator, 100 * ONE_APT, true, true); + + // Validator has a lockup now that they've joined the validator set. + let validator_address = signer::address_of(validator); + let pool_address = dp::get_owned_pool_address(validator_address); + assert!(stake::get_remaining_lockup_secs(pool_address) == LOCKUP_CYCLE_SECONDS, 1); + + // Validator adds more stake while already being active. + // The added stake should go to pending_active to wait for activation when next epoch starts. + stake::mint(validator, 900 * ONE_APT); + dp::add_stake(validator, pool_address, 100 * ONE_APT); + assert!(coin::balance<AptosCoin>(validator_address) == 800 * ONE_APT, 2); + stake::assert_validator_state(pool_address, 100 * ONE_APT, 0, 100 * ONE_APT, 0, 0); + + // Pending_active stake is activated in the new epoch. + // Rewards of 1 coin are also distributed for the existing active stake of 100 coins. + end_epoch(); + assert!(stake::get_validator_state(pool_address) == VALIDATOR_STATUS_ACTIVE, 3); + stake::assert_validator_state(pool_address, 201 * ONE_APT, 0, 0, 0, 0); + + // Request unlock of 100 coins. These 100 coins are moved to pending_inactive and will be unlocked when the + // current lockup expires. + dp::unlock(validator, pool_address, 100 * ONE_APT); + stake::assert_validator_state(pool_address, 10100000001, 0, 0, 9999999999, 0); + + // Enough time has passed so the current lockup cycle should have ended. + // The first epoch after the lockup cycle ended should automatically move unlocked (pending_inactive) stake + // to inactive. + timestamp::fast_forward_seconds(LOCKUP_CYCLE_SECONDS); + end_epoch(); + // Rewards were also minted to pending_inactive, which got all moved to inactive. + stake::assert_validator_state(pool_address, 10201000001, 10099999998, 0, 0, 0); + // Lockup is renewed and validator is still active. + assert!(stake::get_validator_state(pool_address) == VALIDATOR_STATUS_ACTIVE, 4); + assert!(stake::get_remaining_lockup_secs(pool_address) == LOCKUP_CYCLE_SECONDS, 5); + + // Validator withdraws from inactive stake multiple times. + dp::withdraw(validator, pool_address, 50 * ONE_APT); + assert!(coin::balance<AptosCoin>(validator_address) == 84999999999, 6); + stake::assert_validator_state(pool_address, 10201000001, 5099999999, 0, 0, 0); + dp::withdraw(validator, pool_address, 51 * ONE_APT); + assert!(coin::balance<AptosCoin>(validator_address) == 90099999998, 7); + stake::assert_validator_state(pool_address, 10201000001, 0, 0, 0, 0); + + // Enough time has passed again and the validator's lockup is renewed once more. Validator is still active. + timestamp::fast_forward_seconds(LOCKUP_CYCLE_SECONDS); + end_epoch(); + + assert!(stake::get_validator_state(pool_address) == VALIDATOR_STATUS_ACTIVE, 8); + assert!(stake::get_remaining_lockup_secs(pool_address) == LOCKUP_CYCLE_SECONDS, 9); + } + + #[test_only] + public fun initialize_for_test(aptos_framework: &signer) { + initialize_for_test_custom( + aptos_framework, + 100 * ONE_APT, + 10000 * ONE_APT, + LOCKUP_CYCLE_SECONDS, + true, + 1, + 100, + 1000000 + ); + } + + #[test_only] + public fun end_epoch() { + stake::end_epoch(); + reconfiguration::reconfigure_for_test_custom(); + } + + // Convenient function for setting up all required stake initializations. + #[test_only] + public fun initialize_for_test_custom( + aptos_framework: &signer, + minimum_stake: u64, + maximum_stake: u64, + recurring_lockup_secs: u64, + allow_validator_set_change: bool, + rewards_rate_numerator: u64, + rewards_rate_denominator: u64, + voting_power_increase_limit: u64, + ) { + account::create_account_for_test(signer::address_of(aptos_framework)); + stake::initialize_for_test_custom( + aptos_framework, + minimum_stake, + maximum_stake, + recurring_lockup_secs, + allow_validator_set_change, + rewards_rate_numerator, + rewards_rate_denominator, + voting_power_increase_limit + ); + reconfiguration::initialize_for_test(aptos_framework); + // features::change_feature_flags(aptos_framework, vector[ + // DELEGATION_POOLS, + // MODULE_EVENT, + // OPERATOR_BENEFICIARY_CHANGE, + // COMMISSION_CHANGE_DELEGATION_POOL + // ], vector[]); + } + + #[test_only] + public fun generate_identity(): (bls12381::SecretKey, bls12381::PublicKey, bls12381::ProofOfPossession) { + let (sk, pkpop) = bls12381::generate_keys(); + let pop = bls12381::generate_proof_of_possession(&sk); + let unvalidated_pk = bls12381::public_key_with_pop_to_normal(&pkpop); + (sk, unvalidated_pk, pop) + } + + #[test_only] + public fun initialize_test_validator( + public_key: &bls12381::PublicKey, + proof_of_possession: &bls12381::ProofOfPossession, + validator: &signer, + amount: u64, + should_join_validator_set: bool, + should_end_epoch: bool + ) { + let validator_address = signer::address_of(validator); + if (!account::exists_at(signer::address_of(validator))) { + account::create_account_for_test(validator_address); + }; + + dp::initialize_delegation_pool(validator, 0, vector::empty<u8>()); + validator_address = dp::get_owned_pool_address(validator_address); + + let pk_bytes = bls12381::public_key_to_bytes(public_key); + let pop_bytes = bls12381::proof_of_possession_to_bytes(proof_of_possession); + stake::rotate_consensus_key(validator, validator_address, pk_bytes, pop_bytes); + + if (amount > 0) { + mint_and_add_stake(validator, amount); + }; + + if (should_join_validator_set) { + stake::join_validator_set(validator, validator_address); + }; + if (should_end_epoch) { + end_epoch(); + }; + } + + #[test_only] + public fun mint_and_add_stake(account: &signer, amount: u64) { + stake::mint(account, amount); + dp::add_stake(account, dp::get_owned_pool_address(signer::address_of(account)), amount); + } + +} \ No newline at end of file diff --git a/packages/aptos/tests/lbp_usdc_tests.move b/packages/aptos/tests/lbp_usdc_tests.move new file mode 100644 index 0000000..0713cd6 --- /dev/null +++ b/packages/aptos/tests/lbp_usdc_tests.move @@ -0,0 +1,108 @@ +// Test launching new tokens (LEGATO) on LBP when using USDC as the settlement assets + +#[test_only] +module legato_addr::lbp_usdc_tests { + + use std::signer; + + use aptos_framework::account; + use aptos_framework::coin::{Self}; + + use legato_addr::mock_usdc::{Self, USDC_TOKEN}; + use legato_addr::mock_legato::{Self, LEGATO_TOKEN}; + use legato_addr::amm::{Self, LP}; + + const LEGATO_AMOUNT: u64 = 60000000_00000000; // 60 mil. LEGATO + const USDC_AMOUNT: u64 = 500_000_000; // 500 USDC for bootstrap LP + + // Registering pools + #[test(deployer = @legato_addr, lp_provider = @0xdead, user = @0xbeef )] + fun test_register_pools(deployer: &signer, lp_provider: &signer, user: &signer) { + register_pools(deployer, lp_provider, user); + } + + #[test(deployer = @legato_addr, lp_provider = @0xdead, user = @0xbeef )] + fun test_trade_until_stabilized(deployer: &signer, lp_provider: &signer, user: &signer) { + register_pools(deployer, lp_provider, user); + + trade_until_stabilized(user); + } + + #[test_only] + public fun register_pools(deployer: &signer, lp_provider: &signer, user: &signer) { + + amm::init_module_for_testing(deployer); + mock_usdc::init_module_for_testing(deployer); + mock_legato::init_module_for_testing(deployer); + + let deployer_address = signer::address_of(deployer); + let lp_provider_address = signer::address_of(lp_provider); + let user_address = signer::address_of(user); + + account::create_account_for_test(lp_provider_address); + account::create_account_for_test(deployer_address); + account::create_account_for_test(user_address); + account::create_account_for_test( amm::get_config_object_address() ); + + // USDC + mock_usdc::mint( deployer , USDC_AMOUNT ); + assert!( (coin::balance<USDC_TOKEN>( deployer_address )) == USDC_AMOUNT, 0 ); + + // LEGATO + mock_legato::mint( deployer , LEGATO_AMOUNT ); + assert!( (coin::balance<LEGATO_TOKEN>( deployer_address )) == LEGATO_AMOUNT, 0 ); + + amm::register_lbp_pool<USDC_TOKEN, LEGATO_TOKEN >( + deployer, + false, // LEGATO is on Y + 9000, + 6000, + false, + 50000_000000 // 50,000 USDC + ); + + amm::add_liquidity<USDC_TOKEN, LEGATO_TOKEN>( + deployer, + USDC_AMOUNT, + 1, + LEGATO_AMOUNT, + 1 + ); + + } + + #[test_only] + public fun trade_until_stabilized( user: &signer) { + + // Buy LEGATO with 1000 USDC for 50 times + let counter= 0; + let current_weight_legato = 9000; + let current_weight_usdc = 1000; + + mock_usdc::mint( user, 50000_000000 ); // 50,000 USDC + + while ( counter < 50) { + amm::swap<USDC_TOKEN, LEGATO_TOKEN>(user, 1000_000000, 0); // 1,000 USDC + let current_balance = coin::balance<LEGATO_TOKEN>(signer::address_of(user)); + mock_legato::burn( user, current_balance ); + + // Check weights + let (weight_usdc, weight_legato, _, _ ) = amm::lbp_info<USDC_TOKEN, LEGATO_TOKEN>(); + // Keep lowering + assert!( current_weight_legato >= weight_legato, counter ); + assert!( current_weight_usdc <= weight_usdc, counter ); + + current_weight_legato = weight_legato; + current_weight_usdc = weight_usdc; + + counter = counter+1; + }; + + // Check final weights + let (weight_usdc, weight_legato, _, _ ) = amm::lbp_info<USDC_TOKEN, LEGATO_TOKEN>(); + assert!( weight_usdc == 4000, 0 ); + assert!( weight_legato == 6000, 1 ); + + } + +} \ No newline at end of file diff --git a/packages/aptos/tests/vault_tests.move b/packages/aptos/tests/vault_tests.move new file mode 100644 index 0000000..360e6a6 --- /dev/null +++ b/packages/aptos/tests/vault_tests.move @@ -0,0 +1,296 @@ + +#[test_only] +module legato_addr::vault_tests { + + use std::features; + use std::signer; + + use aptos_std::bls12381; + use aptos_std::stake; + use aptos_std::vector; + + use aptos_framework::account; + use aptos_framework::aptos_coin::AptosCoin; + use aptos_framework::coin; + use aptos_framework::reconfiguration; + use aptos_framework::delegation_pool as dp; + use aptos_framework::timestamp; + + use legato_addr::vault; + use legato_addr::vault_token_name::{MAR_2024, JUN_2024}; + + #[test_only] + const EPOCH_DURATION: u64 = 86400; + + #[test_only] + const ONE_APT: u64 = 100000000; // 1x10**8 + + #[test_only] + const LOCKUP_CYCLE_SECONDS: u64 = 3600; + + #[test_only] + const DELEGATION_POOLS: u64 = 11; + + #[test_only] + const MODULE_EVENT: u64 = 26; + + #[test(deployer = @legato_addr,aptos_framework = @aptos_framework, validator_1 = @0xdead, validator_2 = @0x1111, user_1 = @0xbeef, user_2 = @0xfeed)] + fun test_mint_redeem( + deployer: &signer, + aptos_framework: &signer, + validator_1: &signer, + validator_2: &signer, + user_1: &signer, + user_2: &signer + ) { + initialize_for_test(aptos_framework, validator_1, validator_2); + + // Setup Legato vaults + setup_vaults(deployer, signer::address_of(validator_1), signer::address_of(validator_2)); + + // Prepare test accounts + create_test_accounts( deployer, user_1, user_2); + + // Mint APT tokens + stake::mint(user_1, 100 * ONE_APT); + stake::mint(user_2, 200 * ONE_APT); + + assert!(coin::balance<AptosCoin>(signer::address_of(user_1)) == 100 * ONE_APT, 0); + assert!(coin::balance<AptosCoin>(signer::address_of(user_2)) == 200 * ONE_APT, 1); + + // Stake PT tokens. + vault::mint<MAR_2024>( user_1, 100 * ONE_APT); + vault::mint<MAR_2024>( user_2, 200 * ONE_APT); + + // Check PT token balances. + let pt_amount_1 = vault::get_pt_balance<MAR_2024>(signer::address_of(user_1)); + let pt_amount_2 = vault::get_pt_balance<MAR_2024>(signer::address_of(user_2)); + assert!( pt_amount_1 == 101_37836771, 2); // 101.378 PT + assert!( pt_amount_2 == 202_75673543, 3); // 202.756 PT + + // Fast forward 100 epochs. + let i:u64=1; + while(i <= 100) + { + timestamp::fast_forward_seconds(EPOCH_DURATION); + end_epoch(); + i=i+1; // Incrementing the counter + }; + + // Check the staked amount. + let pool_address = dp::get_owned_pool_address(signer::address_of(validator_1) ); + let (pool_staked_amount,_,_) = dp::get_stake(pool_address , vault::get_config_object_address() ); + + assert!( pool_staked_amount == 331_20350570, 4); // 331.203 APT + + // Request redemption of PT tokens. + vault::request_redeem<MAR_2024>( user_1, 101_37836771 ); + + // Perform admin tasks. + vault::admin_proceed_unstake(deployer, signer::address_of(validator_1) ); + + // Fast forward one epoch. + timestamp::fast_forward_seconds(EPOCH_DURATION); + end_epoch(); + + vault::admin_proceed_withdrawal( deployer , signer::address_of(validator_1)); + + // Verify has the correct amount of APT tokens after redemption. + let apt_amount = coin::balance<AptosCoin>(signer::address_of(user_1)); + assert!( apt_amount == 101_37836770, 5); + } + + #[test(deployer = @legato_addr,aptos_framework = @aptos_framework, validator_1 = @0xdead, validator_2 = @0x1111, user_1 = @0xbeef, user_2 = @0xfeed)] + fun test_mint_exit( + deployer: &signer, + aptos_framework: &signer, + validator_1: &signer, + validator_2: &signer, + user_1: &signer, + user_2: &signer + ) { + initialize_for_test(aptos_framework, validator_1, validator_2); + + // Setup Legato vaults + setup_vaults(deployer, signer::address_of(validator_1), signer::address_of(validator_2)); + + // Prepare test accounts + create_test_accounts( deployer, user_1, user_2); + + // Stake PT tokens. + stake::mint(user_1, 100 * ONE_APT); + stake::mint(user_2, 200 * ONE_APT); + + vault::mint<MAR_2024>( user_1, 100 * ONE_APT); + vault::mint<MAR_2024>( user_2, 200 * ONE_APT); + + // Fast forward 10 epochs. + let i:u64=1; + while(i <= 10) + { + timestamp::fast_forward_seconds(EPOCH_DURATION); + end_epoch(); + i=i+1; // Incrementing the counter + }; + + let amount_before = vault::get_pt_balance<MAR_2024>(signer::address_of(user_1)); + assert!( amount_before == 101_37836771, 0); // 101.378 PT + + // Request exit + vault::request_exit<MAR_2024>( user_1, amount_before ); + + // Perform admin tasks. + vault::admin_proceed_unstake(deployer, signer::address_of(validator_1) ); + + // Fast forward one epoch. + timestamp::fast_forward_seconds(EPOCH_DURATION); + end_epoch(); + + vault::admin_proceed_withdrawal( deployer , signer::address_of(validator_1)); + + let apt_amount = coin::balance<AptosCoin>(signer::address_of(user_1)); + assert!( apt_amount == 100_13708438, 0); // 100.137 PT + + } + + #[test_only] + public fun setup_vaults(sender: &signer, validator_1: address, validator_2: address) { + + vault::init_module_for_testing(sender); + + // Update the batch amount to 200 APT. + vault::update_batch_amount(sender, 200 * ONE_APT ); + + // Add the validators to the whitelist. + vault::add_whitelist(sender, validator_1); + vault::add_whitelist(sender, validator_2); + + // Vault #1 matures in 100 epochs. + let maturity_1 = timestamp::now_seconds()+(100*EPOCH_DURATION); + + // Create Vault #1 with an APY of 5%. + vault::new_vault<MAR_2024>(sender, maturity_1, 5, 100); + + // Vault #2 matures in 200 epochs. + let maturity_2 = timestamp::now_seconds()+(200*EPOCH_DURATION); + + // Create Vault #2 with an APY of 4%. + vault::new_vault<JUN_2024>(sender, maturity_2, 4, 100); + } + + #[test_only] + public fun create_test_accounts( + deployer: &signer, + user_1: &signer, + user_2: &signer + ) { + account::create_account_for_test(signer::address_of(user_1)); + account::create_account_for_test(signer::address_of(user_2)); + account::create_account_for_test(signer::address_of(deployer)); + account::create_account_for_test( vault::get_config_object_address() ); + } + + #[test_only] + public fun initialize_for_test( + aptos_framework: &signer, + validator_1: &signer, + validator_2: &signer + ) { + initialize_for_test_custom( + aptos_framework, + 100 * ONE_APT, + 10000 * ONE_APT, + LOCKUP_CYCLE_SECONDS, + true, + 1, + 1000, + 1000000 + ); + let (_sk_1, pk_1, pop_1) = generate_identity(); + + initialize_test_validator(&pk_1, &pop_1, validator_1, 1000 * ONE_APT, true, false); + let (_sk_2, pk_2, pop_2) = generate_identity(); + initialize_test_validator(&pk_2, &pop_2, validator_2, 2000 * ONE_APT, true, true); + } + + #[test_only] + public fun end_epoch() { + stake::end_epoch(); + reconfiguration::reconfigure_for_test_custom(); + } + + // Convenient function for setting up all required stake initializations. + #[test_only] + public fun initialize_for_test_custom( + aptos_framework: &signer, + minimum_stake: u64, + maximum_stake: u64, + recurring_lockup_secs: u64, + allow_validator_set_change: bool, + rewards_rate_numerator: u64, + rewards_rate_denominator: u64, + voting_power_increase_limit: u64, + ) { + account::create_account_for_test(signer::address_of(aptos_framework)); + stake::initialize_for_test_custom( + aptos_framework, + minimum_stake, + maximum_stake, + recurring_lockup_secs, + allow_validator_set_change, + rewards_rate_numerator, + rewards_rate_denominator, + voting_power_increase_limit + ); + reconfiguration::initialize_for_test(aptos_framework); + // features::change_feature_flags(aptos_framework, vector[DELEGATION_POOLS, MODULE_EVENT], vector[]); + } + + #[test_only] + public fun generate_identity(): (bls12381::SecretKey, bls12381::PublicKey, bls12381::ProofOfPossession) { + let (sk, pkpop) = bls12381::generate_keys(); + let pop = bls12381::generate_proof_of_possession(&sk); + let unvalidated_pk = bls12381::public_key_with_pop_to_normal(&pkpop); + (sk, unvalidated_pk, pop) + } + + #[test_only] + public fun initialize_test_validator( + public_key: &bls12381::PublicKey, + proof_of_possession: &bls12381::ProofOfPossession, + validator: &signer, + amount: u64, + should_join_validator_set: bool, + should_end_epoch: bool + ) { + let validator_address = signer::address_of(validator); + if (!account::exists_at(signer::address_of(validator))) { + account::create_account_for_test(validator_address); + }; + + dp::initialize_delegation_pool(validator, 0, vector::empty<u8>()); + validator_address = dp::get_owned_pool_address(validator_address); + + let pk_bytes = bls12381::public_key_to_bytes(public_key); + let pop_bytes = bls12381::proof_of_possession_to_bytes(proof_of_possession); + stake::rotate_consensus_key(validator, validator_address, pk_bytes, pop_bytes); + + if (amount > 0) { + mint_and_add_stake(validator, amount); + }; + + if (should_join_validator_set) { + stake::join_validator_set(validator, validator_address); + }; + if (should_end_epoch) { + end_epoch(); + }; + } + + #[test_only] + public fun mint_and_add_stake(account: &signer, amount: u64) { + stake::mint(account, amount); + dp::add_stake(account, dp::get_owned_pool_address(signer::address_of(account)), amount); + } + +} \ No newline at end of file