diff --git a/e2e/noble_test.go b/e2e/noble_test.go index f043db7..6b1dbd9 100644 --- a/e2e/noble_test.go +++ b/e2e/noble_test.go @@ -3,6 +3,7 @@ package e2e import ( "context" "encoding/json" + "fmt" "testing" "github.com/circlefin/noble-fiattokenfactory/x/fiattokenfactory/types" @@ -153,12 +154,19 @@ func nobleTokenfactory_e2e(t *testing.T, ctx context.Context, tokenfactoryModNam require.NoError(t, err, "failed to get user balance") require.Equal(t, int64(100), userBalance, "user balance should not have incremented while blacklisted") + // authz send to blacklisted account + testAuthZSendFail(t, ctx, nobleValidator, mintingDenom, noble, extraWallets.User2, extraWallets.User, extraWallets.Alice) + // authz send from blacklisted account + testAuthZSendFail(t, ctx, nobleValidator, mintingDenom, noble, extraWallets.User, extraWallets.User2, extraWallets.Alice) + // authz send with blacklisted grantee + testAuthZSendFail(t, ctx, nobleValidator, mintingDenom, noble, extraWallets.User2, extraWallets.Alice, extraWallets.User) + err = nobleValidator.SendFunds(ctx, extraWallets.User2.KeyName(), ibc.WalletAmount{ Address: extraWallets.User.FormattedAddress(), Denom: "token", Amount: 100, }) - require.NoError(t, err, "The tx should have been successfull as that is no the minting denom") + require.NoError(t, err, "The tx should have been successfull as that is not the minting denom") _, err = nobleValidator.ExecTx(ctx, roles.Blacklister.KeyName(), tokenfactoryModName, "unblacklist", extraWallets.User.FormattedAddress(), "-b", "block", @@ -263,6 +271,9 @@ func nobleTokenfactory_e2e(t *testing.T, ctx context.Context, tokenfactoryModNam ) require.NoError(t, err, "minters should be able to be removed while in paused state") + // authz send fails when chain is paused + testAuthZSendFail(t, ctx, nobleValidator, mintingDenom, noble, extraWallets.User2, extraWallets.User, extraWallets.Alice) + _, err = nobleValidator.ExecTx(ctx, roles.Pauser.KeyName(), tokenfactoryModName, "unpause", "-b", "block", ) @@ -282,4 +293,47 @@ func nobleTokenfactory_e2e(t *testing.T, ctx context.Context, tokenfactoryModNam aliceBalance, err = noble.GetBalance(ctx, extraWallets.Alice.FormattedAddress(), mintingDenom) require.NoError(t, err, "failed to get alice balance") require.Equal(t, int64(100), aliceBalance, "alice balance should not have increased while chain is paused") + + testAuthZSendSucceed(t, ctx, nobleValidator, mintingDenom, noble, extraWallets.User, extraWallets.User2, extraWallets.Alice) +} + +func testAuthZSend(t *testing.T, ctx context.Context, nobleValidator *cosmos.ChainNode, mintingDenom string, noble *cosmos.CosmosChain, fromWallet ibc.Wallet, toWallet ibc.Wallet, granteeWallet ibc.Wallet) (string, error) { + grantAuthorization(t, ctx, nobleValidator, mintingDenom, noble, fromWallet, granteeWallet) + + bz, _, _ := nobleValidator.ExecBin(ctx, "tx", "bank", "send", fromWallet.FormattedAddress(), toWallet.FormattedAddress(), fmt.Sprintf("%d%s", 50, mintingDenom), "--chain-id", noble.Config().ChainID, "--generate-only") + _ = nobleValidator.WriteFile(ctx, bz, "tx.json") + + return nobleValidator.ExecTx(ctx, granteeWallet.KeyName(), "authz", "exec", "/var/cosmos-chain/noble-1/tx.json") +} + +func testAuthZSendFail(t *testing.T, ctx context.Context, nobleValidator *cosmos.ChainNode, mintingDenom string, noble *cosmos.CosmosChain, fromWallet ibc.Wallet, toWallet ibc.Wallet, granteeWallet ibc.Wallet) { + toWalletInitialBalance := getBalance(t, ctx, nobleValidator, mintingDenom, noble, toWallet) + + _, err := testAuthZSend(t, ctx, nobleValidator, mintingDenom, noble, fromWallet, toWallet, granteeWallet) + + require.Error(t, err, "failed to block transactions") + toWalletBalance := getBalance(t, ctx, nobleValidator, mintingDenom, noble, toWallet) + require.Equal(t, toWalletInitialBalance, toWalletBalance, "toWallet balance should not have incremented") +} + +func testAuthZSendSucceed(t *testing.T, ctx context.Context, nobleValidator *cosmos.ChainNode, mintingDenom string, noble *cosmos.CosmosChain, fromWallet ibc.Wallet, toWallet ibc.Wallet, granteeWallet ibc.Wallet) { + toWalletInitialBalance := getBalance(t, ctx, nobleValidator, mintingDenom, noble, toWallet) + + _, err := testAuthZSend(t, ctx, nobleValidator, mintingDenom, noble, fromWallet, toWallet, granteeWallet) + + require.NoError(t, err, "failed to execute authz message") + toWalletBalance := getBalance(t, ctx, nobleValidator, mintingDenom, noble, toWallet) + require.Equal(t, toWalletInitialBalance+50, toWalletBalance, "toWallet balance should have incremented") +} + + +func grantAuthorization(t *testing.T, ctx context.Context, nobleValidator *cosmos.ChainNode, mintingDenom string, noble *cosmos.CosmosChain, grantor ibc.Wallet, grantee ibc.Wallet) { + _, err := nobleValidator.ExecTx(ctx, grantor.KeyName(), "authz", "grant", grantee.FormattedAddress(), "send", "--spend-limit", fmt.Sprintf("%d%s", 100, mintingDenom)) + require.NoError(t, err, "failed to grant permissions") +} + +func getBalance(t *testing.T, ctx context.Context, nobleValidator *cosmos.ChainNode, mintingDenom string, noble *cosmos.CosmosChain, wallet ibc.Wallet) int64 { + bal, err := noble.GetBalance(ctx, wallet.FormattedAddress(), mintingDenom) + require.NoError(t, err, "failed to get user balance") + return bal } diff --git a/simapp/ante_handler.go b/simapp/ante_handler.go index b9179d6..6dd6694 100644 --- a/simapp/ante_handler.go +++ b/simapp/ante_handler.go @@ -1,14 +1,11 @@ package simapp import ( + "github.com/circlefin/noble-fiattokenfactory/x/fiattokenfactory" fiattokenfactorykeeper "github.com/circlefin/noble-fiattokenfactory/x/fiattokenfactory/keeper" - fiattokenfactorytypes "github.com/circlefin/noble-fiattokenfactory/x/fiattokenfactory/types" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/types/bech32" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" "github.com/cosmos/cosmos-sdk/x/auth/ante" - banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" - transfertypes "github.com/cosmos/ibc-go/v4/modules/apps/transfer/types" ibcante "github.com/cosmos/ibc-go/v4/modules/core/ante" "github.com/cosmos/ibc-go/v4/modules/core/keeper" ) @@ -23,152 +20,6 @@ type HandlerOptions struct { FiatTokenFactoryKeeper *fiattokenfactorykeeper.Keeper } -type IsPausedDecorator struct { - fiatTokenFactory *fiattokenfactorykeeper.Keeper -} - -func NewIsPausedDecorator(ctf *fiattokenfactorykeeper.Keeper) IsPausedDecorator { - return IsPausedDecorator{ - fiatTokenFactory: ctf, - } -} - -func (ad IsPausedDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (newCtx sdk.Context, err error) { - msgs := tx.GetMsgs() - for _, m := range msgs { - switch m := m.(type) { - case *banktypes.MsgSend, *banktypes.MsgMultiSend, *transfertypes.MsgTransfer: - switch m := m.(type) { - case *banktypes.MsgSend: - for _, c := range m.Amount { - paused, err := checkPausedStatebyTokenFactory(ctx, c, ad.fiatTokenFactory) - if paused { - return ctx, sdkerrors.Wrapf(err, "can not perform token transfers") - } - } - case *banktypes.MsgMultiSend: - for _, i := range m.Inputs { - for _, c := range i.Coins { - paused, err := checkPausedStatebyTokenFactory(ctx, c, ad.fiatTokenFactory) - if paused { - return ctx, sdkerrors.Wrapf(err, "can not perform token transfers") - } - } - } - case *transfertypes.MsgTransfer: - paused, err := checkPausedStatebyTokenFactory(ctx, m.Token, ad.fiatTokenFactory) - if paused { - return ctx, sdkerrors.Wrapf(err, "can not perform token transfers") - } - default: - continue - } - default: - continue - } - } - return next(ctx, tx, simulate) -} - -func checkPausedStatebyTokenFactory(ctx sdk.Context, c sdk.Coin, ctf *fiattokenfactorykeeper.Keeper) (bool, *sdkerrors.Error) { - ctfMintingDenom := ctf.GetMintingDenom(ctx) - if c.Denom == ctfMintingDenom.Denom { - paused := ctf.GetPaused(ctx) - if paused.Paused { - return true, fiattokenfactorytypes.ErrPaused - } - } - return false, nil -} - -type IsBlacklistedDecorator struct { - fiattokenfactory *fiattokenfactorykeeper.Keeper -} - -func NewIsBlacklistedDecorator(ctf *fiattokenfactorykeeper.Keeper) IsBlacklistedDecorator { - return IsBlacklistedDecorator{ - fiattokenfactory: ctf, - } -} - -func (ad IsBlacklistedDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (newCtx sdk.Context, err error) { - msgs := tx.GetMsgs() - for _, m := range msgs { - switch m := m.(type) { - case *banktypes.MsgSend, *banktypes.MsgMultiSend, *transfertypes.MsgTransfer: - switch m := m.(type) { - case *banktypes.MsgSend: - for _, c := range m.Amount { - addresses := []string{m.ToAddress, m.FromAddress} - blacklisted, address, err := checkForBlacklistedAddressByTokenFactory(ctx, addresses, c, ad.fiattokenfactory) - if blacklisted { - return ctx, sdkerrors.Wrapf(err, "an address (%s) is blacklisted and can not send or receive tokens", address) - } - if err != nil { - return ctx, sdkerrors.Wrapf(err, "error decoding address (%s)", address) - } - } - case *banktypes.MsgMultiSend: - for _, i := range m.Inputs { - for _, c := range i.Coins { - addresses := []string{i.Address} - blacklisted, address, err := checkForBlacklistedAddressByTokenFactory(ctx, addresses, c, ad.fiattokenfactory) - if blacklisted { - return ctx, sdkerrors.Wrapf(err, "an address (%s) is blacklisted and can not send or receive tokens", address) - } - if err != nil { - return ctx, sdkerrors.Wrapf(err, "error decoding address (%s)", address) - } - } - } - for _, o := range m.Outputs { - for _, c := range o.Coins { - addresses := []string{o.Address} - blacklisted, address, err := checkForBlacklistedAddressByTokenFactory(ctx, addresses, c, ad.fiattokenfactory) - if blacklisted { - return ctx, sdkerrors.Wrapf(err, "an address (%s) is blacklisted and can not send or receive tokens", address) - } - if err != nil { - return ctx, sdkerrors.Wrapf(err, "error decoding address (%s)", address) - } - } - } - case *transfertypes.MsgTransfer: - addresses := []string{m.Sender, m.Receiver} - blacklisted, address, err := checkForBlacklistedAddressByTokenFactory(ctx, addresses, m.Token, ad.fiattokenfactory) - if blacklisted { - return ctx, sdkerrors.Wrapf(err, "an address (%s) is blacklisted and can not send or receive tokens", address) - } - if err != nil { - return ctx, sdkerrors.Wrapf(err, "error decoding address (%s)", address) - } - } - default: - continue - } - } - return next(ctx, tx, simulate) -} - -// checkForBlacklistedAddressByTokenFactory first checks if the denom being transacted is a mintable asset from a TokenFactory, -// if it is, it checks if the addresses involved in the tx are blacklisted by that specific TokenFactory. -func checkForBlacklistedAddressByTokenFactory(ctx sdk.Context, addresses []string, c sdk.Coin, ctf *fiattokenfactorykeeper.Keeper) (blacklisted bool, blacklistedAddress string, err error) { - ctfMintingDenom := ctf.GetMintingDenom(ctx) - if c.Denom == ctfMintingDenom.Denom { - for _, address := range addresses { - _, addressBz, err := bech32.DecodeAndConvert(address) - if err != nil { - return false, address, err - } - _, found := ctf.GetBlacklisted(ctx, addressBz) - if found { - return true, address, fiattokenfactorytypes.ErrUnauthorized - } - } - } - return false, "", nil -} - // NewAnteHandler creates a new ante handler func NewAnteHandler(options HandlerOptions) (sdk.AnteHandler, error) { if options.AccountKeeper == nil { @@ -192,8 +43,8 @@ func NewAnteHandler(options HandlerOptions) (sdk.AnteHandler, error) { anteDecorators := []sdk.AnteDecorator{ ante.NewSetUpContextDecorator(), ante.NewRejectExtensionOptionsDecorator(), - NewIsBlacklistedDecorator(options.FiatTokenFactoryKeeper), - NewIsPausedDecorator(options.FiatTokenFactoryKeeper), + fiattokenfactory.NewIsBlacklistedDecorator(options.FiatTokenFactoryKeeper), + fiattokenfactory.NewIsPausedDecorator(options.FiatTokenFactoryKeeper), ante.NewMempoolFeeDecorator(), ante.NewValidateBasicDecorator(), ante.NewTxTimeoutHeightDecorator(), diff --git a/x/fiattokenfactory/ante.go b/x/fiattokenfactory/ante.go new file mode 100644 index 0000000..e69b93d --- /dev/null +++ b/x/fiattokenfactory/ante.go @@ -0,0 +1,230 @@ +package fiattokenfactory + +import ( + "errors" + + fiattokenfactorykeeper "github.com/circlefin/noble-fiattokenfactory/x/fiattokenfactory/keeper" + fiattokenfactorytypes "github.com/circlefin/noble-fiattokenfactory/x/fiattokenfactory/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/bech32" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/cosmos-sdk/x/authz" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + transfertypes "github.com/cosmos/ibc-go/v4/modules/apps/transfer/types" +) + +type IsPausedDecorator struct { + fiatTokenFactory *fiattokenfactorykeeper.Keeper +} + +func NewIsPausedDecorator(ctf *fiattokenfactorykeeper.Keeper) IsPausedDecorator { + return IsPausedDecorator{ + fiatTokenFactory: ctf, + } +} + +func (ad IsPausedDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (newCtx sdk.Context, err error) { + msgs := tx.GetMsgs() + + err = ad.CheckMessages(ctx, msgs) + if err != nil { + return ctx, err + } + + return next(ctx, tx, simulate) +} + +func (ad IsPausedDecorator) CheckMessages(ctx sdk.Context, msgs []sdk.Msg) error { + for _, msg := range msgs { + if execMsg, ok := msg.(*authz.MsgExec); ok { + nestedMsgs, err := execMsg.GetMessages() + if err != nil { + return err + } + + return ad.CheckMessages(ctx, nestedMsgs) + } + + switch m := msg.(type) { + case *banktypes.MsgSend: + for _, c := range m.Amount { + paused, err := checkPausedStatebyTokenFactory(ctx, c, ad.fiatTokenFactory) + if paused { + return sdkerrors.Wrapf(err, "can not perform token transfers") + } + } + case *banktypes.MsgMultiSend: + for _, i := range m.Inputs { + for _, c := range i.Coins { + paused, err := checkPausedStatebyTokenFactory(ctx, c, ad.fiatTokenFactory) + if paused { + return sdkerrors.Wrapf(err, "can not perform token transfers") + } + } + } + case *transfertypes.MsgTransfer: + paused, err := checkPausedStatebyTokenFactory(ctx, m.Token, ad.fiatTokenFactory) + if paused { + return sdkerrors.Wrapf(err, "can not perform token transfers") + } + default: + continue + } + } + + return nil +} + +func checkPausedStatebyTokenFactory(ctx sdk.Context, c sdk.Coin, ctf *fiattokenfactorykeeper.Keeper) (bool, *sdkerrors.Error) { + ctfMintingDenom := ctf.GetMintingDenom(ctx) + if c.Denom == ctfMintingDenom.Denom { + paused := ctf.GetPaused(ctx) + if paused.Paused { + return true, fiattokenfactorytypes.ErrPaused + } + } + return false, nil +} + +type IsBlacklistedDecorator struct { + fiattokenfactory *fiattokenfactorykeeper.Keeper +} + +func NewIsBlacklistedDecorator(ctf *fiattokenfactorykeeper.Keeper) IsBlacklistedDecorator { + return IsBlacklistedDecorator{ + fiattokenfactory: ctf, + } +} + +func (ad IsBlacklistedDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (newCtx sdk.Context, err error) { + msgs := tx.GetMsgs() + + err = ad.CheckMessages(ctx, msgs, nil) + if err != nil { + return ctx, err + } + + return next(ctx, tx, simulate) +} + +func (ad IsBlacklistedDecorator) CheckMessages(ctx sdk.Context, msgs []sdk.Msg, grantee *string) error { + for _, msg := range msgs { + if execMsg, ok := msg.(*authz.MsgExec); ok { + nestedMsgs, err := execMsg.GetMessages() + if err != nil { + return err + } + + return ad.CheckMessages(ctx, nestedMsgs, &execMsg.Grantee) + } + + switch m := msg.(type) { + case *banktypes.MsgSend: + for _, c := range m.Amount { + if grantee != nil { + err := checkForBlacklistedAddressByTokenFactory(ctx, *grantee, c, ad.fiattokenfactory) + if errors.Is(err, fiattokenfactorytypes.ErrUnauthorized) { + return sdkerrors.Wrapf(err, "an address (%s) is blacklisted and can not receive tokens", *grantee) + } else if err != nil { + return sdkerrors.Wrapf(err, "error decoding address (%s)", *grantee) + } + } + + err := checkForBlacklistedAddressByTokenFactory(ctx, m.ToAddress, c, ad.fiattokenfactory) + if errors.Is(err, fiattokenfactorytypes.ErrUnauthorized) { + return sdkerrors.Wrapf(err, "an address (%s) is blacklisted and can not receive tokens", m.ToAddress) + } else if err != nil { + return sdkerrors.Wrapf(err, "error decoding address (%s)", m.ToAddress) + } + err = checkForBlacklistedAddressByTokenFactory(ctx, m.FromAddress, c, ad.fiattokenfactory) + if errors.Is(err, fiattokenfactorytypes.ErrUnauthorized) { + return sdkerrors.Wrapf(err, "an address (%s) is blacklisted and can not send tokens", m.FromAddress) + } else if err != nil { + return sdkerrors.Wrapf(err, "error decoding address (%s)", m.FromAddress) + } + } + case *banktypes.MsgMultiSend: + for _, i := range m.Inputs { + for _, c := range i.Coins { + if grantee != nil { + err := checkForBlacklistedAddressByTokenFactory(ctx, *grantee, c, ad.fiattokenfactory) + if errors.Is(err, fiattokenfactorytypes.ErrUnauthorized) { + return sdkerrors.Wrapf(err, "an address (%s) is blacklisted and can not receive tokens", *grantee) + } else if err != nil { + return sdkerrors.Wrapf(err, "error decoding address (%s)", *grantee) + } + } + + err := checkForBlacklistedAddressByTokenFactory(ctx, i.Address, c, ad.fiattokenfactory) + if errors.Is(err, fiattokenfactorytypes.ErrUnauthorized) { + return sdkerrors.Wrapf(err, "an address (%s) is blacklisted and can not send or receive tokens", i.Address) + } else if err != nil { + return sdkerrors.Wrapf(err, "error decoding address (%s)", i.Address) + } + } + } + for _, o := range m.Outputs { + for _, c := range o.Coins { + if grantee != nil { + err := checkForBlacklistedAddressByTokenFactory(ctx, *grantee, c, ad.fiattokenfactory) + if errors.Is(err, fiattokenfactorytypes.ErrUnauthorized) { + return sdkerrors.Wrapf(err, "an address (%s) is blacklisted and can not receive tokens", *grantee) + } else if err != nil { + return sdkerrors.Wrapf(err, "error decoding address (%s)", *grantee) + } + } + + err := checkForBlacklistedAddressByTokenFactory(ctx, o.Address, c, ad.fiattokenfactory) + if errors.Is(err, fiattokenfactorytypes.ErrUnauthorized) { + return sdkerrors.Wrapf(err, "an address (%s) is blacklisted and can not send or receive tokens", o.Address) + } else if err != nil { + return sdkerrors.Wrapf(err, "error decoding address (%s)", o.Address) + } + } + } + case *transfertypes.MsgTransfer: + if grantee != nil { + err := checkForBlacklistedAddressByTokenFactory(ctx, *grantee, m.Token, ad.fiattokenfactory) + if errors.Is(err, fiattokenfactorytypes.ErrUnauthorized) { + return sdkerrors.Wrapf(err, "an address (%s) is blacklisted and can not receive tokens", *grantee) + } else if err != nil { + return sdkerrors.Wrapf(err, "error decoding address (%s)", *grantee) + } + } + + err := checkForBlacklistedAddressByTokenFactory(ctx, m.Sender, m.Token, ad.fiattokenfactory) + if errors.Is(err, fiattokenfactorytypes.ErrUnauthorized) { + return sdkerrors.Wrapf(err, "an address (%s) is blacklisted and can not send tokens", m.Sender) + } else if err != nil { + return sdkerrors.Wrapf(err, "error decoding address (%s)", m.Sender) + } + err = checkForBlacklistedAddressByTokenFactory(ctx, m.Receiver, m.Token, ad.fiattokenfactory) + if errors.Is(err, fiattokenfactorytypes.ErrUnauthorized) { + return sdkerrors.Wrapf(err, "an address (%s) is blacklisted and can not receive tokens", m.Receiver) + } else if err != nil { + return sdkerrors.Wrapf(err, "error decoding address (%s)", m.Receiver) + } + default: + continue + } + } + + return nil +} + +// checkForBlacklistedAddressByTokenFactory first checks if the denom being transacted is a mintable asset from a TokenFactory, +// if it is, it checks if the address involved in the tx is blacklisted by that specific TokenFactory. +func checkForBlacklistedAddressByTokenFactory(ctx sdk.Context, address string, c sdk.Coin, ctf *fiattokenfactorykeeper.Keeper) error { + ctfMintingDenom := ctf.GetMintingDenom(ctx) + if c.Denom == ctfMintingDenom.Denom { + _, addressBz, err := bech32.DecodeAndConvert(address) + if err != nil { + return err + } + _, found := ctf.GetBlacklisted(ctx, addressBz) + if found { + return fiattokenfactorytypes.ErrUnauthorized + } + } + return nil +}