diff --git a/bench-exchange/README.md b/bench-exchange/README.md new file mode 100644 index 0000000000..87dfea00aa --- /dev/null +++ b/bench-exchange/README.md @@ -0,0 +1,508 @@ +# token-exchange +Solana Token Exchange Bench + +If you can't wait; jump to [Running the exchange](#Running-the-exchange) to +learn how to start and interact with the exchange. + +### Table of Contents +[Overview](#Overview)
+[Premiss](#Premiss)
+[Exchange startup](#Exchange-startup)
+[Trade requests](#Trade-requests)
+[Trade cancellations](#Trade-cancellations)
+[Trade swap](#Trade-swap)
+[Exchange program operations](#Exchange-program-operations)
+[Quotes and OHLCV](#Quotes-and-OHLCV)
+[Investor strategies](#Investor-strategies)
+[Running the exchange](#Running-the-exchange)
+ +## Overview + +An exchange is a marketplace where one asset can be traded for another. This +demo demonstrates one way to host an exchange on the Solana blockchain by +emulating a currency exchange. + +The assets are virtual tokens held by investors who may post trade requests to +the exchange. A broker monitors the exchange and posts swap requests for +matching trade orders. All the transactions can execute concurrently. + +## Premise + +- Exchange + - An exchange is a marketplace where one asset can be traded for another. + The exchange in this demo is the on-chain program that implements the + tokens and the policies for trading those tokens. +- Token + - A virtual asset that can be owned, traded, and holds virtual intrinsic value + compared to other assets. There are four types of tokens in this demo, A, + B, C, D. Each one may be traded for another. +- Token account + - An account owned by the exchange that holds a quantity of one type of token. +- Account request + - A request to create a token account +- Token request + - A request to deposit tokens of a particular type into a token account. +- Token pair + - A unique ordered list of two tokens. For the four types of tokens used in + this demo, the valid pairs are AB, AC, AD, BC, BD, CD. +- Direction of trade + - Describes which token in the pair the investor wants to sell and buy and can + be either "To" or "From". For example, if an investor issues a "To" trade + for "AB" then they which to exchange A tokens to B tokens. A "From" order + would read the other way, A tokens from B tokens. +- Price ratio + - An expression of the relative prices of two tokens. They consist of the + price of the primary token and the price of the secondary token. For + simplicity sake, the primary token's price is always 1, which forces the + secondary to be the common denominator. For example, if token A was worth + 2 and token B was worth 6, the price ratio would be 1:3 or just 3. Price + ratios are represented as fixed point numbers. The fixed point scaler is + defined in + [exchange_state.rs](https://github.com/solana-labs/solana/blob/c2fdd1362a029dcf89c8907c562d2079d977df11/programs/exchange_api/src/exchange_state.rs#L7) +- Trade request + - A Solana transaction executed by the exchange requesting the trade of one + type of token for another. Trade requests are made up of the token pair, + the direction of the trade, quantity of the primary token, the price ratio, + and the two token accounts to be credited/deducted. An example trade + request looks like "T AB 5 2" which reads "Exchange 5 A tokens to B tokens + at a price ratio of 1:2" A fulfilled trade would result in 5 A tokens + deducted and 10 B tokens credited to the trade initiator's token accounts. + Successful trade requests result in a trade order. +- Trade order + - The result of a successful trade request. Trade orders are stored in + accounts owned by the submitter of the trade request. They can only be + canceled by their owner but can be used by anyone in a trade swap. They + contain the same information as the trade request. +- Price spread + - The difference between the two matching trade orders. The spread is the + profit of the broker initiating the swap request. +- Swap requirements + - Policies that result in a successful trade swap. +- Swap request + - A request to exchange tokens between to trade orders +- Trade swap + - A successful trade. A swap consists of two matching trade orders that meet + swap requirements. A trade swap may not wholly satisfy one or both of the + trade orders in which case the trade orders are adjusted appropriately. As + long as the swap requirements are met there will be an exchange of tokens + between accounts. Any price spread is deposited into the broker's profit + account. All trade swaps are recorded in a new account for posterity. +- Investor + - Individual investors who hold a number of tokens and wish to trade them on + the exchange. Investors operate as Solana thin clients who own a set of + accounts containing tokens and/or trade requests. Investors post + transactions to the exchange in order to request tokens and post or cancel + trade requests. +- Broker + - An agent who facilitates trading between investors. Brokers operate as + Solana thin clients who monitor all the trade orders looking for a trade + match. Once found, the broker issues a swap request to the exchange. + Brokers are the engine of the exchange and are rewarded for their efforts by + accumulating the price spreads of the swaps they initiate. Brokers also + provide current bid/ask price and OHLCV (Open, High, Low, Close, Volume) + information on demand via a public network port. +- Transaction fees + - Solana transaction fees are paid for by the transaction submitters who are + the Investors and Brokers. + +## Exchange startup + +The exchange is up and running when it reaches a state where it can take +investor's trades and broker's swap requests. To achieve this state the +following must occur in order: + +- Start the Solana blockchain +- Start the broker thin-client +- The broker subscribes to change notifications for all the accounts owned by + the exchange program id. The subscription is managed via Solana's JSON RPC + interface. +- The broker starts responding to queries for bid/ask price and OHLCV + +The broker responding successfully to price and OHLCV requests is the signal to +the investors that trades submitted after that point will be analyzed. + +Investors will initially query the exchange to discover their current balance +for each type of token. If the investor does not already have an account for +each type of token, they will submit account requests. Brokers as well will +request accounts to hold the tokens they earn by initiating trade swaps. + +```rust +/// Supported token types +pub enum Token { + A, + B, + C, + D, +} + +/// Supported token pairs +pub enum TokenPair { + AB, + AC, + AD, + BC, + BD, + CD, +} + +pub enum ExchangeInstruction { + /// New token account + /// key 0 - Signer + /// key 1 - New token account + AccountRequest, +} + +/// Token accounts are populated with this structure +pub struct TokenAccountInfo { + /// Investor who owns this account + pub owner: Pubkey, + /// Current number of tokens this account holds + pub tokens: Tokens, +} +``` + +For this demo investors or brokers can request more tokens from the exchange at +any time by submitting token requests. In non-demos, an exchange of this type +would provide another way to exchange a 3rd party asset into tokens. + +To request tokens, investors submit transfer requests: + +```rust +pub enum ExchangeInstruction { + /// Transfer tokens between two accounts + /// key 0 - Account to transfer tokens to + /// key 1 - Account to transfer tokens from. This can be the exchange program itself, + /// the exchange has a limitless number of tokens it can transfer. + TransferRequest(Token, u64), +} +``` + +## Trade requests + +When an investor decides to exchange a token of one type for another, they +submit a transaction to the Solana Blockchain containing a trade request, which, +if successful, is turned into a trade order. Trade orders do not expire but are +cancellable. When a trade order is created, tokens are deducted from a token +account and the trade order acts as an escrow. The tokens are held until the +trade order is fulfilled or canceled. If the direction is `To`, then the number +of `tokens` are deducted from the primary account, if `From` then `tokens` +multiplied by `price` are deducted from the secondary account. Trade orders are +no longer valid when the number of `tokens` goes to zero, at which point they +can no longer be used. + +```rust +/// Direction of the exchange between two tokens in a pair +pub enum Direction { + /// Trade first token type (primary) in the pair 'To' the second + To, + /// Trade first token type in the pair 'From' the second (secondary) + From, +} + +pub struct TradeRequestInfo { + /// Direction of trade + pub direction: Direction, + + /// Token pair to trade + pub pair: TokenPair, + + /// Number of tokens to exchange; refers to the primary or the secondary depending on the direction + pub tokens: u64, + + /// The price ratio the primary price over the secondary price. The primary price is fixed + /// and equal to the variable `SCALER`. + pub price: u64, + + /// Token account to deposit tokens on successful swap + pub dst_account: Pubkey, +} + +pub enum ExchangeInstruction { + /// Trade request + /// key 0 - Signer + /// key 1 - Account in which to record the swap + /// key 2 - Token account associated with this trade + TradeRequest(TradeRequestInfo), +} + +/// Trade accounts are populated with this structure +pub struct TradeOrderInfo { + /// Owner of the trade order + pub owner: Pubkey, + /// Direction of the exchange + pub direction: Direction, + /// Token pair indicating two tokens to exchange, first is primary + pub pair: TokenPair, + /// Number of tokens to exchange; primary or secondary depending on direction + pub tokens: u64, + /// Scaled price of the secondary token given the primary is equal to the scale value + /// If scale is 1 and price is 2 then ratio is 1:2 or 1 primary token for 2 secondary tokens + pub price: u64, + /// account which the tokens were source from. The trade account holds the tokens in escrow + /// until either one or more part of a swap or the trade is canceled. + pub src_account: Pubkey, + /// account which the tokens the tokens will be deposited into on a successful trade + pub dst_account: Pubkey, +} +``` + +## Trade cancellations + +An investor may cancel a trade at anytime, but only trades they own. If the +cancellation is successful, any tokens held in escrow are returned to the +account from which they came. + +```rust +pub enum ExchangeInstruction { + /// Trade cancellation + /// key 0 - Signer + /// key 1 -Trade order to cancel + TradeCancellation, +} +``` + +## Trade swaps + +The broker is monitoring the accounts assigned to the exchange program and +building a trade-order table. The trade order table is used to identify +matching trade orders which could be fulfilled. When a match is found the +broker should issue a swap request. Swap requests may not satisfy the entirety +of either order, but the exchange will greedily fulfill it. Any leftover tokens +in either account will keep the trade order valid for further swap requests in +the future. + +Matching trade orders are defined by the following swap requirements: + +- Opposite polarity (one `To` and one `From`) +- Operate on the same token pair +- The price ratio of the `From` order is greater than or equal to the `To` order +- There are sufficient tokens to perform the trade + +Orders can be written in the following format: + +`investor direction pair quantity price-ratio` + +For example: + +- `1 T AB 2 1` + - Investor 1 wishes to exchange 2 A tokens to B tokens at a ratio of 1 A to 1 + B +- `2 F AC 6 1.2` + - Investor 2 wishes to exchange A tokens from 6 B tokens at a ratio of 1 A + from 1.2 B + +An order table could look something like the following. Notice how the columns +are sorted low to high and high to low, respectively. Prices are dramatic and +whole for clarity. + +|Row| To | From | +|---|-------------|------------| +| 1 | 1 T AB 2 4 | 2 F AB 2 8 | +| 2 | 1 T AB 1 4 | 2 F AB 2 8 | +| 3 | 1 T AB 6 6 | 2 F AB 2 7 | +| 4 | 1 T AB 2 8 | 2 F AB 3 6 | +| 5 | 1 T AB 2 10 | 2 F AB 1 5 | + +As part of a successful swap request, the exchange will credit tokens to the +broker's account equal to the difference in the price ratios or the two orders. +These tokens are considered the broker's profit for initiating the trade. + +The broker would initiate the following swap on the order table above: + + - Row 1, To: Investor 1 trades 2 A tokens to 8 B tokens + - Row 1, From: Investor 2 trades 2 A tokens from 8 B tokens + - Broker takes 8 B tokens as profit + +Both row 1 trades are fully realized, table becomes: + +|Row| To | From | +|---|-------------|------------| +| 1 | 1 T AB 1 4 | 2 F AB 2 8 | +| 2 | 1 T AB 6 6 | 2 F AB 2 7 | +| 3 | 1 T AB 2 8 | 2 F AB 3 6 | +| 4 | 1 T AB 2 10 | 2 F AB 1 5 | + +The broker would initiate the following swap: + + - Row 1, To: Investor 1 trades 1 A token to 4 B tokens + - Row 1, From: Investor 2 trades 1 A token from 4 B tokens + - Broker takes 4 B tokens as profit + +Row 1 From is not fully realized, table becomes: + +|Row| To | From | +|---|-------------|------------| +| 1 | 1 T AB 6 6 | 2 F AB 1 8 | +| 2 | 1 T AB 2 8 | 2 F AB 2 7 | +| 3 | 1 T AB 2 10 | 2 F AB 3 6 | +| 4 | | 2 F AB 1 5 | + +The broker would initiate the following swap: + + - Row 1, To: Investor 1 trades 1 A token to 6 B tokens + - Row 1, From: Investor 2 trades 1 A token from 6 B tokens + - Broker takes 2 B tokens as profit + +Row 1 To is now fully realized, table becomes: + +|Row| To | From | +|---|-------------|------------| +| 1 | 1 T AB 5 6 | 2 F AB 2 7 | +| 2 | 1 T AB 2 8 | 2 F AB 3 5 | +| 3 | 1 T AB 2 10 | 2 F AB 1 5 | + +The broker would initiate the following last swap: + + - Row 1, To: Investor 1 trades 2 A token to 12 B tokens + - Row 1, From: Investor 2 trades 2 A token from 12 B tokens + - Broker takes 4 B tokens as profit + +Table becomes: + +|Row| To | From | +|---|-------------|------------| +| 1 | 1 T AB 3 6 | 2 F AB 3 5 | +| 2 | 1 T AB 2 8 | 2 F AB 1 5 | +| 3 | 1 T AB 2 10 | | + +At this point the lowest To's price is larger than the largest From's price so +no more swaps would be initiated until new orders came in. + +```rust +pub enum ExchangeInstruction { + /// Trade swap request + /// key 0 - Signer + /// key 1 - Account in which to record the swap + /// key 2 - 'To' trade order + /// key 3 - `From` trade order + /// key 4 - Token account associated with the To Trade + /// key 5 - Token account associated with From trade + /// key 6 - Token account in which to deposit the brokers profit from the swap. + SwapRequest, +} + +/// Swap accounts are populated with this structure +pub struct TradeSwapInfo { + /// Pair swapped + pub pair: TokenPair, + /// `To` trade order + pub to_trade_order: Pubkey, + /// `From` trade order + pub from_trade_order: Pubkey, + /// Number of primary tokens exchanged + pub primary_tokens: u64, + /// Price the primary tokens were exchanged for + pub primary_price: u64, + /// Number of secondary tokens exchanged + pub secondary_tokens: u64, + /// Price the secondary tokens were exchanged for + pub secondary_price: u64, +} +``` + +## Exchange program operations + +Putting all the commands together from above, the following operations will be +supported by the on-chain exchange program: + +```rust +pub enum ExchangeInstruction { + /// New token account + /// key 0 - Signer + /// key 1 - New token account + AccountRequest, + + /// Transfer tokens between two accounts + /// key 0 - Account to transfer tokens to + /// key 1 - Account to transfer tokens from. This can be the exchange program itself, + /// the exchange has a limitless number of tokens it can transfer. + TransferRequest(Token, u64), + + /// Trade request + /// key 0 - Signer + /// key 1 - Account in which to record the swap + /// key 2 - Token account associated with this trade + TradeRequest(TradeRequestInfo), + + /// Trade cancellation + /// key 0 - Signer + /// key 1 -Trade order to cancel + TradeCancellation, + + /// Trade swap request + /// key 0 - Signer + /// key 1 - Account in which to record the swap + /// key 2 - 'To' trade order + /// key 3 - `From` trade order + /// key 4 - Token account associated with the To Trade + /// key 5 - Token account associated with From trade + /// key 6 - Token account in which to deposit the brokers profit from the swap. + SwapRequest, +} +``` + +## Quotes and OHLCV + +The broker will provide current bid/ask price quotes based on trade actively and +also provide OHLCV based on some time window. The details of how the bid/ask +price quotes are calculated are yet to be decided. + +## Investor strategies + +To make a compelling demo, the investors needs to provide interesting trade +behavior. Something as simple as a randomly twiddled baseline would be a +minimum starting point. + +## Running the exchange + +The exchange bench posts trades and swaps matches as fast as it can. + +You might want to bump the duration up +to 60 seconds and the batch size to 1000 for better numbers. You can modify those +in client_demo/src/demo.rs::test_exchange_local_cluster. + +The following command runs the bench: + +```bash +$ RUST_LOG=solana_bench_exchange=info cargo test --release -- --nocapture test_exchange_local_cluster +``` + +To also see the cluster messages: + +```bash +$ RUST_LOG=solana_bench_exchange=info,solana=info cargo test --release -- --nocapture test_exchange_local_cluster +``` + + +