Skip to content

Commit fce2e31

Browse files
ValarDragonp0mvn
andauthored
Add TWAP spec (osmosis-labs#2203)
* Add README * Update for PR suggestions * Apply suggestions from code review Co-authored-by: Roman <[email protected]> Co-authored-by: Roman <[email protected]>
1 parent 01144f6 commit fce2e31

File tree

2 files changed

+150
-7
lines changed

2 files changed

+150
-7
lines changed

.markdownlint.yml

+5-2
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@ MD003:
44
# Heading style
55
style: "atx"
66
# Can't disable MD007 :/
7-
# MD007: false
7+
MD007: false
88
MD009: false
9+
MD010:
10+
code_blocks: false
911
MD013:
1012
code_blocks: false
11-
MD024: false
13+
MD024: false
14+
MD037: false # breaks on latex

x/gamm/twap/README.md

+145-5
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,149 @@
1-
# TWAP
1+
# TWAP (Time Weighted Average Price)
22

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.
44

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.
66

7-
## Basic architecture notes
7+
## Arithmetic mean TWAP
88

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

Comments
 (0)