-
Notifications
You must be signed in to change notification settings - Fork 365
/
accumulator.go
144 lines (127 loc) · 5.17 KB
/
accumulator.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
package types
import (
"fmt"
"math"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// An Accumulator handles calculating and tracking global reward distributions.
type Accumulator struct {
PreviousAccumulationTime time.Time
Indexes RewardIndexes
}
func NewAccumulator(previousAccrual time.Time, indexes RewardIndexes) *Accumulator {
return &Accumulator{
PreviousAccumulationTime: previousAccrual,
Indexes: indexes,
}
}
// Accumulate accrues rewards up to the current time.
//
// It calculates new rewards and adds them to the reward indexes for the period from PreviousAccumulationTime to currentTime.
// It stores the currentTime in PreviousAccumulationTime to be used for later accumulations.
//
// Rewards are not accrued for times outside of the start and end times of a reward period.
// If a period ends before currentTime, the PreviousAccrualTime is shortened to the end time. This allows accumulate to be called sequentially on consecutive reward periods.
//
// totalSourceShares is the sum of all users' source shares. For example:total btcb supplied to hard, total usdx borrowed from all bnb CDPs, or total shares in a swap pool.
func (acc *Accumulator) Accumulate(period MultiRewardPeriod, totalSourceShares sdk.Dec, currentTime time.Time) {
acc.AccumulateDecCoins(
period.Start,
period.End,
sdk.NewDecCoinsFromCoins(period.RewardsPerSecond...),
totalSourceShares,
currentTime,
)
}
// AccumulateDecCoins
func (acc *Accumulator) AccumulateDecCoins(
periodStart time.Time,
periodEnd time.Time,
periodRewardsPerSecond sdk.DecCoins,
totalSourceShares sdk.Dec,
currentTime time.Time,
) {
accumulationDuration := acc.getTimeElapsedWithinLimits(acc.PreviousAccumulationTime, currentTime, periodStart, periodEnd)
indexesIncrement := acc.calculateNewRewards(periodRewardsPerSecond, totalSourceShares, accumulationDuration)
acc.Indexes = acc.Indexes.Add(indexesIncrement)
acc.PreviousAccumulationTime = minTime(periodEnd, currentTime)
}
// getTimeElapsedWithinLimits returns the duration between start and end times, capped by min and max times.
// If the start and end range is outside the min to max time range then zero duration is returned.
func (*Accumulator) getTimeElapsedWithinLimits(start, end, limitMin, limitMax time.Time) time.Duration {
if start.After(end) {
panic(fmt.Sprintf("start time (%s) cannot be after end time (%s)", start, end))
}
if limitMin.After(limitMax) {
panic(fmt.Sprintf("minimum limit time (%s) cannot be after maximum limit time (%s)", limitMin, limitMax))
}
if start.After(limitMax) || end.Before(limitMin) {
// no intersection between the start-end and limitMin-limitMax time ranges
return 0
}
return minTime(end, limitMax).Sub(maxTime(start, limitMin))
}
// calculateNewRewards calculates the amount to increase the global reward indexes by, for a given reward rate, duration, and number of source shares.
// The total rewards to distribute in this block are given by reward rate * duration. This value divided by the sum of all source shares to give
// total rewards per source share, which is what the indexes store.
// Note, duration is rounded to the nearest second to keep rewards calculation consistent with kava-7.
func (*Accumulator) calculateNewRewards(rewardsPerSecond sdk.DecCoins, totalSourceShares sdk.Dec, duration time.Duration) RewardIndexes {
if totalSourceShares.LTE(sdk.ZeroDec()) {
// When there is zero source shares, there is no users with deposits/borrows/delegations to pay out the current block's rewards to.
// So drop the rewards and pay out nothing.
return nil
}
durationSeconds := int64(math.RoundToEven(duration.Seconds()))
if durationSeconds <= 0 {
// If the duration is zero, there will be no increment.
// So return an empty increment instead of one full of zeros.
return nil
}
increment := NewRewardIndexesFromCoins(rewardsPerSecond)
increment = increment.Mul(sdk.NewDec(durationSeconds)).Quo(totalSourceShares)
return increment
}
// minTime returns the earliest of two times.
func minTime(t1, t2 time.Time) time.Time {
if t2.Before(t1) {
return t2
}
return t1
}
// maxTime returns the latest of two times.
func maxTime(t1, t2 time.Time) time.Time {
if t2.After(t1) {
return t2
}
return t1
}
// NewRewardIndexesFromCoins is a helper function to initialize a RewardIndexes slice with the values from a Coins slice.
func NewRewardIndexesFromCoins(coins sdk.DecCoins) RewardIndexes {
var indexes RewardIndexes
for _, coin := range coins {
indexes = append(indexes, NewRewardIndex(coin.Denom, coin.Amount))
}
return indexes
}
func CalculatePerSecondRewards(
periodStart time.Time,
periodEnd time.Time,
periodRewardsPerSecond sdk.DecCoins,
previousTime, currentTime time.Time,
) (sdk.DecCoins, time.Time) {
duration := (&Accumulator{}).getTimeElapsedWithinLimits(
previousTime,
currentTime,
periodStart,
periodEnd,
)
upTo := minTime(periodEnd, currentTime)
durationSeconds := int64(math.RoundToEven(duration.Seconds()))
if durationSeconds <= 0 {
// If the duration is zero, there will be no increment.
// So return an empty increment instead of one full of zeros.
return nil, upTo // TODO
}
return periodRewardsPerSecond.MulDec(sdk.NewDec(durationSeconds)), upTo
}