The following contract is a swap and forward contract that takes the received tokens, swaps them via a swaprouter contract, and sends them to an IBC receiver.
The goal is to use this contract to provide crosschain swaps: sending an ICS20 transfer on chain A, receiving it on osmosis, swapping for a different token, and forwarding to a different chain.
There are two versions of the crosschain swaps contract. The first version allows users to swap via instructions in the memo as described above. This is enough to do swaps with arbitrary complexity but still requires users to build the memo manually, which implies knowing about the channels used to transfer the tokens in each intermediate hop, the canonical ibc denoms of tokens on osmosis, and when to use packet forward middleware vs callbacks.
The second version of the contract simplifies this for users by keeping registries with the necessary information and doing the token unwinding automatically.
This documentation is for the latest version of the contract. For v1, see the previous docs
To instantiate the contract, you need to specify the following parameters:
- swap_contract: the swaprouter contract to be used
- governor: The address that will be allowed to manage which swap_contract to use
{"swap_contract": "osmo1thiscontract", "governor": "osmo1..."}
Assuming the current implementation of the wasm middleware on Osmosis v14 (x/ibc-hooks/v0.0.6
), the memo
of an IBC transfer to do crosschain swaps would look as follows:
{"wasm": {
"contract": "osmo1crosschainswapscontract",
"msg": {
"osmosis_swap": {
"output_denom":"token1",
"slippage":{"twap": {"slippage_percentage":"20", "window_seconds": 10}},
"receiver":"juno1receiver",
"on_failed_delivery": "do_nothing",
"next_memo":null
}
}
}}
TODO: Expand documentation on how to specify receivers
Channels are determined by the prefixes specified in the contract during instantiation, so the user needs to provide a receiver with the supported prefix. This will probably change in the future
The slippage
can be set to a percentage of the twap price (as shown above), or as
the minimum amount of tokens expected to be received: {"min_output_amount": "100"}
.
The on_failed_delivery
field can be set to do_nothing
or a local recovery addr
via {"local_recovery_addr": "osmo1..."}
. If set to do_nothing
, the contract will
not track the packet, and the user will not be able to recover the funds if the packet
fails. If set to a local recovery addr, the contract will track the packet, and
the specified address will be able to execute {"recover": {}}
on the crosschain swaps
contract to recover the funds.
The next_memo
key, if provided, will be added to the IBC transfer as the memo
for that transfer. This can be useful if the receiving chain also has IBC hooks
on transfers. In that case, this can be used to specify how the receiver should
deal with the received tokens (mostly useful when the receiver is a contract or
another ibc actor).
Any JSON object is accepted as a valid memo, as long as it doesn't contain the key "ibc_callback". That key is used internally for the contract to track the success or failure of the packet delivery.
This is general high level description of failures happen during cross chain swaps.
When doing cross chain transfers and swaps it is important to know where funds will end up in case of inability to transfer tokens, swap tokens, or some generic infrastructure failures.
We will consider the scenario where a user transfers tokens from chain A to Osmosis, swaps, and transfers the result further to chain B.
When user sends tokens for XCS, in case of packet timeout or failure, the transaction on Osmosis is never settled and the user is returned all his tokens.
It is important to consider who the sender is here. If the sender is a contract (like the outpost
contract, which is a thin wrapper around IBC sends),
the contract is considered the sender and tokens will be refunded to it. It is the contract's responsibility to allow the original user to recover these funds.
The outpost contract does not currently provide this functionality as it is currently mostly used as an example for integrators. If tokens were to get stuck on
the outpost contract, the user would need either a contract migration or help from governance to recover them.
In the case of multi-hop sends, the behaviour may vary (and depends on the versions of packet forward middleware implemented in the intermediate chains. In the latest version of pfm, the tx will fail and the tokens will be refunded to the sender. on XCSv1, if the intermediate chain does not implement PFM, the tokens will be stuck on the intermediate chain; XCSv2, however, implements a registry of which chains that support PFM and will not allow sending tokens to chains that don't support it.
In case of success of swap, swapped tokens are forwarded to chain B. If for any reason delivery to chain B fails,
the tokens are retained on the crosschain-swaps
contract and can be recovered by the failed delivery address if specified.
Failed delivery addresses are an osmosis account. The owner of that account needs to be able to use it to call the contract
and the recovered tokens will be sent to that account.
Osmosis ensures that both timeout and failures of IBC packets attempted to be delivered to B work like that.
The contract will return the following response:
pub struct CrosschainSwapResponse {
pub sent_amount: Uint128,
pub denom: String,
pub channel_id: String,
pub receiver: String,
pub packet_sequence: u64,
}
or as json:
{"sent_amount": "818", "denom": "token0", "channel_id": "channel-42", "receiver": "juno1receiver", "packet_sequence": 17}
Here are some usage examples of sending tokens on the live versions of the contracts:
To swap from a chain's native token to another chain's native token.
For this example we will swap JUNO stored in the Juno, for ATOM and send it to Gaia.
To do that you can execute an IBC transfer on the first chain (juno) with a memo like the following:
{
"wasm": {
"contract": "osmo1uwk8xc6q0s6t5qcpr6rht3sczu6du83xq8pwxjua0hfj5hzcnh3sqxwvxs", // XCS contract
"msg": {
"osmosis_swap": {
"output_denom": "ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2", // ATOM on osmosis
"slippage": {
"twap": {
"slippage_percentage": "20",
"window_seconds": 10
}
},
"receiver": "cosmos1tfu4j7nzfhtex2wyp946rm02748zxu8w0kvt87", // Address on gaia
"on_failed_delivery": "do_nothing"
}
}
}
}
The full command to execute this is:
junod --node https://rpc.cosmos.directory:443/juno tx ibc-transfer transfer transfer channel-0 osmo1uwk8xc6q0s6t5qcpr6rht3sczu6du83xq8pwxjua0hfj5hzcnh3sqxwvxs 100ujuno \
--from testaccount -y --gas auto --gas-prices 0.1ujuno --gas-adjustment 1.3 \
--memo '{"wasm":{"contract":"osmo1uwk8xc6q0s6t5qcpr6rht3sczu6du83xq8pwxjua0hfj5hzcnh3sqxwvxs","msg":{"osmosis_swap":{"output_denom":"ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2","slippage":{"twap":{"slippage_percentage":"20","window_seconds":10}},"receiver":"cosmos1tfu4j7nzfhtex2wyp946rm02748zxu8w0kvt87","on_failed_delivery":"do_nothing"}}}}'
TODO: Add an example in TS
Similarly, if you wanted to swap for a token that is native to osmosis (osmo, ion, or tokenfactory tokens) you can use a memo like the one above but with the appropriate values for the token and receiver.
{
"wasm": {
"contract": "osmo1uwk8xc6q0s6t5qcpr6rht3sczu6du83xq8pwxjua0hfj5hzcnh3sqxwvxs",
"msg": {
"osmosis_swap": {
"output_denom": "uosmo", // Osmosis native token
"slippage": {
"twap": {
"slippage_percentage": "20",
"window_seconds": 10
}
},
"receiver": "juno1tfu4j7nzfhtex2wyp946rm02748zxu8wey0sqz", // The receiver is now on juno
"on_failed_delivery": "do_nothing"
}
}
}
}
junod --node https://rpc.cosmos.directory:443/juno tx ibc-transfer transfer transfer channel-0 osmo1uwk8xc6q0s6t5qcpr6rht3sczu6du83xq8pwxjua0hfj5hzcnh3sqxwvxs 100738ujuno \
--from testaccount -y --gas auto --gas-prices 0.1ujuno --gas-adjustment 1.3 \
--memo '{"wasm":{"contract":"osmo1uwk8xc6q0s6t5qcpr6rht3sczu6du83xq8pwxjua0hfj5hzcnh3sqxwvxs","msg":{"osmosis_swap":{"output_denom":"uosmo","slippage":{"twap":{"slippage_percentage":"20","window_seconds":10}},"receiver":"juno1tfu4j7nzfhtex2wyp946rm02748zxu8wey0sqz","on_failed_delivery":"do_nothing"}}}}'
TODO: Add an example in TS
Sometimes, the token you want to receive is neither native to the chain initiating the swap, nor to osmosis.
In this case we will need to unwind the path of this token using the packet forward middleware (which needs to be supported by the intermediate chain).
For this example, we will swap OSMO stored in juno to ATOM and send it back to juno.
If we did this like the previous examples we would end up with a token that isn't recognized as ATOM on juno. This is because the pools on osmosis use ATOM sent directly from gaia and not via juno (i.e.: osmosis(ATOM) != osmosis(juno(ATOM))).
To fix this, we will use the packet forward middleware to unwind the path of the token. The final memo would be:
{
"wasm": {
"contract": "osmo1uwk8xc6q0s6t5qcpr6rht3sczu6du83xq8pwxjua0hfj5hzcnh3sqxwvxs",
"msg": {
"osmosis_swap": {
"output_denom": "ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2", // ATOM on osmosis
"receiver": "cosmos1tfu4j7nzfhtex2wyp946rm02748zxu8w0kvt87", // Intermediate receiver
"slippage": {
"twap": {
"slippage_percentage": "20",
"window_seconds": 10
}
},
"next_memo": {
"forward": {
"receiver": "juno1tfu4j7nzfhtex2wyp946rm02748zxu8wey0sqz", // Final receiver
"port": "transfer",
"channel": "channel-207" // Juno's channel on gaia
}
},
"on_failed_delivery": "do_nothing"
}
}
}
}
Note that in this example we are using OSMO stored in juno, so the token we send is
ibc/ED07A3391A112B175915CD8FAF43A2DA8E4790EDE12566649D0C2F97716B8518
and not ujuno
like in previous examples.
junod --node https://rpc.cosmos.directory:443/juno tx ibc-transfer transfer transfer channel-0 osmo1uwk8xc6q0s6t5qcpr6rht3sczu6du83xq8pwxjua0hfj5hzcnh3sqxwvxs 100ibc/ED07A3391A112B175915CD8FAF43A2DA8E4790EDE12566649D0C2F97716B8518 \
--from testaccount -y --gas auto --gas-prices 0.1ujuno --gas-adjustment 1.3 \
--memo '{"wasm":{"contract":"osmo1uwk8xc6q0s6t5qcpr6rht3sczu6du83xq8pwxjua0hfj5hzcnh3sqxwvxs","msg":{"osmosis_swap":{"output_denom":"ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2","receiver":"cosmos1tfu4j7nzfhtex2wyp946rm02748zxu8w0kvt87","slippage":{"twap":{"slippage_percentage":"20","window_seconds":10}},"next_memo":{"forward":{"receiver":"juno1tfu4j7nzfhtex2wyp946rm02748zxu8wey0sqz","port":"transfer","channel":"channel-207"}},"on_failed_delivery":"do_nothing"}}}}'
Similarly to the above example, if the token we want to send send for swapping is not the native to the sender or to osmosis, then we need to do the denom path unwinding as part of the IBC packet sent.
For this example, we will use ATOM stored on juno, swap it for JUNO and send it to gaia.
{
"forward": {
"receiver": "osmo1uwk8xc6q0s6t5qcpr6rht3sczu6du83xq8pwxjua0hfj5hzcnh3sqxwvxs", // XCS contract
"port": "transfer",
"channel": "channel-141", // Osmosis channel on gaia
"next": {
"wasm": {
"contract": "osmo1uwk8xc6q0s6t5qcpr6rht3sczu6du83xq8pwxjua0hfj5hzcnh3sqxwvxs", // XCS contract
"msg": {
"osmosis_swap": {
"output_denom": "ibc/46B44899322F3CD854D2D46DEEF881958467CDD4B3B10086DA49296BBED94BED", // Juno denom on osmosis
"receiver": "juno1tfu4j7nzfhtex2wyp946rm02748zxu8wey0sqz", // temp receiver
"slippage": {
"twap": {
"slippage_percentage": "20",
"window_seconds": 10
}
},
"next_memo": {
"forward": {
"receiver": "cosmos1tfu4j7nzfhtex2wyp946rm02748zxu8w0kvt87", // final receiver
"port": "transfer",
"channel": "channel-1" // gaia's channel on juno
}
},
"on_failed_delivery": "do_nothing"
}
}
}
}
}
}
The full command is:
junod --node https://rpc.cosmos.directory:443/juno tx ibc-transfer transfer transfer channel-1 cosmos1tfu4j7nzfhtex2wyp946rm02748zxu8w0kvt87 1ibc/C4CFF46FD6DE35CA4CF4CE031E643C8FDC9BA4B99AE598E9B0ED98FE3A2319F9 \
--from testaccount -y --gas auto --gas-prices 0.1ujuno --gas-adjustment 1.3 \
--memo '{"forward":{"receiver":"osmo1uwk8xc6q0s6t5qcpr6rht3sczu6du83xq8pwxjua0hfj5hzcnh3sqxwvxs","port":"transfer","channel":"channel-141","next":{"wasm":{"contract":"osmo1uwk8xc6q0s6t5qcpr6rht3sczu6du83xq8pwxjua0hfj5hzcnh3sqxwvxs","msg":{"osmosis_swap":{"output_denom":"ibc/46B44899322F3CD854D2D46DEEF881958467CDD4B3B10086DA49296BBED94BED","receiver":"juno1tfu4j7nzfhtex2wyp946rm02748zxu8wey0sqz","slippage":{"twap":{"slippage_percentage":"20","window_seconds":10}},"next_memo":{"forward":{"receiver":"cosmos1tfu4j7nzfhtex2wyp946rm02748zxu8w0kvt87","port":"transfer","channel":"channel-1"}},"on_failed_delivery":"do_nothing"}}}}}}'
Note that as opposed to the previous example, we send the tokens to gaia first, so the channel we use
is channel-1 (gaia) and the (temporary) receiver is a cosmos1 address. Similarly, the token we send is
ibc/C4CFF46FD6DE35CA4CF4CE031E643C8FDC9BA4B99AE598E9B0ED98FE3A2319F9
which is
ATOM on juno.
XCSv2 should take care of the complexities of unwrapping tokens and sending them to the correct chain. When that is finished, we should document it here.
TODO: Add examples when the contracts are ready and deployed
To use this contract for crosschain swaps, the following are needed:
- The chain needs a wasm execute middleware that executes a contract when receiving a wasm directive in the memo.
- The swaprouter contract should be instantiated
This guide will walk you through the process of manually testing the cross-chain swapping functionality in Osmosis.
To test this contract, we will use the localrelayer tool. This tool will setup two osmosis testnets and a relayer between them.
- osmosisd command line tool installed and configured.
- jenv command line tool installed, if you don't have it you can generate the json manually or modify the commands accordingly.
- jq command line tool installed.
- localrelayer tool installed, configured and running.
Compile the contracts using the Rust workspace optimizer:
cd osmosis/cosmwasm
docker run --rm -v "$(pwd)":/code \
--mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \
--mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \
cosmwasm/workspace-optimizer-arm64:0.12.13
Create an alias for chainA and chainB commands that will be used throughout the guide:
alias chainA="osmosisd --node http://localhost:26657 --chain-id localosmosis-a"
alias chainB="osmosisd --node http://localhost:36657 --chain-id localosmosis-b"
Prepare other variables that we will use across the test:
VALIDATOR=$(osmosisd keys show validator -a --keyring-backend test)
CHANNEL_ID="channel-0"
args="--keyring-backend test --gas auto --gas-prices 0.1uosmo --gas-adjustment 2 --broadcast-mode block --yes"
TX_FLAGS=($args)
First, you need to generate the keys that will be used in the test:
echo "bottom loan skill merry east cradle onion journey palm apology verb edit desert impose absurd oil bubble sweet glove shallow size build burst effort" | osmosisd --keyring-backend test keys add validator --recover
echo "increase bread alpha rigid glide amused approve oblige print asset idea enact lawn proof unfold jeans rabbit audit return chuckle valve rather cactus great" | osmosisd --keyring-backend test keys add faucet --recover
This will generate two keys, validator and faucet and will be used to send money to the validator on both chains.
chainA tx bank send faucet "$VALIDATOR" 3000000000uosmo "${TX_FLAGS[@]}"
chainB tx bank send faucet "$VALIDATOR" 3000000000uosmo "${TX_FLAGS[@]}"
We will use the validator account as our main account for this test. This sends 1000000000uosmo from the faucet that account.
Chain B will be the chain that represents osmosis, so it needs to have IBC'd tokens from chain A so that we can setup a pool in it
chainA tx ibc-transfer transfer transfer $CHANNEL_ID "$VALIDATOR" 600000000uosmo --from validator "${TX_FLAGS[@]}"
Get the denomination of the token:
export DENOM=$(chainB q bank balances "$VALIDATOR" -o json | jq -r '.balances[] | select(.denom | contains("ibc")) | .denom')
Here, we export DENOM so that it later can be used with jenv
.
To create the pool we will need a pool definition file:
cat > sample_pool.json <<EOF
{
"weights": "1${DENOM},1uosmo",
"initial-deposit": "1000000${DENOM},1000000uosmo",
"swap-fee": "0.01",
"exit-fee": "0.01",
"future-governor": "168h"
}
EOF
Create the pool:
chainB tx gamm create-pool --pool-file sample_pool.json --from validator "${TX_FLAGS[@]}"
export the pool id:
export POOL_ID=$(chainB query gamm pools -o json | jq -r '.pools[-1].id')
Store the contract:
chainB tx wasm store ./artifacts/swaprouter.wasm --from validator "${TX_FLAGS[@]}"
Get the code id:
SWAPROUTER_CODE_ID=$(chainB query wasm list-code -o json | jq -r '.code_infos[-1].code_id')
Instantiate the contract:
MSG=$(jenv -c '{"owner": $VALIDATOR}')
chainB tx wasm instantiate "$SWAPROUTER_CODE_ID" "$MSG" --from validator --admin $VALIDATOR --label swaprouter --yes -b block --keyring-backend test "${TX_FLAGS[@]}"
Export the swaprouter contract address:
export SWAPROUTER_ADDRESS=$(chainB query wasm list-contract-by-code "$SWAPROUTER_CODE_ID" -o json | jq -r '.contracts[-1]')
MSG=$(jenv -c '{"set_route":{"input_denom":$DENOM,"output_denom":"uosmo","pool_route":[{"pool_id":$POOL_ID,"token_out_denom":"uosmo"}]}}')
chainB tx wasm execute "$SWAPROUTER_ADDRESS" "$MSG" --from validator -y --keyring-backend test "${TX_FLAGS[@]}"
Store the contract:
chainB tx wasm store ./artifacts/crosschain_swaps.wasm --from validator "${TX_FLAGS[@]}"
Get the code id:
CROSSCHAINSWAPS_CODE_ID=$(chainB query wasm list-code -o json | jq -r '.code_infos[-1].code_id')
Instantiate the contract:
MSG=$(jenv -c '{"swap_contract": $SWAPROUTER_ADDRESS, "channels": [["osmo", $CHANNEL_ID]], "governor": $VALIDATOR}')
chainB tx wasm instantiate "$CROSSCHAINSWAPS_CODE_ID" "$MSG" --from validator --admin $VALIDATOR --label=crosschain_swaps --yes -b block --keyring-backend test "${TX_FLAGS[@]}"
The channels here describe the allowed channels that the cross chain swap contract will accept for the receiver.
They are represented as a list of pairs of the source chain bech32 address prefix and the channel id.
In this case, if the receiver is "osmo", then the tokens will be sent through the channel with id $CHANNEL_ID. No other bech32 prefixes are allowed in this example.
export CROSSCHAIN_SWAPS_ADDRESS=$(chainB query wasm list-contract-by-code "$CROSSCHAINSWAPS_CODE_ID" -o json | jq -r '.contracts | [last][0]')
Now we're ready to perform the swap. To be able to verify that the swap was successful, we need to check the balance of the receiver before executing this swap:
chainA query bank balances "$VALIDATOR"
Note this so that we can later check how it has changed.
Now the exciting bit! We will perform a crosschain swap from chainA/osmo to chainB/uosmo.
This will send 100 uosmo from chainA to chainB, swap it for 100 uosmo on chainB, and then send it via IBC back to chain A.
The command to do this is:
MEMO=$(jenv -c '{"wasm": {"contract": $CROSSCHAIN_SWAPS_ADDRESS, "msg": {"osmosis_swap":{"output_denom":"uosmo","slippage":{"twap": {"slippage_percentage":"20", "window_seconds": 10}},"receiver":$VALIDATOR, "on_failed_delivery": "do_nothing"}}}}')
chainA tx ibc-transfer transfer transfer $CHANNEL_ID $CROSSCHAIN_SWAPS_ADDRESS 100uosmo \
--from validator -y "${TX_FLAGS[@]}" \
--memo "$MEMO"
The memo here is a JSON object that describes what osmosis should execute when it receives the transaction. The "contract" field is the address of a smart contract to be executed and the "msg" field is the message to be passed to that contract.
The msg describes the specific details of the cross-chain swap.
The "osmosis_swap" contains the following required fields: output_denom, slippage, receiver, and on_failed_delivery fields. All denominations inside the memo are expressed in the denoms on the chain doing the swap (chainB).
- The output_denom field specifies the denomination of the coin being received in the swap, which is "uosmo".
- The slippage field is used to specify the acceptable price slippage for the swap, here we use a 20% of the time-weighted-average price with a 10-second window.
- The receiver field specifies the address of the recipient of the swap.
- The on_failed_delivery field specifies what should happen in case the swap cannot be executed, which is set to "do_nothing"
After executing this transaction, the relayer will send it to chain B, which will process it, and generate a new IBC package to be sent back to chain A with the resulting (swapped) tokens. This will be relayed to chain A and an acknowledgement sent back to chain B to finalize the second IBC transaction.
sequenceDiagram
autonumber
actor Alice
actor Alice
Alice->>ChainA: Send Transfer M1
Note over ChainA,Relayer: Block committed.
Relayer-->>ChainB: Relay M1
critical Execute Contract
ChainB->>ChainB: Swap tokens
ChainB->>ChainB: Send IBC tx M2
end
Note over ChainB,Relayer: Block committed.
Relayer-->>ChainA: Ack M1
Relayer-->>ChainA: Relay M2
ChainA->>Alice: Send Swapped Tokens
Note over ChainA,Relayer: Block committed.
Relayer-->>ChainB: Ack M2
where M1 is the message sent above, and M2 is the transfer of the swapped tokens to the receiver (in this case, the receiver is the same as the sender)
To verify the swap, check the balance of the validator on chain A:
chainA query bank balances "$VALIDATOR"
It should contain IBC'd tokens with the appropriate denom.
TODO: Add instructions for testing on testnets.
Right now the testnets are not configured to allow crosschain swaps. We can update this when those are available.
The following is the procedure to test the contracts on a testnet.
You will need:
- An osmosis testnet
- Another testnet that supports IBC memo with an open channel to the osmosis testnet
- Tokens on the both osmosis testnets