Skip to main content

Multi-hop Routing

Overview

There are some cases where liquidity between a pair may be low or not exist and you need to perform a multi-hop swap between two or more pairs. The Router contract enables multi-hop swap operations for Cosmos native and CW20 tokens. Swaps execute one-by-one and the last swap will return the ask token.

The initial swap will contain the offer token which determines how the contract message gets proccessed by the Router contract. If the offer token is a native token, we can send an ExecuteMsg to the Router contract directly. Otherwise (if it's a CW20 token), we will need to encode our message within a Cw20ExecuteMsg that gets sent from the address of the token we are swapping.

Native Routing

Use when the initial offer asset is a Cosmos native token.

execute_swap_operations

To create a native multi-hop swap, you need to execute a contract message pointing to the execute_swap_operations endpoint in the Router contract

operations is the only required parameter and holds information regarding each swap stored in a vector containing objects of type SwapOperation.

NOTE

SwapOperation is a helper enum that filters between astro_swaps and native_swaps. The former gets processed as swaps through Astroport's liquidity pools and the latter gets swapped natively (only between native assets).

This should not be confused with Native Routing. Our initial offer asset in this section is still a native asset despite our swap being an astro_swap. As an astro_swap, we are simply swapping between a native and CW20 token through an Astroport liquidity pool.


_37
{
_37
"execute_swap_operations": {
_37
"operations": [
_37
{
_37
"astro_swap": {
_37
"offer_asset_info": {
_37
"native_token": {
_37
"denom": "uluna"
_37
}
_37
},
_37
"ask_asset_info": {
_37
"token": {
_37
"contract_addr": usdcAddress
_37
}
_37
}
_37
}
_37
},
_37
{
_37
"astro_swap": {
_37
"offer_asset_info": {
_37
"token": {
_37
"contract_addr": usdcAddress
_37
}
_37
},
_37
"ask_asset_info": {
_37
"token": {
_37
"contract_addr": zeroAddress
_37
}
_37
}
_37
}
_37
}
_37
],
_37
"minimum_receive": beliefPrice,
_37
"max_spread": "0.005",
_37
"to": "..."
_37
}
_37
}

Coins

Since our initial offer token is a native asset, we need to send the native token along with the transaction message.

The code in this section is an example of what a complete swap message may look like using feather.js to send Coins with our transaction.


_60
// testnet contract addresses
_60
const routerAddress = 'terra1na348k6rvwxje9jj6ftpsapfeyaejxjeq6tuzdmzysps20l6z23smnlv64';
_60
const usdcAddress = 'terra14m3gtr8072t0hsefclfn50w4tdcphn66hvcu8qqf9wmtjyc9uasszqckyc'
_60
const zeroAddress = 'terra1juvel8n8tn3m3tnkq779nktrk7hf9t2kprx7wzs7jlz0gcaqrmqq6rlre5'
_60
_60
const nativeRouterSwap = new MsgExecuteContract(
_60
wallet.key.accAddress('terra'),
_60
routerAddress,
_60
{
_60
"execute_swap_operations": {
_60
"operations": [
_60
{
_60
"astro_swap": {
_60
"offer_asset_info": {
_60
"native_token": {
_60
"denom": "uluna"
_60
}
_60
},
_60
"ask_asset_info": {
_60
"token": {
_60
"contract_addr": usdcAddress
_60
}
_60
}
_60
}
_60
},
_60
{
_60
"astro_swap": {
_60
"offer_asset_info": {
_60
"token": {
_60
"contract_addr": usdcAddress
_60
}
_60
},
_60
"ask_asset_info": {
_60
"token": {
_60
"contract_addr": zeroAddress
_60
}
_60
}
_60
}
_60
}
_60
],
_60
"minimum_receive": beliefPrice,
_60
"max_spread": "0.005",
_60
"to": "..."
_60
}
_60
},
_60
new Coins({ "uluna": "100000" })
_60
)
_60
_60
const fee = new Fee(2000000, { uluna: 200000 });
_60
_60
// // BROADCAST TRANSACTION
_60
const tx = await wallet.createAndSignTx({
_60
msgs: [nativeRouterSwap],
_60
fee,
_60
chainID: 'pisco-1',
_60
});
_60
_60
const result = await lcd.tx.broadcast(tx, 'pisco-1');
_60
_60
console.log(result);

Optional Parameters

The execute_swap_operations endpoint takes in the following optional parameters:

  • minimum_receive: Minimum expected return amount
  • max_spread: The difference between the ask amount before and after the swap. If the swap spread exceeds the provided max limit, the swap will fail.
  • to: Address receiving tokens (if different from sender)

_37
{
_37
"execute_swap_operations": {
_37
"operations": [
_37
{
_37
"astro_swap": {
_37
"offer_asset_info": {
_37
"native_token": {
_37
"denom": "uluna"
_37
}
_37
},
_37
"ask_asset_info": {
_37
"token": {
_37
"contract_addr": usdcAddress
_37
}
_37
}
_37
}
_37
},
_37
{
_37
"astro_swap": {
_37
"offer_asset_info": {
_37
"token": {
_37
"contract_addr": usdcAddress
_37
}
_37
},
_37
"ask_asset_info": {
_37
"token": {
_37
"contract_addr": zeroAddress
_37
}
_37
}
_37
}
_37
}
_37
],
_37
"minimum_receive": beliefPrice,
_37
"max_spread": "0.005",
_37
"to": "..."
_37
}
_37
}

minimum_receive + max_spread

If minimum_receive is provided in combination with max_spread, the pool will check the difference between the return amount (using belief_price) and the real pool price.

minimum_receive +/- the max_spread is the range of possible acceptable prices for this swap.


_37
{
_37
"execute_swap_operations": {
_37
"operations": [
_37
{
_37
"astro_swap": {
_37
"offer_asset_info": {
_37
"native_token": {
_37
"denom": "uluna"
_37
}
_37
},
_37
"ask_asset_info": {
_37
"token": {
_37
"contract_addr": usdcAddress
_37
}
_37
}
_37
}
_37
},
_37
{
_37
"astro_swap": {
_37
"offer_asset_info": {
_37
"token": {
_37
"contract_addr": usdcAddress
_37
}
_37
},
_37
"ask_asset_info": {
_37
"token": {
_37
"contract_addr": zeroAddress
_37
}
_37
}
_37
}
_37
}
_37
],
_37
"minimum_receive": beliefPrice,
_37
"max_spread": "0.005",
_37
"to": "..."
_37
}
_37
}

Calculating minimum_receive

Astroport's native solution to obtain minimum_receive is to query the simulate_swap_operations endpoint from the Router contract and to pass the result of this query into the swap message.


_37
{
_37
"execute_swap_operations": {
_37
"operations": [
_37
{
_37
"astro_swap": {
_37
"offer_asset_info": {
_37
"native_token": {
_37
"denom": "uluna"
_37
}
_37
},
_37
"ask_asset_info": {
_37
"token": {
_37
"contract_addr": usdcAddress
_37
}
_37
}
_37
}
_37
},
_37
{
_37
"astro_swap": {
_37
"offer_asset_info": {
_37
"token": {
_37
"contract_addr": usdcAddress
_37
}
_37
},
_37
"ask_asset_info": {
_37
"token": {
_37
"contract_addr": zeroAddress
_37
}
_37
}
_37
}
_37
}
_37
],
_37
"minimum_receive": beliefPrice,
_37
"max_spread": "0.005",
_37
"to": "..."
_37
}
_37
}

simulate_swap_operations

The simulate_swap_operations endpoint takes in two parameters:

  • offer_amount: The amount of tokens to swap
  • operations: The swap operations to perform

The operations parameter takes in the same vector of SwapOperation objects that our swap message contains.


_35
{
_35
"simulate_swap_operations": {
_35
"offer_amount": "100000",
_35
"operations": [
_35
{
_35
"astro_swap": {
_35
"offer_asset_info": {
_35
"native_token": {
_35
"denom": "uluna"
_35
}
_35
},
_35
"ask_asset_info": {
_35
"token": {
_35
"contract_addr": usdcAddress
_35
}
_35
}
_35
}
_35
},
_35
{
_35
"astro_swap": {
_35
"offer_asset_info": {
_35
"token": {
_35
"contract_addr": usdcAddress
_35
}
_35
},
_35
"ask_asset_info": {
_35
"token": {
_35
"contract_addr": zeroAddress
_35
}
_35
}
_35
}
_35
}
_35
]
_35
}
_35
}

execute_swap_operations

To create a native multi-hop swap, you need to execute a contract message pointing to the execute_swap_operations endpoint in the Router contract

operations is the only required parameter and holds information regarding each swap stored in a vector containing objects of type SwapOperation.

NOTE

SwapOperation is a helper enum that filters between astro_swaps and native_swaps. The former gets processed as swaps through Astroport's liquidity pools and the latter gets swapped natively (only between native assets).

This should not be confused with Native Routing. Our initial offer asset in this section is still a native asset despite our swap being an astro_swap. As an astro_swap, we are simply swapping between a native and CW20 token through an Astroport liquidity pool.

Coins

Since our initial offer token is a native asset, we need to send the native token along with the transaction message.

The code in this section is an example of what a complete swap message may look like using feather.js to send Coins with our transaction.

Optional Parameters

The execute_swap_operations endpoint takes in the following optional parameters:

  • minimum_receive: Minimum expected return amount
  • max_spread: The difference between the ask amount before and after the swap. If the swap spread exceeds the provided max limit, the swap will fail.
  • to: Address receiving tokens (if different from sender)

minimum_receive + max_spread

If minimum_receive is provided in combination with max_spread, the pool will check the difference between the return amount (using belief_price) and the real pool price.

minimum_receive +/- the max_spread is the range of possible acceptable prices for this swap.

Calculating minimum_receive

Astroport's native solution to obtain minimum_receive is to query the simulate_swap_operations endpoint from the Router contract and to pass the result of this query into the swap message.

simulate_swap_operations

The simulate_swap_operations endpoint takes in two parameters:

  • offer_amount: The amount of tokens to swap
  • operations: The swap operations to perform

The operations parameter takes in the same vector of SwapOperation objects that our swap message contains.


_37
{
_37
"execute_swap_operations": {
_37
"operations": [
_37
{
_37
"astro_swap": {
_37
"offer_asset_info": {
_37
"native_token": {
_37
"denom": "uluna"
_37
}
_37
},
_37
"ask_asset_info": {
_37
"token": {
_37
"contract_addr": usdcAddress
_37
}
_37
}
_37
}
_37
},
_37
{
_37
"astro_swap": {
_37
"offer_asset_info": {
_37
"token": {
_37
"contract_addr": usdcAddress
_37
}
_37
},
_37
"ask_asset_info": {
_37
"token": {
_37
"contract_addr": zeroAddress
_37
}
_37
}
_37
}
_37
}
_37
],
_37
"minimum_receive": beliefPrice,
_37
"max_spread": "0.005",
_37
"to": "..."
_37
}
_37
}

CW20 Routing

Use when the initial offer asset is a CW20 token.

Cw20HookMsg

To perform a CW20 swap, you need to execute a contract message pointing to the send endpoint in the CW20 contract of the token you are swapping.

send includes the contract the tokens are being sent to (router contract), the amount to send/swap, and a binary encoded msg containing our contract call.


_44
{
_44
"send": {
_44
"contract": routerAddress,
_44
"amount": "1000000",
_44
"msg": toBase64(
_44
{
_44
"execute_swap_operations": {
_44
"operations": [
_44
{
_44
"astro_swap": {
_44
"offer_asset_info": {
_44
"token": {
_44
"contract_addr": astroAddress
_44
}
_44
},
_44
"ask_asset_info": {
_44
"native_token": {
_44
"denom": "uluna"
_44
}
_44
}
_44
}
_44
},
_44
{
_44
"astro_swap": {
_44
"offer_asset_info": {
_44
"native_token": {
_44
"denom": "uluna"
_44
}
_44
},
_44
"ask_asset_info": {
_44
"token": {
_44
"contract_addr": steakAddress
_44
}
_44
}
_44
}
_44
}
_44
],
_44
"minimum_receive": nativeBeliefPrice,
_44
"max_spread": "0.005"
_44
}
_44
}
_44
)
_44
}
_44
}

execute_swap_operations

Our encoded swap message performs a contract call to the execute_swap_operations endpoint in the Router contract. This is the same operation covered above in Native Routing.


_44
{
_44
"send": {
_44
"contract": routerAddress,
_44
"amount": "1000000",
_44
"msg": toBase64(
_44
{
_44
"execute_swap_operations": {
_44
"operations": [
_44
{
_44
"astro_swap": {
_44
"offer_asset_info": {
_44
"token": {
_44
"contract_addr": astroAddress
_44
}
_44
},
_44
"ask_asset_info": {
_44
"native_token": {
_44
"denom": "uluna"
_44
}
_44
}
_44
}
_44
},
_44
{
_44
"astro_swap": {
_44
"offer_asset_info": {
_44
"native_token": {
_44
"denom": "uluna"
_44
}
_44
},
_44
"ask_asset_info": {
_44
"token": {
_44
"contract_addr": steakAddress
_44
}
_44
}
_44
}
_44
}
_44
],
_44
"minimum_receive": nativeBeliefPrice,
_44
"max_spread": "0.005"
_44
}
_44
}
_44
)
_44
}
_44
}

Encoding our Msg

To encode our message, there are two common options:

The code in this section uses a custom function (toBase64) to display our binary message - this function needs to be defined elsewhere to be used. The actual string representation of our message would be an encoded binary.


_44
{
_44
"send": {
_44
"contract": routerAddress,
_44
"amount": "1000000",
_44
"msg": toBase64(
_44
{
_44
"execute_swap_operations": {
_44
"operations": [
_44
{
_44
"astro_swap": {
_44
"offer_asset_info": {
_44
"token": {
_44
"contract_addr": astroAddress
_44
}
_44
},
_44
"ask_asset_info": {
_44
"native_token": {
_44
"denom": "uluna"
_44
}
_44
}
_44
}
_44
},
_44
{
_44
"astro_swap": {
_44
"offer_asset_info": {
_44
"native_token": {
_44
"denom": "uluna"
_44
}
_44
},
_44
"ask_asset_info": {
_44
"token": {
_44
"contract_addr": steakAddress
_44
}
_44
}
_44
}
_44
}
_44
],
_44
"minimum_receive": nativeBeliefPrice,
_44
"max_spread": "0.005"
_44
}
_44
}
_44
)
_44
}
_44
}

toBase64 Encoder

The code in this section is an example of what a send message may look like feather.js to define our custom function and encode our message.


_65
const toBase64 = (obj) => {
_65
return Buffer.from(JSON.stringify(obj)).toString("base64");
_65
};
_65
_65
const cw20RouterSwap = new MsgExecuteContract(
_65
wallet.key.accAddress('terra'),
_65
astroAddress,
_65
{
_65
"send": {
_65
"contract": routerAddress,
_65
"amount": "1000000",
_65
"msg": toBase64(
_65
{
_65
"execute_swap_operations": {
_65
"operations": [
_65
{
_65
"astro_swap": {
_65
"offer_asset_info": {
_65
"token": {
_65
"contract_addr": astroAddress
_65
}
_65
},
_65
"ask_asset_info": {
_65
"native_token": {
_65
"denom": "uluna"
_65
}
_65
}
_65
}
_65
},
_65
{
_65
"astro_swap": {
_65
"offer_asset_info": {
_65
"native_token": {
_65
"denom": "uluna"
_65
}
_65
},
_65
"ask_asset_info": {
_65
"token": {
_65
"contract_addr": steakAddress
_65
}
_65
}
_65
}
_65
}
_65
],
_65
"minimum_receive": nativeBeliefPrice,
_65
"max_spread": "0.005"
_65
}
_65
}
_65
)
_65
}
_65
}
_65
)
_65
_65
const fee = new Fee(2000000, { uluna: 200000 });
_65
_65
// // BROADCAST TRANSACTION
_65
const tx = await wallet.createAndSignTx({
_65
msgs: [cw20RouterSwap],
_65
fee,
_65
chainID: 'pisco-1',
_65
});
_65
_65
const result = await lcd.tx.broadcast(tx, 'pisco-1');
_65
_65
console.log(result);

Calculating minimum_receive

Lastly, calculating minimum_receive for CW20 swaps is the exact same as it is for native swaps.

Note that since we are performing a CW20 swap, our initial offer asset is a CW20 token and not a native asset like uluna, but the logic remains the same. To learn more about this topic, refer to the section above.


_35
{
_35
"simulate_swap_operations": {
_35
"offer_amount": "1000000",
_35
"operations": [
_35
{
_35
"astro_swap": {
_35
"offer_asset_info": {
_35
"token": {
_35
"contract_addr": astroAddress
_35
}
_35
},
_35
"ask_asset_info": {
_35
"native_token": {
_35
"denom": "uluna"
_35
}
_35
}
_35
}
_35
},
_35
{
_35
"astro_swap": {
_35
"offer_asset_info": {
_35
"native_token": {
_35
"denom": "uluna"
_35
}
_35
},
_35
"ask_asset_info": {
_35
"token": {
_35
"contract_addr": steakAddress
_35
}
_35
}
_35
}
_35
}
_35
]
_35
}
_35
}

Cw20HookMsg

To perform a CW20 swap, you need to execute a contract message pointing to the send endpoint in the CW20 contract of the token you are swapping.

send includes the contract the tokens are being sent to (router contract), the amount to send/swap, and a binary encoded msg containing our contract call.

execute_swap_operations

Our encoded swap message performs a contract call to the execute_swap_operations endpoint in the Router contract. This is the same operation covered above in Native Routing.

Encoding our Msg

To encode our message, there are two common options:

The code in this section uses a custom function (toBase64) to display our binary message - this function needs to be defined elsewhere to be used. The actual string representation of our message would be an encoded binary.

toBase64 Encoder

The code in this section is an example of what a send message may look like feather.js to define our custom function and encode our message.

Calculating minimum_receive

Lastly, calculating minimum_receive for CW20 swaps is the exact same as it is for native swaps.

Note that since we are performing a CW20 swap, our initial offer asset is a CW20 token and not a native asset like uluna, but the logic remains the same. To learn more about this topic, refer to the section above.


_44
{
_44
"send": {
_44
"contract": routerAddress,
_44
"amount": "1000000",
_44
"msg": toBase64(
_44
{
_44
"execute_swap_operations": {
_44
"operations": [
_44
{
_44
"astro_swap": {
_44
"offer_asset_info": {
_44
"token": {
_44
"contract_addr": astroAddress
_44
}
_44
},
_44
"ask_asset_info": {
_44
"native_token": {
_44
"denom": "uluna"
_44
}
_44
}
_44
}
_44
},
_44
{
_44
"astro_swap": {
_44
"offer_asset_info": {
_44
"native_token": {
_44
"denom": "uluna"
_44
}
_44
},
_44
"ask_asset_info": {
_44
"token": {
_44
"contract_addr": steakAddress
_44
}
_44
}
_44
}
_44
}
_44
],
_44
"minimum_receive": nativeBeliefPrice,
_44
"max_spread": "0.005"
_44
}
_44
}
_44
)
_44
}
_44
}