From 34a49b16a0fd48ceb45f185af408e7295fed5987 Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Mon, 18 Jul 2022 14:31:25 -0500 Subject: [PATCH 01/72] TWAP wip --- x/gamm/twap/abci.go | 4 ++-- x/gamm/twap/api.go | 22 ++++++++++++++++++++++ x/gamm/twap/hook_listener.go | 15 +++++++++++++++ 3 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 x/gamm/twap/api.go diff --git a/x/gamm/twap/abci.go b/x/gamm/twap/abci.go index 060da2b2525..04bf392a7b7 100644 --- a/x/gamm/twap/abci.go +++ b/x/gamm/twap/abci.go @@ -4,10 +4,10 @@ import sdk "github.com/cosmos/cosmos-sdk/types" func (k twapkeeper) endBlockLogic(ctx sdk.Context) { // TODO: Update TWAP entries - // step 1: Get all altered pool ids + // Step 1: Get all altered pool ids changedPoolIds := k.getChangedPools(ctx) if len(changedPoolIds) == 0 { return } - // 'altered pool ids' should be automatically cleared + // Step 2: } diff --git a/x/gamm/twap/api.go b/x/gamm/twap/api.go new file mode 100644 index 00000000000..d598fa33885 --- /dev/null +++ b/x/gamm/twap/api.go @@ -0,0 +1,22 @@ +package twap + +import ( + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// GetArithmeticTwap returns an arithmetic TWAP result from (startTime, endTime), +// for the `quoteAsset / baseAsset` ratio on `poolId`. +// startTime and endTime do not have to be real block times that occured, +// this function will interpolate between startTime. +// if endTime = now, we do {X} +// startTime must be in time range {X}, recommended parameterization for mainnet is {Y} +func (k twapkeeper) GetArithmeticTwap( + poolId uint64, + quoteAssetDenom string, + baseAssetDenom string, + startTime time.Time, + endTime time.Time) (sdk.Dec, error) { + return sdk.Dec{}, nil +} diff --git a/x/gamm/twap/hook_listener.go b/x/gamm/twap/hook_listener.go index 2a5163577d4..91911ebe03b 100644 --- a/x/gamm/twap/hook_listener.go +++ b/x/gamm/twap/hook_listener.go @@ -3,10 +3,25 @@ package twap import ( sdk "github.com/cosmos/cosmos-sdk/types" + epochtypes "github.com/osmosis-labs/osmosis/v10/x/epochs/types" "github.com/osmosis-labs/osmosis/v10/x/gamm/types" ) var _ types.GammHooks = &gammhook{} +var _ epochtypes.EpochHooks = &epochhook{} + +type epochhook struct { + k twapkeeper +} + +func (hook *epochhook) AfterEpochEnd(ctx sdk.Context, epochIdentifier string, epochNumber int64) { + // TODO: + // if epochIdentifier == hook.k.PruneIdentifier() { + // hook.k.pruneOldTwaps(ctx) + // } +} + +func (hook *epochhook) BeforeEpochStart(ctx sdk.Context, epochIdentifier string, epochNumber int64) {} type gammhook struct { k twapkeeper From 32e040639ebef24413223ae2b55162a6ebf357aa Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Tue, 19 Jul 2022 08:40:57 -0500 Subject: [PATCH 02/72] Add proto files --- .../gamm/twap/v1beta1/twap_record.proto | 57 ++ x/gamm/twap/types/twap_record.pb.go | 849 ++++++++++++++++++ 2 files changed, 906 insertions(+) create mode 100644 proto/osmosis/gamm/twap/v1beta1/twap_record.proto create mode 100644 x/gamm/twap/types/twap_record.pb.go diff --git a/proto/osmosis/gamm/twap/v1beta1/twap_record.proto b/proto/osmosis/gamm/twap/v1beta1/twap_record.proto new file mode 100644 index 00000000000..ab51f4ce54a --- /dev/null +++ b/proto/osmosis/gamm/twap/v1beta1/twap_record.proto @@ -0,0 +1,57 @@ +syntax = "proto3"; +package osmosis.gamm.twap.v1beta1; + +import "gogoproto/gogo.proto"; +import "google/protobuf/any.proto"; +import "cosmos_proto/cosmos.proto"; +import "cosmos/base/v1beta1/coin.proto"; +import "google/protobuf/timestamp.proto"; + +option go_package = "github.com/osmosis-labs/osmosis/v10/x/gamm/twap/types"; + +// A TWAP record should be indexed in state by pool_id, (asset pair), timestamp +// The asset pair assets should be lexicographically sorted. +// Technically (pool_id, asset_0_denom, asset_1_denom, height) do not need to +// appear in the struct however we view this as the wrong performance tradeoff +// given SDK today. Would rather we optimize for readability and correctness, +// than an optimal state storage format. The system bottleneck is elsewhere for +// now. +message TwapRecord { + option (gogoproto.equal) = true; + option (gogoproto.goproto_stringer) = false; + + uint64 pool_id = 1; + // Lexicographically smaller denom of the pair + string asset_0_denom = 2; + // Lexicographically larger denom of the pair + string asset_1_denom = 3; + // height this record corresponds to, for debugging purposes + uint64 height = 4 [ + (gogoproto.moretags) = "yaml:\"record_height\"", + (gogoproto.jsontag) = "record_height" + ]; + // This field should only exist until we have a global registry in the state + // machine, mapping prior block heights within {TIME RANGE} to times. + google.protobuf.Timestamp time = 5 [ + (gogoproto.nullable) = false, + (gogoproto.stdtime) = true, + (gogoproto.moretags) = "yaml:\"record_time\"" + ]; + + string p0_arithmetic_twap_accumulator = 6 [ + (gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec", + (gogoproto.nullable) = false + ]; + string p1_arithmetic_twap_accumulator = 7 [ + (gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec", + (gogoproto.nullable) = false + ]; + // string geometric_twap_accumulator = 7 [(gogoproto.customtype) = + // "github.com/cosmos/cosmos-sdk/types.Dec", + // (gogoproto.nullable) = false]; +} + +// GenesisState defines the gamm module's genesis state. +message GenesisState { + repeated TwapRecord twaps = 1 [ (cosmos_proto.accepts_interface) = "PoolI" ]; +} diff --git a/x/gamm/twap/types/twap_record.pb.go b/x/gamm/twap/types/twap_record.pb.go new file mode 100644 index 00000000000..4bf1397b652 --- /dev/null +++ b/x/gamm/twap/types/twap_record.pb.go @@ -0,0 +1,849 @@ +// Code generated by protoc-gen-gogo. DO NOT EDIT. +// source: osmosis/gamm/twap/v1beta1/twap_record.proto + +package types + +import ( + fmt "fmt" + _ "github.com/cosmos/cosmos-proto" + _ "github.com/cosmos/cosmos-sdk/codec/types" + _ "github.com/cosmos/cosmos-sdk/types" + github_com_cosmos_cosmos_sdk_types "github.com/cosmos/cosmos-sdk/types" + _ "github.com/gogo/protobuf/gogoproto" + proto "github.com/gogo/protobuf/proto" + _ "github.com/gogo/protobuf/types" + github_com_gogo_protobuf_types "github.com/gogo/protobuf/types" + io "io" + math "math" + math_bits "math/bits" + time "time" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf +var _ = time.Kitchen + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package + +// A TWAP record should be indexed in state by pool_id, (asset pair), timestamp +// The asset pair assets should be lexicographically sorted. +// Technically (pool_id, asset_0_denom, asset_1_denom, height) do not need to +// appear in the struct however we view this as the wrong performance tradeoff +// given SDK today. Would rather we optimize for readability and correctness, +// than an optimal state storage format. The system bottleneck is elsewhere for +// now. +type TwapRecord struct { + PoolId uint64 `protobuf:"varint,1,opt,name=pool_id,json=poolId,proto3" json:"pool_id,omitempty"` + // Lexicographically smaller denom of the pair + Asset_0Denom string `protobuf:"bytes,2,opt,name=asset_0_denom,json=asset0Denom,proto3" json:"asset_0_denom,omitempty"` + // Lexicographically larger denom of the pair + Asset_1Denom string `protobuf:"bytes,3,opt,name=asset_1_denom,json=asset1Denom,proto3" json:"asset_1_denom,omitempty"` + // height this record corresponds to, for debugging purposes + Height uint64 `protobuf:"varint,4,opt,name=height,proto3" json:"record_height" yaml:"record_height"` + // This field should only exist until we have a global registry in the state + // machine, mapping prior block heights within {TIME RANGE} to times. + Time time.Time `protobuf:"bytes,5,opt,name=time,proto3,stdtime" json:"time" yaml:"record_time"` + P0ArithmeticTwapAccumulator github_com_cosmos_cosmos_sdk_types.Dec `protobuf:"bytes,6,opt,name=p0_arithmetic_twap_accumulator,json=p0ArithmeticTwapAccumulator,proto3,customtype=github.com/cosmos/cosmos-sdk/types.Dec" json:"p0_arithmetic_twap_accumulator"` + P1ArithmeticTwapAccumulator github_com_cosmos_cosmos_sdk_types.Dec `protobuf:"bytes,7,opt,name=p1_arithmetic_twap_accumulator,json=p1ArithmeticTwapAccumulator,proto3,customtype=github.com/cosmos/cosmos-sdk/types.Dec" json:"p1_arithmetic_twap_accumulator"` +} + +func (m *TwapRecord) Reset() { *m = TwapRecord{} } +func (*TwapRecord) ProtoMessage() {} +func (*TwapRecord) Descriptor() ([]byte, []int) { + return fileDescriptor_a81e54bd4e35cf12, []int{0} +} +func (m *TwapRecord) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *TwapRecord) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_TwapRecord.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *TwapRecord) XXX_Merge(src proto.Message) { + xxx_messageInfo_TwapRecord.Merge(m, src) +} +func (m *TwapRecord) XXX_Size() int { + return m.Size() +} +func (m *TwapRecord) XXX_DiscardUnknown() { + xxx_messageInfo_TwapRecord.DiscardUnknown(m) +} + +var xxx_messageInfo_TwapRecord proto.InternalMessageInfo + +func (m *TwapRecord) GetPoolId() uint64 { + if m != nil { + return m.PoolId + } + return 0 +} + +func (m *TwapRecord) GetAsset_0Denom() string { + if m != nil { + return m.Asset_0Denom + } + return "" +} + +func (m *TwapRecord) GetAsset_1Denom() string { + if m != nil { + return m.Asset_1Denom + } + return "" +} + +func (m *TwapRecord) GetHeight() uint64 { + if m != nil { + return m.Height + } + return 0 +} + +func (m *TwapRecord) GetTime() time.Time { + if m != nil { + return m.Time + } + return time.Time{} +} + +// GenesisState defines the gamm module's genesis state. +type GenesisState struct { + Twaps []*TwapRecord `protobuf:"bytes,1,rep,name=twaps,proto3" json:"twaps,omitempty"` +} + +func (m *GenesisState) Reset() { *m = GenesisState{} } +func (m *GenesisState) String() string { return proto.CompactTextString(m) } +func (*GenesisState) ProtoMessage() {} +func (*GenesisState) Descriptor() ([]byte, []int) { + return fileDescriptor_a81e54bd4e35cf12, []int{1} +} +func (m *GenesisState) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *GenesisState) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_GenesisState.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *GenesisState) XXX_Merge(src proto.Message) { + xxx_messageInfo_GenesisState.Merge(m, src) +} +func (m *GenesisState) XXX_Size() int { + return m.Size() +} +func (m *GenesisState) XXX_DiscardUnknown() { + xxx_messageInfo_GenesisState.DiscardUnknown(m) +} + +var xxx_messageInfo_GenesisState proto.InternalMessageInfo + +func (m *GenesisState) GetTwaps() []*TwapRecord { + if m != nil { + return m.Twaps + } + return nil +} + +func init() { + proto.RegisterType((*TwapRecord)(nil), "osmosis.gamm.twap.v1beta1.TwapRecord") + proto.RegisterType((*GenesisState)(nil), "osmosis.gamm.twap.v1beta1.GenesisState") +} + +func init() { + proto.RegisterFile("osmosis/gamm/twap/v1beta1/twap_record.proto", fileDescriptor_a81e54bd4e35cf12) +} + +var fileDescriptor_a81e54bd4e35cf12 = []byte{ + // 514 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x53, 0xcd, 0x6e, 0xd3, 0x40, + 0x10, 0xce, 0x92, 0x9f, 0xd2, 0x0d, 0xbd, 0x58, 0x95, 0x70, 0x83, 0x64, 0x47, 0x96, 0x40, 0x41, + 0x28, 0xbb, 0x71, 0x11, 0x97, 0xde, 0x12, 0x55, 0x20, 0x2e, 0x80, 0x4c, 0xc5, 0x81, 0x8b, 0xb5, + 0x76, 0x16, 0xc7, 0xc2, 0x9b, 0xb5, 0xbc, 0x9b, 0x96, 0xbc, 0x45, 0x8f, 0x1c, 0xfb, 0x10, 0x3c, + 0x44, 0xc5, 0xa9, 0x47, 0xc4, 0xc1, 0xa0, 0xe4, 0x82, 0x38, 0x56, 0x3c, 0x00, 0xda, 0x5d, 0xa7, + 0x49, 0x91, 0xca, 0xa1, 0xa7, 0xe4, 0x9b, 0xf9, 0xe6, 0x9b, 0xf9, 0x76, 0xc6, 0xf0, 0x09, 0x17, + 0x8c, 0x8b, 0x54, 0xe0, 0x84, 0x30, 0x86, 0xe5, 0x09, 0xc9, 0xf1, 0xb1, 0x1f, 0x51, 0x49, 0x7c, + 0x0d, 0xc2, 0x82, 0xc6, 0xbc, 0x18, 0xa3, 0xbc, 0xe0, 0x92, 0x5b, 0x7b, 0x15, 0x19, 0x29, 0x32, + 0x52, 0x79, 0x54, 0x91, 0x3b, 0xbb, 0x09, 0x4f, 0xb8, 0x66, 0x61, 0xf5, 0xcf, 0x14, 0x74, 0xf6, + 0x12, 0xce, 0x93, 0x8c, 0x62, 0x8d, 0xa2, 0xd9, 0x07, 0x4c, 0xa6, 0xf3, 0x55, 0x2a, 0xd6, 0x62, + 0xa1, 0xa9, 0x31, 0xa0, 0x4a, 0x39, 0x06, 0xe1, 0x88, 0x08, 0x7a, 0x35, 0x4d, 0xcc, 0xd3, 0x69, + 0x95, 0x77, 0xff, 0x55, 0x95, 0x29, 0xa3, 0x42, 0x12, 0x96, 0x1b, 0x82, 0xf7, 0xa7, 0x0e, 0xe1, + 0xd1, 0x09, 0xc9, 0x03, 0x3d, 0xbc, 0x75, 0x1f, 0x6e, 0xe5, 0x9c, 0x67, 0x61, 0x3a, 0xb6, 0x41, + 0x17, 0xf4, 0x1a, 0x41, 0x4b, 0xc1, 0x97, 0x63, 0xcb, 0x83, 0x3b, 0x44, 0x08, 0x2a, 0xc3, 0x41, + 0x38, 0xa6, 0x53, 0xce, 0xec, 0x3b, 0x5d, 0xd0, 0xdb, 0x0e, 0xda, 0x3a, 0x38, 0x38, 0x54, 0xa1, + 0x35, 0xc7, 0xaf, 0x38, 0xf5, 0x0d, 0x8e, 0x6f, 0x38, 0x43, 0xd8, 0x9a, 0xd0, 0x34, 0x99, 0x48, + 0xbb, 0xa1, 0xf4, 0x47, 0x8f, 0x7f, 0x97, 0xee, 0x8e, 0x79, 0xb9, 0xd0, 0x24, 0x2e, 0x4b, 0x77, + 0x77, 0x4e, 0x58, 0x76, 0xe0, 0x5d, 0x0b, 0x7b, 0x41, 0x55, 0x68, 0xbd, 0x82, 0x0d, 0xe5, 0xc2, + 0x6e, 0x76, 0x41, 0xaf, 0xbd, 0xdf, 0x41, 0xc6, 0x22, 0x5a, 0x59, 0x44, 0x47, 0x2b, 0x8b, 0x23, + 0xe7, 0xbc, 0x74, 0x6b, 0x97, 0xa5, 0x6b, 0x5d, 0xd3, 0x53, 0xc5, 0xde, 0xe9, 0x0f, 0x17, 0x04, + 0x5a, 0xc7, 0x12, 0xd0, 0xc9, 0x07, 0x21, 0x29, 0x52, 0x39, 0x61, 0x54, 0xa6, 0x71, 0xa8, 0xb7, + 0x49, 0xe2, 0x78, 0xc6, 0x66, 0x19, 0x91, 0xbc, 0xb0, 0x5b, 0xca, 0xc7, 0x08, 0x29, 0xb5, 0xef, + 0xa5, 0xfb, 0x28, 0x49, 0xe5, 0x64, 0x16, 0xa1, 0x98, 0xb3, 0x6a, 0x19, 0xd5, 0x4f, 0x5f, 0x8c, + 0x3f, 0x62, 0x39, 0xcf, 0xa9, 0x40, 0x87, 0x34, 0x0e, 0x1e, 0xe4, 0x83, 0xe1, 0x95, 0xa8, 0x7a, + 0xe3, 0xe1, 0x5a, 0x52, 0x37, 0xf5, 0xff, 0xdb, 0x74, 0xeb, 0x96, 0x4d, 0xfd, 0x1b, 0x9b, 0x1e, + 0xdc, 0xfd, 0x7c, 0xe6, 0xd6, 0x7e, 0x9d, 0xb9, 0xc0, 0x7b, 0x07, 0xef, 0xbd, 0xa0, 0x53, 0x2a, + 0x52, 0xf1, 0x56, 0x12, 0x49, 0xad, 0xe7, 0xb0, 0xa9, 0x06, 0x10, 0x36, 0xe8, 0xd6, 0x7b, 0xed, + 0xfd, 0x87, 0xe8, 0xc6, 0xf3, 0x45, 0xeb, 0x6b, 0x19, 0x6d, 0x7f, 0xfd, 0xd2, 0x6f, 0xbe, 0x51, + 0x07, 0x12, 0x98, 0xf2, 0xd1, 0xeb, 0xf3, 0x85, 0x03, 0x2e, 0x16, 0x0e, 0xf8, 0xb9, 0x70, 0xc0, + 0xe9, 0xd2, 0xa9, 0x5d, 0x2c, 0x9d, 0xda, 0xb7, 0xa5, 0x53, 0x7b, 0xff, 0x6c, 0xc3, 0x40, 0x25, + 0xde, 0xcf, 0x48, 0x24, 0x56, 0x00, 0x1f, 0xfb, 0x03, 0xfc, 0x69, 0xe3, 0xdb, 0xd2, 0x9e, 0xa2, + 0x96, 0x5e, 0xeb, 0xd3, 0xbf, 0x01, 0x00, 0x00, 0xff, 0xff, 0x7a, 0x64, 0xff, 0x8f, 0x7d, 0x03, + 0x00, 0x00, +} + +func (this *TwapRecord) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + that1, ok := that.(*TwapRecord) + if !ok { + that2, ok := that.(TwapRecord) + if ok { + that1 = &that2 + } else { + return false + } + } + if that1 == nil { + return this == nil + } else if this == nil { + return false + } + if this.PoolId != that1.PoolId { + return false + } + if this.Asset_0Denom != that1.Asset_0Denom { + return false + } + if this.Asset_1Denom != that1.Asset_1Denom { + return false + } + if this.Height != that1.Height { + return false + } + if !this.Time.Equal(that1.Time) { + return false + } + if !this.P0ArithmeticTwapAccumulator.Equal(that1.P0ArithmeticTwapAccumulator) { + return false + } + if !this.P1ArithmeticTwapAccumulator.Equal(that1.P1ArithmeticTwapAccumulator) { + return false + } + return true +} +func (m *TwapRecord) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *TwapRecord) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *TwapRecord) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + { + size := m.P1ArithmeticTwapAccumulator.Size() + i -= size + if _, err := m.P1ArithmeticTwapAccumulator.MarshalTo(dAtA[i:]); err != nil { + return 0, err + } + i = encodeVarintTwapRecord(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x3a + { + size := m.P0ArithmeticTwapAccumulator.Size() + i -= size + if _, err := m.P0ArithmeticTwapAccumulator.MarshalTo(dAtA[i:]); err != nil { + return 0, err + } + i = encodeVarintTwapRecord(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x32 + n1, err1 := github_com_gogo_protobuf_types.StdTimeMarshalTo(m.Time, dAtA[i-github_com_gogo_protobuf_types.SizeOfStdTime(m.Time):]) + if err1 != nil { + return 0, err1 + } + i -= n1 + i = encodeVarintTwapRecord(dAtA, i, uint64(n1)) + i-- + dAtA[i] = 0x2a + if m.Height != 0 { + i = encodeVarintTwapRecord(dAtA, i, uint64(m.Height)) + i-- + dAtA[i] = 0x20 + } + if len(m.Asset_1Denom) > 0 { + i -= len(m.Asset_1Denom) + copy(dAtA[i:], m.Asset_1Denom) + i = encodeVarintTwapRecord(dAtA, i, uint64(len(m.Asset_1Denom))) + i-- + dAtA[i] = 0x1a + } + if len(m.Asset_0Denom) > 0 { + i -= len(m.Asset_0Denom) + copy(dAtA[i:], m.Asset_0Denom) + i = encodeVarintTwapRecord(dAtA, i, uint64(len(m.Asset_0Denom))) + i-- + dAtA[i] = 0x12 + } + if m.PoolId != 0 { + i = encodeVarintTwapRecord(dAtA, i, uint64(m.PoolId)) + i-- + dAtA[i] = 0x8 + } + return len(dAtA) - i, nil +} + +func (m *GenesisState) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *GenesisState) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *GenesisState) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if len(m.Twaps) > 0 { + for iNdEx := len(m.Twaps) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.Twaps[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintTwapRecord(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0xa + } + } + return len(dAtA) - i, nil +} + +func encodeVarintTwapRecord(dAtA []byte, offset int, v uint64) int { + offset -= sovTwapRecord(v) + base := offset + for v >= 1<<7 { + dAtA[offset] = uint8(v&0x7f | 0x80) + v >>= 7 + offset++ + } + dAtA[offset] = uint8(v) + return base +} +func (m *TwapRecord) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.PoolId != 0 { + n += 1 + sovTwapRecord(uint64(m.PoolId)) + } + l = len(m.Asset_0Denom) + if l > 0 { + n += 1 + l + sovTwapRecord(uint64(l)) + } + l = len(m.Asset_1Denom) + if l > 0 { + n += 1 + l + sovTwapRecord(uint64(l)) + } + if m.Height != 0 { + n += 1 + sovTwapRecord(uint64(m.Height)) + } + l = github_com_gogo_protobuf_types.SizeOfStdTime(m.Time) + n += 1 + l + sovTwapRecord(uint64(l)) + l = m.P0ArithmeticTwapAccumulator.Size() + n += 1 + l + sovTwapRecord(uint64(l)) + l = m.P1ArithmeticTwapAccumulator.Size() + n += 1 + l + sovTwapRecord(uint64(l)) + return n +} + +func (m *GenesisState) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if len(m.Twaps) > 0 { + for _, e := range m.Twaps { + l = e.Size() + n += 1 + l + sovTwapRecord(uint64(l)) + } + } + return n +} + +func sovTwapRecord(x uint64) (n int) { + return (math_bits.Len64(x|1) + 6) / 7 +} +func sozTwapRecord(x uint64) (n int) { + return sovTwapRecord(uint64((x << 1) ^ uint64((int64(x) >> 63)))) +} +func (m *TwapRecord) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTwapRecord + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: TwapRecord: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: TwapRecord: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field PoolId", wireType) + } + m.PoolId = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTwapRecord + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.PoolId |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Asset_0Denom", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTwapRecord + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthTwapRecord + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthTwapRecord + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Asset_0Denom = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Asset_1Denom", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTwapRecord + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthTwapRecord + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthTwapRecord + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Asset_1Denom = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 4: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Height", wireType) + } + m.Height = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTwapRecord + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Height |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 5: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Time", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTwapRecord + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthTwapRecord + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthTwapRecord + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := github_com_gogo_protobuf_types.StdTimeUnmarshal(&m.Time, dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 6: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field P0ArithmeticTwapAccumulator", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTwapRecord + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthTwapRecord + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthTwapRecord + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := m.P0ArithmeticTwapAccumulator.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 7: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field P1ArithmeticTwapAccumulator", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTwapRecord + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthTwapRecord + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthTwapRecord + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := m.P1ArithmeticTwapAccumulator.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipTwapRecord(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthTwapRecord + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *GenesisState) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTwapRecord + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: GenesisState: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: GenesisState: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Twaps", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTwapRecord + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthTwapRecord + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthTwapRecord + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Twaps = append(m.Twaps, &TwapRecord{}) + if err := m.Twaps[len(m.Twaps)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipTwapRecord(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthTwapRecord + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func skipTwapRecord(dAtA []byte) (n int, err error) { + l := len(dAtA) + iNdEx := 0 + depth := 0 + for iNdEx < l { + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowTwapRecord + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + wireType := int(wire & 0x7) + switch wireType { + case 0: + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowTwapRecord + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + iNdEx++ + if dAtA[iNdEx-1] < 0x80 { + break + } + } + case 1: + iNdEx += 8 + case 2: + var length int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowTwapRecord + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + length |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if length < 0 { + return 0, ErrInvalidLengthTwapRecord + } + iNdEx += length + case 3: + depth++ + case 4: + if depth == 0 { + return 0, ErrUnexpectedEndOfGroupTwapRecord + } + depth-- + case 5: + iNdEx += 4 + default: + return 0, fmt.Errorf("proto: illegal wireType %d", wireType) + } + if iNdEx < 0 { + return 0, ErrInvalidLengthTwapRecord + } + if depth == 0 { + return iNdEx, nil + } + } + return 0, io.ErrUnexpectedEOF +} + +var ( + ErrInvalidLengthTwapRecord = fmt.Errorf("proto: negative length found during unmarshaling") + ErrIntOverflowTwapRecord = fmt.Errorf("proto: integer overflow") + ErrUnexpectedEndOfGroupTwapRecord = fmt.Errorf("proto: unexpected end of group") +) From 6292394a5b930760ba999081248fc2e8c8fa9fcb Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Tue, 19 Jul 2022 12:49:07 -0500 Subject: [PATCH 03/72] more twap WIP --- .../gamm/twap/v1beta1/twap_record.proto | 3 - x/gamm/twap/types/twap_record.pb.go | 112 ++++++------------ 2 files changed, 35 insertions(+), 80 deletions(-) diff --git a/proto/osmosis/gamm/twap/v1beta1/twap_record.proto b/proto/osmosis/gamm/twap/v1beta1/twap_record.proto index ab51f4ce54a..52e8ace8c0c 100644 --- a/proto/osmosis/gamm/twap/v1beta1/twap_record.proto +++ b/proto/osmosis/gamm/twap/v1beta1/twap_record.proto @@ -17,9 +17,6 @@ option go_package = "github.com/osmosis-labs/osmosis/v10/x/gamm/twap/types"; // than an optimal state storage format. The system bottleneck is elsewhere for // now. message TwapRecord { - option (gogoproto.equal) = true; - option (gogoproto.goproto_stringer) = false; - uint64 pool_id = 1; // Lexicographically smaller denom of the pair string asset_0_denom = 2; diff --git a/x/gamm/twap/types/twap_record.pb.go b/x/gamm/twap/types/twap_record.pb.go index 4bf1397b652..947c1744de2 100644 --- a/x/gamm/twap/types/twap_record.pb.go +++ b/x/gamm/twap/types/twap_record.pb.go @@ -53,8 +53,9 @@ type TwapRecord struct { P1ArithmeticTwapAccumulator github_com_cosmos_cosmos_sdk_types.Dec `protobuf:"bytes,7,opt,name=p1_arithmetic_twap_accumulator,json=p1ArithmeticTwapAccumulator,proto3,customtype=github.com/cosmos/cosmos-sdk/types.Dec" json:"p1_arithmetic_twap_accumulator"` } -func (m *TwapRecord) Reset() { *m = TwapRecord{} } -func (*TwapRecord) ProtoMessage() {} +func (m *TwapRecord) Reset() { *m = TwapRecord{} } +func (m *TwapRecord) String() string { return proto.CompactTextString(m) } +func (*TwapRecord) ProtoMessage() {} func (*TwapRecord) Descriptor() ([]byte, []int) { return fileDescriptor_a81e54bd4e35cf12, []int{0} } @@ -175,84 +176,41 @@ func init() { } var fileDescriptor_a81e54bd4e35cf12 = []byte{ - // 514 bytes of a gzipped FileDescriptorProto + // 500 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x53, 0xcd, 0x6e, 0xd3, 0x40, - 0x10, 0xce, 0x92, 0x9f, 0xd2, 0x0d, 0xbd, 0x58, 0x95, 0x70, 0x83, 0x64, 0x47, 0x96, 0x40, 0x41, - 0x28, 0xbb, 0x71, 0x11, 0x97, 0xde, 0x12, 0x55, 0x20, 0x2e, 0x80, 0x4c, 0xc5, 0x81, 0x8b, 0xb5, - 0x76, 0x16, 0xc7, 0xc2, 0x9b, 0xb5, 0xbc, 0x9b, 0x96, 0xbc, 0x45, 0x8f, 0x1c, 0xfb, 0x10, 0x3c, - 0x44, 0xc5, 0xa9, 0x47, 0xc4, 0xc1, 0xa0, 0xe4, 0x82, 0x38, 0x56, 0x3c, 0x00, 0xda, 0x5d, 0xa7, - 0x49, 0x91, 0xca, 0xa1, 0xa7, 0xe4, 0x9b, 0xf9, 0xe6, 0x9b, 0xf9, 0x76, 0xc6, 0xf0, 0x09, 0x17, - 0x8c, 0x8b, 0x54, 0xe0, 0x84, 0x30, 0x86, 0xe5, 0x09, 0xc9, 0xf1, 0xb1, 0x1f, 0x51, 0x49, 0x7c, - 0x0d, 0xc2, 0x82, 0xc6, 0xbc, 0x18, 0xa3, 0xbc, 0xe0, 0x92, 0x5b, 0x7b, 0x15, 0x19, 0x29, 0x32, - 0x52, 0x79, 0x54, 0x91, 0x3b, 0xbb, 0x09, 0x4f, 0xb8, 0x66, 0x61, 0xf5, 0xcf, 0x14, 0x74, 0xf6, - 0x12, 0xce, 0x93, 0x8c, 0x62, 0x8d, 0xa2, 0xd9, 0x07, 0x4c, 0xa6, 0xf3, 0x55, 0x2a, 0xd6, 0x62, - 0xa1, 0xa9, 0x31, 0xa0, 0x4a, 0x39, 0x06, 0xe1, 0x88, 0x08, 0x7a, 0x35, 0x4d, 0xcc, 0xd3, 0x69, - 0x95, 0x77, 0xff, 0x55, 0x95, 0x29, 0xa3, 0x42, 0x12, 0x96, 0x1b, 0x82, 0xf7, 0xa7, 0x0e, 0xe1, - 0xd1, 0x09, 0xc9, 0x03, 0x3d, 0xbc, 0x75, 0x1f, 0x6e, 0xe5, 0x9c, 0x67, 0x61, 0x3a, 0xb6, 0x41, - 0x17, 0xf4, 0x1a, 0x41, 0x4b, 0xc1, 0x97, 0x63, 0xcb, 0x83, 0x3b, 0x44, 0x08, 0x2a, 0xc3, 0x41, - 0x38, 0xa6, 0x53, 0xce, 0xec, 0x3b, 0x5d, 0xd0, 0xdb, 0x0e, 0xda, 0x3a, 0x38, 0x38, 0x54, 0xa1, - 0x35, 0xc7, 0xaf, 0x38, 0xf5, 0x0d, 0x8e, 0x6f, 0x38, 0x43, 0xd8, 0x9a, 0xd0, 0x34, 0x99, 0x48, - 0xbb, 0xa1, 0xf4, 0x47, 0x8f, 0x7f, 0x97, 0xee, 0x8e, 0x79, 0xb9, 0xd0, 0x24, 0x2e, 0x4b, 0x77, - 0x77, 0x4e, 0x58, 0x76, 0xe0, 0x5d, 0x0b, 0x7b, 0x41, 0x55, 0x68, 0xbd, 0x82, 0x0d, 0xe5, 0xc2, - 0x6e, 0x76, 0x41, 0xaf, 0xbd, 0xdf, 0x41, 0xc6, 0x22, 0x5a, 0x59, 0x44, 0x47, 0x2b, 0x8b, 0x23, - 0xe7, 0xbc, 0x74, 0x6b, 0x97, 0xa5, 0x6b, 0x5d, 0xd3, 0x53, 0xc5, 0xde, 0xe9, 0x0f, 0x17, 0x04, - 0x5a, 0xc7, 0x12, 0xd0, 0xc9, 0x07, 0x21, 0x29, 0x52, 0x39, 0x61, 0x54, 0xa6, 0x71, 0xa8, 0xb7, - 0x49, 0xe2, 0x78, 0xc6, 0x66, 0x19, 0x91, 0xbc, 0xb0, 0x5b, 0xca, 0xc7, 0x08, 0x29, 0xb5, 0xef, - 0xa5, 0xfb, 0x28, 0x49, 0xe5, 0x64, 0x16, 0xa1, 0x98, 0xb3, 0x6a, 0x19, 0xd5, 0x4f, 0x5f, 0x8c, - 0x3f, 0x62, 0x39, 0xcf, 0xa9, 0x40, 0x87, 0x34, 0x0e, 0x1e, 0xe4, 0x83, 0xe1, 0x95, 0xa8, 0x7a, - 0xe3, 0xe1, 0x5a, 0x52, 0x37, 0xf5, 0xff, 0xdb, 0x74, 0xeb, 0x96, 0x4d, 0xfd, 0x1b, 0x9b, 0x1e, - 0xdc, 0xfd, 0x7c, 0xe6, 0xd6, 0x7e, 0x9d, 0xb9, 0xc0, 0x7b, 0x07, 0xef, 0xbd, 0xa0, 0x53, 0x2a, - 0x52, 0xf1, 0x56, 0x12, 0x49, 0xad, 0xe7, 0xb0, 0xa9, 0x06, 0x10, 0x36, 0xe8, 0xd6, 0x7b, 0xed, - 0xfd, 0x87, 0xe8, 0xc6, 0xf3, 0x45, 0xeb, 0x6b, 0x19, 0x6d, 0x7f, 0xfd, 0xd2, 0x6f, 0xbe, 0x51, - 0x07, 0x12, 0x98, 0xf2, 0xd1, 0xeb, 0xf3, 0x85, 0x03, 0x2e, 0x16, 0x0e, 0xf8, 0xb9, 0x70, 0xc0, - 0xe9, 0xd2, 0xa9, 0x5d, 0x2c, 0x9d, 0xda, 0xb7, 0xa5, 0x53, 0x7b, 0xff, 0x6c, 0xc3, 0x40, 0x25, - 0xde, 0xcf, 0x48, 0x24, 0x56, 0x00, 0x1f, 0xfb, 0x03, 0xfc, 0x69, 0xe3, 0xdb, 0xd2, 0x9e, 0xa2, - 0x96, 0x5e, 0xeb, 0xd3, 0xbf, 0x01, 0x00, 0x00, 0xff, 0xff, 0x7a, 0x64, 0xff, 0x8f, 0x7d, 0x03, - 0x00, 0x00, + 0x10, 0x8e, 0xc9, 0x4f, 0xd5, 0x0d, 0xbd, 0x58, 0x95, 0x70, 0x83, 0x64, 0x47, 0x96, 0x40, 0x41, + 0x28, 0xbb, 0x76, 0x11, 0x17, 0x6e, 0xb1, 0x2a, 0x10, 0x17, 0x40, 0xa6, 0xe2, 0xc0, 0xc5, 0x5a, + 0xdb, 0x8b, 0x63, 0xe1, 0xcd, 0x5a, 0xde, 0x4d, 0x4b, 0xde, 0xa2, 0x0f, 0xc3, 0x43, 0x54, 0x9c, + 0x7a, 0x44, 0x1c, 0x0c, 0x4a, 0x6e, 0x3d, 0xf6, 0x09, 0xd0, 0xee, 0x3a, 0x4d, 0x8a, 0x14, 0x0e, + 0x3d, 0x25, 0xdf, 0xcc, 0x37, 0xdf, 0xcc, 0xb7, 0x33, 0x06, 0xcf, 0x19, 0xa7, 0x8c, 0xe7, 0x1c, + 0x65, 0x98, 0x52, 0x24, 0xce, 0x71, 0x89, 0xce, 0xfc, 0x98, 0x08, 0xec, 0x2b, 0x10, 0x55, 0x24, + 0x61, 0x55, 0x0a, 0xcb, 0x8a, 0x09, 0x66, 0x1e, 0x35, 0x64, 0x28, 0xc9, 0x50, 0xe6, 0x61, 0x43, + 0x1e, 0x1c, 0x66, 0x2c, 0x63, 0x8a, 0x85, 0xe4, 0x3f, 0x5d, 0x30, 0x38, 0xca, 0x18, 0xcb, 0x0a, + 0x82, 0x14, 0x8a, 0xe7, 0x5f, 0x10, 0x9e, 0x2d, 0xd6, 0xa9, 0x44, 0x89, 0x45, 0xba, 0x46, 0x83, + 0x26, 0x65, 0x6b, 0x84, 0x62, 0xcc, 0xc9, 0xed, 0x34, 0x09, 0xcb, 0x67, 0x4d, 0xde, 0xf9, 0x57, + 0x55, 0xe4, 0x94, 0x70, 0x81, 0x69, 0xa9, 0x09, 0xee, 0x75, 0x1b, 0x80, 0xd3, 0x73, 0x5c, 0x86, + 0x6a, 0x78, 0xf3, 0x11, 0xd8, 0x2b, 0x19, 0x2b, 0xa2, 0x3c, 0xb5, 0x8c, 0xa1, 0x31, 0xea, 0x84, + 0x3d, 0x09, 0xdf, 0xa6, 0xa6, 0x0b, 0x0e, 0x30, 0xe7, 0x44, 0x44, 0x5e, 0x94, 0x92, 0x19, 0xa3, + 0xd6, 0x83, 0xa1, 0x31, 0xda, 0x0f, 0xfb, 0x2a, 0xe8, 0x9d, 0xc8, 0xd0, 0x86, 0xe3, 0x37, 0x9c, + 0xf6, 0x16, 0xc7, 0xd7, 0x9c, 0x09, 0xe8, 0x4d, 0x49, 0x9e, 0x4d, 0x85, 0xd5, 0x91, 0xfa, 0xc1, + 0xb3, 0xeb, 0xda, 0x39, 0xd0, 0x2f, 0x17, 0xe9, 0xc4, 0x4d, 0xed, 0x1c, 0x2e, 0x30, 0x2d, 0x5e, + 0xb9, 0x77, 0xc2, 0x6e, 0xd8, 0x14, 0x9a, 0xef, 0x40, 0x47, 0xba, 0xb0, 0xba, 0x43, 0x63, 0xd4, + 0x3f, 0x1e, 0x40, 0x6d, 0x11, 0xae, 0x2d, 0xc2, 0xd3, 0xb5, 0xc5, 0xc0, 0xbe, 0xac, 0x9d, 0xd6, + 0x4d, 0xed, 0x98, 0x77, 0xf4, 0x64, 0xb1, 0x7b, 0xf1, 0xdb, 0x31, 0x42, 0xa5, 0x63, 0x72, 0x60, + 0x97, 0x5e, 0x84, 0xab, 0x5c, 0x4c, 0x29, 0x11, 0x79, 0x12, 0xa9, 0x6d, 0xe2, 0x24, 0x99, 0xd3, + 0x79, 0x81, 0x05, 0xab, 0xac, 0x9e, 0xf4, 0x11, 0x40, 0xa9, 0xf6, 0xab, 0x76, 0x9e, 0x66, 0xb9, + 0x98, 0xce, 0x63, 0x98, 0x30, 0xda, 0x2c, 0xa3, 0xf9, 0x19, 0xf3, 0xf4, 0x2b, 0x12, 0x8b, 0x92, + 0x70, 0x78, 0x42, 0x92, 0xf0, 0x71, 0xe9, 0x4d, 0x6e, 0x45, 0xe5, 0x1b, 0x4f, 0x36, 0x92, 0xaa, + 0xa9, 0xff, 0xdf, 0xa6, 0x7b, 0xf7, 0x6c, 0xea, 0xef, 0x6c, 0xea, 0x7e, 0x02, 0x0f, 0xdf, 0x90, + 0x19, 0xe1, 0x39, 0xff, 0x28, 0xb0, 0x20, 0xe6, 0x6b, 0xd0, 0x95, 0x6d, 0xb9, 0x65, 0x0c, 0xdb, + 0xa3, 0xfe, 0xf1, 0x13, 0xb8, 0xf3, 0x68, 0xe1, 0xe6, 0x46, 0x82, 0xfd, 0x1f, 0xdf, 0xc7, 0xdd, + 0x0f, 0xf2, 0x2c, 0x42, 0x5d, 0x1e, 0xbc, 0xbf, 0x5c, 0xda, 0xc6, 0xd5, 0xd2, 0x36, 0xfe, 0x2c, + 0x6d, 0xe3, 0x62, 0x65, 0xb7, 0xae, 0x56, 0x76, 0xeb, 0xe7, 0xca, 0x6e, 0x7d, 0x7e, 0xb9, 0x35, + 0x76, 0x23, 0x3e, 0x2e, 0x70, 0xcc, 0xd7, 0x00, 0x9d, 0xf9, 0x1e, 0xfa, 0xb6, 0xf5, 0x45, 0x29, + 0x27, 0x71, 0x4f, 0x2d, 0xf3, 0xc5, 0xdf, 0x00, 0x00, 0x00, 0xff, 0xff, 0x11, 0xd7, 0xb3, 0x2c, + 0x73, 0x03, 0x00, 0x00, } -func (this *TwapRecord) Equal(that interface{}) bool { - if that == nil { - return this == nil - } - - that1, ok := that.(*TwapRecord) - if !ok { - that2, ok := that.(TwapRecord) - if ok { - that1 = &that2 - } else { - return false - } - } - if that1 == nil { - return this == nil - } else if this == nil { - return false - } - if this.PoolId != that1.PoolId { - return false - } - if this.Asset_0Denom != that1.Asset_0Denom { - return false - } - if this.Asset_1Denom != that1.Asset_1Denom { - return false - } - if this.Height != that1.Height { - return false - } - if !this.Time.Equal(that1.Time) { - return false - } - if !this.P0ArithmeticTwapAccumulator.Equal(that1.P0ArithmeticTwapAccumulator) { - return false - } - if !this.P1ArithmeticTwapAccumulator.Equal(that1.P1ArithmeticTwapAccumulator) { - return false - } - return true -} func (m *TwapRecord) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) From daf4d2d78f72e2a19a9e657b97b36839a4893180 Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Tue, 19 Jul 2022 14:52:26 -0500 Subject: [PATCH 04/72] sync --- .../gamm/twap/v1beta1/twap_record.proto | 4 +- x/gamm/twap/abci.go | 13 --- x/gamm/twap/api.go | 9 +- x/gamm/twap/keeper.go | 2 +- x/gamm/twap/logic.go | 29 +++++ x/gamm/twap/store.go | 39 +++++++ x/gamm/twap/types/keys.go | 49 +++++++++ x/gamm/twap/types/twap_record.pb.go | 102 +++++++++--------- 8 files changed, 179 insertions(+), 68 deletions(-) delete mode 100644 x/gamm/twap/abci.go create mode 100644 x/gamm/twap/logic.go diff --git a/proto/osmosis/gamm/twap/v1beta1/twap_record.proto b/proto/osmosis/gamm/twap/v1beta1/twap_record.proto index 52e8ace8c0c..0fa6ef35c31 100644 --- a/proto/osmosis/gamm/twap/v1beta1/twap_record.proto +++ b/proto/osmosis/gamm/twap/v1beta1/twap_record.proto @@ -19,9 +19,9 @@ option go_package = "github.com/osmosis-labs/osmosis/v10/x/gamm/twap/types"; message TwapRecord { uint64 pool_id = 1; // Lexicographically smaller denom of the pair - string asset_0_denom = 2; + string asset0_denom = 2; // Lexicographically larger denom of the pair - string asset_1_denom = 3; + string asset1_denom = 3; // height this record corresponds to, for debugging purposes uint64 height = 4 [ (gogoproto.moretags) = "yaml:\"record_height\"", diff --git a/x/gamm/twap/abci.go b/x/gamm/twap/abci.go deleted file mode 100644 index 04bf392a7b7..00000000000 --- a/x/gamm/twap/abci.go +++ /dev/null @@ -1,13 +0,0 @@ -package twap - -import sdk "github.com/cosmos/cosmos-sdk/types" - -func (k twapkeeper) endBlockLogic(ctx sdk.Context) { - // TODO: Update TWAP entries - // Step 1: Get all altered pool ids - changedPoolIds := k.getChangedPools(ctx) - if len(changedPoolIds) == 0 { - return - } - // Step 2: -} diff --git a/x/gamm/twap/api.go b/x/gamm/twap/api.go index d598fa33885..0877253ff25 100644 --- a/x/gamm/twap/api.go +++ b/x/gamm/twap/api.go @@ -1,14 +1,17 @@ package twap import ( + "errors" "time" sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/osmosis-labs/osmosis/v10/x/gamm/twap/types" ) // GetArithmeticTwap returns an arithmetic TWAP result from (startTime, endTime), // for the `quoteAsset / baseAsset` ratio on `poolId`. -// startTime and endTime do not have to be real block times that occured, +// startTime and endTime do not have to be real block times that occurred, // this function will interpolate between startTime. // if endTime = now, we do {X} // startTime must be in time range {X}, recommended parameterization for mainnet is {Y} @@ -20,3 +23,7 @@ func (k twapkeeper) GetArithmeticTwap( endTime time.Time) (sdk.Dec, error) { return sdk.Dec{}, nil } + +func (k twapkeeper) GetLatestAccumulatorRecord(poolId uint64, asset0Denom string, asset1Denom string) (types.TwapRecord, error) { + return types.TwapRecord{}, errors.New("unimplemented") +} diff --git a/x/gamm/twap/keeper.go b/x/gamm/twap/keeper.go index b3a189b2701..54a8ce02430 100644 --- a/x/gamm/twap/keeper.go +++ b/x/gamm/twap/keeper.go @@ -3,6 +3,6 @@ package twap import sdk "github.com/cosmos/cosmos-sdk/types" type twapkeeper struct { - // storeKey sdk.StoreKey + storeKey sdk.StoreKey transientKey sdk.TransientStoreKey } diff --git a/x/gamm/twap/logic.go b/x/gamm/twap/logic.go new file mode 100644 index 00000000000..4ba0e62065f --- /dev/null +++ b/x/gamm/twap/logic.go @@ -0,0 +1,29 @@ +package twap + +import ( + "errors" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func (k twapkeeper) endBlockLogic(ctx sdk.Context) { + // TODO: Update TWAP entries + // Step 1: Get all altered pool ids + changedPoolIds := k.getChangedPools(ctx) + if len(changedPoolIds) == 0 { + return + } + // Step 2: +} + +func (k twapkeeper) updateTWAPs(ctx sdk.Context, poolId uint64) error { + twaps, err := k.getAllMostRecentTWAPsForPool(ctx, poolId) + if err != nil { + return err + } + for _, twap := range twaps { + // TODO: Update logic + _ = twap + } + return errors.New("Not yet implemented") +} diff --git a/x/gamm/twap/store.go b/x/gamm/twap/store.go index 171a678f897..6be9faf1b11 100644 --- a/x/gamm/twap/store.go +++ b/x/gamm/twap/store.go @@ -4,6 +4,9 @@ import ( "encoding/binary" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/gogo/protobuf/proto" + + "github.com/osmosis-labs/osmosis/v10/x/gamm/twap/types" ) func (k twapkeeper) trackChangedPool(ctx sdk.Context, poolId uint64) { @@ -28,3 +31,39 @@ func (k twapkeeper) getChangedPools(ctx sdk.Context) []uint64 { } return alteredPoolIds } + +func (k twapkeeper) storeHistoricalTWAP(ctx sdk.Context, twap types.TwapRecord) { + store := ctx.KVStore(k.storeKey) + key := types.FormatHistoricalTWAPKey(twap.PoolId, twap.Time, twap.Asset0Denom, twap.Asset1Denom) + + bz, err := proto.Marshal(&twap) + if err != nil { + panic(err) + } + + store.Set(key, bz) +} + +func (k twapkeeper) getMostRecentTWAP(ctx sdk.Context, poolId uint64, asset0Denom string, asset1Denom string) (types.TwapRecord, error) { + store := ctx.KVStore(k.storeKey) + key := types.FormatMostRecentTWAPKey(poolId, asset0Denom, asset1Denom) + bz := store.Get(key) + return types.ParseTwapFromBz(bz) +} + +func (k twapkeeper) getAllMostRecentTWAPsForPool(ctx sdk.Context, poolId uint64) ([]types.TwapRecord, error) { + store := ctx.KVStore(k.storeKey) + return types.GetAllMostRecentTwapsForPool(store, poolId) +} + +func (k twapkeeper) storeMostRecentTWAP(ctx sdk.Context, twap types.TwapRecord) { + store := ctx.KVStore(k.storeKey) + key := types.FormatMostRecentTWAPKey(twap.PoolId, twap.Asset0Denom, twap.Asset1Denom) + + bz, err := proto.Marshal(&twap) + if err != nil { + panic(err) + } + + store.Set(key, bz) +} diff --git a/x/gamm/twap/types/keys.go b/x/gamm/twap/types/keys.go index 5020db04d7c..b7a9d5b816d 100644 --- a/x/gamm/twap/types/keys.go +++ b/x/gamm/twap/types/keys.go @@ -1,5 +1,14 @@ package types +import ( + "errors" + fmt "fmt" + time "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/gogo/protobuf/proto" +) + const ( ModuleName = "twap" @@ -8,6 +17,46 @@ const ( RouterKey = ModuleName QuerierRoute = ModuleName + // Contract: Coin denoms cannot contain this character + KeySeparator = "|" ) var AlteredPoolIdsPrefix = []byte{0} + +var mostRecentTWAPsPrefix = "recent_twap" + KeySeparator +var historicalTWAPsPrefix = "historical_twap" + KeySeparator + +// TODO: make utility command to automatically interlace separators + +func FormatMostRecentTWAPKey(poolId uint64, denom1 string, denom2 string) []byte { + return []byte(fmt.Sprintf("%s%s%d%s%s%s%s", mostRecentTWAPsPrefix, KeySeparator, poolId, KeySeparator, denom1, KeySeparator, denom2)) +} + +func FormatHistoricalTWAPKey(poolId uint64, accumulatorWriteTime time.Time, denom1 string, denom2 string) []byte { + return []byte(fmt.Sprintf("%s%s%d%s%s%s%s%s%s", historicalTWAPsPrefix, KeySeparator, poolId, KeySeparator, accumulatorWriteTime, KeySeparator, denom1, KeySeparator, denom2)) +} + +func GetAllMostRecentTwapsForPool(store sdk.KVStore, poolId uint64) ([]TwapRecord, error) { + startPrefix := fmt.Sprintf("%s%s%d%s", mostRecentTWAPsPrefix, KeySeparator, poolId, KeySeparator) + endPrefix := fmt.Sprintf("%s%s%d%s", mostRecentTWAPsPrefix, KeySeparator, poolId+1, KeySeparator) + iter := store.Iterator([]byte(startPrefix), []byte(endPrefix)) + defer iter.Close() + twaps := []TwapRecord{} + for ; iter.Valid(); iter.Next() { + val := iter.Value() + twap, err := ParseTwapFromBz(val) + if err != nil { + return twaps, err + } + twaps = append(twaps, twap) + } + return twaps, nil +} + +func ParseTwapFromBz(bz []byte) (twap TwapRecord, err error) { + if len(bz) > 0 { + return TwapRecord{}, errors.New("twap not found") + } + err = proto.Unmarshal(bz, &twap) + return twap, err +} diff --git a/x/gamm/twap/types/twap_record.pb.go b/x/gamm/twap/types/twap_record.pb.go index 947c1744de2..45e1213aa5d 100644 --- a/x/gamm/twap/types/twap_record.pb.go +++ b/x/gamm/twap/types/twap_record.pb.go @@ -41,9 +41,9 @@ const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package type TwapRecord struct { PoolId uint64 `protobuf:"varint,1,opt,name=pool_id,json=poolId,proto3" json:"pool_id,omitempty"` // Lexicographically smaller denom of the pair - Asset_0Denom string `protobuf:"bytes,2,opt,name=asset_0_denom,json=asset0Denom,proto3" json:"asset_0_denom,omitempty"` + Asset0Denom string `protobuf:"bytes,2,opt,name=asset0_denom,json=asset0Denom,proto3" json:"asset0_denom,omitempty"` // Lexicographically larger denom of the pair - Asset_1Denom string `protobuf:"bytes,3,opt,name=asset_1_denom,json=asset1Denom,proto3" json:"asset_1_denom,omitempty"` + Asset1Denom string `protobuf:"bytes,3,opt,name=asset1_denom,json=asset1Denom,proto3" json:"asset1_denom,omitempty"` // height this record corresponds to, for debugging purposes Height uint64 `protobuf:"varint,4,opt,name=height,proto3" json:"record_height" yaml:"record_height"` // This field should only exist until we have a global registry in the state @@ -93,16 +93,16 @@ func (m *TwapRecord) GetPoolId() uint64 { return 0 } -func (m *TwapRecord) GetAsset_0Denom() string { +func (m *TwapRecord) GetAsset0Denom() string { if m != nil { - return m.Asset_0Denom + return m.Asset0Denom } return "" } -func (m *TwapRecord) GetAsset_1Denom() string { +func (m *TwapRecord) GetAsset1Denom() string { if m != nil { - return m.Asset_1Denom + return m.Asset1Denom } return "" } @@ -176,39 +176,39 @@ func init() { } var fileDescriptor_a81e54bd4e35cf12 = []byte{ - // 500 bytes of a gzipped FileDescriptorProto + // 499 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x53, 0xcd, 0x6e, 0xd3, 0x40, - 0x10, 0x8e, 0xc9, 0x4f, 0xd5, 0x0d, 0xbd, 0x58, 0x95, 0x70, 0x83, 0x64, 0x47, 0x96, 0x40, 0x41, - 0x28, 0xbb, 0x76, 0x11, 0x17, 0x6e, 0xb1, 0x2a, 0x10, 0x17, 0x40, 0xa6, 0xe2, 0xc0, 0xc5, 0x5a, + 0x10, 0x8e, 0x69, 0x92, 0xaa, 0x9b, 0x72, 0xb1, 0x2a, 0xe1, 0x06, 0xc9, 0x0e, 0x96, 0x40, 0x41, + 0x28, 0xbb, 0x71, 0x11, 0x17, 0x6e, 0xb1, 0x2a, 0x10, 0x17, 0x40, 0xa6, 0xe2, 0xc0, 0xc5, 0x5a, 0xdb, 0x8b, 0x63, 0xe1, 0xcd, 0x5a, 0xde, 0x4d, 0x4b, 0xde, 0xa2, 0x0f, 0xc3, 0x43, 0x54, 0x9c, - 0x7a, 0x44, 0x1c, 0x0c, 0x4a, 0x6e, 0x3d, 0xf6, 0x09, 0xd0, 0xee, 0x3a, 0x4d, 0x8a, 0x14, 0x0e, - 0x3d, 0x25, 0xdf, 0xcc, 0x37, 0xdf, 0xcc, 0xb7, 0x33, 0x06, 0xcf, 0x19, 0xa7, 0x8c, 0xe7, 0x1c, - 0x65, 0x98, 0x52, 0x24, 0xce, 0x71, 0x89, 0xce, 0xfc, 0x98, 0x08, 0xec, 0x2b, 0x10, 0x55, 0x24, - 0x61, 0x55, 0x0a, 0xcb, 0x8a, 0x09, 0x66, 0x1e, 0x35, 0x64, 0x28, 0xc9, 0x50, 0xe6, 0x61, 0x43, - 0x1e, 0x1c, 0x66, 0x2c, 0x63, 0x8a, 0x85, 0xe4, 0x3f, 0x5d, 0x30, 0x38, 0xca, 0x18, 0xcb, 0x0a, - 0x82, 0x14, 0x8a, 0xe7, 0x5f, 0x10, 0x9e, 0x2d, 0xd6, 0xa9, 0x44, 0x89, 0x45, 0xba, 0x46, 0x83, - 0x26, 0x65, 0x6b, 0x84, 0x62, 0xcc, 0xc9, 0xed, 0x34, 0x09, 0xcb, 0x67, 0x4d, 0xde, 0xf9, 0x57, - 0x55, 0xe4, 0x94, 0x70, 0x81, 0x69, 0xa9, 0x09, 0xee, 0x75, 0x1b, 0x80, 0xd3, 0x73, 0x5c, 0x86, - 0x6a, 0x78, 0xf3, 0x11, 0xd8, 0x2b, 0x19, 0x2b, 0xa2, 0x3c, 0xb5, 0x8c, 0xa1, 0x31, 0xea, 0x84, - 0x3d, 0x09, 0xdf, 0xa6, 0xa6, 0x0b, 0x0e, 0x30, 0xe7, 0x44, 0x44, 0x5e, 0x94, 0x92, 0x19, 0xa3, - 0xd6, 0x83, 0xa1, 0x31, 0xda, 0x0f, 0xfb, 0x2a, 0xe8, 0x9d, 0xc8, 0xd0, 0x86, 0xe3, 0x37, 0x9c, - 0xf6, 0x16, 0xc7, 0xd7, 0x9c, 0x09, 0xe8, 0x4d, 0x49, 0x9e, 0x4d, 0x85, 0xd5, 0x91, 0xfa, 0xc1, - 0xb3, 0xeb, 0xda, 0x39, 0xd0, 0x2f, 0x17, 0xe9, 0xc4, 0x4d, 0xed, 0x1c, 0x2e, 0x30, 0x2d, 0x5e, - 0xb9, 0x77, 0xc2, 0x6e, 0xd8, 0x14, 0x9a, 0xef, 0x40, 0x47, 0xba, 0xb0, 0xba, 0x43, 0x63, 0xd4, - 0x3f, 0x1e, 0x40, 0x6d, 0x11, 0xae, 0x2d, 0xc2, 0xd3, 0xb5, 0xc5, 0xc0, 0xbe, 0xac, 0x9d, 0xd6, - 0x4d, 0xed, 0x98, 0x77, 0xf4, 0x64, 0xb1, 0x7b, 0xf1, 0xdb, 0x31, 0x42, 0xa5, 0x63, 0x72, 0x60, - 0x97, 0x5e, 0x84, 0xab, 0x5c, 0x4c, 0x29, 0x11, 0x79, 0x12, 0xa9, 0x6d, 0xe2, 0x24, 0x99, 0xd3, - 0x79, 0x81, 0x05, 0xab, 0xac, 0x9e, 0xf4, 0x11, 0x40, 0xa9, 0xf6, 0xab, 0x76, 0x9e, 0x66, 0xb9, - 0x98, 0xce, 0x63, 0x98, 0x30, 0xda, 0x2c, 0xa3, 0xf9, 0x19, 0xf3, 0xf4, 0x2b, 0x12, 0x8b, 0x92, - 0x70, 0x78, 0x42, 0x92, 0xf0, 0x71, 0xe9, 0x4d, 0x6e, 0x45, 0xe5, 0x1b, 0x4f, 0x36, 0x92, 0xaa, - 0xa9, 0xff, 0xdf, 0xa6, 0x7b, 0xf7, 0x6c, 0xea, 0xef, 0x6c, 0xea, 0x7e, 0x02, 0x0f, 0xdf, 0x90, - 0x19, 0xe1, 0x39, 0xff, 0x28, 0xb0, 0x20, 0xe6, 0x6b, 0xd0, 0x95, 0x6d, 0xb9, 0x65, 0x0c, 0xdb, - 0xa3, 0xfe, 0xf1, 0x13, 0xb8, 0xf3, 0x68, 0xe1, 0xe6, 0x46, 0x82, 0xfd, 0x1f, 0xdf, 0xc7, 0xdd, - 0x0f, 0xf2, 0x2c, 0x42, 0x5d, 0x1e, 0xbc, 0xbf, 0x5c, 0xda, 0xc6, 0xd5, 0xd2, 0x36, 0xfe, 0x2c, - 0x6d, 0xe3, 0x62, 0x65, 0xb7, 0xae, 0x56, 0x76, 0xeb, 0xe7, 0xca, 0x6e, 0x7d, 0x7e, 0xb9, 0x35, - 0x76, 0x23, 0x3e, 0x2e, 0x70, 0xcc, 0xd7, 0x00, 0x9d, 0xf9, 0x1e, 0xfa, 0xb6, 0xf5, 0x45, 0x29, - 0x27, 0x71, 0x4f, 0x2d, 0xf3, 0xc5, 0xdf, 0x00, 0x00, 0x00, 0xff, 0xff, 0x11, 0xd7, 0xb3, 0x2c, - 0x73, 0x03, 0x00, 0x00, + 0x7a, 0x44, 0x1c, 0x0c, 0x4a, 0x2e, 0x88, 0x63, 0x9f, 0x00, 0xed, 0x4f, 0xd2, 0x14, 0xa9, 0x1c, + 0x38, 0x25, 0xdf, 0xcc, 0x37, 0xdf, 0xcc, 0xb7, 0x33, 0x06, 0x4f, 0x18, 0xa7, 0x8c, 0x17, 0x1c, + 0xe5, 0x98, 0x52, 0x24, 0xce, 0x70, 0x85, 0x4e, 0x83, 0x84, 0x08, 0x1c, 0x28, 0x10, 0xd7, 0x24, + 0x65, 0x75, 0x06, 0xab, 0x9a, 0x09, 0x66, 0x1f, 0x1a, 0x32, 0x94, 0x64, 0x28, 0xf3, 0xd0, 0x90, + 0xfb, 0x07, 0x39, 0xcb, 0x99, 0x62, 0x21, 0xf9, 0x4f, 0x17, 0xf4, 0x0f, 0x73, 0xc6, 0xf2, 0x92, + 0x20, 0x85, 0x92, 0xf9, 0x47, 0x84, 0x67, 0x8b, 0x75, 0x2a, 0x55, 0x62, 0xb1, 0xae, 0xd1, 0xc0, + 0xa4, 0x5c, 0x8d, 0x50, 0x82, 0x39, 0xd9, 0x4c, 0x93, 0xb2, 0x62, 0x66, 0xf2, 0xde, 0xdf, 0xaa, + 0xa2, 0xa0, 0x84, 0x0b, 0x4c, 0x2b, 0x4d, 0xf0, 0x7f, 0xed, 0x00, 0x70, 0x72, 0x86, 0xab, 0x48, + 0x0d, 0x6f, 0xdf, 0x03, 0xbb, 0x15, 0x63, 0x65, 0x5c, 0x64, 0x8e, 0x35, 0xb0, 0x86, 0xed, 0xa8, + 0x2b, 0xe1, 0xab, 0xcc, 0x7e, 0x00, 0xf6, 0x31, 0xe7, 0x44, 0x8c, 0xe3, 0x8c, 0xcc, 0x18, 0x75, + 0xee, 0x0c, 0xac, 0xe1, 0x5e, 0xd4, 0xd3, 0xb1, 0x63, 0x19, 0xda, 0x50, 0x02, 0x43, 0xd9, 0xd9, + 0xa2, 0x04, 0x9a, 0x32, 0x01, 0xdd, 0x29, 0x29, 0xf2, 0xa9, 0x70, 0xda, 0x52, 0x3d, 0x7c, 0xfc, + 0xbb, 0xf1, 0xee, 0xea, 0x77, 0x8b, 0x75, 0xe2, 0xaa, 0xf1, 0x0e, 0x16, 0x98, 0x96, 0xcf, 0xfd, + 0x1b, 0x61, 0x3f, 0x32, 0x85, 0xf6, 0x6b, 0xd0, 0x96, 0x1e, 0x9c, 0xce, 0xc0, 0x1a, 0xf6, 0x8e, + 0xfa, 0x50, 0x1b, 0x84, 0x6b, 0x83, 0xf0, 0x64, 0x6d, 0x30, 0x74, 0x2f, 0x1a, 0xaf, 0x75, 0xd5, + 0x78, 0xf6, 0x0d, 0x3d, 0x59, 0xec, 0x9f, 0xff, 0xf0, 0xac, 0x48, 0xe9, 0xd8, 0x1c, 0xb8, 0xd5, + 0x38, 0xc6, 0x75, 0x21, 0xa6, 0x94, 0x88, 0x22, 0x8d, 0xd5, 0x2e, 0x71, 0x9a, 0xce, 0xe9, 0xbc, + 0xc4, 0x82, 0xd5, 0x4e, 0x57, 0xfa, 0x08, 0xa1, 0x54, 0xfb, 0xde, 0x78, 0x8f, 0xf2, 0x42, 0x4c, + 0xe7, 0x09, 0x4c, 0x19, 0x35, 0xab, 0x30, 0x3f, 0x23, 0x9e, 0x7d, 0x42, 0x62, 0x51, 0x11, 0x0e, + 0x8f, 0x49, 0x1a, 0xdd, 0xaf, 0xc6, 0x93, 0x8d, 0xa8, 0x7c, 0xe1, 0xc9, 0xb5, 0xa4, 0x6a, 0x1a, + 0xfc, 0xb3, 0xe9, 0xee, 0x7f, 0x36, 0x0d, 0x6e, 0x6d, 0xea, 0xbf, 0x07, 0xfb, 0x2f, 0xc9, 0x8c, + 0xf0, 0x82, 0xbf, 0x13, 0x58, 0x10, 0xfb, 0x05, 0xe8, 0xc8, 0xb6, 0xdc, 0xb1, 0x06, 0x3b, 0xc3, + 0xde, 0xd1, 0x43, 0x78, 0xeb, 0xc9, 0xc2, 0xeb, 0x0b, 0x09, 0xf7, 0xbe, 0x7e, 0x19, 0x75, 0xde, + 0xca, 0xa3, 0x88, 0x74, 0x79, 0xf8, 0xe6, 0x62, 0xe9, 0x5a, 0x97, 0x4b, 0xd7, 0xfa, 0xb9, 0x74, + 0xad, 0xf3, 0x95, 0xdb, 0xba, 0x5c, 0xb9, 0xad, 0x6f, 0x2b, 0xb7, 0xf5, 0xe1, 0xd9, 0xd6, 0xd8, + 0x46, 0x7c, 0x54, 0xe2, 0x84, 0xaf, 0x01, 0x3a, 0x0d, 0xc6, 0xe8, 0xf3, 0xd6, 0xf7, 0xa4, 0x9c, + 0x24, 0x5d, 0xb5, 0xcc, 0xa7, 0x7f, 0x02, 0x00, 0x00, 0xff, 0xff, 0x31, 0x0f, 0x2c, 0xd0, 0x71, + 0x03, 0x00, 0x00, } func (m *TwapRecord) Marshal() (dAtA []byte, err error) { @@ -264,17 +264,17 @@ func (m *TwapRecord) MarshalToSizedBuffer(dAtA []byte) (int, error) { i-- dAtA[i] = 0x20 } - if len(m.Asset_1Denom) > 0 { - i -= len(m.Asset_1Denom) - copy(dAtA[i:], m.Asset_1Denom) - i = encodeVarintTwapRecord(dAtA, i, uint64(len(m.Asset_1Denom))) + if len(m.Asset1Denom) > 0 { + i -= len(m.Asset1Denom) + copy(dAtA[i:], m.Asset1Denom) + i = encodeVarintTwapRecord(dAtA, i, uint64(len(m.Asset1Denom))) i-- dAtA[i] = 0x1a } - if len(m.Asset_0Denom) > 0 { - i -= len(m.Asset_0Denom) - copy(dAtA[i:], m.Asset_0Denom) - i = encodeVarintTwapRecord(dAtA, i, uint64(len(m.Asset_0Denom))) + if len(m.Asset0Denom) > 0 { + i -= len(m.Asset0Denom) + copy(dAtA[i:], m.Asset0Denom) + i = encodeVarintTwapRecord(dAtA, i, uint64(len(m.Asset0Denom))) i-- dAtA[i] = 0x12 } @@ -343,11 +343,11 @@ func (m *TwapRecord) Size() (n int) { if m.PoolId != 0 { n += 1 + sovTwapRecord(uint64(m.PoolId)) } - l = len(m.Asset_0Denom) + l = len(m.Asset0Denom) if l > 0 { n += 1 + l + sovTwapRecord(uint64(l)) } - l = len(m.Asset_1Denom) + l = len(m.Asset1Denom) if l > 0 { n += 1 + l + sovTwapRecord(uint64(l)) } @@ -434,7 +434,7 @@ func (m *TwapRecord) Unmarshal(dAtA []byte) error { } case 2: if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Asset_0Denom", wireType) + return fmt.Errorf("proto: wrong wireType = %d for field Asset0Denom", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { @@ -462,11 +462,11 @@ func (m *TwapRecord) Unmarshal(dAtA []byte) error { if postIndex > l { return io.ErrUnexpectedEOF } - m.Asset_0Denom = string(dAtA[iNdEx:postIndex]) + m.Asset0Denom = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex case 3: if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Asset_1Denom", wireType) + return fmt.Errorf("proto: wrong wireType = %d for field Asset1Denom", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { @@ -494,7 +494,7 @@ func (m *TwapRecord) Unmarshal(dAtA []byte) error { if postIndex > l { return io.ErrUnexpectedEOF } - m.Asset_1Denom = string(dAtA[iNdEx:postIndex]) + m.Asset1Denom = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex case 4: if wireType != 0 { From 3f2b7a72a29bf2b171cddd9af95f66f6afb81b1b Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Tue, 19 Jul 2022 16:52:46 -0500 Subject: [PATCH 05/72] More TWAP stub progress --- osmoutils/iter_helper.go | 31 +++++++++++++++++++++++- osmoutils/slice_helper.go | 1 + x/gamm/keeper/pool.go | 7 ++++++ x/gamm/twap/api.go | 1 + x/gamm/twap/keeper.go | 8 +++++- x/gamm/twap/logic.go | 26 ++++++++++++++------ x/gamm/twap/module.go | 11 ++++----- x/gamm/twap/store.go | 18 +++----------- x/gamm/twap/types/expected_interfaces.go | 15 ++++++++++++ x/gamm/twap/types/keys.go | 15 +++--------- x/gamm/twap/types/utils.go | 29 ++++++++++++++++++++++ 11 files changed, 119 insertions(+), 43 deletions(-) create mode 100644 x/gamm/twap/types/expected_interfaces.go create mode 100644 x/gamm/twap/types/utils.go diff --git a/osmoutils/iter_helper.go b/osmoutils/iter_helper.go index 2f8a48abd03..e60e877f4c1 100644 --- a/osmoutils/iter_helper.go +++ b/osmoutils/iter_helper.go @@ -1,6 +1,9 @@ package osmoutils -import "github.com/cosmos/cosmos-sdk/store" +import ( + "github.com/cosmos/cosmos-sdk/store" + "github.com/gogo/protobuf/proto" +) func GatherAllKeysFromStore(storeObj store.KVStore) []string { iterator := storeObj.Iterator(nil, nil) @@ -12,3 +15,29 @@ func GatherAllKeysFromStore(storeObj store.KVStore) []string { } return keys } + +func GatherValuesFromStore[T any](storeObj store.KVStore, keyStart []byte, keyEnd []byte, parseValue func([]byte) (T, error)) ([]T, error) { + iterator := storeObj.Iterator(keyStart, keyEnd) + defer iterator.Close() + + values := []T{} + for ; iterator.Valid(); iterator.Next() { + val, err := parseValue(iterator.Value()) + if err != nil { + return nil, err + } + values = append(values, val) + } + return values, nil +} + +// MustSet runs store.Set(key, proto.Marshal(value)) +// but panics on any error. +func MustSet(storeObj store.KVStore, key []byte, value proto.Message) { + bz, err := proto.Marshal(value) + if err != nil { + panic(err) + } + + storeObj.Set(key, bz) +} diff --git a/osmoutils/slice_helper.go b/osmoutils/slice_helper.go index 1128789c19a..f2f0c3b202a 100644 --- a/osmoutils/slice_helper.go +++ b/osmoutils/slice_helper.go @@ -7,6 +7,7 @@ import ( ) // SortSlice sorts a slice of type T elements that implement constraints.Ordered. +// Mutates input slice s func SortSlice[T constraints.Ordered](s []T) { sort.Slice(s, func(i, j int) bool { return s[i] < s[j] diff --git a/x/gamm/keeper/pool.go b/x/gamm/keeper/pool.go index 9617ffb3f03..dae14c7a654 100644 --- a/x/gamm/keeper/pool.go +++ b/x/gamm/keeper/pool.go @@ -8,6 +8,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/osmosis-labs/osmosis/v10/osmoutils" "github.com/osmosis-labs/osmosis/v10/x/gamm/pool-models/balancer" "github.com/osmosis-labs/osmosis/v10/x/gamm/pool-models/stableswap" "github.com/osmosis-labs/osmosis/v10/x/gamm/types" @@ -197,6 +198,12 @@ func (k Keeper) DeletePool(ctx sdk.Context, poolId uint64) error { // return nil // } +func (k Keeper) GetPoolDenoms(ctx sdk.Context, poolId uint64) ([]string, error) { + pool, err := k.GetPoolAndPoke(ctx, poolId) + denoms := osmoutils.CoinsDenoms(pool.GetTotalPoolLiquidity(ctx)) + return denoms, err +} + // setNextPoolNumber sets next pool number. func (k Keeper) setNextPoolNumber(ctx sdk.Context, poolNumber uint64) { store := ctx.KVStore(k.storeKey) diff --git a/x/gamm/twap/api.go b/x/gamm/twap/api.go index 0877253ff25..5ddcb340896 100644 --- a/x/gamm/twap/api.go +++ b/x/gamm/twap/api.go @@ -24,6 +24,7 @@ func (k twapkeeper) GetArithmeticTwap( return sdk.Dec{}, nil } +// GetLatestAccumulatorRecord returns a TwapRecord struct that can be stored func (k twapkeeper) GetLatestAccumulatorRecord(poolId uint64, asset0Denom string, asset1Denom string) (types.TwapRecord, error) { return types.TwapRecord{}, errors.New("unimplemented") } diff --git a/x/gamm/twap/keeper.go b/x/gamm/twap/keeper.go index 54a8ce02430..6a96a6e72ef 100644 --- a/x/gamm/twap/keeper.go +++ b/x/gamm/twap/keeper.go @@ -1,8 +1,14 @@ package twap -import sdk "github.com/cosmos/cosmos-sdk/types" +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/osmosis-labs/osmosis/v10/x/gamm/twap/types" +) type twapkeeper struct { storeKey sdk.StoreKey transientKey sdk.TransientStoreKey + + gammkeeper types.AmmInterface } diff --git a/x/gamm/twap/logic.go b/x/gamm/twap/logic.go index 4ba0e62065f..ea3675d9acf 100644 --- a/x/gamm/twap/logic.go +++ b/x/gamm/twap/logic.go @@ -4,16 +4,16 @@ import ( "errors" sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/osmosis-labs/osmosis/v10/x/gamm/twap/types" ) -func (k twapkeeper) endBlockLogic(ctx sdk.Context) { - // TODO: Update TWAP entries - // Step 1: Get all altered pool ids - changedPoolIds := k.getChangedPools(ctx) - if len(changedPoolIds) == 0 { - return - } - // Step 2: +func (k twapkeeper) afterCreatePool(ctx sdk.Context, poolId uint64) error { + denoms, err := k.gammkeeper.GetPoolDenoms(ctx, poolId) + denomPairs0, denomPairs1 := types.GetAllUniqueDenomPairs(denoms) + // for every denom pair do create twap + _, _ = denomPairs0, denomPairs1 + return err } func (k twapkeeper) updateTWAPs(ctx sdk.Context, poolId uint64) error { @@ -27,3 +27,13 @@ func (k twapkeeper) updateTWAPs(ctx sdk.Context, poolId uint64) error { } return errors.New("Not yet implemented") } + +func (k twapkeeper) endBlockLogic(ctx sdk.Context) { + // TODO: Update TWAP entries + // Step 1: Get all altered pool ids + changedPoolIds := k.getChangedPools(ctx) + if len(changedPoolIds) == 0 { + return + } + // Step 2: +} diff --git a/x/gamm/twap/module.go b/x/gamm/twap/module.go index e15b401601f..ceba1484a75 100644 --- a/x/gamm/twap/module.go +++ b/x/gamm/twap/module.go @@ -42,12 +42,11 @@ func (AppModuleBasic) DefaultGenesis(cdc codec.JSONCodec) json.RawMessage { // ValidateGenesis performs genesis state validation for the gamm module. func (AppModuleBasic) ValidateGenesis(cdc codec.JSONCodec, config client.TxEncodingConfig, bz json.RawMessage) error { - // var genState types.GenesisState - // if err := cdc.UnmarshalJSON(bz, &genState); err != nil { - // return fmt.Errorf("failed to unmarshal %s genesis state: %w", types.ModuleName, err) - // } - // return genState.Validate() - return nil + var genState types.GenesisState + if err := cdc.UnmarshalJSON(bz, &genState); err != nil { + return fmt.Errorf("failed to unmarshal %s genesis state: %w", types.ModuleName, err) + } + return genState.Validate() } //--------------------------------------- diff --git a/x/gamm/twap/store.go b/x/gamm/twap/store.go index 6be9faf1b11..bc5b671fa9e 100644 --- a/x/gamm/twap/store.go +++ b/x/gamm/twap/store.go @@ -4,8 +4,8 @@ import ( "encoding/binary" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/gogo/protobuf/proto" + "github.com/osmosis-labs/osmosis/v10/osmoutils" "github.com/osmosis-labs/osmosis/v10/x/gamm/twap/types" ) @@ -35,13 +35,7 @@ func (k twapkeeper) getChangedPools(ctx sdk.Context) []uint64 { func (k twapkeeper) storeHistoricalTWAP(ctx sdk.Context, twap types.TwapRecord) { store := ctx.KVStore(k.storeKey) key := types.FormatHistoricalTWAPKey(twap.PoolId, twap.Time, twap.Asset0Denom, twap.Asset1Denom) - - bz, err := proto.Marshal(&twap) - if err != nil { - panic(err) - } - - store.Set(key, bz) + osmoutils.MustSet(store, key, &twap) } func (k twapkeeper) getMostRecentTWAP(ctx sdk.Context, poolId uint64, asset0Denom string, asset1Denom string) (types.TwapRecord, error) { @@ -59,11 +53,5 @@ func (k twapkeeper) getAllMostRecentTWAPsForPool(ctx sdk.Context, poolId uint64) func (k twapkeeper) storeMostRecentTWAP(ctx sdk.Context, twap types.TwapRecord) { store := ctx.KVStore(k.storeKey) key := types.FormatMostRecentTWAPKey(twap.PoolId, twap.Asset0Denom, twap.Asset1Denom) - - bz, err := proto.Marshal(&twap) - if err != nil { - panic(err) - } - - store.Set(key, bz) + osmoutils.MustSet(store, key, &twap) } diff --git a/x/gamm/twap/types/expected_interfaces.go b/x/gamm/twap/types/expected_interfaces.go new file mode 100644 index 00000000000..cf2a028a7ad --- /dev/null +++ b/x/gamm/twap/types/expected_interfaces.go @@ -0,0 +1,15 @@ +package types + +import sdk "github.com/cosmos/cosmos-sdk/types" + +// CalculateSpotPrice returns the spot price of the quote asset in terms of the base asset, +// using the specified pool. +// E.g. if pool 1 traded 2 atom for 3 osmo, the quote asset was atom, and the base asset was osmo, +// this would return 1.5. (Meaning that 1 atom costs 1.5 osmo) +type AmmInterface interface { + GetPoolDenoms(ctx sdk.Context, poolId uint64) (denoms []string, err error) + GetSpotPrice(ctx sdk.Context, + poolID uint64, + baseAssetDenom string, + quoteAssetDenom string) (price sdk.Dec, err error) +} diff --git a/x/gamm/twap/types/keys.go b/x/gamm/twap/types/keys.go index b7a9d5b816d..30bb8f7d133 100644 --- a/x/gamm/twap/types/keys.go +++ b/x/gamm/twap/types/keys.go @@ -7,6 +7,8 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/gogo/protobuf/proto" + + "github.com/osmosis-labs/osmosis/v10/osmoutils" ) const ( @@ -39,18 +41,7 @@ func FormatHistoricalTWAPKey(poolId uint64, accumulatorWriteTime time.Time, deno func GetAllMostRecentTwapsForPool(store sdk.KVStore, poolId uint64) ([]TwapRecord, error) { startPrefix := fmt.Sprintf("%s%s%d%s", mostRecentTWAPsPrefix, KeySeparator, poolId, KeySeparator) endPrefix := fmt.Sprintf("%s%s%d%s", mostRecentTWAPsPrefix, KeySeparator, poolId+1, KeySeparator) - iter := store.Iterator([]byte(startPrefix), []byte(endPrefix)) - defer iter.Close() - twaps := []TwapRecord{} - for ; iter.Valid(); iter.Next() { - val := iter.Value() - twap, err := ParseTwapFromBz(val) - if err != nil { - return twaps, err - } - twaps = append(twaps, twap) - } - return twaps, nil + return osmoutils.GatherValuesFromStore(store, []byte(startPrefix), []byte(endPrefix), ParseTwapFromBz) } func ParseTwapFromBz(bz []byte) (twap TwapRecord, err error) { diff --git a/x/gamm/twap/types/utils.go b/x/gamm/twap/types/utils.go new file mode 100644 index 00000000000..5aeaa7da85d --- /dev/null +++ b/x/gamm/twap/types/utils.go @@ -0,0 +1,29 @@ +package types + +import "sort" + +// GetAllUniqueDenomPairs returns all unique pairs of denoms, where for every pair +// (X, Y), X >= Y. +// The pair (X,Y) should only appear once in the list +// +// NOTE: Sorts the input denoms slice. +// (Should not be a problem, as this should come from coins.Denoms(), which returns a sorted order) +func GetAllUniqueDenomPairs(denoms []string) ([]string, []string) { + sort.Strings(denoms) + numPairs := len(denoms) * (len(denoms) - 1) / 2 + pairGT := make([]string, 0, numPairs) + pairLT := make([]string, 0, numPairs) + for i := 0; i < len(denoms); i++ { + for j := i + 1; j < len(denoms); j++ { + pairGT = append(pairGT, denoms[i]) + pairLT = append(pairLT, denoms[j]) + } + } + // sanity check + for i := 0; i < numPairs; i++ { + if pairGT[i] == pairLT[i] { + panic("input had duplicated denom") + } + } + return pairGT, pairLT +} From e452c242c92efb69197d40ba0769940daa4d6fb0 Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Tue, 19 Jul 2022 20:47:38 -0500 Subject: [PATCH 06/72] Add update logic --- go.mod | 2 +- .../gamm/twap/v1beta1/twap_record.proto | 2 +- x/gamm/twap/hook_listener.go | 24 +++++-- x/gamm/twap/logic.go | 55 +++++++++++--- x/gamm/twap/module.go | 4 +- x/gamm/twap/store.go | 7 ++ x/gamm/twap/types/twap_record.pb.go | 72 +++++++++---------- x/gamm/twap/types/utils.go | 21 +++++- x/gamm/types/hooks.go | 17 +++++ x/pool-incentives/keeper/hooks.go | 3 + 10 files changed, 152 insertions(+), 55 deletions(-) diff --git a/go.mod b/go.mod index fe15d4903b3..148cf09f1a9 100644 --- a/go.mod +++ b/go.mod @@ -79,7 +79,7 @@ require ( github.com/coinbase/rosetta-sdk-go v0.7.0 // indirect github.com/confio/ics23/go v0.7.0 // indirect github.com/containerd/continuity v0.3.0 // indirect - github.com/cosmos/btcutil v1.0.4 // indirect + github.com/cosmos/btcutil v1.0.4 github.com/cosmos/gorocksdb v1.2.0 // indirect github.com/cosmos/ledger-cosmos-go v0.11.1 // indirect github.com/cosmos/ledger-go v0.9.2 // indirect diff --git a/proto/osmosis/gamm/twap/v1beta1/twap_record.proto b/proto/osmosis/gamm/twap/v1beta1/twap_record.proto index 0fa6ef35c31..b4ad1b4a650 100644 --- a/proto/osmosis/gamm/twap/v1beta1/twap_record.proto +++ b/proto/osmosis/gamm/twap/v1beta1/twap_record.proto @@ -23,7 +23,7 @@ message TwapRecord { // Lexicographically larger denom of the pair string asset1_denom = 3; // height this record corresponds to, for debugging purposes - uint64 height = 4 [ + int64 height = 4 [ (gogoproto.moretags) = "yaml:\"record_height\"", (gogoproto.jsontag) = "record_height" ]; diff --git a/x/gamm/twap/hook_listener.go b/x/gamm/twap/hook_listener.go index 91911ebe03b..f88ab8100dd 100644 --- a/x/gamm/twap/hook_listener.go +++ b/x/gamm/twap/hook_listener.go @@ -29,17 +29,31 @@ type gammhook struct { // AfterPoolCreated is called after CreatePool func (hook *gammhook) AfterPoolCreated(ctx sdk.Context, sender sdk.AccAddress, poolId uint64) { - // TODO: Log pool creation to begin creating TWAPs for it + err := hook.k.afterCreatePool(ctx, poolId) + // Will halt pool creation + if err != nil { + panic(err) + } +} + +func (hook *gammhook) BeforeSwap(ctx sdk.Context, _ sdk.AccAddress, poolId uint64) { + err := hook.k.updateTwapIfNotRedundant(ctx, poolId) + if err != nil { + panic(err) + } } func (hook *gammhook) AfterSwap(ctx sdk.Context, sender sdk.AccAddress, poolId uint64, input sdk.Coins, output sdk.Coins) { - // Log that this pool had a potential spot price change - hook.k.trackChangedPool(ctx, poolId) +} + +func (hook *gammhook) BeforeJoinPool(ctx sdk.Context, sender sdk.AccAddress, poolId uint64) { + err := hook.k.updateTwapIfNotRedundant(ctx, poolId) + if err != nil { + panic(err) + } } func (hook *gammhook) AfterJoinPool(ctx sdk.Context, sender sdk.AccAddress, poolId uint64, enterCoins sdk.Coins, shareOutAmount sdk.Int) { - // Log that this pool had a potential spot price change - hook.k.trackChangedPool(ctx, poolId) } func (hook *gammhook) AfterExitPool(_ sdk.Context, _ sdk.AccAddress, _ uint64, _ sdk.Int, _ sdk.Coins) { diff --git a/x/gamm/twap/logic.go b/x/gamm/twap/logic.go index ea3675d9acf..153b022efbf 100644 --- a/x/gamm/twap/logic.go +++ b/x/gamm/twap/logic.go @@ -1,8 +1,6 @@ package twap import ( - "errors" - sdk "github.com/cosmos/cosmos-sdk/types" "github.com/osmosis-labs/osmosis/v10/x/gamm/twap/types" @@ -11,21 +9,62 @@ import ( func (k twapkeeper) afterCreatePool(ctx sdk.Context, poolId uint64) error { denoms, err := k.gammkeeper.GetPoolDenoms(ctx, poolId) denomPairs0, denomPairs1 := types.GetAllUniqueDenomPairs(denoms) - // for every denom pair do create twap - _, _ = denomPairs0, denomPairs1 + for i := 0; i < len(denomPairs0); i++ { + record := types.NewTwapRecord(ctx, poolId, denomPairs0[i], denomPairs1[i]) + k.storeMostRecentTWAP(ctx, record) + } return err } +func (k twapkeeper) updateTwapIfNotRedundant(ctx sdk.Context, poolId uint64) error { + if k.hasPoolChangedThisBlock(ctx, poolId) { + return nil + } + err := k.updateTWAPs(ctx, poolId) + if err != nil { + return err + } + k.trackChangedPool(ctx, poolId) + return nil +} + func (k twapkeeper) updateTWAPs(ctx sdk.Context, poolId uint64) error { + // Will only err if pool doesn't have most recent entry set twaps, err := k.getAllMostRecentTWAPsForPool(ctx, poolId) if err != nil { return err } - for _, twap := range twaps { - // TODO: Update logic - _ = twap + + for _, record := range twaps { + k.storeHistoricalTWAP(ctx, record) + timeDelta := ctx.BlockTime().Sub(record.Time) + + // no update if were in the same block. + // should be caught earlier, but secondary check. + if int(timeDelta) <= 0 { + return nil + } + + record.Height = ctx.BlockHeight() + record.Time = ctx.BlockTime() + + // TODO: Think about order + sp0, err := k.gammkeeper.GetSpotPrice(ctx, poolId, record.Asset0Denom, record.Asset1Denom) + // TODO: Document in what situations it can error + if err != nil { + return err + } + sp1, err := k.gammkeeper.GetSpotPrice(ctx, poolId, record.Asset0Denom, record.Asset1Denom) + if err != nil { + return err + } + + // TODO: Think about overflow + record.P0ArithmeticTwapAccumulator.AddMut(sp0.MulInt64(int64(timeDelta))) + record.P1ArithmeticTwapAccumulator.AddMut(sp1.MulInt64(int64(timeDelta))) + k.storeMostRecentTWAP(ctx, record) } - return errors.New("Not yet implemented") + return nil } func (k twapkeeper) endBlockLogic(ctx sdk.Context) { diff --git a/x/gamm/twap/module.go b/x/gamm/twap/module.go index ceba1484a75..3487de09621 100644 --- a/x/gamm/twap/module.go +++ b/x/gamm/twap/module.go @@ -127,10 +127,8 @@ func (am AppModule) ExportGenesis(ctx sdk.Context, cdc codec.JSONCodec) json.Raw // BeginBlock performs a no-op. func (AppModule) BeginBlock(_ sdk.Context, _ abci.RequestBeginBlock) {} -// EndBlock returns the end blocker for the gamm module. It returns no validator -// updates. +// EndBlock performs a no-op. func (am AppModule) EndBlock(ctx sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate { - am.tk.endBlockLogic(ctx) return []abci.ValidatorUpdate{} } diff --git a/x/gamm/twap/store.go b/x/gamm/twap/store.go index bc5b671fa9e..65b34fa4f89 100644 --- a/x/gamm/twap/store.go +++ b/x/gamm/twap/store.go @@ -18,6 +18,13 @@ func (k twapkeeper) trackChangedPool(ctx sdk.Context, poolId uint64) { store.Set(poolIdBz, sentinelExistsValue) } +func (k twapkeeper) hasPoolChangedThisBlock(ctx sdk.Context, poolId uint64) bool { + store := ctx.TransientStore(&k.transientKey) + poolIdBz := make([]byte, 8) + binary.LittleEndian.PutUint64(poolIdBz, poolId) + return store.Has(poolIdBz) +} + func (k twapkeeper) getChangedPools(ctx sdk.Context) []uint64 { store := ctx.TransientStore(&k.transientKey) iter := store.Iterator(nil, nil) diff --git a/x/gamm/twap/types/twap_record.pb.go b/x/gamm/twap/types/twap_record.pb.go index 45e1213aa5d..ef6de33621d 100644 --- a/x/gamm/twap/types/twap_record.pb.go +++ b/x/gamm/twap/types/twap_record.pb.go @@ -45,7 +45,7 @@ type TwapRecord struct { // Lexicographically larger denom of the pair Asset1Denom string `protobuf:"bytes,3,opt,name=asset1_denom,json=asset1Denom,proto3" json:"asset1_denom,omitempty"` // height this record corresponds to, for debugging purposes - Height uint64 `protobuf:"varint,4,opt,name=height,proto3" json:"record_height" yaml:"record_height"` + Height int64 `protobuf:"varint,4,opt,name=height,proto3" json:"record_height" yaml:"record_height"` // This field should only exist until we have a global registry in the state // machine, mapping prior block heights within {TIME RANGE} to times. Time time.Time `protobuf:"bytes,5,opt,name=time,proto3,stdtime" json:"time" yaml:"record_time"` @@ -107,7 +107,7 @@ func (m *TwapRecord) GetAsset1Denom() string { return "" } -func (m *TwapRecord) GetHeight() uint64 { +func (m *TwapRecord) GetHeight() int64 { if m != nil { return m.Height } @@ -176,39 +176,39 @@ func init() { } var fileDescriptor_a81e54bd4e35cf12 = []byte{ - // 499 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x53, 0xcd, 0x6e, 0xd3, 0x40, - 0x10, 0x8e, 0x69, 0x92, 0xaa, 0x9b, 0x72, 0xb1, 0x2a, 0xe1, 0x06, 0xc9, 0x0e, 0x96, 0x40, 0x41, - 0x28, 0xbb, 0x71, 0x11, 0x17, 0x6e, 0xb1, 0x2a, 0x10, 0x17, 0x40, 0xa6, 0xe2, 0xc0, 0xc5, 0x5a, - 0xdb, 0x8b, 0x63, 0xe1, 0xcd, 0x5a, 0xde, 0x4d, 0x4b, 0xde, 0xa2, 0x0f, 0xc3, 0x43, 0x54, 0x9c, - 0x7a, 0x44, 0x1c, 0x0c, 0x4a, 0x2e, 0x88, 0x63, 0x9f, 0x00, 0xed, 0x4f, 0xd2, 0x14, 0xa9, 0x1c, - 0x38, 0x25, 0xdf, 0xcc, 0x37, 0xdf, 0xcc, 0xb7, 0x33, 0x06, 0x4f, 0x18, 0xa7, 0x8c, 0x17, 0x1c, - 0xe5, 0x98, 0x52, 0x24, 0xce, 0x70, 0x85, 0x4e, 0x83, 0x84, 0x08, 0x1c, 0x28, 0x10, 0xd7, 0x24, - 0x65, 0x75, 0x06, 0xab, 0x9a, 0x09, 0x66, 0x1f, 0x1a, 0x32, 0x94, 0x64, 0x28, 0xf3, 0xd0, 0x90, - 0xfb, 0x07, 0x39, 0xcb, 0x99, 0x62, 0x21, 0xf9, 0x4f, 0x17, 0xf4, 0x0f, 0x73, 0xc6, 0xf2, 0x92, - 0x20, 0x85, 0x92, 0xf9, 0x47, 0x84, 0x67, 0x8b, 0x75, 0x2a, 0x55, 0x62, 0xb1, 0xae, 0xd1, 0xc0, - 0xa4, 0x5c, 0x8d, 0x50, 0x82, 0x39, 0xd9, 0x4c, 0x93, 0xb2, 0x62, 0x66, 0xf2, 0xde, 0xdf, 0xaa, - 0xa2, 0xa0, 0x84, 0x0b, 0x4c, 0x2b, 0x4d, 0xf0, 0x7f, 0xed, 0x00, 0x70, 0x72, 0x86, 0xab, 0x48, - 0x0d, 0x6f, 0xdf, 0x03, 0xbb, 0x15, 0x63, 0x65, 0x5c, 0x64, 0x8e, 0x35, 0xb0, 0x86, 0xed, 0xa8, - 0x2b, 0xe1, 0xab, 0xcc, 0x7e, 0x00, 0xf6, 0x31, 0xe7, 0x44, 0x8c, 0xe3, 0x8c, 0xcc, 0x18, 0x75, - 0xee, 0x0c, 0xac, 0xe1, 0x5e, 0xd4, 0xd3, 0xb1, 0x63, 0x19, 0xda, 0x50, 0x02, 0x43, 0xd9, 0xd9, - 0xa2, 0x04, 0x9a, 0x32, 0x01, 0xdd, 0x29, 0x29, 0xf2, 0xa9, 0x70, 0xda, 0x52, 0x3d, 0x7c, 0xfc, - 0xbb, 0xf1, 0xee, 0xea, 0x77, 0x8b, 0x75, 0xe2, 0xaa, 0xf1, 0x0e, 0x16, 0x98, 0x96, 0xcf, 0xfd, - 0x1b, 0x61, 0x3f, 0x32, 0x85, 0xf6, 0x6b, 0xd0, 0x96, 0x1e, 0x9c, 0xce, 0xc0, 0x1a, 0xf6, 0x8e, - 0xfa, 0x50, 0x1b, 0x84, 0x6b, 0x83, 0xf0, 0x64, 0x6d, 0x30, 0x74, 0x2f, 0x1a, 0xaf, 0x75, 0xd5, - 0x78, 0xf6, 0x0d, 0x3d, 0x59, 0xec, 0x9f, 0xff, 0xf0, 0xac, 0x48, 0xe9, 0xd8, 0x1c, 0xb8, 0xd5, - 0x38, 0xc6, 0x75, 0x21, 0xa6, 0x94, 0x88, 0x22, 0x8d, 0xd5, 0x2e, 0x71, 0x9a, 0xce, 0xe9, 0xbc, - 0xc4, 0x82, 0xd5, 0x4e, 0x57, 0xfa, 0x08, 0xa1, 0x54, 0xfb, 0xde, 0x78, 0x8f, 0xf2, 0x42, 0x4c, - 0xe7, 0x09, 0x4c, 0x19, 0x35, 0xab, 0x30, 0x3f, 0x23, 0x9e, 0x7d, 0x42, 0x62, 0x51, 0x11, 0x0e, - 0x8f, 0x49, 0x1a, 0xdd, 0xaf, 0xc6, 0x93, 0x8d, 0xa8, 0x7c, 0xe1, 0xc9, 0xb5, 0xa4, 0x6a, 0x1a, - 0xfc, 0xb3, 0xe9, 0xee, 0x7f, 0x36, 0x0d, 0x6e, 0x6d, 0xea, 0xbf, 0x07, 0xfb, 0x2f, 0xc9, 0x8c, - 0xf0, 0x82, 0xbf, 0x13, 0x58, 0x10, 0xfb, 0x05, 0xe8, 0xc8, 0xb6, 0xdc, 0xb1, 0x06, 0x3b, 0xc3, - 0xde, 0xd1, 0x43, 0x78, 0xeb, 0xc9, 0xc2, 0xeb, 0x0b, 0x09, 0xf7, 0xbe, 0x7e, 0x19, 0x75, 0xde, - 0xca, 0xa3, 0x88, 0x74, 0x79, 0xf8, 0xe6, 0x62, 0xe9, 0x5a, 0x97, 0x4b, 0xd7, 0xfa, 0xb9, 0x74, - 0xad, 0xf3, 0x95, 0xdb, 0xba, 0x5c, 0xb9, 0xad, 0x6f, 0x2b, 0xb7, 0xf5, 0xe1, 0xd9, 0xd6, 0xd8, - 0x46, 0x7c, 0x54, 0xe2, 0x84, 0xaf, 0x01, 0x3a, 0x0d, 0xc6, 0xe8, 0xf3, 0xd6, 0xf7, 0xa4, 0x9c, - 0x24, 0x5d, 0xb5, 0xcc, 0xa7, 0x7f, 0x02, 0x00, 0x00, 0xff, 0xff, 0x31, 0x0f, 0x2c, 0xd0, 0x71, - 0x03, 0x00, 0x00, + // 501 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x53, 0xc1, 0x6e, 0xd3, 0x40, + 0x10, 0xcd, 0x92, 0x26, 0x55, 0x37, 0xe5, 0x62, 0x55, 0xc2, 0x0d, 0x92, 0x1d, 0x22, 0x81, 0x8c, + 0x50, 0x76, 0xe3, 0x22, 0x2e, 0xdc, 0x62, 0x55, 0x20, 0x2e, 0x80, 0x4c, 0xc5, 0x81, 0x8b, 0xb5, + 0xb6, 0x17, 0xc7, 0xc2, 0x9b, 0xb5, 0xbc, 0x9b, 0x96, 0xfc, 0x45, 0x3f, 0x86, 0x8f, 0xa8, 0x38, + 0xf5, 0x88, 0x38, 0x18, 0x94, 0x5c, 0x10, 0xc7, 0x7e, 0x01, 0xda, 0x5d, 0x27, 0x4d, 0x91, 0xca, + 0x81, 0x53, 0xf2, 0x66, 0xde, 0xbc, 0x99, 0xb7, 0x33, 0x86, 0x4f, 0xb8, 0x60, 0x5c, 0xe4, 0x02, + 0x67, 0x84, 0x31, 0x2c, 0xcf, 0x48, 0x89, 0x4f, 0xfd, 0x98, 0x4a, 0xe2, 0x6b, 0x10, 0x55, 0x34, + 0xe1, 0x55, 0x8a, 0xca, 0x8a, 0x4b, 0x6e, 0x1d, 0x36, 0x64, 0xa4, 0xc8, 0x48, 0xe5, 0x51, 0x43, + 0xee, 0x1f, 0x64, 0x3c, 0xe3, 0x9a, 0x85, 0xd5, 0x3f, 0x53, 0xd0, 0x3f, 0xcc, 0x38, 0xcf, 0x0a, + 0x8a, 0x35, 0x8a, 0xe7, 0x1f, 0x31, 0x99, 0x2d, 0xd6, 0xa9, 0x44, 0x8b, 0x45, 0xa6, 0xc6, 0x80, + 0x26, 0xe5, 0x18, 0x84, 0x63, 0x22, 0xe8, 0x66, 0x9a, 0x84, 0xe7, 0xb3, 0x26, 0xef, 0xfe, 0xad, + 0x2a, 0x73, 0x46, 0x85, 0x24, 0xac, 0x34, 0x84, 0xe1, 0xaf, 0x36, 0x84, 0x27, 0x67, 0xa4, 0x0c, + 0xf5, 0xf0, 0xd6, 0x3d, 0xb8, 0x5b, 0x72, 0x5e, 0x44, 0x79, 0x6a, 0x83, 0x01, 0xf0, 0x76, 0xc2, + 0xae, 0x82, 0xaf, 0x52, 0xeb, 0x01, 0xdc, 0x27, 0x42, 0x50, 0x39, 0x8e, 0x52, 0x3a, 0xe3, 0xcc, + 0xbe, 0x33, 0x00, 0xde, 0x5e, 0xd8, 0x33, 0xb1, 0x63, 0x15, 0xda, 0x50, 0xfc, 0x86, 0xd2, 0xde, + 0xa2, 0xf8, 0x86, 0x32, 0x81, 0xdd, 0x29, 0xcd, 0xb3, 0xa9, 0xb4, 0x77, 0x06, 0xc0, 0x6b, 0x07, + 0x8f, 0x7f, 0xd7, 0xee, 0x5d, 0xf3, 0x6e, 0x91, 0x49, 0x5c, 0xd5, 0xee, 0xc1, 0x82, 0xb0, 0xe2, + 0xf9, 0xf0, 0x46, 0x78, 0x18, 0x36, 0x85, 0xd6, 0x6b, 0xb8, 0xa3, 0x3c, 0xd8, 0x9d, 0x01, 0xf0, + 0x7a, 0x47, 0x7d, 0x64, 0x0c, 0xa2, 0xb5, 0x41, 0x74, 0xb2, 0x36, 0x18, 0x38, 0x17, 0xb5, 0xdb, + 0xba, 0xaa, 0x5d, 0xeb, 0x86, 0x9e, 0x2a, 0x1e, 0x9e, 0xff, 0x70, 0x41, 0xa8, 0x75, 0x2c, 0x01, + 0x9d, 0x72, 0x1c, 0x91, 0x2a, 0x97, 0x53, 0x46, 0x65, 0x9e, 0x44, 0x7a, 0x97, 0x24, 0x49, 0xe6, + 0x6c, 0x5e, 0x10, 0xc9, 0x2b, 0xbb, 0xab, 0x7c, 0x04, 0x48, 0xa9, 0x7d, 0xaf, 0xdd, 0x47, 0x59, + 0x2e, 0xa7, 0xf3, 0x18, 0x25, 0x9c, 0x35, 0xab, 0x68, 0x7e, 0x46, 0x22, 0xfd, 0x84, 0xe5, 0xa2, + 0xa4, 0x02, 0x1d, 0xd3, 0x24, 0xbc, 0x5f, 0x8e, 0x27, 0x1b, 0x51, 0xf5, 0xc2, 0x93, 0x6b, 0x49, + 0xdd, 0xd4, 0xff, 0x67, 0xd3, 0xdd, 0xff, 0x6c, 0xea, 0xdf, 0xda, 0x74, 0xf8, 0x1e, 0xee, 0xbf, + 0xa4, 0x33, 0x2a, 0x72, 0xf1, 0x4e, 0x12, 0x49, 0xad, 0x17, 0xb0, 0xa3, 0xda, 0x0a, 0x1b, 0x0c, + 0xda, 0x5e, 0xef, 0xe8, 0x21, 0xba, 0xf5, 0x64, 0xd1, 0xf5, 0x85, 0x04, 0x7b, 0x5f, 0xbf, 0x8c, + 0x3a, 0x6f, 0xd5, 0x51, 0x84, 0xa6, 0x3c, 0x78, 0x73, 0xb1, 0x74, 0xc0, 0xe5, 0xd2, 0x01, 0x3f, + 0x97, 0x0e, 0x38, 0x5f, 0x39, 0xad, 0xcb, 0x95, 0xd3, 0xfa, 0xb6, 0x72, 0x5a, 0x1f, 0x9e, 0x6d, + 0x8d, 0xdd, 0x88, 0x8f, 0x0a, 0x12, 0x8b, 0x35, 0xc0, 0xa7, 0xfe, 0x18, 0x7f, 0xde, 0xfa, 0x9e, + 0xb4, 0x93, 0xb8, 0xab, 0x97, 0xf9, 0xf4, 0x4f, 0x00, 0x00, 0x00, 0xff, 0xff, 0x6b, 0xb2, 0xc4, + 0x2c, 0x71, 0x03, 0x00, 0x00, } func (m *TwapRecord) Marshal() (dAtA []byte, err error) { @@ -510,7 +510,7 @@ func (m *TwapRecord) Unmarshal(dAtA []byte) error { } b := dAtA[iNdEx] iNdEx++ - m.Height |= uint64(b&0x7F) << shift + m.Height |= int64(b&0x7F) << shift if b < 0x80 { break } diff --git a/x/gamm/twap/types/utils.go b/x/gamm/twap/types/utils.go index 5aeaa7da85d..f7d6274fd49 100644 --- a/x/gamm/twap/types/utils.go +++ b/x/gamm/twap/types/utils.go @@ -1,6 +1,25 @@ package types -import "sort" +import ( + "sort" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func NewTwapRecord(ctx sdk.Context, poolId uint64, denom0 string, denom1 string) TwapRecord { + if !(denom0 > denom1) { + panic("precondition denom0 > denom1 not satisfied") + } + return TwapRecord{ + PoolId: poolId, + Asset0Denom: denom0, + Asset1Denom: denom1, + Height: ctx.BlockHeight(), + Time: ctx.BlockTime(), + P0ArithmeticTwapAccumulator: sdk.ZeroDec(), + P1ArithmeticTwapAccumulator: sdk.ZeroDec(), + } +} // GetAllUniqueDenomPairs returns all unique pairs of denoms, where for every pair // (X, Y), X >= Y. diff --git a/x/gamm/types/hooks.go b/x/gamm/types/hooks.go index 20969d75245..9602c89f012 100644 --- a/x/gamm/types/hooks.go +++ b/x/gamm/types/hooks.go @@ -5,10 +5,15 @@ import sdk "github.com/cosmos/cosmos-sdk/types" type GammHooks interface { // AfterPoolCreated is called after CreatePool AfterPoolCreated(ctx sdk.Context, sender sdk.AccAddress, poolId uint64) + + BeforeJoinPool(ctx sdk.Context, sender sdk.AccAddress, poolId uint64) // AfterJoinPool is called after JoinPool, JoinSwapExternAmountIn, and JoinSwapShareAmountOut AfterJoinPool(ctx sdk.Context, sender sdk.AccAddress, poolId uint64, enterCoins sdk.Coins, shareOutAmount sdk.Int) + // AfterExitPool is called after ExitPool, ExitSwapShareAmountIn, and ExitSwapExternAmountOut AfterExitPool(ctx sdk.Context, sender sdk.AccAddress, poolId uint64, shareInAmount sdk.Int, exitCoins sdk.Coins) + + BeforeSwap(ctx sdk.Context, sender sdk.AccAddress, poolId uint64) // AfterSwap is called after SwapExactAmountIn and SwapExactAmountOut AfterSwap(ctx sdk.Context, sender sdk.AccAddress, poolId uint64, input sdk.Coins, output sdk.Coins) } @@ -29,6 +34,12 @@ func (h MultiGammHooks) AfterPoolCreated(ctx sdk.Context, sender sdk.AccAddress, } } +func (h MultiGammHooks) BeforeJoinPool(ctx sdk.Context, sender sdk.AccAddress, poolId uint64) { + for i := range h { + h[i].BeforeJoinPool(ctx, sender, poolId) + } +} + func (h MultiGammHooks) AfterJoinPool(ctx sdk.Context, sender sdk.AccAddress, poolId uint64, enterCoins sdk.Coins, shareOutAmount sdk.Int) { for i := range h { h[i].AfterJoinPool(ctx, sender, poolId, enterCoins, shareOutAmount) @@ -41,6 +52,12 @@ func (h MultiGammHooks) AfterExitPool(ctx sdk.Context, sender sdk.AccAddress, po } } +func (h MultiGammHooks) BeforeSwap(ctx sdk.Context, sender sdk.AccAddress, poolId uint64) { + for i := range h { + h[i].BeforeSwap(ctx, sender, poolId) + } +} + func (h MultiGammHooks) AfterSwap(ctx sdk.Context, sender sdk.AccAddress, poolId uint64, input sdk.Coins, output sdk.Coins) { for i := range h { h[i].AfterSwap(ctx, sender, poolId, input, output) diff --git a/x/pool-incentives/keeper/hooks.go b/x/pool-incentives/keeper/hooks.go index c2b49cdf488..8435e18e8ec 100644 --- a/x/pool-incentives/keeper/hooks.go +++ b/x/pool-incentives/keeper/hooks.go @@ -39,6 +39,9 @@ func (h Hooks) AfterExitPool(ctx sdk.Context, sender sdk.AccAddress, poolId uint func (h Hooks) AfterSwap(ctx sdk.Context, sender sdk.AccAddress, poolId uint64, input sdk.Coins, output sdk.Coins) { } +func (h Hooks) BeforeSwap(ctx sdk.Context, sender sdk.AccAddress, poolId uint64) {} +func (h Hooks) BeforeJoinPool(ctx sdk.Context, sender sdk.AccAddress, poolId uint64) {} + // Distribute coins after minter module allocate assets to pool-incentives module. func (h Hooks) AfterDistributeMintedCoin(ctx sdk.Context, mintedCoin sdk.Coin) { // @Sunny, @Tony, @Dev, what comments should we keep after modifying own BeginBlocker to hooks? From c8c033969f6afcfb62844ef3899b77eaaa6052f6 Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Wed, 20 Jul 2022 10:54:06 -0500 Subject: [PATCH 07/72] More TWAP implementation updates --- osmoutils/{iter_helper.go => store_helper.go} | 32 ++++ osmoutils/time_helper.go | 20 ++ .../gamm/twap/v1beta1/twap_record.proto | 15 +- x/gamm/pool-models/balancer/pool.go | 3 +- x/gamm/twap/api.go | 19 +- x/gamm/twap/logic.go | 25 +-- x/gamm/twap/store.go | 57 ++++-- x/gamm/twap/types/expected_interfaces.go | 2 +- x/gamm/twap/types/keys.go | 46 ++++- x/gamm/twap/types/twap_record.pb.go | 171 ++++++++++++++---- x/gamm/twap/types/utils.go | 16 +- 11 files changed, 322 insertions(+), 84 deletions(-) rename osmoutils/{iter_helper.go => store_helper.go} (55%) create mode 100644 osmoutils/time_helper.go diff --git a/osmoutils/iter_helper.go b/osmoutils/store_helper.go similarity index 55% rename from osmoutils/iter_helper.go rename to osmoutils/store_helper.go index e60e877f4c1..8b6fcb90bd4 100644 --- a/osmoutils/iter_helper.go +++ b/osmoutils/store_helper.go @@ -1,6 +1,8 @@ package osmoutils import ( + "errors" + "github.com/cosmos/cosmos-sdk/store" "github.com/gogo/protobuf/proto" ) @@ -31,6 +33,36 @@ func GatherValuesFromStore[T any](storeObj store.KVStore, keyStart []byte, keyEn return values, nil } +func GetValuesUntilDerivedStop[T any](storeObj store.KVStore, keyStart []byte, stopFn func([]byte) bool, parseValue func([]byte) (T, error)) ([]T, error) { + iterator := storeObj.Iterator(keyStart, nil) + defer iterator.Close() + + values := []T{} + for ; iterator.Valid(); iterator.Next() { + if stopFn(iterator.Key()) { + break + } + val, err := parseValue(iterator.Value()) + if err != nil { + return nil, err + } + values = append(values, val) + } + return values, nil +} + +func GetFirstValueAfterPrefix[T any](storeObj store.KVStore, keyStart []byte, parseValue func([]byte) (T, error)) (T, error) { + iterator := storeObj.Iterator(keyStart, nil) + defer iterator.Close() + + if !iterator.Valid() { + var blankValue T + return blankValue, errors.New("No values in iterator") + } + + return parseValue(iterator.Value()) +} + // MustSet runs store.Set(key, proto.Marshal(value)) // but panics on any error. func MustSet(storeObj store.KVStore, key []byte, value proto.Message) { diff --git a/osmoutils/time_helper.go b/osmoutils/time_helper.go new file mode 100644 index 00000000000..05571fa73ea --- /dev/null +++ b/osmoutils/time_helper.go @@ -0,0 +1,20 @@ +package osmoutils + +import ( + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func FormatTimeString(t time.Time) string { + return t.UTC().Round(0).Format(sdk.SortableTimeFormat) +} + +// Parses a string encoded using FormatTimeString back into a time.Time +func ParseTimeString(s string) (time.Time, error) { + t, err := time.Parse(sdk.SortableTimeFormat, s) + if err != nil { + return t, err + } + return t.UTC().Round(0), nil +} diff --git a/proto/osmosis/gamm/twap/v1beta1/twap_record.proto b/proto/osmosis/gamm/twap/v1beta1/twap_record.proto index b4ad1b4a650..8cf6c919db1 100644 --- a/proto/osmosis/gamm/twap/v1beta1/twap_record.proto +++ b/proto/osmosis/gamm/twap/v1beta1/twap_record.proto @@ -35,11 +35,22 @@ message TwapRecord { (gogoproto.moretags) = "yaml:\"record_time\"" ]; - string p0_arithmetic_twap_accumulator = 6 [ + // We store the last spot prices in the struct, so that we can interpolate accumulator values + // for times between when accumulator records are stored. + string p0_last_spot_price = 6 [ (gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec", (gogoproto.nullable) = false ]; - string p1_arithmetic_twap_accumulator = 7 [ + string p1_last_spot_price = 7 [ + (gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec", + (gogoproto.nullable) = false + ]; + + string p0_arithmetic_twap_accumulator = 8 [ + (gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec", + (gogoproto.nullable) = false + ]; + string p1_arithmetic_twap_accumulator = 9 [ (gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec", (gogoproto.nullable) = false ]; diff --git a/x/gamm/pool-models/balancer/pool.go b/x/gamm/pool-models/balancer/pool.go index 51e82bd64d6..a3c93421752 100644 --- a/x/gamm/pool-models/balancer/pool.go +++ b/x/gamm/pool-models/balancer/pool.go @@ -607,7 +607,8 @@ func (p *Pool) applySwap(ctx sdk.Context, tokensIn sdk.Coins, tokensOut sdk.Coin // this is equivalent to spot_price = (Base_supply / Weight_base) / (Quote_supply / Weight_quote) // but cancels out the common term in weight. // -// panics if pool is misconfigured and has any weight as 0. +// panics if the pool in state is incorrect, and has any weight that is 0. +// TODO: Come back and improve docs for this. func (p Pool) SpotPrice(ctx sdk.Context, baseAsset, quoteAsset string) (sdk.Dec, error) { quote, base, err := p.parsePoolAssetsByDenoms(quoteAsset, baseAsset) if err != nil { diff --git a/x/gamm/twap/api.go b/x/gamm/twap/api.go index 5ddcb340896..9ee8b73913c 100644 --- a/x/gamm/twap/api.go +++ b/x/gamm/twap/api.go @@ -1,7 +1,6 @@ package twap import ( - "errors" "time" sdk "github.com/cosmos/cosmos-sdk/types" @@ -16,6 +15,7 @@ import ( // if endTime = now, we do {X} // startTime must be in time range {X}, recommended parameterization for mainnet is {Y} func (k twapkeeper) GetArithmeticTwap( + ctx sdk.Context, poolId uint64, quoteAssetDenom string, baseAssetDenom string, @@ -24,7 +24,20 @@ func (k twapkeeper) GetArithmeticTwap( return sdk.Dec{}, nil } +func (k twapkeeper) GetArithmeticTwapToNow( + ctx sdk.Context, + poolId uint64, + quoteAssetDenom string, + baseAssetDenom string, + startTime time.Time) (sdk.Dec, error) { + return k.GetArithmeticTwap(ctx, poolId, quoteAssetDenom, baseAssetDenom, startTime, ctx.BlockTime()) +} + // GetLatestAccumulatorRecord returns a TwapRecord struct that can be stored -func (k twapkeeper) GetLatestAccumulatorRecord(poolId uint64, asset0Denom string, asset1Denom string) (types.TwapRecord, error) { - return types.TwapRecord{}, errors.New("unimplemented") +func (k twapkeeper) GetLatestAccumulatorRecord(ctx sdk.Context, poolId uint64, asset0Denom string, asset1Denom string) (types.TwapRecord, error) { + // correct ordering of args for db + if asset1Denom > asset0Denom { + asset0Denom, asset1Denom = asset1Denom, asset0Denom + } + return k.getMostRecentTWAP(ctx, poolId, asset0Denom, asset1Denom) } diff --git a/x/gamm/twap/logic.go b/x/gamm/twap/logic.go index 153b022efbf..bd1a5dc180a 100644 --- a/x/gamm/twap/logic.go +++ b/x/gamm/twap/logic.go @@ -10,7 +10,7 @@ func (k twapkeeper) afterCreatePool(ctx sdk.Context, poolId uint64) error { denoms, err := k.gammkeeper.GetPoolDenoms(ctx, poolId) denomPairs0, denomPairs1 := types.GetAllUniqueDenomPairs(denoms) for i := 0; i < len(denomPairs0); i++ { - record := types.NewTwapRecord(ctx, poolId, denomPairs0[i], denomPairs1[i]) + record := types.NewTwapRecord(k.gammkeeper, ctx, poolId, denomPairs0[i], denomPairs1[i]) k.storeMostRecentTWAP(ctx, record) } return err @@ -48,16 +48,9 @@ func (k twapkeeper) updateTWAPs(ctx sdk.Context, poolId uint64) error { record.Height = ctx.BlockHeight() record.Time = ctx.BlockTime() - // TODO: Think about order - sp0, err := k.gammkeeper.GetSpotPrice(ctx, poolId, record.Asset0Denom, record.Asset1Denom) - // TODO: Document in what situations it can error - if err != nil { - return err - } - sp1, err := k.gammkeeper.GetSpotPrice(ctx, poolId, record.Asset0Denom, record.Asset1Denom) - if err != nil { - return err - } + // TODO: Ensure order is correct + sp0 := types.MustGetSpotPrice(k.gammkeeper, ctx, poolId, record.Asset0Denom, record.Asset1Denom) + sp1 := types.MustGetSpotPrice(k.gammkeeper, ctx, poolId, record.Asset1Denom, record.Asset0Denom) // TODO: Think about overflow record.P0ArithmeticTwapAccumulator.AddMut(sp0.MulInt64(int64(timeDelta))) @@ -66,13 +59,3 @@ func (k twapkeeper) updateTWAPs(ctx sdk.Context, poolId uint64) error { } return nil } - -func (k twapkeeper) endBlockLogic(ctx sdk.Context) { - // TODO: Update TWAP entries - // Step 1: Get all altered pool ids - changedPoolIds := k.getChangedPools(ctx) - if len(changedPoolIds) == 0 { - return - } - // Step 2: -} diff --git a/x/gamm/twap/store.go b/x/gamm/twap/store.go index 65b34fa4f89..89b2b18aa90 100644 --- a/x/gamm/twap/store.go +++ b/x/gamm/twap/store.go @@ -2,6 +2,8 @@ package twap import ( "encoding/binary" + "errors" + "time" sdk "github.com/cosmos/cosmos-sdk/types" @@ -25,24 +27,20 @@ func (k twapkeeper) hasPoolChangedThisBlock(ctx sdk.Context, poolId uint64) bool return store.Has(poolIdBz) } -func (k twapkeeper) getChangedPools(ctx sdk.Context) []uint64 { - store := ctx.TransientStore(&k.transientKey) - iter := store.Iterator(nil, nil) - defer iter.Close() - - alteredPoolIds := []uint64{} - for ; iter.Key() != nil; iter.Next() { - k := iter.Key() - poolId := binary.LittleEndian.Uint64(k) - alteredPoolIds = append(alteredPoolIds, poolId) - } - return alteredPoolIds +func (k twapkeeper) storeHistoricalTWAP(ctx sdk.Context, twap types.TwapRecord) { + store := ctx.KVStore(k.storeKey) + key1 := types.FormatHistoricalTimeIndexTWAPKey(twap.Time, twap.PoolId, twap.Asset0Denom, twap.Asset1Denom) + key2 := types.FormatHistoricalPoolIndexTWAPKey(twap.PoolId, twap.Time, twap.Asset0Denom, twap.Asset1Denom) + osmoutils.MustSet(store, key1, &twap) + osmoutils.MustSet(store, key2, &twap) } -func (k twapkeeper) storeHistoricalTWAP(ctx sdk.Context, twap types.TwapRecord) { +func (k twapkeeper) deleteHistoricalTWAP(ctx sdk.Context, twap types.TwapRecord) { store := ctx.KVStore(k.storeKey) - key := types.FormatHistoricalTWAPKey(twap.PoolId, twap.Time, twap.Asset0Denom, twap.Asset1Denom) - osmoutils.MustSet(store, key, &twap) + key1 := types.FormatHistoricalTimeIndexTWAPKey(twap.Time, twap.PoolId, twap.Asset0Denom, twap.Asset1Denom) + key2 := types.FormatHistoricalPoolIndexTWAPKey(twap.PoolId, twap.Time, twap.Asset0Denom, twap.Asset1Denom) + store.Delete(key1) + store.Delete(key2) } func (k twapkeeper) getMostRecentTWAP(ctx sdk.Context, poolId uint64, asset0Denom string, asset1Denom string) (types.TwapRecord, error) { @@ -62,3 +60,32 @@ func (k twapkeeper) storeMostRecentTWAP(ctx sdk.Context, twap types.TwapRecord) key := types.FormatMostRecentTWAPKey(twap.PoolId, twap.Asset0Denom, twap.Asset1Denom) osmoutils.MustSet(store, key, &twap) } + +// returns an error if theres no historical record at or before time. +// (Asking for a time too far back) +func (k twapkeeper) getTwapBeforeTime(ctx sdk.Context, poolId uint64, time time.Time, asset0Denom string, asset1Denom string) (types.TwapRecord, error) { + store := ctx.KVStore(k.storeKey) + startKey := types.FormatHistoricalPoolIndexTimePrefix(poolId, time) + // TODO: Optimize to cut down search on asset0Denom, asset1denom. + // Not really important, since primarily envisioning 2 asset pools + stopFn := func(key []byte) bool { + return types.ParseTimeFromHistoricalPoolIndexKey(key).After(time) + } + + twaps, err := osmoutils.GetValuesUntilDerivedStop(store, startKey, stopFn, types.ParseTwapFromBz) + if err != nil { + return types.TwapRecord{}, err + } + if len(twaps) == 0 { + return types.TwapRecord{}, errors.New("looking for a time thats too old, not in the historical index. " + + " Try storing the accumulator value.") + } + + for _, twap := range twaps { + if twap.Asset0Denom == asset0Denom && twap.Asset1Denom == asset1Denom { + return twap, nil + } + } + return types.TwapRecord{}, errors.New("Something went wrong - TWAP not found, but there are twaps available for this time." + + " Were provided asset0denom and asset1denom correct?") +} diff --git a/x/gamm/twap/types/expected_interfaces.go b/x/gamm/twap/types/expected_interfaces.go index cf2a028a7ad..84c7549214d 100644 --- a/x/gamm/twap/types/expected_interfaces.go +++ b/x/gamm/twap/types/expected_interfaces.go @@ -8,7 +8,7 @@ import sdk "github.com/cosmos/cosmos-sdk/types" // this would return 1.5. (Meaning that 1 atom costs 1.5 osmo) type AmmInterface interface { GetPoolDenoms(ctx sdk.Context, poolId uint64) (denoms []string, err error) - GetSpotPrice(ctx sdk.Context, + CalculateSpotPrice(ctx sdk.Context, poolID uint64, baseAssetDenom string, quoteAssetDenom string) (price sdk.Dec, err error) diff --git a/x/gamm/twap/types/keys.go b/x/gamm/twap/types/keys.go index 30bb8f7d133..fc03997c97f 100644 --- a/x/gamm/twap/types/keys.go +++ b/x/gamm/twap/types/keys.go @@ -3,6 +3,7 @@ package types import ( "errors" fmt "fmt" + "strings" time "time" sdk "github.com/cosmos/cosmos-sdk/types" @@ -26,7 +27,8 @@ const ( var AlteredPoolIdsPrefix = []byte{0} var mostRecentTWAPsPrefix = "recent_twap" + KeySeparator -var historicalTWAPsPrefix = "historical_twap" + KeySeparator +var historicalTWAPTimeIndexPrefix = "historical_time_index" + KeySeparator +var historicalTWAPPoolIndexPrefix = "historical_pool_index" + KeySeparator // TODO: make utility command to automatically interlace separators @@ -34,8 +36,46 @@ func FormatMostRecentTWAPKey(poolId uint64, denom1 string, denom2 string) []byte return []byte(fmt.Sprintf("%s%s%d%s%s%s%s", mostRecentTWAPsPrefix, KeySeparator, poolId, KeySeparator, denom1, KeySeparator, denom2)) } -func FormatHistoricalTWAPKey(poolId uint64, accumulatorWriteTime time.Time, denom1 string, denom2 string) []byte { - return []byte(fmt.Sprintf("%s%s%d%s%s%s%s%s%s", historicalTWAPsPrefix, KeySeparator, poolId, KeySeparator, accumulatorWriteTime, KeySeparator, denom1, KeySeparator, denom2)) +// TODO: Replace historical management with ORM, we currently accept 2x write amplification right now. +func FormatHistoricalTimeIndexTWAPKey(accumulatorWriteTime time.Time, poolId uint64, denom1 string, denom2 string) []byte { + timeS := osmoutils.FormatTimeString(accumulatorWriteTime) + return []byte(fmt.Sprintf("%s%s%s%s%d%s%s%s%s", historicalTWAPTimeIndexPrefix, KeySeparator, timeS, KeySeparator, poolId, KeySeparator, denom1, KeySeparator, denom2)) +} + +func FormatHistoricalPoolIndexTWAPKey(poolId uint64, accumulatorWriteTime time.Time, denom1 string, denom2 string) []byte { + timeS := osmoutils.FormatTimeString(accumulatorWriteTime) + return []byte(fmt.Sprintf("%s%s%d%s%s%s%s%s%s", historicalTWAPPoolIndexPrefix, KeySeparator, poolId, KeySeparator, timeS, KeySeparator, denom1, KeySeparator, denom2)) +} + +func FormatHistoricalPoolIndexTimePrefix(poolId uint64, accumulatorWriteTime time.Time) []byte { + timeS := osmoutils.FormatTimeString(accumulatorWriteTime) + return []byte(fmt.Sprintf("%s%s%d%s%s%s", historicalTWAPPoolIndexPrefix, KeySeparator, poolId, KeySeparator, timeS, KeySeparator)) +} + +func ParseTimeFromHistoricalTimeIndexKey(key []byte) time.Time { + keyS := string(key) + s := strings.Split(keyS, KeySeparator) + if len(s) != 5 || s[0] != historicalTWAPTimeIndexPrefix { + panic("Called ParseTimeFromHistoricalTimeIndexKey on incorrectly formatted key") + } + t, err := osmoutils.ParseTimeString(s[1]) + if err != nil { + panic("incorrectly formatted time string in key") + } + return t +} + +func ParseTimeFromHistoricalPoolIndexKey(key []byte) time.Time { + keyS := string(key) + s := strings.Split(keyS, KeySeparator) + if len(s) != 5 || s[0] != historicalTWAPPoolIndexPrefix { + panic("Called ParseTimeFromHistoricalPoolIndexKey on incorrectly formatted key") + } + t, err := osmoutils.ParseTimeString(s[2]) + if err != nil { + panic("incorrectly formatted time string in key") + } + return t } func GetAllMostRecentTwapsForPool(store sdk.KVStore, poolId uint64) ([]TwapRecord, error) { diff --git a/x/gamm/twap/types/twap_record.pb.go b/x/gamm/twap/types/twap_record.pb.go index ef6de33621d..a54a96f1224 100644 --- a/x/gamm/twap/types/twap_record.pb.go +++ b/x/gamm/twap/types/twap_record.pb.go @@ -49,8 +49,10 @@ type TwapRecord struct { // This field should only exist until we have a global registry in the state // machine, mapping prior block heights within {TIME RANGE} to times. Time time.Time `protobuf:"bytes,5,opt,name=time,proto3,stdtime" json:"time" yaml:"record_time"` - P0ArithmeticTwapAccumulator github_com_cosmos_cosmos_sdk_types.Dec `protobuf:"bytes,6,opt,name=p0_arithmetic_twap_accumulator,json=p0ArithmeticTwapAccumulator,proto3,customtype=github.com/cosmos/cosmos-sdk/types.Dec" json:"p0_arithmetic_twap_accumulator"` - P1ArithmeticTwapAccumulator github_com_cosmos_cosmos_sdk_types.Dec `protobuf:"bytes,7,opt,name=p1_arithmetic_twap_accumulator,json=p1ArithmeticTwapAccumulator,proto3,customtype=github.com/cosmos/cosmos-sdk/types.Dec" json:"p1_arithmetic_twap_accumulator"` + P0LastSpotPrice github_com_cosmos_cosmos_sdk_types.Dec `protobuf:"bytes,6,opt,name=p0_last_spot_price,json=p0LastSpotPrice,proto3,customtype=github.com/cosmos/cosmos-sdk/types.Dec" json:"p0_last_spot_price"` + P1LastSpotPrice github_com_cosmos_cosmos_sdk_types.Dec `protobuf:"bytes,7,opt,name=p1_last_spot_price,json=p1LastSpotPrice,proto3,customtype=github.com/cosmos/cosmos-sdk/types.Dec" json:"p1_last_spot_price"` + P0ArithmeticTwapAccumulator github_com_cosmos_cosmos_sdk_types.Dec `protobuf:"bytes,8,opt,name=p0_arithmetic_twap_accumulator,json=p0ArithmeticTwapAccumulator,proto3,customtype=github.com/cosmos/cosmos-sdk/types.Dec" json:"p0_arithmetic_twap_accumulator"` + P1ArithmeticTwapAccumulator github_com_cosmos_cosmos_sdk_types.Dec `protobuf:"bytes,9,opt,name=p1_arithmetic_twap_accumulator,json=p1ArithmeticTwapAccumulator,proto3,customtype=github.com/cosmos/cosmos-sdk/types.Dec" json:"p1_arithmetic_twap_accumulator"` } func (m *TwapRecord) Reset() { *m = TwapRecord{} } @@ -176,39 +178,42 @@ func init() { } var fileDescriptor_a81e54bd4e35cf12 = []byte{ - // 501 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x53, 0xc1, 0x6e, 0xd3, 0x40, - 0x10, 0xcd, 0x92, 0x26, 0x55, 0x37, 0xe5, 0x62, 0x55, 0xc2, 0x0d, 0x92, 0x1d, 0x22, 0x81, 0x8c, - 0x50, 0x76, 0xe3, 0x22, 0x2e, 0xdc, 0x62, 0x55, 0x20, 0x2e, 0x80, 0x4c, 0xc5, 0x81, 0x8b, 0xb5, - 0xb6, 0x17, 0xc7, 0xc2, 0x9b, 0xb5, 0xbc, 0x9b, 0x96, 0xfc, 0x45, 0x3f, 0x86, 0x8f, 0xa8, 0x38, - 0xf5, 0x88, 0x38, 0x18, 0x94, 0x5c, 0x10, 0xc7, 0x7e, 0x01, 0xda, 0x5d, 0x27, 0x4d, 0x91, 0xca, - 0x81, 0x53, 0xf2, 0x66, 0xde, 0xbc, 0x99, 0xb7, 0x33, 0x86, 0x4f, 0xb8, 0x60, 0x5c, 0xe4, 0x02, - 0x67, 0x84, 0x31, 0x2c, 0xcf, 0x48, 0x89, 0x4f, 0xfd, 0x98, 0x4a, 0xe2, 0x6b, 0x10, 0x55, 0x34, - 0xe1, 0x55, 0x8a, 0xca, 0x8a, 0x4b, 0x6e, 0x1d, 0x36, 0x64, 0xa4, 0xc8, 0x48, 0xe5, 0x51, 0x43, - 0xee, 0x1f, 0x64, 0x3c, 0xe3, 0x9a, 0x85, 0xd5, 0x3f, 0x53, 0xd0, 0x3f, 0xcc, 0x38, 0xcf, 0x0a, - 0x8a, 0x35, 0x8a, 0xe7, 0x1f, 0x31, 0x99, 0x2d, 0xd6, 0xa9, 0x44, 0x8b, 0x45, 0xa6, 0xc6, 0x80, - 0x26, 0xe5, 0x18, 0x84, 0x63, 0x22, 0xe8, 0x66, 0x9a, 0x84, 0xe7, 0xb3, 0x26, 0xef, 0xfe, 0xad, - 0x2a, 0x73, 0x46, 0x85, 0x24, 0xac, 0x34, 0x84, 0xe1, 0xaf, 0x36, 0x84, 0x27, 0x67, 0xa4, 0x0c, - 0xf5, 0xf0, 0xd6, 0x3d, 0xb8, 0x5b, 0x72, 0x5e, 0x44, 0x79, 0x6a, 0x83, 0x01, 0xf0, 0x76, 0xc2, - 0xae, 0x82, 0xaf, 0x52, 0xeb, 0x01, 0xdc, 0x27, 0x42, 0x50, 0x39, 0x8e, 0x52, 0x3a, 0xe3, 0xcc, - 0xbe, 0x33, 0x00, 0xde, 0x5e, 0xd8, 0x33, 0xb1, 0x63, 0x15, 0xda, 0x50, 0xfc, 0x86, 0xd2, 0xde, - 0xa2, 0xf8, 0x86, 0x32, 0x81, 0xdd, 0x29, 0xcd, 0xb3, 0xa9, 0xb4, 0x77, 0x06, 0xc0, 0x6b, 0x07, - 0x8f, 0x7f, 0xd7, 0xee, 0x5d, 0xf3, 0x6e, 0x91, 0x49, 0x5c, 0xd5, 0xee, 0xc1, 0x82, 0xb0, 0xe2, - 0xf9, 0xf0, 0x46, 0x78, 0x18, 0x36, 0x85, 0xd6, 0x6b, 0xb8, 0xa3, 0x3c, 0xd8, 0x9d, 0x01, 0xf0, - 0x7a, 0x47, 0x7d, 0x64, 0x0c, 0xa2, 0xb5, 0x41, 0x74, 0xb2, 0x36, 0x18, 0x38, 0x17, 0xb5, 0xdb, - 0xba, 0xaa, 0x5d, 0xeb, 0x86, 0x9e, 0x2a, 0x1e, 0x9e, 0xff, 0x70, 0x41, 0xa8, 0x75, 0x2c, 0x01, - 0x9d, 0x72, 0x1c, 0x91, 0x2a, 0x97, 0x53, 0x46, 0x65, 0x9e, 0x44, 0x7a, 0x97, 0x24, 0x49, 0xe6, - 0x6c, 0x5e, 0x10, 0xc9, 0x2b, 0xbb, 0xab, 0x7c, 0x04, 0x48, 0xa9, 0x7d, 0xaf, 0xdd, 0x47, 0x59, - 0x2e, 0xa7, 0xf3, 0x18, 0x25, 0x9c, 0x35, 0xab, 0x68, 0x7e, 0x46, 0x22, 0xfd, 0x84, 0xe5, 0xa2, - 0xa4, 0x02, 0x1d, 0xd3, 0x24, 0xbc, 0x5f, 0x8e, 0x27, 0x1b, 0x51, 0xf5, 0xc2, 0x93, 0x6b, 0x49, - 0xdd, 0xd4, 0xff, 0x67, 0xd3, 0xdd, 0xff, 0x6c, 0xea, 0xdf, 0xda, 0x74, 0xf8, 0x1e, 0xee, 0xbf, - 0xa4, 0x33, 0x2a, 0x72, 0xf1, 0x4e, 0x12, 0x49, 0xad, 0x17, 0xb0, 0xa3, 0xda, 0x0a, 0x1b, 0x0c, - 0xda, 0x5e, 0xef, 0xe8, 0x21, 0xba, 0xf5, 0x64, 0xd1, 0xf5, 0x85, 0x04, 0x7b, 0x5f, 0xbf, 0x8c, - 0x3a, 0x6f, 0xd5, 0x51, 0x84, 0xa6, 0x3c, 0x78, 0x73, 0xb1, 0x74, 0xc0, 0xe5, 0xd2, 0x01, 0x3f, - 0x97, 0x0e, 0x38, 0x5f, 0x39, 0xad, 0xcb, 0x95, 0xd3, 0xfa, 0xb6, 0x72, 0x5a, 0x1f, 0x9e, 0x6d, - 0x8d, 0xdd, 0x88, 0x8f, 0x0a, 0x12, 0x8b, 0x35, 0xc0, 0xa7, 0xfe, 0x18, 0x7f, 0xde, 0xfa, 0x9e, - 0xb4, 0x93, 0xb8, 0xab, 0x97, 0xf9, 0xf4, 0x4f, 0x00, 0x00, 0x00, 0xff, 0xff, 0x6b, 0xb2, 0xc4, - 0x2c, 0x71, 0x03, 0x00, 0x00, + // 546 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x94, 0xcf, 0x6e, 0xd3, 0x40, + 0x10, 0xc6, 0x63, 0xf2, 0xa7, 0x64, 0x53, 0x84, 0x64, 0x55, 0xc2, 0x0d, 0x92, 0x1d, 0x22, 0x81, + 0x82, 0x50, 0xd6, 0x76, 0x11, 0x17, 0x6e, 0x89, 0x2a, 0x10, 0x12, 0x82, 0xca, 0xad, 0x38, 0xc0, + 0xc1, 0x5a, 0x3b, 0x8b, 0x63, 0xe1, 0xcd, 0xae, 0xbc, 0x9b, 0x96, 0xbc, 0x45, 0x9f, 0x81, 0x67, + 0xe0, 0x21, 0x2a, 0x4e, 0x3d, 0x22, 0x0e, 0x06, 0x25, 0x37, 0x8e, 0x7d, 0x02, 0xb4, 0xbb, 0x4e, + 0x9a, 0x80, 0xca, 0x21, 0x27, 0x7b, 0x76, 0xbe, 0xf9, 0x7d, 0x33, 0xab, 0xb1, 0xc1, 0x13, 0xca, + 0x09, 0xe5, 0x29, 0x77, 0x13, 0x44, 0x88, 0x2b, 0xce, 0x10, 0x73, 0x4f, 0xfd, 0x08, 0x0b, 0xe4, + 0xab, 0x20, 0xcc, 0x71, 0x4c, 0xf3, 0x11, 0x64, 0x39, 0x15, 0xd4, 0xdc, 0x2f, 0xc5, 0x50, 0x8a, + 0xa1, 0xcc, 0xc3, 0x52, 0xdc, 0xde, 0x4b, 0x68, 0x42, 0x95, 0xca, 0x95, 0x6f, 0xba, 0xa0, 0xbd, + 0x9f, 0x50, 0x9a, 0x64, 0xd8, 0x55, 0x51, 0x34, 0xfd, 0xe8, 0xa2, 0xc9, 0x6c, 0x99, 0x8a, 0x15, + 0x2c, 0xd4, 0x35, 0x3a, 0x28, 0x53, 0xb6, 0x8e, 0xdc, 0x08, 0x71, 0xbc, 0xea, 0x26, 0xa6, 0xe9, + 0xa4, 0xcc, 0x3b, 0x7f, 0x53, 0x45, 0x4a, 0x30, 0x17, 0x88, 0x30, 0x2d, 0xe8, 0x7e, 0xa9, 0x03, + 0x70, 0x72, 0x86, 0x58, 0xa0, 0x9a, 0x37, 0xef, 0x81, 0x1d, 0x46, 0x69, 0x16, 0xa6, 0x23, 0xcb, + 0xe8, 0x18, 0xbd, 0x5a, 0xd0, 0x90, 0xe1, 0xab, 0x91, 0xf9, 0x00, 0xec, 0x22, 0xce, 0xb1, 0xf0, + 0xc2, 0x11, 0x9e, 0x50, 0x62, 0xdd, 0xea, 0x18, 0xbd, 0x66, 0xd0, 0xd2, 0x67, 0x87, 0xf2, 0x68, + 0x25, 0xf1, 0x4b, 0x49, 0x75, 0x4d, 0xe2, 0x6b, 0xc9, 0x00, 0x34, 0xc6, 0x38, 0x4d, 0xc6, 0xc2, + 0xaa, 0x75, 0x8c, 0x5e, 0x75, 0xf8, 0xf8, 0x77, 0xe1, 0xdc, 0xd1, 0xf7, 0x16, 0xea, 0xc4, 0x55, + 0xe1, 0xec, 0xcd, 0x10, 0xc9, 0x9e, 0x77, 0x37, 0x8e, 0xbb, 0x41, 0x59, 0x68, 0xbe, 0x01, 0x35, + 0x39, 0x83, 0x55, 0xef, 0x18, 0xbd, 0xd6, 0x41, 0x1b, 0xea, 0x01, 0xe1, 0x72, 0x40, 0x78, 0xb2, + 0x1c, 0x70, 0x68, 0x5f, 0x14, 0x4e, 0xe5, 0xaa, 0x70, 0xcc, 0x0d, 0x9e, 0x2c, 0xee, 0x9e, 0xff, + 0x74, 0x8c, 0x40, 0x71, 0xcc, 0x0f, 0xc0, 0x64, 0x5e, 0x98, 0x21, 0x2e, 0x42, 0xce, 0xa8, 0x08, + 0x59, 0x9e, 0xc6, 0xd8, 0x6a, 0xc8, 0xde, 0x87, 0x50, 0x12, 0x7e, 0x14, 0xce, 0xa3, 0x24, 0x15, + 0xe3, 0x69, 0x04, 0x63, 0x4a, 0xca, 0xeb, 0x2f, 0x1f, 0x7d, 0x3e, 0xfa, 0xe4, 0x8a, 0x19, 0xc3, + 0x1c, 0x1e, 0xe2, 0x38, 0xb8, 0xcb, 0xbc, 0xd7, 0x88, 0x8b, 0x63, 0x46, 0xc5, 0x91, 0xc4, 0x28, + 0xb8, 0xff, 0x0f, 0x7c, 0x67, 0x4b, 0xb8, 0xbf, 0x09, 0xe7, 0xc0, 0x66, 0x5e, 0x88, 0xf2, 0x54, + 0x8c, 0x09, 0x16, 0x69, 0x1c, 0xaa, 0x2d, 0x44, 0x71, 0x3c, 0x25, 0xd3, 0x0c, 0x09, 0x9a, 0x5b, + 0xb7, 0xb7, 0x32, 0xba, 0xcf, 0xbc, 0xc1, 0x0a, 0x2a, 0x77, 0x63, 0x70, 0x8d, 0x54, 0xa6, 0xfe, + 0x7f, 0x4d, 0x9b, 0x5b, 0x9a, 0xfa, 0x37, 0x9a, 0x76, 0xdf, 0x81, 0xdd, 0x97, 0x78, 0x82, 0x79, + 0xca, 0x8f, 0x05, 0x12, 0xd8, 0x7c, 0x01, 0xea, 0xd2, 0x96, 0x5b, 0x46, 0xa7, 0xda, 0x6b, 0x1d, + 0x3c, 0x84, 0x37, 0x7e, 0x6c, 0xf0, 0x7a, 0xb7, 0x87, 0xcd, 0x6f, 0x5f, 0xfb, 0xf5, 0x23, 0xb9, + 0xce, 0x81, 0x2e, 0x1f, 0xbe, 0xbd, 0x98, 0xdb, 0xc6, 0xe5, 0xdc, 0x36, 0x7e, 0xcd, 0x6d, 0xe3, + 0x7c, 0x61, 0x57, 0x2e, 0x17, 0x76, 0xe5, 0xfb, 0xc2, 0xae, 0xbc, 0x7f, 0xb6, 0xd6, 0x76, 0x09, + 0xef, 0x67, 0x28, 0xe2, 0xcb, 0xc0, 0x3d, 0xf5, 0x3d, 0xf7, 0xf3, 0xda, 0x9f, 0x40, 0x4d, 0x12, + 0x35, 0xd4, 0x1a, 0x3e, 0xfd, 0x13, 0x00, 0x00, 0xff, 0xff, 0x52, 0x5b, 0x8e, 0x25, 0x2b, 0x04, + 0x00, 0x00, } func (m *TwapRecord) Marshal() (dAtA []byte, err error) { @@ -240,7 +245,7 @@ func (m *TwapRecord) MarshalToSizedBuffer(dAtA []byte) (int, error) { i = encodeVarintTwapRecord(dAtA, i, uint64(size)) } i-- - dAtA[i] = 0x3a + dAtA[i] = 0x4a { size := m.P0ArithmeticTwapAccumulator.Size() i -= size @@ -250,6 +255,26 @@ func (m *TwapRecord) MarshalToSizedBuffer(dAtA []byte) (int, error) { i = encodeVarintTwapRecord(dAtA, i, uint64(size)) } i-- + dAtA[i] = 0x42 + { + size := m.P1LastSpotPrice.Size() + i -= size + if _, err := m.P1LastSpotPrice.MarshalTo(dAtA[i:]); err != nil { + return 0, err + } + i = encodeVarintTwapRecord(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x3a + { + size := m.P0LastSpotPrice.Size() + i -= size + if _, err := m.P0LastSpotPrice.MarshalTo(dAtA[i:]); err != nil { + return 0, err + } + i = encodeVarintTwapRecord(dAtA, i, uint64(size)) + } + i-- dAtA[i] = 0x32 n1, err1 := github_com_gogo_protobuf_types.StdTimeMarshalTo(m.Time, dAtA[i-github_com_gogo_protobuf_types.SizeOfStdTime(m.Time):]) if err1 != nil { @@ -356,6 +381,10 @@ func (m *TwapRecord) Size() (n int) { } l = github_com_gogo_protobuf_types.SizeOfStdTime(m.Time) n += 1 + l + sovTwapRecord(uint64(l)) + l = m.P0LastSpotPrice.Size() + n += 1 + l + sovTwapRecord(uint64(l)) + l = m.P1LastSpotPrice.Size() + n += 1 + l + sovTwapRecord(uint64(l)) l = m.P0ArithmeticTwapAccumulator.Size() n += 1 + l + sovTwapRecord(uint64(l)) l = m.P1ArithmeticTwapAccumulator.Size() @@ -549,6 +578,74 @@ func (m *TwapRecord) Unmarshal(dAtA []byte) error { } iNdEx = postIndex case 6: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field P0LastSpotPrice", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTwapRecord + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthTwapRecord + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthTwapRecord + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := m.P0LastSpotPrice.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 7: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field P1LastSpotPrice", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTwapRecord + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthTwapRecord + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthTwapRecord + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := m.P1LastSpotPrice.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 8: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field P0ArithmeticTwapAccumulator", wireType) } @@ -582,7 +679,7 @@ func (m *TwapRecord) Unmarshal(dAtA []byte) error { return err } iNdEx = postIndex - case 7: + case 9: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field P1ArithmeticTwapAccumulator", wireType) } diff --git a/x/gamm/twap/types/utils.go b/x/gamm/twap/types/utils.go index f7d6274fd49..43f31873cf1 100644 --- a/x/gamm/twap/types/utils.go +++ b/x/gamm/twap/types/utils.go @@ -6,21 +6,35 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ) -func NewTwapRecord(ctx sdk.Context, poolId uint64, denom0 string, denom1 string) TwapRecord { +func NewTwapRecord(k AmmInterface, ctx sdk.Context, poolId uint64, denom0 string, denom1 string) TwapRecord { if !(denom0 > denom1) { panic("precondition denom0 > denom1 not satisfied") } + sp0 := MustGetSpotPrice(k, ctx, poolId, denom0, denom1) + sp1 := MustGetSpotPrice(k, ctx, poolId, denom1, denom0) return TwapRecord{ PoolId: poolId, Asset0Denom: denom0, Asset1Denom: denom1, Height: ctx.BlockHeight(), Time: ctx.BlockTime(), + P0LastSpotPrice: sp0, + P1LastSpotPrice: sp1, P0ArithmeticTwapAccumulator: sdk.ZeroDec(), P1ArithmeticTwapAccumulator: sdk.ZeroDec(), } } +// mustGetSpotPrice returns the spot price for the given pool id, and denom0 in terms of denom1. +// Panics if the pool state is misconfigured, which will halt any tx that interacts with this. +func MustGetSpotPrice(k AmmInterface, ctx sdk.Context, poolId uint64, denom0 string, denom1 string) sdk.Dec { + sp, err := k.CalculateSpotPrice(ctx, poolId, denom0, denom1) + if err != nil { + panic(err) + } + return sp +} + // GetAllUniqueDenomPairs returns all unique pairs of denoms, where for every pair // (X, Y), X >= Y. // The pair (X,Y) should only appear once in the list From 840c799d82895fd7c99fb2e60e06e034f4bf2822 Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Wed, 20 Jul 2022 11:24:23 -0500 Subject: [PATCH 08/72] Do app integration --- app/keepers/keepers.go | 11 +++++++++++ app/keepers/keys.go | 4 +++- app/keepers/modules.go | 2 ++ app/modules.go | 4 ++++ x/gamm/twap/api.go | 6 +++--- x/gamm/twap/hook_listener.go | 8 ++++++-- x/gamm/twap/keeper.go | 14 +++++++++++--- x/gamm/twap/logic.go | 14 +++++++------- x/gamm/twap/module.go | 25 +++++++------------------ x/gamm/twap/store.go | 20 ++++++++++---------- x/gamm/twap/types/utils.go | 5 +++++ 11 files changed, 69 insertions(+), 44 deletions(-) diff --git a/app/keepers/keepers.go b/app/keepers/keepers.go index 90121bd8fd4..aeecb95a0e5 100644 --- a/app/keepers/keepers.go +++ b/app/keepers/keepers.go @@ -52,6 +52,8 @@ import ( epochskeeper "github.com/osmosis-labs/osmosis/v10/x/epochs/keeper" epochstypes "github.com/osmosis-labs/osmosis/v10/x/epochs/types" gammkeeper "github.com/osmosis-labs/osmosis/v10/x/gamm/keeper" + "github.com/osmosis-labs/osmosis/v10/x/gamm/twap" + twaptypes "github.com/osmosis-labs/osmosis/v10/x/gamm/twap/types" gammtypes "github.com/osmosis-labs/osmosis/v10/x/gamm/types" incentiveskeeper "github.com/osmosis-labs/osmosis/v10/x/incentives/keeper" incentivestypes "github.com/osmosis-labs/osmosis/v10/x/incentives/types" @@ -98,6 +100,7 @@ type AppKeepers struct { TransferKeeper *ibctransferkeeper.Keeper EvidenceKeeper *evidencekeeper.Keeper GAMMKeeper *gammkeeper.Keeper + TwapKeeper *twap.Keeper LockupKeeper *lockupkeeper.Keeper EpochsKeeper *epochskeeper.Keeper IncentivesKeeper *incentiveskeeper.Keeper @@ -241,6 +244,12 @@ func (appKeepers *AppKeepers) InitNormalKeepers( appKeepers.AccountKeeper, appKeepers.BankKeeper, appKeepers.DistrKeeper) appKeepers.GAMMKeeper = &gammKeeper + appKeepers.TwapKeeper = twap.NewKeeper( + appKeepers.keys[twaptypes.StoreKey], + appKeepers.tkeys[twaptypes.TransientStoreKey], + appKeepers.GetSubspace(twaptypes.ModuleName), + appKeepers.GAMMKeeper) + appKeepers.LockupKeeper = lockupkeeper.NewKeeper( appCodec, appKeepers.keys[lockuptypes.StoreKey], @@ -454,6 +463,7 @@ func (appKeepers *AppKeepers) SetupHooks() { gammtypes.NewMultiGammHooks( // insert gamm hooks receivers here appKeepers.PoolIncentivesKeeper.Hooks(), + appKeepers.TwapKeeper.GammHooks(), ), ) @@ -512,6 +522,7 @@ func KVStoreKeys() []string { ibctransfertypes.StoreKey, capabilitytypes.StoreKey, gammtypes.StoreKey, + twaptypes.StoreKey, lockuptypes.StoreKey, incentivestypes.StoreKey, epochstypes.StoreKey, diff --git a/app/keepers/keys.go b/app/keepers/keys.go index 350f50a7279..c3dc16a6bd8 100644 --- a/app/keepers/keys.go +++ b/app/keepers/keys.go @@ -4,6 +4,8 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types" paramstypes "github.com/cosmos/cosmos-sdk/x/params/types" + + twaptypes "github.com/osmosis-labs/osmosis/v10/x/gamm/twap/types" ) // GenerateKeys generates new keys (KV Store, Transient store, and memory store). @@ -13,7 +15,7 @@ func (appKeepers *AppKeepers) GenerateKeys() { appKeepers.keys = sdk.NewKVStoreKeys(KVStoreKeys()...) // Define transient store keys - appKeepers.tkeys = sdk.NewTransientStoreKeys(paramstypes.TStoreKey) + appKeepers.tkeys = sdk.NewTransientStoreKeys(paramstypes.TStoreKey, twaptypes.TransientStoreKey) // MemKeys are for information that is stored only in RAM. appKeepers.memKeys = sdk.NewMemoryStoreKeys(capabilitytypes.MemStoreKey) diff --git a/app/keepers/modules.go b/app/keepers/modules.go index cf6555b3252..07218549e7b 100644 --- a/app/keepers/modules.go +++ b/app/keepers/modules.go @@ -30,6 +30,7 @@ import ( _ "github.com/osmosis-labs/osmosis/v10/client/docs/statik" "github.com/osmosis-labs/osmosis/v10/x/epochs" "github.com/osmosis-labs/osmosis/v10/x/gamm" + "github.com/osmosis-labs/osmosis/v10/x/gamm/twap" "github.com/osmosis-labs/osmosis/v10/x/incentives" "github.com/osmosis-labs/osmosis/v10/x/lockup" "github.com/osmosis-labs/osmosis/v10/x/mint" @@ -74,6 +75,7 @@ var AppModuleBasics = []module.AppModuleBasic{ transfer.AppModuleBasic{}, vesting.AppModuleBasic{}, gamm.AppModuleBasic{}, + twap.AppModuleBasic{}, txfees.AppModuleBasic{}, incentives.AppModuleBasic{}, lockup.AppModuleBasic{}, diff --git a/app/modules.go b/app/modules.go index ba9e72149ea..740fd36bb24 100644 --- a/app/modules.go +++ b/app/modules.go @@ -47,6 +47,8 @@ import ( "github.com/osmosis-labs/osmosis/v10/x/epochs" epochstypes "github.com/osmosis-labs/osmosis/v10/x/epochs/types" "github.com/osmosis-labs/osmosis/v10/x/gamm" + "github.com/osmosis-labs/osmosis/v10/x/gamm/twap" + twaptypes "github.com/osmosis-labs/osmosis/v10/x/gamm/twap/types" gammtypes "github.com/osmosis-labs/osmosis/v10/x/gamm/types" "github.com/osmosis-labs/osmosis/v10/x/incentives" incentivestypes "github.com/osmosis-labs/osmosis/v10/x/incentives/types" @@ -121,6 +123,7 @@ func appModules( params.NewAppModule(*app.ParamsKeeper), app.TransferModule, gamm.NewAppModule(appCodec, *app.GAMMKeeper, app.AccountKeeper, app.BankKeeper), + twap.NewAppModule(*app.TwapKeeper), txfees.NewAppModule(appCodec, *app.TxFeesKeeper), incentives.NewAppModule(appCodec, *app.IncentivesKeeper, app.AccountKeeper, app.BankKeeper, app.EpochsKeeper), lockup.NewAppModule(appCodec, *app.LockupKeeper, app.AccountKeeper, app.BankKeeper), @@ -190,6 +193,7 @@ func OrderInitGenesis(allModuleNames []string) []string { ibchost.ModuleName, icatypes.ModuleName, gammtypes.ModuleName, + twaptypes.ModuleName, txfeestypes.ModuleName, genutiltypes.ModuleName, evidencetypes.ModuleName, diff --git a/x/gamm/twap/api.go b/x/gamm/twap/api.go index 9ee8b73913c..81c5baec719 100644 --- a/x/gamm/twap/api.go +++ b/x/gamm/twap/api.go @@ -14,7 +14,7 @@ import ( // this function will interpolate between startTime. // if endTime = now, we do {X} // startTime must be in time range {X}, recommended parameterization for mainnet is {Y} -func (k twapkeeper) GetArithmeticTwap( +func (k Keeper) GetArithmeticTwap( ctx sdk.Context, poolId uint64, quoteAssetDenom string, @@ -24,7 +24,7 @@ func (k twapkeeper) GetArithmeticTwap( return sdk.Dec{}, nil } -func (k twapkeeper) GetArithmeticTwapToNow( +func (k Keeper) GetArithmeticTwapToNow( ctx sdk.Context, poolId uint64, quoteAssetDenom string, @@ -34,7 +34,7 @@ func (k twapkeeper) GetArithmeticTwapToNow( } // GetLatestAccumulatorRecord returns a TwapRecord struct that can be stored -func (k twapkeeper) GetLatestAccumulatorRecord(ctx sdk.Context, poolId uint64, asset0Denom string, asset1Denom string) (types.TwapRecord, error) { +func (k Keeper) GetLatestAccumulatorRecord(ctx sdk.Context, poolId uint64, asset0Denom string, asset1Denom string) (types.TwapRecord, error) { // correct ordering of args for db if asset1Denom > asset0Denom { asset0Denom, asset1Denom = asset1Denom, asset0Denom diff --git a/x/gamm/twap/hook_listener.go b/x/gamm/twap/hook_listener.go index f88ab8100dd..5605871df84 100644 --- a/x/gamm/twap/hook_listener.go +++ b/x/gamm/twap/hook_listener.go @@ -11,7 +11,7 @@ var _ types.GammHooks = &gammhook{} var _ epochtypes.EpochHooks = &epochhook{} type epochhook struct { - k twapkeeper + k Keeper } func (hook *epochhook) AfterEpochEnd(ctx sdk.Context, epochIdentifier string, epochNumber int64) { @@ -24,7 +24,11 @@ func (hook *epochhook) AfterEpochEnd(ctx sdk.Context, epochIdentifier string, ep func (hook *epochhook) BeforeEpochStart(ctx sdk.Context, epochIdentifier string, epochNumber int64) {} type gammhook struct { - k twapkeeper + k Keeper +} + +func (k Keeper) GammHooks() types.GammHooks { + return &gammhook{k} } // AfterPoolCreated is called after CreatePool diff --git a/x/gamm/twap/keeper.go b/x/gamm/twap/keeper.go index 6a96a6e72ef..dc5bfce292d 100644 --- a/x/gamm/twap/keeper.go +++ b/x/gamm/twap/keeper.go @@ -3,12 +3,20 @@ package twap import ( sdk "github.com/cosmos/cosmos-sdk/types" + paramtypes "github.com/cosmos/cosmos-sdk/x/params/types" + "github.com/osmosis-labs/osmosis/v10/x/gamm/twap/types" ) -type twapkeeper struct { +type Keeper struct { storeKey sdk.StoreKey - transientKey sdk.TransientStoreKey + transientKey *sdk.TransientStoreKey + + paramSpace paramtypes.Subspace + + ammkeeper types.AmmInterface +} - gammkeeper types.AmmInterface +func NewKeeper(storeKey sdk.StoreKey, transientKey *sdk.TransientStoreKey, paramSpace paramtypes.Subspace, ammKeeper types.AmmInterface) *Keeper { + return &Keeper{storeKey: storeKey, transientKey: transientKey, paramSpace: paramSpace, ammkeeper: ammKeeper} } diff --git a/x/gamm/twap/logic.go b/x/gamm/twap/logic.go index bd1a5dc180a..c3f3913651f 100644 --- a/x/gamm/twap/logic.go +++ b/x/gamm/twap/logic.go @@ -6,17 +6,17 @@ import ( "github.com/osmosis-labs/osmosis/v10/x/gamm/twap/types" ) -func (k twapkeeper) afterCreatePool(ctx sdk.Context, poolId uint64) error { - denoms, err := k.gammkeeper.GetPoolDenoms(ctx, poolId) +func (k Keeper) afterCreatePool(ctx sdk.Context, poolId uint64) error { + denoms, err := k.ammkeeper.GetPoolDenoms(ctx, poolId) denomPairs0, denomPairs1 := types.GetAllUniqueDenomPairs(denoms) for i := 0; i < len(denomPairs0); i++ { - record := types.NewTwapRecord(k.gammkeeper, ctx, poolId, denomPairs0[i], denomPairs1[i]) + record := types.NewTwapRecord(k.ammkeeper, ctx, poolId, denomPairs0[i], denomPairs1[i]) k.storeMostRecentTWAP(ctx, record) } return err } -func (k twapkeeper) updateTwapIfNotRedundant(ctx sdk.Context, poolId uint64) error { +func (k Keeper) updateTwapIfNotRedundant(ctx sdk.Context, poolId uint64) error { if k.hasPoolChangedThisBlock(ctx, poolId) { return nil } @@ -28,7 +28,7 @@ func (k twapkeeper) updateTwapIfNotRedundant(ctx sdk.Context, poolId uint64) err return nil } -func (k twapkeeper) updateTWAPs(ctx sdk.Context, poolId uint64) error { +func (k Keeper) updateTWAPs(ctx sdk.Context, poolId uint64) error { // Will only err if pool doesn't have most recent entry set twaps, err := k.getAllMostRecentTWAPsForPool(ctx, poolId) if err != nil { @@ -49,8 +49,8 @@ func (k twapkeeper) updateTWAPs(ctx sdk.Context, poolId uint64) error { record.Time = ctx.BlockTime() // TODO: Ensure order is correct - sp0 := types.MustGetSpotPrice(k.gammkeeper, ctx, poolId, record.Asset0Denom, record.Asset1Denom) - sp1 := types.MustGetSpotPrice(k.gammkeeper, ctx, poolId, record.Asset1Denom, record.Asset0Denom) + sp0 := types.MustGetSpotPrice(k.ammkeeper, ctx, poolId, record.Asset0Denom, record.Asset1Denom) + sp1 := types.MustGetSpotPrice(k.ammkeeper, ctx, poolId, record.Asset1Denom, record.Asset0Denom) // TODO: Think about overflow record.P0ArithmeticTwapAccumulator.AddMut(sp0.MulInt64(int64(timeDelta))) diff --git a/x/gamm/twap/module.go b/x/gamm/twap/module.go index 3487de09621..1102e2f9795 100644 --- a/x/gamm/twap/module.go +++ b/x/gamm/twap/module.go @@ -1,7 +1,6 @@ package twap import ( - "context" "encoding/json" "fmt" @@ -16,9 +15,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" - "github.com/osmosis-labs/osmosis/v10/x/gamm/keeper" - twaptypes "github.com/osmosis-labs/osmosis/v10/x/gamm/twap/types" - "github.com/osmosis-labs/osmosis/v10/x/gamm/types" + "github.com/osmosis-labs/osmosis/v10/x/gamm/twap/types" ) var ( @@ -27,10 +24,9 @@ var ( ) type AppModuleBasic struct { - cdc codec.Codec } -func (AppModuleBasic) Name() string { return twaptypes.ModuleName } +func (AppModuleBasic) Name() string { return types.ModuleName } func (AppModuleBasic) RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) { } @@ -55,7 +51,7 @@ func (b AppModuleBasic) RegisterRESTRoutes(ctx client.Context, r *mux.Router) { } func (b AppModuleBasic) RegisterGRPCGatewayRoutes(clientCtx client.Context, mux *runtime.ServeMux) { - types.RegisterQueryHandlerClient(context.Background(), mux, types.NewQueryClient(clientCtx)) //nolint:errcheck + // types.RegisterQueryHandlerClient(context.Background(), mux, types.NewQueryClient(clientCtx)) //nolint:errcheck } func (b AppModuleBasic) GetTxCmd() *cobra.Command { @@ -75,23 +71,16 @@ func (AppModuleBasic) RegisterInterfaces(registry codectypes.InterfaceRegistry) type AppModule struct { AppModuleBasic - ak types.AccountKeeper - bk types.BankKeeper - gk keeper.Keeper - tk twapkeeper + k Keeper } func (am AppModule) RegisterServices(cfg module.Configurator) { } -func NewAppModule(cdc codec.Codec, keeper keeper.Keeper, - accountKeeper types.AccountKeeper, bankKeeper types.BankKeeper, -) AppModule { +func NewAppModule(twapKeeper Keeper) AppModule { return AppModule{ - AppModuleBasic: AppModuleBasic{cdc: cdc}, - gk: keeper, - ak: accountKeeper, - bk: bankKeeper, + AppModuleBasic: AppModuleBasic{}, + k: twapKeeper, } } diff --git a/x/gamm/twap/store.go b/x/gamm/twap/store.go index 89b2b18aa90..97a591971c6 100644 --- a/x/gamm/twap/store.go +++ b/x/gamm/twap/store.go @@ -11,8 +11,8 @@ import ( "github.com/osmosis-labs/osmosis/v10/x/gamm/twap/types" ) -func (k twapkeeper) trackChangedPool(ctx sdk.Context, poolId uint64) { - store := ctx.TransientStore(&k.transientKey) +func (k Keeper) trackChangedPool(ctx sdk.Context, poolId uint64) { + store := ctx.TransientStore(k.transientKey) poolIdBz := make([]byte, 8) binary.LittleEndian.PutUint64(poolIdBz, poolId) // just has to not be empty, for store to work / not register as a delete. @@ -20,14 +20,14 @@ func (k twapkeeper) trackChangedPool(ctx sdk.Context, poolId uint64) { store.Set(poolIdBz, sentinelExistsValue) } -func (k twapkeeper) hasPoolChangedThisBlock(ctx sdk.Context, poolId uint64) bool { - store := ctx.TransientStore(&k.transientKey) +func (k Keeper) hasPoolChangedThisBlock(ctx sdk.Context, poolId uint64) bool { + store := ctx.TransientStore(k.transientKey) poolIdBz := make([]byte, 8) binary.LittleEndian.PutUint64(poolIdBz, poolId) return store.Has(poolIdBz) } -func (k twapkeeper) storeHistoricalTWAP(ctx sdk.Context, twap types.TwapRecord) { +func (k Keeper) storeHistoricalTWAP(ctx sdk.Context, twap types.TwapRecord) { store := ctx.KVStore(k.storeKey) key1 := types.FormatHistoricalTimeIndexTWAPKey(twap.Time, twap.PoolId, twap.Asset0Denom, twap.Asset1Denom) key2 := types.FormatHistoricalPoolIndexTWAPKey(twap.PoolId, twap.Time, twap.Asset0Denom, twap.Asset1Denom) @@ -35,7 +35,7 @@ func (k twapkeeper) storeHistoricalTWAP(ctx sdk.Context, twap types.TwapRecord) osmoutils.MustSet(store, key2, &twap) } -func (k twapkeeper) deleteHistoricalTWAP(ctx sdk.Context, twap types.TwapRecord) { +func (k Keeper) deleteHistoricalTWAP(ctx sdk.Context, twap types.TwapRecord) { store := ctx.KVStore(k.storeKey) key1 := types.FormatHistoricalTimeIndexTWAPKey(twap.Time, twap.PoolId, twap.Asset0Denom, twap.Asset1Denom) key2 := types.FormatHistoricalPoolIndexTWAPKey(twap.PoolId, twap.Time, twap.Asset0Denom, twap.Asset1Denom) @@ -43,19 +43,19 @@ func (k twapkeeper) deleteHistoricalTWAP(ctx sdk.Context, twap types.TwapRecord) store.Delete(key2) } -func (k twapkeeper) getMostRecentTWAP(ctx sdk.Context, poolId uint64, asset0Denom string, asset1Denom string) (types.TwapRecord, error) { +func (k Keeper) getMostRecentTWAP(ctx sdk.Context, poolId uint64, asset0Denom string, asset1Denom string) (types.TwapRecord, error) { store := ctx.KVStore(k.storeKey) key := types.FormatMostRecentTWAPKey(poolId, asset0Denom, asset1Denom) bz := store.Get(key) return types.ParseTwapFromBz(bz) } -func (k twapkeeper) getAllMostRecentTWAPsForPool(ctx sdk.Context, poolId uint64) ([]types.TwapRecord, error) { +func (k Keeper) getAllMostRecentTWAPsForPool(ctx sdk.Context, poolId uint64) ([]types.TwapRecord, error) { store := ctx.KVStore(k.storeKey) return types.GetAllMostRecentTwapsForPool(store, poolId) } -func (k twapkeeper) storeMostRecentTWAP(ctx sdk.Context, twap types.TwapRecord) { +func (k Keeper) storeMostRecentTWAP(ctx sdk.Context, twap types.TwapRecord) { store := ctx.KVStore(k.storeKey) key := types.FormatMostRecentTWAPKey(twap.PoolId, twap.Asset0Denom, twap.Asset1Denom) osmoutils.MustSet(store, key, &twap) @@ -63,7 +63,7 @@ func (k twapkeeper) storeMostRecentTWAP(ctx sdk.Context, twap types.TwapRecord) // returns an error if theres no historical record at or before time. // (Asking for a time too far back) -func (k twapkeeper) getTwapBeforeTime(ctx sdk.Context, poolId uint64, time time.Time, asset0Denom string, asset1Denom string) (types.TwapRecord, error) { +func (k Keeper) getTwapBeforeTime(ctx sdk.Context, poolId uint64, time time.Time, asset0Denom string, asset1Denom string) (types.TwapRecord, error) { store := ctx.KVStore(k.storeKey) startKey := types.FormatHistoricalPoolIndexTimePrefix(poolId, time) // TODO: Optimize to cut down search on asset0Denom, asset1denom. diff --git a/x/gamm/twap/types/utils.go b/x/gamm/twap/types/utils.go index 43f31873cf1..77b05c47f57 100644 --- a/x/gamm/twap/types/utils.go +++ b/x/gamm/twap/types/utils.go @@ -60,3 +60,8 @@ func GetAllUniqueDenomPairs(denoms []string) ([]string, []string) { } return pairGT, pairLT } + +// TODO +func (g *GenesisState) Validate() error { + return nil +} From 4a650f19eeb00df704b8fc09f608747a2f1368b3 Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Wed, 20 Jul 2022 11:42:12 -0500 Subject: [PATCH 09/72] Fix simulator & bug it found --- osmoutils/slice_helper.go | 9 +++++++++ x/gamm/twap/module.go | 3 +-- x/gamm/twap/types/utils.go | 17 ++++++++++++----- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/osmoutils/slice_helper.go b/osmoutils/slice_helper.go index f2f0c3b202a..68134e66e64 100644 --- a/osmoutils/slice_helper.go +++ b/osmoutils/slice_helper.go @@ -23,3 +23,12 @@ func Filter[T interface{}](filter func(T) bool, s []T) []T { } return filteredSlice } + +func ReverseSlice[T any](s []T) []T { + newSlice := make([]T, len(s)) + maxIndex := len(s) - 1 + for i := 0; i < len(s); i++ { + newSlice[maxIndex-i] = s[i] + } + return newSlice +} diff --git a/x/gamm/twap/module.go b/x/gamm/twap/module.go index 1102e2f9795..cf31ac723a8 100644 --- a/x/gamm/twap/module.go +++ b/x/gamm/twap/module.go @@ -32,8 +32,7 @@ func (AppModuleBasic) RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) { } func (AppModuleBasic) DefaultGenesis(cdc codec.JSONCodec) json.RawMessage { - return json.RawMessage{} - // return cdc.MustMarshalJSON(types.DefaultGenesis()) + return cdc.MustMarshalJSON(&types.GenesisState{}) } // ValidateGenesis performs genesis state validation for the gamm module. diff --git a/x/gamm/twap/types/utils.go b/x/gamm/twap/types/utils.go index 77b05c47f57..3b5631b00c7 100644 --- a/x/gamm/twap/types/utils.go +++ b/x/gamm/twap/types/utils.go @@ -1,14 +1,17 @@ package types import ( + fmt "fmt" "sort" sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/osmosis-labs/osmosis/v10/osmoutils" ) func NewTwapRecord(k AmmInterface, ctx sdk.Context, poolId uint64, denom0 string, denom1 string) TwapRecord { if !(denom0 > denom1) { - panic("precondition denom0 > denom1 not satisfied") + panic(fmt.Sprintf("precondition denom0 > denom1 not satisfied. denom0 %s | denom1 %s", denom0, denom1)) } sp0 := MustGetSpotPrice(k, ctx, poolId, denom0, denom1) sp1 := MustGetSpotPrice(k, ctx, poolId, denom1, denom0) @@ -42,14 +45,18 @@ func MustGetSpotPrice(k AmmInterface, ctx sdk.Context, poolId uint64, denom0 str // NOTE: Sorts the input denoms slice. // (Should not be a problem, as this should come from coins.Denoms(), which returns a sorted order) func GetAllUniqueDenomPairs(denoms []string) ([]string, []string) { + // get denoms in descending order sort.Strings(denoms) + reverseDenoms := osmoutils.ReverseSlice(denoms) + numPairs := len(denoms) * (len(denoms) - 1) / 2 pairGT := make([]string, 0, numPairs) pairLT := make([]string, 0, numPairs) - for i := 0; i < len(denoms); i++ { - for j := i + 1; j < len(denoms); j++ { - pairGT = append(pairGT, denoms[i]) - pairLT = append(pairLT, denoms[j]) + + for i := 0; i < len(reverseDenoms); i++ { + for j := i + 1; j < len(reverseDenoms); j++ { + pairGT = append(pairGT, reverseDenoms[i]) + pairLT = append(pairLT, reverseDenoms[j]) } } // sanity check From 16a0ce4e9634697cf956f2b8c54c19d843b31666 Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Wed, 20 Jul 2022 12:56:21 -0500 Subject: [PATCH 10/72] More logic added / data flow reordered a bit --- .../gamm/twap/v1beta1/twap_record.proto | 4 +- x/gamm/twap/README.md | 17 ++++++- x/gamm/twap/api.go | 15 ++++++- x/gamm/twap/logic.go | 44 +++++++++++++++++-- x/gamm/twap/store.go | 4 +- x/gamm/twap/types/twap_record.pb.go | 4 +- x/gamm/twap/types/utils.go | 9 ++++ 7 files changed, 87 insertions(+), 10 deletions(-) diff --git a/proto/osmosis/gamm/twap/v1beta1/twap_record.proto b/proto/osmosis/gamm/twap/v1beta1/twap_record.proto index 8cf6c919db1..c44266c721b 100644 --- a/proto/osmosis/gamm/twap/v1beta1/twap_record.proto +++ b/proto/osmosis/gamm/twap/v1beta1/twap_record.proto @@ -35,8 +35,8 @@ message TwapRecord { (gogoproto.moretags) = "yaml:\"record_time\"" ]; - // We store the last spot prices in the struct, so that we can interpolate accumulator values - // for times between when accumulator records are stored. + // We store the last spot prices in the struct, so that we can interpolate + // accumulator values for times between when accumulator records are stored. string p0_last_spot_price = 6 [ (gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec", (gogoproto.nullable) = false diff --git a/x/gamm/twap/README.md b/x/gamm/twap/README.md index 6afca121295..317977c2e3a 100644 --- a/x/gamm/twap/README.md +++ b/x/gamm/twap/README.md @@ -2,8 +2,21 @@ We maintain TWAP entries for every gamm pool. -NOTE: Not yet integrated into state machine, this package is a stub. +## Module API + +## File layout + +**api.go** is the main file you should look at for what you should depend upon. + +**logic.go** is the main file you should look at for how the TWAP implementation works. + +- types/* - Implement TwapRecord, GenesisState. Define AMM interface, and methods to format keys. +- api.go - Public API, that other users / modules can/should depend on +- hook_listener.go - Defines hooks & calls to logic.go, for triggering actions on +- keeper.go - generic SDK boilerplate (defining a wrapper for store keys + params) +- logic.go - Implements all TWAP module 'logic'. (Arithmetic, defining what to get/set where, etc.) +- module.go - SDK AppModule interface implementation. +- store.go - Managing logic for getting and setting things to underlying stores ## Basic architecture notes -We maintain the list of pools altered within in a block \ No newline at end of file diff --git a/x/gamm/twap/api.go b/x/gamm/twap/api.go index 81c5baec719..2bbb5149b9e 100644 --- a/x/gamm/twap/api.go +++ b/x/gamm/twap/api.go @@ -21,6 +21,10 @@ func (k Keeper) GetArithmeticTwap( baseAssetDenom string, startTime time.Time, endTime time.Time) (sdk.Dec, error) { + if endTime.Equal(ctx.BlockTime()) { + return k.GetArithmeticTwapToNow(ctx, poolId, quoteAssetDenom, baseAssetDenom, startTime) + } + // startTwapRecord, err := k.getTwapBeforeTime(ctx, poolId, startTime, quoteAssetDenom, baseAssetDenom) return sdk.Dec{}, nil } @@ -30,7 +34,16 @@ func (k Keeper) GetArithmeticTwapToNow( quoteAssetDenom string, baseAssetDenom string, startTime time.Time) (sdk.Dec, error) { - return k.GetArithmeticTwap(ctx, poolId, quoteAssetDenom, baseAssetDenom, startTime, ctx.BlockTime()) + startRecord, err := k.getStartRecord(ctx, poolId, startTime, quoteAssetDenom, baseAssetDenom) + if err != nil { + return sdk.Dec{}, err + } + endRecord, err := k.GetLatestAccumulatorRecord(ctx, poolId, quoteAssetDenom, baseAssetDenom) + if err != nil { + return sdk.Dec{}, err + } + twap := k.getArithmeticTwap(startRecord, endRecord) + return twap, nil } // GetLatestAccumulatorRecord returns a TwapRecord struct that can be stored diff --git a/x/gamm/twap/logic.go b/x/gamm/twap/logic.go index c3f3913651f..3d40801fe68 100644 --- a/x/gamm/twap/logic.go +++ b/x/gamm/twap/logic.go @@ -1,6 +1,8 @@ package twap import ( + "time" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/osmosis-labs/osmosis/v10/x/gamm/twap/types" @@ -36,7 +38,6 @@ func (k Keeper) updateTWAPs(ctx sdk.Context, poolId uint64) error { } for _, record := range twaps { - k.storeHistoricalTWAP(ctx, record) timeDelta := ctx.BlockTime().Sub(record.Time) // no update if were in the same block. @@ -53,9 +54,46 @@ func (k Keeper) updateTWAPs(ctx sdk.Context, poolId uint64) error { sp1 := types.MustGetSpotPrice(k.ammkeeper, ctx, poolId, record.Asset1Denom, record.Asset0Denom) // TODO: Think about overflow - record.P0ArithmeticTwapAccumulator.AddMut(sp0.MulInt64(int64(timeDelta))) - record.P1ArithmeticTwapAccumulator.AddMut(sp1.MulInt64(int64(timeDelta))) + record.P0ArithmeticTwapAccumulator.AddMut(types.SpotPriceTimesDuration(sp0, timeDelta)) + record.P1ArithmeticTwapAccumulator.AddMut(types.SpotPriceTimesDuration(sp1, timeDelta)) k.storeMostRecentTWAP(ctx, record) } return nil } + +func (k Keeper) getStartRecord(ctx sdk.Context, poolId uint64, time time.Time, assetA string, assetB string) (types.TwapRecord, error) { + if !(assetA > assetB) { + assetA, assetB = assetB, assetA + } + record, err := k.getRecordAtOrBeforeTime(ctx, poolId, time, assetA, assetB) + if err != nil { + return types.TwapRecord{}, err + } + record = interpolateRecord(record, time) + return record, nil +} + +// pre-condition: interpolateTime >= record.Time +func interpolateRecord(record types.TwapRecord, interpolateTime time.Time) types.TwapRecord { + if record.Time.Equal(interpolateTime) { + return record + } + interpolatedRecord := record + timeDelta := interpolateTime.Sub(record.Time) + interpolatedRecord.Time = interpolateTime + + interpolatedRecord.P0ArithmeticTwapAccumulator = interpolatedRecord.P0ArithmeticTwapAccumulator.Add( + types.SpotPriceTimesDuration(record.P0LastSpotPrice, timeDelta)) + interpolatedRecord.P1ArithmeticTwapAccumulator = interpolatedRecord.P1ArithmeticTwapAccumulator.Add( + types.SpotPriceTimesDuration(record.P1LastSpotPrice, timeDelta)) + + return interpolatedRecord +} + +// For now just assuming p0 price, TODO switch between the two +// precondition: endRecord.Time > startRecord.Time +func (k Keeper) getArithmeticTwap(startRecord types.TwapRecord, endRecord types.TwapRecord) sdk.Dec { + timeDelta := endRecord.Time.Sub(startRecord.Time) + accumDiff := endRecord.P0ArithmeticTwapAccumulator.Sub(startRecord.P0ArithmeticTwapAccumulator) + return types.AccumDiffDivDuration(accumDiff, timeDelta) +} diff --git a/x/gamm/twap/store.go b/x/gamm/twap/store.go index 97a591971c6..f9090f4c69f 100644 --- a/x/gamm/twap/store.go +++ b/x/gamm/twap/store.go @@ -59,16 +59,18 @@ func (k Keeper) storeMostRecentTWAP(ctx sdk.Context, twap types.TwapRecord) { store := ctx.KVStore(k.storeKey) key := types.FormatMostRecentTWAPKey(twap.PoolId, twap.Asset0Denom, twap.Asset1Denom) osmoutils.MustSet(store, key, &twap) + k.storeHistoricalTWAP(ctx, twap) } // returns an error if theres no historical record at or before time. // (Asking for a time too far back) -func (k Keeper) getTwapBeforeTime(ctx sdk.Context, poolId uint64, time time.Time, asset0Denom string, asset1Denom string) (types.TwapRecord, error) { +func (k Keeper) getRecordAtOrBeforeTime(ctx sdk.Context, poolId uint64, time time.Time, asset0Denom string, asset1Denom string) (types.TwapRecord, error) { store := ctx.KVStore(k.storeKey) startKey := types.FormatHistoricalPoolIndexTimePrefix(poolId, time) // TODO: Optimize to cut down search on asset0Denom, asset1denom. // Not really important, since primarily envisioning 2 asset pools stopFn := func(key []byte) bool { + // TODO: Make remember first seen time. Currently only works for exact start return types.ParseTimeFromHistoricalPoolIndexKey(key).After(time) } diff --git a/x/gamm/twap/types/twap_record.pb.go b/x/gamm/twap/types/twap_record.pb.go index a54a96f1224..13ac05dcea7 100644 --- a/x/gamm/twap/types/twap_record.pb.go +++ b/x/gamm/twap/types/twap_record.pb.go @@ -48,7 +48,9 @@ type TwapRecord struct { Height int64 `protobuf:"varint,4,opt,name=height,proto3" json:"record_height" yaml:"record_height"` // This field should only exist until we have a global registry in the state // machine, mapping prior block heights within {TIME RANGE} to times. - Time time.Time `protobuf:"bytes,5,opt,name=time,proto3,stdtime" json:"time" yaml:"record_time"` + Time time.Time `protobuf:"bytes,5,opt,name=time,proto3,stdtime" json:"time" yaml:"record_time"` + // We store the last spot prices in the struct, so that we can interpolate + // accumulator values for times between when accumulator records are stored. P0LastSpotPrice github_com_cosmos_cosmos_sdk_types.Dec `protobuf:"bytes,6,opt,name=p0_last_spot_price,json=p0LastSpotPrice,proto3,customtype=github.com/cosmos/cosmos-sdk/types.Dec" json:"p0_last_spot_price"` P1LastSpotPrice github_com_cosmos_cosmos_sdk_types.Dec `protobuf:"bytes,7,opt,name=p1_last_spot_price,json=p1LastSpotPrice,proto3,customtype=github.com/cosmos/cosmos-sdk/types.Dec" json:"p1_last_spot_price"` P0ArithmeticTwapAccumulator github_com_cosmos_cosmos_sdk_types.Dec `protobuf:"bytes,8,opt,name=p0_arithmetic_twap_accumulator,json=p0ArithmeticTwapAccumulator,proto3,customtype=github.com/cosmos/cosmos-sdk/types.Dec" json:"p0_arithmetic_twap_accumulator"` diff --git a/x/gamm/twap/types/utils.go b/x/gamm/twap/types/utils.go index 3b5631b00c7..60b6d44a35a 100644 --- a/x/gamm/twap/types/utils.go +++ b/x/gamm/twap/types/utils.go @@ -3,6 +3,7 @@ package types import ( fmt "fmt" "sort" + time "time" sdk "github.com/cosmos/cosmos-sdk/types" @@ -68,6 +69,14 @@ func GetAllUniqueDenomPairs(denoms []string) ([]string, []string) { return pairGT, pairLT } +func SpotPriceTimesDuration(sp sdk.Dec, timeDelta time.Duration) sdk.Dec { + return sp.MulInt64(int64(timeDelta)) +} + +func AccumDiffDivDuration(accumDiff sdk.Dec, timeDelta time.Duration) sdk.Dec { + return accumDiff.QuoInt64(int64(timeDelta)) +} + // TODO func (g *GenesisState) Validate() error { return nil From b9c36a0c169bdb913d2e1fef00c97341d7b9cd27 Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Wed, 20 Jul 2022 12:58:44 -0500 Subject: [PATCH 11/72] Add to migration handler --- app/upgrades/v11/constants.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/upgrades/v11/constants.go b/app/upgrades/v11/constants.go index fa9fa4455c7..6a1cc34ff4a 100644 --- a/app/upgrades/v11/constants.go +++ b/app/upgrades/v11/constants.go @@ -2,6 +2,7 @@ package v11 import ( "github.com/osmosis-labs/osmosis/v10/app/upgrades" + twaptypes "github.com/osmosis-labs/osmosis/v10/x/gamm/twap/types" store "github.com/cosmos/cosmos-sdk/store/types" ) @@ -12,5 +13,8 @@ const UpgradeName = "v11" var Upgrade = upgrades.Upgrade{ UpgradeName: UpgradeName, CreateUpgradeHandler: CreateUpgradeHandler, - StoreUpgrades: store.StoreUpgrades{}, + StoreUpgrades: store.StoreUpgrades{ + Added: []string{twaptypes.StoreKey}, + Deleted: []string{}, // double check bech32ibc + }, } From 7c96ede29e099fb5823d9a6d37ab3cb55a67d95a Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Wed, 20 Jul 2022 14:04:14 -0500 Subject: [PATCH 12/72] Fix lint (by making prune stub) --- x/gamm/twap/hook_listener.go | 7 +++---- x/gamm/twap/keeper.go | 5 +++++ x/gamm/twap/logic.go | 5 +++++ 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/x/gamm/twap/hook_listener.go b/x/gamm/twap/hook_listener.go index 5605871df84..fef1e41af67 100644 --- a/x/gamm/twap/hook_listener.go +++ b/x/gamm/twap/hook_listener.go @@ -15,10 +15,9 @@ type epochhook struct { } func (hook *epochhook) AfterEpochEnd(ctx sdk.Context, epochIdentifier string, epochNumber int64) { - // TODO: - // if epochIdentifier == hook.k.PruneIdentifier() { - // hook.k.pruneOldTwaps(ctx) - // } + if epochIdentifier == hook.k.PruneEpochIdentifier(ctx) { + hook.k.pruneOldTwaps(ctx) + } } func (hook *epochhook) BeforeEpochStart(ctx sdk.Context, epochIdentifier string, epochNumber int64) {} diff --git a/x/gamm/twap/keeper.go b/x/gamm/twap/keeper.go index dc5bfce292d..46ad1cccbe3 100644 --- a/x/gamm/twap/keeper.go +++ b/x/gamm/twap/keeper.go @@ -20,3 +20,8 @@ type Keeper struct { func NewKeeper(storeKey sdk.StoreKey, transientKey *sdk.TransientStoreKey, paramSpace paramtypes.Subspace, ammKeeper types.AmmInterface) *Keeper { return &Keeper{storeKey: storeKey, transientKey: transientKey, paramSpace: paramSpace, ammkeeper: ammKeeper} } + +// TODO: make this read from a parameter, or hardcode it. +func (k *Keeper) PruneEpochIdentifier(ctx sdk.Context) string { + return "daily" +} diff --git a/x/gamm/twap/logic.go b/x/gamm/twap/logic.go index 3d40801fe68..fb10618be15 100644 --- a/x/gamm/twap/logic.go +++ b/x/gamm/twap/logic.go @@ -61,6 +61,11 @@ func (k Keeper) updateTWAPs(ctx sdk.Context, poolId uint64) error { return nil } +func (k Keeper) pruneOldTwaps(ctx sdk.Context) { + // TODO: Implement this code + k.deleteHistoricalTWAP(ctx, types.TwapRecord{}) +} + func (k Keeper) getStartRecord(ctx sdk.Context, poolId uint64, time time.Time, assetA string, assetB string) (types.TwapRecord, error) { if !(assetA > assetB) { assetA, assetB = assetB, assetA From d96b1d9995dc3823ae1f94fe873f7c7c58fac7fb Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Wed, 20 Jul 2022 16:02:31 -0500 Subject: [PATCH 13/72] integrate BeforeSwap hook, add key format test --- x/gamm/keeper/pool.go | 1 + x/gamm/twap/hook_listener.go | 2 +- x/gamm/twap/hook_test.go | 7 ++++ x/gamm/twap/keeper_test.go | 21 ++++++++++++ x/gamm/twap/types/keys.go | 22 ++++++------ x/gamm/twap/types/keys_test.go | 56 +++++++++++++++++++++++++++++++ x/gamm/types/hooks.go | 15 +++------ x/pool-incentives/keeper/hooks.go | 2 +- 8 files changed, 104 insertions(+), 22 deletions(-) create mode 100644 x/gamm/twap/hook_test.go create mode 100644 x/gamm/twap/keeper_test.go create mode 100644 x/gamm/twap/types/keys_test.go diff --git a/x/gamm/keeper/pool.go b/x/gamm/keeper/pool.go index dae14c7a654..17e54bbf8a1 100644 --- a/x/gamm/keeper/pool.go +++ b/x/gamm/keeper/pool.go @@ -47,6 +47,7 @@ func (k Keeper) GetPoolAndPoke(ctx sdk.Context, poolId uint64) (types.PoolI, err // Get pool and check if the pool is active, i.e. allowed to be swapped against. func (k Keeper) getPoolForSwap(ctx sdk.Context, poolId uint64) (types.PoolI, error) { + k.hooks.BeforeSwap(ctx, poolId) pool, err := k.GetPoolAndPoke(ctx, poolId) if err != nil { return &balancer.Pool{}, err diff --git a/x/gamm/twap/hook_listener.go b/x/gamm/twap/hook_listener.go index fef1e41af67..e5e11a7469a 100644 --- a/x/gamm/twap/hook_listener.go +++ b/x/gamm/twap/hook_listener.go @@ -39,7 +39,7 @@ func (hook *gammhook) AfterPoolCreated(ctx sdk.Context, sender sdk.AccAddress, p } } -func (hook *gammhook) BeforeSwap(ctx sdk.Context, _ sdk.AccAddress, poolId uint64) { +func (hook *gammhook) BeforeSwap(ctx sdk.Context, poolId uint64) { err := hook.k.updateTwapIfNotRedundant(ctx, poolId) if err != nil { panic(err) diff --git a/x/gamm/twap/hook_test.go b/x/gamm/twap/hook_test.go new file mode 100644 index 00000000000..753ebdcb857 --- /dev/null +++ b/x/gamm/twap/hook_test.go @@ -0,0 +1,7 @@ +package twap_test + +// TestCreatePoolFlow tests that upon a pool being created, +// we have made the correct store entries. +func (suite *TestSuite) TestCreatePoolFlow() { + suite.Setup() +} diff --git a/x/gamm/twap/keeper_test.go b/x/gamm/twap/keeper_test.go new file mode 100644 index 00000000000..633225637af --- /dev/null +++ b/x/gamm/twap/keeper_test.go @@ -0,0 +1,21 @@ +package twap_test + +import ( + "testing" + + "github.com/stretchr/testify/suite" + + "github.com/osmosis-labs/osmosis/v10/app/apptesting" +) + +type TestSuite struct { + apptesting.KeeperTestHelper +} + +func TestSuiteRun(t *testing.T) { + suite.Run(t, new(TestSuite)) +} + +func (suite *TestSuite) SetupTest() { + suite.Setup() +} diff --git a/x/gamm/twap/types/keys.go b/x/gamm/twap/types/keys.go index fc03997c97f..06ac698478d 100644 --- a/x/gamm/twap/types/keys.go +++ b/x/gamm/twap/types/keys.go @@ -24,38 +24,40 @@ const ( KeySeparator = "|" ) -var AlteredPoolIdsPrefix = []byte{0} +var mostRecentTWAPsNoSeparator = "recent_twap" +var historicalTWAPTimeIndexNoSeparator = "historical_time_index" +var historicalTWAPPoolIndexNoSeparator = "historical_pool_index" -var mostRecentTWAPsPrefix = "recent_twap" + KeySeparator -var historicalTWAPTimeIndexPrefix = "historical_time_index" + KeySeparator -var historicalTWAPPoolIndexPrefix = "historical_pool_index" + KeySeparator +var mostRecentTWAPsPrefix = mostRecentTWAPsNoSeparator + KeySeparator +var historicalTWAPTimeIndexPrefix = historicalTWAPTimeIndexNoSeparator + KeySeparator +var historicalTWAPPoolIndexPrefix = historicalTWAPPoolIndexNoSeparator + KeySeparator // TODO: make utility command to automatically interlace separators func FormatMostRecentTWAPKey(poolId uint64, denom1 string, denom2 string) []byte { - return []byte(fmt.Sprintf("%s%s%d%s%s%s%s", mostRecentTWAPsPrefix, KeySeparator, poolId, KeySeparator, denom1, KeySeparator, denom2)) + return []byte(fmt.Sprintf("%s%d%s%s%s%s", mostRecentTWAPsPrefix, poolId, KeySeparator, denom1, KeySeparator, denom2)) } // TODO: Replace historical management with ORM, we currently accept 2x write amplification right now. func FormatHistoricalTimeIndexTWAPKey(accumulatorWriteTime time.Time, poolId uint64, denom1 string, denom2 string) []byte { timeS := osmoutils.FormatTimeString(accumulatorWriteTime) - return []byte(fmt.Sprintf("%s%s%s%s%d%s%s%s%s", historicalTWAPTimeIndexPrefix, KeySeparator, timeS, KeySeparator, poolId, KeySeparator, denom1, KeySeparator, denom2)) + return []byte(fmt.Sprintf("%s%s%s%d%s%s%s%s", historicalTWAPTimeIndexPrefix, timeS, KeySeparator, poolId, KeySeparator, denom1, KeySeparator, denom2)) } func FormatHistoricalPoolIndexTWAPKey(poolId uint64, accumulatorWriteTime time.Time, denom1 string, denom2 string) []byte { timeS := osmoutils.FormatTimeString(accumulatorWriteTime) - return []byte(fmt.Sprintf("%s%s%d%s%s%s%s%s%s", historicalTWAPPoolIndexPrefix, KeySeparator, poolId, KeySeparator, timeS, KeySeparator, denom1, KeySeparator, denom2)) + return []byte(fmt.Sprintf("%s%d%s%s%s%s%s%s", historicalTWAPPoolIndexPrefix, poolId, KeySeparator, timeS, KeySeparator, denom1, KeySeparator, denom2)) } func FormatHistoricalPoolIndexTimePrefix(poolId uint64, accumulatorWriteTime time.Time) []byte { timeS := osmoutils.FormatTimeString(accumulatorWriteTime) - return []byte(fmt.Sprintf("%s%s%d%s%s%s", historicalTWAPPoolIndexPrefix, KeySeparator, poolId, KeySeparator, timeS, KeySeparator)) + return []byte(fmt.Sprintf("%s%d%s%s%s", historicalTWAPPoolIndexPrefix, poolId, KeySeparator, timeS, KeySeparator)) } func ParseTimeFromHistoricalTimeIndexKey(key []byte) time.Time { keyS := string(key) s := strings.Split(keyS, KeySeparator) - if len(s) != 5 || s[0] != historicalTWAPTimeIndexPrefix { + if len(s) != 5 || s[0] != historicalTWAPTimeIndexNoSeparator { panic("Called ParseTimeFromHistoricalTimeIndexKey on incorrectly formatted key") } t, err := osmoutils.ParseTimeString(s[1]) @@ -68,7 +70,7 @@ func ParseTimeFromHistoricalTimeIndexKey(key []byte) time.Time { func ParseTimeFromHistoricalPoolIndexKey(key []byte) time.Time { keyS := string(key) s := strings.Split(keyS, KeySeparator) - if len(s) != 5 || s[0] != historicalTWAPPoolIndexPrefix { + if len(s) != 5 || s[0] != historicalTWAPPoolIndexNoSeparator { panic("Called ParseTimeFromHistoricalPoolIndexKey on incorrectly formatted key") } t, err := osmoutils.ParseTimeString(s[2]) diff --git a/x/gamm/twap/types/keys_test.go b/x/gamm/twap/types/keys_test.go new file mode 100644 index 00000000000..f2dc0e3b986 --- /dev/null +++ b/x/gamm/twap/types/keys_test.go @@ -0,0 +1,56 @@ +package types + +import ( + "testing" + time "time" + + "github.com/stretchr/testify/require" +) + +func TestFormatMostRecentTWAPKey(t *testing.T) { + tests := map[string]struct { + poolId uint64 + denom1 string + denom2 string + want string + }{ + "standard": {poolId: 1, denom1: "B", denom2: "A", want: "recent_twap|1|B|A"}, + "standard2digit": {poolId: 10, denom1: "B", denom2: "A", want: "recent_twap|10|B|A"}, + "maxPoolId": {poolId: ^uint64(0), denom1: "B", denom2: "A", want: "recent_twap|18446744073709551615|B|A"}, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + got := FormatMostRecentTWAPKey(tt.poolId, tt.denom1, tt.denom2) + require.Equal(t, tt.want, string(got)) + }) + } +} + +func TestFormatHistoricalTwapKeys(t *testing.T) { + // go playground default time + // 2009-11-10 23:00:00 +0000 UTC m=+0.000000001 + baseTime := time.Unix(1257894000, 0).UTC() + tests := map[string]struct { + poolId uint64 + time time.Time + denom1 string + denom2 string + wantPoolIndex string + wantTimeIndex string + }{ + "standard": {poolId: 1, time: baseTime, denom1: "B", denom2: "A", wantTimeIndex: "historical_time_index|2009-11-10T23:00:00.000000000|1|B|A", wantPoolIndex: "historical_pool_index|1|2009-11-10T23:00:00.000000000|B|A"}, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + gotTimeKey := FormatHistoricalTimeIndexTWAPKey(tt.time, tt.poolId, tt.denom1, tt.denom2) + gotPoolKey := FormatHistoricalPoolIndexTWAPKey(tt.poolId, tt.time, tt.denom1, tt.denom2) + require.Equal(t, tt.wantTimeIndex, string(gotTimeKey)) + require.Equal(t, tt.wantPoolIndex, string(gotPoolKey)) + + parsedTime := ParseTimeFromHistoricalTimeIndexKey(gotTimeKey) + require.Equal(t, tt.time, parsedTime) + parsedTime = ParseTimeFromHistoricalPoolIndexKey(gotPoolKey) + require.Equal(t, tt.time, parsedTime) + }) + } +} diff --git a/x/gamm/types/hooks.go b/x/gamm/types/hooks.go index 9602c89f012..b763dcdd67f 100644 --- a/x/gamm/types/hooks.go +++ b/x/gamm/types/hooks.go @@ -6,14 +6,15 @@ type GammHooks interface { // AfterPoolCreated is called after CreatePool AfterPoolCreated(ctx sdk.Context, sender sdk.AccAddress, poolId uint64) - BeforeJoinPool(ctx sdk.Context, sender sdk.AccAddress, poolId uint64) // AfterJoinPool is called after JoinPool, JoinSwapExternAmountIn, and JoinSwapShareAmountOut AfterJoinPool(ctx sdk.Context, sender sdk.AccAddress, poolId uint64, enterCoins sdk.Coins, shareOutAmount sdk.Int) // AfterExitPool is called after ExitPool, ExitSwapShareAmountIn, and ExitSwapExternAmountOut AfterExitPool(ctx sdk.Context, sender sdk.AccAddress, poolId uint64, shareInAmount sdk.Int, exitCoins sdk.Coins) - BeforeSwap(ctx sdk.Context, sender sdk.AccAddress, poolId uint64) + // BeforeSwap is called before JoinSwapExternAmountIn, JoinSwapShareAmountOut, ExitSwapShareAmountIn, ExitSwapExternAmountOut + // SwapExactAmountIn and SwapExactAmountOut + BeforeSwap(ctx sdk.Context, poolId uint64) // AfterSwap is called after SwapExactAmountIn and SwapExactAmountOut AfterSwap(ctx sdk.Context, sender sdk.AccAddress, poolId uint64, input sdk.Coins, output sdk.Coins) } @@ -34,12 +35,6 @@ func (h MultiGammHooks) AfterPoolCreated(ctx sdk.Context, sender sdk.AccAddress, } } -func (h MultiGammHooks) BeforeJoinPool(ctx sdk.Context, sender sdk.AccAddress, poolId uint64) { - for i := range h { - h[i].BeforeJoinPool(ctx, sender, poolId) - } -} - func (h MultiGammHooks) AfterJoinPool(ctx sdk.Context, sender sdk.AccAddress, poolId uint64, enterCoins sdk.Coins, shareOutAmount sdk.Int) { for i := range h { h[i].AfterJoinPool(ctx, sender, poolId, enterCoins, shareOutAmount) @@ -52,9 +47,9 @@ func (h MultiGammHooks) AfterExitPool(ctx sdk.Context, sender sdk.AccAddress, po } } -func (h MultiGammHooks) BeforeSwap(ctx sdk.Context, sender sdk.AccAddress, poolId uint64) { +func (h MultiGammHooks) BeforeSwap(ctx sdk.Context, poolId uint64) { for i := range h { - h[i].BeforeSwap(ctx, sender, poolId) + h[i].BeforeSwap(ctx, poolId) } } diff --git a/x/pool-incentives/keeper/hooks.go b/x/pool-incentives/keeper/hooks.go index 8435e18e8ec..4f183a68661 100644 --- a/x/pool-incentives/keeper/hooks.go +++ b/x/pool-incentives/keeper/hooks.go @@ -39,7 +39,7 @@ func (h Hooks) AfterExitPool(ctx sdk.Context, sender sdk.AccAddress, poolId uint func (h Hooks) AfterSwap(ctx sdk.Context, sender sdk.AccAddress, poolId uint64, input sdk.Coins, output sdk.Coins) { } -func (h Hooks) BeforeSwap(ctx sdk.Context, sender sdk.AccAddress, poolId uint64) {} +func (h Hooks) BeforeSwap(ctx sdk.Context, poolId uint64) {} func (h Hooks) BeforeJoinPool(ctx sdk.Context, sender sdk.AccAddress, poolId uint64) {} // Distribute coins after minter module allocate assets to pool-incentives module. From 20f94fd5c984fe29003e95fdf169e1114da9a589 Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Wed, 20 Jul 2022 16:08:55 -0500 Subject: [PATCH 14/72] More keys tests --- x/gamm/twap/types/keys.go | 2 +- x/gamm/twap/types/keys_test.go | 31 +++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/x/gamm/twap/types/keys.go b/x/gamm/twap/types/keys.go index 06ac698478d..0045eb9af74 100644 --- a/x/gamm/twap/types/keys.go +++ b/x/gamm/twap/types/keys.go @@ -87,7 +87,7 @@ func GetAllMostRecentTwapsForPool(store sdk.KVStore, poolId uint64) ([]TwapRecor } func ParseTwapFromBz(bz []byte) (twap TwapRecord, err error) { - if len(bz) > 0 { + if len(bz) == 0 { return TwapRecord{}, errors.New("twap not found") } err = proto.Unmarshal(bz, &twap) diff --git a/x/gamm/twap/types/keys_test.go b/x/gamm/twap/types/keys_test.go index f2dc0e3b986..29d065e263d 100644 --- a/x/gamm/twap/types/keys_test.go +++ b/x/gamm/twap/types/keys_test.go @@ -4,6 +4,8 @@ import ( "testing" time "time" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/gogo/protobuf/proto" "github.com/stretchr/testify/require" ) @@ -54,3 +56,32 @@ func TestFormatHistoricalTwapKeys(t *testing.T) { }) } } + +func TestParseTwapFromBz(t *testing.T) { + baseTime := time.Unix(1257894000, 0).UTC() + tests := map[string]struct { + record TwapRecord + }{ + "standard": {TwapRecord{ + PoolId: 123, + Asset0Denom: "B", + Asset1Denom: "A", + Height: 1, + Time: baseTime, + P0LastSpotPrice: sdk.NewDecWithPrec(1, 5), + P1LastSpotPrice: sdk.NewDecWithPrec(2, 5), // inconsistent value + P0ArithmeticTwapAccumulator: sdk.ZeroDec(), + P1ArithmeticTwapAccumulator: sdk.ZeroDec(), + }}, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + bz, err := proto.Marshal(&tt.record) + require.NoError(t, err) + record, err := ParseTwapFromBz(bz) + require.NoError(t, err) + + require.Equal(t, tt.record, record) + }) + } +} From 11d6071947ff1f3fe876142726b295b0ebd9597b Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Wed, 20 Jul 2022 16:40:31 -0500 Subject: [PATCH 15/72] Add TestGetAllUniqueDenomPairs --- x/gamm/twap/types/utils_test.go | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 x/gamm/twap/types/utils_test.go diff --git a/x/gamm/twap/types/utils_test.go b/x/gamm/twap/types/utils_test.go new file mode 100644 index 00000000000..3105f8349e3 --- /dev/null +++ b/x/gamm/twap/types/utils_test.go @@ -0,0 +1,28 @@ +package types + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestGetAllUniqueDenomPairs(t *testing.T) { + tests := map[string]struct { + denoms []string + wantedPairGT []string + wantedPairLT []string + }{ + "basic": {[]string{"A", "B"}, []string{"B"}, []string{"A"}}, + "basicRev": {[]string{"B", "A"}, []string{"B"}, []string{"A"}}, + // AB > A + "prefixed": {[]string{"A", "AB"}, []string{"AB"}, []string{"A"}}, + "basic-3": {[]string{"A", "B", "C"}, []string{"C", "C", "B"}, []string{"B", "A", "A"}}, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + pairGT, pairLT := GetAllUniqueDenomPairs(tt.denoms) + require.Equal(t, pairGT, tt.wantedPairGT) + require.Equal(t, pairLT, tt.wantedPairLT) + }) + } +} From 00e05d369f2a8e56e5211c3f7238bf93b12d3ad6 Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Wed, 20 Jul 2022 21:46:43 -0500 Subject: [PATCH 16/72] Test for the create pool flow --- osmoutils/store_helper.go | 12 ++++++++++-- x/gamm/twap/export_test.go | 21 +++++++++++++++++++++ x/gamm/twap/hook_test.go | 21 +++++++++++++++++++-- x/gamm/twap/keeper_test.go | 3 +++ x/gamm/twap/logic.go | 7 +++++++ x/gamm/twap/module.go | 1 + x/gamm/twap/store.go | 12 ++++++++++-- x/gamm/twap/types/keys.go | 8 ++++---- x/gamm/twap/types/keys_test.go | 7 ++++++- 9 files changed, 81 insertions(+), 11 deletions(-) create mode 100644 x/gamm/twap/export_test.go diff --git a/osmoutils/store_helper.go b/osmoutils/store_helper.go index 8b6fcb90bd4..3492e07991f 100644 --- a/osmoutils/store_helper.go +++ b/osmoutils/store_helper.go @@ -2,6 +2,7 @@ package osmoutils import ( "errors" + "fmt" "github.com/cosmos/cosmos-sdk/store" "github.com/gogo/protobuf/proto" @@ -34,10 +35,14 @@ func GatherValuesFromStore[T any](storeObj store.KVStore, keyStart []byte, keyEn } func GetValuesUntilDerivedStop[T any](storeObj store.KVStore, keyStart []byte, stopFn func([]byte) bool, parseValue func([]byte) (T, error)) ([]T, error) { - iterator := storeObj.Iterator(keyStart, nil) + // SDK iterator is broken for nil end time, and non-nil start time + // https://github.com/cosmos/cosmos-sdk/issues/12661 + // hence we use []byte{0xff} + iterator := storeObj.Iterator(keyStart, []byte{0xff}) defer iterator.Close() values := []T{} + fmt.Println(iterator.Valid()) for ; iterator.Valid(); iterator.Next() { if stopFn(iterator.Key()) { break @@ -52,7 +57,10 @@ func GetValuesUntilDerivedStop[T any](storeObj store.KVStore, keyStart []byte, s } func GetFirstValueAfterPrefix[T any](storeObj store.KVStore, keyStart []byte, parseValue func([]byte) (T, error)) (T, error) { - iterator := storeObj.Iterator(keyStart, nil) + // SDK iterator is broken for nil end time, and non-nil start time + // https://github.com/cosmos/cosmos-sdk/issues/12661 + // hence we use []byte{0xff} + iterator := storeObj.Iterator(keyStart, []byte{0xff}) defer iterator.Close() if !iterator.Valid() { diff --git a/x/gamm/twap/export_test.go b/x/gamm/twap/export_test.go new file mode 100644 index 00000000000..a1894c314b4 --- /dev/null +++ b/x/gamm/twap/export_test.go @@ -0,0 +1,21 @@ +package twap + +import ( + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/osmosis-labs/osmosis/v10/x/gamm/twap/types" +) + +func (k Keeper) GetMostRecentTWAP(ctx sdk.Context, poolId uint64, asset0Denom string, asset1Denom string) (types.TwapRecord, error) { + return k.getMostRecentTWAP(ctx, poolId, asset0Denom, asset1Denom) +} + +func (k Keeper) GetAllMostRecentTWAPsForPool(ctx sdk.Context, poolId uint64) ([]types.TwapRecord, error) { + return k.getAllMostRecentTWAPsForPool(ctx, poolId) +} + +func (k Keeper) GetRecordAtOrBeforeTime(ctx sdk.Context, poolId uint64, time time.Time, asset0Denom string, asset1Denom string) (types.TwapRecord, error) { + return k.getRecordAtOrBeforeTime(ctx, poolId, time, asset0Denom, asset1Denom) +} diff --git a/x/gamm/twap/hook_test.go b/x/gamm/twap/hook_test.go index 753ebdcb857..76a5c0e0676 100644 --- a/x/gamm/twap/hook_test.go +++ b/x/gamm/twap/hook_test.go @@ -1,7 +1,24 @@ package twap_test +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/osmosis-labs/osmosis/v10/x/gamm/twap/types" +) + // TestCreatePoolFlow tests that upon a pool being created, // we have made the correct store entries. -func (suite *TestSuite) TestCreatePoolFlow() { - suite.Setup() +func (suite *TestSuite) TestCreateTwoAssetPoolFlow() { + poolLiquidity := sdk.NewCoins(sdk.NewInt64Coin("token/A", 100), sdk.NewInt64Coin("token/B", 100)) + poolId := suite.PrepareUni2PoolWithAssets(poolLiquidity[0], poolLiquidity[1]) + + expectedTwap := types.NewTwapRecord(suite.App.GAMMKeeper, suite.Ctx, poolId, "token/B", "token/A") + + twap, err := suite.twapkeeper.GetMostRecentTWAP(suite.Ctx, poolId, "token/B", "token/A") + suite.Require().NoError(err) + suite.Require().Equal(expectedTwap, twap) + + twap, err = suite.twapkeeper.GetRecordAtOrBeforeTime(suite.Ctx, poolId, suite.Ctx.BlockTime(), "token/B", "token/A") + suite.Require().NoError(err) + suite.Require().Equal(expectedTwap, twap) } diff --git a/x/gamm/twap/keeper_test.go b/x/gamm/twap/keeper_test.go index 633225637af..1cf231ed926 100644 --- a/x/gamm/twap/keeper_test.go +++ b/x/gamm/twap/keeper_test.go @@ -6,10 +6,12 @@ import ( "github.com/stretchr/testify/suite" "github.com/osmosis-labs/osmosis/v10/app/apptesting" + "github.com/osmosis-labs/osmosis/v10/x/gamm/twap" ) type TestSuite struct { apptesting.KeeperTestHelper + twapkeeper *twap.Keeper } func TestSuiteRun(t *testing.T) { @@ -18,4 +20,5 @@ func TestSuiteRun(t *testing.T) { func (suite *TestSuite) SetupTest() { suite.Setup() + suite.twapkeeper = suite.App.TwapKeeper } diff --git a/x/gamm/twap/logic.go b/x/gamm/twap/logic.go index fb10618be15..20d311f78e3 100644 --- a/x/gamm/twap/logic.go +++ b/x/gamm/twap/logic.go @@ -14,6 +14,7 @@ func (k Keeper) afterCreatePool(ctx sdk.Context, poolId uint64) error { for i := 0; i < len(denomPairs0); i++ { record := types.NewTwapRecord(k.ammkeeper, ctx, poolId, denomPairs0[i], denomPairs1[i]) k.storeMostRecentTWAP(ctx, record) + k.storeHistoricalTWAP(ctx, record) } return err } @@ -61,6 +62,10 @@ func (k Keeper) updateTWAPs(ctx sdk.Context, poolId uint64) error { return nil } +func (k Keeper) endBlock(ctx sdk.Context) { + // TODO: Update all LastSpotPrice's +} + func (k Keeper) pruneOldTwaps(ctx sdk.Context) { // TODO: Implement this code k.deleteHistoricalTWAP(ctx, types.TwapRecord{}) @@ -87,6 +92,8 @@ func interpolateRecord(record types.TwapRecord, interpolateTime time.Time) types timeDelta := interpolateTime.Sub(record.Time) interpolatedRecord.Time = interpolateTime + // TODO: Were currently using the wrong LastSpotPrice, we need to get it from EndBlock for changed pools. + interpolatedRecord.P0ArithmeticTwapAccumulator = interpolatedRecord.P0ArithmeticTwapAccumulator.Add( types.SpotPriceTimesDuration(record.P0LastSpotPrice, timeDelta)) interpolatedRecord.P1ArithmeticTwapAccumulator = interpolatedRecord.P1ArithmeticTwapAccumulator.Add( diff --git a/x/gamm/twap/module.go b/x/gamm/twap/module.go index cf31ac723a8..8787595c299 100644 --- a/x/gamm/twap/module.go +++ b/x/gamm/twap/module.go @@ -117,6 +117,7 @@ func (AppModule) BeginBlock(_ sdk.Context, _ abci.RequestBeginBlock) {} // EndBlock performs a no-op. func (am AppModule) EndBlock(ctx sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate { + am.k.endBlock(ctx) return []abci.ValidatorUpdate{} } diff --git a/x/gamm/twap/store.go b/x/gamm/twap/store.go index f9090f4c69f..bfb302d223f 100644 --- a/x/gamm/twap/store.go +++ b/x/gamm/twap/store.go @@ -3,6 +3,7 @@ package twap import ( "encoding/binary" "errors" + "fmt" "time" sdk "github.com/cosmos/cosmos-sdk/types" @@ -33,6 +34,8 @@ func (k Keeper) storeHistoricalTWAP(ctx sdk.Context, twap types.TwapRecord) { key2 := types.FormatHistoricalPoolIndexTWAPKey(twap.PoolId, twap.Time, twap.Asset0Denom, twap.Asset1Denom) osmoutils.MustSet(store, key1, &twap) osmoutils.MustSet(store, key2, &twap) + fmt.Println(string(key2)) + fmt.Println(types.ParseTwapFromBz(store.Get(key2))) } func (k Keeper) deleteHistoricalTWAP(ctx sdk.Context, twap types.TwapRecord) { @@ -59,7 +62,6 @@ func (k Keeper) storeMostRecentTWAP(ctx sdk.Context, twap types.TwapRecord) { store := ctx.KVStore(k.storeKey) key := types.FormatMostRecentTWAPKey(twap.PoolId, twap.Asset0Denom, twap.Asset1Denom) osmoutils.MustSet(store, key, &twap) - k.storeHistoricalTWAP(ctx, twap) } // returns an error if theres no historical record at or before time. @@ -67,11 +69,17 @@ func (k Keeper) storeMostRecentTWAP(ctx sdk.Context, twap types.TwapRecord) { func (k Keeper) getRecordAtOrBeforeTime(ctx sdk.Context, poolId uint64, time time.Time, asset0Denom string, asset1Denom string) (types.TwapRecord, error) { store := ctx.KVStore(k.storeKey) startKey := types.FormatHistoricalPoolIndexTimePrefix(poolId, time) + + fmt.Println(string(startKey)) // TODO: Optimize to cut down search on asset0Denom, asset1denom. // Not really important, since primarily envisioning 2 asset pools stopFn := func(key []byte) bool { // TODO: Make remember first seen time. Currently only works for exact start - return types.ParseTimeFromHistoricalPoolIndexKey(key).After(time) + t, err := types.ParseTimeFromHistoricalPoolIndexKey(key) + if err != nil { + return true + } + return t.After(time) } twaps, err := osmoutils.GetValuesUntilDerivedStop(store, startKey, stopFn, types.ParseTwapFromBz) diff --git a/x/gamm/twap/types/keys.go b/x/gamm/twap/types/keys.go index 0045eb9af74..90829937285 100644 --- a/x/gamm/twap/types/keys.go +++ b/x/gamm/twap/types/keys.go @@ -67,17 +67,17 @@ func ParseTimeFromHistoricalTimeIndexKey(key []byte) time.Time { return t } -func ParseTimeFromHistoricalPoolIndexKey(key []byte) time.Time { +func ParseTimeFromHistoricalPoolIndexKey(key []byte) (time.Time, error) { keyS := string(key) s := strings.Split(keyS, KeySeparator) if len(s) != 5 || s[0] != historicalTWAPPoolIndexNoSeparator { - panic("Called ParseTimeFromHistoricalPoolIndexKey on incorrectly formatted key") + return time.Time{}, fmt.Errorf("Called ParseTimeFromHistoricalPoolIndexKey on incorrectly formatted key: %v", s) } t, err := osmoutils.ParseTimeString(s[2]) if err != nil { - panic("incorrectly formatted time string in key") + return time.Time{}, fmt.Errorf("incorrectly formatted time string in key %s : %v", keyS, err) } - return t + return t, nil } func GetAllMostRecentTwapsForPool(store sdk.KVStore, poolId uint64) ([]TwapRecord, error) { diff --git a/x/gamm/twap/types/keys_test.go b/x/gamm/twap/types/keys_test.go index 29d065e263d..13fc334c5b0 100644 --- a/x/gamm/twap/types/keys_test.go +++ b/x/gamm/twap/types/keys_test.go @@ -1,6 +1,7 @@ package types import ( + "strings" "testing" time "time" @@ -51,8 +52,12 @@ func TestFormatHistoricalTwapKeys(t *testing.T) { parsedTime := ParseTimeFromHistoricalTimeIndexKey(gotTimeKey) require.Equal(t, tt.time, parsedTime) - parsedTime = ParseTimeFromHistoricalPoolIndexKey(gotPoolKey) + parsedTime, err := ParseTimeFromHistoricalPoolIndexKey(gotPoolKey) require.Equal(t, tt.time, parsedTime) + require.NoError(t, err) + + poolIndexPrefix := FormatHistoricalPoolIndexTimePrefix(tt.poolId, tt.time) + require.True(t, strings.HasPrefix(string(gotPoolKey), string(poolIndexPrefix))) }) } } From 52ac746c98d68770f0da5b843e8183c387290028 Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Wed, 20 Jul 2022 22:08:57 -0500 Subject: [PATCH 17/72] Correct historical iterations --- osmoutils/store_helper.go | 28 +++++++++++++++++++++------- x/gamm/twap/store.go | 29 ++++++++++++++++++----------- 2 files changed, 39 insertions(+), 18 deletions(-) diff --git a/osmoutils/store_helper.go b/osmoutils/store_helper.go index 3492e07991f..029900ec877 100644 --- a/osmoutils/store_helper.go +++ b/osmoutils/store_helper.go @@ -2,7 +2,6 @@ package osmoutils import ( "errors" - "fmt" "github.com/cosmos/cosmos-sdk/store" "github.com/gogo/protobuf/proto" @@ -38,16 +37,31 @@ func GetValuesUntilDerivedStop[T any](storeObj store.KVStore, keyStart []byte, s // SDK iterator is broken for nil end time, and non-nil start time // https://github.com/cosmos/cosmos-sdk/issues/12661 // hence we use []byte{0xff} - iterator := storeObj.Iterator(keyStart, []byte{0xff}) - defer iterator.Close() + keyEnd := []byte{0xff} + return GetIterValuesWithStop(storeObj, keyStart, keyEnd, false, stopFn, parseValue) +} + +func GetIterValuesWithStop[T any]( + storeObj store.KVStore, + keyStart []byte, + keyEnd []byte, + reverse bool, + stopFn func([]byte) bool, + parseValue func([]byte) (T, error)) ([]T, error) { + var iter store.Iterator + if reverse { + iter = storeObj.ReverseIterator(keyStart, keyEnd) + } else { + iter = storeObj.Iterator(keyStart, keyEnd) + } + defer iter.Close() values := []T{} - fmt.Println(iterator.Valid()) - for ; iterator.Valid(); iterator.Next() { - if stopFn(iterator.Key()) { + for ; iter.Valid(); iter.Next() { + if stopFn(iter.Key()) { break } - val, err := parseValue(iterator.Value()) + val, err := parseValue(iter.Value()) if err != nil { return nil, err } diff --git a/x/gamm/twap/store.go b/x/gamm/twap/store.go index bfb302d223f..e290128202f 100644 --- a/x/gamm/twap/store.go +++ b/x/gamm/twap/store.go @@ -35,7 +35,6 @@ func (k Keeper) storeHistoricalTWAP(ctx sdk.Context, twap types.TwapRecord) { osmoutils.MustSet(store, key1, &twap) osmoutils.MustSet(store, key2, &twap) fmt.Println(string(key2)) - fmt.Println(types.ParseTwapFromBz(store.Get(key2))) } func (k Keeper) deleteHistoricalTWAP(ctx sdk.Context, twap types.TwapRecord) { @@ -66,23 +65,31 @@ func (k Keeper) storeMostRecentTWAP(ctx sdk.Context, twap types.TwapRecord) { // returns an error if theres no historical record at or before time. // (Asking for a time too far back) -func (k Keeper) getRecordAtOrBeforeTime(ctx sdk.Context, poolId uint64, time time.Time, asset0Denom string, asset1Denom string) (types.TwapRecord, error) { +func (k Keeper) getRecordAtOrBeforeTime(ctx sdk.Context, poolId uint64, t time.Time, asset0Denom string, asset1Denom string) (types.TwapRecord, error) { store := ctx.KVStore(k.storeKey) - startKey := types.FormatHistoricalPoolIndexTimePrefix(poolId, time) - - fmt.Println(string(startKey)) - // TODO: Optimize to cut down search on asset0Denom, asset1denom. - // Not really important, since primarily envisioning 2 asset pools + // We make an iteration from time=t + 1ns, to time=0 for this pool. + // Note that we cannot get any time entries from t + 1ns, as the key would be `prefix|t+1ns` + // and the end for a reverse iterator is exclusive. Thus the largest key that can be returned + // begins with a prefix of `prefix|t` + startKey := types.FormatHistoricalPoolIndexTimePrefix(poolId, time.Unix(0, 0)) + endKey := types.FormatHistoricalPoolIndexTimePrefix(poolId, t.Add(time.Nanosecond)) + lastParsedTime := time.Time{} stopFn := func(key []byte) bool { - // TODO: Make remember first seen time. Currently only works for exact start - t, err := types.ParseTimeFromHistoricalPoolIndexKey(key) + // halt iteration if we can't parse the time, or we've successfully parsed + // a time, and have iterated beyond records for that time. + parsedTime, err := types.ParseTimeFromHistoricalPoolIndexKey(key) if err != nil { return true } - return t.After(time) + if lastParsedTime.After(parsedTime) { + return true + } + lastParsedTime = parsedTime + return false } - twaps, err := osmoutils.GetValuesUntilDerivedStop(store, startKey, stopFn, types.ParseTwapFromBz) + reverseIterate := true + twaps, err := osmoutils.GetIterValuesWithStop(store, startKey, endKey, reverseIterate, stopFn, types.ParseTwapFromBz) if err != nil { return types.TwapRecord{}, err } From bde6f0aaf2af2a728956b35ef22a70c7a6661a2c Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Wed, 20 Jul 2022 22:59:54 -0500 Subject: [PATCH 18/72] Add TrackChangedPool tests --- app/apptesting/test_suite.go | 10 ++++++++++ x/gamm/twap/README.md | 3 ++- x/gamm/twap/export_test.go | 8 ++++++++ x/gamm/twap/keeper_test.go | 11 +++++++++++ x/gamm/twap/store.go | 4 ++-- x/gamm/twap/store_test.go | 34 +++++++++++++++++++++++++++++++++ x/gamm/twap/types/utils_test.go | 20 ++++++++++++------- 7 files changed, 80 insertions(+), 10 deletions(-) create mode 100644 x/gamm/twap/store_test.go diff --git a/app/apptesting/test_suite.go b/app/apptesting/test_suite.go index 9f7a6ecf6eb..152a5a7d3c1 100644 --- a/app/apptesting/test_suite.go +++ b/app/apptesting/test_suite.go @@ -61,6 +61,16 @@ func (s *KeeperTestHelper) CreateTestContext() sdk.Context { return sdk.NewContext(ms, tmtypes.Header{}, false, logger) } +// CreateTestContext creates a test context. +func (s *KeeperTestHelper) Commit() { + oldHeight := s.Ctx.BlockHeight() + oldHeader := s.Ctx.BlockHeader() + s.App.Commit() + newHeader := tmtypes.Header{Height: oldHeight + 1, ChainID: oldHeader.ChainID, Time: time.Now().UTC()} + s.App.BeginBlock(abci.RequestBeginBlock{Header: newHeader}) + s.Ctx = s.App.GetBaseApp().NewContext(false, newHeader) +} + // FundAcc funds target address with specified amount. func (s *KeeperTestHelper) FundAcc(acc sdk.AccAddress, amounts sdk.Coins) { err := simapp.FundAccount(s.App.BankKeeper, s.Ctx, acc, amounts) diff --git a/x/gamm/twap/README.md b/x/gamm/twap/README.md index 317977c2e3a..75312f1d36b 100644 --- a/x/gamm/twap/README.md +++ b/x/gamm/twap/README.md @@ -18,5 +18,6 @@ We maintain TWAP entries for every gamm pool. - module.go - SDK AppModule interface implementation. - store.go - Managing logic for getting and setting things to underlying stores -## Basic architecture notes +## Store layout +Every pool has a TWAP stored in state for every asset pair. diff --git a/x/gamm/twap/export_test.go b/x/gamm/twap/export_test.go index a1894c314b4..d685250d817 100644 --- a/x/gamm/twap/export_test.go +++ b/x/gamm/twap/export_test.go @@ -19,3 +19,11 @@ func (k Keeper) GetAllMostRecentTWAPsForPool(ctx sdk.Context, poolId uint64) ([] func (k Keeper) GetRecordAtOrBeforeTime(ctx sdk.Context, poolId uint64, time time.Time, asset0Denom string, asset1Denom string) (types.TwapRecord, error) { return k.getRecordAtOrBeforeTime(ctx, poolId, time, asset0Denom, asset1Denom) } + +func (k Keeper) TrackChangedPool(ctx sdk.Context, poolId uint64) { + k.trackChangedPool(ctx, poolId) +} + +func (k Keeper) HasPoolChangedThisBlock(ctx sdk.Context, poolId uint64) bool { + return k.hasPoolChangedThisBlock(ctx, poolId) +} diff --git a/x/gamm/twap/keeper_test.go b/x/gamm/twap/keeper_test.go index 1cf231ed926..eefc26dad6a 100644 --- a/x/gamm/twap/keeper_test.go +++ b/x/gamm/twap/keeper_test.go @@ -2,11 +2,13 @@ package twap_test import ( "testing" + "time" "github.com/stretchr/testify/suite" "github.com/osmosis-labs/osmosis/v10/app/apptesting" "github.com/osmosis-labs/osmosis/v10/x/gamm/twap" + "github.com/osmosis-labs/osmosis/v10/x/gamm/twap/types" ) type TestSuite struct { @@ -22,3 +24,12 @@ func (suite *TestSuite) SetupTest() { suite.Setup() suite.twapkeeper = suite.App.TwapKeeper } + +func newEmptyPriceRecord(poolId uint64, t time.Time, asset0 string, asset1 string) types.TwapRecord { + return types.TwapRecord{ + PoolId: poolId, + Time: t, + Asset0Denom: asset0, + Asset1Denom: asset1, + } +} diff --git a/x/gamm/twap/store.go b/x/gamm/twap/store.go index e290128202f..69f70431db1 100644 --- a/x/gamm/twap/store.go +++ b/x/gamm/twap/store.go @@ -103,6 +103,6 @@ func (k Keeper) getRecordAtOrBeforeTime(ctx sdk.Context, poolId uint64, t time.T return twap, nil } } - return types.TwapRecord{}, errors.New("Something went wrong - TWAP not found, but there are twaps available for this time." + - " Were provided asset0denom and asset1denom correct?") + return types.TwapRecord{}, errors.New("something went wrong - TWAP not found, but there are twaps available for this time." + + " Were provided asset0denom and asset1denom correct, and in order (asset0 > asset1)?") } diff --git a/x/gamm/twap/store_test.go b/x/gamm/twap/store_test.go new file mode 100644 index 00000000000..9fdbcf80dbe --- /dev/null +++ b/x/gamm/twap/store_test.go @@ -0,0 +1,34 @@ +package twap_test + +// TestTrackChangedPool takes a list of poolIds as test cases, and runs one list per block. +// Every simulated block, checks that all cumulatively tracked pool IDs thus far are not marked as changed. +// Then runs k.trackChangedPool on every item in the test case list. +// Appends these all to our cumulative list. +// Finally checks that hasPoolChangedThisBlock registers the items in our list. +// +// This achieves testing the functionality that we depend on, that this clears every end block. +func (s *TestSuite) TestTrackChangedPool() { + cumulativeIds := map[uint64]bool{} + tests := map[string][]uint64{ + "single": {1}, + "duplicated": {1, 1}, + "four": {1, 2, 3, 4}, + } + for name, test := range tests { + s.Run(name, func() { + // Test that no cumulative ID registers as tracked + for k := range cumulativeIds { + s.Require().False(s.twapkeeper.HasPoolChangedThisBlock(s.Ctx, k)) + } + // Track every pool in list + for _, v := range test { + cumulativeIds[v] = true + s.twapkeeper.TrackChangedPool(s.Ctx, v) + } + for _, v := range test { + s.Require().True(s.twapkeeper.HasPoolChangedThisBlock(s.Ctx, v)) + } + s.Commit() + }) + } +} diff --git a/x/gamm/twap/types/utils_test.go b/x/gamm/twap/types/utils_test.go index 3105f8349e3..6d700d57dc0 100644 --- a/x/gamm/twap/types/utils_test.go +++ b/x/gamm/twap/types/utils_test.go @@ -4,6 +4,8 @@ import ( "testing" "github.com/stretchr/testify/require" + + "github.com/osmosis-labs/osmosis/v10/osmoutils" ) func TestGetAllUniqueDenomPairs(t *testing.T) { @@ -11,18 +13,22 @@ func TestGetAllUniqueDenomPairs(t *testing.T) { denoms []string wantedPairGT []string wantedPairLT []string + panics bool }{ - "basic": {[]string{"A", "B"}, []string{"B"}, []string{"A"}}, - "basicRev": {[]string{"B", "A"}, []string{"B"}, []string{"A"}}, + "basic": {[]string{"A", "B"}, []string{"B"}, []string{"A"}, false}, + "basicRev": {[]string{"B", "A"}, []string{"B"}, []string{"A"}, false}, // AB > A - "prefixed": {[]string{"A", "AB"}, []string{"AB"}, []string{"A"}}, - "basic-3": {[]string{"A", "B", "C"}, []string{"C", "C", "B"}, []string{"B", "A", "A"}}, + "prefixed": {[]string{"A", "AB"}, []string{"AB"}, []string{"A"}, false}, + "basic-3": {[]string{"A", "B", "C"}, []string{"C", "C", "B"}, []string{"B", "A", "A"}, false}, + "panics": {[]string{"A", "A"}, []string{}, []string{}, true}, } for name, tt := range tests { t.Run(name, func(t *testing.T) { - pairGT, pairLT := GetAllUniqueDenomPairs(tt.denoms) - require.Equal(t, pairGT, tt.wantedPairGT) - require.Equal(t, pairLT, tt.wantedPairLT) + osmoutils.ConditionalPanic(t, tt.panics, func() { + pairGT, pairLT := GetAllUniqueDenomPairs(tt.denoms) + require.Equal(t, pairGT, tt.wantedPairGT) + require.Equal(t, pairLT, tt.wantedPairLT) + }) }) } } From 4378f9bff2adaa55adcb8b1bf0e56d67865e3da7 Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Wed, 20 Jul 2022 23:59:52 -0500 Subject: [PATCH 19/72] Add interpolate record test --- x/gamm/twap/api.go | 12 ++++--- x/gamm/twap/export_test.go | 8 +++-- x/gamm/twap/hook_test.go | 2 +- x/gamm/twap/logic.go | 17 +++++++-- x/gamm/twap/logic_test.go | 70 ++++++++++++++++++++++++++++++++++++++ x/gamm/twap/store.go | 8 +++-- 6 files changed, 106 insertions(+), 11 deletions(-) create mode 100644 x/gamm/twap/logic_test.go diff --git a/x/gamm/twap/api.go b/x/gamm/twap/api.go index 2bbb5149b9e..44b8a36b29c 100644 --- a/x/gamm/twap/api.go +++ b/x/gamm/twap/api.go @@ -38,7 +38,7 @@ func (k Keeper) GetArithmeticTwapToNow( if err != nil { return sdk.Dec{}, err } - endRecord, err := k.GetLatestAccumulatorRecord(ctx, poolId, quoteAssetDenom, baseAssetDenom) + endRecord, err := k.GetBeginBlockAccumulatorRecord(ctx, poolId, quoteAssetDenom, baseAssetDenom) if err != nil { return sdk.Dec{}, err } @@ -46,11 +46,15 @@ func (k Keeper) GetArithmeticTwapToNow( return twap, nil } -// GetLatestAccumulatorRecord returns a TwapRecord struct that can be stored -func (k Keeper) GetLatestAccumulatorRecord(ctx sdk.Context, poolId uint64, asset0Denom string, asset1Denom string) (types.TwapRecord, error) { +// GetCurrentAccumulatorRecord returns a TwapRecord struct corresponding to the state of pool `poolId` +// as of the beginning of the block this is called on. +// This uses the state of the beginning of the block, as if there were swaps since the block has started, +// these swaps have had no time to be arbitraged back. +// This accumulator can be stored, to compute wider ranged twaps. +func (k Keeper) GetBeginBlockAccumulatorRecord(ctx sdk.Context, poolId uint64, asset0Denom string, asset1Denom string) (types.TwapRecord, error) { // correct ordering of args for db if asset1Denom > asset0Denom { asset0Denom, asset1Denom = asset1Denom, asset0Denom } - return k.getMostRecentTWAP(ctx, poolId, asset0Denom, asset1Denom) + return k.getMostRecentRecord(ctx, poolId, asset0Denom, asset1Denom) } diff --git a/x/gamm/twap/export_test.go b/x/gamm/twap/export_test.go index d685250d817..9dba4a2a313 100644 --- a/x/gamm/twap/export_test.go +++ b/x/gamm/twap/export_test.go @@ -8,8 +8,8 @@ import ( "github.com/osmosis-labs/osmosis/v10/x/gamm/twap/types" ) -func (k Keeper) GetMostRecentTWAP(ctx sdk.Context, poolId uint64, asset0Denom string, asset1Denom string) (types.TwapRecord, error) { - return k.getMostRecentTWAP(ctx, poolId, asset0Denom, asset1Denom) +func (k Keeper) GetMostRecentRecordStoreRepresentation(ctx sdk.Context, poolId uint64, asset0Denom string, asset1Denom string) (types.TwapRecord, error) { + return k.getMostRecentRecordStoreRepresentation(ctx, poolId, asset0Denom, asset1Denom) } func (k Keeper) GetAllMostRecentTWAPsForPool(ctx sdk.Context, poolId uint64) ([]types.TwapRecord, error) { @@ -27,3 +27,7 @@ func (k Keeper) TrackChangedPool(ctx sdk.Context, poolId uint64) { func (k Keeper) HasPoolChangedThisBlock(ctx sdk.Context, poolId uint64) bool { return k.hasPoolChangedThisBlock(ctx, poolId) } + +func InterpolateRecord(record types.TwapRecord, t time.Time) types.TwapRecord { + return interpolateRecord(record, t) +} diff --git a/x/gamm/twap/hook_test.go b/x/gamm/twap/hook_test.go index 76a5c0e0676..5027769f5db 100644 --- a/x/gamm/twap/hook_test.go +++ b/x/gamm/twap/hook_test.go @@ -14,7 +14,7 @@ func (suite *TestSuite) TestCreateTwoAssetPoolFlow() { expectedTwap := types.NewTwapRecord(suite.App.GAMMKeeper, suite.Ctx, poolId, "token/B", "token/A") - twap, err := suite.twapkeeper.GetMostRecentTWAP(suite.Ctx, poolId, "token/B", "token/A") + twap, err := suite.twapkeeper.GetMostRecentRecordStoreRepresentation(suite.Ctx, poolId, "token/B", "token/A") suite.Require().NoError(err) suite.Require().Equal(expectedTwap, twap) diff --git a/x/gamm/twap/logic.go b/x/gamm/twap/logic.go index 20d311f78e3..0275957ca5f 100644 --- a/x/gamm/twap/logic.go +++ b/x/gamm/twap/logic.go @@ -67,8 +67,9 @@ func (k Keeper) endBlock(ctx sdk.Context) { } func (k Keeper) pruneOldTwaps(ctx sdk.Context) { - // TODO: Implement this code - k.deleteHistoricalTWAP(ctx, types.TwapRecord{}) + // TODO: Read this from parameter + lastAllowedTime := ctx.BlockTime().Add(-48 * time.Hour) + k.pruneRecordsBeforeTime(ctx, lastAllowedTime) } func (k Keeper) getStartRecord(ctx sdk.Context, poolId uint64, time time.Time, assetA string, assetB string) (types.TwapRecord, error) { @@ -83,6 +84,18 @@ func (k Keeper) getStartRecord(ctx sdk.Context, poolId uint64, time time.Time, a return record, nil } +func (k Keeper) getMostRecentRecord(ctx sdk.Context, poolId uint64, assetA string, assetB string) (types.TwapRecord, error) { + if !(assetA > assetB) { + assetA, assetB = assetB, assetA + } + record, err := k.getMostRecentRecordStoreRepresentation(ctx, poolId, assetA, assetB) + if err != nil { + return types.TwapRecord{}, err + } + record = interpolateRecord(record, ctx.BlockTime()) + return record, nil +} + // pre-condition: interpolateTime >= record.Time func interpolateRecord(record types.TwapRecord, interpolateTime time.Time) types.TwapRecord { if record.Time.Equal(interpolateTime) { diff --git a/x/gamm/twap/logic_test.go b/x/gamm/twap/logic_test.go new file mode 100644 index 00000000000..6abcb6b7244 --- /dev/null +++ b/x/gamm/twap/logic_test.go @@ -0,0 +1,70 @@ +package twap_test + +import ( + "testing" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + + "github.com/osmosis-labs/osmosis/v10/x/gamm/twap" + "github.com/osmosis-labs/osmosis/v10/x/gamm/twap/types" +) + +var zeroDec = sdk.ZeroDec() +var oneDec = sdk.OneDec() +var twoDec = oneDec.Add(oneDec) + +func newRecord(t time.Time, sp0, accum0, accum1 sdk.Dec) types.TwapRecord { + return types.TwapRecord{ + Time: t, + P0LastSpotPrice: sp0, + P1LastSpotPrice: sdk.OneDec().Quo(sp0), + P0ArithmeticTwapAccumulator: accum0, + P1ArithmeticTwapAccumulator: accum1, + } +} + +func TestInterpolateRecord(t *testing.T) { + // make an expected record, we adjust other values in the test case. + newExpRecord := func(accum0, accum1 sdk.Dec) types.TwapRecord { + return types.TwapRecord{P0ArithmeticTwapAccumulator: accum0, + P1ArithmeticTwapAccumulator: accum1} + } + + OneSec := sdk.NewDec(1e9) + tests := map[string]struct { + record types.TwapRecord + interpolateTime time.Time + expRecord types.TwapRecord + }{ + "0accum": { + record: newRecord(time.Unix(1, 0), sdk.NewDec(10), zeroDec, zeroDec), + interpolateTime: time.Unix(2, 0), + expRecord: newExpRecord(OneSec.MulInt64(10), OneSec.QuoInt64(10)), + }, + "small starting accumulators": { + record: newRecord(time.Unix(1, 0), sdk.NewDec(10), oneDec, twoDec), + interpolateTime: time.Unix(2, 0), + expRecord: newExpRecord(oneDec.Add(OneSec.MulInt64(10)), twoDec.Add(OneSec.QuoInt64(10))), + }, + "larger time interval": { + record: newRecord(time.Unix(11, 0), sdk.NewDec(10), oneDec, twoDec), + interpolateTime: time.Unix(55, 0), + expRecord: newExpRecord(oneDec.Add(OneSec.MulInt64(44*10)), twoDec.Add(OneSec.MulInt64(44).QuoInt64(10))), + }, + // TODO: Overflow tests + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + // correct expected record based off copy/paste values + test.expRecord.Time = test.interpolateTime + test.expRecord.P0LastSpotPrice = test.record.P0LastSpotPrice + test.expRecord.P1LastSpotPrice = test.record.P1LastSpotPrice + + gotRecord := twap.InterpolateRecord(test.record, test.interpolateTime) + require.Equal(t, test.expRecord, gotRecord) + }) + } +} diff --git a/x/gamm/twap/store.go b/x/gamm/twap/store.go index 69f70431db1..cfb05f48f85 100644 --- a/x/gamm/twap/store.go +++ b/x/gamm/twap/store.go @@ -37,7 +37,11 @@ func (k Keeper) storeHistoricalTWAP(ctx sdk.Context, twap types.TwapRecord) { fmt.Println(string(key2)) } -func (k Keeper) deleteHistoricalTWAP(ctx sdk.Context, twap types.TwapRecord) { +func (k Keeper) pruneRecordsBeforeTime(ctx sdk.Context, lastTime time.Time) { + // TODO: Stub +} + +func (k Keeper) deleteHistoricalRecord(ctx sdk.Context, twap types.TwapRecord) { store := ctx.KVStore(k.storeKey) key1 := types.FormatHistoricalTimeIndexTWAPKey(twap.Time, twap.PoolId, twap.Asset0Denom, twap.Asset1Denom) key2 := types.FormatHistoricalPoolIndexTWAPKey(twap.PoolId, twap.Time, twap.Asset0Denom, twap.Asset1Denom) @@ -45,7 +49,7 @@ func (k Keeper) deleteHistoricalTWAP(ctx sdk.Context, twap types.TwapRecord) { store.Delete(key2) } -func (k Keeper) getMostRecentTWAP(ctx sdk.Context, poolId uint64, asset0Denom string, asset1Denom string) (types.TwapRecord, error) { +func (k Keeper) getMostRecentRecordStoreRepresentation(ctx sdk.Context, poolId uint64, asset0Denom string, asset1Denom string) (types.TwapRecord, error) { store := ctx.KVStore(k.storeKey) key := types.FormatMostRecentTWAPKey(poolId, asset0Denom, asset1Denom) bz := store.Get(key) From 4563c1d750502770a652baf91fc742d9229035b5 Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Thu, 21 Jul 2022 00:25:58 -0500 Subject: [PATCH 20/72] Fix endblock flow --- x/gamm/twap/export_test.go | 8 +++--- x/gamm/twap/hook_listener.go | 14 +++------- x/gamm/twap/logic.go | 50 +++++++++++++++++++----------------- x/gamm/twap/logic_test.go | 5 ++++ x/gamm/twap/store.go | 21 ++++++++++----- x/gamm/twap/store_test.go | 27 ++++++++++--------- 6 files changed, 69 insertions(+), 56 deletions(-) diff --git a/x/gamm/twap/export_test.go b/x/gamm/twap/export_test.go index 9dba4a2a313..27c9835b077 100644 --- a/x/gamm/twap/export_test.go +++ b/x/gamm/twap/export_test.go @@ -12,8 +12,8 @@ func (k Keeper) GetMostRecentRecordStoreRepresentation(ctx sdk.Context, poolId u return k.getMostRecentRecordStoreRepresentation(ctx, poolId, asset0Denom, asset1Denom) } -func (k Keeper) GetAllMostRecentTWAPsForPool(ctx sdk.Context, poolId uint64) ([]types.TwapRecord, error) { - return k.getAllMostRecentTWAPsForPool(ctx, poolId) +func (k Keeper) GetAllMostRecentRecordsForPool(ctx sdk.Context, poolId uint64) ([]types.TwapRecord, error) { + return k.getAllMostRecentRecordsForPool(ctx, poolId) } func (k Keeper) GetRecordAtOrBeforeTime(ctx sdk.Context, poolId uint64, time time.Time, asset0Denom string, asset1Denom string) (types.TwapRecord, error) { @@ -24,8 +24,8 @@ func (k Keeper) TrackChangedPool(ctx sdk.Context, poolId uint64) { k.trackChangedPool(ctx, poolId) } -func (k Keeper) HasPoolChangedThisBlock(ctx sdk.Context, poolId uint64) bool { - return k.hasPoolChangedThisBlock(ctx, poolId) +func (k Keeper) GetChangedPools(ctx sdk.Context) []uint64 { + return k.getChangedPools(ctx) } func InterpolateRecord(record types.TwapRecord, t time.Time) types.TwapRecord { diff --git a/x/gamm/twap/hook_listener.go b/x/gamm/twap/hook_listener.go index e5e11a7469a..e57eb6db2ea 100644 --- a/x/gamm/twap/hook_listener.go +++ b/x/gamm/twap/hook_listener.go @@ -39,24 +39,16 @@ func (hook *gammhook) AfterPoolCreated(ctx sdk.Context, sender sdk.AccAddress, p } } +// TODO: delete func (hook *gammhook) BeforeSwap(ctx sdk.Context, poolId uint64) { - err := hook.k.updateTwapIfNotRedundant(ctx, poolId) - if err != nil { - panic(err) - } } func (hook *gammhook) AfterSwap(ctx sdk.Context, sender sdk.AccAddress, poolId uint64, input sdk.Coins, output sdk.Coins) { -} - -func (hook *gammhook) BeforeJoinPool(ctx sdk.Context, sender sdk.AccAddress, poolId uint64) { - err := hook.k.updateTwapIfNotRedundant(ctx, poolId) - if err != nil { - panic(err) - } + hook.k.trackChangedPool(ctx, poolId) } func (hook *gammhook) AfterJoinPool(ctx sdk.Context, sender sdk.AccAddress, poolId uint64, enterCoins sdk.Coins, shareOutAmount sdk.Int) { + hook.k.trackChangedPool(ctx, poolId) } func (hook *gammhook) AfterExitPool(_ sdk.Context, _ sdk.AccAddress, _ uint64, _ sdk.Int, _ sdk.Coins) { diff --git a/x/gamm/twap/logic.go b/x/gamm/twap/logic.go index 0275957ca5f..bca80cd71cd 100644 --- a/x/gamm/twap/logic.go +++ b/x/gamm/twap/logic.go @@ -13,32 +13,19 @@ func (k Keeper) afterCreatePool(ctx sdk.Context, poolId uint64) error { denomPairs0, denomPairs1 := types.GetAllUniqueDenomPairs(denoms) for i := 0; i < len(denomPairs0); i++ { record := types.NewTwapRecord(k.ammkeeper, ctx, poolId, denomPairs0[i], denomPairs1[i]) - k.storeMostRecentTWAP(ctx, record) - k.storeHistoricalTWAP(ctx, record) + k.storeNewRecord(ctx, record) } return err } -func (k Keeper) updateTwapIfNotRedundant(ctx sdk.Context, poolId uint64) error { - if k.hasPoolChangedThisBlock(ctx, poolId) { - return nil - } - err := k.updateTWAPs(ctx, poolId) - if err != nil { - return err - } - k.trackChangedPool(ctx, poolId) - return nil -} - func (k Keeper) updateTWAPs(ctx sdk.Context, poolId uint64) error { // Will only err if pool doesn't have most recent entry set - twaps, err := k.getAllMostRecentTWAPsForPool(ctx, poolId) + records, err := k.getAllMostRecentRecordsForPool(ctx, poolId) if err != nil { return err } - for _, record := range twaps { + for _, record := range records { timeDelta := ctx.BlockTime().Sub(record.Time) // no update if were in the same block. @@ -51,19 +38,32 @@ func (k Keeper) updateTWAPs(ctx sdk.Context, poolId uint64) error { record.Time = ctx.BlockTime() // TODO: Ensure order is correct - sp0 := types.MustGetSpotPrice(k.ammkeeper, ctx, poolId, record.Asset0Denom, record.Asset1Denom) - sp1 := types.MustGetSpotPrice(k.ammkeeper, ctx, poolId, record.Asset1Denom, record.Asset0Denom) + newSp0 := types.MustGetSpotPrice(k.ammkeeper, ctx, poolId, record.Asset0Denom, record.Asset1Denom) + newSp1 := types.MustGetSpotPrice(k.ammkeeper, ctx, poolId, record.Asset1Denom, record.Asset0Denom) // TODO: Think about overflow - record.P0ArithmeticTwapAccumulator.AddMut(types.SpotPriceTimesDuration(sp0, timeDelta)) - record.P1ArithmeticTwapAccumulator.AddMut(types.SpotPriceTimesDuration(sp1, timeDelta)) - k.storeMostRecentTWAP(ctx, record) + // Update accumulators based on last block / update's spot price + record.P0ArithmeticTwapAccumulator.AddMut(types.SpotPriceTimesDuration(record.P0LastSpotPrice, timeDelta)) + record.P1ArithmeticTwapAccumulator.AddMut(types.SpotPriceTimesDuration(record.P1LastSpotPrice, timeDelta)) + + // set last spot price to be last price of this block. This is what will get used in interpolation. + record.P0LastSpotPrice = newSp0 + record.P1LastSpotPrice = newSp1 + + k.storeNewRecord(ctx, record) } return nil } func (k Keeper) endBlock(ctx sdk.Context) { - // TODO: Update all LastSpotPrice's + changedPoolIds := k.getChangedPools(ctx) + for _, id := range changedPoolIds { + err := k.updateTWAPs(ctx, id) + if err != nil { + panic(err) + } + } + // 'altered pool ids' gets automatically cleared by being a transient store } func (k Keeper) pruneOldTwaps(ctx sdk.Context) { @@ -96,6 +96,8 @@ func (k Keeper) getMostRecentRecord(ctx sdk.Context, poolId uint64, assetA strin return record, nil } +// interpolate record updates the record's accumulator values and time to the interpolate time. +// // pre-condition: interpolateTime >= record.Time func interpolateRecord(record types.TwapRecord, interpolateTime time.Time) types.TwapRecord { if record.Time.Equal(interpolateTime) { @@ -105,7 +107,9 @@ func interpolateRecord(record types.TwapRecord, interpolateTime time.Time) types timeDelta := interpolateTime.Sub(record.Time) interpolatedRecord.Time = interpolateTime - // TODO: Were currently using the wrong LastSpotPrice, we need to get it from EndBlock for changed pools. + // record.LastSpotPrice is the last spot price from the block the record was created in, + // thus it is treated as the effective spot price at the interpolation time. + // (As there was no change until the next block began) interpolatedRecord.P0ArithmeticTwapAccumulator = interpolatedRecord.P0ArithmeticTwapAccumulator.Add( types.SpotPriceTimesDuration(record.P0LastSpotPrice, timeDelta)) diff --git a/x/gamm/twap/logic_test.go b/x/gamm/twap/logic_test.go index 6abcb6b7244..02f11c0296f 100644 --- a/x/gamm/twap/logic_test.go +++ b/x/gamm/twap/logic_test.go @@ -53,6 +53,11 @@ func TestInterpolateRecord(t *testing.T) { interpolateTime: time.Unix(55, 0), expRecord: newExpRecord(oneDec.Add(OneSec.MulInt64(44*10)), twoDec.Add(OneSec.MulInt64(44).QuoInt64(10))), }, + "same time": { + record: newRecord(time.Unix(1, 0), sdk.NewDec(10), oneDec, twoDec), + interpolateTime: time.Unix(1, 0), + expRecord: newExpRecord(oneDec, twoDec), + }, // TODO: Overflow tests } diff --git a/x/gamm/twap/store.go b/x/gamm/twap/store.go index cfb05f48f85..122d4e3ec80 100644 --- a/x/gamm/twap/store.go +++ b/x/gamm/twap/store.go @@ -21,11 +21,18 @@ func (k Keeper) trackChangedPool(ctx sdk.Context, poolId uint64) { store.Set(poolIdBz, sentinelExistsValue) } -func (k Keeper) hasPoolChangedThisBlock(ctx sdk.Context, poolId uint64) bool { +func (k Keeper) getChangedPools(ctx sdk.Context) []uint64 { store := ctx.TransientStore(k.transientKey) - poolIdBz := make([]byte, 8) - binary.LittleEndian.PutUint64(poolIdBz, poolId) - return store.Has(poolIdBz) + iter := store.Iterator(nil, nil) + defer iter.Close() + + alteredPoolIds := []uint64{} + for ; iter.Valid(); iter.Next() { + k := iter.Key() + poolId := binary.LittleEndian.Uint64(k) + alteredPoolIds = append(alteredPoolIds, poolId) + } + return alteredPoolIds } func (k Keeper) storeHistoricalTWAP(ctx sdk.Context, twap types.TwapRecord) { @@ -41,6 +48,7 @@ func (k Keeper) pruneRecordsBeforeTime(ctx sdk.Context, lastTime time.Time) { // TODO: Stub } +//nolint:unused,deadcode func (k Keeper) deleteHistoricalRecord(ctx sdk.Context, twap types.TwapRecord) { store := ctx.KVStore(k.storeKey) key1 := types.FormatHistoricalTimeIndexTWAPKey(twap.Time, twap.PoolId, twap.Asset0Denom, twap.Asset1Denom) @@ -56,15 +64,16 @@ func (k Keeper) getMostRecentRecordStoreRepresentation(ctx sdk.Context, poolId u return types.ParseTwapFromBz(bz) } -func (k Keeper) getAllMostRecentTWAPsForPool(ctx sdk.Context, poolId uint64) ([]types.TwapRecord, error) { +func (k Keeper) getAllMostRecentRecordsForPool(ctx sdk.Context, poolId uint64) ([]types.TwapRecord, error) { store := ctx.KVStore(k.storeKey) return types.GetAllMostRecentTwapsForPool(store, poolId) } -func (k Keeper) storeMostRecentTWAP(ctx sdk.Context, twap types.TwapRecord) { +func (k Keeper) storeNewRecord(ctx sdk.Context, twap types.TwapRecord) { store := ctx.KVStore(k.storeKey) key := types.FormatMostRecentTWAPKey(twap.PoolId, twap.Asset0Denom, twap.Asset1Denom) osmoutils.MustSet(store, key, &twap) + k.storeHistoricalTWAP(ctx, twap) } // returns an error if theres no historical record at or before time. diff --git a/x/gamm/twap/store_test.go b/x/gamm/twap/store_test.go index 9fdbcf80dbe..e3fdb31c5a2 100644 --- a/x/gamm/twap/store_test.go +++ b/x/gamm/twap/store_test.go @@ -1,32 +1,35 @@ package twap_test // TestTrackChangedPool takes a list of poolIds as test cases, and runs one list per block. -// Every simulated block, checks that all cumulatively tracked pool IDs thus far are not marked as changed. +// Every simulated block, checks that there no changed pools. // Then runs k.trackChangedPool on every item in the test case list. -// Appends these all to our cumulative list. -// Finally checks that hasPoolChangedThisBlock registers the items in our list. +// Then checks that changed pools returns the list, deduplicated. // // This achieves testing the functionality that we depend on, that this clears every end block. func (s *TestSuite) TestTrackChangedPool() { - cumulativeIds := map[uint64]bool{} tests := map[string][]uint64{ - "single": {1}, - "duplicated": {1, 1}, - "four": {1, 2, 3, 4}, + "single": {1}, + "duplicated": {1, 1}, + "four": {1, 2, 3, 4}, + "many with dups": {1, 2, 3, 4, 3, 2, 1}, } for name, test := range tests { s.Run(name, func() { // Test that no cumulative ID registers as tracked - for k := range cumulativeIds { - s.Require().False(s.twapkeeper.HasPoolChangedThisBlock(s.Ctx, k)) - } + changedPools := s.twapkeeper.GetChangedPools(s.Ctx) + s.Require().Empty(changedPools) + // Track every pool in list + cumulativeIds := map[uint64]bool{} for _, v := range test { cumulativeIds[v] = true s.twapkeeper.TrackChangedPool(s.Ctx, v) } - for _, v := range test { - s.Require().True(s.twapkeeper.HasPoolChangedThisBlock(s.Ctx, v)) + + changedPools = s.twapkeeper.GetChangedPools(s.Ctx) + s.Require().Equal(len(cumulativeIds), len(changedPools)) + for _, v := range changedPools { + s.Require().True(cumulativeIds[v]) } s.Commit() }) From 2cd49aee382965b1906ca2685426762acba4f7fc Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Thu, 21 Jul 2022 01:31:56 -0500 Subject: [PATCH 21/72] Add UpdateRecord test --- x/gamm/twap/README.md | 19 +++++++++ x/gamm/twap/api.go | 12 ++++-- x/gamm/twap/export_test.go | 4 ++ x/gamm/twap/keeper_test.go | 3 ++ x/gamm/twap/logic.go | 86 ++++++++++++++++++++++---------------- x/gamm/twap/logic_test.go | 63 +++++++++++++++++++++++----- x/gamm/twap/types/utils.go | 4 +- 7 files changed, 140 insertions(+), 51 deletions(-) diff --git a/x/gamm/twap/README.md b/x/gamm/twap/README.md index 75312f1d36b..48b05656e21 100644 --- a/x/gamm/twap/README.md +++ b/x/gamm/twap/README.md @@ -4,6 +4,25 @@ We maintain TWAP entries for every gamm pool. ## Module API +```go +// GetArithmeticTwap returns an arithmetic time weighted average price. +// The returned twap is the time weighted average price (TWAP) of: +// * the base asset, in units of the quote asset (1 unit of base = x units of quote) +// * from (startTime, endTime), +// * as determined by prices from AMM pool `poolId`. +// +// The +// +// startTime and endTime do not have to be real block times that occurred, +// this function will interpolate between startTime. +// if endTime = now, we do {X} +// startTime must be in time range {X}, recommended parameterization for mainnet is {Y} +func (k Keeper) GetArithmeticTwap(ctx sdk.Context, + poolId uint64, + baseAssetDenom string, quoteAssetDenom string, + startTime time.Time, endTime time.Time) (sdk.Dec, error) { +``` + ## File layout **api.go** is the main file you should look at for what you should depend upon. diff --git a/x/gamm/twap/api.go b/x/gamm/twap/api.go index 44b8a36b29c..21723eae0be 100644 --- a/x/gamm/twap/api.go +++ b/x/gamm/twap/api.go @@ -8,8 +8,14 @@ import ( "github.com/osmosis-labs/osmosis/v10/x/gamm/twap/types" ) -// GetArithmeticTwap returns an arithmetic TWAP result from (startTime, endTime), -// for the `quoteAsset / baseAsset` ratio on `poolId`. +// GetArithmeticTwap returns an arithmetic time weighted average price. +// The returned twap is the time weighted average price (TWAP) of: +// * the base asset, in units of the quote asset (1 unit of base = x units of quote) +// * from (startTime, endTime), +// * as determined by prices from AMM pool `poolId`. +// +// The +// // startTime and endTime do not have to be real block times that occurred, // this function will interpolate between startTime. // if endTime = now, we do {X} @@ -42,7 +48,7 @@ func (k Keeper) GetArithmeticTwapToNow( if err != nil { return sdk.Dec{}, err } - twap := k.getArithmeticTwap(startRecord, endRecord) + twap := k.getArithmeticTwap(startRecord, endRecord, quoteAssetDenom) return twap, nil } diff --git a/x/gamm/twap/export_test.go b/x/gamm/twap/export_test.go index 27c9835b077..b1a8cb4583c 100644 --- a/x/gamm/twap/export_test.go +++ b/x/gamm/twap/export_test.go @@ -28,6 +28,10 @@ func (k Keeper) GetChangedPools(ctx sdk.Context) []uint64 { return k.getChangedPools(ctx) } +func (k Keeper) UpdateRecord(ctx sdk.Context, record types.TwapRecord) (types.TwapRecord, error) { + return k.updateRecord(ctx, record) +} + func InterpolateRecord(record types.TwapRecord, t time.Time) types.TwapRecord { return interpolateRecord(record, t) } diff --git a/x/gamm/twap/keeper_test.go b/x/gamm/twap/keeper_test.go index eefc26dad6a..e62a84be998 100644 --- a/x/gamm/twap/keeper_test.go +++ b/x/gamm/twap/keeper_test.go @@ -4,6 +4,7 @@ import ( "testing" "time" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/suite" "github.com/osmosis-labs/osmosis/v10/app/apptesting" @@ -11,6 +12,8 @@ import ( "github.com/osmosis-labs/osmosis/v10/x/gamm/twap/types" ) +var defaultUniV2Coins = sdk.NewCoins(sdk.NewInt64Coin("token/B", 100), sdk.NewInt64Coin("token/A", 100)) + type TestSuite struct { apptesting.KeeperTestHelper twapkeeper *twap.Keeper diff --git a/x/gamm/twap/logic.go b/x/gamm/twap/logic.go index bca80cd71cd..bf9b64e2a40 100644 --- a/x/gamm/twap/logic.go +++ b/x/gamm/twap/logic.go @@ -18,7 +18,18 @@ func (k Keeper) afterCreatePool(ctx sdk.Context, poolId uint64) error { return err } -func (k Keeper) updateTWAPs(ctx sdk.Context, poolId uint64) error { +func (k Keeper) endBlock(ctx sdk.Context) { + changedPoolIds := k.getChangedPools(ctx) + for _, id := range changedPoolIds { + err := k.updateRecords(ctx, id) + if err != nil { + panic(err) + } + } + // 'altered pool ids' gets automatically cleared by being a transient store +} + +func (k Keeper) updateRecords(ctx sdk.Context, poolId uint64) error { // Will only err if pool doesn't have most recent entry set records, err := k.getAllMostRecentRecordsForPool(ctx, poolId) if err != nil { @@ -26,44 +37,44 @@ func (k Keeper) updateTWAPs(ctx sdk.Context, poolId uint64) error { } for _, record := range records { - timeDelta := ctx.BlockTime().Sub(record.Time) - - // no update if were in the same block. - // should be caught earlier, but secondary check. - if int(timeDelta) <= 0 { - return nil + newRecord, err := k.updateRecord(ctx, record) + if err != nil { + return err } + k.storeNewRecord(ctx, newRecord) + } + return nil +} - record.Height = ctx.BlockHeight() - record.Time = ctx.BlockTime() +// mutates record argument, but not with all the changes. +// Use the return value, and drop usage of the argument. +func (k Keeper) updateRecord(ctx sdk.Context, record types.TwapRecord) (types.TwapRecord, error) { + timeDelta := ctx.BlockTime().Sub(record.Time) - // TODO: Ensure order is correct - newSp0 := types.MustGetSpotPrice(k.ammkeeper, ctx, poolId, record.Asset0Denom, record.Asset1Denom) - newSp1 := types.MustGetSpotPrice(k.ammkeeper, ctx, poolId, record.Asset1Denom, record.Asset0Denom) + // no update if were in the same block. + // should be caught earlier, but secondary check. + if int(timeDelta) <= 0 { + return types.TwapRecord{}, nil + } - // TODO: Think about overflow - // Update accumulators based on last block / update's spot price - record.P0ArithmeticTwapAccumulator.AddMut(types.SpotPriceTimesDuration(record.P0LastSpotPrice, timeDelta)) - record.P1ArithmeticTwapAccumulator.AddMut(types.SpotPriceTimesDuration(record.P1LastSpotPrice, timeDelta)) + record.Height = ctx.BlockHeight() + record.Time = ctx.BlockTime() - // set last spot price to be last price of this block. This is what will get used in interpolation. - record.P0LastSpotPrice = newSp0 - record.P1LastSpotPrice = newSp1 + // TODO: Ensure order is correct + newSp0 := types.MustGetSpotPrice(k.ammkeeper, ctx, record.PoolId, record.Asset0Denom, record.Asset1Denom) + newSp1 := types.MustGetSpotPrice(k.ammkeeper, ctx, record.PoolId, record.Asset1Denom, record.Asset0Denom) - k.storeNewRecord(ctx, record) - } - return nil -} + // TODO: Think about overflow + // Update accumulators based on last block / update's spot price + // TODO: Combine this with interpolateRecord + record.P0ArithmeticTwapAccumulator.AddMut(types.SpotPriceTimesDuration(record.P0LastSpotPrice, timeDelta)) + record.P1ArithmeticTwapAccumulator.AddMut(types.SpotPriceTimesDuration(record.P1LastSpotPrice, timeDelta)) -func (k Keeper) endBlock(ctx sdk.Context) { - changedPoolIds := k.getChangedPools(ctx) - for _, id := range changedPoolIds { - err := k.updateTWAPs(ctx, id) - if err != nil { - panic(err) - } - } - // 'altered pool ids' gets automatically cleared by being a transient store + // set last spot price to be last price of this block. This is what will get used in interpolation. + record.P0LastSpotPrice = newSp0 + record.P1LastSpotPrice = newSp1 + + return record, nil } func (k Keeper) pruneOldTwaps(ctx sdk.Context) { @@ -119,10 +130,15 @@ func interpolateRecord(record types.TwapRecord, interpolateTime time.Time) types return interpolatedRecord } -// For now just assuming p0 price, TODO switch between the two +// TODO: Test math, test p0 vs p1 correctness // precondition: endRecord.Time > startRecord.Time -func (k Keeper) getArithmeticTwap(startRecord types.TwapRecord, endRecord types.TwapRecord) sdk.Dec { +func (k Keeper) getArithmeticTwap(startRecord types.TwapRecord, endRecord types.TwapRecord, quoteAsset string) sdk.Dec { timeDelta := endRecord.Time.Sub(startRecord.Time) - accumDiff := endRecord.P0ArithmeticTwapAccumulator.Sub(startRecord.P0ArithmeticTwapAccumulator) + var accumDiff sdk.Dec + if quoteAsset == startRecord.Asset0Denom { + accumDiff = endRecord.P0ArithmeticTwapAccumulator.Sub(startRecord.P0ArithmeticTwapAccumulator) + } else { + accumDiff = endRecord.P1ArithmeticTwapAccumulator.Sub(startRecord.P1ArithmeticTwapAccumulator) + } return types.AccumDiffDivDuration(accumDiff, timeDelta) } diff --git a/x/gamm/twap/logic_test.go b/x/gamm/twap/logic_test.go index 02f11c0296f..091f45b0ae7 100644 --- a/x/gamm/twap/logic_test.go +++ b/x/gamm/twap/logic_test.go @@ -14,25 +14,33 @@ import ( var zeroDec = sdk.ZeroDec() var oneDec = sdk.OneDec() var twoDec = oneDec.Add(oneDec) +var OneSec = sdk.NewDec(1e9) func newRecord(t time.Time, sp0, accum0, accum1 sdk.Dec) types.TwapRecord { return types.TwapRecord{ - Time: t, - P0LastSpotPrice: sp0, - P1LastSpotPrice: sdk.OneDec().Quo(sp0), - P0ArithmeticTwapAccumulator: accum0, - P1ArithmeticTwapAccumulator: accum1, + Asset0Denom: defaultUniV2Coins[0].Denom, + Asset1Denom: defaultUniV2Coins[1].Denom, + Time: t, + P0LastSpotPrice: sp0, + P1LastSpotPrice: sdk.OneDec().Quo(sp0), + // make new copies + P0ArithmeticTwapAccumulator: accum0.Add(sdk.ZeroDec()), + P1ArithmeticTwapAccumulator: accum1.Add(sdk.ZeroDec()), } } -func TestInterpolateRecord(t *testing.T) { - // make an expected record, we adjust other values in the test case. - newExpRecord := func(accum0, accum1 sdk.Dec) types.TwapRecord { - return types.TwapRecord{P0ArithmeticTwapAccumulator: accum0, - P1ArithmeticTwapAccumulator: accum1} +// make an expected record for math tests, we adjust other values in the test runner. +func newExpRecord(accum0, accum1 sdk.Dec) types.TwapRecord { + return types.TwapRecord{ + Asset0Denom: defaultUniV2Coins[0].Denom, + Asset1Denom: defaultUniV2Coins[1].Denom, + // make new copies + P0ArithmeticTwapAccumulator: accum0.Add(sdk.ZeroDec()), + P1ArithmeticTwapAccumulator: accum1.Add(sdk.ZeroDec()), } +} - OneSec := sdk.NewDec(1e9) +func TestInterpolateRecord(t *testing.T) { tests := map[string]struct { record types.TwapRecord interpolateTime time.Time @@ -73,3 +81,36 @@ func TestInterpolateRecord(t *testing.T) { }) } } + +func (s *TestSuite) TestUpdateTwap() { + poolId := s.PrepareUni2PoolWithAssets(defaultUniV2Coins[0], defaultUniV2Coins[1]) + newSp := sdk.OneDec() + + tests := map[string]struct { + record types.TwapRecord + updateTime time.Time + expRecord types.TwapRecord + }{ + "0 accum start": { + record: newRecord(time.Unix(1, 0), sdk.NewDec(10), zeroDec, zeroDec), + updateTime: time.Unix(2, 0), + expRecord: newExpRecord(OneSec.MulInt64(10), OneSec.QuoInt64(10)), + }, + } + for name, test := range tests { + s.Run(name, func() { + // setup common, block time, pool Id, expected spot prices + s.Ctx = s.Ctx.WithBlockTime(test.updateTime.UTC()) + test.record.PoolId = poolId + test.expRecord.PoolId = poolId + test.expRecord.P0LastSpotPrice = newSp + test.expRecord.P1LastSpotPrice = newSp + test.expRecord.Height = s.Ctx.BlockHeight() + test.expRecord.Time = s.Ctx.BlockTime() + + newRecord, err := s.twapkeeper.UpdateRecord(s.Ctx, test.record) + s.Require().NoError(err) + s.Require().Equal(test.expRecord, newRecord) + }) + } +} diff --git a/x/gamm/twap/types/utils.go b/x/gamm/twap/types/utils.go index 60b6d44a35a..fda20c5acea 100644 --- a/x/gamm/twap/types/utils.go +++ b/x/gamm/twap/types/utils.go @@ -31,8 +31,8 @@ func NewTwapRecord(k AmmInterface, ctx sdk.Context, poolId uint64, denom0 string // mustGetSpotPrice returns the spot price for the given pool id, and denom0 in terms of denom1. // Panics if the pool state is misconfigured, which will halt any tx that interacts with this. -func MustGetSpotPrice(k AmmInterface, ctx sdk.Context, poolId uint64, denom0 string, denom1 string) sdk.Dec { - sp, err := k.CalculateSpotPrice(ctx, poolId, denom0, denom1) +func MustGetSpotPrice(k AmmInterface, ctx sdk.Context, poolId uint64, baseAssetDenom string, quoteAssetDenom string) sdk.Dec { + sp, err := k.CalculateSpotPrice(ctx, poolId, baseAssetDenom, quoteAssetDenom) if err != nil { panic(err) } From 50d0891b5efc49d8134f512555bad86367d72e67 Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Thu, 21 Jul 2022 01:55:20 -0500 Subject: [PATCH 22/72] de-dup interpolate, update record logic --- x/gamm/twap/logic.go | 63 ++++++++++++++++++++------------------------ 1 file changed, 28 insertions(+), 35 deletions(-) diff --git a/x/gamm/twap/logic.go b/x/gamm/twap/logic.go index bf9b64e2a40..469b820d369 100644 --- a/x/gamm/twap/logic.go +++ b/x/gamm/twap/logic.go @@ -49,32 +49,41 @@ func (k Keeper) updateRecords(ctx sdk.Context, poolId uint64) error { // mutates record argument, but not with all the changes. // Use the return value, and drop usage of the argument. func (k Keeper) updateRecord(ctx sdk.Context, record types.TwapRecord) (types.TwapRecord, error) { - timeDelta := ctx.BlockTime().Sub(record.Time) + newRecord := recordWithUpdatedAccumulators(record, ctx.BlockTime()) - // no update if were in the same block. - // should be caught earlier, but secondary check. - if int(timeDelta) <= 0 { - return types.TwapRecord{}, nil - } - - record.Height = ctx.BlockHeight() - record.Time = ctx.BlockTime() + newRecord.Height = ctx.BlockHeight() // TODO: Ensure order is correct newSp0 := types.MustGetSpotPrice(k.ammkeeper, ctx, record.PoolId, record.Asset0Denom, record.Asset1Denom) newSp1 := types.MustGetSpotPrice(k.ammkeeper, ctx, record.PoolId, record.Asset1Denom, record.Asset0Denom) - // TODO: Think about overflow - // Update accumulators based on last block / update's spot price - // TODO: Combine this with interpolateRecord - record.P0ArithmeticTwapAccumulator.AddMut(types.SpotPriceTimesDuration(record.P0LastSpotPrice, timeDelta)) - record.P1ArithmeticTwapAccumulator.AddMut(types.SpotPriceTimesDuration(record.P1LastSpotPrice, timeDelta)) - // set last spot price to be last price of this block. This is what will get used in interpolation. - record.P0LastSpotPrice = newSp0 - record.P1LastSpotPrice = newSp1 + newRecord.P0LastSpotPrice = newSp0 + newRecord.P1LastSpotPrice = newSp1 - return record, nil + return newRecord, nil +} + +// interpolate record returns a record, with updated accumulator values and time for provided newTime. +// +// pre-condition: newTime >= record.Time +func recordWithUpdatedAccumulators(record types.TwapRecord, newTime time.Time) types.TwapRecord { + if record.Time.Equal(newTime) { + return record + } + newRecord := record + timeDelta := newTime.Sub(record.Time) + newRecord.Time = newTime + + // record.LastSpotPrice is the last spot price from the block the record was created in, + // thus it is treated as the effective spot price until the new time. + // (As there was no change until at or after this time) + // TODO: Think about overflow + newRecord.P0ArithmeticTwapAccumulator = newRecord.P0ArithmeticTwapAccumulator.Add( + types.SpotPriceTimesDuration(record.P0LastSpotPrice, timeDelta)) + newRecord.P1ArithmeticTwapAccumulator = newRecord.P1ArithmeticTwapAccumulator.Add( + types.SpotPriceTimesDuration(record.P1LastSpotPrice, timeDelta)) + return newRecord } func (k Keeper) pruneOldTwaps(ctx sdk.Context) { @@ -111,23 +120,7 @@ func (k Keeper) getMostRecentRecord(ctx sdk.Context, poolId uint64, assetA strin // // pre-condition: interpolateTime >= record.Time func interpolateRecord(record types.TwapRecord, interpolateTime time.Time) types.TwapRecord { - if record.Time.Equal(interpolateTime) { - return record - } - interpolatedRecord := record - timeDelta := interpolateTime.Sub(record.Time) - interpolatedRecord.Time = interpolateTime - - // record.LastSpotPrice is the last spot price from the block the record was created in, - // thus it is treated as the effective spot price at the interpolation time. - // (As there was no change until the next block began) - - interpolatedRecord.P0ArithmeticTwapAccumulator = interpolatedRecord.P0ArithmeticTwapAccumulator.Add( - types.SpotPriceTimesDuration(record.P0LastSpotPrice, timeDelta)) - interpolatedRecord.P1ArithmeticTwapAccumulator = interpolatedRecord.P1ArithmeticTwapAccumulator.Add( - types.SpotPriceTimesDuration(record.P1LastSpotPrice, timeDelta)) - - return interpolatedRecord + return recordWithUpdatedAccumulators(record, interpolateTime) } // TODO: Test math, test p0 vs p1 correctness From ee6bf3d644425ed393d4f67ceffd626a417fa3cd Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Thu, 21 Jul 2022 02:14:15 -0500 Subject: [PATCH 23/72] After swap hook trigger test --- app/apptesting/gamm.go | 18 ++++++++++++++++++ x/gamm/twap/export_test.go | 2 +- x/gamm/twap/hook_test.go | 30 ++++++++++++++++++------------ x/gamm/twap/keeper_test.go | 2 +- x/gamm/twap/logic.go | 9 +++------ x/gamm/twap/logic_test.go | 3 +-- 6 files changed, 42 insertions(+), 22 deletions(-) diff --git a/app/apptesting/gamm.go b/app/apptesting/gamm.go index f269249be34..a6de6def851 100644 --- a/app/apptesting/gamm.go +++ b/app/apptesting/gamm.go @@ -96,3 +96,21 @@ func (s *KeeperTestHelper) PrepareBalancerPoolWithPoolAsset(assets []balancer.Po s.NoError(err) return poolId } + +func (s *KeeperTestHelper) RunBasicSwap(poolId uint64) { + denoms, err := s.App.GAMMKeeper.GetPoolDenoms(s.Ctx, poolId) + s.Require().NoError(err) + + swapIn := sdk.NewCoins(sdk.NewCoin(denoms[0], sdk.NewInt(1000))) + s.FundAcc(s.TestAccs[0], swapIn) + + msg := gammtypes.MsgSwapExactAmountIn{ + Sender: string(s.TestAccs[0]), + Routes: []gammtypes.SwapAmountInRoute{{PoolId: poolId, TokenOutDenom: denoms[1]}}, + TokenIn: swapIn[0], + TokenOutMinAmount: sdk.ZeroInt(), + } + // TODO: switch to message + _, err = s.App.GAMMKeeper.SwapExactAmountIn(s.Ctx, s.TestAccs[0], poolId, msg.TokenIn, denoms[1], msg.TokenOutMinAmount) + s.Require().NoError(err) +} diff --git a/x/gamm/twap/export_test.go b/x/gamm/twap/export_test.go index b1a8cb4583c..15163a01488 100644 --- a/x/gamm/twap/export_test.go +++ b/x/gamm/twap/export_test.go @@ -28,7 +28,7 @@ func (k Keeper) GetChangedPools(ctx sdk.Context) []uint64 { return k.getChangedPools(ctx) } -func (k Keeper) UpdateRecord(ctx sdk.Context, record types.TwapRecord) (types.TwapRecord, error) { +func (k Keeper) UpdateRecord(ctx sdk.Context, record types.TwapRecord) types.TwapRecord { return k.updateRecord(ctx, record) } diff --git a/x/gamm/twap/hook_test.go b/x/gamm/twap/hook_test.go index 5027769f5db..ca1928068c8 100644 --- a/x/gamm/twap/hook_test.go +++ b/x/gamm/twap/hook_test.go @@ -1,24 +1,30 @@ package twap_test import ( - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/osmosis-labs/osmosis/v10/x/gamm/twap/types" ) // TestCreatePoolFlow tests that upon a pool being created, // we have made the correct store entries. -func (suite *TestSuite) TestCreateTwoAssetPoolFlow() { - poolLiquidity := sdk.NewCoins(sdk.NewInt64Coin("token/A", 100), sdk.NewInt64Coin("token/B", 100)) - poolId := suite.PrepareUni2PoolWithAssets(poolLiquidity[0], poolLiquidity[1]) +func (s *TestSuite) TestCreateTwoAssetPoolFlow() { + poolId := s.PrepareUni2PoolWithAssets(defaultUniV2Coins[0], defaultUniV2Coins[1]) + + expectedTwap := types.NewTwapRecord(s.App.GAMMKeeper, s.Ctx, poolId, "token/B", "token/A") - expectedTwap := types.NewTwapRecord(suite.App.GAMMKeeper, suite.Ctx, poolId, "token/B", "token/A") + twap, err := s.twapkeeper.GetMostRecentRecordStoreRepresentation(s.Ctx, poolId, "token/B", "token/A") + s.Require().NoError(err) + s.Require().Equal(expectedTwap, twap) + + twap, err = s.twapkeeper.GetRecordAtOrBeforeTime(s.Ctx, poolId, s.Ctx.BlockTime(), "token/B", "token/A") + s.Require().NoError(err) + s.Require().Equal(expectedTwap, twap) +} - twap, err := suite.twapkeeper.GetMostRecentRecordStoreRepresentation(suite.Ctx, poolId, "token/B", "token/A") - suite.Require().NoError(err) - suite.Require().Equal(expectedTwap, twap) +// Tests that after a swap, we are triggering internal tracking logic for a pool. +func (s *TestSuite) TestSwapTriggeringTrackPoolId() { + poolId := s.PrepareUni2PoolWithAssets(defaultUniV2Coins[0], defaultUniV2Coins[1]) + s.BeginNewBlock(false) + s.RunBasicSwap(poolId) - twap, err = suite.twapkeeper.GetRecordAtOrBeforeTime(suite.Ctx, poolId, suite.Ctx.BlockTime(), "token/B", "token/A") - suite.Require().NoError(err) - suite.Require().Equal(expectedTwap, twap) + s.Require().Equal([]uint64{poolId}, s.twapkeeper.GetChangedPools(s.Ctx)) } diff --git a/x/gamm/twap/keeper_test.go b/x/gamm/twap/keeper_test.go index e62a84be998..4d8165a0831 100644 --- a/x/gamm/twap/keeper_test.go +++ b/x/gamm/twap/keeper_test.go @@ -12,7 +12,7 @@ import ( "github.com/osmosis-labs/osmosis/v10/x/gamm/twap/types" ) -var defaultUniV2Coins = sdk.NewCoins(sdk.NewInt64Coin("token/B", 100), sdk.NewInt64Coin("token/A", 100)) +var defaultUniV2Coins = sdk.NewCoins(sdk.NewInt64Coin("token/B", 1_000_000_000), sdk.NewInt64Coin("token/A", 1_000_000_000)) type TestSuite struct { apptesting.KeeperTestHelper diff --git a/x/gamm/twap/logic.go b/x/gamm/twap/logic.go index 469b820d369..0e864508013 100644 --- a/x/gamm/twap/logic.go +++ b/x/gamm/twap/logic.go @@ -37,10 +37,7 @@ func (k Keeper) updateRecords(ctx sdk.Context, poolId uint64) error { } for _, record := range records { - newRecord, err := k.updateRecord(ctx, record) - if err != nil { - return err - } + newRecord := k.updateRecord(ctx, record) k.storeNewRecord(ctx, newRecord) } return nil @@ -48,7 +45,7 @@ func (k Keeper) updateRecords(ctx sdk.Context, poolId uint64) error { // mutates record argument, but not with all the changes. // Use the return value, and drop usage of the argument. -func (k Keeper) updateRecord(ctx sdk.Context, record types.TwapRecord) (types.TwapRecord, error) { +func (k Keeper) updateRecord(ctx sdk.Context, record types.TwapRecord) types.TwapRecord { newRecord := recordWithUpdatedAccumulators(record, ctx.BlockTime()) newRecord.Height = ctx.BlockHeight() @@ -61,7 +58,7 @@ func (k Keeper) updateRecord(ctx sdk.Context, record types.TwapRecord) (types.Tw newRecord.P0LastSpotPrice = newSp0 newRecord.P1LastSpotPrice = newSp1 - return newRecord, nil + return newRecord } // interpolate record returns a record, with updated accumulator values and time for provided newTime. diff --git a/x/gamm/twap/logic_test.go b/x/gamm/twap/logic_test.go index 091f45b0ae7..4e3dfb33463 100644 --- a/x/gamm/twap/logic_test.go +++ b/x/gamm/twap/logic_test.go @@ -108,8 +108,7 @@ func (s *TestSuite) TestUpdateTwap() { test.expRecord.Height = s.Ctx.BlockHeight() test.expRecord.Time = s.Ctx.BlockTime() - newRecord, err := s.twapkeeper.UpdateRecord(s.Ctx, test.record) - s.Require().NoError(err) + newRecord := s.twapkeeper.UpdateRecord(s.Ctx, test.record) s.Require().Equal(test.expRecord, newRecord) }) } From 7e94ca17e15bd4d20e1be340efe6b1135e1ecc8e Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Thu, 21 Jul 2022 02:19:57 -0500 Subject: [PATCH 24/72] types comments --- osmoutils/slice_helper.go | 2 ++ x/gamm/twap/types/expected_interfaces.go | 9 +++++---- x/gamm/twap/types/utils.go | 1 - 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/osmoutils/slice_helper.go b/osmoutils/slice_helper.go index 68134e66e64..2dc10c87603 100644 --- a/osmoutils/slice_helper.go +++ b/osmoutils/slice_helper.go @@ -24,6 +24,8 @@ func Filter[T interface{}](filter func(T) bool, s []T) []T { return filteredSlice } +// ReverseSlice returns a reversed copy of the input slice. +// Does not mutate argument. func ReverseSlice[T any](s []T) []T { newSlice := make([]T, len(s)) maxIndex := len(s) - 1 diff --git a/x/gamm/twap/types/expected_interfaces.go b/x/gamm/twap/types/expected_interfaces.go index 84c7549214d..0af93f75285 100644 --- a/x/gamm/twap/types/expected_interfaces.go +++ b/x/gamm/twap/types/expected_interfaces.go @@ -2,12 +2,13 @@ package types import sdk "github.com/cosmos/cosmos-sdk/types" -// CalculateSpotPrice returns the spot price of the quote asset in terms of the base asset, -// using the specified pool. -// E.g. if pool 1 traded 2 atom for 3 osmo, the quote asset was atom, and the base asset was osmo, -// this would return 1.5. (Meaning that 1 atom costs 1.5 osmo) +// AmmInterface is the functionality needed from a given pool ID, in order to maintain records and serve TWAPs. type AmmInterface interface { GetPoolDenoms(ctx sdk.Context, poolId uint64) (denoms []string, err error) + // CalculateSpotPrice returns the spot price of the quote asset in terms of the base asset, + // using the specified pool. + // E.g. if pool 1 traded 2 atom for 3 osmo, the quote asset was atom, and the base asset was osmo, + // this would return 1.5. (Meaning that 1 atom costs 1.5 osmo) CalculateSpotPrice(ctx sdk.Context, poolID uint64, baseAssetDenom string, diff --git a/x/gamm/twap/types/utils.go b/x/gamm/twap/types/utils.go index fda20c5acea..c266426bf4d 100644 --- a/x/gamm/twap/types/utils.go +++ b/x/gamm/twap/types/utils.go @@ -44,7 +44,6 @@ func MustGetSpotPrice(k AmmInterface, ctx sdk.Context, poolId uint64, baseAssetD // The pair (X,Y) should only appear once in the list // // NOTE: Sorts the input denoms slice. -// (Should not be a problem, as this should come from coins.Denoms(), which returns a sorted order) func GetAllUniqueDenomPairs(denoms []string) ([]string, []string) { // get denoms in descending order sort.Strings(denoms) From 73e9970229e7e2e467aaf2b221bbf91c11ddaade Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Thu, 21 Jul 2022 02:36:22 -0500 Subject: [PATCH 25/72] TestGetBeginBlockAccumulatorRecord scaffolding --- x/gamm/twap/api_test.go | 42 ++++++++++++++++++++++++++++++++++++++ x/gamm/twap/export_test.go | 4 ++++ 2 files changed, 46 insertions(+) create mode 100644 x/gamm/twap/api_test.go diff --git a/x/gamm/twap/api_test.go b/x/gamm/twap/api_test.go new file mode 100644 index 00000000000..0b387e694ce --- /dev/null +++ b/x/gamm/twap/api_test.go @@ -0,0 +1,42 @@ +package twap_test + +import ( + "github.com/osmosis-labs/osmosis/v10/x/gamm/twap/types" +) + +func (s *TestSuite) TestGetBeginBlockAccumulatorRecord() { + poolId := s.PrepareUni2PoolWithAssets(defaultUniV2Coins[0], defaultUniV2Coins[1]) + denomA := defaultUniV2Coins[0].Denom + denomB := defaultUniV2Coins[1].Denom + + tests := map[string]struct { + setRecords []types.TwapRecord + expRecord types.TwapRecord + poolId uint64 + quoteDenom string + baseDenom string + expError bool + }{ + "no record": {[]types.TwapRecord{}, types.TwapRecord{}, 4, denomA, denomB, true}, + } + for name, tc := range tests { + s.Run(name, func() { + // setup records + for _, r := range tc.setRecords { + // set pool id if not not provided + if r.PoolId == 0 { + r.PoolId = poolId + } + s.twapkeeper.StoreNewRecord(s.Ctx, r) + } + actualRecord, err := s.twapkeeper.GetBeginBlockAccumulatorRecord(s.Ctx, tc.poolId, tc.baseDenom, tc.quoteDenom) + if tc.expError { + s.Require().Error(err) + return + } else { + s.Require().NoError(err) + } + s.Require().Equal(tc.expRecord, actualRecord) + }) + } +} diff --git a/x/gamm/twap/export_test.go b/x/gamm/twap/export_test.go index 15163a01488..0bc5d81d550 100644 --- a/x/gamm/twap/export_test.go +++ b/x/gamm/twap/export_test.go @@ -8,6 +8,10 @@ import ( "github.com/osmosis-labs/osmosis/v10/x/gamm/twap/types" ) +func (k Keeper) StoreNewRecord(ctx sdk.Context, record types.TwapRecord) { + k.storeNewRecord(ctx, record) +} + func (k Keeper) GetMostRecentRecordStoreRepresentation(ctx sdk.Context, poolId uint64, asset0Denom string, asset1Denom string) (types.TwapRecord, error) { return k.getMostRecentRecordStoreRepresentation(ctx, poolId, asset0Denom, asset1Denom) } From f381788eb58b0da970fe288abc77b0f9cd78cc5d Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Thu, 21 Jul 2022 09:03:17 -0500 Subject: [PATCH 26/72] Complete TestGetBeginBlockAccumulatorRecord --- x/gamm/twap/api_test.go | 50 +++++++++++++++++++++++++++++--------- x/gamm/twap/keeper_test.go | 12 +++++++++ 2 files changed, 51 insertions(+), 11 deletions(-) diff --git a/x/gamm/twap/api_test.go b/x/gamm/twap/api_test.go index 0b387e694ce..c60f5d8415e 100644 --- a/x/gamm/twap/api_test.go +++ b/x/gamm/twap/api_test.go @@ -1,35 +1,63 @@ package twap_test import ( + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/osmosis-labs/osmosis/v10/x/gamm/twap/types" ) func (s *TestSuite) TestGetBeginBlockAccumulatorRecord() { poolId := s.PrepareUni2PoolWithAssets(defaultUniV2Coins[0], defaultUniV2Coins[1]) - denomA := defaultUniV2Coins[0].Denom - denomB := defaultUniV2Coins[1].Denom + denomA, denomB := defaultUniV2Coins[1].Denom, defaultUniV2Coins[0].Denom + initStartRecord := newRecord(s.Ctx.BlockTime(), sdk.OneDec(), sdk.ZeroDec(), sdk.ZeroDec()) + initStartRecord.PoolId, initStartRecord.Height = poolId, s.Ctx.BlockHeight() + initStartRecord.Asset0Denom, initStartRecord.Asset1Denom = denomA, denomB + + zeroAccumTenPoint1Record := recordWithUpdatedSpotPrice(initStartRecord, sdk.NewDec(10), sdk.NewDecWithPrec(1, 1)) + + blankRecord := types.TwapRecord{} + defaultTime := s.Ctx.BlockTime() + + tPlusOneSec := defaultTime.Add(time.Second) tests := map[string]struct { - setRecords []types.TwapRecord + // if start record is blank, don't do any sets + startRecord types.TwapRecord + // We set it to have the updated time expRecord types.TwapRecord + time time.Time poolId uint64 quoteDenom string baseDenom string expError bool }{ - "no record": {[]types.TwapRecord{}, types.TwapRecord{}, 4, denomA, denomB, true}, + "no record (wrong pool ID)": {blankRecord, blankRecord, defaultTime, 4, denomA, denomB, true}, + "default record": {blankRecord, initStartRecord, defaultTime, 1, denomA, denomB, false}, + "one second later record": {blankRecord, recordWithUpdatedAccum(initStartRecord, OneSec, OneSec), tPlusOneSec, 1, denomA, denomB, false}, + "idempotent overwrite": {initStartRecord, initStartRecord, defaultTime, 1, denomA, denomB, false}, + "idempotent overwrite2": {initStartRecord, recordWithUpdatedAccum(initStartRecord, OneSec, OneSec), tPlusOneSec, 1, denomA, denomB, false}, + "diff spot price": {zeroAccumTenPoint1Record, + recordWithUpdatedAccum(zeroAccumTenPoint1Record, OneSec.MulInt64(10), OneSec.QuoInt64(10)), + tPlusOneSec, 1, denomA, denomB, false}, + // TODO: Overflow } for name, tc := range tests { s.Run(name, func() { - // setup records - for _, r := range tc.setRecords { - // set pool id if not not provided - if r.PoolId == 0 { - r.PoolId = poolId - } - s.twapkeeper.StoreNewRecord(s.Ctx, r) + // setup time + s.Ctx = s.Ctx.WithBlockTime(tc.time) + tc.expRecord.Time = tc.time + + // setup record + initSetRecord := tc.startRecord + if (tc.startRecord == types.TwapRecord{}) { + initSetRecord = initStartRecord } + s.twapkeeper.StoreNewRecord(s.Ctx, initSetRecord) + actualRecord, err := s.twapkeeper.GetBeginBlockAccumulatorRecord(s.Ctx, tc.poolId, tc.baseDenom, tc.quoteDenom) + if tc.expError { s.Require().Error(err) return diff --git a/x/gamm/twap/keeper_test.go b/x/gamm/twap/keeper_test.go index 4d8165a0831..2a50264bac5 100644 --- a/x/gamm/twap/keeper_test.go +++ b/x/gamm/twap/keeper_test.go @@ -36,3 +36,15 @@ func newEmptyPriceRecord(poolId uint64, t time.Time, asset0 string, asset1 strin Asset1Denom: asset1, } } + +func recordWithUpdatedAccum(record types.TwapRecord, accum0 sdk.Dec, accum1 sdk.Dec) types.TwapRecord { + record.P0ArithmeticTwapAccumulator = accum0 + record.P1ArithmeticTwapAccumulator = accum1 + return record +} + +func recordWithUpdatedSpotPrice(record types.TwapRecord, sp0 sdk.Dec, sp1 sdk.Dec) types.TwapRecord { + record.P0LastSpotPrice = sp0 + record.P1LastSpotPrice = sp1 + return record +} From 21804e97b5f82aee919fb29f066f172a734f2704 Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Thu, 21 Jul 2022 09:16:15 -0500 Subject: [PATCH 27/72] Improve code comment, fix lint --- .markdownlint.yml | 2 ++ x/gamm/twap/api.go | 2 +- x/gamm/twap/api_test.go | 3 +-- x/gamm/twap/keeper_test.go | 13 ++++++++++--- x/gamm/twap/logic.go | 11 +++++++---- 5 files changed, 21 insertions(+), 10 deletions(-) diff --git a/.markdownlint.yml b/.markdownlint.yml index 7a7a4ae914d..e0f9e913d51 100644 --- a/.markdownlint.yml +++ b/.markdownlint.yml @@ -6,6 +6,8 @@ MD003: # Can't disable MD007 :/ # MD007: false MD009: false +MD010: + code_blocks: false MD013: code_blocks: false MD024: false \ No newline at end of file diff --git a/x/gamm/twap/api.go b/x/gamm/twap/api.go index 21723eae0be..0bc0992f624 100644 --- a/x/gamm/twap/api.go +++ b/x/gamm/twap/api.go @@ -40,7 +40,7 @@ func (k Keeper) GetArithmeticTwapToNow( quoteAssetDenom string, baseAssetDenom string, startTime time.Time) (sdk.Dec, error) { - startRecord, err := k.getStartRecord(ctx, poolId, startTime, quoteAssetDenom, baseAssetDenom) + startRecord, err := k.getInterpolatedRecord(ctx, poolId, startTime, quoteAssetDenom, baseAssetDenom) if err != nil { return sdk.Dec{}, err } diff --git a/x/gamm/twap/api_test.go b/x/gamm/twap/api_test.go index c60f5d8415e..77445399281 100644 --- a/x/gamm/twap/api_test.go +++ b/x/gamm/twap/api_test.go @@ -9,8 +9,7 @@ import ( ) func (s *TestSuite) TestGetBeginBlockAccumulatorRecord() { - poolId := s.PrepareUni2PoolWithAssets(defaultUniV2Coins[0], defaultUniV2Coins[1]) - denomA, denomB := defaultUniV2Coins[1].Denom, defaultUniV2Coins[0].Denom + poolId, denomA, denomB := s.setupDefaultPool() initStartRecord := newRecord(s.Ctx.BlockTime(), sdk.OneDec(), sdk.ZeroDec(), sdk.ZeroDec()) initStartRecord.PoolId, initStartRecord.Height = poolId, s.Ctx.BlockHeight() initStartRecord.Asset0Denom, initStartRecord.Asset1Denom = denomA, denomB diff --git a/x/gamm/twap/keeper_test.go b/x/gamm/twap/keeper_test.go index 2a50264bac5..d1a4ea43243 100644 --- a/x/gamm/twap/keeper_test.go +++ b/x/gamm/twap/keeper_test.go @@ -23,9 +23,16 @@ func TestSuiteRun(t *testing.T) { suite.Run(t, new(TestSuite)) } -func (suite *TestSuite) SetupTest() { - suite.Setup() - suite.twapkeeper = suite.App.TwapKeeper +func (s *TestSuite) SetupTest() { + s.Setup() + s.twapkeeper = s.App.TwapKeeper +} + +// sets up a new two asset pool, with spot price 1 +func (s *TestSuite) setupDefaultPool() (poolId uint64, denomA, denomB string) { + poolId = s.PrepareUni2PoolWithAssets(defaultUniV2Coins[0], defaultUniV2Coins[1]) + denomA, denomB = defaultUniV2Coins[1].Denom, defaultUniV2Coins[0].Denom + return } func newEmptyPriceRecord(poolId uint64, t time.Time, asset0 string, asset1 string) types.TwapRecord { diff --git a/x/gamm/twap/logic.go b/x/gamm/twap/logic.go index 0e864508013..df6f0f8bc3a 100644 --- a/x/gamm/twap/logic.go +++ b/x/gamm/twap/logic.go @@ -89,19 +89,22 @@ func (k Keeper) pruneOldTwaps(ctx sdk.Context) { k.pruneRecordsBeforeTime(ctx, lastAllowedTime) } -func (k Keeper) getStartRecord(ctx sdk.Context, poolId uint64, time time.Time, assetA string, assetB string) (types.TwapRecord, error) { +// getInterpolatedRecord returns a record for this pool, representing its accumulator state at time `t`. +// This is achieved by getting the record `r` that is at, or immediately preceding in state time `t`. +// To be clear: the record r s.t. `t - r.Time` is minimized AND `t >= r.Time` +func (k Keeper) getInterpolatedRecord(ctx sdk.Context, poolId uint64, t time.Time, assetA, assetB string) (types.TwapRecord, error) { if !(assetA > assetB) { assetA, assetB = assetB, assetA } - record, err := k.getRecordAtOrBeforeTime(ctx, poolId, time, assetA, assetB) + record, err := k.getRecordAtOrBeforeTime(ctx, poolId, t, assetA, assetB) if err != nil { return types.TwapRecord{}, err } - record = interpolateRecord(record, time) + record = interpolateRecord(record, t) return record, nil } -func (k Keeper) getMostRecentRecord(ctx sdk.Context, poolId uint64, assetA string, assetB string) (types.TwapRecord, error) { +func (k Keeper) getMostRecentRecord(ctx sdk.Context, poolId uint64, assetA, assetB string) (types.TwapRecord, error) { if !(assetA > assetB) { assetA, assetB = assetB, assetA } From 7936a5e4412659e9c05e933a13fcf7f1123917db Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Thu, 21 Jul 2022 09:59:25 -0500 Subject: [PATCH 28/72] Compute Arithmetic Twap test casess --- osmoutils/dec_helper.go | 2 ++ x/gamm/twap/api.go | 14 ++++++++-- x/gamm/twap/api_test.go | 3 +- x/gamm/twap/export_test.go | 4 +++ x/gamm/twap/logic.go | 9 +++++- x/gamm/twap/logic_test.go | 57 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 83 insertions(+), 6 deletions(-) diff --git a/osmoutils/dec_helper.go b/osmoutils/dec_helper.go index bc5c053d074..84e90af1636 100644 --- a/osmoutils/dec_helper.go +++ b/osmoutils/dec_helper.go @@ -6,6 +6,8 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ) +var OneThird sdk.Dec = sdk.MustNewDecFromStr("3.333333333333333333") + // intended to be used with require/assert: require.True(DecEq(...)) // TODO: Replace with function in SDK types package when we update func DecApproxEq(t *testing.T, d1 sdk.Dec, d2 sdk.Dec, tol sdk.Dec) (*testing.T, bool, string, string, string) { diff --git a/x/gamm/twap/api.go b/x/gamm/twap/api.go index 0bc0992f624..79948cb9b8d 100644 --- a/x/gamm/twap/api.go +++ b/x/gamm/twap/api.go @@ -30,8 +30,16 @@ func (k Keeper) GetArithmeticTwap( if endTime.Equal(ctx.BlockTime()) { return k.GetArithmeticTwapToNow(ctx, poolId, quoteAssetDenom, baseAssetDenom, startTime) } - // startTwapRecord, err := k.getTwapBeforeTime(ctx, poolId, startTime, quoteAssetDenom, baseAssetDenom) - return sdk.Dec{}, nil + startRecord, err := k.getInterpolatedRecord(ctx, poolId, startTime, quoteAssetDenom, baseAssetDenom) + if err != nil { + return sdk.Dec{}, err + } + endRecord, err := k.getInterpolatedRecord(ctx, poolId, endTime, quoteAssetDenom, baseAssetDenom) + if err != nil { + return sdk.Dec{}, err + } + twap := computeArithmeticTwap(startRecord, endRecord, quoteAssetDenom) + return twap, nil } func (k Keeper) GetArithmeticTwapToNow( @@ -48,7 +56,7 @@ func (k Keeper) GetArithmeticTwapToNow( if err != nil { return sdk.Dec{}, err } - twap := k.getArithmeticTwap(startRecord, endRecord, quoteAssetDenom) + twap := computeArithmeticTwap(startRecord, endRecord, quoteAssetDenom) return twap, nil } diff --git a/x/gamm/twap/api_test.go b/x/gamm/twap/api_test.go index 77445399281..e1212d926b3 100644 --- a/x/gamm/twap/api_test.go +++ b/x/gamm/twap/api_test.go @@ -60,9 +60,8 @@ func (s *TestSuite) TestGetBeginBlockAccumulatorRecord() { if tc.expError { s.Require().Error(err) return - } else { - s.Require().NoError(err) } + s.Require().NoError(err) s.Require().Equal(tc.expRecord, actualRecord) }) } diff --git a/x/gamm/twap/export_test.go b/x/gamm/twap/export_test.go index 0bc5d81d550..fbdfc15e9a0 100644 --- a/x/gamm/twap/export_test.go +++ b/x/gamm/twap/export_test.go @@ -36,6 +36,10 @@ func (k Keeper) UpdateRecord(ctx sdk.Context, record types.TwapRecord) types.Twa return k.updateRecord(ctx, record) } +func ComputeArithmeticTwap(startRecord types.TwapRecord, endRecord types.TwapRecord, quoteAsset string) sdk.Dec { + return computeArithmeticTwap(startRecord, endRecord, quoteAsset) +} + func InterpolateRecord(record types.TwapRecord, t time.Time) types.TwapRecord { return interpolateRecord(record, t) } diff --git a/x/gamm/twap/logic.go b/x/gamm/twap/logic.go index df6f0f8bc3a..48635de0b48 100644 --- a/x/gamm/twap/logic.go +++ b/x/gamm/twap/logic.go @@ -125,8 +125,15 @@ func interpolateRecord(record types.TwapRecord, interpolateTime time.Time) types // TODO: Test math, test p0 vs p1 correctness // precondition: endRecord.Time > startRecord.Time -func (k Keeper) getArithmeticTwap(startRecord types.TwapRecord, endRecord types.TwapRecord, quoteAsset string) sdk.Dec { +func computeArithmeticTwap(startRecord types.TwapRecord, endRecord types.TwapRecord, quoteAsset string) sdk.Dec { timeDelta := endRecord.Time.Sub(startRecord.Time) + // if time difference is 0, then return the last spot price based off of start. + if timeDelta == time.Duration(0) { + if quoteAsset == startRecord.Asset0Denom { + return startRecord.P0LastSpotPrice + } + return startRecord.P1LastSpotPrice + } var accumDiff sdk.Dec if quoteAsset == startRecord.Asset0Denom { accumDiff = endRecord.P0ArithmeticTwapAccumulator.Sub(startRecord.P0ArithmeticTwapAccumulator) diff --git a/x/gamm/twap/logic_test.go b/x/gamm/twap/logic_test.go index 4e3dfb33463..5baddec0b1a 100644 --- a/x/gamm/twap/logic_test.go +++ b/x/gamm/twap/logic_test.go @@ -7,6 +7,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" + "github.com/osmosis-labs/osmosis/v10/osmoutils" "github.com/osmosis-labs/osmosis/v10/x/gamm/twap" "github.com/osmosis-labs/osmosis/v10/x/gamm/twap/types" ) @@ -113,3 +114,59 @@ func (s *TestSuite) TestUpdateTwap() { }) } } + +func TestComputeArithmeticTwap(t *testing.T) { + denom0, denom1 := "token/B", "token/A" + newOneSidedRecord := func(time time.Time, accum sdk.Dec, useP0 bool) types.TwapRecord { + record := types.TwapRecord{Time: time, Asset0Denom: denom0, Asset1Denom: denom1} + if useP0 { + record.P0ArithmeticTwapAccumulator = accum + } else { + record.P1ArithmeticTwapAccumulator = accum + } + return record + } + + type testCase struct { + startRecord types.TwapRecord + endRecord types.TwapRecord + quoteAsset string + expTwap sdk.Dec + } + + baseTime := time.Unix(1257894000, 0).UTC() + + testCaseFromDeltas := func(startAccum, accumDiff sdk.Dec, timeDelta time.Duration, expectedTwap sdk.Dec) testCase { + return testCase{ + newOneSidedRecord(baseTime, startAccum, true), + newOneSidedRecord(baseTime.Add(timeDelta), startAccum.Add(accumDiff), true), + denom0, + expectedTwap, + } + } + plusOneSec := baseTime.Add(time.Second) + tests := map[string]testCase{ + "basic: spot price = 1 for one second, 0 init accumulator": { + newOneSidedRecord(baseTime, sdk.ZeroDec(), true), + newOneSidedRecord(plusOneSec, OneSec, true), + denom0, + sdk.OneDec(), + }, + // "same record: denom0, unset spot price": { + // newOneSidedRecord(baseTime, sdk.ZeroDec(), true), + // newOneSidedRecord(baseTime, sdk.ZeroDec(), true), + // denom0, + // nil, + // }, + "accumulator = 10*OneSec, t=5s. 0 base accum": testCaseFromDeltas(sdk.ZeroDec(), OneSec.MulInt64(10), 5*time.Second, sdk.NewDec(2)), + "accumulator = 10*OneSec, t=3s. 0 base accum": testCaseFromDeltas(sdk.ZeroDec(), OneSec.MulInt64(10), 3*time.Second, osmoutils.OneThird), + "accumulator = 10*OneSec, t=100s. 0 base accum": testCaseFromDeltas(sdk.ZeroDec(), OneSec.MulInt64(10), 100*time.Second, sdk.NewDecWithPrec(1, 1)), + // TODO: Overflow, rounding, same record tests + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + actualTwap := twap.ComputeArithmeticTwap(test.startRecord, test.endRecord, test.quoteAsset) + require.Equal(t, test.expTwap, actualTwap) + }) + } +} From 27b9392c5216b26fe4c39ca4635cd8add1f0ccc9 Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Thu, 21 Jul 2022 12:24:00 -0500 Subject: [PATCH 29/72] Add Arithmetic TWAP computation method & testing methodology to doc --- x/gamm/twap/README.md | 45 +++++++++++++++++++++++++++++++++++++++-- x/gamm/twap/api.go | 2 +- x/gamm/twap/api_test.go | 20 ++++++++++++++++++ 3 files changed, 64 insertions(+), 3 deletions(-) diff --git a/x/gamm/twap/README.md b/x/gamm/twap/README.md index 48b05656e21..180ab754b7c 100644 --- a/x/gamm/twap/README.md +++ b/x/gamm/twap/README.md @@ -1,6 +1,18 @@ -# TWAP +# TWAP (Time Weighted Average Price) -We maintain TWAP entries for every gamm pool. +The TWAP package is responsible for being able to serve TWAPs for every AMM pool. + +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. + +## Arithmetic mean TWAP + +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)$. (Notice that the latest price `p_n` isn't used, as it has lasted for a time interval of `0` seconds in this range!) + +To illustrate with an example, given the sequence: `(0s, $1), (2s, $5), (5s, $1)`, the arithmetic mean TWAP is: $\frac{\$1 * (2s - 0s) + \$5 * (5s - 3s)}{5s - 0s} = \frac{\$10}{5} = \$2$. + +## Computation via accumulators method + +The prior example for how to compute the TWAP takes linear time, which is unsuitable for use in a blockchain setting. ## Module API @@ -40,3 +52,32 @@ func (k Keeper) GetArithmeticTwap(ctx sdk.Context, ## Store layout Every pool has a TWAP stored in state for every asset pair. + +## Testing Methodology + +The pre-release testing methodology planned for the twap module is: + +- [ ] Using table driven unit tests to test all foreseen cases the module can be within + - hook testing + - All swaps correctly trigger twap record updates + - Create pools cause records to be created + - store + - EndBlock triggers all relevant twaps to be saved correctly + - Block commit wipes temporary stores + - logic + - Make tables of expected input / output cases for: + - getMostRecentRecord + - getInterpolatedRecord + - updateRecord + - computeArithmeticTwap + - Test overflow handling in all relevant arithmetic + - Complete testing code coverage (up to return err lines) for logic.go file + - API + - Unit tests for the public API, under foreseeable setup conditions +- [ ] Integration into the Osmosis simulator + - 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. +- [ ] Mutation testing usage + - integration of the TWAP module into go mutation testing: https://github.com/osmosis-labs/go-mutesting + - The success we've seen with the tokenfactory module, is it succeeds at surfacing behavior for untested behavior. + e.g. if you delete a line, or change the direction of a conditional, does a test catch it. + - 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. \ No newline at end of file diff --git a/x/gamm/twap/api.go b/x/gamm/twap/api.go index 79948cb9b8d..e80729e86ac 100644 --- a/x/gamm/twap/api.go +++ b/x/gamm/twap/api.go @@ -14,7 +14,7 @@ import ( // * from (startTime, endTime), // * as determined by prices from AMM pool `poolId`. // -// The +// The way the arithmetic twap works // // startTime and endTime do not have to be real block times that occurred, // this function will interpolate between startTime. diff --git a/x/gamm/twap/api_test.go b/x/gamm/twap/api_test.go index e1212d926b3..dffcc60c872 100644 --- a/x/gamm/twap/api_test.go +++ b/x/gamm/twap/api_test.go @@ -66,3 +66,23 @@ func (s *TestSuite) TestGetBeginBlockAccumulatorRecord() { }) } } + +// func (s *TestSuite) TestGetArithmeticTwapToNow() { +// tests := map[string]struct { +// // if start record is blank, don't do any sets +// setupRecords []types.TwapRecord +// latestRecord types.TwapRecord +// // We set it to have the updated time +// expRecord types.TwapRecord +// time time.Time +// poolId uint64 +// quoteDenom string +// baseDenom string +// expError bool +// }{} +// for name, tc := range tests { +// s.Run(name, func() { + +// }) +// } +// } From ab320cc0987aa59e7efc29ca580c1d74236c0657 Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Thu, 21 Jul 2022 13:42:28 -0500 Subject: [PATCH 30/72] More documentation updates --- x/gamm/twap/README.md | 35 +++++++++++++++++++++++++++-------- x/gamm/twap/api.go | 23 ++++++++++++++++++----- 2 files changed, 45 insertions(+), 13 deletions(-) diff --git a/x/gamm/twap/README.md b/x/gamm/twap/README.md index 180ab754b7c..cb04c210abe 100644 --- a/x/gamm/twap/README.md +++ b/x/gamm/twap/README.md @@ -12,10 +12,17 @@ To illustrate with an example, given the sequence: `(0s, $1), (2s, $5), (5s, $1) ## Computation via accumulators method -The prior example for how to compute the TWAP takes linear time, which is unsuitable for use in a blockchain setting. +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). + +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 $a_n = \sum_{i=0}^{n-1} p_i (t_{i+1} - t_i)$. 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. + +In Osmosis, we maintain accumulator records for every pool, for the last 48 hours. We also maintain within each accumulator record in state, the latest spot price. This allows us to interpolate accumulation records between times. Namely, if I want the twap from `t=10s` to `t=15s`, but the time records are at `9s, 13s, 17s`, this is fine. Using the latest spot price in each record, we create the accumulator value for `t=10` by computing `a_10 = a_9 + a_9_latest_spot_price * (10s - 9s)`, and `a_15 = a_13 + a_13_latest_spot_price * (15s - 13s)`. Given these interpolated accumulation values, we can compute the TWAP as before. + ## Module API +The primary intended API is `GetArithmeticTwap`, which is documented below, and has a similar cosmwasm binding. + ```go // GetArithmeticTwap returns an arithmetic time weighted average price. // The returned twap is the time weighted average price (TWAP) of: @@ -23,21 +30,32 @@ The prior example for how to compute the TWAP takes linear time, which is unsuit // * from (startTime, endTime), // * as determined by prices from AMM pool `poolId`. // -// The -// // startTime and endTime do not have to be real block times that occurred, -// this function will interpolate between startTime. -// if endTime = now, we do {X} -// startTime must be in time range {X}, recommended parameterization for mainnet is {Y} +// the state machine will interpolate the accumulator values for those times +// from the latest Twap accumulation record prior to the provided time. +// +// startTime must be within 48 hours of ctx.BlockTime(), if you need older TWAPs, +// you will have to maintain the accumulator yourself. +// +// This function will error if: +// * startTime > endTime +// * endTime in the future +// * startTime older than 48 hours OR pool creation +// * pool with id poolId does not exist, or does not contain quoteAssetDenom, baseAssetDenom +// +// N.B. If there is a notable use case, the state machine could maintain more historical records, e.g. at one per hour. func (k Keeper) GetArithmeticTwap(ctx sdk.Context, poolId uint64, baseAssetDenom string, quoteAssetDenom string, startTime time.Time, endTime time.Time) (sdk.Dec, error) { ``` -## File layout +There are convenience methods for `GetArithmeticTwapToNow` which sets `endTime = ctx.BlockTime()`, and has minor gas reduction. +For users who need TWAPs outside the 48 hours stored in the state machine, you can get the latest accumulation store record from `GetBeginBlockAccumulatorRecord` + +## Code layout -**api.go** is the main file you should look at for what you should depend upon. +**api.go** is the main file you should look at as a user of this module. **logic.go** is the main file you should look at for how the TWAP implementation works. @@ -76,6 +94,7 @@ The pre-release testing methodology planned for the twap module is: - Unit tests for the public API, under foreseeable setup conditions - [ ] Integration into the Osmosis simulator - 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. + - Not currently in scope for release blocking, but planned: Integration for gas tracking, to ensure gas of reads/writes does not grow with time. - [ ] Mutation testing usage - integration of the TWAP module into go mutation testing: https://github.com/osmosis-labs/go-mutesting - The success we've seen with the tokenfactory module, is it succeeds at surfacing behavior for untested behavior. diff --git a/x/gamm/twap/api.go b/x/gamm/twap/api.go index e80729e86ac..8c24791b05d 100644 --- a/x/gamm/twap/api.go +++ b/x/gamm/twap/api.go @@ -1,6 +1,7 @@ package twap import ( + "errors" "time" sdk "github.com/cosmos/cosmos-sdk/types" @@ -14,12 +15,20 @@ import ( // * from (startTime, endTime), // * as determined by prices from AMM pool `poolId`. // -// The way the arithmetic twap works -// // startTime and endTime do not have to be real block times that occurred, -// this function will interpolate between startTime. -// if endTime = now, we do {X} -// startTime must be in time range {X}, recommended parameterization for mainnet is {Y} +// the state machine will interpolate the accumulator values for those times +// from the latest Twap accumulation record prior to the provided time. +// +// startTime must be within 48 hours of ctx.BlockTime(), if you need older TWAPs, +// you will have to maintain the accumulator yourself. +// +// This function will error if: +// * startTime > endTime +// * endTime in the future +// * startTime older than 48 hours OR pool creation +// * pool with id poolId does not exist, or does not contain quoteAssetDenom, baseAssetDenom +// +// N.B. If there is a notable use case, the state machine could maintain more historical records, e.g. at one per hour. func (k Keeper) GetArithmeticTwap( ctx sdk.Context, poolId uint64, @@ -29,6 +38,10 @@ func (k Keeper) GetArithmeticTwap( endTime time.Time) (sdk.Dec, error) { if endTime.Equal(ctx.BlockTime()) { return k.GetArithmeticTwapToNow(ctx, poolId, quoteAssetDenom, baseAssetDenom, startTime) + } else if endTime.After(ctx.BlockTime()) { + return sdk.Dec{}, errors.New("called GetArithmeticTwap with an end time in the future") + } else if startTime.After(endTime) { + return sdk.Dec{}, errors.New("called GetArithmeticTwap with a start time that is after the end time") } startRecord, err := k.getInterpolatedRecord(ctx, poolId, startTime, quoteAssetDenom, baseAssetDenom) if err != nil { From 65ab26a54c8fd9f91a752a65b0321009cbb52726 Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Thu, 21 Jul 2022 13:51:24 -0500 Subject: [PATCH 31/72] Add migration test note --- x/gamm/twap/README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/x/gamm/twap/README.md b/x/gamm/twap/README.md index cb04c210abe..027f4a65f96 100644 --- a/x/gamm/twap/README.md +++ b/x/gamm/twap/README.md @@ -25,7 +25,7 @@ The primary intended API is `GetArithmeticTwap`, which is documented below, and ```go // GetArithmeticTwap returns an arithmetic time weighted average price. -// The returned twap is the time weighted average price (TWAP) of: +// The returned twap is the time weighted average price (TWAP), using the arithmetic mean, of: // * the base asset, in units of the quote asset (1 unit of base = x units of quote) // * from (startTime, endTime), // * as determined by prices from AMM pool `poolId`. @@ -51,7 +51,7 @@ func (k Keeper) GetArithmeticTwap(ctx sdk.Context, ``` There are convenience methods for `GetArithmeticTwapToNow` which sets `endTime = ctx.BlockTime()`, and has minor gas reduction. -For users who need TWAPs outside the 48 hours stored in the state machine, you can get the latest accumulation store record from `GetBeginBlockAccumulatorRecord` +For users who need TWAPs outside the 48 hours stored in the state machine, you can get the latest accumulation store record from `GetBeginBlockAccumulatorRecord`. ## Code layout @@ -69,13 +69,13 @@ For users who need TWAPs outside the 48 hours stored in the state machine, you c ## Store layout -Every pool has a TWAP stored in state for every asset pair. +We maintain twap accumulation records for every AMM pool on Osmosis. ## Testing Methodology The pre-release testing methodology planned for the twap module is: -- [ ] Using table driven unit tests to test all foreseen cases the module can be within +- [ ] Using table driven unit tests to test all foreseen states of the module - hook testing - All swaps correctly trigger twap record updates - Create pools cause records to be created @@ -92,6 +92,8 @@ The pre-release testing methodology planned for the twap module is: - Complete testing code coverage (up to return err lines) for logic.go file - API - Unit tests for the public API, under foreseeable setup conditions +- [ ] End to end migration tests + - Tests that migration of Osmosis pools created prior to the TWAP upgrade, get TWAPs recorded starting at the v11 upgrade. - [ ] Integration into the Osmosis simulator - 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. - Not currently in scope for release blocking, but planned: Integration for gas tracking, to ensure gas of reads/writes does not grow with time. From d54b266ae360bd7869b3505587ffbcc901538397 Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Thu, 21 Jul 2022 14:00:25 -0500 Subject: [PATCH 32/72] disable some lints --- .markdownlint.yml | 5 +++-- x/gamm/twap/README.md | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.markdownlint.yml b/.markdownlint.yml index e0f9e913d51..85174e573cf 100644 --- a/.markdownlint.yml +++ b/.markdownlint.yml @@ -4,10 +4,11 @@ MD003: # Heading style style: "atx" # Can't disable MD007 :/ -# MD007: false +MD007: false MD009: false MD010: code_blocks: false MD013: code_blocks: false -MD024: false \ No newline at end of file +MD024: false +MD037: false # breaks on latex \ No newline at end of file diff --git a/x/gamm/twap/README.md b/x/gamm/twap/README.md index 027f4a65f96..a18a0d867a6 100644 --- a/x/gamm/twap/README.md +++ b/x/gamm/twap/README.md @@ -69,7 +69,7 @@ For users who need TWAPs outside the 48 hours stored in the state machine, you c ## Store layout -We maintain twap accumulation records for every AMM pool on Osmosis. +We maintain twap accumulation records for every AMM pool on Osmosis. Because Osmosis supports multi-asset pools, a complicating factor is that ## Testing Methodology From 9c0d6e5bdaeddb29f6b168f9584e1584ec3c1500 Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Thu, 21 Jul 2022 14:28:16 -0500 Subject: [PATCH 33/72] Add store layout docs --- app/modules.go | 2 +- x/gamm/twap/README.md | 27 +++++++++++++++++++++++++-- x/gamm/twap/hook_test.go | 2 ++ x/gamm/twap/logic.go | 1 + 4 files changed, 29 insertions(+), 3 deletions(-) diff --git a/app/modules.go b/app/modules.go index 740fd36bb24..382131bbb71 100644 --- a/app/modules.go +++ b/app/modules.go @@ -168,7 +168,7 @@ func orderBeginBlockers(allModuleNames []string) []string { // OrderEndBlockers returns EndBlockers (crisis, govtypes, staking) with no relative order. func OrderEndBlockers(allModuleNames []string) []string { ord := partialord.NewPartialOrdering(allModuleNames) - // only Osmosis modules with endblock code are: crisis, govtypes, staking + // only Osmosis modules with endblock code are: twap, crisis, govtypes, staking // we don't care about the relative ordering between them. return ord.TotalOrdering() } diff --git a/x/gamm/twap/README.md b/x/gamm/twap/README.md index a18a0d867a6..0e064094fe7 100644 --- a/x/gamm/twap/README.md +++ b/x/gamm/twap/README.md @@ -69,7 +69,30 @@ For users who need TWAPs outside the 48 hours stored in the state machine, you c ## Store layout -We maintain twap accumulation records for every AMM pool on Osmosis. Because Osmosis supports multi-asset pools, a complicating factor is that +We maintain twap accumulation records for every AMM pool on Osmosis. + +Because Osmosis supports multi-asset pools, a complicating factor is that we have to store a record for every asset pair in the pool. +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`. + +Each twap record stores [(source)](https://github.com/osmosis-labs/osmosis/tree/main/proto/osmosis/gamm/twap): +* last spot price of base asset A in terms of quote asset B +* last spot price of base asset B in terms of quote asset A +* Accumulation value of base asset A in terms of quote asset B +* Accumulation value of base asset B in terms of quote asset A + +All TWAP records are indexed in state by the time of write. + +A new TWAP record is created in two situations: +* When a pool is created +* In the `EndBlock`, if the block contains any potentially price changing event for the pool. (Swap, LP, Exit) + +When a pool is created, records are created with the current spot price of the pool. + +During EndBlock, new records are created, with: +* The accumulator's updated based upon the most recent prior accumulator's stored last spot price +* The LastSpotPrice's equal to the EndBlock spot price. + +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. ## Testing Methodology @@ -98,7 +121,7 @@ The pre-release testing methodology planned for the twap module is: - 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. - Not currently in scope for release blocking, but planned: Integration for gas tracking, to ensure gas of reads/writes does not grow with time. - [ ] Mutation testing usage - - integration of the TWAP module into go mutation testing: https://github.com/osmosis-labs/go-mutesting + - integration of the TWAP module into [go mutation testing](https://github.com/osmosis-labs/go-mutesting): - The success we've seen with the tokenfactory module, is it succeeds at surfacing behavior for untested behavior. e.g. if you delete a line, or change the direction of a conditional, does a test catch it. - 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. \ No newline at end of file diff --git a/x/gamm/twap/hook_test.go b/x/gamm/twap/hook_test.go index ca1928068c8..cc0105a556b 100644 --- a/x/gamm/twap/hook_test.go +++ b/x/gamm/twap/hook_test.go @@ -20,6 +20,8 @@ func (s *TestSuite) TestCreateTwoAssetPoolFlow() { s.Require().Equal(expectedTwap, twap) } +// TODO: Write test for create pool, swap, and EndBlock in same block, we use post-swap spot price in record. + // Tests that after a swap, we are triggering internal tracking logic for a pool. func (s *TestSuite) TestSwapTriggeringTrackPoolId() { poolId := s.PrepareUni2PoolWithAssets(defaultUniV2Coins[0], defaultUniV2Coins[1]) diff --git a/x/gamm/twap/logic.go b/x/gamm/twap/logic.go index 48635de0b48..589f3eb2efa 100644 --- a/x/gamm/twap/logic.go +++ b/x/gamm/twap/logic.go @@ -15,6 +15,7 @@ func (k Keeper) afterCreatePool(ctx sdk.Context, poolId uint64) error { record := types.NewTwapRecord(k.ammkeeper, ctx, poolId, denomPairs0[i], denomPairs1[i]) k.storeNewRecord(ctx, record) } + k.trackChangedPool(ctx, poolId) return err } From 6e16a814284742ec1bd0dd1d903ec32a084958a5 Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Thu, 21 Jul 2022 14:34:37 -0500 Subject: [PATCH 34/72] Fix numbers in the example --- x/gamm/twap/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x/gamm/twap/README.md b/x/gamm/twap/README.md index 0e064094fe7..95e49490b05 100644 --- a/x/gamm/twap/README.md +++ b/x/gamm/twap/README.md @@ -8,7 +8,7 @@ A time weighted average price is a function that takes a sequence of `(time, pri 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)$. (Notice that the latest price `p_n` isn't used, as it has lasted for a time interval of `0` seconds in this range!) -To illustrate with an example, given the sequence: `(0s, $1), (2s, $5), (5s, $1)`, the arithmetic mean TWAP is: $\frac{\$1 * (2s - 0s) + \$5 * (5s - 3s)}{5s - 0s} = \frac{\$10}{5} = \$2$. +To illustrate with an example, given the sequence: `(0s, $1), (4s, $6), (5s, $1)`, the arithmetic mean TWAP is: $\frac{\$1 * (4s - 0s) + \$6 * (5s - 4s)}{5s - 0s} = \frac{\$10}{5} = \$2$. ## Computation via accumulators method @@ -47,7 +47,7 @@ The primary intended API is `GetArithmeticTwap`, which is documented below, and func (k Keeper) GetArithmeticTwap(ctx sdk.Context, poolId uint64, baseAssetDenom string, quoteAssetDenom string, - startTime time.Time, endTime time.Time) (sdk.Dec, error) { + startTime time.Time, endTime time.Time) (sdk.Dec, error) { ... } ``` There are convenience methods for `GetArithmeticTwapToNow` which sets `endTime = ctx.BlockTime()`, and has minor gas reduction. From e7ed1b234bef0c9d0d4a4c1a28ff69fda4b85a88 Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Thu, 21 Jul 2022 14:36:02 -0500 Subject: [PATCH 35/72] Apply suggestions from code review Co-authored-by: Roman --- x/gamm/twap/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/x/gamm/twap/README.md b/x/gamm/twap/README.md index 95e49490b05..7913ad78416 100644 --- a/x/gamm/twap/README.md +++ b/x/gamm/twap/README.md @@ -14,7 +14,7 @@ To illustrate with an example, given the sequence: `(0s, $1), (4s, $6), (5s, $1) 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). -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 $a_n = \sum_{i=0}^{n-1} p_i (t_{i+1} - t_i)$. 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. +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 $a_n = \sum_{i=0}^{n-1} p_i (t_{i+1} - t_i)$. 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. In Osmosis, we maintain accumulator records for every pool, for the last 48 hours. We also maintain within each accumulator record in state, the latest spot price. This allows us to interpolate accumulation records between times. Namely, if I want the twap from `t=10s` to `t=15s`, but the time records are at `9s, 13s, 17s`, this is fine. Using the latest spot price in each record, we create the accumulator value for `t=10` by computing `a_10 = a_9 + a_9_latest_spot_price * (10s - 9s)`, and `a_15 = a_13 + a_13_latest_spot_price * (15s - 13s)`. Given these interpolated accumulation values, we can compute the TWAP as before. @@ -119,9 +119,9 @@ The pre-release testing methodology planned for the twap module is: - Tests that migration of Osmosis pools created prior to the TWAP upgrade, get TWAPs recorded starting at the v11 upgrade. - [ ] Integration into the Osmosis simulator - 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. - - Not currently in scope for release blocking, but planned: Integration for gas tracking, to ensure gas of reads/writes does not grow with time. + - Not currently deemed release blocking, but planned: Integration for gas tracking, to ensure gas of reads/writes does not grow with time. - [ ] Mutation testing usage - integration of the TWAP module into [go mutation testing](https://github.com/osmosis-labs/go-mutesting): - - The success we've seen with the tokenfactory module, is it succeeds at surfacing behavior for untested behavior. - e.g. if you delete a line, or change the direction of a conditional, does a test catch it. + - The success we've seen with the `tokenfactory` module. It succeeds at surfacing behavior for untested logic. + e.g. if you delete a line, or change the direction of a conditional, mutation tests show if regular Go tests catch it. - 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. \ No newline at end of file From 6bc824246246eb7df000a8c062978b48a12c5e25 Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Thu, 21 Jul 2022 14:39:27 -0500 Subject: [PATCH 36/72] See if double line latex fixes github rendering --- x/gamm/twap/README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/x/gamm/twap/README.md b/x/gamm/twap/README.md index 95e49490b05..f102a41324a 100644 --- a/x/gamm/twap/README.md +++ b/x/gamm/twap/README.md @@ -6,9 +6,11 @@ A time weighted average price is a function that takes a sequence of `(time, pri ## Arithmetic mean TWAP -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)$. (Notice that the latest price `p_n` isn't used, as it has lasted for a time interval of `0` seconds in this range!) +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)$$ +Notice that the latest price `p_n` isn't used, as it has lasted for a time interval of `0` seconds in this range! -To illustrate with an example, given the sequence: `(0s, $1), (4s, $6), (5s, $1)`, the arithmetic mean TWAP is: $\frac{\$1 * (4s - 0s) + \$6 * (5s - 4s)}{5s - 0s} = \frac{\$10}{5} = \$2$. +To illustrate with an example, given the sequence: `(0s, $1), (4s, $6), (5s, $1)`, the arithmetic mean TWAP is: +$$\frac{\$1 * (4s - 0s) + \$6 * (5s - 4s)}{5s - 0s} = \frac{\$10}{5} = \$2$$ ## Computation via accumulators method From f87e608823545b33269f298a31cf8e0b3867fe73 Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Thu, 21 Jul 2022 14:41:01 -0500 Subject: [PATCH 37/72] Fix remaining summation numerator --- x/gamm/twap/README.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/x/gamm/twap/README.md b/x/gamm/twap/README.md index 191f609c8ce..5740656cba4 100644 --- a/x/gamm/twap/README.md +++ b/x/gamm/twap/README.md @@ -16,9 +16,17 @@ $$\frac{\$1 * (4s - 0s) + \$6 * (5s - 4s)}{5s - 0s} = \frac{\$10}{5} = \$2$$ 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). -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 $a_n = \sum_{i=0}^{n-1} p_i (t_{i+1} - t_i)$. 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. - -In Osmosis, we maintain accumulator records for every pool, for the last 48 hours. We also maintain within each accumulator record in state, the latest spot price. This allows us to interpolate accumulation records between times. Namely, if I want the twap from `t=10s` to `t=15s`, but the time records are at `9s, 13s, 17s`, this is fine. Using the latest spot price in each record, we create the accumulator value for `t=10` by computing `a_10 = a_9 + a_9_latest_spot_price * (10s - 9s)`, and `a_15 = a_13 + a_13_latest_spot_price * (15s - 13s)`. Given these interpolated accumulation values, we can compute the TWAP as before. +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 +$$a_n = \sum_{i=0}^{n-1} p_i (t_{i+1} - t_i)$$ +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. + +In Osmosis, we maintain accumulator records for every pool, for the last 48 hours. +We also maintain within each accumulator record in state, the latest spot price. +This allows us to interpolate accumulation records between times. +Namely, if I want the twap from `t=10s` to `t=15s`, but the time records are at `9s, 13s, 17s`, this is fine. +Using the latest spot price in each record, we create the accumulator value for `t=10` by computing +`a_10 = a_9 + a_9_latest_spot_price * (10s - 9s)`, and `a_15 = a_13 + a_13_latest_spot_price * (15s - 13s)`. +Given these interpolated accumulation values, we can compute the TWAP as before. ## Module API From a389b082b2b4a163d8da53cc1e42c1817e72f079 Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Thu, 21 Jul 2022 14:53:35 -0500 Subject: [PATCH 38/72] Try to fix markdown lints --- x/gamm/twap/README.md | 44 +++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/x/gamm/twap/README.md b/x/gamm/twap/README.md index 5740656cba4..d720127a42e 100644 --- a/x/gamm/twap/README.md +++ b/x/gamm/twap/README.md @@ -109,29 +109,29 @@ In the event that a pool is created, and has a swap in the same block, the recor The pre-release testing methodology planned for the twap module is: - [ ] Using table driven unit tests to test all foreseen states of the module - - hook testing - - All swaps correctly trigger twap record updates - - Create pools cause records to be created - - store - - EndBlock triggers all relevant twaps to be saved correctly - - Block commit wipes temporary stores - - logic - - Make tables of expected input / output cases for: - - getMostRecentRecord - - getInterpolatedRecord - - updateRecord - - computeArithmeticTwap - - Test overflow handling in all relevant arithmetic - - Complete testing code coverage (up to return err lines) for logic.go file - - API - - Unit tests for the public API, under foreseeable setup conditions + - hook testing + - All swaps correctly trigger twap record updates + - Create pools cause records to be created + - store + - EndBlock triggers all relevant twaps to be saved correctly + - Block commit wipes temporary stores + - logic + - Make tables of expected input / output cases for: + - getMostRecentRecord + - getInterpolatedRecord + - updateRecord + - computeArithmeticTwap + - Test overflow handling in all relevant arithmetic + - Complete testing code coverage (up to return err lines) for logic.go file + - API + - Unit tests for the public API, under foreseeable setup conditions - [ ] End to end migration tests - - Tests that migration of Osmosis pools created prior to the TWAP upgrade, get TWAPs recorded starting at the v11 upgrade. + - Tests that migration of Osmosis pools created prior to the TWAP upgrade, get TWAPs recorded starting at the v11 upgrade. - [ ] Integration into the Osmosis simulator - - 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. - - Not currently deemed release blocking, but planned: Integration for gas tracking, to ensure gas of reads/writes does not grow with time. + - 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. + - Not currently deemed release blocking, but planned: Integration for gas tracking, to ensure gas of reads/writes does not grow with time. - [ ] Mutation testing usage - - integration of the TWAP module into [go mutation testing](https://github.com/osmosis-labs/go-mutesting): - - The success we've seen with the `tokenfactory` module. It succeeds at surfacing behavior for untested logic. - e.g. if you delete a line, or change the direction of a conditional, mutation tests show if regular Go tests catch it. + - integration of the TWAP module into [go mutation testing](https://github.com/osmosis-labs/go-mutesting): + - We've seen with the `tokenfactory` module that it succeeds at surfacing behavior for untested logic. + e.g. if you delete a line, or change the direction of a conditional, mutation tests show if regular Go tests catch it. - 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. \ No newline at end of file From 42df66a5791e37fc00bf6614fcb1ffab0ec82bca Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Thu, 21 Jul 2022 15:02:00 -0500 Subject: [PATCH 39/72] Fix remianing markdown lint --- x/gamm/twap/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/x/gamm/twap/README.md b/x/gamm/twap/README.md index d720127a42e..5b9694b04ff 100644 --- a/x/gamm/twap/README.md +++ b/x/gamm/twap/README.md @@ -117,10 +117,10 @@ The pre-release testing methodology planned for the twap module is: - Block commit wipes temporary stores - logic - Make tables of expected input / output cases for: - - getMostRecentRecord - - getInterpolatedRecord - - updateRecord - - computeArithmeticTwap + - getMostRecentRecord + - getInterpolatedRecord + - updateRecord + - computeArithmeticTwap - Test overflow handling in all relevant arithmetic - Complete testing code coverage (up to return err lines) for logic.go file - API From 81f478bf134914d64af5af78b0d0b6823554eb96 Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Thu, 21 Jul 2022 22:17:36 -0500 Subject: [PATCH 40/72] Fix incorrect interface in genesis proto description --- .../gamm/twap/v1beta1/twap_record.proto | 4 +- x/gamm/twap/types/twap_record.pb.go | 71 +++++++++---------- 2 files changed, 36 insertions(+), 39 deletions(-) diff --git a/proto/osmosis/gamm/twap/v1beta1/twap_record.proto b/proto/osmosis/gamm/twap/v1beta1/twap_record.proto index c44266c721b..b6685529668 100644 --- a/proto/osmosis/gamm/twap/v1beta1/twap_record.proto +++ b/proto/osmosis/gamm/twap/v1beta1/twap_record.proto @@ -60,6 +60,4 @@ message TwapRecord { } // GenesisState defines the gamm module's genesis state. -message GenesisState { - repeated TwapRecord twaps = 1 [ (cosmos_proto.accepts_interface) = "PoolI" ]; -} +message GenesisState { repeated TwapRecord twaps = 1; } diff --git a/x/gamm/twap/types/twap_record.pb.go b/x/gamm/twap/types/twap_record.pb.go index 13ac05dcea7..c29041b23e0 100644 --- a/x/gamm/twap/types/twap_record.pb.go +++ b/x/gamm/twap/types/twap_record.pb.go @@ -180,42 +180,41 @@ func init() { } var fileDescriptor_a81e54bd4e35cf12 = []byte{ - // 546 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x94, 0xcf, 0x6e, 0xd3, 0x40, - 0x10, 0xc6, 0x63, 0xf2, 0xa7, 0x64, 0x53, 0x84, 0x64, 0x55, 0xc2, 0x0d, 0x92, 0x1d, 0x22, 0x81, - 0x82, 0x50, 0xd6, 0x76, 0x11, 0x17, 0x6e, 0x89, 0x2a, 0x10, 0x12, 0x82, 0xca, 0xad, 0x38, 0xc0, - 0xc1, 0x5a, 0x3b, 0x8b, 0x63, 0xe1, 0xcd, 0xae, 0xbc, 0x9b, 0x96, 0xbc, 0x45, 0x9f, 0x81, 0x67, - 0xe0, 0x21, 0x2a, 0x4e, 0x3d, 0x22, 0x0e, 0x06, 0x25, 0x37, 0x8e, 0x7d, 0x02, 0xb4, 0xbb, 0x4e, - 0x9a, 0x80, 0xca, 0x21, 0x27, 0x7b, 0x76, 0xbe, 0xf9, 0x7d, 0x33, 0xab, 0xb1, 0xc1, 0x13, 0xca, - 0x09, 0xe5, 0x29, 0x77, 0x13, 0x44, 0x88, 0x2b, 0xce, 0x10, 0x73, 0x4f, 0xfd, 0x08, 0x0b, 0xe4, - 0xab, 0x20, 0xcc, 0x71, 0x4c, 0xf3, 0x11, 0x64, 0x39, 0x15, 0xd4, 0xdc, 0x2f, 0xc5, 0x50, 0x8a, - 0xa1, 0xcc, 0xc3, 0x52, 0xdc, 0xde, 0x4b, 0x68, 0x42, 0x95, 0xca, 0x95, 0x6f, 0xba, 0xa0, 0xbd, - 0x9f, 0x50, 0x9a, 0x64, 0xd8, 0x55, 0x51, 0x34, 0xfd, 0xe8, 0xa2, 0xc9, 0x6c, 0x99, 0x8a, 0x15, - 0x2c, 0xd4, 0x35, 0x3a, 0x28, 0x53, 0xb6, 0x8e, 0xdc, 0x08, 0x71, 0xbc, 0xea, 0x26, 0xa6, 0xe9, - 0xa4, 0xcc, 0x3b, 0x7f, 0x53, 0x45, 0x4a, 0x30, 0x17, 0x88, 0x30, 0x2d, 0xe8, 0x7e, 0xa9, 0x03, - 0x70, 0x72, 0x86, 0x58, 0xa0, 0x9a, 0x37, 0xef, 0x81, 0x1d, 0x46, 0x69, 0x16, 0xa6, 0x23, 0xcb, - 0xe8, 0x18, 0xbd, 0x5a, 0xd0, 0x90, 0xe1, 0xab, 0x91, 0xf9, 0x00, 0xec, 0x22, 0xce, 0xb1, 0xf0, - 0xc2, 0x11, 0x9e, 0x50, 0x62, 0xdd, 0xea, 0x18, 0xbd, 0x66, 0xd0, 0xd2, 0x67, 0x87, 0xf2, 0x68, - 0x25, 0xf1, 0x4b, 0x49, 0x75, 0x4d, 0xe2, 0x6b, 0xc9, 0x00, 0x34, 0xc6, 0x38, 0x4d, 0xc6, 0xc2, - 0xaa, 0x75, 0x8c, 0x5e, 0x75, 0xf8, 0xf8, 0x77, 0xe1, 0xdc, 0xd1, 0xf7, 0x16, 0xea, 0xc4, 0x55, - 0xe1, 0xec, 0xcd, 0x10, 0xc9, 0x9e, 0x77, 0x37, 0x8e, 0xbb, 0x41, 0x59, 0x68, 0xbe, 0x01, 0x35, - 0x39, 0x83, 0x55, 0xef, 0x18, 0xbd, 0xd6, 0x41, 0x1b, 0xea, 0x01, 0xe1, 0x72, 0x40, 0x78, 0xb2, - 0x1c, 0x70, 0x68, 0x5f, 0x14, 0x4e, 0xe5, 0xaa, 0x70, 0xcc, 0x0d, 0x9e, 0x2c, 0xee, 0x9e, 0xff, - 0x74, 0x8c, 0x40, 0x71, 0xcc, 0x0f, 0xc0, 0x64, 0x5e, 0x98, 0x21, 0x2e, 0x42, 0xce, 0xa8, 0x08, - 0x59, 0x9e, 0xc6, 0xd8, 0x6a, 0xc8, 0xde, 0x87, 0x50, 0x12, 0x7e, 0x14, 0xce, 0xa3, 0x24, 0x15, - 0xe3, 0x69, 0x04, 0x63, 0x4a, 0xca, 0xeb, 0x2f, 0x1f, 0x7d, 0x3e, 0xfa, 0xe4, 0x8a, 0x19, 0xc3, - 0x1c, 0x1e, 0xe2, 0x38, 0xb8, 0xcb, 0xbc, 0xd7, 0x88, 0x8b, 0x63, 0x46, 0xc5, 0x91, 0xc4, 0x28, - 0xb8, 0xff, 0x0f, 0x7c, 0x67, 0x4b, 0xb8, 0xbf, 0x09, 0xe7, 0xc0, 0x66, 0x5e, 0x88, 0xf2, 0x54, - 0x8c, 0x09, 0x16, 0x69, 0x1c, 0xaa, 0x2d, 0x44, 0x71, 0x3c, 0x25, 0xd3, 0x0c, 0x09, 0x9a, 0x5b, - 0xb7, 0xb7, 0x32, 0xba, 0xcf, 0xbc, 0xc1, 0x0a, 0x2a, 0x77, 0x63, 0x70, 0x8d, 0x54, 0xa6, 0xfe, - 0x7f, 0x4d, 0x9b, 0x5b, 0x9a, 0xfa, 0x37, 0x9a, 0x76, 0xdf, 0x81, 0xdd, 0x97, 0x78, 0x82, 0x79, - 0xca, 0x8f, 0x05, 0x12, 0xd8, 0x7c, 0x01, 0xea, 0xd2, 0x96, 0x5b, 0x46, 0xa7, 0xda, 0x6b, 0x1d, - 0x3c, 0x84, 0x37, 0x7e, 0x6c, 0xf0, 0x7a, 0xb7, 0x87, 0xcd, 0x6f, 0x5f, 0xfb, 0xf5, 0x23, 0xb9, - 0xce, 0x81, 0x2e, 0x1f, 0xbe, 0xbd, 0x98, 0xdb, 0xc6, 0xe5, 0xdc, 0x36, 0x7e, 0xcd, 0x6d, 0xe3, - 0x7c, 0x61, 0x57, 0x2e, 0x17, 0x76, 0xe5, 0xfb, 0xc2, 0xae, 0xbc, 0x7f, 0xb6, 0xd6, 0x76, 0x09, - 0xef, 0x67, 0x28, 0xe2, 0xcb, 0xc0, 0x3d, 0xf5, 0x3d, 0xf7, 0xf3, 0xda, 0x9f, 0x40, 0x4d, 0x12, - 0x35, 0xd4, 0x1a, 0x3e, 0xfd, 0x13, 0x00, 0x00, 0xff, 0xff, 0x52, 0x5b, 0x8e, 0x25, 0x2b, 0x04, - 0x00, 0x00, + // 535 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x93, 0x4f, 0x6f, 0xd3, 0x3e, + 0x18, 0xc7, 0x9b, 0x5f, 0xff, 0xec, 0x37, 0x77, 0x08, 0x29, 0x9a, 0x44, 0x56, 0xa4, 0xa4, 0x54, + 0x02, 0x15, 0xa1, 0x39, 0xc9, 0x10, 0x17, 0x38, 0xb5, 0x9a, 0x84, 0x10, 0x08, 0x50, 0xb6, 0x13, + 0x1c, 0x22, 0x27, 0x35, 0xa9, 0x45, 0x5c, 0x5b, 0xb1, 0xbb, 0xd1, 0x77, 0xb1, 0xd7, 0xc0, 0xab, + 0xd9, 0x71, 0x47, 0xc4, 0x21, 0xa0, 0xf6, 0xc6, 0x71, 0xaf, 0x00, 0xd9, 0x4e, 0xbb, 0x16, 0x34, + 0x0e, 0x3d, 0x25, 0x8f, 0x9f, 0xef, 0xf3, 0xf9, 0x3e, 0x8f, 0xf5, 0x18, 0x3c, 0x61, 0x82, 0x32, + 0x41, 0x84, 0x9f, 0x21, 0x4a, 0x7d, 0x79, 0x8e, 0xb8, 0x7f, 0x16, 0x26, 0x58, 0xa2, 0x50, 0x07, + 0x71, 0x81, 0x53, 0x56, 0x8c, 0x20, 0x2f, 0x98, 0x64, 0xf6, 0x41, 0x25, 0x86, 0x4a, 0x0c, 0x55, + 0x1e, 0x56, 0xe2, 0xce, 0x7e, 0xc6, 0x32, 0xa6, 0x55, 0xbe, 0xfa, 0x33, 0x05, 0x9d, 0x83, 0x8c, + 0xb1, 0x2c, 0xc7, 0xbe, 0x8e, 0x92, 0xe9, 0x27, 0x1f, 0x4d, 0x66, 0xcb, 0x54, 0xaa, 0x61, 0xb1, + 0xa9, 0x31, 0x41, 0x95, 0x72, 0x4d, 0xe4, 0x27, 0x48, 0xe0, 0x55, 0x37, 0x29, 0x23, 0x93, 0x2a, + 0xef, 0xfd, 0x49, 0x95, 0x84, 0x62, 0x21, 0x11, 0xe5, 0x46, 0xd0, 0xfb, 0xda, 0x04, 0xe0, 0xf4, + 0x1c, 0xf1, 0x48, 0x37, 0x6f, 0xdf, 0x03, 0x3b, 0x9c, 0xb1, 0x3c, 0x26, 0x23, 0xc7, 0xea, 0x5a, + 0xfd, 0x46, 0xd4, 0x52, 0xe1, 0xab, 0x91, 0xfd, 0x00, 0xec, 0x21, 0x21, 0xb0, 0x0c, 0xe2, 0x11, + 0x9e, 0x30, 0xea, 0xfc, 0xd7, 0xb5, 0xfa, 0xbb, 0x51, 0xdb, 0x9c, 0x1d, 0xab, 0xa3, 0x95, 0x24, + 0xac, 0x24, 0xf5, 0x35, 0x49, 0x68, 0x24, 0x03, 0xd0, 0x1a, 0x63, 0x92, 0x8d, 0xa5, 0xd3, 0xe8, + 0x5a, 0xfd, 0xfa, 0xf0, 0xf1, 0xaf, 0xd2, 0xbb, 0x63, 0xee, 0x2d, 0x36, 0x89, 0xeb, 0xd2, 0xdb, + 0x9f, 0x21, 0x9a, 0x3f, 0xef, 0x6d, 0x1c, 0xf7, 0xa2, 0xaa, 0xd0, 0x7e, 0x0b, 0x1a, 0x6a, 0x06, + 0xa7, 0xd9, 0xb5, 0xfa, 0xed, 0xa3, 0x0e, 0x34, 0x03, 0xc2, 0xe5, 0x80, 0xf0, 0x74, 0x39, 0xe0, + 0xd0, 0xbd, 0x2c, 0xbd, 0xda, 0x75, 0xe9, 0xd9, 0x1b, 0x3c, 0x55, 0xdc, 0xbb, 0xf8, 0xe1, 0x59, + 0x91, 0xe6, 0xd8, 0x1f, 0x81, 0xcd, 0x83, 0x38, 0x47, 0x42, 0xc6, 0x82, 0x33, 0x19, 0xf3, 0x82, + 0xa4, 0xd8, 0x69, 0xa9, 0xde, 0x87, 0x50, 0x11, 0xbe, 0x97, 0xde, 0xa3, 0x8c, 0xc8, 0xf1, 0x34, + 0x81, 0x29, 0xa3, 0xd5, 0xf5, 0x57, 0x9f, 0x43, 0x31, 0xfa, 0xec, 0xcb, 0x19, 0xc7, 0x02, 0x1e, + 0xe3, 0x34, 0xba, 0xcb, 0x83, 0x37, 0x48, 0xc8, 0x13, 0xce, 0xe4, 0x7b, 0x85, 0xd1, 0xf0, 0xf0, + 0x2f, 0xf8, 0xce, 0x96, 0xf0, 0x70, 0x13, 0x2e, 0x80, 0xcb, 0x83, 0x18, 0x15, 0x44, 0x8e, 0x29, + 0x96, 0x24, 0x8d, 0xf5, 0x16, 0xa2, 0x34, 0x9d, 0xd2, 0x69, 0x8e, 0x24, 0x2b, 0x9c, 0xff, 0xb7, + 0x32, 0xba, 0xcf, 0x83, 0xc1, 0x0a, 0xaa, 0x76, 0x63, 0x70, 0x83, 0xd4, 0xa6, 0xe1, 0x3f, 0x4d, + 0x77, 0xb7, 0x34, 0x0d, 0x6f, 0x35, 0xed, 0xbd, 0x06, 0x7b, 0x2f, 0xf1, 0x04, 0x0b, 0x22, 0x4e, + 0x24, 0x92, 0xd8, 0x7e, 0x01, 0x9a, 0xca, 0x56, 0x38, 0x56, 0xb7, 0xde, 0x6f, 0x1f, 0x3d, 0x84, + 0xb7, 0x3e, 0x36, 0x78, 0xb3, 0xdb, 0x91, 0xa9, 0x19, 0xbe, 0xbb, 0x9c, 0xbb, 0xd6, 0xd5, 0xdc, + 0xb5, 0x7e, 0xce, 0x5d, 0xeb, 0x62, 0xe1, 0xd6, 0xae, 0x16, 0x6e, 0xed, 0xdb, 0xc2, 0xad, 0x7d, + 0x78, 0xb6, 0xd6, 0x6b, 0x45, 0x3c, 0xcc, 0x51, 0x22, 0x96, 0x81, 0x7f, 0x16, 0x06, 0xfe, 0x97, + 0xb5, 0xe7, 0xaf, 0xdb, 0x4f, 0x5a, 0x7a, 0xf7, 0x9e, 0xfe, 0x0e, 0x00, 0x00, 0xff, 0xff, 0x31, + 0x20, 0x99, 0x21, 0x20, 0x04, 0x00, 0x00, } func (m *TwapRecord) Marshal() (dAtA []byte, err error) { From 30b2a115868861ba0fe3432fecea14afc50f0035 Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Thu, 21 Jul 2022 22:24:17 -0500 Subject: [PATCH 41/72] Bring in changes from twap_types PR comments --- x/gamm/twap/types/keys.go | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/x/gamm/twap/types/keys.go b/x/gamm/twap/types/keys.go index 90829937285..32d683cf615 100644 --- a/x/gamm/twap/types/keys.go +++ b/x/gamm/twap/types/keys.go @@ -24,27 +24,29 @@ const ( KeySeparator = "|" ) -var mostRecentTWAPsNoSeparator = "recent_twap" -var historicalTWAPTimeIndexNoSeparator = "historical_time_index" -var historicalTWAPPoolIndexNoSeparator = "historical_pool_index" - -var mostRecentTWAPsPrefix = mostRecentTWAPsNoSeparator + KeySeparator -var historicalTWAPTimeIndexPrefix = historicalTWAPTimeIndexNoSeparator + KeySeparator -var historicalTWAPPoolIndexPrefix = historicalTWAPPoolIndexNoSeparator + KeySeparator +var ( + mostRecentTWAPsNoSeparator = "recent_twap" + historicalTWAPTimeIndexNoSeparator = "historical_time_index" + historicalTWAPPoolIndexNoSeparator = "historical_pool_index" + + mostRecentTWAPsPrefix = mostRecentTWAPsNoSeparator + KeySeparator + historicalTWAPTimeIndexPrefix = historicalTWAPTimeIndexNoSeparator + KeySeparator + historicalTWAPPoolIndexPrefix = historicalTWAPPoolIndexNoSeparator + KeySeparator +) // TODO: make utility command to automatically interlace separators -func FormatMostRecentTWAPKey(poolId uint64, denom1 string, denom2 string) []byte { +func FormatMostRecentTWAPKey(poolId uint64, denom1, denom2 string) []byte { return []byte(fmt.Sprintf("%s%d%s%s%s%s", mostRecentTWAPsPrefix, poolId, KeySeparator, denom1, KeySeparator, denom2)) } // TODO: Replace historical management with ORM, we currently accept 2x write amplification right now. -func FormatHistoricalTimeIndexTWAPKey(accumulatorWriteTime time.Time, poolId uint64, denom1 string, denom2 string) []byte { +func FormatHistoricalTimeIndexTWAPKey(accumulatorWriteTime time.Time, poolId uint64, denom1, denom2 string) []byte { timeS := osmoutils.FormatTimeString(accumulatorWriteTime) return []byte(fmt.Sprintf("%s%s%s%d%s%s%s%s", historicalTWAPTimeIndexPrefix, timeS, KeySeparator, poolId, KeySeparator, denom1, KeySeparator, denom2)) } -func FormatHistoricalPoolIndexTWAPKey(poolId uint64, accumulatorWriteTime time.Time, denom1 string, denom2 string) []byte { +func FormatHistoricalPoolIndexTWAPKey(poolId uint64, accumulatorWriteTime time.Time, denom1, denom2 string) []byte { timeS := osmoutils.FormatTimeString(accumulatorWriteTime) return []byte(fmt.Sprintf("%s%d%s%s%s%s%s%s", historicalTWAPPoolIndexPrefix, poolId, KeySeparator, timeS, KeySeparator, denom1, KeySeparator, denom2)) } From 1d23ec6225a4c034b062f9d731da7f6daf6f6367 Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Thu, 21 Jul 2022 22:28:23 -0500 Subject: [PATCH 42/72] Change NewTwapRecord to return an error --- x/gamm/twap/hook_test.go | 3 ++- x/gamm/twap/logic.go | 6 +++++- x/gamm/twap/types/utils.go | 6 +++--- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/x/gamm/twap/hook_test.go b/x/gamm/twap/hook_test.go index cc0105a556b..0a792c86c7b 100644 --- a/x/gamm/twap/hook_test.go +++ b/x/gamm/twap/hook_test.go @@ -9,7 +9,8 @@ import ( func (s *TestSuite) TestCreateTwoAssetPoolFlow() { poolId := s.PrepareUni2PoolWithAssets(defaultUniV2Coins[0], defaultUniV2Coins[1]) - expectedTwap := types.NewTwapRecord(s.App.GAMMKeeper, s.Ctx, poolId, "token/B", "token/A") + expectedTwap, err := types.NewTwapRecord(s.App.GAMMKeeper, s.Ctx, poolId, "token/B", "token/A") + s.Require().NoError(err) twap, err := s.twapkeeper.GetMostRecentRecordStoreRepresentation(s.Ctx, poolId, "token/B", "token/A") s.Require().NoError(err) diff --git a/x/gamm/twap/logic.go b/x/gamm/twap/logic.go index 589f3eb2efa..6c46f9a404c 100644 --- a/x/gamm/twap/logic.go +++ b/x/gamm/twap/logic.go @@ -12,7 +12,11 @@ func (k Keeper) afterCreatePool(ctx sdk.Context, poolId uint64) error { denoms, err := k.ammkeeper.GetPoolDenoms(ctx, poolId) denomPairs0, denomPairs1 := types.GetAllUniqueDenomPairs(denoms) for i := 0; i < len(denomPairs0); i++ { - record := types.NewTwapRecord(k.ammkeeper, ctx, poolId, denomPairs0[i], denomPairs1[i]) + record, err := types.NewTwapRecord(k.ammkeeper, ctx, poolId, denomPairs0[i], denomPairs1[i]) + // err should be impossible given GetAllUniqueDenomPairs guarantees + if err != nil { + return err + } k.storeNewRecord(ctx, record) } k.trackChangedPool(ctx, poolId) diff --git a/x/gamm/twap/types/utils.go b/x/gamm/twap/types/utils.go index c266426bf4d..ff62cc13f8b 100644 --- a/x/gamm/twap/types/utils.go +++ b/x/gamm/twap/types/utils.go @@ -10,9 +10,9 @@ import ( "github.com/osmosis-labs/osmosis/v10/osmoutils" ) -func NewTwapRecord(k AmmInterface, ctx sdk.Context, poolId uint64, denom0 string, denom1 string) TwapRecord { +func NewTwapRecord(k AmmInterface, ctx sdk.Context, poolId uint64, denom0 string, denom1 string) (TwapRecord, error) { if !(denom0 > denom1) { - panic(fmt.Sprintf("precondition denom0 > denom1 not satisfied. denom0 %s | denom1 %s", denom0, denom1)) + return TwapRecord{}, fmt.Errorf("precondition denom0 > denom1 not satisfied. denom0 %s | denom1 %s", denom0, denom1) } sp0 := MustGetSpotPrice(k, ctx, poolId, denom0, denom1) sp1 := MustGetSpotPrice(k, ctx, poolId, denom1, denom0) @@ -26,7 +26,7 @@ func NewTwapRecord(k AmmInterface, ctx sdk.Context, poolId uint64, denom0 string P1LastSpotPrice: sp1, P0ArithmeticTwapAccumulator: sdk.ZeroDec(), P1ArithmeticTwapAccumulator: sdk.ZeroDec(), - } + }, nil } // mustGetSpotPrice returns the spot price for the given pool id, and denom0 in terms of denom1. From f05f76654dd4c5cf8d95c12ca174a9c0b93fec20 Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Thu, 21 Jul 2022 22:44:52 -0500 Subject: [PATCH 43/72] Delete beforeswap --- x/gamm/keeper/pool.go | 1 - x/gamm/twap/hook_listener.go | 4 ---- x/gamm/types/hooks.go | 9 --------- x/pool-incentives/keeper/hooks.go | 1 - 4 files changed, 15 deletions(-) diff --git a/x/gamm/keeper/pool.go b/x/gamm/keeper/pool.go index 17e54bbf8a1..dae14c7a654 100644 --- a/x/gamm/keeper/pool.go +++ b/x/gamm/keeper/pool.go @@ -47,7 +47,6 @@ func (k Keeper) GetPoolAndPoke(ctx sdk.Context, poolId uint64) (types.PoolI, err // Get pool and check if the pool is active, i.e. allowed to be swapped against. func (k Keeper) getPoolForSwap(ctx sdk.Context, poolId uint64) (types.PoolI, error) { - k.hooks.BeforeSwap(ctx, poolId) pool, err := k.GetPoolAndPoke(ctx, poolId) if err != nil { return &balancer.Pool{}, err diff --git a/x/gamm/twap/hook_listener.go b/x/gamm/twap/hook_listener.go index e57eb6db2ea..e916f5fca53 100644 --- a/x/gamm/twap/hook_listener.go +++ b/x/gamm/twap/hook_listener.go @@ -39,10 +39,6 @@ func (hook *gammhook) AfterPoolCreated(ctx sdk.Context, sender sdk.AccAddress, p } } -// TODO: delete -func (hook *gammhook) BeforeSwap(ctx sdk.Context, poolId uint64) { -} - func (hook *gammhook) AfterSwap(ctx sdk.Context, sender sdk.AccAddress, poolId uint64, input sdk.Coins, output sdk.Coins) { hook.k.trackChangedPool(ctx, poolId) } diff --git a/x/gamm/types/hooks.go b/x/gamm/types/hooks.go index b763dcdd67f..e63c9ff50b9 100644 --- a/x/gamm/types/hooks.go +++ b/x/gamm/types/hooks.go @@ -12,9 +12,6 @@ type GammHooks interface { // AfterExitPool is called after ExitPool, ExitSwapShareAmountIn, and ExitSwapExternAmountOut AfterExitPool(ctx sdk.Context, sender sdk.AccAddress, poolId uint64, shareInAmount sdk.Int, exitCoins sdk.Coins) - // BeforeSwap is called before JoinSwapExternAmountIn, JoinSwapShareAmountOut, ExitSwapShareAmountIn, ExitSwapExternAmountOut - // SwapExactAmountIn and SwapExactAmountOut - BeforeSwap(ctx sdk.Context, poolId uint64) // AfterSwap is called after SwapExactAmountIn and SwapExactAmountOut AfterSwap(ctx sdk.Context, sender sdk.AccAddress, poolId uint64, input sdk.Coins, output sdk.Coins) } @@ -47,12 +44,6 @@ func (h MultiGammHooks) AfterExitPool(ctx sdk.Context, sender sdk.AccAddress, po } } -func (h MultiGammHooks) BeforeSwap(ctx sdk.Context, poolId uint64) { - for i := range h { - h[i].BeforeSwap(ctx, poolId) - } -} - func (h MultiGammHooks) AfterSwap(ctx sdk.Context, sender sdk.AccAddress, poolId uint64, input sdk.Coins, output sdk.Coins) { for i := range h { h[i].AfterSwap(ctx, sender, poolId, input, output) diff --git a/x/pool-incentives/keeper/hooks.go b/x/pool-incentives/keeper/hooks.go index 4f183a68661..e452c3d1993 100644 --- a/x/pool-incentives/keeper/hooks.go +++ b/x/pool-incentives/keeper/hooks.go @@ -39,7 +39,6 @@ func (h Hooks) AfterExitPool(ctx sdk.Context, sender sdk.AccAddress, poolId uint func (h Hooks) AfterSwap(ctx sdk.Context, sender sdk.AccAddress, poolId uint64, input sdk.Coins, output sdk.Coins) { } -func (h Hooks) BeforeSwap(ctx sdk.Context, poolId uint64) {} func (h Hooks) BeforeJoinPool(ctx sdk.Context, sender sdk.AccAddress, poolId uint64) {} // Distribute coins after minter module allocate assets to pool-incentives module. From 285e35674d8514e2b166989cdeedd0ca45da5d12 Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Thu, 21 Jul 2022 22:48:11 -0500 Subject: [PATCH 44/72] Delete missed obsolete hook --- x/pool-incentives/keeper/hooks.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/x/pool-incentives/keeper/hooks.go b/x/pool-incentives/keeper/hooks.go index e452c3d1993..c2b49cdf488 100644 --- a/x/pool-incentives/keeper/hooks.go +++ b/x/pool-incentives/keeper/hooks.go @@ -39,8 +39,6 @@ func (h Hooks) AfterExitPool(ctx sdk.Context, sender sdk.AccAddress, poolId uint func (h Hooks) AfterSwap(ctx sdk.Context, sender sdk.AccAddress, poolId uint64, input sdk.Coins, output sdk.Coins) { } -func (h Hooks) BeforeJoinPool(ctx sdk.Context, sender sdk.AccAddress, poolId uint64) {} - // Distribute coins after minter module allocate assets to pool-incentives module. func (h Hooks) AfterDistributeMintedCoin(ctx sdk.Context, mintedCoin sdk.Coin) { // @Sunny, @Tony, @Dev, what comments should we keep after modifying own BeginBlocker to hooks? From 63fdef7c0a553b331c33967a166cb459bb255caf Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Fri, 22 Jul 2022 15:47:35 -0500 Subject: [PATCH 45/72] Add migration logic --- app/upgrades/v11/upgrades.go | 8 ++++++++ x/gamm/twap/hook_listener.go | 12 ++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/app/upgrades/v11/upgrades.go b/app/upgrades/v11/upgrades.go index 25af86205cf..c6855112f61 100644 --- a/app/upgrades/v11/upgrades.go +++ b/app/upgrades/v11/upgrades.go @@ -39,6 +39,14 @@ func CreateUpgradeHandler( bpm.StoreConsensusParams(ctx, cp) } + // Initialize TWAP state + // TODO: Get allPoolIds from gamm keeper, and write test for migration. + allPoolIds := []uint64{} + err := keepers.TwapKeeper.MigrateExistingPools(ctx, allPoolIds) + if err != nil { + return nil, err + } + return mm.RunMigrations(ctx, configurator, fromVM) } } diff --git a/x/gamm/twap/hook_listener.go b/x/gamm/twap/hook_listener.go index e0f41b7ab4c..d04f6100de3 100644 --- a/x/gamm/twap/hook_listener.go +++ b/x/gamm/twap/hook_listener.go @@ -1,14 +1,22 @@ package twap import ( - "fmt" - sdk "github.com/cosmos/cosmos-sdk/types" epochtypes "github.com/osmosis-labs/osmosis/v10/x/epochs/types" "github.com/osmosis-labs/osmosis/v10/x/gamm/types" ) +func (k Keeper) MigrateExistingPools(ctx sdk.Context, poolIds []uint64) error { + for _, pool := range poolIds { + err := k.afterCreatePool(ctx, pool) + if err != nil { + return err + } + } + return nil +} + var _ types.GammHooks = &gammhook{} var _ epochtypes.EpochHooks = &epochhook{} From 6ae4d70440b981c39baed6718ce9d66829e63e34 Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Fri, 22 Jul 2022 16:24:06 -0500 Subject: [PATCH 46/72] Add test for TestGetAllMostRecentRecordsForPool --- x/gamm/twap/keeper_test.go | 5 ++++ x/gamm/twap/store.go | 2 -- x/gamm/twap/store_test.go | 56 ++++++++++++++++++++++++++++++++++++++ x/gamm/twap/types/keys.go | 4 +-- 4 files changed, 63 insertions(+), 4 deletions(-) diff --git a/x/gamm/twap/keeper_test.go b/x/gamm/twap/keeper_test.go index d1a4ea43243..83e908a2452 100644 --- a/x/gamm/twap/keeper_test.go +++ b/x/gamm/twap/keeper_test.go @@ -41,6 +41,11 @@ func newEmptyPriceRecord(poolId uint64, t time.Time, asset0 string, asset1 strin Time: t, Asset0Denom: asset0, Asset1Denom: asset1, + + P0LastSpotPrice: sdk.ZeroDec(), + P1LastSpotPrice: sdk.ZeroDec(), + P0ArithmeticTwapAccumulator: sdk.ZeroDec(), + P1ArithmeticTwapAccumulator: sdk.ZeroDec(), } } diff --git a/x/gamm/twap/store.go b/x/gamm/twap/store.go index 122d4e3ec80..712c61c230c 100644 --- a/x/gamm/twap/store.go +++ b/x/gamm/twap/store.go @@ -3,7 +3,6 @@ package twap import ( "encoding/binary" "errors" - "fmt" "time" sdk "github.com/cosmos/cosmos-sdk/types" @@ -41,7 +40,6 @@ func (k Keeper) storeHistoricalTWAP(ctx sdk.Context, twap types.TwapRecord) { key2 := types.FormatHistoricalPoolIndexTWAPKey(twap.PoolId, twap.Time, twap.Asset0Denom, twap.Asset1Denom) osmoutils.MustSet(store, key1, &twap) osmoutils.MustSet(store, key2, &twap) - fmt.Println(string(key2)) } func (k Keeper) pruneRecordsBeforeTime(ctx sdk.Context, lastTime time.Time) { diff --git a/x/gamm/twap/store_test.go b/x/gamm/twap/store_test.go index e3fdb31c5a2..39cc101894d 100644 --- a/x/gamm/twap/store_test.go +++ b/x/gamm/twap/store_test.go @@ -1,5 +1,11 @@ package twap_test +import ( + "time" + + "github.com/osmosis-labs/osmosis/v10/x/gamm/twap/types" +) + // TestTrackChangedPool takes a list of poolIds as test cases, and runs one list per block. // Every simulated block, checks that there no changed pools. // Then runs k.trackChangedPool on every item in the test case list. @@ -35,3 +41,53 @@ func (s *TestSuite) TestTrackChangedPool() { }) } } + +// TestGetAllMostRecentRecordsForPool takes a list of records as test cases, +// and runs storeNewRecord for everything in sequence. +// Then it runs GetAllMostRecentRecordsForPool, and sees if its equal to expected + +func (s *TestSuite) TestGetAllMostRecentRecordsForPool() { + baseTime := time.Unix(1257894000, 0).UTC() + tPlusOne := baseTime.Add(time.Second) + baseRecord := newEmptyPriceRecord(1, baseTime, "tokenB", "tokenA") + tPlusOneRecord := newEmptyPriceRecord(1, tPlusOne, "tokenB", "tokenA") + tests := map[string]struct { + recordsToSet []types.TwapRecord + poolId uint64 + expectedRecords []types.TwapRecord + }{ + "set single record": { + []types.TwapRecord{baseRecord}, + 1, + []types.TwapRecord{baseRecord}, + }, + "query non-existent pool": { + []types.TwapRecord{baseRecord}, + 2, + []types.TwapRecord{}, + }, + "set two records": { + []types.TwapRecord{baseRecord, tPlusOneRecord}, + 1, + []types.TwapRecord{tPlusOneRecord}, + }, + "settwo records, reverse order": { + // The last record, independent of time, takes precedence for most recent. + []types.TwapRecord{tPlusOneRecord, baseRecord}, + 1, + []types.TwapRecord{baseRecord}, + }, + } + + for name, test := range tests { + s.Run(name, func() { + s.SetupTest() + for _, record := range test.recordsToSet { + s.twapkeeper.StoreNewRecord(s.Ctx, record) + } + actualRecords, err := s.twapkeeper.GetAllMostRecentRecordsForPool(s.Ctx, test.poolId) + s.Require().NoError(err) + s.Require().Equal(test.expectedRecords, actualRecords) + }) + } +} diff --git a/x/gamm/twap/types/keys.go b/x/gamm/twap/types/keys.go index 32d683cf615..277e794b047 100644 --- a/x/gamm/twap/types/keys.go +++ b/x/gamm/twap/types/keys.go @@ -83,8 +83,8 @@ func ParseTimeFromHistoricalPoolIndexKey(key []byte) (time.Time, error) { } func GetAllMostRecentTwapsForPool(store sdk.KVStore, poolId uint64) ([]TwapRecord, error) { - startPrefix := fmt.Sprintf("%s%s%d%s", mostRecentTWAPsPrefix, KeySeparator, poolId, KeySeparator) - endPrefix := fmt.Sprintf("%s%s%d%s", mostRecentTWAPsPrefix, KeySeparator, poolId+1, KeySeparator) + startPrefix := fmt.Sprintf("%s%d%s", mostRecentTWAPsPrefix, poolId, KeySeparator) + endPrefix := fmt.Sprintf("%s%d%s", mostRecentTWAPsPrefix, poolId+1, KeySeparator) return osmoutils.GatherValuesFromStore(store, []byte(startPrefix), []byte(endPrefix), ParseTwapFromBz) } From 800d3410526cf97c2f7cff71ee3e082435fcb162 Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Fri, 22 Jul 2022 16:31:53 -0500 Subject: [PATCH 47/72] Make multi-asset tc --- x/gamm/twap/store_test.go | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/x/gamm/twap/store_test.go b/x/gamm/twap/store_test.go index 39cc101894d..be9adc96683 100644 --- a/x/gamm/twap/store_test.go +++ b/x/gamm/twap/store_test.go @@ -45,7 +45,6 @@ func (s *TestSuite) TestTrackChangedPool() { // TestGetAllMostRecentRecordsForPool takes a list of records as test cases, // and runs storeNewRecord for everything in sequence. // Then it runs GetAllMostRecentRecordsForPool, and sees if its equal to expected - func (s *TestSuite) TestGetAllMostRecentRecordsForPool() { baseTime := time.Unix(1257894000, 0).UTC() tPlusOne := baseTime.Add(time.Second) @@ -71,12 +70,23 @@ func (s *TestSuite) TestGetAllMostRecentRecordsForPool() { 1, []types.TwapRecord{tPlusOneRecord}, }, - "settwo records, reverse order": { + "set two records, reverse order": { // The last record, independent of time, takes precedence for most recent. []types.TwapRecord{tPlusOneRecord, baseRecord}, 1, []types.TwapRecord{baseRecord}, }, + "set multi-asset pool record": { + []types.TwapRecord{ + newEmptyPriceRecord(1, baseTime, "tokenB", "tokenA"), + newEmptyPriceRecord(1, baseTime, "tokenC", "tokenB"), + newEmptyPriceRecord(1, baseTime, "tokenC", "tokenA")}, + 1, + []types.TwapRecord{ + newEmptyPriceRecord(1, baseTime, "tokenB", "tokenA"), + newEmptyPriceRecord(1, baseTime, "tokenC", "tokenA"), + newEmptyPriceRecord(1, baseTime, "tokenC", "tokenB")}, + }, } for name, test := range tests { From 7d1ada741b633f2c8b6b4083afe34a029e194262 Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Fri, 22 Jul 2022 17:15:02 -0500 Subject: [PATCH 48/72] test TestGetRecordAtOrBeforeTime --- x/gamm/twap/keeper_test.go | 1 + x/gamm/twap/store_test.go | 75 +++++++++++++++++++++++++++++++++++++- 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/x/gamm/twap/keeper_test.go b/x/gamm/twap/keeper_test.go index 83e908a2452..4ff8a68e015 100644 --- a/x/gamm/twap/keeper_test.go +++ b/x/gamm/twap/keeper_test.go @@ -13,6 +13,7 @@ import ( ) var defaultUniV2Coins = sdk.NewCoins(sdk.NewInt64Coin("token/B", 1_000_000_000), sdk.NewInt64Coin("token/A", 1_000_000_000)) +var baseTime = time.Unix(1257894000, 0).UTC() type TestSuite struct { apptesting.KeeperTestHelper diff --git a/x/gamm/twap/store_test.go b/x/gamm/twap/store_test.go index be9adc96683..e3a62e3bd18 100644 --- a/x/gamm/twap/store_test.go +++ b/x/gamm/twap/store_test.go @@ -46,7 +46,6 @@ func (s *TestSuite) TestTrackChangedPool() { // and runs storeNewRecord for everything in sequence. // Then it runs GetAllMostRecentRecordsForPool, and sees if its equal to expected func (s *TestSuite) TestGetAllMostRecentRecordsForPool() { - baseTime := time.Unix(1257894000, 0).UTC() tPlusOne := baseTime.Add(time.Second) baseRecord := newEmptyPriceRecord(1, baseTime, "tokenB", "tokenA") tPlusOneRecord := newEmptyPriceRecord(1, tPlusOne, "tokenB", "tokenA") @@ -101,3 +100,77 @@ func (s *TestSuite) TestGetAllMostRecentRecordsForPool() { }) } } + +// TestGetAllMostRecentRecordsForPool takes a list of records as test cases, +// and runs storeNewRecord for everything in sequence. +// Then it runs GetRecordAtOrBeforeTime, and sees if its equal to expected +func (s *TestSuite) TestGetRecordAtOrBeforeTime() { + type getRecordInput struct { + poolId uint64 + t time.Time + asset0Denom string + asset1Denom string + } + defaultInputAt := func(t time.Time) getRecordInput { return getRecordInput{1, t, "tokenB", "tokenA"} } + defaultRevInputAt := func(t time.Time) getRecordInput { return getRecordInput{1, t, "tokenA", "tokenB"} } + baseRecord := newEmptyPriceRecord(1, baseTime, "tokenB", "tokenA") + tMin1 := baseTime.Add(-time.Second) + tMin1Record := newEmptyPriceRecord(1, tMin1, "tokenB", "tokenA") + tPlus1 := baseTime.Add(time.Second) + tPlus1Record := newEmptyPriceRecord(1, tPlus1, "tokenB", "tokenA") + + tests := map[string]struct { + recordsToSet []types.TwapRecord + input getRecordInput + expectedRecord types.TwapRecord + expErr bool + }{ + "no entries": {[]types.TwapRecord{}, defaultInputAt(baseTime), baseRecord, true}, + "get at latest (exact)": {[]types.TwapRecord{baseRecord}, defaultInputAt(baseTime), baseRecord, false}, + "rev at latest (exact)": {[]types.TwapRecord{baseRecord}, defaultRevInputAt(baseTime), baseRecord, true}, + + "get latest (exact) w/ past entries": { + []types.TwapRecord{tMin1Record, baseRecord}, defaultInputAt(baseTime), baseRecord, false}, + "get entry (exact) w/ a subsequent entry": { + []types.TwapRecord{tMin1Record, baseRecord}, defaultInputAt(tMin1), tMin1Record, false}, + "get sandwitched entry (exact)": { + []types.TwapRecord{tMin1Record, baseRecord, tPlus1Record}, defaultInputAt(baseTime), baseRecord, false}, + "rev sandwitched entry (exact)": { + []types.TwapRecord{tMin1Record, baseRecord, tPlus1Record}, defaultRevInputAt(baseTime), baseRecord, true}, + + "get future": {[]types.TwapRecord{baseRecord}, defaultInputAt(tPlus1), baseRecord, false}, + "get future w/ past entries": {[]types.TwapRecord{tMin1Record, baseRecord}, defaultInputAt(tPlus1), baseRecord, false}, + + "get in between entries (2 entry)": { + []types.TwapRecord{tMin1Record, baseRecord}, + defaultInputAt(baseTime.Add(-time.Millisecond)), tMin1Record, false}, + "get in between entries (3 entry)": { + []types.TwapRecord{tMin1Record, baseRecord, tPlus1Record}, + defaultInputAt(baseTime.Add(-time.Millisecond)), tMin1Record, false}, + "get in between entries (3 entry) #2": { + []types.TwapRecord{tMin1Record, baseRecord, tPlus1Record}, + defaultInputAt(baseTime.Add(time.Millisecond)), baseRecord, false}, + + "query too old": { + []types.TwapRecord{tMin1Record, baseRecord, tPlus1Record}, + defaultInputAt(baseTime.Add(-time.Second * 2)), + baseRecord, true}, + } + for name, test := range tests { + s.Run(name, func() { + s.SetupTest() + for _, record := range test.recordsToSet { + s.twapkeeper.StoreNewRecord(s.Ctx, record) + } + record, err := s.twapkeeper.GetRecordAtOrBeforeTime( + s.Ctx, + test.input.poolId, test.input.t, test.input.asset0Denom, test.input.asset1Denom) + if test.expErr { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().Equal(test.expectedRecord, record) + }) + } +} From 0151b677bd7532ac167aefa02b612173245d40be Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Fri, 22 Jul 2022 17:17:04 -0500 Subject: [PATCH 49/72] add wrong pool ID input case --- x/gamm/twap/store_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/x/gamm/twap/store_test.go b/x/gamm/twap/store_test.go index e3a62e3bd18..97765d4673f 100644 --- a/x/gamm/twap/store_test.go +++ b/x/gamm/twap/store_test.go @@ -112,6 +112,7 @@ func (s *TestSuite) TestGetRecordAtOrBeforeTime() { asset1Denom string } defaultInputAt := func(t time.Time) getRecordInput { return getRecordInput{1, t, "tokenB", "tokenA"} } + wrongPoolIdInputAt := func(t time.Time) getRecordInput { return getRecordInput{2, t, "tokenB", "tokenA"} } defaultRevInputAt := func(t time.Time) getRecordInput { return getRecordInput{1, t, "tokenA", "tokenB"} } baseRecord := newEmptyPriceRecord(1, baseTime, "tokenB", "tokenA") tMin1 := baseTime.Add(-time.Second) @@ -155,6 +156,10 @@ func (s *TestSuite) TestGetRecordAtOrBeforeTime() { []types.TwapRecord{tMin1Record, baseRecord, tPlus1Record}, defaultInputAt(baseTime.Add(-time.Second * 2)), baseRecord, true}, + + "non-existent pool ID": { + []types.TwapRecord{tMin1Record, baseRecord, tPlus1Record}, + wrongPoolIdInputAt(baseTime), baseRecord, true}, } for name, test := range tests { s.Run(name, func() { From 4779ff4aa6eeb0b84c063fe79fb89cb973cbb1c7 Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Fri, 22 Jul 2022 17:29:47 -0500 Subject: [PATCH 50/72] Bring docs update from twap_store pr --- x/gamm/twap/store.go | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/x/gamm/twap/store.go b/x/gamm/twap/store.go index 712c61c230c..133d850307c 100644 --- a/x/gamm/twap/store.go +++ b/x/gamm/twap/store.go @@ -11,6 +11,9 @@ import ( "github.com/osmosis-labs/osmosis/v10/x/gamm/twap/types" ) +// trackChangedPool places an entry into a transient store, +// to track that this pool changed this block. +// This tracking is for use in EndBlock, to create new TWAP records. func (k Keeper) trackChangedPool(ctx sdk.Context, poolId uint64) { store := ctx.TransientStore(k.transientKey) poolIdBz := make([]byte, 8) @@ -20,6 +23,9 @@ func (k Keeper) trackChangedPool(ctx sdk.Context, poolId uint64) { store.Set(poolIdBz, sentinelExistsValue) } +// getChangedPools returns all poolIDs that changed this block. +// This is to be guaranteed by trackChangedPool being called on every +// price-affecting pool action. func (k Keeper) getChangedPools(ctx sdk.Context) []uint64 { store := ctx.TransientStore(k.transientKey) iter := store.Iterator(nil, nil) @@ -34,6 +40,7 @@ func (k Keeper) getChangedPools(ctx sdk.Context) []uint64 { return alteredPoolIds } +// storeHistoricalTWAP writes a twap to the store, in all needed indexing. func (k Keeper) storeHistoricalTWAP(ctx sdk.Context, twap types.TwapRecord) { store := ctx.KVStore(k.storeKey) key1 := types.FormatHistoricalTimeIndexTWAPKey(twap.Time, twap.PoolId, twap.Asset0Denom, twap.Asset1Denom) @@ -55,6 +62,11 @@ func (k Keeper) deleteHistoricalRecord(ctx sdk.Context, twap types.TwapRecord) { store.Delete(key2) } +// getMostRecentRecordStoreRepresentation returns the most recent twap record in the store +// for the provided (pool, asset0, asset1) triplet. +// Its called store representation, because most recent record can refer to it being +// interpolated to the current block time, or after events in this block. +// Neither of which apply to the record this returns. func (k Keeper) getMostRecentRecordStoreRepresentation(ctx sdk.Context, poolId uint64, asset0Denom string, asset1Denom string) (types.TwapRecord, error) { store := ctx.KVStore(k.storeKey) key := types.FormatMostRecentTWAPKey(poolId, asset0Denom, asset1Denom) @@ -62,11 +74,14 @@ func (k Keeper) getMostRecentRecordStoreRepresentation(ctx sdk.Context, poolId u return types.ParseTwapFromBz(bz) } +// getMostRecentRecordStoreRepresentation returns all most recent twap records +// (in state representation) for the provided pool id. func (k Keeper) getAllMostRecentRecordsForPool(ctx sdk.Context, poolId uint64) ([]types.TwapRecord, error) { store := ctx.KVStore(k.storeKey) return types.GetAllMostRecentTwapsForPool(store, poolId) } +// storeNewRecord stores a record, in both the most recent record store and historical stores. func (k Keeper) storeNewRecord(ctx sdk.Context, twap types.TwapRecord) { store := ctx.KVStore(k.storeKey) key := types.FormatMostRecentTWAPKey(twap.PoolId, twap.Asset0Denom, twap.Asset1Denom) @@ -74,8 +89,17 @@ func (k Keeper) storeNewRecord(ctx sdk.Context, twap types.TwapRecord) { k.storeHistoricalTWAP(ctx, twap) } -// returns an error if theres no historical record at or before time. -// (Asking for a time too far back) +// getRecordAtOrBeforeTime on a given input (id, t, asset0, asset1) +// returns the TWAP record from state for (id, t', asset0, asset1), +// where t' is such that: +// * t' <= t +// * there exists no t'' in state, where t'' < t' +// +// This returns an error if: +// * there is no historical record in state at or before t +// - Occurs if t is older than pruning period, or pool creation date. +// * there is no record for the asset pair (asset0, asset1) in particular +// - e.g. asset not in pool, or provided in wrong order. func (k Keeper) getRecordAtOrBeforeTime(ctx sdk.Context, poolId uint64, t time.Time, asset0Denom string, asset1Denom string) (types.TwapRecord, error) { store := ctx.KVStore(k.storeKey) // We make an iteration from time=t + 1ns, to time=0 for this pool. From 8e4f7d5a9391c485ee771036bd55fd1a30ca1c15 Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Fri, 22 Jul 2022 21:04:10 -0500 Subject: [PATCH 51/72] Pull changes from twap_store from review --- x/gamm/twap/store.go | 1 + x/gamm/twap/store_test.go | 44 ++++++++++++++++++++++++--------------- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/x/gamm/twap/store.go b/x/gamm/twap/store.go index 133d850307c..013936aedeb 100644 --- a/x/gamm/twap/store.go +++ b/x/gamm/twap/store.go @@ -1,3 +1,4 @@ +//nolint:unused,deadcode package twap import ( diff --git a/x/gamm/twap/store_test.go b/x/gamm/twap/store_test.go index 97765d4673f..50712277af5 100644 --- a/x/gamm/twap/store_test.go +++ b/x/gamm/twap/store_test.go @@ -9,7 +9,7 @@ import ( // TestTrackChangedPool takes a list of poolIds as test cases, and runs one list per block. // Every simulated block, checks that there no changed pools. // Then runs k.trackChangedPool on every item in the test case list. -// Then checks that changed pools returns the list, deduplicated. +// Then, checks that changed pools return the list, deduplicated. // // This achieves testing the functionality that we depend on, that this clears every end block. func (s *TestSuite) TestTrackChangedPool() { @@ -55,33 +55,38 @@ func (s *TestSuite) TestGetAllMostRecentRecordsForPool() { expectedRecords []types.TwapRecord }{ "set single record": { - []types.TwapRecord{baseRecord}, - 1, - []types.TwapRecord{baseRecord}, + recordsToSet: []types.TwapRecord{baseRecord}, + poolId: 1, + expectedRecords: []types.TwapRecord{baseRecord}, }, "query non-existent pool": { - []types.TwapRecord{baseRecord}, - 2, - []types.TwapRecord{}, + recordsToSet: []types.TwapRecord{baseRecord}, + poolId: 2, + expectedRecords: []types.TwapRecord{}, + }, + "set single record, different pool ID": { + recordsToSet: []types.TwapRecord{newEmptyPriceRecord(2, baseTime, "tokenB", "tokenA")}, + poolId: 2, + expectedRecords: []types.TwapRecord{newEmptyPriceRecord(2, baseTime, "tokenB", "tokenA")}, }, "set two records": { - []types.TwapRecord{baseRecord, tPlusOneRecord}, - 1, - []types.TwapRecord{tPlusOneRecord}, + recordsToSet: []types.TwapRecord{baseRecord, tPlusOneRecord}, + poolId: 1, + expectedRecords: []types.TwapRecord{tPlusOneRecord}, }, "set two records, reverse order": { // The last record, independent of time, takes precedence for most recent. - []types.TwapRecord{tPlusOneRecord, baseRecord}, - 1, - []types.TwapRecord{baseRecord}, + recordsToSet: []types.TwapRecord{tPlusOneRecord, baseRecord}, + poolId: 1, + expectedRecords: []types.TwapRecord{baseRecord}, }, "set multi-asset pool record": { - []types.TwapRecord{ + recordsToSet: []types.TwapRecord{ newEmptyPriceRecord(1, baseTime, "tokenB", "tokenA"), newEmptyPriceRecord(1, baseTime, "tokenC", "tokenB"), newEmptyPriceRecord(1, baseTime, "tokenC", "tokenA")}, - 1, - []types.TwapRecord{ + poolId: 1, + expectedRecords: []types.TwapRecord{ newEmptyPriceRecord(1, baseTime, "tokenB", "tokenA"), newEmptyPriceRecord(1, baseTime, "tokenC", "tokenA"), newEmptyPriceRecord(1, baseTime, "tokenC", "tokenB")}, @@ -101,7 +106,7 @@ func (s *TestSuite) TestGetAllMostRecentRecordsForPool() { } } -// TestGetAllMostRecentRecordsForPool takes a list of records as test cases, +// TestGetRecordAtOrBeforeTime takes a list of records as test cases, // and runs storeNewRecord for everything in sequence. // Then it runs GetRecordAtOrBeforeTime, and sees if its equal to expected func (s *TestSuite) TestGetRecordAtOrBeforeTime() { @@ -160,6 +165,11 @@ func (s *TestSuite) TestGetRecordAtOrBeforeTime() { "non-existent pool ID": { []types.TwapRecord{tMin1Record, baseRecord, tPlus1Record}, wrongPoolIdInputAt(baseTime), baseRecord, true}, + "pool2 record get": { + recordsToSet: []types.TwapRecord{newEmptyPriceRecord(2, baseTime, "tokenB", "tokenA")}, + input: wrongPoolIdInputAt(baseTime), + expectedRecord: newEmptyPriceRecord(2, baseTime, "tokenB", "tokenA"), + expErr: false}, } for name, test := range tests { s.Run(name, func() { From 8c77b0b490cd97672d595a0cd0b1082b02601264 Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Fri, 22 Jul 2022 21:44:03 -0500 Subject: [PATCH 52/72] Update Spec --- x/gamm/twap/README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/x/gamm/twap/README.md b/x/gamm/twap/README.md index 5b9694b04ff..6ba0fd85003 100644 --- a/x/gamm/twap/README.md +++ b/x/gamm/twap/README.md @@ -104,6 +104,18 @@ During EndBlock, new records are created, with: 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. +### Tracking spot-price changing events in a block + +The flow by which we currently track spot price changing events in a block is as follows: +* AMM hook triggers for Swapping, LPing or Exiting a pool +* TWAP listens for this hook, and adds this pool ID to a local tracker +* In end block, TWAP iterates over every changed pool that block, based on the local tracker, and updates their TWAP records +* In end block, TWAP clears the changed pool list, so its blank by the next block. + +The mechanism by which we maintain this changed pool list, is the SDK `Transient Store`. +The transient store is a KV store in the SDK, that stores entries in memory, for the duration of a block, +and then clears on the block committing. This is done to save on gas (and I/O for the state machine). + ## Testing Methodology The pre-release testing methodology planned for the twap module is: From 483d05e37282d2b2b4bbeb4afa7b91dcc0bbc57e Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Fri, 22 Jul 2022 22:19:05 -0500 Subject: [PATCH 53/72] Apply suggestions from docs review Co-authored-by: Roman --- x/gamm/twap/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/x/gamm/twap/README.md b/x/gamm/twap/README.md index 6ba0fd85003..1e0a9474666 100644 --- a/x/gamm/twap/README.md +++ b/x/gamm/twap/README.md @@ -79,7 +79,7 @@ For users who need TWAPs outside the 48 hours stored in the state machine, you c ## Store layout -We maintain twap accumulation records for every AMM pool on Osmosis. +We maintain TWAP accumulation records for every AMM pool on Osmosis. Because Osmosis supports multi-asset pools, a complicating factor is that we have to store a record for every asset pair in the pool. 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`. @@ -98,9 +98,9 @@ A new TWAP record is created in two situations: When a pool is created, records are created with the current spot price of the pool. -During EndBlock, new records are created, with: -* The accumulator's updated based upon the most recent prior accumulator's stored last spot price -* The LastSpotPrice's equal to the EndBlock spot price. +During `EndBlock`, new records are created, with: +* The accumulator's value is updated based upon the most recent prior accumulator's stored last spot price +* The `LastSpotPrice` value is equal to the EndBlock spot price. 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. From b7a475411c37ab48079cb31f957626823b0c68dc Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Fri, 22 Jul 2022 23:17:55 -0500 Subject: [PATCH 54/72] Add further tests --- x/gamm/twap/api_test.go | 28 ++++++++++++++++++++ x/gamm/twap/logic.go | 4 +-- x/gamm/twap/logic_test.go | 55 ++++++++++++++++++++++++++++++--------- 3 files changed, 73 insertions(+), 14 deletions(-) diff --git a/x/gamm/twap/api_test.go b/x/gamm/twap/api_test.go index dffcc60c872..bdaac8c81af 100644 --- a/x/gamm/twap/api_test.go +++ b/x/gamm/twap/api_test.go @@ -67,6 +67,34 @@ func (s *TestSuite) TestGetBeginBlockAccumulatorRecord() { } } +func (s *TestSuite) TestGetArithmeticTwap() { + type getTwapInput struct { + poolId uint64 + quoteAssetDenom string + baseAssetDenom string + startTime time.Time + endTime time.Time + } + + tests := map[string]struct { + recordsToSet []types.TwapRecord + ctxTime time.Time + input getTwapInput + result types.TwapRecord + expError bool + }{} + for name, test := range tests { + s.Run(name, func() { + s.SetupTest() + for _, record := range test.recordsToSet { + s.twapkeeper.StoreNewRecord(s.Ctx, record) + } + s.Ctx = s.Ctx.WithBlockTime(test.ctxTime) + + }) + } +} + // func (s *TestSuite) TestGetArithmeticTwapToNow() { // tests := map[string]struct { // // if start record is blank, don't do any sets diff --git a/x/gamm/twap/logic.go b/x/gamm/twap/logic.go index 6c46f9a404c..5f9838cee6b 100644 --- a/x/gamm/twap/logic.go +++ b/x/gamm/twap/logic.go @@ -135,9 +135,9 @@ func computeArithmeticTwap(startRecord types.TwapRecord, endRecord types.TwapRec // if time difference is 0, then return the last spot price based off of start. if timeDelta == time.Duration(0) { if quoteAsset == startRecord.Asset0Denom { - return startRecord.P0LastSpotPrice + return endRecord.P0LastSpotPrice } - return startRecord.P1LastSpotPrice + return endRecord.P1LastSpotPrice } var accumDiff sdk.Dec if quoteAsset == startRecord.Asset0Denom { diff --git a/x/gamm/twap/logic_test.go b/x/gamm/twap/logic_test.go index 5baddec0b1a..ef3ef12cd2c 100644 --- a/x/gamm/twap/logic_test.go +++ b/x/gamm/twap/logic_test.go @@ -115,6 +115,10 @@ func (s *TestSuite) TestUpdateTwap() { } } +// TestComputeArithmeticTwap tests ComputeArithmeticTwap on various inputs. +// The test vectors are structured by setting up different start and records, +// based on time interval, and their accumulator values. +// Then an expected TWAP is provided in each test case, to compare against computed. func TestComputeArithmeticTwap(t *testing.T) { denom0, denom1 := "token/B", "token/A" newOneSidedRecord := func(time time.Time, accum sdk.Dec, useP0 bool) types.TwapRecord { @@ -124,6 +128,8 @@ func TestComputeArithmeticTwap(t *testing.T) { } else { record.P1ArithmeticTwapAccumulator = accum } + record.P0LastSpotPrice = sdk.ZeroDec() + record.P1LastSpotPrice = sdk.OneDec() return record } @@ -134,8 +140,6 @@ func TestComputeArithmeticTwap(t *testing.T) { expTwap sdk.Dec } - baseTime := time.Unix(1257894000, 0).UTC() - testCaseFromDeltas := func(startAccum, accumDiff sdk.Dec, timeDelta time.Duration, expectedTwap sdk.Dec) testCase { return testCase{ newOneSidedRecord(baseTime, startAccum, true), @@ -144,6 +148,16 @@ func TestComputeArithmeticTwap(t *testing.T) { expectedTwap, } } + testCaseFromDeltasAsset1 := func(startAccum, accumDiff sdk.Dec, timeDelta time.Duration, expectedTwap sdk.Dec) testCase { + return testCase{ + newOneSidedRecord(baseTime, startAccum, false), + newOneSidedRecord(baseTime.Add(timeDelta), startAccum.Add(accumDiff), false), + denom1, + expectedTwap, + } + } + tenSecAccum := OneSec.MulInt64(10) + pointOneAccum := OneSec.QuoInt64(10) plusOneSec := baseTime.Add(time.Second) tests := map[string]testCase{ "basic: spot price = 1 for one second, 0 init accumulator": { @@ -152,16 +166,33 @@ func TestComputeArithmeticTwap(t *testing.T) { denom0, sdk.OneDec(), }, - // "same record: denom0, unset spot price": { - // newOneSidedRecord(baseTime, sdk.ZeroDec(), true), - // newOneSidedRecord(baseTime, sdk.ZeroDec(), true), - // denom0, - // nil, - // }, - "accumulator = 10*OneSec, t=5s. 0 base accum": testCaseFromDeltas(sdk.ZeroDec(), OneSec.MulInt64(10), 5*time.Second, sdk.NewDec(2)), - "accumulator = 10*OneSec, t=3s. 0 base accum": testCaseFromDeltas(sdk.ZeroDec(), OneSec.MulInt64(10), 3*time.Second, osmoutils.OneThird), - "accumulator = 10*OneSec, t=100s. 0 base accum": testCaseFromDeltas(sdk.ZeroDec(), OneSec.MulInt64(10), 100*time.Second, sdk.NewDecWithPrec(1, 1)), - // TODO: Overflow, rounding, same record tests + "same record: denom0, end spot price = 0": { + newOneSidedRecord(baseTime, sdk.ZeroDec(), true), + newOneSidedRecord(baseTime, sdk.ZeroDec(), true), + denom0, + sdk.ZeroDec(), + }, + "same record: denom1, end spot price = 1": { + newOneSidedRecord(baseTime, sdk.ZeroDec(), true), + newOneSidedRecord(baseTime, sdk.ZeroDec(), true), + denom1, + sdk.OneDec(), + }, + "accumulator = 10*OneSec, t=5s. 0 base accum": testCaseFromDeltas(sdk.ZeroDec(), tenSecAccum, 5*time.Second, sdk.NewDec(2)), + "accumulator = 10*OneSec, t=3s. 0 base accum": testCaseFromDeltas(sdk.ZeroDec(), tenSecAccum, 3*time.Second, osmoutils.OneThird), + "accumulator = 10*OneSec, t=100s. 0 base accum": testCaseFromDeltas(sdk.ZeroDec(), tenSecAccum, 100*time.Second, sdk.NewDecWithPrec(1, 1)), + + // test that base accum has no impact + "accumulator = 10*OneSec, t=5s. 10 base accum": testCaseFromDeltas( + sdk.NewDec(10), tenSecAccum, 5*time.Second, sdk.NewDec(2)), + "accumulator = 10*OneSec, t=3s. 10*second base accum": testCaseFromDeltas( + tenSecAccum, tenSecAccum, 3*time.Second, osmoutils.OneThird), + "accumulator = 10*OneSec, t=100s. .1*second base accum": testCaseFromDeltas( + pointOneAccum, tenSecAccum, 100*time.Second, sdk.NewDecWithPrec(1, 1)), + + "accumulator = 10*OneSec, t=100s. 0 base accum (asset 1)": testCaseFromDeltasAsset1(sdk.ZeroDec(), OneSec.MulInt64(10), 100*time.Second, sdk.NewDecWithPrec(1, 1)), + + // TODO: Overflow, rounding } for name, test := range tests { t.Run(name, func(t *testing.T) { From 40692098add8a16589f477a5a616da5569492757 Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Sat, 23 Jul 2022 06:53:13 -0500 Subject: [PATCH 55/72] Make TestGetArithmeticTwap test, fix ordering of base and quote asset in args --- x/gamm/twap/api.go | 19 +++++----- x/gamm/twap/api_test.go | 71 +++++++++++++++++++++++++++++++++++--- x/gamm/twap/keeper_test.go | 19 +++++++++- x/gamm/twap/store.go | 5 +-- 4 files changed, 98 insertions(+), 16 deletions(-) diff --git a/x/gamm/twap/api.go b/x/gamm/twap/api.go index 8c24791b05d..3bee36b57b7 100644 --- a/x/gamm/twap/api.go +++ b/x/gamm/twap/api.go @@ -32,22 +32,23 @@ import ( func (k Keeper) GetArithmeticTwap( ctx sdk.Context, poolId uint64, - quoteAssetDenom string, baseAssetDenom string, + quoteAssetDenom string, startTime time.Time, endTime time.Time) (sdk.Dec, error) { + if startTime.After(endTime) { + return sdk.Dec{}, errors.New("called GetArithmeticTwap with a start time that is after the end time") + } if endTime.Equal(ctx.BlockTime()) { - return k.GetArithmeticTwapToNow(ctx, poolId, quoteAssetDenom, baseAssetDenom, startTime) + return k.GetArithmeticTwapToNow(ctx, poolId, baseAssetDenom, quoteAssetDenom, startTime) } else if endTime.After(ctx.BlockTime()) { return sdk.Dec{}, errors.New("called GetArithmeticTwap with an end time in the future") - } else if startTime.After(endTime) { - return sdk.Dec{}, errors.New("called GetArithmeticTwap with a start time that is after the end time") } - startRecord, err := k.getInterpolatedRecord(ctx, poolId, startTime, quoteAssetDenom, baseAssetDenom) + startRecord, err := k.getInterpolatedRecord(ctx, poolId, startTime, baseAssetDenom, quoteAssetDenom) if err != nil { return sdk.Dec{}, err } - endRecord, err := k.getInterpolatedRecord(ctx, poolId, endTime, quoteAssetDenom, baseAssetDenom) + endRecord, err := k.getInterpolatedRecord(ctx, poolId, endTime, baseAssetDenom, quoteAssetDenom) if err != nil { return sdk.Dec{}, err } @@ -58,14 +59,14 @@ func (k Keeper) GetArithmeticTwap( func (k Keeper) GetArithmeticTwapToNow( ctx sdk.Context, poolId uint64, - quoteAssetDenom string, baseAssetDenom string, + quoteAssetDenom string, startTime time.Time) (sdk.Dec, error) { - startRecord, err := k.getInterpolatedRecord(ctx, poolId, startTime, quoteAssetDenom, baseAssetDenom) + startRecord, err := k.getInterpolatedRecord(ctx, poolId, startTime, baseAssetDenom, quoteAssetDenom) if err != nil { return sdk.Dec{}, err } - endRecord, err := k.GetBeginBlockAccumulatorRecord(ctx, poolId, quoteAssetDenom, baseAssetDenom) + endRecord, err := k.GetBeginBlockAccumulatorRecord(ctx, poolId, baseAssetDenom, quoteAssetDenom) if err != nil { return sdk.Dec{}, err } diff --git a/x/gamm/twap/api_test.go b/x/gamm/twap/api_test.go index bdaac8c81af..2c343643526 100644 --- a/x/gamm/twap/api_test.go +++ b/x/gamm/twap/api_test.go @@ -76,13 +76,67 @@ func (s *TestSuite) TestGetArithmeticTwap() { endTime time.Time } + makeSimpleTwapInput := func(startTime time.Time, endTime time.Time, isQuoteTokenA bool) getTwapInput { + quoteAssetDenom, baseAssetDenom := denom0, denom1 + if isQuoteTokenA { + baseAssetDenom, quoteAssetDenom = quoteAssetDenom, baseAssetDenom + } + return getTwapInput{1, quoteAssetDenom, baseAssetDenom, startTime, endTime} + } + + quoteAssetA := true + quoteAssetB := false + tPlusOne := baseTime.Add(time.Second) + // base record is a record with t=baseTime, sp0=10, sp1=.1, accumulators set to 0 + baseRecord := newTwapRecordWithDefaults(baseTime, sdk.NewDec(10), sdk.ZeroDec(), sdk.ZeroDec()) + tests := map[string]struct { recordsToSet []types.TwapRecord ctxTime time.Time input getTwapInput - result types.TwapRecord - expError bool - }{} + expTwap sdk.Dec + expErrorStr string + }{ + "(single record) start and end point to same record": { + recordsToSet: []types.TwapRecord{baseRecord}, + ctxTime: baseTime.Add(time.Minute), + input: makeSimpleTwapInput(baseTime, tPlusOne, quoteAssetA), + expTwap: sdk.NewDec(10), + }, + "(single record) start and end point to same record, use sp1": { + recordsToSet: []types.TwapRecord{baseRecord}, + ctxTime: baseTime.Add(time.Minute), + input: makeSimpleTwapInput(baseTime, tPlusOne, quoteAssetB), + expTwap: sdk.NewDecWithPrec(1, 1), + }, + + // error catching + "end time in future": { + recordsToSet: []types.TwapRecord{baseRecord}, + ctxTime: baseTime, + input: makeSimpleTwapInput(baseTime, tPlusOne, quoteAssetA), + expErrorStr: "future", + }, + "start time after end time": { + recordsToSet: []types.TwapRecord{baseRecord}, + ctxTime: baseTime, + input: makeSimpleTwapInput(tPlusOne, baseTime, quoteAssetA), + expErrorStr: "after", + }, + "start time too old (end time = now)": { + recordsToSet: []types.TwapRecord{baseRecord}, + ctxTime: baseTime, + input: makeSimpleTwapInput(baseTime.Add(-time.Hour), baseTime, quoteAssetA), + expErrorStr: "too old", + }, + "start time too old": { + recordsToSet: []types.TwapRecord{baseRecord}, + ctxTime: baseTime.Add(time.Second), + input: makeSimpleTwapInput(baseTime.Add(-time.Hour), baseTime, quoteAssetA), + expErrorStr: "too old", + }, + // TODO: overflow tests, multi-asset pool handling, make more record interpolation cases + } for name, test := range tests { s.Run(name, func() { s.SetupTest() @@ -90,7 +144,16 @@ func (s *TestSuite) TestGetArithmeticTwap() { s.twapkeeper.StoreNewRecord(s.Ctx, record) } s.Ctx = s.Ctx.WithBlockTime(test.ctxTime) - + twap, err := s.twapkeeper.GetArithmeticTwap(s.Ctx, test.input.poolId, + test.input.quoteAssetDenom, test.input.baseAssetDenom, + test.input.startTime, test.input.endTime) + if test.expErrorStr != "" { + s.Require().Error(err) + s.Require().Contains(err.Error(), test.expErrorStr) + return + } + s.Require().NoError(err) + s.Require().Equal(test.expTwap, twap) }) } } diff --git a/x/gamm/twap/keeper_test.go b/x/gamm/twap/keeper_test.go index 4ff8a68e015..afd142ae625 100644 --- a/x/gamm/twap/keeper_test.go +++ b/x/gamm/twap/keeper_test.go @@ -12,7 +12,10 @@ import ( "github.com/osmosis-labs/osmosis/v10/x/gamm/twap/types" ) -var defaultUniV2Coins = sdk.NewCoins(sdk.NewInt64Coin("token/B", 1_000_000_000), sdk.NewInt64Coin("token/A", 1_000_000_000)) +// TODO: Consider switching this everywhere +var denom0 = "token/B" +var denom1 = "token/A" +var defaultUniV2Coins = sdk.NewCoins(sdk.NewInt64Coin(denom0, 1_000_000_000), sdk.NewInt64Coin(denom1, 1_000_000_000)) var baseTime = time.Unix(1257894000, 0).UTC() type TestSuite struct { @@ -36,6 +39,20 @@ func (s *TestSuite) setupDefaultPool() (poolId uint64, denomA, denomB string) { return } +func newTwapRecordWithDefaults(t time.Time, sp0, accum0, accum1 sdk.Dec) types.TwapRecord { + return types.TwapRecord{ + PoolId: 1, + Time: t, + Asset0Denom: denom0, + Asset1Denom: denom1, + + P0LastSpotPrice: sp0, + P1LastSpotPrice: sdk.OneDec().Quo(sp0), + P0ArithmeticTwapAccumulator: accum0, + P1ArithmeticTwapAccumulator: accum1, + } +} + func newEmptyPriceRecord(poolId uint64, t time.Time, asset0 string, asset1 string) types.TwapRecord { return types.TwapRecord{ PoolId: poolId, diff --git a/x/gamm/twap/store.go b/x/gamm/twap/store.go index 013936aedeb..4c31bd2a169 100644 --- a/x/gamm/twap/store.go +++ b/x/gamm/twap/store.go @@ -4,6 +4,7 @@ package twap import ( "encoding/binary" "errors" + "fmt" "time" sdk "github.com/cosmos/cosmos-sdk/types" @@ -139,6 +140,6 @@ func (k Keeper) getRecordAtOrBeforeTime(ctx sdk.Context, poolId uint64, t time.T return twap, nil } } - return types.TwapRecord{}, errors.New("something went wrong - TWAP not found, but there are twaps available for this time." + - " Were provided asset0denom and asset1denom correct, and in order (asset0 > asset1)?") + return types.TwapRecord{}, fmt.Errorf("TWAP not found, but there are twaps available for this time."+ + " Were provided asset0denom and asset1denom (%s, %s) correct, and in order (asset0 > asset1)?", asset0Denom, asset1Denom) } From d481b1d180733250b227190e5049265a51e44974 Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Sat, 23 Jul 2022 07:38:10 -0500 Subject: [PATCH 56/72] Add more test cases --- osmoutils/dec_helper.go | 2 +- x/gamm/twap/api.go | 1 + x/gamm/twap/api_test.go | 69 ++++++++++++++++++++++++++------------- x/gamm/twap/logic_test.go | 11 ++++--- 4 files changed, 56 insertions(+), 27 deletions(-) diff --git a/osmoutils/dec_helper.go b/osmoutils/dec_helper.go index 84e90af1636..76fc66d1d7c 100644 --- a/osmoutils/dec_helper.go +++ b/osmoutils/dec_helper.go @@ -6,7 +6,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ) -var OneThird sdk.Dec = sdk.MustNewDecFromStr("3.333333333333333333") +var ThreePlusOneThird sdk.Dec = sdk.MustNewDecFromStr("3.333333333333333333") // intended to be used with require/assert: require.True(DecEq(...)) // TODO: Replace with function in SDK types package when we update diff --git a/x/gamm/twap/api.go b/x/gamm/twap/api.go index 3bee36b57b7..b6797eed63a 100644 --- a/x/gamm/twap/api.go +++ b/x/gamm/twap/api.go @@ -56,6 +56,7 @@ func (k Keeper) GetArithmeticTwap( return twap, nil } +// GetArithmeticTwapToNow returns GetArithmeticTwap on the input, with endTime being fixed to ctx.BlockTime() func (k Keeper) GetArithmeticTwapToNow( ctx sdk.Context, poolId uint64, diff --git a/x/gamm/twap/api_test.go b/x/gamm/twap/api_test.go index 2c343643526..71b0fff531e 100644 --- a/x/gamm/twap/api_test.go +++ b/x/gamm/twap/api_test.go @@ -5,6 +5,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/osmosis-labs/osmosis/v10/osmoutils" "github.com/osmosis-labs/osmosis/v10/x/gamm/twap/types" ) @@ -89,6 +90,12 @@ func (s *TestSuite) TestGetArithmeticTwap() { tPlusOne := baseTime.Add(time.Second) // base record is a record with t=baseTime, sp0=10, sp1=.1, accumulators set to 0 baseRecord := newTwapRecordWithDefaults(baseTime, sdk.NewDec(10), sdk.ZeroDec(), sdk.ZeroDec()) + // record with t=baseTime+10, sp0=5, sp1=.2, accumulators updated from baseRecord + tPlus10sp5Record := newTwapRecordWithDefaults( + baseTime.Add(10*time.Second), sdk.NewDec(5), OneSec.MulInt64(10*10), OneSec) + // record with t=baseTime+20, sp0=2, sp1=.5, accumulators updated from tPlus10sp5Record + // tPlus20sp2Record := newTwapRecordWithDefaults( + // baseTime.Add(20*time.Second), sdk.NewDec(2), OneSec.MulInt64(10*10+5*10), OneSec.MulInt64(3)) tests := map[string]struct { recordsToSet []types.TwapRecord @@ -97,18 +104,56 @@ func (s *TestSuite) TestGetArithmeticTwap() { expTwap sdk.Dec expErrorStr string }{ - "(single record) start and end point to same record": { + "(1 record) start and end point to same record": { recordsToSet: []types.TwapRecord{baseRecord}, ctxTime: baseTime.Add(time.Minute), input: makeSimpleTwapInput(baseTime, tPlusOne, quoteAssetA), expTwap: sdk.NewDec(10), }, - "(single record) start and end point to same record, use sp1": { + "(1 record) start and end point to same record, use sp1": { recordsToSet: []types.TwapRecord{baseRecord}, ctxTime: baseTime.Add(time.Minute), input: makeSimpleTwapInput(baseTime, tPlusOne, quoteAssetB), expTwap: sdk.NewDecWithPrec(1, 1), }, + "(1 record) start and end point to same record, end time = now": { + recordsToSet: []types.TwapRecord{baseRecord}, + ctxTime: baseTime.Add(time.Minute), + input: makeSimpleTwapInput(baseTime, baseTime.Add(time.Minute), quoteAssetA), + expTwap: sdk.NewDec(10), + }, + + "(2 record) start and end point to same record": { + recordsToSet: []types.TwapRecord{baseRecord, tPlus10sp5Record}, + ctxTime: baseTime.Add(time.Minute), + input: makeSimpleTwapInput(baseTime, tPlusOne, quoteAssetA), + expTwap: sdk.NewDec(10), + }, + "(2 record) start and end exact, different records": { + recordsToSet: []types.TwapRecord{baseRecord, tPlus10sp5Record}, + ctxTime: baseTime.Add(time.Minute), + input: makeSimpleTwapInput(baseTime, baseTime.Add(10*time.Second), quoteAssetA), + expTwap: sdk.NewDec(10), + }, + "(2 record) start exact, end after second record": { + recordsToSet: []types.TwapRecord{baseRecord, tPlus10sp5Record}, + ctxTime: baseTime.Add(time.Minute), + input: makeSimpleTwapInput(baseTime, baseTime.Add(20*time.Second), quoteAssetA), + expTwap: sdk.NewDecWithPrec(75, 1), // 10 for 10s, 5 for 10s + }, + "(2 record) start exact, end after second record, sp1": { + recordsToSet: []types.TwapRecord{baseRecord, tPlus10sp5Record}, + ctxTime: baseTime.Add(time.Minute), + input: makeSimpleTwapInput(baseTime, baseTime.Add(20*time.Second), quoteAssetB), + expTwap: sdk.NewDecWithPrec(15, 2), // .1 for 10s, .2 for 10s + }, + "(2 record) start and end interpolated": { + recordsToSet: []types.TwapRecord{baseRecord, tPlus10sp5Record}, + ctxTime: baseTime.Add(time.Minute), + input: makeSimpleTwapInput(baseTime.Add(5*time.Second), baseTime.Add(20*time.Second), quoteAssetA), + // 10 for 5s, 5 for 10s = 100/15 = 6 + 2/3 = 6.66666666 + expTwap: osmoutils.ThreePlusOneThird.MulInt64(2), + }, // error catching "end time in future": { @@ -157,23 +202,3 @@ func (s *TestSuite) TestGetArithmeticTwap() { }) } } - -// func (s *TestSuite) TestGetArithmeticTwapToNow() { -// tests := map[string]struct { -// // if start record is blank, don't do any sets -// setupRecords []types.TwapRecord -// latestRecord types.TwapRecord -// // We set it to have the updated time -// expRecord types.TwapRecord -// time time.Time -// poolId uint64 -// quoteDenom string -// baseDenom string -// expError bool -// }{} -// for name, tc := range tests { -// s.Run(name, func() { - -// }) -// } -// } diff --git a/x/gamm/twap/logic_test.go b/x/gamm/twap/logic_test.go index ef3ef12cd2c..301f1401c90 100644 --- a/x/gamm/twap/logic_test.go +++ b/x/gamm/twap/logic_test.go @@ -178,15 +178,18 @@ func TestComputeArithmeticTwap(t *testing.T) { denom1, sdk.OneDec(), }, - "accumulator = 10*OneSec, t=5s. 0 base accum": testCaseFromDeltas(sdk.ZeroDec(), tenSecAccum, 5*time.Second, sdk.NewDec(2)), - "accumulator = 10*OneSec, t=3s. 0 base accum": testCaseFromDeltas(sdk.ZeroDec(), tenSecAccum, 3*time.Second, osmoutils.OneThird), - "accumulator = 10*OneSec, t=100s. 0 base accum": testCaseFromDeltas(sdk.ZeroDec(), tenSecAccum, 100*time.Second, sdk.NewDecWithPrec(1, 1)), + "accumulator = 10*OneSec, t=5s. 0 base accum": testCaseFromDeltas( + sdk.ZeroDec(), tenSecAccum, 5*time.Second, sdk.NewDec(2)), + "accumulator = 10*OneSec, t=3s. 0 base accum": testCaseFromDeltas( + sdk.ZeroDec(), tenSecAccum, 3*time.Second, osmoutils.ThreePlusOneThird), + "accumulator = 10*OneSec, t=100s. 0 base accum": testCaseFromDeltas( + sdk.ZeroDec(), tenSecAccum, 100*time.Second, sdk.NewDecWithPrec(1, 1)), // test that base accum has no impact "accumulator = 10*OneSec, t=5s. 10 base accum": testCaseFromDeltas( sdk.NewDec(10), tenSecAccum, 5*time.Second, sdk.NewDec(2)), "accumulator = 10*OneSec, t=3s. 10*second base accum": testCaseFromDeltas( - tenSecAccum, tenSecAccum, 3*time.Second, osmoutils.OneThird), + tenSecAccum, tenSecAccum, 3*time.Second, osmoutils.ThreePlusOneThird), "accumulator = 10*OneSec, t=100s. .1*second base accum": testCaseFromDeltas( pointOneAccum, tenSecAccum, 100*time.Second, sdk.NewDecWithPrec(1, 1)), From 4a48f4d81c4972d4f6ab090fa1c840eab7dc8681 Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Sat, 23 Jul 2022 07:56:00 -0500 Subject: [PATCH 57/72] Add endblock flow test --- app/apptesting/test_suite.go | 2 +- x/gamm/twap/hook_test.go | 48 ++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/app/apptesting/test_suite.go b/app/apptesting/test_suite.go index 152a5a7d3c1..c9e01f5b659 100644 --- a/app/apptesting/test_suite.go +++ b/app/apptesting/test_suite.go @@ -66,7 +66,7 @@ func (s *KeeperTestHelper) Commit() { oldHeight := s.Ctx.BlockHeight() oldHeader := s.Ctx.BlockHeader() s.App.Commit() - newHeader := tmtypes.Header{Height: oldHeight + 1, ChainID: oldHeader.ChainID, Time: time.Now().UTC()} + newHeader := tmtypes.Header{Height: oldHeight + 1, ChainID: oldHeader.ChainID, Time: oldHeader.Time.Add(time.Second)} s.App.BeginBlock(abci.RequestBeginBlock{Header: newHeader}) s.Ctx = s.App.GetBaseApp().NewContext(false, newHeader) } diff --git a/x/gamm/twap/hook_test.go b/x/gamm/twap/hook_test.go index 0a792c86c7b..3d7bf5a85ab 100644 --- a/x/gamm/twap/hook_test.go +++ b/x/gamm/twap/hook_test.go @@ -1,6 +1,8 @@ package twap_test import ( + "time" + "github.com/osmosis-labs/osmosis/v10/x/gamm/twap/types" ) @@ -31,3 +33,49 @@ func (s *TestSuite) TestSwapTriggeringTrackPoolId() { s.Require().Equal([]uint64{poolId}, s.twapkeeper.GetChangedPools(s.Ctx)) } + +// TestSwapAndEndBlockTriggeringSave tests that if we: +// * create a pool in block 1 +// * swap in block 2 +// then after block 2 end block, we have saved records for the pool, +// for both block 1 & 2, with distinct spot prices in their records, and accumulators incremented. +// TODO: Abstract this to be more table driven, and test more pool / block setups. +func (s *TestSuite) TestSwapAndEndBlockTriggeringSave() { + s.Ctx = s.Ctx.WithBlockTime(baseTime) + poolId := s.PrepareUni2PoolWithAssets(defaultUniV2Coins[0], defaultUniV2Coins[1]) + expectedHistoricalTwap, err := types.NewTwapRecord(s.App.GAMMKeeper, s.Ctx, poolId, "token/B", "token/A") + s.Require().NoError(err) + + s.EndBlock() + s.Commit() // clear transient store + // Now on a clean state after a create pool + s.Require().Equal(baseTime.Add(time.Second), s.Ctx.BlockTime()) + s.RunBasicSwap(poolId) + + // accumulators are default right here + expectedLatestTwapUpToAccum, err := types.NewTwapRecord(s.App.GAMMKeeper, s.Ctx, poolId, "token/B", "token/A") + s.Require().NoError(err) + // ensure different spot prices + s.Require().NotEqual(expectedHistoricalTwap.P0LastSpotPrice, expectedLatestTwapUpToAccum.P0LastSpotPrice) + s.Require().NotEqual(expectedHistoricalTwap.P1LastSpotPrice, expectedLatestTwapUpToAccum.P1LastSpotPrice) + + s.EndBlock() + + // check records + historicalOldTwap, err := s.twapkeeper.GetRecordAtOrBeforeTime(s.Ctx, poolId, baseTime, "token/B", "token/A") + s.Require().NoError(err) + s.Require().Equal(expectedHistoricalTwap, historicalOldTwap) + + latestTwap, err := s.twapkeeper.GetMostRecentRecordStoreRepresentation(s.Ctx, poolId, "token/B", "token/A") + s.Require().NoError(err) + s.Require().Equal(latestTwap.P0LastSpotPrice, expectedLatestTwapUpToAccum.P0LastSpotPrice) + s.Require().Equal(latestTwap.P1LastSpotPrice, expectedLatestTwapUpToAccum.P1LastSpotPrice) + + latestTwap2, err := s.twapkeeper.GetRecordAtOrBeforeTime(s.Ctx, poolId, s.Ctx.BlockTime(), "token/B", "token/A") + s.Require().NoError(err) + s.Require().Equal(latestTwap, latestTwap2) + + // check accumulators incremented - we test details of correct increment in logic + s.Require().True(latestTwap.P0ArithmeticTwapAccumulator.GT(historicalOldTwap.P0ArithmeticTwapAccumulator)) + s.Require().True(latestTwap.P1ArithmeticTwapAccumulator.GT(historicalOldTwap.P1ArithmeticTwapAccumulator)) +} From ae4aa1a9e8e2c4cfd27fe086610495b841b7dfcb Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Fri, 5 Aug 2022 09:37:09 -0500 Subject: [PATCH 58/72] error msg update --- x/gamm/twap/api.go | 8 +++++--- x/gamm/twap/store.go | 5 ++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/x/gamm/twap/api.go b/x/gamm/twap/api.go index b6797eed63a..05704a0fe36 100644 --- a/x/gamm/twap/api.go +++ b/x/gamm/twap/api.go @@ -1,7 +1,7 @@ package twap import ( - "errors" + "fmt" "time" sdk "github.com/cosmos/cosmos-sdk/types" @@ -37,12 +37,14 @@ func (k Keeper) GetArithmeticTwap( startTime time.Time, endTime time.Time) (sdk.Dec, error) { if startTime.After(endTime) { - return sdk.Dec{}, errors.New("called GetArithmeticTwap with a start time that is after the end time") + return sdk.Dec{}, fmt.Errorf("called GetArithmeticTwap with a start time that is after the end time."+ + " (start time %s, end time %s)", startTime, endTime) } if endTime.Equal(ctx.BlockTime()) { return k.GetArithmeticTwapToNow(ctx, poolId, baseAssetDenom, quoteAssetDenom, startTime) } else if endTime.After(ctx.BlockTime()) { - return sdk.Dec{}, errors.New("called GetArithmeticTwap with an end time in the future") + return sdk.Dec{}, fmt.Errorf("called GetArithmeticTwap with an end time in the future."+ + " (end time %s, current time %s)", endTime, ctx.BlockTime()) } startRecord, err := k.getInterpolatedRecord(ctx, poolId, startTime, baseAssetDenom, quoteAssetDenom) if err != nil { diff --git a/x/gamm/twap/store.go b/x/gamm/twap/store.go index e7bf9a551b8..5c6d0074073 100644 --- a/x/gamm/twap/store.go +++ b/x/gamm/twap/store.go @@ -3,7 +3,6 @@ package twap import ( "encoding/binary" - "errors" "fmt" "time" @@ -131,8 +130,8 @@ func (k Keeper) getRecordAtOrBeforeTime(ctx sdk.Context, poolId uint64, t time.T return types.TwapRecord{}, err } if len(twaps) == 0 { - return types.TwapRecord{}, errors.New("looking for a time thats too old, not in the historical index. " + - " Try storing the accumulator value.") + return types.TwapRecord{}, fmt.Errorf("looking for a time thats too old, not in the historical index. "+ + " Try storing the accumulator value. (requested time %s)", t) } for _, twap := range twaps { From 1d02a33efcb0cbc98186845b702833d0613bca15 Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Fri, 5 Aug 2022 09:54:15 -0500 Subject: [PATCH 59/72] rename to listeners.go --- x/gamm/twap/{hook_listener.go => listeners.go} | 0 x/gamm/twap/{hook_test.go => listeners_test.go} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename x/gamm/twap/{hook_listener.go => listeners.go} (100%) rename x/gamm/twap/{hook_test.go => listeners_test.go} (100%) diff --git a/x/gamm/twap/hook_listener.go b/x/gamm/twap/listeners.go similarity index 100% rename from x/gamm/twap/hook_listener.go rename to x/gamm/twap/listeners.go diff --git a/x/gamm/twap/hook_test.go b/x/gamm/twap/listeners_test.go similarity index 100% rename from x/gamm/twap/hook_test.go rename to x/gamm/twap/listeners_test.go From 654c82a46ea40408c9aa6fd164be1a564a6c3e9d Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Fri, 5 Aug 2022 10:02:52 -0500 Subject: [PATCH 60/72] Add comment for record creation on pool creation --- x/gamm/twap/logic.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/x/gamm/twap/logic.go b/x/gamm/twap/logic.go index 5f9838cee6b..7cc573ede69 100644 --- a/x/gamm/twap/logic.go +++ b/x/gamm/twap/logic.go @@ -17,6 +17,10 @@ func (k Keeper) afterCreatePool(ctx sdk.Context, poolId uint64) error { if err != nil { return err } + // we create a record here, because we need the record to exist in the event + // that there is a swap against this pool in this same block. + // furthermore, this protects against an edge case where a pool is created + // during EndBlock, after twapkeeper's endblock. k.storeNewRecord(ctx, record) } k.trackChangedPool(ctx, poolId) From f5a60083b84edceaebda0dca0753cb4380836b1a Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Fri, 5 Aug 2022 10:22:21 -0500 Subject: [PATCH 61/72] api_test requested updates --- x/gamm/twap/api_test.go | 25 +++++++------------------ x/gamm/twap/keeper_test.go | 2 ++ 2 files changed, 9 insertions(+), 18 deletions(-) diff --git a/x/gamm/twap/api_test.go b/x/gamm/twap/api_test.go index 71b0fff531e..5c13cb5f560 100644 --- a/x/gamm/twap/api_test.go +++ b/x/gamm/twap/api_test.go @@ -17,11 +17,6 @@ func (s *TestSuite) TestGetBeginBlockAccumulatorRecord() { zeroAccumTenPoint1Record := recordWithUpdatedSpotPrice(initStartRecord, sdk.NewDec(10), sdk.NewDecWithPrec(1, 1)) - blankRecord := types.TwapRecord{} - defaultTime := s.Ctx.BlockTime() - - tPlusOneSec := defaultTime.Add(time.Second) - tests := map[string]struct { // if start record is blank, don't do any sets startRecord types.TwapRecord @@ -33,14 +28,14 @@ func (s *TestSuite) TestGetBeginBlockAccumulatorRecord() { baseDenom string expError bool }{ - "no record (wrong pool ID)": {blankRecord, blankRecord, defaultTime, 4, denomA, denomB, true}, - "default record": {blankRecord, initStartRecord, defaultTime, 1, denomA, denomB, false}, - "one second later record": {blankRecord, recordWithUpdatedAccum(initStartRecord, OneSec, OneSec), tPlusOneSec, 1, denomA, denomB, false}, - "idempotent overwrite": {initStartRecord, initStartRecord, defaultTime, 1, denomA, denomB, false}, - "idempotent overwrite2": {initStartRecord, recordWithUpdatedAccum(initStartRecord, OneSec, OneSec), tPlusOneSec, 1, denomA, denomB, false}, + "no record (wrong pool ID)": {initStartRecord, initStartRecord, baseTime, 4, denomA, denomB, true}, + "default record": {initStartRecord, initStartRecord, baseTime, 1, denomA, denomB, false}, + "one second later record": {initStartRecord, recordWithUpdatedAccum(initStartRecord, OneSec, OneSec), tPlusOne, 1, denomA, denomB, false}, + "idempotent overwrite": {initStartRecord, initStartRecord, baseTime, 1, denomA, denomB, false}, + "idempotent overwrite2": {initStartRecord, recordWithUpdatedAccum(initStartRecord, OneSec, OneSec), tPlusOne, 1, denomA, denomB, false}, "diff spot price": {zeroAccumTenPoint1Record, recordWithUpdatedAccum(zeroAccumTenPoint1Record, OneSec.MulInt64(10), OneSec.QuoInt64(10)), - tPlusOneSec, 1, denomA, denomB, false}, + tPlusOne, 1, denomA, denomB, false}, // TODO: Overflow } for name, tc := range tests { @@ -49,12 +44,7 @@ func (s *TestSuite) TestGetBeginBlockAccumulatorRecord() { s.Ctx = s.Ctx.WithBlockTime(tc.time) tc.expRecord.Time = tc.time - // setup record - initSetRecord := tc.startRecord - if (tc.startRecord == types.TwapRecord{}) { - initSetRecord = initStartRecord - } - s.twapkeeper.StoreNewRecord(s.Ctx, initSetRecord) + s.twapkeeper.StoreNewRecord(s.Ctx, tc.startRecord) actualRecord, err := s.twapkeeper.GetBeginBlockAccumulatorRecord(s.Ctx, tc.poolId, tc.baseDenom, tc.quoteDenom) @@ -87,7 +77,6 @@ func (s *TestSuite) TestGetArithmeticTwap() { quoteAssetA := true quoteAssetB := false - tPlusOne := baseTime.Add(time.Second) // base record is a record with t=baseTime, sp0=10, sp1=.1, accumulators set to 0 baseRecord := newTwapRecordWithDefaults(baseTime, sdk.NewDec(10), sdk.ZeroDec(), sdk.ZeroDec()) // record with t=baseTime+10, sp0=5, sp1=.2, accumulators updated from baseRecord diff --git a/x/gamm/twap/keeper_test.go b/x/gamm/twap/keeper_test.go index afd142ae625..c1662d0e006 100644 --- a/x/gamm/twap/keeper_test.go +++ b/x/gamm/twap/keeper_test.go @@ -17,6 +17,7 @@ var denom0 = "token/B" var denom1 = "token/A" var defaultUniV2Coins = sdk.NewCoins(sdk.NewInt64Coin(denom0, 1_000_000_000), sdk.NewInt64Coin(denom1, 1_000_000_000)) var baseTime = time.Unix(1257894000, 0).UTC() +var tPlusOne = baseTime.Add(time.Second) type TestSuite struct { apptesting.KeeperTestHelper @@ -30,6 +31,7 @@ func TestSuiteRun(t *testing.T) { func (s *TestSuite) SetupTest() { s.Setup() s.twapkeeper = s.App.TwapKeeper + s.Ctx = s.Ctx.WithBlockTime(baseTime) } // sets up a new two asset pool, with spot price 1 From aa3d79f3339dccb3526bc708cc25ab1290ee6e5c Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Fri, 5 Aug 2022 10:52:59 -0500 Subject: [PATCH 62/72] Update x/gamm/twap/export_test.go Co-authored-by: Aleksandr Bezobchuk --- x/gamm/twap/export_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x/gamm/twap/export_test.go b/x/gamm/twap/export_test.go index fbdfc15e9a0..78c056087b7 100644 --- a/x/gamm/twap/export_test.go +++ b/x/gamm/twap/export_test.go @@ -36,7 +36,7 @@ func (k Keeper) UpdateRecord(ctx sdk.Context, record types.TwapRecord) types.Twa return k.updateRecord(ctx, record) } -func ComputeArithmeticTwap(startRecord types.TwapRecord, endRecord types.TwapRecord, quoteAsset string) sdk.Dec { +func ComputeArithmeticTwap(startRecord, endRecord types.TwapRecord, quoteAsset string) sdk.Dec { return computeArithmeticTwap(startRecord, endRecord, quoteAsset) } From ccf67b89c5dbb4729e521ca8a097bc3b4f75036d Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Fri, 5 Aug 2022 11:13:19 -0500 Subject: [PATCH 63/72] var block & denom0 / denom1 usage --- x/gamm/twap/keeper_test.go | 12 ++++++----- x/gamm/twap/listeners_test.go | 16 +++++++-------- x/gamm/twap/logic_test.go | 1 - x/gamm/twap/store_test.go | 38 +++++++++++++++++------------------ 4 files changed, 34 insertions(+), 33 deletions(-) diff --git a/x/gamm/twap/keeper_test.go b/x/gamm/twap/keeper_test.go index c1662d0e006..94784bbae54 100644 --- a/x/gamm/twap/keeper_test.go +++ b/x/gamm/twap/keeper_test.go @@ -13,11 +13,13 @@ import ( ) // TODO: Consider switching this everywhere -var denom0 = "token/B" -var denom1 = "token/A" -var defaultUniV2Coins = sdk.NewCoins(sdk.NewInt64Coin(denom0, 1_000_000_000), sdk.NewInt64Coin(denom1, 1_000_000_000)) -var baseTime = time.Unix(1257894000, 0).UTC() -var tPlusOne = baseTime.Add(time.Second) +var ( + denom0 = "token/B" + denom1 = "token/A" + defaultUniV2Coins = sdk.NewCoins(sdk.NewInt64Coin(denom0, 1_000_000_000), sdk.NewInt64Coin(denom1, 1_000_000_000)) + baseTime = time.Unix(1257894000, 0).UTC() + tPlusOne = baseTime.Add(time.Second) +) type TestSuite struct { apptesting.KeeperTestHelper diff --git a/x/gamm/twap/listeners_test.go b/x/gamm/twap/listeners_test.go index 3d7bf5a85ab..3128cddebe1 100644 --- a/x/gamm/twap/listeners_test.go +++ b/x/gamm/twap/listeners_test.go @@ -11,14 +11,14 @@ import ( func (s *TestSuite) TestCreateTwoAssetPoolFlow() { poolId := s.PrepareUni2PoolWithAssets(defaultUniV2Coins[0], defaultUniV2Coins[1]) - expectedTwap, err := types.NewTwapRecord(s.App.GAMMKeeper, s.Ctx, poolId, "token/B", "token/A") + expectedTwap, err := types.NewTwapRecord(s.App.GAMMKeeper, s.Ctx, poolId, denom0, denom1) s.Require().NoError(err) - twap, err := s.twapkeeper.GetMostRecentRecordStoreRepresentation(s.Ctx, poolId, "token/B", "token/A") + twap, err := s.twapkeeper.GetMostRecentRecordStoreRepresentation(s.Ctx, poolId, denom0, denom1) s.Require().NoError(err) s.Require().Equal(expectedTwap, twap) - twap, err = s.twapkeeper.GetRecordAtOrBeforeTime(s.Ctx, poolId, s.Ctx.BlockTime(), "token/B", "token/A") + twap, err = s.twapkeeper.GetRecordAtOrBeforeTime(s.Ctx, poolId, s.Ctx.BlockTime(), denom0, denom1) s.Require().NoError(err) s.Require().Equal(expectedTwap, twap) } @@ -43,7 +43,7 @@ func (s *TestSuite) TestSwapTriggeringTrackPoolId() { func (s *TestSuite) TestSwapAndEndBlockTriggeringSave() { s.Ctx = s.Ctx.WithBlockTime(baseTime) poolId := s.PrepareUni2PoolWithAssets(defaultUniV2Coins[0], defaultUniV2Coins[1]) - expectedHistoricalTwap, err := types.NewTwapRecord(s.App.GAMMKeeper, s.Ctx, poolId, "token/B", "token/A") + expectedHistoricalTwap, err := types.NewTwapRecord(s.App.GAMMKeeper, s.Ctx, poolId, denom0, denom1) s.Require().NoError(err) s.EndBlock() @@ -53,7 +53,7 @@ func (s *TestSuite) TestSwapAndEndBlockTriggeringSave() { s.RunBasicSwap(poolId) // accumulators are default right here - expectedLatestTwapUpToAccum, err := types.NewTwapRecord(s.App.GAMMKeeper, s.Ctx, poolId, "token/B", "token/A") + expectedLatestTwapUpToAccum, err := types.NewTwapRecord(s.App.GAMMKeeper, s.Ctx, poolId, denom0, denom1) s.Require().NoError(err) // ensure different spot prices s.Require().NotEqual(expectedHistoricalTwap.P0LastSpotPrice, expectedLatestTwapUpToAccum.P0LastSpotPrice) @@ -62,16 +62,16 @@ func (s *TestSuite) TestSwapAndEndBlockTriggeringSave() { s.EndBlock() // check records - historicalOldTwap, err := s.twapkeeper.GetRecordAtOrBeforeTime(s.Ctx, poolId, baseTime, "token/B", "token/A") + historicalOldTwap, err := s.twapkeeper.GetRecordAtOrBeforeTime(s.Ctx, poolId, baseTime, denom0, denom1) s.Require().NoError(err) s.Require().Equal(expectedHistoricalTwap, historicalOldTwap) - latestTwap, err := s.twapkeeper.GetMostRecentRecordStoreRepresentation(s.Ctx, poolId, "token/B", "token/A") + latestTwap, err := s.twapkeeper.GetMostRecentRecordStoreRepresentation(s.Ctx, poolId, denom0, denom1) s.Require().NoError(err) s.Require().Equal(latestTwap.P0LastSpotPrice, expectedLatestTwapUpToAccum.P0LastSpotPrice) s.Require().Equal(latestTwap.P1LastSpotPrice, expectedLatestTwapUpToAccum.P1LastSpotPrice) - latestTwap2, err := s.twapkeeper.GetRecordAtOrBeforeTime(s.Ctx, poolId, s.Ctx.BlockTime(), "token/B", "token/A") + latestTwap2, err := s.twapkeeper.GetRecordAtOrBeforeTime(s.Ctx, poolId, s.Ctx.BlockTime(), denom0, denom1) s.Require().NoError(err) s.Require().Equal(latestTwap, latestTwap2) diff --git a/x/gamm/twap/logic_test.go b/x/gamm/twap/logic_test.go index 301f1401c90..6e905b799e9 100644 --- a/x/gamm/twap/logic_test.go +++ b/x/gamm/twap/logic_test.go @@ -120,7 +120,6 @@ func (s *TestSuite) TestUpdateTwap() { // based on time interval, and their accumulator values. // Then an expected TWAP is provided in each test case, to compare against computed. func TestComputeArithmeticTwap(t *testing.T) { - denom0, denom1 := "token/B", "token/A" newOneSidedRecord := func(time time.Time, accum sdk.Dec, useP0 bool) types.TwapRecord { record := types.TwapRecord{Time: time, Asset0Denom: denom0, Asset1Denom: denom1} if useP0 { diff --git a/x/gamm/twap/store_test.go b/x/gamm/twap/store_test.go index 50712277af5..1f0898da0c6 100644 --- a/x/gamm/twap/store_test.go +++ b/x/gamm/twap/store_test.go @@ -46,9 +46,9 @@ func (s *TestSuite) TestTrackChangedPool() { // and runs storeNewRecord for everything in sequence. // Then it runs GetAllMostRecentRecordsForPool, and sees if its equal to expected func (s *TestSuite) TestGetAllMostRecentRecordsForPool() { - tPlusOne := baseTime.Add(time.Second) - baseRecord := newEmptyPriceRecord(1, baseTime, "tokenB", "tokenA") - tPlusOneRecord := newEmptyPriceRecord(1, tPlusOne, "tokenB", "tokenA") + denomA, denomB, denomC := "tokenA", "tokenB", "tokenC" + baseRecord := newEmptyPriceRecord(1, baseTime, denomB, denomA) + tPlusOneRecord := newEmptyPriceRecord(1, tPlusOne, denomB, denomA) tests := map[string]struct { recordsToSet []types.TwapRecord poolId uint64 @@ -65,9 +65,9 @@ func (s *TestSuite) TestGetAllMostRecentRecordsForPool() { expectedRecords: []types.TwapRecord{}, }, "set single record, different pool ID": { - recordsToSet: []types.TwapRecord{newEmptyPriceRecord(2, baseTime, "tokenB", "tokenA")}, + recordsToSet: []types.TwapRecord{newEmptyPriceRecord(2, baseTime, denomB, denomA)}, poolId: 2, - expectedRecords: []types.TwapRecord{newEmptyPriceRecord(2, baseTime, "tokenB", "tokenA")}, + expectedRecords: []types.TwapRecord{newEmptyPriceRecord(2, baseTime, denomB, denomA)}, }, "set two records": { recordsToSet: []types.TwapRecord{baseRecord, tPlusOneRecord}, @@ -82,14 +82,14 @@ func (s *TestSuite) TestGetAllMostRecentRecordsForPool() { }, "set multi-asset pool record": { recordsToSet: []types.TwapRecord{ - newEmptyPriceRecord(1, baseTime, "tokenB", "tokenA"), - newEmptyPriceRecord(1, baseTime, "tokenC", "tokenB"), - newEmptyPriceRecord(1, baseTime, "tokenC", "tokenA")}, + newEmptyPriceRecord(1, baseTime, denomB, denomA), + newEmptyPriceRecord(1, baseTime, denomC, denomB), + newEmptyPriceRecord(1, baseTime, denomC, denomA)}, poolId: 1, expectedRecords: []types.TwapRecord{ - newEmptyPriceRecord(1, baseTime, "tokenB", "tokenA"), - newEmptyPriceRecord(1, baseTime, "tokenC", "tokenA"), - newEmptyPriceRecord(1, baseTime, "tokenC", "tokenB")}, + newEmptyPriceRecord(1, baseTime, denomB, denomA), + newEmptyPriceRecord(1, baseTime, denomC, denomA), + newEmptyPriceRecord(1, baseTime, denomC, denomB)}, }, } @@ -116,14 +116,14 @@ func (s *TestSuite) TestGetRecordAtOrBeforeTime() { asset0Denom string asset1Denom string } - defaultInputAt := func(t time.Time) getRecordInput { return getRecordInput{1, t, "tokenB", "tokenA"} } - wrongPoolIdInputAt := func(t time.Time) getRecordInput { return getRecordInput{2, t, "tokenB", "tokenA"} } - defaultRevInputAt := func(t time.Time) getRecordInput { return getRecordInput{1, t, "tokenA", "tokenB"} } - baseRecord := newEmptyPriceRecord(1, baseTime, "tokenB", "tokenA") + defaultInputAt := func(t time.Time) getRecordInput { return getRecordInput{1, t, denom0, denom1} } + wrongPoolIdInputAt := func(t time.Time) getRecordInput { return getRecordInput{2, t, denom0, denom1} } + defaultRevInputAt := func(t time.Time) getRecordInput { return getRecordInput{1, t, denom1, denom0} } + baseRecord := newEmptyPriceRecord(1, baseTime, denom0, denom1) tMin1 := baseTime.Add(-time.Second) - tMin1Record := newEmptyPriceRecord(1, tMin1, "tokenB", "tokenA") + tMin1Record := newEmptyPriceRecord(1, tMin1, denom0, denom1) tPlus1 := baseTime.Add(time.Second) - tPlus1Record := newEmptyPriceRecord(1, tPlus1, "tokenB", "tokenA") + tPlus1Record := newEmptyPriceRecord(1, tPlus1, denom0, denom1) tests := map[string]struct { recordsToSet []types.TwapRecord @@ -166,9 +166,9 @@ func (s *TestSuite) TestGetRecordAtOrBeforeTime() { []types.TwapRecord{tMin1Record, baseRecord, tPlus1Record}, wrongPoolIdInputAt(baseTime), baseRecord, true}, "pool2 record get": { - recordsToSet: []types.TwapRecord{newEmptyPriceRecord(2, baseTime, "tokenB", "tokenA")}, + recordsToSet: []types.TwapRecord{newEmptyPriceRecord(2, baseTime, denom0, denom1)}, input: wrongPoolIdInputAt(baseTime), - expectedRecord: newEmptyPriceRecord(2, baseTime, "tokenB", "tokenA"), + expectedRecord: newEmptyPriceRecord(2, baseTime, denom0, denom1), expErr: false}, } for name, test := range tests { From 884206823f0ae7a1197be13792fdda7c968ac198 Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Fri, 5 Aug 2022 12:19:33 -0500 Subject: [PATCH 64/72] Improve ComputeArithmeticTwap comment --- x/gamm/twap/logic.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/x/gamm/twap/logic.go b/x/gamm/twap/logic.go index 7cc573ede69..455d2c3f2f2 100644 --- a/x/gamm/twap/logic.go +++ b/x/gamm/twap/logic.go @@ -132,8 +132,10 @@ func interpolateRecord(record types.TwapRecord, interpolateTime time.Time) types return recordWithUpdatedAccumulators(record, interpolateTime) } -// TODO: Test math, test p0 vs p1 correctness -// precondition: endRecord.Time > startRecord.Time +// precondition: endRecord.Time >= startRecord.Time +// if (endRecord.Time == startRecord.Time) returns endRecord.LastSpotPrice +// else returns +// (endRecord.Accumulator - startRecord.Accumulator) / (endRecord.Time - startRecord.Time) func computeArithmeticTwap(startRecord types.TwapRecord, endRecord types.TwapRecord, quoteAsset string) sdk.Dec { timeDelta := endRecord.Time.Sub(startRecord.Time) // if time difference is 0, then return the last spot price based off of start. From 36db073015ffad8900333ece65206acf81a83d30 Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Fri, 5 Aug 2022 12:22:11 -0500 Subject: [PATCH 65/72] Review lint --- x/gamm/twap/logic_test.go | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/x/gamm/twap/logic_test.go b/x/gamm/twap/logic_test.go index 6e905b799e9..a3d7e9b3d4c 100644 --- a/x/gamm/twap/logic_test.go +++ b/x/gamm/twap/logic_test.go @@ -160,22 +160,22 @@ func TestComputeArithmeticTwap(t *testing.T) { plusOneSec := baseTime.Add(time.Second) tests := map[string]testCase{ "basic: spot price = 1 for one second, 0 init accumulator": { - newOneSidedRecord(baseTime, sdk.ZeroDec(), true), - newOneSidedRecord(plusOneSec, OneSec, true), - denom0, - sdk.OneDec(), + startRecord: newOneSidedRecord(baseTime, sdk.ZeroDec(), true), + endRecord: newOneSidedRecord(plusOneSec, OneSec, true), + quoteAsset: denom0, + expTwap: sdk.OneDec(), }, "same record: denom0, end spot price = 0": { - newOneSidedRecord(baseTime, sdk.ZeroDec(), true), - newOneSidedRecord(baseTime, sdk.ZeroDec(), true), - denom0, - sdk.ZeroDec(), + startRecord: newOneSidedRecord(baseTime, sdk.ZeroDec(), true), + endRecord: newOneSidedRecord(baseTime, sdk.ZeroDec(), true), + quoteAsset: denom0, + expTwap: sdk.ZeroDec(), }, "same record: denom1, end spot price = 1": { - newOneSidedRecord(baseTime, sdk.ZeroDec(), true), - newOneSidedRecord(baseTime, sdk.ZeroDec(), true), - denom1, - sdk.OneDec(), + startRecord: newOneSidedRecord(baseTime, sdk.ZeroDec(), true), + endRecord: newOneSidedRecord(baseTime, sdk.ZeroDec(), true), + quoteAsset: denom1, + expTwap: sdk.OneDec(), }, "accumulator = 10*OneSec, t=5s. 0 base accum": testCaseFromDeltas( sdk.ZeroDec(), tenSecAccum, 5*time.Second, sdk.NewDec(2)), From 203018be875003f9da26550d2c23ad5aadce7673 Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Fri, 5 Aug 2022 12:26:11 -0500 Subject: [PATCH 66/72] Fix dangling comment --- x/gamm/twap/logic.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x/gamm/twap/logic.go b/x/gamm/twap/logic.go index 455d2c3f2f2..0811d5f2465 100644 --- a/x/gamm/twap/logic.go +++ b/x/gamm/twap/logic.go @@ -28,6 +28,8 @@ func (k Keeper) afterCreatePool(ctx sdk.Context, poolId uint64) error { } func (k Keeper) endBlock(ctx sdk.Context) { + // get changed pools grabs all altered pool ids from the transient store. + // 'altered pool ids' gets automatically cleared on commit by being a transient store changedPoolIds := k.getChangedPools(ctx) for _, id := range changedPoolIds { err := k.updateRecords(ctx, id) @@ -35,7 +37,6 @@ func (k Keeper) endBlock(ctx sdk.Context) { panic(err) } } - // 'altered pool ids' gets automatically cleared by being a transient store } func (k Keeper) updateRecords(ctx sdk.Context, poolId uint64) error { From 8ebcac3fffbb870d3f01358a1a9f9207ba29ad68 Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Fri, 5 Aug 2022 12:27:04 -0500 Subject: [PATCH 67/72] Delete obsolete TODO --- x/gamm/twap/logic.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/x/gamm/twap/logic.go b/x/gamm/twap/logic.go index 0811d5f2465..5dd705b2371 100644 --- a/x/gamm/twap/logic.go +++ b/x/gamm/twap/logic.go @@ -57,10 +57,8 @@ func (k Keeper) updateRecords(ctx sdk.Context, poolId uint64) error { // Use the return value, and drop usage of the argument. func (k Keeper) updateRecord(ctx sdk.Context, record types.TwapRecord) types.TwapRecord { newRecord := recordWithUpdatedAccumulators(record, ctx.BlockTime()) - newRecord.Height = ctx.BlockHeight() - // TODO: Ensure order is correct newSp0 := types.MustGetSpotPrice(k.ammkeeper, ctx, record.PoolId, record.Asset0Denom, record.Asset1Denom) newSp1 := types.MustGetSpotPrice(k.ammkeeper, ctx, record.PoolId, record.Asset1Denom, record.Asset0Denom) From 29bd221eb1a216b72988616d97af0af92b0731ab Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Fri, 5 Aug 2022 12:35:52 -0500 Subject: [PATCH 68/72] Delete InterpolateRecord, just call recordWithUpdatedAccumulators --- x/gamm/twap/export_test.go | 4 ++-- x/gamm/twap/logic.go | 14 ++++---------- x/gamm/twap/logic_test.go | 4 ++-- 3 files changed, 8 insertions(+), 14 deletions(-) diff --git a/x/gamm/twap/export_test.go b/x/gamm/twap/export_test.go index 78c056087b7..fa8d503af7c 100644 --- a/x/gamm/twap/export_test.go +++ b/x/gamm/twap/export_test.go @@ -40,6 +40,6 @@ func ComputeArithmeticTwap(startRecord, endRecord types.TwapRecord, quoteAsset s return computeArithmeticTwap(startRecord, endRecord, quoteAsset) } -func InterpolateRecord(record types.TwapRecord, t time.Time) types.TwapRecord { - return interpolateRecord(record, t) +func RecordWithUpdatedAccumulators(record types.TwapRecord, t time.Time) types.TwapRecord { + return recordWithUpdatedAccumulators(record, t) } diff --git a/x/gamm/twap/logic.go b/x/gamm/twap/logic.go index 5dd705b2371..fdbd967bbac 100644 --- a/x/gamm/twap/logic.go +++ b/x/gamm/twap/logic.go @@ -69,7 +69,8 @@ func (k Keeper) updateRecord(ctx sdk.Context, record types.TwapRecord) types.Twa return newRecord } -// interpolate record returns a record, with updated accumulator values and time for provided newTime. +// recordWithUpdatedAccumulators returns a record, with updated accumulator values and time for provided newTime. +// otherwise referred to as "interpolating the record" to the target time. // // pre-condition: newTime >= record.Time func recordWithUpdatedAccumulators(record types.TwapRecord, newTime time.Time) types.TwapRecord { @@ -108,7 +109,7 @@ func (k Keeper) getInterpolatedRecord(ctx sdk.Context, poolId uint64, t time.Tim if err != nil { return types.TwapRecord{}, err } - record = interpolateRecord(record, t) + record = recordWithUpdatedAccumulators(record, t) return record, nil } @@ -120,17 +121,10 @@ func (k Keeper) getMostRecentRecord(ctx sdk.Context, poolId uint64, assetA, asse if err != nil { return types.TwapRecord{}, err } - record = interpolateRecord(record, ctx.BlockTime()) + record = recordWithUpdatedAccumulators(record, ctx.BlockTime()) return record, nil } -// interpolate record updates the record's accumulator values and time to the interpolate time. -// -// pre-condition: interpolateTime >= record.Time -func interpolateRecord(record types.TwapRecord, interpolateTime time.Time) types.TwapRecord { - return recordWithUpdatedAccumulators(record, interpolateTime) -} - // precondition: endRecord.Time >= startRecord.Time // if (endRecord.Time == startRecord.Time) returns endRecord.LastSpotPrice // else returns diff --git a/x/gamm/twap/logic_test.go b/x/gamm/twap/logic_test.go index a3d7e9b3d4c..2363fd54a7d 100644 --- a/x/gamm/twap/logic_test.go +++ b/x/gamm/twap/logic_test.go @@ -41,7 +41,7 @@ func newExpRecord(accum0, accum1 sdk.Dec) types.TwapRecord { } } -func TestInterpolateRecord(t *testing.T) { +func TestRecordWithUpdatedAccumulators(t *testing.T) { tests := map[string]struct { record types.TwapRecord interpolateTime time.Time @@ -77,7 +77,7 @@ func TestInterpolateRecord(t *testing.T) { test.expRecord.P0LastSpotPrice = test.record.P0LastSpotPrice test.expRecord.P1LastSpotPrice = test.record.P1LastSpotPrice - gotRecord := twap.InterpolateRecord(test.record, test.interpolateTime) + gotRecord := twap.RecordWithUpdatedAccumulators(test.record, test.interpolateTime) require.Equal(t, test.expRecord, gotRecord) }) } From 3e8fb91fb43702660cddf62cc460de9118b979fa Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Fri, 5 Aug 2022 12:48:24 -0500 Subject: [PATCH 69/72] Clarify accumulator setting in api test --- x/gamm/twap/api_test.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/x/gamm/twap/api_test.go b/x/gamm/twap/api_test.go index 5c13cb5f560..eb707f9fda1 100644 --- a/x/gamm/twap/api_test.go +++ b/x/gamm/twap/api_test.go @@ -80,8 +80,11 @@ func (s *TestSuite) TestGetArithmeticTwap() { // base record is a record with t=baseTime, sp0=10, sp1=.1, accumulators set to 0 baseRecord := newTwapRecordWithDefaults(baseTime, sdk.NewDec(10), sdk.ZeroDec(), sdk.ZeroDec()) // record with t=baseTime+10, sp0=5, sp1=.2, accumulators updated from baseRecord + // accum0 = 10 seconds * (spot price = 10), accum1 = 10 seconds * (spot price = .1) + accum0, accum1 := OneSec.MulInt64(10*10), OneSec tPlus10sp5Record := newTwapRecordWithDefaults( - baseTime.Add(10*time.Second), sdk.NewDec(5), OneSec.MulInt64(10*10), OneSec) + baseTime.Add(10*time.Second), sdk.NewDec(5), accum0, accum1) + // TODO: Make use of the below for test cases: // record with t=baseTime+20, sp0=2, sp1=.5, accumulators updated from tPlus10sp5Record // tPlus20sp2Record := newTwapRecordWithDefaults( // baseTime.Add(20*time.Second), sdk.NewDec(2), OneSec.MulInt64(10*10+5*10), OneSec.MulInt64(3)) From b568faa868452bea5c057e506f06e9dc14767e76 Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Fri, 5 Aug 2022 13:03:11 -0500 Subject: [PATCH 70/72] Delete extra line --- x/gamm/twap/logic_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/x/gamm/twap/logic_test.go b/x/gamm/twap/logic_test.go index 2363fd54a7d..bd375a5fdc1 100644 --- a/x/gamm/twap/logic_test.go +++ b/x/gamm/twap/logic_test.go @@ -157,11 +157,10 @@ func TestComputeArithmeticTwap(t *testing.T) { } tenSecAccum := OneSec.MulInt64(10) pointOneAccum := OneSec.QuoInt64(10) - plusOneSec := baseTime.Add(time.Second) tests := map[string]testCase{ "basic: spot price = 1 for one second, 0 init accumulator": { startRecord: newOneSidedRecord(baseTime, sdk.ZeroDec(), true), - endRecord: newOneSidedRecord(plusOneSec, OneSec, true), + endRecord: newOneSidedRecord(tPlusOne, OneSec, true), quoteAsset: denom0, expTwap: sdk.OneDec(), }, From 6b60e707f3201ed8c8980a599bb21e4dead09c3f Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Fri, 5 Aug 2022 13:17:32 -0500 Subject: [PATCH 71/72] Apply code review doc update Co-authored-by: Roman --- x/gamm/twap/logic.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x/gamm/twap/logic.go b/x/gamm/twap/logic.go index fdbd967bbac..5af9d7597e0 100644 --- a/x/gamm/twap/logic.go +++ b/x/gamm/twap/logic.go @@ -125,6 +125,8 @@ func (k Keeper) getMostRecentRecord(ctx sdk.Context, poolId uint64, assetA, asse return record, nil } +// computeArithmeticTwap computes and returns an arithmetic TWAP between +// two records given the quote asset. // precondition: endRecord.Time >= startRecord.Time // if (endRecord.Time == startRecord.Time) returns endRecord.LastSpotPrice // else returns From e800f9f61148480e839eb787fa5d4131adbeffbd Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Fri, 5 Aug 2022 14:42:25 -0500 Subject: [PATCH 72/72] Add test case roman pointed out as missinsg --- x/gamm/twap/logic_test.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/x/gamm/twap/logic_test.go b/x/gamm/twap/logic_test.go index bd375a5fdc1..cbdd5e3305c 100644 --- a/x/gamm/twap/logic_test.go +++ b/x/gamm/twap/logic_test.go @@ -164,6 +164,14 @@ func TestComputeArithmeticTwap(t *testing.T) { quoteAsset: denom0, expTwap: sdk.OneDec(), }, + // this test just shows what happens in case the records are reversed. + // It should return the correct result, even though this is incorrect internal API usage + "invalid call: reversed records of above": { + startRecord: newOneSidedRecord(tPlusOne, OneSec, true), + endRecord: newOneSidedRecord(baseTime, sdk.ZeroDec(), true), + quoteAsset: denom0, + expTwap: sdk.OneDec(), + }, "same record: denom0, end spot price = 0": { startRecord: newOneSidedRecord(baseTime, sdk.ZeroDec(), true), endRecord: newOneSidedRecord(baseTime, sdk.ZeroDec(), true),