Skip to content

Commit

Permalink
Merge pull request #17 from irisnet/ht/feegrant
Browse files Browse the repository at this point in the history
Ht/feegrant
  • Loading branch information
MaricoHan authored Apr 16, 2022
2 parents ae0bba5 + 499350b commit dc2516a
Show file tree
Hide file tree
Showing 4 changed files with 231 additions and 1 deletion.
10 changes: 9 additions & 1 deletion feegrant/codec.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,19 @@ func init() {
amino.Seal()
}

// RegisterInterfaces No duplicate registration
// RegisterInterfaces registers the interfaces types with the interface registry
func RegisterInterfaces(registry types.InterfaceRegistry) {
registry.RegisterImplementations(
(*sdk.Msg)(nil),
&MsgGrantAllowance{},
&MsgRevokeAllowance{},
)

registry.RegisterInterface(
"cosmos.feegrant.v1beta1.FeeAllowanceI",
(*FeeAllowanceI)(nil),
&BasicAllowance{},
&PeriodicAllowance{},
&AllowedMsgAllowance{},
)
}
122 changes: 122 additions & 0 deletions feegrant/filtered_fee.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package feegrant

import (
"fmt"
"github.com/gogo/protobuf/proto"

"github.com/irisnet/core-sdk-go/common/codec/types"
sdk "github.com/irisnet/core-sdk-go/types"
)

// TODO: Revisit this once we have propoer gas fee framework.
// Tracking issues https://github.com/cosmos/cosmos-sdk/issues/9054, https://github.com/cosmos/cosmos-sdk/discussions/9072
const (
gasCostPerIteration = uint64(10)
)

var _ FeeAllowanceI = (*AllowedMsgAllowance)(nil)
var _ types.UnpackInterfacesMessage = (*AllowedMsgAllowance)(nil)

// UnpackInterfaces implements UnpackInterfacesMessage.UnpackInterfaces
func (a *AllowedMsgAllowance) UnpackInterfaces(unpacker types.AnyUnpacker) error {
var allowance FeeAllowanceI
return unpacker.UnpackAny(a.Allowance, &allowance)
}

// NewAllowedMsgAllowance creates new filtered fee allowance.
func NewAllowedMsgAllowance(allowance FeeAllowanceI, allowedMsgs []string) (*AllowedMsgAllowance, error) {
msg, ok := allowance.(proto.Message)
if !ok {
return nil, sdk.Wrap(fmt.Errorf("failed packing protobuf message to Any : cannot proto marshal %T", msg))
}
any, err := types.NewAnyWithValue(msg)
if err != nil {
return nil, err
}

return &AllowedMsgAllowance{
Allowance: any,
AllowedMessages: allowedMsgs,
}, nil
}

// GetAllowance returns allowed fee allowance.
func (a *AllowedMsgAllowance) GetAllowance() (FeeAllowanceI, error) {
allowance, ok := a.Allowance.GetCachedValue().(FeeAllowanceI)
if !ok {
return nil, sdk.Wrap(fmt.Errorf("message not allowed : %s", "failed to get allowance"))
}

return allowance, nil
}

// SetAllowance sets allowed fee allowance.
func (a *AllowedMsgAllowance) SetAllowance(allowance FeeAllowanceI) error {
var err error
a.Allowance, err = types.NewAnyWithValue(allowance.(proto.Message))
if err != nil {
return sdk.Wrap(fmt.Errorf("failed packing protobuf message to Any : cannot proto marshal %T", allowance))
}

return nil
}

// Accept method checks for the filtered messages has valid expiry
func (a *AllowedMsgAllowance) Accept(ctx sdk.Context, fee sdk.Coins, msgs []sdk.Msg) (bool, error) {
if !a.allMsgTypesAllowed(ctx, msgs) {
return false, sdk.Wrap(fmt.Errorf("message not allowed : %s", "message does not exist in allowed messages"))
}

allowance, err := a.GetAllowance()
if err != nil {
return false, err
}

remove, err := allowance.Accept(ctx, fee, msgs)
if err == nil && !remove {
if err = a.SetAllowance(allowance); err != nil {
return false, err
}
}
return remove, err
}

func (a *AllowedMsgAllowance) allowedMsgsToMap(ctx sdk.Context) map[string]bool {
msgsMap := make(map[string]bool, len(a.AllowedMessages))
for _, msg := range a.AllowedMessages {
ctx.GasMeter().ConsumeGas(gasCostPerIteration, "check msg")
msgsMap[msg] = true
}

return msgsMap
}

func (a *AllowedMsgAllowance) allMsgTypesAllowed(ctx sdk.Context, msgs []sdk.Msg) bool {
msgsMap := a.allowedMsgsToMap(ctx)

for _, msg := range msgs {
ctx.GasMeter().ConsumeGas(gasCostPerIteration, "check msg")
if !msgsMap[sdk.MsgTypeURL(msg)] {
return false
}
}

return true
}

// ValidateBasic implements FeeAllowance and enforces basic sanity checks
func (a *AllowedMsgAllowance) ValidateBasic() error {
if a.Allowance == nil {
return sdk.Wrap(fmt.Errorf("no allowance : %s", "allowance should not be empty"))
}
if len(a.AllowedMessages) == 0 {
return sdk.Wrap(fmt.Errorf("allowed messages are empty : %s", "allowed messages shouldn't be empty"))
}

allowance, err := a.GetAllowance()
if err != nil {
return err
}

return allowance.ValidateBasic()
}
95 changes: 95 additions & 0 deletions feegrant/periodic_fee.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package feegrant

import (
"fmt"
sdk "github.com/irisnet/core-sdk-go/types"
"time"
)

var _ FeeAllowanceI = (*PeriodicAllowance)(nil)

func (m *PeriodicAllowance) Accept(ctx sdk.Context, fee sdk.Coins, _ []sdk.Msg) (remove bool, err error) {
blockTime := ctx.BlockTime()

if m.Basic.Expiration != nil && blockTime.After(*m.Basic.Expiration) {
return true, sdk.Wrap(fmt.Errorf("fee allowance expired : %s", "absolute limit"))
}

m.tryResetPeriod(blockTime)

// deduct from both the current period and the max amount
var isNeg bool
m.PeriodCanSpend, isNeg = m.PeriodCanSpend.SafeSub(fee)
if isNeg {
return false, sdk.Wrap(fmt.Errorf("fee limit exceeded : %s", "period limit"))
}

if m.Basic.SpendLimit != nil {
m.Basic.SpendLimit, isNeg = m.Basic.SpendLimit.SafeSub(fee)
if isNeg {
return false, sdk.Wrap(fmt.Errorf("fee limit exceeded : %s", "absolute limit"))
}

return m.Basic.SpendLimit.IsZero(), nil
}

return false, nil
}

// tryResetPeriod will check if the PeriodReset has been hit. If not, it is a no-op.
// If we hit the reset period, it will top up the PeriodCanSpend amount to
// min(PeriodSpendLimit, Basic.SpendLimit) so it is never more than the maximum allowed.
// It will also update the PeriodReset. If we are within one Period, it will update from the
// last PeriodReset (eg. if you always do one tx per day, it will always reset the same time)
// If we are more then one period out (eg. no activity in a week), reset is one Period from the execution of this method
func (m *PeriodicAllowance) tryResetPeriod(blockTime time.Time) {
if blockTime.Before(m.PeriodReset) {
return
}

// set PeriodCanSpend to the lesser of Basic.SpendLimit and PeriodSpendLimit
if _, isNeg := m.Basic.SpendLimit.SafeSub(m.PeriodSpendLimit); isNeg && !m.Basic.SpendLimit.Empty() {
m.PeriodCanSpend = m.Basic.SpendLimit
} else {
m.PeriodCanSpend = m.PeriodSpendLimit
}

// If we are within the period, step from expiration (eg. if you always do one tx per day, it will always reset the same time)
// If we are more then one period out (eg. no activity in m week), reset is one period from this time
m.PeriodReset = m.PeriodReset.Add(m.Period)
if blockTime.After(m.PeriodReset) {
m.PeriodReset = blockTime.Add(m.Period)
}
}

func (m *PeriodicAllowance) ValidateBasic() error {
if err := m.Basic.ValidateBasic(); err != nil {
return err
}

if !m.PeriodSpendLimit.IsValid() {
return sdk.Wrap(fmt.Errorf("invalid coins , spend amount is invalid: %v", m.PeriodSpendLimit))
}
if !m.PeriodSpendLimit.IsAllPositive() {
return sdk.Wrap(fmt.Errorf("invalid coins : %s", "spend limit must be positive"))
}
if !m.PeriodCanSpend.IsValid() {
return sdk.Wrap(fmt.Errorf("invalid coins , can spend amount is invalid: %v", m.PeriodCanSpend))
}
// We allow 0 for CanSpend
if m.PeriodCanSpend.IsAnyNegative() {
return sdk.Wrap(fmt.Errorf("invalid coins : %s", "can spend must not be negative"))
}

// ensure PeriodSpendLimit can be subtracted from total (same coin types)
if m.Basic.SpendLimit != nil && !m.PeriodSpendLimit.DenomsSubsetOf(m.Basic.SpendLimit) {
return sdk.Wrap(fmt.Errorf("invalid coins : %s", "period spend limit has different currency than basic spend limit"))
}

// check times
if m.Period.Seconds() < 0 {
return sdk.Wrap(fmt.Errorf("invalid duration : %s", "negative clock step"))
}

return nil
}
5 changes: 5 additions & 0 deletions types/tx_msg.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,8 @@ type TxDecoder func(txBytes []byte) (Tx, error)

// TxEncoder marshals transaction to bytes
type TxEncoder func(tx Tx) ([]byte, error)

// MsgTypeURL returns the TypeURL of a `sdk.Msg`.
func MsgTypeURL(msg Msg) string {
return "/" + proto.MessageName(msg)
}

0 comments on commit dc2516a

Please sign in to comment.