diff --git a/app/helpers/contracts.go b/app/helpers/contracts.go new file mode 100644 index 000000000..d4023e694 --- /dev/null +++ b/app/helpers/contracts.go @@ -0,0 +1,38 @@ +package helpers + +import ( + wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" + + storetypes "github.com/cosmos/cosmos-sdk/store/types" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// Execute contract, recover from panic +func ExecuteContract(k wasmtypes.ContractOpsKeeper, childCtx sdk.Context, contractAddr sdk.AccAddress, msgBz []byte, err *error) { + // Recover from panic, return error + defer func() { + if recoveryError := recover(); recoveryError != nil { + // Determine error associated with panic + if isOutofGas, msg := IsOutOfGasError(recoveryError); isOutofGas { + *err = ErrOutOfGas.Wrapf("%s", msg) + } else { + *err = ErrContractExecutionPanic.Wrapf("%s", recoveryError) + } + } + }() + + // Execute contract with sudo + _, *err = k.Sudo(childCtx, contractAddr, msgBz) +} + +// Check if error is out of gas error +func IsOutOfGasError(err any) (bool, string) { + switch e := err.(type) { + case storetypes.ErrorOutOfGas: + return true, e.Descriptor + case storetypes.ErrorGasOverflow: + return true, e.Descriptor + default: + return false, "" + } +} diff --git a/app/helpers/global_errors.go b/app/helpers/global_errors.go new file mode 100644 index 000000000..385ddb54e --- /dev/null +++ b/app/helpers/global_errors.go @@ -0,0 +1,20 @@ +package helpers + +import ( + errorsmod "cosmossdk.io/errors" + + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +const codespace = "juno-global" + +var ( + ErrInvalidAddress = sdkerrors.ErrInvalidAddress + ErrContractNotRegistered = errorsmod.Register(codespace, 1, "contract not registered") + ErrContractAlreadyRegistered = errorsmod.Register(codespace, 2, "contract already registered") + ErrContractNotAdmin = errorsmod.Register(codespace, 3, "sender is not the contract admin") + ErrContractNotCreator = errorsmod.Register(codespace, 4, "sender is not the contract creator") + ErrInvalidCWContract = errorsmod.Register(codespace, 5, "invalid CosmWasm contract") + ErrOutOfGas = errorsmod.Register(codespace, 6, "contract execution ran out of gas") + ErrContractExecutionPanic = errorsmod.Register(codespace, 7, "contract execution panicked") +) diff --git a/interchaintest/contracts/README.md b/interchaintest/contracts/README.md index 47014aee6..65d80cf18 100644 --- a/interchaintest/contracts/README.md +++ b/interchaintest/contracts/README.md @@ -8,7 +8,11 @@ A list of the contracts here which are pre-compiled in other repos. > ibchooks_counter.wasm -> > cw_testburn.wasm -> > clock_example.wasm -> +> clock_example_high_gas.wasm -> +> clock_example_migrate.wasm -> +> clock_example_no_sudo.wasm -> > juno_staking_hooks_example.wasm -> +> juno_staking_hooks_high_gas_example.wasm -> > cw721_base - https://github.com/CosmWasm/cw-nfts/releases/download/v0.17.0/cw721_base.wasm > cw721-receiver - https://github.com/CosmWasm/cw-nfts/pull/144 \ No newline at end of file diff --git a/interchaintest/contracts/juno_staking_hooks_high_gas_example.wasm b/interchaintest/contracts/juno_staking_hooks_high_gas_example.wasm new file mode 100644 index 000000000..4c5d90df8 Binary files /dev/null and b/interchaintest/contracts/juno_staking_hooks_high_gas_example.wasm differ diff --git a/interchaintest/module_cwhooks_test.go b/interchaintest/module_cwhooks_test.go index 5ed0284be..2bba0b94e 100644 --- a/interchaintest/module_cwhooks_test.go +++ b/interchaintest/module_cwhooks_test.go @@ -57,6 +57,25 @@ func TestJunoCwHooks(t *testing.T) { require.Equal(t, user.FormattedAddress(), res.Data.DelegatorAddress) require.Equal(t, fmt.Sprintf("%d.000000000000000000", stakeAmt), res.Data.Shares) + // HIGH GAS TEST + // Setup a high gas contract + _, contractAddr = helpers.SetupContract(t, ctx, juno, user.KeyName(), "contracts/juno_staking_hooks_high_gas_example.wasm", `{}`) + + // Register staking contract + helpers.RegisterCwHooksStaking(t, ctx, juno, user, contractAddr) + sc = helpers.GetCwHooksStakingContracts(t, ctx, juno) + require.Equal(t, 2, len(sc)) + + // Perform a Staking Action + stakeAmt = 1_000_000 + helpers.StakeTokens(t, ctx, juno, user, valoper, fmt.Sprintf("%d%s", stakeAmt, juno.Config().Denom)) + + // Query the smart contract, should panic and not update value + res = helpers.GetCwStakingHookLastDelegationChange(t, ctx, juno, contractAddr, user.FormattedAddress()) + require.Equal(t, "", res.Data.ValidatorAddress) + require.Equal(t, "", res.Data.DelegatorAddress) + require.Equal(t, "", res.Data.Shares) + t.Cleanup(func() { _ = ic.Close() }) diff --git a/x/clock/abci.go b/x/clock/abci.go index 6679b5e5a..ca15e1489 100644 --- a/x/clock/abci.go +++ b/x/clock/abci.go @@ -5,10 +5,10 @@ import ( "github.com/cometbft/cometbft/libs/log" - storetypes "github.com/cosmos/cosmos-sdk/store/types" "github.com/cosmos/cosmos-sdk/telemetry" sdk "github.com/cosmos/cosmos-sdk/types" + helpers "github.com/CosmosContracts/juno/v19/app/helpers" "github.com/CosmosContracts/juno/v19/x/clock/keeper" "github.com/CosmosContracts/juno/v19/x/clock/types" ) @@ -51,7 +51,7 @@ func EndBlocker(ctx sdk.Context, k keeper.Keeper) { childCtx := ctx.WithGasMeter(sdk.NewGasMeter(p.ContractGasLimit)) // Execute contract - executeContract(k, childCtx, contractAddr, &err) + helpers.ExecuteContract(k.GetContractKeeper(), childCtx, contractAddr, endBlockSudoMessage, &err) if handleError(ctx, k, logger, errorExecs, &errorExists, err, idx, contract.ContractAddress) { continue } @@ -90,33 +90,3 @@ func handleError( return err != nil } - -// Execute contract, recover from panic -func executeContract(k keeper.Keeper, childCtx sdk.Context, contractAddr sdk.AccAddress, err *error) { - // Recover from panic, return error - defer func() { - if recoveryError := recover(); recoveryError != nil { - // Determine error associated with panic - if isOutofGas, msg := isOutOfGasError(recoveryError); isOutofGas { - *err = types.ErrOutOfGas.Wrapf("%s", msg) - } else { - *err = types.ErrContractExecutionPanic.Wrapf("%s", recoveryError) - } - } - }() - - // Execute contract with sudo - _, *err = k.GetContractKeeper().Sudo(childCtx, contractAddr, endBlockSudoMessage) -} - -// Check if error is out of gas error -func isOutOfGasError(err any) (bool, string) { - switch e := err.(type) { - case storetypes.ErrorOutOfGas: - return true, e.Descriptor - case storetypes.ErrorGasOverflow: - return true, e.Descriptor - default: - return false, "" - } -} diff --git a/x/clock/keeper/clock.go b/x/clock/keeper/clock.go index 0519ad08d..d646b231c 100644 --- a/x/clock/keeper/clock.go +++ b/x/clock/keeper/clock.go @@ -5,6 +5,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/query" + globalerrors "github.com/CosmosContracts/juno/v19/app/helpers" "github.com/CosmosContracts/juno/v19/x/clock/types" ) @@ -42,7 +43,7 @@ func (k Keeper) IsClockContract(ctx sdk.Context, contractAddress string) bool { func (k Keeper) GetClockContract(ctx sdk.Context, contractAddress string) (*types.ClockContract, error) { // Check if the contract is registered if !k.IsClockContract(ctx, contractAddress) { - return nil, types.ErrContractNotRegistered + return nil, globalerrors.ErrContractNotRegistered } // Get the KV store @@ -134,7 +135,7 @@ func (k Keeper) RemoveContract(ctx sdk.Context, contractAddress string) { func (k Keeper) RegisterContract(ctx sdk.Context, senderAddress string, contractAddress string) error { // Check if the contract is already registered if k.IsClockContract(ctx, contractAddress) { - return types.ErrContractAlreadyRegistered + return globalerrors.ErrContractAlreadyRegistered } // Ensure the sender is the contract admin or creator @@ -153,7 +154,7 @@ func (k Keeper) RegisterContract(ctx sdk.Context, senderAddress string, contract func (k Keeper) UnregisterContract(ctx sdk.Context, senderAddress string, contractAddress string) error { // Check if the contract is registered in either store if !k.IsClockContract(ctx, contractAddress) { - return types.ErrContractNotRegistered + return globalerrors.ErrContractNotRegistered } // Ensure the sender is the contract admin or creator @@ -208,7 +209,7 @@ func (k Keeper) IsContractManager(ctx sdk.Context, senderAddress string, contrac // Ensure the contract is a cosm wasm contract if ok := k.wasmKeeper.HasContractInfo(ctx, contractAddr); !ok { - return false, types.ErrInvalidCWContract + return false, globalerrors.ErrInvalidCWContract } // Get the contract info @@ -221,9 +222,9 @@ func (k Keeper) IsContractManager(ctx sdk.Context, senderAddress string, contrac // Check if the sender is the admin or creator if adminExists && !isSenderAdmin { - return false, types.ErrContractNotAdmin + return false, globalerrors.ErrContractNotAdmin } else if !adminExists && !isSenderCreator { - return false, types.ErrContractNotCreator + return false, globalerrors.ErrContractNotCreator } return true, nil diff --git a/x/clock/keeper/querier.go b/x/clock/keeper/querier.go index 48b4bc642..83f1414f9 100644 --- a/x/clock/keeper/querier.go +++ b/x/clock/keeper/querier.go @@ -5,6 +5,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" + globalerrors "github.com/CosmosContracts/juno/v19/app/helpers" "github.com/CosmosContracts/juno/v19/x/clock/types" ) @@ -38,7 +39,7 @@ func (q Querier) ClockContract(stdCtx context.Context, req *types.QueryClockCont // Ensure the contract address is valid if _, err := sdk.AccAddressFromBech32(req.ContractAddress); err != nil { - return nil, types.ErrInvalidAddress + return nil, globalerrors.ErrInvalidAddress } contract, err := q.keeper.GetClockContract(ctx, req.ContractAddress) diff --git a/x/clock/types/errors.go b/x/clock/types/errors.go index ea3adc307..d8d05404b 100644 --- a/x/clock/types/errors.go +++ b/x/clock/types/errors.go @@ -2,21 +2,10 @@ package types import ( errorsmod "cosmossdk.io/errors" - - sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" ) var ( - ErrInvalidAddress = sdkerrors.ErrInvalidAddress - ErrContractNotRegistered = errorsmod.Register(ModuleName, 1, "contract not registered") - ErrContractAlreadyRegistered = errorsmod.Register(ModuleName, 2, "contract already registered") - ErrContractRegisterNotAdmin = errorsmod.Register(ModuleName, 3, "this address is not the contract admin, cannot register") - ErrContractNotAdmin = errorsmod.Register(ModuleName, 4, "sender is not the contract admin") - ErrContractNotCreator = errorsmod.Register(ModuleName, 5, "sender is not the contract creator") - ErrContractJailed = errorsmod.Register(ModuleName, 6, "contract is jailed") - ErrContractNotJailed = errorsmod.Register(ModuleName, 7, "contract is not jailed") - ErrContractAlreadyJailed = errorsmod.Register(ModuleName, 8, "contract is already jailed") - ErrInvalidCWContract = errorsmod.Register(ModuleName, 9, "invalid CosmWasm contract") - ErrOutOfGas = errorsmod.Register(ModuleName, 10, "contract execution ran out of gas") - ErrContractExecutionPanic = errorsmod.Register(ModuleName, 11, "contract execution panicked") + ErrContractJailed = errorsmod.Register(ModuleName, 1, "contract is jailed") + ErrContractNotJailed = errorsmod.Register(ModuleName, 2, "contract is not jailed") + ErrContractAlreadyJailed = errorsmod.Register(ModuleName, 3, "contract is already jailed") ) diff --git a/x/clock/types/msgs.go b/x/clock/types/msgs.go index b94c60a44..4f8569ecf 100644 --- a/x/clock/types/msgs.go +++ b/x/clock/types/msgs.go @@ -4,6 +4,8 @@ import ( "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" + + globalerrors "github.com/CosmosContracts/juno/v19/app/helpers" ) const ( @@ -133,7 +135,7 @@ func (msg *MsgUpdateParams) ValidateBasic() error { func validateAddresses(addresses ...string) error { for _, address := range addresses { if _, err := sdk.AccAddressFromBech32(address); err != nil { - return errors.Wrapf(ErrInvalidAddress, "invalid address: %s", address) + return errors.Wrapf(globalerrors.ErrInvalidAddress, "invalid address: %s", address) } } diff --git a/x/cw-hooks/keeper/contracts.go b/x/cw-hooks/keeper/contracts.go index b86188680..421365f88 100644 --- a/x/cw-hooks/keeper/contracts.go +++ b/x/cw-hooks/keeper/contracts.go @@ -3,6 +3,8 @@ package keeper import ( "github.com/cosmos/cosmos-sdk/store/prefix" sdk "github.com/cosmos/cosmos-sdk/types" + + helpers "github.com/CosmosContracts/juno/v19/app/helpers" ) func (k Keeper) SetContract(ctx sdk.Context, keyPrefix []byte, contractAddr sdk.AccAddress) { @@ -63,7 +65,10 @@ func (k Keeper) ExecuteMessageOnContracts(ctx sdk.Context, keyPrefix []byte, msg for _, c := range k.GetAllContracts(ctx, keyPrefix) { gasLimitCtx := ctx.WithGasMeter(sdk.NewGasMeter(p.ContractGasLimit)) addr := sdk.AccAddress(c.Bytes()) - if _, err := k.GetContractKeeper().Sudo(gasLimitCtx, addr, msgBz); err != nil { + + var err error + helpers.ExecuteContract(k.GetContractKeeper(), gasLimitCtx, addr, msgBz, &err) + if err != nil { k.Logger(ctx).Error("ExecuteMessageOnContracts err", err, "contract", addr.String()) return err } diff --git a/x/feepay/keeper/feepay.go b/x/feepay/keeper/feepay.go index 609d8a7a9..e4feed9f1 100644 --- a/x/feepay/keeper/feepay.go +++ b/x/feepay/keeper/feepay.go @@ -9,6 +9,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/query" + globalerrors "github.com/CosmosContracts/juno/v19/app/helpers" "github.com/CosmosContracts/juno/v19/x/feepay/types" ) @@ -22,7 +23,7 @@ func (k Keeper) IsContractRegistered(ctx sdk.Context, contractAddr string) bool func (k Keeper) GetContract(ctx sdk.Context, contractAddress string) (*types.FeePayContract, error) { // Return nil, contract not registered if !k.IsContractRegistered(ctx, contractAddress) { - return nil, types.ErrContractNotRegistered + return nil, globalerrors.ErrContractNotRegistered } store := prefix.NewStore(ctx.KVStore(k.storeKey), StoreKeyContracts) @@ -92,7 +93,7 @@ func (k Keeper) GetAllContracts(ctx sdk.Context) []types.FeePayContract { func (k Keeper) RegisterContract(ctx sdk.Context, rfp *types.MsgRegisterFeePayContract) error { // Return false because the contract was already registered if k.IsContractRegistered(ctx, rfp.FeePayContract.ContractAddress) { - return types.ErrContractAlreadyRegistered + return globalerrors.ErrContractAlreadyRegistered } // Check if sender is the owner of the cw contract @@ -102,7 +103,7 @@ func (k Keeper) RegisterContract(ctx sdk.Context, rfp *types.MsgRegisterFeePayCo } if ok := k.wasmKeeper.HasContractInfo(ctx, contractAddr); !ok { - return types.ErrInvalidCWContract + return globalerrors.ErrInvalidCWContract } // Get the contract owner @@ -141,7 +142,7 @@ func (k Keeper) UnregisterContract(ctx sdk.Context, rfp *types.MsgUnregisterFeeP // Ensure CW contract is valid if ok := k.wasmKeeper.HasContractInfo(ctx, contractAddr); !ok { - return types.ErrInvalidCWContract + return globalerrors.ErrInvalidCWContract } // Get the contract info @@ -278,7 +279,7 @@ func (k Keeper) UpdateContractWalletLimit(ctx sdk.Context, fpc *types.FeePayCont } if ok := k.wasmKeeper.HasContractInfo(ctx, contractAddr); !ok { - return types.ErrInvalidCWContract + return globalerrors.ErrInvalidCWContract } // Get the contract info & ensure sender is the manager @@ -316,9 +317,9 @@ func (k Keeper) IsContractManager(senderAddress string, contractInfo *wasmtypes. isSenderCreator := contractInfo.Creator == senderAddress if adminExists && !isSenderAdmin { - return false, types.ErrContractNotAdmin + return false, globalerrors.ErrContractNotAdmin } else if !adminExists && !isSenderCreator { - return false, types.ErrContractNotCreator + return false, globalerrors.ErrContractNotCreator } return true, nil diff --git a/x/feepay/keeper/msg_server.go b/x/feepay/keeper/msg_server.go index 2fa864a4b..87598cf2b 100644 --- a/x/feepay/keeper/msg_server.go +++ b/x/feepay/keeper/msg_server.go @@ -8,6 +8,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + globalerrors "github.com/CosmosContracts/juno/v19/app/helpers" "github.com/CosmosContracts/juno/v19/x/feepay/types" ) @@ -39,7 +40,7 @@ func (k Keeper) FundFeePayContract(goCtx context.Context, msg *types.MsgFundFeeP // Validate sender address senderAddr, err := sdk.AccAddressFromBech32(msg.SenderAddress) if err != nil { - return nil, errorsmod.Wrapf(types.ErrInvalidAddress, "invalid sender address: %s", msg.SenderAddress) + return nil, errorsmod.Wrapf(globalerrors.ErrInvalidAddress, "invalid sender address: %s", msg.SenderAddress) } return &types.MsgFundFeePayContractResponse{}, k.FundContract(ctx, contract, senderAddr, msg.Amount) diff --git a/x/feepay/keeper/querier.go b/x/feepay/keeper/querier.go index bdf60c875..ff5d1b66e 100644 --- a/x/feepay/keeper/querier.go +++ b/x/feepay/keeper/querier.go @@ -5,6 +5,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" + globalerrors "github.com/CosmosContracts/juno/v19/app/helpers" "github.com/CosmosContracts/juno/v19/x/feepay/types" ) @@ -24,7 +25,7 @@ func NewQuerier(k Keeper) Querier { func (q Querier) FeePayContract(ctx context.Context, req *types.QueryFeePayContract) (*types.QueryFeePayContractResponse, error) { // Check if contract address are valid if _, err := sdk.AccAddressFromBech32(req.ContractAddress); err != nil { - return nil, types.ErrInvalidAddress + return nil, globalerrors.ErrInvalidAddress } sdkCtx := sdk.UnwrapSDKContext(ctx) @@ -55,11 +56,11 @@ func (q Querier) FeePayContracts(ctx context.Context, req *types.QueryFeePayCont func (q Querier) FeePayContractUses(ctx context.Context, req *types.QueryFeePayContractUses) (*types.QueryFeePayContractUsesResponse, error) { // Check if wallet & contract address are valid if _, err := sdk.AccAddressFromBech32(req.ContractAddress); err != nil { - return nil, types.ErrInvalidAddress + return nil, globalerrors.ErrInvalidAddress } if _, err := sdk.AccAddressFromBech32(req.WalletAddress); err != nil { - return nil, types.ErrInvalidAddress + return nil, globalerrors.ErrInvalidAddress } sdkCtx := sdk.UnwrapSDKContext(ctx) @@ -84,11 +85,11 @@ func (q Querier) FeePayContractUses(ctx context.Context, req *types.QueryFeePayC func (q Querier) FeePayWalletIsEligible(ctx context.Context, req *types.QueryFeePayWalletIsEligible) (*types.QueryFeePayWalletIsEligibleResponse, error) { // Check if wallet & contract address are valid if _, err := sdk.AccAddressFromBech32(req.ContractAddress); err != nil { - return nil, types.ErrInvalidAddress + return nil, globalerrors.ErrInvalidAddress } if _, err := sdk.AccAddressFromBech32(req.WalletAddress); err != nil { - return nil, types.ErrInvalidAddress + return nil, globalerrors.ErrInvalidAddress } sdkCtx := sdk.UnwrapSDKContext(ctx) diff --git a/x/feepay/types/errors.go b/x/feepay/types/errors.go index 7c25f8f34..d323afb9d 100644 --- a/x/feepay/types/errors.go +++ b/x/feepay/types/errors.go @@ -2,22 +2,13 @@ package types import ( errorsmod "cosmossdk.io/errors" - - sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" ) var ( - ErrInvalidAddress = sdkerrors.ErrInvalidAddress - ErrContractNotRegistered = errorsmod.Register(ModuleName, 1, "contract not registered") - ErrContractAlreadyRegistered = errorsmod.Register(ModuleName, 2, "contract already registered") - ErrContractRegisterNotAdmin = errorsmod.Register(ModuleName, 3, "this address is not the contract admin, cannot register") - ErrContractNotEnoughFunds = errorsmod.Register(ModuleName, 4, "contract does not have enough funds") - ErrWalletExceededUsageLimit = errorsmod.Register(ModuleName, 5, "wallet exceeded usage limit") - ErrContractNotAdmin = errorsmod.Register(ModuleName, 6, "sender is not the contract admin") - ErrContractNotCreator = errorsmod.Register(ModuleName, 7, "sender is not the contract creator") - ErrInvalidWalletLimit = errorsmod.Register(ModuleName, 8, "invalid wallet limit; must be between 0 and 1,000,000") - ErrInvalidJunoFundAmount = errorsmod.Register(ModuleName, 9, "fee pay contracts only accept juno funds") - ErrInvalidCWContract = errorsmod.Register(ModuleName, 10, "invalid CosmWasm contract") - ErrFeePayDisabled = errorsmod.Register(ModuleName, 11, "the FeePay module is disabled") - ErrDeductFees = errorsmod.Register(ModuleName, 12, "error deducting fees") + ErrContractNotEnoughFunds = errorsmod.Register(ModuleName, 1, "contract does not have enough funds") + ErrWalletExceededUsageLimit = errorsmod.Register(ModuleName, 2, "wallet exceeded usage limit") + ErrInvalidWalletLimit = errorsmod.Register(ModuleName, 3, "invalid wallet limit; must be between 0 and 1,000,000") + ErrInvalidJunoFundAmount = errorsmod.Register(ModuleName, 4, "fee pay contracts only accept juno funds") + ErrFeePayDisabled = errorsmod.Register(ModuleName, 5, "the FeePay module is disabled") + ErrDeductFees = errorsmod.Register(ModuleName, 6, "error deducting fees") )