-
Notifications
You must be signed in to change notification settings - Fork 49
/
tax.go
213 lines (174 loc) · 6.43 KB
/
tax.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
package ante
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
authz "github.com/cosmos/cosmos-sdk/x/authz"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
core "github.com/classic-terra/core/types"
marketexported "github.com/classic-terra/core/x/market/exported"
oracleexported "github.com/classic-terra/core/x/oracle/exported"
wasmexported "github.com/classic-terra/core/x/wasm/exported"
)
// MaxOracleMsgGasUsage is constant expected oracle msg gas cost
const MaxOracleMsgGasUsage = uint64(100_000)
// TaxFeeDecorator will check if the transaction's fee is at least as large
// as tax + the local validator's minimum gasFee (defined in validator config)
// and record tax proceeds to treasury module to track tax proceeds.
// If fee is too low, decorator returns error and tx is rejected from mempool.
// Note this only applies when ctx.CheckTx = true
// If fee is high enough or not CheckTx, then call next AnteHandler
// CONTRACT: Tx must implement FeeTx to use MempoolFeeDecorator
type TaxFeeDecorator struct {
treasuryKeeper TreasuryKeeper
}
// NewTaxFeeDecorator returns new tax fee decorator instance
func NewTaxFeeDecorator(treasuryKeeper TreasuryKeeper) TaxFeeDecorator {
return TaxFeeDecorator{
treasuryKeeper: treasuryKeeper,
}
}
// AnteHandle handles msg tax fee checking
func (tfd TaxFeeDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (newCtx sdk.Context, err error) {
feeTx, ok := tx.(sdk.FeeTx)
if !ok {
return ctx, sdkerrors.Wrap(sdkerrors.ErrTxDecode, "Tx must be a FeeTx")
}
feeCoins := feeTx.GetFee()
gas := feeTx.GetGas()
msgs := feeTx.GetMsgs()
if !simulate {
// Compute taxes
taxes := FilterMsgAndComputeTax(ctx, tfd.treasuryKeeper, msgs...)
// Mempool fee validation
// No fee validation for oracle txs
if ctx.IsCheckTx() &&
!(isOracleTx(ctx, msgs) && gas <= uint64(len(msgs))*MaxOracleMsgGasUsage) {
if err := EnsureSufficientMempoolFees(ctx, gas, feeCoins, taxes); err != nil {
return ctx, sdkerrors.Wrapf(sdkerrors.ErrInsufficientFee, err.Error())
}
}
// Ensure paid fee is enough to cover taxes
if _, hasNeg := feeCoins.SafeSub(taxes); hasNeg {
return ctx, sdkerrors.Wrapf(sdkerrors.ErrInsufficientFee, "insufficient fees; got: %s required: %s", feeCoins, taxes)
}
// Record tax proceeds
if !taxes.IsZero() {
tfd.treasuryKeeper.RecordEpochTaxProceeds(ctx, taxes)
}
}
return next(ctx, tx, simulate)
}
// EnsureSufficientMempoolFees verifies that the given transaction has supplied
// enough fees(gas + stability) to cover a proposer's minimum fees. A result object is returned
// indicating success or failure.
//
// Contract: This should only be called during CheckTx as it cannot be part of
// consensus.
func EnsureSufficientMempoolFees(ctx sdk.Context, gas uint64, feeCoins sdk.Coins, taxes sdk.Coins) error {
requiredFees := sdk.Coins{}
minGasPrices := ctx.MinGasPrices()
if !minGasPrices.IsZero() {
requiredFees = make(sdk.Coins, len(minGasPrices))
// Determine the required fees by multiplying each required minimum gas
// price by the gas limit, where fee = ceil(minGasPrice * gasLimit).
glDec := sdk.NewDec(int64(gas))
for i, gp := range minGasPrices {
fee := gp.Amount.Mul(glDec)
requiredFees[i] = sdk.NewCoin(gp.Denom, fee.Ceil().RoundInt())
}
}
// Before checking gas prices, remove taxed from fee
var hasNeg bool
if feeCoins, hasNeg = feeCoins.SafeSub(taxes); hasNeg {
return fmt.Errorf("insufficient fees; got: %q, required: %q = %q(gas) +%q(stability)", feeCoins.Add(taxes...), requiredFees.Add(taxes...), requiredFees, taxes)
}
if !requiredFees.IsZero() && !feeCoins.IsAnyGTE(requiredFees) {
return fmt.Errorf("insufficient fees; got: %q, required: %q = %q(gas) +%q(stability)", feeCoins.Add(taxes...), requiredFees.Add(taxes...), requiredFees, taxes)
}
return nil
}
// FilterMsgAndComputeTax computes the stability tax on MsgSend and MsgMultiSend.
func FilterMsgAndComputeTax(ctx sdk.Context, tk TreasuryKeeper, msgs ...sdk.Msg) sdk.Coins {
taxes := sdk.Coins{}
for _, msg := range msgs {
switch msg := msg.(type) {
case *banktypes.MsgSend:
if !tk.HasBurnTaxExemptionAddress(ctx, msg.FromAddress, msg.ToAddress) {
taxes = taxes.Add(computeTax(ctx, tk, msg.Amount)...)
}
case *banktypes.MsgMultiSend:
tainted := 0
for _, input := range msg.Inputs {
if tk.HasBurnTaxExemptionAddress(ctx, input.Address) {
tainted++
}
}
for _, output := range msg.Outputs {
if tk.HasBurnTaxExemptionAddress(ctx, output.Address) {
tainted++
}
}
if tainted != len(msg.Inputs)+len(msg.Outputs) {
for _, input := range msg.Inputs {
taxes = taxes.Add(computeTax(ctx, tk, input.Coins)...)
}
}
case *marketexported.MsgSwapSend:
taxes = taxes.Add(computeTax(ctx, tk, sdk.NewCoins(msg.OfferCoin))...)
case *wasmexported.MsgInstantiateContract:
taxes = taxes.Add(computeTax(ctx, tk, msg.InitCoins)...)
case *wasmexported.MsgExecuteContract:
taxes = taxes.Add(computeTax(ctx, tk, msg.Coins)...)
case *authz.MsgExec:
messages, err := msg.GetMessages()
if err != nil {
panic(err)
}
taxes = taxes.Add(FilterMsgAndComputeTax(ctx, tk, messages...)...)
}
}
return taxes
}
// computes the stability tax according to tax-rate and tax-cap
func computeTax(ctx sdk.Context, tk TreasuryKeeper, principal sdk.Coins) sdk.Coins {
currHeight := ctx.BlockHeight()
taxRate := tk.GetTaxRate(ctx)
if taxRate.Equal(sdk.ZeroDec()) {
return sdk.Coins{}
}
taxes := sdk.Coins{}
for _, coin := range principal {
// Originally only a stability tax on UST. Changed to tax Luna as well after TaxPowerUpgradeHeight
if (coin.Denom == core.MicroLunaDenom || coin.Denom == sdk.DefaultBondDenom) && currHeight < TaxPowerUpgradeHeight {
continue
}
if coin.Denom == sdk.DefaultBondDenom {
continue
}
taxDue := sdk.NewDecFromInt(coin.Amount).Mul(taxRate).TruncateInt()
// If tax due is greater than the tax cap, cap!
taxCap := tk.GetTaxCap(ctx, coin.Denom)
if taxDue.GT(taxCap) {
taxDue = taxCap
}
if taxDue.Equal(sdk.ZeroInt()) {
continue
}
taxes = taxes.Add(sdk.NewCoin(coin.Denom, taxDue))
}
return taxes
}
func isOracleTx(ctx sdk.Context, msgs []sdk.Msg) bool {
for _, msg := range msgs {
switch msg.(type) {
case *oracleexported.MsgAggregateExchangeRatePrevote:
continue
case *oracleexported.MsgAggregateExchangeRateVote:
continue
default:
return false
}
}
return true
}