diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bafaabd1..ffb5e6200 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -79,6 +79,7 @@ needed to include double quotes around the hexadecimal string. - [#2162](https://github.com/NibiruChain/nibiru/pull/2162) - test(testutil): try retrying for 'panic: pebbledb: closed' - [#2167](https://github.com/NibiruChain/nibiru/pull/2167) - refactor(evm): removed blockGasUsed transient variable - [#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 #### Nibiru EVM | Before Audit 2 - 2024-12-06 @@ -346,7 +347,6 @@ Nibiru v1.3.0 adds interchain accounts. - [#1859](https://github.com/NibiruChain/nibiru/pull/1859) - refactor(oracle): add oracle slashing events - --- [LEGACY CHANGELOG](./LEGACY-CHANGELOG.md) diff --git a/app/evmante/evmante_can_transfer.go b/app/evmante/evmante_can_transfer.go index 0f8cd0d06..f47e7f0b1 100644 --- a/app/evmante/evmante_can_transfer.go +++ b/app/evmante/evmante_can_transfer.go @@ -7,11 +7,10 @@ import ( "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" - gethcommon "github.com/ethereum/go-ethereum/common" gethcore "github.com/ethereum/go-ethereum/core/types" + "github.com/NibiruChain/nibiru/v2/eth" "github.com/NibiruChain/nibiru/v2/x/evm" - "github.com/NibiruChain/nibiru/v2/x/evm/statedb" ) // CanTransferDecorator checks if the sender is allowed to transfer funds according to the EVM block @@ -25,7 +24,6 @@ type CanTransferDecorator struct { func (ctd CanTransferDecorator) AnteHandle( ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler, ) (sdk.Context, error) { - params := ctd.GetParams(ctx) ethCfg := evm.EthereumConfig(ctd.EVMKeeper.EthChainID(ctx)) signer := gethcore.MakeSigner(ethCfg, big.NewInt(ctx.BlockHeight())) @@ -40,7 +38,7 @@ func (ctd CanTransferDecorator) AnteHandle( baseFeeWeiPerGas := evm.NativeToWei(ctd.EVMKeeper.BaseFeeMicronibiPerGas(ctx)) - coreMsg, err := msgEthTx.AsMessage(signer, baseFeeWeiPerGas) + evmMsg, err := msgEthTx.AsMessage(signer, baseFeeWeiPerGas) if err != nil { return ctx, errors.Wrapf( err, @@ -59,37 +57,27 @@ func (ctd CanTransferDecorator) AnteHandle( return ctx, errors.Wrapf( sdkerrors.ErrInsufficientFee, "gas fee cap (wei) less than block base fee (wei); (%s < %s)", - coreMsg.GasFeeCap(), baseFeeWeiPerGas, + evmMsg.GasFeeCap(), baseFeeWeiPerGas, ) } - cfg := &statedb.EVMConfig{ - ChainConfig: ethCfg, - Params: params, - // Note that we use an empty coinbase here because the field is not - // used during this Ante Handler. - BlockCoinbase: gethcommon.Address{}, - BaseFeeWei: baseFeeWeiPerGas, - } - - stateDB := ctd.NewStateDB( - ctx, - statedb.NewEmptyTxConfig(gethcommon.BytesToHash(ctx.HeaderHash().Bytes())), - ) - evmInstance := ctd.EVMKeeper.NewEVM(ctx, coreMsg, cfg, evm.NewNoOpTracer(), stateDB) - // check that caller has enough balance to cover asset transfer for **topmost** call // NOTE: here the gas consumed is from the context with the infinite gas meter - if coreMsg.Value().Sign() > 0 && - !evmInstance.Context.CanTransfer(stateDB, coreMsg.From(), coreMsg.Value()) { - balanceWei := stateDB.GetBalance(coreMsg.From()) - return ctx, errors.Wrapf( - sdkerrors.ErrInsufficientFunds, - "failed to transfer %s wei (balance=%s) from address %s using the EVM block context transfer function", - coreMsg.Value(), - balanceWei, - coreMsg.From(), - ) + + if evmMsg.Value().Sign() > 0 { + nibiruAddr := eth.EthAddrToNibiruAddr(evmMsg.From()) + balanceNative := ctd.Bank.GetBalance(ctx, nibiruAddr, evm.EVMBankDenom).Amount.BigInt() + balanceWei := evm.NativeToWei(balanceNative) + + if balanceWei.Cmp(evmMsg.Value()) < 0 { + return ctx, errors.Wrapf( + sdkerrors.ErrInsufficientFunds, + "failed to transfer %s wei ( balance=%s )from address %s using the EVM block context transfer function", + evmMsg.Value(), + balanceWei, + evmMsg.From(), + ) + } } } diff --git a/app/evmante/evmante_can_transfer_test.go b/app/evmante/evmante_can_transfer_test.go index 381597624..cf3408484 100644 --- a/app/evmante/evmante_can_transfer_test.go +++ b/app/evmante/evmante_can_transfer_test.go @@ -14,9 +14,8 @@ import ( func (s *TestSuite) TestCanTransferDecorator() { testCases := []struct { name string - txSetup func(deps *evmtest.TestDeps) sdk.FeeTx - ctxSetup func(deps *evmtest.TestDeps) beforeTxSetup func(deps *evmtest.TestDeps, sdb *statedb.StateDB) + txSetup func(deps *evmtest.TestDeps) sdk.FeeTx wantErr string }{ { @@ -92,9 +91,6 @@ func (s *TestSuite) TestCanTransferDecorator() { anteDec := evmante.CanTransferDecorator{deps.App.AppKeepers.EvmKeeper} tx := tc.txSetup(&deps) - if tc.ctxSetup != nil { - tc.ctxSetup(&deps) - } if tc.beforeTxSetup != nil { tc.beforeTxSetup(&deps, stateDB) err := stateDB.Commit() diff --git a/eth/rpc/backend/backend_suite_test.go b/eth/rpc/backend/backend_suite_test.go index 86b4dd0e0..dec06920f 100644 --- a/eth/rpc/backend/backend_suite_test.go +++ b/eth/rpc/backend/backend_suite_test.go @@ -102,7 +102,7 @@ func (s *BackendSuite) SetupSuite() { // Send Transfer TX and use the results in the tests s.Require().NoError(err) - transferTxHash = s.SendNibiViaEthTransfer(recipient, amountToSend, true) + transferTxHash = s.SendNibiViaEthTransfer(recipient, amountToSend, true /*waitForNextBlock*/) blockNumber, blockHash, _ := WaitForReceipt(s, transferTxHash) s.Require().NotNil(blockNumber) s.Require().NotNil(blockHash) @@ -151,7 +151,7 @@ func (s *BackendSuite) DeployTestContract(waitForNextBlock bool) (gethcommon.Has &gethcore.LegacyTx{ Nonce: uint64(nonce), Data: bytecodeForCall, - Gas: 1500_000, + Gas: 1_500_000, GasPrice: big.NewInt(1), }, waitForNextBlock, @@ -177,7 +177,7 @@ func SendTransaction(s *BackendSuite, tx *gethcore.LegacyTx, waitForNextBlock bo // WaitForReceipt waits for a transaction to be included in a block, returns block number, block hash and receipt func WaitForReceipt(s *BackendSuite, txHash gethcommon.Hash) (*big.Int, *gethcommon.Hash, *backend.TransactionReceipt) { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) defer cancel() for { diff --git a/eth/rpc/backend/gas_used_test.go b/eth/rpc/backend/gas_used_test.go index c5b06983c..0ad3ed0c3 100644 --- a/eth/rpc/backend/gas_used_test.go +++ b/eth/rpc/backend/gas_used_test.go @@ -154,10 +154,6 @@ func (s *BackendSuite) TestGasUsedFunTokens() { s.Require().NotNil(blockNumber2) s.Require().NotNil(blockNumber3) - // TXs should have been included in the same block - s.Require().Equal(blockNumber1, blockNumber2) - s.Require().Equal(blockNumber2, blockNumber3) - // 1 and 3 should pass and 2 should fail s.Require().Equal(gethcore.ReceiptStatusSuccessful, receipt1.Status) s.Require().Equal(gethcore.ReceiptStatusFailed, receipt2.Status) diff --git a/eth/rpc/backend/utils.go b/eth/rpc/backend/utils.go index bb689bcce..3ea0f710a 100644 --- a/eth/rpc/backend/utils.go +++ b/eth/rpc/backend/utils.go @@ -270,7 +270,9 @@ func ShouldIgnoreGasUsed(res *abci.ResponseDeliverTx) bool { } // GetLogsFromBlockResults returns the list of event logs from the tendermint block result response -func GetLogsFromBlockResults(blockRes *tmrpctypes.ResultBlockResults) ([][]*gethcore.Log, error) { +func GetLogsFromBlockResults( + blockRes *tmrpctypes.ResultBlockResults, +) ([][]*gethcore.Log, error) { blockLogs := [][]*gethcore.Log{} for _, txResult := range blockRes.TxsResults { logs, err := AllTxLogsFromEvents(txResult.Events) diff --git a/gosdk/gosdk_test.go b/gosdk/gosdk_test.go index 36acc1488..df1e6d039 100644 --- a/gosdk/gosdk_test.go +++ b/gosdk/gosdk_test.go @@ -73,25 +73,26 @@ func (s *TestSuite) ConnectGrpc() { s.grpcConn = grpcConn } -func (s *TestSuite) DoTestNewQueryClient() { - _, err := gosdk.NewQuerier(s.grpcConn) - s.NoError(err) -} - func (s *TestSuite) TestNewNibiruSdk() { rpcEndpt := s.val.RPCAddress nibiruSdk, err := gosdk.NewNibiruSdk(s.cfg.ChainID, s.grpcConn, rpcEndpt) s.NoError(err) s.nibiruSdk = &nibiruSdk - s.nibiruSdk.Keyring = s.val.ClientCtx.Keyring - s.Run("DoTestBroadcastMsgs", func() { s.DoTestBroadcastMsgs() }) + s.Run("DoTestBroadcastMsgs", func() { + s.DoTestBroadcastMsgs() + }) s.Run("DoTestBroadcastMsgsGrpc", func() { - s.NoError(s.network.WaitForNextBlock()) + for t := 0; t < 4; t++ { + s.NoError(s.network.WaitForNextBlock()) + } s.DoTestBroadcastMsgsGrpc() }) - s.Run("DoTestNewQueryClient", s.DoTestNewQueryClient) + s.Run("DoTestNewQueryClient", func() { + _, err := gosdk.NewQuerier(s.grpcConn) + s.NoError(err) + }) } // FIXME: Q: What is the node home for a local validator? diff --git a/x/common/testutil/testnetwork/network.go b/x/common/testutil/testnetwork/network.go index 53509c4cf..6cdc13feb 100644 --- a/x/common/testutil/testnetwork/network.go +++ b/x/common/testutil/testnetwork/network.go @@ -494,7 +494,7 @@ func (n *Network) LatestHeight() (int64, error) { // committed after a given block. If that height is not reached within a timeout, // an error is returned. Regardless, the latest height queried is returned. func (n *Network) WaitForHeight(h int64) (int64, error) { - return n.WaitForHeightWithTimeout(h, 40*time.Second) + return n.WaitForHeightWithTimeout(h, 5*time.Minute) } // WaitForHeightWithTimeout is the same as WaitForHeight except the caller can diff --git a/x/evm/evmmodule/genesis_test.go b/x/evm/evmmodule/genesis_test.go index cc9d8c247..a0a9b6d23 100644 --- a/x/evm/evmmodule/genesis_test.go +++ b/x/evm/evmmodule/genesis_test.go @@ -23,8 +23,7 @@ type Suite struct { // TestKeeperSuite: Runs all the tests in the suite. func TestKeeperSuite(t *testing.T) { - s := new(Suite) - suite.Run(t, s) + suite.Run(t, new(Suite)) } // TestExportInitGenesis @@ -48,21 +47,23 @@ func (s *Suite) TestExportInitGenesis() { deployResp, err := evmtest.DeployContract(&deps, erc20Contract) s.Require().NoError(err) erc20Addr := deployResp.ContractAddr + + evmObj, _ := deps.NewEVM() totalSupply, err := deps.EvmKeeper.ERC20().LoadERC20BigInt( - deps.Ctx, erc20Contract.ABI, erc20Addr, "totalSupply", + deps.Ctx, evmObj, erc20Contract.ABI, erc20Addr, "totalSupply", ) s.Require().NoError(err) // Transfer ERC-20 tokens to user A - _, _, err = deps.EvmKeeper.ERC20().Transfer(erc20Addr, fromUser, toUserA, amountToSendA, deps.Ctx) + _, _, err = deps.EvmKeeper.ERC20().Transfer(erc20Addr, fromUser, toUserA, amountToSendA, deps.Ctx, evmObj) s.Require().NoError(err) // Transfer ERC-20 tokens to user B - _, _, err = deps.EvmKeeper.ERC20().Transfer(erc20Addr, fromUser, toUserB, amountToSendB, deps.Ctx) + _, _, err = deps.EvmKeeper.ERC20().Transfer(erc20Addr, fromUser, toUserB, amountToSendB, deps.Ctx, evmObj) s.Require().NoError(err) // Create fungible token from bank coin - funToken := evmtest.CreateFunTokenForBankCoin(&deps, "unibi", &s.Suite) + funToken := evmtest.CreateFunTokenForBankCoin(deps, "unibi", &s.Suite) s.Require().NoError(err) funTokenAddr := funToken.Erc20Addr.Address @@ -98,15 +99,15 @@ func (s *Suite) TestExportInitGenesis() { evmmodule.InitGenesis(deps.Ctx, deps.EvmKeeper, deps.App.AccountKeeper, *evmGenesisState) // Verify erc20 balances for users A, B and sender - balance, err := deps.EvmKeeper.ERC20().BalanceOf(erc20Addr, toUserA, deps.Ctx) + balance, err := deps.EvmKeeper.ERC20().BalanceOf(erc20Addr, toUserA, deps.Ctx, evmObj) s.Require().NoError(err) s.Require().Equal(amountToSendA, balance) - balance, err = deps.EvmKeeper.ERC20().BalanceOf(erc20Addr, toUserB, deps.Ctx) + balance, err = deps.EvmKeeper.ERC20().BalanceOf(erc20Addr, toUserB, deps.Ctx, evmObj) s.Require().NoError(err) s.Require().Equal(amountToSendB, balance) - balance, err = deps.EvmKeeper.ERC20().BalanceOf(erc20Addr, fromUser, deps.Ctx) + balance, err = deps.EvmKeeper.ERC20().BalanceOf(erc20Addr, fromUser, deps.Ctx, evmObj) s.Require().NoError(err) s.Require().Equal( new(big.Int).Sub(totalSupply, big.NewInt(amountToSendA.Int64()+amountToSendB.Int64())), @@ -122,7 +123,7 @@ func (s *Suite) TestExportInitGenesis() { s.Require().True(funTokens[0].IsMadeFromCoin) // Check that fungible token balance of user C is correct - balance, err = deps.EvmKeeper.ERC20().BalanceOf(funTokenAddr, toUserC, deps.Ctx) + balance, err = deps.EvmKeeper.ERC20().BalanceOf(funTokenAddr, toUserC, deps.Ctx, evmObj) s.Require().NoError(err) s.Require().Equal(amountToSendC, balance) } diff --git a/x/evm/evmtest/erc20.go b/x/evm/evmtest/erc20.go index df7751d00..feb426209 100644 --- a/x/evm/evmtest/erc20.go +++ b/x/evm/evmtest/erc20.go @@ -7,6 +7,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" bank "github.com/cosmos/cosmos-sdk/x/bank/types" gethcommon "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" @@ -15,19 +16,52 @@ import ( "github.com/NibiruChain/nibiru/v2/x/evm" ) -func AssertERC20BalanceEqual( +func AssertERC20BalanceEqualWithDescription( t *testing.T, deps TestDeps, + evmObj *vm.EVM, erc20, account gethcommon.Address, expectedBalance *big.Int, + description string, ) { - AssertERC20BalanceEqualWithDescription(t, deps, erc20, account, expectedBalance, "") + actualBalance, err := deps.EvmKeeper.ERC20().BalanceOf(erc20, account, deps.Ctx, evmObj) + var errSuffix string + if description == "" { + errSuffix = description + } else { + errSuffix = ": " + description + } + assert.NoError(t, err, errSuffix) + assert.Equalf(t, expectedBalance.String(), actualBalance.String(), + "expected %s, got %s: %s", expectedBalance, actualBalance, errSuffix, + ) +} + +func AssertBankBalanceEqualWithDescription( + t *testing.T, + deps TestDeps, + denom string, + account gethcommon.Address, + expectedBalance *big.Int, + description string, +) { + bech32Addr := eth.EthAddrToNibiruAddr(account) + actualBalance := deps.App.BankKeeper.GetBalance(deps.Ctx, bech32Addr, denom).Amount.BigInt() + var errSuffix string + if description == "" { + errSuffix = description + } else { + errSuffix = ": " + description + } + assert.Equalf(t, expectedBalance.String(), actualBalance.String(), + "expected %s, got %s: %s", expectedBalance, actualBalance, errSuffix, + ) } // CreateFunTokenForBankCoin: Uses the "TestDeps.Sender" account to create a // "FunToken" mapping for a new coin func CreateFunTokenForBankCoin( - deps *TestDeps, bankDenom string, s *suite.Suite, + deps TestDeps, bankDenom string, s *suite.Suite, ) (funtoken evm.FunToken) { if deps.App.BankKeeper.HasDenomMetaData(deps.Ctx, bankDenom) { s.Failf("setting bank.DenomMetadata would overwrite existing denom \"%s\"", bankDenom) @@ -86,18 +120,6 @@ func CreateFunTokenForBankCoin( return funtoken } -func AssertBankBalanceEqual( - t *testing.T, - deps TestDeps, - denom string, - account gethcommon.Address, - expectedBalance *big.Int, -) { - AssertBankBalanceEqualWithDescription( - t, deps, denom, account, expectedBalance, "", - ) -} - // BigPow multiplies "amount" by 10 to the "pow10Exp". func BigPow(amount *big.Int, pow10Exp uint8) (powAmount *big.Int) { pow10 := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(pow10Exp)), nil) @@ -112,9 +134,9 @@ type FunTokenBalanceAssert struct { Description string } -func (bals FunTokenBalanceAssert) Assert(t *testing.T, deps TestDeps) { +func (bals FunTokenBalanceAssert) Assert(t *testing.T, deps TestDeps, evmObj *vm.EVM) { AssertERC20BalanceEqualWithDescription( - t, deps, bals.FunToken.Erc20Addr.Address, bals.Account, bals.BalanceERC20, + t, deps, evmObj, bals.FunToken.Erc20Addr.Address, bals.Account, bals.BalanceERC20, bals.Description, ) AssertBankBalanceEqualWithDescription( @@ -123,47 +145,6 @@ func (bals FunTokenBalanceAssert) Assert(t *testing.T, deps TestDeps) { ) } -func AssertERC20BalanceEqualWithDescription( - t *testing.T, - deps TestDeps, - erc20, account gethcommon.Address, - expectedBalance *big.Int, - description string, -) { - actualBalance, err := deps.EvmKeeper.ERC20().BalanceOf(erc20, account, deps.Ctx) - var errSuffix string - if description == "" { - errSuffix = description - } else { - errSuffix = ": " + description - } - assert.NoError(t, err, errSuffix) - assert.Equalf(t, expectedBalance.String(), actualBalance.String(), - "expected %s, got %s", expectedBalance, actualBalance, - errSuffix, - ) -} - -func AssertBankBalanceEqualWithDescription( - t *testing.T, - deps TestDeps, - denom string, - account gethcommon.Address, - expectedBalance *big.Int, - description string, -) { - bech32Addr := eth.EthAddrToNibiruAddr(account) - actualBalance := deps.App.BankKeeper.GetBalance(deps.Ctx, bech32Addr, denom).Amount.BigInt() - var errSuffix string - if description == "" { - errSuffix = description - } else { - errSuffix = ": " + description - } - assert.Equalf(t, expectedBalance.String(), actualBalance.String(), - "expected %s, got %s", expectedBalance, actualBalance, errSuffix) -} - const ( // FunTokenGasLimitSendToEvm consists of gas for 3 calls: // 1. transfer erc20 from sender to module diff --git a/x/evm/evmtest/eth_test.go b/x/evm/evmtest/eth_test.go index 1d5feee47..acd49ddfd 100644 --- a/x/evm/evmtest/eth_test.go +++ b/x/evm/evmtest/eth_test.go @@ -2,7 +2,6 @@ package evmtest_test import ( - "math/big" "testing" "github.com/stretchr/testify/suite" @@ -19,27 +18,11 @@ func TestSuiteEVM(t *testing.T) { } func (s *Suite) TestSampleFns() { - s.T().Log("Test NewEthTxMsg") - ethTxMsg := evmtest.NewEthTxMsgs(1)[0] - err := ethTxMsg.ValidateBasic() - s.NoError(err) - s.T().Log("Test NewEthTxMsgs") for _, ethTxMsg := range evmtest.NewEthTxMsgs(3) { s.NoError(ethTxMsg.ValidateBasic()) } - s.T().Log("Test NewEthTxMsgs") + s.T().Log("Test NewEthTxMsgAsCmt") _, _, _ = evmtest.NewEthTxMsgAsCmt(s.T()) } - -func (s *Suite) TestERC20Helpers() { - deps := evmtest.NewTestDeps() - funtoken := evmtest.CreateFunTokenForBankCoin(&deps, "token", &s.Suite) - evmtest.FunTokenBalanceAssert{ - FunToken: funtoken, - Account: deps.Sender.EthAddr, - BalanceBank: big.NewInt(0), - BalanceERC20: big.NewInt(0), - }.Assert(s.T(), deps) -} diff --git a/x/evm/evmtest/evmante.go b/x/evm/evmtest/evmante.go index 005c88b35..6a72f3718 100644 --- a/x/evm/evmtest/evmante.go +++ b/x/evm/evmtest/evmante.go @@ -20,7 +20,7 @@ var NextNoOpAnteHandler sdk.AnteHandler = func( func HappyTransferTx(deps *TestDeps, nonce uint64) *evm.MsgEthereumTx { to := NewEthPrivAcc().EthAddr - ethContractCreationTxParams := &evm.EvmTxArgs{ + evmTxArgs := &evm.EvmTxArgs{ ChainID: deps.App.EvmKeeper.EthChainID(deps.Ctx), Nonce: nonce, Amount: big.NewInt(10), @@ -28,7 +28,7 @@ func HappyTransferTx(deps *TestDeps, nonce uint64) *evm.MsgEthereumTx { GasPrice: evm.NativeToWei(big.NewInt(1)), To: &to, } - tx := evm.NewTx(ethContractCreationTxParams) + tx := evm.NewTx(evmTxArgs) tx.From = deps.Sender.EthAddr.Hex() return tx } @@ -67,14 +67,15 @@ func BuildTx( } func HappyCreateContractTx(deps *TestDeps) *evm.MsgEthereumTx { - ethContractCreationTxParams := &evm.EvmTxArgs{ + evmTxArgs := &evm.EvmTxArgs{ ChainID: deps.App.EvmKeeper.EthChainID(deps.Ctx), Nonce: 1, Amount: big.NewInt(10), GasLimit: GasLimitCreateContract().Uint64(), GasPrice: evm.NativeToWei(big.NewInt(1)), + To: nil, } - tx := evm.NewTx(ethContractCreationTxParams) + tx := evm.NewTx(evmTxArgs) tx.From = deps.Sender.EthAddr.Hex() return tx } diff --git a/x/evm/evmtest/test_deps.go b/x/evm/evmtest/test_deps.go index 6d2e830af..36df35d8f 100644 --- a/x/evm/evmtest/test_deps.go +++ b/x/evm/evmtest/test_deps.go @@ -4,10 +4,9 @@ import ( "context" sdk "github.com/cosmos/cosmos-sdk/types" - gethcommon "github.com/ethereum/go-ethereum/common" - gethcore "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" "github.com/NibiruChain/nibiru/v2/app" "github.com/NibiruChain/nibiru/v2/app/codec" @@ -34,14 +33,13 @@ func NewTestDeps() TestDeps { eth.RegisterInterfaces(encCfg.InterfaceRegistry) app, ctx := testapp.NewNibiruTestAppAndContext() ctx = ctx.WithChainID(eth.EIP155ChainID_Testnet) - ethAcc := NewEthPrivAcc() return TestDeps{ App: app, Ctx: ctx, EncCfg: encCfg, EvmKeeper: app.EvmKeeper, GenState: evm.DefaultGenesisState(), - Sender: ethAcc, + Sender: NewEthPrivAcc(), } } @@ -49,11 +47,17 @@ func (deps TestDeps) NewStateDB() *statedb.StateDB { return deps.EvmKeeper.NewStateDB( deps.Ctx, statedb.NewEmptyTxConfig( - gethcommon.BytesToHash(deps.Ctx.HeaderHash().Bytes()), + gethcommon.BytesToHash(deps.Ctx.HeaderHash()), ), ) } +func (deps TestDeps) NewEVM() (*vm.EVM, *statedb.StateDB) { + stateDB := deps.EvmKeeper.NewStateDB(deps.Ctx, statedb.NewEmptyTxConfig(gethcommon.BytesToHash(deps.Ctx.HeaderHash()))) + evmObj := deps.EvmKeeper.NewEVM(deps.Ctx, MOCK_GETH_MESSAGE, deps.EvmKeeper.GetEVMConfig(deps.Ctx), evm.NewNoOpTracer(), stateDB) + return evmObj, stateDB +} + func (deps *TestDeps) GethSigner() gethcore.Signer { return gethcore.LatestSignerForChainID(deps.App.EvmKeeper.EthChainID(deps.Ctx)) } diff --git a/x/evm/evmtest/tx.go b/x/evm/evmtest/tx.go index d68d16621..440e83556 100644 --- a/x/evm/evmtest/tx.go +++ b/x/evm/evmtest/tx.go @@ -66,7 +66,7 @@ func DeployContract( } bytecodeForCall := append(contract.Bytecode, packedArgs...) - nonce := deps.NewStateDB().GetNonce(deps.Sender.EthAddr) + nonce := deps.EvmKeeper.GetAccNonce(deps.Ctx, deps.Sender.EthAddr) ethTxMsg, gethSigner, krSigner, err := GenerateEthTxMsgAndSigner( evm.JsonTxArgs{ Nonce: (*hexutil.Uint64)(&nonce), @@ -76,11 +76,12 @@ func DeployContract( ) if err != nil { return nil, errors.Wrap(err, "failed to generate and sign eth tx msg") - } else if err := ethTxMsg.Sign(gethSigner, krSigner); err != nil { + } + if err := ethTxMsg.Sign(gethSigner, krSigner); err != nil { return nil, errors.Wrap(err, "failed to generate and sign eth tx msg") } - resp, err := deps.App.EvmKeeper.EthereumTx(sdk.WrapSDKContext(deps.Ctx), ethTxMsg) + resp, err := deps.EvmKeeper.EthereumTx(sdk.WrapSDKContext(deps.Ctx), ethTxMsg) if err != nil { return nil, errors.Wrap(err, "failed to execute ethereum tx") } @@ -142,34 +143,6 @@ func DeployAndExecuteERC20Transfer( return erc20Transfer, predecessors, contractAddr } -func CallContractTx( - deps *TestDeps, - contractAddr gethcommon.Address, - input []byte, - sender EthPrivKeyAcc, -) (ethTxMsg *evm.MsgEthereumTx, resp *evm.MsgEthereumTxResponse, err error) { - nonce := deps.NewStateDB().GetNonce(sender.EthAddr) - ethTxMsg, gethSigner, krSigner, err := GenerateEthTxMsgAndSigner(evm.JsonTxArgs{ - From: &sender.EthAddr, - To: &contractAddr, - Nonce: (*hexutil.Uint64)(&nonce), - Data: (*hexutil.Bytes)(&input), - }, deps, sender) - if err != nil { - err = fmt.Errorf("CallContract error during tx generation: %w", err) - return - } - - err = ethTxMsg.Sign(gethSigner, krSigner) - if err != nil { - err = fmt.Errorf("CallContract error during signature: %w", err) - return - } - - resp, err = deps.EvmKeeper.EthereumTx(deps.GoCtx(), ethTxMsg) - return ethTxMsg, resp, err -} - var DefaultEthCallGasLimit = srvconfig.DefaultEthCallGasLimit // GenerateEthTxMsgAndSigner estimates gas, sets gas limit and returns signer for @@ -230,7 +203,7 @@ func (tx TxTransferWei) Build() (evmTxMsg *evm.MsgEthereumTx, err error) { deps, gethcore.LegacyTxType, innerTxData, - deps.NewStateDB().GetNonce(ethAcc.EthAddr), + deps.EvmKeeper.GetAccNonce(deps.Ctx, ethAcc.EthAddr), &to, amountWei, gasLimit, @@ -385,3 +358,17 @@ func NewEthTxMsgFromTxData( ethTxMsg.From = deps.Sender.EthAddr.Hex() return ethTxMsg, ethTxMsg.Sign(deps.GethSigner(), deps.Sender.KeyringSigner) } + +var MOCK_GETH_MESSAGE = gethcore.NewMessage( + evm.EVM_MODULE_ADDRESS, + nil, + 0, + big.NewInt(0), + 0, + big.NewInt(0), + big.NewInt(0), + big.NewInt(0), + []byte{}, + gethcore.AccessList{}, + false, +) diff --git a/x/evm/evmtest/tx_test.go b/x/evm/evmtest/tx_test.go index 33e2e7140..63cd7573d 100644 --- a/x/evm/evmtest/tx_test.go +++ b/x/evm/evmtest/tx_test.go @@ -5,66 +5,12 @@ import ( "math/big" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/ethereum/go-ethereum/crypto" "github.com/NibiruChain/nibiru/v2/x/common/testutil/testapp" "github.com/NibiruChain/nibiru/v2/x/evm" - "github.com/NibiruChain/nibiru/v2/x/evm/embeds" "github.com/NibiruChain/nibiru/v2/x/evm/evmtest" ) -func (s *Suite) TestCallContractTx() { - deps := evmtest.NewTestDeps() - - s.T().Log("Deploy some ERC20") - deployArgs := []any{"name", "SYMBOL", uint8(18)} - deployResp, err := evmtest.DeployContract( - &deps, - embeds.SmartContract_ERC20Minter, - deployArgs..., - ) - s.Require().NoError(err, deployResp) - contractAddr := crypto.CreateAddress(deps.Sender.EthAddr, deployResp.Nonce) - gotContractAddr := deployResp.ContractAddr - s.Require().Equal(contractAddr, gotContractAddr) - - s.T().Log("expect zero balance") - { - wantBal := big.NewInt(0) - evmtest.AssertERC20BalanceEqual( - s.T(), deps, contractAddr, deps.Sender.EthAddr, wantBal, - ) - } - - abi := deployResp.ContractData.ABI - s.T().Log("mint some tokens") - { - amount := big.NewInt(69_420) - to := deps.Sender.EthAddr - callArgs := []any{to, amount} - input, err := abi.Pack( - "mint", callArgs..., - ) - s.Require().NoError(err) - _, resp, err := evmtest.CallContractTx( - &deps, - contractAddr, - input, - deps.Sender, - ) - s.Require().NoError(err) - s.Require().Empty(resp.VmError) - } - - s.T().Log("expect nonzero balance") - { - wantBal := big.NewInt(69_420) - evmtest.AssertERC20BalanceEqual( - s.T(), deps, contractAddr, deps.Sender.EthAddr, wantBal, - ) - } -} - func (s *Suite) TestTransferWei() { deps := evmtest.NewTestDeps() @@ -85,8 +31,8 @@ func (s *Suite) TestTransferWei() { s.Require().NoErrorf(err, "%#v", evmResp) s.False(evmResp.Failed(), "%#v", evmResp) - evmtest.AssertBankBalanceEqual( - s.T(), deps, evm.EVMBankDenom, deps.Sender.EthAddr, big.NewInt(69_000), + evmtest.AssertBankBalanceEqualWithDescription( + s.T(), deps, evm.EVMBankDenom, deps.Sender.EthAddr, big.NewInt(69_000), "expect nonzero balance", ) s.Run("DeployAndExecuteERC20Transfer", func() { diff --git a/x/evm/keeper/bank_extension_test.go b/x/evm/keeper/bank_extension_test.go index b5b6a999d..7ac29daea 100644 --- a/x/evm/keeper/bank_extension_test.go +++ b/x/evm/keeper/bank_extension_test.go @@ -328,18 +328,13 @@ func (s *Suite) TestStateDBReadonlyInvariant() { type StateDBWithExplanation struct { StateDB *statedb.StateDB Explanation string - ExpectEqual bool } var stateDBs []StateDBWithExplanation stateDBs = append(stateDBs, StateDBWithExplanation{ StateDB: deps.App.EvmKeeper.Bank.StateDB, Explanation: "initial DB after some EthereumTx", - ExpectEqual: true, }) - resetBank := func(deps *evmtest.TestDeps) { - deps.App.EvmKeeper.Bank.StateDB = stateDBs[0].StateDB - } s.T().Log("eth_call") { @@ -355,7 +350,6 @@ func (s *Suite) TestStateDBReadonlyInvariant() { stateDBs = append(stateDBs, StateDBWithExplanation{ StateDB: deps.App.EvmKeeper.Bank.StateDB, Explanation: "DB after eth_call query", - ExpectEqual: true, }) } @@ -364,27 +358,20 @@ func (s *Suite) TestStateDBReadonlyInvariant() { balOfSender := deps.App.BankKeeper.GetBalance( deps.Ctx, deps.Sender.NibiruAddr, evm.EVMBankDenom) tooManyTokensWei := evm.NativeToWei(balOfSender.Amount.AddRaw(420).BigInt()) - evmResp, err := evmtest.TxTransferWei{ + txTransferWei := evmtest.TxTransferWei{ Deps: &deps, To: to.EthAddr, AmountWei: tooManyTokensWei, - }.Run() + } + evmResp, err := txTransferWei.Run() s.Require().NoErrorf(err, "%#v", evmResp) s.Require().Contains(evmResp.VmError, "insufficient balance for transfer") stateDBs = append(stateDBs, StateDBWithExplanation{ StateDB: deps.App.EvmKeeper.Bank.StateDB, Explanation: "DB after EthereumTx with vmError", - ExpectEqual: false, }) } - resetBank(&deps) - stateDBs = append(stateDBs, StateDBWithExplanation{ - StateDB: deps.App.EvmKeeper.Bank.StateDB, - Explanation: "sanity check with original ctx", - ExpectEqual: true, - }) - s.T().Log(`EthereumTx success, err == nil, no vmError"`) { sendCoins := sdk.NewCoins(sdk.NewInt64Coin(evm.EVMBankDenom, 420)) @@ -408,7 +395,6 @@ func (s *Suite) TestStateDBReadonlyInvariant() { stateDBs = append(stateDBs, StateDBWithExplanation{ StateDB: deps.App.EvmKeeper.Bank.StateDB, Explanation: "DB after EthereumTx success", - ExpectEqual: false, }) for _, err := range []error{ @@ -436,10 +422,6 @@ func (s *Suite) TestStateDBReadonlyInvariant() { first = db.StateDB continue } - if db.ExpectEqual { - s.True(first == db.StateDB, db.Explanation) - continue - } - s.False(first == db.StateDB, db.Explanation) + s.True(first == db.StateDB, db.Explanation) } } diff --git a/x/evm/keeper/call_contract.go b/x/evm/keeper/call_contract.go index 8bf199785..e38bbd4a2 100644 --- a/x/evm/keeper/call_contract.go +++ b/x/evm/keeper/call_contract.go @@ -7,7 +7,6 @@ import ( "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" - gethabi "github.com/ethereum/go-ethereum/accounts/abi" gethcommon "github.com/ethereum/go-ethereum/common" gethcore "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" @@ -15,38 +14,6 @@ import ( "github.com/NibiruChain/nibiru/v2/x/evm" ) -// CallContract invokes a smart contract on the method specified by [methodName] -// using the given [args]. -// -// Parameters: -// - ctx: The SDK context for the transaction. -// - abi: The ABI (Application Binary Interface) of the smart contract. -// - fromAcc: The Ethereum address of the account initiating the contract call. -// - contract: Pointer to the Ethereum address of the contract to be called. -// - commit: Boolean flag indicating whether to commit the transaction (true) or simulate it (false). -// - methodName: The name of the contract method to be called. -// - args: Variadic parameter for the arguments to be passed to the contract method. -// -// Note: This function handles both contract method calls and simulations, -// depending on the 'commit' parameter. -func (k Keeper) CallContract( - ctx sdk.Context, - abi *gethabi.ABI, - fromAcc gethcommon.Address, - contract *gethcommon.Address, - commit bool, - gasLimit uint64, - methodName string, - args ...any, -) (evmResp *evm.MsgEthereumTxResponse, err error) { - contractInput, err := abi.Pack(methodName, args...) - if err != nil { - return nil, fmt.Errorf("failed to pack ABI args: %w", err) - } - evmResp, _, err = k.CallContractWithInput(ctx, fromAcc, contract, commit, contractInput, gasLimit) - return evmResp, err -} - // CallContractWithInput invokes a smart contract with the given [contractInput] // or deploys a new contract. // @@ -63,12 +30,13 @@ func (k Keeper) CallContract( // depending on the 'commit' parameter. It uses a default gas limit. func (k Keeper) CallContractWithInput( ctx sdk.Context, + evmObj *vm.EVM, fromAcc gethcommon.Address, contract *gethcommon.Address, commit bool, contractInput []byte, gasLimit uint64, -) (evmResp *evm.MsgEthereumTxResponse, evmObj *vm.EVM, err error) { +) (evmResp *evm.MsgEthereumTxResponse, err error) { // This is a `defer` pattern to add behavior that runs in the case that the // error is non-nil, creating a concise way to add extra information. defer HandleOutOfGasPanic(&err, "CallContractError") @@ -89,23 +57,11 @@ func (k Keeper) CallContractWithInput( !commit, // isFake ) - // Apply EVM message - evmCfg, err := k.GetEVMConfig( - ctx, - sdk.ConsAddress(ctx.BlockHeader().ProposerAddress), - k.EthChainID(ctx), - ) - if err != nil { - err = errors.Wrapf(err, "failed to load EVM config") - return - } - // Generating TxConfig with an empty tx hash as there is no actual eth tx // sent by a user txConfig := k.TxConfig(ctx, gethcommon.BigToHash(big.NewInt(0))) - - evmResp, evmObj, err = k.ApplyEvmMsg( - ctx, evmMsg, evm.NewNoOpTracer(), commit, evmCfg, txConfig, true, + evmResp, err = k.ApplyEvmMsg( + ctx, evmMsg, evmObj, evm.NewNoOpTracer(), commit, txConfig.TxHash, true, ) if err != nil { err = errors.Wrap(err, "failed to apply ethereum core message") @@ -137,5 +93,5 @@ func (k Keeper) CallContractWithInput( // blockTxIdx := uint64(txConfig.TxIndex) + 1 // k.EvmState.BlockTxIndex.Set(ctx, blockTxIdx) } - return evmResp, evmObj, nil + return evmResp, nil } diff --git a/x/evm/keeper/erc20.go b/x/evm/keeper/erc20.go index ef8140f92..e91b3ef7f 100644 --- a/x/evm/keeper/erc20.go +++ b/x/evm/keeper/erc20.go @@ -10,6 +10,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" gethabi "github.com/ethereum/go-ethereum/accounts/abi" gethcommon "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" "github.com/NibiruChain/nibiru/v2/x/evm" "github.com/NibiruChain/nibiru/v2/x/evm/embeds" @@ -66,10 +67,14 @@ See [nibiru/x/evm/embeds]. [nibiru/x/evm/embeds]: https://github.com/NibiruChain/nibiru/v2/tree/main/x/evm/embeds */ func (e erc20Calls) Mint( - contract, from, to gethcommon.Address, amount *big.Int, - ctx sdk.Context, + erc20Contract, sender, recipient gethcommon.Address, amount *big.Int, + ctx sdk.Context, evmObj *vm.EVM, ) (evmResp *evm.MsgEthereumTxResponse, err error) { - return e.CallContract(ctx, e.ABI, from, &contract, true, getCallGasWithLimit(ctx, Erc20GasLimitExecute), "mint", to, amount) + contractInput, err := e.ABI.Pack("mint", recipient, amount) + if err != nil { + return nil, err + } + return e.CallContractWithInput(ctx, evmObj, sender, &erc20Contract, false /*commit*/, contractInput, getCallGasWithLimit(ctx, Erc20GasLimitExecute)) } /* @@ -83,15 +88,19 @@ Transfer implements "ERC20.transfer" ``` */ func (e erc20Calls) Transfer( - contract, from, to gethcommon.Address, amount *big.Int, - ctx sdk.Context, + erc20Contract, sender, recipient gethcommon.Address, amount *big.Int, + ctx sdk.Context, evmObj *vm.EVM, ) (balanceIncrease *big.Int, resp *evm.MsgEthereumTxResponse, err error) { - recipientBalanceBefore, err := e.BalanceOf(contract, to, ctx) + recipientBalanceBefore, err := e.BalanceOf(erc20Contract, recipient, ctx, evmObj) if err != nil { return balanceIncrease, nil, errors.Wrap(err, "failed to retrieve recipient balance") } - resp, err = e.CallContract(ctx, e.ABI, from, &contract, true, getCallGasWithLimit(ctx, Erc20GasLimitExecute), "transfer", to, amount) + contractInput, err := e.ABI.Pack("transfer", recipient, amount) + if err != nil { + return balanceIncrease, nil, err + } + resp, err = e.CallContractWithInput(ctx, evmObj, sender, &erc20Contract, false /*commit*/, contractInput, getCallGasWithLimit(ctx, Erc20GasLimitExecute)) if err != nil { return balanceIncrease, nil, err } @@ -108,7 +117,7 @@ func (e erc20Calls) Transfer( return balanceIncrease, nil, fmt.Errorf("transfer executed but returned success=false") } - recipientBalanceAfter, err := e.BalanceOf(contract, to, ctx) + recipientBalanceAfter, err := e.BalanceOf(erc20Contract, recipient, ctx, evmObj) if err != nil { return balanceIncrease, nil, errors.Wrap(err, "failed to retrieve recipient balance") } @@ -122,7 +131,7 @@ func (e erc20Calls) Transfer( if balanceIncrease.Sign() <= 0 { return balanceIncrease, nil, fmt.Errorf( "amount of ERC20 tokens received MUST be positive: the balance of recipient %s would've changed by %v for token %s", - to.Hex(), balanceIncrease.String(), contract.Hex(), + recipient.Hex(), balanceIncrease.String(), erc20Contract.Hex(), ) } @@ -133,9 +142,9 @@ func (e erc20Calls) Transfer( // Implements "ERC20.balanceOf". func (e erc20Calls) BalanceOf( contract, account gethcommon.Address, - ctx sdk.Context, + ctx sdk.Context, evmObj *vm.EVM, ) (out *big.Int, err error) { - return e.LoadERC20BigInt(ctx, e.ABI, contract, "balanceOf", account) + return e.LoadERC20BigInt(ctx, evmObj, e.ABI, contract, "balanceOf", account) } /* @@ -147,44 +156,53 @@ Burn implements "ERC20Burnable.burn" ``` */ func (e erc20Calls) Burn( - contract, from gethcommon.Address, amount *big.Int, - ctx sdk.Context, + erc20Contract, sender gethcommon.Address, amount *big.Int, + ctx sdk.Context, evmObj *vm.EVM, ) (evmResp *evm.MsgEthereumTxResponse, err error) { - return e.CallContract(ctx, e.ABI, from, &contract, true, getCallGasWithLimit(ctx, Erc20GasLimitExecute), "burn", amount) + contractInput, err := e.ABI.Pack("burn", amount) + if err != nil { + return nil, err + } + return e.CallContractWithInput(ctx, evmObj, sender, &erc20Contract, false /*commit*/, contractInput, getCallGasWithLimit(ctx, Erc20GasLimitExecute)) } -func (k Keeper) LoadERC20Name( - ctx sdk.Context, abi *gethabi.ABI, erc20 gethcommon.Address, +func (e erc20Calls) LoadERC20Name( + ctx sdk.Context, evmObj *vm.EVM, abi *gethabi.ABI, erc20 gethcommon.Address, ) (out string, err error) { - return k.LoadERC20String(ctx, abi, erc20, "name") + return e.loadERC20String(ctx, evmObj, abi, erc20, "name") } -func (k Keeper) LoadERC20Symbol( - ctx sdk.Context, abi *gethabi.ABI, erc20 gethcommon.Address, +func (e erc20Calls) LoadERC20Symbol( + ctx sdk.Context, evmObj *vm.EVM, abi *gethabi.ABI, erc20 gethcommon.Address, ) (out string, err error) { - return k.LoadERC20String(ctx, abi, erc20, "symbol") + return e.loadERC20String(ctx, evmObj, abi, erc20, "symbol") } -func (k Keeper) LoadERC20Decimals( - ctx sdk.Context, abi *gethabi.ABI, erc20 gethcommon.Address, +func (e erc20Calls) LoadERC20Decimals( + ctx sdk.Context, evmObj *vm.EVM, abi *gethabi.ABI, erc20 gethcommon.Address, ) (out uint8, err error) { - return k.loadERC20Uint8(ctx, abi, erc20, "decimals") + return e.loadERC20Uint8(ctx, evmObj, abi, erc20, "decimals") } -func (k Keeper) LoadERC20String( +func (e erc20Calls) loadERC20String( ctx sdk.Context, + evmObj *vm.EVM, erc20Abi *gethabi.ABI, erc20Contract gethcommon.Address, methodName string, ) (out string, err error) { - res, err := k.CallContract( + input, err := erc20Abi.Pack(methodName) + if err != nil { + return out, err + } + res, err := e.Keeper.CallContractWithInput( ctx, - erc20Abi, + evmObj, evm.EVM_MODULE_ADDRESS, &erc20Contract, false, + input, getCallGasWithLimit(ctx, Erc20GasLimitQuery), - methodName, ) if err != nil { return out, err @@ -200,19 +218,25 @@ func (k Keeper) LoadERC20String( return erc20Val.Value, err } -func (k Keeper) loadERC20Uint8( +func (e erc20Calls) loadERC20Uint8( ctx sdk.Context, + evmObj *vm.EVM, erc20Abi *gethabi.ABI, erc20Contract gethcommon.Address, methodName string, ) (out uint8, err error) { - res, err := k.CallContract( - ctx, erc20Abi, + input, err := erc20Abi.Pack(methodName) + if err != nil { + return out, err + } + res, err := e.Keeper.CallContractWithInput( + ctx, + evmObj, evm.EVM_MODULE_ADDRESS, &erc20Contract, false, + input, getCallGasWithLimit(ctx, Erc20GasLimitQuery), - methodName, ) if err != nil { return out, err @@ -228,22 +252,26 @@ func (k Keeper) loadERC20Uint8( return erc20Val.Value, err } -func (k Keeper) LoadERC20BigInt( +func (e erc20Calls) LoadERC20BigInt( ctx sdk.Context, + evmObj *vm.EVM, abi *gethabi.ABI, contract gethcommon.Address, methodName string, args ...any, ) (out *big.Int, err error) { - res, err := k.CallContract( + input, err := abi.Pack(methodName, args...) + if err != nil { + return nil, err + } + evmResp, err := e.Keeper.CallContractWithInput( ctx, - abi, + evmObj, evm.EVM_MODULE_ADDRESS, &contract, false, + input, getCallGasWithLimit(ctx, Erc20GasLimitQuery), - methodName, - args..., ) if err != nil { return nil, err @@ -251,7 +279,7 @@ func (k Keeper) LoadERC20BigInt( erc20BigInt := new(ERC20BigInt) err = abi.UnpackIntoInterface( - erc20BigInt, methodName, res.Ret, + erc20BigInt, methodName, evmResp.Ret, ) if err != nil { return nil, err diff --git a/x/evm/keeper/erc20_test.go b/x/evm/keeper/erc20_test.go index 9b9ef9715..18c50d77a 100644 --- a/x/evm/keeper/erc20_test.go +++ b/x/evm/keeper/erc20_test.go @@ -11,60 +11,93 @@ import ( func (s *Suite) TestERC20Calls() { deps := evmtest.NewTestDeps() bankDenom := "ibc/btc" - funtoken := evmtest.CreateFunTokenForBankCoin(&deps, bankDenom, &s.Suite) - contract := funtoken.Erc20Addr.Address + funtoken := evmtest.CreateFunTokenForBankCoin(deps, bankDenom, &s.Suite) + erc20 := funtoken.Erc20Addr.Address - s.T().Log("Mint tokens - Fail from non-owner") - { + s.Run("Mint tokens - Fail from non-owner", func() { + evmObj, _ := deps.NewEVM() _, err := deps.EvmKeeper.ERC20().Mint( - contract, deps.Sender.EthAddr, evm.EVM_MODULE_ADDRESS, - big.NewInt(69_420), deps.Ctx, + erc20, deps.Sender.EthAddr, evm.EVM_MODULE_ADDRESS, + big.NewInt(69_420), deps.Ctx, evmObj, ) s.ErrorContains(err, "Ownable: caller is not the owner") - } + }) - s.T().Log("Mint tokens - Success") - { - _, err := deps.EvmKeeper.ERC20().Mint(contract, evm.EVM_MODULE_ADDRESS, evm.EVM_MODULE_ADDRESS, big.NewInt(69_420), deps.Ctx) + s.Run("successfully mint 69420 tokens", func() { + evmObj, stateDB := deps.NewEVM() + _, err := deps.EvmKeeper.ERC20().Mint( + erc20, /*erc20Addr*/ + evm.EVM_MODULE_ADDRESS, /*sender*/ + evm.EVM_MODULE_ADDRESS, /*recipient*/ + big.NewInt(69_420), /*amount*/ + deps.Ctx, + evmObj, + ) s.Require().NoError(err) + s.Require().NoError(stateDB.Commit()) - evmtest.AssertERC20BalanceEqual(s.T(), deps, contract, deps.Sender.EthAddr, big.NewInt(0)) - evmtest.AssertERC20BalanceEqual(s.T(), deps, contract, evm.EVM_MODULE_ADDRESS, big.NewInt(69_420)) - } + evmtest.AssertERC20BalanceEqualWithDescription(s.T(), deps, evmObj, erc20, evm.EVM_MODULE_ADDRESS, big.NewInt(69_420), "expect 69420 tokens") + evmtest.AssertERC20BalanceEqualWithDescription(s.T(), deps, evmObj, erc20, deps.Sender.EthAddr, big.NewInt(0), "expect zero tokens") + }) - s.T().Log("Transfer - Not enough funds") - { - amt := big.NewInt(9_420) - _, _, err := deps.EvmKeeper.ERC20().Transfer(contract, deps.Sender.EthAddr, evm.EVM_MODULE_ADDRESS, amt, deps.Ctx) + s.Run("Transfer - Not enough funds", func() { + evmObj, _ := deps.NewEVM() + _, _, err := deps.EvmKeeper.ERC20().Transfer( + erc20, deps.Sender.EthAddr, evm.EVM_MODULE_ADDRESS, + big.NewInt(9_420), deps.Ctx, evmObj, + ) s.ErrorContains(err, "ERC20: transfer amount exceeds balance") // balances unchanged - evmtest.AssertERC20BalanceEqual(s.T(), deps, contract, deps.Sender.EthAddr, big.NewInt(0)) - evmtest.AssertERC20BalanceEqual(s.T(), deps, contract, evm.EVM_MODULE_ADDRESS, big.NewInt(69_420)) - } + evmtest.AssertERC20BalanceEqualWithDescription(s.T(), deps, evmObj, erc20, evm.EVM_MODULE_ADDRESS, big.NewInt(69_420), "expect nonzero balance") + evmtest.AssertERC20BalanceEqualWithDescription(s.T(), deps, evmObj, erc20, deps.Sender.EthAddr, big.NewInt(0), "expect zero balance") + }) - s.T().Log("Transfer - Success (sanity check)") - { - amt := big.NewInt(9_420) + s.Run("Transfer - Success (sanity check)", func() { + evmObj, stateDB := deps.NewEVM() sentAmt, _, err := deps.EvmKeeper.ERC20().Transfer( - contract, evm.EVM_MODULE_ADDRESS, deps.Sender.EthAddr, amt, deps.Ctx, + erc20, /*erc20Addr*/ + evm.EVM_MODULE_ADDRESS, /*sender*/ + deps.Sender.EthAddr, /*recipient*/ + big.NewInt(9_420), /*amount*/ + deps.Ctx, + evmObj, ) s.Require().NoError(err) - evmtest.AssertERC20BalanceEqual( - s.T(), deps, contract, deps.Sender.EthAddr, big.NewInt(9_420)) - evmtest.AssertERC20BalanceEqual( - s.T(), deps, contract, evm.EVM_MODULE_ADDRESS, big.NewInt(60_000)) - s.Require().Equal(sentAmt.String(), amt.String()) - } + s.Require().NoError(stateDB.Commit()) + evmtest.AssertERC20BalanceEqualWithDescription( + s.T(), deps, evmObj, erc20, deps.Sender.EthAddr, big.NewInt(9_420), "expect nonzero balance") + evmtest.AssertERC20BalanceEqualWithDescription( + s.T(), deps, evmObj, erc20, evm.EVM_MODULE_ADDRESS, big.NewInt(60_000), "expect nonzero balance") + s.Require().EqualValues(big.NewInt(9_420), sentAmt) + }) - s.T().Log("Burn tokens - Allowed as non-owner") - { - _, err := deps.EvmKeeper.ERC20().Burn(contract, deps.Sender.EthAddr, big.NewInt(420), deps.Ctx) + s.Run("Burn tokens - Allowed as non-owner", func() { + evmObj, stateDB := deps.NewEVM() + _, err := deps.EvmKeeper.ERC20().Burn( + erc20, /*erc20Addr*/ + deps.Sender.EthAddr, /*sender*/ + big.NewInt(6_000), /*amount*/ + deps.Ctx, + evmObj, + ) s.Require().NoError(err) + s.Require().NoError(stateDB.Commit()) + evmtest.AssertERC20BalanceEqualWithDescription(s.T(), deps, evmObj, erc20, deps.Sender.EthAddr, big.NewInt(3_420), "expect 3420 tokens") + evmtest.AssertERC20BalanceEqualWithDescription(s.T(), deps, evmObj, erc20, evm.EVM_MODULE_ADDRESS, big.NewInt(60_000), "expect 60000 tokens") + }) - _, err = deps.EvmKeeper.ERC20().Burn(contract, evm.EVM_MODULE_ADDRESS, big.NewInt(6_000), deps.Ctx) + s.Run("Burn tokens - Allowed as owner", func() { + evmObj, stateDB := deps.NewEVM() + _, err := deps.EvmKeeper.ERC20().Burn( + erc20, /*erc20Addr*/ + evm.EVM_MODULE_ADDRESS, /*sender*/ + big.NewInt(6_000), /*amount*/ + deps.Ctx, + evmObj, + ) s.Require().NoError(err) - - evmtest.AssertERC20BalanceEqual(s.T(), deps, contract, deps.Sender.EthAddr, big.NewInt(9_000)) - evmtest.AssertERC20BalanceEqual(s.T(), deps, contract, evm.EVM_MODULE_ADDRESS, big.NewInt(54_000)) - } + s.Require().NoError(stateDB.Commit()) + evmtest.AssertERC20BalanceEqualWithDescription(s.T(), deps, evmObj, erc20, deps.Sender.EthAddr, big.NewInt(3_420), "expect 3420 tokens") + evmtest.AssertERC20BalanceEqualWithDescription(s.T(), deps, evmObj, erc20, evm.EVM_MODULE_ADDRESS, big.NewInt(54_000), "expect 54000 tokens") + }) } diff --git a/x/evm/keeper/funtoken_from_coin.go b/x/evm/keeper/funtoken_from_coin.go index e1b6d602b..abfddd2ec 100644 --- a/x/evm/keeper/funtoken_from_coin.go +++ b/x/evm/keeper/funtoken_from_coin.go @@ -2,11 +2,13 @@ package keeper import ( "fmt" + "math/big" "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" bank "github.com/cosmos/cosmos-sdk/x/bank/types" gethcommon "github.com/ethereum/go-ethereum/common" + gethcore "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/NibiruChain/nibiru/v2/eth" @@ -75,15 +77,42 @@ func (k *Keeper) deployERC20ForBankCoin( if err != nil { return gethcommon.Address{}, errors.Wrap(err, "failed to pack ABI args") } - bytecodeForCall := append(embeds.SmartContract_ERC20Minter.Bytecode, packedArgs...) + input := append(embeds.SmartContract_ERC20Minter.Bytecode, packedArgs...) - // nil address for contract creation - evmResp, _, err := k.CallContractWithInput( - ctx, evm.EVM_MODULE_ADDRESS, nil, true, bytecodeForCall, Erc20GasLimitDeploy, + evmMsg := gethcore.NewMessage( + evm.EVM_MODULE_ADDRESS, + nil, /*contract*/ + k.GetAccNonce(ctx, evm.EVM_MODULE_ADDRESS), + big.NewInt(0), /*amount*/ + Erc20GasLimitDeploy, + big.NewInt(0), /*gasFeeCap*/ + big.NewInt(0), /*gasTipCap*/ + big.NewInt(0), /*gasPrice*/ + input, + gethcore.AccessList{}, + false, /*isFake*/ + ) + evmCfg := k.GetEVMConfig(ctx) + txConfig := k.TxConfig(ctx, gethcommon.BigToHash(big.NewInt(0))) + stateDB := k.Bank.StateDB + if stateDB == nil { + stateDB = k.NewStateDB(ctx, txConfig) + } + evmObj := k.NewEVM(ctx, evmMsg, evmCfg, nil /*tracer*/, stateDB) + evmResp, err := k.CallContractWithInput( + ctx, evmObj, evm.EVM_MODULE_ADDRESS, nil, true /*commit*/, input, Erc20GasLimitDeploy, ) if err != nil { return gethcommon.Address{}, errors.Wrap(err, "failed to deploy ERC20 contract") } + + err = stateDB.Commit() + 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") return erc20Addr, nil diff --git a/x/evm/keeper/funtoken_from_coin_test.go b/x/evm/keeper/funtoken_from_coin_test.go index 9dc018a71..f611fd438 100644 --- a/x/evm/keeper/funtoken_from_coin_test.go +++ b/x/evm/keeper/funtoken_from_coin_test.go @@ -23,13 +23,16 @@ import ( func (s *FunTokenFromCoinSuite) TestCreateFunTokenFromCoin() { deps := evmtest.NewTestDeps() - - // Compute contract address. FindERC20 should fail - nonce := deps.NewStateDB().GetNonce(deps.Sender.EthAddr) - contractAddress := crypto.CreateAddress(deps.Sender.EthAddr, nonce) - metadata, err := deps.EvmKeeper.FindERC20Metadata(deps.Ctx, contractAddress) - s.Require().Error(err) - s.Require().Nil(metadata) + s.Run("Compute contract address. FindERC20 should fail", func() { + evmObj, _ := deps.NewEVM() + metadata, err := deps.EvmKeeper.FindERC20Metadata( + deps.Ctx, + evmObj, + crypto.CreateAddress(evm.EVM_MODULE_ADDRESS, deps.EvmKeeper.GetAccNonce(deps.Ctx, evm.EVM_MODULE_ADDRESS)), + ) + s.Require().Error(err) + s.Require().Nil(metadata) + }) s.T().Log("Setup: Create a coin in the bank state") bankDenom := "sometoken" @@ -47,129 +50,116 @@ func (s *FunTokenFromCoinSuite) TestCreateFunTokenFromCoin() { Symbol: "TOKEN", }) - s.T().Log("sad: not enough funds to create fun token") - _, err = deps.EvmKeeper.CreateFunToken( - sdk.WrapSDKContext(deps.Ctx), - &evm.MsgCreateFunToken{ - FromBankDenom: bankDenom, - Sender: deps.Sender.NibiruAddr.String(), - }, - ) - s.Require().ErrorContains(err, "insufficient funds") - - // Give the sender funds for the fee - s.Require().NoError(testapp.FundAccount( - deps.App.BankKeeper, - deps.Ctx, - deps.Sender.NibiruAddr, - deps.EvmKeeper.FeeForCreateFunToken(deps.Ctx), - )) - - s.T().Log("sad: invalid bank denom") - _, err = deps.EvmKeeper.CreateFunToken( - sdk.WrapSDKContext(deps.Ctx), - &evm.MsgCreateFunToken{ - FromBankDenom: "doesn't exist", - Sender: deps.Sender.NibiruAddr.String(), - }, - ) - s.Require().Error(err) - - s.T().Log("happy: CreateFunToken for the bank coin") - s.Require().NoError(testapp.FundAccount( - deps.App.BankKeeper, - deps.Ctx, - deps.Sender.NibiruAddr, - deps.EvmKeeper.FeeForCreateFunToken(deps.Ctx), - )) - createFuntokenResp, err := deps.EvmKeeper.CreateFunToken( - sdk.WrapSDKContext(deps.Ctx), - &evm.MsgCreateFunToken{ - FromBankDenom: bankDenom, - Sender: deps.Sender.NibiruAddr.String(), - }, - ) - s.Require().NoError(err) - - erc20Addr := createFuntokenResp.FuntokenMapping.Erc20Addr - - s.Equal( - createFuntokenResp.FuntokenMapping, - evm.FunToken{ - Erc20Addr: erc20Addr, - BankDenom: bankDenom, - IsMadeFromCoin: true, - }, - ) - - s.T().Log("Expect ERC20 to be deployed") - _, err = deps.EvmKeeper.Code(deps.Ctx, &evm.QueryCodeRequest{ - Address: erc20Addr.String(), + s.Run("insufficient funds to create funtoken", func() { + s.T().Log("sad: not enough funds to create fun token") + _, err := deps.EvmKeeper.CreateFunToken( + sdk.WrapSDKContext(deps.Ctx), + &evm.MsgCreateFunToken{ + FromBankDenom: bankDenom, + Sender: deps.Sender.NibiruAddr.String(), + }, + ) + s.Require().ErrorContains(err, "insufficient funds") }) - s.Require().NoError(err) - s.T().Log("Expect ERC20 metadata on contract") - info, err := deps.EvmKeeper.FindERC20Metadata(deps.Ctx, erc20Addr.Address) - s.Require().NoError(err, info) - s.Equal( - keeper.ERC20Metadata{ - Name: bankDenom, - Symbol: "TOKEN", - Decimals: 0, - }, *info, - ) - - // Event "EventFunTokenCreated" must present - testutil.RequireContainsTypedEvent( - s.T(), - deps.Ctx, - &evm.EventFunTokenCreated{ - BankDenom: bankDenom, - Erc20ContractAddress: erc20Addr.String(), - Creator: deps.Sender.NibiruAddr.String(), - IsMadeFromCoin: true, - }, - ) + s.Run("invalid bank denom", func() { + s.Require().NoError(testapp.FundAccount( + deps.App.BankKeeper, + deps.Ctx, + deps.Sender.NibiruAddr, + deps.EvmKeeper.FeeForCreateFunToken(deps.Ctx), + )) + _, err := deps.EvmKeeper.CreateFunToken( + sdk.WrapSDKContext(deps.Ctx), + &evm.MsgCreateFunToken{ + FromBankDenom: "doesn't exist", + Sender: deps.Sender.NibiruAddr.String(), + }, + ) + s.Require().Error(err) + }) - s.T().Log("sad: CreateFunToken for the bank coin: already registered") - // Give the sender funds for the fee - s.Require().NoError(testapp.FundAccount( - deps.App.BankKeeper, - deps.Ctx, - deps.Sender.NibiruAddr, - deps.EvmKeeper.FeeForCreateFunToken(deps.Ctx), - )) - _, err = deps.EvmKeeper.CreateFunToken( - sdk.WrapSDKContext(deps.Ctx), - &evm.MsgCreateFunToken{ - FromBankDenom: bankDenom, - Sender: deps.Sender.NibiruAddr.String(), - }, - ) - s.Require().ErrorContains(err, "funtoken mapping already created") + s.Run("happy: CreateFunToken for the bank coin", func() { + s.Require().NoError(testapp.FundAccount( + deps.App.BankKeeper, + deps.Ctx, + deps.Sender.NibiruAddr, + deps.EvmKeeper.FeeForCreateFunToken(deps.Ctx), + )) + expectedErc20Addr := crypto.CreateAddress(evm.EVM_MODULE_ADDRESS, deps.EvmKeeper.GetAccNonce(deps.Ctx, evm.EVM_MODULE_ADDRESS)) + createFuntokenResp, err := deps.EvmKeeper.CreateFunToken( + sdk.WrapSDKContext(deps.Ctx), + &evm.MsgCreateFunToken{ + FromBankDenom: bankDenom, + Sender: deps.Sender.NibiruAddr.String(), + }, + ) + s.Require().NoError(err) + + s.Equal( + createFuntokenResp.FuntokenMapping, + evm.FunToken{ + Erc20Addr: eth.EIP55Addr{Address: expectedErc20Addr}, + BankDenom: bankDenom, + IsMadeFromCoin: true, + }, + ) + actualErc20Addr := createFuntokenResp.FuntokenMapping.Erc20Addr + + s.T().Log("Expect ERC20 to be deployed") + _, err = deps.EvmKeeper.Code(deps.Ctx, &evm.QueryCodeRequest{ + Address: actualErc20Addr.String(), + }) + s.Require().NoError(err) + + s.T().Log("Expect ERC20 metadata on contract") + evmObj, _ := deps.NewEVM() + info, err := deps.EvmKeeper.FindERC20Metadata(deps.Ctx, evmObj, actualErc20Addr.Address) + s.Require().NoError(err, info) + s.Equal( + keeper.ERC20Metadata{ + Name: bankDenom, + Symbol: "TOKEN", + Decimals: 0, + }, *info, + ) + + // Event "EventFunTokenCreated" must present + testutil.RequireContainsTypedEvent( + s.T(), + deps.Ctx, + &evm.EventFunTokenCreated{ + BankDenom: bankDenom, + Erc20ContractAddress: actualErc20Addr.String(), + Creator: deps.Sender.NibiruAddr.String(), + IsMadeFromCoin: true, + }, + ) + }) - s.T().Log("sad: bank denom metadata not registered") - // Give the sender funds for the fee - s.Require().NoError(testapp.FundAccount( - deps.App.BankKeeper, - deps.Ctx, - deps.Sender.NibiruAddr, - deps.EvmKeeper.FeeForCreateFunToken(deps.Ctx), - )) - _, err = deps.EvmKeeper.CreateFunToken( - sdk.WrapSDKContext(deps.Ctx), - &evm.MsgCreateFunToken{ - FromBankDenom: "some random denom", - Sender: deps.Sender.NibiruAddr.String(), - }, - ) - s.Require().ErrorContains(err, "bank coin denom should have bank metadata for denom") + s.Run("sad: CreateFunToken for the bank coin: already registered", func() { + // Give the sender funds for the fee + s.Require().NoError(testapp.FundAccount( + deps.App.BankKeeper, + deps.Ctx, + deps.Sender.NibiruAddr, + deps.EvmKeeper.FeeForCreateFunToken(deps.Ctx), + )) + _, err := deps.EvmKeeper.CreateFunToken( + sdk.WrapSDKContext(deps.Ctx), + &evm.MsgCreateFunToken{ + FromBankDenom: bankDenom, + Sender: deps.Sender.NibiruAddr.String(), + }, + ) + s.Require().ErrorContains(err, "funtoken mapping already created") + }) } func (s *FunTokenFromCoinSuite) TestConvertCoinToEvmAndBack() { deps := evmtest.NewTestDeps() + evmObj, _ := deps.NewEVM() alice := evmtest.NewEthPrivAcc() - bankDenom := evm.EVMBankDenom // Initial setup funToken := s.fundAndCreateFunToken(deps, 100) @@ -179,7 +169,7 @@ func (s *FunTokenFromCoinSuite) TestConvertCoinToEvmAndBack() { sdk.WrapSDKContext(deps.Ctx), &evm.MsgConvertCoinToEvm{ Sender: deps.Sender.NibiruAddr.String(), - BankCoin: sdk.NewCoin(bankDenom, sdk.NewInt(10)), + BankCoin: sdk.NewCoin(evm.EVMBankDenom, sdk.NewInt(10)), ToEthAddr: eth.EIP55Addr{ Address: alice.EthAddr, }, @@ -195,76 +185,80 @@ func (s *FunTokenFromCoinSuite) TestConvertCoinToEvmAndBack() { Sender: deps.Sender.NibiruAddr.String(), Erc20ContractAddress: funToken.Erc20Addr.String(), ToEthAddr: alice.EthAddr.String(), - BankCoin: sdk.NewCoin(bankDenom, sdk.NewInt(10)), + BankCoin: sdk.NewCoin(evm.EVMBankDenom, sdk.NewInt(10)), }, ) // Check 1: module balance - moduleBalance := deps.App.BankKeeper.GetBalance(deps.Ctx, authtypes.NewModuleAddress(evm.ModuleName), bankDenom) + moduleBalance := deps.App.BankKeeper.GetBalance(deps.Ctx, authtypes.NewModuleAddress(evm.ModuleName), evm.EVMBankDenom) s.Require().Equal(sdk.NewInt(10), moduleBalance.Amount) // Check 2: Sender balance - senderBalance := deps.App.BankKeeper.GetBalance(deps.Ctx, deps.Sender.NibiruAddr, bankDenom) + senderBalance := deps.App.BankKeeper.GetBalance(deps.Ctx, deps.Sender.NibiruAddr, evm.EVMBankDenom) s.Require().Equal(sdk.NewInt(90), senderBalance.Amount) // Check 3: erc-20 balance - balance, err := deps.EvmKeeper.ERC20().BalanceOf(funToken.Erc20Addr.Address, alice.EthAddr, deps.Ctx) + balance, err := deps.EvmKeeper.ERC20().BalanceOf(funToken.Erc20Addr.Address, alice.EthAddr, deps.Ctx, evmObj) s.Require().NoError(err) s.Require().Zero(balance.Cmp(big.NewInt(10))) - s.T().Log("sad: Convert more bank coin to erc-20, insufficient funds") - _, err = deps.EvmKeeper.ConvertCoinToEvm( - sdk.WrapSDKContext(deps.Ctx), - &evm.MsgConvertCoinToEvm{ - Sender: deps.Sender.NibiruAddr.String(), - BankCoin: sdk.NewCoin(bankDenom, sdk.NewInt(100)), - ToEthAddr: eth.EIP55Addr{ - Address: alice.EthAddr, + s.Run("sad: Convert more bank coin to erc-20, insufficient funds", func() { + _, err = deps.EvmKeeper.ConvertCoinToEvm( + sdk.WrapSDKContext(deps.Ctx), + &evm.MsgConvertCoinToEvm{ + Sender: deps.Sender.NibiruAddr.String(), + BankCoin: sdk.NewCoin(evm.EVMBankDenom, sdk.NewInt(100)), + ToEthAddr: eth.EIP55Addr{ + Address: alice.EthAddr, + }, }, - }, - ) - s.Require().ErrorContains(err, "insufficient funds") + ) + s.Require().ErrorContains(err, "insufficient funds") + }) s.T().Log("Convert erc-20 to back to bank coin") - _, err = deps.EvmKeeper.CallContract( - deps.Ctx, - embeds.SmartContract_FunToken.ABI, - alice.EthAddr, - &precompile.PrecompileAddr_FunToken, - true, - evmtest.FunTokenGasLimitSendToEvm, + contractInput, err := embeds.SmartContract_FunToken.ABI.Pack( "sendToBank", funToken.Erc20Addr.Address, big.NewInt(10), deps.Sender.NibiruAddr.String(), ) s.Require().NoError(err) + evmObj, _ = deps.NewEVM() + _, err = deps.EvmKeeper.CallContractWithInput( + deps.Ctx, + evmObj, + alice.EthAddr, // from + &precompile.PrecompileAddr_FunToken, // to + true, // commit + contractInput, + evmtest.FunTokenGasLimitSendToEvm, + ) + s.Require().NoError(err) // Check 1: module balance - moduleBalance = deps.App.BankKeeper.GetBalance(deps.Ctx, authtypes.NewModuleAddress(evm.ModuleName), bankDenom) + moduleBalance = deps.App.BankKeeper.GetBalance(deps.Ctx, authtypes.NewModuleAddress(evm.ModuleName), evm.EVMBankDenom) s.Require().True(moduleBalance.Amount.Equal(sdk.ZeroInt())) // Check 2: Sender balance - senderBalance = deps.App.BankKeeper.GetBalance(deps.Ctx, deps.Sender.NibiruAddr, bankDenom) + senderBalance = deps.App.BankKeeper.GetBalance(deps.Ctx, deps.Sender.NibiruAddr, evm.EVMBankDenom) s.Require().Equal(sdk.NewInt(100), senderBalance.Amount) // Check 3: erc-20 balance - balance, err = deps.EvmKeeper.ERC20().BalanceOf(funToken.Erc20Addr.Address, alice.EthAddr, deps.Ctx) + balance, err = deps.EvmKeeper.ERC20().BalanceOf(funToken.Erc20Addr.Address, alice.EthAddr, deps.Ctx, evmObj) s.Require().NoError(err) s.Require().Equal("0", balance.String()) s.T().Log("sad: Convert more erc-20 to back to bank coin, insufficient funds") - _, err = deps.EvmKeeper.CallContract( + evmObj, _ = deps.NewEVM() + _, err = deps.EvmKeeper.CallContractWithInput( deps.Ctx, - embeds.SmartContract_FunToken.ABI, - alice.EthAddr, - &precompile.PrecompileAddr_FunToken, - true, + evmObj, + alice.EthAddr, // from + &precompile.PrecompileAddr_FunToken, // to + true, // commit + contractInput, evmtest.FunTokenGasLimitSendToEvm, - "sendToBank", - funToken.Erc20Addr.Address, - big.NewInt(10), - deps.Sender.NibiruAddr.String(), ) s.Require().ErrorContains(err, "transfer amount exceeds balance") } @@ -289,6 +283,7 @@ func (s *FunTokenFromCoinSuite) TestConvertCoinToEvmAndBack() { // - Module account: 0 NIBI escrowed func (s *FunTokenFromCoinSuite) TestNativeSendThenPrecompileSend() { deps := evmtest.NewTestDeps() + evmObj, _ := deps.NewEVM() bankDenom := evm.EVMBankDenom // Initial setup @@ -313,11 +308,11 @@ func (s *FunTokenFromCoinSuite) TestNativeSendThenPrecompileSend() { testContractNibiAddr, sdk.NewCoins(sdk.NewCoin(bankDenom, sdk.NewIntFromBigInt(sendAmt)))), ) - evmtest.AssertBankBalanceEqual( - s.T(), deps, bankDenom, testContractAddr, sendAmt, + evmtest.AssertBankBalanceEqualWithDescription( + s.T(), deps, bankDenom, testContractAddr, sendAmt, "expect 10 balance", ) - evmtest.AssertBankBalanceEqual( - s.T(), deps, bankDenom, evm.EVM_MODULE_ADDRESS, big.NewInt(0), + evmtest.AssertBankBalanceEqualWithDescription( + s.T(), deps, bankDenom, evm.EVM_MODULE_ADDRESS, big.NewInt(0), "expect 0 balance", ) s.T().Log("Convert bank coin to erc-20: give test contract 10 WNIBI (erc20)") @@ -335,13 +330,13 @@ func (s *FunTokenFromCoinSuite) TestNativeSendThenPrecompileSend() { Account: testContractAddr, BalanceBank: sendAmt, BalanceERC20: sendAmt, - }.Assert(s.T(), deps) + }.Assert(s.T(), deps, evmObj) evmtest.FunTokenBalanceAssert{ FunToken: funtoken, Account: evm.EVM_MODULE_ADDRESS, BalanceBank: sendAmt, BalanceERC20: big.NewInt(0), - }.Assert(s.T(), deps) + }.Assert(s.T(), deps, evmObj) // Alice hex and Alice bech32 is the same address in different representation, // so funds are expected to be available in Alice's bank wallet @@ -351,25 +346,29 @@ func (s *FunTokenFromCoinSuite) TestNativeSendThenPrecompileSend() { Account: alice.EthAddr, BalanceBank: big.NewInt(0), BalanceERC20: big.NewInt(0), - }.Assert(s.T(), deps) + }.Assert(s.T(), deps, evmObj) s.T().Log("call test contract") newSendAmtSendToBank := new(big.Int).Quo(sendAmt, big.NewInt(2)) newSendAmtEvmTransfer := evm.NativeToWei(newSendAmtSendToBank) - evmResp, err := deps.EvmKeeper.CallContract( + + contractInput, err := embeds.SmartContract_TestNativeSendThenPrecompileSendJson.ABI.Pack( + "nativeSendThenPrecompileSend", + alice.EthAddr, /*to*/ + newSendAmtEvmTransfer, /*amount*/ + alice.NibiruAddr.String(), /*to*/ + newSendAmtSendToBank, /*amount*/ + ) + s.Require().NoError(err) + evmObj, _ = deps.NewEVM() + evmResp, err := deps.EvmKeeper.CallContractWithInput( deps.Ctx, - embeds.SmartContract_TestNativeSendThenPrecompileSendJson.ABI, + evmObj, deps.Sender.EthAddr, &testContractAddr, true, - 10_000_000, // 100% sufficient gas - "nativeSendThenPrecompileSend", - []any{ - alice.EthAddr, // nativeRecipient - newSendAmtEvmTransfer, // nativeAmount (wei units) - alice.NibiruAddr.String(), // precompileRecipient - newSendAmtSendToBank, // precompileAmount - }..., + contractInput, + evmtest.FunTokenGasLimitSendToEvm, ) s.Require().NoError(err) s.Empty(evmResp.VmError) @@ -381,34 +380,37 @@ func (s *FunTokenFromCoinSuite) TestNativeSendThenPrecompileSend() { BalanceBank: new(big.Int).Mul( newSendAmtSendToBank, big.NewInt(2)), BalanceERC20: big.NewInt(0), - }.Assert(s.T(), deps) + }.Assert(s.T(), deps, evmObj) evmtest.FunTokenBalanceAssert{ FunToken: funtoken, Account: testContractAddr, BalanceBank: big.NewInt(5), BalanceERC20: big.NewInt(5), - }.Assert(s.T(), deps) + }.Assert(s.T(), deps, evmObj) evmtest.FunTokenBalanceAssert{ FunToken: funtoken, Account: evm.EVM_MODULE_ADDRESS, BalanceBank: big.NewInt(5), BalanceERC20: big.NewInt(0), - }.Assert(s.T(), deps) + }.Assert(s.T(), deps, evmObj) - evmResp, err = deps.EvmKeeper.CallContract( + contractInput, err = embeds.SmartContract_TestNativeSendThenPrecompileSendJson.ABI.Pack( + "justPrecompileSend", + alice.NibiruAddr.String(), /*to*/ + newSendAmtSendToBank, /*amount*/ + ) + s.Require().NoError(err) + evmObj, _ = deps.NewEVM() + evmResp, err = deps.EvmKeeper.CallContractWithInput( deps.Ctx, - embeds.SmartContract_TestNativeSendThenPrecompileSendJson.ABI, + evmObj, deps.Sender.EthAddr, &testContractAddr, true, - 10_000_000, // 100% sufficient gas - "justPrecompileSend", - []any{ - alice.NibiruAddr.String(), // precompileRecipient - newSendAmtSendToBank, // precompileAmount - }..., + contractInput, + evmtest.DefaultEthCallGasLimit, ) s.Require().NoError(err) s.Empty(evmResp.VmError) @@ -420,21 +422,21 @@ func (s *FunTokenFromCoinSuite) TestNativeSendThenPrecompileSend() { BalanceBank: new(big.Int).Mul( newSendAmtSendToBank, big.NewInt(3)), BalanceERC20: big.NewInt(0), - }.Assert(s.T(), deps) + }.Assert(s.T(), deps, evmObj) evmtest.FunTokenBalanceAssert{ FunToken: funtoken, Account: testContractAddr, BalanceBank: big.NewInt(5), BalanceERC20: big.NewInt(0), - }.Assert(s.T(), deps) + }.Assert(s.T(), deps, evmObj) evmtest.FunTokenBalanceAssert{ FunToken: funtoken, Account: evm.EVM_MODULE_ADDRESS, BalanceBank: big.NewInt(0), BalanceERC20: big.NewInt(0), - }.Assert(s.T(), deps) + }.Assert(s.T(), deps, evmObj) s.Require().Greater(gasUsedFor2Ops, gasUsedFor1Op, "2 operations should consume more gas") } @@ -455,9 +457,8 @@ func (s *FunTokenFromCoinSuite) TestNativeSendThenPrecompileSend() { // - Module account: 1 NIBI escrowed (which Alice holds as 1 WNIBI) func (s *FunTokenFromCoinSuite) TestERC20TransferThenPrecompileSend() { deps := evmtest.NewTestDeps() - bankDenom := evm.EVMBankDenom + evmObj, _ := deps.NewEVM() - // Initial setup funToken := s.fundAndCreateFunToken(deps, 10e6) s.T().Log("Deploy Test Contract") @@ -467,7 +468,6 @@ func (s *FunTokenFromCoinSuite) TestERC20TransferThenPrecompileSend() { funToken.Erc20Addr.Address, ) s.Require().NoError(err) - testContractAddr := deployResp.ContractAddr s.T().Log("Convert bank coin to erc-20: give test contract 10 WNIBI (erc20)") @@ -475,28 +475,55 @@ func (s *FunTokenFromCoinSuite) TestERC20TransferThenPrecompileSend() { sdk.WrapSDKContext(deps.Ctx), &evm.MsgConvertCoinToEvm{ Sender: deps.Sender.NibiruAddr.String(), - BankCoin: sdk.NewCoin(bankDenom, sdk.NewInt(10e6)), + BankCoin: sdk.NewCoin(evm.EVMBankDenom, sdk.NewInt(10e6)), ToEthAddr: eth.EIP55Addr{Address: testContractAddr}, }, ) s.Require().NoError(err) + // check balances + evmtest.FunTokenBalanceAssert{ + FunToken: funToken, + Account: testContractAddr, + BalanceBank: big.NewInt(0), + BalanceERC20: big.NewInt(10e6), + }.Assert(s.T(), deps, evmObj) + + evmtest.FunTokenBalanceAssert{ + FunToken: funToken, + Account: evm.EVM_MODULE_ADDRESS, + BalanceBank: big.NewInt(10e6), + BalanceERC20: big.NewInt(0), + }.Assert(s.T(), deps, evmObj) + + evmtest.FunTokenBalanceAssert{ + FunToken: funToken, + Account: deps.Sender.EthAddr, + BalanceBank: big.NewInt(0), + BalanceERC20: big.NewInt(0), + }.Assert(s.T(), deps, evmObj) + // Alice hex and Alice bech32 is the same address in different representation alice := evmtest.NewEthPrivAcc() s.T().Log("call test contract") - _, err = deps.EvmKeeper.CallContract( - deps.Ctx, - embeds.SmartContract_TestERC20TransferThenPrecompileSend.ABI, - deps.Sender.EthAddr, - &testContractAddr, - true, - 10_000_000, // 100% sufficient gas + contractInput, err := embeds.SmartContract_TestERC20TransferThenPrecompileSend.ABI.Pack( "erc20TransferThenPrecompileSend", - alice.EthAddr, - big.NewInt(1e6), // erc20 created with 6 decimals - alice.NibiruAddr.String(), - big.NewInt(9e6), // for precompile sendToBank: 6 decimals + alice.EthAddr, /*to*/ + big.NewInt(1e6), /*amount*/ + alice.NibiruAddr.String(), /*to*/ + big.NewInt(9e6), /*amount*/ + ) + s.Require().NoError(err) + evmObj, _ = deps.NewEVM() + _, err = deps.EvmKeeper.CallContractWithInput( + deps.Ctx, + evmObj, + deps.Sender.EthAddr, // from + &testContractAddr, // to + true, // commit + contractInput, + 10_000_000, // gas limit ) s.Require().NoError(err) @@ -506,7 +533,7 @@ func (s *FunTokenFromCoinSuite) TestERC20TransferThenPrecompileSend() { BalanceBank: big.NewInt(9e6), BalanceERC20: big.NewInt(1e6), Description: "Alice has 9 NIBI / 1 WNIBI", - }.Assert(s.T(), deps) + }.Assert(s.T(), deps, evmObj) evmtest.FunTokenBalanceAssert{ FunToken: funToken, @@ -514,7 +541,7 @@ func (s *FunTokenFromCoinSuite) TestERC20TransferThenPrecompileSend() { BalanceBank: big.NewInt(0), BalanceERC20: big.NewInt(0), Description: "Test contract 0 NIBI / 0 WNIBI", - }.Assert(s.T(), deps) + }.Assert(s.T(), deps, evmObj) evmtest.FunTokenBalanceAssert{ FunToken: funToken, @@ -522,7 +549,7 @@ func (s *FunTokenFromCoinSuite) TestERC20TransferThenPrecompileSend() { BalanceBank: big.NewInt(1e6), BalanceERC20: big.NewInt(0e6), Description: "Module account has 1 NIBI escrowed", - }.Assert(s.T(), deps) + }.Assert(s.T(), deps, evmObj) } // TestPrecompileSelfCallRevert @@ -544,7 +571,7 @@ func (s *FunTokenFromCoinSuite) TestERC20TransferThenPrecompileSend() { // - Module account: 10 NIBI escrowed (which Test contract holds as 10 WNIBI) func (s *FunTokenFromCoinSuite) TestPrecompileSelfCallRevert() { deps := evmtest.NewTestDeps() - bankDenom := evm.EVMBankDenom + evmObj, _ := deps.NewEVM() // Initial setup funToken := s.fundAndCreateFunToken(deps, 10e6) @@ -556,7 +583,6 @@ func (s *FunTokenFromCoinSuite) TestPrecompileSelfCallRevert() { funToken.Erc20Addr.Address, ) s.Require().NoError(err) - testContractAddr := deployResp.ContractAddr s.T().Log("Convert bank coin to erc-20: give test contract 10 WNIBI (erc20)") @@ -564,7 +590,7 @@ func (s *FunTokenFromCoinSuite) TestPrecompileSelfCallRevert() { sdk.WrapSDKContext(deps.Ctx), &evm.MsgConvertCoinToEvm{ Sender: deps.Sender.NibiruAddr.String(), - BankCoin: sdk.NewCoin(bankDenom, sdk.NewInt(10e6)), + BankCoin: sdk.NewCoin(evm.EVMBankDenom, sdk.NewInt(10e6)), ToEthAddr: eth.EIP55Addr{Address: testContractAddr}, }, ) @@ -575,7 +601,7 @@ func (s *FunTokenFromCoinSuite) TestPrecompileSelfCallRevert() { deps.App.BankKeeper, deps.Ctx, eth.EthAddrToNibiruAddr(testContractAddr), - sdk.NewCoins(sdk.NewCoin(bankDenom, sdk.NewInt(10e6))), + sdk.NewCoins(sdk.NewCoin(evm.EVMBankDenom, sdk.NewInt(10e6))), )) evmtest.FunTokenBalanceAssert{ @@ -584,7 +610,7 @@ func (s *FunTokenFromCoinSuite) TestPrecompileSelfCallRevert() { BalanceBank: big.NewInt(10e6), BalanceERC20: big.NewInt(10e6), Description: "Initial contract state sanity check: 10 NIBI / 10 WNIBI", - }.Assert(s.T(), deps) + }.Assert(s.T(), deps, evmObj) // Create Alice and Charles. Contract will try to send Alice native coins and // send Charles tokens via sendToBank @@ -592,18 +618,22 @@ func (s *FunTokenFromCoinSuite) TestPrecompileSelfCallRevert() { charles := evmtest.NewEthPrivAcc() s.T().Log("call test contract") - _, err = deps.EvmKeeper.CallContract( + contractInput, err := embeds.SmartContract_TestPrecompileSelfCallRevert.ABI.Pack( + "selfCallTransferFunds", + alice.EthAddr, + evm.NativeToWei(big.NewInt(1e6)), + charles.NibiruAddr.String(), + big.NewInt(9e6), + ) + s.Require().NoError(err) + _, err = deps.EvmKeeper.CallContractWithInput( deps.Ctx, - embeds.SmartContract_TestPrecompileSelfCallRevert.ABI, + evmObj, deps.Sender.EthAddr, &testContractAddr, true, + contractInput, evmtest.FunTokenGasLimitSendToEvm, - "selfCallTransferFunds", - alice.EthAddr, - evm.NativeToWei(big.NewInt(1e6)), // native send uses wei units, - charles.NibiruAddr.String(), - big.NewInt(9e6), // for precompile sendToBank: 6 decimals ) s.Require().NoError(err) @@ -613,7 +643,7 @@ func (s *FunTokenFromCoinSuite) TestPrecompileSelfCallRevert() { BalanceBank: big.NewInt(0), BalanceERC20: big.NewInt(0), Description: "Alice has 0 NIBI / 0 WNIBI", - }.Assert(s.T(), deps) + }.Assert(s.T(), deps, evmObj) evmtest.FunTokenBalanceAssert{ FunToken: funToken, @@ -621,7 +651,7 @@ func (s *FunTokenFromCoinSuite) TestPrecompileSelfCallRevert() { BalanceBank: big.NewInt(0), BalanceERC20: big.NewInt(0), Description: "Charles has 0 NIBI / 0 WNIBI", - }.Assert(s.T(), deps) + }.Assert(s.T(), deps, evmObj) evmtest.FunTokenBalanceAssert{ FunToken: funToken, @@ -629,7 +659,7 @@ func (s *FunTokenFromCoinSuite) TestPrecompileSelfCallRevert() { BalanceBank: big.NewInt(10e6), BalanceERC20: big.NewInt(10e6), Description: "Test contract has 10 NIBI / 10 WNIBI", - }.Assert(s.T(), deps) + }.Assert(s.T(), deps, evmObj) evmtest.FunTokenBalanceAssert{ FunToken: funToken, @@ -637,7 +667,7 @@ func (s *FunTokenFromCoinSuite) TestPrecompileSelfCallRevert() { BalanceBank: big.NewInt(10e6), BalanceERC20: big.NewInt(0), Description: "Module account has 10 NIBI escrowed", - }.Assert(s.T(), deps) + }.Assert(s.T(), deps, evmObj) } // fundAndCreateFunToken creates initial setup for tests @@ -680,14 +710,6 @@ func (s *FunTokenFromCoinSuite) fundAndCreateFunToken(deps evmtest.TestDeps, uni ) s.Require().NoError(err) - erc20Decimals, err := deps.EvmKeeper.LoadERC20Decimals( - deps.Ctx, - embeds.SmartContract_ERC20Minter.ABI, - createFunTokenResp.FuntokenMapping.Erc20Addr.Address, - ) - s.Require().NoError(err) - s.Require().Equal(erc20Decimals, uint8(6)) - return createFunTokenResp.FuntokenMapping } diff --git a/x/evm/keeper/funtoken_from_erc20.go b/x/evm/keeper/funtoken_from_erc20.go index f1fa3b0b2..b393811f9 100644 --- a/x/evm/keeper/funtoken_from_erc20.go +++ b/x/evm/keeper/funtoken_from_erc20.go @@ -5,13 +5,17 @@ import ( "fmt" "math/big" + "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" bank "github.com/cosmos/cosmos-sdk/x/bank/types" gethcommon "github.com/ethereum/go-ethereum/common" + gethcore "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" "github.com/NibiruChain/nibiru/v2/eth" "github.com/NibiruChain/nibiru/v2/x/evm" "github.com/NibiruChain/nibiru/v2/x/evm/embeds" + "github.com/NibiruChain/nibiru/v2/x/evm/statedb" ) // FindERC20Metadata retrieves the metadata of an ERC20 token. @@ -25,20 +29,21 @@ import ( // - err: An error if metadata retrieval fails. func (k Keeper) FindERC20Metadata( ctx sdk.Context, + evmObj *vm.EVM, contract gethcommon.Address, ) (info *ERC20Metadata, err error) { // Load name, symbol, decimals - name, err := k.LoadERC20Name(ctx, embeds.SmartContract_ERC20Minter.ABI, contract) + name, err := k.ERC20().LoadERC20Name(ctx, evmObj, embeds.SmartContract_ERC20Minter.ABI, contract) if err != nil { return nil, err } - symbol, err := k.LoadERC20Symbol(ctx, embeds.SmartContract_ERC20Minter.ABI, contract) + symbol, err := k.ERC20().LoadERC20Symbol(ctx, evmObj, embeds.SmartContract_ERC20Minter.ABI, contract) if err != nil { return nil, err } - decimals, err := k.LoadERC20Decimals(ctx, embeds.SmartContract_ERC20Minter.ABI, contract) + decimals, err := k.ERC20().LoadERC20Decimals(ctx, evmObj, embeds.SmartContract_ERC20Minter.ABI, contract) if err != nil { return nil, err } @@ -110,13 +115,32 @@ func (k *Keeper) createFunTokenFromERC20( ) (funtoken *evm.FunToken, err error) { // 1 | ERC20 already registered with FunToken? if funtokens := k.FunTokens.Collect(ctx, k.FunTokens.Indexes.ERC20Addr.ExactMatch(ctx, erc20)); len(funtokens) > 0 { - return funtoken, fmt.Errorf("funtoken mapping already created for ERC20 \"%s\"", erc20) + return nil, fmt.Errorf("funtoken mapping already created for ERC20 \"%s\"", erc20) } // 2 | Get existing ERC20 metadata - erc20Info, err := k.FindERC20Metadata(ctx, erc20) + // We use dummy values for the tx config and evm config because we aren't in an actual end user transaction, it's just a state query. + stateDB := k.Bank.StateDB + if stateDB == nil { + stateDB = k.NewStateDB(ctx, statedb.NewEmptyTxConfig(gethcommon.BytesToHash(ctx.HeaderHash()))) + } + evmMsg := gethcore.NewMessage( + evm.EVM_MODULE_ADDRESS, + &erc20, + 0, + big.NewInt(0), + 0, + big.NewInt(0), + big.NewInt(0), + big.NewInt(0), + []byte{}, + gethcore.AccessList{}, + false, + ) + evmObj := k.NewEVM(ctx, evmMsg, k.GetEVMConfig(ctx), evm.NewNoOpTracer(), stateDB) + erc20Info, err := k.FindERC20Metadata(ctx, evmObj, erc20) if err != nil { - return funtoken, err + return nil, err } bankDenom := fmt.Sprintf("erc20/%s", erc20.String()) @@ -124,10 +148,10 @@ func (k *Keeper) createFunTokenFromERC20( // 3 | Coin already registered with FunToken? _, isFound := k.Bank.GetDenomMetaData(ctx, bankDenom) if isFound { - return funtoken, fmt.Errorf("bank coin denom already registered with denom \"%s\"", bankDenom) + return nil, fmt.Errorf("bank coin denom already registered with denom \"%s\"", bankDenom) } if funtokens := k.FunTokens.Collect(ctx, k.FunTokens.Indexes.BankDenom.ExactMatch(ctx, bankDenom)); len(funtokens) > 0 { - return funtoken, fmt.Errorf("funtoken mapping already created for bank denom \"%s\"", bankDenom) + return nil, fmt.Errorf("funtoken mapping already created for bank denom \"%s\"", bankDenom) } // 4 | Set bank coin denom metadata in state @@ -135,7 +159,7 @@ func (k *Keeper) createFunTokenFromERC20( err = bankMetadata.Validate() if err != nil { - return funtoken, fmt.Errorf("failed to validate bank metadata: %w", err) + return nil, fmt.Errorf("failed to validate bank metadata: %w", err) } k.Bank.SetDenomMetaData(ctx, bankMetadata) @@ -148,6 +172,13 @@ func (k *Keeper) createFunTokenFromERC20( IsMadeFromCoin: false, } + err = stateDB.Commit() + 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/funtoken_from_erc20_test.go b/x/evm/keeper/funtoken_from_erc20_test.go index ddb469818..1a0c4cd1f 100644 --- a/x/evm/keeper/funtoken_from_erc20_test.go +++ b/x/evm/keeper/funtoken_from_erc20_test.go @@ -26,8 +26,6 @@ func (s *FunTokenFromErc20Suite) TestCreateFunTokenFromERC20() { // assert that the ERC20 contract is not deployed expectedERC20Addr := crypto.CreateAddress(deps.Sender.EthAddr, deps.NewStateDB().GetNonce(deps.Sender.EthAddr)) - _, err := deps.EvmKeeper.FindERC20Metadata(deps.Ctx, expectedERC20Addr) - s.Error(err) s.T().Log("Deploy ERC20") metadata := keeper.ERC20Metadata{ @@ -42,9 +40,11 @@ func (s *FunTokenFromErc20Suite) TestCreateFunTokenFromERC20() { s.Require().NoError(err) s.Require().Equal(expectedERC20Addr, deployResp.ContractAddr) - info, err := deps.EvmKeeper.FindERC20Metadata(deps.Ctx, deployResp.ContractAddr) + evmObj, _ := deps.NewEVM() + + actualMetadata, err := deps.EvmKeeper.FindERC20Metadata(deps.Ctx, evmObj, deployResp.ContractAddr) s.Require().NoError(err) - s.Require().Equal(metadata, *info) + s.Require().Equal(metadata, *actualMetadata) _, err = deps.EvmKeeper.Code(deps.Ctx, &evm.QueryCodeRequest{ Address: expectedERC20Addr.String(), @@ -55,72 +55,117 @@ func (s *FunTokenFromErc20Suite) TestCreateFunTokenFromERC20() { Address: deployResp.ContractAddr, } - s.T().Log("sad: insufficient funds to create FunToken mapping") - _, err = deps.EvmKeeper.CreateFunToken( - sdk.WrapSDKContext(deps.Ctx), - &evm.MsgCreateFunToken{ - FromErc20: &erc20Addr, - Sender: deps.Sender.NibiruAddr.String(), - }, - ) - s.Require().ErrorContains(err, "insufficient funds") + s.Run("sad: insufficient funds to create FunToken mapping", func() { + _, err = deps.EvmKeeper.CreateFunToken( + sdk.WrapSDKContext(deps.Ctx), + &evm.MsgCreateFunToken{ + FromErc20: &erc20Addr, + Sender: deps.Sender.NibiruAddr.String(), + }, + ) + s.Require().ErrorContains(err, "insufficient funds") + }) - s.T().Log("happy: CreateFunToken for the ERC20") - s.Require().NoError(testapp.FundAccount( - deps.App.BankKeeper, - deps.Ctx, - deps.Sender.NibiruAddr, - deps.EvmKeeper.FeeForCreateFunToken(deps.Ctx), - )) + s.Run("happy: CreateFunToken for the ERC20", func() { + s.Require().NoError(testapp.FundAccount( + deps.App.BankKeeper, + deps.Ctx, + deps.Sender.NibiruAddr, + deps.EvmKeeper.FeeForCreateFunToken(deps.Ctx), + )) + + resp, err := deps.EvmKeeper.CreateFunToken( + sdk.WrapSDKContext(deps.Ctx), + &evm.MsgCreateFunToken{ + FromErc20: &erc20Addr, + Sender: deps.Sender.NibiruAddr.String(), + }, + ) + s.Require().NoError(err, "erc20 %s", erc20Addr) + + expectedBankDenom := fmt.Sprintf("erc20/%s", expectedERC20Addr.String()) + s.Equal( + resp.FuntokenMapping, + evm.FunToken{ + Erc20Addr: erc20Addr, + BankDenom: expectedBankDenom, + IsMadeFromCoin: false, + }) + + // Event "EventFunTokenCreated" must present + testutil.RequireContainsTypedEvent( + s.T(), + deps.Ctx, + &evm.EventFunTokenCreated{ + BankDenom: expectedBankDenom, + Erc20ContractAddress: erc20Addr.String(), + Creator: deps.Sender.NibiruAddr.String(), + IsMadeFromCoin: false, + }, + ) + + bankDenomMetadata, _ := deps.App.BankKeeper.GetDenomMetaData(deps.Ctx, expectedBankDenom) + s.Require().Equal(bank.Metadata{ + Description: fmt.Sprintf( + "ERC20 token \"%s\" represented as a Bank Coin with a corresponding FunToken mapping", erc20Addr.String(), + ), + DenomUnits: []*bank.DenomUnit{ + {Denom: expectedBankDenom, Exponent: 0}, + {Denom: metadata.Symbol, Exponent: uint32(metadata.Decimals)}, + }, + Base: expectedBankDenom, + Display: metadata.Symbol, + Name: metadata.Name, + Symbol: metadata.Symbol, + URI: "", + URIHash: "", + }, bankDenomMetadata) + }) - resp, err := deps.EvmKeeper.CreateFunToken( - sdk.WrapSDKContext(deps.Ctx), - &evm.MsgCreateFunToken{ - FromErc20: &erc20Addr, - Sender: deps.Sender.NibiruAddr.String(), - }, - ) - s.Require().NoError(err, "erc20 %s", erc20Addr) - - expectedBankDenom := fmt.Sprintf("erc20/%s", expectedERC20Addr.String()) - s.Equal( - resp.FuntokenMapping, - evm.FunToken{ - Erc20Addr: erc20Addr, - BankDenom: expectedBankDenom, - IsMadeFromCoin: false, - }) - - // Event "EventFunTokenCreated" must present - testutil.RequireContainsTypedEvent( - s.T(), - deps.Ctx, - &evm.EventFunTokenCreated{ - BankDenom: expectedBankDenom, - Erc20ContractAddress: erc20Addr.String(), - Creator: deps.Sender.NibiruAddr.String(), - IsMadeFromCoin: false, - }, - ) - bankMetadata, _ := deps.App.BankKeeper.GetDenomMetaData(deps.Ctx, expectedBankDenom) - s.Require().Equal(bank.Metadata{ - Description: fmt.Sprintf( - "ERC20 token \"%s\" represented as a Bank Coin with a corresponding FunToken mapping", erc20Addr.String(), - ), - DenomUnits: []*bank.DenomUnit{ - {Denom: expectedBankDenom, Exponent: 0}, - {Denom: metadata.Symbol, Exponent: uint32(metadata.Decimals)}, - }, - Base: expectedBankDenom, - Display: metadata.Symbol, - Name: metadata.Name, - Symbol: metadata.Symbol, - URI: "", - URIHash: "", - }, bankMetadata) - - s.T().Log("sad: CreateFunToken for the ERC20: already registered") - // Give the sender funds for the fee + s.Run("sad: CreateFunToken for the ERC20: already registered", func() { + // Give the sender funds for the fee + s.Require().NoError(testapp.FundAccount( + deps.App.BankKeeper, + deps.Ctx, + deps.Sender.NibiruAddr, + deps.EvmKeeper.FeeForCreateFunToken(deps.Ctx), + )) + + _, err = deps.EvmKeeper.CreateFunToken( + sdk.WrapSDKContext(deps.Ctx), + &evm.MsgCreateFunToken{ + FromErc20: &erc20Addr, + Sender: deps.Sender.NibiruAddr.String(), + }, + ) + s.ErrorContains(err, "funtoken mapping already created") + }) + + s.Run("sad: CreateFunToken for the ERC20: invalid sender", func() { + _, err = deps.EvmKeeper.CreateFunToken( + sdk.WrapSDKContext(deps.Ctx), + &evm.MsgCreateFunToken{ + FromErc20: &erc20Addr, + }, + ) + s.ErrorContains(err, "invalid sender") + }) + + s.Run("sad: CreateFunToken for the ERC20: missing erc20 address", func() { + _, err = deps.EvmKeeper.CreateFunToken( + sdk.WrapSDKContext(deps.Ctx), + &evm.MsgCreateFunToken{ + FromErc20: nil, + FromBankDenom: "", + Sender: deps.Sender.NibiruAddr.String(), + }, + ) + s.ErrorContains(err, "either the \"from_erc20\" or \"from_bank_denom\" must be set") + }) +} + +func (s *FunTokenFromErc20Suite) TestSendFromEvmToBank_MadeFromErc20() { + deps := evmtest.NewTestDeps() s.Require().NoError(testapp.FundAccount( deps.App.BankKeeper, deps.Ctx, @@ -128,41 +173,6 @@ func (s *FunTokenFromErc20Suite) TestCreateFunTokenFromERC20() { deps.EvmKeeper.FeeForCreateFunToken(deps.Ctx), )) - _, err = deps.EvmKeeper.CreateFunToken( - sdk.WrapSDKContext(deps.Ctx), - &evm.MsgCreateFunToken{ - FromErc20: &erc20Addr, - Sender: deps.Sender.NibiruAddr.String(), - }, - ) - s.ErrorContains(err, "funtoken mapping already created") - - s.T().Log("sad: CreateFunToken for the ERC20: invalid sender") - - _, err = deps.EvmKeeper.CreateFunToken( - sdk.WrapSDKContext(deps.Ctx), - &evm.MsgCreateFunToken{ - FromErc20: &erc20Addr, - }, - ) - s.ErrorContains(err, "invalid sender") - - s.T().Log("sad: CreateFunToken for the ERC20: missing erc20 address") - - _, err = deps.EvmKeeper.CreateFunToken( - sdk.WrapSDKContext(deps.Ctx), - &evm.MsgCreateFunToken{ - FromErc20: nil, - FromBankDenom: "", - Sender: deps.Sender.NibiruAddr.String(), - }, - ) - s.ErrorContains(err, "either the \"from_erc20\" or \"from_bank_denom\" must be set") -} - -func (s *FunTokenFromErc20Suite) TestSendFromEvmToBank() { - deps := evmtest.NewTestDeps() - s.T().Log("Deploy ERC20") metadata := keeper.ERC20Metadata{ Name: "erc20name", @@ -176,13 +186,6 @@ func (s *FunTokenFromErc20Suite) TestSendFromEvmToBank() { s.Require().NoError(err) s.T().Log("CreateFunToken for the ERC20") - s.Require().NoError(testapp.FundAccount( - deps.App.BankKeeper, - deps.Ctx, - deps.Sender.NibiruAddr, - deps.EvmKeeper.FeeForCreateFunToken(deps.Ctx), - )) - resp, err := deps.EvmKeeper.CreateFunToken( sdk.WrapSDKContext(deps.Ctx), &evm.MsgCreateFunToken{ @@ -196,57 +199,57 @@ func (s *FunTokenFromErc20Suite) TestSendFromEvmToBank() { bankDemon := resp.FuntokenMapping.BankDenom s.T().Logf("mint erc20 tokens to %s", deps.Sender.EthAddr.String()) - _, err = deps.EvmKeeper.CallContract( + contractInput, err := embeds.SmartContract_ERC20Minter.ABI.Pack("mint", deps.Sender.EthAddr, big.NewInt(69_420)) + s.Require().NoError(err) + evmObj, _ := deps.NewEVM() + _, err = deps.EvmKeeper.CallContractWithInput( deps.Ctx, - embeds.SmartContract_ERC20Minter.ABI, - deps.Sender.EthAddr, - &deployResp.ContractAddr, - true, + evmObj, + deps.Sender.EthAddr, /*from*/ + &deployResp.ContractAddr, /*to*/ + true, /*commit*/ + contractInput, keeper.Erc20GasLimitExecute, - "mint", - deps.Sender.EthAddr, - big.NewInt(69_420), ) s.Require().NoError(err) randomAcc := testutil.AccAddress() - s.T().Log("send erc20 tokens to Bank") - _, err = deps.EvmKeeper.CallContract( + s.T().Log("happy: send erc20 tokens to Bank") + contractInput, err = embeds.SmartContract_FunToken.ABI.Pack("sendToBank", deployResp.ContractAddr, big.NewInt(1), randomAcc.String()) + s.Require().NoError(err) + evmObj, _ = deps.NewEVM() + _, err = deps.EvmKeeper.CallContractWithInput( deps.Ctx, - embeds.SmartContract_FunToken.ABI, - deps.Sender.EthAddr, - &precompile.PrecompileAddr_FunToken, - true, + evmObj, + deps.Sender.EthAddr, /*from*/ + &precompile.PrecompileAddr_FunToken, /*to*/ + true, /*commit*/ + contractInput, evmtest.FunTokenGasLimitSendToEvm, - "sendToBank", - deployResp.ContractAddr, - big.NewInt(1), - randomAcc.String(), ) s.Require().NoError(err) s.T().Log("check balances") - evmtest.AssertERC20BalanceEqual(s.T(), deps, deployResp.ContractAddr, deps.Sender.EthAddr, big.NewInt(69_419)) - evmtest.AssertERC20BalanceEqual(s.T(), deps, deployResp.ContractAddr, evm.EVM_MODULE_ADDRESS, big.NewInt(1)) + evmtest.AssertERC20BalanceEqualWithDescription(s.T(), deps, evmObj, deployResp.ContractAddr, deps.Sender.EthAddr, big.NewInt(69_419), "expect nonzero balance") + evmtest.AssertERC20BalanceEqualWithDescription(s.T(), deps, evmObj, deployResp.ContractAddr, evm.EVM_MODULE_ADDRESS, big.NewInt(1), "expect nonzero balance") s.Require().Equal(sdk.NewInt(1), deps.App.BankKeeper.GetBalance(deps.Ctx, randomAcc, bankDemon).Amount, ) s.T().Log("sad: send too many erc20 tokens to Bank") - evmResp, err := deps.EvmKeeper.CallContract( + contractInput, err = embeds.SmartContract_FunToken.ABI.Pack("sendToBank", deployResp.ContractAddr, big.NewInt(70_000), randomAcc.String()) + s.Require().NoError(err) + evmObj, _ = deps.NewEVM() + evmResp, err := deps.EvmKeeper.CallContractWithInput( deps.Ctx, - embeds.SmartContract_FunToken.ABI, - deps.Sender.EthAddr, - &precompile.PrecompileAddr_FunToken, - true, + evmObj, + deps.Sender.EthAddr, /*from*/ + &precompile.PrecompileAddr_FunToken, /*to*/ + true, /*commit*/ + contractInput, evmtest.FunTokenGasLimitSendToEvm, - "sendToBank", - deployResp.ContractAddr, - big.NewInt(70_000), - randomAcc.String(), ) - s.T().Log("check balances") s.Require().Error(err, evmResp.String()) s.T().Log("send Bank tokens back to erc20") @@ -262,8 +265,9 @@ func (s *FunTokenFromErc20Suite) TestSendFromEvmToBank() { s.Require().NoError(err) s.T().Log("check balances") - evmtest.AssertERC20BalanceEqual(s.T(), deps, deployResp.ContractAddr, deps.Sender.EthAddr, big.NewInt(69_420)) - evmtest.AssertERC20BalanceEqual(s.T(), deps, deployResp.ContractAddr, evm.EVM_MODULE_ADDRESS, big.NewInt(0)) + evmObj, _ = deps.NewEVM() + evmtest.AssertERC20BalanceEqualWithDescription(s.T(), deps, evmObj, deployResp.ContractAddr, deps.Sender.EthAddr, big.NewInt(69_420), "expect nonzero balance") + evmtest.AssertERC20BalanceEqualWithDescription(s.T(), deps, evmObj, deployResp.ContractAddr, evm.EVM_MODULE_ADDRESS, big.NewInt(0), "expect nonzero balance") s.Require().True( deps.App.BankKeeper.GetBalance(deps.Ctx, randomAcc, bankDemon).Amount.Equal(sdk.NewInt(0)), ) @@ -326,6 +330,12 @@ func (s *FunTokenFromErc20Suite) TestCreateFunTokenFromERC20MaliciousName() { // Fun token should be created but sending from erc20 to bank should fail with out of gas func (s *FunTokenFromErc20Suite) TestFunTokenFromERC20MaliciousTransfer() { deps := evmtest.NewTestDeps() + s.Require().NoError(testapp.FundAccount( + deps.App.BankKeeper, + deps.Ctx, + deps.Sender.NibiruAddr, + deps.EvmKeeper.FeeForCreateFunToken(deps.Ctx), + )) s.T().Log("Deploy ERC20MaliciousTransfer") metadata := keeper.ERC20Metadata{ @@ -344,13 +354,6 @@ func (s *FunTokenFromErc20Suite) TestFunTokenFromERC20MaliciousTransfer() { } s.T().Log("happy: CreateFunToken for ERC20 with malicious transfer") - s.Require().NoError(testapp.FundAccount( - deps.App.BankKeeper, - deps.Ctx, - deps.Sender.NibiruAddr, - deps.EvmKeeper.FeeForCreateFunToken(deps.Ctx), - )) - _, err = deps.EvmKeeper.CreateFunToken( sdk.WrapSDKContext(deps.Ctx), &evm.MsgCreateFunToken{ @@ -362,17 +365,17 @@ func (s *FunTokenFromErc20Suite) TestFunTokenFromERC20MaliciousTransfer() { randomAcc := testutil.AccAddress() s.T().Log("send erc20 tokens to cosmos") - _, err = deps.EvmKeeper.CallContract( + input, err := embeds.SmartContract_FunToken.ABI.Pack("sendToBank", deployResp.ContractAddr, big.NewInt(1), randomAcc.String()) + s.Require().NoError(err) + evmObj, _ := deps.NewEVM() + _, err = deps.EvmKeeper.CallContractWithInput( deps.Ctx, - embeds.SmartContract_FunToken.ABI, - deps.Sender.EthAddr, + evmObj, + evm.EVM_MODULE_ADDRESS, &precompile.PrecompileAddr_FunToken, true, + input, evmtest.FunTokenGasLimitSendToEvm, - "sendToBank", - deployResp.ContractAddr, - big.NewInt(1), - randomAcc.String(), ) s.Require().ErrorContains(err, "gas required exceeds allowance") } @@ -381,6 +384,12 @@ func (s *FunTokenFromErc20Suite) TestFunTokenFromERC20MaliciousTransfer() { // with a malicious recursive balanceOf() and transfer() functions. func (s *FunTokenFromErc20Suite) TestFunTokenInfiniteRecursionERC20() { deps := evmtest.NewTestDeps() + s.Require().NoError(testapp.FundAccount( + deps.App.BankKeeper, + deps.Ctx, + deps.Sender.NibiruAddr, + deps.EvmKeeper.FeeForCreateFunToken(deps.Ctx), + )) s.T().Log("Deploy InfiniteRecursionERC20") metadata := keeper.ERC20Metadata{ @@ -399,13 +408,6 @@ func (s *FunTokenFromErc20Suite) TestFunTokenInfiniteRecursionERC20() { } s.T().Log("happy: CreateFunToken for ERC20 with infinite recursion") - s.Require().NoError(testapp.FundAccount( - deps.App.BankKeeper, - deps.Ctx, - deps.Sender.NibiruAddr, - deps.EvmKeeper.FeeForCreateFunToken(deps.Ctx), - )) - _, err = deps.EvmKeeper.CreateFunToken( sdk.WrapSDKContext(deps.Ctx), &evm.MsgCreateFunToken{ @@ -416,28 +418,32 @@ func (s *FunTokenFromErc20Suite) TestFunTokenInfiniteRecursionERC20() { s.Require().NoError(err) s.T().Log("happy: call attackBalance()") - res, err := deps.EvmKeeper.CallContract( + contractInput, err := embeds.SmartContract_TestInfiniteRecursionERC20.ABI.Pack("attackBalance") + s.Require().NoError(err) + evmObj, _ := deps.NewEVM() + _, err = deps.EvmKeeper.CallContractWithInput( deps.Ctx, - embeds.SmartContract_TestInfiniteRecursionERC20.ABI, - deps.Sender.EthAddr, - &erc20Addr.Address, - false, + evmObj, + deps.Sender.EthAddr, /*from*/ + &erc20Addr.Address, /*to*/ + false, /*commit*/ + contractInput, 10_000_000, - "attackBalance", ) s.Require().NoError(err) - s.Require().NotNil(res) - s.Require().Empty(res.VmError) - s.T().Log("sad: call attackBalance()") - _, err = deps.EvmKeeper.CallContract( + s.T().Log("sad: call attackTransfer()") + contractInput, err = embeds.SmartContract_TestInfiniteRecursionERC20.ABI.Pack("attackTransfer") + s.Require().NoError(err) + evmObj, _ = deps.NewEVM() + _, err = deps.EvmKeeper.CallContractWithInput( deps.Ctx, - embeds.SmartContract_TestInfiniteRecursionERC20.ABI, - deps.Sender.EthAddr, - &erc20Addr.Address, - true, + evmObj, + deps.Sender.EthAddr, /*from*/ + &erc20Addr.Address, /*to*/ + true, /*commit*/ + contractInput, 10_000_000, - "attackTransfer", ) s.Require().ErrorContains(err, "execution reverted") } @@ -446,6 +452,12 @@ func (s *FunTokenFromErc20Suite) TestFunTokenInfiniteRecursionERC20() { // Test ensures that after sending ERC20 token to coin and back, all bank coins are burned. func (s *FunTokenFromErc20Suite) TestSendERC20WithFee() { deps := evmtest.NewTestDeps() + s.Require().NoError(testapp.FundAccount( + deps.App.BankKeeper, + deps.Ctx, + deps.Sender.NibiruAddr, + deps.EvmKeeper.FeeForCreateFunToken(deps.Ctx), + )) s.T().Log("Deploy ERC20") metadata := keeper.ERC20Metadata{ @@ -459,13 +471,6 @@ func (s *FunTokenFromErc20Suite) TestSendERC20WithFee() { s.Require().NoError(err) s.T().Log("CreateFunToken for the ERC20 with fee") - s.Require().NoError(testapp.FundAccount( - deps.App.BankKeeper, - deps.Ctx, - deps.Sender.NibiruAddr, - deps.EvmKeeper.FeeForCreateFunToken(deps.Ctx), - )) - resp, err := deps.EvmKeeper.CreateFunToken( sdk.WrapSDKContext(deps.Ctx), &evm.MsgCreateFunToken{ @@ -481,24 +486,30 @@ func (s *FunTokenFromErc20Suite) TestSendERC20WithFee() { randomAcc := testutil.AccAddress() s.T().Log("send erc20 tokens to Bank") - _, err = deps.EvmKeeper.CallContract( + contractInput, err := embeds.SmartContract_FunToken.ABI.Pack( + "sendToBank", + deployResp.ContractAddr, /*erc20Addr*/ + big.NewInt(100), /*amount*/ + randomAcc.String(), /*to*/ + ) + s.Require().NoError(err) + evmObj, _ := deps.NewEVM() + _, err = deps.EvmKeeper.CallContractWithInput( deps.Ctx, - embeds.SmartContract_FunToken.ABI, - deps.Sender.EthAddr, - &precompile.PrecompileAddr_FunToken, - true, + evmObj, + deps.Sender.EthAddr, /*from*/ + &precompile.PrecompileAddr_FunToken, /*to*/ + true, /*commit*/ + contractInput, evmtest.FunTokenGasLimitSendToEvm, - "sendToBank", - deployResp.ContractAddr, - big.NewInt(100), - randomAcc.String(), ) s.Require().NoError(err) s.T().Log("check balances") - evmtest.AssertERC20BalanceEqual(s.T(), deps, deployResp.ContractAddr, deps.Sender.EthAddr, big.NewInt(900)) - evmtest.AssertERC20BalanceEqual(s.T(), deps, deployResp.ContractAddr, deployResp.ContractAddr, big.NewInt(10)) - evmtest.AssertERC20BalanceEqual(s.T(), deps, deployResp.ContractAddr, evm.EVM_MODULE_ADDRESS, big.NewInt(90)) + evmtest.AssertERC20BalanceEqualWithDescription(s.T(), deps, evmObj, deployResp.ContractAddr, deps.Sender.EthAddr, big.NewInt(900), "expect 900 balance") + evmtest.AssertERC20BalanceEqualWithDescription(s.T(), deps, evmObj, deployResp.ContractAddr, deployResp.ContractAddr, big.NewInt(10), "expect 10 balance") + evmtest.AssertERC20BalanceEqualWithDescription(s.T(), deps, evmObj, deployResp.ContractAddr, evm.EVM_MODULE_ADDRESS, big.NewInt(90), "expect 90 balance") + s.Require().Equal(sdk.NewInt(90), deps.App.BankKeeper.GetBalance(deps.Ctx, randomAcc, bankDemon).Amount) s.T().Log("send Bank tokens back to erc20") @@ -514,9 +525,10 @@ func (s *FunTokenFromErc20Suite) TestSendERC20WithFee() { s.Require().NoError(err) s.T().Log("check balances") - evmtest.AssertERC20BalanceEqual(s.T(), deps, deployResp.ContractAddr, deps.Sender.EthAddr, big.NewInt(981)) - evmtest.AssertERC20BalanceEqual(s.T(), deps, deployResp.ContractAddr, deployResp.ContractAddr, big.NewInt(19)) - evmtest.AssertERC20BalanceEqual(s.T(), deps, deployResp.ContractAddr, evm.EVM_MODULE_ADDRESS, big.NewInt(0)) + evmObj, _ = deps.NewEVM() + evmtest.AssertERC20BalanceEqualWithDescription(s.T(), deps, evmObj, deployResp.ContractAddr, deps.Sender.EthAddr, big.NewInt(981), "expect 981 balance") + evmtest.AssertERC20BalanceEqualWithDescription(s.T(), deps, evmObj, deployResp.ContractAddr, deployResp.ContractAddr, big.NewInt(19), "expect 19 balance") + evmtest.AssertERC20BalanceEqualWithDescription(s.T(), deps, evmObj, deployResp.ContractAddr, evm.EVM_MODULE_ADDRESS, big.NewInt(0), "expect 0 balance") s.Require().True(deps.App.BankKeeper.GetBalance(deps.Ctx, randomAcc, bankDemon).Amount.Equal(sdk.NewInt(0))) s.Require().True(deps.App.BankKeeper.GetBalance(deps.Ctx, evm.EVM_MODULE_ADDRESS_NIBI, bankDemon).Amount.Equal(sdk.NewInt(0))) } diff --git a/x/evm/keeper/grpc_query.go b/x/evm/keeper/grpc_query.go index b96cd5ddb..3255b8f73 100644 --- a/x/evm/keeper/grpc_query.go +++ b/x/evm/keeper/grpc_query.go @@ -270,17 +270,13 @@ func (k *Keeper) EthCall( if err != nil { return nil, grpcstatus.Error(grpccodes.InvalidArgument, err.Error()) } - chainID := k.EthChainID(ctx) - cfg, err := k.GetEVMConfig(ctx, ParseProposerAddr(ctx, req.ProposerAddress), chainID) - if err != nil { - return nil, grpcstatus.Error(grpccodes.Internal, err.Error()) - } + evmCfg := k.GetEVMConfig(ctx) // ApplyMessageWithConfig expect correct nonce set in msg nonce := k.GetAccNonce(ctx, args.GetFrom()) args.Nonce = (*hexutil.Uint64)(&nonce) - msg, err := args.ToMessage(req.GasCap, cfg.BaseFeeWei) + msg, err := args.ToMessage(req.GasCap, evmCfg.BaseFeeWei) if err != nil { return nil, grpcstatus.Error(grpccodes.InvalidArgument, err.Error()) } @@ -288,7 +284,9 @@ func (k *Keeper) EthCall( txConfig := statedb.NewEmptyTxConfig(gethcommon.BytesToHash(ctx.HeaderHash())) // pass false to not commit StateDB - res, _, err := k.ApplyEvmMsg(ctx, msg, nil, false, cfg, txConfig, false) + stateDB := statedb.New(ctx, k, txConfig) + evm := k.NewEVM(ctx, msg, evmCfg, nil /*tracer*/, stateDB) + res, err := k.ApplyEvmMsg(ctx, msg, evm, nil /*tracer*/, false /*commit*/, txConfig.TxHash, false /*fullRefundLeftoverGas*/) if err != nil { return nil, grpcstatus.Error(grpccodes.Internal, err.Error()) } @@ -326,18 +324,14 @@ func (k Keeper) EstimateGasForEvmCallType( } ctx := sdk.UnwrapSDKContext(goCtx) - chainID := k.EthChainID(ctx) - cfg, err := k.GetEVMConfig(ctx, ParseProposerAddr(ctx, req.ProposerAddress), chainID) - if err != nil { - return nil, grpcstatus.Error(grpccodes.Internal, "failed to load evm config") - } + evmCfg := k.GetEVMConfig(ctx) if req.GasCap < gethparams.TxGas { return nil, grpcstatus.Errorf(grpccodes.InvalidArgument, "gas cap cannot be lower than %d", gethparams.TxGas) } var args evm.JsonTxArgs - err = json.Unmarshal(req.Args, &args) + err := json.Unmarshal(req.Args, &args) if err != nil { return nil, grpcstatus.Error(grpccodes.InvalidArgument, err.Error()) } @@ -375,7 +369,7 @@ func (k Keeper) EstimateGasForEvmCallType( gasCap = hi // convert the tx args to an ethereum message - msg, err := args.ToMessage(req.GasCap, cfg.BaseFeeWei) + evmMsg, err := args.ToMessage(req.GasCap, evmCfg.BaseFeeWei) if err != nil { return nil, grpcstatus.Error(grpccodes.Internal, err.Error()) } @@ -385,27 +379,27 @@ func (k Keeper) EstimateGasForEvmCallType( // helper to check if a gas allowance results in an executable transaction. executable := func(gas uint64) (vmError bool, rsp *evm.MsgEthereumTxResponse, err error) { // update the message with the new gas value - msg = gethcore.NewMessage( - msg.From(), - msg.To(), - msg.Nonce(), - msg.Value(), + evmMsg = gethcore.NewMessage( + evmMsg.From(), + evmMsg.To(), + evmMsg.Nonce(), + evmMsg.Value(), gas, - msg.GasPrice(), - msg.GasFeeCap(), - msg.GasTipCap(), - msg.Data(), - msg.AccessList(), - msg.IsFake(), + evmMsg.GasPrice(), + evmMsg.GasFeeCap(), + evmMsg.GasTipCap(), + evmMsg.Data(), + evmMsg.AccessList(), + evmMsg.IsFake(), ) tmpCtx := ctx if fromType == evm.CallTypeRPC { tmpCtx, _ = ctx.CacheContext() - acct := k.GetAccount(tmpCtx, msg.From()) + acct := k.GetAccount(tmpCtx, evmMsg.From()) - from := msg.From() + from := evmMsg.From() if acct == nil { acc := k.accountKeeper.NewAccountWithAddress(tmpCtx, from[:]) k.accountKeeper.SetAccount(tmpCtx, acc) @@ -418,14 +412,16 @@ func (k Keeper) EstimateGasForEvmCallType( return true, nil, err } // resetting the gasMeter after increasing the sequence to have an accurate gas estimation on EVM extensions transactions - gasMeter := eth.NewInfiniteGasMeterWithLimit(msg.Gas()) + gasMeter := eth.NewInfiniteGasMeterWithLimit(evmMsg.Gas()) tmpCtx = tmpCtx.WithGasMeter(gasMeter). WithKVGasConfig(storetypes.GasConfig{}). WithTransientKVGasConfig(storetypes.GasConfig{}) } // pass false to not commit StateDB txConfig := statedb.NewEmptyTxConfig(gethcommon.BytesToHash(ctx.HeaderHash().Bytes())) - rsp, _, err = k.ApplyEvmMsg(tmpCtx, msg, nil, false, cfg, txConfig, false) + stateDB := statedb.New(ctx, &k, txConfig) + evmObj := k.NewEVM(tmpCtx, evmMsg, evmCfg, nil /*tracer*/, stateDB) + rsp, err = k.ApplyEvmMsg(tmpCtx, evmMsg, evmObj, nil /*tracer*/, false /*commit*/, txConfig.TxHash, false /*fullRefundLeftoverGas*/) if err != nil { if errors.Is(err, core.ErrIntrinsicGas) { return true, nil, nil // Special case, raise gas limit @@ -492,20 +488,14 @@ func (k Keeper) TraceTx( Block: &cmtproto.BlockParams{MaxGas: req.BlockMaxGas}, }) - chainID := k.EthChainID(ctx) - cfg, err := k.GetEVMConfig(ctx, ParseProposerAddr(ctx, req.ProposerAddress), chainID) - if err != nil { - return nil, grpcstatus.Errorf(grpccodes.Internal, "failed to load evm config: %s", err.Error()) - } - + evmCfg := k.GetEVMConfig(ctx) // compute and use base fee of the height that is being traced - baseFeeMicronibiPerGas := k.BaseFeeMicronibiPerGas(ctx) - if baseFeeMicronibiPerGas != nil { - cfg.BaseFeeWei = baseFeeMicronibiPerGas + baseFeeWeiPerGas := k.BaseFeeWeiPerGas(ctx) + if baseFeeWeiPerGas != nil { + evmCfg.BaseFeeWei = baseFeeWeiPerGas } - signer := gethcore.MakeSigner(cfg.ChainConfig, big.NewInt(ctx.BlockHeight())) - + signer := gethcore.MakeSigner(evmCfg.ChainConfig, big.NewInt(ctx.BlockHeight())) txConfig := statedb.NewEmptyTxConfig(gethcommon.BytesToHash(ctx.HeaderHash().Bytes())) // gas used at this point corresponds to GetProposerAddress & CalculateBaseFee @@ -514,7 +504,7 @@ func (k Keeper) TraceTx( for i, tx := range req.Predecessors { ethTx := tx.AsTransaction() - msg, err := ethTx.AsMessage(signer, cfg.BaseFeeWei) + msg, err := ethTx.AsMessage(signer, evmCfg.BaseFeeWei) if err != nil { continue } @@ -524,7 +514,9 @@ func (k Keeper) TraceTx( ctx = ctx.WithGasMeter(eth.NewInfiniteGasMeterWithLimit(msg.Gas())). WithKVGasConfig(storetypes.GasConfig{}). WithTransientKVGasConfig(storetypes.GasConfig{}) - rsp, _, err := k.ApplyEvmMsg(ctx, msg, evm.NewNoOpTracer(), true, cfg, txConfig, false) + stateDB := statedb.New(ctx, &k, txConfig) + evmObj := k.NewEVM(ctx, msg, evmCfg, nil /*tracer*/, stateDB) + rsp, err := k.ApplyEvmMsg(ctx, msg, evmObj, nil /*tracer*/, false /*commit*/, txConfig.TxHash, false /*fullRefundLeftoverGas*/) if err != nil { continue } @@ -543,12 +535,12 @@ func (k Keeper) TraceTx( tracerConfig, _ = json.Marshal(req.TraceConfig.TracerConfig) } - msg, err := tx.AsMessage(signer, cfg.BaseFeeWei) + msg, err := tx.AsMessage(signer, evmCfg.BaseFeeWei) if err != nil { return nil, err } - result, _, err := k.TraceEthTxMsg(ctx, cfg, txConfig, msg, req.TraceConfig, false, tracerConfig) + result, _, err := k.TraceEthTxMsg(ctx, evmCfg, txConfig, msg, req.TraceConfig, tracerConfig) if err != nil { // error will be returned with detail status from traceTx return nil, err @@ -591,16 +583,12 @@ func (k Keeper) TraceCall( Block: &cmtproto.BlockParams{MaxGas: req.BlockMaxGas}, }) - chainID := k.EthChainID(ctx) - cfg, err := k.GetEVMConfig(ctx, ParseProposerAddr(ctx, req.ProposerAddress), chainID) - if err != nil { - return nil, grpcstatus.Errorf(grpccodes.Internal, "failed to load evm config: %s", err.Error()) - } + evmCfg := k.GetEVMConfig(ctx) // compute and use base fee of the height that is being traced baseFeeMicronibi := k.BaseFeeMicronibiPerGas(ctx) if baseFeeMicronibi != nil { - cfg.BaseFeeWei = baseFeeMicronibi + evmCfg.BaseFeeWei = baseFeeMicronibi } txConfig := statedb.NewEmptyTxConfig(gethcommon.BytesToHash(ctx.HeaderHash().Bytes())) @@ -617,7 +605,7 @@ func (k Keeper) TraceCall( if err != nil { return nil, grpcstatus.Errorf(grpccodes.Internal, "failed to unpack tx data: %s", err.Error()) } - msg := gethcore.NewMessage( + evmMsg := gethcore.NewMessage( gethcommon.HexToAddress(msgEthTx.From), txData.GetTo(), txData.GetNonce(), @@ -628,9 +616,9 @@ func (k Keeper) TraceCall( txData.GetGasTipCapWei(), txData.GetData(), txData.GetAccessList(), - false, + false, // isFake ) - result, _, err := k.TraceEthTxMsg(ctx, cfg, txConfig, msg, req.TraceConfig, false, tracerConfig) + result, _, err := k.TraceEthTxMsg(ctx, evmCfg, txConfig, evmMsg, req.TraceConfig, tracerConfig) if err != nil { // error will be returned with detail status from traceTx return nil, err @@ -678,17 +666,12 @@ func (k Keeper) TraceBlock( Block: &cmtproto.BlockParams{MaxGas: req.BlockMaxGas}, }) - chainID := k.EthChainID(ctx) - - cfg, err := k.GetEVMConfig(ctx, ParseProposerAddr(ctx, req.ProposerAddress), chainID) - if err != nil { - return nil, grpcstatus.Error(grpccodes.Internal, "failed to load evm config") - } + evmCfg := k.GetEVMConfig(ctx) // compute and use base fee of height that is being traced if baseFeeMicronibiPerGas := k.BaseFeeMicronibiPerGas(ctx); baseFeeMicronibiPerGas != nil { baseFeeWeiPerGas := evm.NativeToWei(baseFeeMicronibiPerGas) - cfg.BaseFeeWei = baseFeeWeiPerGas + evmCfg.BaseFeeWei = baseFeeWeiPerGas } var tracerConfig json.RawMessage if req.TraceConfig != nil && req.TraceConfig.TracerConfig != nil { @@ -696,7 +679,7 @@ func (k Keeper) TraceBlock( tracerConfig, _ = json.Marshal(req.TraceConfig.TracerConfig) } - signer := gethcore.MakeSigner(cfg.ChainConfig, big.NewInt(ctx.BlockHeight())) + signer := gethcore.MakeSigner(evmCfg.ChainConfig, big.NewInt(ctx.BlockHeight())) txsLength := len(req.Txs) results := make([]*evm.TxTraceResult, 0, txsLength) @@ -707,12 +690,12 @@ func (k Keeper) TraceBlock( ethTx := tx.AsTransaction() txConfig.TxHash = ethTx.Hash() txConfig.TxIndex = uint(i) - msg, err := ethTx.AsMessage(signer, cfg.BaseFeeWei) + msg, err := ethTx.AsMessage(signer, evmCfg.BaseFeeWei) if err != nil { result.Error = err.Error() continue } - traceResult, logIndex, err := k.TraceEthTxMsg(ctx, cfg, txConfig, msg, req.TraceConfig, true, tracerConfig) + traceResult, logIndex, err := k.TraceEthTxMsg(ctx, evmCfg, txConfig, msg, req.TraceConfig, tracerConfig) if err != nil { result.Error = err.Error() } else { @@ -736,11 +719,10 @@ func (k Keeper) TraceBlock( // nextLogIndex, error). func (k *Keeper) TraceEthTxMsg( ctx sdk.Context, - cfg *statedb.EVMConfig, + evmCfg statedb.EVMConfig, txConfig statedb.TxConfig, msg gethcore.Message, traceConfig *evm.TraceConfig, - commitMessage bool, tracerJSONConfig json.RawMessage, ) (*any, uint, error) { // Assemble the structured logger or the JavaScript tracer @@ -806,7 +788,9 @@ func (k *Keeper) TraceEthTxMsg( ctx = ctx.WithGasMeter(eth.NewInfiniteGasMeterWithLimit(msg.Gas())). WithKVGasConfig(storetypes.GasConfig{}). WithTransientKVGasConfig(storetypes.GasConfig{}) - res, _, err := k.ApplyEvmMsg(ctx, msg, tracer, commitMessage, cfg, txConfig, false) + stateDB := statedb.New(ctx, k, txConfig) + evmObj := k.NewEVM(ctx, msg, evmCfg, tracer, stateDB) + res, err := k.ApplyEvmMsg(ctx, msg, evmObj, tracer, false /*commit*/, txConfig.TxHash, false /*fullRefundLeftoverGas*/) if err != nil { return nil, 0, grpcstatus.Error(grpccodes.Internal, err.Error()) } diff --git a/x/evm/keeper/msg_server.go b/x/evm/keeper/msg_server.go index 5c9377518..982f798ac 100644 --- a/x/evm/keeper/msg_server.go +++ b/x/evm/keeper/msg_server.go @@ -46,24 +46,22 @@ func (k *Keeper) EthereumTx( ctx := sdk.UnwrapSDKContext(goCtx) tx := txMsg.AsTransaction() - - evmConfig, err := k.GetEVMConfig(ctx, ctx.BlockHeader().ProposerAddress, k.EthChainID(ctx)) - if err != nil { - return nil, errors.Wrap(err, "failed to load evm config") - } txConfig := k.TxConfig(ctx, tx.Hash()) + evmCfg := k.GetEVMConfig(ctx) // get the signer according to the chain rules from the config and block height - signer := gethcore.MakeSigner(evmConfig.ChainConfig, big.NewInt(ctx.BlockHeight())) - evmMsg, err := tx.AsMessage(signer, evmConfig.BaseFeeWei) + evmMsg, err := tx.AsMessage(gethcore.NewLondonSigner(evmCfg.ChainConfig.ChainID), evmCfg.BaseFeeWei) if err != nil { - return nil, errors.Wrap(err, "failed to return ethereum transaction as core message") + return nil, errors.Wrap(err, "failed to convert ethereum transaction as core message") } // ApplyEvmMsg - Perform the EVM State transition - refundLeftoverGas := false - var tracer vm.EVMLogger = nil - evmResp, _, err = k.ApplyEvmMsg(ctx, evmMsg, tracer, true, evmConfig, txConfig, refundLeftoverGas) + stateDB := k.Bank.StateDB + if stateDB == nil { + stateDB = k.NewStateDB(ctx, txConfig) + } + 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 { return nil, errors.Wrap(err, "error applying ethereum core message") } @@ -76,7 +74,7 @@ func (k *Keeper) EthereumTx( if evmMsg.Gas() > evmResp.GasUsed { refundGas = evmMsg.Gas() - evmResp.GasUsed } - weiPerGas := txMsg.EffectiveGasPriceWeiPerGas(evmConfig.BaseFeeWei) + weiPerGas := txMsg.EffectiveGasPriceWeiPerGas(evmCfg.BaseFeeWei) if err = k.RefundGas(ctx, evmMsg.From(), refundGas, weiPerGas); err != nil { return nil, errors.Wrapf(err, "error refunding leftover gas to sender %s", evmMsg.From()) } @@ -108,7 +106,7 @@ func (k *Keeper) EthereumTx( func (k *Keeper) NewEVM( ctx sdk.Context, msg core.Message, - evmConfig *statedb.EVMConfig, + evmCfg statedb.EVMConfig, tracer vm.EVMLogger, stateDB vm.StateDB, ) *vm.EVM { @@ -120,21 +118,21 @@ func (k *Keeper) NewEVM( CanTransfer: core.CanTransfer, Transfer: core.Transfer, GetHash: k.GetHashFn(ctx), - Coinbase: evmConfig.BlockCoinbase, + Coinbase: evmCfg.BlockCoinbase, GasLimit: eth.BlockGasLimit(ctx), BlockNumber: big.NewInt(ctx.BlockHeight()), Time: big.NewInt(ctx.BlockHeader().Time.Unix()), Difficulty: big.NewInt(0), // unused. Only required in PoW context - BaseFee: evmConfig.BaseFeeWei, + BaseFee: evmCfg.BaseFeeWei, Random: &pseudoRandom, } txCtx := core.NewEVMTxContext(msg) if tracer == nil { - tracer = k.Tracer(ctx, msg, evmConfig.ChainConfig) + tracer = k.Tracer(ctx, msg, evmCfg.ChainConfig) } - vmConfig := k.VMConfig(ctx, msg, evmConfig, tracer) - theEvm := vm.NewEVM(blockCtx, txCtx, stateDB, evmConfig.ChainConfig, vmConfig) + vmConfig := k.VMConfig(ctx, &evmCfg, tracer) + theEvm := vm.NewEVM(blockCtx, txCtx, stateDB, evmCfg.ChainConfig, vmConfig) theEvm.WithPrecompiles(k.precompiles.InternalData(), k.precompiles.Keys()) return theEvm } @@ -243,39 +241,15 @@ func (k Keeper) GetHashFn(ctx sdk.Context) vm.GetHashFunc { // // For internal calls like funtokens, user does not specify gas limit explicitly. // In this case we don't apply any caps for refund and refund 100% -func (k *Keeper) ApplyEvmMsg(ctx sdk.Context, +func (k *Keeper) ApplyEvmMsg( + ctx sdk.Context, msg core.Message, + evmObj *vm.EVM, tracer vm.EVMLogger, commit bool, - evmConfig *statedb.EVMConfig, - txConfig statedb.TxConfig, + txHash gethcommon.Hash, fullRefundLeftoverGas bool, -) (resp *evm.MsgEthereumTxResponse, evmObj *vm.EVM, err error) { - var ( - // return bytes from evm execution - ret []byte - // vm errors do not imply a failed query, thus they don't populate the - // function's "err" value. - vmErr error - ) - - var ( - stateDB *statedb.StateDB - // save a reference to return to the previous stateDB - oldStateDB *statedb.StateDB = k.Bank.StateDB - ) - - defer func() { - if commit && err == nil && resp != nil { - k.Bank.StateDB = stateDB - } else { - k.Bank.StateDB = oldStateDB - } - }() - - stateDB = k.NewStateDB(ctx, txConfig) - evmObj = k.NewEVM(ctx, msg, evmConfig, tracer, stateDB) - +) (resp *evm.MsgEthereumTxResponse, err error) { leftoverGas := msg.Gas() // Allow the tracer captures the tx level events, mainly the gas consumption. @@ -296,7 +270,7 @@ func (k *Keeper) ApplyEvmMsg(ctx sdk.Context, ) if err != nil { // should have already been checked on Ante Handler - return nil, evmObj, errors.Wrap(err, "ApplyEvmMsg: intrinsic gas overflowed") + return nil, errors.Wrap(err, "ApplyEvmMsg: intrinsic gas overflowed") } // Check if the provided gas in the message is enough to cover the intrinsic @@ -307,7 +281,7 @@ func (k *Keeper) ApplyEvmMsg(ctx sdk.Context, // don't go through Ante Handler. if leftoverGas < intrinsicGas { // eth_estimateGas will check for this exact error - return nil, evmObj, errors.Wrapf( + return nil, errors.Wrapf( core.ErrIntrinsicGas, "ApplyEvmMsg: provided msg.Gas (%d) is less than intrinsic gas cost (%d)", leftoverGas, intrinsicGas, @@ -318,7 +292,7 @@ func (k *Keeper) ApplyEvmMsg(ctx sdk.Context, // access list preparation is moved from ante handler to here, because it's // needed when `ApplyMessage` is called under contexts where ante handlers // are not run, for example `eth_call` and `eth_estimateGas`. - stateDB.PrepareAccessList( + evmObj.StateDB.PrepareAccessList( msg.From(), msg.To(), evmObj.ActivePrecompiles(params.Rules{}), @@ -327,13 +301,16 @@ func (k *Keeper) ApplyEvmMsg(ctx sdk.Context, msgWei, err := ParseWeiAsMultipleOfMicronibi(msg.Value()) if err != nil { - return nil, evmObj, errors.Wrapf(err, "ApplyEvmMsg: invalid wei amount %s", msg.Value()) + return nil, errors.Wrapf(err, "ApplyEvmMsg: invalid wei amount %s", msg.Value()) } // take over the nonce management from evm: // - reset sender's nonce to msg.Nonce() before calling evm. // - increase sender's nonce by one no matter the result. - stateDB.SetNonce(sender.Address(), msg.Nonce()) + evmObj.StateDB.SetNonce(sender.Address(), msg.Nonce()) + + var ret []byte + var vmErr error if contractCreation { ret, _, leftoverGas, vmErr = evmObj.Create( sender, @@ -351,7 +328,7 @@ func (k *Keeper) ApplyEvmMsg(ctx sdk.Context, ) } // Increment nonce after processing the message - stateDB.SetNonce(sender.Address(), msg.Nonce()+1) + evmObj.StateDB.SetNonce(sender.Address(), msg.Nonce()+1) // EVM execution error needs to be available for the JSON-RPC client var vmError string @@ -361,13 +338,15 @@ func (k *Keeper) ApplyEvmMsg(ctx sdk.Context, // The dirty states in `StateDB` is either committed or discarded after return if commit { - if err := stateDB.Commit(); err != nil { - return nil, evmObj, errors.Wrap(err, "ApplyEvmMsg: failed to commit stateDB") + 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 { - return nil, evmObj, errors.Wrapf(core.ErrGasUintOverflow, "ApplyEvmMsg: message gas limit (%d) < leftover gas (%d)", msg.Gas(), leftoverGas) + return nil, errors.Wrapf(core.ErrGasUintOverflow, "ApplyEvmMsg: message gas limit (%d) < leftover gas (%d)", msg.Gas(), leftoverGas) } // TODO: UD-DEBUG: Clarify text below. @@ -386,13 +365,13 @@ func (k *Keeper) ApplyEvmMsg(ctx sdk.Context, refundQuotient = 1 // 100% refund } temporaryGasUsed := msg.Gas() - leftoverGas - refund := GasToRefund(stateDB.GetRefund(), temporaryGasUsed, refundQuotient) + refund := GasToRefund(evmObj.StateDB.GetRefund(), temporaryGasUsed, refundQuotient) // update leftoverGas and temporaryGasUsed with refund amount leftoverGas += refund temporaryGasUsed -= refund if msg.Gas() < leftoverGas { - return nil, evmObj, errors.Wrapf(core.ErrGasUintOverflow, "ApplyEvmMsg: message gas limit (%d) < leftover gas (%d)", msg.Gas(), leftoverGas) + return nil, errors.Wrapf(core.ErrGasUintOverflow, "ApplyEvmMsg: message gas limit (%d) < leftover gas (%d)", msg.Gas(), leftoverGas) } // Min gas used is a % of gasLimit @@ -407,9 +386,9 @@ func (k *Keeper) ApplyEvmMsg(ctx sdk.Context, GasUsed: gasUsed, VmError: vmError, Ret: ret, - Logs: evm.NewLogsFromEth(stateDB.Logs()), - Hash: txConfig.TxHash.Hex(), - }, evmObj, nil + Logs: evm.NewLogsFromEth(evmObj.StateDB.(*statedb.StateDB).Logs()), + Hash: txHash.Hex(), + }, nil } func ParseWeiAsMultipleOfMicronibi(weiInt *big.Int) (newWeiInt *big.Int, err error) { @@ -547,16 +526,37 @@ func (k Keeper) convertCoinToEvmBornCoin( // 2 | Mint ERC20 tokens to the recipient erc20Addr := funTokenMapping.Erc20Addr.Address - evmResp, err := k.CallContract( - ctx, - embeds.SmartContract_ERC20Minter.ABI, + contractInput, err := embeds.SmartContract_ERC20Minter.ABI.Pack("mint", recipient, coin.Amount.BigInt()) + if err != nil { + return nil, err + } + evmMsg := gethcore.NewMessage( evm.EVM_MODULE_ADDRESS, &erc20Addr, + k.GetAccNonce(ctx, evm.EVM_MODULE_ADDRESS), + big.NewInt(0), + Erc20GasLimitExecute, + big.NewInt(0), + big.NewInt(0), + big.NewInt(0), + contractInput, + gethcore.AccessList{}, true, + ) + txConfig := k.TxConfig(ctx, gethcommon.Hash{}) + var stateDB *statedb.StateDB = k.Bank.StateDB + if stateDB == nil { + stateDB = k.NewStateDB(ctx, txConfig) + } + evmObj := k.NewEVM(ctx, evmMsg, k.GetEVMConfig(ctx), nil /*tracer*/, stateDB) + evmResp, err := k.CallContractWithInput( + ctx, + evmObj, + evm.EVM_MODULE_ADDRESS, + &erc20Addr, + true, /*commit*/ + contractInput, Erc20GasLimitExecute, - "mint", - recipient, - coin.Amount.BigInt(), ) if err != nil { return nil, err @@ -567,6 +567,13 @@ func (k Keeper) convertCoinToEvmBornCoin( return nil, fmt.Errorf("failed to mint erc-20 tokens of contract %s", erc20Addr.String()) } + + err = stateDB.Commit() + if err != nil { + return nil, errors.Wrap(err, "failed to commit stateDB") + } + k.Bank.StateDB = nil + _ = ctx.EventManager().EmitTypedEvent(&evm.EventConvertCoinToEvm{ Sender: sender.String(), Erc20ContractAddress: erc20Addr.String(), @@ -588,6 +595,12 @@ func (k Keeper) convertCoinToEvmBornERC20( coin sdk.Coin, funTokenMapping evm.FunToken, ) (*evm.MsgConvertCoinToEvmResponse, error) { + // needs to run first to populate the StateDB on the BankKeeperExtension + var stateDB *statedb.StateDB = k.Bank.StateDB + if stateDB == nil { + stateDB = k.NewStateDB(ctx, k.TxConfig(ctx, gethcommon.Hash{})) + } + erc20Addr := funTokenMapping.Erc20Addr.Address // 1 | Caller transfers Bank Coins to be converted to ERC20 tokens. if err := k.Bank.SendCoinsFromAccountToModule( @@ -599,6 +612,14 @@ func (k Keeper) convertCoinToEvmBornERC20( return nil, errors.Wrap(err, "error sending Bank Coins to the EVM") } + // 3 | In the FunToken ERC20 → BC conversion process that preceded this + // TxMsg, the Bank Coins were minted. Consequently, to preserve an invariant + // on the sum of the FunToken's bank and ERC20 supply, we burn the coins here + // in the BC → ERC20 conversion. + if err := k.Bank.BurnCoins(ctx, evm.ModuleName, sdk.NewCoins(coin)); err != nil { + return nil, errors.Wrap(err, "failed to burn coins") + } + // 2 | EVM sends ERC20 tokens to the "to" account. // This should never fail due to the EVM account lacking ERc20 fund because // the account must have sent the EVM module ERC20 tokens in the mapping @@ -608,25 +629,42 @@ func (k Keeper) convertCoinToEvmBornERC20( // converted to its Bank Coin representation, a balance of the ERC20 is left // inside the EVM module account in order to convert the coins back to // ERC20s. - _, _, err := k.ERC20().Transfer( + contractInput, err := embeds.SmartContract_ERC20Minter.ABI.Pack("transfer", recipient, coin.Amount.BigInt()) + if err != nil { + return nil, err + } + evmMsg := gethcore.NewMessage( + evm.EVM_MODULE_ADDRESS, + &erc20Addr, + k.GetAccNonce(ctx, evm.EVM_MODULE_ADDRESS), + big.NewInt(0), + Erc20GasLimitExecute, + big.NewInt(0), + big.NewInt(0), + big.NewInt(0), + contractInput, + gethcore.AccessList{}, + true, + ) + evmObj := k.NewEVM(ctx, evmMsg, k.GetEVMConfig(ctx), nil /*tracer*/, stateDB) + _, _, err = k.ERC20().Transfer( erc20Addr, evm.EVM_MODULE_ADDRESS, recipient, coin.Amount.BigInt(), ctx, + evmObj, ) if err != nil { return nil, errors.Wrap(err, "failed to transfer ERC-20 tokens") } - // 3 | In the FunToken ERC20 → BC conversion process that preceded this - // TxMsg, the Bank Coins were minted. Consequently, to preserve an invariant - // on the sum of the FunToken's bank and ERC20 supply, we burn the coins here - // in the BC → ERC20 conversion. - err = k.Bank.BurnCoins(ctx, evm.ModuleName, sdk.NewCoins(coin)) - if err != nil { - return nil, errors.Wrap(err, "failed to burn coins") + // Commit the stateDB to the BankKeeperExtension because we don't go through + // ApplyEvmMsg at all in this tx. + 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/keeper/random_test.go b/x/evm/keeper/random_test.go index 606c224cc..b08ee2065 100644 --- a/x/evm/keeper/random_test.go +++ b/x/evm/keeper/random_test.go @@ -11,13 +11,14 @@ import ( // TestRandom tests the random value generation within the EVM. func (s *Suite) TestRandom() { deps := evmtest.NewTestDeps() + evmObj, _ := deps.NewEVM() deployResp, err := evmtest.DeployContract(&deps, embeds.SmartContract_TestRandom) s.Require().NoError(err) randomContractAddr := deployResp.ContractAddr // highjacked LoadERC20BigInt method as it perfectly fits the need of this test - random1, err := deps.EvmKeeper.LoadERC20BigInt( - deps.Ctx, embeds.SmartContract_TestRandom.ABI, randomContractAddr, "getRandom", + random1, err := deps.EvmKeeper.ERC20().LoadERC20BigInt( + deps.Ctx, evmObj, embeds.SmartContract_TestRandom.ABI, randomContractAddr, "getRandom", ) s.Require().NoError(err) s.Require().NotNil(random1) @@ -25,8 +26,9 @@ func (s *Suite) TestRandom() { // Update block time to check that random changes deps.Ctx = deps.Ctx.WithBlockTime(deps.Ctx.BlockTime().Add(1 * time.Second)) - random2, err := deps.EvmKeeper.LoadERC20BigInt( - deps.Ctx, embeds.SmartContract_TestRandom.ABI, randomContractAddr, "getRandom", + evmObj, _ = deps.NewEVM() + random2, err := deps.EvmKeeper.ERC20().LoadERC20BigInt( + deps.Ctx, evmObj, embeds.SmartContract_TestRandom.ABI, randomContractAddr, "getRandom", ) s.Require().NoError(err) s.Require().NotNil(random1) diff --git a/x/evm/keeper/vm_config.go b/x/evm/keeper/vm_config.go index 2f8232ad6..1bf53fbbd 100644 --- a/x/evm/keeper/vm_config.go +++ b/x/evm/keeper/vm_config.go @@ -2,37 +2,22 @@ package keeper import ( - "math/big" - - "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" - stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/vm" + "github.com/NibiruChain/nibiru/v2/app/appconst" "github.com/NibiruChain/nibiru/v2/x/evm" "github.com/NibiruChain/nibiru/v2/x/evm/statedb" ) -func (k *Keeper) GetEVMConfig( - ctx sdk.Context, proposerAddress sdk.ConsAddress, chainID *big.Int, -) (*statedb.EVMConfig, error) { - params := k.GetParams(ctx) - ethCfg := evm.EthereumConfig(chainID) - - // get the coinbase address from the block proposer - coinbase, err := k.GetCoinbaseAddress(ctx, proposerAddress) - if err != nil { - return nil, errors.Wrap(err, "failed to obtain coinbase address") - } - - return &statedb.EVMConfig{ - Params: params, - ChainConfig: ethCfg, - BlockCoinbase: coinbase, +func (k *Keeper) GetEVMConfig(ctx sdk.Context) statedb.EVMConfig { + return statedb.EVMConfig{ + Params: k.GetParams(ctx), + ChainConfig: evm.EthereumConfig(appconst.GetEthChainID(ctx.ChainID())), + BlockCoinbase: k.GetCoinbaseAddress(ctx), BaseFeeWei: k.BaseFeeWeiPerGas(ctx), - }, nil + } } // TxConfig loads `TxConfig` from current transient storage @@ -51,7 +36,7 @@ func (k *Keeper) TxConfig( // EIPs enabled on the module parameters. The config generated uses the default // JumpTable from the EVM. func (k Keeper) VMConfig( - ctx sdk.Context, _ core.Message, cfg *statedb.EVMConfig, tracer vm.EVMLogger, + ctx sdk.Context, cfg *statedb.EVMConfig, tracer vm.EVMLogger, ) vm.Config { var debug bool if _, ok := tracer.(evm.NoOpTracer); !ok { @@ -71,18 +56,16 @@ func (k Keeper) VMConfig( // current block. It corresponds to the [COINBASE op code]. // // [COINBASE op code]: https://ethereum.org/en/developers/docs/evm/opcodes/ -func (k Keeper) GetCoinbaseAddress(ctx sdk.Context, proposerAddress sdk.ConsAddress) (common.Address, error) { - validator, found := k.stakingKeeper.GetValidatorByConsAddr(ctx, ParseProposerAddr(ctx, proposerAddress)) +func (k Keeper) GetCoinbaseAddress(ctx sdk.Context) common.Address { + proposerAddress := sdk.ConsAddress(ctx.BlockHeader().ProposerAddress) + validator, found := k.stakingKeeper.GetValidatorByConsAddr(ctx, proposerAddress) if !found { - return common.Address{}, errors.Wrapf( - stakingtypes.ErrNoValidatorFound, - "failed to retrieve validator from block proposer address %s", - proposerAddress.String(), - ) + // should never happen, but just in case, return an empty address + // we don't really care about the coinbase adresss since we're PoS and not PoW + return common.Address{} } - coinbase := common.BytesToAddress(validator.GetOperator()) - return coinbase, nil + return common.BytesToAddress(validator.GetOperator()) } // ParseProposerAddr returns current block proposer's address when provided diff --git a/x/evm/msg.go b/x/evm/msg.go index b11e59a0a..ec7520bb1 100644 --- a/x/evm/msg.go +++ b/x/evm/msg.go @@ -41,12 +41,6 @@ var ( // NewTx returns a reference to a new Ethereum transaction message. func NewTx( tx *EvmTxArgs, -) *MsgEthereumTx { - return newMsgEthereumTx(tx) -} - -func newMsgEthereumTx( - tx *EvmTxArgs, ) *MsgEthereumTx { var ( cid, amt, gp *sdkmath.Int diff --git a/x/evm/precompile/funtoken.go b/x/evm/precompile/funtoken.go index c77248d3f..ddce4fe79 100644 --- a/x/evm/precompile/funtoken.go +++ b/x/evm/precompile/funtoken.go @@ -69,15 +69,15 @@ func (p precompileFunToken) Run( method := startResult.Method switch PrecompileMethod(method.Name) { case FunTokenMethod_sendToBank: - bz, err = p.sendToBank(startResult, contract.CallerAddress, readonly) + bz, err = p.sendToBank(startResult, contract.CallerAddress, readonly, evm) case FunTokenMethod_balance: - bz, err = p.balance(startResult, contract) + bz, err = p.balance(startResult, contract, evm) case FunTokenMethod_bankBalance: bz, err = p.bankBalance(startResult, contract) case FunTokenMethod_whoAmI: bz, err = p.whoAmI(startResult, contract) case FunTokenMethod_sendToEvm: - bz, err = p.sendToEvm(startResult, contract.CallerAddress, readonly) + bz, err = p.sendToEvm(startResult, contract.CallerAddress, readonly, evm) case FunTokenMethod_bankMsgSend: bz, err = p.bankMsgSend(startResult, contract.CallerAddress, readonly) default: @@ -135,6 +135,7 @@ func (p precompileFunToken) sendToBank( startResult OnRunStartResult, caller gethcommon.Address, readOnly bool, + evmObj *vm.EVM, ) (bz []byte, err error) { ctx, method, args := startResult.CacheCtx, startResult.Method, startResult.Args if err := assertNotReadonlyTx(readOnly, method); err != nil { @@ -170,9 +171,15 @@ func (p precompileFunToken) sendToBank( return nil, fmt.Errorf("\"to\" is not a valid address (%s): %w", to, err) } - // Caller transfers ERC20 to the EVM account - transferTo := evm.EVM_MODULE_ADDRESS - gotAmount, transferResp, err := p.evmKeeper.ERC20().Transfer(erc20, caller, transferTo, amount, ctx) + // Caller transfers ERC20 to the EVM module account + gotAmount, transferResp, err := p.evmKeeper.ERC20().Transfer( + erc20, /*erc20*/ + caller, /*from*/ + evm.EVM_MODULE_ADDRESS, /*to*/ + amount, /*value*/ + ctx, + evmObj, + ) if err != nil { return nil, fmt.Errorf("error in ERC20.transfer from caller to EVM account: %w", err) } @@ -185,7 +192,7 @@ func (p precompileFunToken) sendToBank( // owns the ERC20 contract and was the original minter of the ERC20 tokens. // Since we're sending them away and want accurate total supply tracking, the // tokens need to be burned. - burnResp, e := p.evmKeeper.ERC20().Burn(erc20, evm.EVM_MODULE_ADDRESS, gotAmount, ctx) + burnResp, e := p.evmKeeper.ERC20().Burn(erc20, evm.EVM_MODULE_ADDRESS, gotAmount, ctx, evmObj) if e != nil { err = fmt.Errorf("ERC20.Burn: %w", e) return @@ -196,7 +203,6 @@ func (p precompileFunToken) sendToBank( // 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.evmKeeper.Bank.StateDB = startResult.StateDB err = p.evmKeeper.Bank.MintCoins(ctx, evm.ModuleName, sdk.NewCoins(coinToSend)) if err != nil { return nil, fmt.Errorf("mint failed for module \"%s\" (%s): contract caller %s: %w", @@ -211,7 +217,6 @@ func (p precompileFunToken) sendToBank( // 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.evmKeeper.Bank.StateDB = startResult.StateDB err = p.evmKeeper.Bank.SendCoinsFromModuleToAccount( ctx, evm.ModuleName, @@ -292,6 +297,7 @@ func (p precompileFunToken) parseArgsSendToBank(args []any) ( func (p precompileFunToken) balance( start OnRunStartResult, contract *vm.Contract, + evmObj *vm.EVM, ) (bz []byte, err error) { method, args, ctx := start.Method, start.Args, start.CacheCtx defer func() { @@ -309,7 +315,7 @@ func (p precompileFunToken) balance( return } - erc20Bal, err := p.evmKeeper.ERC20().BalanceOf(funtoken.Erc20Addr.Address, addrEth, ctx) + erc20Bal, err := p.evmKeeper.ERC20().BalanceOf(funtoken.Erc20Addr.Address, addrEth, ctx, evmObj) if err != nil { return } @@ -540,6 +546,7 @@ func (p precompileFunToken) sendToEvm( startResult OnRunStartResult, caller gethcommon.Address, readOnly bool, + evmObj *vm.EVM, ) ([]byte, error) { ctx, method, args := startResult.CacheCtx, startResult.Method, startResult.Args if err := assertNotReadonlyTx(readOnly, method); err != nil { @@ -585,7 +592,12 @@ func (p precompileFunToken) sendToEvm( // 2) mint (or unescrow) the ERC20 erc20Addr := funtoken.Erc20Addr.Address actualAmt, err := p.mintOrUnescrowERC20( - ctx, erc20Addr, toEthAddr, coinToSend.Amount.BigInt(), funtoken, + ctx, + erc20Addr, /*erc20Contract*/ + toEthAddr, /*to*/ + coinToSend.Amount.BigInt(), /*amount*/ + funtoken, /*funtoken*/ + evmObj, ) if err != nil { return nil, err @@ -610,16 +622,18 @@ func (p precompileFunToken) mintOrUnescrowERC20( to gethcommon.Address, amount *big.Int, funtoken evm.FunToken, + evmObj *vm.EVM, ) (*big.Int, error) { // If funtoken is "IsMadeFromCoin", we own the ERC20 contract, so we can mint. // If not, we do a transfer from EVM module to 'to' address using escrowed tokens. if funtoken.IsMadeFromCoin { _, err := p.evmKeeper.ERC20().Mint( - erc20Addr, - evm.EVM_MODULE_ADDRESS, - to, + erc20Addr, /*erc20Contract*/ + evm.EVM_MODULE_ADDRESS, /*from*/ + to, /*to*/ amount, ctx, + evmObj, ) if err != nil { return nil, fmt.Errorf("mint erc20 error: %w", err) @@ -627,26 +641,13 @@ func (p precompileFunToken) mintOrUnescrowERC20( // For an owner-minted contract, the entire `amount` is minted. return amount, nil } else { - balBefore, err := p.evmKeeper.ERC20().BalanceOf(erc20Addr, to, ctx) - if err != nil { - return nil, fmt.Errorf("balanceOf to check erc20 error: %w", err) - } - _, _, err = p.evmKeeper.ERC20().Transfer( - erc20Addr, evm.EVM_MODULE_ADDRESS, to, amount, ctx, + balanceIncrease, _, err := p.evmKeeper.ERC20().Transfer( + erc20Addr, evm.EVM_MODULE_ADDRESS, to, amount, ctx, evmObj, ) if err != nil { return nil, fmt.Errorf("erc20.transfer from module to user: %w", err) } - - balAfter, err := p.evmKeeper.ERC20().BalanceOf(erc20Addr, to, ctx) - if err != nil { - return nil, fmt.Errorf("balanceOf to check erc20 error: %w", err) - } - actualReceived := new(big.Int).Sub(balAfter, balBefore) - if actualReceived.Sign() <= 0 { - return nil, fmt.Errorf("failed: no tokens were actually unescrowed") - } - return actualReceived, nil + return balanceIncrease, nil } } diff --git a/x/evm/precompile/funtoken_test.go b/x/evm/precompile/funtoken_test.go index 964676d50..2cd2bd34b 100644 --- a/x/evm/precompile/funtoken_test.go +++ b/x/evm/precompile/funtoken_test.go @@ -5,9 +5,9 @@ import ( "math/big" "testing" - sdkmath "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" gethcommon "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" "github.com/NibiruChain/nibiru/v2/eth" @@ -21,22 +21,15 @@ import ( ) // TestSuite: Runs all the tests in the suite. -func TestSuite(t *testing.T) { - suite.Run(t, new(UtilsSuite)) - suite.Run(t, new(FuntokenSuite)) - suite.Run(t, new(WasmSuite)) -} - type FuntokenSuite struct { suite.Suite - deps evmtest.TestDeps } -func (s *FuntokenSuite) SetupSuite() { - s.deps = evmtest.NewTestDeps() +func TestFuntokenSuite(t *testing.T) { + suite.Run(t, new(FuntokenSuite)) } -func (s *FuntokenSuite) TestFailToPackABI() { +func TestFailToPackABI(t *testing.T) { testcases := []struct { name string methodName string @@ -75,119 +68,86 @@ func (s *FuntokenSuite) TestFailToPackABI() { }, } - abi := embeds.SmartContract_FunToken.ABI - for _, tc := range testcases { - s.Run(tc.name, func() { - input, err := abi.Pack(tc.methodName, tc.callArgs...) - s.ErrorContains(err, tc.wantError) - s.Nil(input) + t.Run(tc.name, func(t *testing.T) { + input, err := embeds.SmartContract_FunToken.ABI.Pack(tc.methodName, tc.callArgs...) + require.ErrorContains(t, err, tc.wantError) + require.Nil(t, input) }) } } -func (s *FuntokenSuite) TestWhoAmI() { +func TestWhoAmI(t *testing.T) { deps := evmtest.NewTestDeps() + callWhoAmI := func(arg string) (evmResp *evm.MsgEthereumTxResponse, err error) { + fmt.Printf("arg: %s", arg) + contractInput, err := embeds.SmartContract_FunToken.ABI.Pack("whoAmI", arg) + require.NoError(t, err) + evmObj, _ := deps.NewEVM() + return deps.EvmKeeper.CallContractWithInput( + deps.Ctx, + evmObj, + deps.Sender.EthAddr, + &precompile.PrecompileAddr_FunToken, + false, + contractInput, + evmtest.FunTokenGasLimitSendToEvm, + ) + } + for accIdx, acc := range []evmtest.EthPrivKeyAcc{ deps.Sender, evmtest.NewEthPrivAcc(), } { - s.T().Logf("test account %d, use both address formats", accIdx) - callWhoAmIWithArg := func(arg string) (evmResp *evm.MsgEthereumTxResponse, err error) { - fmt.Printf("arg: %s", arg) - return deps.EvmKeeper.CallContract( - deps.Ctx, - embeds.SmartContract_FunToken.ABI, - deps.Sender.EthAddr, - &precompile.PrecompileAddr_FunToken, - false, - keeper.Erc20GasLimitExecute, - "whoAmI", - []any{ - arg, // who - }..., - ) - } + t.Logf("test account %d, use both address formats", accIdx) + for _, arg := range []string{acc.NibiruAddr.String(), acc.EthAddr.Hex()} { - evmResp, err := callWhoAmIWithArg(arg) - s.Require().NoError(err, evmResp) + evmResp, err := callWhoAmI(arg) + require.NoError(t, err) gotAddrEth, gotAddrBech32, err := new(FunTokenWhoAmIReturn).ParseFromResp(evmResp) - s.NoError(err) - s.Equal(acc.EthAddr.Hex(), gotAddrEth.Hex()) - s.Equal(acc.NibiruAddr.String(), gotAddrBech32) + require.NoError(t, err) + require.Equal(t, acc.EthAddr.Hex(), gotAddrEth.Hex()) + require.Equal(t, acc.NibiruAddr.String(), gotAddrBech32) } // Sad path check - evmResp, err := callWhoAmIWithArg("not_an_address") - s.Require().ErrorContains( - err, "could not parse address as Nibiru Bech32 or Ethereum hexadecimal", evmResp, - ) - } -} - -// FunTokenWhoAmIReturn holds the return values from the "IFuntoken.whoAmI" -// method. The return bytes from successful calls of that method can be ABI -// unpacked into this struct. -type FunTokenWhoAmIReturn struct { - NibiruAcc struct { - EthAddr gethcommon.Address `abi:"ethAddr"` - Bech32Addr string `abi:"bech32Addr"` - } `abi:"whoAddrs"` -} - -func (out FunTokenWhoAmIReturn) ParseFromResp( - evmResp *evm.MsgEthereumTxResponse, -) (ethAddr gethcommon.Address, bech32Addr string, err error) { - err = embeds.SmartContract_FunToken.ABI.UnpackIntoInterface( - &out, - "whoAmI", - evmResp.Ret, - ) - if err != nil { - return + _, err := callWhoAmI("not_an_address") + require.ErrorContains(t, err, "could not parse address as Nibiru Bech32 or Ethereum hexadecimal") } - return out.NibiruAcc.EthAddr, out.NibiruAcc.Bech32Addr, nil } func (s *FuntokenSuite) TestHappyPath() { deps := evmtest.NewTestDeps() s.T().Log("Create FunToken mapping and ERC20") - bankDenom := "anycoin" - funtoken := evmtest.CreateFunTokenForBankCoin(&deps, bankDenom, &s.Suite) - + bankDenom := "unibi" + funtoken := evmtest.CreateFunTokenForBankCoin(deps, bankDenom, &s.Suite) erc20 := funtoken.Erc20Addr.Address - s.T().Log("Balances of the ERC20 should start empty") - evmtest.AssertERC20BalanceEqual(s.T(), deps, erc20, deps.Sender.EthAddr, big.NewInt(0)) - evmtest.AssertERC20BalanceEqual(s.T(), deps, erc20, evm.EVM_MODULE_ADDRESS, big.NewInt(0)) - s.Require().NoError(testapp.FundAccount( deps.App.BankKeeper, deps.Ctx, deps.Sender.NibiruAddr, - sdk.NewCoins(sdk.NewCoin(funtoken.BankDenom, sdk.NewInt(69_420))), + sdk.NewCoins(sdk.NewCoin(bankDenom, sdk.NewInt(69_420))), )) - s.Run("IFunToken.bankBalance", func() { - s.Require().NotEmpty(funtoken.BankDenom) - evmResp, err := deps.EvmKeeper.CallContract( + s.Run("IFunToken.bankBalance()", func() { + contractInput, err := embeds.SmartContract_FunToken.ABI.Pack("bankBalance", deps.Sender.EthAddr, funtoken.BankDenom) + s.Require().NoError(err) + evmObj, _ := deps.NewEVM() + evmResp, err := deps.EvmKeeper.CallContractWithInput( deps.Ctx, - embeds.SmartContract_FunToken.ABI, + evmObj, deps.Sender.EthAddr, &precompile.PrecompileAddr_FunToken, false, - keeper.Erc20GasLimitExecute, - "bankBalance", - []any{ - deps.Sender.EthAddr, // who - funtoken.BankDenom, // bankDenom - }..., + contractInput, + evmtest.FunTokenGasLimitSendToEvm, ) s.Require().NoError(err, evmResp) bal, ethAddr, bech32Addr, err := new(FunTokenBankBalanceReturn).ParseFromResp(evmResp) s.NoError(err) - s.Require().Equal("69420", bal.String()) + s.Require().Zero(bal.Cmp(big.NewInt(69_420))) s.Equal(deps.Sender.EthAddr.Hex(), ethAddr.Hex()) s.Equal(deps.Sender.NibiruAddr.String(), bech32Addr) }) @@ -196,7 +156,7 @@ func (s *FuntokenSuite) TestHappyPath() { sdk.WrapSDKContext(deps.Ctx), &evm.MsgConvertCoinToEvm{ Sender: deps.Sender.NibiruAddr.String(), - BankCoin: sdk.NewCoin(funtoken.BankDenom, sdk.NewInt(69_420)), + BankCoin: sdk.NewCoin(bankDenom, sdk.NewInt(69_420)), ToEthAddr: eth.EIP55Addr{ Address: deps.Sender.EthAddr, }, @@ -204,65 +164,73 @@ func (s *FuntokenSuite) TestHappyPath() { ) s.Require().NoError(err) - s.T().Log("Mint tokens - Fail from non-owner") - { - _, err = deps.EvmKeeper.CallContract(deps.Ctx, embeds.SmartContract_ERC20Minter.ABI, deps.Sender.EthAddr, &erc20, true, keeper.Erc20GasLimitExecute, "mint", deps.Sender.EthAddr, big.NewInt(69_420)) + s.Run("Mint tokens - Fail from non-owner", func() { + contractInput, err := embeds.SmartContract_ERC20Minter.ABI.Pack("mint", deps.Sender.EthAddr, big.NewInt(69_420)) + evmObj, _ := deps.NewEVM() + s.Require().NoError(err) + _, err = deps.EvmKeeper.CallContractWithInput( + deps.Ctx, + evmObj, + deps.Sender.EthAddr, + &erc20, + false, + contractInput, + evmtest.FunTokenGasLimitSendToEvm, + ) s.ErrorContains(err, "Ownable: caller is not the owner") - } - - randomAcc := testutil.AccAddress() + }) - s.T().Log("Send NIBI (FunToken) using precompile") - amtToSend := int64(420) - callArgs := []any{erc20, big.NewInt(amtToSend), randomAcc.String()} - input, err := embeds.SmartContract_FunToken.ABI.Pack(string(precompile.FunTokenMethod_sendToBank), callArgs...) - s.NoError(err) - - s.Require().NoError(testapp.FundFeeCollector(deps.App.BankKeeper, deps.Ctx, sdkmath.NewInt(20))) - _, ethTxResp, err := evmtest.CallContractTx( - &deps, - precompile.PrecompileAddr_FunToken, - input, - deps.Sender, - ) - s.Require().NoError(err) - s.Require().Empty(ethTxResp.VmError) - s.True(deps.App.BankKeeper == deps.App.EvmKeeper.Bank) + s.Run("IFunToken.sendToBank()", func() { + randomAcc := testutil.AccAddress() - evmtest.AssertERC20BalanceEqual( - s.T(), deps, erc20, deps.Sender.EthAddr, big.NewInt(69_000), - ) - evmtest.AssertERC20BalanceEqual( - s.T(), deps, erc20, evm.EVM_MODULE_ADDRESS, big.NewInt(0), - ) - s.Equal(sdk.NewInt(420).String(), - deps.App.BankKeeper.GetBalance(deps.Ctx, randomAcc, funtoken.BankDenom).Amount.String(), - ) - s.Require().NotNil(deps.EvmKeeper.Bank.StateDB) + input, err := embeds.SmartContract_FunToken.ABI.Pack(string(precompile.FunTokenMethod_sendToBank), erc20, big.NewInt(420), randomAcc.String()) + s.NoError(err) - s.T().Log("Parse the response contract addr and response bytes") - var sentAmt *big.Int - err = embeds.SmartContract_FunToken.ABI.UnpackIntoInterface( - &sentAmt, - string(precompile.FunTokenMethod_sendToBank), - ethTxResp.Ret, - ) - s.NoError(err) - s.Require().Equal("420", sentAmt.String()) + evmObj, _ := deps.NewEVM() - s.Run("IFuntoken.balance", func() { - evmResp, err := deps.EvmKeeper.CallContract( + ethTxResp, err := deps.EvmKeeper.CallContractWithInput( deps.Ctx, - embeds.SmartContract_FunToken.ABI, + evmObj, deps.Sender.EthAddr, &precompile.PrecompileAddr_FunToken, - false, + true, /*commit*/ + input, keeper.Erc20GasLimitExecute, - "balance", - []any{ - deps.Sender.EthAddr, // who - erc20, // funtoken - }..., + ) + s.Require().NoError(err) + s.Require().Empty(ethTxResp.VmError) + s.True(deps.App.BankKeeper == deps.App.EvmKeeper.Bank) + + evmtest.AssertERC20BalanceEqualWithDescription( + s.T(), deps, evmObj, erc20, deps.Sender.EthAddr, big.NewInt(69_000), "expect 69000 balance", + ) + evmtest.AssertERC20BalanceEqualWithDescription( + s.T(), deps, evmObj, erc20, evm.EVM_MODULE_ADDRESS, big.NewInt(0), "expect 0 balance", + ) + s.Require().True(deps.App.BankKeeper.GetBalance(deps.Ctx, randomAcc, funtoken.BankDenom).Amount.Equal(sdk.NewInt(420))) + + s.T().Log("Parse the response contract addr and response bytes") + var sentAmt *big.Int + s.NoError(embeds.SmartContract_FunToken.ABI.UnpackIntoInterface( + &sentAmt, + string(precompile.FunTokenMethod_sendToBank), + ethTxResp.Ret, + )) + s.Require().Zero(sentAmt.Cmp(big.NewInt(420))) + }) + + s.Run("IFuntoken.balance", func() { + contractInput, err := embeds.SmartContract_FunToken.ABI.Pack("balance", deps.Sender.EthAddr, erc20) + s.Require().NoError(err) + evmObj, _ := deps.NewEVM() + evmResp, err := deps.EvmKeeper.CallContractWithInput( + deps.Ctx, + evmObj, + deps.Sender.EthAddr, // from + &precompile.PrecompileAddr_FunToken, // to + false, // commit + contractInput, + keeper.Erc20GasLimitQuery, ) s.Require().NoError(err, evmResp) @@ -271,79 +239,15 @@ func (s *FuntokenSuite) TestHappyPath() { s.Equal(funtoken.Erc20Addr, bals.FunToken.Erc20Addr) s.Equal(funtoken.BankDenom, bals.FunToken.BankDenom) s.Equal(deps.Sender.EthAddr, bals.Account) - s.Equal("0", bals.BalanceBank.String()) - s.Equal("69000", bals.BalanceERC20.String()) + s.Zero(bals.BalanceBank.Cmp(big.NewInt(0))) + s.Zero(bals.BalanceERC20.Cmp(big.NewInt(69_000))) }) } -// FunTokenBalanceReturn holds the return values from the "IFuntoken.balance" -// method. The return bytes from successful calls of that method can be ABI -// unpacked into this struct. -type FunTokenBalanceReturn struct { - Erc20Bal *big.Int `abi:"erc20Balance"` - BankBal *big.Int `abi:"bankBalance"` - Token struct { - Erc20 gethcommon.Address `abi:"erc20"` - BankDenom string `abi:"bankDenom"` - } `abi:"token"` - NibiruAcc struct { - EthAddr gethcommon.Address `abi:"ethAddr"` - Bech32Addr string `abi:"bech32Addr"` - } `abi:"whoAddrs"` -} - -func (out FunTokenBalanceReturn) ParseFromResp( - evmResp *evm.MsgEthereumTxResponse, -) (bals evmtest.FunTokenBalanceAssert, err error) { - err = embeds.SmartContract_FunToken.ABI.UnpackIntoInterface( - &out, - "balance", - evmResp.Ret, - ) - if err != nil { - return - } - return evmtest.FunTokenBalanceAssert{ - FunToken: evm.FunToken{ - Erc20Addr: eth.EIP55Addr{Address: out.Token.Erc20}, - BankDenom: out.Token.BankDenom, - }, - Account: out.NibiruAcc.EthAddr, - BalanceBank: out.BankBal, - BalanceERC20: out.Erc20Bal, - }, nil -} - -// FunTokenBankBalanceReturn holds the return values from the -// "IFuntoken.bankBalance" method. The return bytes from successful calls of that -// method can be ABI unpacked into this struct. -type FunTokenBankBalanceReturn struct { - BankBal *big.Int `abi:"bankBalance"` - NibiruAcc struct { - EthAddr gethcommon.Address `abi:"ethAddr"` - Bech32Addr string `abi:"bech32Addr"` - } `abi:"whoAddrs"` -} - -func (out FunTokenBankBalanceReturn) ParseFromResp( - evmResp *evm.MsgEthereumTxResponse, -) (bal *big.Int, ethAddr gethcommon.Address, bech32Addr string, err error) { - err = embeds.SmartContract_FunToken.ABI.UnpackIntoInterface( - &out, - "bankBalance", - evmResp.Ret, - ) - if err != nil { - return - } - return out.BankBal, out.NibiruAcc.EthAddr, out.NibiruAcc.Bech32Addr, nil -} - func (s *FuntokenSuite) TestPrecompileLocalGas() { - deps := s.deps + deps := evmtest.NewTestDeps() + funtoken := evmtest.CreateFunTokenForBankCoin(deps, evm.EVMBankDenom, &s.Suite) randomAcc := testutil.AccAddress() - bankDenom := "unibi" - funtoken := evmtest.CreateFunTokenForBankCoin(&s.deps, bankDenom, &s.Suite) deployResp, err := evmtest.DeployContract( &deps, embeds.SmartContract_TestFunTokenPrecompileLocalGas, @@ -360,72 +264,92 @@ func (s *FuntokenSuite) TestPrecompileLocalGas() { sdk.NewCoins(sdk.NewCoin(funtoken.BankDenom, sdk.NewInt(1000))), )) - s.T().Log("Fund contract with erc20 coins") - _, err = deps.EvmKeeper.ConvertCoinToEvm( - sdk.WrapSDKContext(deps.Ctx), - &evm.MsgConvertCoinToEvm{ - Sender: deps.Sender.NibiruAddr.String(), - BankCoin: sdk.NewCoin(funtoken.BankDenom, sdk.NewInt(1000)), - ToEthAddr: eth.EIP55Addr{ - Address: contractAddr, + s.Run("Fund contract with erc20 coins", func() { + _, err = deps.EvmKeeper.ConvertCoinToEvm( + sdk.WrapSDKContext(deps.Ctx), + &evm.MsgConvertCoinToEvm{ + Sender: deps.Sender.NibiruAddr.String(), + BankCoin: sdk.NewCoin(funtoken.BankDenom, sdk.NewInt(1000)), + ToEthAddr: eth.EIP55Addr{ + Address: contractAddr, + }, }, - }, - ) - s.Require().NoError(err) + ) + s.Require().NoError(err) + }) - s.T().Log("Happy: callBankSend with default gas") - _, err = deps.EvmKeeper.CallContract( - deps.Ctx, - embeds.SmartContract_TestFunTokenPrecompileLocalGas.ABI, - deps.Sender.EthAddr, - &contractAddr, - true, - evmtest.FunTokenGasLimitSendToEvm, - "callBankSend", - big.NewInt(1), - randomAcc.String(), - ) - s.Require().NoError(err) + s.Run("Happy: callBankSend with default gas", func() { + contractInput, err := embeds.SmartContract_TestFunTokenPrecompileLocalGas.ABI.Pack( + "callBankSend", + big.NewInt(1), + randomAcc.String(), + ) + s.Require().NoError(err) + evmObj, _ := deps.NewEVM() + _, err = deps.EvmKeeper.CallContractWithInput( + deps.Ctx, + evmObj, + deps.Sender.EthAddr, + &contractAddr, + true, + contractInput, + evmtest.FunTokenGasLimitSendToEvm, + ) + s.Require().NoError(err) + }) - s.T().Log("Happy: callBankSend with local gas - sufficient gas amount") - _, err = deps.EvmKeeper.CallContract( - deps.Ctx, - embeds.SmartContract_TestFunTokenPrecompileLocalGas.ABI, - deps.Sender.EthAddr, - &contractAddr, - true, - evmtest.FunTokenGasLimitSendToEvm, // gasLimit for the entire call - "callBankSendLocalGas", - big.NewInt(1), // erc20 amount - randomAcc.String(), // to - big.NewInt(int64(evmtest.FunTokenGasLimitSendToEvm)), // customGas - ) - s.Require().NoError(err) + s.Run("Happy: callBankSend with local gas - sufficient gas amount", func() { + contractInput, err := embeds.SmartContract_TestFunTokenPrecompileLocalGas.ABI.Pack( + "callBankSendLocalGas", + big.NewInt(1), + randomAcc.String(), + big.NewInt(int64(evmtest.FunTokenGasLimitSendToEvm)), + ) + s.Require().NoError(err) + evmObj, _ := deps.NewEVM() + _, err = deps.EvmKeeper.CallContractWithInput( + deps.Ctx, + evmObj, + deps.Sender.EthAddr, + &contractAddr, + true, + contractInput, + evmtest.FunTokenGasLimitSendToEvm, // gasLimit for the entire call + ) + s.Require().NoError(err) + }) - s.T().Log("Sad: callBankSend with local gas - insufficient gas amount") - _, err = deps.EvmKeeper.CallContract( - deps.Ctx, - embeds.SmartContract_TestFunTokenPrecompileLocalGas.ABI, - deps.Sender.EthAddr, - &contractAddr, - true, - evmtest.FunTokenGasLimitSendToEvm, // gasLimit for the entire call - "callBankSendLocalGas", - big.NewInt(1), // erc20 amount - randomAcc.String(), // to - big.NewInt(50_000), // customGas - too small - ) - s.Require().ErrorContains(err, "execution reverted") + s.Run("Sad: callBankSend with local gas - insufficient gas amount", func() { + contractInput, err := embeds.SmartContract_TestFunTokenPrecompileLocalGas.ABI.Pack( + "callBankSendLocalGas", + big.NewInt(1), + randomAcc.String(), + big.NewInt(50_000), // customGas - too small + ) + s.Require().NoError(err) + evmObj, _ := deps.NewEVM() + _, err = deps.EvmKeeper.CallContractWithInput( + deps.Ctx, + evmObj, + deps.Sender.EthAddr, + &contractAddr, + true, + contractInput, + evmtest.FunTokenGasLimitSendToEvm, // gasLimit for the entire call + ) + s.Require().ErrorContains(err, "execution reverted") + }) } -func (s *FuntokenSuite) TestSendToEvm() { +func (s *FuntokenSuite) TestSendToEvm_MadeFromCoin() { deps := evmtest.NewTestDeps() - s.Require().NoError(testapp.FundFeeCollector(deps.App.BankKeeper, deps.Ctx, sdkmath.NewInt(20))) + + s.T().Log("create evmObj") + evmObj, _ := deps.NewEVM() s.T().Log("1) Create a new FunToken from coin 'ulibi'") bankDenom := "ulibi" - funtoken := evmtest.CreateFunTokenForBankCoin(&deps, bankDenom, &s.Suite) - fmt.Println(funtoken) + funtoken := evmtest.CreateFunTokenForBankCoin(deps, bankDenom, &s.Suite) erc20Addr := funtoken.Erc20Addr.Address s.T().Log("2) Fund the sender with some ulibi on the bank side") @@ -437,111 +361,100 @@ func (s *FuntokenSuite) TestSendToEvm() { ) s.Require().NoError(err) - s.T().Log("Check the user starts with 0 ERC20 tokens") - evmtest.AssertERC20BalanceEqual(s.T(), deps, erc20Addr, deps.Sender.EthAddr, big.NewInt(0)) - - s.T().Log("3) Call the new method: sendToEvm(string bankDenom, uint256 amount, string to)") - callArgs := []any{ - bankDenom, - big.NewInt(1000), // amount - deps.Sender.EthAddr.Hex(), // 'to' can be bech32 or hex - } - - input, err := embeds.SmartContract_FunToken.ABI.Pack( - "sendToEvm", - callArgs..., - ) - s.Require().NoError(err) - - _, ethTxResp, err := evmtest.CallContractTx( - &deps, - precompile.PrecompileAddr_FunToken, - input, - deps.Sender, - ) - s.Require().NoError(err) - s.Require().Empty(ethTxResp.VmError, "sendToEvm VMError") + s.Run("Call sendToEvm(string bankDenom, uint256 amount, string to)", func() { + contractInput, err := embeds.SmartContract_FunToken.ABI.Pack( + "sendToEvm", + bankDenom, + big.NewInt(1000), + deps.Sender.EthAddr.Hex(), + ) + s.Require().NoError(err) - // 1000 tokens are escrowed on module address - s.EqualValues(1000, deps.App.BankKeeper.GetBalance(deps.Ctx, evm.EVM_MODULE_ADDRESS[:], bankDenom).Amount.BigInt().Int64()) + ethTxResp, err := deps.EvmKeeper.CallContractWithInput( + deps.Ctx, + evmObj, + deps.Sender.EthAddr, + &precompile.PrecompileAddr_FunToken, + true, + contractInput, + evmtest.FunTokenGasLimitSendToEvm, + ) + s.Require().NoError(err) + s.Require().Empty(ethTxResp.VmError, "sendToEvm VMError") - s.T().Log("4) The response returns the actual minted/unescrowed amount") - var actualMinted *big.Int - err = embeds.SmartContract_FunToken.ABI.UnpackIntoInterface( - &actualMinted, "sendToEvm", ethTxResp.Ret, - ) - s.Require().NoError(err) - s.Require().EqualValues(1000, actualMinted.Int64(), "expect 1000 minted to EVM") + s.T().Log("4) The response returns the actual minted/unescrowed amount") + var amountSent *big.Int + err = embeds.SmartContract_FunToken.ABI.UnpackIntoInterface( + &amountSent, "sendToEvm", ethTxResp.Ret, + ) + s.Require().NoError(err) + s.Require().EqualValues(1000, amountSent.Int64(), "expect 1000 minted to EVM") - s.T().Log("Check the user lost 1000 ulibi in bank") - wantBank := big.NewInt(234) // 1234 - 1000 => 234 - bankBal := deps.App.BankKeeper.GetBalance(deps.Ctx, deps.Sender.NibiruAddr, bankDenom).Amount.BigInt() - s.EqualValues(wantBank, bankBal, "did user lose 1000 ulibi from bank?") + s.T().Log("Check the user lost 1000 ulibi in bank") + bankBal := deps.App.BankKeeper.GetBalance(deps.Ctx, deps.Sender.NibiruAddr, bankDenom).Amount.BigInt() + s.EqualValues(big.NewInt(234), bankBal, "did user lose 1000 ulibi from bank?") - // check the evm module account balance - wantEvm := big.NewInt(1000) - evmBal := deps.App.BankKeeper.GetBalance(deps.Ctx, evm.EVM_MODULE_ADDRESS[:], bankDenom).Amount.BigInt() - s.EqualValues(wantEvm, evmBal, "did evm module properly mint ulibi?") + // check the evm module account balance + s.EqualValues(big.NewInt(1000), deps.App.BankKeeper.GetBalance(deps.Ctx, evm.EVM_MODULE_ADDRESS_NIBI, bankDenom).Amount.BigInt()) - s.T().Log("Check the user gained 1000 in ERC20 representation") - evmtest.AssertERC20BalanceEqual(s.T(), deps, erc20Addr, deps.Sender.EthAddr, big.NewInt(1000)) + s.T().Log("Check the user gained 1000 in ERC20 representation") + evmtest.AssertERC20BalanceEqualWithDescription(s.T(), deps, evmObj, erc20Addr, deps.Sender.EthAddr, big.NewInt(1000), "expect 1000 balance") + }) //----------------------------------------------------------------------- // 5) Now send some tokens *back* to the bank via `sendToBank`. //----------------------------------------------------------------------- // We'll pick a brand new random account to receive them. - recipient := testutil.AccAddress() - s.T().Logf("5) Sending 400 tokens back from EVM to Cosmos bank => recipient: %s", recipient) - - sendBackArgs := []any{ - erc20Addr, // address erc20 - big.NewInt(400), // amount - recipient.String(), // to - } - - inputSendBack, err := embeds.SmartContract_FunToken.ABI.Pack( - string(precompile.FunTokenMethod_sendToBank), - sendBackArgs..., - ) - s.Require().NoError(err) - _, ethTxResp2, err := evmtest.CallContractTx( - &deps, - precompile.PrecompileAddr_FunToken, - inputSendBack, - deps.Sender, - ) - s.Require().NoError(err) - s.Require().Empty(ethTxResp2.VmError, "sendToBank VMError") - - s.T().Log("Parse the returned amount from `sendToBank`") - var actualSentBack *big.Int - err = embeds.SmartContract_FunToken.ABI.UnpackIntoInterface( - &actualSentBack, string(precompile.FunTokenMethod_sendToBank), - ethTxResp2.Ret, - ) - s.Require().NoError(err) - s.Require().EqualValues(400, actualSentBack.Int64(), "expect 400 minted back to bank") - - s.T().Log("Check sender's EVM balance has decreased by 400") - // The sender started with 1000 after the first sendToEvm - evmtest.AssertERC20BalanceEqual( - s.T(), - deps, - erc20Addr, - deps.Sender.EthAddr, - big.NewInt(600), // 1000 - 400 - ) + s.Run("Sending 400 tokens back from EVM to Cosmos bank => recipient:", func() { + recipient := testutil.AccAddress() + contractInput, err := embeds.SmartContract_FunToken.ABI.Pack( + string(precompile.FunTokenMethod_sendToBank), + erc20Addr, + big.NewInt(400), + recipient.String(), + ) + s.Require().NoError(err) - s.T().Log("Check the bank side got 400 more") - recipientBal := deps.App.BankKeeper.GetBalance(deps.Ctx, recipient, bankDenom).Amount.BigInt() - s.Require().EqualValues(400, recipientBal.Int64(), "did the recipient get 400?") + ethExResp, err := deps.EvmKeeper.CallContractWithInput( + deps.Ctx, + evmObj, + deps.Sender.EthAddr, + &precompile.PrecompileAddr_FunToken, + true, + contractInput, + evmtest.FunTokenGasLimitSendToEvm, + ) + s.Require().NoError(err) + s.Require().Empty(ethExResp.VmError, "sendToBank VMError") + + s.T().Log("Parse the returned amount from `sendToBank`") + var actualSent *big.Int + err = embeds.SmartContract_FunToken.ABI.UnpackIntoInterface( + &actualSent, string(precompile.FunTokenMethod_sendToBank), + ethExResp.Ret, + ) + s.Require().NoError(err) + s.Require().EqualValues(big.NewInt(400), actualSent, "expect 400 minted back to bank") + + s.T().Log("Check sender's EVM balance has decreased by 400") + // The sender started with 1000 after the first sendToEvm + evmtest.AssertERC20BalanceEqualWithDescription( + s.T(), + deps, + evmObj, + erc20Addr, + deps.Sender.EthAddr, + big.NewInt(600), // 1000 - 400 + "expect 600 balance", + ) - s.T().Log("Confirm module account doesn't keep them (burn or escrow) for bank-based tokens") - moduleBal := deps.App.BankKeeper.GetBalance(deps.Ctx, evm.EVM_MODULE_ADDRESS[:], bankDenom).Amount.BigInt() - s.Require().EqualValues(600, moduleBal.Int64(), "module should now have 600 left escrowed") + s.T().Log("Check the bank side got 400 more") + s.Require().EqualValues(big.NewInt(400), deps.App.BankKeeper.GetBalance(deps.Ctx, recipient, bankDenom).Amount.BigInt(), "did the recipient get 400?") - s.T().Log("Done! We sent tokens to EVM, then back to the bank, verifying the final balances.") + s.T().Log("Confirm module account doesn't keep them (burn or escrow) for bank-based tokens") + s.Require().EqualValues(big.NewInt(600), deps.App.BankKeeper.GetBalance(deps.Ctx, evm.EVM_MODULE_ADDRESS[:], bankDenom).Amount.BigInt(), "module should now have 600 left escrowed") + }) } func bigTokens(n int64) *big.Int { @@ -549,78 +462,88 @@ func bigTokens(n int64) *big.Int { return new(big.Int).Mul(big.NewInt(n), e18) } -func (s *FuntokenSuite) TestSendToEvm_NotMadeFromCoin() { +func (s *FuntokenSuite) TestSendToEvm_MadeFromERC20() { // Create ERC20 token // EVM Transfer - Send 500 tokens to Bob (EVM) // sendToBank - Send 100 tokens from bob to alice's bank balance (EVM -> Cosmos) - // - mint cosmos token // - escrow erc20 token + // - mint cosmos token // sendToEVM - Send 100 tokens from alice to bob's EVM address (Cosmos -> EVM) // - burn cosmos token // - unescrow erc20 token deps := evmtest.NewTestDeps() - s.Require().NoError(testapp.FundFeeCollector(deps.App.BankKeeper, deps.Ctx, sdkmath.NewInt(20))) + evmObj, _ := deps.NewEVM() - bob := evmtest.NewEthPrivAcc() alice := evmtest.NewEthPrivAcc() + bob := evmtest.NewEthPrivAcc() // Fund user so they can create funtoken from an ERC20 - createFunTokenFee := deps.EvmKeeper.FeeForCreateFunToken(deps.Ctx) s.Require().NoError(testapp.FundAccount( deps.App.BankKeeper, deps.Ctx, deps.Sender.NibiruAddr, - createFunTokenFee, + deps.EvmKeeper.FeeForCreateFunToken(deps.Ctx), )) // Deploy an ERC20 with 18 decimals erc20Resp, err := evmtest.DeployContract(&deps, embeds.SmartContract_TestERC20) - s.Require().NoError(err, "failed to deploy test ERC20") erc20Addr := erc20Resp.ContractAddr // the initial supply was sent to the deployer - evmtest.AssertERC20BalanceEqual(s.T(), deps, erc20Addr, deps.Sender.EthAddr, bigTokens(1000000)) + evmtest.AssertERC20BalanceEqualWithDescription(s.T(), deps, evmObj, erc20Addr, deps.Sender.EthAddr, bigTokens(1_000_000), "expect nonzero balance") // create fun token from that erc20 - _, err = deps.EvmKeeper.CreateFunToken(sdk.WrapSDKContext(deps.Ctx), &evm.MsgCreateFunToken{ - Sender: deps.Sender.NibiruAddr.String(), - FromErc20: ð.EIP55Addr{Address: erc20Addr}, - }) + _, err = deps.EvmKeeper.CreateFunToken( + sdk.WrapSDKContext(deps.Ctx), + &evm.MsgCreateFunToken{ + Sender: deps.Sender.NibiruAddr.String(), + FromErc20: ð.EIP55Addr{Address: erc20Addr}, + }, + ) s.Require().NoError(err) // Transfer 500 tokens to bob => 500 * 10^18 raw - deployerAddr := gethcommon.HexToAddress(erc20Resp.EthTxMsg.From) - _, err = deps.EvmKeeper.CallContract( + contractInput, err := embeds.SmartContract_TestERC20.ABI.Pack( + "transfer", + bob.EthAddr, + bigTokens(500), // 500 in human sense + ) + s.Require().NoError(err) + _, err = deps.EvmKeeper.CallContractWithInput( deps.Ctx, - embeds.SmartContract_TestERC20.ABI, - deployerAddr, + evmObj, + deps.Sender.EthAddr, &erc20Addr, true, + contractInput, keeper.Erc20GasLimitExecute, - "transfer", - bob.EthAddr, - bigTokens(500), // 500 in human sense ) s.Require().NoError(err) // Now user should have 500 tokens => raw is 500 * 10^18 - evmtest.AssertERC20BalanceEqual(s.T(), deps, erc20Addr, bob.EthAddr, bigTokens(500)) + evmtest.AssertERC20BalanceEqualWithDescription(s.T(), deps, evmObj, erc20Addr, bob.EthAddr, bigTokens(500), "expect nonzero balance") // sendToBank: e.g. 100 tokens => 100 * 1e18 raw // expects to escrow on EVM side and mint on cosmos side - input, err := embeds.SmartContract_FunToken.ABI.Pack( + contractInput, err = embeds.SmartContract_FunToken.ABI.Pack( string(precompile.FunTokenMethod_sendToBank), - []any{ - erc20Addr, // address - bigTokens(100), - alice.NibiruAddr.String(), - }..., + erc20Addr, // address + bigTokens(100), + alice.NibiruAddr.String(), ) s.Require().NoError(err) - _, resp, err := evmtest.CallContractTx(&deps, precompile.PrecompileAddr_FunToken, input, bob) + resp, err := deps.EvmKeeper.CallContractWithInput( + deps.Ctx, + evmObj, + bob.EthAddr, /* from */ + &precompile.PrecompileAddr_FunToken, /* to */ + true, /* commit */ + contractInput, + evmtest.FunTokenGasLimitSendToEvm, /* gasLimit */ + ) s.Require().NoError(err) s.Require().Empty(resp.VmError) @@ -629,40 +552,135 @@ func (s *FuntokenSuite) TestSendToEvm_NotMadeFromCoin() { s.Require().EqualValues(bigTokens(100), bankBal.Amount.BigInt()) // Expect user to have 400 tokens => 400 * 10^18 - evmtest.AssertERC20BalanceEqual(s.T(), deps, erc20Addr, bob.EthAddr, bigTokens(400)) + evmtest.AssertERC20BalanceEqualWithDescription(s.T(), deps, evmObj, erc20Addr, bob.EthAddr, bigTokens(400), "expect Bob's balance to be 400") // 100 tokens are escrowed - evmtest.AssertERC20BalanceEqual(s.T(), deps, erc20Addr, evm.EVM_MODULE_ADDRESS, bigTokens(100)) + evmtest.AssertERC20BalanceEqualWithDescription(s.T(), deps, evmObj, erc20Addr, evm.EVM_MODULE_ADDRESS, bigTokens(100), "expect EVM module to escrow 100 tokens") // Finally sendToEvm(100) -> (expects to burn on cosmos side and unescrow in the EVM side) - input2, err := embeds.SmartContract_FunToken.ABI.Pack( - "sendToEvm", - []any{ + s.Run("send 100 tokens back to Bob", func() { + contractInput, err := embeds.SmartContract_FunToken.ABI.Pack( + "sendToEvm", bankBal.Denom, bigTokens(100), bob.EthAddr.Hex(), - }..., - ) - s.Require().NoError(err) - _, resp2, err := evmtest.CallContractTx(&deps, precompile.PrecompileAddr_FunToken, input2, alice) - s.Require().NoError(err) - s.Require().Empty(resp2.VmError) + ) + s.Require().NoError(err) + resp, err = deps.EvmKeeper.CallContractWithInput( + deps.Ctx, + evmObj, + alice.EthAddr, + &precompile.PrecompileAddr_FunToken, + true, + contractInput, + evmtest.FunTokenGasLimitSendToEvm, + ) + s.Require().NoError(err) + s.Require().Empty(resp.VmError) + }) // no bank side left for alice balAfter := deps.App.BankKeeper.GetBalance(deps.Ctx, alice.NibiruAddr, bankBal.Denom).Amount.BigInt() s.Require().EqualValues(bigTokens(0), balAfter) // check bob has 500 tokens again => 500 * 1e18 - evmtest.AssertERC20BalanceEqual(s.T(), deps, erc20Addr, bob.EthAddr, bigTokens(500)) + evmtest.AssertERC20BalanceEqualWithDescription(s.T(), deps, evmObj, erc20Addr, bob.EthAddr, bigTokens(500), "expect nonzero balance") // check evm module account's balance, it should have escrowed some tokens // unescrow the tokens - evmtest.AssertERC20BalanceEqual(s.T(), deps, erc20Addr, evm.EVM_MODULE_ADDRESS, bigTokens(0)) + evmtest.AssertERC20BalanceEqualWithDescription(s.T(), deps, evmObj, erc20Addr, evm.EVM_MODULE_ADDRESS, bigTokens(0), "expect zero balance") // burns the bank tokens evmBal2 := deps.App.BankKeeper.GetBalance(deps.Ctx, evm.EVM_MODULE_ADDRESS[:], bankBal.Denom).Amount.BigInt() s.Require().EqualValues(bigTokens(0), evmBal2) // user has 500 tokens again => 500 * 1e18 - evmtest.AssertERC20BalanceEqual(s.T(), deps, erc20Addr, bob.EthAddr, bigTokens(500)) + evmtest.AssertERC20BalanceEqualWithDescription(s.T(), deps, evmObj, erc20Addr, bob.EthAddr, bigTokens(500), "expect nonzero balance") +} + +// FunTokenWhoAmIReturn holds the return values from the "IFuntoken.whoAmI" +// method. The return bytes from successful calls of that method can be ABI +// unpacked into this struct. +type FunTokenWhoAmIReturn struct { + NibiruAcc struct { + EthAddr gethcommon.Address `abi:"ethAddr"` + Bech32Addr string `abi:"bech32Addr"` + } `abi:"whoAddrs"` +} + +func (out FunTokenWhoAmIReturn) ParseFromResp( + evmResp *evm.MsgEthereumTxResponse, +) (ethAddr gethcommon.Address, bech32Addr string, err error) { + err = embeds.SmartContract_FunToken.ABI.UnpackIntoInterface( + &out, + "whoAmI", + evmResp.Ret, + ) + if err != nil { + return + } + return out.NibiruAcc.EthAddr, out.NibiruAcc.Bech32Addr, nil +} + +// FunTokenBalanceReturn holds the return values from the "IFuntoken.balance" +// method. The return bytes from successful calls of that method can be ABI +// unpacked into this struct. +type FunTokenBalanceReturn struct { + Erc20Bal *big.Int `abi:"erc20Balance"` + BankBal *big.Int `abi:"bankBalance"` + Token struct { + Erc20 gethcommon.Address `abi:"erc20"` + BankDenom string `abi:"bankDenom"` + } `abi:"token"` + NibiruAcc struct { + EthAddr gethcommon.Address `abi:"ethAddr"` + Bech32Addr string `abi:"bech32Addr"` + } `abi:"whoAddrs"` +} + +func (out FunTokenBalanceReturn) ParseFromResp( + evmResp *evm.MsgEthereumTxResponse, +) (bals evmtest.FunTokenBalanceAssert, err error) { + err = embeds.SmartContract_FunToken.ABI.UnpackIntoInterface( + &out, + "balance", + evmResp.Ret, + ) + if err != nil { + return + } + return evmtest.FunTokenBalanceAssert{ + FunToken: evm.FunToken{ + Erc20Addr: eth.EIP55Addr{Address: out.Token.Erc20}, + BankDenom: out.Token.BankDenom, + }, + Account: out.NibiruAcc.EthAddr, + BalanceBank: out.BankBal, + BalanceERC20: out.Erc20Bal, + }, nil +} + +// FunTokenBankBalanceReturn holds the return values from the +// "IFuntoken.bankBalance" method. The return bytes from successful calls of that +// method can be ABI unpacked into this struct. +type FunTokenBankBalanceReturn struct { + BankBal *big.Int `abi:"bankBalance"` + NibiruAcc struct { + EthAddr gethcommon.Address `abi:"ethAddr"` + Bech32Addr string `abi:"bech32Addr"` + } `abi:"whoAddrs"` +} + +func (out FunTokenBankBalanceReturn) ParseFromResp( + evmResp *evm.MsgEthereumTxResponse, +) (bal *big.Int, ethAddr gethcommon.Address, bech32Addr string, err error) { + err = embeds.SmartContract_FunToken.ABI.UnpackIntoInterface( + &out, + "bankBalance", + evmResp.Ret, + ) + if err != nil { + return + } + return out.BankBal, out.NibiruAcc.EthAddr, out.NibiruAcc.Bech32Addr, nil } diff --git a/x/evm/precompile/nibiru_evm_utils_test.go b/x/evm/precompile/nibiru_evm_utils_test.go index 33348f281..d4121f81b 100644 --- a/x/evm/precompile/nibiru_evm_utils_test.go +++ b/x/evm/precompile/nibiru_evm_utils_test.go @@ -3,6 +3,7 @@ package precompile_test import ( "encoding/json" "fmt" + "testing" abci "github.com/cometbft/cometbft/abci/types" sdk "github.com/cosmos/cosmos-sdk/types" @@ -24,6 +25,10 @@ type UtilsSuite struct { suite.Suite } +func TestUtilsSuite(t *testing.T) { + suite.Run(t, new(UtilsSuite)) +} + func (s *UtilsSuite) TestAttrsToJSON() { testCases := []struct { name string diff --git a/x/evm/precompile/oracle_test.go b/x/evm/precompile/oracle_test.go index 6c738ee90..affb7b666 100644 --- a/x/evm/precompile/oracle_test.go +++ b/x/evm/precompile/oracle_test.go @@ -62,15 +62,20 @@ func (s *OracleSuite) TestOracle_HappyPath() { resp *evm.MsgEthereumTxResponse, err error, ) { - return deps.EvmKeeper.CallContract( + contractInput, err := embeds.SmartContract_Oracle.ABI.Pack( + string(precompile.OracleMethod_queryExchangeRate), + "unibi:uusd", + ) + s.Require().NoError(err) + evmObj, _ := deps.NewEVM() + return deps.EvmKeeper.CallContractWithInput( ctx, - embeds.SmartContract_Oracle.ABI, + evmObj, deps.Sender.EthAddr, &precompile.PrecompileAddr_Oracle, false, + contractInput, OracleGasLimitQuery, - "queryExchangeRate", - "unibi:uusd", ) } @@ -121,15 +126,21 @@ func (s *OracleSuite) TestOracle_HappyPath() { ctx := deps.Ctx. WithBlockTime(secondsLater). WithBlockHeight(deps.Ctx.BlockHeight() + 50) - resp, err := deps.EvmKeeper.CallContract( + + contractInput, err := embeds.SmartContract_Oracle.ABI.Pack( + string(precompile.OracleMethod_chainLinkLatestRoundData), + "unibi:uusd", + ) + s.Require().NoError(err) + evmObj, _ := deps.NewEVM() + resp, err := deps.EvmKeeper.CallContractWithInput( ctx, - embeds.SmartContract_Oracle.ABI, + evmObj, deps.Sender.EthAddr, &precompile.PrecompileAddr_Oracle, false, + contractInput, OracleGasLimitQuery, - "chainLinkLatestRoundData", - "unibi:uusd", ) s.NoError(err) diff --git a/x/evm/precompile/test/export.go b/x/evm/precompile/test/export.go index 43c728fe6..e37c99b09 100644 --- a/x/evm/precompile/test/export.go +++ b/x/evm/precompile/test/export.go @@ -30,10 +30,10 @@ const ( // SetupWasmContracts stores all Wasm bytecode and has the "deps.Sender" // instantiate each Wasm contract using the precompile. -func SetupWasmContracts(deps *evmtest.TestDeps, s *suite.Suite) ( +func SetupWasmContracts(deps *evmtest.TestDeps, evmObj *vm.EVM, s *suite.Suite) ( contracts []sdk.AccAddress, ) { - wasmCodes := DeployWasmBytecode(s, deps.Ctx, deps.Sender.NibiruAddr, deps.App) + wasmCodes := deployWasmBytecode(s, deps.Ctx, deps.Sender.NibiruAddr, deps.App) instantiateArgs := []struct { InstantiateMsg []byte @@ -51,11 +51,10 @@ func SetupWasmContracts(deps *evmtest.TestDeps, s *suite.Suite) ( for i, wasmCode := range wasmCodes { s.T().Logf("Instantiate using Wasm precompile: %s", wasmCode.binPath) - codeId := wasmCode.codeId m := wasm.MsgInstantiateContract{ Admin: "", - CodeID: codeId, + CodeID: wasmCode.codeId, Label: instantiateArgs[i].Label, Msg: instantiateArgs[i].InstantiateMsg, } @@ -63,22 +62,30 @@ func SetupWasmContracts(deps *evmtest.TestDeps, s *suite.Suite) ( msgArgsBz, err := json.Marshal(m.Msg) s.NoError(err) - ethTxResp, err := deps.EvmKeeper.CallContract( + contractInput, err := embeds.SmartContract_Wasm.ABI.Pack( + string(precompile.WasmMethod_instantiate), + m.Admin, m.CodeID, msgArgsBz, m.Label, []precompile.WasmBankCoin{}, + ) + s.NoError(err) + + ethTxResp, err := deps.EvmKeeper.CallContractWithInput( deps.Ctx, - embeds.SmartContract_Wasm.ABI, + evmObj, deps.Sender.EthAddr, &precompile.PrecompileAddr_Wasm, true, + contractInput, WasmGasLimitInstantiate, - string(precompile.WasmMethod_instantiate), - []any{m.Admin, m.CodeID, msgArgsBz, m.Label, []precompile.WasmBankCoin{}}..., ) s.Require().NoError(err) s.Require().NotEmpty(ethTxResp.Ret) s.T().Log("Parse the response contract addr and response bytes") - vals, err := embeds.SmartContract_Wasm.ABI.Unpack(string(precompile.WasmMethod_instantiate), ethTxResp.Ret) + vals, err := embeds.SmartContract_Wasm.ABI.Unpack( + string(precompile.WasmMethod_instantiate), + ethTxResp.Ret, + ) s.Require().NoError(err) contractAddr, err := sdk.AccAddressFromBech32(vals[0].(string)) @@ -89,9 +96,9 @@ func SetupWasmContracts(deps *evmtest.TestDeps, s *suite.Suite) ( return contracts } -// DeployWasmBytecode is a setup function that stores all Wasm bytecode used in +// deployWasmBytecode is a setup function that stores all Wasm bytecode used in // the test suite. -func DeployWasmBytecode( +func deployWasmBytecode( s *suite.Suite, ctx sdk.Context, sender sdk.AccAddress, @@ -156,6 +163,7 @@ func DeployWasmBytecode( func AssertWasmCounterState( s *suite.Suite, deps evmtest.TestDeps, + evmObj *vm.EVM, wasmContract sdk.AccAddress, wantCount int64, ) { @@ -165,26 +173,28 @@ func AssertWasmCounterState( } `) - ethTxResp, err := deps.EvmKeeper.CallContract( + contractInput, err := embeds.SmartContract_Wasm.ABI.Pack( + string(precompile.WasmMethod_query), + wasmContract.String(), + msgArgsBz, + ) + s.Require().NoError(err) + + ethTxResp, err := deps.EvmKeeper.CallContractWithInput( deps.Ctx, - embeds.SmartContract_Wasm.ABI, + evmObj, deps.Sender.EthAddr, &precompile.PrecompileAddr_Wasm, - true, + false, + contractInput, WasmGasLimitQuery, - string(precompile.WasmMethod_query), - []any{ - // string memory contractAddr - wasmContract.String(), - // bytes memory req - msgArgsBz, - }..., ) s.Require().NoError(err) s.Require().NotEmpty(ethTxResp.Ret) s.T().Log("Parse the response contract addr and response bytes") s.T().Logf("ethTxResp.Ret: %s", ethTxResp.Ret) + var queryResp []byte err = embeds.SmartContract_Wasm.ABI.UnpackIntoInterface( // Since there's only one return value, don't unpack as a slice. @@ -197,7 +207,6 @@ func AssertWasmCounterState( s.Require().NoError(err) s.T().Logf("queryResp: %s", queryResp) - s.T().Log("Response is a JSON-encoded struct from the Wasm contract") var wasmMsg wasm.RawContractMessage s.NoError(json.Unmarshal(queryResp, &wasmMsg)) s.NoError(wasmMsg.ValidateBasic()) @@ -255,10 +264,11 @@ type QueryMsgCountResp struct { func IncrementWasmCounterWithExecuteMulti( s *suite.Suite, deps *evmtest.TestDeps, + evmObj *vm.EVM, wasmContract sdk.AccAddress, times uint, - finalizeTx bool, -) (evmObj *vm.EVM) { + commit bool, +) { msgArgsBz := []byte(` { "increment": {} @@ -298,24 +308,24 @@ func IncrementWasmCounterWithExecuteMulti( ) s.Require().NoError(err) - ethTxResp, evmObj, err := deps.EvmKeeper.CallContractWithInput( + ethTxResp, err := deps.EvmKeeper.CallContractWithInput( deps.Ctx, + evmObj, deps.Sender.EthAddr, &precompile.PrecompileAddr_Wasm, - finalizeTx, + commit, input, WasmGasLimitExecute, ) s.Require().NoError(err) s.Require().NotEmpty(ethTxResp.Ret) - return evmObj } func IncrementWasmCounterWithExecuteMultiViaVMCall( s *suite.Suite, deps *evmtest.TestDeps, wasmContract sdk.AccAddress, - times uint, + times int, finalizeTx bool, evmObj *vm.EVM, ) error { @@ -337,34 +347,26 @@ func IncrementWasmCounterWithExecuteMultiViaVMCall( ContractAddr string `json:"contractAddr"` MsgArgs []byte `json:"msgArgs"` Funds []precompile.WasmBankCoin `json:"funds"` - }{ - {wasmContract.String(), msgArgsBz, funds}, - } - if times == 0 { - executeMsgs = executeMsgs[:0] // force empty - } else { - for i := uint(1); i < times; i++ { - executeMsgs = append(executeMsgs, executeMsgs[0]) - } + }{} + for i := 0; i < times; i++ { + executeMsgs = append(executeMsgs, struct { + ContractAddr string `json:"contractAddr"` + MsgArgs []byte `json:"msgArgs"` + Funds []precompile.WasmBankCoin `json:"funds"` + }{wasmContract.String(), msgArgsBz, funds}) } - s.Require().Len(executeMsgs, int(times)) // sanity check assertion - callArgs := []any{ - executeMsgs, - } - input, err := embeds.SmartContract_Wasm.ABI.Pack( + contractInput, err := embeds.SmartContract_Wasm.ABI.Pack( string(precompile.WasmMethod_executeMulti), - callArgs..., + executeMsgs, ) s.Require().NoError(err) - contract := precompile.PrecompileAddr_Wasm - leftoverGas := serverconfig.DefaultEthCallGasLimit _, _, err = evmObj.Call( vm.AccountRef(deps.Sender.EthAddr), - contract, - input, - leftoverGas, + precompile.PrecompileAddr_Wasm, + contractInput, + serverconfig.DefaultEthCallGasLimit, big.NewInt(0), ) return err diff --git a/x/evm/precompile/wasm_test.go b/x/evm/precompile/wasm_test.go index 778fe422c..3bedd115b 100644 --- a/x/evm/precompile/wasm_test.go +++ b/x/evm/precompile/wasm_test.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "math/big" + "testing" wasm "github.com/CosmWasm/wasmd/x/wasm/types" @@ -29,96 +30,110 @@ type WasmSuite struct { suite.Suite } +func TestWasmSuite(t *testing.T) { + suite.Run(t, new(WasmSuite)) +} + func (s *WasmSuite) TestExecuteHappy() { deps := evmtest.NewTestDeps() - wasmContracts := test.SetupWasmContracts(&deps, &s.Suite) + evmObj, _ := deps.NewEVM() + + wasmContracts := test.SetupWasmContracts(&deps, evmObj, &s.Suite) wasmContract := wasmContracts[0] // nibi_stargate.wasm - s.T().Log("Execute: create denom") - msgArgsBz := []byte(` - { "create_denom": { + s.Run("create denom", func() { + msgArgsBz := []byte(` + { + "create_denom": { "subdenom": "ETH" - } + } } `) - var funds []precompile.WasmBankCoin - fundsJson, err := json.Marshal(funds) - s.NoErrorf(err, "fundsJson: %s", fundsJson) - err = json.Unmarshal(fundsJson, &funds) - s.Require().NoError(err, "fundsJson %s, funds %s", fundsJson, funds) + var funds []precompile.WasmBankCoin + fundsJson, err := json.Marshal(funds) + s.NoErrorf(err, "fundsJson: %s", fundsJson) + err = json.Unmarshal(fundsJson, &funds) + s.Require().NoError(err, "fundsJson %s, funds %s", fundsJson, funds) - ethTxResp, err := deps.EvmKeeper.CallContract( - deps.Ctx, - embeds.SmartContract_Wasm.ABI, - deps.Sender.EthAddr, - &precompile.PrecompileAddr_Wasm, - true, - WasmGasLimitExecute, - string(precompile.WasmMethod_execute), - []any{ + contractInput, err := embeds.SmartContract_Wasm.ABI.Pack( + string(precompile.WasmMethod_execute), wasmContract.String(), msgArgsBz, funds, - }..., - ) - s.Require().NoError(err) - s.Require().NotEmpty(ethTxResp.Ret) + ) + s.Require().NoError(err) + ethTxResp, err := deps.EvmKeeper.CallContractWithInput( + deps.Ctx, + evmObj, + deps.Sender.EthAddr, + &precompile.PrecompileAddr_Wasm, + true, + contractInput, + WasmGasLimitExecute, + ) + s.Require().NoError(err) + s.Require().NotEmpty(ethTxResp.Ret) + }) s.T().Log("Execute: mint tokens") - coinDenom := tokenfactory.TFDenom{ - Creator: wasmContract.String(), - Subdenom: "ETH", - }.Denom().String() - msgArgsBz = []byte(fmt.Sprintf(` - { - "mint": { - "coin": { "amount": "69420", "denom": "%s" }, - "mint_to": "%s" - } - } - `, coinDenom, deps.Sender.NibiruAddr)) - - ethTxResp, err = deps.EvmKeeper.CallContract( - deps.Ctx, - embeds.SmartContract_Wasm.ABI, - deps.Sender.EthAddr, - &precompile.PrecompileAddr_Wasm, - true, - WasmGasLimitExecute, - string(precompile.WasmMethod_execute), - []any{ + s.Run("mint tokens", func() { + coinDenom := tokenfactory.TFDenom{ + Creator: wasmContract.String(), + Subdenom: "ETH", + }.Denom().String() + msgArgsBz := []byte(fmt.Sprintf(` + { + "mint": { + "coin": { "amount": "69420", "denom": "%s" }, + "mint_to": "%s" + } + } + `, coinDenom, deps.Sender.NibiruAddr)) + contractInput, err := embeds.SmartContract_Wasm.ABI.Pack( + string(precompile.WasmMethod_execute), wasmContract.String(), msgArgsBz, - funds, - }..., - ) - - s.Require().NoError(err) - s.Require().NotEmpty(ethTxResp.Ret) - evmtest.AssertBankBalanceEqual( - s.T(), deps, coinDenom, deps.Sender.EthAddr, big.NewInt(69_420), - ) + []precompile.WasmBankCoin{}, + ) + s.Require().NoError(err) + + ethTxResp, err := deps.EvmKeeper.CallContractWithInput( + deps.Ctx, + evmObj, + deps.Sender.EthAddr, + &precompile.PrecompileAddr_Wasm, + true, + contractInput, + WasmGasLimitExecute, + ) + + s.Require().NoError(err) + s.Require().NotEmpty(ethTxResp.Ret) + evmtest.AssertBankBalanceEqualWithDescription( + s.T(), deps, coinDenom, deps.Sender.EthAddr, big.NewInt(69_420), "expect 69420 balance") + }) } func (s *WasmSuite) TestExecuteMultiHappy() { deps := evmtest.NewTestDeps() - wasmContracts := test.SetupWasmContracts(&deps, &s.Suite) + evmObj, _ := deps.NewEVM() + wasmContracts := test.SetupWasmContracts(&deps, evmObj, &s.Suite) wasmContract := wasmContracts[1] // hello_world_counter.wasm // count = 0 - test.AssertWasmCounterState(&s.Suite, deps, wasmContract, 0) + test.AssertWasmCounterState(&s.Suite, deps, evmObj, wasmContract, 0) // count += 2 test.IncrementWasmCounterWithExecuteMulti( - &s.Suite, &deps, wasmContract, 2, true) + &s.Suite, &deps, evmObj, wasmContract, 2, true) // count = 2 - test.AssertWasmCounterState(&s.Suite, deps, wasmContract, 2) + test.AssertWasmCounterState(&s.Suite, deps, evmObj, wasmContract, 2) s.assertWasmCounterStateRaw(deps, wasmContract, 2) // count += 67 test.IncrementWasmCounterWithExecuteMulti( - &s.Suite, &deps, wasmContract, 67, true) + &s.Suite, &deps, evmObj, wasmContract, 67, true) // count = 69 - test.AssertWasmCounterState(&s.Suite, deps, wasmContract, 69) + test.AssertWasmCounterState(&s.Suite, deps, evmObj, wasmContract, 69) s.assertWasmCounterStateRaw(deps, wasmContract, 69) } @@ -135,18 +150,22 @@ func (s *WasmSuite) assertWasmCounterStateRaw( wasmContract sdk.AccAddress, wantCount int64, ) { - ethTxResp, err := deps.EvmKeeper.CallContract( + contractInput, err := embeds.SmartContract_Wasm.ABI.Pack( + string(precompile.WasmMethod_queryRaw), + wasmContract.String(), + []byte(`state`), + ) + s.Require().NoError(err) + evmObj, _ := deps.NewEVM() + + ethTxResp, err := deps.EvmKeeper.CallContractWithInput( deps.Ctx, - embeds.SmartContract_Wasm.ABI, + evmObj, deps.Sender.EthAddr, &precompile.PrecompileAddr_Wasm, true, + contractInput, WasmGasLimitQuery, - string(precompile.WasmMethod_queryRaw), - []any{ - wasmContract.String(), - []byte(`state`), - }..., ) s.Require().NoError(err) s.Require().NotEmpty(ethTxResp.Ret) @@ -308,15 +327,21 @@ func (s *WasmSuite) TestSadArgsExecute() { s.Run(tc.name, func() { deps := evmtest.NewTestDeps() - ethTxResp, err := deps.EvmKeeper.CallContract( + contractInput, err := abi.Pack( + string(tc.methodName), + tc.callArgs..., + ) + s.Require().NoError(err) + evmObj, _ := deps.NewEVM() + + ethTxResp, err := deps.EvmKeeper.CallContractWithInput( deps.Ctx, - abi, + evmObj, deps.Sender.EthAddr, &precompile.PrecompileAddr_Wasm, true, + contractInput, WasmGasLimitExecute, - string(tc.methodName), - tc.callArgs..., ) s.Require().ErrorContains(err, tc.wantError, "ethTxResp %v", ethTxResp) @@ -332,6 +357,7 @@ type WasmExecuteMsg struct { func (s *WasmSuite) TestExecuteMultiValidation() { deps := evmtest.NewTestDeps() + evmObj, _ := deps.NewEVM() s.Require().NoError(testapp.FundAccount( deps.App.BankKeeper, @@ -340,7 +366,7 @@ func (s *WasmSuite) TestExecuteMultiValidation() { sdk.NewCoins(sdk.NewCoin("unibi", sdk.NewInt(100))), )) - wasmContracts := test.SetupWasmContracts(&deps, &s.Suite) + wasmContracts := test.SetupWasmContracts(&deps, evmObj, &s.Suite) wasmContract := wasmContracts[1] // hello_world_counter.wasm invalidMsgArgsBz := []byte(`{"invalid": "json"}`) // Invalid message format @@ -441,15 +467,20 @@ func (s *WasmSuite) TestExecuteMultiValidation() { for _, tc := range testCases { s.Run(tc.name, func() { - ethTxResp, err := deps.EvmKeeper.CallContract( + contractInput, err := embeds.SmartContract_Wasm.ABI.Pack( + string(precompile.WasmMethod_executeMulti), + tc.executeMsgs, + ) + s.Require().NoError(err) + evmObj, _ := deps.NewEVM() + ethTxResp, err := deps.EvmKeeper.CallContractWithInput( deps.Ctx, - embeds.SmartContract_Wasm.ABI, + evmObj, deps.Sender.EthAddr, &precompile.PrecompileAddr_Wasm, true, + contractInput, WasmGasLimitExecute, - string(precompile.WasmMethod_executeMulti), - []any{tc.executeMsgs}..., ) if tc.wantError != "" { @@ -467,11 +498,13 @@ func (s *WasmSuite) TestExecuteMultiValidation() { // in the batch fails validation func (s *WasmSuite) TestExecuteMultiPartialExecution() { deps := evmtest.NewTestDeps() - wasmContracts := test.SetupWasmContracts(&deps, &s.Suite) + evmObj, _ := deps.NewEVM() + + wasmContracts := test.SetupWasmContracts(&deps, evmObj, &s.Suite) wasmContract := wasmContracts[1] // hello_world_counter.wasm // First verify initial state is 0 - test.AssertWasmCounterState(&s.Suite, deps, wasmContract, 0) + test.AssertWasmCounterState(&s.Suite, deps, evmObj, wasmContract, 0) // Create a batch where the second message will fail validation executeMsgs := []WasmExecuteMsg{ @@ -487,15 +520,19 @@ func (s *WasmSuite) TestExecuteMultiPartialExecution() { }, } - ethTxResp, err := deps.EvmKeeper.CallContract( + contractInput, err := embeds.SmartContract_Wasm.ABI.Pack( + string(precompile.WasmMethod_executeMulti), + executeMsgs, + ) + s.Require().NoError(err) + ethTxResp, err := deps.EvmKeeper.CallContractWithInput( deps.Ctx, - embeds.SmartContract_Wasm.ABI, + evmObj, deps.Sender.EthAddr, &precompile.PrecompileAddr_Wasm, true, + contractInput, WasmGasLimitExecute, - string(precompile.WasmMethod_executeMulti), - []any{executeMsgs}..., ) // Verify that the call failed @@ -503,5 +540,5 @@ func (s *WasmSuite) TestExecuteMultiPartialExecution() { s.Require().Contains(err.Error(), "unknown variant") // Verify that no state changes occurred - test.AssertWasmCounterState(&s.Suite, deps, wasmContract, 0) + test.AssertWasmCounterState(&s.Suite, deps, evmObj, wasmContract, 0) } diff --git a/x/evm/statedb/journal_test.go b/x/evm/statedb/journal_test.go index 058c09542..27582e69f 100644 --- a/x/evm/statedb/journal_test.go +++ b/x/evm/statedb/journal_test.go @@ -10,106 +10,132 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/ethereum/go-ethereum/core/vm" - "github.com/NibiruChain/nibiru/v2/x/evm/keeper" - serverconfig "github.com/NibiruChain/nibiru/v2/app/server/config" "github.com/NibiruChain/nibiru/v2/x/common" "github.com/NibiruChain/nibiru/v2/x/common/testutil/testapp" "github.com/NibiruChain/nibiru/v2/x/evm" "github.com/NibiruChain/nibiru/v2/x/evm/embeds" "github.com/NibiruChain/nibiru/v2/x/evm/evmtest" + "github.com/NibiruChain/nibiru/v2/x/evm/keeper" "github.com/NibiruChain/nibiru/v2/x/evm/precompile/test" "github.com/NibiruChain/nibiru/v2/x/evm/statedb" ) -func (s *Suite) TestComplexJournalChanges() { +func (s *Suite) TestCommitRemovesDirties() { deps := evmtest.NewTestDeps() - bankDenom := evm.EVMBankDenom - s.Require().NoError(testapp.FundAccount( - deps.App.BankKeeper, - deps.Ctx, - deps.Sender.NibiruAddr, - sdk.NewCoins(sdk.NewCoin(bankDenom, sdk.NewInt(69_420))), - )) - - s.T().Log("Set up helloworldcounter.wasm") - helloWorldCounterWasm := test.SetupWasmContracts(&deps, &s.Suite)[1] - fmt.Printf("wasmContract: %s\n", helloWorldCounterWasm) + evmObj, _ := deps.NewEVM() - s.T().Log("Assert before transition") - test.AssertWasmCounterState( - &s.Suite, deps, helloWorldCounterWasm, 0, - ) - - deployArgs := []any{"name", "SYMBOL", uint8(18)} deployResp, err := evmtest.DeployContract( &deps, embeds.SmartContract_ERC20Minter, - deployArgs..., + "name", + "SYMBOL", + uint8(18), ) s.Require().NoError(err, deployResp) + erc20 := deployResp.ContractAddr - contract := deployResp.ContractAddr - to, amount := deps.Sender.EthAddr, big.NewInt(69_420) - input, err := deps.EvmKeeper.ERC20().ABI.Pack("mint", to, amount) + input, err := deps.EvmKeeper.ERC20().ABI.Pack("mint", deps.Sender.EthAddr, big.NewInt(69_420)) s.Require().NoError(err) - _, evmObj, err := deps.EvmKeeper.CallContractWithInput( + _, err = deps.EvmKeeper.CallContractWithInput( deps.Ctx, - deps.Sender.EthAddr, - &contract, - true, + evmObj, + deps.Sender.EthAddr, // caller + &erc20, // contract + true, // commit input, keeper.Erc20GasLimitExecute, ) s.Require().NoError(err) + s.Require().EqualValues(0, evmObj.StateDB.(*statedb.StateDB).DebugDirtiesCount()) +} - s.Run("Populate dirty journal entries. Remove with Commit", func() { - stateDB := evmObj.StateDB.(*statedb.StateDB) - s.Equal(0, stateDB.DebugDirtiesCount()) - - randomAcc := evmtest.NewEthPrivAcc().EthAddr - balDelta := evm.NativeToWei(big.NewInt(4)) - // 2 dirties from [createObjectChange, balanceChange] - stateDB.AddBalance(randomAcc, balDelta) - // 1 dirties from [balanceChange] - stateDB.AddBalance(randomAcc, balDelta) - // 1 dirties from [balanceChange] - stateDB.SubBalance(randomAcc, balDelta) - if stateDB.DebugDirtiesCount() != 4 { - debugDirtiesCountMismatch(stateDB, s.T()) - s.FailNow("expected 4 dirty journal changes") - } +func (s *Suite) TestCommitRemovesDirties_OnlyStateDB() { + deps := evmtest.NewTestDeps() + evmObj, _ := deps.NewEVM() + stateDB := evmObj.StateDB.(*statedb.StateDB) - s.T().Log("StateDB.Commit, then Dirties should be gone") - err = stateDB.Commit() - s.NoError(err) - if stateDB.DebugDirtiesCount() != 0 { - debugDirtiesCountMismatch(stateDB, s.T()) - s.FailNow("expected 0 dirty journal changes") - } - }) + randomAcc := evmtest.NewEthPrivAcc().EthAddr + balDelta := evm.NativeToWei(big.NewInt(4)) + // 2 dirties from [createObjectChange, balanceChange] + stateDB.AddBalance(randomAcc, balDelta) + // 1 dirties from [balanceChange] + stateDB.AddBalance(randomAcc, balDelta) + // 1 dirties from [balanceChange] + stateDB.SubBalance(randomAcc, balDelta) + if stateDB.DebugDirtiesCount() != 4 { + debugDirtiesCountMismatch(stateDB, s.T()) + s.FailNow("expected 4 dirty journal changes") + } + + s.T().Log("StateDB.Commit, then Dirties should be gone") + err := stateDB.Commit() + s.NoError(err) + if stateDB.DebugDirtiesCount() != 0 { + debugDirtiesCountMismatch(stateDB, s.T()) + s.FailNow("expected 0 dirty journal changes") + } +} + +func (s *Suite) TestContractCallsAnotherContract() { + deps := evmtest.NewTestDeps() + evmObj, _ := deps.NewEVM() + stateDB := evmObj.StateDB.(*statedb.StateDB) + + s.Require().NoError(testapp.FundAccount( + deps.App.BankKeeper, + deps.Ctx, + deps.Sender.NibiruAddr, + sdk.NewCoins(sdk.NewCoin(evm.EVMBankDenom, sdk.NewInt(69_420))), + )) - s.Run("Emulate a contract that calls another contract", func() { - randomAcc := evmtest.NewEthPrivAcc().EthAddr - to, amount := randomAcc, big.NewInt(69_000) - input, err := embeds.SmartContract_ERC20Minter.ABI.Pack("transfer", to, amount) + deployResp, err := evmtest.DeployContract( + &deps, + embeds.SmartContract_ERC20Minter, + "name", + "SYMBOL", + uint8(18), + ) + s.Require().NoError(err, deployResp) + erc20 := deployResp.ContractAddr + + s.Run("Mint 69_420 tokens", func() { + contractInput, err := deps.EvmKeeper.ERC20().ABI.Pack("mint", deps.Sender.EthAddr, big.NewInt(69_420)) + s.Require().NoError(err) + _, err = deps.EvmKeeper.CallContractWithInput( + deps.Ctx, + evmObj, + deps.Sender.EthAddr, // caller + &erc20, // contract + true, // commit + contractInput, + keeper.Erc20GasLimitExecute, + ) s.Require().NoError(err) + }) + + randomAcc := evmtest.NewEthPrivAcc().EthAddr + contractInput, err := embeds.SmartContract_ERC20Minter.ABI.Pack("transfer", randomAcc, big.NewInt(69_000)) + s.Require().NoError(err) + + s.Run("Transfer 69_000 tokens", func() { + s.T().Log("Transfer 69_000 tokens") - leftoverGas := serverconfig.DefaultEthCallGasLimit _, _, err = evmObj.Call( vm.AccountRef(deps.Sender.EthAddr), - contract, - input, - leftoverGas, + erc20, + contractInput, + serverconfig.DefaultEthCallGasLimit, big.NewInt(0), ) s.Require().NoError(err) - stateDB := evmObj.StateDB.(*statedb.StateDB) if stateDB.DebugDirtiesCount() != 2 { debugDirtiesCountMismatch(stateDB, s.T()) s.FailNowf("expected 2 dirty journal changes", "%#v", stateDB.Journal) } + }) + s.Run("Transfer 69_000 tokens", func() { // The contract calling itself is invalid in this context. // Note the comment in vm.Contract: // @@ -121,117 +147,104 @@ func (s *Suite) TestComplexJournalChanges() { // // ... // } // // + _, _, err = evmObj.Call( - vm.AccountRef(contract), - contract, - input, - leftoverGas, + vm.AccountRef(erc20), + erc20, + contractInput, + serverconfig.DefaultEthCallGasLimit, big.NewInt(0), ) s.Require().ErrorContains(err, vm.ErrExecutionReverted.Error()) }) +} - s.Run("Precompile calls populate snapshots", func() { - s.T().Log("commitEvmTx=true, expect 0 dirty journal entries") - commitEvmTx := true - evmObj = test.IncrementWasmCounterWithExecuteMulti( - &s.Suite, &deps, helloWorldCounterWasm, 7, commitEvmTx, - ) - // assertions after run - test.AssertWasmCounterState( - &s.Suite, deps, helloWorldCounterWasm, 7, - ) - stateDB, ok := evmObj.StateDB.(*statedb.StateDB) - s.Require().True(ok, "error retrieving StateDB from the EVM") - if stateDB.DebugDirtiesCount() != 0 { - debugDirtiesCountMismatch(stateDB, s.T()) - s.FailNow("expected 0 dirty journal changes") - } +func (s *Suite) TestJournalReversion() { + deps := evmtest.NewTestDeps() + evmObj, _ := deps.NewEVM() + stateDB := evmObj.StateDB.(*statedb.StateDB) + s.Require().NoError(testapp.FundAccount( + deps.App.BankKeeper, + deps.Ctx, + deps.Sender.NibiruAddr, + sdk.NewCoins(sdk.NewCoin(evm.EVMBankDenom, sdk.NewInt(69_420))), + )) - s.T().Log("commitEvmTx=false, expect dirty journal entries") - commitEvmTx = false - evmObj = test.IncrementWasmCounterWithExecuteMulti( - &s.Suite, &deps, helloWorldCounterWasm, 5, commitEvmTx, - ) - stateDB, ok = evmObj.StateDB.(*statedb.StateDB) - s.Require().True(ok, "error retrieving StateDB from the EVM") + s.T().Log("Set up helloworldcounter.wasm") + wasmContracts := test.SetupWasmContracts(&deps, evmObj, &s.Suite) + helloWorldCounterWasm := wasmContracts[1] + fmt.Printf("wasmContract: %s\n", helloWorldCounterWasm) - s.T().Log("Expect exactly 1 dirty journal entry for the precompile snapshot") - if stateDB.DebugDirtiesCount() != 1 { - debugDirtiesCountMismatch(stateDB, s.T()) - s.FailNow("expected 1 dirty journal change") - } + s.T().Log("commitEvmTx=true, expect 0 dirty journal entries") + test.IncrementWasmCounterWithExecuteMulti( + &s.Suite, &deps, evmObj, helloWorldCounterWasm, 7, true, + ) + if stateDB.DebugDirtiesCount() != 0 { + debugDirtiesCountMismatch(stateDB, s.T()) + s.FailNowf("statedb dirty count mismatch", "expected 0 dirty journal changes, but instead got: %d", stateDB.DebugDirtiesCount()) + } - s.T().Log("Expect no change since the StateDB has not been committed") - test.AssertWasmCounterState( - &s.Suite, deps, helloWorldCounterWasm, 7, // 7 = 7 + 0 - ) + s.T().Log("commitEvmTx=false, expect dirty journal entries") + test.IncrementWasmCounterWithExecuteMulti( + &s.Suite, &deps, evmObj, helloWorldCounterWasm, 5, false, + ) + s.T().Log("Expect exactly 1 dirty journal entry for the precompile snapshot") + if stateDB.DebugDirtiesCount() != 1 { + debugDirtiesCountMismatch(stateDB, s.T()) + s.FailNowf("statedb dirty count mismatch", "expected 1 dirty journal change, but instead got: %d", stateDB.DebugDirtiesCount()) + } - s.T().Log("Expect change to persist on the StateDB cacheCtx") - cacheCtx := stateDB.GetCacheContext() - s.NotNil(cacheCtx) - deps.Ctx = *cacheCtx - test.AssertWasmCounterState( - &s.Suite, deps, helloWorldCounterWasm, 12, // 12 = 7 + 5 - ) - // NOTE: that the [StateDB.Commit] fn has not been called yet. We're still - // mid-transaction. + s.T().Log("Expect to see the pending changes included") + test.AssertWasmCounterState( + &s.Suite, deps, evmObj, helloWorldCounterWasm, 12, // 12 = 7 + 5 + ) - s.T().Log("EVM revert operation should bring about the old state") - err = test.IncrementWasmCounterWithExecuteMultiViaVMCall( - &s.Suite, &deps, helloWorldCounterWasm, 50, commitEvmTx, evmObj, - ) - stateDBPtr := evmObj.StateDB.(*statedb.StateDB) - s.Require().Equal(stateDB, stateDBPtr) - s.Require().NoError(err) - s.T().Log(heredoc.Doc(`At this point, 2 precompile calls have succeeded. + // NOTE: that the [StateDB.Commit] fn has not been called yet. We're still + // mid-transaction. + + s.T().Log("EVM revert operation should bring about the old state") + err := test.IncrementWasmCounterWithExecuteMultiViaVMCall( + &s.Suite, &deps, helloWorldCounterWasm, 50, false, evmObj, + ) + s.Require().NoError(err) + s.T().Log(heredoc.Doc(`At this point, 2 precompile calls have succeeded. One that increments the counter to 7 + 5, and another for +50. The StateDB has not been committed. We expect to be able to revert to both snapshots and see the prior states.`)) - cacheCtx = stateDB.GetCacheContext() - deps.Ctx = *cacheCtx - test.AssertWasmCounterState( - &s.Suite, deps, helloWorldCounterWasm, 7+5+50, - ) + test.AssertWasmCounterState( + &s.Suite, deps, evmObj, helloWorldCounterWasm, 7+5+50, + ) - errFn := common.TryCatch(func() { - // There were only two EVM calls. - // Thus, there are only 2 snapshots: 0 and 1. - // We should not be able to revert to a third one. - stateDB.RevertToSnapshot(2) - }) - s.Require().ErrorContains(errFn(), "revision id 2 cannot be reverted") - - stateDB.RevertToSnapshot(1) - cacheCtx = stateDB.GetCacheContext() - s.NotNil(cacheCtx) - deps.Ctx = *cacheCtx - test.AssertWasmCounterState( - &s.Suite, deps, helloWorldCounterWasm, 7+5, - ) + errFn := common.TryCatch(func() { + // a revision that doesn't exist + stateDB.RevertToSnapshot(9000) + }) + s.Require().ErrorContains(errFn(), "revision id 9000 cannot be reverted") - stateDB.RevertToSnapshot(0) - cacheCtx = stateDB.GetCacheContext() - s.NotNil(cacheCtx) - deps.Ctx = *cacheCtx - test.AssertWasmCounterState( - &s.Suite, deps, helloWorldCounterWasm, 7, // state before precompile called - ) + stateDB.RevertToSnapshot(5) + test.AssertWasmCounterState( + &s.Suite, deps, evmObj, helloWorldCounterWasm, 7+5, + ) - err = stateDB.Commit() - deps.Ctx = stateDB.GetEvmTxContext() - test.AssertWasmCounterState( - &s.Suite, deps, helloWorldCounterWasm, 7, // state before precompile called - ) - }) + stateDB.RevertToSnapshot(3) + test.AssertWasmCounterState( + &s.Suite, deps, evmObj, helloWorldCounterWasm, 7, // state before precompile called + ) + + err = stateDB.Commit() + s.Require().NoError(err) + s.Require().EqualValues(0, stateDB.DebugDirtiesCount()) + test.AssertWasmCounterState( + &s.Suite, deps, evmObj, helloWorldCounterWasm, 7, // state before precompile called + ) } -func debugDirtiesCountMismatch(db *statedb.StateDB, t *testing.T) string { +func debugDirtiesCountMismatch(db *statedb.StateDB, t *testing.T) { lines := []string{} dirties := db.DebugDirties() stateObjects := db.DebugStateObjects() - for addr, dirtyCountForAddr := range dirties { - lines = append(lines, fmt.Sprintf("Dirty addr: %s, dirtyCountForAddr=%d", addr, dirtyCountForAddr)) + for addr, dirtyCount := range dirties { + lines = append(lines, fmt.Sprintf("Dirty addr: %s, dirtyCount=%d", addr, dirtyCount)) // Inspect the actual state object maybeObj := stateObjects[addr] @@ -255,5 +268,4 @@ func debugDirtiesCountMismatch(db *statedb.StateDB, t *testing.T) string { } t.Log("debugDirtiesCountMismatch:\n", strings.Join(lines, "\n")) - return "" } diff --git a/x/evm/statedb/statedb.go b/x/evm/statedb/statedb.go index 1306dfc7f..f711c6cb3 100644 --- a/x/evm/statedb/statedb.go +++ b/x/evm/statedb/statedb.go @@ -261,7 +261,11 @@ func (s *StateDB) getStateObject(addr common.Address) *stateObject { } // If no live objects are available, load it from keeper - account := s.keeper.GetAccount(s.evmTxCtx, addr) + ctx := s.evmTxCtx + if s.writeToCommitCtxFromCacheCtx != nil { + ctx = s.cacheCtx + } + account := s.keeper.GetAccount(ctx, addr) if account == nil { return nil } @@ -322,7 +326,11 @@ func (s *StateDB) ForEachStorage(addr common.Address, cb func(key, value common. if so == nil { return nil } - s.keeper.ForEachStorage(s.evmTxCtx, addr, func(key, value common.Hash) bool { + ctx := s.evmTxCtx + if s.writeToCommitCtxFromCacheCtx != nil { + ctx = s.cacheCtx + } + s.keeper.ForEachStorage(ctx, addr, func(key, value common.Hash) bool { if value, dirty := so.DirtyStorage[key]; dirty { return cb(key, value) } diff --git a/x/evm/statedb/statedb_test.go b/x/evm/statedb/statedb_test.go index a89444042..fc0a1fdc0 100644 --- a/x/evm/statedb/statedb_test.go +++ b/x/evm/statedb/statedb_test.go @@ -8,7 +8,7 @@ import ( gethcore "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto" - s "github.com/stretchr/testify/suite" + "github.com/stretchr/testify/suite" "github.com/NibiruChain/nibiru/v2/x/common/set" "github.com/NibiruChain/nibiru/v2/x/evm/evmtest" @@ -30,11 +30,11 @@ var ( // TestSuite runs the entire test suite. func TestSuite(t *testing.T) { - s.Run(t, new(Suite)) + suite.Run(t, new(Suite)) } type Suite struct { - s.Suite + suite.Suite } // CollectContractStorage is a helper function that collects all storage key-value pairs diff --git a/x/evm/tx.go b/x/evm/tx.go index ba409a855..6c58c0f9c 100644 --- a/x/evm/tx.go +++ b/x/evm/tx.go @@ -14,6 +14,7 @@ import ( // EvmTxArgs encapsulates all possible params to create all EVM txs types. // This includes LegacyTx, DynamicFeeTx and AccessListTx +// TODO(k-yang): replace with JsonTxArgs. type EvmTxArgs struct { Nonce uint64 GasLimit uint64