-
Notifications
You must be signed in to change notification settings - Fork 557
/
taker_fee.go
260 lines (228 loc) · 11.9 KB
/
taker_fee.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
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
package poolmanager
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/osmosis-labs/osmosis/osmomath"
appparams "github.com/osmosis-labs/osmosis/v22/app/params"
"github.com/osmosis-labs/osmosis/osmoutils"
"github.com/osmosis-labs/osmosis/v22/x/poolmanager/types"
txfeestypes "github.com/osmosis-labs/osmosis/v22/x/txfees/types"
)
func (k Keeper) GetDefaultTakerFee(ctx sdk.Context) sdk.Dec {
var defaultTakerFee sdk.Dec
k.paramSpace.Get(ctx, types.KeyDefaultTakerFee, &defaultTakerFee)
return defaultTakerFee
}
// SetDenomPairTakerFee sets the taker fee for the given trading pair.
// If the taker fee for this denom pair matches the default taker fee, then
// it is deleted from state.
func (k Keeper) SetDenomPairTakerFee(ctx sdk.Context, denom0, denom1 string, takerFee osmomath.Dec) {
store := ctx.KVStore(k.storeKey)
// if given taker fee is equal to the default taker fee,
// delete whatever we have in current state to use default taker fee.
// TODO: This logic is actually wrong imo, where it can be valid to set an override over the default.
if takerFee.Equal(k.GetDefaultTakerFee(ctx)) {
store.Delete(types.FormatDenomTradePairKey(denom0, denom1))
return
} else {
osmoutils.MustSetDec(store, types.FormatDenomTradePairKey(denom0, denom1), takerFee)
}
}
// SenderValidationSetDenomPairTakerFee sets the taker fee for the given trading pair iff the sender's address
// also exists in the pool manager taker fee admin address list.
func (k Keeper) SenderValidationSetDenomPairTakerFee(ctx sdk.Context, sender, denom0, denom1 string, takerFee osmomath.Dec) error {
adminAddresses := k.GetParams(ctx).TakerFeeParams.AdminAddresses
isAdmin := false
for _, admin := range adminAddresses {
if admin == sender {
isAdmin = true
break
}
}
if !isAdmin {
return fmt.Errorf("%s is not in the pool manager taker fee admin address list", sender)
}
k.SetDenomPairTakerFee(ctx, denom0, denom1, takerFee)
ctx.EventManager().EmitEvents(sdk.Events{
sdk.NewEvent(
types.TypeMsgSetDenomPairTakerFee,
sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory),
sdk.NewAttribute(sdk.AttributeKeySender, sender),
sdk.NewAttribute(types.AttributeKeyDenom0, denom0),
sdk.NewAttribute(types.AttributeKeyDenom1, denom1),
sdk.NewAttribute(types.AttributeKeyTakerFee, takerFee.String()),
),
})
return nil
}
// GetTradingPairTakerFee returns the taker fee for the given trading pair.
// If the trading pair does not exist, it returns the default taker fee.
func (k Keeper) GetTradingPairTakerFee(ctx sdk.Context, denom0, denom1 string) (osmomath.Dec, error) {
store := ctx.KVStore(k.storeKey)
key := types.FormatDenomTradePairKey(denom0, denom1)
takerFee := &sdk.DecProto{}
found, err := osmoutils.Get(store, key, takerFee)
if err != nil {
return osmomath.Dec{}, err
}
if !found {
return k.GetDefaultTakerFee(ctx), nil
}
return takerFee.Dec, nil
}
// GetAllTradingPairTakerFees returns all the custom taker fees for trading pairs.
func (k Keeper) GetAllTradingPairTakerFees(ctx sdk.Context) ([]types.DenomPairTakerFee, error) {
store := ctx.KVStore(k.storeKey)
iterator := sdk.KVStoreReversePrefixIterator(store, types.DenomTradePairPrefix)
defer iterator.Close()
var takerFees []types.DenomPairTakerFee
for ; iterator.Valid(); iterator.Next() {
takerFee := &sdk.DecProto{}
osmoutils.MustGet(store, iterator.Key(), takerFee)
denom0, denom1, err := types.ParseDenomTradePairKey(iterator.Key())
if err != nil {
return nil, err
}
takerFees = append(takerFees, types.DenomPairTakerFee{
Denom0: denom0,
Denom1: denom1,
TakerFee: takerFee.Dec,
})
}
return takerFees, nil
}
var zeroDec = osmomath.ZeroDec()
// chargeTakerFee extracts the taker fee from the given tokenIn and sends it to the appropriate
// module account. It returns the tokenIn after the taker fee has been extracted.
// If the sender is in the taker fee reduced whitelisted, it returns the tokenIn without extracting the taker fee.
// In the future, we might charge a lower taker fee as opposed to no fee at all.
// TODO: Gas optimize this function, its expensive in both gas and CPU.
func (k Keeper) chargeTakerFee(ctx sdk.Context, tokenIn sdk.Coin, tokenOutDenom string, sender sdk.AccAddress, exactIn bool) (sdk.Coin, error) {
feeCollectorForStakingRewardsName := txfeestypes.FeeCollectorForStakingRewardsName
feeCollectorForCommunityPoolName := txfeestypes.FeeCollectorForCommunityPoolName
defaultTakerFeeDenom := appparams.BaseCoinUnit
reducedFeeWhitelist := []string{}
k.paramSpace.Get(ctx, types.KeyReducedTakerFeeByWhitelist, &reducedFeeWhitelist)
// Determine if eligible to bypass taker fee.
if osmoutils.Contains(reducedFeeWhitelist, sender.String()) {
return tokenIn, nil
}
takerFee, err := k.GetTradingPairTakerFee(ctx, tokenIn.Denom, tokenOutDenom)
if err != nil {
return sdk.Coin{}, err
}
var tokenInAfterTakerFee sdk.Coin
var takerFeeCoin sdk.Coin
if exactIn {
tokenInAfterTakerFee, takerFeeCoin = CalcTakerFeeExactIn(tokenIn, takerFee)
} else {
tokenInAfterTakerFee, takerFeeCoin = CalcTakerFeeExactOut(tokenIn, takerFee)
}
// N.B. We truncate from the community pool calculation, then remove that from the total, and use the remaining for staking rewards.
// If we truncate both, these can leave tokens in the users wallet when swapping and exact amount in, which is bad UX.
// We determine the distributution of the taker fee based on its denom
// If the denom is the base denom:
takerFeeAmtRemaining := takerFeeCoin.Amount
if takerFeeCoin.Denom == defaultTakerFeeDenom {
osmoTakerFeeDistribution := types.TakerFeeDistributionPercentage{}
k.paramSpace.Get(ctx, types.KeyOsmoTakerFeeDistribution, &osmoTakerFeeDistribution)
// Community Pool:
if osmoTakerFeeDistribution.CommunityPool.GT(zeroDec) && takerFeeAmtRemaining.GT(osmomath.ZeroInt()) {
// Osmo community pool funds is a direct send
osmoTakerFeeToCommunityPoolDec := takerFeeAmtRemaining.ToLegacyDec().Mul(osmoTakerFeeDistribution.CommunityPool)
osmoTakerFeeToCommunityPoolCoin := sdk.NewCoin(defaultTakerFeeDenom, osmoTakerFeeToCommunityPoolDec.TruncateInt())
err := k.communityPoolKeeper.FundCommunityPool(ctx, sdk.NewCoins(osmoTakerFeeToCommunityPoolCoin), sender)
if err != nil {
return sdk.Coin{}, err
}
err = k.UpdateTakerFeeTrackerForCommunityPoolByDenom(ctx, osmoTakerFeeToCommunityPoolCoin.Denom, osmoTakerFeeToCommunityPoolCoin.Amount)
if err != nil {
ctx.Logger().Error("Error updating taker fee tracker for community pool by denom", "error", err)
}
takerFeeAmtRemaining = takerFeeAmtRemaining.Sub(osmoTakerFeeToCommunityPoolCoin.Amount)
}
// Staking Rewards:
if osmoTakerFeeDistribution.StakingRewards.GT(zeroDec) && takerFeeAmtRemaining.GT(osmomath.ZeroInt()) {
// Osmo staking rewards funds are sent to the non native fee pool module account (even though its native, we want to distribute at the same time as the non native fee tokens)
// We could stream these rewards via the fee collector account, but this is decision to be made by governance.
osmoTakerFeeToStakingRewardsCoin := sdk.NewCoin(defaultTakerFeeDenom, takerFeeAmtRemaining)
err := k.bankKeeper.SendCoinsFromAccountToModule(ctx, sender, feeCollectorForStakingRewardsName, sdk.NewCoins(osmoTakerFeeToStakingRewardsCoin))
if err != nil {
return sdk.Coin{}, err
}
err = k.UpdateTakerFeeTrackerForStakersByDenom(ctx, osmoTakerFeeToStakingRewardsCoin.Denom, osmoTakerFeeToStakingRewardsCoin.Amount)
if err != nil {
ctx.Logger().Error("Error updating taker fee tracker for stakers by denom", "error", err)
}
}
// If the denom is not the base denom:
} else {
nonOsmoTakerFeeDistribution := types.TakerFeeDistributionPercentage{}
k.paramSpace.Get(ctx, types.KeyNonOsmoTakerFeeDistribution, &nonOsmoTakerFeeDistribution)
authorizedQuoteDenoms := []string{}
k.paramSpace.Get(ctx, types.KeyAuthorizedQuoteDenoms, &authorizedQuoteDenoms)
// Community Pool:
if nonOsmoTakerFeeDistribution.CommunityPool.GT(zeroDec) && takerFeeAmtRemaining.GT(osmomath.ZeroInt()) {
denomIsWhitelisted := isDenomWhitelisted(takerFeeCoin.Denom, authorizedQuoteDenoms)
// If the non osmo denom is a whitelisted quote asset, we send to the community pool
if denomIsWhitelisted {
nonOsmoTakerFeeToCommunityPoolDec := takerFeeAmtRemaining.ToLegacyDec().Mul(nonOsmoTakerFeeDistribution.CommunityPool)
nonOsmoTakerFeeToCommunityPoolCoin := sdk.NewCoin(tokenIn.Denom, nonOsmoTakerFeeToCommunityPoolDec.TruncateInt())
err := k.communityPoolKeeper.FundCommunityPool(ctx, sdk.NewCoins(nonOsmoTakerFeeToCommunityPoolCoin), sender)
if err != nil {
return sdk.Coin{}, err
}
err = k.UpdateTakerFeeTrackerForCommunityPoolByDenom(ctx, nonOsmoTakerFeeToCommunityPoolCoin.Denom, nonOsmoTakerFeeToCommunityPoolCoin.Amount)
if err != nil {
ctx.Logger().Error("Error updating taker fee tracker for community pool by denom", "error", err)
}
takerFeeAmtRemaining = takerFeeAmtRemaining.Sub(nonOsmoTakerFeeToCommunityPoolCoin.Amount)
} else {
// If the non osmo denom is not a whitelisted asset, we send to the non native fee pool for community pool module account.
// At epoch, this account swaps the non native, non whitelisted assets for XXX and sends to the community pool.
nonOsmoTakerFeeToCommunityPoolDec := takerFeeAmtRemaining.ToLegacyDec().Mul(nonOsmoTakerFeeDistribution.CommunityPool)
nonOsmoTakerFeeToCommunityPoolCoin := sdk.NewCoin(tokenIn.Denom, nonOsmoTakerFeeToCommunityPoolDec.TruncateInt())
err := k.bankKeeper.SendCoinsFromAccountToModule(ctx, sender, feeCollectorForCommunityPoolName, sdk.NewCoins(nonOsmoTakerFeeToCommunityPoolCoin))
if err != nil {
return sdk.Coin{}, err
}
err = k.UpdateTakerFeeTrackerForCommunityPoolByDenom(ctx, nonOsmoTakerFeeToCommunityPoolCoin.Denom, nonOsmoTakerFeeToCommunityPoolCoin.Amount)
if err != nil {
ctx.Logger().Error("Error updating taker fee tracker for community pool by denom", "error", err)
}
takerFeeAmtRemaining = takerFeeAmtRemaining.Sub(nonOsmoTakerFeeToCommunityPoolCoin.Amount)
}
}
// Staking Rewards:
if nonOsmoTakerFeeDistribution.StakingRewards.GT(zeroDec) && takerFeeAmtRemaining.GT(osmomath.ZeroInt()) {
// Non Osmo staking rewards are sent to the non native fee pool module account
nonOsmoTakerFeeToStakingRewardsCoin := sdk.NewCoin(takerFeeCoin.Denom, takerFeeAmtRemaining)
err := k.bankKeeper.SendCoinsFromAccountToModule(ctx, sender, feeCollectorForStakingRewardsName, sdk.NewCoins(nonOsmoTakerFeeToStakingRewardsCoin))
if err != nil {
return sdk.Coin{}, err
}
err = k.UpdateTakerFeeTrackerForStakersByDenom(ctx, nonOsmoTakerFeeToStakingRewardsCoin.Denom, nonOsmoTakerFeeToStakingRewardsCoin.Amount)
if err != nil {
ctx.Logger().Error("Error updating taker fee tracker for stakers by denom", "error", err)
}
}
}
return tokenInAfterTakerFee, nil
}
// Returns remaining amount in to swap, and takerFeeCoins.
// returns (1 - takerFee) * tokenIn, takerFee * tokenIn
func CalcTakerFeeExactIn(tokenIn sdk.Coin, takerFee osmomath.Dec) (sdk.Coin, sdk.Coin) {
takerFeeFactor := osmomath.OneDec().SubMut(takerFee)
// TODO: Remove .ToLegacyDec and instead do MulInt. Need to test state compat.
amountInAfterSubTakerFee := tokenIn.Amount.ToLegacyDec().MulTruncate(takerFeeFactor)
tokenInAfterSubTakerFee := sdk.Coin{Denom: tokenIn.Denom, Amount: amountInAfterSubTakerFee.TruncateInt()}
takerFeeCoin := sdk.Coin{Denom: tokenIn.Denom, Amount: tokenIn.Amount.Sub(tokenInAfterSubTakerFee.Amount)}
return tokenInAfterSubTakerFee, takerFeeCoin
}
func CalcTakerFeeExactOut(tokenIn sdk.Coin, takerFee osmomath.Dec) (sdk.Coin, sdk.Coin) {
takerFeeFactor := osmomath.OneDec().SubMut(takerFee)
amountInAfterAddTakerFee := tokenIn.Amount.ToLegacyDec().Quo(takerFeeFactor)
tokenInAfterAddTakerFee := sdk.Coin{Denom: tokenIn.Denom, Amount: amountInAfterAddTakerFee.Ceil().TruncateInt()}
takerFeeCoin := sdk.Coin{Denom: tokenIn.Denom, Amount: tokenInAfterAddTakerFee.Amount.Sub(tokenIn.Amount)}
return tokenInAfterAddTakerFee, takerFeeCoin
}