diff --git a/app/app.go b/app/app.go index db3a838d..906d2f96 100644 --- a/app/app.go +++ b/app/app.go @@ -707,7 +707,7 @@ func NewArchwayApp( rewards.NewAppModule(app.appCodec, app.Keepers.RewardsKeeper), cwfees.NewAppModule(app.Keepers.CWFeesKeeper), genmsg.NewAppModule(app.MsgServiceRouter()), - callback.NewAppModule(app.appCodec, app.Keepers.CallbackKeeper, app.Keepers.WASMKeeper), + callback.NewAppModule(app.appCodec, app.Keepers.CallbackKeeper, app.Keepers.WASMKeeper, app.Keepers.CWErrorsKeeper), cwica.NewAppModule(appCodec, app.Keepers.CWICAKeeper, app.Keepers.AccountKeeper), cwerrors.NewAppModule(app.appCodec, app.Keepers.CWErrorsKeeper, app.Keepers.WASMKeeper), crisis.NewAppModule(&app.Keepers.CrisisKeeper, skipGenesisInvariants, app.getSubspace(crisistypes.ModuleName)), // always be last to make sure that it checks for all invariants and not only part of them diff --git a/proto/archway/callback/v1/errors.proto b/proto/archway/callback/v1/errors.proto new file mode 100644 index 00000000..17a5fc9c --- /dev/null +++ b/proto/archway/callback/v1/errors.proto @@ -0,0 +1,14 @@ +syntax = "proto3"; +package archway.callback.v1; + +import "gogoproto/gogo.proto"; + +option go_package = "github.com/archway-network/archway/x/callback/types"; + +// ModuleErrors defines the module level error codes +enum ModuleErrors { + // ERR_UNKNOWN is the default error code + ERR_UNKNOWN = 0; + // ERR_OUT_OF_GAS is the error code when the contract callback exceeds the gas limit allowed by the module + ERR_OUT_OF_GAS = 1; +} \ No newline at end of file diff --git a/x/callback/abci.go b/x/callback/abci.go index adef4dca..685916dd 100644 --- a/x/callback/abci.go +++ b/x/callback/abci.go @@ -1,9 +1,12 @@ package callback import ( + "errors" + "cosmossdk.io/collections" abci "github.com/cometbft/cometbft/abci/types" sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" "github.com/archway-network/archway/pkg" "github.com/archway-network/archway/x/callback/keeper" @@ -11,13 +14,13 @@ import ( ) // EndBlocker fetches all the callbacks registered for the current block height and executes them -func EndBlocker(ctx sdk.Context, k keeper.Keeper, wk types.WasmKeeperExpected) []abci.ValidatorUpdate { - k.IterateCallbacksByHeight(ctx, ctx.BlockHeight(), callbackExec(ctx, k, wk)) +func EndBlocker(ctx sdk.Context, k keeper.Keeper, wk types.WasmKeeperExpected, ek types.ErrorsKeeperExpected) []abci.ValidatorUpdate { + k.IterateCallbacksByHeight(ctx, ctx.BlockHeight(), callbackExec(ctx, k, wk, ek)) return nil } // callbackExec returns a function which executes the callback and deletes it from state after execution -func callbackExec(ctx sdk.Context, k keeper.Keeper, wk types.WasmKeeperExpected) func(types.Callback) bool { +func callbackExec(ctx sdk.Context, k keeper.Keeper, wk types.WasmKeeperExpected, ek types.ErrorsKeeperExpected) func(types.Callback) bool { logger := k.Logger(ctx) return func(callback types.Callback) bool { // creating CallbackMsg which is encoded to json and passed as input to contract execution @@ -53,12 +56,32 @@ func callbackExec(ctx sdk.Context, k keeper.Keeper, wk types.WasmKeeperExpected) err.Error(), ) + errorCode := types.ModuleErrors_ERR_UNKNOWN + // check if out of gas error + var outOfGasError sdkerrors.Error + if errors.As(err, &outOfGasError) && outOfGasError.Is(sdkerrors.ErrOutOfGas) { + errorCode = types.ModuleErrors_ERR_OUT_OF_GAS + } + + // Save error in the errors keeper + sudoErr := types.NewSudoError( + errorCode, + callback.ContractAddress, + callbackMsgString, + err.Error(), + ) + err := ek.SetError(ctx, sudoErr) + if err != nil { + panic(err) + } + // This is because gasUsed amount returned is greater than the gas limit. cuz ofc. // so we set it to callback.MaxGasLimit so when we do txFee refund, we arent trying to refund more than we should // e.g if callback.MaxGasLimit is 10, but gasUsed is 100, we need to use 10 to calculate txFeeRefund. // else the module will pay back more than it took from the user 💀 // TLDR; this ensures in case of "out of gas error", we keep all txFees and refund nothing. gasUsed = callback.MaxGasLimit + } else { logger.Info( "callback executed successfully", diff --git a/x/callback/abci_test.go b/x/callback/abci_test.go index ef52dd15..8821c39b 100644 --- a/x/callback/abci_test.go +++ b/x/callback/abci_test.go @@ -87,7 +87,7 @@ func TestEndBlocker(t *testing.T) { // Increment block height and run end blocker ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 1) require.Equal(t, ctx.BlockHeight(), reqMsg.CallbackHeight) - _ = callbackabci.EndBlocker(ctx, keeper, chain.GetApp().Keepers.WASMKeeper) + _ = callbackabci.EndBlocker(ctx, keeper, chain.GetApp().Keepers.WASMKeeper, chain.GetApp().Keepers.CWErrorsKeeper) // Checking if the count value is as expected count := getCount(t, chain, ctx, contractAddr) @@ -118,7 +118,7 @@ func TestEndBlocker(t *testing.T) { // Increment block height and run end blocker ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 1) - _ = callbackabci.EndBlocker(ctx, keeper, chain.GetApp().Keepers.WASMKeeper) + _ = callbackabci.EndBlocker(ctx, keeper, chain.GetApp().Keepers.WASMKeeper, chain.GetApp().Keepers.CWErrorsKeeper) // Checking if the count value has incremented. // Should have incremented as the callback should have access to higher gas limit as it was registered before the gas limit was reduced @@ -140,7 +140,7 @@ func TestEndBlocker(t *testing.T) { // Increment block height and run end blocker ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 1) - _ = callbackabci.EndBlocker(ctx, keeper, chain.GetApp().Keepers.WASMKeeper) + _ = callbackabci.EndBlocker(ctx, keeper, chain.GetApp().Keepers.WASMKeeper, chain.GetApp().Keepers.CWErrorsKeeper) // Checking if the count value has incremented. Should not have incremented as the callback failed due to out of gas error count = getCount(t, chain, ctx, contractAddr) diff --git a/x/callback/module.go b/x/callback/module.go index 03169ff2..05a994ef 100644 --- a/x/callback/module.go +++ b/x/callback/module.go @@ -80,16 +80,18 @@ func (a AppModuleBasic) GetQueryCmd() *cobra.Command { type AppModule struct { AppModuleBasic - keeper keeper.Keeper - wasmKeeper types.WasmKeeperExpected + keeper keeper.Keeper + wasmKeeper types.WasmKeeperExpected + errorsKeeper types.ErrorsKeeperExpected } // NewAppModule creates a new AppModule object. -func NewAppModule(cdc codec.Codec, keeper keeper.Keeper, wk types.WasmKeeperExpected) AppModule { +func NewAppModule(cdc codec.Codec, keeper keeper.Keeper, wk types.WasmKeeperExpected, ek types.ErrorsKeeperExpected) AppModule { return AppModule{ AppModuleBasic: AppModuleBasic{cdc: cdc}, keeper: keeper, wasmKeeper: wk, + errorsKeeper: ek, } } @@ -129,5 +131,5 @@ func (a AppModule) BeginBlock(ctx sdk.Context, block abci.RequestBeginBlock) {} // EndBlock returns the end blocker for the module. It returns no validator updates. func (a AppModule) EndBlock(ctx sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate { - return EndBlocker(ctx, a.keeper, a.wasmKeeper) + return EndBlocker(ctx, a.keeper, a.wasmKeeper, a.errorsKeeper) } diff --git a/x/callback/types/errors.go b/x/callback/types/errors.go index 29ef2a2e..1d4ca5c3 100644 --- a/x/callback/types/errors.go +++ b/x/callback/types/errors.go @@ -1,6 +1,10 @@ package types -import errorsmod "cosmossdk.io/errors" +import ( + errorsmod "cosmossdk.io/errors" + + cwerrortypes "github.com/archway-network/archway/x/cwerrors/types" +) var ( DefaultCodespace = ModuleName @@ -14,3 +18,14 @@ var ( ErrCallbackHeightTooFarInFuture = errorsmod.Register(DefaultCodespace, 9, "callback request height is too far in the future") ErrBlockFilled = errorsmod.Register(DefaultCodespace, 10, "block filled with max capacity of callbacks") ) + +// NewSudoError creates a new sudo error instance to pass on to the errors module +func NewSudoError(errorCode ModuleErrors, contractAddr string, inputPayload string, errMsg string) cwerrortypes.SudoError { + return cwerrortypes.SudoError{ + ModuleName: ModuleName, + ErrorCode: uint32(errorCode), + ContractAddress: contractAddr, + InputPayload: inputPayload, + ErrorMessage: errMsg, + } +} diff --git a/x/callback/types/errors.pb.go b/x/callback/types/errors.pb.go new file mode 100644 index 00000000..48bd4e1c --- /dev/null +++ b/x/callback/types/errors.pb.go @@ -0,0 +1,73 @@ +// Code generated by protoc-gen-gogo. DO NOT EDIT. +// source: archway/callback/v1/errors.proto + +package types + +import ( + fmt "fmt" + _ "github.com/cosmos/gogoproto/gogoproto" + proto "github.com/cosmos/gogoproto/proto" + math "math" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package + +// ModuleErrors defines the module level error codes +type ModuleErrors int32 + +const ( + // ERR_UNKNOWN is the default error code + ModuleErrors_ERR_UNKNOWN ModuleErrors = 0 + // ERR_OUT_OF_GAS is the error code when the contract callback exceeds the gas limit allowed by the module + ModuleErrors_ERR_OUT_OF_GAS ModuleErrors = 1 +) + +var ModuleErrors_name = map[int32]string{ + 0: "ERR_UNKNOWN", + 1: "ERR_OUT_OF_GAS", +} + +var ModuleErrors_value = map[string]int32{ + "ERR_UNKNOWN": 0, + "ERR_OUT_OF_GAS": 1, +} + +func (x ModuleErrors) String() string { + return proto.EnumName(ModuleErrors_name, int32(x)) +} + +func (ModuleErrors) EnumDescriptor() ([]byte, []int) { + return fileDescriptor_f0078bfce91cddb8, []int{0} +} + +func init() { + proto.RegisterEnum("archway.callback.v1.ModuleErrors", ModuleErrors_name, ModuleErrors_value) +} + +func init() { proto.RegisterFile("archway/callback/v1/errors.proto", fileDescriptor_f0078bfce91cddb8) } + +var fileDescriptor_f0078bfce91cddb8 = []byte{ + // 196 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x52, 0x48, 0x2c, 0x4a, 0xce, + 0x28, 0x4f, 0xac, 0xd4, 0x4f, 0x4e, 0xcc, 0xc9, 0x49, 0x4a, 0x4c, 0xce, 0xd6, 0x2f, 0x33, 0xd4, + 0x4f, 0x2d, 0x2a, 0xca, 0x2f, 0x2a, 0xd6, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x12, 0x86, 0xaa, + 0xd0, 0x83, 0xa9, 0xd0, 0x2b, 0x33, 0x94, 0x12, 0x49, 0xcf, 0x4f, 0xcf, 0x07, 0xcb, 0xeb, 0x83, + 0x58, 0x10, 0xa5, 0x5a, 0xc6, 0x5c, 0x3c, 0xbe, 0xf9, 0x29, 0xa5, 0x39, 0xa9, 0xae, 0x60, 0x03, + 0x84, 0xf8, 0xb9, 0xb8, 0x5d, 0x83, 0x82, 0xe2, 0x43, 0xfd, 0xbc, 0xfd, 0xfc, 0xc3, 0xfd, 0x04, + 0x18, 0x84, 0x84, 0xb8, 0xf8, 0x40, 0x02, 0xfe, 0xa1, 0x21, 0xf1, 0xfe, 0x6e, 0xf1, 0xee, 0x8e, + 0xc1, 0x02, 0x8c, 0x4e, 0xbe, 0x27, 0x1e, 0xc9, 0x31, 0x5e, 0x78, 0x24, 0xc7, 0xf8, 0xe0, 0x91, + 0x1c, 0xe3, 0x84, 0xc7, 0x72, 0x0c, 0x17, 0x1e, 0xcb, 0x31, 0xdc, 0x78, 0x2c, 0xc7, 0x10, 0x65, + 0x9c, 0x9e, 0x59, 0x92, 0x51, 0x9a, 0xa4, 0x97, 0x9c, 0x9f, 0xab, 0x0f, 0x75, 0x84, 0x6e, 0x5e, + 0x6a, 0x49, 0x79, 0x7e, 0x51, 0x36, 0x8c, 0xaf, 0x5f, 0x81, 0x70, 0x78, 0x49, 0x65, 0x41, 0x6a, + 0x71, 0x12, 0x1b, 0xd8, 0x29, 0xc6, 0x80, 0x00, 0x00, 0x00, 0xff, 0xff, 0x31, 0xaf, 0xa9, 0x06, + 0xd9, 0x00, 0x00, 0x00, +} diff --git a/x/callback/types/expected_keepers.go b/x/callback/types/expected_keepers.go index d0caac37..db2046a9 100644 --- a/x/callback/types/expected_keepers.go +++ b/x/callback/types/expected_keepers.go @@ -3,6 +3,7 @@ package types import ( wasmdtypes "github.com/CosmWasm/wasmd/x/wasm/types" + cwerrortypes "github.com/archway-network/archway/x/cwerrors/types" rewardstypes "github.com/archway-network/archway/x/rewards/types" sdk "github.com/cosmos/cosmos-sdk/types" @@ -25,3 +26,7 @@ type BankKeeperExpected interface { SendCoinsFromModuleToModule(ctx sdk.Context, senderModule, recipientModule string, amt sdk.Coins) error BlockedAddr(addr sdk.AccAddress) bool } + +type ErrorsKeeperExpected interface { + SetError(ctx sdk.Context, sudoErr cwerrortypes.SudoError) error +}