-
Notifications
You must be signed in to change notification settings - Fork 208
/
keeper.go
215 lines (184 loc) · 7.92 KB
/
keeper.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
package keeper
import (
"errors"
"fmt"
"time"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
paramstypes "github.com/cosmos/cosmos-sdk/x/params/types"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
"github.com/tendermint/tendermint/libs/log"
"github.com/tendermint/tendermint/libs/math"
"github.com/NibiruChain/collections"
"github.com/NibiruChain/nibiru/x/common/asset"
"github.com/NibiruChain/nibiru/x/oracle/types"
)
// Keeper of the oracle store
type Keeper struct {
cdc codec.BinaryCodec
storeKey sdk.StoreKey
paramSpace paramstypes.Subspace
AccountKeeper types.AccountKeeper
bankKeeper types.BankKeeper
distrKeeper types.DistributionKeeper
StakingKeeper types.StakingKeeper
distrModuleName string
ExchangeRates collections.Map[asset.Pair, sdk.Dec]
FeederDelegations collections.Map[sdk.ValAddress, sdk.AccAddress]
MissCounters collections.Map[sdk.ValAddress, uint64]
Prevotes collections.Map[sdk.ValAddress, types.AggregateExchangeRatePrevote]
Votes collections.Map[sdk.ValAddress, types.AggregateExchangeRateVote]
// PriceSnapshots maps types.PriceSnapshot to the asset.Pair of the snapshot and the creation timestamp as keys.Uint64Key.
PriceSnapshots collections.Map[collections.Pair[asset.Pair, time.Time], types.PriceSnapshot]
WhitelistedPairs collections.KeySet[asset.Pair]
PairRewards collections.IndexedMap[uint64, types.PairReward, PairRewardsIndexes]
PairRewardsID collections.Sequence
}
type PairRewardsIndexes struct {
// RewardsByPair is the index that maps rewards associated with specific pairs.
RewardsByPair collections.MultiIndex[asset.Pair, uint64, types.PairReward]
}
func (p PairRewardsIndexes) IndexerList() []collections.Indexer[uint64, types.PairReward] {
return []collections.Indexer[uint64, types.PairReward]{p.RewardsByPair}
}
// NewKeeper constructs a new keeper for oracle
func NewKeeper(cdc codec.BinaryCodec, storeKey sdk.StoreKey,
paramspace paramstypes.Subspace, accountKeeper types.AccountKeeper,
bankKeeper types.BankKeeper, distrKeeper types.DistributionKeeper,
stakingKeeper types.StakingKeeper, distrName string) Keeper {
// ensure oracle module account is set
if addr := accountKeeper.GetModuleAddress(types.ModuleName); addr == nil {
panic(fmt.Sprintf("%s module account has not been set", types.ModuleName))
}
// set KeyTable if it has not already been set
if !paramspace.HasKeyTable() {
paramspace = paramspace.WithKeyTable(types.ParamKeyTable())
}
return Keeper{
cdc: cdc,
storeKey: storeKey,
paramSpace: paramspace,
AccountKeeper: accountKeeper,
bankKeeper: bankKeeper,
distrKeeper: distrKeeper,
StakingKeeper: stakingKeeper,
distrModuleName: distrName,
ExchangeRates: collections.NewMap(storeKey, 1, asset.PairKeyEncoder, collections.DecValueEncoder),
PriceSnapshots: collections.NewMap(storeKey, 10, collections.PairKeyEncoder(asset.PairKeyEncoder, collections.TimeKeyEncoder), collections.ProtoValueEncoder[types.PriceSnapshot](cdc)),
FeederDelegations: collections.NewMap(storeKey, 2, collections.ValAddressKeyEncoder, collections.AccAddressValueEncoder),
MissCounters: collections.NewMap(storeKey, 3, collections.ValAddressKeyEncoder, collections.Uint64ValueEncoder),
Prevotes: collections.NewMap(storeKey, 4, collections.ValAddressKeyEncoder, collections.ProtoValueEncoder[types.AggregateExchangeRatePrevote](cdc)),
Votes: collections.NewMap(storeKey, 5, collections.ValAddressKeyEncoder, collections.ProtoValueEncoder[types.AggregateExchangeRateVote](cdc)),
WhitelistedPairs: collections.NewKeySet(storeKey, 6, asset.PairKeyEncoder),
PairRewards: collections.NewIndexedMap(
storeKey, 7,
collections.Uint64KeyEncoder, collections.ProtoValueEncoder[types.PairReward](cdc),
PairRewardsIndexes{
RewardsByPair: collections.NewMultiIndex(storeKey, 8, asset.PairKeyEncoder, collections.Uint64KeyEncoder, func(v types.PairReward) asset.Pair {
return v.Pair
}),
}),
PairRewardsID: collections.NewSequence(storeKey, 9),
}
}
// Logger returns a module-specific logger.
func (k Keeper) Logger(ctx sdk.Context) log.Logger {
return ctx.Logger().With("module", fmt.Sprintf("x/%s", types.ModuleName))
}
// ValidateFeeder return the given feeder is allowed to feed the message or not
func (k Keeper) ValidateFeeder(
ctx sdk.Context, feederAddr sdk.AccAddress, validatorAddr sdk.ValAddress,
) error {
// A validator delegates price feeder consent to itself by default.
// Thus, we only need to verify consent for price feeder addresses that don't
// match the validator address.
if !feederAddr.Equals(validatorAddr) {
delegate := k.FeederDelegations.GetOr(
ctx, validatorAddr, sdk.AccAddress(validatorAddr))
if !delegate.Equals(feederAddr) {
return sdkerrors.Wrapf(
types.ErrNoVotingPermission,
"wanted: %s, got: %s", delegate.String(), feederAddr.String())
}
}
// Check that the given validator is in the active set for consensus.
if val := k.StakingKeeper.Validator(ctx, validatorAddr); val == nil || !val.IsBonded() {
return sdkerrors.Wrapf(
stakingtypes.ErrNoValidatorFound,
"validator %s is not active set", validatorAddr.String())
}
return nil
}
/*
calcTwap walks through a slice of PriceSnapshots and tallies up the prices weighted
by the amount of time they were active for.
NOTE: Callers of this function should check if the snapshot slice is empty before
calling 'calcTwap'.
*/
func (k Keeper) calcTwap(ctx sdk.Context, snapshots []types.PriceSnapshot) (price sdk.Dec, err error) {
if len(snapshots) == 0 {
return price, errors.New("cannot calculate TWAP with empty snapshot slice")
} else if len(snapshots) == 1 {
return snapshots[0].Price, nil
}
twapLookBack := k.GetParams(ctx).TwapLookbackWindow.Milliseconds()
firstTimeStamp := ctx.BlockTime().UnixMilli() - twapLookBack
cumulativePrice := sdk.ZeroDec()
firstTimeStamp = math.MaxInt64(snapshots[0].TimestampMs, firstTimeStamp)
for i, s := range snapshots {
var nextTimestampMs int64
var timestampStart int64
if i == 0 {
timestampStart = firstTimeStamp
} else {
timestampStart = s.TimestampMs
}
if i == len(snapshots)-1 {
// if we're at the last snapshot, then consider that price as ongoing until the current blocktime
nextTimestampMs = ctx.BlockTime().UnixMilli()
} else {
nextTimestampMs = snapshots[i+1].TimestampMs
}
price := s.Price.MulInt64(nextTimestampMs - timestampStart)
cumulativePrice = cumulativePrice.Add(price)
}
return cumulativePrice.QuoInt64(ctx.BlockTime().UnixMilli() - firstTimeStamp), nil
}
func (k Keeper) GetExchangeRateTwap(ctx sdk.Context, pair asset.Pair) (price sdk.Dec, err error) {
snapshots := k.PriceSnapshots.Iterate(
ctx,
collections.PairRange[asset.Pair, time.Time]{}.
Prefix(pair).
StartInclusive(
ctx.BlockTime().Add(-1*k.GetParams(ctx).TwapLookbackWindow)).
EndInclusive(
ctx.BlockTime()),
).Values()
if len(snapshots) == 0 {
// if there are no snapshots, return -1 for the price
return sdk.OneDec().Neg(), types.ErrNoValidTWAP
}
return k.calcTwap(ctx, snapshots)
}
func (k Keeper) GetExchangeRate(ctx sdk.Context, pair asset.Pair) (price sdk.Dec, err error) {
return k.ExchangeRates.Get(ctx, pair)
}
// SetPrice sets the price for a pair as well as the price snapshot.
func (k Keeper) SetPrice(ctx sdk.Context, pair asset.Pair, price sdk.Dec) {
k.ExchangeRates.Insert(ctx, pair, price)
key := collections.Join(pair, ctx.BlockTime())
timestampMs := ctx.BlockTime().UnixMilli()
k.PriceSnapshots.Insert(ctx, key, types.PriceSnapshot{
Pair: pair,
Price: price,
TimestampMs: timestampMs,
})
if err := ctx.EventManager().EmitTypedEvent(&types.OraclePriceUpdate{
Pair: pair.String(),
Price: price,
TimestampMs: timestampMs,
}); err != nil {
ctx.Logger().Error("failed to emit OraclePriceUpdate", "pair", pair, "error", err)
}
}