contract; use std::{ asset::{ burn, mint, mint_to, transfer, }, bytes::Bytes, call_frames::msg_asset_id, context::{ msg_amount, this_balance, }, hash::Hash, storage::storage_string::*, storage::storage_vec::*, string::String, }; use standards::{ src20::{ SetDecimalsEvent, SetNameEvent, SetSymbolEvent, SRC20, TotalSupplyEvent, }, src5::{ SRC5, State, }, }; use utils::utils::{build_lp_name, get_lp_asset, is_stable, validate_pool_id}; use utils::src20_utils::get_symbol_and_decimals; use math::pool_math::{ calculate_fee, get_max_protocol_fee, initial_liquidity, min, proportional_value, validate_curve, }; use interfaces::{callee::IBaseCallee, hook::IBaseHook, mira_amm::MiraAMM}; use interfaces::data_structures::{Asset, PoolId, PoolInfo, PoolMetadata,}; use interfaces::errors::{AmmError, InputError}; use interfaces::events::{BurnEvent, CreatePoolEvent, MintEvent, SwapEvent}; use sway_libs::{ ownership::{ _owner, initialize_ownership, only_owner, transfer_ownership, }, reentrancy::reentrancy_guard, }; configurable { /// Liquidity provider fee for volatile pools. 0,3%, in basis points LP_FEE_VOLATILE: u64 = 30, /// Liquidity provider fee for stable pools. 0,05%, in basis points LP_FEE_STABLE: u64 = 5, } storage { /// Pools storage pools: StorageMap = StorageMap {}, /// Total number of pools total_pools: u64 = 0, /// Total reserves of specific assets across all pools total_reserves: StorageMap = StorageMap {}, /// The total supply of coins for a specific asset minted by this contract. lp_total_supply: StorageMap = StorageMap {}, /// The name of a specific asset minted by this contract. lp_name: StorageMap = StorageMap {}, /// Protocol fees in basis points for volatile and stable pools, respectively. protocol_fees: (u64, u64) = (0, 0), /// Hook to call on all reserve updates. hook: Option = None, } const MINIMUM_LIQUIDITY: u64 = 1000; const LP_TOKEN_DECIMALS: u8 = 9; const LP_TOKEN_SYMBOL = __to_str_array("MIRA-LP"); #[storage(write)] fn initialize_lp_asset(sender: Identity, lp_asset: AssetId, name: String) { storage.lp_name.get(lp_asset).write_slice(name); log(SetNameEvent { asset: lp_asset, name: Some(name), sender, }); log(SetSymbolEvent { asset: lp_asset, symbol: Some(String::from_ascii_str(from_str_array(LP_TOKEN_SYMBOL))), sender, }); log(SetDecimalsEvent { asset: lp_asset, decimals: LP_TOKEN_DECIMALS, sender, }); } #[storage(write)] fn update_total_supply(sender: Identity, lp_asset: AssetId, new_supply: u64) { storage.lp_total_supply.insert(lp_asset, new_supply); log(TotalSupplyEvent { asset: lp_asset, supply: new_supply, sender, }); } #[storage(read)] fn get_pool_option(pool_id: PoolId) -> Option { validate_pool_id(pool_id); storage.pools.get(pool_id).try_read() } #[storage(read)] fn get_pool(pool_id: PoolId) -> PoolInfo { let pool = get_pool_option(pool_id); require(pool.is_some(), InputError::PoolDoesNotExist(pool_id)); pool.unwrap() } #[storage(read)] fn get_total_reserve(asset_id: AssetId) -> u64 { storage.total_reserves.get(asset_id).try_read().unwrap_or(0) } #[storage(read, write)] fn update_total_reserve(asset_id: AssetId, amount_in: u64, amount_out: u64) { let old_reserve = get_total_reserve(asset_id); let new_reserve = old_reserve + amount_in - amount_out; let balance = this_balance(asset_id); require( balance >= new_reserve, InputError::PoolInvariantViolation((balance, new_reserve)), ); storage.total_reserves.insert(asset_id, new_reserve); } #[storage(read)] fn get_lp_total_supply(asset_id: AssetId) -> Option { storage.lp_total_supply.get(asset_id).try_read() } #[storage(read)] fn lp_asset_exists(asset: AssetId) -> bool { get_lp_total_supply(asset).is_some() } #[storage(read, write)] fn initialize_pool( pool_id: PoolId, decimals_0: u8, decimals_1: u8, lp_name: String, ) { require( get_pool_option(pool_id) .is_none(), InputError::PoolAlreadyExists(pool_id), ); let (_, pool_lp_asset) = get_lp_asset(pool_id); require( !lp_asset_exists(pool_lp_asset), InputError::LPTokenHashCollision, ); let pool_info = PoolInfo::new(pool_id, decimals_0, decimals_1); storage.pools.insert(pool_id, pool_info); storage.total_pools.write(storage.total_pools.read() + 1); let sender = msg_sender().unwrap(); initialize_lp_asset(sender, pool_lp_asset, lp_name); update_total_supply(sender, pool_lp_asset, 0); } #[storage(read, write)] fn mint_lp_asset(pool_id: PoolId, to: Identity, amount: u64) -> Asset { let (pool_lp_asset_sub_id, pool_lp_asset) = get_lp_asset(pool_id); // must be present in the storage let lp_total_supply = get_lp_total_supply(pool_lp_asset).unwrap(); update_total_supply( msg_sender() .unwrap(), pool_lp_asset, lp_total_supply + amount, ); mint_to(to, pool_lp_asset_sub_id, amount); Asset::new(pool_lp_asset, amount) } /// Burns the provided amount of LP token. Returns the initial total supply, prior the burn operation #[storage(read, write)] fn burn_lp_asset(pool_id: PoolId, burned_liquidity: Asset) -> u64 { let (pool_lp_asset_sub_id, pool_lp_asset) = get_lp_asset(pool_id); require( burned_liquidity .id == pool_lp_asset, InputError::InvalidAsset(burned_liquidity.id), ); require(burned_liquidity.amount > 0, InputError::ZeroInputAmount); // must be present in the storage let lp_total_supply = get_lp_total_supply(pool_lp_asset).unwrap(); require( lp_total_supply >= burned_liquidity .amount, AmmError::InsufficientLiquidity, ); update_total_supply( msg_sender() .unwrap(), pool_lp_asset, lp_total_supply - burned_liquidity .amount, ); burn(pool_lp_asset_sub_id, burned_liquidity.amount); lp_total_supply } #[storage(read)] fn get_pool_liquidity(pool_id: PoolId) -> Asset { let (_, pool_lp_asset) = get_lp_asset(pool_id); // must be present in the storage let liquidity = get_lp_total_supply(pool_lp_asset).unwrap(); Asset::new(pool_lp_asset, liquidity) } #[storage(read)] fn get_amount_in(asset_id: AssetId) -> u64 { let total_reserve = get_total_reserve(asset_id); let balance = this_balance(asset_id); balance - total_reserve } #[storage(read)] fn get_amount_in_accounting_out(asset_id: AssetId, amount_out: u64, to: Identity) -> (u64, u64) { let total_reserve = get_total_reserve(asset_id); let balance = if (to == Identity::ContractId(ContractId::this())) { // Account cases when assets are transferred to this contract (usually in multihops). // We don't account them as part of pool balances, so that they're treated as inputs // for the next swap. this_balance(asset_id) - amount_out } else { this_balance(asset_id) }; let after_out = total_reserve - amount_out; let amount_in = if balance > after_out { balance - after_out } else { 0 }; (balance, amount_in) } #[storage(read, write)] fn update_reserves( pool: PoolInfo, amount_0_in: u64, amount_1_in: u64, amount_0_out: u64, amount_1_out: u64, ) { let reserve_0 = pool.reserve_0 + amount_0_in - amount_0_out; let reserve_1 = pool.reserve_1 + amount_1_in - amount_1_out; let updated_pool = pool.copy_with_reserves(reserve_0, reserve_1); storage.pools.insert(pool.id, updated_pool); update_total_reserve(pool.id.0, amount_0_in, amount_0_out); update_total_reserve(pool.id.1, amount_1_in, amount_1_out); } fn transfer_assets( pool_id: PoolId, to: Identity, asset_0_out: u64, asset_1_out: u64, ) { if (asset_0_out > 0) { transfer(to, pool_id.0, asset_0_out); } if (asset_1_out > 0) { transfer(to, pool_id.1, asset_1_out); } } #[storage(read)] fn get_protocol_fees() -> (u64, u64) { storage.protocol_fees.read() } fn get_lp_pool_fee(pool_id: PoolId, amount_0: u64, amount_1: u64) -> (u64, u64) { let fee = if is_stable(pool_id) { LP_FEE_STABLE } else { LP_FEE_VOLATILE }; (calculate_fee(amount_0, fee), calculate_fee(amount_1, fee)) } #[storage(read)] fn get_protocol_pool_fee(pool_id: PoolId, amount_0: u64, amount_1: u64) -> (u64, u64) { let (protocol_fee_volatile, protocol_fee_stable) = get_protocol_fees(); let fee = if is_stable(pool_id) { protocol_fee_stable } else { protocol_fee_volatile }; (calculate_fee(amount_0, fee), calculate_fee(amount_1, fee)) } #[storage(read)] fn get_fee_recipient() -> Option { match _owner() { State::Initialized(owner) => Some(owner), _ => None, } } impl SRC5 for Contract { #[storage(read)] fn owner() -> State { _owner() } } #[storage(read)] fn get_hook() -> Option { storage.hook.read() } #[storage(read)] fn call_hook( pool_id: PoolId, to: Identity, asset_0_in: u64, asset_1_in: u64, asset_0_out: u64, asset_1_out: u64, lp_token: u64, ) { if let Some(hook) = get_hook() { abi(IBaseHook, hook .into()) .hook( pool_id, msg_sender() .unwrap(), to, asset_0_in, asset_1_in, asset_0_out, asset_1_out, lp_token, ); } } impl SRC20 for Contract { #[storage(read)] fn total_assets() -> u64 { storage.total_pools.read() } #[storage(read)] fn total_supply(asset: AssetId) -> Option { get_lp_total_supply(asset) } #[storage(read)] fn name(asset: AssetId) -> Option { storage.lp_name.get(asset).read_slice() } #[storage(read)] fn symbol(asset: AssetId) -> Option { if lp_asset_exists(asset) { Some(String::from_ascii_str(from_str_array(LP_TOKEN_SYMBOL))) } else { None } } #[storage(read)] fn decimals(asset: AssetId) -> Option { if lp_asset_exists(asset) { Some(LP_TOKEN_DECIMALS) } else { None } } } impl MiraAMM for Contract { #[storage(read, write)] fn create_pool( token_0_contract_id: ContractId, token_0_sub_id: SubId, token_1_contract_id: ContractId, token_1_sub_id: SubId, is_stable: bool, ) -> PoolId { reentrancy_guard(); let token_0_id = AssetId::new(token_0_contract_id, token_0_sub_id); let token_1_id = AssetId::new(token_1_contract_id, token_1_sub_id); let pool_id: PoolId = (token_0_id, token_1_id, is_stable); let (symbol_0, decimals_0) = get_symbol_and_decimals(token_0_contract_id, token_0_id); let (symbol_1, decimals_1) = get_symbol_and_decimals(token_1_contract_id, token_1_id); let lp_name = build_lp_name(symbol_0, symbol_1); initialize_pool(pool_id, decimals_0, decimals_1, lp_name); log(CreatePoolEvent { pool_id, decimals_0, decimals_1, }); pool_id } #[storage(read)] fn pool_metadata(pool_id: PoolId) -> Option { match get_pool_option(pool_id) { Some(pool) => Some(PoolMetadata::from_pool_and_liquidity(pool, get_pool_liquidity(pool_id))), None => None, } } #[storage(read)] fn fees() -> (u64, u64, u64, u64) { let (protocol_fee_volatile, protocol_fee_stable) = get_protocol_fees(); (LP_FEE_VOLATILE, LP_FEE_STABLE, protocol_fee_volatile, protocol_fee_stable) } #[storage(write)] fn set_protocol_fees(volatile_fee: u64, stable_fee: u64) { only_owner(); // protocol fees cannot exceed 20% of the LP fees require( volatile_fee <= get_max_protocol_fee(LP_FEE_VOLATILE) && stable_fee <= get_max_protocol_fee(LP_FEE_STABLE), InputError::ProtocolFeesAreTooHigh, ); storage.protocol_fees.write((volatile_fee, stable_fee)); } #[storage(write)] fn set_hook(contract_id: Option) { only_owner(); // sway doesn't allow to check if a contract id implements an interface storage.hook.write(contract_id); } #[storage(read)] fn hook() -> Option { get_hook() } #[storage(read, write)] fn mint(pool_id: PoolId, to: Identity) -> Asset { reentrancy_guard(); let mut pool = get_pool(pool_id); let asset_0_in = get_amount_in(pool_id.0); let asset_1_in = get_amount_in(pool_id.1); let total_liquidity = get_pool_liquidity(pool_id).amount; let added_liquidity: u64 = if total_liquidity == 0 { let _ = mint_lp_asset(pool_id, get_fee_recipient().unwrap(), MINIMUM_LIQUIDITY); let init_liquidity = initial_liquidity(asset_0_in, asset_1_in); require( init_liquidity > MINIMUM_LIQUIDITY, AmmError::CannotAddLessThanMinimumLiquidity, ); init_liquidity - MINIMUM_LIQUIDITY } else { min( proportional_value(asset_0_in, total_liquidity, pool.reserve_0), proportional_value(asset_1_in, total_liquidity, pool.reserve_1), ) }; require(added_liquidity > 0, AmmError::NoLiquidityAdded); let minted = mint_lp_asset(pool_id, to, added_liquidity); update_reserves(pool, asset_0_in, asset_1_in, 0, 0); log(MintEvent { pool_id, recipient: to, liquidity: minted, asset_0_in, asset_1_in, }); call_hook(pool_id, to, asset_0_in, asset_1_in, 0, 0, minted.amount); minted } #[payable] #[storage(read, write)] fn burn(pool_id: PoolId, to: Identity) -> (u64, u64) { reentrancy_guard(); let burned_liquidity = Asset::new(msg_asset_id(), msg_amount()); let total_liquidity = burn_lp_asset(pool_id, burned_liquidity); let mut pool = get_pool(pool_id); let asset_0_out = proportional_value(burned_liquidity.amount, pool.reserve_0, total_liquidity); let asset_1_out = proportional_value(burned_liquidity.amount, pool.reserve_1, total_liquidity); transfer(to, pool_id.0, asset_0_out); transfer(to, pool_id.1, asset_1_out); update_reserves(pool, 0, 0, asset_0_out, asset_1_out); log(BurnEvent { pool_id, recipient: to, liquidity: burned_liquidity, asset_0_out, asset_1_out, }); call_hook( pool_id, to, 0, 0, asset_0_out, asset_1_out, burned_liquidity .amount, ); (asset_0_out, asset_1_out) } #[payable] #[storage(read, write)] fn swap( pool_id: PoolId, asset_0_out: u64, asset_1_out: u64, to: Identity, data: Option, ) { reentrancy_guard(); let mut pool = get_pool(pool_id); require( asset_0_out > 0 || asset_1_out > 0, InputError::ZeroOutputAmount, ); require( asset_0_out < pool.reserve_0 && asset_1_out < pool.reserve_1, AmmError::InsufficientLiquidity, ); // Optimistically transfer assets transfer_assets(pool_id, to, asset_0_out, asset_1_out); if let Some(d) = data { abi(IBaseCallee, to .as_contract_id() .unwrap() .into()) .hook(msg_sender().unwrap(), asset_0_out, asset_1_out, d); } let (balance_0, asset_0_in) = get_amount_in_accounting_out(pool_id.0, asset_0_out, to); let (balance_1, asset_1_in) = get_amount_in_accounting_out(pool_id.1, asset_1_out, to); require( asset_0_in > 0 || asset_1_in > 0, InputError::ZeroInputAmount, ); let (protocol_fee_0, protocol_fee_1) = get_protocol_pool_fee(pool_id, asset_0_in, asset_1_in); let (lp_fee_0, lp_fee_1) = get_lp_pool_fee(pool_id, asset_0_in, asset_1_in); transfer_assets( pool_id, get_fee_recipient() .unwrap(), protocol_fee_0, protocol_fee_1, ); let asset_0_in_adjusted = asset_0_in - protocol_fee_0; let asset_1_in_adjusted = asset_1_in - protocol_fee_1; let balance_0_adjusted = balance_0 - lp_fee_0 - protocol_fee_0; let balance_1_adjusted = balance_1 - lp_fee_1 - protocol_fee_1; validate_curve( is_stable(pool_id), balance_0_adjusted, balance_1_adjusted, pool.reserve_0, pool.reserve_1, pool.decimals_0, pool.decimals_1, ); update_reserves( pool, asset_0_in_adjusted, asset_1_in_adjusted, asset_0_out, asset_1_out, ); log(SwapEvent { pool_id, recipient: to, asset_0_in, asset_1_in, asset_0_out, asset_1_out, }); call_hook( pool_id, to, asset_0_in, asset_1_in, asset_0_out, asset_1_out, 0, ); } #[storage(read, write)] fn transfer_ownership(new_owner: Identity) { if _owner() == State::Uninitialized { initialize_ownership(new_owner); } else { transfer_ownership(new_owner); } } }