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.
For more information see Understanding THORChain Technology or Concepts.
For wallets/interfaces to interact with THORChain, they need to:
- Connect to THORChain to obtain information from one or more endpoints.
- Construct transactions with the correct memos.
- Send the transactions to THORChain Inbound Vaults.
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.
- Midgard - https://midgard.ninerealms.com/v2/doc
- THORNode - https://thornode.ninerealms.com/thorchain/doc
- Cosmos RPC - https://v1.cosmos.network/rpc/v0.45.1, Example Link
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.
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
2. Query for a swap quote
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
.
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.*
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
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.
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
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.
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
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:
{
"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:
{
"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>
{
...
"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.
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
2. Query for a swap quote
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
.
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.*
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
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.
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
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.
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
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:
{
"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:
{
"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>
{
...
"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.
- 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.
- 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.
- 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.
- Outbound Fee - the fee the Network pays on behalf of the user to send the outbound transaction. See Outbound Fee.
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:
- The Destination Chain outboundFee, or
- The Source Chain outboundFee, or
- $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.
- Layer1 Inbound Confirmation - assuming the inboundTx will be confirmed in the next block, it is the source blockchain block time.
- Observation Counting - time for 67% THORChain Nodes to observe and agree on the inboundTx.
- 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.
- Outbound Delay - dependent on size and network traffic. Large outbounds will be delayed.
- 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.
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:
- 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.
- 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 = 10
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.
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 difference
value 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.
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
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.
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.
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
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.
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.
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.
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.
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
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."
}
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%.
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 withdrawdust_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
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:
https://thornode.ninerealms.com/thorchain/pool/BTC.BTC/savers will show all BTC Savers
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.
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.
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.
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
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."
}
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%.
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 withdrawdust_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
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:
https://thornode.ninerealms.com/thorchain/pool/BTC.BTC/savers will show all BTC Savers
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:
- 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.
- 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
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
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.
- Layer1 Inbound Confirmation - assuming the inbound Tx will be confirmed in the next block, it is the source blockchain block time.
- Observation Counting - time for 67% THORChain Nodes to observe and agree on the inbound Tx.
- 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
- Layer1 Inbound Confirmation - assuming the inbound Tx will be confirmed in the next block, it is the source blockchain block time.
- Observation Counting - time for 67% THORChain Nodes to observe and agree on the inbound Tx.
- Outbound Delay - dependent on size and network traffic. Large outbounds will be delayed.
- 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
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, so100000000 = 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.;
This will register a new THORName called ODIN
with a Bitcoin alias of bc1Address
owner of thorAddress
and preferred asset of BTC.BTC.
You can use Asgardex to post a MsgDeposit with a custom memo. Load your wallet, then open your THORChain wallet page > Deposit > Custom.;
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:
- Register your THORName following instructions above.
- 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:
-
Grab the full USDC name from the Pools endpoint:
ETH.USDC-0XA0B86991C6218B36C1D19D4A2E9EB0CE3606EB48
-
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:- THORChain address:
thor1dl7un46w7l7f3ewrnrm6nq58nerjtp0dradjtd
- THORName:
ac-test
- ETH payout address:
0x6621d872f17109d6601c49edba526ebcfd332d5d
;
The full memo would look like:
~:ac-test:ETH:0x6621d872f17109d6601c49edba526ebcfd332d5d:thor1dl7un46w7l7f3ewrnrm6nq58nerjtp0dradjtd:ETH.USDC-0XA0B86991C6218B36C1D19D4A2E9EB0CE3606EB48
- THORChain address:
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, so100000000 = 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.;
This will register a new THORName called ODIN
with a Bitcoin alias of bc1Address
owner of thorAddress
and preferred asset of BTC.BTC.
You can use Asgardex to post a MsgDeposit with a custom memo. Load your wallet, then open your THORChain wallet page > Deposit > Custom.;
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:
- Register your THORName following instructions above.
- 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:
-
Grab the full USDC name from the Pools endpoint:
ETH.USDC-0XA0B86991C6218B36C1D19D4A2E9EB0CE3606EB48
-
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:- THORChain address:
thor1dl7un46w7l7f3ewrnrm6nq58nerjtp0dradjtd
- THORName:
ac-test
- ETH payout address:
0x6621d872f17109d6601c49edba526ebcfd332d5d
;
The full memo would look like:
~:ac-test:ETH:0x6621d872f17109d6601c49edba526ebcfd332d5d:thor1dl7un46w7l7f3ewrnrm6nq58nerjtp0dradjtd:ETH.USDC-0XA0B86991C6218B36C1D19D4A2E9EB0CE3606EB48
- THORChain address:
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
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.
Thorchain-amm
Conducts actions such as swap, add and remove. It wraps xchain clients and creates a new wallet class for and balance collection.
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
APIs for getting data from THORChain.
- Midgard
- Thornode
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
- Go to the replit code example then press show files. Select the project.json file.
- Locate and then copy the
dependencies
section into your project.json file. - 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 intotalFees
- Ensures
totalFees
is not greater thaninput
. - 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 TxsTransactions
- 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 assetsasync
- Utitilies forasync
handlingbn
- Utitilies for usingbignumber.js
chain
- Utilities for multi-chainstring
- 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 BaseAmountAssetAmount
: a BaseAmount*10^8. E.g. 1 BTC = 1 in Asset Amount.Asset
: Asset details {Chain, symbol, ticker, isSynth}
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.
Variable | Data Type | Comments |
---|---|---|
input | CryptoAmount | Inbound asset and amount |
destinationAsset | Asset | Outbound asset |
destinationAddress | String | Outbound asset address |
slipLimit | BigNumber | Optional: Used to set LIM |
affiliateFeePercent | number | Optional: 0-0.1 allowed |
affiliateAddress | Address | Optional: THOR address |
interfaceID | string | Optional: Assigned interface ID |
SwapEstimate
The internal return type is used within estimateSwap
after the calculation is done.
Variable | Data Type | Comments |
---|---|---|
totalFees | TotalFees | All fees for swap |
slipPercentage | BigNumber | Actual slip of the swap |
netOutput | CryptoAmount | Input - totalFees |
waitTimeSeconds | number | Estimated time for the swap |
canSwap | boolean | False if there is an issue |
errors | string array | Contains 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.
Variable | Data Type | Comments |
---|---|---|
txEstimate | SwapEstimate | Swap details |
memo | string | Constructed memo THORChain will understand |
expiry | DateTime | When the SwapEstimate information will no longer be valid |
toAddress | string | Current Asgard Vault address from inbound_address |
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 | ||
---|---|---|
hash | string | inbound Tx Hash |
url | string | Block exploer url |
waitTimeSeconds | number | Estimated time for the swap |
Connecting to THORChain
The Network Information comes from four sources:
- Midgard: Consumer information relating to swaps, pools, and volume. Dashboards will primarily interact with Midgard.
- THORNode: Raw blockchain data provided by the THORChain state machine. THORChain wallets and block explorers will query THORChain-specific information here.
- Cosmos RPC: Used to query for generic CosmosSDK information.
- Tendermint RPC: Used to query for consensus-related information.
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:
- https://midgard.thorswap.net/v2/doc
- https://midgard.ninerealms.com/v2/doc
- https://midgard.thorchain.liquify.com/v2/doc
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):
- https://thornode.thorswap.net/thorchain/doc
- https://thornode.ninerealms.com/thorchain/doc
- https://thornode.thorchain.liquify.com/thorchain/doc
- Pre-hard-fork blocks 4786559 and earlier
https://thornode-v0.ninerealms.com/thorchain/doc
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:
- Midgard: Consumer information relating to swaps, pools, and volume. Dashboards will primarily interact with Midgard.
- THORNode: Raw blockchain data provided by the THORChain state machine. THORChain wallets and block explorers will query THORChain-specific information here.
- Cosmos RPC: Used to query for generic CosmosSDK information.
- Tendermint RPC: Used to query for consensus-related information.
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:
- https://midgard.thorswap.net/v2/doc
- https://midgard.ninerealms.com/v2/doc
- https://midgard.thorchain.liquify.com/v2/doc
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):
- https://thornode.thorswap.net/thorchain/doc
- https://thornode.ninerealms.com/thorchain/doc
- https://thornode.thorchain.liquify.com/thorchain/doc
- Pre-hard-fork blocks 4786559 and earlier
https://thornode-v0.ninerealms.com/thorchain/doc
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
Getting the Asgard Vault
Vaults are fetched from the /inbound_addresses
:
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"
},
...
]
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
.
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"
}
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 NameAddress
: 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"
}
"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.
- Looking at the
/inbound_addresses
endpoint and inspecting the halted flag. - 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. All memos are listed here.
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.
Guides have been created for Swap, Savers and Lending to enable quoting and the automatic construction of memos for simplicity.
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.
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.
In addition, some parameters are optional. Simply leave them blank, but retain the :
separator:
FUNCTION:PARAM1:::PARAM4
Permitted Functions
The following functions can be put into a memo:
- SWAP
- DEPOSIT Savers
- WITHDRAW Savers
- OPEN Loan
- REPAY Loan
- ADD Liquidity
- WITHDRAW Liquidity
- BOND, UNBOND & LEAVE
- DONATE & RESERVE
- MIGRATE
- NOOP
Swap
Perform a swap.
SWAP:ASSET:DESTADDR:LIM/INTERVAL/QUANTITY:AFFILIATE:FEE
Perform a swap.
SWAP:ASSET:DESTADDR:LIM/INTERVAL/QUANTITY:AFFILIATE:FEE
Parameter | Notes | Conditions |
---|---|---|
Payload | Send the asset to swap. | Must be an active pool on THORChain. |
SWAP | The swap handler. | Also s , = |
:ASSET | The asset identifier. | Can be shortened. |
:DESTADDR | The destination address to send to. | Can use THORName. |
:LIM | The trade limit, i.e., set 100000000 to get a minimum of 1 full asset, else a refund. | Optional, 1e8 or Scientific Notation. |
/INTERVAL | Swap interval in blocks. | Optional, 1 means do not stream. |
/QUANTITY | Swap Quantity. Swap interval times every Interval blocks. | Optional, if 0, network will determine the number of swaps. |
:AFFILIATE | The affiliate address. RUNE is sent to Affiliate. | Optional. Must be THORName or THOR Address. |
:FEE | The affiliate fee. Limited from 0 to 1000 Basis Points. | Optional. Limited from 0 to 1000 Basis Points. |
Syntactic Examples:
SWAP:ASSET:DESTADDR
simple swapSWAP:ASSET:DESTADDR:LIM
swap with trade limitSWAP:ASSET:DESTADDR:LIM/1/1
swap with limit, do not stream swapSWAP:ASSET:DESTADDR:LIM/3/0
swap with limit, optimise swap amount, every 3 blocksSWAP:ASSET:DESTADDR:LIM/1/0:AFFILIATE:FEE
swap with limit, optimised and affiliate fee
Actual Examples:
SWAP:ETH.ETH:0xe6a30f4f3bad978910e2cbb4d97581f5b5a0ade0
- swap to Ether, send output to the specified address.SWAP:ETH.ETH:0xe6a30f4f3bad978910e2cbb4d97581f5b5a0ade0:10000000,
same as above except the Ether output should be more than 0.1 Ether else refund.SWAP:ETH.ETH:0xe6a30f4f3bad978910e2cbb4d97581f5b5a0ade0:10000000/1/1,
same as above except explicitly stated, do not stream the swap.SWAP:ETH.ETH:0xe6a30f4f3bad978910e2cbb4d97581f5b5a0ade0:10000000/3/0,
same as above except told to allow streaming swap, mini swap every 3 blocks and THORChain to work out the number of swaps required to achieve optimal price efficiency.SWAP:ETH.ETH:0xe6a30f4f3bad978910e2cbb4d97581f5b5a0ade0:10000000/3/0:t:10
- same as above except will send 10 basis points from the input and send it tot
(THORSwap's THORName).
The above memo can be further reduced to:
=:ETH.ETH:0xe6a30f4f3bad978910e2cbb4d97581f5b5a0ade0:1e6/3/0:t:10
Other examples:
=: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, six swaps, every two blocks.
Deposit Savers
ADD:POOL::AFFILIATE:FEE
Depositing savers can work without a memo; however, memos are recommended to be explicit about the transaction intent.
Parameter | Notes | Conditions |
---|---|---|
Payload | The asset to add liquidity with. | Must be supported by THORChain. |
ADD | The Deposit handler. | Also a + |
:POOL | The pool to add liquidity to. | Gas and stablecoin pools only. |
: | Must be empty | Optional, Required if adding affiliate and fee |
:AFFILIATE | The affiliate address. RUNE is sent to Affiliate. | Optional. Must be THORName or THOR Address. |
:FEE | The affiliate fee. Limited from 0 to 1000 Basis Points. | Optional. Limited from 0 to 1000 Basis Points. |
Examples:
+:BTC/BTC
add to the BTC Savings Vaulta:ETH/ETH
add to the ETH Savings Vault+:BTC/BTC::t:10
Deposit with a 10 basis points affiliate
Withdraw Savers
WITHDRAW:POOL
Parameter | Notes | Extra |
---|---|---|
Payload | Send the dust threshold of the asset to cause the transaction to be picked up by THORChain. | Caution Dust Limits: BTC,BCH,LTC chains 10k sats; DOGE 1m Sats; ETH 0 wei; THOR 0 RUNE. |
WITHDRAW | The withdraw handler. | Also - wd |
:POOL | The pool to withdraw liquidity from. | Gas and stablecoin pools only. |
:BASISPOINTS | Basis points (0-10000, where 10000=100%). | Optional. Limited from 0 to 1000 Basis Points. |
Examples:
-:BTC/BTC:10000
Withdraw 100% from BTC Saversw:ETH/ETH:5000
Withdraw 50% from ETH Savers
Open Loan
LOAN+:ASSET:DESTADDR:MINOUT:AFFILIATE:FEE
Parameter | Notes | Conditions |
---|---|---|
Payload | The collateral to open the loan with. | Must be L1 supported by THORChain. |
LOAN+ | The Loan Open handler. | also $+ |
:ASSET | Target debt asset identifier. | Can be shortened. |
:DESTADDR | The destination address to send the debt to. | Can use THORName. |
:MINOUT | Similar to LIM, Min debt amount, else a refund. | Optional, 1e8 format. |
:AFFILIATE | The affiliate address. The affiliate is added to the pool as an LP. | Optional. Must be THORName or THOR Address. |
:FEE | The affiliate fee. Fee is allocated to the affiliate. | Optional. Limited from 0 to 1000 Basis Points. |
Examples:
$+: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
LOAN-:ASSET:DESTADDR:MINOUT
Parameter | Notes | Conditions |
---|---|---|
Payload | The repayment for the loan. | Must be L1 supported on THORChain. |
LOAN- | The Loan Repayment handler. | also $- |
:ASSET | Target collateral asset identifier. | Can be shortened. |
:DESTADDR | The destination address to send the collateral to. | Can use THORName. |
:MINOUT | Similar to LIM, Min collateral to receive else a refund. | Optional, 1e8 format, loan needs to be fully repaid to close. |
Examples:
LOAN-:BTC.BTC:bc1qp2t4hl4jr6wjfzv28tsdyjysw7p5armf7px55w
Repay BTC loan owned by owner bc1qp2t4hl4jr6wjfzv28tsdyjysw7p5armf7px55w.LOAN-:ETH.ETH:0xe9973cb51ee04446a54ffca73446d33f133d2f49:404204059
. Repay ETH loan owned by0xe9973cb51ee04446a54ffca73446d33f133d2f49
and receive at least 4.04 ETH collateral back, else send back a refund.
Add Liquidity
There are rules for adding liquidity, see the rules here.
ADD:POOL:PAIREDADDR:AFFILIATE:FEE
Parameter | Notes | Conditions |
---|---|---|
Payload | The asset to add liquidity with. | Must be supported by THORChain. |
ADD | The Add Liquidity handler. | also a + |
:POOL | The pool to add liquidity to. | Can be shortened. |
:PAIREDADDR | The 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. |
:AFFILIATE | The affiliate address. The affiliate is added to the pool as an LP. | Optional. Must be THORName or THOR Address. |
:FEE | The affiliate fee. Fee is allocated to the affiliate. | Optional. Limited from 0 to 1000 Basis Points. |
Examples:
ADD:POOL
single-sided add liquidity. If this is a position's first add, liquidity can only be withdrawn to the same address.+:POOL:PAIREDADDR
add on both sides.+:POOL:PAIREDADDR:AFFILIATE:FEE
add with affiliate+:BTC.BTC:
Withdraw Liquidity
Withdraw liquidity from a pool.
A withdrawal can be either dual-sided (withdrawn based on pool's price) or entirely single-sided (converted to one side and sent out).
WD:POOL:BASISPOINTS:ASSET
Parameter | Notes | Extra |
---|---|---|
Payload | Send the dust threshold of the asset to cause the transaction to be picked up by THORChain. | Caution Dust Limits: BTC,BCH,LTC chains 10k sats; DOGE 1m Sats; ETH 0 wei; THOR 0 RUNE. |
WITHDRAW | The withdraw handler. | Also - wd |
:POOL | The pool to withdraw liquidity from. | Can be shortened. |
:BASISPOINTS | Basis points (0-10000, where 10000=100%). | |
:ASSET | Single-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, eg:wd:BTC.BTC:5000:BTC.BTC
DONATE & RESERVE
Donate to a pool or the RESERVE.
DONATE:POOL
Parameter | Notes | Extra |
---|---|---|
Payload | The asset to donate to a THORChain pool. | Must be supported by THORChain. Can be RUNE or ASSET. |
DONATE | The donate handler. | Also % |
:POOL | The pool to withdraw liquidity from. | Can be shortened. |
Example: DONATE:ETH.ETH
- Donate to the ETH pool.
RESERVE
Parameter | Notes | Extra |
---|---|---|
Payload | THOR.RUNE. | The RUNE to credit to the RESERVE. |
RESERVE | The reserve handler. |
BOND, UNBOND & LEAVE
Perform node maintenance features. Also see Pooled Nodes.
BOND:NODEADDR:PROVIDER:FEE
Parameter | Notes | Extra |
---|---|---|
Payload | The asset to bond to a Node. | Must be RUNE. |
BOND | The bond handler. | Anytime. |
:NODEADDR | The node to bond with. | |
:PROVIDER | Whitelist in a provider. | Optional, add a provider |
:FEE | Specify an Operator Fee in Basis Points. | Optional, default will be the mimir value (2000 Basis Points). Can be changed anytime. |
UNBOND:NODEADDR:AMOUNT
Parameter | Notes | Extra |
---|---|---|
Payload | None required. | Use MsgDeposit . |
UNBOND | The unbond handler. | |
:NODEADDR | The node to unbond from. | Must be in standby only. |
:AMOUNT | The amount to unbond. | In 1e8 format. If setting more than actual bond, then capped at bond. |
:PROVIDER | Unwhitelist a provider. | Optional, remove a provider |
LEAVE:NODEADDR
Parameter | Notes | Extra |
---|---|---|
Payload | None required. | Use MsgDeposit . |
LEAVE | The leave handler. | |
:NODEADDR | The 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
MIRGRATE
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
Parameter | Notes | Extra |
---|---|---|
Payload | Assets migrating | |
MIGRATE | The migrate Handler | |
:BlockHeight | THORChain Blockhight to migrate | Must be a valid blockheight |
Example: MIGRATE:3494355
NOOP
Dev-centric functions to fix THORChain state. Caution: may cause loss of funds if not done exactly right at the right time.
*NOOP
**
Parameter | Notes | Extra |
---|---|---|
Payload | The asset to credit to a vault. | Must be ASSET or RUNE. |
NOOP | The noop handler. | Adds to the vault balance, but does not add to the pool. |
:NOVAULT | Do not credit the vault. | Optional. Just fix the insolvency issue. |
Refunds
The following are the conditions for refunds:
Condition | Notes |
---|---|
Invalid MEMO | If the MEMO is incorrect the user will be refunded. |
Invalid Assets | If the asset for the transaction is incorrect (adding an asset into a wrong pool) the user will be refunded. |
Invalid Transaction Type | If the user is performing a multi-send vs a send for a particular transaction, they are refunded. |
Exceeding Price Limit | If the final value achieved in a trade differs to expected, they are refunded. |
Refunds cost fees to prevent Denial of Service attacks. The user will pay the correct outbound fee for that chain.
Other Internal Memos
donate
- add funds to a pool (example:DONATE:ETH.ETH
).consolidate
- consolidate UTXO transactions.ragnarok
- only used to delist pools.yggdrasilfund
andyggdrasilreturn
- not used as Yggdrasil vaults are no longer used (ADR 002).switch
- no longer used as killswich has ended.reserve
- Used to add RUNE to the Reserve Module as MsgSend to network modules is disallowed.
Asset Notation
THORChain uses a CHAIN.ASSET notation for all assets. TICKER and ID are added where required. The asset full notation is pictured.
There are three kinds of assets within THORChain:
- Layer 1 Assets - CHAIN.ASSET
- Synthetic Assets - CHAIN/Asset
- Derived Assets - THOR.ASSET
Examples
Asset | Notation |
---|---|
Bitcoin | BTC.BTC (Native BTC) |
Bitcoin | BTC/BTC (Synthetic BTC) |
Bitcoin | THOR.BTC (Derived BTC) |
Ethereum | ETH.ETH |
USDT | ETH.USDT-0xdac17f958d2ee523a2206206994597c13d831ec7 |
BNB | BNB.BNB (Native) |
BNB | BNB/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
andAVAX.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 andBNB.RUNE-67C
denoted Testnet RUNE.
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 ofCHAIN.ASSET, e
.g. Synthetic BTC isBTC/BTC
and Synthetic USDT isETH/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
andAVAX/USDC
are different Synthetic assets created and redeemable on different chains.
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:
-
SWAP:ETH.ETH:0xe6a30f4f3bad978910e2cbb4d97581f5b5a0ade0:1612345678:thor1el4ufmhll3yw7zxzszvfakrk66j7fx0tvcslym:10
can be reduced to
=:e:dx:161e6:t:10
-
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:
Shortened Asset Names
Native asset names can be shortened to reduce the length of the memo. The exact list is here.
Shorten Asset | Asset Notation |
---|---|
r | THOR.RUNE |
a | AVAX.AVAX |
b | BTC.BTC |
c | BCH.BCH |
e | ETH.ETH |
g | GAIA.ATOM |
n | BNB.BNB |
s | BSC.BNB |
d | DOGE.DOGE |
e | ETH.ETH |
l | LTC.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 astr
.
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:
Function | Abbreviated | Recommended |
---|---|---|
Swap | s | = |
Add / Deposit | + | |
Withdraw | wd | - |
Loan Open | Loan+ | $+ |
Loan Close | Loan- | $- |
THORName | name, n | ~ |
Limit Order | Limitto | lo |
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
Network Halts
If the network is halted, do not send funds. The easiest check to do is if halted = true
on the inbound addresses endpoint.
In most cases funds won't be lost if they are sent when halted, but they may be significantly delayed.
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 halted.
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.
- Specific Chain Signing Halt - Allows inbound transactions but stops the signing of outbound transactions. Outbound transactions are queued. This is the least impactful halt.
- Mimir setting is
HALTSIGNING[Chain]
, e.g.HALTSIGNINGBNB
- Mimir setting is
- Specific Chain Liquidity Provider Pause - addition and withdrawal of liquidity are suspended but swaps and other transactions are processed.
- Mimir setting is
PAUSELP[Chain]
, e,g,PAUSELPBCH
for BCH
- Mimir setting is
- 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.
- Mimir setting is
HALT[Chain]TRADING
, e,g,HALTBCHTRADING
for BCH
- Mimir setting is
- 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.
- Mimir setting is
HALT[Chain]CHAIN
, e,g,HALTBCHCHAIN
for BCH.
- Mimir setting is
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.
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.
ILP Management
- ILP is managed by the integer setting
FULLIMPLOSSPROTECTIONBLOCKS
, the number of blocks after which impermanent loss protection reaches 100% (zero = disabled). Impermanent loss protection scales linearly between an LP'slast_add_height
andlast_add_heigh
t + `FULLIMPLOSSPROTECTIONBLOCKS`.
- As of January 2023, nodes voted to
DEPRECATEILP
(ADR 005). As a result, theILPCUTOFF
mimir was set to block height9450000
(approx. 30 days after node voted passed). After this block, new LPs (or changes to existing LPs) do not receive impermanent loss protection. Existing LPs that have not added to their LP continue to receive ILP in perpetuity.
See also Constants and Mimir.
Fees
Overview
There are 4 different fees the user should know about.
- Inbound Fee (sourceChain: gasRate * txSize)
- Affiliate Fee (affiliateFee * swapAmount)
- Liquidity Fee (swapSlip * swapAmount)
- 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).
THORChain calculates and posts fee rates at https://thornode.ninerealms.com/thorchain/inbound_addresses
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.
- Register a THORName if not done already. This is done with a
MsgDeposit
posted to the THORChain network. - 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:
-
Grab the full USDC name from the Pools endpoint:
ETH.USDC-0XA0B86991C6218B36C1D19D4A2E9EB0CE3606EB48
-
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:- THORChain address:
thor1dl7un46w7l7f3ewrnrm6nq58nerjtp0dradjtd
- THORName:
ac-test
- ETH payout address:
0x6621d872f17109d6601c49edba526ebcfd332d5d
The full memo would look like:
~:ac-test:ETH:0x6621d872f17109d6601c49edba526ebcfd332d5d:thor1dl7un46w7l7f3ewrnrm6nq58nerjtp0dradjtd:ETH.USDC-0XA0B86991C6218B36C1D19D4A2E9EB0CE3606EB48
- THORChain address:
You can use Asgardex to post a MsgDeposit with a custom memo. Load your wallet, then open your THORChain wallet page > Deposit > Custom.
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.
- Inbound Fee (user wallet controlled, not THORChain controlled)
- Affiliate Fee (if any) - skimmed from the input.
- Swap Fee (denoted in output asset)
- 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:
- The Destination Chain outbound_fee
- The Source Chain outbound_fee
- $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_rate
is 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_rate
is 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)
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.
- Inbound Confirmation
- Observation Counting
- Confirmation Counting
- Outbound Delay
- 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.
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.
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.
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, but the maximum delay is 1hr. It does this by computing the value of the outbound transaction 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.
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
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
- Use the Quote endpoint to get the estimated fee.
- 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.
- Every minute, poll Midgard and see if the swap is processed.
- 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!
- 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
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
.
Memo limited to 80 bytes on BTC, BCH, LTC and DOGE. Use abbreviated options and THORNames where possible.
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.
Override randomised VOUT ordering; THORChain requires specific output ordering. Funds using wrong ordering are very likely to be lost.
EVM Chains
{{#embed 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
ETH is sent and received as an internal transaction. Your wallet may not be set to read internal balances and transactions.
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)
-
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
andpbts
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
-
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.
{{#embed https://gitlab.com/thorchain/asgardex-common/asgardex-util/-/tree/master/src/calc }}
{{#embed 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 log 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:
- User swaps long-tail ERC20 to ETH in SushiSwap, then swaps that ETH to BTC.
- 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.
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 log 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:
- User swaps long-tail ERC20 to ETH in SushiSwap, then swaps that ETH to BTC.
- 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.
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|
Parameter | Nodes | Conditions |
---|---|---|
:DEXAggregatorAddr | The whitelisted aggregator contract. | Can use the last x characters of the address to fuzz match it. |
:FinalTokenAddr | The final token (must be on 1INCH Whitelist) | Can be shortened |
:minAmountOut | The 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. |
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:
- Swap asset is
ETH.ETH
2386f3848
is the last nine characters of the aggregator contract address0x0a44986b70527154e9F4290eC14e5f0D1C861822
is the final asset address- 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
{{#embed 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
xcode-select xcode-select --install
- 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
Popular Commands
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
xcode-select xcode-select --install
- 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
Popular Commands
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
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
- Create a milestone using the release version (e.g. Release-1.116.0)
- Tag issues & PRs using the milestone, so we can identify which PR is on which version
- PRs need to get approved by #thornode-team and #thorsec. Once approved, merge to
develop
branch - 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
- From your release branch, run
make build-mocknet
. - Create a mocknet cluster using
make reset-mocknet-cluster
(follow README.md). - 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
- Identify unexpected log / behaviour, and investigate it.
Release to stagenet
Build stagenet
-
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. -
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
- Create a test plan either mentally, on Discord or on Notion (e.g. Stagenet 1.99 Test Plan)
- 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:
- Ensure all bifrost are running the latest version
- Ensure
loadchains.go
has successfully connected to the daemon. - Ensure the new daemon is fully sync'd.
- Trigger a churn to create the asgard vault.
- Create a pool by sending L1 and RUNE to asgard.
- Once the pool is seeded with enough LP to pay outbound fees, churn the network again.
- 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.
- Apply the
stagenet
image(s) one-by-one. - Wait for them to fully initialize and rejoin consensus.
- Run
make set-version
. - Repeat until all validators on latest version.
- Check:
curl thornode:1317/thorchain/version
Validate stagenet
- Conduct your Stagenet Test Plan.
- Document any findings or issues in
#stagenet
on Discord. - Determine if any changes need to be made to the release candidate.
Release to mainnet
Build mainnet
-
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) -
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
- 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) - 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
- Quickly go through all the PRs in the release.
- Apply the latest changes to a standby node and monitor the following:
- THORNode pod didn't get into
CrashloopBackoff
- Version has been set correctly
- Bifrost started correctly.
- THORNode pod didn't get into
Release
- 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
). - 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. - After the tag is created, go to the UI, click
Create Release
. Use the PR log for the description again. - Post release announcement in #thornode-mainnet. Use previous messages as a template. Be sure to update the version number and tag URL.
- 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
- Create a milestone using the release version (e.g. Release-1.116.0)
- Tag issues & PRs using the milestone, so we can identify which PR is on which version
- PRs need to get approved by #thornode-team and #thorsec. Once approved, merge to
develop
branch - 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
- From your release branch, run
make build-mocknet
. - Create a mocknet cluster using
make reset-mocknet-cluster
(follow README.md). - 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
- Identify unexpected log / behaviour, and investigate it.
Release to stagenet
Build stagenet
-
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. -
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
- Create a test plan either mentally, on Discord or on Notion (e.g. Stagenet 1.99 Test Plan)
- 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:
- Ensure all bifrost are running the latest version
- Ensure
loadchains.go
has successfully connected to the daemon. - Ensure the new daemon is fully sync'd.
- Trigger a churn to create the asgard vault.
- Create a pool by sending L1 and RUNE to asgard.
- Once the pool is seeded with enough LP to pay outbound fees, churn the network again.
- 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.
- Apply the
stagenet
image(s) one-by-one. - Wait for them to fully initialize and rejoin consensus.
- Run
make set-version
. - Repeat until all validators on latest version.
- Check:
curl thornode:1317/thorchain/version
Validate stagenet
- Conduct your Stagenet Test Plan.
- Document any findings or issues in
#stagenet
on Discord. - Determine if any changes need to be made to the release candidate.
Release to mainnet
Build mainnet
-
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) -
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
- 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) - 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
- Quickly go through all the PRs in the release.
- Apply the latest changes to a standby node and monitor the following:
- THORNode pod didn't get into
CrashloopBackoff
- Version has been set correctly
- Bifrost started correctly.
- THORNode pod didn't get into
Release
- 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
). - 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. - After the tag is created, go to the UI, click
Create Release
. Use the PR log for the description again. - Post release announcement in #thornode-mainnet. Use previous messages as a template. Be sure to update the version number and tag URL.
- 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
- Pool Token Whitelist - allows to be a pool on THORChain
- DEX Token Whitelist - allows to be swapped to using DEX Aggregation
- 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 callrouter.depositWithExpiry(params, +15minsUNIXSeconds)
, - If support
swapOut(params)
, must have a function exactlyswapOut(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
)
- Set admin mimir
ChurnInterval
->432000
to stop churn - Set admin mimir
StopSolvencyCheck<CHAIN>
->1
to stop Solvency checker onCHAIN
, this will make sure the migration fund will not cause solvency checker to halt the chain - Set admin mimir
MimirUpgradeContract<CHAIN>
->1
to update the router - Set admin mimir
ChurnInterval
->43200
- Wait a churn to kick off , and make sure funds have been migrated from older router to new router. And vault retired successfully
- Set admin mimir
StopSolvencyCheck<CHAIN>
->0
to resume solvency checker onCHAIN
Mimir Abilities
Tx Out
OutboundTransactionFee
: Amount of rune to withhold on all outbound transactions (1e8 notation)
Scheduled Outbound
MaxTxOutOffset
: Max number of blocks a scheduled outbound transaction can be delayed
MinTxOutVolumeThreshold
: Quantity of outbound value (in 1e8 rune) in a block before its 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)
Synths
MaxSynthPerAssetDepth
: The amount of synths allowed per pool relative to the pool depth
BurnSynths
: Enable/Disable burning synths
MintSynths
: Enable/Disable minting synths
VirtualMultSynths
: The amount of increase the pool depths for calculating swap fees of synths
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
: Max rune capped on the pools
Impermanet Loss Protection
FullImpLossProtectionBlocks
: Number of blocks before an LP gets full imp loss protection
ILP-DISABLED-<asset>
: Enable/Disable imp loss protection per asset
Chain Management
HaltChainGlobal
: Pause all chains (chain clients)
Halt<chain>Chain
: Pause a specific blockchain via mimir or detected double-spend
SolvencyHalt<chain>Chain
: Solvency checker auto halts chain. Chain will be auto un-halted once solvency is regained
NodePauseChainGlobal
: Individual node controlled means to pause all chains
NodePauseChainBlocks
: Number of block a node operator can pause/resume the chains for
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
MaximumBondInRune
: Sets an upper cap on how much a node can bond
MinimumBondInRune
: Sets a lower bound on bond for a node to be considered to be churned in
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
Yggdrasil Management
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
Churning
AsgardSize
: Defines the number of members to an Asgard vault
MinSlashPointsForBadValidator
: Min 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
: Max 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
MaxNodeToChurnOutForLowVersion
: Max 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 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
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
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
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
file | func | logic |
---|---|---|
common/address.go | func NewAddress(address string) (Address, error) | Add logic to parse an address |
common/chain.go | func (c Chain) GetGasAsset() Asset | Return gas asset for the chain |
common/chain.go | define a chain variable at the top | like https://gitlab.com/thorchain/thornode/-/blob/develop/common/chain.go#L22 |
common/gas.go | func UpdateGasPrice(tx Tx, asset Asset, units []cosmos.Uint) []cosmos.Uint | add logic in regards to how to update gas |
common/asset.go | define an asset | like https://gitlab.com/thorchain/thornode/-/blob/develop/common/asset.go#L22 |
common/pubkey.go | func (pubKey PubKey) GetAddress(chain Chain) (Address, error) | add logic to get address from a pubic key |
build/docker/components/newchain.yml | docker 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.yml | Update 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
- Create a new folder under the root folder, like "newchain-daemon"
- Add new helm chart to run the chain client daemon
- 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
- First, create a new folder under bifrost\pkg\chainclients
- 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()
}
- 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)
}
- 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
-
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), andnode-launcher
repos.- All PRs should meet the public requirements set forth in Technical Requirements and Guidelines.
-
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
- 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.
-
Admin Mimir: Halt the new chain and disable trading until rollout is complete.
-
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. -
Enable Bifrost Scanning: The final
node-launcher
PR will be merged, and NOs instructed to perform a finalmake install
to enable Bifrost scanning. -
Admin Mimir: Unhalt the chain to enable Bifrost scanning.
-
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
- Ensure a "mocknet" (local development network) service for the chain daemon is be added (
build/docker/docker-compose.yml
). - Ensure 70% or greater unit test coverage.
- Ensure a
<chain>_DISABLED
environment variable is respected in the Bifrost initialization script atbuild/scripts/bifrost.sh
. - 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).
- 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?
- Does the chain client properly whitelist valid assets and reject invalid assets?
- Does the chain client properly convert asset values to/from the 8 decimal point standard of thornode?
- Is gas reporting deterministic? Every Bifrost must agree, or THORChain will not reach consensus.
- 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.
- Image PR
- Add a Dockerfile at
ci/images/<chain>/Dockerfile
. - Ensure all source versions in the Dockerfile are pinned to a specific git hash.
- Add a Dockerfile at
- Services PR
- Use an existing chain directory as a template for the new chain daemon configuration, reference the PR for the last added chain.
- Ensure the resource request sizes for the daemon are slightly over-provisioned (~20%) to the average expected utilization under normal operation.
- Extend the
get_node_service
function inscripts/core.sh
with the service so that it is available for the standard make targets. - Extend the
deploy_fullnode
function inscripts/core.sh
with--set <daemon-name>.enabled=false
in both the diff and install commands. - Ensure the
<chain>_DISABLED
environment variable is used to disable the chain via a variable inbifrost/values.yaml
.
- Enable PR
- Update
bifrost/values.yaml
to enable the chain.
- Update
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
- 002 - Remove Yggdrasil Vaults
- 003 - Floored Outbound Fee
- 004 - Keyshare Backups
- 005 - Deprecate Impermanent Loss Protection
- 006 - Enable POL
Deprecated
None
Rejected
None
Proposed
On Pause
- 001 - ThorChat by request of author
ADR Creation Process
- Copy the TEMPLATE.md file. Use the following filename pattern:
adr-next_number-title.md
- Create a draft Pull Request if you want to get an early feedback.
- Make sure the context and a solution is clear and well documented.
- Add an entry to a list in the README file.
- 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.
- 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.
- Once the motivation is validated, a GitHub Pull Request (PR) is created with a new document based on the
TEMPLATE.md
. - 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. - 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.
- Pros:
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)
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:
- Code complexity with ygg vaults have in past opened up exploit loopholes
- Ygg vaults often go insolvent and lock node bonds
- Increased vault-management costs (ygg funding)
- 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:
However, 3 things have happened since:
- Asgard Vault Sharding logic, introduced in Q2 2021, split asgards to allow scaling past 40 nodes
- Synths, launched in Q1 2022, absorbed a significant part of arbitrage volume, reducing demand on L1 outbounds
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
STOPFUNDYGGDRASIL = 1
to stop yggs being funded (node churns will slowly empty yggs back to Asgard)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)
- Put out for a temperature check
- If well-received, then go via
admin-mimir
(notnode-mimir
because this is a network-security optimisation, and it may need to be quickly rolled back in case issues)
admin-mimir
STOPFUNDYGGDRASIL = 1
to stop yggs being funded (node churns will slowly empty yggs back to Asgard)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:
- Roll back to ygg-vaults
- 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:
- 10-100 times higher than a synth swap
- 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:
- Compromised (not necessarily malicious) infrastructure, tooling, operator machines
- Forced provider shutdown due to censorship, unpaid accounts, etc
- 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:
- Validate BIP39 mnemonic.
- 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:
- Check that encrypted output is not equal to input.
- Check that decrypted output equals the input.
- Check that the output does NOT contain the input.
- Check that the output does NOT contain the passphrase.
- Check that the output does NOT contain any word of the passphrase.
Positives
- 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.
- Embedding the shares in the
TssPool
messages ensures that the shares are preserved immediately at the time of creation.
Negatives
- 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.
- 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
- 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
- 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
- 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
- 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
- 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
- Additional dependency, complexity, backup point of failure for IPFS integration.
Open Questions
The following questions are generally relevant for any approach taken.
- Symmetric encryption with mnemonic or asymmetric with key (generated from mnemonic)?
Update: It seems devs are mostly satisfied with currently proposed symmetric approach.
- 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
- 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.
- 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
to4500
POLBuffer
to500
PoL will enter at4500 + 500 = 50%
but exit at4500 - 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
to3000
POLBuffer
to2000
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
to4500
: POL will continue adding RUNE to a pool until the synth depth of that pool is 45%.POLBuffer
to500
: 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
to1
: POL will move the pool price at most 0.01% in one blockPOLMaxNetworkDeposit
to1000000000000
: 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 thePOLMaxNetworkDeposit
.POL-BTC-BTC
to1
: 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
to1
: 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
to6000
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
to1
) ENABLEPOL
to1
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:
- Increase
FundMigrationInterval
to3600
(5x current), which will result in the time to churn out taking roughly half of a current churn cycle. - Allow nodes a few weeks to prepare for the change in cadence and then increase the churn interval to
129600
(3x current) andFundMigrationInterval
to14400
, 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
- 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.
- 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
- Overhaul fee price denomination
- 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 **
Fee | Description | Amount | Recipient |
---|---|---|---|
Liquidity Fee | Paid on every swap | Proportional to slip | 100% to Network participants intra-block |
L1 Outbound Fee | L1 Outbounds | Ideally 1:1 gas spent, but a minimum of $1.00 is enforced to pay for TSS resources | Reserve |
Native Outbound Fee | RUNE and synth outbounds | 0.02 RUNE | Reserve |
Native Transaction Fee | RUNE and synth transfers | 0.02 RUNE | Reserve |
TNS Fees | Fees to register TNS | 10 RUNE + 10 RUNE per year | Reserve |
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:
-
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).
-
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.
-
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.
-
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.
-
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.
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
- Read https://gitlab.com/thorchain/thornode/-/blob/develop/docs/newchain.md
- Bifrost: Start by forking one of the existing Bifrosts (UTXO, EVM or BFT).
- Daemon: Add the chain daemon to THORChain/Node-Launcher https://gitlab.com/thorchain/thornode/-/tree/develop/bifrost/pkg/chainclients
- Smoke Tests: Build out the smoke tests for the chain. This ensures the connection is robustly tested.
- 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
- Read https://gitlab.com/thorchain/thornode/-/blob/develop/docs/newchain.md
- Bifrost: Start by forking one of the existing Bifrosts (UTXO, EVM or BFT).
- Daemon: Add the chain daemon to THORChain/Node-Launcher https://gitlab.com/thorchain/thornode/-/tree/develop/bifrost/pkg/chainclients
- Smoke Tests: Build out the smoke tests for the chain. This ensures the connection is robustly tested.
- 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:
- Observer (Scans blocks and packages up events to be witnessed to THORChain)
- 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.
{{#embed https://gitlab.com/thorchain/thornode/-/blob/develop/bifrost/observer/observe.go#L117 }}
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.
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:
- Detect the gas spent in each block, then detect the block size. The gas rate is thus the
gasSpent / blockSize
- This is now the average gasRate, which is typically 50--100% higher than the lowest gas rate to get in the block.
- 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
{{#embed https://gitlab.com/thorchain/thornode/-/blob/develop/bifrost/pkg/chainclients/bitcoin/client.go }}
Signer
{{#embed 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).
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
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.
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.
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
{{#embed 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
).
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.
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.
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:
{{#embed 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:
{{#embed https://gitlab.com/thorchain/thornode/-/merge_requests/2085/diffs }}