diff --git a/CHANGELOG.md b/CHANGELOG.md index ebb4c91df..6081d958d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -75,6 +75,7 @@ JSON encoding for the `EIP55Addr` struct was not following the Go conventions an needed to include double quotes around the hexadecimal string. - [#2156](https://github.com/NibiruChain/nibiru/pull/2156) - test(evm-e2e): add E2E test using the Nibiru Oracle's ChainLink impl - [#2157](https://github.com/NibiruChain/nibiru/pull/2157) - fix(evm): Fix unit inconsistency related to AuthInfo.Fee and txData.Fee using effective fee +- [#2159](https://github.com/NibiruChain/nibiru/pull/2159) - chore(evm): Augment the Wasm msg handler so that wasm contracts cannot send MsgEthereumTx - [#2160](https://github.com/NibiruChain/nibiru/pull/2160) - fix(evm-precompile): use bank.MsgServer Send in precompile IFunToken.bankMsgSend - [#2161](https://github.com/NibiruChain/nibiru/pull/2161) - fix(evm): added tx logs events to the funtoken related txs - [#2162](https://github.com/NibiruChain/nibiru/pull/2162) - test(testutil): try retrying for 'panic: pebbledb: closed' @@ -82,6 +83,10 @@ needed to include double quotes around the hexadecimal string. - [#2168](https://github.com/NibiruChain/nibiru/pull/2168) - chore(evm-solidity): Move unrelated docs, gen-embeds, and add Solidity docs - [#2165](https://github.com/NibiruChain/nibiru/pull/2165) - fix(evm): use Singleton StateDB pattern for EVM txs - [#2169](https://github.com/NibiruChain/nibiru/pull/2169) - fix(evm): Better handling erc20 metadata +- [#2170](https://github.com/NibiruChain/nibiru/pull/2170) - chore: Remove redundant allowUnprotectedTxs +- [#2172](https://github.com/NibiruChain/nibiru/pull/2172) - chore: close iterator in IterateEpochInfo +- [#2173](https://github.com/NibiruChain/nibiru/pull/2173) - fix(evm): clear `StateDB` between calls + #### Nibiru EVM | Before Audit 2 - 2024-12-06 The codebase went through a third-party [Code4rena diff --git a/app/app.go b/app/app.go index 9aa9cf73e..992f8e775 100644 --- a/app/app.go +++ b/app/app.go @@ -113,7 +113,11 @@ func init() { } // GetWasmOpts build wasm options -func GetWasmOpts(nibiru NibiruApp, appOpts servertypes.AppOptions) []wasmkeeper.Option { +func GetWasmOpts( + nibiru NibiruApp, + appOpts servertypes.AppOptions, + wasmMsgHandlerArgs wasmext.MsgHandlerArgs, +) []wasmkeeper.Option { var wasmOpts []wasmkeeper.Option if cast.ToBool(appOpts.Get("telemetry.enabled")) { wasmOpts = append(wasmOpts, wasmkeeper.WithVMCacheMetrics(prometheus.DefaultRegisterer)) @@ -122,6 +126,7 @@ func GetWasmOpts(nibiru NibiruApp, appOpts servertypes.AppOptions) []wasmkeeper. return append(wasmOpts, wasmext.NibiruWasmOptions( nibiru.GRPCQueryRouter(), nibiru.appCodec, + wasmMsgHandlerArgs, )...) } diff --git a/app/evmante/evmante_sigverify.go b/app/evmante/evmante_sigverify.go index 31c4a1a7f..ff9918180 100644 --- a/app/evmante/evmante_sigverify.go +++ b/app/evmante/evmante_sigverify.go @@ -47,9 +47,8 @@ func (esvd EthSigVerificationDecorator) AnteHandle( ) } - allowUnprotectedTxs := false ethTx := msgEthTx.AsTransaction() - if !allowUnprotectedTxs && !ethTx.Protected() { + if !ethTx.Protected() { return ctx, errors.Wrapf( sdkerrors.ErrNotSupported, "rejected unprotected Ethereum transaction. "+ diff --git a/app/keepers.go b/app/keepers.go index d3025cd65..fc24f561e 100644 --- a/app/keepers.go +++ b/app/keepers.go @@ -104,6 +104,7 @@ import ( // Nibiru Custom Modules "github.com/NibiruChain/nibiru/v2/app/keepers" + "github.com/NibiruChain/nibiru/v2/app/wasmext" "github.com/NibiruChain/nibiru/v2/eth" "github.com/NibiruChain/nibiru/v2/x/common" "github.com/NibiruChain/nibiru/v2/x/devgas/v1" @@ -451,6 +452,17 @@ func (app *NibiruApp) InitKeepers( // For example, if there are bindings for the x/inflation module, then the app // passed to GetWasmOpts must already have a non-nil InflationKeeper. supportedFeatures := strings.Join(wasmdapp.AllCapabilities(), ",") + + wmha := wasmext.MsgHandlerArgs{ + Router: app.MsgServiceRouter(), + Ics4Wrapper: app.ibcFeeKeeper, + ChannelKeeper: app.ibcKeeper.ChannelKeeper, + CapabilityKeeper: app.ScopedWasmKeeper, + BankKeeper: app.BankKeeper, + Unpacker: appCodec, + PortSource: app.ibcTransferKeeper, + } + app.WasmMsgHandlerArgs = wmha app.WasmKeeper = wasmkeeper.NewKeeper( appCodec, keys[wasmtypes.StoreKey], @@ -458,18 +470,18 @@ func (app *NibiruApp) InitKeepers( app.BankKeeper, app.StakingKeeper, distrkeeper.NewQuerier(app.DistrKeeper), - app.ibcFeeKeeper, // ISC4 Wrapper: fee IBC middleware - app.ibcKeeper.ChannelKeeper, + wmha.Ics4Wrapper, // ISC4 Wrapper: fee IBC middleware + wmha.ChannelKeeper, &app.ibcKeeper.PortKeeper, - app.ScopedWasmKeeper, - app.ibcTransferKeeper, - app.MsgServiceRouter(), + wmha.CapabilityKeeper, + wmha.PortSource, + wmha.Router, app.GRPCQueryRouter(), wasmDir, wasmConfig, supportedFeatures, govModuleAddr, - GetWasmOpts(*app, appOpts)..., + GetWasmOpts(*app, appOpts, wmha)..., ) // DevGas uses WasmKeeper diff --git a/app/keepers/all_keepers.go b/app/keepers/all_keepers.go index 4692905c7..43278e3d5 100644 --- a/app/keepers/all_keepers.go +++ b/app/keepers/all_keepers.go @@ -21,6 +21,7 @@ import ( // --------------------------------------------------------------- // Nibiru Custom Modules + "github.com/NibiruChain/nibiru/v2/app/wasmext" devgaskeeper "github.com/NibiruChain/nibiru/v2/x/devgas/v1/keeper" epochskeeper "github.com/NibiruChain/nibiru/v2/x/epochs/keeper" evmkeeper "github.com/NibiruChain/nibiru/v2/x/evm/keeper" @@ -66,6 +67,8 @@ type PublicKeepers struct { EvmKeeper *evmkeeper.Keeper // WASM keepers - WasmKeeper wasmkeeper.Keeper + WasmKeeper wasmkeeper.Keeper + WasmMsgHandlerArgs wasmext.MsgHandlerArgs + ScopedWasmKeeper capabilitykeeper.ScopedKeeper } diff --git a/app/wasmext/stargate_query_test.go b/app/wasmext/stargate_query_test.go index 948310790..1b8f981f1 100644 --- a/app/wasmext/stargate_query_test.go +++ b/app/wasmext/stargate_query_test.go @@ -3,7 +3,6 @@ package wasmext_test import ( "fmt" "strings" - "testing" "github.com/cosmos/gogoproto/proto" "github.com/stretchr/testify/assert" @@ -36,7 +35,8 @@ Given only the `PB_MSG.PACKAGE` and the `PB_MSG.NAME` of either the query request or response, we should know the `QueryRequest::Stargate.path` deterministically. */ -func TestWasmAcceptedStargateQueries(t *testing.T) { +func (s *Suite) TestWasmAcceptedStargateQueries() { + t := s.T() t.Log("stargateQueryPaths: Add nibiru query paths from GRPC service descriptions") queryServiceDescriptions := []grpc.ServiceDesc{ epochs.GrpcQueryServiceDesc(), diff --git a/app/wasmext/wasm.go b/app/wasmext/wasm.go index c71c38a23..6ccfa2329 100644 --- a/app/wasmext/wasm.go +++ b/app/wasmext/wasm.go @@ -1,9 +1,17 @@ package wasmext import ( + "github.com/NibiruChain/nibiru/v2/x/evm" + + "cosmossdk.io/errors" wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" + wasm "github.com/CosmWasm/wasmd/x/wasm/types" + wasmvmtypes "github.com/CosmWasm/wasmvm/types" "github.com/cosmos/cosmos-sdk/baseapp" "github.com/cosmos/cosmos-sdk/codec" + sdkcodec "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" ) // NibiruWasmOptions: Wasm Options are extension points to instantiate the Wasm @@ -11,6 +19,7 @@ import ( func NibiruWasmOptions( grpcQueryRouter *baseapp.GRPCQueryRouter, appCodec codec.Codec, + msgHandlerArgs MsgHandlerArgs, ) []wasmkeeper.Option { wasmQueryOption := wasmkeeper.WithQueryPlugins(&wasmkeeper.QueryPlugins{ Stargate: wasmkeeper.AcceptListStargateQuerier( @@ -20,5 +29,110 @@ func NibiruWasmOptions( ), }) - return []wasmkeeper.Option{wasmQueryOption} + wasmMsgHandlerOption := wasmkeeper.WithMessageHandler(WasmMessageHandler(msgHandlerArgs)) + + return []wasmkeeper.Option{ + wasmQueryOption, + wasmMsgHandlerOption, + } +} + +func (h SDKMessageHandler) handleSdkMessage(ctx sdk.Context, contractAddr sdk.Address, msg sdk.Msg) (*sdk.Result, error) { + if err := msg.ValidateBasic(); err != nil { + return nil, err + } + + // make sure this account can send it + for _, acct := range msg.GetSigners() { + if !acct.Equals(contractAddr) { + return nil, errors.Wrap(sdkerrors.ErrUnauthorized, "contract doesn't have permission") + } + } + + msgTypeUrl := sdk.MsgTypeURL(msg) + if msgTypeUrl == sdk.MsgTypeURL(new(evm.MsgEthereumTx)) { + return nil, errors.Wrap(sdkerrors.ErrUnauthorized, "Wasm VM to EVM call pattern is not yet supported") + } + + // find the handler and execute it + if handler := h.router.Handler(msg); handler != nil { + // ADR 031 request type routing + msgResult, err := handler(ctx, msg) + return msgResult, err + } + // legacy sdk.Msg routing + // Assuming that the app developer has migrated all their Msgs to + // proto messages and has registered all `Msg services`, then this + // path should never be called, because all those Msgs should be + // registered within the `msgServiceRouter` already. + return nil, errors.Wrapf(sdkerrors.ErrUnknownRequest, "can't route message %+v", msg) +} + +type MsgHandlerArgs struct { + Router MessageRouter + Ics4Wrapper wasm.ICS4Wrapper + ChannelKeeper wasm.ChannelKeeper + CapabilityKeeper wasm.CapabilityKeeper + BankKeeper wasm.Burner + Unpacker sdkcodec.AnyUnpacker + PortSource wasm.ICS20TransferPortSource +} + +// SDKMessageHandler can handles messages that can be encoded into sdk.Message types and routed. +type SDKMessageHandler struct { + router MessageRouter + encoders msgEncoder +} + +// MessageRouter ADR 031 request type routing +type MessageRouter interface { + Handler(msg sdk.Msg) baseapp.MsgServiceHandler +} + +// msgEncoder is an extension point to customize encodings +type msgEncoder interface { + // Encode converts wasmvm message to n cosmos message types + Encode(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) ([]sdk.Msg, error) +} + +// WasmMessageHandler is a replacement constructor for +// [wasmkeeper.NewDefaultMessageHandler] inside of [wasmkeeper.NewKeeper]. +func WasmMessageHandler( + args MsgHandlerArgs, +) wasmkeeper.Messenger { + encoders := wasmkeeper.DefaultEncoders(args.Unpacker, args.PortSource) + return wasmkeeper.NewMessageHandlerChain( + NewSDKMessageHandler(args.Router, encoders), + wasmkeeper.NewIBCRawPacketHandler(args.Ics4Wrapper, args.ChannelKeeper, args.CapabilityKeeper), + wasmkeeper.NewBurnCoinMessageHandler(args.BankKeeper), + ) +} + +func NewSDKMessageHandler(router MessageRouter, encoders msgEncoder) SDKMessageHandler { + return SDKMessageHandler{ + router: router, + encoders: encoders, + } +} + +func (h SDKMessageHandler) DispatchMsg(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) { + sdkMsgs, err := h.encoders.Encode(ctx, contractAddr, contractIBCPortID, msg) + if err != nil { + return nil, nil, err + } + for _, sdkMsg := range sdkMsgs { + res, err := h.handleSdkMessage(ctx, contractAddr, sdkMsg) + if err != nil { + return nil, nil, err + } + // append data + data = append(data, res.Data) + // append events + sdkEvents := make([]sdk.Event, len(res.Events)) + for i := range res.Events { + sdkEvents[i] = sdk.Event(res.Events[i]) + } + events = append(events, sdkEvents...) + } + return } diff --git a/app/wasmext/wasmext_test.go b/app/wasmext/wasmext_test.go new file mode 100644 index 000000000..7788633d5 --- /dev/null +++ b/app/wasmext/wasmext_test.go @@ -0,0 +1,98 @@ +package wasmext_test + +import ( + "math/big" + "testing" + + wasmvm "github.com/CosmWasm/wasmvm/types" + sdkcodec "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" + bank "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/stretchr/testify/suite" + + "github.com/NibiruChain/nibiru/v2/app/wasmext" + "github.com/NibiruChain/nibiru/v2/x/common/testutil/testapp" + "github.com/NibiruChain/nibiru/v2/x/evm" + "github.com/NibiruChain/nibiru/v2/x/evm/evmtest" +) + +type Suite struct { + suite.Suite +} + +func TestWasmExtSuite(t *testing.T) { + suite.Run(t, new(Suite)) +} + +// WasmVM to EVM call pattern is not yet supported. This test verifies the +// Nibiru's [wasmkeeper.Option] function as expected. +func (s *Suite) TestEvmFilter() { + deps := evmtest.NewTestDeps() + // wk := wasmkeeper.NewDefaultPermissionKeeper(deps.App.WasmKeeper) + wasmMsgHandler := wasmext.WasmMessageHandler(deps.App.WasmMsgHandlerArgs) + + s.T().Log("Create a valid Ethereum tx msg") + + to := evmtest.NewEthPrivAcc() + ethTxMsg, err := evmtest.TxTransferWei{ + Deps: &deps, + To: to.EthAddr, + AmountWei: evm.NativeToWei(big.NewInt(420)), + }.Build() + s.NoError(err) + + s.T().Log("Validate Eth tx msg proto encoding as wasmvm.StargateMsg") + wasmContractAddr := deps.Sender.NibiruAddr + protoValueBz, err := deps.EncCfg.Codec.Marshal(ethTxMsg) + s.Require().NoError(err, "expect ethTxMsg to proto marshal", protoValueBz) + + _, ok := deps.EncCfg.Codec.(sdkcodec.AnyUnpacker) + s.Require().True(ok, "codec must be an AnyUnpacker") + + pbAny, err := sdkcodec.NewAnyWithValue(ethTxMsg) + s.NoError(err) + pbAnyBz, err := pbAny.Marshal() + s.NoError(err, pbAnyBz) + + var sdkMsg sdk.Msg + err = deps.EncCfg.Codec.UnpackAny(pbAny, &sdkMsg) + s.Require().NoError(err) + s.Equal("/eth.evm.v1.MsgEthereumTx", sdk.MsgTypeURL(sdkMsg)) + + s.T().Log("Dispatch the Eth tx msg from Wasm (unsuccessfully)") + _, _, err = wasmMsgHandler.DispatchMsg( + deps.Ctx, + wasmContractAddr, + "ibcport-unused", + wasmvm.CosmosMsg{ + Stargate: &wasmvm.StargateMsg{ + TypeURL: sdk.MsgTypeURL(ethTxMsg), + Value: protoValueBz, + }, + }, + ) + s.Require().ErrorContains(err, "Wasm VM to EVM call pattern is not yet supported") + + coins := sdk.NewCoins(sdk.NewInt64Coin(evm.EVMBankDenom, 420)) // arbitrary constant + err = testapp.FundAccount(deps.App.BankKeeper, deps.Ctx, deps.Sender.NibiruAddr, coins) + s.NoError(err) + txMsg := &bank.MsgSend{ + FromAddress: deps.Sender.NibiruAddr.String(), + ToAddress: evmtest.NewEthPrivAcc().NibiruAddr.String(), + Amount: []sdk.Coin{sdk.NewInt64Coin(evm.EVMBankDenom, 20)}, + } + protoValueBz, err = deps.EncCfg.Codec.Marshal(txMsg) + s.NoError(err) + _, _, err = wasmMsgHandler.DispatchMsg( + deps.Ctx, + wasmContractAddr, + "ibcport-unused", + wasmvm.CosmosMsg{ + Stargate: &wasmvm.StargateMsg{ + TypeURL: sdk.MsgTypeURL(txMsg), + Value: protoValueBz, + }, + }, + ) + s.Require().NoError(err) +} diff --git a/x/epochs/keeper/epoch.go b/x/epochs/keeper/epoch.go index 46f45714d..9a5bc30db 100644 --- a/x/epochs/keeper/epoch.go +++ b/x/epochs/keeper/epoch.go @@ -66,6 +66,7 @@ func (k Keeper) IterateEpochInfo( iterate := k.Epochs.Iterate(ctx, &collections.Range[string]{}) i := int64(0) + defer iterate.Close() for ; iterate.Valid(); iterate.Next() { epoch := iterate.Value() stop := fn(i, epoch) diff --git a/x/evm/embeds/artifacts/contracts/TestPrecompileSendToBankThenERC20Transfer.sol/TestPrecompileSendToBankThenERC20Transfer.json b/x/evm/embeds/artifacts/contracts/TestPrecompileSendToBankThenERC20Transfer.sol/TestPrecompileSendToBankThenERC20Transfer.json new file mode 100644 index 000000000..ba4e68635 --- /dev/null +++ b/x/evm/embeds/artifacts/contracts/TestPrecompileSendToBankThenERC20Transfer.sol/TestPrecompileSendToBankThenERC20Transfer.json @@ -0,0 +1,60 @@ +{ + "_format": "hh-sol-artifact-1", + "contractName": "TestPrecompileSendToBankThenERC20Transfer", + "sourceName": "contracts/TestPrecompileSendToBankThenERC20Transfer.sol", + "abi": [ + { + "inputs": [ + { + "internalType": "address", + "name": "_erc20", + "type": "address" + }, + { + "internalType": "string", + "name": "_recipient", + "type": "string" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "attack", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "erc20", + "outputs": [ + { + "internalType": "contract IERC20", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "recipient", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + } + ], + "bytecode": "0x60806040523480156200001157600080fd5b5060405162000d8c38038062000d8c833981810160405281019062000037919062000289565b816000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555080600190816200008891906200053a565b50505062000621565b6000604051905090565b600080fd5b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000620000d282620000a5565b9050919050565b620000e481620000c5565b8114620000f057600080fd5b50565b6000815190506200010481620000d9565b92915050565b600080fd5b600080fd5b6000601f19601f8301169050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6200015f8262000114565b810181811067ffffffffffffffff8211171562000181576200018062000125565b5b80604052505050565b60006200019662000091565b9050620001a4828262000154565b919050565b600067ffffffffffffffff821115620001c757620001c662000125565b5b620001d28262000114565b9050602081019050919050565b60005b83811015620001ff578082015181840152602081019050620001e2565b60008484015250505050565b6000620002226200021c84620001a9565b6200018a565b9050828152602081018484840111156200024157620002406200010f565b5b6200024e848285620001df565b509392505050565b600082601f8301126200026e576200026d6200010a565b5b8151620002808482602086016200020b565b91505092915050565b60008060408385031215620002a357620002a26200009b565b5b6000620002b385828601620000f3565b925050602083015167ffffffffffffffff811115620002d757620002d6620000a0565b5b620002e58582860162000256565b9150509250929050565b600081519050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b600060028204905060018216806200034257607f821691505b602082108103620003585762000357620002fa565b5b50919050565b60008190508160005260206000209050919050565b60006020601f8301049050919050565b600082821b905092915050565b600060088302620003c27fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8262000383565b620003ce868362000383565b95508019841693508086168417925050509392505050565b6000819050919050565b6000819050919050565b60006200041b620004156200040f84620003e6565b620003f0565b620003e6565b9050919050565b6000819050919050565b6200043783620003fa565b6200044f620004468262000422565b84845462000390565b825550505050565b600090565b6200046662000457565b620004738184846200042c565b505050565b5b818110156200049b576200048f6000826200045c565b60018101905062000479565b5050565b601f821115620004ea57620004b4816200035e565b620004bf8462000373565b81016020851015620004cf578190505b620004e7620004de8562000373565b83018262000478565b50505b505050565b600082821c905092915050565b60006200050f60001984600802620004ef565b1980831691505092915050565b60006200052a8383620004fc565b9150826002028217905092915050565b6200054582620002ef565b67ffffffffffffffff81111562000561576200056062000125565b5b6200056d825462000329565b6200057a8282856200049f565b600060209050601f831160018114620005b257600084156200059d578287015190505b620005a985826200051c565b86555062000619565b601f198416620005c2866200035e565b60005b82811015620005ec57848901518255600182019150602085019450602081019050620005c5565b868310156200060c578489015162000608601f891682620004fc565b8355505b6001600288020188555050505b505050505050565b61075b80620006316000396000f3fe608060405234801561001057600080fd5b50600436106100415760003560e01c806366d003ac14610046578063785e9e86146100645780639e5faafc14610082575b600080fd5b61004e61008c565b60405161005b91906103b6565b60405180910390f35b61006c61011a565b6040516100799190610457565b60405180910390f35b61008a61013e565b005b60018054610099906104a1565b80601f01602080910402602001604051908101604052809291908181526020018280546100c5906104a1565b80156101125780601f106100e757610100808354040283529160200191610112565b820191906000526020600020905b8154815290600101906020018083116100f557829003601f168201915b505050505081565b60008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff166370a08231306040518263ffffffff1660e01b815260040161019a91906104f3565b602060405180830381865afa1580156101b7573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906101db9190610549565b905061080073ffffffffffffffffffffffffffffffffffffffff1663e77a47bf60008054906101000a900473ffffffffffffffffffffffffffffffffffffffff168360016040518463ffffffff1660e01b815260040161023d9392919061061e565b6020604051808303816000875af115801561025c573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906102809190610549565b5060008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1663a9059cbb61dead60016040518363ffffffff1660e01b81526004016102df929190610697565b6020604051808303816000875af11580156102fe573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061032291906106f8565b5050565b600081519050919050565b600082825260208201905092915050565b60005b83811015610360578082015181840152602081019050610345565b60008484015250505050565b6000601f19601f8301169050919050565b600061038882610326565b6103928185610331565b93506103a2818560208601610342565b6103ab8161036c565b840191505092915050565b600060208201905081810360008301526103d0818461037d565b905092915050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000819050919050565b600061041d610418610413846103d8565b6103f8565b6103d8565b9050919050565b600061042f82610402565b9050919050565b600061044182610424565b9050919050565b61045181610436565b82525050565b600060208201905061046c6000830184610448565b92915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b600060028204905060018216806104b957607f821691505b6020821081036104cc576104cb610472565b5b50919050565b60006104dd826103d8565b9050919050565b6104ed816104d2565b82525050565b600060208201905061050860008301846104e4565b92915050565b600080fd5b6000819050919050565b61052681610513565b811461053157600080fd5b50565b6000815190506105438161051d565b92915050565b60006020828403121561055f5761055e61050e565b5b600061056d84828501610534565b91505092915050565b61057f81610513565b82525050565b60008190508160005260206000209050919050565b600081546105a7816104a1565b6105b18186610331565b945060018216600081146105cc57600181146105e257610615565b60ff198316865281151560200286019350610615565b6105eb85610585565b60005b8381101561060d578154818901526001820191506020810190506105ee565b808801955050505b50505092915050565b600060608201905061063360008301866104e4565b6106406020830185610576565b8181036040830152610652818461059a565b9050949350505050565b6000819050919050565b600061068161067c6106778461065c565b6103f8565b610513565b9050919050565b61069181610666565b82525050565b60006040820190506106ac60008301856104e4565b6106b96020830184610688565b9392505050565b60008115159050919050565b6106d5816106c0565b81146106e057600080fd5b50565b6000815190506106f2816106cc565b92915050565b60006020828403121561070e5761070d61050e565b5b600061071c848285016106e3565b9150509291505056fea26469706673582212208ef08251d818031ab4c74d22a618b6305b406be371abda8aeb6b83c46f6c195a64736f6c63430008180033", + "deployedBytecode": "0x608060405234801561001057600080fd5b50600436106100415760003560e01c806366d003ac14610046578063785e9e86146100645780639e5faafc14610082575b600080fd5b61004e61008c565b60405161005b91906103b6565b60405180910390f35b61006c61011a565b6040516100799190610457565b60405180910390f35b61008a61013e565b005b60018054610099906104a1565b80601f01602080910402602001604051908101604052809291908181526020018280546100c5906104a1565b80156101125780601f106100e757610100808354040283529160200191610112565b820191906000526020600020905b8154815290600101906020018083116100f557829003601f168201915b505050505081565b60008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff166370a08231306040518263ffffffff1660e01b815260040161019a91906104f3565b602060405180830381865afa1580156101b7573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906101db9190610549565b905061080073ffffffffffffffffffffffffffffffffffffffff1663e77a47bf60008054906101000a900473ffffffffffffffffffffffffffffffffffffffff168360016040518463ffffffff1660e01b815260040161023d9392919061061e565b6020604051808303816000875af115801561025c573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906102809190610549565b5060008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1663a9059cbb61dead60016040518363ffffffff1660e01b81526004016102df929190610697565b6020604051808303816000875af11580156102fe573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061032291906106f8565b5050565b600081519050919050565b600082825260208201905092915050565b60005b83811015610360578082015181840152602081019050610345565b60008484015250505050565b6000601f19601f8301169050919050565b600061038882610326565b6103928185610331565b93506103a2818560208601610342565b6103ab8161036c565b840191505092915050565b600060208201905081810360008301526103d0818461037d565b905092915050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000819050919050565b600061041d610418610413846103d8565b6103f8565b6103d8565b9050919050565b600061042f82610402565b9050919050565b600061044182610424565b9050919050565b61045181610436565b82525050565b600060208201905061046c6000830184610448565b92915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b600060028204905060018216806104b957607f821691505b6020821081036104cc576104cb610472565b5b50919050565b60006104dd826103d8565b9050919050565b6104ed816104d2565b82525050565b600060208201905061050860008301846104e4565b92915050565b600080fd5b6000819050919050565b61052681610513565b811461053157600080fd5b50565b6000815190506105438161051d565b92915050565b60006020828403121561055f5761055e61050e565b5b600061056d84828501610534565b91505092915050565b61057f81610513565b82525050565b60008190508160005260206000209050919050565b600081546105a7816104a1565b6105b18186610331565b945060018216600081146105cc57600181146105e257610615565b60ff198316865281151560200286019350610615565b6105eb85610585565b60005b8381101561060d578154818901526001820191506020810190506105ee565b808801955050505b50505092915050565b600060608201905061063360008301866104e4565b6106406020830185610576565b8181036040830152610652818461059a565b9050949350505050565b6000819050919050565b600061068161067c6106778461065c565b6103f8565b610513565b9050919050565b61069181610666565b82525050565b60006040820190506106ac60008301856104e4565b6106b96020830184610688565b9392505050565b60008115159050919050565b6106d5816106c0565b81146106e057600080fd5b50565b6000815190506106f2816106cc565b92915050565b60006020828403121561070e5761070d61050e565b5b600061071c848285016106e3565b9150509291505056fea26469706673582212208ef08251d818031ab4c74d22a618b6305b406be371abda8aeb6b83c46f6c195a64736f6c63430008180033", + "linkReferences": {}, + "deployedLinkReferences": {} +} diff --git a/x/evm/embeds/contracts/TestPrecompileSendToBankThenERC20Transfer.sol b/x/evm/embeds/contracts/TestPrecompileSendToBankThenERC20Transfer.sol new file mode 100644 index 000000000..d54dc54a3 --- /dev/null +++ b/x/evm/embeds/contracts/TestPrecompileSendToBankThenERC20Transfer.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "./IFunToken.sol"; + +contract TestPrecompileSendToBankThenERC20Transfer { + IERC20 public erc20; + string public recipient; + + constructor(address _erc20, string memory _recipient) { + erc20 = IERC20(_erc20); + recipient = _recipient; + } + + function attack() public { + // transfer this contract's entire balance to the recipient + uint balance = erc20.balanceOf(address(this)); + // sendToBank should reduce balance to zero + FUNTOKEN_PRECOMPILE.sendToBank(address(erc20), balance, recipient); + + // this call should fail because of the balance is zero + erc20.transfer(0x000000000000000000000000000000000000dEaD, 1); + } +} diff --git a/x/evm/embeds/embeds.go b/x/evm/embeds/embeds.go index 8bd093e34..f08e26b5d 100644 --- a/x/evm/embeds/embeds.go +++ b/x/evm/embeds/embeds.go @@ -45,6 +45,8 @@ var ( testRandom []byte //go:embed artifacts/contracts/MKR.sol/DSToken.json testMetadataBytes32 []byte + //go:embed artifacts/contracts/TestPrecompileSendToBankThenERC20Transfer.sol/TestPrecompileSendToBankThenERC20Transfer.json + testPrecompileSendToBankThenERC20Transfer []byte ) var ( @@ -148,6 +150,12 @@ var ( Name: "MKR.sol", EmbedJSON: testMetadataBytes32, } + + // SmartContract_TestPrecompileSendToBankThenERC20Transfer is a test contract that sends to bank then calls ERC20 transfer + SmartContract_TestPrecompileSendToBankThenERC20Transfer = CompiledEvmContract{ + Name: "TestPrecompileSendToBankThenERC20Transfer.sol", + EmbedJSON: testPrecompileSendToBankThenERC20Transfer, + } ) func init() { @@ -166,6 +174,7 @@ func init() { SmartContract_TestERC20TransferWithFee.MustLoad() SmartContract_TestRandom.MustLoad() SmartContract_TestBytes32Metadata.MustLoad() + SmartContract_TestPrecompileSendToBankThenERC20Transfer.MustLoad() } type CompiledEvmContract struct { diff --git a/x/evm/embeds/package-lock.json b/x/evm/embeds/package-lock.json index 149b1ef1b..99bb2520b 100644 --- a/x/evm/embeds/package-lock.json +++ b/x/evm/embeds/package-lock.json @@ -1,12 +1,12 @@ { "name": "@nibiruchain/solidity", - "version": "0.0.1", + "version": "0.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@nibiruchain/solidity", - "version": "0.0.1", + "version": "0.0.2", "license": "MIT", "dependencies": { "@openzeppelin/contracts": "^4.9.0" diff --git a/x/evm/keeper/funtoken_from_coin.go b/x/evm/keeper/funtoken_from_coin.go index abfddd2ec..294d72964 100644 --- a/x/evm/keeper/funtoken_from_coin.go +++ b/x/evm/keeper/funtoken_from_coin.go @@ -98,6 +98,9 @@ func (k *Keeper) deployERC20ForBankCoin( if stateDB == nil { stateDB = k.NewStateDB(ctx, txConfig) } + defer func() { + k.Bank.StateDB = nil + }() evmObj := k.NewEVM(ctx, evmMsg, evmCfg, nil /*tracer*/, stateDB) evmResp, err := k.CallContractWithInput( ctx, evmObj, evm.EVM_MODULE_ADDRESS, nil, true /*commit*/, input, Erc20GasLimitDeploy, @@ -110,8 +113,6 @@ func (k *Keeper) deployERC20ForBankCoin( if err != nil { return gethcommon.Address{}, errors.Wrap(err, "failed to commit stateDB") } - // Don't need the StateDB anymore because it's not usable after committing - k.Bank.StateDB = nil ctx.GasMeter().ConsumeGas(evmResp.GasUsed, "deploy erc20 funtoken contract") diff --git a/x/evm/keeper/funtoken_from_coin_test.go b/x/evm/keeper/funtoken_from_coin_test.go index a858c4589..7a75afca6 100644 --- a/x/evm/keeper/funtoken_from_coin_test.go +++ b/x/evm/keeper/funtoken_from_coin_test.go @@ -28,7 +28,8 @@ func (s *FunTokenFromCoinSuite) TestCreateFunTokenFromCoin() { metadata, err := deps.EvmKeeper.FindERC20Metadata( deps.Ctx, evmObj, - crypto.CreateAddress(evm.EVM_MODULE_ADDRESS, deps.EvmKeeper.GetAccNonce(deps.Ctx, evm.EVM_MODULE_ADDRESS)), + crypto.CreateAddress( + evm.EVM_MODULE_ADDRESS, deps.EvmKeeper.GetAccNonce(deps.Ctx, evm.EVM_MODULE_ADDRESS)), nil, ) s.Require().Error(err) @@ -572,7 +573,6 @@ func (s *FunTokenFromCoinSuite) TestERC20TransferThenPrecompileSend() { // - Module account: 10 NIBI escrowed (which Test contract holds as 10 WNIBI) func (s *FunTokenFromCoinSuite) TestPrecompileSelfCallRevert() { deps := evmtest.NewTestDeps() - evmObj, _ := deps.NewEVM() // Initial setup funToken := s.fundAndCreateFunToken(deps, 10e6) @@ -605,6 +605,7 @@ func (s *FunTokenFromCoinSuite) TestPrecompileSelfCallRevert() { sdk.NewCoins(sdk.NewCoin(evm.EVMBankDenom, sdk.NewInt(10e6))), )) + evmObj, _ := deps.NewEVM() evmtest.FunTokenBalanceAssert{ FunToken: funToken, Account: testContractAddr, @@ -619,6 +620,7 @@ func (s *FunTokenFromCoinSuite) TestPrecompileSelfCallRevert() { charles := evmtest.NewEthPrivAcc() s.T().Log("call test contract") + evmObj, _ = deps.NewEVM() contractInput, err := embeds.SmartContract_TestPrecompileSelfCallRevert.ABI.Pack( "selfCallTransferFunds", alice.EthAddr, @@ -671,6 +673,104 @@ func (s *FunTokenFromCoinSuite) TestPrecompileSelfCallRevert() { }.Assert(s.T(), deps, evmObj) } +// TestPrecompileSelfCallRevert +// 1. Creates a funtoken from coin. +// 2. Calls the test contract +// a. sendToBank +// b. erc20 transfer +// +// INITIAL STATE: +// - Test contract funds: 10 WNIBI +// CONTRACT CALL: +// - Sends 10 WNIBI to Alice, and try to send 1 NIBI to Bob +// EXPECTED: +// - all changes reverted because of not enough balance +// - Test contract funds: 10 WNIBI +// - Alice: 10 WNIBI +// - Bob: 0 NIBI +// - Module account: 10 NIBI escrowed (which Test contract holds as 10 WNIBI) +func (s *FunTokenFromCoinSuite) TestPrecompileSendToBankThenErc20Transfer() { + deps := evmtest.NewTestDeps() + + // Initial setup + funToken := s.fundAndCreateFunToken(deps, 10e6) + + s.T().Log("Deploy Test Contract") + deployResp, err := evmtest.DeployContract( + &deps, + embeds.SmartContract_TestPrecompileSendToBankThenERC20Transfer, + funToken.Erc20Addr.Address, + deps.Sender.NibiruAddr.String(), + ) + s.Require().NoError(err) + testContractAddr := deployResp.ContractAddr + + s.T().Log("Convert bank coin to erc-20: give test contract 10 WNIBI (erc20)") + _, err = deps.EvmKeeper.ConvertCoinToEvm( + sdk.WrapSDKContext(deps.Ctx), + &evm.MsgConvertCoinToEvm{ + Sender: deps.Sender.NibiruAddr.String(), + BankCoin: sdk.NewCoin(evm.EVMBankDenom, sdk.NewInt(10e6)), + ToEthAddr: eth.EIP55Addr{Address: testContractAddr}, + }, + ) + s.Require().NoError(err) + + // Create Alice and Bob. Contract will try to send Alice native coins and + // send Bob ERC20 tokens. + alice := evmtest.NewEthPrivAcc() + bob := evmtest.NewEthPrivAcc() + + s.T().Log("call test contract") + contractInput, err := embeds.SmartContract_TestPrecompileSendToBankThenERC20Transfer.ABI.Pack( + "attack", + ) + s.Require().NoError(err) + evmObj, _ := deps.NewEVM() + _, err = deps.EvmKeeper.CallContractWithInput( + deps.Ctx, + evmObj, + deps.Sender.EthAddr, + &testContractAddr, + true, + contractInput, + evmtest.FunTokenGasLimitSendToEvm, + ) + s.Require().ErrorContains(err, "execution reverted") + + evmtest.FunTokenBalanceAssert{ + FunToken: funToken, + Account: alice.EthAddr, + BalanceBank: big.NewInt(0), + BalanceERC20: big.NewInt(0), + Description: "Alice has 0 NIBI / 0 WNIBI", + }.Assert(s.T(), deps, evmObj) + + evmtest.FunTokenBalanceAssert{ + FunToken: funToken, + Account: bob.EthAddr, + BalanceBank: big.NewInt(0), + BalanceERC20: big.NewInt(0), + Description: "Charles has 0 NIBI / 0 WNIBI", + }.Assert(s.T(), deps, evmObj) + + evmtest.FunTokenBalanceAssert{ + FunToken: funToken, + Account: testContractAddr, + BalanceBank: big.NewInt(0), + BalanceERC20: big.NewInt(10e6), + Description: "Test contract has 10 NIBI / 10 WNIBI", + }.Assert(s.T(), deps, evmObj) + + evmtest.FunTokenBalanceAssert{ + FunToken: funToken, + Account: evm.EVM_MODULE_ADDRESS, + BalanceBank: big.NewInt(10e6), + BalanceERC20: big.NewInt(0), + Description: "Module account has 10 NIBI escrowed", + }.Assert(s.T(), deps, evmObj) +} + // fundAndCreateFunToken creates initial setup for tests func (s *FunTokenFromCoinSuite) fundAndCreateFunToken(deps evmtest.TestDeps, unibiAmount int64) evm.FunToken { bankDenom := evm.EVMBankDenom diff --git a/x/evm/keeper/funtoken_from_erc20.go b/x/evm/keeper/funtoken_from_erc20.go index 5eb55ce4a..36c071768 100644 --- a/x/evm/keeper/funtoken_from_erc20.go +++ b/x/evm/keeper/funtoken_from_erc20.go @@ -132,6 +132,9 @@ func (k *Keeper) createFunTokenFromERC20( if stateDB == nil { stateDB = k.NewStateDB(ctx, statedb.NewEmptyTxConfig(gethcommon.BytesToHash(ctx.HeaderHash()))) } + defer func() { + k.Bank.StateDB = nil + }() evmMsg := gethcore.NewMessage( evm.EVM_MODULE_ADDRESS, &erc20, @@ -184,8 +187,6 @@ func (k *Keeper) createFunTokenFromERC20( if err != nil { return nil, errors.Wrap(err, "failed to commit stateDB") } - // Don't need the StateDB anymore because it's not usable after committing - k.Bank.StateDB = nil return funtoken, k.FunTokens.SafeInsert( ctx, erc20, bankDenom, false, diff --git a/x/evm/keeper/msg_server.go b/x/evm/keeper/msg_server.go index 982f798ac..94acd33ac 100644 --- a/x/evm/keeper/msg_server.go +++ b/x/evm/keeper/msg_server.go @@ -60,6 +60,9 @@ func (k *Keeper) EthereumTx( if stateDB == nil { stateDB = k.NewStateDB(ctx, txConfig) } + defer func() { + k.Bank.StateDB = nil + }() evmObj := k.NewEVM(ctx, evmMsg, evmCfg, nil /*tracer*/, stateDB) evmResp, err = k.ApplyEvmMsg(ctx, evmMsg, evmObj, nil /*tracer*/, true /*commit*/, txConfig.TxHash, false /*fullRefundLeftoverGas*/) if err != nil { @@ -341,8 +344,6 @@ func (k *Keeper) ApplyEvmMsg( if err := evmObj.StateDB.(*statedb.StateDB).Commit(); err != nil { return nil, errors.Wrap(err, "ApplyEvmMsg: failed to commit stateDB") } - // after we commit, the StateDB is no longer usable so we discard it and let the Golang garbage collector dispose of it - k.Bank.StateDB = nil } // Rare case of uint64 gas overflow if msg.Gas() < leftoverGas { @@ -544,10 +545,13 @@ func (k Keeper) convertCoinToEvmBornCoin( true, ) txConfig := k.TxConfig(ctx, gethcommon.Hash{}) - var stateDB *statedb.StateDB = k.Bank.StateDB + stateDB := k.Bank.StateDB if stateDB == nil { stateDB = k.NewStateDB(ctx, txConfig) } + defer func() { + k.Bank.StateDB = nil + }() evmObj := k.NewEVM(ctx, evmMsg, k.GetEVMConfig(ctx), nil /*tracer*/, stateDB) evmResp, err := k.CallContractWithInput( ctx, @@ -572,7 +576,6 @@ func (k Keeper) convertCoinToEvmBornCoin( if err != nil { return nil, errors.Wrap(err, "failed to commit stateDB") } - k.Bank.StateDB = nil _ = ctx.EventManager().EmitTypedEvent(&evm.EventConvertCoinToEvm{ Sender: sender.String(), @@ -596,10 +599,13 @@ func (k Keeper) convertCoinToEvmBornERC20( funTokenMapping evm.FunToken, ) (*evm.MsgConvertCoinToEvmResponse, error) { // needs to run first to populate the StateDB on the BankKeeperExtension - var stateDB *statedb.StateDB = k.Bank.StateDB + stateDB := k.Bank.StateDB if stateDB == nil { stateDB = k.NewStateDB(ctx, k.TxConfig(ctx, gethcommon.Hash{})) } + defer func() { + k.Bank.StateDB = nil + }() erc20Addr := funTokenMapping.Erc20Addr.Address // 1 | Caller transfers Bank Coins to be converted to ERC20 tokens. @@ -664,7 +670,6 @@ func (k Keeper) convertCoinToEvmBornERC20( if err := stateDB.Commit(); err != nil { return nil, errors.Wrap(err, "failed to commit stateDB") } - k.Bank.StateDB = nil // Emit event with the actual amount received _ = ctx.EventManager().EmitTypedEvent(&evm.EventConvertCoinToEvm{ diff --git a/x/evm/precompile/funtoken.go b/x/evm/precompile/funtoken.go index ddce4fe79..6a1aa210b 100644 --- a/x/evm/precompile/funtoken.go +++ b/x/evm/precompile/funtoken.go @@ -59,7 +59,6 @@ func (p precompileFunToken) Run( if err != nil { return nil, err } - p.evmKeeper.Bank.StateDB = startResult.StateDB // Gracefully handles "out of gas" defer HandleOutOfGasPanic(&err)() diff --git a/x/evm/precompile/wasm.go b/x/evm/precompile/wasm.go index 2ecfbd0ae..bfb9ff9d8 100644 --- a/x/evm/precompile/wasm.go +++ b/x/evm/precompile/wasm.go @@ -48,11 +48,6 @@ func (p precompileWasm) Run( abciEventsStartIdx := len(startResult.CacheCtx.EventManager().Events()) - // NOTE: The NibiruBankKeeper needs to reference the current [vm.StateDB] before - // any operation that has the potential to use Bank send methods. This will - // guarantee that [evmkeeper.Keeper.SetAccBalance] journal changes are - // recorded if wei (NIBI) is transferred. - p.Bank.StateDB = startResult.StateDB switch PrecompileMethod(startResult.Method.Name) { case WasmMethod_execute: bz, err = p.execute(startResult, contract.CallerAddress, readonly)