Introduction

Overview

THORChain is a decentralised cross-chain liquidity protocol that allows users to add liquidity or swap over that liquidity. It does not peg or wrap assets. Swaps are processed as easily as making a single on-chain transaction.

THORChain works by observing transactions to its vaults across all the chains it supports. When the majority of nodes observe funds flowing into the system, they agree on the user's intent (usually expressed through a memo within a transaction) and take the appropriate action.

Info

For more information see Understanding THORChain Technology or Concepts.

For wallets/interfaces to interact with THORChain, they need to:

  1. Connect to THORChain to obtain information from one or more endpoints.
  2. Construct transactions with the correct memos.
  3. Send the transactions to THORChain Inbound Vaults.

Info

Front-end guides have been developed for fast and simple implementation.

Front-end Development Guides

Native Swaps Guide

Frontend developers can use THORChain to access decentralised layer1 swaps between BTC, ETH, BNB, ATOM and more.

Native Savings Guide

THORChain offers a Savings product, which earns yield from Swap fees. Deposit Layer1 Assets to earn in-kind yield. No lockups, penalties, impermanent loss, minimums, maximums or KYC.

Aggregators

Aggregators can deploy contracts that use custom swapIn and swapOut cross-chain aggregation to perform swaps before and after THORChain.

Eg, swap from an asset on Sushiswap, then THORChain, then an asset on TraderJoe in one transaction.

Concepts

In-depth guides to understand THORChain's implementation have been created.

Libraries

Several libraries exist to allow for rapid integration. xchainjs has seen the most development is recommended.

Eg, swap from layer 1 ETH to BTC and back.

Analytics

Analysts can build on Midgard or Flipside to access cross-chain metrics and analytics. See Connecting to THORChain for more information.

Connecting to THORChain

THORChain has several APIs with Swagger documentation.

See Connecting to THORChain for more information.

Support and Questions

Join the THORChain Dev Discord for any questions or assistance.

Quickstart Guide

Introduction

THORChain allows native L1 Swaps. On-chain Memos are used instruct THORChain how to swap, with the option to add price limits and affiliate fees. THORChain nodes observe the inbound transactions and when the majority have observed the transactions, the transaction is processed by threshold-signature transactions from THORChain vaults.

Let's demonstrate decentralized, non-custodial cross-chain swaps. In this example, we will build a transaction that instructs THORChain to swap native Bitcoin to native Ethereum in one transaction.

Info

The following examples use a free, hosted API provided by Nine Realms. If you want to run your own full node, please see connecting-to-thorchain.md.

1. Determine the correct asset name

THORChain uses a specific asset notation. Available assets are at: Pools Endpoint.

BTC => BTC.BTC
ETH => ETH.ETH

Info

Only available pools can be used. (where 'status' == Available)

2. Query for a swap quote

Info

All amounts are 1e8. Multiply native asset amounts by 100000000 when dealing with amounts in THORChain. 1 BTC = 100,000,000.

Request: Swap 1 BTC to ETH and send the ETH to 0x3021c479f7f8c9f1d5c7d8523ba5e22c0bcb5430.

https://thornode.ninerealms.com/thorchain/quote/swap?from_asset=BTC.BTC&to_asset=ETH.ETH&amount=100000000&destination=0x86d526d6624AbC0178cF7296cD538Ecc080A95F1

Response:

{
  "dust_threshold": "10000",
  "expected_amount_out": "1619355520",
  "expiry": 1689143119,
  "fees": {
    "affiliate": "0",
    "asset": "ETH.ETH",
    "outbound": "240000"
  },
  "inbound_address": "bc1qpzs9rm82m08u48842ka59hyxu36wsgzqlt6e3t",
  "inbound_confirmation_blocks": 1,
  "inbound_confirmation_seconds": 600,
  "max_streaming_quantity": 0,
  "memo": "=:ETH.ETH:0x86d526d6624AbC0178cF7296cD538Ecc080A95F1",
  "notes": "First output should be to inbound_address, second output should be change back to self, third output should be OP_RETURN, limited to 80 bytes. Do not send below the dust threshold. Do not use exotic spend scripts, locks or address formats (P2WSH with Bech32 address format preferred).",
  "outbound_delay_blocks": 305,
  "outbound_delay_seconds": 1830,
  "recommended_min_amount_in": "60000",
  "slippage_bps": 49,
  "streaming_swap_blocks": 0,
  "total_swap_seconds": 2430,
  "warning": "Do not cache this response. Do not send funds after the expiry."
}

If you send 1 BTC to bc1qlccxv985m20qvd8g5yp6g9lc0wlc70v6zlalz8 with the memo =:ETH.ETH:0x3021c479f7f8c9f1d5c7d8523ba5e22c0bcb5430, you can expect to receive 13.4493552 ETH.

For security reasons, your inbound transaction will be delayed by 600 seconds (1 BTC Block) and 2040 seconds (or 136 native THORChain blocks) for the outbound transaction, 2640 seconds all up*. You will pay an outbound gas fee of 0.0048 ETH and will incur 41 basis points (0.41%) of slippage.*

Info

Full quote swap endpoint specification can be found here: https://thornode.ninerealms.com/thorchain/doc/.

See an example implementation here.

If you'd prefer to calculate the swap yourself, see the Fees section to understand what fees need to be accounted for in the output amount. Also, review the Transaction Memos section to understand how to create the swap memos.

3. Sign and send transactions on the from_asset chain

Construct, sign and broadcast a transaction on the BTC network with the following parameters:

Amount => 1.0

Recipient => bc1qlccxv985m20qvd8g5yp6g9lc0wlc70v6zlalz8

Memo => =:ETH.ETH:0x3021c479f7f8c9f1d5c7d8523ba5e22c0bcb5430

Warning

Never cache inbound addresses! Quotes should only be considered valid for 10 minutes. Sending funds to an old inbound address will result in loss of funds.

Info

Learn more about how to construct inbound transactions for each chain type here: Sending Transactions

4. Receive tokens

Once a majority of nodes have observed your inbound BTC transaction, they will sign the Ethereum funds out of the network and send them to the address specified in your transaction. You have just completed a non-custodial, cross-chain swap by simply sending a native L1 transaction.

Additional Considerations

Warning

There is a rate limit of 1 request per second per IP address on /quote endpoints. It is advised to put a timeout on frontend components input fields, so that a request for quote only fires at most once per second. If not implemented correctly, you will receive 503 errors.

Success

For best results, request a new quote right before the user submits a transaction. This will tell you whether the expected_amount_out has changed or if the inbound_address has changed. Ensuring that the expected_amount_out is still valid will lead to better user experience and less frequent failed transactions.

Price Limits

Specify tolerance_bps to give users control over the maximum slip they are willing to experience before canceling the trade. If not specified, users will pay an unbounded amount of slip.

https://thornode.ninerealms.com/thorchain/quote/swap?amount=100000000&from_asset=BTC.BTC&to_asset=ETH.ETH&destination=0x3021c479f7f8c9f1d5c7d8523ba5e22c0bcb5430&tolerance_bps=100

https://thornode.ninerealms.com/thorchain/quote/swap?amount=100000000&from_asset=BTC.BTC&to_asset=ETH.ETH&destination=0x3021c479f7f8c9f1d5c7d8523ba5e22c0bcb5430&tolerance_bps=100

Notice how a minimum amount (1342846539 / ~13.42 ETH) has been appended to the end of the memo. This tells THORChain to revert the transaction if the transacted amount is more than 100 basis points less than what the expected_amount_out returns.

Affiliate Fees

Specify affiliate and affiliate_bps to skim a percentage of the swap as an affiliate fee. When a valid affiliate address and affiliate basis points are present in the memo, the protocol will skim affiliate_bps from the inbound swap amount and swap this to $RUNE with the affiliate address as the destination address.

Params:

  • affiliate: Can be a THORName or valid THORChain address
  • affiliate_bps: 0-1000 basis points

Memo format: =:BTC.BTC:<destination_addr>:<limit>:<affiliate>:<affiliate_bps>

Quote example:

https://thornode.ninerealms.com/thorchain/quote/swap?amount=100000000&from_asset=BTC.BTC&to_asset=ETH.ETH&destination=0x3021c479f7f8c9f1d5c7d8523ba5e22c0bcb5430&affiliate=thorname&affiliate_bps=10

{
  "dust_threshold": "10000",
  "expected_amount_out": "1603383828",
  "expiry": 1688973775,
  "fees": {
    "affiliate": "1605229",
    "asset": "ETH.ETH",
    "outbound": "240000"
  },
  "inbound_address": "bc1qhkutxeluztncm5pq0ckpm75hztrv7m7nhhh94d",
  "inbound_confirmation_blocks": 1,
  "inbound_confirmation_seconds": 600,
  "max_streaming_quantity": 0,
  "memo": "=:ETH.ETH:0x3021c479f7f8c9f1d5c7d8523ba5e22c0bcb5430::thorname:10",
  "notes": "First output should be to inbound_address, second output should be change back to self, third output should be OP_RETURN, limited to 80 bytes. Do not send below the dust threshold. Do not use exotic spend scripts, locks or address formats (P2WSH with Bech32 address format preferred).",
  "outbound_delay_blocks": 303,
  "outbound_delay_seconds": 1818,
  "recommended_min_amount_in": "72000",
  "slippage_bps": 49,
  "streaming_swap_blocks": 0,
  "total_swap_seconds": 2418,
  "warning": "Do not cache this response. Do not send funds after the expiry."
}

Notice how thorname:10 has been appended to the end of the memo. This instructs THORChain to skim 10 basis points from the swap. The user should still expect to receive the expected_amount_out, meaning the affiliate fee has already been subtracted from this number.

For more information on affiliate fees: fees.md.

Streaming Swaps

Streaming Swaps can be used to break up the trade to reduce slip fees.

Params:

  • streaming_interval: # of THORChain blocks between each subswap. Larger # of blocks gives arb bots more time to rebalance pools. For deeper/more active pools a value of 1 is most likely okay. For shallower/less active pools a larger value should be considered.
  • streaming_quantity: # of subswaps to execute. If this value is omitted or set to 0 the protocol will calculate the # of subswaps such that each subswap has a slippage of 5 bps.

Memo format: =:BTC.BTC:<destination_addr>:<limit>/<streaming_interval>/<streaming_quantity>

Quote example:

https://stagenet-thornode.ninerealms.com/thorchain/quote/swap?amount=100000000&from_asset=BTC.BTC&to_asset=ETH.ETH&destination=0x3021c479f7f8c9f1d5c7d8523ba5e22c0bcb5430&streaming_interval=10

{
  "approx_streaming_savings": 0.99930555,
  "dust_threshold": "10000",
  "expected_amount_out": "145448080",
  "expiry": 1689117597,
  "fees": {
    "affiliate": "0",
    "asset": "ETH.ETH",
    "outbound": "480000"
  },
  "inbound_address": "bc1qk2z8luw2afwuugndynegn72dkv45av5hyjrtm8",
  "inbound_confirmation_blocks": 1,
  "inbound_confirmation_seconds": 600,
  "max_streaming_quantity": 1440,
  "memo": "=:ETH.ETH:0x3021c479f7f8c9f1d5c7d8523ba5e22c0bcb5430:0/10/1440",
  "notes": "First output should be to inbound_address, second output should be change back to self, third output should be OP_RETURN, limited to 80 bytes. Do not send below the dust threshold. Do not use exotic spend scripts, locks or address formats (P2WSH with Bech32 address format preferred).",
  "outbound_delay_blocks": 76,
  "outbound_delay_seconds": 456,
  "recommended_min_amount_in": "158404",
  "slippage_bps": 8176,
  "streaming_swap_blocks": 14400,
  "streaming_swap_seconds": 86400,
  "total_swap_seconds": 87456,
  "warning": "Do not cache this response. Do not send funds after the expiry."
}

Notice how approx_streaming_savings shows the savings by using streaming swaps. total_swap_seconds also shows the amount of time the swap will take.

Custom Refund Address

By default, in the case of a refund the protocol will return the inbound swap to the original sender. However, in the case of protocol <> protocol interactions, many times the original sender is a smart contract, and not the user's EOA. In these cases, a custom refund address can be defined in the memo, which will ensure the user will receive the refund and not the smart contract.

Params:

  • refund_address: User's refund address. Needs to be a valid address for the inbound asset, otherwise refunds will be returned to the sender

Memo format: =:BTC.BTC:<destination>/<refund_address>

Quote example: https://thornode.ninerealms.com/thorchain/quote/swap?amount=100000000&from_asset=ETH.ETH&to_asset=BTC.BTC&destination=bc1qyl7wjm2ldfezgnjk2c78adqlk7dvtm8sd7gn0q&refund_address=0x3021c479f7f8c9f1d5c7d8523ba5e22c0bcb5430

{
  ...
  "memo": "=:BTC.BTC:bc1qyl7wjm2ldfezgnjk2c78adqlk7dvtm8sd7gn0q/0x3021c479f7f8c9f1d5c7d8523ba5e22c0bcb5430",
  ...
}

Error Handling

The quote swap endpoint simulates all of the logic of an actual swap transaction. It ships with comprehensive error handling.

![Price Tolerance Error](../.gitbook/assets/image (6).png) This error means the swap cannot be completed given your price tolerance.

![Destination Address Error](../.gitbook/assets/image (1).png) This error ensures the destination address is for the chain specified by to_asset.

![Affiliate Address Length Error](../.gitbook/assets/image (4).png) This error is due to the fact the affiliate address is too long given the source chain's memo length requirements. Try registering a THORName to shorten the memo.

![Asset Not Found Error](../.gitbook/assets/image (2).png) This error means the requested asset does not exist.

![Bound Checks Error](../.gitbook/assets/image (3).png) Bound checks are made on both affiliate_bps and tolerance_bps.

Support

Developers experiencing issues with these APIs can go to the Developer Discord for assistance. Interface developers should subscribe to the #interface-alerts channel for information pertinent to the endpoints and functionality discussed here.

Quickstart Guide

Introduction

THORChain allows native L1 Swaps. On-chain Memos are used instruct THORChain how to swap, with the option to add price limits and affiliate fees. THORChain nodes observe the inbound transactions and when the majority have observed the transactions, the transaction is processed by threshold-signature transactions from THORChain vaults.

Let's demonstrate decentralized, non-custodial cross-chain swaps. In this example, we will build a transaction that instructs THORChain to swap native Bitcoin to native Ethereum in one transaction.

Info

The following examples use a free, hosted API provided by Nine Realms. If you want to run your own full node, please see connecting-to-thorchain.md.

1. Determine the correct asset name

THORChain uses a specific asset notation. Available assets are at: Pools Endpoint.

BTC => BTC.BTC
ETH => ETH.ETH

Info

Only available pools can be used. (where 'status' == Available)

2. Query for a swap quote

Info

All amounts are 1e8. Multiply native asset amounts by 100000000 when dealing with amounts in THORChain. 1 BTC = 100,000,000.

Request: Swap 1 BTC to ETH and send the ETH to 0x3021c479f7f8c9f1d5c7d8523ba5e22c0bcb5430.

https://thornode.ninerealms.com/thorchain/quote/swap?from_asset=BTC.BTC&to_asset=ETH.ETH&amount=100000000&destination=0x86d526d6624AbC0178cF7296cD538Ecc080A95F1

Response:

{
  "dust_threshold": "10000",
  "expected_amount_out": "1619355520",
  "expiry": 1689143119,
  "fees": {
    "affiliate": "0",
    "asset": "ETH.ETH",
    "outbound": "240000"
  },
  "inbound_address": "bc1qpzs9rm82m08u48842ka59hyxu36wsgzqlt6e3t",
  "inbound_confirmation_blocks": 1,
  "inbound_confirmation_seconds": 600,
  "max_streaming_quantity": 0,
  "memo": "=:ETH.ETH:0x86d526d6624AbC0178cF7296cD538Ecc080A95F1",
  "notes": "First output should be to inbound_address, second output should be change back to self, third output should be OP_RETURN, limited to 80 bytes. Do not send below the dust threshold. Do not use exotic spend scripts, locks or address formats (P2WSH with Bech32 address format preferred).",
  "outbound_delay_blocks": 305,
  "outbound_delay_seconds": 1830,
  "recommended_min_amount_in": "60000",
  "slippage_bps": 49,
  "streaming_swap_blocks": 0,
  "total_swap_seconds": 2430,
  "warning": "Do not cache this response. Do not send funds after the expiry."
}

If you send 1 BTC to bc1qlccxv985m20qvd8g5yp6g9lc0wlc70v6zlalz8 with the memo =:ETH.ETH:0x3021c479f7f8c9f1d5c7d8523ba5e22c0bcb5430, you can expect to receive 13.4493552 ETH.

For security reasons, your inbound transaction will be delayed by 600 seconds (1 BTC Block) and 2040 seconds (or 136 native THORChain blocks) for the outbound transaction, 2640 seconds all up*. You will pay an outbound gas fee of 0.0048 ETH and will incur 41 basis points (0.41%) of slippage.*

Info

Full quote swap endpoint specification can be found here: https://thornode.ninerealms.com/thorchain/doc/.

See an example implementation here.

If you'd prefer to calculate the swap yourself, see the Fees section to understand what fees need to be accounted for in the output amount. Also, review the Transaction Memos section to understand how to create the swap memos.

3. Sign and send transactions on the from_asset chain

Construct, sign and broadcast a transaction on the BTC network with the following parameters:

Amount => 1.0

Recipient => bc1qlccxv985m20qvd8g5yp6g9lc0wlc70v6zlalz8

Memo => =:ETH.ETH:0x3021c479f7f8c9f1d5c7d8523ba5e22c0bcb5430

Warning

Never cache inbound addresses! Quotes should only be considered valid for 10 minutes. Sending funds to an old inbound address will result in loss of funds.

Info

Learn more about how to construct inbound transactions for each chain type here: Sending Transactions

4. Receive tokens

Once a majority of nodes have observed your inbound BTC transaction, they will sign the Ethereum funds out of the network and send them to the address specified in your transaction. You have just completed a non-custodial, cross-chain swap by simply sending a native L1 transaction.

Additional Considerations

Warning

There is a rate limit of 1 request per second per IP address on /quote endpoints. It is advised to put a timeout on frontend components input fields, so that a request for quote only fires at most once per second. If not implemented correctly, you will receive 503 errors.

Success

For best results, request a new quote right before the user submits a transaction. This will tell you whether the expected_amount_out has changed or if the inbound_address has changed. Ensuring that the expected_amount_out is still valid will lead to better user experience and less frequent failed transactions.

Price Limits

Specify tolerance_bps to give users control over the maximum slip they are willing to experience before canceling the trade. If not specified, users will pay an unbounded amount of slip.

https://thornode.ninerealms.com/thorchain/quote/swap?amount=100000000&from_asset=BTC.BTC&to_asset=ETH.ETH&destination=0x3021c479f7f8c9f1d5c7d8523ba5e22c0bcb5430&tolerance_bps=100

https://thornode.ninerealms.com/thorchain/quote/swap?amount=100000000&from_asset=BTC.BTC&to_asset=ETH.ETH&destination=0x3021c479f7f8c9f1d5c7d8523ba5e22c0bcb5430&tolerance_bps=100

Notice how a minimum amount (1342846539 / ~13.42 ETH) has been appended to the end of the memo. This tells THORChain to revert the transaction if the transacted amount is more than 100 basis points less than what the expected_amount_out returns.

Affiliate Fees

Specify affiliate and affiliate_bps to skim a percentage of the swap as an affiliate fee. When a valid affiliate address and affiliate basis points are present in the memo, the protocol will skim affiliate_bps from the inbound swap amount and swap this to $RUNE with the affiliate address as the destination address.

Params:

  • affiliate: Can be a THORName or valid THORChain address
  • affiliate_bps: 0-1000 basis points

Memo format: =:BTC.BTC:<destination_addr>:<limit>:<affiliate>:<affiliate_bps>

Quote example:

https://thornode.ninerealms.com/thorchain/quote/swap?amount=100000000&from_asset=BTC.BTC&to_asset=ETH.ETH&destination=0x3021c479f7f8c9f1d5c7d8523ba5e22c0bcb5430&affiliate=thorname&affiliate_bps=10

{
  "dust_threshold": "10000",
  "expected_amount_out": "1603383828",
  "expiry": 1688973775,
  "fees": {
    "affiliate": "1605229",
    "asset": "ETH.ETH",
    "outbound": "240000"
  },
  "inbound_address": "bc1qhkutxeluztncm5pq0ckpm75hztrv7m7nhhh94d",
  "inbound_confirmation_blocks": 1,
  "inbound_confirmation_seconds": 600,
  "max_streaming_quantity": 0,
  "memo": "=:ETH.ETH:0x3021c479f7f8c9f1d5c7d8523ba5e22c0bcb5430::thorname:10",
  "notes": "First output should be to inbound_address, second output should be change back to self, third output should be OP_RETURN, limited to 80 bytes. Do not send below the dust threshold. Do not use exotic spend scripts, locks or address formats (P2WSH with Bech32 address format preferred).",
  "outbound_delay_blocks": 303,
  "outbound_delay_seconds": 1818,
  "recommended_min_amount_in": "72000",
  "slippage_bps": 49,
  "streaming_swap_blocks": 0,
  "total_swap_seconds": 2418,
  "warning": "Do not cache this response. Do not send funds after the expiry."
}

Notice how thorname:10 has been appended to the end of the memo. This instructs THORChain to skim 10 basis points from the swap. The user should still expect to receive the expected_amount_out, meaning the affiliate fee has already been subtracted from this number.

For more information on affiliate fees: fees.md.

Streaming Swaps

Streaming Swaps can be used to break up the trade to reduce slip fees.

Params:

  • streaming_interval: # of THORChain blocks between each subswap. Larger # of blocks gives arb bots more time to rebalance pools. For deeper/more active pools a value of 1 is most likely okay. For shallower/less active pools a larger value should be considered.
  • streaming_quantity: # of subswaps to execute. If this value is omitted or set to 0 the protocol will calculate the # of subswaps such that each subswap has a slippage of 5 bps.

Memo format: =:BTC.BTC:<destination_addr>:<limit>/<streaming_interval>/<streaming_quantity>

Quote example:

https://stagenet-thornode.ninerealms.com/thorchain/quote/swap?amount=100000000&from_asset=BTC.BTC&to_asset=ETH.ETH&destination=0x3021c479f7f8c9f1d5c7d8523ba5e22c0bcb5430&streaming_interval=10

{
  "approx_streaming_savings": 0.99930555,
  "dust_threshold": "10000",
  "expected_amount_out": "145448080",
  "expiry": 1689117597,
  "fees": {
    "affiliate": "0",
    "asset": "ETH.ETH",
    "outbound": "480000"
  },
  "inbound_address": "bc1qk2z8luw2afwuugndynegn72dkv45av5hyjrtm8",
  "inbound_confirmation_blocks": 1,
  "inbound_confirmation_seconds": 600,
  "max_streaming_quantity": 1440,
  "memo": "=:ETH.ETH:0x3021c479f7f8c9f1d5c7d8523ba5e22c0bcb5430:0/10/1440",
  "notes": "First output should be to inbound_address, second output should be change back to self, third output should be OP_RETURN, limited to 80 bytes. Do not send below the dust threshold. Do not use exotic spend scripts, locks or address formats (P2WSH with Bech32 address format preferred).",
  "outbound_delay_blocks": 76,
  "outbound_delay_seconds": 456,
  "recommended_min_amount_in": "158404",
  "slippage_bps": 8176,
  "streaming_swap_blocks": 14400,
  "streaming_swap_seconds": 86400,
  "total_swap_seconds": 87456,
  "warning": "Do not cache this response. Do not send funds after the expiry."
}

Notice how approx_streaming_savings shows the savings by using streaming swaps. total_swap_seconds also shows the amount of time the swap will take.

Custom Refund Address

By default, in the case of a refund the protocol will return the inbound swap to the original sender. However, in the case of protocol <> protocol interactions, many times the original sender is a smart contract, and not the user's EOA. In these cases, a custom refund address can be defined in the memo, which will ensure the user will receive the refund and not the smart contract.

Params:

  • refund_address: User's refund address. Needs to be a valid address for the inbound asset, otherwise refunds will be returned to the sender

Memo format: =:BTC.BTC:<destination>/<refund_address>

Quote example: https://thornode.ninerealms.com/thorchain/quote/swap?amount=100000000&from_asset=ETH.ETH&to_asset=BTC.BTC&destination=bc1qyl7wjm2ldfezgnjk2c78adqlk7dvtm8sd7gn0q&refund_address=0x3021c479f7f8c9f1d5c7d8523ba5e22c0bcb5430

{
  ...
  "memo": "=:BTC.BTC:bc1qyl7wjm2ldfezgnjk2c78adqlk7dvtm8sd7gn0q/0x3021c479f7f8c9f1d5c7d8523ba5e22c0bcb5430",
  ...
}

Error Handling

The quote swap endpoint simulates all of the logic of an actual swap transaction. It ships with comprehensive error handling.

![Price Tolerance Error](../.gitbook/assets/image (6).png) This error means the swap cannot be completed given your price tolerance.

![Destination Address Error](../.gitbook/assets/image (1).png) This error ensures the destination address is for the chain specified by to_asset.

![Affiliate Address Length Error](../.gitbook/assets/image (4).png) This error is due to the fact the affiliate address is too long given the source chain's memo length requirements. Try registering a THORName to shorten the memo.

![Asset Not Found Error](../.gitbook/assets/image (2).png) This error means the requested asset does not exist.

![Bound Checks Error](../.gitbook/assets/image (3).png) Bound checks are made on both affiliate_bps and tolerance_bps.

Support

Developers experiencing issues with these APIs can go to the Developer Discord for assistance. Interface developers should subscribe to the #interface-alerts channel for information pertinent to the endpoints and functionality discussed here.

Fees and Wait Times

Fee Types

Users pay up to four kinds of fees when conducting a swap.

  1. Layer1 Network Fees (gas): paid by the user when sending the asset to THORChain to be swapped. This is controlled by the user's wallet.
  2. Slip Fee: protects the pool from being manipulated by large swaps. Calculated as a function of transaction size and current pool depth. The slip fee formula is explained here and an example implementation is here.
  3. Affiliate Fee - (optional) a percentage skimmed from the inbound amount that can be paid to exchanges or wallet providers. Wallets can now accept fees in any THORChain-supported asset (USDC, BTC, etc). Check the "Preferred Asset for Affiliate Fees" section in fees.md for more details and setup information.
  4. Outbound Fee - the fee the Network pays on behalf of the user to send the outbound transaction. See Outbound Fee.

Info

The Swap Quote endpoint will calculate and show all fees.

See the fees section for full details.

Refunds and Minimum Swap Amount

If a transaction fails, it is refunded, thus it will pay the outboundFee for the SourceChain not the DestinationChain. Thus devs should always swap an amount that is a maximum of the following, multiplied by at least a 4x buffer to allow for gas spikes:

  1. The Destination Chain outboundFee, or
  2. The Source Chain outboundFee, or
  3. $1.00 (the minimum outboundFee).

For convenience, a recommended_min_amount_in is included on the Swap Quote endpoint, which is the value described above. This value is priced in the inbound asset of the quote request (in 1e8). This should be the minimum-allowed swap amount for the requested quote.

Wait Times

There are four phases of a transaction sent to THORChain each taking time to complete.

  1. Layer1 Inbound Confirmation - assuming the inboundTx will be confirmed in the next block, it is the source blockchain block time.
  2. Observation Counting - time for 67% THORChain Nodes to observe and agree on the inboundTx.
  3. Confirmation Counting - for non-instant finality blockchains, the amount of time THORChain will wait before processing to protect against double spends and re-org attacks.
  4. Outbound Delay - dependent on size and network traffic. Large outbounds will be delayed.
  5. Layer1 Outbound Confirmation - Outbound blockchain block time.

Wait times can be between a few seconds up to an hour. The assets being swapped, the size of the swap and the current network traffic within THORChain will determine the wait time.

Info

The Swap Quote endpoint will calculate points 3 and 4.

See the delays.md section for full details.

Streaming Swaps

Streaming Swaps is a means for a swapper to get better price execution if they are patient. This ensures Capital Efficiency while still keeping with the philosophy "impatient people pay more".

There are two important parts to streaming swaps:

  1. The interval part of the stream allows arbs enough time to rebalance intra-swap - this means the capital demands of swaps are met throughout, instead of after.
  2. The quantity part of the stream allows the swapper to reduce the size of their sub-swap so each is executed with less slip (so the total swap will be executed with less slip) without losing capital to on-chain L1 fees.

If a swapper is willing to be patient, they can execute the swap with a better price, by allowing arbs to rebalance the pool between the streaming swaps.

Once all swaps are executed and the streaming swap is completed, the target token is sent to the user (minus outbound fees).

Streaming Swaps is similar to a Time Weighted Average Price (TWAP) trade however it is restricted to 24 hours (Mimir STREAMINGSWAPMAXLENGTH = 14400 blocks).

Using Streaming Swaps

To utilise a streaming swap, use the following within a Memo:

Trade Target or Limit / Swap Interval / Swap Quantity.

  • Limit or Trade Target: Uses the trade limit to set the maximum asset ratio at which a mini-swap can occur; otherwise, a refund is issued.
  • Interval: Block separation of each swap. For example, a value of 10 means a mini-swap is performed every 10 blocks.
  • Quantity: The number of swaps to be conducted. If set to 0, the network will determine the appropriate quantity.

Using the values Limit/10/5 would conduct five mini-swaps with a block separation of 10. Only swaps that achieve the specified asset ratio (defined by Limit) will be performed, while others will result in a refund.

On each swap attempt, the network will track how much (in funds) failed to swap and how much was successful. After all swap attempts are made (specified by "swap quantity"), the network will send out all successfully swapped value, and the remaining source asset via refund (that failed to swap for some reason, most likely due to the trade target).

If the first swap attempt fails for some reason, the entire streaming swap is refunded and no further attempts will be made. If the swap quantity is set to zero, the network will determine the number of swaps on its own with a focus on the lowest fees and maximize the number of trades.

Minimum Swap Size

A min swap size is placed on the network for streaming swaps (Mimir StreamingSwapMinBPFee = 5 Basis Points). This is the minimum slip for each individual swap within a streaming swap allowed. This also puts a cap on the number of swaps in a streaming swap. This allows the network to be more friendly to large trades, while also keeping revenues up for small or medium-sized trades.

Calculate Optimal Swap

The network works out the optimal streaming swap solution based on the Mimumn Swap Size and the swapAmount.

Single Swap: To calculate the minimum swap size for a single swap, you take 2.5 basis points (bps) of the depth of the pool. The formula is as follows:

Example using BTC Pool:

  • BTC Rune Depth = 20,007,476 RUNE
  • StreamingSwapMinBPFee = 5 bp

MinimumSwapSize = 0.0005 * 20,007,476 = 10,003. RUNE

Double Swap: When dealing with two pools of arbitrary depths and aiming for a precise 5 bps swap fee (set by StreamingSwapMinBPFee), you need to create a virtual pool size called runeDepth using the following formula:

r1 represents the rune depth of pool1, and r2 represents the rune depth of pool2.

The runeDepth is then used with 1.25 bps (half of 2.5 bps since there are two swaps), which gives you the minimum swap size that results in a 5 bps swap fee.

Success

The larger the difference between the pools, the more the virtual pool skews towards the smaller pool. This results in less rewards given to the larger pool, and more rewards given to the smaller pool.

Example using BTC and ETH Pool

  • BTC Rune Depth = 20,007,476 RUNE
  • ETH Rune Depth = 8,870,648 RUNE
  • StreamingSwapMinBPFee = 5 bp

virtualRuneDepth = (2*20,007,476*8,870,648) / (20,007,476 + 8,870,648) = 12,291,607 RUNE

MinimumSwapSize = (0.0005/4) * 12,291,607 = 1536.45 RUNE

Swap Count

The number of swaps required is determined by dividing the swap Amount by the minimum swap size calculated in the previous step.

The swapAmount represents the total amount to be swapped.

Example: swap 20,000 RUNE worth of BTC to ETH. (approx 0.653 BTC).

20,000 / 3,072.90 = 6.5 = 7 Swaps.

Comparing Price Execution

The difference between streaming swaps and non-streaming swaps can be calculated using the swap count with the following formula:

The differencevalue represents the percentage of the swap fee saved compared to doing the same swap with a regular fee structure. There higher the swapCount, the bigger the difference.

Example:

  • (7-1)/7 = 6/7 = 85% better price execution by being patient.

Quick Start Guide

Lending allows users to deposit native collateral, and then create a debt at a collateralization ratio CR (collateralization ratio). The debt is always denominated in USD (aka TOR) regardless of what L1 asset the user receives.

Note

Streaming swaps is enabled for lending.

Open a Loan Quote

Lending Quote endpoints have been created to simplify the implementation process.

Request: Loan quote using 1 BTC as collateral, target debt asset is USDT at 0XDAC17F958D2EE523A2206206994597C13D831EC7

https://thornode.ninerealms.com/thorchain/quote/loan/open?from_asset=BTC.BTC&amount=10000000&to_asset=ETH.USDT-0xdac17f958d2ee523a2206206994597c13d831ec7&destination=0xe7062003a7be4df3a86127293a0d6b1f54c04220

Response:

{
  "dust_threshold": "10000",
  "expected_amount_out": "112302802900",
  "expected_collateral_deposited": "9997829",
  "expected_collateralization_ratio": "31467",
  "expected_debt_issued": "112887730000",
  "expiry": 1698901398,
  "fees": {
    "asset": "ETH.USDT-0XDAC17F958D2EE523A2206206994597C13D831EC7",
    "liquidity": "114988700",
    "outbound": "444599700",
    "slippage_bps": 10,
    "total": "559588400",
    "total_bps": 49
  },
  "inbound_address": "bc1qmed4v5am2hcg8furkeff2pczdnt0qu4flke420",
  "inbound_confirmation_blocks": 1,
  "inbound_confirmation_seconds": 600,
  "memo": "$+:ETH.USDT:0xe7062003a7be4df3a86127293a0d6b1f54c04220",
  "notes": "First output should be to inbound_address, second output should be change back to self, third output should be OP_RETURN, limited to 80 bytes. Do not send below the dust threshold. Do not use exotic spend scripts, locks or address formats (P2WSH with Bech32 address format preferred).",
  "outbound_delay_blocks": 3,
  "outbound_delay_seconds": 18,
  "recommended_min_amount_in": "156000",
  "warning": "Do not cache this response. Do not send funds after the expiry."
}

If you send 1 BTC to bc1q2hldv0pmy9mcpddj2qrvdgcx6pw6h6h7gqytwy with the memo $+:ETH.USDT:0xe7062003a7be4df3a86127293a0d6b1f54c04220 you will receive approx. 1128.8773 USDT debt sent to 0xe7062003a7be4df3a86127293a0d6b1f54c04220 with a CR of 314.6% and will incur 49 basis points (0.49%) slippage.

Danger

The Inbound_Address changes regularly, do not cache!

Warning

Loans cannot be repaid until a minimum time has passed, as determined by LOANREPAYMENTMATURITY, which is currently set as the current block height plus LOANREPAYMENTMATURITY. Currently, LOANREPAYMENTMATURITY is set to 432,000 blocks, equivalent to 30 days. Increasing the collateral on an existing loan to obtain additional debit resets the period.

Close a Loan

Request: Repay a loan using USDT where BTC.BTC was used as colloteral. Note any asset can be used to repay a loan. https://thornode.ninerealms.com/thorchain/quote/loan/close?from_asset=BTC.BTC&amount=114947930000&to_asset=BTC.BTC&loan_owner=bc1q089j003xwj07uuavt2as5r45a95k5zzrhe4ac3

Response:

{
  "dust_threshold": "10000",
  "expected_amount_out": "9985158",
  "expected_collateral_withdrawn": "9997123",
  "expected_debt_repaid": "390985054444080",
  "expiry": 1698897875,
  "fees": {
    "asset": "BTC.BTC",
    "liquidity": "38196994221",
    "outbound": "7500",
    "slippage_bps": 4347,
    "total": "38197001721",
    "total_bps": 38253777
  },
  "inbound_address": "bc1q69vcdslg0vfy4ne3nj7te5p9cvu2y4vq8t3x99",
  "inbound_confirmation_blocks": 192,
  "inbound_confirmation_seconds": 115200,
  "memo": "$-:BTC.BTC:bc1q089j003xwj07uuavt2as5r45a95k5zzrhe4ac3",
  "notes": "First output should be to inbound_address, second output should be change back to self, third output should be OP_RETURN, limited to 80 bytes. Do not send below the dust threshold. Do not use exotic spend scripts, locks or address formats (P2WSH with Bech32 address format preferred).",
  "outbound_delay_blocks": 12,
  "outbound_delay_seconds": 72,
  "recommended_min_amount_in": "30000",
  "warning": "Do not cache this response. Do not send funds after the expiry."
}

If you send 1149.47 USDT with a memo $-:BTC.BTC:bc1q089j003xwj07uuavt2as5r45a95k5zzrhe4ac3 of you will repay your loan down.

Borrowers Position

Request:
Get brower's positin in the BTC pool who tool out a loan from bc1q089j003xwj07uuavt2as5r45a95k5zzrhe4ac3
https://thornode.ninerealms.com/thorchain/pool/BTC.BTC/borrower/bc1q089j003xwj07uuavt2as5r45a95k5zzrhe4ac3\

Response:

{
  "asset": "BTC.BTC",
  "collateral_current": "9997123",
  "collateral_deposited": "9997123",
  "collateral_withdrawn": "0",
  "debt_current": "114947930000",
  "debt_issued": "114947930000",
  "debt_repaid": "0",
  "last_open_height": 12252923,
  "last_repay_height": 0,
  "owner": "bc1q089j003xwj07uuavt2as5r45a95k5zzrhe4ac3"
}

The borrower has provided 0.0997 BTC and has a current TOR debt of $1149.78. No repayments have been yet.

Support

Developers experiencing issues with these APIs can go to the Developer Discord for assistance. Interface developers should subscribe to the #interface-alerts channel for information pertinent to the endpoints and functionality discussed here.

Quick Start Guide

Lending allows users to deposit native collateral, and then create a debt at a collateralization ratio CR (collateralization ratio). The debt is always denominated in USD (aka TOR) regardless of what L1 asset the user receives.

Note

Streaming swaps is enabled for lending.

Open a Loan Quote

Lending Quote endpoints have been created to simplify the implementation process.

Request: Loan quote using 1 BTC as collateral, target debt asset is USDT at 0XDAC17F958D2EE523A2206206994597C13D831EC7

https://thornode.ninerealms.com/thorchain/quote/loan/open?from_asset=BTC.BTC&amount=10000000&to_asset=ETH.USDT-0xdac17f958d2ee523a2206206994597c13d831ec7&destination=0xe7062003a7be4df3a86127293a0d6b1f54c04220

Response:

{
  "dust_threshold": "10000",
  "expected_amount_out": "112302802900",
  "expected_collateral_deposited": "9997829",
  "expected_collateralization_ratio": "31467",
  "expected_debt_issued": "112887730000",
  "expiry": 1698901398,
  "fees": {
    "asset": "ETH.USDT-0XDAC17F958D2EE523A2206206994597C13D831EC7",
    "liquidity": "114988700",
    "outbound": "444599700",
    "slippage_bps": 10,
    "total": "559588400",
    "total_bps": 49
  },
  "inbound_address": "bc1qmed4v5am2hcg8furkeff2pczdnt0qu4flke420",
  "inbound_confirmation_blocks": 1,
  "inbound_confirmation_seconds": 600,
  "memo": "$+:ETH.USDT:0xe7062003a7be4df3a86127293a0d6b1f54c04220",
  "notes": "First output should be to inbound_address, second output should be change back to self, third output should be OP_RETURN, limited to 80 bytes. Do not send below the dust threshold. Do not use exotic spend scripts, locks or address formats (P2WSH with Bech32 address format preferred).",
  "outbound_delay_blocks": 3,
  "outbound_delay_seconds": 18,
  "recommended_min_amount_in": "156000",
  "warning": "Do not cache this response. Do not send funds after the expiry."
}

If you send 1 BTC to bc1q2hldv0pmy9mcpddj2qrvdgcx6pw6h6h7gqytwy with the memo $+:ETH.USDT:0xe7062003a7be4df3a86127293a0d6b1f54c04220 you will receive approx. 1128.8773 USDT debt sent to 0xe7062003a7be4df3a86127293a0d6b1f54c04220 with a CR of 314.6% and will incur 49 basis points (0.49%) slippage.

Danger

The Inbound_Address changes regularly, do not cache!

Warning

Loans cannot be repaid until a minimum time has passed, as determined by LOANREPAYMENTMATURITY, which is currently set as the current block height plus LOANREPAYMENTMATURITY. Currently, LOANREPAYMENTMATURITY is set to 432,000 blocks, equivalent to 30 days. Increasing the collateral on an existing loan to obtain additional debit resets the period.

Close a Loan

Request: Repay a loan using USDT where BTC.BTC was used as colloteral. Note any asset can be used to repay a loan. https://thornode.ninerealms.com/thorchain/quote/loan/close?from_asset=BTC.BTC&amount=114947930000&to_asset=BTC.BTC&loan_owner=bc1q089j003xwj07uuavt2as5r45a95k5zzrhe4ac3

Response:

{
  "dust_threshold": "10000",
  "expected_amount_out": "9985158",
  "expected_collateral_withdrawn": "9997123",
  "expected_debt_repaid": "390985054444080",
  "expiry": 1698897875,
  "fees": {
    "asset": "BTC.BTC",
    "liquidity": "38196994221",
    "outbound": "7500",
    "slippage_bps": 4347,
    "total": "38197001721",
    "total_bps": 38253777
  },
  "inbound_address": "bc1q69vcdslg0vfy4ne3nj7te5p9cvu2y4vq8t3x99",
  "inbound_confirmation_blocks": 192,
  "inbound_confirmation_seconds": 115200,
  "memo": "$-:BTC.BTC:bc1q089j003xwj07uuavt2as5r45a95k5zzrhe4ac3",
  "notes": "First output should be to inbound_address, second output should be change back to self, third output should be OP_RETURN, limited to 80 bytes. Do not send below the dust threshold. Do not use exotic spend scripts, locks or address formats (P2WSH with Bech32 address format preferred).",
  "outbound_delay_blocks": 12,
  "outbound_delay_seconds": 72,
  "recommended_min_amount_in": "30000",
  "warning": "Do not cache this response. Do not send funds after the expiry."
}

If you send 1149.47 USDT with a memo $-:BTC.BTC:bc1q089j003xwj07uuavt2as5r45a95k5zzrhe4ac3 of you will repay your loan down.

Borrowers Position

Request:
Get brower's positin in the BTC pool who tool out a loan from bc1q089j003xwj07uuavt2as5r45a95k5zzrhe4ac3
https://thornode.ninerealms.com/thorchain/pool/BTC.BTC/borrower/bc1q089j003xwj07uuavt2as5r45a95k5zzrhe4ac3\

Response:

{
  "asset": "BTC.BTC",
  "collateral_current": "9997123",
  "collateral_deposited": "9997123",
  "collateral_withdrawn": "0",
  "debt_current": "114947930000",
  "debt_issued": "114947930000",
  "debt_repaid": "0",
  "last_open_height": 12252923,
  "last_repay_height": 0,
  "owner": "bc1q089j003xwj07uuavt2as5r45a95k5zzrhe4ac3"
}

The borrower has provided 0.0997 BTC and has a current TOR debt of $1149.78. No repayments have been yet.

Support

Developers experiencing issues with these APIs can go to the Developer Discord for assistance. Interface developers should subscribe to the #interface-alerts channel for information pertinent to the endpoints and functionality discussed here.

Quickstart Guide

Introduction

THORChain allows users to deposit Layer1 assets into its network to earn asset-denominated yield without RUNE asset exposure, or being aware of THORChain’s network.

There is no permission, authentication or prior steps, so developers can get started and allow their users to earn asset-denominated yield simply by sending layer1 transactions to THORChain vaults.

Under the hood, THORChain deposits the user’s Layer1 asset into a liquidity pool which earns yield. This yield is tracked and paid to the user’s deposit value. Users can withdraw their Layer1 asset, including the yield earned. There is no slashing, penalties, timelocks, or account minimum/maximums. The only fees paid are the Layer1 fees to make a deposit and withdraw transaction (as necessitated), and a slip-based fee on entry and exit to stop price manipulation attacks. Both of these are transparent and within the user’s control.

Note

Streaming swaps is enabled for savers.

Quote for a Savers Quote

Savers Quote endpoints have been created to simplify the implementation process.

Add 1 BTC to Savers.

Request: Add 1 BTC to Savers

https://thornode.ninerealms.com/thorchain/quote/saver/deposit?asset=BTC.BTC&amount=100000000

Response:

{
  "dust_threshold": "10000",
  "expected_amount_deposit": "99932291",
  "expected_amount_out": "99932291",
  "expiry": 1700263119,
  "fees": {
    "affiliate": "0",
    "asset": "BTC/BTC",
    "liquidity": "67672",
    "outbound": "355",
    "slippage_bps": 6,
    "total": "68027",
    "total_bps": 6
  },
  "inbound_address": "bc1qe7lfmet2l5j7ypsd6ln300jt8mg3dt2q3darj8",
  "inbound_confirmation_blocks": 1,
  "inbound_confirmation_seconds": 600,
  "memo": "+:BTC/BTC",
  "notes": "First output should be to inbound_address, second output should be change back to self, third output should be OP_RETURN, limited to 80 bytes. Do not send below the dust threshold. Do not use exotic spend scripts, locks or address formats (P2WSH with Bech32 address format preferred).",
  "recommended_min_amount_in": "10000",
  "slippage_bps": 13,
  "warning": "Do not cache this response. Do not send funds after the expiry."
}

If you send 1 BTC to bc1quuf5sr444km2zlgrg654mjdfgkuzayfs7nqrfmwith the memo +:BTC/BTC, you can expect 0.99932 BTC will and will incur 13 basis points (0.13%) of slippage.

Danger

The Inbound_Address changes regularly, do not cache!

Danger

Inbound transactions should not be delayed for any reason else there is risk funds will be sent to an unreachable address. Use standard transactions, check the inbound address before sending and use the recommended gas rate to ensure transactions are confirmed in the next block to the latest Inbound_Address.

For security reasons, your inbound transaction will be delayed by 1 BTC Block.

Info

Full quote saving endpoint specification can be found here: https://thornode.ninerealms.com/thorchain/doc/.

See an example implementation here.

User withdrawing all of their BTC Saver's position.

Request: Withdraw 100% of BTC Savers for bc1qy9rjlz5w3tqn7m3reh3y48n8del4y8z42sswx5

https://thornode.ninerealms.com/thorchain/quote/saver/withdraw?asset=BTC.BTC&address=bc1qy9rjlz5w3tqn7m3reh3y48n8del4y8z42sswx5&withdraw_bps=10000

Response:

{
  "dust_amount": "20000",
  "dust_threshold": "10000",
  "expected_amount_out": "297234276",
  "expiry": 1698901306,
  "fees": {
    "affiliate": "0",
    "asset": "BTC.BTC",
    "liquidity": "150576",
    "outbound": "39000",
    "slippage_bps": 5,
    "total": "189576",
    "total_bps": 6
  },
  "inbound_address": "bc1qmed4v5am2hcg8furkeff2pczdnt0qu4flke420",
  "memo": "-:BTC/BTC:10000",
  "notes": "First output should be to inbound_address, second output should be change back to self, third output should be OP_RETURN, limited to 80 bytes. Do not send below the dust threshold. Do not use exotic spend scripts, locks or address formats (P2WSH with Bech32 address format preferred).",
  "outbound_delay_blocks": 548,
  "outbound_delay_seconds": 3288,
  "slippage_bps": 60,
  "warning": "Do not cache this response. Do not send funds after the expiry."
}

Warning

Deposit and withdraw interfaces will return inbound_address and memo fields that can be used to construct the transaction. Do not cache theinbound_address field!

Basic Mechanics

Users can add assets to a vault by sending assets directly to the chain’s vault address found on the /thorchain/inbound_addresses endpoint. Quote endpoints will also return this.

1. Find the L1 vault address

https://thornode.ninerealms.com/thorchain/inbound_addresses

Example:

curl -SL https://thornode.ninerealms.com/thorchain/inbound_addresses | jq '.[] | select(.chain == "BTC") | .address'
=> “bc1q556ljv5y4rkdt4p46usx86esljs3xqjxyntlyd”

2. Determine if there is capacity available to mint new synths

There is a cap on how many synths can be minted as a function of liquidity depth. To do this, find synth_mint_paused = false on the /pool endpoint

curl -SL https://thornode.ninerealms.com/thorchain/pools | jq '.[] | select(.asset == "BTC.BTC") | .synth_mint_paused'

3. Send memoless savers transactions

Both Saver Deposit and Withdraw transactions can be done without memos (optional memos can be included if a wallet wishes, see Transaction Memos, since there is a marginal transaction cost savings to including memos).

To deposit, users should send any amount of asset they wish (avoiding dust amounts). The network will read the deposit and user address, then add them into the Saver Vault automatically.

To withdraw, the user should send a specific dust amount of asset (avoiding the dust threshold), from an amount 0 units above the dust threshold, to an amount 10,000 units above the threshold.
10000 units is read as “withdraw 10000 basis points”, which is 100%.

Info

The dust threshold is the point at which the network will ignore the amount sent to stop dust attacks (widely seen on UTXO chains).

Specific rules for each chain and action are as follows:

  • Each chain has a defined dust_threshold in base units
  • For asset amounts in the range: [ dust_threshold + 1 : dust_threshold + 10,000], the network will withdraw dust_threshold - 10,000 basis points from the user’s Savers position
  • For asset amounts greater than dust_threshold + 10,000, the network will add to the user’s Savers position

The dust_threshold for each chain are defined as:

  • BTC: 10,000 sats
  • BCH: 10,000 sats
  • LTC: 10,000 sats
  • DOGE: 100,000,000 sats
  • ETH,AVAX: 0 wei
  • ATOM: 0 uatom
  • BNB: 0 nbnb

Info

Transactions with asset amounts equal to or below the dust_threshold for the chain will be ignored to prevent dust attacks. Ensure you are converting the “human readable” amount (1 BTC) to the correct gas units (100,000,000 sats)

Examples:

  • User wants to deposit 100,000 sats (0.001 BTC): Wallet signs an inbound tx to THORChain’s BTC /inbound_addresses vault address from the user with 100,000 sats. This will be added to the user’s Savers position.
  • User wants to withdraw 50% of their BTC Savers position: Wallet signs an inbound with 15,000 sats 50% = 5,000 basis points + 10,000[BTC dust_threshold to THORChain’s BTC vault
  • User wants to withdraw 10% of their ETH Savers position: Wallet signs an inbound with 1,000 wei (10% = 1,000 basis points + 0 [ETH dust_threshold]) to THORChain’s ETH vault
  • User wants to deposit 10,000 sats to their DOGE Savers position: Not possible transactions below the dust_threshold for each chain are ignored to prevent dust attacks.
  • User wants to deposit 20,000 sats to their BTC Savers position: Not possible with memoless, the user’s deposit will be interpreted as a withdraw:100%. Instead the user should use a memo.

translates to: “withdraw 10,000 basis points, or 100% of address’ savings.

Historical Data & Performance

An important consideration for UIs when implementing this feature is how to display:

  • an address’ present performance (targeted at retaining current savers)
  • past performance of savings vaults (targeted at attracting potential savers)

Present Performance

A user is likely to want to know the following things:

  • What is the redeemable value of my share in the Savings Vault?
  • What is the absolute amount and % yield I have earned to date on my stake?

The latter can be derived from the former.

yield_percent = (1 - (depositValue / redeemableValue)) * 100

saver’s address: bc1qcxssye4j6730h7ehgega3gyykkuwgdgmmpu62n
myUnits => curl -SL https://thornode.ninerealms.com/thorchain/pool/BTC.BTC/savers | jq '.[] | select(.asset_address == "bc1qcxssye4j6730h7ehgega3gyykkuwgdgmmpu62n") | .units'
saverUnits => curl -SL https://thornode.ninerealms.com/thorchain/pools | jq '.[] | select(.asset == "BTC.BTC") | .savers_units'
saverDepth => curl -SL https://thornode.ninerealms.com/thorchain/pools | jq '.[] | select(.asset == "BTC.BTC") | .savers_depth'

Past Performance

The easy way to determine lifetime performance of the savers vault is to look back 7 days, find the saver value, then compare it with the current saver value.

Example code:\

Support

Developers experiencing issues with these APIs can go to the Developer Discord for assistance. Interface developers should subscribe to the #interface-alerts channel for information pertinent to the endpoints and functionality discussed here.

Quickstart Guide

Introduction

THORChain allows users to deposit Layer1 assets into its network to earn asset-denominated yield without RUNE asset exposure, or being aware of THORChain’s network.

There is no permission, authentication or prior steps, so developers can get started and allow their users to earn asset-denominated yield simply by sending layer1 transactions to THORChain vaults.

Under the hood, THORChain deposits the user’s Layer1 asset into a liquidity pool which earns yield. This yield is tracked and paid to the user’s deposit value. Users can withdraw their Layer1 asset, including the yield earned. There is no slashing, penalties, timelocks, or account minimum/maximums. The only fees paid are the Layer1 fees to make a deposit and withdraw transaction (as necessitated), and a slip-based fee on entry and exit to stop price manipulation attacks. Both of these are transparent and within the user’s control.

Note

Streaming swaps is enabled for savers.

Quote for a Savers Quote

Savers Quote endpoints have been created to simplify the implementation process.

Add 1 BTC to Savers.

Request: Add 1 BTC to Savers

https://thornode.ninerealms.com/thorchain/quote/saver/deposit?asset=BTC.BTC&amount=100000000

Response:

{
  "dust_threshold": "10000",
  "expected_amount_deposit": "99932291",
  "expected_amount_out": "99932291",
  "expiry": 1700263119,
  "fees": {
    "affiliate": "0",
    "asset": "BTC/BTC",
    "liquidity": "67672",
    "outbound": "355",
    "slippage_bps": 6,
    "total": "68027",
    "total_bps": 6
  },
  "inbound_address": "bc1qe7lfmet2l5j7ypsd6ln300jt8mg3dt2q3darj8",
  "inbound_confirmation_blocks": 1,
  "inbound_confirmation_seconds": 600,
  "memo": "+:BTC/BTC",
  "notes": "First output should be to inbound_address, second output should be change back to self, third output should be OP_RETURN, limited to 80 bytes. Do not send below the dust threshold. Do not use exotic spend scripts, locks or address formats (P2WSH with Bech32 address format preferred).",
  "recommended_min_amount_in": "10000",
  "slippage_bps": 13,
  "warning": "Do not cache this response. Do not send funds after the expiry."
}

If you send 1 BTC to bc1quuf5sr444km2zlgrg654mjdfgkuzayfs7nqrfmwith the memo +:BTC/BTC, you can expect 0.99932 BTC will and will incur 13 basis points (0.13%) of slippage.

Danger

The Inbound_Address changes regularly, do not cache!

Danger

Inbound transactions should not be delayed for any reason else there is risk funds will be sent to an unreachable address. Use standard transactions, check the inbound address before sending and use the recommended gas rate to ensure transactions are confirmed in the next block to the latest Inbound_Address.

For security reasons, your inbound transaction will be delayed by 1 BTC Block.

Info

Full quote saving endpoint specification can be found here: https://thornode.ninerealms.com/thorchain/doc/.

See an example implementation here.

User withdrawing all of their BTC Saver's position.

Request: Withdraw 100% of BTC Savers for bc1qy9rjlz5w3tqn7m3reh3y48n8del4y8z42sswx5

https://thornode.ninerealms.com/thorchain/quote/saver/withdraw?asset=BTC.BTC&address=bc1qy9rjlz5w3tqn7m3reh3y48n8del4y8z42sswx5&withdraw_bps=10000

Response:

{
  "dust_amount": "20000",
  "dust_threshold": "10000",
  "expected_amount_out": "297234276",
  "expiry": 1698901306,
  "fees": {
    "affiliate": "0",
    "asset": "BTC.BTC",
    "liquidity": "150576",
    "outbound": "39000",
    "slippage_bps": 5,
    "total": "189576",
    "total_bps": 6
  },
  "inbound_address": "bc1qmed4v5am2hcg8furkeff2pczdnt0qu4flke420",
  "memo": "-:BTC/BTC:10000",
  "notes": "First output should be to inbound_address, second output should be change back to self, third output should be OP_RETURN, limited to 80 bytes. Do not send below the dust threshold. Do not use exotic spend scripts, locks or address formats (P2WSH with Bech32 address format preferred).",
  "outbound_delay_blocks": 548,
  "outbound_delay_seconds": 3288,
  "slippage_bps": 60,
  "warning": "Do not cache this response. Do not send funds after the expiry."
}

Warning

Deposit and withdraw interfaces will return inbound_address and memo fields that can be used to construct the transaction. Do not cache theinbound_address field!

Basic Mechanics

Users can add assets to a vault by sending assets directly to the chain’s vault address found on the /thorchain/inbound_addresses endpoint. Quote endpoints will also return this.

1. Find the L1 vault address

https://thornode.ninerealms.com/thorchain/inbound_addresses

Example:

curl -SL https://thornode.ninerealms.com/thorchain/inbound_addresses | jq '.[] | select(.chain == "BTC") | .address'
=> “bc1q556ljv5y4rkdt4p46usx86esljs3xqjxyntlyd”

2. Determine if there is capacity available to mint new synths

There is a cap on how many synths can be minted as a function of liquidity depth. To do this, find synth_mint_paused = false on the /pool endpoint

curl -SL https://thornode.ninerealms.com/thorchain/pools | jq '.[] | select(.asset == "BTC.BTC") | .synth_mint_paused'

3. Send memoless savers transactions

Both Saver Deposit and Withdraw transactions can be done without memos (optional memos can be included if a wallet wishes, see Transaction Memos, since there is a marginal transaction cost savings to including memos).

To deposit, users should send any amount of asset they wish (avoiding dust amounts). The network will read the deposit and user address, then add them into the Saver Vault automatically.

To withdraw, the user should send a specific dust amount of asset (avoiding the dust threshold), from an amount 0 units above the dust threshold, to an amount 10,000 units above the threshold.
10000 units is read as “withdraw 10000 basis points”, which is 100%.

Info

The dust threshold is the point at which the network will ignore the amount sent to stop dust attacks (widely seen on UTXO chains).

Specific rules for each chain and action are as follows:

  • Each chain has a defined dust_threshold in base units
  • For asset amounts in the range: [ dust_threshold + 1 : dust_threshold + 10,000], the network will withdraw dust_threshold - 10,000 basis points from the user’s Savers position
  • For asset amounts greater than dust_threshold + 10,000, the network will add to the user’s Savers position

The dust_threshold for each chain are defined as:

  • BTC: 10,000 sats
  • BCH: 10,000 sats
  • LTC: 10,000 sats
  • DOGE: 100,000,000 sats
  • ETH,AVAX: 0 wei
  • ATOM: 0 uatom
  • BNB: 0 nbnb

Info

Transactions with asset amounts equal to or below the dust_threshold for the chain will be ignored to prevent dust attacks. Ensure you are converting the “human readable” amount (1 BTC) to the correct gas units (100,000,000 sats)

Examples:

  • User wants to deposit 100,000 sats (0.001 BTC): Wallet signs an inbound tx to THORChain’s BTC /inbound_addresses vault address from the user with 100,000 sats. This will be added to the user’s Savers position.
  • User wants to withdraw 50% of their BTC Savers position: Wallet signs an inbound with 15,000 sats 50% = 5,000 basis points + 10,000[BTC dust_threshold to THORChain’s BTC vault
  • User wants to withdraw 10% of their ETH Savers position: Wallet signs an inbound with 1,000 wei (10% = 1,000 basis points + 0 [ETH dust_threshold]) to THORChain’s ETH vault
  • User wants to deposit 10,000 sats to their DOGE Savers position: Not possible transactions below the dust_threshold for each chain are ignored to prevent dust attacks.
  • User wants to deposit 20,000 sats to their BTC Savers position: Not possible with memoless, the user’s deposit will be interpreted as a withdraw:100%. Instead the user should use a memo.

translates to: “withdraw 10,000 basis points, or 100% of address’ savings.

Historical Data & Performance

An important consideration for UIs when implementing this feature is how to display:

  • an address’ present performance (targeted at retaining current savers)
  • past performance of savings vaults (targeted at attracting potential savers)

Present Performance

A user is likely to want to know the following things:

  • What is the redeemable value of my share in the Savings Vault?
  • What is the absolute amount and % yield I have earned to date on my stake?

The latter can be derived from the former.

yield_percent = (1 - (depositValue / redeemableValue)) * 100

saver’s address: bc1qcxssye4j6730h7ehgega3gyykkuwgdgmmpu62n
myUnits => curl -SL https://thornode.ninerealms.com/thorchain/pool/BTC.BTC/savers | jq '.[] | select(.asset_address == "bc1qcxssye4j6730h7ehgega3gyykkuwgdgmmpu62n") | .units'
saverUnits => curl -SL https://thornode.ninerealms.com/thorchain/pools | jq '.[] | select(.asset == "BTC.BTC") | .savers_units'
saverDepth => curl -SL https://thornode.ninerealms.com/thorchain/pools | jq '.[] | select(.asset == "BTC.BTC") | .savers_depth'

Past Performance

The easy way to determine lifetime performance of the savers vault is to look back 7 days, find the saver value, then compare it with the current saver value.

Example code:\

Support

Developers experiencing issues with these APIs can go to the Developer Discord for assistance. Interface developers should subscribe to the #interface-alerts channel for information pertinent to the endpoints and functionality discussed here.

Fees and Wait Times

Fees

Users pay two kinds of fees when entering or exiting Savings Vaults:

  1. Layer1 Network Fees (gas): paid by the user when depositing or paid by the network when withdrawing and subtracted from the user's redemption value.
  2. Slip Fees: protects the pool from being manipulated by large deposits/withdraws. Calculated as a function of transaction size and current pool depth.

The following are required to determine approximate deposit / withdrawal fees:

outboundFee = curl -SL https://thornode.ninerealms.com/thorchain/inbound_addresses | jq '.[] | select(.chain == "BTC") | .outbound_fee'
=> 30000

poolDepth = curl -SL https://thornode.ninerealms.com/thorchain/pools | jq '.[] | select(.asset == "BTC.BTC") | .balance_asset'
=> 68352710830 => 683.5 BTC

Info

The Quote endpoints will return fee estimates.

Deposit Fees

Example: user is depositing 1.0 BTC into the network, which has 1000 BTC in the pool, with 30k sats outboundFee.

The user will pay ~1/3rd of the THORChain's outbound fee to send assets to Savings Vault, using their typical wallet fee settings (note, this is an estimate only).

totalFee = networkFee + liquidityFee

networkFee = 0.33 * outboundFee = 10,000 sats

liquidityFee = depositAmount / (depositAmount + poolDepth) * depositAmount
liquidityFee = 1.0 / (1.0+10000) * 1.0 = 99000 sats

total fee = 109,000 sats

Withdrawal Fees

Example: user is withdrawing 1.1 BTC from the network, which has 1000 BTC in the pool, with 30k outboundFee.

totalFee = networkFee + liquidityFee

networkFee = outboundFee = 30,000 sats

liquidityFee = withdrawAmount / (withdrawAmount + poolDepth) * withdrawAmount
liquidityFee = 1.1 / (1.1 + 1001.1) * 1.1 = 120,734 sats

total fee = 150,734 sats

Info

Remember, the liquidityFee is entirely dependent on the size of the transaction the user is wishing to do. They may wish to do smaller transactions over a period of time to reduce fees.

Wait Times

When depositing, there are three phases to the transaction.

  1. Layer1 Inbound Confirmation - assuming the inbound Tx will be confirmed in the next block, it is the source blockchain block time.
  2. Observation Counting - time for 67% THORChain Nodes to observe and agree on the inbound Tx.
  3. Confirmation Counting - for non-instant finality blockchains, the amount of time THORChain will wait before processing to protect against double spends and re-org attacks.

When withdrawing using the dust threshold, there are three phases to the transaction

  1. Layer1 Inbound Confirmation - assuming the inbound Tx will be confirmed in the next block, it is the source blockchain block time.
  2. Observation Counting - time for 67% THORChain Nodes to observe and agree on the inbound Tx.
  3. Outbound Delay - dependent on size and network traffic. Large outbounds will be delayed.
  4. Layer1 Outbound Confirmation - Outbound blockchain block time.

Wait times can be between a few seconds up to an hour. The assets being swapped, the size of the swap and the current network traffic within THORChain will determine the wait time

Info

The Quote endpoint will calculate wait times.

See the delays.md section for full details.

THORName Guide

Summary

THORNames are THORChain's vanity address system that allows affiliates to collect fees and track their user's transactions. THORNames exist on the THORChain L1, so you will need a THORChain address and $RUNE to create and manage a THORName.;

THORNames have the following properties:

  • Name: The THORName's string. Between 1-30 hexadecimal characters and -_+ special characters.;
  • Owner: This is the THORChain address that owns the THORName
  • Aliases: THORNames can have an alias address for any external chain supported by THORChain, and can have an alias for the THORChain L1 that is different than the owner.
  • Expiry: THORChain Block-height at which the THORName expires.
  • Preferred Asset: The asset to pay out affiliate fees in. This can be any asset supported by THORChain.;

Create a THORName

THORNames are created by posting a MsgDeposit to the THORChain network with the appropriate memo and enough $RUNE to cover the registration fee and to pay for the amount of blocks the THORName should be registered for.;

  • Registration fee: tns_register_fee_rune on the Network endpoint. This value is in 1e8, so 100000000 = 1 $RUNE
  • Per block fee: tns_fee_per_block_rune on the same endpoint, also in 1e8.;

For example, for a new THORName to be registered for 10 years the amount paid would be:

amt = tns_register_fee_rune + tns_fee_per_block_rune * 10 * 5256000

5256000 = avg # of blocks per year

The expiration of the THORName will automatically be set to the number of blocks in the future that was paid for minus the registration fee.

Memo Format:

Memo template is: ~:name:chain:address:?owner:?preferredAsset:?expiry

  • name: Your THORName. Must be unique, between 1-30 characters, hexadecimal and -_+ special characters.;
  • chain: The chain of the alias to set.;
  • address: The alias address. Must be an address of chain.
  • owner: THORChain address of owner (optional).
  • preferredAsset: Asset to receive fees in. Must be supported be an active pool on THORChain. Value should be asset property from the Pools endpoint.;

Info

Example: ~:ODIN:BTC:bc1Address:thorAddress:BTC.BTC

This will register a new THORName called ODIN with a Bitcoin alias of bc1Address owner of thorAddress and preferred asset of BTC.BTC.

Info

You can use Asgardex to post a MsgDeposit with a custom memo. Load your wallet, then open your THORChain wallet page > Deposit > Custom.;

Info

View your THORName's configuration at the THORName endpoint:

e.g. https://thornode.ninerealms.com/thorchain/thorname/{name}

Renewing your THORName

All THORName's have a expiration represented by a THORChain block-height. Once the expiration block-height has passed, another THORChain address can claim the THORName and any associated balance in the Affiliate Fee Collector Module (Read #preferred-asset-for-affiliate-fees), so it's important to monitor this and renew your THORName as needed.;

To keep your THORName registered you can extend the registration period (move back the expiration block height), by posting a MsgDeposit with the correct THORName memo and $RUNE amount.;

Memo:

~:ODIN:THOR:<thor-alias-address>

(Chain and alias address are required, so just use current values to keep alias unchanged).

$RUNE Amount:

rune_amt = num_blocks_to_extend * tns_fee_per_block

(Remember this value will be in 1e8, so adjust accordingly for your transaction).

Preferred Asset for Affiliate Fees

Starting in THORNode V116, affiliates can collect their fees in the asset of their choice (choosing from the assets that have a pool on THORChain). In order to collect fees in a preferred asset, affiliates must use a THORName in their swap memos.;

How it Works

If an affiliate's THORName has the proper preferred asset configuration set, the network will begin collecting their affiliate fees in $RUNE in the AffiliateCollector module. Once the accrued RUNE in the module is greater than PreferredAssetOutboundFeeMultiplier* outbound_fee of the preferred asset's chain, the network initiates a swap from $RUNE -> Preferred Asset on behalf of the affiliate. At the time of writing, PreferredAssetOutboundFeeMultiplier is set to 100, so the preferred asset swap happens when the outbound fee is 1% of the accrued $RUNE.;

Configuring a Preferred Asset for a THORName:

  1. Register your THORName following instructions above.
  2. Set your preferred asset's chain alias (the address you'll be paid out to), and your preferred asset. Note: your preferred asset must be currently supported by THORChain.

For example, if you wanted to be paid out in USDC you would:

  1. Grab the full USDC name from the Pools endpoint: ETH.USDC-0XA0B86991C6218B36C1D19D4A2E9EB0CE3606EB48

  2. Post a MsgDeposit to the THORChain network with the appropriate memo to register your THORName, set your preferred asset as USDC, and set your Ethereum network address alias. Assuming the following info:

    1. THORChain address: thor1dl7un46w7l7f3ewrnrm6nq58nerjtp0dradjtd
    2. THORName: ac-test
    3. ETH payout address: 0x6621d872f17109d6601c49edba526ebcfd332d5d;

    The full memo would look like:

    ~:ac-test:ETH:0x6621d872f17109d6601c49edba526ebcfd332d5d:thor1dl7un46w7l7f3ewrnrm6nq58nerjtp0dradjtd:ETH.USDC-0XA0B86991C6218B36C1D19D4A2E9EB0CE3606EB48

Info

You will also need a THOR alias set to collect affiliate fees. Use another MsgDeposit with memo: ~:<thorname>:THOR:<thorchain-address> to set your THOR alias. Your THOR alias address can be the same as your owner address, but won't be used for anything if a preferred asset is set.;

Once you successfully post your MsgDeposit you can verify that your THORName is configured properly. View your THORName info from THORNode at the following endpoint:
https://thornode.ninerealms.com/thorchain/thorname/ac-test

The response should look like:

{
  "affiliate_collector_rune": "0",
  "aliases": [
    {
      "address": "0x6621d872f17109d6601c49edba526ebcfd332d5d",
      "chain": "ETH"
    },
    {
      "address": "thor1dl7un46w7l7f3ewrnrm6nq58nerjtp0dradjtd",
      "chain": "THOR"
    }
  ],
  "expire_block_height": 22061405,
  "name": "ac-test",
  "owner": "thor1dl7un46w7l7f3ewrnrm6nq58nerjtp0dradjtd",
  "preferred_asset": "ETH.USDC-0XA0B86991C6218B36C1D19D4A2E9EB0CE3606EB48"
}

Your THORName is now properly configured and any affiliate fees will begin accruing in the AffiliateCollector module. You can verify that fees are being collected by checking the affiliate_collector_rune value of the above endpoint.

THORName Guide

Summary

THORNames are THORChain's vanity address system that allows affiliates to collect fees and track their user's transactions. THORNames exist on the THORChain L1, so you will need a THORChain address and $RUNE to create and manage a THORName.;

THORNames have the following properties:

  • Name: The THORName's string. Between 1-30 hexadecimal characters and -_+ special characters.;
  • Owner: This is the THORChain address that owns the THORName
  • Aliases: THORNames can have an alias address for any external chain supported by THORChain, and can have an alias for the THORChain L1 that is different than the owner.
  • Expiry: THORChain Block-height at which the THORName expires.
  • Preferred Asset: The asset to pay out affiliate fees in. This can be any asset supported by THORChain.;

Create a THORName

THORNames are created by posting a MsgDeposit to the THORChain network with the appropriate memo and enough $RUNE to cover the registration fee and to pay for the amount of blocks the THORName should be registered for.;

  • Registration fee: tns_register_fee_rune on the Network endpoint. This value is in 1e8, so 100000000 = 1 $RUNE
  • Per block fee: tns_fee_per_block_rune on the same endpoint, also in 1e8.;

For example, for a new THORName to be registered for 10 years the amount paid would be:

amt = tns_register_fee_rune + tns_fee_per_block_rune * 10 * 5256000

5256000 = avg # of blocks per year

The expiration of the THORName will automatically be set to the number of blocks in the future that was paid for minus the registration fee.

Memo Format:

Memo template is: ~:name:chain:address:?owner:?preferredAsset:?expiry

  • name: Your THORName. Must be unique, between 1-30 characters, hexadecimal and -_+ special characters.;
  • chain: The chain of the alias to set.;
  • address: The alias address. Must be an address of chain.
  • owner: THORChain address of owner (optional).
  • preferredAsset: Asset to receive fees in. Must be supported be an active pool on THORChain. Value should be asset property from the Pools endpoint.;

Info

Example: ~:ODIN:BTC:bc1Address:thorAddress:BTC.BTC

This will register a new THORName called ODIN with a Bitcoin alias of bc1Address owner of thorAddress and preferred asset of BTC.BTC.

Info

You can use Asgardex to post a MsgDeposit with a custom memo. Load your wallet, then open your THORChain wallet page > Deposit > Custom.;

Info

View your THORName's configuration at the THORName endpoint:

e.g. https://thornode.ninerealms.com/thorchain/thorname/{name}

Renewing your THORName

All THORName's have a expiration represented by a THORChain block-height. Once the expiration block-height has passed, another THORChain address can claim the THORName and any associated balance in the Affiliate Fee Collector Module (Read #preferred-asset-for-affiliate-fees), so it's important to monitor this and renew your THORName as needed.;

To keep your THORName registered you can extend the registration period (move back the expiration block height), by posting a MsgDeposit with the correct THORName memo and $RUNE amount.;

Memo:

~:ODIN:THOR:<thor-alias-address>

(Chain and alias address are required, so just use current values to keep alias unchanged).

$RUNE Amount:

rune_amt = num_blocks_to_extend * tns_fee_per_block

(Remember this value will be in 1e8, so adjust accordingly for your transaction).

Preferred Asset for Affiliate Fees

Starting in THORNode V116, affiliates can collect their fees in the asset of their choice (choosing from the assets that have a pool on THORChain). In order to collect fees in a preferred asset, affiliates must use a THORName in their swap memos.;

How it Works

If an affiliate's THORName has the proper preferred asset configuration set, the network will begin collecting their affiliate fees in $RUNE in the AffiliateCollector module. Once the accrued RUNE in the module is greater than PreferredAssetOutboundFeeMultiplier* outbound_fee of the preferred asset's chain, the network initiates a swap from $RUNE -> Preferred Asset on behalf of the affiliate. At the time of writing, PreferredAssetOutboundFeeMultiplier is set to 100, so the preferred asset swap happens when the outbound fee is 1% of the accrued $RUNE.;

Configuring a Preferred Asset for a THORName:

  1. Register your THORName following instructions above.
  2. Set your preferred asset's chain alias (the address you'll be paid out to), and your preferred asset. Note: your preferred asset must be currently supported by THORChain.

For example, if you wanted to be paid out in USDC you would:

  1. Grab the full USDC name from the Pools endpoint: ETH.USDC-0XA0B86991C6218B36C1D19D4A2E9EB0CE3606EB48

  2. Post a MsgDeposit to the THORChain network with the appropriate memo to register your THORName, set your preferred asset as USDC, and set your Ethereum network address alias. Assuming the following info:

    1. THORChain address: thor1dl7un46w7l7f3ewrnrm6nq58nerjtp0dradjtd
    2. THORName: ac-test
    3. ETH payout address: 0x6621d872f17109d6601c49edba526ebcfd332d5d;

    The full memo would look like:

    ~:ac-test:ETH:0x6621d872f17109d6601c49edba526ebcfd332d5d:thor1dl7un46w7l7f3ewrnrm6nq58nerjtp0dradjtd:ETH.USDC-0XA0B86991C6218B36C1D19D4A2E9EB0CE3606EB48

Info

You will also need a THOR alias set to collect affiliate fees. Use another MsgDeposit with memo: ~:<thorname>:THOR:<thorchain-address> to set your THOR alias. Your THOR alias address can be the same as your owner address, but won't be used for anything if a preferred asset is set.;

Once you successfully post your MsgDeposit you can verify that your THORName is configured properly. View your THORName info from THORNode at the following endpoint:
https://thornode.ninerealms.com/thorchain/thorname/ac-test

The response should look like:

{
  "affiliate_collector_rune": "0",
  "aliases": [
    {
      "address": "0x6621d872f17109d6601c49edba526ebcfd332d5d",
      "chain": "ETH"
    },
    {
      "address": "thor1dl7un46w7l7f3ewrnrm6nq58nerjtp0dradjtd",
      "chain": "THOR"
    }
  ],
  "expire_block_height": 22061405,
  "name": "ac-test",
  "owner": "thor1dl7un46w7l7f3ewrnrm6nq58nerjtp0dradjtd",
  "preferred_asset": "ETH.USDC-0XA0B86991C6218B36C1D19D4A2E9EB0CE3606EB48"
}

Your THORName is now properly configured and any affiliate fees will begin accruing in the AffiliateCollector module. You can verify that fees are being collected by checking the affiliate_collector_rune value of the above endpoint.

Tutorials

Find Savers Position

Endpoints have been made to look up a savers position quickly.

Savers Position using Thornode

Request: Get BTC saver information for the address 33XBYjiR3B7g8755mCB56aHtxQYL2Go9xf https://thornode.ninerealms.com/thorchain/pool/BTC.BTC/saver/33XBYjiR3B7g8755mCB56aHtxQYL2Go9xf

Response:

{
  "asset": "BTC.BTC",
  "asset_address": "33XBYjiR3B7g8755mCB56aHtxQYL2Go9xf",
  "last_add_height": 8794877,
  "units": "71338",
  "asset_deposit_value": "71723",
  "asset_redeem_value": "71830",
  "growth_pct": "0.001491850591860352"
}

Returns all savers for a given asset. To get all savers you can use https://thornode.ninerealms.com/thorchain/pool/BTC.BTC/savers

Savers Position using Midgard

Request Get Savers Position for address 33XBYjiR3B7g8755mCB56aHtxQYL2Go9xf

https://midgard.ninerealms.com/v2/saver/33XBYjiR3B7g8755mCB56aHtxQYL2Go9xf

Response:

{
  "pools": [
    {
      "assetAddress": "33XBYjiR3B7g8755mCB56aHtxQYL2Go9xf",
      "assetBalance": "71723",
      "assetWithdrawn": "0",
      "dateFirstAdded": "1671838673",
      "dateLastAdded": "1671838673",
      "pool": "BTC.BTC",
      "saverUnits": "71338"
    }
  ]
}

Find Liquidity Position

Similar to savers, looking up the liquidity position with a given address is possible.

Liquidity Provider Position using Thornode

Request: Get liquidity provider information in the BTC pool for the address bc1q00nrswtpp3zddgc0uvppuszhnr8k8zfcdps9gn https://thornode.ninerealms.com/thorchain/pool/BTC.BTC/liquidity_provider/bc1q00nrswtpp3zddgc0uvppuszhnr8k8zfcdps9gn

Response:

{
  "asset": "BTC.BTC",
  "asset_address": "bc1q00nrswtpp3zddgc0uvppuszhnr8k8zfcdps9gn",
  "last_add_height": 6332320,
  "units": "3190637579",
  "pending_rune": "0",
  "pending_asset": "0",
  "rune_deposit_value": "5340160943",
  "asset_deposit_value": "548543",
  "rune_redeem_value": "6217698938",
  "asset_redeem_value": "500382",
  "luvi_deposit_value": "1696309",
  "luvi_redeem_value": "1748188",
  "luvi_growth_pct": "0.030583460914255598"
}

Liquidity Provider Position using Midgard

Several endpoints exist however the member's endpoint is the most comprehensive

Request: Get liquidity provider information for the address bc1q0kmdagyqhkzw4sgs7f0vycxw7jhexw0rl9x9as

https://midgard.ninerealms.com/v2/member/thor169lfsnv2myg8yrudx4353xakq44756w9830crc

Response:

{
  "pools": [
    {
      "assetAdded": "67500000",
      "assetAddress": "bc1qw5cj49wng7jpfg2zq6ca5py7uctq4maulyc66c",
      "assetPending": "0",
      "assetWithdrawn": "0",
      "dateFirstAdded": "1669373649",
      "dateLastAdded": "1669373649",
      "liquidityUnits": "466600725237",
      "pool": "BTC.BTC",
      "runeAdded": "955003804620",
      "runeAddress": "thor169lfsnv2myg8yrudx4353xakq44756w9830crc",
      "runePending": "0",
      "runeWithdrawn": "0"
    }
    ...
  ]
}

Any address can be used with this endpoint, e.g. bc1q0kmdagyqhkzw4sgs7f0vycxw7jhexw0rl9x9as with ?showSavers=true to show any savers position also.

https://midgard.ninerealms.com/v2/member/bc1q0kmdagyqhkzw4sgs7f0vycxw7jhexw0rl9x9as?showSavers=true

User Transaction History

Actions within THORChain can be obtained from Midgard which will list the actions taken by any given address.

Request: List actions by the address bnb1hsmrred449qcmhg9sa42deejr8nurwsqgu9ga4

https://midgard.ninerealms.com/v2/actions?address=bnb1hsmrred449qcmhg9sa42deejr8nurwsqgu9ga4

Response:

{
  "actions": [
    {
      "date": "1647866221415353933",
      "height": "4778782",
      "in": [
        {
          "address": "thor169lfsnv2myg8yrudx4353xakq44756w9830crc",
          "coins": [
            {
              "amount": "63684757953",
              "asset": "THOR.RUNE"
            }
          ],
          "txID": "ED1384012BA129B889CCF3285A1FB73B127101A0924F49B64FE58A6939FA47C4"
        },
        {
          "address": "bnb1hsmrred449qcmhg9sa42deejr8nurwsqgu9ga4",
          "coins": [
            {
              "amount": "541348102046",
              "asset": "BNB.BUSD-BD1"
            }
          ],
          "txID": "F8CEAF2EA762D08AE22CC173BC4B2781B082927990C4F623D2629C4EE2BEC93F"
        }
      ],
      "metadata": {
        "addLiquidity": {
          "liquidityUnits": "38152218105"
        }
      },
      "out": [],
      "pools": [
        "BNB.BUSD-BD1"
      ],
      "status": "success",
      "type": "addLiquidity"
    },
    ....
  ],
  "count": "6"
}

Will also include savers' actions. The Action endpoint is very flexible, see the docs.

Check the status of a Transaction

Transactions can take time to fully process once sent to THORChain.

Request: Get the status for BTC tx A56B423250020E4960D9836C6F843E1D3333FAE583C9CA26776F0D68DA69CE4A sent to the Savers vault. https://thornode.ninerealms.com/thorchain/alpha/tx/status/A56B423250020E4960D9836C6F843E1D3333FAE583C9CA26776F0D68DA69CE4A

Response:

{
  "tx": {
    "id": "A56B423250020E4960D9836C6F843E1D3333FAE583C9CA26776F0D68DA69CE4A",
    "chain": "BTC",
    "from_address": "bc1qmlw9x4xnkmyd5xgtvn5cuwc5jcq033g4cj2ur9",
    "to_address": "bc1q02hrv5y4dm7rux2swg020yzykhaufrglyv7kkj",
    "coins": [
      {
        "asset": "BTC.BTC",
        "amount": "30051812"
      }
    ],
    "gas": [
      {
        "asset": "BTC.BTC",
        "amount": "2500"
      }
    ],
    "memo": "+:BTC/BTC:t:0"
  },
  "stages": {
    "inbound_observed": {
      "completed": true
    },
    "inbound_finalised": {
      "completed": true
    }
  }
}

Note this endpoint is in alpha and the response will differ for swaps.

For more details information, https://thornode.ninerealms.com/thorchain/tx/A56B423250020E4960D9836C6F843E1D3333FAE583C9CA26776F0D68DA69CE4A/signers can be used looking for updated_vault

TypeScript (Web)

xchainJs is an implementation reference for THORChain.

Overview

Overview

XChainJS is an open-source library with a common interface for multiple blockchains, built for simple and fast integration for wallets and Dexs and more. xChainjs is designed to abstract THORChain and specific blockchain complexity and to provide an easy-to-use API for developers.

The packages implement the complexity detailed in the other sections of this site.

xChain has several key modules allowing powerful functionality.

Thorchain-query

Allows easy information retrieval and estimates from THORChain.

Query Package

Thorchain-amm

Conducts actions such as swap, add and remove. It wraps xchain clients and creates a new wallet class for and balance collection.

AMM Package

Chain clients

For every blockchain connected to THORChain with a common interface.

Current clients implemented are**:**

  • xchain-avax
  • xchain-binance
  • xchain-bitcoin
  • xchain-bitcoincash
  • xchain-cosmos
  • xchain-doge
  • xchain-ethereum
  • xchain-litecoin
  • xchain-mayachain
  • xchain-thorchain

Client Packages

APIs for getting data from THORChain.

  • Midgard
  • Thornode

Packages Breakdown

See the package breakdown for more information.

Install Procedures

Ensure you have the following

  • npm --version v8.5.5 or above
  • node --version v16.15.0
  • yarn --version v1.22.18 or above

Create a new project by creating a new folder, then type npx tsc --init.

Finding required dependencies

The replit code examples have all the required packages within the project.json file, just copy the project dependencies into your own project.json.

Example for the query-package, estimateSwap packages

  1. Go to the replit code example then press show files. Select the project.json file.
  2. Locate and then copy the dependencies section into your project.json file.
  3. From the command line, type yarn. This will download and install the required packages.

The code is available on GitHub and managed by several key developers. Reach out at Telegram group: https://t.me/xchainjs for more information.

Query Package

This package is designed to obtain any desired information from THORChain that a developer could want. While it uses Midgard and Thornode packages it is much more powerful as it knows where to get the best information and how to package the information for easy consumption by developers or higher functions.

It exposes several simple functions that implement all of THORChain's complexity to allow easy information retrieval. The Query package does not perform any actions on THORChain or send transactions, that is the job of the Thorchain-AMM package.

Code examples in Replit

Currently implemented functions are listed below with code examples. Press the Run button to run the code and see the output. Press Show files, and select index.ts to see the code. Select package.json to see all the package dependencies. Repo link and install instructions.

Estimate Swap

Provides estimated swap information for a single or double swap for any given two assets within THORChain. Designed to be used by interfaces, see more info here. EstimateSwap will do the following:

  • Validate swap inputs
  • Checks for network or chain halts
  • Get the latest pool data from Midgard
  • Work out the swap slip, swap fee and output
  • Deducts all fees from the input amount (inbound, swap, outbound and any affiliate) in the correct order to produce netOutput and detail fees in totalFees
  • Ensures totalFees is not greater than input.
  • Work out the expected wait time including confirmation counting and outbound delay.
  • Get the current Asgard Vault address for the inbound asset
  • Advise if a swap is possible and provide a reason if it is not.

Note: This will be the best estimate using the information available, however exact values will be different depending on pool depth changes and network congestion.

Savers

Shows use of the savers quote endpoints.

Check Balance

Checks the liquidity position of a given address in a given pool. Retrieves information such as current value, ILP coverage, pool share and last added.

Check Transaction

Provide the status of a transaction given an input hash (e.g. the output of doSwap). Looks at the different stages a transaction can be in and report.

In development

Estimate Add Liquidity

Provides an estimate for given add liquidity parameters such as slip fee, transaction fees, and expected wait time. Supports symmetrical, asymmetrical and uneven additions.

Estimate Remove Liquidity

Provides information for a planned withdrawal for a given liquidity position and withdrawal percentage. Information such as fees, wait time and ILP coverage

List Pools

Lists all the pool detail within THORChain.

Network Values

List current network values from constants and mimir. If mimir override exists, it is displayed instead.

If there is a function you want to be added, reach out in Telegram or the dev discord server.

AMM Package

While the Query package allows quick and powerful information retrieval from THORChain such as a swap estimate., this package performs the actions (sends the transactions), such as a swap, add and remove.

As a general rule, this package should be used in conjunction with the query package to first check if an action is going to be possible be performing the action.

Example: call estimateSwap first to see if the swap is going to be successful before calling doSwap, as doSwap will not check.

Code examples in Replit

Currently implemented functions are listed below with code examples. Press the Run button to run the code and see the output. Press,Show Files, and select index.ts to see the code. Select package.json to see all the package dependencies. Github link and install instructions.

DoSwap

Executes a swap from a given wallet. This will result in the inbound transaction into THORChain.

DoSwap runs EstimateSwap first then if successful sends a transaction with a constructed transaction memo using a chain client. Do swap can be used with an existing xchain client implementation or a custom wallet and will return the transaction hash of the inbound transaction.

A seed is provided in the example but it has no funds so it will error.

Savers

Adds and removed liquidity from Savers. Requires a seed with funds.

Add Liquidity

Adds liquidity to a pool. Provide both assets for the pool. lp type is determined from the amount of the asset. The example is a single-sided rune deposit. A seed is provided in the example but it has no funds so it will error.

Remove Liquidity

Removes Liquidity from a pool. The opposite of adding liquidity.

Client Packages

Full Multichain Wallet Example

A wallet class has been created that instantiates every chain client and leverages the interface which greatly simplifies working with wallets and THORChain. See the below code example.

Client Packages breakdown

Client packages have been created for each blockchain that connects to THORChain. All clients implement xchain-crypto which acts like a super class and gives each client a common interface.

Common functions with code examples are:

Config and Set Up of a Wallet

Some chains require address history to query balances and Txs

Querying

All clients implement these functions. While most can use the same code, some have slight client differences.

  • Get Explorer URL - for the specific blockchain
  • Get Balance - returns the balance of an address
  • Get Transactions - returns a simplified array of recent transactions for an address.
  • Get Transaction Data - returns transaction information from the transaction ID/hash.

See below for a Bitcoin example. Also see Ethereum, Binance, THORChain, Cosmos, and Avalanche examples.

Some chains require address history to query balances and Txs

Transactions

  • Get Fees - get the transaction fee for the chain, separate from THORChain fees
  • Transfer - transfer funds from one wallet to another.
  • Purge - When a wallet is "locked" the private key should be purged in each client by setting it back to null.

See http://docs.xchainjs.org/xchain-client/overview.html for more information.

Packages Breakdown

How xChainjs is constructed

xchain-[chain] clients

Each blockchian that is integrated into THORChain has a corresponding xchain client with a suite of functionality to work with that chain. They all extend the xchain-client class.

xchain-thorchain-amm

Thorchain automatic market maker that uses Thornode & Midgard Api's AMM functions like swapping, adding and removing liquidity. It wraps xchain clients and creates a new wallet class and balance collection.

xchain-thorchain-query

Uses midgard and thornode Api's to query Thorchain for information. This module should be used as the starting place get any THORChain information that resides in THORNode or Midgard as it does the heaving lifting and configuration.

Default endpoints are provided with redundancy, custom THORNode or Midgard endpoints can be provided in the constructor.

xchain-midgard

This package is built from OpenAPI-generator. It is used by the thorchain-query.

Thorchain-query contains midgard class that uses xchain-midgard and the following end points:

  • /v2/thorchain/mimir
  • /v2/thorchain/inbound_addresses
  • /v2/thorchain/constants
  • /v2/thorchain/queue

For simplicity, is recommended to use the midgard class within thorchain-query instead of using the midgard package directly.

Midgard Configuration in thorchain-query

Default endpoints defaultMidgardConfig are provided with redundancy within the Midgard class.

// How thorchain-query constructs midgard
const defaultMidgardConfig: Record<Network, MidgardConfig> = {
  mainnet: {
    apiRetries: 3,
    midgardBaseUrls: [
      'https://midgard.ninerealms.com',
      'https://midgard.thorchain.info',
      'https://midgard.thorswap.net',
    ],
  },
  ...
  export class Midgard {
  private config: MidgardConfig
  readonly network: Network
  private midgardApis: MidgardApi[]

  constructor(network: Network = Network.Mainnet, config?: MidgardConfig) {
    this.network = network
    this.config = config ?? defaultMidgardConfig[this.network]
    axiosRetry(axios, { retries: this.config.apiRetries, retryDelay: axiosRetry.exponentialDelay })
    this.midgardApis = this.config.midgardBaseUrls.map((url) => new MidgardApi(new Configuration({ basePath: url })))
  }

Custom Midgard endpoints can be provided in the constructor using the MidgardConfig type.

// adding custom endpoints
  const network = Network.Mainnet
  const customMidgardConfig: MidgardConfig = {
    apiRetries: 3,
    midgardBaseUrls: [
      'https://midgard.customURL.com',
    ],
  }
  const midgard = new Midgard(network, customMidgardConfig)
}

See ListPools for a working example.

xchain-thornode

This package is built from OpenAPI-generator and is also used by the thorchain-query. The design is similar to the midgard. Thornode should only be used when time-sensitive data is required else midgard should be used.

// How thorchain-query constructs thornode
const defaultThornodeConfig: Record<Network, ThornodeConfig> = {
  mainnet: {
    apiRetries: 3,
    thornodeBaseUrls: [
      `https://thornode.ninerealms.com`,
      `https://thornode.thorswap.net`,
      `https://thornode.thorchain.info`,
    ],
  },
  ...
  export class Thornode {
  private config: ThornodeConfig
  private network: Network
 ...
  constructor(network: Network = Network.Mainnet, config?: ThornodeConfig) {
    this.network = network
    this.config = config ?? defaultThornodeConfig[this.network]
    axiosRetry(axios, { retries: this.config.apiRetries, retryDelay: axiosRetry.exponentialDelay })
    this.transactionsApi = this.config.thornodeBaseUrls.map(
      (url) => new TransactionsApi(new Configuration({ basePath: url })),
    )
    this.queueApi = this.config.thornodeBaseUrls.map((url) => new QueueApi(new Configuration({ basePath: url })))
    this.networkApi = this.config.thornodeBaseUrls.map((url) => new NetworkApi(new Configuration({ basePath: url })))
    this.poolsApi = this.config.thornodeBaseUrls.map((url) => new PoolsApi(new Configuration({ basePath: url })))
    this.liquidityProvidersApi = this.config.thornodeBaseUrls.map(
      (url) => new LiquidityProvidersApi(new Configuration({ basePath: url })),
    )
  }

Thornode Configuration in thorchain-query

As with the midgard package, thornode can also be given custom end points via the ThornodeConfig type.

xchain-util

A helper packager used by all the other packages. It has the following modules:

  • asset - Utilities for handling assets
  • async - Utitilies for async handling
  • bn - Utitilies for using bignumber.js
  • chain - Utilities for multi-chain
  • string - Utilities for strings

Coding Guide

A coding overview to xchainjs.

General

The foundation of xchainjs is defined in the xchain-util package

  • Address: a crypto address as a string.
  • BaseAmount: a bigNumber in 1e8 format. E.g. 1 BTC = 100,000,000 in BaseAmount
  • AssetAmount: a BaseAmount*10^8. E.g. 1 BTC = 1 in Asset Amount.
  • Asset: Asset details {Chain, symbol, ticker, isSynth}

Info

All Assets must conform to the Asset Notation

assetFromString() is used to quickly create assets and will assign chain and synth.

  • CryptoAmount: is a class that has:
 baseAmount: BaseAmount
 readonly asset: Asset

All crypto should use the CryptoAmount object with the understanding they are in BaseAmount format. An example to switch between them:

// Define 1 BTC as CryptoAmount
oneBtc = new CryptoAmount(
  assetToBase(assetAmount(1)),
  assetFromStringEx(`BTC.BTC`),
);
// Print 1 BTC in BaseAmount
console.log(oneBtc.amount().toNumber().toFixed()); // 100000000
// Print 1 BTC in Asset Amount
console.log(oneBtc.AssetAmount().amount().toNumber().toFixed()); // 1

Query

Major data types for the thorchain-query package.

EstimateSwapParams

SwapInput

The input Type for estimateSwap. This is designed to be created by interfaces and passed into EstimateSwap. Also see Swap Memo for more information.

VariableData TypeComments
inputCryptoAmountInbound asset and amount
destinationAssetAssetOutbound asset
destinationAddressStringOutbound asset address
slipLimitBigNumberOptional: Used to set LIM
affiliateFeePercentnumberOptional: 0-0.1 allowed
affiliateAddressAddressOptional: THOR address
interfaceIDstringOptional: Assigned interface ID

SwapEstimate

The internal return type is used within estimateSwap after the calculation is done.

VariableData TypeComments
totalFeesTotalFeesAll fees for swap
slipPercentageBigNumberActual slip of the swap
netOutputCryptoAmountInput - totalFees
waitTimeSecondsnumberEstimated time for the swap
canSwapbooleanFalse if there is an issue
errorsstring arrayContains info if canSwap is false

TxDetails

Return type of estimateSwap. This is designed to be used by interfaces to give them all the information they need to display to the user.

VariableData TypeComments
txEstimateSwapEstimateSwap details
memostringConstructed memo THORChain will understand
expiryDateTimeWhen the SwapEstimate information will no longer be valid
toAddressstringCurrent Asgard Vault address from inbound_address

Danger

Do not use toAddress after expiry as the Asgard vault rotates

AMM

Major data types for the thorchain-query package.

ExecuteSwap

Input Type for doSwap where a swap will be actually conducted. Based on EstimateSwapParams.

TxSubmitted

Variable
hashstringinbound Tx Hash
urlstringBlock exploer url
waitTimeSecondsnumberEstimated time for the swap

Connecting to THORChain

The Network Information comes from four sources:

  1. Midgard: Consumer information relating to swaps, pools, and volume. Dashboards will primarily interact with Midgard.
  2. THORNode: Raw blockchain data provided by the THORChain state machine. THORChain wallets and block explorers will query THORChain-specific information here.
  3. Cosmos RPC: Used to query for generic CosmosSDK information.
  4. Tendermint RPC: Used to query for consensus-related information.

Info

The below endpoints are run by specific organisations for public use. There is a cost to running these services. If you want to run your own full node, please see https://docs.thorchain.org/thornodes/overview.

Midgard

Midgard returns time-series information regarding the THORChain network, such as volume, pool information, users, liquidity providers and more. It also proxies to THORNode to reduce burden on the network. Runs on every node.

Mainnet:

Stagenet:

THORNode

THORNode returns application-specific information regarding the THORChain state machine, such as balances, transactions and more. Careful querying this too much - you could overload the public nodes. Consider running your own node. Runs on every node.

Mainnet (for post-hard-fork blocks 4786560 and later):

Stagenet:

Cosmos RPC

The Cosmos RPC allows Cosmos base blockchain information to be returned. However, not all endpoints have been enabled.

Endpoints guide: https://v1.cosmos.network/rpc/v0.45.1

Example URL https://thornode.ninerealms.com/cosmos/bank/v1beta1/balances/thor1dheycdevq39qlkxs2a6wuuzyn4aqxhve4qxtxt

Tendermint RPC

The Tendermint RPC allows Tendermint consensus information to be returned.

Any Node Ports:

  • MAINNET Port: 27147
  • STAGENET Port: 26657

Endpoints guide.

https://docs.tendermint.com/master/rpc/#/

Mainnet:

URLs (for post-hard-fork blocks 4786560 and later)

Pre-hard-fork blocks 4786559 and earlier.

Stagenet:

P2P

P2P is the network layer between nodes, useful for network debugging.

MAINNET Port: 27146

STAGENET Port: 26656

P2P Guide
https://docs.tendermint.com/master/spec/p2p/

Connecting to THORChain

The Network Information comes from four sources:

  1. Midgard: Consumer information relating to swaps, pools, and volume. Dashboards will primarily interact with Midgard.
  2. THORNode: Raw blockchain data provided by the THORChain state machine. THORChain wallets and block explorers will query THORChain-specific information here.
  3. Cosmos RPC: Used to query for generic CosmosSDK information.
  4. Tendermint RPC: Used to query for consensus-related information.

Info

The below endpoints are run by specific organisations for public use. There is a cost to running these services. If you want to run your own full node, please see https://docs.thorchain.org/thornodes/overview.

Midgard

Midgard returns time-series information regarding the THORChain network, such as volume, pool information, users, liquidity providers and more. It also proxies to THORNode to reduce burden on the network. Runs on every node.

Mainnet:

Stagenet:

THORNode

THORNode returns application-specific information regarding the THORChain state machine, such as balances, transactions and more. Careful querying this too much - you could overload the public nodes. Consider running your own node. Runs on every node.

Mainnet (for post-hard-fork blocks 4786560 and later):

Stagenet:

Cosmos RPC

The Cosmos RPC allows Cosmos base blockchain information to be returned. However, not all endpoints have been enabled.

Endpoints guide: https://v1.cosmos.network/rpc/v0.45.1

Example URL https://thornode.ninerealms.com/cosmos/bank/v1beta1/balances/thor1dheycdevq39qlkxs2a6wuuzyn4aqxhve4qxtxt

Tendermint RPC

The Tendermint RPC allows Tendermint consensus information to be returned.

Any Node Ports:

  • MAINNET Port: 27147
  • STAGENET Port: 26657

Endpoints guide.

https://docs.tendermint.com/master/rpc/#/

Mainnet:

URLs (for post-hard-fork blocks 4786560 and later)

Pre-hard-fork blocks 4786559 and earlier.

Stagenet:

P2P

P2P is the network layer between nodes, useful for network debugging.

MAINNET Port: 27146

STAGENET Port: 26656

P2P Guide
https://docs.tendermint.com/master/spec/p2p/

Querying THORChain

Supported Address Formats

Below are the list of supported Address Formats. Not using this risks loss of funds.

ChainSupported Address FormatNotes
BTCP2WSH /w Bech32 (preferred), P2WPKH /w Bech32, P2PKH, P2SHDo not send to/from with P2TR. Do not send below the dust threshold. Do not use exotic spend scripts, locks or address formats
ETHEIP-55Do not send to or from contract addresses.
BNBBech32
BSCEIP-55Do not send to or from contract addresses.
AVAXEIP-55Do not send to or from contract addresses.
DOGEBech32
LTCBech32
BCHBech32
GAIA (cosmoshub)Bech32

All inbound_address support this format.

⚠️ THORChain does NOT currently support BTC Taproot. User funds will be lost if sent to or from a taproot address!

Getting the Asgard Vault

Vaults are fetched from the /inbound_addresses endpoint:

https://thornode.ninerealms.com/thorchain/inbound_addresses

You need to select the address of the Chain the inbound transaction will go to.

The address will be the current active Asgard Address that accepts inbounds. Do not cache these address as they change regularly. Do not delay inbound transactions (e.g. do not use future timeLocks).

Example Output, each connected chain will be displayed.


{
    "address": "bc1q2taly7tynxvmmw5n2048wv56cyhmnc6lvx7737",
    "chain": "BTC",
    "chain_lp_actions_paused": false,
    "chain_trading_paused": false,
    "dust_threshold": "10000",
    "gas_rate": "19",
    "gas_rate_units": "satsperbyte",
    "global_trading_paused": false,
    "halted": false,
    "outbound_fee": "39000",
    "outbound_tx_size": "1000",
    "pub_key": "thorpub1addwnpepq22rph4ed3nkp6lp060nmuqy3p0axadaklnvcs4qfrgyq6zl0rrux9jxkxj"
  }

```admonish danger
Never cache vault addresses, they churn regularly

Danger

Inbound transactions should not be delayed for any reason else there is risk funds will be sent to an unreachable address. Use standard transactions, check the inbound address before sending and use the recommended gas rate to ensure transactions are confirmed in the next block to the latest Inbound_Address.

Danger

Check for the halted parameter and never send funds if it is set to true

Warning

If a chain has a router on the inbound address endpoint, then everything must be deposited via the router. The router is a contract that the user first approves, and the deposit call transfers the asset into the network and emits an event to THORChain.


This is done because "tokens" on protocols don't support memos on-chain, thus need to be wrapped by a router which can force a memo.

Note: you can transfer the base asset, eg ETH, directly to the address and skip the router, but it is recommended to deposit everything via the router.

{
  "address": "0x500b62a37c1afe79d59b373639512d03e3c4f5e8",
  "chain": "ETH",
  "gas_rate": "70",
  "halted": false,
  "pub_key": "thorpub1addwnpepq05w4xwaswph29ksls25ymjkypav30t8ktyu2dqzkxqk3pkf2l5zklvfzef",
  "router": "0xD37BbE5744D730a1d98d8DC97c42F0Ca46aD7146"
}

Warning

If you connect to a public Midgard, you must be conscious of the fact that you can be phished and could send money to the WRONG vault. You should do safety checks, i.e. comparing with other nodes, or even inspecting the vault itself for the presence of funds. You should also consider running your own 'fullnode' instance to query for trusted data.

  • Chain: Chain Name
  • Address: Asgard Vault inbound address for that chain.,
  • Halted: Boolean, if the chain is halted. This should be monitored.
  • gas_rate: rate to be used, e.g. in Stats or GWei. See Fees.

Displaying available pairs

Use the /pools endpoint of Midgard to retrieve all swappable assets on THORChain. The response will be an array of objects like this:

{
  "asset": "BNB.BTCB-1DE",
  "assetDepth": "11262499812",
  "assetPrice": "11205.698400479405",
  "assetPriceUSD": "38316.644098172634",
  "liquidityUnits": "61816750778660",
  "poolAPY": "0.24483254735655713",
  "runeDepth": "126204176128728",
  "status": "available",
  "synthSupply": "0",
  "synthUnits": "0",
  "units": "61816750778660",
  "volume24h": "67544420820530"
}

Info

Only pools with "status": "available" are available to trade

Info

Make sure to manually add Native $RUNE as a swappable asset.

Info

"assetPrice" tells you the asset's price in RUNE (RUNE Depth/AssetDepth ). In the above example

1 BNB.BTCB-1DE = 11,205 RUNE

Decimals and Base Units

All values on THORChain (thornode and Midgard) are given in 1e8 eg, 100000000 base units (like Bitcoin), and unless postpended by "USD", they are in units of RUNE. Even 1e18 assets, such as ETH.ETH, are shortened to 1e8. 1e6 Assets like ETH.USDC, are padded to 1e8. THORNode will tell you the decimals for each asset, giving you the opportunity to convert back to native units in your interface.

See code examples using the THORChain xchain package here https://github.com/xchainjs/xchainjs-lib/tree/master/packages/xchain-thorchain

Finding Chain Status

There are two ways to see if a Chain is halted.

  1. Looking at the /inbound_addresses endpoint and inspecting the halted flag.
  2. Looking at Mimir and inspecting the HALT[Chain]TRADING setting. See network-halts.md for more details.

Transaction Memos

Overview

Transactions to THORChain pass user intent with the MEMO field on their respective chains. THORChain inspects the transaction object and the MEMO in order to process the transaction, so care must be taken to ensure the MEMO and the transaction are both valid. If not, THORChain will automatically refund the assets. Memos are set in inbound transactions unless specified.

THORChain uses specific asset notation for all assets. Assets and functions can be abbreviated, and affiliate addresses and asset amounts can be shortened to reduce memo length, including through use of scientific notation. Some parameters can also refer to a THORName instead of an address.

Guides have been created for Swap, Savers and Lending to enable quoting and the automatic construction of memos for simplicity.

All memos are listed in the relevant THORChain source code variable stringToTxTypeMap.

Memo Size Limits

THORChain has a memo size limit of 250 bytes. Any inbound tx sent with a larger memo will be ignored. Additionally, memos on UTXO chains are further constrained by the OP_RETURN size limit, which is 80 bytes.

Dust Thresholds

THORChain has various dust thresholds (dust limits), defined on a per-chain basis. Refer to the THORNode inbound addresses endpoint for details, specifically the dust_threshold field.

Format

All memos follow the format: FUNCTION:PARAM1:PARAM2:PARAM3:PARAM4

The function is invoked by a string, which in turn calls a particular handler in the state machine. The state machine parses the memo looking for the parameters which it simply decodes from human-readable strings.

Some parameters are optional. Simply leave them blank but retain the : separator, e.g., FUNCTION:PARAM1:::PARAM4.

Permitted Functions

The following functions can be put into a memo:

  1. SWAP
  2. DEPOSIT Savers
  3. WITHDRAW Savers
  4. OPEN Loan
  5. REPAY Loan
  6. ADD Liquidity
  7. WITHDRAW Liquidity
  8. ADD Trade Account
  9. WITHDRAW Trade Account
  10. BOND, UNBOND & LEAVE
  11. DONATE & RESERVE
  12. MIGRATE
  13. NOOP

Swap

Perform an asset swap.

SWAP:ASSET:DESTADDR:LIM/INTERVAL/QUANTITY:AFFILIATE:FEE

Info

For the DEX aggregator-oriented variation of the SWAP memo, see Aggregators Memos.

ParameterNotesConditions
PayloadSend the asset to swap.Must be an active pool on THORChain.
SWAPThe swap handler.Also s or =
:ASSETThe asset identifier.Can be shortened.
:DESTADDRThe destination address to send to.Can use THORName.
:LIMThe trade limit, i.e., set 100000000 to get a minimum of 1 full asset, else a refund.Optional. 1e8 or scientific notation.
/INTERVALSwap interval in blocks.Optional. If 0, do not stream.
/QUANTITYSwap Quantity. Swap interval times every Interval blocks.Optional. If 0, network will determine the number of swaps.
:AFFILIATEThe affiliate address.Optional. Must be a THORName or THOR Address.
:FEEThe affiliate fee. RUNE is sent to affiliate.Optional. Ranges from 0 to 1000 Basis Points.

Syntactic Examples:

  • SWAP:ASSET:DESTADDR — simple swap
  • SWAP:ASSET:DESTADDR:LIM — swap with trade limit
  • SWAP:ASSET:DESTADDR:LIM/0/1 — swap with limit, do not stream swap
  • SWAP:ASSET:DESTADDR:LIM/3/0 — swap with limit, optimise swap amount, every 3 blocks
  • SWAP:ASSET:DESTADDR:LIM/1/0:AFFILIATE:FEE — swap with limit, optimised and affiliate fee

Real-world Examples:

  • SWAP:ETH.ETH:0xe6a30f4f3bad978910e2cbb4d97581f5b5a0ade0 — swap to Ether, send output to the specified address
  • SWAP:ETH.ETH:0xe6a30f4f3bad978910e2cbb4d97581f5b5a0ade0:10000000 — same as above except the ETH output should be more than 0.1 ETH else refund
  • SWAP:ETH.ETH:0xe6a30f4f3bad978910e2cbb4d97581f5b5a0ade0:10000000/1/1 — same as above except do not stream the swap
  • SWAP:ETH.ETH:0xe6a30f4f3bad978910e2cbb4d97581f5b5a0ade0:10000000/3/0 — same as above except streaming the swap, every 3 blocks, and THORChain to calculate the number of swaps required to achieve optimal price efficiency
  • SWAP:ETH.ETH:0xe6a30f4f3bad978910e2cbb4d97581f5b5a0ade0:10000000/3/0:t:10 — same as above except sends 10 basis points from the input to affiliate t (THORSwap)
  • s:ETH.ETH:0xe6a30f4f3bad978910e2cbb4d97581f5b5a0ade0:1e6/3/0:t:10 — same as above except with a reduced memo and scientific notation trade limit
  • =:r:thor1el4ufmhll3yw7zxzszvfakrk66j7fx0tvcslym:19779138111 — swap to at least 197.79 RUNE
  • =:BNB/BUSD-BD1:thor15s4apx9ap7lazpsct42nmvf0t6am4r3w0r64f2:628197586176 — swap to at least 6281.9 Synthetic BUSD
  • =:BNB.BNB:bnb108n64knfm38f0mm23nkreqqmpc7rpcw89sqqw5:544e6/2/6 — swap to at least 5.4 BNB, using streaming swaps, 6 swaps, every 2 blocks

Deposit Savers

Deposit an asset into THORChain Savers.

ADD:POOL::AFFILIATE:FEE

ParameterNotesConditions
PayloadThe asset to add liquidity with.Must be supported by THORChain.
ADDThe deposit handler.Also a or +
:POOLThe pool to add liquidity to.Gas and stablecoin pools only.
:Must be empty.Optional. Required if adding affiliate and fee.
:AFFILIATEThe affiliate address.Optional. Must be a THORName or THOR Address.
:FEEThe affiliate fee. RUNE is sent to affiliate.Optional. Ranges from 0 to 1000 Basis Points.

Examples:

  • ADD:ETH/ETH — deposit into the ETH Savings Vault
  • +:BTC/BTC::t:10 — deposit into the BTC Savings Vault, with 10 basis points being sent to affiliate t (THORSwap)
  • a:DOGE/DOGE — deposit into the DOGE Savings Vault

Info

Depositing into Savers can also work without a memo, however memos are recommended to be explicit about the transaction intent.

Withdraw Savers

Withdraw an asset from THORChain Savers.

WITHDRAW:POOL:BASISPOINTS

ParameterNotesExtra
PayloadSend the dust threshold of the asset to cause the transaction to be picked up by THORChain.Dust thresholds must be met.
WITHDRAWThe withdraw handler.Also - or wd
:POOLThe pool to withdraw liquidity from.Gas and stablecoin pools only.
:BASISPOINTSBasis points.Optional. Range 0-10000, where 10000 = 100%.

Examples:

  • WITHDRAW:BTC/BTC:10000 — withdraw 100% from BTC Savers
  • -:ETH/ETH:5000 — withdraw 50% from ETH Savers
  • wd:BTC/BTC:1000 — withdraw 10% from BTC Savers

Info

Withdrawing from Savers can be also be done without a memo.

Open Loan

Open a loan on THORChain.

LOAN+:ASSET:DESTADDR:MINOUT:AFFILIATE:FEE

ParameterNotesConditions
PayloadThe collateral to open the loan with.Must be L1 supported by THORChain.
LOAN+The loan open handler.Also $+
:ASSETTarget debt asset identifier.Can be shortened.
:DESTADDRThe destination address to send the debt to.Can use THORName.
:MINOUTMinimum debt amount, else a refund. Similar to :LIM.Optional. 1e8 format.
:AFFILIATEThe affiliate address.Optional. Must be a THORName or THOR Address.
:FEEThe affiliate fee. RUNE is sent to affiliate.Optional. Ranges from 0 to 1000 Basis Points.

Examples:

  • LOAN+:BNB.BUSD:bnb177kuwn6n9fv83txq04y2tkcsp97s4yclz9k7dh — open a loan with BUSD as the debt asset
  • $+:ETH.USDC-0XA0B86991C6218B36C1D19D4A2E9EB0CE3606EB48:0x1c7b17362c84287bd1184447e6dfeaf920c31bbe:10400000000 — open a loan where the debt is at least 104 USDT

Repay Loan

Repay a loan on THORChain.

LOAN-:ASSET:DESTADDR:MINOUT

ParameterNotesConditions
PayloadThe repayment for the loan.Must be L1 supported on THORChain.
LOAN-The loan repayment handler.Also $-
:ASSETTarget collateral asset identifier.Can be shortened.
:DESTADDRThe destination address to send the collateral to.Can use a THORName.
:MINOUTMinimum collateral to receive, else a refund. Similar to :LIM.Optional. 1e8 format, loan needs to be fully repaid to close.

Examples:

  • LOAN-:BTC.BTC:bc1qp2t4hl4jr6wjfzv28tsdyjysw7p5armf7px55w — repay BTC loan owned by owner bc1qp2t4hl4jr6wjfzv28tsdyjysw7p5armf7px55w
  • $-:ETH.ETH:0xe9973cb51ee04446a54ffca73446d33f133d2f49:404204059 — repay ETH loan owned by 0xe9973cb51ee04446a54ffca73446d33f133d2f49 and receive at least 4.04 ETH collateral back, else refund

Add Liquidity

Add liquidity to a pool.

ADD:POOL:PAIREDADDR:AFFILIATE:FEE

There are rules for adding liquidity, see the rules here.

ParameterNotesConditions
PayloadThe asset to add liquidity with.Must be supported by THORChain.
ADDThe add liquidity handler.Also a or +
:POOLThe pool to add liquidity to.Can be shortened.
:PAIREDADDRThe other address to link with. If on external chain, link to THOR address. If on THORChain, link to external address. If a paired address is found, the LP is matched and added. If none is found, the liquidity is put into pending.Optional. If not specified, a single-sided add-liquidity action is created.
:AFFILIATEThe affiliate address. The affiliate is added to the pool as an LP.Optional. Must be a THORName or THOR Address.
:FEEThe affiliate fee. RUNE is sent to affiliate.Optional. Ranges from 0 to 1000 Basis Points.

Examples:

  • ADD:BTC.BTC — add liquidity single-sided. If this is a position's first add, liquidity can only be withdrawn to the same address
  • a:POOL:PAIREDADDR — add on both sides (dual-sided)
  • +:POOL:PAIREDADDR:AFFILIATE:FEE — add dual-sided with affiliate
  • +:ETH.ETH: — add liquidity with position pending

Withdraw Liquidity

Withdraw liquidity from a pool.

WITHDRAW:POOL:BASISPOINTS:ASSET

A withdrawal can be either dual-sided (withdrawn based on pool's price) or entirely single-sided (converted to one side and sent out).

ParameterNotesExtra
PayloadSend the dust threshold of the asset to cause the transaction to be picked up by THORChain.Dust thresholds must be met.
WITHDRAWThe withdraw liquidity handler.Also - or wd
:POOLThe pool to withdraw liquidity from.Can be shortened.
:BASISPOINTSBasis points.Range 0-10000, where 10000 = 100%.
:ASSETSingle-sided withdraw to one side.Optional. Can be shortened. Must be either RUNE or the ASSET.

Examples:

  • WITHDRAW:POOL:10000 — dual-sided 100% withdraw liquidity. If a single-address position, this withdraws single-sidedly instead
  • -:POOL:1000 — dual-sided 10% withdraw liquidity
  • wd:POOL:5000:ASSET — withdraw 50% liquidity as the asset specified while the rest stays in the pool, e.g., w:BTC.BTC:5000:BTC.BTC

Add Trade Account

TRADE+:ADDR

Adds an L1 asset to the Trade Account.

ParameterNotesExtra
PayloadThe asset to add to the Trade AccountMust be a L1 asset and supported by THORChain.
TRADE+The trade account handler.
ADDRMust be a thor addressSpecifies the owner

Example: TRADE+:thor1x2whgc2nt665y0kc44uywhynazvp0l8tp0vtu6 - Add the sent asset and amount to the Trade Account.

Withdraw Trade Account

Withdraws an L1 asset from the Trade Account.

TRADE-:ADDR

ParameterNotesExtra
PayloadThe Trade Asset to be withdrawn and amountUse MsgDeposit.
TRADE-The trade account handler.
ADDRL1 address to which the withdrawal will be sentCannot be a thor address

Note: Trade Asset and Amount are determined by the coins within the MsgDeposit. Transaction fee in RUNE does apply.

Examples:

  • TRADE-:0x3021c479f7f8c9f1d5c7d8523ba5e22c0bcb5430- Withdraw 1 AVAX from the Trade Account and sent to 0x3021c479f7f8c9f1d5c7d8523ba5e22c0bcb5430

    {"body":{"messages":[{"":"/types.MsgDeposit","coins":[{"asset":"AVAX~AVAX","amount":"100000000","decimals":"0"}],"memo":"trade-:0x3021c479f7f8c9f1d5c7d8523ba5e22c0bcb5430","signer":"thor19phfqh3ce3nnjhh0cssn433nydq9shx7wfmk7k"}],"memo":"","timeout_height":"0","extension_options":[],"non_critical_extension_options":[]},"auth_info":{"signer_infos":[],"fee":{"amount":[],"gas_limit":"200000","payer":"","granter":""}},"signatures":[]}
    
  • TRADE-:bc1qp8278yutn09r2wu3jrc8xg2a7hgdgwv2gvsdyw - Withdraw 0.1 BTC from the Trade Account and sent to bc1qp8278yutn09r2wu3jrc8xg2a7hgdgwv2gvsdyw

    {"body":{"messages":[{"":"/types.MsgDeposit","coins":[{"asset":"BTC~BTC","amount":"10000000","decimals":"0"}],"memo":"trade-:bc1qp8278yutn09r2wu3jrc8xg2a7hgdgwv2gvsdyw","signer":"thor19phfqh3ce3nnjhh0cssn433nydq9shx7wfmk7k"}],"memo":"","timeout_height":"0","extension_options":[],"non_critical_extension_options":[]},"auth_info":{"signer_infos":[],"fee":{"amount":[],"gas_limit":"200000","payer":"","granter":""}},"signatures":[]}
    

Donate to a pool.

DONATE:POOL

ParameterNotesExtra
PayloadThe asset to donate to a THORChain pool.Must be supported by THORChain. Can be RUNE or ASSET.
DONATEThe donate handler.Also d
:POOLThe pool to withdraw liquidity from.Can be shortened.

Examples:

  • DONATE:ETH.ETH — donate to the ETH pool

RESERVE

Donate to the THORChain Reserve.

ParameterNotesExtra
PayloadTHOR.RUNEThe RUNE to credit to the THORChain Reserve.
RESERVEThe reserve handler.

BOND, UNBOND and LEAVE

Perform node maintenance features. Also see Pooled Nodes.

BOND:NODEADDR:PROVIDER:FEE

ParameterNotesExtra
PayloadTHOR.RUNEThe asset to bond to a Node.
BONDThe bond handler.
:NODEADDRThe node to bond with.
:PROVIDERWhitelist in a provider.Optional. Add a provider.
:FEESpecify an Operator Fee in Basis Points.Optional. Default will be the mimir value (2000 Basis Points). Can be changed anytime.

UNBOND:NODEADDR:AMOUNT:PROVIDER

ParameterNotesExtra
PayloadNone required.Use MsgDeposit.
UNBONDThe unbond handler.
:NODEADDRThe node to unbond from.Must be in standby only.
:AMOUNTThe amount to unbond.In 1e8 format. If setting more than actual bond, then capped at bond.
:PROVIDERUnwhitelist a provider.Optional. Remove a provider.

LEAVE:NODEADDR

ParameterNotesExtra
PayloadNone required.Use MsgDeposit.
LEAVEThe leave handler.
:NODEADDRThe node to force to leave.If in Active, request a churn out to Standby for 1 churn cycle. If in Standby, forces a permanent leave.

Examples:

  • BOND:thor19m4kqulyqvya339jfja84h6qp8tkjgxuxa4n4a
  • UNBOND:thor1x2whgc2nt665y0kc44uywhynazvp0l8tp0vtu6:750000000000
  • LEAVE:thor1hlhdm0ngr2j4lt8tt8wuvqxz6aus58j57nxnps

MIGRATE

Internal memo type used to mark migration transactions between a retiring vault and a new Asgard vault during churn. Special THORChain triggered outbound tx without a related inbound tx.

MIGRATE:BLOCKHEIGHT

ParameterNotesExtra
PayloadAssets migrating.
MIGRATEThe migrate handler.
:BLOCKHEIGHTTHORChain block height to migrate.Must be a valid block height.

Example:

NOOP

Dev-centric functions used to fix THORChain state.

Danger

May cause loss of funds if not performed correctly and at the right time.

NOOP:NOVAULT

ParameterNotesExtra
PayloadThe asset to credit to a vault.Must be ASSET or RUNE.
NOOPThe no-op handler.Adds to the vault balance, but does not add to the pool.
:NOVAULTDo not credit the vault.Optional. Just fix the insolvency issue.

Refunds

The following are the conditions for refunds:

ConditionNotes
Invalid MEMOIf the MEMO is incorrect the user will be refunded.
Invalid AssetsIf the asset for the transaction is incorrect (adding an asset into a wrong pool) the user will be refunded.
Invalid Transaction TypeIf the user is performing a multi-send vs a send for a particular transaction, they are refunded.
Exceeding Price LimitIf the final value achieved in a trade differs to expected, they are refunded.

Refunds cost fees to prevent DoS (denial-of-service) attacks. The user will pay the correct outbound fee for that chain. Refund memo is sent within a outbound transaction.

Other Internal Memos

  • consolidate — consolidate UTXO transactions
  • limito or lo — limit order functions (to be implemented)
  • name or n or ~ — THORName operations; see THORName Guide
  • out — for outbound transaction, set within a outbound transaction
  • ragnarok — used to delist pools, set within a outbound transaction
  • switchkillswitch operations (deprecated)
  • yggdrasil+ and yggdrasil- — Yggdrasil vault operations (deprecated; see ADR002)

Asset Notation

THORChain uses a CHAIN.ASSET notation for all assets. TICKER and ID are added where required. The asset full notation is pictured.

L1 Asset Notation

There are three kinds of assets within THORChain:

  1. Layer 1 Assets - CHAIN.ASSET
  2. Synthetic Assets - CHAIN/Asset
  3. Trade Assets - CHAIN~ASSET
  4. Derived Assets - THOR.ASSET

Examples

AssetNotation
BitcoinBTC.BTC (Native BTC)
BitcoinBTC/BTC (Synthetic BTC)
BitcoinTHOR.BTC (Derived BTC)
BitcoinBTC~BTC (Trade BTC)
EthereumETH.ETH
USDTETH.USDT-0xdac17f958d2ee523a2206206994597c13d831ec7
BNBBNB.BNB (Native)
BNBBNB/BNB (Synth)
RUNE (BEP2)BNB.RUNE-B1A
RUNE (NATIVE)THOR.RUNE

Layer 1 Assets

  • Layer 1 (L1) chains are always denoted as CHAIN.ASSET, e.g. BTC.BTC.
  • As two tokens can live on different blockchains, the chain can be used to distinguish them. Example: USDC is on the Ethereum Chain and Avalanche Chain and is denoted as ETH.USDC and AVAX.USDC respectively; note the contract address (ticker) was removed for simplicity.
  • Tickers are added to denote assets and are required in the full name. For EVM based Chains, the ticker is the ERC20 Contract address, e.g. ETH.USDC-0XA0B86991C6218B36C1D19D4A2E9EB0CE3606EB48. This ensures the asset is specific to the contract address. The pools listshows assets using the full notation.
  • IDs are required for the Binance Beacon chain as BNB.RUNE is not sufficient to distinguish between Mainnet and Testnet assets. For example, BNB.RUNE-B1A denoted Mainnet RUNE and BNB.RUNE-67C denoted Testnet RUNE.

Danger

THOR.RUNE is the only RUNE asset in use. All other RUNE assets on other chains are no longer in use and have no value within THORChain.

Synthetic Assets

  • Synthetic Assets are denoted as CHAIN/ASSET instead of CHAIN.ASSET, e.g. Synthetic BTC is BTC/BTC and Synthetic USDT is ETH/USDT. While Synthetic assets live on the THORChain blockchain, they retain their CHAIN identifier.
  • Synthetic Assets can only be created from THORChain-supported L1 assets and are only denoted as CHAIN/ASSET, no ticker or ID is required.
  • Chain differentiation is also used for Synthetics, e.g. ETH/USDC and AVAX/USDC are different Synthetic assets created and redeemable on different chains.

Trade Assets

  • Trade Assets are held within a Trade Account and are created from Layer1 assets, denoted as CHAIN~ASSET. The Bitcoin trading asset is BTC~BTC.
  • Unlike synths which are minted as coins in the Cosmos bank module, trade asset balances are recorded in a custom keeper and cannot be transfer from one native address to another.

Derived Assets

  • Derived Assets, currently specific to Lending, are denoted as THOR.ASSET. E.g. THOR.BTC is Derived Bitcoin.
  • All Derived Assets live on the THORChain blockchain and do not have a Chain identifier.
  • Currently, Derived Assets are used internally within THORChain only.

Memo Length Reduction

Reducing Memo Size

Given the complexity of memos, they can become very long, beyond the limits of chains like Bitcoin. Various methods have been developed to significantly shorten memo length.

Example:

  1. SWAP:ETH.ETH:0xe6a30f4f3bad978910e2cbb4d97581f5b5a0ade0:1612345678:thor1el4ufmhll3yw7zxzszvfakrk66j7fx0tvcslym:10

    can be reduced to =:e:dx:161e6:t:10

  2. SWAP:ETH.USDT-0xdac17f958d2ee523a2206206994597c13d831ec7:0xe6a30f4f3bad978910e2cbb4d97581f5b5a0ade0:10012345678:thor1el4ufmhll3yw7zxzszvfakrk66j7fx0tvcslym:10

    can be reduced to =:ETH.USDT:dx:100e7:t:10

The examples below use the following features to reduce memo length:

  1. Shortened Asset Names
  2. THORNames
  3. Shortened Function
  4. Asset Abbreviations
  5. Scientific Notation

Shortened Asset Names

Native asset names can be shortened to reduce the length of the memo. The exact list is here.

Shorten AssetAsset Notation
rTHOR.RUNE
aAVAX.AVAX
bBTC.BTC
cBCH.BCH
eETH.ETH
gGAIA.ATOM
nBNB.BNB
sBSC.BNB
dDOGE.DOGE
eETH.ETH
lLTC.LTC

Example Swaps:

  • =:ETH.ETH:0x388C818CA8B9251b393131C08a736A67ccB19297 is reduced to =:e:0x388C818CA8B9251b393131C08a736A67ccB19297, Swap for Ether.
  • =:r:thor1el4ufmhll3yw7zxzszvfakrk66j7fx0tvcslym - Swap to RUNE.

Asset Abbreviations

Assets can be abbreviated using fuzzy logic. The following will all be matched appropriately. If there are conflicts, then the deepest pool is matched to prevent attacks.

Notation
ETH.USDT
ETH.USDT-ec7
ETH.USDT-6994597c13d831ec7
ETH.USDT-0xdac17f958d2ee523a2206206994597c13d831ec7

THORNames

THORNames allows a custom name to be assigned to an address, like an alias, so the address does not need to be specified.

Example:

  • thor1nt2d4kmj0xd6xxm3m82tac3d20y05dm0vv7ur3 can be specified as tr.

See the THORName Creation Guide to create your own. This is used greatly to specify the affiliate address.

Shortened Functions

Memos contain functions such as Swap or Add, which describe the user's intent and are sent along with specific parameters. Functions can be reduced in the following way:

FunctionAbbreviatedRecommended
Swaps=
Add / Deposit+
Withdrawwd-
Loan OpenLoan+$+
Loan CloseLoan-$-
THORNamename, n~
Limit OrderLimittolo

Example:

=SWAP:e:0x388C818CA8B9251b393131C08a736A67ccB19297 is reduced to =:e:0x388C818CA8B9251b393131C08a736A67ccB19297

Scientific Notation

In THORChain memos and the state machine, asset amounts are expressed as Base in 1e8 format requiring many digits to express an amount. E.g. 0.01 BTC is expressed as 1000000 and 5 Ether is expressed as 500000000.

To help save space in memos, scientific notation can shorten memos by specifying both significant digits and the amount of trailing zeros. Note that using scientific notation in memos always leads to a loss of precision, so ensure enough significant digits are used to express the amount properly. For example, using 161e6 to represent 1612345678 results in a loss of precision.

Examples:

  • In memo: 1e8 -> THORChain reads: 100000000
  • In memo: 51e7 -> THORChain reads: 510000000

Full Memo Example:

  • SWAP:ETH.ETH:0x388C818CA8B9251b393131C08a736A67ccB19297:1612341234:thor19emplkuphjk2y9gkkv06m8vcstc0ufn4pevv5u:10

    is reduced to =:e:0x388C818CA8B9251b393131C08a736A67ccB19297:161e6:t:10

Swapper Clout

Swapper Clout allows traders to have immediate and faster swaps while maintaining the security features of delayed outbounds.

Clout is established by fees paid (in RUNE) for swaps/trades. The more fees paid, the higher the clout. Swappers with a high clout have proven themselves to be highly aligned to the project and therefore can reap the rewards by getting faster trade execution. The higher the clout, the less their outbound transactions are delayed or removed entirely if their score is high enough.

By reducing traders's outbound delay time, Swapper clout reduces the outbound queue allowing for a better UX for normal users.

Implementation Detail

For each swap, the fees paid (in RUNE) are divided by 2, and associated with the sender and recipient addresses. This number only increases. When/if the feature is implemented, devs will do a historical chain analysis and create every address's initial clout score, based on historical trades.

When the outbound delay is calculated, the clout score is subtracted from the outbound value (in RUNE), causing the delay amount to be reduced (maybe even eliminated). If there is already a scheduled outbound with the same address, the value of the clout applied reduces (or removes) the clout applied to this outbound txn (increasing delay). This is to ensure that clout is collectively applied to all current outbounds, not on a one-by-one basis. This is to ensure that an individual cannot have a clout score of 100 RUNE, and make infinite zero-delay swaps of 100 RUNE value simultaneously.

To calculate delay:

  • ov: Outbound transaction value in RUNE
  • c: Current address clout score
  • cu: Current Clout Utilisation

This feature rewards power users and arbitrage bots to facilitate a better trading experience, potentially enabling near-instant trade execution. Specifically, it aids arbitrage bots in operating more efficiently and managing pool prices effectively. Since the majority of trade volume originates from arbitrage bots and power users, their outbound transactions do not contribute to the delay experienced by infrequent organic traders. This means that even a one-time trade from a known address will experience significantly reduced delays. However, delays will persist for individuals conducting large swaps from an unknown address.

More information in the original MR

Trade Accounts

More capital-efficient trading and arbitration than Synthetics

Trade accounts provide professional traders (mostly arbitrage bots) a method to execute instant trades on THORChain without involving Layer1 transactions on external blockchains. Trade Accounts creates a new type of asset, backed by the network security rather than the liquidity in a pool (Synthetics), or by the RUNE asset (Derived Assets).

Arbitrage bots can arbitrage the pools faster and with more capital efficiency than Synthetics can. This is because Synthetics adds or removes from one side of the pool depth but not the other, causing the pool to move only half the distance in terms of price. For example a $100 RUNE --> BTC swap requires $200 of Synthetic BTC to be burned to correct the price. Trade accounts have twice the efficiency, so $100 RUNE --> BTC swap would require $100 from trade accounts to correct the price. This allows arbitrageurs to restore big deviations quicker using less capital.

How it Works

  1. Traders deposit Layer1 assets into the network, minting a Trade Asset in a 1:1 ratio within a Network Trade module held by the network, not the user's wallet. This is held outside of the Liquidity Pools.
  2. Trader receives accredited shares of this module relative to their deposit versus module depth. This is done using the same logic as savers.
  3. Trader can swap/trade assets <> RUNE (or other trade asset) to and from the trade module. Because this occurs completely within THORNode, execution times are fast and efficient. Swap fees are the same as any other Layer1 swap.
  4. Trader can withdraw some or all of their balance from their Trade Account. Outbound delay applies when they withdraw.

RUNE and Synthetics cannot be added to the Trade Account.

Security

As assets within the trade account are not held in the pools, the combined pool and trade account value (combined Layer1 asset value) could exceed the total bonded. To ensure this does not occur:

  1. The calculation of the Incentive Pendulum now operates based on Layer1 assets versus bonds, rather than solely on pool depths versus bonds. This ensures there is always "space" for arbitrageurs to exist in the network and be able to arbitrage pools effectively (versus synths hitting caps).
  2. If the combined Layer1 asset value exceeds the total bonded value, trade assets are sold/liquidated (reducing liability) to buy RUNE and are deposited into the bond module (increasing security). In this scenario a Trade Account may be subject to a negative interest rate. This safeguard effectively redistributes liquidity from all Trade Account holders to Active Node Operators and only occurs if the Incentive Pendulum reaches a fully underbonded state.

Warning

Trade Accounts are yet to be activated.

Network Halts

Warning

If the network is halted, do not send funds. The easiest check to do is if halted = true on the inbound addresses endpoint.

Info

In most cases funds won't be lost if they are sent when halted, but they may be significantly delayed.

Danger

In the worse case if THORChain suffers a consensus halt the inbound_addresses endpoint will freeze with halted = false but the network is actually hard-halted. In this case running a fullnode is beneficial, because the last block will become stale after 6 seconds and interfaces can detect this.

Interfaces that provide LP management can provide more feedback to the user what specifically is paused.

There are levels of granularity the network has to control itself and chains in the event of issues. Interfaces need to monitor these settings and apply appropriate controls in their interfaces, inform users and prevent unsupported actions.

All activity is controlled within Mimir and needs to be observed by interfaces and acted upon. Also, see a description of Constants and Mimir.

Halt flags are Boolean. For clarity 0 = false, no issues and > 0 = true (usually 1), halt in effect.

Halt/ Pause Management

Each chain has granular control allowing each chain to be halted or resumed on a specific chain as required. Network-level halting is also possible.

  1. Specific Chain Signing Halt - Allows inbound transactions but stops the signing of outbound transactions. Outbound transactions are queued. This is the least impactful halt.
    1. Mimir setting is HALTSIGNING[Chain], e.g. HALTSIGNINGBNB
  2. Specific Chain Liquidity Provider Pause - addition and withdrawal of liquidity are suspended but swaps and other transactions are processed.
    1. Mimir setting is PAUSELP[Chain], e,g, PAUSELPBCH for BCH
  3. Specific Chain Trading Halt - Transactions on external chains are observed but not processed, only refunds are given. THORNode's Bifrost is running, nodes are synced to the tip therefore trading resumption can happen very quickly.
    1. Mimir setting is HALT[Chain]TRADING, e,g, HALTBCHTRADING for BCH
  4. Specific Chain Halt - Serious halt where transitions on that chain are no longer observed and THORNodes will not be synced to the chain tip, usually their Bifrost offline. Resumption will require a majority of nodes syncing to the tip before trading can commence.
    1. Mimir setting is HALT[Chain]CHAIN, e,g, HALTBCHCHAIN for BCH.

Warning

Chain specific halts do occur and need to be monitored and reacted to when they occur. Users should not be able to send transactions via an interface when a halt is in effect.

Network Level Halts

  • Network Pause LP PAUSELP = 1 Addition and withdrawal of liquidity are suspended for all pools but swaps and other transactions are processed.
  • Network Pause Lending PAUSELOANS = 1 Opening and closing of loans is paused for all loans.
  • Network Trading Halt HALTTRADING = 1 Will stop all trading for every connected chain. The THORChain blockchain will continue and native RUNE transactions will be processed.

There is no Network level chain halt setting as the THORChain Blockchain continually needs to produce blocks.

A chain halt is possible in which case Mimir or Midgard will not return data. This can happen if the chain suffers consensus failure or more than 1/3 of nodes are switched off. If this occurs the Dev Discord Server #interface-alerts will issue alerts.

Warning

While very rare, a network level halt is possible and should be monitored for.

Synth Management

Synths minting and redeeming can be enabled and disabled using flags. There is also a Synth mint limit. The setting are:

  • MINTSYNTHS - controls whether synths can be minted (swapping from L1 to synth)
  • BURNSYNTHS controls whether synths can be burned (swapping from synth to L1)
  • MAXSYNTHPERPOOLDEPTH - controls the synth depth limit for each pool, expressed in basis points of the total pool depth (asset + RUNE). For example: 5000 basis points equals 50% of the total pool. If the pool contains 100 BTC and 100 BTC worth of RUNE, a 50% MAXSYNTHPERPOOLDEPTH allows 100 BTC of synthetic assets to be minted.

Lending Management

Lending Pause PAUSELOANS = 1 - Opening and closing of loans are suspended

Trade Accounts

Trade Accounts Pause TradeAccountsEnabled = 1 - Adding to and withdrawing from the Trade Account is enabled.

See also Constants and Mimir.

Fees

Overview

There are 4 different fees the user should know about.

  1. Inbound Fee (sourceChain: gasRate * txSize)
  2. Affiliate Fee (affiliateFee * swapAmount)
  3. Liquidity Fee (swapSlip * swapAmount)
  4. Outbound Fee (destinationChain: gasRate * txSize)

Terms

  • SourceChain: the chain the user is swapping from
  • DestinationChain: the chain the user is swapping to txSize: the size of the transaction in bytes (or units)
  • gasRate: the current gas rate of the external network
  • swapAmount: the amount the user is swapping swapSlip: the slip created by the
  • swapAmount, as a function of poolDepth
  • affiliateFee: optional fee set by interface in basis points

Fees Detail

Inbound Fee

This is the fee the user pays to make a transaction on the source chain, which the user pays directly themselves. The gas rate recommended to use is fast where the tx is guaranteed to be committed in the next block. Any longer and the user will be waiting a long time for their swap and their price will be invalid (thus they may get an unnecessary refund).

Success

THORChain calculates and posts fee rates at https://thornode.ninerealms.com/thorchain/inbound_addresses

Warning

Always use a "fast" or "fastest" fee, if the transaction is not confirmed in time, it could be abandoned by the network or failed due to old prices. You should allow your users to cancel or re-try with higher fees.

Liquidity Fee

This is simply the slip created by the transaction multiplied by its amount. It is priced and deducted from the destination amount automatically by the protocol.

Affiliate Fee

In the swap transaction you build for your users you can include an affiliate fee for your exchange (accepted in $RUNE or a synthetic asset, so you will need a $RUNE address).

  • The affiliate fee is in basis points (0-10,000) and will be deducted from the inbound swap amount from the user.
  • If the inbound swap asset is a native THORChain asset ($RUNE or synth) the affiliate fee amount will be deducted directly from the transaction amount.
  • If the inbound swap asset is on any other chain the network will submit a swap to $RUNE with the destination address as your affiliate fee address.
  • If the affiliate is added to an ADDLP tx, then the affiliate is included in the network as an LP.

SWAP:CHAIN.ASSET:DESTINATION:LIMIT:AFFILIATE:FEE

Read https://medium.com/thorchain/affiliate-fees-on-thorchain-17cbc176a11b for more information.

Preferred Asset for Affiliate Fees

Affiliates can collect their fees in the asset of their choice (choosing from the assets that have a pool on THORChain). In order to collect fees in a preferred asset, affiliates must use a THORName in their swap memos.

How it Works

If an affiliate's THORName has the proper preferred asset configuration set, the network will begin collecting their affiliate fees in $RUNE in the AffiliateCollector module. Once the accrued RUNE in the module is greater than PreferredAssetOutboundFeeMultiplier* outbound_fee of the preferred asset's chain, the network initiates a swap from $RUNE -> Preferred Asset on behalf of the affiliate. At the time of writing, PreferredAssetOutboundFeeMultiplier is set to 100, so the preferred asset swap happens when the outbound fee is 1% of the accrued $RUNE.

Configuring a Preferred Asset for a THORName.

  1. Register a THORName if not done already. This is done with a MsgDeposit posted to the THORChain network.
  2. Set your preferred asset's chain alias (the address you'll be paid out to), and your preferred asset. Note: your preferred asset must be currently supported by THORChain.

For example, if you wanted to be paid out in USDC you would:

  1. Grab the full USDC name from the Pools endpoint: ETH.USDC-0XA0B86991C6218B36C1D19D4A2E9EB0CE3606EB48

  2. Post a MsgDeposit to the THORChain network with the appropriate memo to register your THORName, set your preferred asset as USDC, and set your Ethereum network address alias. Assuming the following info:

    1. THORChain address: thor1dl7un46w7l7f3ewrnrm6nq58nerjtp0dradjtd
    2. THORName: ac-test
    3. ETH payout address: 0x6621d872f17109d6601c49edba526ebcfd332d5d

    The full memo would look like:

    ~:ac-test:ETH:0x6621d872f17109d6601c49edba526ebcfd332d5d:thor1dl7un46w7l7f3ewrnrm6nq58nerjtp0dradjtd:ETH.USDC-0XA0B86991C6218B36C1D19D4A2E9EB0CE3606EB48

Info

You can use Asgardex to post a MsgDeposit with a custom memo. Load your wallet, then open your THORChain wallet page > Deposit > Custom.

Info

You will also need a THOR alias set to collect affiliate fees. Use another MsgDeposit with memo: ~:<thorname>:THOR:<thorchain-address> to set your THOR alias. Your THOR alias address can be the same as your owner address, but won't be used for anything if a preferred asset is set.

Once you successfully post your MsgDeposit you can verify that your THORName is configured properly. View your THORName info from THORNode at the following endpoint:
https://thornode.ninerealms.com/thorchain/thorname/ac-test

The response should look like:

{
  "affiliate_collector_rune": "0",
  "aliases": [
    {
      "address": "0x6621d872f17109d6601c49edba526ebcfd332d5d",
      "chain": "ETH"
    },
    {
      "address": "thor1dl7un46w7l7f3ewrnrm6nq58nerjtp0dradjtd",
      "chain": "THOR"
    }
  ],
  "expire_block_height": 22061405,
  "name": "ac-test",
  "owner": "thor1dl7un46w7l7f3ewrnrm6nq58nerjtp0dradjtd",
  "preferred_asset": "ETH.USDC-0XA0B86991C6218B36C1D19D4A2E9EB0CE3606EB48"
}

Your THORName is now properly configured and any affiliate fees will begin accruing in the AffiliateCollector module. You can verify that fees are being collected by checking the affiliate_collector_rune value of the above endpoint.

Outbound Fee

This is the fee the Network pays on behalf of the user to send the outbound transaction. To adequately pay for network resources (TSS, compute, state storage) the fee is marked up from what nodes actually pay on-chain by an "Outbound Fee Multiplier" (OFM).

The OFM moves between a MaxOutboundFeeMultiplier and a MinOutboundFeeMultiplier(defined as Network Constants or as Mimir Values), based on the network's current outbound fee "surplus" in relation to a "target surplus". The outbound fee "surplus" is the cumulative difference (in $RUNE) between what the users are charged for outbound fees and what the nodes actually pay. As the network books a "surplus" the OFM slowly decreases from the Max to the Min. Current values for the OFM can be found on the Network Endpoint.

The minimum Outbound Layer1 Fee the network will charge is on /thorchain/mimir and is priced in USD (based on THORChain's USD pool prices). This means really cheap chains still pay their fair share. It is currently set to 100000000 = $1.00

See Outbound Fee for more information.

Fee Ordering for Swaps

Fees are taken in the following order when conducting a swap.

  1. Inbound Fee (user wallet controlled, not THORChain controlled)
  2. Affiliate Fee (if any) - skimmed from the input.
  3. Swap Fee (denoted in output asset)
  4. Outbound Fee (taken from the swap output)

To work out the total fees, fees should be converted to a common asset (e.g. RUNE or USD) then added up. Total fees should be less than the input else it is likely to result in a refund.

Refunds and Minimum Swappable Amount

If a transaction fails, it is refunded, thus it will pay the outboundFee for the SourceChain not the DestinationChain. Thus devs should always swap an amount that is a maximum of the following, multiplier by a buffer of at least 4x to allow for sudden gas spikes:

  1. The Destination Chain outbound_fee
  2. The Source Chain outbound_fee
  3. $1.00 (the minimum)

The outbound_fee for each chain is returned on the Inbound Addresses endpoint, priced in the gas asset.

It is strongly recommended to use the recommended_min_amount_in value that is included on the Swap Quote endpoint, which is the calculation described above. This value is priced in the inbound asset of the quote request (in 1e8). This should be the minimum-allowed swap amount for the requested quote.

Remember, if the swap limit is not met or the swap is otherwise refunded the outbound_fee of the Source Chain will be deducted from the input amount, so give your users enough room.

Understanding gas_rate

THORNode keeps track of current gas prices. Access these at the /inbound_addresses endpoint of the THORNode API. The response is an array of objects like this:

{
    "chain": "ETH",
    "pub_key": "thorpub1addwnpepqdlx0avvuax3x9skwcpvmvsvhdtnw6hr5a0398vkcvn9nk2ytpdx5cpp70n",
    "address": "0x74ce1c3556a6d864de82575b36c3d1fb9c303a80",
    "router": "0x3624525075b88B24ecc29CE226b0CEc1fFcB6976",
    "halted": false,
    "gas_rate": "10"
    "gas_rate_units": "satsperbyte",
    "outbound_fee": "30000",
    "outbound_tx_size": "1000",
}

The gas_rate property can be used to estimate network fees for each chain the swap interacts with. For example, if the swap is BTC -> ETH the swap will incur fees on the bitcoin network and Ethereum network. The gas_rate property works differently on each chain "type" (e.g. EVM, UTXO, BFT).

The gas_rate_units explain what the rate is for chain, as a prompt to the developer.

The outbound_tx_size is what THORChain internally budgets as a typical transaction size for each chain.

The outbound_fee is gas_rate * outbound_tx_size * OFM and developers can use this to budget for the fee to be charged to the user. The current Outbound Fee Multiplier (OFM) can be found on the Network Endpoint.

Keep in mind the outbound_fee is priced in the gas asset of each chain. For chains with tokens, be sure to convert the outbound_fee to the outbound token to determine how much will be taken from the outbound amount. To do this, use the getValueOfAsset1InAsset2 formula described in the Math section.

Fee Calculation by Chain

THORChain (Native Rune)

The THORChain blockchain has a set 0.02 RUNE fee. This is set within the THORChain Constants by NativeTransactionFee. As THORChain is 1e8, 2000000 TOR = 0.02 RUNE

Binance Chain

THORChain uses the gas_rate as the flat Binance Chain transaction fee.

E.g. If the gas_rate = 11250 then fee is 0.0011250 BNB.

UTXO Chains like Bitcoin

For UXTO chains link Bitcoin, gas_rateis denoted in Satoshis. The gas_rate is calculated by looking at the average previous block fee seen by the THORNodes.

All THORChain transactions use BECH32 so a standard tx size of 250 bytes can be used. The standard UTXO fee is then gas_rate* 250.

EVM Chains like Ethereum

For EVM chains like Ethereum, gas_rateis denoted in GWEI. The gas_rate is calculated by looking at the average previous block fee seen by the THORNodes

An Ether Tx fee is: gasRate * 10^9 (GWEI) * 21000 (units).

An ERC20 Tx is larger: gasRate * 10^9 (GWEI) * 70000 (units)

Success

THORChain calculates and posts gas fee rates at https://thornode.ninerealms.com/thorchain/inbound_addresses

Delays

Overview

There are four phases of a transaction sent to THORChain.

  1. Inbound Confirmation
  2. Observation Counting
  3. Confirmation Counting
  4. Outbound Delay
  5. Outbound Confirmation

Wait times can be between a few seconds to several hours. The assets being swapped, the size of the swap and the current network traffic within THORChain will determine the wait time.

Inbound Confirmation

This depends purely on the host chain and is out of the control of THORChain.

  • Bitcoin/BitcoinCash: ~10 minutes
  • Litecoin: ~2.5 minutes
  • Dogecoin: ~60 seconds
  • ETH: ~15 seconds
  • Cosmos: ~6 seconds

Observation Counting

THORNodes have to witness to THORChain when they see a transaction. It could seconds to minutes depending on how fast nodes can scan their blockchains to find transactions. Once 67% of THORNodes see a tx, then it can be confirmed. You can count the number of nodes that have seen a tx by counting the signatures in the signers parameter or look at the status field on the /tx endpoint.

Example: https://thornode.ninerealms.com/thorchain/tx/0AAA205438B6409CBA11DED8C8F63794D719CF4E3818B85117259311E3ADEA0E

Confirmation Counting

THORChain has to defend against 51% attacks, which it does by counting to economic finality for each block (the value of the block relative to the value of the block reward). It tracks both, then computes the number of blocks to wait. It then populates this on the /tx endpoint.

Example: https://thornode.ninerealms.com/thorchain/tx/0AAA205438B6409CBA11DED8C8F63794D719CF4E3818B85117259311E3ADEA0E

block_height is the external height it first saw it.

finalise_height is the external height it needs to see before it will confirm it.

Warning

An event is not sent until the external block height crosses finalise_height so Midgard will NOT see the tx until confirmation-counted.

Examples:

  • 10 BTC: 2 blocks
  • 50 ETH: 16 blocks
  • 100 LTC: 9 blocks

Outbound Delay

THORChain throttles all outputs to prevent fund loss attacks. The maximum delay is 720 blocks which is approx 1 hour. Outbound delay worked out by computing the value of the outbound transaction in RUNE then applying an artificial delay. If the tx is in "scheduled", it will be delayed by a number of blocks. Once it is "outbound" it is being processed. See more information here.

Info

Arbs and Traders who have trade history can have a reduced wait time to do Swapper Clout.

Queue:

https://thornode.ninerealms.com/thorchain/queue

Delayed txOuts:

https://thornode.ninerealms.com/thorchain/queue/scheduled

Finalised txOuts:

https://thornode.ninerealms.com/thorchain/queue/outbound

Swap Clout:

Clout is the cumulative total fees paid (in RUNE) for a given address. Swappers with a high clout have proven themselves to be highly aligned to the project and therefore can reap the rewards by getting faster trade execution. Clout is deducted from the RUNE value of an outbound before a delay relative to the outbound value is applied. Clout is removed from an address when an outbound using it is scheduled, and the clout is reclaimed when the outbound achieves observation consensus.

Outbound Confirmation

This depends purely on the host chain and is out of the control of THORChain.

  • Bitcoin/BitcoinCash: ~10 minutes
  • Litecoin: ~2.5 minutes
  • Dogecoin: ~60 seconds
  • ETH: ~15 seconds
  • Cosmos: ~6 seconds

How to Handle Delays

Follow these guidelines

  1. Use the Quote endpoint to get the estimated fee.
  2. Don't leave the user with a swap screen spinner, instead, move the swap to a "pending state" with a 10minute countdown. Let the user exit the app, perhaps even send them a notification after.
  3. Every minute, poll Midgard and see if the swap is processed.
  4. Once processed, you can inform the user, perhaps surprise them if the swap is done faster

Sending Transactions

Confirm you have:

  • Connected to Midgard or THORNode
  • Located the latest vault (and router) for the chain
  • Prepared the transaction details (and memo)
  • Checked the network is not halted for your transaction

You are ready to make the transaction and swap via THORChain.

UTXO Chains

⚠️ THORChain does NOT currently support BTC Taproot. User funds will be lost if sent to or from a taproot address!

  • Ensure the address type is supported
  • Send the transaction with Asgard vault as VOUT0
  • Pass all change back to the VIN0 address in a subsequent VOUT e.g. VOUT1
  • Include the memo as an OP_RETURN in a subsequent VOUT e.g. VOUT2
  • Use a high enough gas_rate to be included
  • Do not send below the dust threshold (10k Sats BTC, BCH, LTC, 1m DOGE), exhaustive values can be found on the Inbound Addresses endpoint
  • Do not send funds that are part of a transaction with more than 10 outputs

Warning

Inbound transactions should not be delayed for any reason else there is risk funds will be sent to an unreachable address. Use standard transactions, check the Inbound_Address before sending and use the recommended gas rate to ensure transactions are confirmed in the next block to the latest Inbound_Address.

Info

Memo limited to 80 bytes on BTC, BCH, LTC and DOGE. Use abbreviated options and THORNames where possible.

Warning

Do not use HD wallets that forward the change to a new address, because THORChain IDs the user as the address in VIN0. The user must keep their VIN0 address funded for refunds.

Danger

Override randomised VOUT ordering; THORChain requires specific output ordering. Funds using wrong ordering are very likely to be lost.

EVM Chains

https://gitlab.com/thorchain/ethereum/eth-router/-/blob/master/contracts/THORChain_Router.sol#L66

depositWithExpiry(vault, asset, amount, memo, expiry)
  • If ERC20, approve the router to spend an allowance of the token first
  • Send the transaction as a depositWithExpiry() on the router
  • Vault is the Asgard vault address, asset is the token address to swap, memo as a string
  • Use an expiry which is +60mins on the current time (if the tx is delayed, it will get refunded). The timestamp is in seconds (Solidity's block.timestamp).
  • Use a high enough gas_rate to be included, otherwise the tx will get stuck

Info

ETH is 0x0000000000000000000000000000000000000000

Danger

ETH is sent and received as an internal transaction. Your wallet may not be set to read internal balances and transactions.

Danger

Do not send assets from a smart contract (including smart contract wallets) without adding your contract to the whitelist. As a security measure, Thorchain ignores transactions coming from unknown smart contracts, resulting in a loss of funds.

BFT Chains

  • Send the transaction to the Asgard vault
  • Include the memo
  • Only use the base asset as the choice for gas asset

THORChain

To initiate a $RUNE -> $ASSET swap a MsgDeposit must be broadcasted to the THORChain blockchain. The MsgDeposit does not have a destination address, and has the following properties. The full definition can be found here.

MsgDeposit{
    Coins:  coins,
    Memo:   memo,
    Signer: signer,
}

If you are using Javascript, CosmJS is the recommended package to build and broadcast custom message types. Here is a walkthrough.

Code Examples (Javascript)

  1. Generate codec files. To build/broadcast native transactions in Javascript/Typescript, the protobuf files need to be generated into js types. The below script uses pbjs and pbts to generate the types using the relevant files from the THORNode repo. Alternatively, the .js and .d.ts files can be downloaded directly from the XChainJS repo.

    #!/bin/bash
    
    # this script checks out thornode master and generates the proto3 typescript buindings for MsgDeposit and MsgSend
    
    MSG_COMPILED_OUTPUTFILE=src/types/proto/MsgCompiled.js
    MSG_COMPILED_TYPES_OUTPUTFILE=src/types/proto/MsgCompiled.d.ts
    
    TMP_DIR=$(mktemp -d)
    
    tput setaf 2; echo "Checking out https://gitlab.com/thorchain/thornode  to $TMP_DIR";tput sgr0
    (cd $TMP_DIR && git clone https://gitlab.com/thorchain/thornode)
    
    # Generate msgs
    tput setaf 2; echo "Generating $MSG_COMPILED_OUTPUTFILE";tput sgr0
    yarn run pbjs -w commonjs  -t static-module $TMP_DIR/thornode/proto/thorchain/v1/common/common.proto $TMP_DIR/thornode/proto/thorchain/v1/x/thorchain/types/msg_deposit.proto $TMP_DIR/thornode/proto/thorchain/v1/x/thorchain/types/msg_send.proto $TMP_DIR/thornode/third_party/proto/cosmos/base/v1beta1/coin.proto -o $MSG_COMPILED_OUTPUTFILE
    
    tput setaf 2; echo "Generating $MSG_COMPILED_TYPES_OUTPUTFILE";tput sgr0
    yarn run pbts  $MSG_COMPILED_OUTPUTFILE -o $MSG_COMPILED_TYPES_OUTPUTFILE
    
    tput setaf 2; echo "Removing $TMP_DIR/thornode";tput sgr0
    rm -rf $TMP_DIR
    
  2. Using @cosmjs build/broadcast the TX.

    const {
      DirectSecp256k1HdWallet,
      Registry,
    } = require("@cosmjs/proto-signing");
    const {
      defaultRegistryTypes: defaultStargateTypes,
      SigningStargateClient,
    } = require("@cosmjs/stargate");
    const { stringToPath } = require("@cosmjs/crypto");
    const bech32 = require("bech32-buffer");
    
    const { MsgDeposit } = require("./types/MsgCompiled").types;
    
    async function main() {
      const myRegistry = new Registry(defaultStargateTypes);
      myRegistry.register("/types.MsgDeposit", MsgDeposit);
    
      const signerMnemonic = "mnemonic here";
      const signerAddr = "thor1...";
    
      const signer = await DirectSecp256k1HdWallet.fromMnemonic(signerMnemonic, {
        prefix: "thor", // THORChain prefix
        hdPaths: [stringToPath("m/44'/931'/0'/0/0")], // THORChain HD Path
      });
    
      const client = await SigningStargateClient.connectWithSigner(
        "https://rpc.ninerealms.com/",
        signer,
        { registry: myRegistry },
      );
    
      const memo = `=:BNB/BNB:${signerAddr}`; // THORChain memo
    
      const msg = {
        coins: [
          {
            asset: {
              chain: "THOR",
              symbol: "RUNE",
              ticker: "RUNE",
            },
            amount: "100000000", // Value in 1e8 (100000000 = 1 RUNE)
          },
        ],
        memo: memo,
        signer: bech32.decode(signerAddr).data,
      };
    
      const depositMsg = {
        typeUrl: "/types.MsgDeposit",
        value: MsgDeposit.fromObject(msg),
      };
    
      const fee = {
        amount: [],
        gas: "50000000", // Set arbitrarily high gas limit; this is not actually deducted from user account.
      };
    
      const response = await client.signAndBroadcast(
        signerAddr,
        [depositMsg],
        fee,
        memo,
      );
      console.log("response: ", response);
    
      if (response.code !== 0) {
        console.log("Error: ", response.rawLog);
      } else {
        console.log("Success!");
      }
    }
    
    main();
    

Native Transaction Fee

As of ADR-009, the native transaction fee for $RUNE transfers or inbound swaps is USD-denominated, but ultimately paid in $RUNE, which means the fee is dynamic. Interfaces should pull the native transaction fee from THORNode before each new transaction is built/broadcasted.

THORNode Network Endpoint: /thorchain/network

{
  ...
  "native_outbound_fee_rune": "2000000", // (1e8) Outbound fee for $Asset -> $RUNE swaps
  "native_tx_fee_rune": "2000000", // (1e8) Fee for $RUNE transfers or $RUNE -> $Asset swaps
  ...
  "rune_price_in_tor": "354518918", // (1e8) Current $RUNE price in USD
  ...
}

The native transaction fee is automatically deducted from the user's account for $RUNE transfers and inbound swaps. Ensure the user's balance exceeds tx amount + native_tx_fee_rune before broadcasting the transaction.

Code Libraries

XCHAINJS

Started as a wallet library for creating keystores, getting balances and history, as well as signing and broadcasting transactions but has now expanded as a one-stop shop for THORChain functionality.

https://docs.xchainjs.org/overview/

https://github.com/xchainjs/xchainjs-lib

SwapKit

SwapKit, powered by THORSwap, offers a composable, user-friendly Partner API/SDK on top of THORChain's cross-chain liquidity protocol.

https://docs.thorswap.finance/swapkit-docs/

https://github.com/thorswap/SwapKit

XCHAINPY

XCHAINJS ported to python.

https://github.com/xchainjs/xchainpy-lib

XChainDart

XChainJs ported to Dart

https://github.com/dragonsdex/xchaindart

THORCHAIN-IOS

iOS library built in swift for connecting to THORChain and getting the right transaction details.

https://github.com/thorchain/thorchain-ios

Others

Other packages are available, built by the community to help with access to THORChain.

https://github.com/thorswap/thorchain.js

https://github.com/thorswap/midgard-sdk-v1

Math

​Math Library

The following libraries implement the math below.

https://gitlab.com/thorchain/asgardex-common/asgardex-util/-/tree/master/src/calc

https://github.com/xchainjs/xchainjs-lib/blob/master/packages/xchain-thorchain-query/src/utils/swap.ts

Example Data

All the examples below use the following snapshotted BTC and BUSD Pool data

https://thornode.ninerealms.com/thorchain/pool/BTC.BTC​ ​https://thornode.ninerealms.com/thorchain/pool/BNB.BUSD-BD1

{
 "LP_units": "117582615428135",
 "asset": "BNB.BUSD-BD1",
 "balance_asset": "952382623537567",
 "balance_rune": "508868258770825",
 "pending_inbound_asset": "310872270739",
 "pending_inbound_rune": "1701596418307",
 "pool_units": "134664599295503",
 "status": "Available",
 "synth_supply": "241616351972821",
 "synth_units": "17081983867368"
},
{
 "LP_units": "476785169622350",
 "asset": "BTC.BTC",
 "balance_asset": "81439552768",
 "balance_rune": "863897777396922",
 "pending_inbound_asset": "386699833",
 "pending_inbound_rune": "216117023429",
 "pool_units": "492710913491074",
 "status": "Available",
 "synth_supply": "5264691415",
 "synth_units": "15925743868724"
}

Prices

price = \frac{quoteBalance}{baseBalance}=\frac{USD}{RUNE} = $RUNE

Prices of all assets on THORChain are in ratios of each other, based on the depths of the pools. The quote asset is the "pricing" asset, and the base asset is the asset to be quoted. Ie, for the $ value of RUNE, the quote asset is USD and the base asset is RUNE.

Example

Let's take the BTC and BUSD Pool data

https://thornode.ninerealms.com/thorchain/pool/BTC.BTC

https://thornode.ninerealms.com/thorchain/pool/BNB.BUSD-BD1

The $BTC Price of RUNE is BTC/RUNE = 81439552768/863897777396922 = 0.000094 BTC

The $BUSD price of BTC is (BUSD/RUNE) * (RUNE/BTC) = (952382623537567/508868258770825) * (863897777396922/81439552768) = 19,854 BUSD

export const getValueOfAssetInRune = (inputAsset: BaseAmount, pool: PoolData): BaseAmount => {
  // formula: ((a * R) / A) => R per A (Runeper$)
  const t = inputAsset.amount()
  const R = pool.runeBalance.amount()
  const A = pool.assetBalance.amount()
  const result = t.times(R).div(A)
  return baseAmount(result)
}

export const getValueOfRuneInAsset = (inputRune: BaseAmount, pool: PoolData): BaseAmount => {
  // formula: ((r * A) / R) => A per R ($perRune)
  const r = inputRune.amount()
  const R = pool.runeBalance.amount()
  const A = pool.assetBalance.amount()
  const result = r.times(A).div(R)
  return baseAmount(result)
}
export const getValueOfAsset1InAsset2 = (inputAsset: BaseAmount, pool1: PoolData, pool2: PoolData): BaseAmount => {
  // formula: (A2 / R) * (R / A1) => A2/A1 => A2 per A1 ($ per Asset)
  const oneAsset = assetToBase(assetAmount(1))
  // Note: All calculation needs to be done in `AssetAmount` (not `BaseAmount`)
  const A2perR = baseToAsset(getValueOfRuneInAsset(oneAsset, pool2))
  const RperA1 = baseToAsset(getValueOfAssetInRune(inputAsset, pool1))
  const result = A2perR.amount().times(RperA1.amount())
  // transform result back from `AssetAmount` into `BaseAmount`
  return assetToBase(assetAmount(result))
}

Slippage

Slippage is simply the transaction divided by its corresponding depth.

Ie, swapping 10 BTC to RUNE = 1000000000 / (1000000000 + 81439552768) = 0.012 = 1.2%

Since THORChain has all pools in RUNE, a cross-asset (double) swap would involve two swaps in two pools, thus the slip needs to be doubled.

Here's a reference implementation of calculating slip for a double swap:

// Calculate swap output with slippage
function calcSwapOutput(inputAmount, pool, toRune) {
  // formula: (inputAmount * inputBalance * outputBalance) / (inputAmount + inputBalance) ^ 2
  const inputBalance = toRune ? pool.assetBalance : pool.runeBalance; // input is asset if toRune
  const outputBalance = toRune ? pool.runeBalance : pool.assetBalance; // output is rune if toRune
  const numerator = inputAmount * inputBalance * outputBalance;
  const denominator = Math.pow(inputAmount + inputBalance, 2);
  const result = numerator / denominator;
  return result;
}

// Calculate swap slippage
function calcSwapSlip(inputAmount, pool, toRune) {
  // formula: (inputAmount) / (inputAmount + inputBalance)
  const inputBalance = toRune ? pool.assetBalance : pool.runeBalance; // input is asset if toRune
  const result = inputAmount / (inputAmount + inputBalance);
  return result;
}

// Calculate swap slippage for double swap
function calcDoubleSwapSlip(inputAmount, pool1, pool2) {
  // formula: calcSwapSlip1(input1) + calcSwapSlip2(calcSwapOutput1 => input2)
  const swapSlip1 = calcSwapSlip(inputAmount, pool1, true);
  const r = calcSwapOutput(inputAmount, pool1, true);
  const swapSlip2 = calcSwapSlip(r, pool2, false);
  const result = swapSlip1 + swapSlip2;
  return result;
}

Source: https://gitlab.com/thorchain/asgardex-common/asgardex-util/-/blob/master/src/calc/swap.ts

Swap Output

​The output in a swap is the CLP formula.

Ie, output after swapping 10 BTC: (1000000000 * 81439552768 * 863897777396922)/ (1000000000 + 81439552768)^2 = 10352052898302 = 103520 RUNE

export const getSwapOutput = (inputAmount: BaseAmount, pool: PoolData, toRune: boolean): BaseAmount => {
  // formula: (x * X * Y) / (x + X) ^ 2
  const x = inputAmount.amount()
  const X = toRune ? pool.assetBalance.amount() : pool.runeBalance.amount() // input is asset if toRune
  const Y = toRune ? pool.runeBalance.amount() : pool.assetBalance.amount() // output is rune if toRune
  const numerator = x.times(X).times(Y)
  const denominator = x.plus(X).pow(2)
  const result = numerator.div(denominator)
  return baseAmount(result)
}
export const getDoubleSwapOutput = (inputAmount: BaseAmount, pool1: PoolData, pool2: PoolData): BaseAmount => {
  // formula: getSwapOutput(pool1) => getSwapOutput(pool2)
  const r = getSwapOutput(inputAmount, pool1, true)
  const output = getSwapOutput(r, pool2, false)
  return output
}

Swap Input

X = inputBalance

Y = outputBalance, y = outputAmount

The swap formula can be reversed to specify what needs to be deposited to get a certain output.

export const getSwapInput = (toRune: boolean, pool: PoolData, outputAmount: BaseAmount): BaseAmount => {
  // formula: (((X*Y)/y - 2*X) - sqrt(((X*Y)/y - 2*X)^2 - 4*X^2))/2
  // (part1 - sqrt(part1 - part2))/2
  const X = toRune ? pool.assetBalance.amount() : pool.runeBalance.amount() // input is asset if toRune
  const Y = toRune ? pool.runeBalance.amount() : pool.assetBalance.amount() // output is rune if toRune
  const y = outputAmount.amount()
  const part1 = X.times(Y).div(y).minus(X.times(2))
  const part2 = X.pow(2).times(4)
  const result = part1.minus(part1.pow(2).minus(part2).sqrt()).div(2)
  return baseAmount(result)
}

LP Units Add

  • (P): Existing Pool Units
  • (R): runeBalance, (A): assetBalance
  • (r): runeAdded, (a): assetAdded

The units to give an LP depend on the existing units, as well as the assets they are adding, and the depths of the pool they are adding to.

LP Units Withdrawn

  • (L): Liquidity units owned
  • (P): Pool Units
  • (X): depth of side

THORChain allows LPs to redeem a Basis Points amount of their position (out of 10000). To find out how much the user will get, multiply this by each side.

export const getPoolShare = (unitData: UnitData, pool: PoolData): StakeData => {
  // formula: (rune * part) / total; (asset * part) / total
  const units = unitData.stakeUnits.amount()
  const total = unitData.totalUnits.amount()
  const R = pool.runeBalance.amount()
  const T = pool.assetBalance.amount()
  const asset = T.times(units).div(total)
  const rune = R.times(units).div(total)
  const stakeData = {
    asset: baseAmount(asset),
    rune: baseAmount(rune)
  }
  return stakeData
}

Aggregator Overview

Overview

THORChain will only support a set number of assets and is not designed to support long tailed assets. If a user wants to swap from a long tail ERC20 asset to Bitcoin, they have to use an Ethereum AMM like Sushi Swap to swap the ERC20 asset to ETH then they can swap the ETH to BTC.;

The same process applies for long tail tokens on other chains such as Avalanche and Cosmos.;

Aggregator is the ability for a user swap long tail assets via leveraging a supported on-chain AMMs and THORChain in one transaction.;

To support cross-chain aggregation, THORChain whitelists aggregator contracts that can call into THORChain (Swap In), or receive calls (Swap Out). Chains that do not have on-chain AMMs (like Bitcoin) cannot support SwapIn, but they can support SwapOut, since they can pass a memo to THORChain.;

ETH swap contracts such as Sushi Swap to convert to/from THORChain support L1 tokens such as BTC. Example, in one transaction:

  1. User swaps long-tail ERC20 to ETH in SushiSwap, then swaps that ETH to BTC.
  2. User swaps BTC into ETH, then swaps that ETH into long-tail ERC20

There can be multiple aggregators. The first thorchain aggregator will use Sushiswap only and use ETH as the base asset. Aggregators need to follow a spec for compatibility with THORChain. Any THORChain ecosystem project can launch their own aggregator and get it whitelisted into THORChain. They can add custom/exotic routing logic if they wish.

Warning

Destination addresses should only be user-controlled addresses, not smart contract addresses.;

SwapIn

The SwapIn is called by the User, which then passes a memo to THORChain to do the final swap.;

User -> Call Into Aggregator -> Swap Via AMM -> Deposit into THORChain -> Swap to Base Asset

Eg: Swap long tail ERC20 via Sushiswap into BTC on THORChain.;

Transaction Example using UniSwap to swap ETH.ENJ to BNB.BNB.

SwapOut

The SwapOut is called by the User invoking the aggregator memo on THORChain.;

The User needs to pass the aggregator contract address in the memo. THORChain will perform the swap to the preferred Base Asset for that chain. The rest of the parameters, being to, asset, limit are what is passed by THORChain in the SwapOut call for further execution.

User -> Deposit into THORChain -> Swap to Base Asset -> Call into Aggregator -> Swap Via AMM

Eg: Swap from BTC on THORChain to long tail ERC20 via Sushiswap. See Memos.;

Combined

A user can combine the two. Swapping In first, then passing an Aggregator Memo to THORChain. This will cause THORChain to perform a SwapOut.

User -> Swap In -> THORChain -> Swap Out

Eg: Swap long tail ERC20 via Sushiswap into ETH on THORChain to LUNA then long tail CW20 via TerraSwap.

EVM Implementation

CosmWasm Implementation

For SwapIn The caller must first execute a MsgExecuteContract, then call a MsgSend into THORChain vaults with the correct memo.;

For SwapOut THORChain will execute a MsgExecuteContract which then sends the final asset to the user. If failed, THORChain will execute the fallback and send the member the base asset instead.

Deploying An Aggregator

If you would like to deploy your own aggregator with your own custom logic, deploy it with the principles above, then submit a PR for it to get whitelisted on THORChain.

Example: https://gitlab.com/thorchain/thornode/-/merge_requests/2132

Aggregator Overview

Overview

THORChain will only support a set number of assets and is not designed to support long tailed assets. If a user wants to swap from a long tail ERC20 asset to Bitcoin, they have to use an Ethereum AMM like Sushi Swap to swap the ERC20 asset to ETH then they can swap the ETH to BTC.;

The same process applies for long tail tokens on other chains such as Avalanche and Cosmos.;

Aggregator is the ability for a user swap long tail assets via leveraging a supported on-chain AMMs and THORChain in one transaction.;

To support cross-chain aggregation, THORChain whitelists aggregator contracts that can call into THORChain (Swap In), or receive calls (Swap Out). Chains that do not have on-chain AMMs (like Bitcoin) cannot support SwapIn, but they can support SwapOut, since they can pass a memo to THORChain.;

ETH swap contracts such as Sushi Swap to convert to/from THORChain support L1 tokens such as BTC. Example, in one transaction:

  1. User swaps long-tail ERC20 to ETH in SushiSwap, then swaps that ETH to BTC.
  2. User swaps BTC into ETH, then swaps that ETH into long-tail ERC20

There can be multiple aggregators. The first thorchain aggregator will use Sushiswap only and use ETH as the base asset. Aggregators need to follow a spec for compatibility with THORChain. Any THORChain ecosystem project can launch their own aggregator and get it whitelisted into THORChain. They can add custom/exotic routing logic if they wish.

Warning

Destination addresses should only be user-controlled addresses, not smart contract addresses.;

SwapIn

The SwapIn is called by the User, which then passes a memo to THORChain to do the final swap.;

User -> Call Into Aggregator -> Swap Via AMM -> Deposit into THORChain -> Swap to Base Asset

Eg: Swap long tail ERC20 via Sushiswap into BTC on THORChain.;

Transaction Example using UniSwap to swap ETH.ENJ to BNB.BNB.

SwapOut

The SwapOut is called by the User invoking the aggregator memo on THORChain.;

The User needs to pass the aggregator contract address in the memo. THORChain will perform the swap to the preferred Base Asset for that chain. The rest of the parameters, being to, asset, limit are what is passed by THORChain in the SwapOut call for further execution.

User -> Deposit into THORChain -> Swap to Base Asset -> Call into Aggregator -> Swap Via AMM

Eg: Swap from BTC on THORChain to long tail ERC20 via Sushiswap. See Memos.;

Combined

A user can combine the two. Swapping In first, then passing an Aggregator Memo to THORChain. This will cause THORChain to perform a SwapOut.

User -> Swap In -> THORChain -> Swap Out

Eg: Swap long tail ERC20 via Sushiswap into ETH on THORChain to LUNA then long tail CW20 via TerraSwap.

EVM Implementation

CosmWasm Implementation

For SwapIn The caller must first execute a MsgExecuteContract, then call a MsgSend into THORChain vaults with the correct memo.;

For SwapOut THORChain will execute a MsgExecuteContract which then sends the final asset to the user. If failed, THORChain will execute the fallback and send the member the base asset instead.

Deploying An Aggregator

If you would like to deploy your own aggregator with your own custom logic, deploy it with the principles above, then submit a PR for it to get whitelisted on THORChain.

Example: https://gitlab.com/thorchain/thornode/-/merge_requests/2132

Memos

Swap Memo (from here)

In order to support SwapOut DEX Aggregation feature, a few more fields added into the swap memo.

SWAP:ASSET:DESTADDR:LIM:AFFILIATE:FEE:DEXAggregatorAddr:FinalTokenAddr:MinAmountOut|

ParameterNodesConditions
:DEXAggregatorAddrThe whitelisted aggregator contract.Can use the last x characters of the address to fuzz match it.
:FinalTokenAddrThe final token (must be on 1INCH Whitelist)Can be shortened
:minAmountOutThe parameter to pass into AmountOutMin in AMM contracts.Handled by the aggregator, so:
1. Can be 0 (no protection).
2. Can be in any decimals
3. Can be in % or BasisPoints, then converted to a price at the time of swap by the aggregator contract.

Thornode accepts integers and scientific notation. Both 100000000 and 1e8 would forward uint256 100000000 to the aggregator contract.

Success

If you include a vertical pipe (|) at the end of the memo, any data following it will be sent as an outbound memo to the specified outbound address. This feature enables developers to send generic data to contracts cross-chain.

Additional ObserveTxIn field

In order to support SwapOut Dex Aggregation feature safely , a few more fields have been added into tx out item

[
  {
    "chain": "ETH",
    "to_address": "0x3fd2d4ce97b082d4bce3f9fee2a3d60668d2f473",
    "vault_pub_key": "tthorpub1addwnpepq02wq8hwgmwge6p9yzwscyfp0kjv823kres7l7tcv89nn2zfu3jguu5s4qa",
    "coin": {
      "asset": "ETH.ETH",
      "amount": "19053206"
    },
    "memo": "OUT:EA7D80B3EB709319A6577AF6CF4DEFF67975D4F5A93CD8817E7FF04A048D1C5C",
    "max_gas": [
      {
        "asset": "ETH.ETH",
        "amount": "240000",
        "decimals": 8
      }
    ],
    "gas_rate": 3,
    "in_hash": "EA7D80B3EB709319A6577AF6CF4DEFF67975D4F5A93CD8817E7FF04A048D1C5C",
    "aggregator": "0x69800327b38A4CeF30367Dec3f64c2f2386f3848",    <-------------------- NEW
    "aggregator_target_asset": "0x0a44986b70527154e9F4290eC14e5f0D1C861822", <-------------------- NEW
    "aggregator_target_limit": "1000" <-------------------- NEW , but optional
  }
]

Also the same fields have been added to ObservedTx so THORNode can verify that bifrost did send out the transaction per instruction, use the aggregator per instructed , and pass target asset and target limit to the aggregator correctly

How to swap out with dex aggregator?

If i want to swap RUNE to random ERC20 asset that is not list on THORChain , but is list on SushiSwap for example

thornode tx thorchain deposit 200000000000 RUNE '=:ETH.ETH:0x3fd2d4ce97b082d4bce3f9fee2a3d60668d2f473::::2386f3848:0x0a44986b70527154e9F4290eC14e5f0D1C861822' --chain-id thorchain --node tcp://$THORNODE_IP:26657 --from {from user} --keyring-backend=file --yes --gas 20000000

Note:

  1. Swap asset is ETH.ETH
  2. 2386f3848 is the last nine characters of the aggregator contract address
  3. 0x0a44986b70527154e9F4290eC14e5f0D1C861822 is the final asset address
  4. Keep in mind SwapOut is best effort, when aggregator contract failed to perform the requested swap , then user will get ETH.ETH instead of the final asset it request

EVM Implementation

THORChain Aggregator Example

https://gitlab.com/thorchain/ethereum/eth-router/-/blob/master/contracts/THORChain_Aggregator.sol

Tokens must be on the ETH Whitelist. The destination address should be a user control address, not a contract address.

SwapIn

The aggregator contract needs a swapIn function similar to the one below. First, swap the token via an on-chain AMM, then call into THORChain and pass the correct memo to execute the next swap.

function swapIn(
    address tcVault,
    address tcRouter,
    string calldata tcMemo,
    address token,
    uint amount,
    uint amountOutMin,
    uint256 deadline
    ) public nonReentrant {
    uint256 _safeAmount = safeTransferFrom(token, amount); // Transfer asset
    safeApprove(token, address(swapRouter), amount);
    address[] memory path = new address[](2);
    path[0] = token; path[1] = WETH;
    swapRouter.swapExactTokensForETH(_safeAmount, amountOutMin, path, address(this), deadline);
    _safeAmount = address(this).balance;
    iROUTER(tcRouter).depositWithExpiry{value:_safeAmount}(payable(tcVault), ETH, _safeAmount, tcMemo, deadline);
}

Transaction Example. Note the destination address is not a contract address.

SwapOut

The THORChain router uses transferOutAndCall() to call the aggregator with a max GasLimit of 400k units.

It is a particular function that also handles a swap fail by sending the user the base asset directly (ie, breached AmountOutMin, or could not find the finaltoken). The user will need to do the swap manually.

The parameters for this function are passed to THORChain by the user's original memo.

function transferOutAndCall(address payable target, address finalToken, address to, uint256 amountOutMin, string memory memo) public payable nonReentrant {
        uint256 _safeAmount = msg.value;
        (bool success, ) = target.call{value:_safeAmount}(abi.encodeWithSignature("swapOut(address,address,uint256)", finalToken, to, amountOutMin));
        if (!success) {
            payable(address(to)).transfer(_safeAmount); // If can't swap, just send the recipient the ETH
        }
        emit TransferOutAndCall(msg.sender, target, address(0), _safeAmount, finalToken, to, amountOutMin, memo);
    }

The swapOut function will only be passed three parameters from the THORChain Router and it must comply with the function signature (name, parameters). It can then call an on-chain AMM to execute the swap. It will only ever be given the base asset (eg ETH).

Here is an example to call UniV2 router:

function swapOut(address token, address to, uint256 amountOutMin) public payable nonReentrant {
        address[] memory path = nelw address[](2);
        path[0] = WETH; path[1] = token;
        swapRouter.swapExactETHForTokens{value: msg.value}(amountOutMin, path, to, type(uint).max);
    }

Overview

Install (Mac)

Prerequisites

  1. xcode-select xcode-select --install
  2. Homebrew: https://brew.sh

GoLang

Install go v1.18.1: https://go.dev/doc/install

# Set PATH
export GOROOT=/usr/local/go
export GOPATH=$HOME/go
export GOBIN=$GOPATH/bin
export PATH=$PATH:$GOROOT:$GOPATH:$GOBIN

Protobuf

# Install Protobuf
brew install protobuf
brew install protoc-gen-go
go install google.golang.org/protobuf/cmd/protoc-gen-go@latestellina

GNU Utils

# Install GNU find
brew install findutils

# Set PATH
export PATH=$(brew --prefix)/opt/findutils/libexec/gnubin:$PATH

Docker

# Install docker
brew install homebrew/cask/docker

THORNode

# Clone repo and install dependencies
git clone https://gitlab.com/thorchain/thornode
# Docker must be started...
make openapi
make protob-docker
make install

Commands

thornode --help

THORChain Network

Usage:
  THORChain [command]

Available Commands:
  add-genesis-account Add a genesis account to genesis.json
  collect-gentxs      Collect genesis txs and output a genesis.json file
  debug               Tool for helping with debugging your application
  ed25519             Generate an ed25519 keys
  export              Export state to JSON
  gentx               Generate a genesis tx carrying a self delegation
  help                Help about any command
  init                Initialize private validator, p2p, genesis, and application configuration files
  keys                Manage your application's keys
  migrate             Migrate genesis to a specified target version
  pubkey              Convert Proto3 JSON encoded pubkey to bech32 format
  query               Querying subcommands
  start               Run the full node
  status              Query remote node for status
  tendermint          Tendermint subcommands
  tx                  Transactions subcommands
  unsafe-reset-all    Resets the blockchain database, removes address book files, and resets data/priv_validator_state.json to the genesis state
  validate-genesis    validates the genesis file at the default location or at the location passed as an arg
  version             Print the application binary version information

Flags:
  -h, --help                help for THORChain
      --home string         directory for config and data (default "/Users/dev/.thornode")
      --log_format string   The logging format (json|plain) (default "plain")
      --log_level string    The logging level (trace|debug|info|warn|error|fatal|panic) (default "info")
      --trace               print out full stack trace on errors

Add new account

thornode keys add {accountName}

Add existing account (via mnemonic)

thornode keys add {accountName} --recover

List all accounts

thornode keys list

Send Transaction

Create Transaction

# Sender: thor1505gp5h48zd24uexrfgka70fg8ccedafsnj0e3
# Receiver: thor1gutjhrw4xlu3n3p3k3r0vexl2xknq3nv8ux9fy
# Amount: 1 RUNE (in 1e8 notation)
thorcli tx bank send thor1505gp5h48zd24uexrfgka70fg8ccedafsnj0e3 thor1gutjhrw4xlu3n3p3k3r0vexl2xknq3nv8ux9fy 100000000rune --chain-id thorchain-mainnet-v1 --node https://rpc.ninerealms.com:443 --gas 3000000 --generate-only >> tx_raw.json

This will output a file called tx_raw.json. Edit this file and change the @type field from /cosmos.bank.v1beta1.MsgSend to /types.MsgSend.

The tx_raw.json transaction should look like this:

{
  "body": {
    "messages": [
      {
        "@type": "/types.MsgSend",
        "from_address": "thor1505gp5h48zd24uexrfgka70fg8ccedafsnj0e3",
        "to_address": "thor1gutjhrw4xlu3n3p3k3r0vexl2xknq3nv8ux9fy",
        "amount": [{ "denom": "rune", "amount": "100000000" }]
      }
    ],
    "memo": "",
    "timeout_height": "0",
    "extension_options": [],
    "non_critical_extension_options": []
  },
  "auth_info": {
    "signer_infos": [],
    "fee": { "amount": [], "gas_limit": "3000000", "payer": "", "granter": "" }
  },
  "signatures": []
}

Sign Transaction

thornode tx sign tx_raw.json --from {accountName} --sign-mode amino-json --chain-id thorchain-mainnet-v1 --node https://rpc.ninerealms.com:443 >> tx.json

This will output a file called tx.json.

Broadcast Transaction

thornode tx broadcast tx.json --chain-id thorchain-mainnet-v1 --node https://rpc.ninerealms.com:443 --gas auto

Overview

Install (Mac)

Prerequisites

  1. xcode-select xcode-select --install
  2. Homebrew: https://brew.sh

GoLang

Install go v1.18.1: https://go.dev/doc/install

# Set PATH
export GOROOT=/usr/local/go
export GOPATH=$HOME/go
export GOBIN=$GOPATH/bin
export PATH=$PATH:$GOROOT:$GOPATH:$GOBIN

Protobuf

# Install Protobuf
brew install protobuf
brew install protoc-gen-go
go install google.golang.org/protobuf/cmd/protoc-gen-go@latestellina

GNU Utils

# Install GNU find
brew install findutils

# Set PATH
export PATH=$(brew --prefix)/opt/findutils/libexec/gnubin:$PATH

Docker

# Install docker
brew install homebrew/cask/docker

THORNode

# Clone repo and install dependencies
git clone https://gitlab.com/thorchain/thornode
# Docker must be started...
make openapi
make protob-docker
make install

Commands

thornode --help

THORChain Network

Usage:
  THORChain [command]

Available Commands:
  add-genesis-account Add a genesis account to genesis.json
  collect-gentxs      Collect genesis txs and output a genesis.json file
  debug               Tool for helping with debugging your application
  ed25519             Generate an ed25519 keys
  export              Export state to JSON
  gentx               Generate a genesis tx carrying a self delegation
  help                Help about any command
  init                Initialize private validator, p2p, genesis, and application configuration files
  keys                Manage your application's keys
  migrate             Migrate genesis to a specified target version
  pubkey              Convert Proto3 JSON encoded pubkey to bech32 format
  query               Querying subcommands
  start               Run the full node
  status              Query remote node for status
  tendermint          Tendermint subcommands
  tx                  Transactions subcommands
  unsafe-reset-all    Resets the blockchain database, removes address book files, and resets data/priv_validator_state.json to the genesis state
  validate-genesis    validates the genesis file at the default location or at the location passed as an arg
  version             Print the application binary version information

Flags:
  -h, --help                help for THORChain
      --home string         directory for config and data (default "/Users/dev/.thornode")
      --log_format string   The logging format (json|plain) (default "plain")
      --log_level string    The logging level (trace|debug|info|warn|error|fatal|panic) (default "info")
      --trace               print out full stack trace on errors

Add new account

thornode keys add {accountName}

Add existing account (via mnemonic)

thornode keys add {accountName} --recover

List all accounts

thornode keys list

Send Transaction

Create Transaction

# Sender: thor1505gp5h48zd24uexrfgka70fg8ccedafsnj0e3
# Receiver: thor1gutjhrw4xlu3n3p3k3r0vexl2xknq3nv8ux9fy
# Amount: 1 RUNE (in 1e8 notation)
thorcli tx bank send thor1505gp5h48zd24uexrfgka70fg8ccedafsnj0e3 thor1gutjhrw4xlu3n3p3k3r0vexl2xknq3nv8ux9fy 100000000rune --chain-id thorchain-mainnet-v1 --node https://rpc.ninerealms.com:443 --gas 3000000 --generate-only >> tx_raw.json

This will output a file called tx_raw.json. Edit this file and change the @type field from /cosmos.bank.v1beta1.MsgSend to /types.MsgSend.

The tx_raw.json transaction should look like this:

{
  "body": {
    "messages": [
      {
        "@type": "/types.MsgSend",
        "from_address": "thor1505gp5h48zd24uexrfgka70fg8ccedafsnj0e3",
        "to_address": "thor1gutjhrw4xlu3n3p3k3r0vexl2xknq3nv8ux9fy",
        "amount": [{ "denom": "rune", "amount": "100000000" }]
      }
    ],
    "memo": "",
    "timeout_height": "0",
    "extension_options": [],
    "non_critical_extension_options": []
  },
  "auth_info": {
    "signer_infos": [],
    "fee": { "amount": [], "gas_limit": "3000000", "payer": "", "granter": "" }
  },
  "signatures": []
}

Sign Transaction

thornode tx sign tx_raw.json --from {accountName} --sign-mode amino-json --chain-id thorchain-mainnet-v1 --node https://rpc.ninerealms.com:443 >> tx.json

This will output a file called tx.json.

Broadcast Transaction

thornode tx broadcast tx.json --chain-id thorchain-mainnet-v1 --node https://rpc.ninerealms.com:443 --gas auto

Multisig

Setup Multisig

First, collect the pubkeys that will be part of the multisig. They can be printed using thorcli:

thornode keys show person1 --pubkey

Then share the pubkey with the other parties. Each party can add these pubkeys:

thornode keys add person2 --pubkey {pubkey}

Each party can create the multisig (here a 2/3):

thornode keys add multisig --multisig person1,person2,person3 --multisig-threshold 2

Create Transaction

Any of the parties can create the raw transaction:

# Sender: thor1505gp5h48zd24uexrfgka70fg8ccedafsnj0e3
# Receiver: thor1gutjhrw4xlu3n3p3k3r0vexl2xknq3nv8ux9fy
# Amount: 1 RUNE (in 1e8 notation)
thorcli tx bank send thor1505gp5h48zd24uexrfgka70fg8ccedafsnj0e3 thor1gutjhrw4xlu3n3p3k3r0vexl2xknq3nv8ux9fy 100000000rune --chain-id thorchain-mainnet-v1 --node https://rpc.ninerealms.com:443 --gas 3000000 --generate-only >> tx_raw.json

This will output a file called tx_raw.json. Edit this file and change the @type field from /cosmos.bank.v1beta1.MsgSend to /types.MsgSend.

The tx_raw.json transaction should look like this:

{
  "body": {
    "messages": [
      {
        "@type": "/types.MsgSend",
        "from_address": "thor1505gp5h48zd24uexrfgka70fg8ccedafsnj0e3",
        "to_address": "thor1gutjhrw4xlu3n3p3k3r0vexl2xknq3nv8ux9fy",
        "amount": [{ "denom": "rune", "amount": "100000000" }]
      }
    ],
    "memo": "",
    "timeout_height": "0",
    "extension_options": [],
    "non_critical_extension_options": []
  },
  "auth_info": {
    "signer_infos": [],
    "fee": { "amount": [], "gas_limit": "3000000", "payer": "", "granter": "" }
  },
  "signatures": []
}

Sign Transaction

The transaction needs to be signed by 2 of the 3 parties (as configured above, when setting up the multisig).

From Person 1

thornode tx sign --from person1 --multisig multisig tx_raw.json --sign-mode amino-json --chain-id thorchain-mainnet-v1 --node https://rpc.ninerealms.com:443 >> tx_signed_1.json

This will output a file called tx_signed_1.json.

From Person 2

thornode tx sign --from person2 --multisig multisig tx_raw.json --sign-mode amino-json --chain-id thorchain-mainnet-v1 --node https://rpc.ninerealms.com:443 >> tx_signed_2.json

This will output a file called tx_signed_2.json.

Build Transaction

Gather Signatures

The party, who wants to broadcast the transaction, needs to gather all json signature files from the other parties.

Multisig Sign

First, get the sequence and account number for the multisig address:

curl https://thornode.ninerealms.com/cosmos/auth/v1beta1/accounts/thor1505gp5h48zd24uexrfgka70fg8ccedafsnj0e3

Then combine the signatures into a single one (make sure to update the account number -a and the sequence number -s:

# Account number: 33401 (see curl output)
# Sequence number: 0 (see curl output)
thornode tx multisign tx_raw.json multisig tx_signed_1.json tx_signed_2.json -a 33401 -s 0 --from multisig --chain-id thorchain-mainnet-v1 --node https://rpc.ninerealms.com:443 >> tx.json

This will output a final file called tx.json.

Broadcast Transaction

thornode tx broadcast tx.json --chain-id thorchain-mainnet-v1 --node https://rpc.ninerealms.com:443 --gas auto

THORSafe

Info

THORSafe does not support Ledger yet!

THORSafe is a multisig frontend (developed by THORSwap): https://app.thorswap.finance/thorsafe

Offline Ledger Support

When used in conjunction with a locally-running fullnode, the THORNode CLI + Ledger provides the ultimate, privacy-focused "offline, no-tracking" experience. Interact directly with the THORChain network to bond validators, swap RUNE (or synthetic assets) and administrate LPs from a cold-wallet.

Accounts

Ledger accounts can be added by appending --ledger to the command. The default index is 0.

thornode keys add ledger1 --ledger --index=1

Usage

Signing transactions requires confirmation through the Ledger. Everything else works the same.

Versioning

THORNode is following semantic version. MAJOR.MINOR.PATCH(0.77.1)

The MAJOR version currently is updated per soft-fork.

Minor version need to update when the network introduce some none backward compatible changes.

Patch version, is backward compatible, usually changes only in bifrost

Prepare for release

  1. Create a milestone using the release version (e.g. Release-1.116.0)
  2. Tag issues & PRs using the milestone, so we can identify which PR is on which version
  3. PRs need to get approved by #thornode-team and #thorsec. Once approved, merge to develop branch
  4. Once all PRs for a version have been merged, create a release branch from develop such as: release-1.116.0.

Test release candidate locally

  1. From your release branch, run make build-mocknet.
  2. Create a mocknet cluster using make reset-mocknet-cluster (follow README.md).
  3. Sanity check the following features work:
    • Genesis node start up successfully
    • Bifrost startup correctly, and start to observe all chains
    • Create pools for BNB/BTC/BCH/LTC/ETH/USDT
    • Add liquidity to BNB/BTC/BCH/LTC/ETH/USDT pools
    • Bond new validator
    • Set version
    • Set node keys
    • Set IP Address
    • Churn successful, cluster grow from 1 genesis node to 4 nodes
    • Fund migration successfully
    • Some swaps, RUNE -> BTC, BTC -> BNB etc.
    • Mocknet grow from four nodes -> five nodes, which include keygen, migration
    • Node can leave
  4. Identify unexpected log / behaviour, and investigate it.

Release to stagenet

Build stagenet

  1. Merge release branch e.g. release-1.116.0 branch -> stagenet branch. Once the changes are pushed, the stagenet image should be created automatically by pipeline.

  2. Make sure build-thornode pipeline is successful, you should be able to see the docker image has been built and tagged successfully:

    Successfully built bbf5fe970c75
    stagenet: digest: sha256:8ec7a9c832ad13fc28d0af440b5cddfec8e21b4a311903ad92fe0cab0433faac
    stagenet-1: digest: sha256:8ec7a9c832ad13fc28d0af440b5cddfec8e21b4a311903ad92fe0cab0433faac
    stagenet-1.112: digest: sha256:8ec7a9c832ad13fc28d0af440b5cddfec8e21b4a311903ad92fe0cab0433faac
    stagenet-1.112.0: digest: sha256:8ec7a9c832ad13fc28d0af440b5cddfec8e21b4a311903ad92fe0cab0433faac
    

Stagenet test plan

  1. Create a test plan either mentally, on Discord or on Notion (e.g. Stagenet 1.99 Test Plan)
  2. Consider what changes have shipped:
    • New features may require a dedicated test plan, as above for Savers. Consider the expected vs. actual result for querier endpoints before/after different transactions are made on-chain.
    • New chains will require the following process:
      1. Ensure all bifrost are running the latest version
      2. Ensure loadchains.go has successfully connected to the daemon.
      3. Ensure the new daemon is fully sync'd.
      4. Trigger a churn to create the asgard vault.
      5. Create a pool by sending L1 and RUNE to asgard.
      6. Once the pool is seeded with enough LP to pay outbound fees, churn the network again.
      7. Test inbound TX are observed and scheduled outbound TX are sent by doing swaps.
    • Changes to mimir should be set after the new version is adopted. The same should be done for mainnet.
    • If protos are added or changed, it's a good idea to send messages on-chain with the next proto both before and after consensus is reached on the new version.
    • Include a stagenet store migration if one is to take place in mainnet as well. Be sure to check the pools endpoint (or other to-be-changed state) before and after the version increments.
    • Sanity different UIs (only Asgardex and THORSwap support stagenet at this time).
    • If making changes to chain clients or a chain daemon has been updated (node-launcher/ci/images): make sure the daemon is up-to-date and fully sync'd, then trigger both an outbound and observe an inbound for the affected chain(s).

These are just a few examples. Each release may contain unique functionality or infrastructure changes that need to be tested.

Deploy stagenet

The stagenet maintainer does not need to keep the upstream node-launcher values for stagenet up-to-date. They are there as a reference. State can be kept locally.

The node-launcher repo will require the stagenet digest hash (e.g. 8ec7a9c832ad13fc28d0af440b5cddfec8e21b4a311903ad92fe0cab0433faac). Get this from the build-thornode CI step above. Be sure you didn't accidentally copy the mocknet digest hash. Using the mocknet image in stagenet will cause a consnesus failure.

  1. Apply the stagenet image(s) one-by-one.
  2. Wait for them to fully initialize and rejoin consensus.
  3. Run make set-version.
  4. Repeat until all validators on latest version.
  5. Check: curl thornode:1317/thorchain/version

Validate stagenet

  1. Conduct your Stagenet Test Plan.
  2. Document any findings or issues in #stagenet on Discord.
  3. Determine if any changes need to be made to the release candidate.

Release to mainnet

Build mainnet

  1. Merge the release branch (e.g. release-1.116.0 -> mainnet). Once the changes are pushed, the mainnet image should be created automatically by pipeline (e.g.: https://gitlab.com/thorchain/thornode/-/jobs/4682407839)

  2. Make sure build-thornode pipeline is successful, you should be able to see the docker image has been built and tagged successfully:

    Successfully built d92da6e9c460
    mainnet-1.116.0: digest: sha256:58df167b2c515a0cf4f4093ca27ca49d85cd1201801f9baa3ffcdafaaa138bcb
    mainnet-1.116.0: digest: sha256:58df167b2c515a0cf4f4093ca27ca49d85cd1201801f9baa3ffcdafaaa138bcb
    mainnet-1.116.0: digest: sha256:58df167b2c515a0cf4f4093ca27ca49d85cd1201801f9baa3ffcdafaaa138bcb
    mainnet-1.116.0: digest: sha256:58df167b2c515a0cf4f4093ca27ca49d85cd1201801f9baa3ffcdafaaa138bcb
    

Raise PR in node-launcher

  1. Raise PR to release version to node-launcher/thornode-stack/mainnet.yaml, bumping the version and tag according to the last step. (e.g. https://gitlab.com/thorchain/devops/node-launcher/-/merge_requests/876/diffs#16eb49b6065b1a08dae8d22c10d771efcce894af_4_2)
  2. Post the PR to #devops channel, and tag @thornode-team @thorsec @Nine Realms teams to approve. It will need at least 4 approvals.

Release to mainnet

Pre-release check

  1. Quickly go through all the PRs in the release.
  2. Apply the latest changes to a standby node and monitor the following:
    1. THORNode pod didn't get into CrashloopBackoff
    2. Version has been set correctly
    3. Bifrost started correctly.

Release

  1. Run the PR log script to collect all of the PRs tagged in this milestone (e.g. scripts/pr-log.py Release-1.116.0).
  2. Create a tag for the release on the develop branch (e.g. https://gitlab.com/thorchain/thornode/-/tags/v1.116.0). Copy and paste the output from the script above into the description.
  3. After the tag is created, go to the UI, click Create Release. Use the PR log for the description again.
  4. Post release announcement in #thornode-mainnet. Use previous messages as a template. Be sure to update the version number and tag URL.
  5. For mainnet release, post the release announcement in Telegram #THORNode Announcement

Versioning

THORNode is following semantic version. MAJOR.MINOR.PATCH(0.77.1)

The MAJOR version currently is updated per soft-fork.

Minor version need to update when the network introduce some none backward compatible changes.

Patch version, is backward compatible, usually changes only in bifrost

Prepare for release

  1. Create a milestone using the release version (e.g. Release-1.116.0)
  2. Tag issues & PRs using the milestone, so we can identify which PR is on which version
  3. PRs need to get approved by #thornode-team and #thorsec. Once approved, merge to develop branch
  4. Once all PRs for a version have been merged, create a release branch from develop such as: release-1.116.0.

Test release candidate locally

  1. From your release branch, run make build-mocknet.
  2. Create a mocknet cluster using make reset-mocknet-cluster (follow README.md).
  3. Sanity check the following features work:
    • Genesis node start up successfully
    • Bifrost startup correctly, and start to observe all chains
    • Create pools for BNB/BTC/BCH/LTC/ETH/USDT
    • Add liquidity to BNB/BTC/BCH/LTC/ETH/USDT pools
    • Bond new validator
    • Set version
    • Set node keys
    • Set IP Address
    • Churn successful, cluster grow from 1 genesis node to 4 nodes
    • Fund migration successfully
    • Some swaps, RUNE -> BTC, BTC -> BNB etc.
    • Mocknet grow from four nodes -> five nodes, which include keygen, migration
    • Node can leave
  4. Identify unexpected log / behaviour, and investigate it.

Release to stagenet

Build stagenet

  1. Merge release branch e.g. release-1.116.0 branch -> stagenet branch. Once the changes are pushed, the stagenet image should be created automatically by pipeline.

  2. Make sure build-thornode pipeline is successful, you should be able to see the docker image has been built and tagged successfully:

    Successfully built bbf5fe970c75
    stagenet: digest: sha256:8ec7a9c832ad13fc28d0af440b5cddfec8e21b4a311903ad92fe0cab0433faac
    stagenet-1: digest: sha256:8ec7a9c832ad13fc28d0af440b5cddfec8e21b4a311903ad92fe0cab0433faac
    stagenet-1.112: digest: sha256:8ec7a9c832ad13fc28d0af440b5cddfec8e21b4a311903ad92fe0cab0433faac
    stagenet-1.112.0: digest: sha256:8ec7a9c832ad13fc28d0af440b5cddfec8e21b4a311903ad92fe0cab0433faac
    

Stagenet test plan

  1. Create a test plan either mentally, on Discord or on Notion (e.g. Stagenet 1.99 Test Plan)
  2. Consider what changes have shipped:
    • New features may require a dedicated test plan, as above for Savers. Consider the expected vs. actual result for querier endpoints before/after different transactions are made on-chain.
    • New chains will require the following process:
      1. Ensure all bifrost are running the latest version
      2. Ensure loadchains.go has successfully connected to the daemon.
      3. Ensure the new daemon is fully sync'd.
      4. Trigger a churn to create the asgard vault.
      5. Create a pool by sending L1 and RUNE to asgard.
      6. Once the pool is seeded with enough LP to pay outbound fees, churn the network again.
      7. Test inbound TX are observed and scheduled outbound TX are sent by doing swaps.
    • Changes to mimir should be set after the new version is adopted. The same should be done for mainnet.
    • If protos are added or changed, it's a good idea to send messages on-chain with the next proto both before and after consensus is reached on the new version.
    • Include a stagenet store migration if one is to take place in mainnet as well. Be sure to check the pools endpoint (or other to-be-changed state) before and after the version increments.
    • Sanity different UIs (only Asgardex and THORSwap support stagenet at this time).
    • If making changes to chain clients or a chain daemon has been updated (node-launcher/ci/images): make sure the daemon is up-to-date and fully sync'd, then trigger both an outbound and observe an inbound for the affected chain(s).

These are just a few examples. Each release may contain unique functionality or infrastructure changes that need to be tested.

Deploy stagenet

The stagenet maintainer does not need to keep the upstream node-launcher values for stagenet up-to-date. They are there as a reference. State can be kept locally.

The node-launcher repo will require the stagenet digest hash (e.g. 8ec7a9c832ad13fc28d0af440b5cddfec8e21b4a311903ad92fe0cab0433faac). Get this from the build-thornode CI step above. Be sure you didn't accidentally copy the mocknet digest hash. Using the mocknet image in stagenet will cause a consnesus failure.

  1. Apply the stagenet image(s) one-by-one.
  2. Wait for them to fully initialize and rejoin consensus.
  3. Run make set-version.
  4. Repeat until all validators on latest version.
  5. Check: curl thornode:1317/thorchain/version

Validate stagenet

  1. Conduct your Stagenet Test Plan.
  2. Document any findings or issues in #stagenet on Discord.
  3. Determine if any changes need to be made to the release candidate.

Release to mainnet

Build mainnet

  1. Merge the release branch (e.g. release-1.116.0 -> mainnet). Once the changes are pushed, the mainnet image should be created automatically by pipeline (e.g.: https://gitlab.com/thorchain/thornode/-/jobs/4682407839)

  2. Make sure build-thornode pipeline is successful, you should be able to see the docker image has been built and tagged successfully:

    Successfully built d92da6e9c460
    mainnet-1.116.0: digest: sha256:58df167b2c515a0cf4f4093ca27ca49d85cd1201801f9baa3ffcdafaaa138bcb
    mainnet-1.116.0: digest: sha256:58df167b2c515a0cf4f4093ca27ca49d85cd1201801f9baa3ffcdafaaa138bcb
    mainnet-1.116.0: digest: sha256:58df167b2c515a0cf4f4093ca27ca49d85cd1201801f9baa3ffcdafaaa138bcb
    mainnet-1.116.0: digest: sha256:58df167b2c515a0cf4f4093ca27ca49d85cd1201801f9baa3ffcdafaaa138bcb
    

Raise PR in node-launcher

  1. Raise PR to release version to node-launcher/thornode-stack/mainnet.yaml, bumping the version and tag according to the last step. (e.g. https://gitlab.com/thorchain/devops/node-launcher/-/merge_requests/876/diffs#16eb49b6065b1a08dae8d22c10d771efcce894af_4_2)
  2. Post the PR to #devops channel, and tag @thornode-team @thorsec @Nine Realms teams to approve. It will need at least 4 approvals.

Release to mainnet

Pre-release check

  1. Quickly go through all the PRs in the release.
  2. Apply the latest changes to a standby node and monitor the following:
    1. THORNode pod didn't get into CrashloopBackoff
    2. Version has been set correctly
    3. Bifrost started correctly.

Release

  1. Run the PR log script to collect all of the PRs tagged in this milestone (e.g. scripts/pr-log.py Release-1.116.0).
  2. Create a tag for the release on the develop branch (e.g. https://gitlab.com/thorchain/thornode/-/tags/v1.116.0). Copy and paste the output from the script above into the description.
  3. After the tag is created, go to the UI, click Create Release. Use the PR log for the description again.
  4. Post release announcement in #thornode-mainnet. Use previous messages as a template. Be sure to update the version number and tag URL.
  5. For mainnet release, post the release announcement in Telegram #THORNode Announcement

EVM Whitelist Procedure

Overview

Ecosystem devs can ask for tokens/contracts to be added/removed from any THORNode Whitelists using this procedure.

Background

THORNode maintains whitelists to prevent attacks on the network. There are a significant number of degrees of freedom when dealing with the EVM (event spoofing, re-entrancies, self-destructs), as well as economic attacks (zombie tokens, infinite mints etc). Maintaining a standard and whitelist nueters this attack surface.

There are 3 EVM Whitelists

  1. Pool Token Whitelist - allows to be a pool on THORChain
  2. DEX Token Whitelist - allows to be swapped to using DEX Aggregation
  3. Aggregator Whitelist - allows to be an Aggregator to call into, or be called from, the router

Procedure

Once a review cycle, the publisher will ask for new additions to be submitted for review and inclusion. There will be a 48hr cutoff. The publisher will follow the following checklist and will not include the token/contract if it does not meet the requirements.

"must" - unavoidable requirement "should" - loose requirement

Pool Token

  • Must be ERC-20 compliant https://ethereum.org/en/developers/docs/standards/tokens/erc-20/
  • Must not include token transfer fees (taxes)
  • Must not be mintable
  • Must be verified on Etherscan (or equivalent, eg Snowtrace for AVA)
  • Should be economically valuable (greater than $100m mcap)
  • Should be older than 4 years
  • Should have a sponsor willing to provide $1m in bootstrap liquidity

Dex Token

  • Must be listed on an on-chain AMM
  • Must be ERC-20 compliant https://ethereum.org/en/developers/docs/standards/tokens/erc-20/
  • Must be verified on Etherscan

Aggregator

Examples: https://gitlab.com/thorchain/thornode/-/blob/develop/x/thorchain/aggregators/dex_mainnet.go

  • Must be verified on Etherscan (or equivalent, eg Snowtrace for AVA)
  • If support swapIn(params), must call router.depositWithExpiry(params, +15minsUNIXSeconds),
  • If support swapOut(params), must have a function exactly swapOut(address,address,uint256)
  • Must have re-entrancy protection on all functions https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/security/ReentrancyGuard.sol
  • Must not be proxied (https://docs.openzeppelin.com/upgrades-plugins/1.x/proxies)

What is router

On EVM based chain, bifrost rely on Router to emit correct events to determine what had happened, all inbound/outbound transactions go through a smart contract, we call it Router. Current Router is on V4. Each connected EVM-based ETH forks have a router (currently Ethereum and Avalanche C-Chain).

Router contract hold all ERC20 assets, but not the native asset (e.g. ETH or AVAX). The native assets will be sent to asgard address directly.

Where is Router code?

https://gitlab.com/thorchain/ethereum/eth-router , if you need to make changes to this router, please raise a PR in this repository

How to upgrade Router?

Note: Newer version router needs to be compatible with old router.

What you can do?

  • You can add new functions, new events

What you can't do?

  • Don't change existing function signature , Don't add parameter , don't remove parameter , don't change return value etc.
  • Don't change events , don't add new fields , don't remove fields

Router upgrade procedure

Before router upgrade , make sure you already make relevant changes in thornode repo. Replace <chain> in the below variables with the lowercase, shortened chain identifier (e.g. eth, avax).

  • New router has been deployed , and the router address has been updated. <chain>OldRouter is your current router address, <chain>NewRouter is your new router address

Before upgrade , make sure the network is healthy , all active nodes / standby nodes are online. If some nodes are not healthy , bifrost are not online it will cause the node's vault in a bad state

Detail upgrade procedure

Replace <CHAIN> in each Mimir key with capitalized, shortened chain identifier (e.g. ETH, AVAX)

  1. Set admin mimir ChurnInterval -> 432000 to stop churn
  2. Set admin mimir StopSolvencyCheck<CHAIN> -> 1 to stop Solvency checker on CHAIN, this will make sure the migration fund will not cause solvency checker to halt the chain
  3. Set admin mimir MimirUpgradeContract<CHAIN> -> 1 to update the router
  4. Set admin mimir ChurnInterval -> 43200
  5. Wait a churn to kick off , and make sure funds have been migrated from older router to new router. And vault retired successfully
  6. Set admin mimir StopSolvencyCheck<CHAIN> -> 0 to resume solvency checker on CHAIN

Constants and Mimir

Overview

The network launched with a set number of constants, which has not changed. Constants can be overridden via Mimir and nodes have the ability to vote on and change Mimir values.
See Halt Management for halt and pause specific settings.

Mimir setting can be created and changed without a corresponding Constant.

Values

Key

  • No Star or Hash - Constant only, no Mimir override.
  • Star (*) indicates a Mimir override of a Constant
  • Hash (#) indicates Mimir with no Constant.

Outbound Transactions

  • OutboundTransactionFee: Amount of rune to withhold on all outbound transactions (1e8 notation)
  • RescheduleCoalesceBlocks*: The number of blocks to coalesce rescheduled outbounds
  • MaxOutboundAttempts: The maximum retries to reschedule a transaction

Scheduled Outbound

  • MaxTxOutOffset: Maximum number of blocks a scheduled outbound transaction can be delayed
  • MinTxOutVolumeThreshold: Quantity of outbound value (in 1e8 rune) in a block before it's considered "full" and additional value is pushed into the next block
  • TxOutDelayMax: Maximum number of blocks a scheduled transaction can be delayed
  • TxOutDelayRate*: Rate of which scheduled transactions are delayed

Swapping

  • HaltTrading#: Pause all trading
  • Halt<chain>Trading#: Pause trading on a specific chain
  • MaxSwapsPerBlock: Artificial limit on the number of swaps that a single block with process
  • MinSwapsPerBlock: Process all swaps if the queue is equal to or smaller than this number
  • EnableDerivedAssets: Enable/disable derived asset swapping (excludes lending)
  • StreamingSwapMinBPFee*: Minimum swap fee (in basis points) for a streaming swap trade
  • StreamingSwapMaxLength: Maximum number of blocks a streaming swap can trade for
  • StreamingSwapMaxLengthNative: Maximum number of blocks native streaming swaps can trade over
  • TradeAccountsEnabled: Enable/disable trade account

Synths

  • MaxSynthPerAssetDepth: The amount of synths allowed per pool relative to the pool depth
  • MaxSynthPerPoolDepth*: The percentage (in basis points) of how many synths are allowed relative to pool depth of the related pool
  • BurnSynths#: Enable/Disable burning synths
  • MintSynths#: Enable/Disable minting synths
  • VirtualMultSynths: The amount of increase the pool depths for calculating swap fees of synths

Savers

  • MaxSynthsForSaversYield*: The percentage (in basis points) synth per pool where synth yield reaches 0%
  • SaversStreamingSwapsInterval*: For Savers deposits and withdraws, the streaming swaps interval to use for the Native <> Synth swap

POL Management

  • POL-<Asset-Asset>#: Enabled POL for that pool. E.g. POL-BTC-BTC" = 1 enabled POL for the BTC pool.
  • POLBuffer*: the buffer around the POL synth utilization (basis points added to/subtracted from POLTargetSynthPerPoolDepth basis points)
  • POLMaxNetworkDeposit*: Maximum amount of rune deposited into the pools
  • POLMaxPoolMovement*: Maximum amount of rune to enter/exit a pool per iteration - 1 equals one hundredth of a basis point of pool rune depth
  • POLTargetSynthPerPoolDepth*: The target synth per pool depth for POL (basis points)

Lending

  • LendingLever: Controls (in basis points) how much lending is allowed relative to rune supply
  • LoanRepaymentMaturity: Number of blocks before loan has reached maturity and can be repaid
  • MinCR*: Minimum collateralization ratio (basis pts)
  • MaxCR*: Maximum collateralization ratio (basis pts)
  • Lending-THOR-<Asset>#: Lending key for an asset, allows that Asset to be used as colloteral. The lending key for the ETH.ETH pool would be LENDING-THOR-ETH and enabled the THOR-ETH virtual pool.
  • LoanStreamingSwapsInterval*: The block interval between each streaming swap of opening or closing a loan

Derived Assets

  • DerivedDepthBasisPts: Allows mimir to increase or decrease the default derived asset pool depth relative to the anchor pools. 10k == 1x, 20k == 2x, 5k == 0.5x
  • DerivedMinDepth: Sets the minimum derived asset depth in basis points, or pool depth floor.
  • MaxAnchorSlip*: Percentage (in basis points) of how much price slip in the anchor pools will cause the derived asset pool depths to decrease to
  • DerivedMinDepth. For example, 8k basis pts will mean that when there has been 80% price slip in the last MaxAnchorBlocks, the derived asset pool depth will be DerivedMinDepth. So this controls the "reactiveness" of the derived asset pool to the layer1 trade volume.
  • MaxAnchorBlocks: Number of blocks that are summed to get total pool slip. This is the number used to be applied to MaxAnchorSlip
  • TORAnchor-<Asset>#: Enables an asset to be used in the TOR price calculation

LP Management

  • PauseLP#: Pauses the ability for LPs to add/remove liquidity
  • PauseLP<chain>#: Pauses the ability for LPs to add/remove liquidity, per chain
  • MaximumLiquidityRune#: Maximum RUNE capped on the pools known as the ‘soft cap’
  • LiquidityLockUpBlocks: The number of blocks LP can withdraw after their liquidity
  • PendingLiquidityAgeLimit: The number of blocks the network waits before initiating pending liquidity cleanup. Cleanup of all pools lasts for the same duration.

Chain Management

  • HaltChainGlobal#: Pause observations on all chains (chain clients)
  • HaltTrading: Stops swaps and additions, if done, will result in refunds. Observations still occur.
  • Halt<chain>Chain#: Pause a specific blockchain
  • NodePauseChainGlobal#: Individual node controlled means to pause all chains
  • NodePauseChainBlocks: Number of block a node operator can pause/resume the chains for
  • BlocksPerYear: Blocks in a year
  • MaxUTXOsToSpend#: Max UTXOs to be spent in one block
  • MinimumNodesForBFT: Minimum node count to keep the network running. Below this, Ragnarök is performed

Fee Management

  • NativeTransactionFee: RUNE fee on all on chain txs
  • TNSRegisterFee: Registration fee for new THORName, in RUNE
  • TNSFeeOnSale: fee for TNS sale in basis points
  • TNSFeePerBlock: per block cost for TNS, in RUNE
  • PreferredAssetOutboundFeeMultiplier*: The multiplier of the current preferred asset outbound fee, if RUNE balance > multiplier * outbound_fee, a preferred asset swap is triggered
  • MinOutboundFeeMultiplierBasisPoints*: Minimum multiplier applied to base outbound fee charged to user, in basis points
  • MaxOutboundFeeMultiplierBasisPoints: Maximum multiplier applied to base outbound fee charged to user, in basis points

Solvency Checker

  • StopSolvencyCheck#: Enable/Disable Solvency Checker
  • StopSolvencyCheck<chain>#: Enable/Disable Solvency Checker, per chain
  • PermittedSolvencyGap: The amount of funds permitted to be "insolvent". This gives the network a little bit of "wiggle room" for margin of error

Node Management

  • MinimumBondInRune*: Sets a lower bound on bond for a node to be considered to be churned in
  • ValidatorMaxRewardRatio*: the ratio to MinimumBondInRune at which validators stop receiving rewards proportional to their bond
  • MaxBondProviders*: Maximum number of bond providers per mode
  • NodeOperatorFee*: Minimum node operator fee
  • SignerConcurrency*: Number of concurrent signers for active and retiring vaults

Yggdrasil Management

Note

Yggdrasil Vaults are deprecated since ADR-002.

  • YggFundLimit: Funding limit for yggdrasil vaults (percentage)
  • YggFundRetry*: Number of blocks to wait before attempting to fund a yggdrasil again
  • StopFundYggdrasil#: Enable/Disable yggdrasil funding
  • ObservationDelayFlexibility*: Number of blocks of flexibility for a validator to get their slash points taken off for making an observation
  • PoolDepthForYggFundingMin*: The minimum pool depth in RUNE required for ygg funding
  • MinimumNodesForYggdrasil: No yggdrasil vaults if THORNode have less than 6 active nodes

Slashing Management

  • LackOfObservationPenalty: Add two slash points for each block where a node does not observe
  • SigningTransactionPeriod: How many blocks before a request to sign a tx by yggdrasil pool, is counted as delinquent.
  • DoubleSignMaxAge: Number of blocks to limit double signing a block
  • FailKeygenSlashPoints: Slash for 720 blocks, which equals 1 hour
  • FailKeysignSlashPoints: Slash for 2 blocks
  • ObserveSlashPoints: The number of slashpoints for making an observation (redeems later if observation reaches consensus)
  • ObservationDelayFlexibility: Number of blocks of flexibility for a validator to get their slash points taken off for making an observation
  • JailTimeKeygen: Blocks a node account is jailed for failing to keygen. DO NOT drop below TSS timeout
  • JailTimeKeysign: Blocks a node account is jailed for failing to keysign. DO NOT drop below TSS timeout

Churning

  • AsgardSize*: Defines the number of members to an Asgard vault
  • MinSlashPointsForBadValidator: Minimum quantity of slash points needed to be considered "bad" and be marked for churn out
  • BondLockupPeriod: Lockout period that a node must wait before being allowed to unbond
  • ChurnInterval*: Number of blocks between each churn
  • HaltChurning: Pause churning
  • DesiredValidatorSet: Maximum number of validators
  • FundMigrationInterval*: Number of blocks between attempts to migrate funds between asgard vaults during a migration
  • NumberOfNewNodesPerChurn#: Number of targeted additional nodes added to the validator set each churn
  • BadValidatorRedline*: Redline multiplier to find a multitude of bad actors
  • LowBondValidatorRate: Rate to mark a validator to be rotated out for low bond
  • MaxNodeToChurnOutForLowVersion*: Maximum number of validators to churn out for low version each churn

Economics

  • EmissionCurve*: How quickly rune is emitted from the reserve in block rewards
  • IncentiveCurve*: The split between nodes and LPs while the balance is optimal
  • MaxAvailablePools: Maximum number of pools allowed on the network. Gas pools (native pools) are excluded from this.
  • MinRunePoolDepth*: Minimum number of RUNE to be considered to become active
  • PoolCycle*: Number of blocks the network will churn the pools (add/remove new available pools)
  • StagedPoolCost: Number of RUNE (1e8 notation) that a stage pool is deducted on each pool cycle.
  • KillSwitchStart*: Block height to start to kill BEP2 and ERC20 RUNE
  • KillSwitchDuration: Duration (in blocks) until switching is deprecated
  • MinimumPoolLiquidityFee: Minimum liquidity fee an active pool should accumulate to avoid being demoted, set to 0 to disable demote pool based on liquidity fee
  • MaxRuneSupply*: Maximum supply of RUNE

Miscellaneous

  • DollarsPerRune: Manual override of number of dollars per one RUNE. Used for metrics data collection and RUNE calculation from MinimumL1OutboundFeeUSD
  • THORNames: Enable/Disable THORNames
  • TNSRegisterFee: TNS registration fee of new names
  • TNSFeePerBlock: TNS cost per block to retain ownership of a name
  • ArtificialRagnarokBlockHeight: Triggers a chain shutdown and ragnarok
  • NativeTransactionFee: The RUNE fee for a native transaction (gas cost in 1e8 notation)
  • HALTSIGNING<chain>#: Halt signing in a specific chain
  • HALTSIGNING#: Halt signing globally
  • Ragnarok-<Asset>#: Ragnaroks a specific pool

Router Upgrading (DO NOT TOUCH!)

Old keys (pre 1.94.0)

  • MimirRecallFund: Recalls Chain funds, typically used for router upgrades only
  • MimirUpgradeContract: Upgrades contract, typically used for router upgrades only

New keys (1.94.0 and on)

  • MimirRecallFund<CHAIN>: Recalls Chain funds, typically used for router upgrades only
  • MimirUpgradeContract<CHAIN>: Upgrades contract, typically used for router upgrades only

How to add a new chain

On a high level, this is how THORChain interact with external chains

images/newchain.png

For those chains that using cosmos sdk, and has IBC enabled, should be able to integrate with THORChain using IBC, at the moment, IBC is not enabled on THORChain yet.

In order to add a new chain to THORChain, there are a few changes you will need to make.

  • Thornode changes
  • Bifrost changes
  • Node launcher changes
  • Smoke test changes (heimdall)
  • xchainjs changes

Note: At the moment, THORChain only support ECDSA keys, ED25519 will be supported in the near future, you can keep track the progress from here

Thornode changes

There are some changes need to be made in Thornode, detail as following

filefunclogic
common/address.gofunc NewAddress(address string) (Address, error)Add logic to parse an address
common/chain.gofunc (c Chain) GetGasAsset() AssetReturn gas asset for the chain
common/chain.godefine a chain variable at the toplike https://gitlab.com/thorchain/thornode/-/blob/develop/common/chain.go#L22
common/gas.gofunc UpdateGasPrice(tx Tx, asset Asset, units []cosmos.Uint) []cosmos.Uintadd logic in regards to how to update gas
common/asset.godefine an assetlike https://gitlab.com/thorchain/thornode/-/blob/develop/common/asset.go#L22
common/pubkey.gofunc (pubKey PubKey) GetAddress(chain Chain) (Address, error)add logic to get address from a pubic key
build/docker/components/newchain.ymldocker composer file to run the chain client , run it in regtest mode , so as the client will be used for mocknet test
build/docker/components/validator.ymlUpdate the files according to run chain client in docker composer, used it for test purpose
build/docker/components/validator.linux.yml
build/docker/components/standalone.base.yml
build/docker/components/standalone.linux.yml

Node launcher changes

Node launcher is the repository used to launch thorchain node, https://gitlab.com/thorchain/devops/node-launcher.git

  1. Create a new folder under the root folder, like "newchain-daemon"
  2. Add new helm chart to run the chain client daemon
  3. Make sure the autoscaling capabilities are still enough on the max nodes configuration.

Bifrost changes

Bifrost is a key component in THORChain, it is a bridge between THORChain and external chains

  1. First, create a new folder under bifrost\pkg\chainclients
  2. Implement interface ChainClient interface, refer to here
// ChainClient is the interface that wraps basic chain client methods
//
// SignTx       signs transactions
// BroadcastTx  broadcast transactions on the chain associated with the client
// GetChain     get chain
// GetHeight    get chain height
// GetAddress   gets address for public key pool in chain
// GetAccount   gets account from thorclient in cain
// GetConfig    gets the chain configuration
// GetConfirmationCount given a tx in , return the number of blocks it need to wait for confirmation
// ConfirmationCountRead given a tx in , return true/false to indicate whether the tx in is ready to be confirmed
// IsBlockScannerHealthy return true means the blockscanner is healthy ,false otherwise
// Start
// Stop
type ChainClient interface {
 SignTx(tx stypes.TxOutItem, height int64) ([]byte, error)
 BroadcastTx(_ stypes.TxOutItem, _ []byte) (string, error)
 GetHeight() (int64, error)
 GetAddress(poolPubKey common.PubKey) string
 GetAccount(poolPubKey common.PubKey) (common.Account, error)
 GetAccountByAddress(address string) (common.Account, error)
 GetChain() common.Chain
 Start(globalTxsQueue chan stypes.TxIn, globalErrataQueue chan stypes.ErrataBlock)
 GetConfig() config.ChainConfiguration
 GetConfirmationCount(txIn stypes.TxIn) int64
 ConfirmationCountReady(txIn stypes.TxIn) bool
 IsBlockScannerHealthy() bool
 Stop()
}
  1. implement interface BlockScannerFetcher in the chain client you implement

// BlockScannerFetcher define the methods a block scanner need to implement
type BlockScannerFetcher interface {
    // FetchMemPool scan the mempool
    FetchMemPool(height int64) (types.TxIn, error)
    // FetchTxs scan block with the given height
    FetchTxs(height int64) (types.TxIn, error)
    // GetHeight return current block height
    GetHeight() (int64, error)
}

  1. update bifrost/pkg/chainclients/loadchains.go to initialise new chain client

This is a sample PR to add bitcoin cash support, in thornode & bifrost

https://gitlab.com/thorchain/thornode/-/merge_requests/1395

New Chain Integrations

Integrating a new chain into THORChain is an inherently risky process. THORChain inherits the risks (and value) from each chain it connects. Node operators take on risk and cost by adding new chains. Chains should be economically-significant, acceptable risk, and reasonable cost to be considered.

Phase I: Data Gathering and Initial Proposal

Chains should meet a minimum standard to remain listed on THORChain.

  • meet initial listing standards
  • meet pool depth, volume, LP count requirements

A chain that changes its characteristics for the worse, or drops in uptake on THORChain may cause the following issues:

  • become centralised and introduce a risk to the network
  • lose adoption and thus be costly to subsidise for the network

Chains should meet a minimum standard of the following before being listed on THORChain.

  • decentralisation
  • ossification
  • economic value
  • developer support
  • community support

Chain Consequences:

A chain that fails on THORChain may have the following affects:

  • Infinite Mint bug causes theft of pooled assets from LPs
  • Impact to reliability of THORNodes (poor sync, halted churn, double-spend txOuts)
  • Poor LP uptake causes low fee revenue for that chain
  • Waste of developer resources to support the chain
  • Disruption to THORChain when Ragnaroking the chain

Detailed Requirements:

A new chain to be added should meet the following requirements:

Decentralisation

  • Must not be controlled by a single entity that can pause the network or freeze accounts.
  • Must not be controlled by a multisig < 10 signatories
  • If PoS, should have more than 10 Validators

Ossification

  • Must not be younger (since genesis) than 2 years
  • Must not be hard-forking more than once per 6 months

Economic Value

  • Must not be less than 10% of THORChain's FDV
  • Must have existing daily volumes not less than 10% of $RUNE volumes
  • If PoW, must not take longer than 1hour to conf-count a $1k swap

Developer Support

  • Must demonstrate organic developer support
  • Must have functioning node client + wallet js client

Community

  • Must have users that exceed 10% of THORChain's on-chain users

Removing Chains

Chains should meet a minimum standard to remain listed on THORChain.

  • meet initial listing standards
  • meet pool depth, volume, LP count requirements

A chain that changes its characteristics for the worse, or drops in uptake on THORChain may cause the following issues:

  • become centralised and introduce a risk to the network
  • lose adoption and thus be costly to subsidise for the network

A chain should be purged from THORChain if any of the following are sustained over a 6 month period:

  • Breach any of New Chain Standards set out above
  • Have a base asset pool depth that drops below MINRUNEPOOLDEPTH
  • Have daily volumes that drop below $1k for an entire POOLCYCLE
  • Have less than 100 LPs

Proposal of a New Chain:

New chain is proposed in #propose-a-chain, and a new channel created under “Community Chains” in Discord. This is an informal proposal, and should loosely follow the template under Chain Proposal Template.

Node Mimir Vote:

Prompt Node Operators to vote on Halt<Proposed-Chain>Chain=1 view Node Mimir. If a 50% consensus is reached then development of the chain client can be started.

Phase II: Development, Testing, and Auditing

  1. Chain Client Development Period: Community devs of the Proposed Chain build the Bifrost Chain Client, and open a PR to thornode (referencing the Gitlab issue created in the discussion phase), and node-launcher repos.

    1. All PRs should meet the public requirements set forth in Technical Requirements and Guidelines.
  2. Stagenet Merge/Baking Period: Community devs are incentivized to test all necessary functionality as it relates to the new chain integration. Any chain on stagenet that is to be considered for Mainnet will have to go through a defined baking/hardening process set forth

Functionality to be tested:

  • Swapping to/from the asset
  • Adding/withdrawing assets on the chain
  • Minting/burning synths
  • Registering a thorname for the chain
  • Vault funding
  • Vault churning
  • Inbound addresses returned correctly
  • Insolvency on the chain halts the chain
  • Unauthorised tx on the chain (double-spend) halts the chain
  • Chain client does not sign outbound when HaltSigning<Chain> is enabled

Usage requirements:

  • 100 inbound transactions on stagenet
  • 100 outbound transactions on stagenet
  • 100 RUNE of aggregate add liquidity transactions on stagenet
  • 100 RUNE of aggregate withdraw liquidity transactions on stagenet
  1. Chain Client Audit: An expert of the chain (that is not the author) must independently review the chain client and sign off on the safety and validity of its implementation. The final audit must be included in the chain client Pull Request under bifrost/pkg/chainclients/<chain-name>.

Phase III: Mainnet Release

The following steps will be performed by the core team and Nine Realms for the final rollout of the chain.

  1. Admin Mimir: Halt the new chain and disable trading until rollout is complete.

  2. Daemon Release and Sync: Announcement will be made to NOs to make install in order to start the sync process for the new chain daemon.

  3. Enable Bifrost Scanning: The final node-launcher PR will be merged, and NOs instructed to perform a final make install to enable Bifrost scanning.

  4. Admin Mimir: Unhalt the chain to enable Bifrost scanning.

  5. Admin Mimir: Enable trading once nodes have scanned to the tip on the new chain.


Technical Requirements and Guidelines

A new Chain Integration must include a pull request to thornode (referencing the Gitlab issue created in the discussion phase) and node-launcher.

Thornode PR Requirements

  1. Ensure a "mocknet" (local development network) service for the chain daemon is be added (build/docker/docker-compose.yml).
  2. Ensure 70% or greater unit test coverage.
  3. Ensure a <chain>_DISABLED environment variable is respected in the Bifrost initialization script at build/scripts/bifrost.sh.
  4. Lead a live walkthrough (PR author) with the core team, Nine Realms, and any other interested community members. During the walkthrough the author must be able to speak to the questions in (#chain-client-implementation-considerations).
  5. Can an inbound transaction be "spoofed" - i.e. can the Chain Client be tricked into thinking value was transferred into THORChain, when it actually was not?
  6. Does the chain client properly whitelist valid assets and reject invalid assets?
  7. Does the chain client properly convert asset values to/from the 8 decimal point standard of thornode?
  8. Is gas reporting deterministic? Every Bifrost must agree, or THORChain will not reach consensus.
  9. Does the chain client properly report solvency of Asgard vaults?

Node Launcher PR Requirements

There should be 3 PRs in the node-launcher repo - the first to add the Docker image for the chain daemon, the second to add the service, the third to enable scanning in Bifrost. The first must be merged first so that hashes from the image builds may be pinned in the second.

  1. Image PR
    1. Add a Dockerfile at ci/images/<chain>/Dockerfile.
    2. Ensure all source versions in the Dockerfile are pinned to a specific git hash.
  2. Services PR
    1. Use an existing chain directory as a template for the new chain daemon configuration, reference the PR for the last added chain.
    2. Ensure the resource request sizes for the daemon are slightly over-provisioned (~20%) to the average expected utilization under normal operation.
    3. Extend the get_node_service function in scripts/core.sh with the service so that it is available for the standard make targets.
    4. Extend the deploy_fullnode function in scripts/core.sh with --set <daemon-name>.enabled=false in both the diff and install commands.
    5. Ensure the <chain>_DISABLED environment variable is used to disable the chain via a variable in bifrost/values.yaml.
  3. Enable PR
    1. Update bifrost/values.yaml to enable the chain.

Chain Proposal Template

Chain Name:
Chain Type: EVM/UTXO/Cosmos/Other
Hardware Requirements: Memory and Storage
Year started:
Market Cap:
CoinMarketCap Rank:
24hr Volume:
Current DEX Integrations:
Other relevant dApps:
Number of previous hard forks:

order: 1 parent: order: false

Architecture Decision Records (ADR)

This is a location to record all high-level architecture decisions in the THORChain project.

You can read more about the ADR concept in this blog post.

For contributors, please see the PROCESS page for instructions on managing an ADR's lifecycles.

An ADR should provide:

  • Context on the relevant goals and the current state
  • Proposed changes to achieve the goals
  • Summary of pros and cons
  • References
  • Changelog

Note the distinction between an ADR and a spec. The ADR provides the context, intuition, reasoning, and justification for a change in architecture, or for the architecture of something new. The spec is much more compressed and streamlined summary of everything as it stands today.

If recorded decisions turned out to be lacking, convene a discussion, record the new decisions here, and then modify the code to match.

Note the context/background should be written in the present tense.

Table of Contents

Implemented

None

Accepted

Deprecated

None

Rejected

None

Proposed

On Pause

ADR Creation Process

  1. Copy the TEMPLATE.md file. Use the following filename pattern: adr-next_number-title.md
  2. Create a draft Pull Request if you want to get an early feedback.
  3. Make sure the context and a solution is clear and well documented.
  4. Add an entry to a list in the README file.
  5. Create a Pull Request to propose a new ADR.

ADR life cycle

ADR creation is an iterative process. Instead of trying to solve all decisions in a single ADR pull request, we MUST firstly understand the problem and collect feedback through a GitHub Issue.

  1. Every proposal SHOULD start with a new GitHub Issue or be a result of existing Issues. The Issue should contain just a brief proposal summary.
  2. Once the motivation is validated, a GitHub Pull Request (PR) is created with a new document based on the TEMPLATE.md.
  3. An ADR doesn't have to arrive to master with an accepted status in a single PR. If the motivation is clear and the solution is sound, we SHOULD be able to merge it and keep a proposed status. It's preferable to have an iterative approach rather than long, not merged Pull Requests.
  4. If a proposed ADR is merged, then it should clearly document outstanding issues either in ADR document notes or in a GitHub Issue.

The PR SHOULD always be merged. In the case of a faulty ADR, we still prefer to merge it with a rejected status. The only time the ADR SHOULD NOT be merged is if the author abandons it.

Merged ADRs SHOULD NOT be pruned.

ADR status

Status has two components:

{CONSENSUS STATUS} {IMPLEMENTATION STATUS}

IMPLEMENTATION STATUS is either Implemented or Not Implemented.

Consensus Status

DRAFT -> PROPOSED -> LAST CALL yyyy-mm-dd -> ACCEPTED | REJECTED -> SUPERSEDED by ADR-xxx
                  \        |
                   \       |
                    v      v
                     ABANDONED
  • DRAFT: [optional] an ADR which is work in progress, not being ready for a general review. This is to present an early work and get an early feedback in a Draft Pull Request form.
  • PROPOSED: an ADR covering a full solution architecture and still in the review - project stakeholders haven't reached an agreed yet.
  • LAST CALL <date for the last call>: [optional] clear notify that we are close to accept updates. Changing a status to LAST CALL means that social consensus (of Cosmos SDK maintainers) has been reached and we still want to give it a time to let the community react or analyze.
  • ACCEPTED: ADR which will represent a currently implemented or to be implemented architecture design.
  • REJECTED: ADR can go from PROPOSED or ACCEPTED to rejected if the consensus among project stakeholders will decide so.
  • SUPERSEDED by ADR-xxx: ADR which has been superseded by a new ADR.
  • ABANDONED: the ADR is no longer pursued by the original authors.

Language used in ADR

  • The context/background should be written in the present tense.
  • Avoid using a first, personal form.

ADR {ADR-NUMBER}:

Changelog

  • {date}: {changelog}

Status

An architecture decision is considered "proposed" when a PR containing the ADR is submitted. When merged, an ADR must have a status associated with it, which must be one of: "Accepted", "Rejected", "Deprecated" or "Superseded".

An accepted ADR's implementation status must be tracked via a tracking issue, milestone or project board (only one of these is necessary). For example:

Accepted

[Tracking issue](https://gitlab.com/thorchain/thornode/issues/123)
[Milestone](https://gitlab.com/thorchain/thornode/milestones/123)
[Project board](https://gitlab.com/orgs/thorchain/projects/123)

Rejected ADRs are captured as a record of recommendations that we specifically do not (and possibly never) want to implement. The ADR itself must, for posterity, include reasoning as to why it was rejected.

If an ADR is deprecated, simply write "Deprecated" in this section. If an ADR is superseded by one or more other ADRs, provide local a reference to those ADRs, e.g.:

Superseded by [ADR 123](./adr-123.md)

Accepted | Rejected | Deprecated | Superseded by

Context

This section contains all the context one needs to understand the current state, and why there is a problem. It should be as succinct as possible and introduce the high level idea behind the solution.

Alternative Approaches

This section contains information around alternative options that are considered before making a decision. It should contain a explanation on why the alternative approach(es) were not chosen.

Decision

This section records the decision that was made. It is best to record as much info as possible from the discussion that happened. This aids in not having to go back to the Pull Request to get the needed information.

Detailed Design

This section does not need to be filled in at the start of the ADR, but must be completed prior to the merging of the implementation.

Here are some common questions that get answered as part of the detailed design:

  • What are the user requirements?
  • What systems will be affected?
  • What new data structures are needed, what data structures will be changed?
  • What new APIs will be needed, what APIs will be changed?
  • What are the efficiency considerations (time/space)?
  • What are the expected access patterns (load/throughput)?
  • Are there any logging, monitoring or observability needs?
  • Are there any security considerations?
  • Are there any privacy considerations?
  • How will the changes be tested?
  • If the change is large, how will the changes be broken up for ease of review?
  • Will these changes require a breaking (major) release?
  • Does this change require coordination with the SDK or other?

Consequences

This section describes the consequences, after applying the decision. All consequences should be summarized here, not just the "positive" ones.

Positive

Negative

Neutral

References

Are there any relevant PR comments, issues that led up to this, or articles referenced for why we made the given design choice? If so link them here!

  • {reference link}

ADR 001: ThorChat

Changelog

  • 2022-05-07: Created

Status

Paused

Context

Node operator communications are currently conducted over Discord (a centralized service) and are asymmetrical in nature. In order to preserve privacy NOs are encouraged to use make relay which uses a relay bot to post messages into a Discord channel. This leads to a suboptimal communication style.

This document outlines a replacement for that communication channel: ThorChat. ThorChat is a standard opensource chat daemon (MatterMost, IRC, etc.) accessible only via a Tor Hidden Service. Node operators will authenticate using a make go-chat that automatically creates an account based on the node pubkey, and have special access to public channel where only NOs/core community members can talk, for coordinating network operation.

Alternative Approaches

  • Tox - Use a group text chat on Tox.
    • Pros:
      • Even more decentralized.
    • Cons:
      • Less featureful UX.
      • No browser client, requires new app on user side (most written in C).
      • Higher bar for outside community members to jump on and observe.

Decision

TBD

Detailed Design

Architecture

ThorChat will be a four+ pod deployment:

  • Tor relay for terminating the Hidden Service.
  • Nginx for locking down handler paths & caching.
  • Chat server (Mattermost or IRC)
  • Database/storage pod(s) - primary/secondaries as needed for scale.

New development

There are only a few greenfield components needed:

  • Auth plugin for taking signed message from make go-chat and creating/updating specially tagged node operator account in the backend.
  • Audit of chosen chat server codebase for security suitability.

Risks

A chat service's primary operational risks are:

  • Spam/DoS control
  • Account takeover
  • RCE

These pose an additional level of risk in the proposed application, as takeover of the chat server here (by large scale account takeover, or RCE) allows for possible social engineering of Thorchain decisions.

Benefits

This system would allow for simple and more seamless node operator communications, with a minimal burden on participants. Only a Tor-capable browser is required (TorBrowser, the Tor feature in Brave, etc.) and anonymity is not only preserved, it is enforced.

More significantly, node operators will have a significantly better comms experience - reply notifications, presence status, ability to use reacts. Simple polls can be taken with reacts, or plugins could be added for polling/feedback collection, etc.

Operations

The operational burden would be the cost of running the aforementioned services, plus the requisite observability/uptime/on-call duties.

Coordination

As this is a new component separate from the existing thornode network, coordination for rollout of this service is minimal. It would primarily be social, helping make node operators aware of the new primary comms channel and how to access it.

Open Questions

Choice of chat server

MatterMost or an IRC daemon? The tradeoff between these two is feature set vs. security footprint, respectively. Author leans towards MatterMost as it best recreates the current Discord UX, but acknowledges it will require more work to audit and lock down.

Maintenance of Discord

The dependent questions:

  • Shift to using this new chat system for all community discussion vs. just node operator discussion?
  • Maintain the current Discord in parallel or only use as a hot spare?

Consequences

Positive

  • Increased engagement & interaction between/with node operators.
  • Reduced centralization threat.
  • Reduced dependency on corporate infrastructure.

Negative

  • Additional system ops/maintenance burden.
  • Increased attack surface area.

Neutral

  • Split communications, if Discord is still preferred for the general community channels.

References

  • TODO: list of Discord outages.

ADR 002: REMOVE YGG VAULTS

Changelog

  • {date}: {changelog}

Status

Accepted

Context

There are two types of vaults in THORChain:

  • cold, inbound vaults "asgard" using TSS
  • hot, outbound vaults "yggdrasil" using 1of1

This is primarily a result of TSS limitations. During the initial build of THORChain it was found that TSS's quadratic scaling problem (signing times increase quadratically with the size of member committee) would set a limit on the number of outbounds per second.

Key extract from [TSS Benchmark 2020] (https://github.com/thorchain/Resources/blob/master/Whitepapers/THORChain-TSS-Benchmark-July2020.pdf)

image.png

Thus each node retains a 1of1 key to fast-sign outbounds. If they fail to sign, they are slashed and the tx re-delegated to an Asgard. THORChain with 100 nodes can do 100 outbounds a sec due to 100 ygg vaults. The vault funds are secured via economic security, but the following are outstanding (managed) problems:

  1. Code complexity with ygg vaults have in past opened up exploit loopholes
  2. Ygg vaults often go insolvent and lock node bonds
  3. Increased vault-management costs (ygg funding)
  4. Complexity in router upgrades requires logic to recall YggFunds

** Vault Costs ** Ygg funding is one of the larger expenses that increase THORChain's vault management costs: image-1.png

However, 3 things have happened since:

  1. Asgard Vault Sharding logic, introduced in Q2 2021, split asgards to allow scaling past 40 nodes
  2. Synths, launched in Q1 2022, absorbed a significant part of arbitrage volume, reducing demand on L1 outbounds
  3. PoolDepthForYggFundingMin, launched in Q1 2022, retained low-depth pools entirely in asgard vaults to prevent significant splitting of funds

As a result, the system has shown stability and reliability with multiple Asgards (and some pools are entirely asgard-managed), and L1 outbounds have reduced. Thus an opportunity presents itself to remove YggVaults entirely and rely only on Asgards, with no code change required. To do this

  1. STOPFUNDYGGDRASIL = 1 to stop yggs being funded (node churns will slowly empty yggs back to Asgard)
  2. AsgardSize = 20 to reduce asgard size down to 20

Formula to compare new Asgard performance:

throughputMultiplier  = (oldSize^2 / newSize^2) * (oldSize / newSize)

Comparing with 15 seconds per TSS key-sign for 27 nodes (observed performance):

  • 40: 1x at 3 vaults, 0.3tx/sec
  • 20: 8x at 6 vaults, 2tx/sec
  • 16: 15x at 7 vaults, 3tx/sec
  • 14: 23x at 8 vaults, 5tx/sec
  • 12: 37x at 9 vaults, 7tx/sec
  • 9: 87x at 12 vaults, 17tx/sec
  • 6: 296x at 18 vaults, 60tx/sec
  • 3: 2370x at 35 vaults, 474tx/sec <- thought bubble: is there a reason to not do this?

Note1: increasing the count of asgard does not change the economic security of the funds (funds always 100% secured), nor does it change bandwidth requirements, since the same observations are required. The only consideration is fund-splitting. Ie, if someone could demand 10% of the funds of a pool, then TC should be able to fulfil in one tx, (not two from two vaults). So this puts the "limit" at 10 vaults (each with 10% of the funds). But likely users will only demand 2% of the funds of most pools, most of the time. (2% swap is a big swap, and there are only 7/4215 BTC LPs with more than 2% LP). So 50 vaults is "ok", which means AsgardSize = 3 is theoretically ok to attempt.

Note2: the network currently signs at 0.14tx/sec (so AsgardSize = 40 can tolerate current usage, thus AsgardSize = 20 can tolerate an 8x increase in L1 demand).

Note3: Although the network can currently sign at 100tx/sec (100 ygg vaults), it's never been required. Wherever there is a huge gap in performance required vs performance available, almost always compromises can be reclaimed. Here it is complexity and vaulting costs.

Decision

Pros:

  • Removal of complex Ygg Vault logic (the simpler the code, the easier to learn, maintain and reduce attack vectors)
  • Cheaper vaulting costs
  • Removal of ygg insolvencies
  • THORNodes are now 100% non-custodial (no 1of1 keys), means pooledNodes can be encouraged and lightNodes can be explored again

Cons:

  • Reduction in L1 output (100 tx/sec down to 2tx/sec)
  1. Put out for a temperature check
  2. If well-received, then go via admin-mimir (not node-mimir because this is a network-security optimisation, and it may need to be quickly rolled back in case issues)

admin-mimir

  1. STOPFUNDYGGDRASIL = 1 to stop yggs being funded (node churns will slowly empty yggs back to Asgard)
  2. AsgardSize = 20 to reduce asgard size down to 20

Consequences

This will retain ygg-code, but effectively disable the feature. Throughput will decrease, but more than enough (8x margin) with current demand levels.

If issues are observed:

  1. Roll back to ygg-vaults
  2. Double-down and reduce asgardSize even further

Positive

Negative

Neutral

References

  • https://github.com/thorchain/Resources/blob/master/Whitepapers/THORChain-TSS-Benchmark-July2020.pdf
  • https://app.flipsidecrypto.com/dashboard/2022-05-29-network-fees-versus-churn-costs-comparison-b0ynW-

ADR 003: FLOORED OUTBOUND FEE

Changelog

  • {date}: {changelog}

Status

Accepted

Context

Recently ADR-002 Remove Ygg Vaults was passed, which deprecated ygg-vaults and made all outbound TX go thru TSS. This has increased the computational cost on the network to process L1 swaps. Additionally, concerns about chain bloat remain extant; each L1 swap requires a minimum of twice the number of nodes in witness tx, with some large tx up to 3 times. This is around 0.1kb in blockstate (each tx around 300bytes, (3 * 120 * 300 = 0.1kb)), which is kept around indefinitely until the chain hard forks and flattens history (once a year).

Thus an L1 Swap "cost" to the network is:

  • up to 360 state tx
  • 14 nodes in TSS for 10 seconds

The vast majority of swaps on THORChain are arbitrage across pools (this is to be expected with liquidity pools). Synth swaps are available to arbitrage agents and are a single transaction. In comparison a synth swap is at least 360 times cheaper in computation cost than an L1 swap, and should be the preferred swap for the majority of arbs.

Thus an L1 swap should have a minimum fee of 10-100 times more than a synth swap, since it has a cost to the network two orders of magnitude more than a synth swap. A synth swap costs 0.02 RUNE (the cost to perform a tx on THORChain), so an L1 swap should be 1-2 RUNE minimum.

Currently, BNB L1 swaps are cheaper to swap than its own synth, around (0.000075 * $200 = 1.5c), compared to a synth swap of (0.02 * $2 = 4c). It has been observed (and confirmed after discussions with some known arb teams on THORChain) that is this is one of the reasons why BNB L1 swaps are done over BNB synth swaps.

Proposal

Floor the outboundFee to a value which is:

  1. 10-100 times higher than a synth swap
  2. Commensurate with the state storage costs for 0.1kb for 12 months, as well as the compute costs for 14 nodes in TSS for 10 seconds

The value that achieves both is around $1.00. Since the cost of an L1 swap is not linked to RUNE value, it makes more sense to use THORChain's USD-sensing logic to peg this fee to a fixed USD value.

In most other chains the outboundFee charged typically lands in excess of $1.00, so this proposal will only affect the cheap chains, such as BNB.

  • BTC, 10sat/byte, $1.5
  • ETH, 10gwei, $2

** Implementation **

The existing OutboundTransactionFee is used to charge the RUNE value for toRUNE swaps, as well as the synth value for toSYNTH swaps and is currently set to 0.02RUNE. This can be kept.

A new constant of minimumL1OutboundFeeUSD should be added to be 1_0000_0000, which would be read as $1.00.

Charge the outboundFee, then

  • convert to USD value
  • if it is less than minimumL1OutboundFeeUSD, raise it

Use THORChain's USD sensing logic to determine what $1.00 is.

Decision

The following stakeholders and their potential perspectives are discussed:

** Nodes ** Nodes should support, since the network is now raising fees to match their costs (compute and long-term storage), as well as push more swaps to synths and thus reduce demand on L1 witnessing (less slashes for missing observations)

** LPs ** Long-term LPs should support, since the network is raising fees to bolster the RESERVE (their long-term revenue support)

** Transient Swappers ** Transient Swappers should largely be unaffected, since outboundFees on most chains are higher than $1.00, and $1.00 is a very cheap minimum fee to pay for decentralised swaps.

** Arbers ** Arbitrage agents should largely be neutral, since they have the option to switch to using synths which has much lower fees (and is faster).

ADR 004: Keyshare Backups


Changelog

  • 2022-07-15

Status

Accepted


Context

Vaults containing all network funds are composed of keyshares generated by the member nodes of an Asgard at each churn interval, and stored on Bifrost's persistent disk. There are a number of factors to consider that could result in the complete loss of this file that we must consider, to name a few:

  1. Compromised (not necessarily malicious) infrastructure, tooling, operator machines
  2. Forced provider shutdown due to censorship, unpaid accounts, etc
  3. Human error during operation

In order to ensure there is no period of time in which loss of keyshares would incur loss of network funds, operators must immediately back up their keyshares after each churn. Currently the official mechanism for this backup is the utility command make backup in the node-launcher repo, which will copy the keyshares to the operator's local machine. This approach requires responsive and proactive node operators to continuously backup to protect the network, and there is no way for external persons to verify existence of node backups.

Since moving away from Yggdrasil vaults in favor of a greater number of Asgards, some risk is reduced since loss of funds requires losing a supermajority of members, but risk remains. In the ideal scenario, a node operator should be able to securely backup only their mnemonic once and leverage it to recover their node and any corresponding funds.


Decision

TBD - there have been many discussions around this, and the options listed in alternatives are still relevant.


Detailed Design

The proposed design extends the TssPool message sent after vault creation to include a keyshares_backup field, which contains the bytes for the newly created keyshares after churn, compressed with lzma (to reduce chain bloat), and symmetrically encrypted using the node's mnemonic as the passphrase (the same mnemonic generated at node creation used for the thornode private key). The initial pass of this implementation began before the introduction of the ADR process and is currently under review at https://gitlab.com/thorchain/thornode/-/merge_requests/2235. These keyshares will intentionally skip storage in a KV store in the thornode application state to avoid further bloat, instead a CLI utility will be provided to via tci to pull and decrypt the latest keyshare backup for the node from an RPC endpoint, via tci nodes recover-keyshares --address <node-address>

Checks

Sanity checks against mnemonics before encryption:

  1. Validate BIP39 mnemonic.
  2. Validate the entropy of the byte-wise probability distribution of the mnemonic (greater than the minimum of 1e8 randomly generated mnemonics).

Sanity checks against encrypted payload before send:

  1. Check that encrypted output is not equal to input.
  2. Check that decrypted output equals the input.
  3. Check that the output does NOT contain the input.
  4. Check that the output does NOT contain the passphrase.
  5. Check that the output does NOT contain any word of the passphrase.

Positives

  1. Publishing the encrypted keyshares to the chain allows anyone to verify that a sufficient number of keyshares have been preserved such that loss of funds is not possible, so long as NOs have backed up their mnemonic.
  2. Embedding the shares in the TssPool messages ensures that the shares are preserved immediately at the time of creation.

Negatives

  1. Although we compress the shares before encrypting to reduce size, this results in some bloat in chain state. This size is dependent on the number of members in an Asgard, but is on the order of 100Kb in current conditions - breaking the same set of nodes into more asgards reduces the aggregate size of this bloat.
  2. Although we add a significant number of checks to prevent it, there is some risk in publishing these keyshares to a location that is publicly visible. Note that most of the vectors we consider a malicious actor could take (infra, supply chain) would result in them having access to the keyshares before they are encrypted and published anyway.

Potential Suggested Modifications

  1. Only backup some sample (like 50%) of the keyshares in this form - this mitigates some of the unease in negative #2, and still provides a safety net to reduce the likelihood of losing funds if a large percentage of the network was lost.

Alternative Approaches

The main tradeoff is whether or not to publish the encrypted payload somewhere publicly visible - this is a positive since any person can verify and backup the encrypted keyshares of nodes, and a negative since publishing this data could potentially carry some security risk and also adds to bloat. We will outline the alternatives under consideration below in 2 categories to represent this tradeoff and ignore it in the positives and negatives - in all cases the backup is encrypted.

Alternative Approaches (Private Backup)

1. Bifrost Sends Encrypted Keyshare to NO Configured Bucket/Email/ETC

We could deploy a Postfix instance in the cluster to send an email with the encrypted shares to an address the NO configures, or have the NO pass in something like an S3 endpoint and auth token that would be used to push them to the target service.

Negatives
  1. Additional setup and reliance on external services (the provider for the mail server, S3 API, etc).

2. Node Operators Get Slashed Until make backup Heartbeat

This would keep the current approach to backup creation and extend make backup to also send a transaction with a "heartbeat" message - after a certain buffer of blocks after the churn, nodes which have not sent the heartbeat will begin receiving slash points.

Negatives
  1. Requires active participation from node operators to secure backups, could still lose funds if nodes were lost before a supermajority of all vaults engage.

3. Node Operators Manage Separate Cron Backup

This would basically require node operators to manage a machine that has persistent authorization to their Kubernetes cluster, and adding TC_NO_CONFIRM=true NAME=thornode make backup to a crontab.

Negatives
  1. Node operator must maintain, monitor, and secure (it has all the keys) the backup machine separately, since it cannot be on the same infrastructure provider as the node, and must have persistent authorization to the cluster in order to create the backups, which creates additional security risk.

4. Bifrost Sends Encrypted Keyshare to Other Active Bifrosts

This would be similar to the proposed design, but Bifrost would be extended to handle distribution of the encrypted keyshares to other active nodes instead of posting them on chain. Recovery would require cooperation from the nodes that held the backup. There could be a variant of this approach to only send keyshares to a subset of other nodes - these nodes could be randomly selected or perhaps the other members of the same vault. An additional variant could extend this pattern with a verification message posted on chain, so that one node could signal to the network that it has persisted the encrypted keyshares of another node.

Negatives
  1. Additional complexity to add more P2P logic into Bifrost.

Alternative Approaches (Public Backup)

1. Bifrost Sends Encrypted Keyshare to IPFS

Same as proposed designed, but we push backups to IPFS and record the key in the TssPool message.

Negatives
  1. Additional dependency, complexity, backup point of failure for IPFS integration.

Open Questions

The following questions are generally relevant for any approach taken.

  1. Symmetric encryption with mnemonic or asymmetric with key (generated from mnemonic)?

    Update: It seems devs are mostly satisfied with currently proposed symmetric approach.

  2. In either case for #1, which encryption library to prefer (stdlib vs something like age)?

    Update: It seems devs are mostly satisfied with currently proposed usage of age.


References

  • ...

ADR 005: Deprecate Impermanent Loss Protection

Changelog

  • December 12, 2022: Initial commit
  • November 07, 2023: Amendment 1 commit

Status

Amended

Amendment 1, Nov 23: Permanently Sunset ILP

As of Blockheight 13,333,333, ILP is as follows:

ETH.FOX 29,683
BTC.BTC 22,984
ETH.XRUNE 18,431
ETH.TGT 1,914
ETH.ETH 1,433
ETH.UOS 814
BNB.TWT 521
ETH.DPI 262
DOGE.DOGE 240
BNB.BNB 199
BNB.BUSD 100
TOTAL: 76,793

Since ILP is effectively negligible to the vast amount of grandfathered users (who have had this protection available to them the entire time, but did not take it), the protocol should take the opportunity to permanently sunset ILP. LPs should be comfortable with the risks of LPing, which is offset by the higher yields due to synths. The protocol has introduced other features (Savers, Lending) which have their own risk spectrum, so removing one line of risk is a win for the protocol -- since it has to survive both bull and bear markets. Risk management is a crucial part of survivability.

To affect this, nodes should vote by setting

FULLIMPLOSSPROTECTIONBLOCKS 0

Context

Having been necessary to bootstrap liquidity pools and attract capital during THORChain’s early-stage growth, Impermanent Loss Protection has served its purpose. The protocol has since evolved to offer Savings Vaults and Protocol Owned Liquidity (PoL), which give the protocol reserve the ability to take a more long-term outlook. Rather than subsidize LPs impermanent loss, the protocol reserve can take a stake in the pools directly via PoL. Paired with Savings Vaults, the need for dual-sided LP incentives becomes less apparent.

As demonstrated by Bancor’s rapid death spiral—attributed to their implementation of impermanent loss protection—we have seen how the feature can be dangerous at scale (particularly if offered on volatile assets). In THORChain’s case, a sudden loss of value of $RUNE price comparable to the price(s) of other assets may lead to a rapid, large-scale drawdown from the protocol reserve. Some external event (other than ILP being paid out over typical market cycles), such as an exploit or sanctions, could cause such a price drop. In such a scenario, dual-sided LPs may begin exiting and selling $RUNE-denominated impermanent loss protection (ILP) to cover losses, requiring an increasing amount of $RUNE to be pulled from the protocol reserve, further exacerbating the issue.

Therefore, it is necessary to re-evaluate the need for Impermanent Loss Protection. While we have seen that THORChain’s Impermanent Loss Protection (ILP) has remained robust over bull (‘20-21) and bear markets (‘22+), impermanent loss protection remains an outstanding, potentially unbounded liability to the protocol.

Proposed Change

  1. Grandfather existing ILP liabilities. Existing depositors would remain covered in perpetuity. This ensures there is not a rush for the exit. Without grandfathering existing liabilities, LPs wanting to claim existing protections would withdraw. It is estimated that a $RUNE price of $6 negates all existing liabilities. Above that price, ILP liabilities would be effectively zero RUNE.
  2. Thirty (30) days after the vote passes, Impermanent Loss Protection will be disabled for all new LPs. This gives prospective LPs the ability to lock-in ILP for the next until the cutoff date, which may attract new capital to the system.

Alternatives Considered

An alternative to ILP was considered: “Deposit Protection”. The goal of Deposit Protection was to deprecate ILP, while protecting dual-sided LPs from negative LUVI (though not protecting them from impermanent loss). However, upon further consideration, the core team and Nine Realms determined that Deposit Protection does not achieve the stated goal, and that it would still create an unbounded liability to the protocol reserve. This was deemed unacceptable and therefore has been scrapped.

References

  • Deposit Protection: https://gitlab.com/thorchain/thornode/-/issues/1408

ADR 006: Enable POL

Changelog

  • February 17, 2022: Initial commit
  • September 26, 2023: Add all Saver pools to PoL targets

Status

Implemented

Update Nov 23: Lower POL Exit Criteria

As of Nov 23, POL enters at 50% and exits at 40% (4500):

  • POLTargetSynthPerPoolDepth to 4500
  • POLBuffer to 500 PoL will enter at 4500 + 500 = 50% but exit at 4500 - 500 = 40%

The issue is that POL does not stay in the pools long enough to make enough yield to compensate for the Impermanent Loss experienced from the price change as Synth Utilisation drops from 50% to 40%:

BlockHeight: 13,326,840
Overall RUNE deposited: 7,590,445.22 RUNE
Overall RUNE Withdrawn: 5,704,572.68 RUNE
Current RUNE PnL: -430,760.85 RUNE

To let PoL stay in the pools for much longer (but still exit if a pool is being removed from the network or utilisation drops off), mimir should refine PoL parameters:

  • POLTargetSynthPerPoolDepth to 3000
  • POLBuffer to 2000

PoL will enter at 3000 + 2000 = 50% but exit at 3000 - 2000 = 10%. This should give PoL enough time to make yield on deposits and is not losing to Impermanent Loss.

Update Sep 23: Add All Saver Pools to PoL Targets

Recently additional pools (stablecoins) were enabled for Saver positions, but PoL was not activated on those pools. The original Pol ADR was explicit in which pools would receive PoL, but it is not wise to have Saver Pools without PoL protection. This ADR amendment sets out that all Saver pools should receive PoL treatment.

PoL reduces dual-LP leverage and keeps Synth utilization away from Synth Caps. If synths exceed their caps, then the L1 pool has more synthetic counterparts than L1 assets, and becomes top-heavy. PoL adds L1 liquidity to prevent this.

Going forward, any pool activated for Savers should also enable PoL. To sync the pools, the following should be set:

L1 Pools

AVAX.AVAX 1
LTC.LTC 1
BCH.BCH 1
DOGE.DOGE 1
BSC.BNB 1
BNB.BNB 1
DOGE.DOGE 1
GAIA.ATOM 1

StableCoin (TOR Anchor) Pools

POL-AVAX.USDC-0XB97EF9EF8734C71904D8002F8B6BC66DD9C48A6E 1
POL-BNB.BUSD-BD1 1
POL-ETH.USDC-0XA0B86991C6218B36C1D19D4A2E9EB0CE3606EB48 1
POL-ETH.USDT-0XDAC17F958D2EE523A2206206994597C13D831EC7 1

Context

Protocol Owned Liquidity is a mechanism whereby the protocol utilizes the Protocol Reserve to deposit $RUNE asymmetrically into liquidity pools. In effect, it is taking the RUNE-side exposure in dual-sided LPs, reducing synth utilization, so that Savers Vaults can grow. Protocol Owned Liquidity may generate profit or losses to the Protocol Reserve, and care should be taken to determine the timing, assets and amount of POL that is deployed to the network.

A vote is currently underway to raise the MAXSYNTHPERPOOLDEPTH from 5000 to 6000. Nodes have already been instructed that raising the vote to 6000 comes with an implicit understanding that Protocol Owned Liquidity (POL) will be activated as a result (https://discord.com/channels/838986635756044328/839001804812451873/1074682919886528542). This ADR serves to codify the exact parameters being proposed to enable POL.

Proposed Change

  • POLTargetSynthPerPoolDepth to 4500: POL will continue adding RUNE to a pool until the synth depth of that pool is 45%.
  • POLBuffer to 500: Synth utilization must be >5% from the target synth per pool depth in order to add liquidity / remove liquidity. In this context, liquidity will be withdrawn below 40% synth utilization and deposited above 50% synth utilization.
  • POLMaxPoolMovement to 1: POL will move the pool price at most 0.01% in one block
  • POLMaxNetworkDeposit to 1000000000000: start at 10,000 RUNE, with authorization to add up to 10,000,000 RUNE on an incremental basis at developer's discretion. After 10m RUNE, a new vote must be called to further raise the POLMaxNetworkDeposit.
  • POL-BTC-BTC to 1: POL will start adding to the BTC pool immediately, as the pool has reached its synth cap at the time of publication.
  • POL-ETH-ETH to 1: POL will start adding to the ETH pool once it has reached the its synth cap.

The threshold for this ADR to pass are as follows, in chronological order:

  • MAXSYNTHPERPOOLDEPTH to 6000 achieves 2/3rds node vote consensus
  • If the author requests a Motion to Bypass and fewer than 16% of nodes dissent within 7 days (by setting DISSENTPOL to 1)
  • ENABLEPOL to 1 achieves 2/3rds node vote consensus

Alternatives Considered

The pros/cons and alternatives to Protocol Owned Liquidity have been discussed on Discord ad neauseum. Check the #economic-design channel for discussion, as most topics have been covered there. The benefits and risks of POL are complex and cannot be summarized impartially by the author of this ADR. Get involved in the discussion and do your own research.

References

ADR 007: Increase Fund Migration and Churn Interval

Changelog

  • March 27, 2023: Initial commit

Status

Proposed

Context

Currently churns are roughly 3 days and migrations take about 7 hours to complete after keygen for the new vaults has succeeded. Nine Realms has been working hard with many wallets to integrate Thorchain as a backend swap provider, and there have been a few cases where wallet bugs resulted in inbounds sent to retired vaults.

One recent example of a significant instance was a retired vault (https://blockstream.info/address/bc1q7zgtsnxhu7q4v467aqfj646s2ksps2rumhq0pz) that had almost a full BTC sent to it within a few hours of being retired - and the wallet claimed to the user that the lost funds were Thorchain's responsibility. While we still maintain that this is a wallet error and responsibility, in the ideal scenario we would still handle the inbound. The general perspective is that there is high likelihood over coming years that wallet bugs may lead to retired vault inbounds and lost user funds, and we should make the system as forgiving as possible to protect those relations.

The simplest way to ensure late inbounds to old vaults are handled is by increasing the time the vault stays retiring. While this may be a small inconveneince for nodes, providing the largest time window possible is a proactive step to prevent unfortunate experiences for onboarding wallets and end users.

Proposed Change

Nine Realms proposes rolling out this change in 2 stages:

  1. Increase FundMigrationInterval to 3600 (5x current), which will result in the time to churn out taking roughly half of a current churn cycle.
  2. Allow nodes a few weeks to prepare for the change in cadence and then increase the churn interval to 129600 (3x current) and FundMigrationInterval to 14400, which will result in churns approximately every 9 days with vaults remaining retiring for approximately 7 days. If nodes want to churn out and then back in within one round, they will have roughly 2 days at the end of the churn to do so.

Alternatives Considered

  1. Protocol changes to continue observing retired vaults and make a best effort to refund inbounds if a quorum of nodes for the retiring vault remain.
  2. Manual coordination of nodes to reconstitute funds in retired vaults along with manual payout from the treasury on a subjective basis.

Consequences

Positive

  • Inbounds that were stuck or sent to an improperly cached address will have a large time buffer to be observed.
  • Larger churn interval reduces gas spend for migrations, which will slightly reduce outbound fees for users with the advent of https://gitlab.com/thorchain/thornode/-/merge_requests/2835.

Negative

  • Churns will happen less frequently and take longer - if nodes want to churn out for only one round to perform maintenance and then immediately churn back in, they must act quickly.

ADR 008: Implement a Dynamic Outbound Fee Multiplier (DOFM)

Changelog

  • March 30, 2023: Implementation of DOFM merged to go out in v108
  • April 4, 2023: Decision to open discussion/ADR before bringing functionality live
  • April 6, 2023: ADR Opened to get formal decision from NOs/community

Status

Proposed

Context

Currently, users are charged a 3x constant on the gas_rate for outbounds on external L1 blockchains, while the vaults only spend 1.5x the gas_rate when signing/broadcasting the outbound tx. The difference between what the users are charged and what the vaults spend (i.e. the "spread") is pocketed by the reserve as an income stream. Since the start of Multi-chain Chaosnet, the reserve has made about 1 million $RUNE in total from this difference, or about 0.6% of the current Reserve balance. The below dashboards have more information:

https://flipsidecrypto.xyz/Multipartite/reserve-cumulative-income-health-rOUjF2

This constant 3x multiplier of the outbound fee effects all end users of THORChain: it makes swapping more expensive, and eats into the profits of both LPers and Savers. For swappers, especially smaller swappers and those transacting on historically expensive chains like ETH and BTC, this 3x multiplier becomes a major deterrant to using the network: they simply could use a centralized service and get better price execution. For Savers, especially savers in lower yield vaults like BTC, this 3x multiplier eats into profits and increases the time-to-break-even.

At the same time, this "spread" is a constant source of income for the reserve, amounting to 0.6% in aggregate of the total reserve balance at the time of writing. Modifying the outbound fee system to make it cheaper for the end user would mean effectively removing this income source for the reserve.

Alternative Approaches

To make swaps and withdraws cheaper for the end user there are not a lot of other options - the outbound fee is the clear place to reduce fees. Other fees include the liquidity fee, which is determined by the "slippage" formula that is a cornerstone of THORChain's CLP/AMM design; modifying this formula would be a major change in THORChain's economic design and is not adivisable.

In terms of reserve income, another option is to create a new source of income for the reserve to replace the lost income from the outbound fee "spread". Two possible options are:

  • Have the reserve take a small % of liquidity fees from swaps. This wouldn't add any cost to the end user, but would take yield from LPs & Nodes.
  • Increase the base network fee of 0.02 $RUNE, or make this fee dynamic. This would increase costs to swappers.

Decision

Pending

Detailed Design

Create a dynamic "outbound fee multiplier" that moves between a max_multiplier and a min_multiplier based on the current outbound fee "surplus" of the network in relation to a "target" surplus. The "surplus" is the difference between the gas users are charged and the gas the network has spent. As the network's surplus grows in relation to the target surplus, the outbound multiplier will decrease from the max_multiplier, to the min_multiplier. The outbound fee multiplier will then be a "sliding scale" instead of being a constant 3x.

New Mimirs

TargetOutboundFeeSurplusRune: target amount of $RUNE to have as a surplus. Suggested initial value 100_000_00000000 (100,000 $RUNE) MaxOutboundFeeMultiplierBasisPoints: max multiplier in basis points. Suggested initial value: 30_000 MinOutboundFeeMultiplierBasisPoints: min multiplier in basis points. Suggested initial value: 15_000

New Network Properties

outbound_gas_spent_rune: Sum of $RUNE spent by the network on outbounds outbound_gas_withheld_rune: Sum of $RUNE withheld from the user for outbounds

current surplus = outbound_gas_withheld_rune - outbound_gas_spent_rune

The current surplus is compared with the target surplus, and the outbound fee multiplier is adjusted accordingly on a sliding scale: If surplus => target, use the min multiplier. If surplus = 0 use the max multiplier. If surplus > 0 && surplus < target, return the basis points value in between min and max multiplier that represents the "progress" to the target surplus.

Consequences

If the proposed design is implemented and activated, this would slowly decrease the outbound fees for end users, which would have two major consequences. First, swapping and withdrawing from THORChain will become cheaper (up to 2x cheaper when considering outbound fee costs). Secondly, overtime the income that the reserve makes on upcharging users on outbound fees will trend to 0. As mentioned above, the total income that the reserve has made on this system since the start of MCCN amounts to 0.6% of the total reserve balance. Note: the proposed change ensures that the Reserve will never lose money on outbound fee/churn costs.

References

  • Dynamic Outbound Fee Implementation: https://gitlab.com/thorchain/thornode/-/merge_requests/2835
  • Reserve income dashboard: https://flipsidecrypto.xyz/Multipartite/reserve-cumulative-income-health-rOUjF2

ADR 009: Reserve Income and Fee Overhaul

Changelog

  • 17 Apr 23: drafted
  • 11 July 23: removed Reserve Income section due to being a contentiously large change (may be addressed in a separate ADR)

Status

The acceptance of ADR 008 ^(1) necessitates an overhaul of Reserve Income and Fees; this ADR.

Context

ADR 008 seeks to reduce L1 Outbound Fees to a minimum (1:1 gas spent) to make ETH and BTC swaps cheaper, thus drive up L1 swap adoption. In past this fee (the multiplier charged on top of L1 outbounds), drove ~500k of annual income to the RESERVE (2022 terms). ^(2) The L1 Outbound Fee lower bound is priced in USD, but other fees are priced in RUNE.

The community wish to overhaul fees to make them easier to understand, fairer, and more appropriate for the purposes of TC.

Three Goals

  1. Overhaul fee price denomination
  2. Revamp fees to source 500k in annual income to make up for ADR 008 3. Overhaul the role of the RESERVE in fees

** THORChain Fees **

FeeDescriptionAmountRecipient
Liquidity FeePaid on every swapProportional to slip100% to Network participants intra-block
L1 Outbound FeeL1 OutboundsIdeally 1:1 gas spent, but a minimum of $1.00 is enforced to pay for TSS resourcesReserve
Native Outbound FeeRUNE and synth outbounds0.02 RUNEReserve
Native Transaction FeeRUNE and synth transfers0.02 RUNEReserve
TNS FeesFees to register TNS10 RUNE + 10 RUNE per yearReserve

Proposal

** USD Pricing ** All fees users directly pay should be delineated in USD terms using the internal USD price feed.

  • MinimumL1OutboundFeeUSD :1_0000_0000 -> MinimumL1OutboundFeeUSD : 2_0000_0000
  • OutboundTransactionFee : 200_0000 -> NativeOutboundFeeUSD : 2000_0000 (20c)
  • NativeTransactionFee : 200_0000 -> NativeTransactionFeeUSD : 2000_0000 (20c)
  • TNSRegisterFee: 10_0000_0000, -> TNSRegisterFeeUSD: 10_0000_0000, ($10)
  • TNSFeePerBlock: 20, -> TNSFeePerBlockUSD: 20, ($10 per year)

500k Extra Income

To source another 500k in income, the Native Outbound and Transaction fees should be increased from ~0.02R (3c) to 20c (as above), and the MinimumL1OutboundFeeUSD should be repriced from $1.00 to $2.00.

Role of Reserve

The RESERVE is a large pool of capital that is used

- to pay out to Network participants on a smoothing function (reduce volatility)

- fund ILP (deprecated)

- fund Protocol Owned Liquidity (a profit-seeking facility and LP-of-last-resort)

One of the draw-backs from paying fees intra-block is volatility - yield for Savers, LPs and Nodes can fluctuate depending on the daily economic activity of the chain. This begs the question - why not pay all fees into the Reserve and slightly increase the Emissions? This means ALL income goes into a smoothing function and yield would be fairly constant even over periods of 3-6 months. The yield computed daily, monthly or even yearly would be very similar, thus frontends and wallets would align much closer when displaying APR.

Decision

PENDING

Detailed Design

Implementation Requirements

  • revamp fees to use USD pricing - divert 100% of liquidity fees to the RESERVE

Mimir Requirements

  • Set all new fees

Yield will drop by 25% for network participants, so EmissionCurve should be changed from 8 to 6, which will increase it back by +25%

Consequences

Positive

- Network participants will enjoy smoothed yield that doesn't fluctuate monthly, but is the same magnitude

  • Reserve Income is re-established
  • Arbs will have much better PnL tracking since fees are priced in USD

Negative

  • Arbs will pay 20c per synth swap, which may erode synth arb volume

Neutral

  • Exchanges will need to be notified that the transfer fee has increased and how much

- Block Rewards will increase

References

(1) https://gitlab.com/thorchain/thornode/-/blob/develop/docs/architecture/adr-008-implement-dynamic-outbound-fee-multiplier.md (2) https://flipsidecrypto.xyz/Multipartite/reserve-cumulative-income-health-rOUjF2

ADR 010: Introduction of Streaming Swaps

Changelog

  • Initial commit: July 23, 2023

Status

Launching Feature

Context

The current model of network swap fees and price execution on THORChain is directly proportional to the depth of the pool. Larger trades lead to higher fees and consequently, less favorable price execution. This has resulted in a trend where approximately 99% of swaps on THORChain are under $10k in value, as users executing larger trades often find better price execution on other exchanges, typically centralized ones (CEXs). If one does market analysis, one would see that whales control the majority of the spot market which is largely unavailable to THORChain.

To capture a larger market share of trading, the network needs to offer more competitive price execution, particularly for larger (whale) trades.

Proposed Change

This ADR introduces an enhancement to swaps, enabling users to optionally divide larger trades into several autonomous smaller trades. This division allows arbitrage bots to adjust the price multiple times during the swap process.

It is worth make a note that while this will reduce swap fees, it will not reduce other fees such as gas fees, outbound fees, and affiliate fees.

For a comprehensive description of this feature, please refer to this GitLab issue.

As of the date of this writing, the feature has been deployed on our stagenet for several weeks and undergone extensive testing by our developers. The testing document is accessible here.

Advantages

This proposal offers substantial benefits for the network and its users:

  1. Improved Price Execution: This feature allows the network to determine any swap fee (in basis points) for trades of all sizes, between any two supported assets, whilst maintaining the benefits and safeguards that the slip-based fee model provides. This flexibility allows the community to choose its level of competitiveness against other exchanges (CEXs or DEXs).

  2. Increased Trade Volume and User Base: The improved price execution should lead to a significant increase in trade volume, a rise in unique swappers, and an expansion of our market share.

  3. Enhanced Capital Efficiency: Due to swaps being spread out over time, each swap will affect the pool price less, causing the AMM to become significantly more capital efficient.

  4. Support for New Trading Strategies: THORChain will be able to support new trading strategies, such as time-weighted average price (TWAP) and dollar-cost averaging (DCA), further extending its user base and community.

  5. Enhanced Value Proposition: Other THORChain features can also leverage streaming swaps to enhance their value propositions. For example, cheaper entry and exit for savers, and order books with partial fulfillment and better price execution.

Potential Drawbacks

This feature may lead to the network collecting fewer fees per swap, depending on the trade size. Although there may be a decrease in system income, it is anticipated that the increase in trade volume and number of swappers will compensate for this. This proposal represents a shift in THORChain's priorities from profitability to increased adoption and growth (in the short to medium term).

Another potential issue is with the increase in trade volume into the network (but outside of the pools) might result in an unbounded amount of liquidity that the network is securing. While this has always been true for the network, trades have traditionally been "instant", allowing funds to leave the network as quickly as they entered. Now, the network will permit trade value to remain in the network for up to 24 hours, which is adjustable via mimir.

ADR 011: THORFi Lending Feature

Overview

Changelog

  • 2023-07-14: Initial Outline
  • 2023-08-01: Summary of Risk
  • 2023-08-07: Update

Status

  • Proposed

Decision

This section records the decision that was made.

Context

Lending will bring fresh exogenous capital from new users to scale liquidity (both TVL and security), and drive up the capital efficiency of the pools which increases system income and real yield.

Lending allows users to deposit collateral, then create debt at some collateralization ratio CR (collateralization ratio). The debt is always denominated in TOR (despite what asset the user receives). TOR is an algo-usd-stable used internally for lending and cannot be acquired or transferred. These loans have 0% interest, no liquidations, and no expiration. Risk is contained by caps on launch, slip-based fees when opening and closing loans, dynamic CR, and a circuit breaker on RUNE supply.

ELI5 (explain like I'm 5)

"THORChain onboards layer1 collateral to issue TORdollar-based debt. The collateral is stored as equity (RUNE). The more collateral is taken onboard relative to the pool depths, the higher the collateralization ratio for new loans, so users want to get in first. The higher the collateralization ratio, the safer the system becomes. Since there are no liquidations and no interest rate, users are less likely to pay back their loans, which is beneficial to the protocol as the equity-value increases. Also, since the loans remove RUNE from the pools, the pools open back up for more TVL, causing THORChain to scale liquidity and security. The protocol takes on the liability of repaying collateral when the loan closes, so net-benefit is realized when RUNE appreciates faster than collateral assets."

Detailed Design

Detailed Design on GitLab

Economic Reasoning

Scaling Liquidity

Due to PoL and Savers, the network is likely to max out pooled RUNE and send all yield to nodes. The protocol stops scaling until it can recruit new nodes, but this takes time. A faster mechanism to scale security (and TVL) is to attract new capital, buy out the RUNE from the pools and open up caps again. This is the lending design. Whilst the loan was opened, it created a net reduction in RUNE in the pools, allowing more TVL to enter. It also bids on RUNE, allowing security to increase. Additionally, +ve RUNE Price action boosts the value of yields (block rewards). This is why lending scales THORChain.

Contracting Safely

If borrowers start paying back their loans, this reverses the above sequence of events and the system contracts. RUNE is minted and sold, causing sell pressure on the RUNE asset and an increase of the RUNE supply. However, the CR will start dropping for new loans, until a point is hit and new loans are opened under favorable CR terms, encouraging new loans to be taken.

Circuit Breaker

If the RUNE price drops drastically against the majority of its collateral assets (BTC, ETH), then net inflation of RUNE will occur if users start paying back their loans, and it exceeds the margin of RUNE already historically burnt. This inflation could hit the breaker limit. At this point the system pauses new loans to be created and sunsets (turns off) lending (note, all other features of TC still function). At this point, no further inflation of RUNE can occur and the supply arrested. The RESERVE will cover the remaining collateral payouts.

Unwind Path

If the Circuit Breaker is hit, then the protocol will draw from RESERVES, until RESERVES are zero’d. After this, the protocol will attempt to mint over 500m and fire another trigger which auto-halts the entire protocol (swaps, savers, lending, sends). At this point there is still 60m RUNE in the Standby RESERVE that can be used to:

  1. Re-stock the Protocol RESERVE, then
  2. Place remainder in a Claims Module, where collateral holders can pro-rata claim based on their collateral at the time of the halt, paid out in RUNE.
  3. After some time, turn off the Claims module, and redraw unclaimed RUNE back to Standby.

Important to note, the above Unwind Path is not coded, and will require governance to approve and devs to implement. It is just a suggestion for the Community to know there is an Unwind Path possible that does not result in destruction of the entire network.

Proactive Dynamic Burn

Currently, all derived asset slip fees are burnt and this presents a continuous burn on RUNE supply.

A suggestion by devs to prevent the Circuit Breaker ever being hit is a dynamic burn of System Income, removing the 500m auto-halt: At 490m Supply, start burning, linearly from 0% to 20% at 500m Supply Thus there is an ability for protocol to “burn down” its liability over a period of time. Since 500m can never be exceeded, the dynamic burn will allow small borrowers to exit as soon as space for them to mint is opened back up. Large borrowers will be the last to leave. The protocol will keep burning down to 490m, and at some point, market sentiments will change and Borrowers will come back to THORChain.

An additional suggestion is to build more features (such as Perps) which will add more burning into the protocol (via more derived asset pool fees and liquidations).

Loan Caps

For safe scaling, lending is capped based on the amount of outstanding RUNE supply. The monetary policy of the asset is a cap of 500m. Currently, the network is missing ~15m RUNE due to individuals not upgrading original rune assets of BEP2 or ERC20 RUNE. Since we have this 15m gap, this is used to help "fund" lending to start. To help protect the network from hitting the circuit breaker, only 1/3rd of the outstanding RUNE will be used to guide scaling lending (~5m RUNE) in terms of loan collateral value. 1/3rd is selected due to historical analysis of RUNE's price movements, where its largest micro-economic price event (several hacks in early beta days, 2 years ago), we saw a 3x price change downward (relative to BTC). This means that however amount of loans that are taken out, we could see a 3x downward price change AND 100% of loans could close, and we still wouldn't hit the 500m cap (circuit breaker). As more rune is burnt, and the gap is increased from 15m --> 20m, this opens up more space for more loans to be opened. In addition, the market buy and burning of 5m+ RUNE should cause rune's price to naturally out perform BTC. As rune's price out performance BTC, and the ratio in the BTC pool shifts, and more loans can be opened, without changing our risk profile.

Parameter Recommendations

The following recommendation were based off of ranges from Block Science Simulation: https://hackmd.io/R_ksPSG0T6mtjsEE7U9q-w#Parameter-Recommendations

MimirDescriptionRecommendation
LendingLeverRelative to the amount of missing rune from max supply. Throttles loan CR, higher is more risky, lower is less risky, but retards lending growth/adoption.3,333
MaxCRMax CR ratio when LendingLever consumed (pro-rata to pool)50,000
MinCRFirst loan gets this CR20,000
LoanRepaymentMaturityMinimum number of blocks a loan must remain open432000
MaxAnchorBlocksNumber of blocks to sum the slip for Anchor300
MaxAnchorSlipMax slip value for anchor sensitivity6,000
DerivedDepthBasisPtsDepth of derives pools out the gate compared to L1 pool10,000
DerivedMinDepthMin pool depth to shrink to in periods of high volatility1000

Summary of Largest Risks

Block Science reviewed the lending mechanisms exhaustively. The Output of their research includes:

Below are a summary of the largest risks identified

Block Science reviewed the lending mechanisms exhaustively. The Output of their research includes:

Risk Report: https://hackmd.io/@blockscience/H1Q-erh_n Simulation Summary: https://hackmd.io/R_ksPSG0T6mtjsEE7U9q-w CadCad simulation framework: https://gitlab.com/thorchain/misc/cadcad-thorchain/-/tree/main/documentation

Testing Guide

Lending has been tested for success and failure modes on Stagenet. This can be used by devs to review for feature readiness.
Testing Document

Regression Tests

References

ADR 012: Scale Lending

Changelog

  • 2021-01-30: Proposed

Context

Lending

Lending was launched in Q3 2023. As of writing:

  1. 1.3k loans opened with $24m in collateral from ~700 borrowers, to issue $7m in debt
  2. 3.68m RUNE burnt from collateral, with 2m RUNE minted to create debt
  3. 88% percent "full" based on the lending lever
  4. System Risk extremely low, with Full Closure Scenario resulting in 700k RUNE burnt forever (safe)
  5. No observed attacks on the Lending Lever and/or TOR Anchor pool prices

Lending has been shown to be safe, controlled and has almost reached caps. It is time to Scale Lending.

https://flipsidecrypto.xyz/banbannard/⚡-thor-chain-lending-thorchain-lending-fOAKej

https://dashboards.ninerealms.com/#lending

Standby Reserve

The Standby Reserve has 60,000,000 RUNE held in idle, waiting to be deployed. This was allocated since Genesis to be used by the protocol when it was ready for it.

https://runescan.io/address/thor1lj62pg6ryxv2htekqx04nv7wd3g98qf9gfvamy

Current Params

"MAXCR": 50000, //500%
"MINCR": 20000, //200%

Proposal

To scale Lending, reduce risk and increase Lending Lever safely the following is proposed:

  1. Set "MAXCR": 20000 (200%)
  2. Burn the 60,000,000 Standby Reserve

MAXCR

Currently the CR for BTC is at maximum (500%) whilst the CR for ETH is at ~300%.

By reducing the Max Collaterisation Ratio down to 200% the following is achieved

  1. More loans will be opened since terms are favourable
  2. Full Closure Scenario is less likely to net-mint RUNE because the debt-collateral ratio is 2x, not 5x
  3. More loans are unlikely to be closed in bear markets because $collateral is likely to be less than $debt (since only has to fall 2x instead of 5x)

Burn Reserve

By burning the 60m Standby Reserve, the Lending Lever is:

  1. Scaled by 5x (15m to 75m)
  2. Safer for current borrowers since there is more buffer to fill before the RUNE supply circuit breaker is hit at 500m supply
  3. Safer for the protocol by taking the 60m RESERVE out of possible circulation (if it was added to MAIN Reserve)

Other Considerations

The Standby Reserve has always been a "Plan B" for the protocol, a joker up its sleeve to play. It's time to play it. By burning it, it makes all outstanding RUNE notionally more valuable.

Burning it also frees up space for the protocol to Scale Lending (which is a successful feature), as well as space to launch future features:

  1. Stablecoin (will require space under the cap to mint)
  2. Perpetuals (will use derived assets, so needs space)

References

ADR 013: Synth Backstop

Changelog

  • 2024-04-07: Proposed

Context

Synthetic Backstop

The synth code has been in the codebase since before MCCN, but took a year to flip on. Its initial purpose was to give arb bots a much more efficient way to arb the pools without going through exogenous blockchains. According to an analysis from Delphi Digital, these assets had 16x the efficiency of other pool assets.

Later, the community opted to utilize synths for "savers". This feature has effectively doubled the depth of the pools, doubled the volume, and created significant buy pressure for the $RUNE asset. Savers created a more significant demand center for the synthetic asset than originally considered. This increase in synths was beneficial for the protocol, but also increased the risk to the protocol. The community increased the value of synths and its risk once stable savers were added.

It has come time for the community to implement a backstop to synths to curtail any large-scale risk to the protocol. Nodes are asked to vote over the the next 1 week over two proposed solutions. The solution with the most votes will win.

Solution 1 - Forced Ejection

In this design, once the synth utilization grows above the cap (currently 60%), the protocol will force savers to exit. Likely on a "last in, first out" system (in interest to reward our most loyal community members). Savers would be ejected until the synth utilization drops below the cap.

This approach is the most "user friendly", but creates $RUNE sell pressure in a down market while also reducing the pool depths (and trade volume).

Solution 2 - Savers Haircut

In this design, once the synth utilization grows above the cap, any withdrawal of savers would experience a haircut relative to how far over the synth cap the pool is. This would effectively be an "exit fee" that users opt into when they choose to withdraw. This caps the risk that LPs can experience from a down $RUNE price and moves it towards the savers.

This approach is most beneficial to the protocol as it doesn't sell $RUNE and keeps its pools deep, but may cause a PR problem around the savers experience etc.

ADR 14: Reduce Saver Yield Synth Target to Match POL Target

Changelog

  • 04/04/2024: Created

Status

Proposed

Context

The following are the relevant mimir values at the time of writing:

MaxSynthsForSaversYield=6000
POLTargetSynthPerPoolDepth=3000
POLBuffer=2000

These values result in savers receiving positive yield while POL is simultaneously depositing into the pool to lower the synth utilization (when it is over 50%). Suggesting setting the following:

MaxSynthsForSaversYield=5000

While more complicated discussions around saver ejections and synth backstop are pending, this is a simple change to enact a small reduction in synth utilization over time (even if savers do not respond to the reduced yield). Additionally, it avoids the current state where a small share of the POL in effect subsidizes the saver yield (increasing synth utilization), while it is trying to reduce it.

Decision

TBD

Consequences

Savers will not receive yield on pools where synth utilization is over 50% (above which POL currently deposits).

Adding New Chains

Chain Developers should be extremely familiar with how THORChain works, and how their own chain works.

There is now a specific process for the addition of new chains, see: https://gitlab.com/thorchain/thornode/-/blob/develop/docs/chains/README.md

Process

  1. Read https://gitlab.com/thorchain/thornode/-/blob/develop/docs/newchain.md
  2. Bifrost: Start by forking one of the existing Bifrosts (UTXO, EVM or BFT).
  3. Daemon: Add the chain daemon to THORChain/Node-Launcher https://gitlab.com/thorchain/thornode/-/tree/develop/bifrost/pkg/chainclients
  4. Smoke Tests: Build out the smoke tests for the chain. This ensures the connection is robustly tested.
  5. XChainJS: Add a new chain package to xchainjs so the entire ecosystem of wallets can easily support.

Once this is complete, the chain can be added to Stagenet. After some time of demonstrating Stability on Stagenet, the THORChain Node Operator community is polled and if supported, it can be merged to Mainnet.

Once on mainnet, the chain is typically given a period of 12 months to demonstrate uptake and usage. If the chain cannot maintain sufficient demand, it may be removed from the network and all liquidity refunded to LPs.

Adding New Chains

Chain Developers should be extremely familiar with how THORChain works, and how their own chain works.

There is now a specific process for the addition of new chains, see: https://gitlab.com/thorchain/thornode/-/blob/develop/docs/chains/README.md

Process

  1. Read https://gitlab.com/thorchain/thornode/-/blob/develop/docs/newchain.md
  2. Bifrost: Start by forking one of the existing Bifrosts (UTXO, EVM or BFT).
  3. Daemon: Add the chain daemon to THORChain/Node-Launcher https://gitlab.com/thorchain/thornode/-/tree/develop/bifrost/pkg/chainclients
  4. Smoke Tests: Build out the smoke tests for the chain. This ensures the connection is robustly tested.
  5. XChainJS: Add a new chain package to xchainjs so the entire ecosystem of wallets can easily support.

Once this is complete, the chain can be added to Stagenet. After some time of demonstrating Stability on Stagenet, the THORChain Node Operator community is polled and if supported, it can be merged to Mainnet.

Once on mainnet, the chain is typically given a period of 12 months to demonstrate uptake and usage. If the chain cannot maintain sufficient demand, it may be removed from the network and all liquidity refunded to LPs.

Chain Clients

Chain Client

The chain client sits in the /bifrost package which is outside of the core THORChain consensus engine. This is because its purpose is simply to witness events to THORChain. THORChain itself comes to consensus on witnessed events and acts from there.

There are two main parts to each Chain Client:

  1. Observer (Scans blocks and packages up events to be witnessed to THORChain)
  2. Signer (receives txOut data from THORChain and converts into chain-specific signing data, to be signed by either the YGG node or TSS routine)

In addition there are some supporting routines, such as that to store cached witness transactions in local storage. This is used for tracking confs and handling re-orgs.

Scanning Blocks

The block scanner monitors the Asgard Addresses and looks for incoming UTXOs spending to those addresses. When it sees one performs validation on it and witnesses to THORChain. It will also store it in local storage.

Confirmation Counting

Incomings are "conf-counted" such that the sum of all transactions received in a block is measured against the value of the block, and the confirmations required are:

The blockValue is typically just the coinbase reward, which already sums up the fees and subsidies.

To do this, the Bifrost reports every tx immediately, but also specifies a finalisation blockheight. If the confs required is 1, then the tx is immediately processed. If the finalisation height exceeds the current blockheight, then the Bifrost will also wait that many blocks, then send *another* witness transaction as soon as those blocks occur. At this point the transaction can be finalised in the state machine.

https://gitlab.com/thorchain/thornode/-/blob/develop/bifrost/observer/observe.go#L117

Warning

Although THORChain will not act on an inbound transaction that is undergoing conf-counting, it will consume it when it migrates vaults. This means conf-counted UTXOs will not be abandoned if still being finalised.

Info

A malicious re-org would never happen unless the value to gain from re-org exceeds the cost to re-org. The value to gain is the sum of the transactions sent to Asgard, whilst the cost to re-org is taken to be the value of each block -- the sum of fees and subsidies.

Re-orgs

Each Chain Client needs to have re-org logic, since re-orgs will always happen (natural or malicious, is irrelevant). To do this, the Bifrost tracks the last 24 hours of transactions reported in a local KV store. Every time it detects a new block at a previous height it has seen, it checks for the presence of every transaction it has reported. If the transaction is missing then it has been re-orged out.

If so, the Bifrost will prepare an ErrataTx which instructs the state machine to undo all the state associated with that missing transaction. Any losses to the pools are thus socialised to all LPs.

Network Fees

THORChain maintains accurate block-by-block awareness of gas fees, and reports them on /inbound_addresses end-point for anyone to query. These gas fees ensure that state machine can always perform transactions at "next-block" speed. If the network uses too low gas rates then bad things happen, although it can recover. See Outbound Fee.

To do this:

  1. Detect the gas spent in each block, then detect the block size. The gas rate is thus the gasSpent / blockSize
  2. This is now the average gasRate, which is typically 50--100% higher than the lowest gas rate to get in the block.
  3. Witness this to THORChain every block that it changes over a 20 block period, whereby the highest is chosen. This means it ratchets up fast, but comes down slow.

Handling Gas

Every transaction in and out from THORChain vaults need to have gas amount reported, as well as the gas asset used. This is needed by THORChain to accurately deduct this gas from the pools in order to keep the system solvent.

UTXO

Chain Client

Example for Bitcoin.

Observer

https://gitlab.com/thorchain/thornode/-/blob/develop/bifrost/pkg/chainclients/bitcoin/client.go

Signer

https://gitlab.com/thorchain/thornode/-/blob/develop/bifrost/pkg/chainclients/bitcoin/signer.go

Scanning Blocks

The block scanner monitors the Asgard Addresses and looks for incoming UTXOs spending to those addresses. When it sees one performs validation on it and witnesses to THORChain. For Bitcoin, it looks that at least 1 output is spent to Asgard, and searches for another output to have an OP_RETURN. These two outputs form the amount and memo witness to THORChain.

https://gitlab.com/thorchain/thornode/-/blob/develop/bifrost/blockscanner/blockscanner.go

Confirmation Counting

The txValue is the sum of all transactions received in a block to Asgard vaults. The blockValue is the coinbase value, which includes fees and subsidy. If a miner forgets to add a coinbase value (it has happened) a default of 6.25 is used. (This should be updated every 4 years, or use logic to auto-update).

https://gitlab.com/thorchain/thornode/-/blob/4bcaf4f80787d0aaee711388578ed453959ef673/bifrost/pkg/chainclients/bitcoin/client.go#L1084

Re-orgs

Bifrost tracks the BlockCacheSize = 144 blocks of transactions reported in a local KV store. Every time it detects a new block at a previous height it has seen, it checks for the presence of every transaction it has reported. If the transaction is missing then it has been re-orged out. The missing txID is reported to THORChain as an ErrataTx

https://gitlab.com/thorchain/thornode/-/blob/4bcaf4f80787d0aaee711388578ed453959ef673/bifrost/pkg/chainclients/bitcoin/client.go#L337

Network Fees

Reported as sats/byte where the fee rate is computed over the last block. Reports the highest seen in the last 20 blocks. https://gitlab.com/thorchain/thornode/-/blob/4bcaf4f80787d0aaee711388578ed453959ef673/bifrost/pkg/chainclients/bitcoin/client.go#L668

Handling Gas

The gas amount for a transaction is just the difference between outputs and inputs.
https://gitlab.com/thorchain/thornode/-/blob/4bcaf4f80787d0aaee711388578ed453959ef673/bifrost/pkg/chainclients/bitcoin/client.go#L988

Other Considerations

UTXO consolidation

UTXOs consume inputs, and these inputs need to be signed independently. Thus consuming 15 inputs requires 15 times the TSS bandwidth than a single input. To prevent runaway liabilities the client will automatically enter a TSS signing ceremony for Asgard every 15 inputs to consolidate them back to one. This transaction uses the consolidate memo and can be seen regularly on THORChain vaults.

https://gitlab.com/thorchain/thornode/-/blob/4bcaf4f80787d0aaee711388578ed453959ef673/bifrost/pkg/chainclients/bitcoin/client.go#L915

ChildPaysForParent

Asgard cannot consume a pending transaction spent to it, since THORChain requires at least 1 confirmation. However, Ygg Vaults will consume pending transactions, since they continually spend back to themselves and are only funded by Asgard. To do this, outbound transactions from Ygg Vaults are actually witnessed when in the mempool, instead of being confirmed. This allows Ygg vaults to have high swap throughput, even if the swaps are still pending in the mempool.

Warning

Ygg vaults have historically been subject to dust attacks which spend large-size transactions with low fees, causing vaults to lock up. To prevent this, Ygg vaults only consume pending transactions spent to itself.

ReplaceByFee

RBF transactions allow the spender to double-spend with a higher fee. Users can use RBF transactions to spend to Asgard, but RBF does not need to be used in the THORChain vaults. Ygg vaults have CPFP instead.

Wallet Client

UTXO clients implemented in XChainJS have the following nuances:

Fees

The wallet client should spend with a fee rate at least equal to what is reported on inbound_addresses - if not it risks not being confirmed by the time the vault migrates.

Pending UTXOS

Do not consume pending transactions when spending to Asgard (with a memo) since it may consume a low-fee tx and get stuck.

MEMO

The memo is inserted as an OP_RETURN in an output. It can be any output. The MEMO is limited to 80bytes, so it should be trimmed and use abbreviated memos or Asset identifiers where possible.

EVM Chains

Chain Client

Example for Ethereum.

Observer And Signer

https://gitlab.com/thorchain/thornode/-/blob/develop/bifrost/pkg/chainclients/ethereum/ethereum.go

Router

The EVM Bifrost is different to others in that it uses a router to handle deposits into and out of THORChain vaults. The Router is just a means for capturing token deposits and emitting memos.

The Router holds all ERC20s, but forwards ETH to the TSS vault. This allows the TSS Vault to call into the Router and pay gas to move token allowances to vaults.

Instead of paying ERC20s to vault addresses, an allowance to spend is given on the Router. The depositing user gives this allowance to the Asgard vault, which itself moves the allowance to each Ygg vault. Thus each Ygg vault can call into the Router to transfer out inside their allowances. This is a very gas-efficient way of achieving vault funding.

Additionally because of this, the Router is a permissionless contract with no special privileges (there is no owner).

Info

The Router is necessary because the ERC20 standard has no "push" functionality, and no ability to attach native memos. The Router uses the transferFrom "pull" and emits an event with a memo string.

Warning

The V3 Router uses solidity .Send() to transfer ETH assets outbound. When an outbound ETH tx is sent to a contract, it must complete execution with only 2300 Gas. If the recipient runs out of Gas, the network still considers the payment sent. Developers of THORChain UI's should check recipient ETH addresses for the presence of code and warn users who may have complex fallback functions that their payment may not succeed, and they could lose funds. Geth eth.getCode("0xaddress") may be useful.

Warning

The Router does not accept deposits from Smart Contracts. Deposits from Smart Contracts are ignored.

Scanning Blocks

The block scanner monitors the Router events, and can create a witness transaction based on this event.

Confirmation Counting

Incomings are "conf-counted" by comparing their value with ETH (using THORChain pool pricing) and then delayed based on the ETH value of the deposits compared with the ETH block reward + fees.

ETH Cancellation Logic

ETH was found to have very dynamic gas fees, causing vaults to lock up. Since it does not have child-pays-for-parent, each node has tx cancellation logic which it invokes if it finds it has made gas that is still pending after 20 mins.

It does this by simply spending 0 ETH back to itself using the same nonce as the tx that is stuck, using the latest Gas prices.

Gas Fees and Limits

Each node will use up to 200k Gas to make an outbound tx (this covers most ERC20s), however the real cost is closer to 80k Gas units so the user is charged based on spending 80k (the fee they paid is deducted from the final transaction out).

Since it is really disruptive if a tx does not go thru (since it locks up vaults) the ETH Bifrost uses a gas fee which is 1.5x what the current "average" gas price is for a block. This puts the gas fees that TC uses close to "fastest" gas prices.

Re-orgs

The ETH chain re-orgs a lot, and TC is able to monitor and post re-org data to THORChain.

BFT Chains

Assets

Assets on BFT chains are generally native tokens (not contract-based), and can pay their fee in their native tokens. Thus there is no router required.

Re-orgs

BFT chains do not re-org so no re-org logic is required.

Conf-counting

BFT chains do not require any confirmations.

Gas

BFT chains generally have static fees, so fee rates don't need to be updated regularly.

ERC20 Tokens

To minimise the attack surface for ERC20 tokens, THORChain's EVM implementation whitelists ERC20 contracts. The whitelist is managed by 1INCH:

https://tokenlists.org/token-list?url=tokens.1inch.eth

If the token is not found on the list, it can be added by a Pull Request to THORNode. Example:

https://gitlab.com/thorchain/thornode/-/merge_requests/2085/diffs