Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feegrants for authz and smart contracts #206

Merged
merged 5 commits into from
Jun 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions proto/xion/v1/feegrant.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
syntax = "proto3";
package xion.v1;

import "gogoproto/gogo.proto";
import "google/protobuf/any.proto";
import "cosmos_proto/cosmos.proto";
import "cosmos/base/v1beta1/coin.proto";
import "amino/amino.proto";
import "google/protobuf/timestamp.proto";
import "google/protobuf/duration.proto";

option go_package = "github.com/burnt-labs/xion/x/xion/types";

// AuthzAllowance creates allowance only authz message for a specific grantee
message AuthzAllowance {
option (gogoproto.goproto_getters) = false;
option (cosmos_proto.implements_interface) = "cosmos.feegrant.v1beta1.FeeAllowanceI";
option (amino.name) = "cosmos-sdk/AuthzAllowance";

// allowance can be any of basic and periodic fee allowance.
google.protobuf.Any allowance = 1 [(cosmos_proto.accepts_interface) = "cosmos.feegrant.v1beta1.FeeAllowanceI"];

string authz_grantee = 2 [(cosmos_proto.scalar) = "cosmos.AddressString"];
}

// ContractsAllowance creates allowance only for specific contracts
message ContractsAllowance {
option (gogoproto.goproto_getters) = false;
option (cosmos_proto.implements_interface) = "cosmos.feegrant.v1beta1.FeeAllowanceI";
option (amino.name) = "cosmos-sdk/AuthzAllowance";

// allowance can be any of basic and periodic fee allowance.
google.protobuf.Any allowance = 1 [(cosmos_proto.accepts_interface) = "cosmos.feegrant.v1beta1.FeeAllowanceI"];

repeated string contract_addresses = 2 [(cosmos_proto.scalar) = "cosmos.AddressString"];
}
11 changes: 11 additions & 0 deletions x/xion/types/codec.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/msgservice"
authzcodec "github.com/cosmos/cosmos-sdk/x/authz/codec"
"github.com/cosmos/cosmos-sdk/x/feegrant"
govcodec "github.com/cosmos/cosmos-sdk/x/gov/codec"
groupcodec "github.com/cosmos/cosmos-sdk/x/group/codec"
)
Expand All @@ -18,6 +19,9 @@ func RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) {
legacy.RegisterAminoMsg(cdc, &MsgSend{}, "xion/MsgSend")
legacy.RegisterAminoMsg(cdc, &MsgMultiSend{}, "xion/MsgMultiSend")
legacy.RegisterAminoMsg(cdc, &MsgSetPlatformPercentage{}, "xion/MsgSetPlatformPercentage")

cdc.RegisterConcrete(&AuthzAllowance{}, "xion/AuthzAllowance", nil)
cdc.RegisterConcrete(&ContractsAllowance{}, "xion/ContractsAllowance", nil)
}

func RegisterInterfaces(registry types.InterfaceRegistry) {
Expand All @@ -27,6 +31,13 @@ func RegisterInterfaces(registry types.InterfaceRegistry) {
&MsgSetPlatformPercentage{},
)

registry.RegisterInterface(
"cosmos.feegrant.v1beta1.FeeAllowanceI",
(*feegrant.FeeAllowanceI)(nil),
&AuthzAllowance{},
&ContractsAllowance{},
)

msgservice.RegisterMsgServiceDesc(registry, &_Msg_serviceDesc)
}

Expand Down
10 changes: 10 additions & 0 deletions x/xion/types/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package types

import errorsmod "cosmossdk.io/errors"

// Codes for general xion errors
const (
DefaultCodespace = ModuleName
)

var ErrNoAllowedContracts = errorsmod.Register(DefaultCodespace, 2, "no contract addresses specified")
268 changes: 268 additions & 0 deletions x/xion/types/feegrant.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
package types

import (
"time"

wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types"

"github.com/cosmos/gogoproto/proto"

errorsmod "cosmossdk.io/errors"

"github.com/cosmos/cosmos-sdk/codec/types"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/cosmos/cosmos-sdk/x/authz"
"github.com/cosmos/cosmos-sdk/x/feegrant"
)

// TODO: Revisit this once we have proper 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 (
_ feegrant.FeeAllowanceI = (*AuthzAllowance)(nil)
_ feegrant.FeeAllowanceI = (*ContractsAllowance)(nil)
_ types.UnpackInterfacesMessage = (*AuthzAllowance)(nil)
_ types.UnpackInterfacesMessage = (*ContractsAllowance)(nil)
)

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

func NewAuthzAllowance(allowance feegrant.FeeAllowanceI, authzGrantee sdk.AccAddress) (*AuthzAllowance, error) {
msg, ok := allowance.(proto.Message)
if !ok {
return nil, errorsmod.Wrapf(sdkerrors.ErrPackAny, "cannot proto marshal %T", msg)
}
anyAllowance, err := types.NewAnyWithValue(msg)
if err != nil {
return nil, err
}

return &AuthzAllowance{
Allowance: anyAllowance,
AuthzGrantee: authzGrantee.String(),
}, nil
}

// GetAllowance returns allowed fee allowance.
func (a *AuthzAllowance) GetAllowance() (feegrant.FeeAllowanceI, error) {
allowance, ok := a.Allowance.GetCachedValue().(feegrant.FeeAllowanceI)
if !ok {
return nil, errorsmod.Wrap(feegrant.ErrNoAllowance, "failed to get allowance")
}

return allowance, nil
}

// SetAllowance sets allowed fee allowance.
func (a *AuthzAllowance) SetAllowance(allowance feegrant.FeeAllowanceI) error {
var err error
a.Allowance, err = types.NewAnyWithValue(allowance.(proto.Message))
if err != nil {
return errorsmod.Wrapf(sdkerrors.ErrPackAny, "cannot proto marshal %T", allowance)
}

return nil
}

func (a *AuthzAllowance) Accept(ctx sdk.Context, fee sdk.Coins, msgs []sdk.Msg) (bool, error) {
subMsgs, ok := a.allMsgTypesAuthz(ctx, msgs)
if !ok {
return false, errorsmod.Wrap(feegrant.ErrMessageNotAllowed, "messages are not authz")
}

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

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

func (a *AuthzAllowance) allMsgTypesAuthz(ctx sdk.Context, msgs []sdk.Msg) ([]sdk.Msg, bool) {
var subMsgs []sdk.Msg

for _, msg := range msgs {
ctx.GasMeter().ConsumeGas(gasCostPerIteration, "check msg")

authzMsg, ok := msg.(*authz.MsgExec)
if !ok {
return nil, false
}
if authzMsg.Grantee != a.AuthzGrantee {
return nil, false
}

msgMsgs, err := authzMsg.GetMessages()
if err != nil {
return nil, false
}
subMsgs = append(subMsgs, msgMsgs...)
}

return subMsgs, true
}

func (a *AuthzAllowance) ValidateBasic() error {
if a.Allowance == nil {
return errorsmod.Wrap(feegrant.ErrNoAllowance, "allowance should not be empty")
}

if _, err := sdk.AccAddressFromBech32(a.AuthzGrantee); err != nil {
return err
}

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

return allowance.ValidateBasic()
}

func (a *AuthzAllowance) ExpiresAt() (*time.Time, error) {
allowance, err := a.GetAllowance()
if err != nil {
return nil, err
}
return allowance.ExpiresAt()
}

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

func NewContractsAllowance(allowance feegrant.FeeAllowanceI, allowedContractAddrs []sdk.AccAddress) (*ContractsAllowance, error) {
msg, ok := allowance.(proto.Message)
if !ok {
return nil, errorsmod.Wrapf(sdkerrors.ErrPackAny, "cannot proto marshal %T", msg)
}
anyAllowance, err := types.NewAnyWithValue(msg)
if err != nil {
return nil, err
}

allowedAddrStrings := make([]string, len(allowedContractAddrs))
for i, addr := range allowedContractAddrs {
allowedAddrStrings[i] = addr.String()
}

return &ContractsAllowance{
Allowance: anyAllowance,
ContractAddresses: allowedAddrStrings,
}, nil
}

// GetAllowance returns allowed fee allowance.
func (a *ContractsAllowance) GetAllowance() (feegrant.FeeAllowanceI, error) {
allowance, ok := a.Allowance.GetCachedValue().(feegrant.FeeAllowanceI)
if !ok {
return nil, errorsmod.Wrap(feegrant.ErrNoAllowance, "failed to get allowance")
}

return allowance, nil
}

// SetAllowance sets allowed fee allowance.
func (a *ContractsAllowance) SetAllowance(allowance feegrant.FeeAllowanceI) error {
var err error
a.Allowance, err = types.NewAnyWithValue(allowance.(proto.Message))
if err != nil {
return errorsmod.Wrapf(sdkerrors.ErrPackAny, "cannot proto marshal %T", allowance)
}

return nil
}

func (a *ContractsAllowance) Accept(ctx sdk.Context, fee sdk.Coins, msgs []sdk.Msg) (bool, error) {
if !a.allMsgsValidWasmExecs(ctx, msgs) {
return false, errorsmod.Wrap(feegrant.ErrMessageNotAllowed, "messages are not for specific contracts")
}

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 *ContractsAllowance) allowedContractsToMap(ctx sdk.Context) map[string]bool {
addrsMap := make(map[string]bool, len(a.ContractAddresses))
for _, addr := range a.ContractAddresses {
ctx.GasMeter().ConsumeGas(gasCostPerIteration, "check msg")
addrsMap[addr] = true
}

return addrsMap
}

func (a *ContractsAllowance) allMsgsValidWasmExecs(ctx sdk.Context, msgs []sdk.Msg) bool {
addrsMap := a.allowedContractsToMap(ctx)

for _, msg := range msgs {
ctx.GasMeter().ConsumeGas(gasCostPerIteration, "check msg")

wasmMsg, ok := msg.(*wasmtypes.MsgExecuteContract)
if !ok {
return false
}
if !addrsMap[wasmMsg.Contract] {
return false
}
}

return true
}

func (a *ContractsAllowance) ValidateBasic() error {
if a.Allowance == nil {
return errorsmod.Wrap(feegrant.ErrNoAllowance, "allowance should not be empty")
}

if len(a.ContractAddresses) < 1 {
return errorsmod.Wrap(ErrNoAllowedContracts, "must set contracts for feegrant")
}

for _, addr := range a.ContractAddresses {
if _, err := sdk.AccAddressFromBech32(addr); err != nil {
return err
}
}

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

return allowance.ValidateBasic()
}

func (a *ContractsAllowance) ExpiresAt() (*time.Time, error) {
allowance, err := a.GetAllowance()
if err != nil {
return nil, err
}
return allowance.ExpiresAt()
}
Loading
Loading