Skip to content

Commit

Permalink
✨ [API-1723] Add CW20 Support (#67)
Browse files Browse the repository at this point in the history
* ➕ add cw20 to workspace cargo.toml

- also runs make update and make upgrade

* ➕ add cw20 to packages cargo.toml

* ✨ implement ability for entrypoint to receive cw20 token and dispatch swap and action call

* ✨ Implement basic Asset type

* ✨ Implement sent asset validation, transfer_partial, and add sent asset to calldata

* Add todos in entry point execute.rs for cw-20 support

* 🚧 Add swap adapter todos for cw-20 support

* ✅ Add asset tests

* ⚡️ Add nonpayable to sent cw20 token validate

* ✅ Add invalid tests to asset validation

* ✅ add more than one coin test to asset validation, run make fmt

* ✨ implement sub method

* ♻️ convert remaining_coin to asset in entry point contract

* ✨ Implement swap method on asset type

* ✨ Implement astroport swap operation conversion with cw20 support

* ✨ Update astroport swap contract for cw20 support

* ✨ Update osmosis swap adapter to support Asset type in query messages

- No changes made to the ExecuteMsg::Swap since Osmosis does not support cw-20 tokens

* ⚡️ reduce unnecessary code in cw20 validation

* ✨ implement more functionality onto Asset type

* ♻️ Change min_coin to min_asset

* ✨ Update swap methods for cw20 support

* ✨ Update entry point contract for cw20 support

* ✨ Update swap adapter contracts for cw20 support

* Update user_swap_tests for cw20 support

* ✅ update execute swap and action test for asset type

* ✅ update execute post swap action tests for asset type

* 🎨 run make fmt

* ✅ Update osmosis swap adapter tests for asset type

* ✅ Update astroport swap adapter tests for asset type

* ✅ update user swap tests for asset type

* ⬆️ run make update

* ⬆️ run make upgrade

* 📝 Update schemas

* 🐛 add .PHONY to makefile

- Was not working for schema since a folder named schema in same dir

* 🐛 properly query cw20 balance in post swap logic

* ✨ Add receive entrypoint to astroport swap adapter

- Also adds unimplemented in the match for osmosis poolmanager since cw20 tokens are not supported there yet

* ✨ add Asset::new() method

* ✅ add get_current_asset_available test

* ⚡️ Consolidate the diff type into_wasm_msg methods into a single method

* ♻️ reduce re-written code

* 🔥 only have a transfer method on asset, removed unused method

* ♻️ Have new Asset method take in Api instead of DepsMut

- For use with queries

* ♻️ use new Asset method in astroport swap adapter contract

* ✨ Update osmosis swap adapter for queries to return asset type

* ✨ add into_astroport_asset method for Asset and use in astro swap adapter contract

* 🔥 Remove todo

* ♻️ Rename NeutronInstantiateMsg to AstroportInstantiateMsg

* ♻️ Use Asset::new in transfer_funds_back function

* ⚡️ Remove need for sent_asset in entry point contract call

- Now, depending on if the entry point is being called directly or through the cw20 receive, it will validate and generate the sent_asset object itself
- Reduces the server side changes needed
- Updates tests
- Updates schema

* ⚡️ Set entry point contract address as sender on cw20 receive in astroport swap adapter contract

* ⚡️ Remove required sent_asset param to swap adapters

- Now the swap adapters gen sent_asset if needed on their own, similar to how entry point does it
- Updates tests

* ✏️ small clean up

* 🚀 Update deployed contracts: neutron testnet

* 🚀 Update deployed contracts: neutron mainnet

* 🚀 Update deployed contracts: osmosis testnet

* 🚀 Update deployed contracts: osmosis mainnet

* 🚀 Update deployed contracts: terra testnet

* 🚀 Update deployed contracts: terra mainnet

* change recover entrypoint to use min_asset

* Support cw20 swap and action with recover entry point

* refactor execute_swap_and_action_with_recover tests

* run make fmt

* refactor test_reply

* add comment on test cases

* run make fmt

* use coin::default for asset::default_native

* add comment

* change namings of Coin to Asset where applicable

* Change BankSend name to Transfer

- Since it now transfers both coins and cw20 assets

* update schemas

* add sent_asset as a param into non-cw20 entry points

* update schema

* add test files for receive entrypoint

* add CW20 Asset tests for swap and action

* delete repeated test

- now tested in test_execite_swap_and_action.rs

* Add cw20 tests to swap and action with recover

* Add cw20 asset tests to test_reply

* add cw20 asset tests to post swap action

* add cw20 tests to test user swap

* fix bug in swap adapter contract and add tests

- must switch info.sender after the sent_asset is created

* Update deploy.py to be more robust and wait longer

- broadcast_tx_commit was not meant to be used in prod

* update neutron testnet deployed contracts

* update neutron mainnet contracts

* update deploy script to use /cosmos/tx/v1beta1/txs

* update deployed terra testnet contracts

* update terra mainnet deployed contracts

* update osmosis testnet deployed contracts

* update osmosis mainnet deployed contracts

* update chain config urls

* update tests

* run make fmt

* update deployed terra contracts

* update neutron deployed contracts

* add salt to deployed contracts output
  • Loading branch information
NotJeremyLiu authored Nov 15, 2023
1 parent b5b5ec1 commit 08771a9
Show file tree
Hide file tree
Showing 44 changed files with 4,002 additions and 1,430 deletions.
202 changes: 105 additions & 97 deletions Cargo.lock

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@ keywords = ["cosmwasm"]

[workspace.dependencies]
astroport = "2.8"
cosmwasm-schema = "1.3"
cosmwasm-std = { version = "1.3", features = ["stargate"] }
cosmwasm-schema = "1.4"
cosmwasm-std = { version = "1.4", features = ["stargate"] }
cosmos-sdk-proto = { version = "0.19", default-features = false }
cw2 = "1.1"
cw20 = "1.1"
cw-storage-plus = "1"
cw-utils = "1.0.1"
ibc-proto = { version = "0.32.1", default-features = false }
Expand Down
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ fmt:

# Only generates the entry point schema since that is the only
# contract that can be called externally.
.PHONY: schema
schema:
cargo run --package skip-api-entry-point --bin schema

Expand Down
1 change: 1 addition & 0 deletions contracts/adapters/swap/astroport/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ library = []
astroport = { workspace = true }
cosmwasm-schema = { workspace = true }
cosmwasm-std = { workspace = true }
cw20 = { workspace = true }
cw-storage-plus = { workspace = true }
cw-utils = { workspace = true }
skip = { workspace = true }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use cosmwasm_schema::write_api;
use skip::swap::{ExecuteMsg, NeutronInstantiateMsg as InstantiateMsg, QueryMsg};
use skip::swap::{AstroportInstantiateMsg as InstantiateMsg, ExecuteMsg, QueryMsg};

fn main() {
write_api! {
Expand Down
187 changes: 118 additions & 69 deletions contracts/adapters/swap/astroport/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,24 @@ use crate::{
state::{ENTRY_POINT_CONTRACT_ADDRESS, ROUTER_CONTRACT_ADDRESS},
};
use astroport::{
asset::{Asset, AssetInfo},
pair::{
QueryMsg as PairQueryMsg, ReverseSimulationResponse, SimulationResponse,
MAX_ALLOWED_SLIPPAGE,
},
router::ExecuteMsg as RouterExecuteMsg,
};
use cosmwasm_std::{
entry_point, to_binary, Addr, Binary, Coin, Decimal, Deps, DepsMut, Env, MessageInfo, Response,
Uint128, WasmMsg,
entry_point, from_binary, to_binary, Addr, Api, Binary, Decimal, Deps, DepsMut, Env,
MessageInfo, Response, Uint128, WasmMsg,
};
use cw20::{Cw20Coin, Cw20ReceiveMsg};
use cw_utils::one_coin;
use skip::swap::{
execute_transfer_funds_back, ExecuteMsg, NeutronInstantiateMsg as InstantiateMsg, QueryMsg,
SwapOperation,
use skip::{
asset::Asset,
swap::{
execute_transfer_funds_back, AstroportInstantiateMsg as InstantiateMsg, Cw20HookMsg,
ExecuteMsg, QueryMsg, SwapOperation,
},
};

///////////////////
Expand Down Expand Up @@ -56,6 +59,33 @@ pub fn instantiate(
))
}

///////////////
/// RECEIVE ///
///////////////

// Receive is the main entry point for the contract to
// receive cw20 tokens and execute the swap
pub fn receive_cw20(
deps: DepsMut,
env: Env,
mut info: MessageInfo,
cw20_msg: Cw20ReceiveMsg,
) -> ContractResult<Response> {
let sent_asset = Asset::Cw20(Cw20Coin {
address: info.sender.to_string(),
amount: cw20_msg.amount,
});
sent_asset.validate(&deps, &env, &info)?;

// Set the sender to the originating address that triggered the cw20 send call
// This is later validated / enforced to be the entry point contract address
info.sender = deps.api.addr_validate(&cw20_msg.sender)?;

match from_binary(&cw20_msg.msg)? {
Cw20HookMsg::Swap { operations } => execute_swap(deps, env, info, sent_asset, operations),
}
}

///////////////
/// EXECUTE ///
///////////////
Expand All @@ -68,17 +98,29 @@ pub fn execute(
msg: ExecuteMsg,
) -> ContractResult<Response> {
match msg {
ExecuteMsg::Swap { operations } => execute_swap(deps, env, info, operations),
ExecuteMsg::TransferFundsBack { swapper } => {
Ok(execute_transfer_funds_back(deps, env, info, swapper)?)
ExecuteMsg::Receive(cw20_msg) => receive_cw20(deps, env, info, cw20_msg),
ExecuteMsg::Swap { operations } => {
let sent_asset: Asset = one_coin(&info)?.into();
execute_swap(deps, env, info, sent_asset, operations)
}
ExecuteMsg::TransferFundsBack {
swapper,
return_denom,
} => Ok(execute_transfer_funds_back(
deps,
env,
info,
swapper,
return_denom,
)?),
}
}

fn execute_swap(
deps: DepsMut,
env: Env,
info: MessageInfo,
sent_asset: Asset,
operations: Vec<SwapOperation>,
) -> ContractResult<Response> {
// Get entry point contract address from storage
Expand All @@ -89,21 +131,25 @@ fn execute_swap(
return Err(ContractError::Unauthorized);
}

// Get coin in from the message info, error if there is not exactly one coin sent
let coin_in = one_coin(&info)?;

// Create the astroport swap message
let swap_msg = create_astroport_swap_msg(
deps.api,
ROUTER_CONTRACT_ADDRESS.load(deps.storage)?,
coin_in,
operations,
sent_asset,
&operations,
)?;

let return_denom = match operations.last() {
Some(last_op) => last_op.denom_out.clone(),
None => return Err(ContractError::SwapOperationsEmpty),
};

// Create the transfer funds back message
let transfer_funds_back_msg = WasmMsg::Execute {
contract_addr: env.contract.address.to_string(),
msg: to_binary(&ExecuteMsg::TransferFundsBack {
swapper: info.sender,
swapper: entry_point_contract_address,
return_denom,
})?,
funds: vec![],
};
Expand All @@ -120,12 +166,16 @@ fn execute_swap(

// Converts the swap operations to astroport AstroSwap operations
fn create_astroport_swap_msg(
api: &dyn Api,
router_contract_address: Addr,
coin_in: Coin,
swap_operations: Vec<SwapOperation>,
asset_in: Asset,
swap_operations: &[SwapOperation],
) -> ContractResult<WasmMsg> {
// Convert the swap operations to astroport swap operations
let astroport_swap_operations = swap_operations.into_iter().map(From::from).collect();
let astroport_swap_operations = swap_operations
.iter()
.map(|swap_operation| swap_operation.into_astroport_swap_operation(api))
.collect();

// Create the astroport router execute message arguments
let astroport_router_msg_args = RouterExecuteMsg::ExecuteSwapOperations {
Expand All @@ -136,11 +186,10 @@ fn create_astroport_swap_msg(
};

// Create the astroport router swap message
let swap_msg = WasmMsg::Execute {
contract_addr: router_contract_address.to_string(),
msg: to_binary(&astroport_router_msg_args)?,
funds: vec![coin_in],
};
let swap_msg = asset_in.into_wasm_msg(
router_contract_address.to_string(),
to_binary(&astroport_router_msg_args)?,
)?;

Ok(swap_msg)
}
Expand All @@ -155,122 +204,122 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> ContractResult<Binary> {
QueryMsg::RouterContractAddress {} => {
to_binary(&ROUTER_CONTRACT_ADDRESS.load(deps.storage)?)
}
QueryMsg::SimulateSwapExactCoinIn {
coin_in,
QueryMsg::SimulateSwapExactAssetIn {
asset_in,
swap_operations,
} => to_binary(&query_simulate_swap_exact_coin_in(
} => to_binary(&query_simulate_swap_exact_asset_in(
deps,
coin_in,
asset_in,
swap_operations,
)?),
QueryMsg::SimulateSwapExactCoinOut {
coin_out,
QueryMsg::SimulateSwapExactAssetOut {
asset_out,
swap_operations,
} => to_binary(&query_simulate_swap_exact_coin_out(
} => to_binary(&query_simulate_swap_exact_asset_out(
deps,
coin_out,
asset_out,
swap_operations,
)?),
}
.map_err(From::from)
}

// Queries the astroport router contract to simulate a swap exact amount in
fn query_simulate_swap_exact_coin_in(
fn query_simulate_swap_exact_asset_in(
deps: Deps,
coin_in: Coin,
asset_in: Asset,
swap_operations: Vec<SwapOperation>,
) -> ContractResult<Coin> {
) -> ContractResult<Asset> {
// Error if swap operations is empty
let Some(first_op) = swap_operations.first() else {
return Err(ContractError::SwapOperationsEmpty);
};

// Ensure coin_in's denom is the same as the first swap operation's denom in
if coin_in.denom != first_op.denom_in {
// Ensure asset_in's denom is the same as the first swap operation's denom in
if asset_in.denom() != first_op.denom_in {
return Err(ContractError::CoinInDenomMismatch);
}

// Iterate through the swap operations, querying the astroport pool contracts to get the coin out
// for each swap operation, and then updating the coin out for the next swap operation until the
// coin out for the last swap operation is found.
let coin_out = swap_operations.iter().try_fold(
coin_in,
|coin_out, operation| -> Result<_, ContractError> {
let asset_out = swap_operations.iter().try_fold(
asset_in,
|asset_out, operation| -> Result<_, ContractError> {
// Get the astroport offer asset type
let astroport_offer_asset = asset_out.into_astroport_asset(deps.api)?;

// Query the astroport pool contract to get the coin out for the swap operation
let res: SimulationResponse = deps.querier.query_wasm_smart(
&operation.pool,
&PairQueryMsg::Simulation {
offer_asset: Asset {
info: AssetInfo::NativeToken {
denom: coin_out.denom,
},
amount: coin_out.amount,
},
offer_asset: astroport_offer_asset,
ask_asset_info: None,
},
)?;

// Assert the operation does not exceed the max spread limit
assert_max_spread(res.return_amount, res.spread_amount)?;

Ok(Coin {
denom: operation.denom_out.clone(),
amount: res.return_amount,
})
Ok(Asset::new(
deps.api,
&operation.denom_out,
res.return_amount,
))
},
)?;

// Return the coin out
Ok(coin_out)
// Return the asset out
Ok(asset_out)
}

// Queries the astroport pool contracts to simulate a multi-hop swap exact amount out
fn query_simulate_swap_exact_coin_out(
fn query_simulate_swap_exact_asset_out(
deps: Deps,
coin_out: Coin,
asset_out: Asset,
swap_operations: Vec<SwapOperation>,
) -> ContractResult<Coin> {
) -> ContractResult<Asset> {
// Error if swap operations is empty
let Some(last_op) = swap_operations.last() else {
return Err(ContractError::SwapOperationsEmpty);
};

// Ensure coin_out's denom is the same as the last swap operation's denom out
if coin_out.denom != last_op.denom_out {
if asset_out.denom() != last_op.denom_out {
return Err(ContractError::CoinOutDenomMismatch);
}

// Iterate through the swap operations in reverse order, querying the astroport pool contracts
// contracts to get the coin in needed for each swap operation, and then updating the coin in
// needed for the next swap operation until the coin in needed for the first swap operation is found.
let coin_in_needed = swap_operations.iter().rev().try_fold(
coin_out,
|coin_in_needed, operation| -> Result<_, ContractError> {
let asset_in_needed = swap_operations.iter().rev().try_fold(
asset_out,
|asset_in_needed, operation| -> Result<Asset, ContractError> {
// Get the astroport ask asset type
let astroport_ask_asset = asset_in_needed.into_astroport_asset(deps.api)?;

// Query the astroport pool contract to get the coin in needed for the swap operation
let res: ReverseSimulationResponse = deps.querier.query_wasm_smart(
&operation.pool,
&PairQueryMsg::ReverseSimulation {
offer_asset_info: None,
ask_asset: Asset {
info: AssetInfo::NativeToken {
denom: coin_in_needed.denom,
},
amount: coin_in_needed.amount,
},
ask_asset: astroport_ask_asset,
},
)?;

// Assert the operation does not exceed the max spread limit
assert_max_spread(res.offer_amount, res.spread_amount)?;

Ok(Coin {
denom: operation.denom_in.clone(),
amount: res.offer_amount.checked_add(Uint128::one())?,
})
Ok(Asset::new(
deps.api,
&operation.denom_in,
res.offer_amount.checked_add(Uint128::one())?,
))
},
)?;

// Return the coin in needed
Ok(coin_in_needed)
Ok(asset_in_needed)
}

fn assert_max_spread(return_amount: Uint128, spread_amount: Uint128) -> ContractResult<()> {
Expand Down
Loading

0 comments on commit 08771a9

Please sign in to comment.