Swap: Solidity Style ve(3,3) DEX

View on Github
Smart Contract
DeFi
Useful modules to use in a DEX implementation.
Author: Kevin

ve(3,3) represents a voting escrow model emphasizing long-term token holding for governance power, where users lock tokens for voting rights, with power increasing with both the amount and duration of the lock.

A DEX, or Decentralized Exchange is a platform that enables users to exchange cryptocurrencies directly with each other.

This example implements modules including liquidity pool and coin wrappers that can be used in a DEX.


/// This module can be included in a project to enable internal wrapping and unwrapping of coins into fungible assets.
/// This allows the project to only have to store and process fungible assets in core data structures, while still be
/// able to support both native fungible assets and coins. Note that the wrapper fungible assets are INTERNAL ONLY and
/// are not meant to be released to user's accounts outside of the project. Othwerwise, this would create multiple
/// conflicting fungible asset versions of a specific coin in the ecosystem.
///
/// The flow works as follows:
/// 1. Add the coin_wrapper module to the project.
/// 2. Add a friend declaration for any core modules that needs to call wrap/unwrap. Wrap/Unwrap are both friend-only
/// functions so external modules cannot call them and leak the internal fungible assets outside of the project.
/// 3. Add entry functions in the core modules that take coins. Those functions will be calling wrap to create the
/// internal fungible assets and store them.
/// 4. Add entry functions in the core modules that return coins. Those functions will be extract internal fungible
/// assets from the core data structures, unwrap them into and return the coins to the end users.
///
/// The fungible asset wrapper for a coin has the same name, symbol and decimals as the original coin. This allows for
/// easier accounting and tracking of the deposited/withdrawn coins.
module swap::coin_wrapper {
use aptos_framework::account::{Self, SignerCapability};
use aptos_framework::aptos_account;
use aptos_framework::coin::{Self, Coin};
use aptos_framework::fungible_asset::{Self, BurnRef, FungibleAsset, Metadata, MintRef};
use aptos_framework::object::{Self, Object};
use aptos_framework::primary_fungible_store;
use aptos_std::smart_table::{Self, SmartTable};
use aptos_std::string_utils;
use aptos_std::type_info;
use std::string::{Self, String};
use std::option;
use std::signer;
use swap::package_manager;
// Modules in the same package that need to wrap/unwrap coins need to be added as friends here.
friend swap::router;
const COIN_WRAPPER_NAME: vector<u8> = b"COIN_WRAPPER";
/// Stores the refs for a specific fungible asset wrapper for wrapping and unwrapping.
struct FungibleAssetData has store {
// Used during unwrapping to burn the internal fungible assets.
burn_ref: BurnRef,
// Reference to the metadata object.
metadata: Object<Metadata>,
// Used during wrapping to mint the internal fungible assets.
mint_ref: MintRef,
}
/// The resource stored in the main resource account to track all the fungible asset wrappers.
/// This main resource account will also be the one holding all the deposited coins, each of which in a separate
/// CoinStore<CoinType> resource. See coin.move in the Aptos Framework for more details.
struct WrapperAccount has key {
// The signer cap used to withdraw deposited coins from the main resource account during unwrapping so the
// coins can be returned to the end users.
signer_cap: SignerCapability,
// Map from an original coin type (represented as strings such as "0x1::aptos_coin::AptosCoin") to the
// corresponding fungible asset wrapper.
coin_to_fungible_asset: SmartTable<String, FungibleAssetData>,
// Map from a fungible asset wrapper to the original coin type.
fungible_asset_to_coin: SmartTable<Object<Metadata>, String>,
}
/// Create the coin wrapper account to host all the deposited coins.
public entry fun initialize() {
if (is_initialized()) {
return
};
let swap_signer = &package_manager::get_signer();
let (coin_wrapper_signer, signer_cap) = account::create_resource_account(swap_signer, COIN_WRAPPER_NAME);
package_manager::add_address(string::utf8(COIN_WRAPPER_NAME), signer::address_of(&coin_wrapper_signer));
move_to(&coin_wrapper_signer, WrapperAccount {
signer_cap,
coin_to_fungible_asset: smart_table::new(),
fungible_asset_to_coin: smart_table::new(),
});
}
#[view]
public fun is_initialized(): bool {
package_manager::address_exists(string::utf8(COIN_WRAPPER_NAME))
}
#[view]
/// Return the address of the resource account that stores all deposited coins.
public fun wrapper_address(): address {
package_manager::get_address(string::utf8(COIN_WRAPPER_NAME))
}
#[view]
/// Return whether a specific CoinType has a wrapper fungible asset. This is only the case if at least one wrap()
/// call has been made for that CoinType.
public fun is_supported<CoinType>(): bool acquires WrapperAccount {
let coin_type = type_info::type_name<CoinType>();
smart_table::contains(&wrapper_account().coin_to_fungible_asset, coin_type)
}
#[view]
/// Return true if the given fungible asset is a wrapper fungible asset.
public fun is_wrapper(metadata: Object<Metadata>): bool acquires WrapperAccount {
smart_table::contains(&wrapper_account().fungible_asset_to_coin, metadata)
}
#[view]
/// Return the original CoinType for a specific wrapper fungible asset. This errors out if there's no such wrapper.
public fun get_coin_type(metadata: Object<Metadata>): String acquires WrapperAccount {
*smart_table::borrow(&wrapper_account().fungible_asset_to_coin, metadata)
}
#[view]
/// Return the wrapper fungible asset for a specific CoinType. This errors out if there's no such wrapper.
public fun get_wrapper<CoinType>(): Object<Metadata> acquires WrapperAccount {
fungible_asset_data<CoinType>().metadata
}
#[view]
/// Return the original CoinType if the given fungible asset is a wrapper fungible asset. Otherwise, return the
/// given fungible asset itself, which means it's a native fungible asset (not wrapped).
/// The return value is a String such as "0x1::aptos_coin::AptosCoin" for an original coin or "0x12345" for a native
/// fungible asset.
public fun get_original(fungible_asset: Object<Metadata>): String acquires WrapperAccount {
if (is_wrapper(fungible_asset)) {
get_coin_type(fungible_asset)
} else {
format_fungible_asset(fungible_asset)
}
}
#[view]
/// Return the address string of a fungible asset (e.g. "0x1234").
public fun format_fungible_asset(fungible_asset: Object<Metadata>): String {
let fa_address = object::object_address(&fungible_asset);
// This will create "@0x123"
let fa_address_str = string_utils::to_string(&fa_address);
// We want to strip the prefix "@"
string::sub_string(&fa_address_str, 1, string::length(&fa_address_str))
}
/// Wrap the given coins into fungible asset. This will also create the fungible asset wrapper if it doesn't exist
/// yet. The coins will be deposited into the main resource account.
public(friend) fun wrap<CoinType>(coins: Coin<CoinType>): FungibleAsset acquires WrapperAccount {
// Ensure the corresponding fungible asset has already been created.
create_fungible_asset<CoinType>();
// Deposit coins into the main resource account and mint&return the wrapper fungible assets.
let amount = coin::value(&coins);
aptos_account::deposit_coins(wrapper_address(), coins);
let mint_ref = &fungible_asset_data<CoinType>().mint_ref;
fungible_asset::mint(mint_ref, amount)
}
/// Unwrap the given fungible asset into coins. This will burn the fungible asset and withdraw&return the coins from
/// the main resource account.
/// This errors out if the given fungible asset is not a wrapper fungible asset.
public(friend) fun unwrap<CoinType>(fa: FungibleAsset): Coin<CoinType> acquires WrapperAccount {
let amount = fungible_asset::amount(&fa);
let burn_ref = &fungible_asset_data<CoinType>().burn_ref;
fungible_asset::burn(burn_ref, fa);
let wrapper_signer = &account::create_signer_with_capability(&wrapper_account().signer_cap);
coin::withdraw(wrapper_signer, amount)
}
/// Create the fungible asset wrapper for the given CoinType if it doesn't exist yet.
public(friend) fun create_fungible_asset<CoinType>(): Object<Metadata> acquires WrapperAccount {
let coin_type = type_info::type_name<CoinType>();
let wrapper_account = mut_wrapper_account();
let coin_to_fungible_asset = &mut wrapper_account.coin_to_fungible_asset;
let wrapper_signer = &account::create_signer_with_capability(&wrapper_account.signer_cap);
if (!smart_table::contains(coin_to_fungible_asset, coin_type)) {
let metadata_constructor_ref = &object::create_named_object(wrapper_signer, *string::bytes(&coin_type));
primary_fungible_store::create_primary_store_enabled_fungible_asset(
metadata_constructor_ref,
option::none(),
coin::name<CoinType>(),
coin::symbol<CoinType>(),
coin::decimals<CoinType>(),
string::utf8(b""),
string::utf8(b""),
);
let mint_ref = fungible_asset::generate_mint_ref(metadata_constructor_ref);
let burn_ref = fungible_asset::generate_burn_ref(metadata_constructor_ref);
let metadata = object::object_from_constructor_ref<Metadata>(metadata_constructor_ref);
smart_table::add(coin_to_fungible_asset, coin_type, FungibleAssetData {
metadata,
mint_ref,
burn_ref,
});
smart_table::add(&mut wrapper_account.fungible_asset_to_coin, metadata, coin_type);
};
smart_table::borrow(coin_to_fungible_asset, coin_type).metadata
}
inline fun fungible_asset_data<CoinType>(): &FungibleAssetData acquires WrapperAccount {
let coin_type = type_info::type_name<CoinType>();
smart_table::borrow(&wrapper_account().coin_to_fungible_asset, coin_type)
}
inline fun wrapper_account(): &WrapperAccount acquires WrapperAccount {
borrow_global<WrapperAccount>(wrapper_address())
}
inline fun mut_wrapper_account(): &mut WrapperAccount acquires WrapperAccount {
borrow_global_mut<WrapperAccount>(wrapper_address())
}
#[test_only]
friend swap::coin_wrapper_tests;
}