Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(x/twap): incorrect time delta due to nanoseconds in time #4359

Merged
merged 9 commits into from
Feb 20, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* [#3715](https://github.com/osmosis-labs/osmosis/pull/3715) Fix x/gamm (golang API) CalculateSpotPrice, balancer.SpotPrice and Stableswap.SpotPrice base and quote asset.
* [#3746](https://github.com/osmosis-labs/osmosis/pull/3746) Make ApplyFuncIfNoErr logic preserve panics for OutOfGas behavior.
* [#4306](https://github.com/osmosis-labs/osmosis/pull/4306) Prevent adding more tokens to an already finished gauge
* [#4359](https://github.com/osmosis-labs/osmosis/pull/4359) Fix incorrect time delta due to nanoseconds in time causing twap jitter.


## v14.0.1

Expand Down
6 changes: 6 additions & 0 deletions x/twap/keeper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ var (
tMinOne = baseTime.Add(-time.Second)
tPlusOneMin = baseTime.Add(time.Minute)
basePoolId uint64 = 1
oneHundredNanoseconds = 100 * time.Nanosecond
)

type TestSuite struct {
Expand Down Expand Up @@ -479,6 +480,11 @@ func withPrice1Set(twapRecord types.TwapRecord, price1ToSet sdk.Dec) types.TwapR
return twapRecord
}

func withTime(twapRecord types.TwapRecord, time time.Time) types.TwapRecord {
twapRecord.Time = time
return twapRecord
}

func newRecord(poolId uint64, t time.Time, sp0, accum0, accum1, geomAccum sdk.Dec) types.TwapRecord {
return types.TwapRecord{
PoolId: poolId,
Expand Down
2 changes: 1 addition & 1 deletion x/twap/logic.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ func recordWithUpdatedAccumulators(record types.TwapRecord, newTime time.Time) t
return record
}
newRecord := record
timeDelta := newTime.Sub(record.Time)
timeDelta := types.CanonicalTimeMs(newTime) - types.CanonicalTimeMs(record.Time)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we add a one line comment above here on why we use canonical time for the time delta?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is called in multiple places, I elaborated in the method spec instead:
937d148

newRecord.Time = newTime

// record.LastSpotPrice is the last spot price from the block the record was created in,
Expand Down
6 changes: 6 additions & 0 deletions x/twap/logic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,12 @@ func TestRecordWithUpdatedAccumulators(t *testing.T) {
newTime: defaultRecord.Time.Add(time.Second),
expRecord: newExpRecord(oneDec.Add(OneSec), twoDec.Add(OneSec), pointFiveDec),
},

"nanoseconds in time of the original record do not affect final result": {
record: withTime(defaultRecord, defaultRecord.Time.Add(oneHundredNanoseconds)),
newTime: time.Unix(2, 0),
expRecord: newExpRecord(oneDec.Add(OneSec.MulInt64(10)), twoDec.Add(OneSec.QuoInt64(10)), pointFiveDec.Add(geometricTenSecAccum)),
},
}

for name, test := range tests {
Expand Down
4 changes: 2 additions & 2 deletions x/twap/strategy.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func (s *arithmetic) computeTwap(startRecord types.TwapRecord, endRecord types.T
} else {
accumDiff = endRecord.P1ArithmeticTwapAccumulator.Sub(startRecord.P1ArithmeticTwapAccumulator)
}
timeDelta := endRecord.Time.Sub(startRecord.Time)
timeDelta := types.CanonicalTimeMs(endRecord.Time) - types.CanonicalTimeMs(startRecord.Time)
return types.AccumDiffDivDuration(accumDiff, timeDelta)
}

Expand All @@ -47,7 +47,7 @@ func (s *geometric) computeTwap(startRecord types.TwapRecord, endRecord types.Tw
return sdk.ZeroDec()
}

timeDelta := endRecord.Time.Sub(startRecord.Time)
timeDelta := types.CanonicalTimeMs(endRecord.Time) - types.CanonicalTimeMs(startRecord.Time)
arithmeticMeanOfLogPrices := types.AccumDiffDivDuration(accumDiff, timeDelta)

exponent := arithmeticMeanOfLogPrices
Expand Down
14 changes: 14 additions & 0 deletions x/twap/strategy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,13 @@ func (s *TestSuite) TestComputeArithmeticStrategyTwap() {
s, pointOneAccum, tenSecAccum, 100*time.Second, sdk.NewDecWithPrec(1, 1)),

"accumulator = 10*OneSec, t=100s. 0 base accum (asset 1)": testCaseFromDeltasAsset1(s, sdk.ZeroDec(), OneSec.MulInt64(10), 100*time.Second, sdk.NewDecWithPrec(1, 1)),

"start record time with nanoseconds does not change result": {
startRecord: newOneSidedRecord(baseTime.Add(oneHundredNanoseconds), sdk.ZeroDec(), true),
endRecord: newOneSidedRecord(tPlusOne, OneSec, true),
quoteAsset: denom0,
expTwap: sdk.OneDec(),
},
}
for name, test := range tests {
s.Run(name, func() {
Expand Down Expand Up @@ -278,6 +285,13 @@ func (s *TestSuite) TestComputeGeometricStrategyTwap() {

expTwap: sdk.ZeroDec(),
},

"start record time with nanoseconds does not change result": {
startRecord: newOneSidedGeometricRecord(baseTime.Add(oneHundredNanoseconds), sdk.ZeroDec()),
endRecord: newOneSidedGeometricRecord(tPlusOne, geometricTenSecAccum),
quoteAsset: denom0,
expTwap: sdk.NewDec(10),
},
}

for name, tc := range tests {
Expand Down
17 changes: 13 additions & 4 deletions x/twap/types/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,19 @@ func GetAllUniqueDenomPairs(denoms []string) []DenomPair {
// SpotPriceMulDuration returns the spot price multiplied by the time delta,
// that is the spot price between the current and last TWAP record.
// A single second accounts for 1_000_000_000 when converted to int64.
func SpotPriceMulDuration(sp sdk.Dec, timeDelta time.Duration) sdk.Dec {
func SpotPriceMulDuration(sp sdk.Dec, timeDeltaMs int64) sdk.Dec {
return sp.MulInt64(timeDeltaMs)
}

func SpotPriceMulDurationOld(sp sdk.Dec, timeDelta time.Duration) sdk.Dec {
p0mvn marked this conversation as resolved.
Show resolved Hide resolved
deltaMS := timeDelta.Milliseconds()
return sp.MulInt64(deltaMS)
}

// AccumDiffDivDuration returns the accumulated difference divided by the the
// time delta, that is the spot price between the current and last TWAP record.
func AccumDiffDivDuration(accumDiff sdk.Dec, timeDelta time.Duration) sdk.Dec {
deltaMS := timeDelta.Milliseconds()
return accumDiff.QuoInt64(deltaMS)
func AccumDiffDivDuration(accumDiff sdk.Dec, timeDeltaMs int64) sdk.Dec {
return accumDiff.QuoInt64(timeDeltaMs)
}

// LexicographicalOrderDenoms takes two denoms and returns them to be in lexicographically ascending order.
Expand All @@ -62,6 +65,12 @@ func LexicographicalOrderDenoms(denom0, denom1 string) (string, string, error) {
return denom0, denom1, nil
}

// CanonicalTimeMs returns the canonical time in milleseconds used for twap
p0mvn marked this conversation as resolved.
Show resolved Hide resolved
// math computations in UTC. Removes any monotonic clock reading prior to conversion to ms.
func CanonicalTimeMs(twapTime time.Time) int64 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just curious, why is it called "canonical" time?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Canonical with respect to block times. It strips away any mononic part from the clock and returns the time in UTC,

This should already be done given that we use ctx.BlockTime(). However, this is useful for tests since we might not be setting up the time correctly in tests

return twapTime.Round(0).UnixMilli()
}

// DenomPair contains pair of assetA and assetB denoms which belong to a pool.
type DenomPair struct {
Denom0 string
Expand Down
12 changes: 12 additions & 0 deletions x/twap/types/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package types
import (
"fmt"
"testing"
"time"

"github.com/stretchr/testify/require"

Expand Down Expand Up @@ -66,3 +67,14 @@ func TestLexicographicalOrderDenoms(t *testing.T) {
})
}
}

func TestCanonicalTimeMs(t *testing.T) {
const expectedMs int64 = 2

newYorkLocation, err := time.LoadLocation("America/New_York")
require.NoError(t, err)
time := time.Unix(0, int64(time.Millisecond+999999+1)).In(newYorkLocation)

actualTime := CanonicalTimeMs(time)
require.Equal(t, expectedMs, actualTime)
}