|
1 |
| -# TWAP |
| 1 | +# TWAP (Time Weighted Average Price) |
2 | 2 |
|
3 |
| -We maintain TWAP entries for every gamm pool. |
| 3 | +The TWAP package is responsible for being able to serve TWAPs for every AMM pool. |
4 | 4 |
|
5 |
| -NOTE: Not yet integrated into state machine, this package is a stub. |
| 5 | +A time weighted average price is a function that takes a sequence of `(time, price)` pairs, and returns a price representing an 'average' over the entire time period. The method of averaging can vary from the classic arithmetic mean, (such as geometric mean, harmonic mean), however we currently only implement arithmetic mean. |
6 | 6 |
|
7 |
| -## Basic architecture notes |
| 7 | +## Arithmetic mean TWAP |
8 | 8 |
|
9 |
| -We maintain the list of pools altered within in a block |
| 9 | +Using the arithmetic mean, the TWAP of a sequence `(t_i, p_i)`, from `t_0` to `t_n`, indexed by time in ascending order, is: $$\frac{1}{t_n - t_0}\sum_{i=0}^{n-1} p_i (t_{i+1} - t_i)$$ |
| 10 | +Notice that the latest price `p_n` isn't used, as it has lasted for a time interval of `0` seconds in this range! |
| 11 | + |
| 12 | +To illustrate with an example, given the sequence: `(0s, $1), (4s, $6), (5s, $1)`, the arithmetic mean TWAP is: |
| 13 | +$$\frac{\$1 * (4s - 0s) + \$6 * (5s - 4s)}{5s - 0s} = \frac{\$10}{5} = \$2$$ |
| 14 | + |
| 15 | +## Computation via accumulators method |
| 16 | + |
| 17 | +The prior example for how to compute the TWAP takes linear time in the number of time entries in a range, which is too inefficient. We require TWAP operations to have constant time complexity (in the number of records). |
| 18 | + |
| 19 | +This is achieved by using an accumulator. In the case of an arithmetic TWAP, we can maintain an accumulator from `a_n`, representing the numerator of the TWAP expression for the interval `t_0...t_n`, namely |
| 20 | +$$a_n = \sum_{i=0}^{n-1} p_i (t_{i+1} - t_i)$$ |
| 21 | +If we maintain such an accumulator for every pool, with `t_0 = pool_creation_time` to `t_n = current_block_time`, we can easily compute the TWAP for any interval. The TWAP for the time interval of price points `t_i` to `t_j` is then $twap = \frac{a_j - a_i}{t_j - t_i}$, which is constant time given the accumulator values. |
| 22 | + |
| 23 | +In Osmosis, we maintain accumulator records for every pool, for the last 48 hours. |
| 24 | +We also maintain within each accumulator record in state, the latest spot price. |
| 25 | +This allows us to interpolate accumulation records between times. |
| 26 | +Namely, if I want the twap from `t=10s` to `t=15s`, but the time records are at `9s, 13s, 17s`, this is fine. |
| 27 | +Using the latest spot price in each record, we create the accumulator value for `t=10` by computing |
| 28 | +`a_10 = a_9 + a_9_latest_spot_price * (10s - 9s)`, and `a_15 = a_13 + a_13_latest_spot_price * (15s - 13s)`. |
| 29 | +Given these interpolated accumulation values, we can compute the TWAP as before. |
| 30 | + |
| 31 | + |
| 32 | +## Module API |
| 33 | + |
| 34 | +The primary intended API is `GetArithmeticTwap`, which is documented below, and has a similar cosmwasm binding. |
| 35 | + |
| 36 | +```go |
| 37 | +// GetArithmeticTwap returns an arithmetic time weighted average price. |
| 38 | +// The returned twap is the time weighted average price (TWAP), using the arithmetic mean, of: |
| 39 | +// * the base asset, in units of the quote asset (1 unit of base = x units of quote) |
| 40 | +// * from (startTime, endTime), |
| 41 | +// * as determined by prices from AMM pool `poolId`. |
| 42 | +// |
| 43 | +// startTime and endTime do not have to be real block times that occurred, |
| 44 | +// the state machine will interpolate the accumulator values for those times |
| 45 | +// from the latest Twap accumulation record prior to the provided time. |
| 46 | +// |
| 47 | +// startTime must be within 48 hours of ctx.BlockTime(), if you need older TWAPs, |
| 48 | +// you will have to maintain the accumulator yourself. |
| 49 | +// |
| 50 | +// This function will error if: |
| 51 | +// * startTime > endTime |
| 52 | +// * endTime in the future |
| 53 | +// * startTime older than 48 hours OR pool creation |
| 54 | +// * pool with id poolId does not exist, or does not contain quoteAssetDenom, baseAssetDenom |
| 55 | +// |
| 56 | +// N.B. If there is a notable use case, the state machine could maintain more historical records, e.g. at one per hour. |
| 57 | +func (k Keeper) GetArithmeticTwap(ctx sdk.Context, |
| 58 | + poolId uint64, |
| 59 | + baseAssetDenom string, quoteAssetDenom string, |
| 60 | + startTime time.Time, endTime time.Time) (sdk.Dec, error) { ... } |
| 61 | +``` |
| 62 | + |
| 63 | +There are convenience methods for `GetArithmeticTwapToNow` which sets `endTime = ctx.BlockTime()`, and has minor gas reduction. |
| 64 | +For users who need TWAPs outside the 48 hours stored in the state machine, you can get the latest accumulation store record from `GetBeginBlockAccumulatorRecord`. |
| 65 | + |
| 66 | +## Code layout |
| 67 | + |
| 68 | +**api.go** is the main file you should look at as a user of this module. |
| 69 | + |
| 70 | +**logic.go** is the main file you should look at for how the TWAP implementation works. |
| 71 | + |
| 72 | +- types/* - Implement TwapRecord, GenesisState. Define AMM interface, and methods to format keys. |
| 73 | +- api.go - Public API, that other users / modules can/should depend on |
| 74 | +- hook_listener.go - Defines hooks & calls to logic.go, for triggering actions on |
| 75 | +- keeper.go - generic SDK boilerplate (defining a wrapper for store keys + params) |
| 76 | +- logic.go - Implements all TWAP module 'logic'. (Arithmetic, defining what to get/set where, etc.) |
| 77 | +- module.go - SDK AppModule interface implementation. |
| 78 | +- store.go - Managing logic for getting and setting things to underlying stores |
| 79 | + |
| 80 | +## Store layout |
| 81 | + |
| 82 | +We maintain TWAP accumulation records for every AMM pool on Osmosis. |
| 83 | + |
| 84 | +Because Osmosis supports multi-asset pools, a complicating factor is that we have to store a record for every asset pair in the pool. |
| 85 | +For every pool, at a given point in time, we make one twap record entry per unique pair of denoms in the pool. If a pool has `k` denoms, the number of unique pairs is `k * (k - 1) / 2`. |
| 86 | + |
| 87 | +Each twap record stores [(source)](https://github.com/osmosis-labs/osmosis/tree/main/proto/osmosis/gamm/twap): |
| 88 | +* last spot price of base asset A in terms of quote asset B |
| 89 | +* last spot price of base asset B in terms of quote asset A |
| 90 | +* Accumulation value of base asset A in terms of quote asset B |
| 91 | +* Accumulation value of base asset B in terms of quote asset A |
| 92 | + |
| 93 | +All TWAP records are indexed in state by the time of write. |
| 94 | + |
| 95 | +A new TWAP record is created in two situations: |
| 96 | +* When a pool is created |
| 97 | +* In the `EndBlock`, if the block contains any potentially price changing event for the pool. (Swap, LP, Exit) |
| 98 | + |
| 99 | +When a pool is created, records are created with the current spot price of the pool. |
| 100 | + |
| 101 | +During `EndBlock`, new records are created, with: |
| 102 | +* The accumulator's value is updated based upon the most recent prior accumulator's stored last spot price |
| 103 | +* The `LastSpotPrice` value is equal to the EndBlock spot price. |
| 104 | + |
| 105 | +In the event that a pool is created, and has a swap in the same block, the record entries are over written with the end block price. |
| 106 | + |
| 107 | +### Tracking spot-price changing events in a block |
| 108 | + |
| 109 | +The flow by which we currently track spot price changing events in a block is as follows: |
| 110 | +* AMM hook triggers for Swapping, LPing or Exiting a pool |
| 111 | +* TWAP listens for this hook, and adds this pool ID to a local tracker |
| 112 | +* In end block, TWAP iterates over every changed pool in that block, based on the local tracker, and updates their TWAP records |
| 113 | +* In end block, TWAP clears the changed pool list, so it is blank by the next block. |
| 114 | + |
| 115 | +The mechanism by which we maintain this changed pool list, is the SDK `Transient Store`. |
| 116 | +The transient store is a KV store in the SDK, that stores entries in memory, for the duration of a block, |
| 117 | +and then clears on the block committing. This is done to save on gas (and I/O for the state machine). |
| 118 | + |
| 119 | +## Testing Methodology |
| 120 | + |
| 121 | +The pre-release testing methodology planned for the twap module is: |
| 122 | + |
| 123 | +- [ ] Using table driven unit tests to test all foreseen states of the module |
| 124 | + - hook testing |
| 125 | + - All swaps correctly trigger twap record updates |
| 126 | + - Create pools cause records to be created |
| 127 | + - store |
| 128 | + - EndBlock triggers all relevant twaps to be saved correctly |
| 129 | + - Block commit wipes temporary stores |
| 130 | + - logic |
| 131 | + - Make tables of expected input / output cases for: |
| 132 | + - getMostRecentRecord |
| 133 | + - getInterpolatedRecord |
| 134 | + - updateRecord |
| 135 | + - computeArithmeticTwap |
| 136 | + - Test overflow handling in all relevant arithmetic |
| 137 | + - Complete testing code coverage (up to return err lines) for logic.go file |
| 138 | + - API |
| 139 | + - Unit tests for the public API, under foreseeable setup conditions |
| 140 | +- [ ] End to end migration tests |
| 141 | + - Tests that migration of Osmosis pools created prior to the TWAP upgrade, get TWAPs recorded starting at the v11 upgrade. |
| 142 | +- [ ] Integration into the Osmosis simulator |
| 143 | + - The osmosis simulator, simulates building up complex state machine states, in random ways not seen before. We plan on, in a property check, maintaining expected TWAPs for short time ranges, and seeing that the keeper query will return the same value as what we get off of the raw price history for short history intervals. |
| 144 | + - Not currently deemed release blocking, but planned: Integration for gas tracking, to ensure gas of reads/writes does not grow with time. |
| 145 | +- [ ] Mutation testing usage |
| 146 | + - integration of the TWAP module into [go mutation testing](https://github.com/osmosis-labs/go-mutesting): |
| 147 | + - We've seen with the `tokenfactory` module that it succeeds at surfacing behavior for untested logic. |
| 148 | + e.g. if you delete a line, or change the direction of a conditional, mutation tests show if regular Go tests catch it. |
| 149 | + - We expect to get this to a state, where after mutation testing is ran, the only items it mutates, that is not caught in a test, is: Deleting `return err`, or `panic` lines, in the situation where that error return or panic isn't reachable. |
0 commit comments