-
Notifications
You must be signed in to change notification settings - Fork 0
/
fees.go
236 lines (200 loc) · 8.12 KB
/
fees.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
// Copyright 2022 Serv Foundation
// This file is part of the Serv Network packages.
//
// Serv is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The Serv packages are distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the Serv packages. If not, see https://github.com/twobitedd/serv/blob/main/LICENSE
package cosmos
import (
"fmt"
"math"
errorsmod "cosmossdk.io/errors"
sdk "github.com/cosmos/cosmos-sdk/types"
errortypes "github.com/cosmos/cosmos-sdk/types/errors"
authante "github.com/cosmos/cosmos-sdk/x/auth/ante"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
anteutils "github.com/twobitedd/serv/v12/app/ante/utils"
)
// DeductFeeDecorator deducts fees from the first signer of the tx.
// If the first signer does not have the funds to pay for the fees,
// and does not have enough unclaimed staking rewards, then return
// with InsufficientFunds error.
// The next AnteHandler is called if fees are successfully deducted.
//
// CONTRACT: Tx must implement FeeTx interface to use DeductFeeDecorator
type DeductFeeDecorator struct {
accountKeeper authante.AccountKeeper
bankKeeper BankKeeper
distributionKeeper anteutils.DistributionKeeper
feegrantKeeper authante.FeegrantKeeper
stakingKeeper anteutils.StakingKeeper
txFeeChecker anteutils.TxFeeChecker
}
// NewDeductFeeDecorator returns a new DeductFeeDecorator.
func NewDeductFeeDecorator(
ak authante.AccountKeeper,
bk BankKeeper,
dk anteutils.DistributionKeeper,
fk authante.FeegrantKeeper,
sk anteutils.StakingKeeper,
tfc anteutils.TxFeeChecker,
) DeductFeeDecorator {
if tfc == nil {
tfc = checkTxFeeWithValidatorMinGasPrices
}
return DeductFeeDecorator{
accountKeeper: ak,
bankKeeper: bk,
distributionKeeper: dk,
feegrantKeeper: fk,
stakingKeeper: sk,
txFeeChecker: tfc,
}
}
// AnteHandle ensures that the transaction contains valid fee requirements and tries to deduct those
// from the account balance or unclaimed staking rewards, which the transaction sender might have.
func (dfd DeductFeeDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) {
feeTx, ok := tx.(sdk.FeeTx)
if !ok {
return ctx, errorsmod.Wrap(errortypes.ErrTxDecode, "Tx must be a FeeTx")
}
if !simulate && ctx.BlockHeight() > 0 && feeTx.GetGas() <= 0 {
return ctx, errorsmod.Wrap(errortypes.ErrInvalidGasLimit, "must provide positive gas")
}
var (
priority int64
err error
)
fee := feeTx.GetFee()
if !simulate {
fee, priority, err = dfd.txFeeChecker(ctx, feeTx)
if err != nil {
return ctx, err
}
}
feePayer := feeTx.FeePayer()
feeGranter := feeTx.FeeGranter()
if err = dfd.deductFee(ctx, tx, fee, feePayer, feeGranter); err != nil {
return ctx, err
}
newCtx := ctx.WithPriority(priority)
return next(newCtx, tx, simulate)
}
// deductFee checks if the fee payer has enough funds to pay for the fees and deducts them.
// If the spendable balance is not enough, it tries to claim enough staking rewards to cover the fees.
func (dfd DeductFeeDecorator) deductFee(ctx sdk.Context, sdkTx sdk.Tx, fees sdk.Coins, feePayer, feeGranter sdk.AccAddress) error {
if fees.IsZero() {
return nil
}
if addr := dfd.accountKeeper.GetModuleAddress(authtypes.FeeCollectorName); addr == nil {
return fmt.Errorf("fee collector module account (%s) has not been set", authtypes.FeeCollectorName)
}
// by default, deduct fees from feePayer address
deductFeesFrom := feePayer
// if feegranter is set, then deduct the fee from the feegranter account.
// this works only when feegrant is enabled.
if feeGranter != nil {
if dfd.feegrantKeeper == nil {
return errortypes.ErrInvalidRequest.Wrap("fee grants are not enabled")
}
if !feeGranter.Equals(feePayer) {
err := dfd.feegrantKeeper.UseGrantedFees(ctx, feeGranter, feePayer, fees, sdkTx.GetMsgs())
if err != nil {
return errorsmod.Wrapf(err, "%s does not not allow to pay fees for %s", feeGranter, feePayer)
}
}
deductFeesFrom = feeGranter
}
deductFeesFromAcc := dfd.accountKeeper.GetAccount(ctx, deductFeesFrom)
if deductFeesFromAcc == nil {
return errortypes.ErrUnknownAddress.Wrapf("fee payer address: %s does not exist", deductFeesFrom)
}
// deduct the fees
if err := deductFeesFromBalanceOrUnclaimedStakingRewards(ctx, dfd, deductFeesFromAcc, fees); err != nil {
return fmt.Errorf("insufficient funds and failed to claim sufficient staking rewards to pay for fees: %w", err)
}
events := sdk.Events{
sdk.NewEvent(
sdk.EventTypeTx,
sdk.NewAttribute(sdk.AttributeKeyFee, fees.String()),
sdk.NewAttribute(sdk.AttributeKeyFeePayer, deductFeesFrom.String()),
),
}
ctx.EventManager().EmitEvents(events)
return nil
}
// deductFeesFromBalanceOrUnclaimedStakingRewards tries to deduct the fees from the account balance.
// If the account balance is not enough, it tries to claim enough staking rewards to cover the fees.
func deductFeesFromBalanceOrUnclaimedStakingRewards(
ctx sdk.Context, dfd DeductFeeDecorator, deductFeesFromAcc authtypes.AccountI, fees sdk.Coins,
) error {
if err := anteutils.ClaimStakingRewardsIfNecessary(
ctx, dfd.bankKeeper, dfd.distributionKeeper, dfd.stakingKeeper, deductFeesFromAcc.GetAddress(), fees,
); err != nil {
return err
}
return authante.DeductFees(dfd.bankKeeper, ctx, deductFeesFromAcc, fees)
}
// checkTxFeeWithValidatorMinGasPrices implements the default fee logic, where the minimum price per
// unit of gas is fixed and set by each validator, and the tx priority is computed from the gas price.
func checkTxFeeWithValidatorMinGasPrices(ctx sdk.Context, feeTx sdk.FeeTx) (sdk.Coins, int64, error) {
feeCoins := feeTx.GetFee()
gas := feeTx.GetGas()
// Ensure that the provided fees meets a minimum threshold for the validator,
// if this is a CheckTx. This is only for local mempool purposes, and thus
// is only ran on CheckTx.
if ctx.IsCheckTx() {
if err := checkFeeCoinsAgainstMinGasPrices(ctx, feeCoins, gas); err != nil {
return nil, 0, err
}
}
priority := getTxPriority(feeCoins, int64(gas)) //#nosec G701 -- gosec warning about integer overflow is not relevant here
return feeCoins, priority, nil
}
// checkFeeCoinsAgainstMinGasPrices checks if the provided fee coins are greater than or equal to the
// required fees, that are based on the minimum gas prices and the gas. If not, it will return an error.
func checkFeeCoinsAgainstMinGasPrices(ctx sdk.Context, feeCoins sdk.Coins, gas uint64) error {
minGasPrices := ctx.MinGasPrices()
if minGasPrices.IsZero() {
return nil
}
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)) //#nosec G701 -- gosec warning about integer overflow is not relevant here
for i, gp := range minGasPrices {
fee := gp.Amount.Mul(glDec)
requiredFees[i] = sdk.NewCoin(gp.Denom, fee.Ceil().RoundInt())
}
if !feeCoins.IsAnyGTE(requiredFees) {
return errorsmod.Wrapf(errortypes.ErrInsufficientFee, "insufficient fees; got: %s required: %s", feeCoins, requiredFees)
}
return nil
}
// getTxPriority returns a naive tx priority based on the amount of the smallest denomination of the gas price
// provided in a transaction.
// NOTE: This implementation should be used with a great consideration as it opens potential attack vectors
// where txs with multiple coins could not be prioritized as expected.
func getTxPriority(fees sdk.Coins, gas int64) int64 {
var priority int64
for _, c := range fees {
p := int64(math.MaxInt64)
gasPrice := c.Amount.QuoRaw(gas)
if gasPrice.IsInt64() {
p = gasPrice.Int64()
}
if priority == 0 || p < priority {
priority = p
}
}
return priority
}