- Docs
- Developers
- Constant Product AMM
Constant Product Automatic Market Maker (AMM)
Explanation
This contract is an Constant Product Automated Market Maker (AMM) for CosmWasm. This contract allows you to swap tokens. Liquidity providers can add liquidity to the market and receive a certain fee on every transaction that is set on instantiation. The code also includes various error handling and validation checks to ensure the correctness and security of the operations.
This type of AMM is based on the function x*y=k, which establishes a range of prices for two tokens according to the available liquidity of each token. When the supply of token X increases, the token supply of Y must decrease, and vice-versa, to maintain the constant product K. When plotted, the result is a hyperbola where liquidity is always available but at increasingly higher prices, which approach infinity at both ends.
Instantiation
The contract can be instantiated with the following messages
{ "token1_denom": {"cw20": "<CONTRACT_ADDRESS>"}, "token2_denom": {"cw20": "<CONTRACT_ADDRESS>"},}
Token denom can be cw20 for cw20 tokens. cw20 tokens have a contract address. CW20_CODE_ID is the code id for a basic cw20 binary.
Add Liquidity
Allows a user to add liquidity to the pool.
pub fn execute_add_liquidity( deps: DepsMut, info: &MessageInfo, env: Env, min_liquidity: Uint128, token1_amount: Uint128, token2_amount: Uint128, expiration: Option<Expiration>,) -> Result<Response, ContractError> { check_expiration(&expiration, &env.block)?; let token1 = TOKEN1.load(deps.storage)?; let token2 = TOKEN2.load(deps.storage)?; let mut token_supply = TOTAL_STORED.load(deps.storage)?; let liquidity_amount = token1_amount+token2_amount; token_supply+=liquidity_amount; TOTAL_STORED.save(deps.storage, &token_supply)?; if liquidity_amount < min_liquidity { return Err(ContractError::MinLiquidityError { min_liquidity, liquidity_available: liquidity_amount, }); } // Generate cw20 transfer messages if necessary let mut transfer_msgs: Vec<CosmosMsg> = vec![]; if let Cw20(addr) = token1.denom { transfer_msgs.push(get_cw20_transfer_from_msg( &info.sender, &env.contract.address, &addr, token1_amount, )?) } if let Cw20(addr) = token2.denom.clone() { transfer_msgs.push(get_cw20_transfer_from_msg( &info.sender, &env.contract.address, &addr, token2_amount, )?) } TOKEN1.update(deps.storage, |mut token1| -> Result<_, ContractError> { token1.reserve += token1_amount; Ok(token1) })?; TOKEN2.update(deps.storage, |mut token2| -> Result<_, ContractError> { token2.reserve += token2_amount; Ok(token2) })?; Ok(Response::new() .add_messages(transfer_msgs) .add_attributes(vec![ attr("token1_amount", token1_amount), attr("token2_amount", token2_amount), attr("liquidity_received", liquidity_amount), ]))}
Users can add liquidity to the AMM by calling the execute_add_liquidity function. This function takes the desired amounts of two tokens (token1_amount and token2_amount) and mints a corresponding amount of liquidity tokens. The liquidity tokens represent the user's share in the AMM's liquidity pool. The function also transfers the input tokens from the user to the contract.
Remove Liquidity
Allows a user to remove liquidity from the pool.
pub fn execute_remove_liquidity( deps: DepsMut, info: MessageInfo, env: Env, amount: Uint128, min_token1: Uint128, min_token2: Uint128, expiration: Option<Expiration>,) -> Result<Response, ContractError> { check_expiration(&expiration, &env.block)?; let total_token_supply = TOTAL_STORED.load(deps.storage)?; let token1 = TOKEN1.load(deps.storage)?; let token2 = TOKEN2.load(deps.storage)?; if amount > total_token_supply { return Err(ContractError::InsufficientLiquidityError { requested: amount, available: total_token_supply, }); } let token1_amount = amount .checked_mul(token1.reserve) .map_err(StdError::overflow)? .checked_div(total_token_supply) .map_err(StdError::divide_by_zero)?; if token1_amount < min_token1 { return Err(ContractError::MinToken1Error { requested: min_token1, available: token1_amount, }); } let token2_amount = amount .checked_mul(token2.reserve) .map_err(StdError::overflow)? .checked_div(total_token_supply) .map_err(StdError::divide_by_zero)?; if token2_amount < min_token2 { return Err(ContractError::MinToken2Error { requested: min_token2, available: token2_amount, }); } TOKEN1.update(deps.storage, |mut token1| -> Result<_, ContractError> { token1.reserve = token1 .reserve .checked_sub(token1_amount) .map_err(StdError::overflow)?; Ok(token1) })?; TOKEN2.update(deps.storage, |mut token2| -> Result<_, ContractError> { token2.reserve = token2 .reserve .checked_sub(token2_amount) .map_err(StdError::overflow)?; Ok(token2) })?; let token1_transfer_msg = match token1.denom { Denom::Cw20(addr) => get_cw20_transfer_to_msg(&info.sender, &addr, token1_amount)?, Denom::Native(_denom) => {unimplemented!()}, }; let token2_transfer_msg = match token2.denom { Denom::Cw20(addr) => get_cw20_transfer_to_msg(&info.sender, &addr, token2_amount)?, Denom::Native(_denom) => {unimplemented!()}, }; Ok(Response::new() .add_messages(vec![ token1_transfer_msg, token2_transfer_msg, ]) .add_attributes(vec![ attr("liquidity_burned", amount), attr("token1_returned", token1_amount), attr("token2_returned", token2_amount), ]))}
Liquidity providers can remove their liquidity by calling the execute_remove_liquidity function. They specify the amount of liquidity tokens (amount) they want to burn, and the function calculates the proportionate amounts of the underlying tokens (token1_amount and token2_amount). The function transfers the corresponding tokens to the user and decreases the token reserves accordingly.
Swap
Swap one asset for the other
pub fn execute_swap( deps: DepsMut, info: &MessageInfo, input_amount: Uint128, _env: Env, input_token_enum: TokenSelect, recipient: String, min_token: Uint128, expiration: Option<Expiration>,) -> Result<Response, ContractError> { check_expiration(&expiration, &_env.block)?; let input_token_item = match input_token_enum { TokenSelect::Token1 => TOKEN1, TokenSelect::Token2 => TOKEN2, }; let input_token = input_token_item.load(deps.storage)?; let output_token_item = match input_token_enum { TokenSelect::Token1 => TOKEN2, TokenSelect::Token2 => TOKEN1, }; let output_token = output_token_item.load(deps.storage)?; let fees = FEES.load(deps.storage)?; let total_fee_percent = fees.lp_fee_percent + fees.protocol_fee_percent; let token_bought = get_input_price( input_amount, input_token.reserve, output_token.reserve, total_fee_percent, )?; if min_token > token_bought { return Err(ContractError::SwapMinError { min: min_token, available: token_bought, }); } // Calculate fees let protocol_fee_amount = get_protocol_fee_amount(input_amount, fees.protocol_fee_percent)?; let input_amount_minus_protocol_fee = input_amount - protocol_fee_amount; let mut msgs = match input_token.denom.clone() { Denom::Cw20(addr) => vec![get_cw20_transfer_from_msg( &info.sender, &_env.contract.address, &addr, input_amount_minus_protocol_fee, )?], Denom::Native(_) => vec![], }; // Send protocol fee to protocol fee recipient if !protocol_fee_amount.is_zero() { msgs.push(get_fee_transfer_msg( &info.sender, &fees.protocol_fee_recipient, &input_token.denom, protocol_fee_amount, )?) } let recipient = deps.api.addr_validate(&recipient)?; // Create transfer to message msgs.push(match output_token.denom { Denom::Cw20(addr) => get_cw20_transfer_to_msg(&recipient, &addr, token_bought)?, Denom::Native(_denom) => {unimplemented!()}, }); input_token_item.update( deps.storage, |mut input_token| -> Result<_, ContractError> { input_token.reserve = input_token .reserve .checked_add(input_amount_minus_protocol_fee) .map_err(StdError::overflow)?; Ok(input_token) }, )?; output_token_item.update( deps.storage, |mut output_token| -> Result<_, ContractError> { output_token.reserve = output_token .reserve .checked_sub(token_bought) .map_err(StdError::overflow)?; Ok(output_token) }, )?; Ok(Response::new().add_messages(msgs).add_attributes(vec![ attr("native_sold", input_amount), attr("token_bought", token_bought), ]))}
Users can swap tokens using the AMM by calling the execute_swap function. They specify the input token (input_token), the amount to swap (input_amount), and the minimum output amount (min_output). The function calculates the output amount based on the constant product formula and checks if it meets the minimum requirement. If the swap is valid, it transfers the input token from the user to the contract and transfers the output token back to the user.
Configuration Update
To update the AMM configuration
pub fn execute_update_config( deps: DepsMut, info: MessageInfo, new_owner: Option<String>, lp_fee_percent: Decimal, protocol_fee_percent: Decimal, protocol_fee_recipient: String,) -> Result<Response, ContractError> { let owner = OWNER.load(deps.storage)?; if Some(info.sender) != owner { return Err(ContractError::Unauthorized {}); } let new_owner_addr = new_owner .as_ref() .map(|h| deps.api.addr_validate(h)) .transpose()?; OWNER.save(deps.storage, &new_owner_addr)?; let total_fee_percent = lp_fee_percent + protocol_fee_percent; let max_fee_percent = Decimal::from_str(MAX_FEE_PERCENT)?; if total_fee_percent > max_fee_percent { return Err(ContractError::FeesTooHigh { max_fee_percent, total_fee_percent, }); } let protocol_fee_recipient = deps.api.addr_validate(&protocol_fee_recipient)?; let updated_fees = Fees { protocol_fee_recipient: protocol_fee_recipient.clone(), lp_fee_percent, protocol_fee_percent, }; FEES.save(deps.storage, &updated_fees)?; let new_owner = new_owner.unwrap_or_default(); Ok(Response::new().add_attributes(vec![ attr("new_owner", new_owner), attr("lp_fee_percent", lp_fee_percent.to_string()), attr("protocol_fee_percent", protocol_fee_percent.to_string()), attr("protocol_fee_recipient", protocol_fee_recipient.to_string()), ]))}
The AMM's configuration can be updated by the owner using the execute_update_config function. The owner can change the LP (liquidity provider) fee percentage, the protocol fee percentage, and the protocol fee recipient address.
Deposit Freezing
To freeze the deposit to AMM
fn execute_freeze_deposits( deps: DepsMut, sender: Addr, freeze: bool,) -> Result<Response, ContractError> { if let Some(owner) = OWNER.load(deps.storage)? { if sender != owner { return Err(ContractError::UnauthorizedPoolFreeze {}); } } else { return Err(ContractError::UnauthorizedPoolFreeze {}); } FROZEN.save(deps.storage, &freeze)?; Ok(Response::new().add_attribute("action", "freezing-contracts"))}fn check_expiration( expiration: &Option<Expiration>, block: &BlockInfo,) -> Result<(), ContractError> { match expiration { Some(e) => { if e.is_expired(block) { return Err(ContractError::MsgExpirationError {}); } Ok(()) } None => Ok(()), }}
The owner can freeze deposits to the AMM by calling the execute_freeze_deposits function. This prevents users from adding liquidity or swapping tokens. Only the owner can freeze or unfreeze deposits.
Example
To create an AMM with CosmWasm, you can create the following files: lib.rs integration_tests.rs contract.rs msg.rs error.rs state.rs
Cargo.toml
The following dependencies should be added to the Cargo.toml
file:
cw20-base = { version = "0.16", features = ["library"] }
cw20 = { version = "0.16" }
Make sure cw20-base
is added as a library
using features = ["library"]
so as to prevent any issues with adding the instantiate
, execute
and query
entrypoints to the contract.
lib.rs
pub mod contract;pub mod error;mod integration_test;pub mod msg;pub mod state;
integration_tests.rs
#![cfg(test)]use std::borrow::BorrowMut;use crate::error::ContractError;use cosmwasm_std::{coins, Addr, Coin, Decimal, Empty, Uint128};use cw20::{Cw20Coin, Cw20Contract, Cw20ExecuteMsg, Denom};use cw_multi_test::{App, Contract, ContractWrapper, Executor};use std::str::FromStr;use crate::msg::{ExecuteMsg, FeeResponse, InfoResponse, InstantiateMsg, QueryMsg, TokenSelect};fn mock_app() -> App { App::default()}pub fn contract_amm() -> Box<dyn Contract<Empty>> { let contract = ContractWrapper::new( crate::contract::execute, crate::contract::instantiate, crate::contract::query, ); Box::new(contract)}pub fn contract_cw20() -> Box<dyn Contract<Empty>> { let contract = ContractWrapper::new( cw20_base::contract::execute, cw20_base::contract::instantiate, cw20_base::contract::query, ); Box::new(contract)}fn get_info(router: &App, contract_addr: &Addr) -> InfoResponse { router .wrap() .query_wasm_smart(contract_addr, &QueryMsg::Info {}) .unwrap()}fn get_fee(router: &App, contract_addr: &Addr) -> FeeResponse { router .wrap() .query_wasm_smart(contract_addr, &QueryMsg::Fee {}) .unwrap()}fn create_amm( router: &mut App, owner: &Addr, token1_denom: Denom, token2_denom: Denom, lp_fee_percent: Decimal, protocol_fee_percent: Decimal, protocol_fee_recipient: String,) -> Addr { // set up amm contract let amm_id = router.store_code(contract_amm()); let msg = InstantiateMsg { token1_denom, token2_denom, owner: Some(owner.to_string()), lp_fee_percent, protocol_fee_percent, protocol_fee_recipient, }; router .instantiate_contract(amm_id, owner.clone(), &msg, &[], "amm", None) .unwrap()}// CreateCW20 create new cw20 with given initial balance belonging to ownerfn create_cw20( router: &mut App, owner: &Addr, name: String, symbol: String, balance: Uint128,) -> Cw20Contract { // set up cw20 contract with some tokens let cw20_id = router.store_code(contract_cw20()); let msg = cw20_base::msg::InstantiateMsg { name, symbol, decimals: 6, initial_balances: vec![Cw20Coin { address: owner.to_string(), amount: balance, }], mint: None, marketing: None, }; let addr = router .instantiate_contract(cw20_id, owner.clone(), &msg, &[], "CASH", None) .unwrap(); Cw20Contract(addr)}fn bank_balance(router: &mut App, addr: &Addr, denom: String) -> Coin { router .wrap() .query_balance(addr.to_string(), denom) .unwrap()}#[test]// receive cw20 tokens and release upon approvalfn test_instantiate() { let mut router = mock_app(); const NATIVE_TOKEN_DENOM: &str = "juno"; let owner = Addr::unchecked("owner"); let funds = coins(2000, NATIVE_TOKEN_DENOM); router.borrow_mut().init_modules(|router, _, storage| { router.bank.init_balance(storage, &owner, funds).unwrap() }); let cw20_token = create_cw20( &mut router, &owner, "token".to_string(), "CWTOKEN".to_string(), Uint128::new(5000), ); let lp_fee_percent = Decimal::from_str("0.3").unwrap(); let protocol_fee_percent = Decimal::zero(); let amm_addr = create_amm( &mut router, &owner, Denom::Native(NATIVE_TOKEN_DENOM.into()), Denom::Cw20(cw20_token.addr()), lp_fee_percent, protocol_fee_percent, owner.to_string(), ); assert_ne!(cw20_token.addr(), amm_addr); let _info = get_info(&router, &amm_addr); let fee = get_fee(&router, &amm_addr); assert_eq!(fee.lp_fee_percent, lp_fee_percent); assert_eq!(fee.protocol_fee_percent, protocol_fee_percent); assert_eq!(fee.protocol_fee_recipient, owner.to_string()); assert_eq!(fee.owner.unwrap(), owner.to_string()); // Test instantiation with invalid fee amount let lp_fee_percent = Decimal::from_str("1.01").unwrap(); let protocol_fee_percent = Decimal::zero(); let amm_id = router.store_code(contract_amm()); let msg = InstantiateMsg { token1_denom: Denom::Native(NATIVE_TOKEN_DENOM.into()), token2_denom: Denom::Cw20(cw20_token.addr()), owner: Some(owner.to_string()), lp_fee_percent, protocol_fee_percent, protocol_fee_recipient: owner.to_string(), }; let err = router .instantiate_contract(amm_id, owner.clone(), &msg, &[], "amm", None) .unwrap_err() .downcast() .unwrap(); assert_eq!( ContractError::FeesTooHigh { max_fee_percent: Decimal::from_str("1").unwrap(), total_fee_percent: Decimal::from_str("1.01").unwrap() }, err );}#[test]fn update_config() { let mut router = mock_app(); const NATIVE_TOKEN_DENOM: &str = "juno"; let owner = Addr::unchecked("owner"); let funds = coins(2000, NATIVE_TOKEN_DENOM); router.borrow_mut().init_modules(|router, _, storage| { router.bank.init_balance(storage, &owner, funds).unwrap() }); let cw20_token = create_cw20( &mut router, &owner, "token".to_string(), "CWTOKEN".to_string(), Uint128::new(5000), ); let lp_fee_percent = Decimal::from_str("0.3").unwrap(); let protocol_fee_percent = Decimal::zero(); let amm_addr = create_amm( &mut router, &owner, Denom::Native(NATIVE_TOKEN_DENOM.to_string()), Denom::Cw20(cw20_token.addr()), lp_fee_percent, protocol_fee_percent, owner.to_string(), ); let lp_fee_percent = Decimal::from_str("0.15").unwrap(); let protocol_fee_percent = Decimal::from_str("0.15").unwrap(); let msg = ExecuteMsg::UpdateConfig { owner: Some(owner.to_string()), protocol_fee_recipient: "new_fee_recpient".to_string(), lp_fee_percent, protocol_fee_percent, }; let _res = router .execute_contract(owner.clone(), amm_addr.clone(), &msg, &[]) .unwrap(); let fee = get_fee(&router, &amm_addr); assert_eq!(fee.protocol_fee_recipient, "new_fee_recpient".to_string()); assert_eq!(fee.protocol_fee_percent, protocol_fee_percent); assert_eq!(fee.lp_fee_percent, lp_fee_percent); assert_eq!(fee.owner.unwrap(), owner.to_string()); // Try updating config with fee values that are too high let lp_fee_percent = Decimal::from_str("1.01").unwrap(); let protocol_fee_percent = Decimal::zero(); let msg = ExecuteMsg::UpdateConfig { owner: Some(owner.to_string()), protocol_fee_recipient: "new_fee_recpient".to_string(), lp_fee_percent, protocol_fee_percent, }; let err = router .execute_contract(owner.clone(), amm_addr.clone(), &msg, &[]) .unwrap_err() .downcast() .unwrap(); assert_eq!( ContractError::FeesTooHigh { max_fee_percent: Decimal::from_str("1").unwrap(), total_fee_percent: Decimal::from_str("1.01").unwrap() }, err ); // Try updating config with invalid owner, show throw unauthoritzed error let lp_fee_percent = Decimal::from_str("0.21").unwrap(); let protocol_fee_percent = Decimal::from_str("0.09").unwrap(); let msg = ExecuteMsg::UpdateConfig { owner: Some(owner.to_string()), protocol_fee_recipient: owner.to_string(), lp_fee_percent, protocol_fee_percent, }; let err = router .execute_contract( Addr::unchecked("invalid_owner"), amm_addr.clone(), &msg, &[], ) .unwrap_err() .downcast() .unwrap(); assert_eq!(ContractError::Unauthorized {}, err); // Try updating owner and fee params let msg = ExecuteMsg::UpdateConfig { owner: Some("new_owner".to_string()), protocol_fee_recipient: owner.to_string(), lp_fee_percent, protocol_fee_percent, }; let _res = router .execute_contract(owner.clone(), amm_addr.clone(), &msg, &[]) .unwrap(); let fee = get_fee(&router, &amm_addr); assert_eq!(fee.protocol_fee_recipient, owner.to_string()); assert_eq!(fee.protocol_fee_percent, protocol_fee_percent); assert_eq!(fee.lp_fee_percent, lp_fee_percent); assert_eq!(fee.owner.unwrap(), "new_owner".to_string());}#[test]fn test_pass_through_swap() { let mut router = mock_app(); const NATIVE_TOKEN_DENOM: &str = "juno"; let owner = Addr::unchecked("owner"); let funds = coins(2000, NATIVE_TOKEN_DENOM); router.borrow_mut().init_modules(|router, _, storage| { router.bank.init_balance(storage, &owner, funds).unwrap() }); let token1 = create_cw20( &mut router, &owner, "token1".to_string(), "TOKENONE".to_string(), Uint128::new(5000), ); let token2 = create_cw20( &mut router, &owner, "token2".to_string(), "TOKENTWO".to_string(), Uint128::new(5000), ); let lp_fee_percent = Decimal::from_str("0.03").unwrap(); let protocol_fee_percent = Decimal::zero(); let amm = create_amm( &mut router, &owner, Denom::Cw20(token1.addr()), Denom::Cw20(token2.addr()), lp_fee_percent, protocol_fee_percent, owner.to_string(), ); // Add initial liquidity to both pools let allowance_msg = Cw20ExecuteMsg::IncreaseAllowance { spender: amm.to_string(), amount: Uint128::new(100), expires: None, }; let _res = router .execute_contract(owner.clone(), token1.addr(), &allowance_msg, &[]) .unwrap(); let _res = router .execute_contract(owner.clone(), token2.addr(), &allowance_msg, &[]) .unwrap(); let add_liquidity_msg = ExecuteMsg::AddLiquidity { token1_amount: Uint128::new(100), min_liquidity: Uint128::new(100), token2_amount: Uint128::new(100), expiration: None, }; router .execute_contract( owner.clone(), amm.clone(), &add_liquidity_msg, &[Coin { denom: NATIVE_TOKEN_DENOM.into(), amount: Uint128::zero(), }], ) .unwrap(); // Swap token1 for token2 let allowance_msg = Cw20ExecuteMsg::IncreaseAllowance { spender: amm.to_string(), amount: Uint128::new(10), expires: None, }; let _res = router .execute_contract(owner.clone(), token1.addr(), &allowance_msg, &[]) .unwrap(); let swap_msg = ExecuteMsg::Swap { input_token: TokenSelect::Token1, input_amount: Uint128::new(10), min_output: Uint128::new(8), expiration: None, }; let _res = router .execute_contract(owner.clone(), amm.clone(), &swap_msg, &[]) .unwrap(); // ensure balances updated let token1_balance = token1.balance(&router, owner.clone()).unwrap(); assert_eq!(token1_balance, Uint128::new(4890)); let token2_balance = token2.balance(&router, owner.clone()).unwrap(); assert_eq!(token2_balance, Uint128::new(4909)); let amm_native_balance = bank_balance(&mut router, &amm, NATIVE_TOKEN_DENOM.to_string()); assert_eq!(amm_native_balance.amount, Uint128::zero()); // assert internal state is consistent let info_amm: InfoResponse = get_info(&router, &amm); println!("{:?}", info_amm); let token1_balance = token1.balance(&router, amm.clone()).unwrap(); let token2_balance = token2.balance(&router, amm.clone()).unwrap(); println!("{} {}", token1_balance, token2_balance); assert_eq!(info_amm.token2_reserve, token2_balance); assert_eq!(info_amm.token1_reserve, token1_balance);}
contract.rs
use cosmwasm_std::{ attr, entry_point, to_binary, Addr, Binary, BlockInfo, CosmosMsg, Decimal, Deps, DepsMut, Env, MessageInfo, Response, StdError, StdResult, Uint128, Uint256, Uint512, WasmMsg,};use cw2::set_contract_version;use cw20::Denom::Cw20;use cw20::{Cw20ExecuteMsg, Denom, Expiration};use cw20_base::contract::query_balance;use std::convert::TryInto;use std::str::FromStr;use crate::error::ContractError;use crate::msg::{ ExecuteMsg, FeeResponse, InfoResponse, InstantiateMsg, QueryMsg, Token1ForToken2PriceResponse, Token2ForToken1PriceResponse, TokenSelect,};use crate::state::{Fees, Token, FEES, FROZEN, OWNER, TOKEN1, TOKEN2};// Version info for migration infopub const CONTRACT_NAME: &str = "crates.io:product-amm";pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");const FEE_SCALE_FACTOR: Uint128 = Uint128::new(10_000);const MAX_FEE_PERCENT: &str = "1";const FEE_DECIMAL_PRECISION: Uint128 = Uint128::new(10u128.pow(20));// Note, you can use StdResult in some functions where you do not// make use of the custom errors#[cfg_attr(not(feature = "library"), entry_point)]pub fn instantiate( deps: DepsMut, _env: Env, _info: MessageInfo, msg: InstantiateMsg,) -> Result<Response, ContractError> { set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; let token1 = Token { reserve: Uint128::zero(), denom: msg.token1_denom.clone(), }; TOKEN1.save(deps.storage, &token1)?; let token2 = Token { denom: msg.token2_denom.clone(), reserve: Uint128::zero(), }; TOKEN2.save(deps.storage, &token2)?; let owner = msg.owner.map(|h| deps.api.addr_validate(&h)).transpose()?; OWNER.save(deps.storage, &owner)?; let protocol_fee_recipient = deps.api.addr_validate(&msg.protocol_fee_recipient)?; let total_fee_percent = msg.lp_fee_percent + msg.protocol_fee_percent; let max_fee_percent = Decimal::from_str(MAX_FEE_PERCENT)?; if total_fee_percent > max_fee_percent { return Err(ContractError::FeesTooHigh { max_fee_percent, total_fee_percent, }); } let fees = Fees { lp_fee_percent: msg.lp_fee_percent, protocol_fee_percent: msg.protocol_fee_percent, protocol_fee_recipient, }; FEES.save(deps.storage, &fees)?; // Depositing is not frozen by default FROZEN.save(deps.storage, &false)?; Ok(Response::new().add_attribute("key", "instantiate"))}// And declare a custom Error variant for the ones where you will want to make use of it#[cfg_attr(not(feature = "library"), entry_point)]pub fn execute( deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg,) -> Result<Response, ContractError> { match msg { ExecuteMsg::AddLiquidity { token1_amount, min_liquidity, expiration, token2_amount } => { if FROZEN.load(deps.storage)? { return Err(ContractError::FrozenPool {}); } execute_add_liquidity( deps, &info, env, min_liquidity, token1_amount, token2_amount, expiration, ) } ExecuteMsg::RemoveLiquidity { amount, min_token1, min_token2, expiration, } => execute_remove_liquidity(deps, info, env, amount, min_token1, min_token2, expiration), ExecuteMsg::Swap { input_token, input_amount, min_output, expiration, .. } => { if FROZEN.load(deps.storage)? { return Err(ContractError::FrozenPool {}); } execute_swap( deps, &info, input_amount, env, input_token, info.sender.to_string(), min_output, expiration, ) } ExecuteMsg::UpdateConfig { owner, protocol_fee_recipient, lp_fee_percent, protocol_fee_percent, } => execute_update_config( deps, info, owner, lp_fee_percent, protocol_fee_percent, protocol_fee_recipient, ), ExecuteMsg::FreezeDeposits { freeze } => execute_freeze_deposits(deps, info.sender, freeze), }}fn execute_freeze_deposits( deps: DepsMut, sender: Addr, freeze: bool,) -> Result<Response, ContractError> { if let Some(owner) = OWNER.load(deps.storage)? { if sender != owner { return Err(ContractError::UnauthorizedPoolFreeze {}); } } else { return Err(ContractError::UnauthorizedPoolFreeze {}); } FROZEN.save(deps.storage, &freeze)?; Ok(Response::new().add_attribute("action", "freezing-contracts"))}fn check_expiration( expiration: &Option<Expiration>, block: &BlockInfo,) -> Result<(), ContractError> { match expiration { Some(e) => { if e.is_expired(block) { return Err(ContractError::MsgExpirationError {}); } Ok(()) } None => Ok(()), }}pub fn execute_add_liquidity( deps: DepsMut, info: &MessageInfo, env: Env, min_liquidity: Uint128, token1_amount: Uint128, token2_amount: Uint128, expiration: Option<Expiration>,) -> Result<Response, ContractError> { check_expiration(&expiration, &env.block)?; let token1 = TOKEN1.load(deps.storage)?; let token2 = TOKEN2.load(deps.storage)?; let liquidity_amount = token1_amount+token2_amount; if liquidity_amount < min_liquidity { return Err(ContractError::MinLiquidityError { min_liquidity, liquidity_available: liquidity_amount, }); } // Generate cw20 transfer messages if necessary let mut transfer_msgs: Vec<CosmosMsg> = vec![]; if let Cw20(addr) = token1.denom { transfer_msgs.push(get_cw20_transfer_from_msg( &info.sender, &env.contract.address, &addr, token1_amount, )?) } if let Cw20(addr) = token2.denom.clone() { transfer_msgs.push(get_cw20_transfer_from_msg( &info.sender, &env.contract.address, &addr, token2_amount, )?) } TOKEN1.update(deps.storage, |mut token1| -> Result<_, ContractError> { token1.reserve += token1_amount; Ok(token1) })?; TOKEN2.update(deps.storage, |mut token2| -> Result<_, ContractError> { token2.reserve += token2_amount; Ok(token2) })?; Ok(Response::new() .add_messages(transfer_msgs) .add_attributes(vec![ attr("token1_amount", token1_amount), attr("token2_amount", token2_amount), attr("liquidity_received", liquidity_amount), ]))}fn get_cw20_transfer_from_msg( owner: &Addr, recipient: &Addr, token_addr: &Addr, token_amount: Uint128,) -> StdResult<CosmosMsg> { // create transfer cw20 msg let transfer_cw20_msg = Cw20ExecuteMsg::TransferFrom { owner: owner.into(), recipient: recipient.into(), amount: token_amount, }; let exec_cw20_transfer = WasmMsg::Execute { contract_addr: token_addr.into(), msg: to_binary(&transfer_cw20_msg)?, funds: vec![], }; let cw20_transfer_cosmos_msg: CosmosMsg = exec_cw20_transfer.into(); Ok(cw20_transfer_cosmos_msg)}pub fn execute_update_config( deps: DepsMut, info: MessageInfo, new_owner: Option<String>, lp_fee_percent: Decimal, protocol_fee_percent: Decimal, protocol_fee_recipient: String,) -> Result<Response, ContractError> { let owner = OWNER.load(deps.storage)?; if Some(info.sender) != owner { return Err(ContractError::Unauthorized {}); } let new_owner_addr = new_owner .as_ref() .map(|h| deps.api.addr_validate(h)) .transpose()?; OWNER.save(deps.storage, &new_owner_addr)?; let total_fee_percent = lp_fee_percent + protocol_fee_percent; let max_fee_percent = Decimal::from_str(MAX_FEE_PERCENT)?; if total_fee_percent > max_fee_percent { return Err(ContractError::FeesTooHigh { max_fee_percent, total_fee_percent, }); } let protocol_fee_recipient = deps.api.addr_validate(&protocol_fee_recipient)?; let updated_fees = Fees { protocol_fee_recipient: protocol_fee_recipient.clone(), lp_fee_percent, protocol_fee_percent, }; FEES.save(deps.storage, &updated_fees)?; let new_owner = new_owner.unwrap_or_default(); Ok(Response::new().add_attributes(vec![ attr("new_owner", new_owner), attr("lp_fee_percent", lp_fee_percent.to_string()), attr("protocol_fee_percent", protocol_fee_percent.to_string()), attr("protocol_fee_recipient", protocol_fee_recipient.to_string()), ]))}pub fn execute_remove_liquidity( deps: DepsMut, info: MessageInfo, env: Env, amount: Uint128, min_token1: Uint128, min_token2: Uint128, expiration: Option<Expiration>,) -> Result<Response, ContractError> { check_expiration(&expiration, &env.block)?; let token1 = TOKEN1.load(deps.storage)?; let token2 = TOKEN2.load(deps.storage)?; let total_token_supply = token1.reserve+token2.reserve; if amount > total_token_supply { return Err(ContractError::InsufficientLiquidityError { requested: amount, available: total_token_supply, }); } let token1_amount = amount .checked_mul(token1.reserve) .map_err(StdError::overflow)? .checked_div(total_token_supply) .map_err(StdError::divide_by_zero)?; if token1_amount < min_token1 { return Err(ContractError::MinToken1Error { requested: min_token1, available: token1_amount, }); } let token2_amount = amount .checked_mul(token2.reserve) .map_err(StdError::overflow)? .checked_div(total_token_supply) .map_err(StdError::divide_by_zero)?; if token2_amount < min_token2 { return Err(ContractError::MinToken2Error { requested: min_token2, available: token2_amount, }); } TOKEN1.update(deps.storage, |mut token1| -> Result<_, ContractError> { token1.reserve = token1 .reserve .checked_sub(token1_amount) .map_err(StdError::overflow)?; Ok(token1) })?; TOKEN2.update(deps.storage, |mut token2| -> Result<_, ContractError> { token2.reserve = token2 .reserve .checked_sub(token2_amount) .map_err(StdError::overflow)?; Ok(token2) })?; let token1_transfer_msg = match token1.denom { Denom::Cw20(addr) => get_cw20_transfer_to_msg(&info.sender, &addr, token1_amount)?, Denom::Native(_denom) => {unimplemented!()}, }; let token2_transfer_msg = match token2.denom { Denom::Cw20(addr) => get_cw20_transfer_to_msg(&info.sender, &addr, token2_amount)?, Denom::Native(_denom) => {unimplemented!()}, }; Ok(Response::new() .add_messages(vec![ token1_transfer_msg, token2_transfer_msg, ]) .add_attributes(vec![ attr("liquidity_burned", amount), attr("token1_returned", token1_amount), attr("token2_returned", token2_amount), ]))}fn get_cw20_transfer_to_msg( recipient: &Addr, token_addr: &Addr, token_amount: Uint128,) -> StdResult<CosmosMsg> { // create transfer cw20 msg let transfer_cw20_msg = Cw20ExecuteMsg::Transfer { recipient: recipient.into(), amount: token_amount, }; let exec_cw20_transfer = WasmMsg::Execute { contract_addr: token_addr.into(), msg: to_binary(&transfer_cw20_msg)?, funds: vec![], }; let cw20_transfer_cosmos_msg: CosmosMsg = exec_cw20_transfer.into(); Ok(cw20_transfer_cosmos_msg)}fn get_fee_transfer_msg( sender: &Addr, recipient: &Addr, fee_denom: &Denom, amount: Uint128,) -> StdResult<CosmosMsg> { match fee_denom { Denom::Cw20(addr) => get_cw20_transfer_from_msg(sender, recipient, addr, amount), Denom::Native(_denom) => {unimplemented!()}, }}fn fee_decimal_to_uint128(decimal: Decimal) -> StdResult<Uint128> { let result: Uint128 = decimal .atomics() .checked_mul(FEE_SCALE_FACTOR) .map_err(StdError::overflow)?; Ok(result / FEE_DECIMAL_PRECISION)}fn get_input_price( input_amount: Uint128, input_reserve: Uint128, output_reserve: Uint128, fee_percent: Decimal,) -> StdResult<Uint128> { if input_reserve == Uint128::zero() || output_reserve == Uint128::zero() { return Err(StdError::generic_err("No liquidity")); }; let fee_percent = fee_decimal_to_uint128(fee_percent)?; let fee_reduction_percent = FEE_SCALE_FACTOR - fee_percent; let input_amount_with_fee = Uint512::from(input_amount.full_mul(fee_reduction_percent)); let numerator = input_amount_with_fee .checked_mul(Uint512::from(output_reserve)) .map_err(StdError::overflow)?; let denominator = Uint512::from(input_reserve) .checked_mul(Uint512::from(FEE_SCALE_FACTOR)) .map_err(StdError::overflow)? .checked_add(input_amount_with_fee) .map_err(StdError::overflow)?; Ok(numerator .checked_div(denominator) .map_err(StdError::divide_by_zero)? .try_into()?)}fn get_protocol_fee_amount(input_amount: Uint128, fee_percent: Decimal) -> StdResult<Uint128> { if fee_percent.is_zero() { return Ok(Uint128::zero()); } let fee_percent = fee_decimal_to_uint128(fee_percent)?; Ok(input_amount .full_mul(fee_percent) .checked_div(Uint256::from(FEE_SCALE_FACTOR)) .map_err(StdError::divide_by_zero)? .try_into()?)}#[allow(clippy::too_many_arguments)]pub fn execute_swap( deps: DepsMut, info: &MessageInfo, input_amount: Uint128, _env: Env, input_token_enum: TokenSelect, recipient: String, min_token: Uint128, expiration: Option<Expiration>,) -> Result<Response, ContractError> { check_expiration(&expiration, &_env.block)?; let input_token_item = match input_token_enum { TokenSelect::Token1 => TOKEN1, TokenSelect::Token2 => TOKEN2, }; let input_token = input_token_item.load(deps.storage)?; let output_token_item = match input_token_enum { TokenSelect::Token1 => TOKEN2, TokenSelect::Token2 => TOKEN1, }; let output_token = output_token_item.load(deps.storage)?; let fees = FEES.load(deps.storage)?; let total_fee_percent = fees.lp_fee_percent + fees.protocol_fee_percent; let token_bought = get_input_price( input_amount, input_token.reserve, output_token.reserve, total_fee_percent, )?; if min_token > token_bought { return Err(ContractError::SwapMinError { min: min_token, available: token_bought, }); } // Calculate fees let protocol_fee_amount = get_protocol_fee_amount(input_amount, fees.protocol_fee_percent)?; let input_amount_minus_protocol_fee = input_amount - protocol_fee_amount; let mut msgs = match input_token.denom.clone() { Denom::Cw20(addr) => vec![get_cw20_transfer_from_msg( &info.sender, &_env.contract.address, &addr, input_amount_minus_protocol_fee, )?], Denom::Native(_) => vec![], }; // Send protocol fee to protocol fee recipient if !protocol_fee_amount.is_zero() { msgs.push(get_fee_transfer_msg( &info.sender, &fees.protocol_fee_recipient, &input_token.denom, protocol_fee_amount, )?) } let recipient = deps.api.addr_validate(&recipient)?; // Create transfer to message msgs.push(match output_token.denom { Denom::Cw20(addr) => get_cw20_transfer_to_msg(&recipient, &addr, token_bought)?, Denom::Native(_denom) => {unimplemented!()}, }); input_token_item.update( deps.storage, |mut input_token| -> Result<_, ContractError> { input_token.reserve = input_token .reserve .checked_add(input_amount_minus_protocol_fee) .map_err(StdError::overflow)?; Ok(input_token) }, )?; output_token_item.update( deps.storage, |mut output_token| -> Result<_, ContractError> { output_token.reserve = output_token .reserve .checked_sub(token_bought) .map_err(StdError::overflow)?; Ok(output_token) }, )?; Ok(Response::new().add_messages(msgs).add_attributes(vec![ attr("native_sold", input_amount), attr("token_bought", token_bought), ]))}#[cfg_attr(not(feature = "library"), entry_point)]pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> { match msg { QueryMsg::Balance { address } => to_binary(&query_balance(deps, address)?), QueryMsg::Info {} => to_binary(&query_info(deps)?), QueryMsg::Token1ForToken2Price { token1_amount } => { to_binary(&query_token1_for_token2_price(deps, token1_amount)?) } QueryMsg::Token2ForToken1Price { token2_amount } => { to_binary(&query_token2_for_token1_price(deps, token2_amount)?) } QueryMsg::Fee {} => to_binary(&query_fee(deps)?), }}pub fn query_info(deps: Deps) -> StdResult<InfoResponse> { let token1 = TOKEN1.load(deps.storage)?; let token2 = TOKEN2.load(deps.storage)?; // TODO get total supply Ok(InfoResponse { token1_reserve: token1.reserve, token1_denom: token1.denom, token2_reserve: token2.reserve, token2_denom: token2.denom, })}pub fn query_token1_for_token2_price( deps: Deps, token1_amount: Uint128,) -> StdResult<Token1ForToken2PriceResponse> { let token1 = TOKEN1.load(deps.storage)?; let token2 = TOKEN2.load(deps.storage)?; let fees = FEES.load(deps.storage)?; let total_fee_percent = fees.lp_fee_percent + fees.protocol_fee_percent; let token2_amount = get_input_price( token1_amount, token1.reserve, token2.reserve, total_fee_percent, )?; Ok(Token1ForToken2PriceResponse { token2_amount })}pub fn query_token2_for_token1_price( deps: Deps, token2_amount: Uint128,) -> StdResult<Token2ForToken1PriceResponse> { let token1 = TOKEN1.load(deps.storage)?; let token2 = TOKEN2.load(deps.storage)?; let fees = FEES.load(deps.storage)?; let total_fee_percent = fees.lp_fee_percent + fees.protocol_fee_percent; let token1_amount = get_input_price( token2_amount, token2.reserve, token1.reserve, total_fee_percent, )?; Ok(Token2ForToken1PriceResponse { token1_amount })}pub fn query_fee(deps: Deps) -> StdResult<FeeResponse> { let fees = FEES.load(deps.storage)?; let owner = OWNER.load(deps.storage)?.map(|o| o.into_string()); Ok(FeeResponse { owner, lp_fee_percent: fees.lp_fee_percent, protocol_fee_percent: fees.protocol_fee_percent, protocol_fee_recipient: fees.protocol_fee_recipient.into_string(), })}#[cfg(test)]mod tests { use super::*; #[test] fn test_get_input_price() { let fee_percent = Decimal::from_str("0.03").unwrap(); // Base case assert_eq!( get_input_price( Uint128::new(10), Uint128::new(100), Uint128::new(100), fee_percent ) .unwrap(), Uint128::new(9) ); // No input reserve error let err = get_input_price( Uint128::new(10), Uint128::new(0), Uint128::new(100), fee_percent, ) .unwrap_err(); assert_eq!(err, StdError::generic_err("No liquidity")); // No output reserve error let err = get_input_price( Uint128::new(10), Uint128::new(100), Uint128::new(0), fee_percent, ) .unwrap_err(); assert_eq!(err, StdError::generic_err("No liquidity")); // No reserve error let err = get_input_price( Uint128::new(10), Uint128::new(0), Uint128::new(0), fee_percent, ) .unwrap_err(); assert_eq!(err, StdError::generic_err("No liquidity")); }}
msg.rs
use schemars::JsonSchema;use serde::{Deserialize, Serialize};use cosmwasm_std::{Decimal, Uint128};use cw20::{Denom, Expiration};use cosmwasm_schema::QueryResponses;#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]pub struct InstantiateMsg { pub token1_denom: Denom, pub token2_denom: Denom, pub owner: Option<String>, pub protocol_fee_recipient: String, // NOTE: Fees percents are out of 100 e.g., 1 = 1% pub protocol_fee_percent: Decimal, pub lp_fee_percent: Decimal,}#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]pub enum TokenSelect { Token1, Token2,}#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]#[serde(rename_all = "snake_case")]pub enum ExecuteMsg { AddLiquidity { token1_amount: Uint128, token2_amount: Uint128, min_liquidity: Uint128, expiration: Option<Expiration>, }, RemoveLiquidity { amount: Uint128, min_token1: Uint128, min_token2: Uint128, expiration: Option<Expiration>, }, Swap { input_token: TokenSelect, input_amount: Uint128, min_output: Uint128, expiration: Option<Expiration>, }, UpdateConfig { owner: Option<String>, lp_fee_percent: Decimal, protocol_fee_percent: Decimal, protocol_fee_recipient: String, }, // Freeze adding new deposits FreezeDeposits { freeze: bool, },}#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, QueryResponses)]#[serde(rename_all = "snake_case")]pub enum QueryMsg { #[returns(cw20::BalanceResponse)] Balance { address: String }, #[returns(InfoResponse)] Info {}, #[returns(Token1ForToken2PriceResponse)] Token1ForToken2Price { token1_amount: Uint128 }, #[returns(Token2ForToken1PriceResponse)] Token2ForToken1Price { token2_amount: Uint128 }, #[returns(FeeResponse)] Fee {},}#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]pub struct MigrateMsg { pub owner: Option<String>, pub protocol_fee_recipient: String, pub protocol_fee_percent: Decimal, pub lp_fee_percent: Decimal, pub freeze_pool: bool,}#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]pub struct InfoResponse { pub token1_reserve: Uint128, pub token1_denom: Denom, pub token2_reserve: Uint128, pub token2_denom: Denom,}#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, JsonSchema)]pub struct FeeResponse { pub owner: Option<String>, pub lp_fee_percent: Decimal, pub protocol_fee_percent: Decimal, pub protocol_fee_recipient: String,}#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]pub struct Token1ForToken2PriceResponse { pub token2_amount: Uint128,}#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]pub struct Token2ForToken1PriceResponse { pub token1_amount: Uint128,}
error.rs
use cosmwasm_std::{Decimal, StdError, Uint128};use thiserror::Error;#[derive(Error, Debug, PartialEq)]pub enum ContractError { #[error("{0}")] Std(#[from] StdError), #[error("{0}")] Cw20Error(#[from] cw20_base::ContractError), #[error("None Error")] NoneError {}, #[error("Unauthorized")] Unauthorized {}, // Add any other custom errors you like here. // Look at https://docs.rs/thiserror/1.0.21/thiserror/ for details. #[error("Min liquidity error: requested: {min_liquidity}, available: {liquidity_available}")] MinLiquidityError { min_liquidity: Uint128, liquidity_available: Uint128, }, #[error("Max token error: max_token: {max_token}, tokens_required: {tokens_required}")] MaxTokenError { max_token: Uint128, tokens_required: Uint128, }, #[error("Insufficient liquidity error: requested: {requested}, available: {available}")] InsufficientLiquidityError { requested: Uint128, available: Uint128, }, #[error("Min token1 error: requested: {requested}, available: {available}")] MinToken1Error { requested: Uint128, available: Uint128, }, #[error("Min token2 error: requested: {requested}, available: {available}")] MinToken2Error { requested: Uint128, available: Uint128, }, #[error("Incorrect native denom: provided: {provided}, required: {required}")] IncorrectNativeDenom { provided: String, required: String }, #[error("Swap min error: min: {min}, available: {available}")] SwapMinError { min: Uint128, available: Uint128 }, #[error("MsgExpirationError")] MsgExpirationError {}, #[error("Total fee ({total_fee_percent}) percent is higher than max ({max_fee_percent})")] FeesTooHigh { max_fee_percent: Decimal, total_fee_percent: Decimal, }, #[error("InsufficientFunds")] InsufficientFunds {}, #[error("Uknown reply id: {id}")] UnknownReplyId { id: u64 }, #[error("Failed to instantiate lp token")] InstantiateLpTokenError {}, #[error("The output amm provided is invalid")] InvalidOutputPool {}, #[error("Unauthorized pool freeze - sender is not an owner or owner has not been set")] UnauthorizedPoolFreeze {}, #[error("This pools is frozen - you can not deposit or swap tokens")] FrozenPool {},}
state.rs
use schemars::JsonSchema;use serde::{Deserialize, Serialize};use cosmwasm_std::{Addr, Decimal, Uint128};use cw20::Denom;use cw_storage_plus::Item;#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]pub struct Token { pub reserve: Uint128, pub denom: Denom,}pub const TOKEN1: Item<Token> = Item::new("token1");pub const TOKEN2: Item<Token> = Item::new("token2");pub const OWNER: Item<Option<Addr>> = Item::new("owner");#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]pub struct Fees { pub protocol_fee_recipient: Addr, pub protocol_fee_percent: Decimal, pub lp_fee_percent: Decimal,}pub const FEES: Item<Fees> = Item::new("fees");pub const FROZEN: Item<bool> = Item::new("frozen");
Credits: CosmWasm by example. You can check the code on Github or open it with VS code.