diff --git a/.github/workflows/proto-lint.yml b/.github/workflows/proto-lint.yml index 5d769d26c..08b3ddb65 100644 --- a/.github/workflows/proto-lint.yml +++ b/.github/workflows/proto-lint.yml @@ -22,7 +22,7 @@ jobs: # timeout-minutes: 5 # steps: # - uses: actions/checkout@v4 - # - uses: bufbuild/buf-setup-action@v1.44.0 + # - uses: bufbuild/buf-setup-action@v1.45.0 # - uses: bufbuild/buf-lint-action@v1 # with: # input: "proto" @@ -31,7 +31,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: bufbuild/buf-setup-action@v1.44.0 + - uses: bufbuild/buf-setup-action@v1.45.0 with: github_token: ${{ github.token }} - uses: bufbuild/buf-breaking-action@v1 diff --git a/CHANGELOG.md b/CHANGELOG.md index d9878252b..a656fc843 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,17 +40,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -### State Machine Breaking +### Nibiru EVM -#### For next mainnet version +#### Nibiru EVM | Before Audit 2 [Nov, 2024] -- [#1766](https://github.com/NibiruChain/nibiru/pull/1766) - refactor(app-wasmext)!: remove wasmbinding `CosmosMsg::Custom` bindings. -- [#1776](https://github.com/NibiruChain/nibiru/pull/1776) - feat(inflation): make inflation params a collection and add commands to update them -- [#1872](https://github.com/NibiruChain/nibiru/pull/1872) - chore(math): use cosmossdk.io/math to replace sdk types -- [#1874](https://github.com/NibiruChain/nibiru/pull/1874) - chore(proto): remove the proto stringer as per Cosmos SDK migration guidelines -- [#1932](https://github.com/NibiruChain/nibiru/pull/1932) - fix(gosdk): fix keyring import functions +The codebase went through a third-party [Code4rena +Zenith](https://code4rena.com/zenith) Audit, running from 2024-10-07 until +2024-11-01 and including both a primary review period and mitigation/remission +period. This section describes code changes that occured after that audit in +preparation for a second audit starting in November 2024. + +- [#2074](https://github.com/NibiruChain/nibiru/pull/2074) - fix(evm-keeper): better utilize ERC20 metadata during FunToken creation. The bank metadata for a new FunToken mapping ties a connection between the Bank Coin's `DenomUnit` and the ERC20 contract metadata like the name, decimals, and symbol. This change brings parity between EVM wallets, such as MetaMask, and Interchain wallets like Keplr and Leap. +- [#2076](https://github.com/NibiruChain/nibiru/pull/2076) - fix(evm-gas-fees): +Use effective gas price in RefundGas and make sure that units are properly +reflected on all occurences of "base fee" in the codebase. This fixes [#2059](https://github.com/NibiruChain/nibiru/issues/2059) +and the [related comments from @Unique-Divine and @berndartmueller](https://github.com/NibiruChain/nibiru/issues/2059#issuecomment-2408625724). +- [#2084](https://github.com/NibiruChain/nibiru/pull/2084) - feat(evm-forge): foundry support and template for Nibiru EVM develoment +- [#2086](https://github.com/NibiruChain/nibiru/pull/2086) - fix(evm-precomples): +Fix state consistency in precompile execution by ensuring proper journaling of +state changes in the StateDB. This pull request makes sure that state is +committed as expected, fixes the `StateDB.Commit` to follow its guidelines more +closely, and solves for a critical state inconsistency producible from the +FunToken.sol precompiled contract. It also aligns the precompiles to use +consistent setup and dynamic gas calculations, addressing the following tickets. + - https://github.com/NibiruChain/nibiru/issues/2083 + - https://github.com/code-423n4/2024-10-nibiru-zenith/issues/43 + - https://github.com/code-423n4/2024-10-nibiru-zenith/issues/47 +- [#2088](https://github.com/NibiruChain/nibiru/pull/2088) - refactor(evm): remove outdated comment and improper error message text +- [#2089](https://github.com/NibiruChain/nibiru/pull/2089) - better handling of gas consumption within erc20 contract execution +- [#2091](https://github.com/NibiruChain/nibiru/pull/2091) - feat(evm): add fun token creation fee validation -#### Nibiru EVM +#### Nibiru EVM | Before Audit 1 - 2024-10-18 - [#1837](https://github.com/NibiruChain/nibiru/pull/1837) - feat(eth): protos, eth types, and evm module types - [#1838](https://github.com/NibiruChain/nibiru/pull/1838) - feat(eth): Go-ethereum, crypto, encoding, and unit tests for evm/types @@ -70,7 +90,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [#1909](https://github.com/NibiruChain/nibiru/pull/1909) - chore(evm): set is_london true by default and removed from config - [#1911](https://github.com/NibiruChain/nibiru/pull/1911) - chore(evm): simplified config by removing old eth forks - [#1912](https://github.com/NibiruChain/nibiru/pull/1912) - test(evm): unit tests for evm_ante -- [#1914](https://github.com/NibiruChain/nibiru/pull/1914) - refactor(evm): Remove dead code and document non-EVM ante handler- [#1917](https://github.com/NibiruChain/nibiru/pull/1917) - test(e2e-evm): TypeScript support. Type generation from compiled contracts. Formatter for TS code. +- [#1914](https://github.com/NibiruChain/nibiru/pull/1914) - refactor(evm): Remove dead code and document non-EVM ante handler - [#1917](https://github.com/NibiruChain/nibiru/pull/1917) - test(e2e-evm): TypeScript support. Type generation from compiled contracts. Formatter for TS code. - [#1922](https://github.com/NibiruChain/nibiru/pull/1922) - feat(evm): tracer option is read from the config. - [#1936](https://github.com/NibiruChain/nibiru/pull/1936) - feat(evm): EVM fungible token protobufs and encoding tests @@ -108,7 +128,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [#2002](https://github.com/NibiruChain/nibiru/pull/2002) - feat(evm): Add the account query to the EVM command. Cover the CLI with tests. - [#2003](https://github.com/NibiruChain/nibiru/pull/2003) - fix(evm): fix FunToken conversions between Cosmos and EVM - [#2004](https://github.com/NibiruChain/nibiru/pull/2004) - refactor(evm)!: replace `HexAddr` with `EIP55Addr` -- [#2006](https://github.com/NibiruChain/nibiru/pull/2006) - test(evm): e2e tests for eth_* endpoints +- [#2006](https://github.com/NibiruChain/nibiru/pull/2006) - test(evm): e2e tests for eth\_\* endpoints - [#2008](https://github.com/NibiruChain/nibiru/pull/2008) - refactor(evm): clean up precompile setups - [#2013](https://github.com/NibiruChain/nibiru/pull/2013) - chore(evm): Set appropriate gas value for the required gas of the "IFunToken.sol" precompile. - [#2014](https://github.com/NibiruChain/nibiru/pull/2014) - feat(evm): Emit block bloom event in EndBlock hook. @@ -124,7 +144,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [#2044](https://github.com/NibiruChain/nibiru/pull/2044) - feat(evm): evm tx indexer service implemented - [#2045](https://github.com/NibiruChain/nibiru/pull/2045) - test(evm): backend tests with test network and real txs - [#2053](https://github.com/NibiruChain/nibiru/pull/2053) - refactor(evm): converted untyped event to typed and cleaned up -- [#2054](https://github.com/NibiruChain/nibiru/pull/2054) - feat(evm-precompile): Precompile for one-way EVM calls to invoke/execute Wasm contracts. +- [#2054](https://github.com/NibiruChain/nibiru/pull/2054) - feat(evm-precompile): Precompile for one-way EVM calls to invoke/execute Wasm contracts. - [#2060](https://github.com/NibiruChain/nibiru/pull/2060) - fix(evm-precompiles): add assertNumArgs validation - [#2056](https://github.com/NibiruChain/nibiru/pull/2056) - feat(evm): add oracle precompile - [#2065](https://github.com/NibiruChain/nibiru/pull/2065) - refactor(evm)!: Refactor out dead code from the evm.Params @@ -139,6 +159,16 @@ and the [related comments from @Unique-Divine and @berndartmueller](https://gith - [#2092](https://github.com/NibiruChain/nibiru/pull/2092) - feat(evm): add validation for wasm multi message execution +### State Machine Breaking (Other) + +#### For next mainnet version + +- [#1766](https://github.com/NibiruChain/nibiru/pull/1766) - refactor(app-wasmext)!: remove wasmbinding `CosmosMsg::Custom` bindings. +- [#1776](https://github.com/NibiruChain/nibiru/pull/1776) - feat(inflation): make inflation params a collection and add commands to update them +- [#1872](https://github.com/NibiruChain/nibiru/pull/1872) - chore(math): use cosmossdk.io/math to replace sdk types +- [#1874](https://github.com/NibiruChain/nibiru/pull/1874) - chore(proto): remove the proto stringer as per Cosmos SDK migration guidelines +- [#1932](https://github.com/NibiruChain/nibiru/pull/1932) - fix(gosdk): fix keyring import functions + #### Dapp modules: perp, spot, oracle, etc - [#1573](https://github.com/NibiruChain/nibiru/pull/1573) - feat(perp): Close markets and compute settlement price @@ -188,9 +218,9 @@ and the [related comments from @Unique-Divine and @berndartmueller](https://gith - Bump `github.com/supranational/blst` from 0.3.8-0.20220526154634-513d2456b344 to 0.3.11 ([#1851](https://github.com/NibiruChain/nibiru/pull/1851)) - Bump `golangci/golangci-lint-action` from 4 to 6 ([#1854](https://github.com/NibiruChain/nibiru/pull/1854), [#1867](https://github.com/NibiruChain/nibiru/pull/1867)) - Bump `github.com/hashicorp/go-getter` from 1.7.1 to 1.7.5 ([#1858](https://github.com/NibiruChain/nibiru/pull/1858), [#1938](https://github.com/NibiruChain/nibiru/pull/1938)) -- Bump `github.com/btcsuite/btcd` from 0.23.3 to 0.24.0 ([#1862](https://github.com/NibiruChain/nibiru/pull/1862)) +- Bump `github.com/btcsuite/btcd` from 0.23.3 to 0.24.2 ([#1862](https://github.com/NibiruChain/nibiru/pull/1862), [#2070](https://github.com/NibiruChain/nibiru/pull/2070)) - Bump `pozetroninc/github-action-get-latest-release` from 0.7.0 to 0.8.0 ([#1863](https://github.com/NibiruChain/nibiru/pull/1863)) -- Bump `bufbuild/buf-setup-action` from 1.30.1 to 1.44.0 ([#1891](https://github.com/NibiruChain/nibiru/pull/1891), [#1900](https://github.com/NibiruChain/nibiru/pull/1900), [#1923](https://github.com/NibiruChain/nibiru/pull/1923), [#1972](https://github.com/NibiruChain/nibiru/pull/1972), [#1974](https://github.com/NibiruChain/nibiru/pull/1974), [#1988](https://github.com/NibiruChain/nibiru/pull/1988), [#2043](https://github.com/NibiruChain/nibiru/pull/2043), [#2057](https://github.com/NibiruChain/nibiru/pull/2057), [#2062](https://github.com/NibiruChain/nibiru/pull/2062)) +- Bump `bufbuild/buf-setup-action` from 1.30.1 to 1.45.0 ([#1891](https://github.com/NibiruChain/nibiru/pull/1891), [#1900](https://github.com/NibiruChain/nibiru/pull/1900), [#1923](https://github.com/NibiruChain/nibiru/pull/1923), [#1972](https://github.com/NibiruChain/nibiru/pull/1972), [#1974](https://github.com/NibiruChain/nibiru/pull/1974), [#1988](https://github.com/NibiruChain/nibiru/pull/1988), [#2043](https://github.com/NibiruChain/nibiru/pull/2043), [#2057](https://github.com/NibiruChain/nibiru/pull/2057), [#2062](https://github.com/NibiruChain/nibiru/pull/2062), [#2069](https://github.com/NibiruChain/nibiru/pull/2069)) - Bump `axios` from 1.7.3 to 1.7.4 ([#2016](https://github.com/NibiruChain/nibiru/pull/2016)) - Bump `github.com/CosmWasm/wasmvm` from 1.5.0 to 1.5.5 ([#2047](https://github.com/NibiruChain/nibiru/pull/2047)) - Bump `docker/build-push-action` from 5 to 6 ([#1924](https://github.com/NibiruChain/nibiru/pull/1924)) diff --git a/app/evmante/evmante_validate_basic_test.go b/app/evmante/evmante_validate_basic_test.go index 4f0e136ff..3f1263dee 100644 --- a/app/evmante/evmante_validate_basic_test.go +++ b/app/evmante/evmante_validate_basic_test.go @@ -36,6 +36,18 @@ func (s *TestSuite) TestEthValidateBasicDecorator() { }, wantErr: "", }, + { + name: "sad: fail to set params", + txSetup: func(deps *evmtest.TestDeps) sdk.Tx { + return evmtest.HappyCreateContractTx(deps) + }, + paramsSetup: func(deps *evmtest.TestDeps) evm.Params { + return evm.Params{ + CreateFuntokenFee: sdk.NewInt(-1), + } + }, + wantErr: "createFuntokenFee cannot be negative: -1", + }, { name: "happy: ctx recheck should ignore validation", ctxSetup: func(deps *evmtest.TestDeps) { @@ -195,12 +207,16 @@ func (s *TestSuite) TestEthValidateBasicDecorator() { if tc.ctxSetup != nil { tc.ctxSetup(&deps) } + var err error if tc.paramsSetup != nil { - deps.EvmKeeper.SetParams(deps.Ctx, tc.paramsSetup(&deps)) + err = deps.EvmKeeper.SetParams(deps.Ctx, tc.paramsSetup(&deps)) + } + + if err == nil { + _, err = anteDec.AnteHandle( + deps.Ctx, tx, false, evmtest.NextNoOpAnteHandler, + ) } - _, err := anteDec.AnteHandle( - deps.Ctx, tx, false, evmtest.NextNoOpAnteHandler, - ) if tc.wantErr != "" { s.Require().ErrorContains(err, tc.wantErr) return diff --git a/go.mod b/go.mod index a988221f5..158dc9b80 100644 --- a/go.mod +++ b/go.mod @@ -25,7 +25,7 @@ require ( cosmossdk.io/simapp v0.0.0-20230608160436-666c345ad23d github.com/MakeNowJust/heredoc/v2 v2.0.1 github.com/armon/go-metrics v0.4.1 - github.com/btcsuite/btcd v0.24.0 + github.com/btcsuite/btcd v0.24.2 github.com/btcsuite/btcd/btcutil v1.1.5 github.com/cosmos/go-bip39 v1.0.0 github.com/cosmos/gogoproto v1.4.10 diff --git a/go.sum b/go.sum index 618c727f6..2c789f74f 100644 --- a/go.sum +++ b/go.sum @@ -308,8 +308,8 @@ github.com/btcsuite/btcd v0.21.0-beta.0.20201114000516-e9c7a5ac6401/go.mod h1:Sv github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= github.com/btcsuite/btcd v0.22.1/go.mod h1:wqgTSL29+50LRkmOVknEdmt8ZojIzhuWvgu/iptuN7Y= github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A= -github.com/btcsuite/btcd v0.24.0 h1:gL3uHE/IaFj6fcZSu03SvqPMSx7s/dPzfpG/atRwWdo= -github.com/btcsuite/btcd v0.24.0/go.mod h1:K4IDc1593s8jKXIF7yS7yCTSxrknB9z0STzc2j6XgE4= +github.com/btcsuite/btcd v0.24.2 h1:aLmxPguqxza+4ag8R1I2nnJjSu2iFn/kqtHTIImswcY= +github.com/btcsuite/btcd v0.24.2/go.mod h1:5C8ChTkl5ejr3WHj8tkQSCmydiMEPB0ZhQhehpq7Dgg= github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA= github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= github.com/btcsuite/btcd/btcec/v2 v2.2.0/go.mod h1:U7MHm051Al6XmscBQ0BoNydpOTsFAn707034b5nY8zU= diff --git a/x/evm/embeds/package-lock.json b/x/evm/embeds/package-lock.json index 257cdaa23..806b00fe2 100644 --- a/x/evm/embeds/package-lock.json +++ b/x/evm/embeds/package-lock.json @@ -6517,21 +6517,47 @@ "license": "MIT" }, "node_modules/secp256k1": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-4.0.3.tgz", - "integrity": "sha512-NLZVf+ROMxwtEj3Xa562qgv2BK5e2WNmXPiOdVIPLgs6lyTzMvBq0aWTYMI5XCP9jZMVKOcqZLw/Wc4vDkuxhA==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-4.0.4.tgz", + "integrity": "sha512-6JfvwvjUOn8F/jUoBY2Q1v5WY5XS+rj8qSe0v8Y4ezH4InLgTEeOOPQsRll9OV429Pvo6BCHGavIyJfr3TAhsw==", "dev": true, "hasInstallScript": true, - "license": "MIT", "dependencies": { - "elliptic": "^6.5.4", - "node-addon-api": "^2.0.0", + "elliptic": "^6.5.7", + "node-addon-api": "^5.0.0", "node-gyp-build": "^4.2.0" }, "engines": { - "node": ">=10.0.0" + "node": ">=18.0.0" } }, + "node_modules/secp256k1/node_modules/bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", + "dev": true + }, + "node_modules/secp256k1/node_modules/elliptic": { + "version": "6.5.7", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.7.tgz", + "integrity": "sha512-ESVCtTwiA+XhY3wyh24QqRGBoP3rEdDUl3EDUUo9tft074fi19IrdpH7hLCMMP3CIj7jb3W96rn8lt/BqIlt5Q==", + "dev": true, + "dependencies": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/secp256k1/node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", + "dev": true + }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", diff --git a/x/evm/errors.go b/x/evm/errors.go index a32facdcb..9fc6722ac 100644 --- a/x/evm/errors.go +++ b/x/evm/errors.go @@ -74,9 +74,17 @@ var ( func NewExecErrorWithReason(revertReason []byte) *RevertError { result := common.CopyBytes(revertReason) reason, errUnpack := abi.UnpackRevert(result) - err := errors.New("execution reverted") + + var err error + errPrefix := "execution reverted" if errUnpack == nil { - err = fmt.Errorf("execution reverted: %v", reason) + reasonStr := reason + err = fmt.Errorf("%s with reason \"%v\"", errPrefix, reasonStr) + } else if string(result) != "" { + reasonStr := string(result) + err = fmt.Errorf("%s with reason \"%v\"", errPrefix, reasonStr) + } else { + err = errors.New(errPrefix) } return &RevertError{ error: err, diff --git a/x/evm/evmmodule/genesis.go b/x/evm/evmmodule/genesis.go index d67a0c18a..b552b25ea 100644 --- a/x/evm/evmmodule/genesis.go +++ b/x/evm/evmmodule/genesis.go @@ -23,7 +23,10 @@ func InitGenesis( accountKeeper evm.AccountKeeper, genState evm.GenesisState, ) []abci.ValidatorUpdate { - k.SetParams(ctx, genState.Params) + err := k.SetParams(ctx, genState.Params) + if err != nil { + panic(fmt.Errorf("failed to set params: %w", err)) + } // Note that "GetModuleAccount" initializes the module account with permissions // under the hood if it did not already exist. This is important because the diff --git a/x/evm/evmtest/erc20.go b/x/evm/evmtest/erc20.go index ce020036f..d8798f71d 100644 --- a/x/evm/evmtest/erc20.go +++ b/x/evm/evmtest/erc20.go @@ -23,7 +23,7 @@ func AssertERC20BalanceEqual( ) { actualBalance, err := deps.EvmKeeper.ERC20().BalanceOf(erc20, account, deps.Ctx) assert.NoError(t, err) - assert.Zero(t, expectedBalance.Cmp(actualBalance), "expected %s, got %s", expectedBalance, actualBalance) + assert.Equal(t, expectedBalance.String(), actualBalance.String(), "expected %s, got %s", expectedBalance, actualBalance) } // CreateFunTokenForBankCoin: Uses the "TestDeps.Sender" account to create a @@ -99,3 +99,9 @@ func AssertBankBalanceEqual( actualBalance := deps.App.BankKeeper.GetBalance(deps.Ctx, bech32Addr, denom).Amount.BigInt() assert.Zero(t, expectedBalance.Cmp(actualBalance), "expected %s, got %s", expectedBalance, actualBalance) } + +// 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) + return new(big.Int).Mul(amount, pow10) +} diff --git a/x/evm/evmtest/test_deps.go b/x/evm/evmtest/test_deps.go index 9c6015933..1810b1c8f 100644 --- a/x/evm/evmtest/test_deps.go +++ b/x/evm/evmtest/test_deps.go @@ -1,6 +1,8 @@ package evmtest import ( + "context" + sdk "github.com/cosmos/cosmos-sdk/types" gethcommon "github.com/ethereum/go-ethereum/common" @@ -54,3 +56,7 @@ func (deps TestDeps) StateDB() *statedb.StateDB { func (deps *TestDeps) GethSigner() gethcore.Signer { return gethcore.LatestSignerForChainID(deps.App.EvmKeeper.EthChainID(deps.Ctx)) } + +func (deps TestDeps) GoCtx() context.Context { + return sdk.WrapSDKContext(deps.Ctx) +} diff --git a/x/evm/evmtest/tx.go b/x/evm/evmtest/tx.go index 107a15593..dde679851 100644 --- a/x/evm/evmtest/tx.go +++ b/x/evm/evmtest/tx.go @@ -20,6 +20,8 @@ import ( srvconfig "github.com/NibiruChain/nibiru/v2/app/server/config" + "github.com/cosmos/cosmos-sdk/crypto/keyring" + "github.com/NibiruChain/nibiru/v2/x/evm" "github.com/NibiruChain/nibiru/v2/x/evm/embeds" ) @@ -123,7 +125,9 @@ func ExecuteNibiTransfer(deps *TestDeps, t *testing.T) *evm.MsgEthereumTx { To: &recipient, Nonce: (*hexutil.Uint64)(&nonce), } - ethTxMsg, err := GenerateAndSignEthTxMsg(txArgs, deps) + ethTxMsg, gethSigner, krSigner, err := GenerateEthTxMsgAndSigner(txArgs, deps, deps.Sender) + require.NoError(t, err) + err = ethTxMsg.Sign(gethSigner, krSigner) require.NoError(t, err) resp, err := deps.App.EvmKeeper.EthereumTx(sdk.WrapSDKContext(deps.Ctx), ethTxMsg) @@ -153,18 +157,20 @@ func DeployContract( bytecodeForCall := append(contract.Bytecode, packedArgs...) nonce := deps.StateDB().GetNonce(deps.Sender.EthAddr) - msgEthTx, err := GenerateAndSignEthTxMsg( + ethTxMsg, gethSigner, krSigner, err := GenerateEthTxMsgAndSigner( evm.JsonTxArgs{ Nonce: (*hexutil.Uint64)(&nonce), Input: (*hexutil.Bytes)(&bytecodeForCall), From: &deps.Sender.EthAddr, - }, deps, + }, deps, deps.Sender, ) 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 { + return nil, errors.Wrap(err, "failed to generate and sign eth tx msg") } - resp, err := deps.App.EvmKeeper.EthereumTx(sdk.WrapSDKContext(deps.Ctx), msgEthTx) + resp, err := deps.App.EvmKeeper.EthereumTx(sdk.WrapSDKContext(deps.Ctx), ethTxMsg) if err != nil { return nil, errors.Wrap(err, "failed to execute ethereum tx") } @@ -174,7 +180,7 @@ func DeployContract( return &DeployContractResult{ TxResp: resp, - EthTxMsg: msgEthTx, + EthTxMsg: ethTxMsg, ContractData: contract, Nonce: nonce, ContractAddr: crypto.CreateAddress(deps.Sender.EthAddr, nonce), @@ -184,7 +190,11 @@ func DeployContract( // DeployAndExecuteERC20Transfer deploys contract, executes transfer and returns tx hash func DeployAndExecuteERC20Transfer( deps *TestDeps, t *testing.T, -) (erc20Transfer *evm.MsgEthereumTx, predecessors []*evm.MsgEthereumTx) { +) ( + erc20Transfer *evm.MsgEthereumTx, + predecessors []*evm.MsgEthereumTx, + contractAddr gethcommon.Address, +) { // TX 1: Deploy ERC-20 contract deployResp, err := DeployContract(deps, embeds.SmartContract_TestERC20) require.NoError(t, err) @@ -192,7 +202,7 @@ func DeployAndExecuteERC20Transfer( nonce := deployResp.Nonce // Contract address is deterministic - contractAddress := crypto.CreateAddress(deps.Sender.EthAddr, nonce) + contractAddr = crypto.CreateAddress(deps.Sender.EthAddr, nonce) deps.App.Commit() predecessors = []*evm.MsgEthereumTx{ deployResp.EthTxMsg, @@ -206,27 +216,67 @@ func DeployAndExecuteERC20Transfer( nonce = deps.StateDB().GetNonce(deps.Sender.EthAddr) txArgs := evm.JsonTxArgs{ From: &deps.Sender.EthAddr, - To: &contractAddress, + To: &contractAddr, Nonce: (*hexutil.Uint64)(&nonce), Data: (*hexutil.Bytes)(&input), } - erc20Transfer, err = GenerateAndSignEthTxMsg(txArgs, deps) + erc20Transfer, gethSigner, krSigner, err := GenerateEthTxMsgAndSigner(txArgs, deps, deps.Sender) + require.NoError(t, err) + err = erc20Transfer.Sign(gethSigner, krSigner) require.NoError(t, err) - resp, err := deps.App.EvmKeeper.EthereumTx(sdk.WrapSDKContext(deps.Ctx), erc20Transfer) + resp, err := deps.App.EvmKeeper.EthereumTx(deps.GoCtx(), erc20Transfer) require.NoError(t, err) require.Empty(t, resp.VmError) - return erc20Transfer, predecessors + return erc20Transfer, predecessors, contractAddr } -// GenerateAndSignEthTxMsg estimates gas, sets gas limit and sings the tx -func GenerateAndSignEthTxMsg( - jsonTxArgs evm.JsonTxArgs, deps *TestDeps, -) (*evm.MsgEthereumTx, error) { +func CallContractTx( + deps *TestDeps, + contractAddr gethcommon.Address, + input []byte, + sender EthPrivKeyAcc, +) (ethTxMsg *evm.MsgEthereumTx, resp *evm.MsgEthereumTxResponse, err error) { + nonce := deps.StateDB().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 +} + +// GenerateEthTxMsgAndSigner estimates gas, sets gas limit and returns signer for +// the tx. +// +// Usage: +// +// ```go +// evmTxMsg, gethSigner, krSigner, _ := GenerateEthTxMsgAndSigner( +// jsonTxArgs, &deps, sender, +// ) +// err := evmTxMsg.Sign(gethSigner, sender.KeyringSigner) +// ``` +func GenerateEthTxMsgAndSigner( + jsonTxArgs evm.JsonTxArgs, deps *TestDeps, sender EthPrivKeyAcc, +) (evmTxMsg *evm.MsgEthereumTx, gethSigner gethcore.Signer, krSigner keyring.Signer, err error) { estimateArgs, err := json.Marshal(&jsonTxArgs) if err != nil { - return nil, err + return } res, err := deps.App.EvmKeeper.EstimateGas( sdk.WrapSDKContext(deps.Ctx), @@ -238,13 +288,13 @@ func GenerateAndSignEthTxMsg( }, ) if err != nil { - return nil, err + return } jsonTxArgs.Gas = (*hexutil.Uint64)(&res.Gas) - msgEthTx := jsonTxArgs.ToMsgEthTx() - gethSigner := gethcore.LatestSignerForChainID(deps.App.EvmKeeper.EthChainID(deps.Ctx)) - return msgEthTx, msgEthTx.Sign(gethSigner, deps.Sender.KeyringSigner) + evmTxMsg = jsonTxArgs.ToMsgEthTx() + gethSigner = gethcore.LatestSignerForChainID(deps.App.EvmKeeper.EthChainID(deps.Ctx)) + return evmTxMsg, gethSigner, sender.KeyringSigner, nil } func TransferWei( diff --git a/x/evm/evmtest/tx_test.go b/x/evm/evmtest/tx_test.go new file mode 100644 index 000000000..e9cc956c1 --- /dev/null +++ b/x/evm/evmtest/tx_test.go @@ -0,0 +1,90 @@ +// Copyright (c) 2023-2024 Nibi, Inc. +package evmtest_test + +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() + + s.Require().NoError(testapp.FundAccount( + deps.App.BankKeeper, + deps.Ctx, + deps.Sender.NibiruAddr, + sdk.NewCoins(sdk.NewCoin(evm.EVMBankDenom, sdk.NewInt(69_420))), + )) + + randomAcc := evmtest.NewEthPrivAcc() + to := randomAcc.EthAddr + err := evmtest.TransferWei(&deps, to, evm.NativeToWei(big.NewInt(420))) + s.Require().NoError(err) + + evmtest.AssertBankBalanceEqual( + s.T(), deps, evm.EVMBankDenom, deps.Sender.EthAddr, big.NewInt(69_000), + ) + + s.Run("DeployAndExecuteERC20Transfer", func() { + evmtest.DeployAndExecuteERC20Transfer(&deps, s.T()) + }) +} diff --git a/x/evm/keeper/erc20.go b/x/evm/keeper/erc20.go index ce1f6c848..10404bea4 100644 --- a/x/evm/keeper/erc20.go +++ b/x/evm/keeper/erc20.go @@ -56,7 +56,8 @@ func (e erc20Calls) Mint( if err != nil { return nil, fmt.Errorf("failed to pack ABI args: %w", err) } - return e.CallContractWithInput(ctx, from, &contract, true, input) + evmResp, _, err = e.CallContractWithInput(ctx, from, &contract, true, input) + return evmResp, err } /* @@ -77,7 +78,7 @@ func (e erc20Calls) Transfer( if err != nil { return false, fmt.Errorf("failed to pack ABI args: %w", err) } - resp, err := e.CallContractWithInput(ctx, from, &contract, true, input) + resp, _, err := e.CallContractWithInput(ctx, from, &contract, true, input) if err != nil { return false, err } @@ -117,7 +118,8 @@ func (e erc20Calls) Burn( return } commit := true - return e.CallContractWithInput(ctx, from, &contract, commit, input) + evmResp, _, err = e.CallContractWithInput(ctx, from, &contract, commit, input) + return } // CallContract invokes a smart contract on the method specified by [methodName] @@ -148,7 +150,8 @@ func (k Keeper) CallContract( if err != nil { return nil, fmt.Errorf("failed to pack ABI args: %w", err) } - return k.CallContractWithInput(ctx, fromAcc, contract, commit, contractInput) + evmResp, _, err = k.CallContractWithInput(ctx, fromAcc, contract, commit, contractInput) + return evmResp, err } // CallContractWithInput invokes a smart contract with the given [contractInput] @@ -171,7 +174,7 @@ func (k Keeper) CallContractWithInput( contract *gethcommon.Address, commit bool, contractInput []byte, -) (evmResp *evm.MsgEthereumTxResponse, err error) { +) (evmResp *evm.MsgEthereumTxResponse, evmObj *vm.EVM, 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 func() { @@ -207,7 +210,8 @@ func (k Keeper) CallContractWithInput( k.EthChainID(ctx), ) if err != nil { - return nil, errors.Wrapf(err, "failed to load evm config") + err = errors.Wrapf(err, "failed to load EVM config") + return } // Generating TxConfig with an empty tx hash as there is no actual eth tx @@ -217,23 +221,27 @@ func (k Keeper) CallContractWithInput( // Using tmp context to not modify the state in case of evm revert tmpCtx, commitCtx := ctx.CacheContext() - evmResp, err = k.ApplyEvmMsg( + evmResp, evmObj, err = k.ApplyEvmMsg( tmpCtx, evmMsg, evm.NewNoOpTracer(), commit, evmCfg, txConfig, ) if err != nil { // We don't know the actual gas used, so consuming the gas limit k.ResetGasMeterAndConsumeGas(ctx, gasLimit) - return nil, errors.Wrap(err, "failed to apply ethereum core message") + err = errors.Wrap(err, "failed to apply ethereum core message") + return } if evmResp.Failed() { k.ResetGasMeterAndConsumeGas(ctx, evmResp.GasUsed) if evmResp.VmError != vm.ErrOutOfGas.Error() { if evmResp.VmError == vm.ErrExecutionReverted.Error() { - return nil, fmt.Errorf("VMError: %w", evm.NewExecErrorWithReason(evmResp.Ret)) + err = fmt.Errorf("VMError: %w", evm.NewExecErrorWithReason(evmResp.Ret)) + return } - return nil, fmt.Errorf("VMError: %s", evmResp.VmError) + err = fmt.Errorf("VMError: %s", evmResp.VmError) + return } - return nil, fmt.Errorf("gas required exceeds allowance (%d)", gasLimit) + err = fmt.Errorf("gas required exceeds allowance (%d)", gasLimit) + return } else { // Success, committing the state to ctx if commit { @@ -241,18 +249,18 @@ func (k Keeper) CallContractWithInput( totalGasUsed, err := k.AddToBlockGasUsed(ctx, evmResp.GasUsed) if err != nil { k.ResetGasMeterAndConsumeGas(ctx, ctx.GasMeter().Limit()) - return nil, errors.Wrap(err, "error adding transient gas used to block") + return nil, nil, errors.Wrap(err, "error adding transient gas used to block") } k.ResetGasMeterAndConsumeGas(ctx, totalGasUsed) k.updateBlockBloom(ctx, evmResp, uint64(txConfig.LogIndex)) err = k.EmitEthereumTxEvents(ctx, contract, gethcore.LegacyTxType, evmMsg, evmResp) if err != nil { - return nil, errors.Wrap(err, "error emitting ethereum tx events") + return nil, nil, errors.Wrap(err, "error emitting ethereum tx events") } blockTxIdx := uint64(txConfig.TxIndex) + 1 k.EvmState.BlockTxIndex.Set(ctx, blockTxIdx) } - return evmResp, nil + return evmResp, evmObj, nil } } diff --git a/x/evm/keeper/erc20_test.go b/x/evm/keeper/erc20_test.go index d328ea4e6..4b1dc10fa 100644 --- a/x/evm/keeper/erc20_test.go +++ b/x/evm/keeper/erc20_test.go @@ -16,7 +16,10 @@ func (s *Suite) TestERC20Calls() { s.T().Log("Mint tokens - Fail from non-owner") { - _, err := deps.EvmKeeper.ERC20().Mint(contract, deps.Sender.EthAddr, evm.EVM_MODULE_ADDRESS, big.NewInt(69_420), deps.Ctx) + _, err := deps.EvmKeeper.ERC20().Mint( + contract, deps.Sender.EthAddr, evm.EVM_MODULE_ADDRESS, + big.NewInt(69_420), deps.Ctx, + ) s.ErrorContains(err, evm.ErrOwnable) } diff --git a/x/evm/keeper/evm_state.go b/x/evm/keeper/evm_state.go index 426e78d8e..9e78cc17b 100644 --- a/x/evm/keeper/evm_state.go +++ b/x/evm/keeper/evm_state.go @@ -2,6 +2,7 @@ package keeper import ( + "fmt" "math/big" "github.com/NibiruChain/collections" @@ -116,8 +117,13 @@ func (k Keeper) GetParams(ctx sdk.Context) (params evm.Params) { } // SetParams: Setter for the module parameters. -func (k Keeper) SetParams(ctx sdk.Context, params evm.Params) { +func (k Keeper) SetParams(ctx sdk.Context, params evm.Params) (err error) { + if params.CreateFuntokenFee.IsNegative() { + return fmt.Errorf("createFuntokenFee cannot be negative: %s", params.CreateFuntokenFee) + } + k.EvmState.ModuleParams.Set(ctx, params) + return } // SetState updates contract storage and deletes if the value is empty. diff --git a/x/evm/keeper/funtoken_from_coin.go b/x/evm/keeper/funtoken_from_coin.go index 82d7017f6..6f0f2efd0 100644 --- a/x/evm/keeper/funtoken_from_coin.go +++ b/x/evm/keeper/funtoken_from_coin.go @@ -78,7 +78,7 @@ func (k *Keeper) deployERC20ForBankCoin( bytecodeForCall := append(embeds.SmartContract_ERC20Minter.Bytecode, packedArgs...) // nil address for contract creation - _, err = k.CallContractWithInput( + _, _, err = k.CallContractWithInput( ctx, evm.EVM_MODULE_ADDRESS, nil, true, bytecodeForCall, ) if err != nil { diff --git a/x/evm/keeper/funtoken_from_coin_test.go b/x/evm/keeper/funtoken_from_coin_test.go index cb9b87ffb..1f5e3a85a 100644 --- a/x/evm/keeper/funtoken_from_coin_test.go +++ b/x/evm/keeper/funtoken_from_coin_test.go @@ -282,7 +282,7 @@ func (s *FunTokenFromCoinSuite) TestConvertCoinToEvmAndBack() { // Check 3: erc-20 balance balance, err = deps.EvmKeeper.ERC20().BalanceOf(funTokenErc20Addr.Address, alice.EthAddr, deps.Ctx) s.Require().NoError(err) - s.Require().Zero(balance.Cmp(big.NewInt(0))) + 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( diff --git a/x/evm/keeper/funtoken_from_erc20_test.go b/x/evm/keeper/funtoken_from_erc20_test.go index 33623a178..eb209f7ca 100644 --- a/x/evm/keeper/funtoken_from_erc20_test.go +++ b/x/evm/keeper/funtoken_from_erc20_test.go @@ -160,7 +160,7 @@ func (s *FunTokenFromErc20Suite) TestCreateFunTokenFromERC20() { s.ErrorContains(err, "either the \"from_erc20\" or \"from_bank_denom\" must be set") } -func (s *FunTokenFromErc20Suite) TestSendFromEvmToCosmos() { +func (s *FunTokenFromErc20Suite) TestSendFromEvmToBank() { deps := evmtest.NewTestDeps() s.T().Log("Deploy ERC20") @@ -210,7 +210,7 @@ func (s *FunTokenFromErc20Suite) TestSendFromEvmToCosmos() { randomAcc := testutil.AccAddress() - s.T().Log("send erc20 tokens to cosmos") + s.T().Log("send erc20 tokens to Bank") _, err = deps.EvmKeeper.CallContract( deps.Ctx, embeds.SmartContract_FunToken.ABI, @@ -231,8 +231,8 @@ func (s *FunTokenFromErc20Suite) TestSendFromEvmToCosmos() { deps.App.BankKeeper.GetBalance(deps.Ctx, randomAcc, bankDemon).Amount, ) - s.T().Log("sad: send too many erc20 tokens to cosmos") - _, err = deps.EvmKeeper.CallContract( + s.T().Log("sad: send too many erc20 tokens to Bank") + evmResp, err := deps.EvmKeeper.CallContract( deps.Ctx, embeds.SmartContract_FunToken.ABI, deps.Sender.EthAddr, @@ -243,9 +243,10 @@ func (s *FunTokenFromErc20Suite) TestSendFromEvmToCosmos() { big.NewInt(70_000), randomAcc.String(), ) - s.Require().Error(err) + s.T().Log("check balances") + s.Require().Error(err, evmResp.String()) - s.T().Log("send cosmos tokens back to erc20") + s.T().Log("send Bank tokens back to erc20") _, err = deps.EvmKeeper.ConvertCoinToEvm(sdk.WrapSDKContext(deps.Ctx), &evm.MsgConvertCoinToEvm{ ToEthAddr: eth.EIP55Addr{ @@ -264,7 +265,7 @@ func (s *FunTokenFromErc20Suite) TestSendFromEvmToCosmos() { deps.App.BankKeeper.GetBalance(deps.Ctx, randomAcc, bankDemon).Amount.Equal(sdk.NewInt(0)), ) - s.T().Log("sad: send too many cosmos tokens back to erc20") + s.T().Log("sad: send too many Bank tokens back to erc20") _, err = deps.EvmKeeper.ConvertCoinToEvm(sdk.WrapSDKContext(deps.Ctx), &evm.MsgConvertCoinToEvm{ ToEthAddr: eth.EIP55Addr{ diff --git a/x/evm/keeper/grpc_query.go b/x/evm/keeper/grpc_query.go index 1ae3c19be..9cc9290e9 100644 --- a/x/evm/keeper/grpc_query.go +++ b/x/evm/keeper/grpc_query.go @@ -284,7 +284,7 @@ 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) + res, _, err := k.ApplyEvmMsg(ctx, msg, nil, false, cfg, txConfig) if err != nil { return nil, grpcstatus.Error(grpccodes.Internal, err.Error()) } @@ -422,7 +422,7 @@ func (k Keeper) EstimateGasForEvmCallType( WithTransientKVGasConfig(storetypes.GasConfig{}) } // pass false to not commit StateDB - rsp, err = k.ApplyEvmMsg(tmpCtx, msg, nil, false, cfg, txConfig) + rsp, _, err = k.ApplyEvmMsg(tmpCtx, msg, nil, false, cfg, txConfig) if err != nil { if errors.Is(err, core.ErrIntrinsicGas) { return true, nil, nil // Special case, raise gas limit @@ -518,7 +518,7 @@ 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) + rsp, _, err := k.ApplyEvmMsg(ctx, msg, evm.NewNoOpTracer(), true, cfg, txConfig) if err != nil { continue } @@ -663,15 +663,14 @@ func (k Keeper) TraceBlock( contextHeight = 1 } - ctx := sdk.UnwrapSDKContext(goCtx) - ctx = ctx.WithBlockHeight(contextHeight) - ctx = ctx.WithBlockTime(req.BlockTime) - ctx = ctx.WithHeaderHash(gethcommon.Hex2Bytes(req.BlockHash)) - - // to get the base fee we only need the block max gas in the consensus params - ctx = ctx.WithConsensusParams(&cmtproto.ConsensusParams{ - Block: &cmtproto.BlockParams{MaxGas: req.BlockMaxGas}, - }) + ctx := sdk.UnwrapSDKContext(goCtx). + WithBlockHeight(contextHeight). + WithBlockTime(req.BlockTime). + WithHeaderHash(gethcommon.Hex2Bytes(req.BlockHash)). + // to get the base fee we only need the block max gas in the consensus params + WithConsensusParams(&cmtproto.ConsensusParams{ + Block: &cmtproto.BlockParams{MaxGas: req.BlockMaxGas}, + }) chainID := k.EthChainID(ctx) @@ -801,7 +800,7 @@ 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) + res, _, err := k.ApplyEvmMsg(ctx, msg, tracer, commitMessage, cfg, txConfig) if err != nil { return nil, 0, grpcstatus.Error(grpccodes.Internal, err.Error()) } diff --git a/x/evm/keeper/grpc_query_test.go b/x/evm/keeper/grpc_query_test.go index 46ce456d2..b16bea40c 100644 --- a/x/evm/keeper/grpc_query_test.go +++ b/x/evm/keeper/grpc_query_test.go @@ -447,7 +447,9 @@ func (s *Suite) TestQueryCode() { func (s *Suite) TestQueryParams() { deps := evmtest.NewTestDeps() want := evm.DefaultParams() - deps.EvmKeeper.SetParams(deps.Ctx, want) + err := deps.EvmKeeper.SetParams(deps.Ctx, want) + s.NoError(err) + gotResp, err := deps.EvmKeeper.Params(sdk.WrapSDKContext(deps.Ctx), nil) s.NoError(err) got := gotResp.Params @@ -458,7 +460,8 @@ func (s *Suite) TestQueryParams() { // Empty params to test the setter want.EVMChannels = []string{"channel-420"} - deps.EvmKeeper.SetParams(deps.Ctx, want) + err = deps.EvmKeeper.SetParams(deps.Ctx, want) + s.NoError(err) gotResp, err = deps.EvmKeeper.Params(sdk.WrapSDKContext(deps.Ctx), nil) s.Require().NoError(err) got = gotResp.Params @@ -791,7 +794,7 @@ func (s *Suite) TestTraceTx() { { name: "happy: trace erc-20 transfer tx", scenario: func(deps *evmtest.TestDeps) (req In, wantResp Out) { - txMsg, predecessors := evmtest.DeployAndExecuteERC20Transfer(deps, s.T()) + txMsg, predecessors, _ := evmtest.DeployAndExecuteERC20Transfer(deps, s.T()) req = &evm.QueryTraceTxRequest{ Msg: txMsg, @@ -870,7 +873,7 @@ func (s *Suite) TestTraceBlock() { name: "happy: trace erc-20 transfer tx", setup: nil, scenario: func(deps *evmtest.TestDeps) (req In, wantResp Out) { - txMsg, _ := evmtest.DeployAndExecuteERC20Transfer(deps, s.T()) + txMsg, _, _ := evmtest.DeployAndExecuteERC20Transfer(deps, s.T()) req = &evm.QueryTraceBlockRequest{ Txs: []*evm.MsgEthereumTx{ txMsg, diff --git a/x/evm/keeper/msg_server.go b/x/evm/keeper/msg_server.go index 4de3700d3..89a249dd3 100644 --- a/x/evm/keeper/msg_server.go +++ b/x/evm/keeper/msg_server.go @@ -62,7 +62,7 @@ func (k *Keeper) EthereumTx( tmpCtx, commitCtx := ctx.CacheContext() // pass true to commit the StateDB - evmResp, err = k.ApplyEvmMsg(tmpCtx, evmMsg, nil, true, evmConfig, txConfig) + evmResp, _, err = k.ApplyEvmMsg(tmpCtx, evmMsg, nil, true, evmConfig, txConfig) if err != nil { // when a transaction contains multiple msg, as long as one of the msg fails // all gas will be deducted. so is not msg.Gas() @@ -246,14 +246,14 @@ func (k *Keeper) ApplyEvmMsg(ctx sdk.Context, commit bool, evmConfig *statedb.EVMConfig, txConfig statedb.TxConfig, -) (*evm.MsgEthereumTxResponse, error) { +) (resp *evm.MsgEthereumTxResponse, evmObj *vm.EVM, err error) { var ( ret []byte // return bytes from evm execution vmErr error // vm errors do not effect consensus and are therefore not assigned to err ) stateDB := statedb.New(ctx, k, txConfig) - evmObj := k.NewEVM(ctx, msg, evmConfig, tracer, stateDB) + evmObj = k.NewEVM(ctx, msg, evmConfig, tracer, stateDB) leftoverGas := msg.Gas() @@ -272,7 +272,7 @@ func (k *Keeper) ApplyEvmMsg(ctx sdk.Context, intrinsicGas, err := k.GetEthIntrinsicGas(ctx, msg, evmConfig.ChainConfig, contractCreation) if err != nil { // should have already been checked on Ante Handler - return nil, errors.Wrap(err, "intrinsic gas failed") + return nil, evmObj, errors.Wrap(err, "intrinsic gas failed") } // Check if the provided gas in the message is enough to cover the intrinsic @@ -283,7 +283,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, errors.Wrapf( + return nil, evmObj, errors.Wrapf( core.ErrIntrinsicGas, "apply message msg.Gas = %d, intrinsic gas = %d.", leftoverGas, intrinsicGas, @@ -303,7 +303,7 @@ func (k *Keeper) ApplyEvmMsg(ctx sdk.Context, msgWei, err := ParseWeiAsMultipleOfMicronibi(msg.Value()) if err != nil { - return nil, err + return nil, evmObj, err } if contractCreation { @@ -333,7 +333,7 @@ func (k *Keeper) ApplyEvmMsg(ctx sdk.Context, // calculate gas refund if msg.Gas() < leftoverGas { - return nil, errors.Wrap(evm.ErrGasOverflow, "apply message") + return nil, evmObj, errors.Wrap(evm.ErrGasOverflow, "apply message") } // refund gas temporaryGasUsed := msg.Gas() - leftoverGas @@ -352,7 +352,7 @@ 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, fmt.Errorf("failed to commit stateDB: %w", err) + return nil, evmObj, fmt.Errorf("failed to commit stateDB: %w", err) } } @@ -361,11 +361,11 @@ func (k *Keeper) ApplyEvmMsg(ctx sdk.Context, minimumGasUsed := gasLimit.Mul(minGasMultiplier) if !minimumGasUsed.TruncateInt().IsUint64() { - return nil, errors.Wrapf(evm.ErrGasOverflow, "minimumGasUsed(%s) is not a uint64", minimumGasUsed.TruncateInt().String()) + return nil, evmObj, errors.Wrapf(evm.ErrGasOverflow, "minimumGasUsed(%s) is not a uint64", minimumGasUsed.TruncateInt().String()) } if msg.Gas() < leftoverGas { - return nil, errors.Wrapf(evm.ErrGasOverflow, "message gas limit < leftover gas (%d < %d)", msg.Gas(), leftoverGas) + return nil, evmObj, errors.Wrapf(evm.ErrGasOverflow, "message gas limit < leftover gas (%d < %d)", msg.Gas(), leftoverGas) } gasUsed := math.LegacyMaxDec(minimumGasUsed, math.LegacyNewDec(int64(temporaryGasUsed))).TruncateInt().Uint64() @@ -381,7 +381,7 @@ func (k *Keeper) ApplyEvmMsg(ctx sdk.Context, Ret: ret, Logs: evm.NewLogsFromEth(stateDB.Logs()), Hash: txConfig.TxHash.Hex(), - }, nil + }, evmObj, nil } func ParseWeiAsMultipleOfMicronibi(weiInt *big.Int) (newWeiInt *big.Int, err error) { @@ -686,7 +686,7 @@ func (k *Keeper) EmitEthereumTxEvents( // Emit typed events if !evmResp.Failed() { if recipient == nil { // contract creation - var contractAddr = crypto.CreateAddress(msg.From(), msg.Nonce()) + contractAddr := crypto.CreateAddress(msg.From(), msg.Nonce()) _ = ctx.EventManager().EmitTypedEvent(&evm.EventContractDeployed{ Sender: msg.From().Hex(), ContractAddr: contractAddr.String(), diff --git a/x/evm/keeper/msg_update_params.go b/x/evm/keeper/msg_update_params.go index 1098138a3..12dd71fe8 100644 --- a/x/evm/keeper/msg_update_params.go +++ b/x/evm/keeper/msg_update_params.go @@ -18,6 +18,9 @@ func (k *Keeper) UpdateParams( return nil, errors.Wrapf(govtypes.ErrInvalidSigner, "invalid authority, expected %s, got %s", k.authority.String(), req.Authority) } ctx := sdk.UnwrapSDKContext(goCtx) - k.SetParams(ctx, req.Params) + err = k.SetParams(ctx, req.Params) + if err != nil { + return nil, errors.Wrapf(err, "failed to set params") + } return &evm.MsgUpdateParamsResponse{}, nil } diff --git a/x/evm/keeper/vm_config.go b/x/evm/keeper/vm_config.go index 241cae816..2f8232ad6 100644 --- a/x/evm/keeper/vm_config.go +++ b/x/evm/keeper/vm_config.go @@ -39,12 +39,12 @@ func (k *Keeper) GetEVMConfig( func (k *Keeper) TxConfig( ctx sdk.Context, txHash common.Hash, ) statedb.TxConfig { - return statedb.NewTxConfig( - common.BytesToHash(ctx.HeaderHash()), // BlockHash - txHash, // TxHash - uint(k.EvmState.BlockTxIndex.GetOr(ctx, 0)), // TxIndex - uint(k.EvmState.BlockLogSize.GetOr(ctx, 0)), // LogIndex - ) + return statedb.TxConfig{ + BlockHash: common.BytesToHash(ctx.HeaderHash()), + TxHash: txHash, + TxIndex: uint(k.EvmState.BlockTxIndex.GetOr(ctx, 0)), + LogIndex: uint(k.EvmState.BlockLogSize.GetOr(ctx, 0)), + } } // VMConfig creates an EVM configuration from the debug setting and the extra diff --git a/x/evm/logs.go b/x/evm/logs.go index 21bc74db5..3b662964f 100644 --- a/x/evm/logs.go +++ b/x/evm/logs.go @@ -11,14 +11,6 @@ import ( "github.com/NibiruChain/nibiru/v2/eth" ) -// NewTransactionLogs creates a new NewTransactionLogs instance. -func NewTransactionLogs(hash gethcommon.Hash, logs []*Log) TransactionLogs { - return TransactionLogs{ - Hash: hash.String(), - Logs: logs, - } -} - // NewTransactionLogsFromEth creates a new NewTransactionLogs instance using []*ethtypes.Log. func NewTransactionLogsFromEth(hash gethcommon.Hash, ethlogs []*gethcore.Log) TransactionLogs { return TransactionLogs{ diff --git a/x/evm/logs_test.go b/x/evm/logs_test.go index 34cb80a12..66dc1105c 100644 --- a/x/evm/logs_test.go +++ b/x/evm/logs_test.go @@ -193,7 +193,10 @@ func TestConversionFunctions(t *testing.T) { conversionErr := conversionLogs.Validate() // create new transaction logs as copy of old valid one (and validate) - copyLogs := evm.NewTransactionLogs(common.BytesToHash([]byte("tx_hash")), txLogs.Logs) + copyLogs := evm.TransactionLogs{ + Hash: common.BytesToHash([]byte("tx_hash")).Hex(), + Logs: txLogs.Logs, + } copyErr := copyLogs.Validate() require.Nil(t, conversionErr) diff --git a/x/evm/precompile/errors.go b/x/evm/precompile/errors.go index f22ed9f7e..a95989b71 100644 --- a/x/evm/precompile/errors.go +++ b/x/evm/precompile/errors.go @@ -3,11 +3,23 @@ package precompile import ( "errors" "fmt" + "reflect" gethabi "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/core/vm" ) +// ErrPrecompileRun is error function intended for use in a `defer` pattern, +// which modifies the input error in the event that its value becomes non-nil. +// This creates a concise way to prepend extra information to the original error. +func ErrPrecompileRun(err error, p vm.PrecompiledContract) error { + if err != nil { + precompileType := reflect.TypeOf(p).Name() + err = fmt.Errorf("precompile error: failed to run %s: %w", precompileType, err) + } + return err +} + // Error short-hand for type validation func ErrArgTypeValidation(solidityHint string, arg any) error { return fmt.Errorf("type validation failed for (%s) argument: %s", solidityHint, arg) diff --git a/x/evm/precompile/funtoken.go b/x/evm/precompile/funtoken.go index 042544269..5c585c2e9 100644 --- a/x/evm/precompile/funtoken.go +++ b/x/evm/precompile/funtoken.go @@ -3,18 +3,18 @@ package precompile import ( "fmt" "math/big" - "reflect" "sync" "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" + auth "github.com/cosmos/cosmos-sdk/x/auth/types" bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" gethabi "github.com/ethereum/go-ethereum/accounts/abi" gethcommon "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/vm" - gethparams "github.com/ethereum/go-ethereum/params" "github.com/NibiruChain/nibiru/v2/app/keepers" + "github.com/NibiruChain/nibiru/v2/eth" "github.com/NibiruChain/nibiru/v2/x/evm" "github.com/NibiruChain/nibiru/v2/x/evm/embeds" evmkeeper "github.com/NibiruChain/nibiru/v2/x/evm/keeper" @@ -32,17 +32,13 @@ func (p precompileFunToken) Address() gethcommon.Address { return PrecompileAddr_FunToken } +func (p precompileFunToken) ABI() *gethabi.ABI { + return embeds.SmartContract_FunToken.ABI +} + // RequiredGas calculates the cost of calling the precompile in gas units. func (p precompileFunToken) RequiredGas(input []byte) (gasCost uint64) { - // Since [gethparams.TxGas] is the cost per (Ethereum) transaction that does not create - // a contract, it's value can be used to derive an appropriate value for the - // precompile call. The FunToken precompile performs 3 operations, labeled 1-3 - // below: - // 0 | Call the precompile (already counted in gas calculation) - // 1 | Send ERC20 to EVM. - // 2 | Convert ERC20 to coin - // 3 | Send coin to recipient. - return gethparams.TxGas * 2 + return RequiredGas(input, p.ABI()) } const ( @@ -55,39 +51,29 @@ type PrecompileMethod string func (p precompileFunToken) Run( evm *vm.EVM, contract *vm.Contract, readonly bool, ) (bz []byte, 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 func() { - if err != nil { - precompileType := reflect.TypeOf(p).Name() - err = fmt.Errorf("precompile error: failed to run %s: %w", precompileType, err) - } + err = ErrPrecompileRun(err, p) }() - - // 1 | Get context from StateDB - stateDB, ok := evm.StateDB.(*statedb.StateDB) - if !ok { - err = fmt.Errorf("failed to load the sdk.Context from the EVM StateDB") - return - } - ctx := stateDB.GetContext() - - method, args, err := DecomposeInput(embeds.SmartContract_FunToken.ABI, contract.Input) + start, err := OnRunStart(evm, contract, p.ABI()) if err != nil { return nil, err } + method := start.Method switch PrecompileMethod(method.Name) { case FunTokenMethod_BankSend: - bz, err = p.bankSend(ctx, contract.CallerAddress, method, args, readonly) + bz, err = p.bankSend(start, contract.CallerAddress, readonly) default: // Note that this code path should be impossible to reach since // "DecomposeInput" parses methods directly from the ABI. err = fmt.Errorf("invalid method called with name \"%s\"", method.Name) return } - - return + if err != nil { + return nil, err + } + // Dirty journal entries in `StateDB` must be committed + return bz, start.StateDB.Commit() } func PrecompileFunToken(keepers keepers.PublicKeepers) vm.PrecompiledContract { @@ -116,12 +102,11 @@ var executionGuard sync.Mutex // function bankSend(address erc20, uint256 amount, string memory to) external; // ``` func (p precompileFunToken) bankSend( - ctx sdk.Context, + start OnRunStartResult, caller gethcommon.Address, - method *gethabi.Method, - args []any, readOnly bool, ) (bz []byte, err error) { + ctx, method, args := start.Ctx, start.Method, start.Args if e := assertNotReadonlyTx(readOnly, true); e != nil { err = e return @@ -166,7 +151,7 @@ func (p precompileFunToken) bankSend( // EVM account mints FunToken.BankDenom to module account amt := math.NewIntFromBigInt(amount) - coins := sdk.NewCoins(sdk.NewCoin(funtoken.BankDenom, amt)) + coinToSend := sdk.NewCoin(funtoken.BankDenom, amt) if funtoken.IsMadeFromCoin { // If the FunToken mapping was created from a bank coin, then the EVM account // owns the ERC20 contract and was the original minter of the ERC20 tokens. @@ -178,7 +163,7 @@ func (p precompileFunToken) bankSend( return } } else { - err = p.bankKeeper.MintCoins(ctx, evm.ModuleName, coins) + err = SafeMintCoins(ctx, evm.ModuleName, coinToSend, p.bankKeeper, start.StateDB) if err != nil { return nil, fmt.Errorf("mint failed for module \"%s\" (%s): contract caller %s: %w", evm.ModuleName, evm.EVM_MODULE_ADDRESS.Hex(), caller.Hex(), err, @@ -187,7 +172,14 @@ func (p precompileFunToken) bankSend( } // Transfer the bank coin - err = p.bankKeeper.SendCoinsFromModuleToAccount(ctx, evm.ModuleName, toAddr, coins) + err = SafeSendCoinFromModuleToAccount( + ctx, + evm.ModuleName, + toAddr, + coinToSend, + p.bankKeeper, + start.StateDB, + ) if err != nil { return nil, fmt.Errorf("send failed for module \"%s\" (%s): contract caller %s: %w", evm.ModuleName, evm.EVM_MODULE_ADDRESS.Hex(), caller.Hex(), err, @@ -199,6 +191,58 @@ func (p precompileFunToken) bankSend( return method.Outputs.Pack() } +func SafeMintCoins( + ctx sdk.Context, + moduleName string, + amt sdk.Coin, + bk bankkeeper.Keeper, + db *statedb.StateDB, +) error { + err := bk.MintCoins(ctx, evm.ModuleName, sdk.NewCoins(amt)) + if err != nil { + return err + } + if amt.Denom == evm.EVMBankDenom { + evmBech32Addr := auth.NewModuleAddress(evm.ModuleName) + balAfter := bk.GetBalance(ctx, evmBech32Addr, amt.Denom).Amount.BigInt() + db.SetBalanceWei( + evm.EVM_MODULE_ADDRESS, + evm.NativeToWei(balAfter), + ) + } + + return nil +} + +func SafeSendCoinFromModuleToAccount( + ctx sdk.Context, + senderModule string, + recipientAddr sdk.AccAddress, + amt sdk.Coin, + bk bankkeeper.Keeper, + db *statedb.StateDB, +) error { + err := bk.SendCoinsFromModuleToAccount(ctx, senderModule, recipientAddr, sdk.NewCoins(amt)) + if err != nil { + return err + } + if amt.Denom == evm.EVMBankDenom { + evmBech32Addr := auth.NewModuleAddress(evm.ModuleName) + balAfterFrom := bk.GetBalance(ctx, evmBech32Addr, amt.Denom).Amount.BigInt() + db.SetBalanceWei( + evm.EVM_MODULE_ADDRESS, + evm.NativeToWei(balAfterFrom), + ) + + balAfterTo := bk.GetBalance(ctx, recipientAddr, amt.Denom).Amount.BigInt() + db.SetBalanceWei( + eth.NibiruAddrToEthAddr(recipientAddr), + evm.NativeToWei(balAfterTo), + ) + } + return nil +} + func (p precompileFunToken) decomposeBankSendArgs(args []any) ( erc20 gethcommon.Address, amount *big.Int, diff --git a/x/evm/precompile/funtoken_test.go b/x/evm/precompile/funtoken_test.go index 64be0360f..dd5176fb3 100644 --- a/x/evm/precompile/funtoken_test.go +++ b/x/evm/precompile/funtoken_test.go @@ -5,7 +5,7 @@ import ( "testing" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/ethereum/go-ethereum/common" + gethcommon "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/suite" "github.com/NibiruChain/nibiru/v2/eth" @@ -49,19 +49,19 @@ func (s *FuntokenSuite) TestFailToPackABI() { { name: "wrong type for amount", methodName: string(precompile.FunTokenMethod_BankSend), - callArgs: []any{common.HexToAddress("0x7D4B7B8CA7E1a24928Bb96D59249c7a5bd1DfBe6"), "foo", testutil.AccAddress().String()}, + callArgs: []any{gethcommon.HexToAddress("0x7D4B7B8CA7E1a24928Bb96D59249c7a5bd1DfBe6"), "foo", testutil.AccAddress().String()}, wantError: "abi: cannot use string as type ptr as argument", }, { name: "wrong type for recipient", methodName: string(precompile.FunTokenMethod_BankSend), - callArgs: []any{common.HexToAddress("0x7D4B7B8CA7E1a24928Bb96D59249c7a5bd1DfBe6"), big.NewInt(1), 111}, + callArgs: []any{gethcommon.HexToAddress("0x7D4B7B8CA7E1a24928Bb96D59249c7a5bd1DfBe6"), big.NewInt(1), 111}, wantError: "abi: cannot use int as type string as argument", }, { name: "invalid method name", methodName: "foo", - callArgs: []any{common.HexToAddress("0x7D4B7B8CA7E1a24928Bb96D59249c7a5bd1DfBe6"), big.NewInt(1), testutil.AccAddress().String()}, + callArgs: []any{gethcommon.HexToAddress("0x7D4B7B8CA7E1a24928Bb96D59249c7a5bd1DfBe6"), big.NewInt(1), testutil.AccAddress().String()}, wantError: "method 'foo' not found", }, } @@ -112,7 +112,7 @@ func (s *FuntokenSuite) TestHappyPath() { { input, err := embeds.SmartContract_ERC20Minter.ABI.Pack("mint", deps.Sender.EthAddr, big.NewInt(69_420)) s.NoError(err) - _, err = deps.EvmKeeper.CallContractWithInput( + _, _, err = deps.EvmKeeper.CallContractWithInput( deps.Ctx, deps.Sender.EthAddr, &erc20, true, input, ) s.ErrorContains(err, "Ownable: caller is not the owner") @@ -126,14 +126,18 @@ func (s *FuntokenSuite) TestHappyPath() { input, err := embeds.SmartContract_FunToken.ABI.Pack(string(precompile.FunTokenMethod_BankSend), callArgs...) s.NoError(err) - _, err = deps.EvmKeeper.CallContractWithInput( - deps.Ctx, deps.Sender.EthAddr, &precompile.PrecompileAddr_FunToken, true, input, + _, resp, err := evmtest.CallContractTx( + &deps, + precompile.PrecompileAddr_FunToken, + input, + deps.Sender, ) s.Require().NoError(err) + s.Require().Empty(resp.VmError) 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), - deps.App.BankKeeper.GetBalance(deps.Ctx, randomAcc, funtoken.BankDenom).Amount, + s.Equal(sdk.NewInt(420).String(), + deps.App.BankKeeper.GetBalance(deps.Ctx, randomAcc, funtoken.BankDenom).Amount.String(), ) } diff --git a/x/evm/precompile/oracle.go b/x/evm/precompile/oracle.go index b7d283ed7..fb0b2981b 100644 --- a/x/evm/precompile/oracle.go +++ b/x/evm/precompile/oracle.go @@ -2,18 +2,15 @@ package precompile import ( "fmt" - "reflect" 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" - gethparams "github.com/ethereum/go-ethereum/params" "github.com/NibiruChain/nibiru/v2/app/keepers" "github.com/NibiruChain/nibiru/v2/x/common/asset" "github.com/NibiruChain/nibiru/v2/x/evm/embeds" - "github.com/NibiruChain/nibiru/v2/x/evm/statedb" oraclekeeper "github.com/NibiruChain/nibiru/v2/x/oracle/keeper" ) @@ -27,45 +24,28 @@ func (p precompileOracle) Address() gethcommon.Address { } func (p precompileOracle) RequiredGas(input []byte) (gasPrice uint64) { - // Since [gethparams.TxGas] is the cost per (Ethereum) transaction that does not create - // a contract, it's value can be used to derive an appropriate value for the precompile call. - return gethparams.TxGas + return RequiredGas(input, embeds.SmartContract_Oracle.ABI) } const ( - OracleMethod_QueryExchangeRate OracleMethod = "queryExchangeRate" + OracleMethod_queryExchangeRate PrecompileMethod = "queryExchangeRate" ) -type OracleMethod string - // Run runs the precompiled contract func (p precompileOracle) Run( evm *vm.EVM, contract *vm.Contract, readonly bool, ) (bz []byte, 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 func() { - if err != nil { - precompileType := reflect.TypeOf(p).Name() - err = fmt.Errorf("precompile error: failed to run %s: %w", precompileType, err) - } + err = ErrPrecompileRun(err, p) }() - - // 1 | Get context from StateDB - stateDB, ok := evm.StateDB.(*statedb.StateDB) - if !ok { - err = fmt.Errorf("failed to load the sdk.Context from the EVM StateDB") - return - } - ctx := stateDB.GetContext() - - method, args, err := DecomposeInput(embeds.SmartContract_Oracle.ABI, contract.Input) + res, err := OnRunStart(evm, contract, embeds.SmartContract_Oracle.ABI) if err != nil { return nil, err } + method, args, ctx := res.Method, res.Args, res.Ctx - switch OracleMethod(method.Name) { - case OracleMethod_QueryExchangeRate: + switch PrecompileMethod(method.Name) { + case OracleMethod_queryExchangeRate: bz, err = p.queryExchangeRate(ctx, method, args, readonly) default: err = fmt.Errorf("invalid method called with name \"%s\"", method.Name) diff --git a/x/evm/precompile/oracle_test.go b/x/evm/precompile/oracle_test.go index 4d8d0116e..efab80118 100644 --- a/x/evm/precompile/oracle_test.go +++ b/x/evm/precompile/oracle_test.go @@ -21,13 +21,13 @@ func (s *OracleSuite) TestOracle_FailToPackABI() { }{ { name: "wrong amount of call args", - methodName: string(precompile.OracleMethod_QueryExchangeRate), + methodName: string(precompile.OracleMethod_queryExchangeRate), callArgs: []any{"nonsense", "args here", "to see if", "precompile is", "called"}, wantError: "argument count mismatch: got 5 for 1", }, { name: "wrong type for pair", - methodName: string(precompile.OracleMethod_QueryExchangeRate), + methodName: string(precompile.OracleMethod_queryExchangeRate), callArgs: []any{common.HexToAddress("0x7D4B7B8CA7E1a24928Bb96D59249c7a5bd1DfBe6")}, wantError: "abi: cannot use array as type string as argument", }, @@ -58,13 +58,13 @@ func (s *OracleSuite) TestOracle_HappyPath() { deps.App.OracleKeeper.SetPrice(deps.Ctx, "unibi:uusd", sdk.MustNewDecFromStr("0.067")) input, err := embeds.SmartContract_Oracle.ABI.Pack("queryExchangeRate", "unibi:uusd") s.NoError(err) - resp, err := deps.EvmKeeper.CallContractWithInput( + resp, _, err := deps.EvmKeeper.CallContractWithInput( deps.Ctx, deps.Sender.EthAddr, &precompile.PrecompileAddr_Oracle, true, input, ) s.NoError(err) // Check the response - out, err := embeds.SmartContract_Oracle.ABI.Unpack(string(precompile.OracleMethod_QueryExchangeRate), resp.Ret) + out, err := embeds.SmartContract_Oracle.ABI.Unpack(string(precompile.OracleMethod_queryExchangeRate), resp.Ret) s.NoError(err) // Check the response diff --git a/x/evm/precompile/precompile.go b/x/evm/precompile/precompile.go index 38a8744c1..a6bbfefc4 100644 --- a/x/evm/precompile/precompile.go +++ b/x/evm/precompile/precompile.go @@ -24,7 +24,10 @@ import ( "github.com/ethereum/go-ethereum/core/vm" gethparams "github.com/ethereum/go-ethereum/params" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/NibiruChain/nibiru/v2/app/keepers" + "github.com/NibiruChain/nibiru/v2/x/evm/statedb" ) // InitPrecompiles initializes and returns a map of precompiled contracts for the EVM. @@ -130,3 +133,82 @@ func RequiredGas(input []byte, abi *gethabi.ABI) uint64 { argsBzLen := uint64(len(input[4:])) return (costPerByte * argsBzLen) + costFlat } + +type OnRunStartResult struct { + // Args contains the decoded (ABI unpacked) arguments passed to the contract + // as input. + Args []any + + // Ctx is a cached SDK context that allows isolated state + // operations to occur that can be reverted by the EVM's [statedb.StateDB]. + Ctx sdk.Context + + // Method is the ABI method for the precompiled contract call. + Method *gethabi.Method + + StateDB *statedb.StateDB +} + +// OnRunStart prepares the execution environment for a precompiled contract call. +// It handles decoding the input data according the to contract ABI, creates an +// isolated cache context for state changes, and sets up a snapshot for potential +// EVM "reverts". +// +// Args: +// - evm: Instance of the EVM executing the contract +// - contract: Precompiled contract being called +// - abi: [gethabi.ABI] defining the contract's invokable methods. +// +// Example Usage: +// +// ```go +// func (p newPrecompile) Run( +// evm *vm.EVM, contract *vm.Contract, readonly bool +// ) (bz []byte, err error) { +// res, err := OnRunStart(evm, contract, p.ABI()) +// if err != nil { +// return nil, err +// } +// // ... +// // Use res.Ctx for state changes +// // Use res.StateDB.Commit() before any non-EVM state changes +// // to guarantee the context and [statedb.StateDB] are in sync. +// } +// ``` +func OnRunStart( + evm *vm.EVM, contract *vm.Contract, abi *gethabi.ABI, +) (res OnRunStartResult, err error) { + method, args, err := DecomposeInput(abi, contract.Input) + if err != nil { + return res, err + } + + stateDB, ok := evm.StateDB.(*statedb.StateDB) + if !ok { + err = fmt.Errorf("failed to load the sdk.Context from the EVM StateDB") + return + } + ctx := stateDB.GetContext() + if err = stateDB.Commit(); err != nil { + return res, fmt.Errorf("error committing dirty journal entries: %w", err) + } + + return OnRunStartResult{ + Args: args, + Ctx: ctx, + Method: method, + StateDB: stateDB, + }, nil +} + +var precompileMethodIsTxMap map[PrecompileMethod]bool = map[PrecompileMethod]bool{ + WasmMethod_execute: true, + WasmMethod_instantiate: true, + WasmMethod_executeMulti: true, + WasmMethod_query: false, + WasmMethod_queryRaw: false, + + FunTokenMethod_BankSend: true, + + OracleMethod_queryExchangeRate: false, +} diff --git a/x/evm/precompile/test/export.go b/x/evm/precompile/test/export.go new file mode 100644 index 000000000..966dd3359 --- /dev/null +++ b/x/evm/precompile/test/export.go @@ -0,0 +1,316 @@ +package test + +import ( + "encoding/json" + "os" + "os/exec" + "path" + "strings" + + wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" + wasm "github.com/CosmWasm/wasmd/x/wasm/types" + "github.com/ethereum/go-ethereum/core/vm" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/suite" + + "github.com/NibiruChain/nibiru/v2/app" + "github.com/NibiruChain/nibiru/v2/x/evm/embeds" + "github.com/NibiruChain/nibiru/v2/x/evm/evmtest" + "github.com/NibiruChain/nibiru/v2/x/evm/precompile" + "github.com/NibiruChain/nibiru/v2/x/evm/statedb" +) + +// 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) ( + contracts []sdk.AccAddress, +) { + wasmCodes := DeployWasmBytecode(s, deps.Ctx, deps.Sender.NibiruAddr, deps.App) + + otherArgs := []struct { + InstMsg []byte + Label string + }{ + { + InstMsg: []byte("{}"), + Label: "https://github.com/NibiruChain/nibiru-wasm/blob/main/contracts/nibi-stargate/src/contract.rs", + }, + { + InstMsg: []byte(`{"count": 0}`), + Label: "https://github.com/NibiruChain/nibiru-wasm/tree/ec3ab9f09587a11fbdfbd4021c7617eca3912044/contracts/00-hello-world-counter", + }, + } + + for wasmCodeIdx, wasmCode := range wasmCodes { + s.T().Logf("Instantiate using Wasm precompile: %s", wasmCode.binPath) + codeId := wasmCode.codeId + + m := wasm.MsgInstantiateContract{ + Admin: "", + CodeID: codeId, + Label: otherArgs[wasmCodeIdx].Label, + Msg: otherArgs[wasmCodeIdx].InstMsg, + Funds: []sdk.Coin{}, + } + + msgArgsBz, err := json.Marshal(m.Msg) + s.NoError(err) + + var funds []precompile.WasmBankCoin + fundsJson, err := m.Funds.MarshalJSON() + s.NoErrorf(err, "fundsJson: %s", fundsJson) + err = json.Unmarshal(fundsJson, &funds) + s.Require().NoError(err) + + callArgs := []any{m.Admin, m.CodeID, msgArgsBz, m.Label, funds} + input, err := embeds.SmartContract_Wasm.ABI.Pack( + string(precompile.WasmMethod_instantiate), + callArgs..., + ) + s.Require().NoError(err) + + ethTxResp, evmObj, err := deps.EvmKeeper.CallContractWithInput( + deps.Ctx, deps.Sender.EthAddr, &precompile.PrecompileAddr_Wasm, true, input, + ) + s.Require().NoError(err) + s.Require().NotEmpty(ethTxResp.Ret) + + // Finalize transaction + err = evmObj.StateDB.(*statedb.StateDB).Commit() + s.Require().NoError(err) + + s.T().Log("Parse the response contract addr and response bytes") + var contractAddrStr string + var data []byte + err = embeds.SmartContract_Wasm.ABI.UnpackIntoInterface( + &[]any{&contractAddrStr, &data}, + string(precompile.WasmMethod_instantiate), + ethTxResp.Ret, + ) + s.Require().NoError(err) + contractAddr, err := sdk.AccAddressFromBech32(contractAddrStr) + s.NoError(err) + contracts = append(contracts, contractAddr) + } + + return contracts +} + +// DeployWasmBytecode is a setup function that stores all Wasm bytecode used in +// the test suite. +func DeployWasmBytecode( + s *suite.Suite, + ctx sdk.Context, + sender sdk.AccAddress, + nibiru *app.NibiruApp, +) (codeIds []struct { + codeId uint64 + binPath string +}, +) { + // rootPath, _ := exec.Command("go list -m -f {{.Dir}}").Output() + // Run: go list -m -f {{.Dir}} + // This returns the path to the root of the project. + rootPathBz, err := exec.Command("go", "list", "-m", "-f", "{{.Dir}}").Output() + s.Require().NoError(err) + rootPath := strings.Trim(string(rootPathBz), "\n") + for _, pathToWasmBin := range []string{ + // nibi_stargate.wasm is a compiled version of: + // https://github.com/NibiruChain/nibiru-wasm/blob/main/contracts/nibi-stargate/src/contract.rs + "x/tokenfactory/fixture/nibi_stargate.wasm", + + // hello_world_counter.wasm is a compiled version of: + // https://github.com/NibiruChain/nibiru-wasm/tree/ec3ab9f09587a11fbdfbd4021c7617eca3912044/contracts/00-hello-world-counter + "x/evm/precompile/hello_world_counter.wasm", + + // Add other wasm bytecode here if needed... + } { + pathToWasmBin = path.Join(string(rootPath), pathToWasmBin) + wasmBytecode, err := os.ReadFile(pathToWasmBin) + s.Require().NoErrorf( + err, + "rootPath %s, pathToWasmBin %s", rootPath, pathToWasmBin, + ) + + // The "Create" fn is private on the nibiru.WasmKeeper. By placing it as the + // decorated keeper in PermissionedKeeper type, we can access "Create" as a + // public fn. + wasmPermissionedKeeper := wasmkeeper.NewDefaultPermissionKeeper(nibiru.WasmKeeper) + instantiateAccess := &wasm.AccessConfig{ + Permission: wasm.AccessTypeEverybody, + } + codeId, _, err := wasmPermissionedKeeper.Create( + ctx, sender, wasmBytecode, instantiateAccess, + ) + s.Require().NoError(err) + codeIds = append(codeIds, struct { + codeId uint64 + binPath string + }{codeId, pathToWasmBin}) + } + + return codeIds +} + +// From IWasm.query of Wasm.sol: +// +// ```solidity +// function query( +// string memory contractAddr, +// bytes memory req +// ) external view returns (bytes memory response); +// ``` +func AssertWasmCounterState( + s *suite.Suite, + deps evmtest.TestDeps, + wasmContract sdk.AccAddress, + wantCount int64, +) (evmObj *vm.EVM) { + msgArgsBz := []byte(` + { + "count": {} + } + `) + + callArgs := []any{ + // string memory contractAddr + wasmContract.String(), + // bytes memory req + msgArgsBz, + } + input, err := embeds.SmartContract_Wasm.ABI.Pack( + string(precompile.WasmMethod_query), + callArgs..., + ) + s.Require().NoError(err) + + ethTxResp, evmObj, err := deps.EvmKeeper.CallContractWithInput( + deps.Ctx, deps.Sender.EthAddr, &precompile.PrecompileAddr_Wasm, true, input, + ) + 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. + // If there were two or more return values, we'd use + // &[]any{...} + &queryResp, + string(precompile.WasmMethod_query), + ethTxResp.Ret, + ) + 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 + err = json.Unmarshal(queryResp, &wasmMsg) + s.NoError(err) + s.NoError(wasmMsg.ValidateBasic()) + var typedResp QueryMsgCountResp + err = json.Unmarshal(wasmMsg, &typedResp) + s.NoError(err) + + s.EqualValues(wantCount, typedResp.Count) + s.EqualValues(deps.Sender.NibiruAddr.String(), typedResp.Owner) + return evmObj +} + +// Result of QueryMsg::Count from the [hello_world_counter] Wasm contract: +// +// ```rust +// #[cw_serde] +// pub struct State { +// pub count: i64, +// pub owner: Addr, +// } +// ``` +// +// [hello_world_counter]: https://github.com/NibiruChain/nibiru-wasm/tree/ec3ab9f09587a11fbdfbd4021c7617eca3912044/contracts/00-hello-world-counter +type QueryMsgCountResp struct { + Count int64 `json:"count"` + Owner string `json:"owner"` +} + +// From evm/embeds/contracts/Wasm.sol: +// +// ```solidity +// struct WasmExecuteMsg { +// string contractAddr; +// bytes msgArgs; +// BankCoin[] funds; +// } +// +// /// @notice Identical to "execute", except for multiple contract calls. +// function executeMulti( +// WasmExecuteMsg[] memory executeMsgs +// ) payable external returns (bytes[] memory responses); +// ``` +// +// The increment call corresponds to the ExecuteMsg from +// the [hello_world_counter] Wasm contract: +// +// ```rust +// #[cw_serde] +// pub enum ExecuteMsg { +// Increment {}, // Increase count by 1 +// Reset { count: i64 }, // Reset to any i64 value +// } +// ``` +// +// [hello_world_counter]: https://github.com/NibiruChain/nibiru-wasm/tree/ec3ab9f09587a11fbdfbd4021c7617eca3912044/contracts/00-hello-world-counter +func IncrementWasmCounterWithExecuteMulti( + s *suite.Suite, + deps *evmtest.TestDeps, + wasmContract sdk.AccAddress, + times uint, +) (evmObj *vm.EVM) { + msgArgsBz := []byte(` + { + "increment": {} + } + `) + + // Parse funds argument. + var funds []precompile.WasmBankCoin // blank funds + 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) + + // The "times" arg determines the number of messages in the executeMsgs slice + executeMsgs := []struct { + 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]) + } + } + s.Require().Len(executeMsgs, int(times)) // sanity check assertion + + callArgs := []any{ + executeMsgs, + } + input, err := embeds.SmartContract_Wasm.ABI.Pack( + string(precompile.WasmMethod_executeMulti), + callArgs..., + ) + s.Require().NoError(err) + + ethTxResp, evmObj, err := deps.EvmKeeper.CallContractWithInput( + deps.Ctx, deps.Sender.EthAddr, &precompile.PrecompileAddr_Wasm, true, input, + ) + s.Require().NoError(err) + s.Require().NotEmpty(ethTxResp.Ret) + return evmObj +} diff --git a/x/evm/precompile/wasm.go b/x/evm/precompile/wasm.go index 905e512b6..095f449e8 100644 --- a/x/evm/precompile/wasm.go +++ b/x/evm/precompile/wasm.go @@ -2,7 +2,6 @@ package precompile import ( "fmt" - "reflect" sdk "github.com/cosmos/cosmos-sdk/types" @@ -14,8 +13,6 @@ import ( 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/statedb" ) var _ vm.PrecompiledContract = (*precompileWasm)(nil) @@ -32,89 +29,75 @@ const ( WasmMethod_queryRaw PrecompileMethod = "queryRaw" ) -var precompileMethodIsTxMap map[PrecompileMethod]bool = map[PrecompileMethod]bool{ - WasmMethod_execute: true, - WasmMethod_instantiate: true, - WasmMethod_executeMulti: true, - WasmMethod_query: false, - WasmMethod_queryRaw: false, - - FunTokenMethod_BankSend: true, -} - -// Wasm: A struct embedding keepers for read and write operations in Wasm, such -// as execute, query, and instantiate. -type Wasm struct { - *wasmkeeper.PermissionedKeeper - wasmkeeper.Keeper -} - -func PrecompileWasm(keepers keepers.PublicKeepers) vm.PrecompiledContract { - return precompileWasm{ - Wasm: Wasm{ - wasmkeeper.NewDefaultPermissionKeeper(keepers.WasmKeeper), - keepers.WasmKeeper, - }, - } -} - -type precompileWasm struct { - Wasm Wasm -} - -func (p precompileWasm) Address() gethcommon.Address { - return PrecompileAddr_Wasm -} - -// RequiredGas calculates the cost of calling the precompile in gas units. -func (p precompileWasm) RequiredGas(input []byte) (gasCost uint64) { - return RequiredGas(input, embeds.SmartContract_Wasm.ABI) -} - // Run runs the precompiled contract func (p precompileWasm) Run( evm *vm.EVM, contract *vm.Contract, readonly bool, ) (bz []byte, 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 func() { - if err != nil { - precompileType := reflect.TypeOf(p).Name() - err = fmt.Errorf("precompile error: failed to run %s: %w", precompileType, err) - } + err = ErrPrecompileRun(err, p) }() - - method, args, err := DecomposeInput(embeds.SmartContract_Wasm.ABI, contract.Input) + start, err := OnRunStart(evm, contract, p.ABI()) if err != nil { return nil, err } - - stateDB, ok := evm.StateDB.(*statedb.StateDB) - if !ok { - err = fmt.Errorf("failed to load the sdk.Context from the EVM StateDB") - return - } - ctx := stateDB.GetContext() + method := start.Method switch PrecompileMethod(method.Name) { case WasmMethod_execute: - bz, err = p.execute(ctx, contract.CallerAddress, method, args, readonly) + bz, err = p.execute(start, contract.CallerAddress, readonly) case WasmMethod_query: - bz, err = p.query(ctx, method, args, contract) + bz, err = p.query(start, contract) case WasmMethod_instantiate: - bz, err = p.instantiate(ctx, contract.CallerAddress, method, args, readonly) + bz, err = p.instantiate(start, contract.CallerAddress, readonly) case WasmMethod_executeMulti: - bz, err = p.executeMulti(ctx, contract.CallerAddress, method, args, readonly) + bz, err = p.executeMulti(start, contract.CallerAddress, readonly) case WasmMethod_queryRaw: - bz, err = p.queryRaw(ctx, method, args, contract) + bz, err = p.queryRaw(start, contract) default: // Note that this code path should be impossible to reach since // "DecomposeInput" parses methods directly from the ABI. err = fmt.Errorf("invalid method called with name \"%s\"", method.Name) return } + if err != nil { + return nil, err + } + + // Dirty journal entries in `StateDB` must be committed + return bz, start.StateDB.Commit() +} + +type precompileWasm struct { + Wasm Wasm +} + +func (p precompileWasm) Address() gethcommon.Address { + return PrecompileAddr_Wasm +} + +func (p precompileWasm) ABI() *gethabi.ABI { + return embeds.SmartContract_Wasm.ABI +} - return +// RequiredGas calculates the cost of calling the precompile in gas units. +func (p precompileWasm) RequiredGas(input []byte) (gasCost uint64) { + return RequiredGas(input, p.ABI()) +} + +// Wasm: A struct embedding keepers for read and write operations in Wasm, such +// as execute, query, and instantiate. +type Wasm struct { + *wasmkeeper.PermissionedKeeper + wasmkeeper.Keeper +} + +func PrecompileWasm(keepers keepers.PublicKeepers) vm.PrecompiledContract { + return precompileWasm{ + Wasm: Wasm{ + wasmkeeper.NewDefaultPermissionKeeper(keepers.WasmKeeper), + keepers.WasmKeeper, + }, + } } // execute invokes a Wasm contract's "ExecuteMsg", which corresponds to @@ -137,12 +120,11 @@ func (p precompileWasm) Run( // - funds: Optional funds to supply during the execute call. It's // uncommon to use this field, so you'll pass an empty array most of the time. func (p precompileWasm) execute( - ctx sdk.Context, + start OnRunStartResult, caller gethcommon.Address, - method *gethabi.Method, - args []any, readOnly bool, ) (bz []byte, err error) { + method, args, ctx := start.Method, start.Args, start.Ctx defer func() { if err != nil { err = ErrMethodCalled(method, err) @@ -178,11 +160,10 @@ func (p precompileWasm) execute( // ) external view returns (bytes memory response); // ``` func (p precompileWasm) query( - ctx sdk.Context, - method *gethabi.Method, - args []any, + start OnRunStartResult, contract *vm.Contract, ) (bz []byte, err error) { + method, args, ctx := start.Method, start.Args, start.Ctx defer func() { if err != nil { err = ErrMethodCalled(method, err) @@ -223,12 +204,11 @@ func (p precompileWasm) query( // ) payable external returns (string memory contractAddr, bytes memory data); // ``` func (p precompileWasm) instantiate( - ctx sdk.Context, + start OnRunStartResult, caller gethcommon.Address, - method *gethabi.Method, - args []any, readOnly bool, ) (bz []byte, err error) { + method, args, ctx := start.Method, start.Args, start.Ctx defer func() { if err != nil { err = ErrMethodCalled(method, err) @@ -275,12 +255,11 @@ func (p precompileWasm) instantiate( // ) payable external returns (bytes[] memory responses); // ``` func (p precompileWasm) executeMulti( - ctx sdk.Context, + start OnRunStartResult, caller gethcommon.Address, - method *gethabi.Method, - args []any, readOnly bool, ) (bz []byte, err error) { + method, args, ctx := start.Method, start.Args, start.Ctx defer func() { if err != nil { err = ErrMethodCalled(method, err) @@ -364,11 +343,10 @@ func (p precompileWasm) executeMulti( // - bz: The encoded raw data stored at the queried key // - err: Any error that occurred during the query func (p precompileWasm) queryRaw( - ctx sdk.Context, - method *gethabi.Method, - args []any, + start OnRunStartResult, contract *vm.Contract, ) (bz []byte, err error) { + method, args, ctx := start.Method, start.Args, start.Ctx defer func() { if err != nil { err = ErrMethodCalled(method, err) diff --git a/x/evm/precompile/wasm_test.go b/x/evm/precompile/wasm_test.go index b492497cd..21bddc301 100644 --- a/x/evm/precompile/wasm_test.go +++ b/x/evm/precompile/wasm_test.go @@ -4,17 +4,15 @@ import ( "encoding/json" "fmt" "math/big" - "os" - wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" wasm "github.com/CosmWasm/wasmd/x/wasm/types" - "github.com/NibiruChain/nibiru/v2/app" "github.com/NibiruChain/nibiru/v2/x/common/testutil" "github.com/NibiruChain/nibiru/v2/x/common/testutil/testapp" "github.com/NibiruChain/nibiru/v2/x/evm/embeds" "github.com/NibiruChain/nibiru/v2/x/evm/evmtest" "github.com/NibiruChain/nibiru/v2/x/evm/precompile" + "github.com/NibiruChain/nibiru/v2/x/evm/precompile/test" tokenfactory "github.com/NibiruChain/nibiru/v2/x/tokenfactory/types" sdk "github.com/cosmos/cosmos-sdk/types" @@ -25,127 +23,9 @@ type WasmSuite struct { suite.Suite } -// 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) ( - contracts []sdk.AccAddress, -) { - wasmCodes := DeployWasmBytecode(s, deps.Ctx, deps.Sender.NibiruAddr, deps.App) - - otherArgs := []struct { - InstMsg []byte - Label string - }{ - { - InstMsg: []byte("{}"), - Label: "https://github.com/NibiruChain/nibiru-wasm/blob/main/contracts/nibi-stargate/src/contract.rs", - }, - { - InstMsg: []byte(`{"count": 0}`), - Label: "https://github.com/NibiruChain/nibiru-wasm/tree/ec3ab9f09587a11fbdfbd4021c7617eca3912044/contracts/00-hello-world-counter", - }, - } - - for wasmCodeIdx, wasmCode := range wasmCodes { - s.T().Logf("Instantiate using Wasm precompile: %s", wasmCode.binPath) - codeId := wasmCode.codeId - - m := wasm.MsgInstantiateContract{ - Admin: "", - CodeID: codeId, - Label: otherArgs[wasmCodeIdx].Label, - Msg: otherArgs[wasmCodeIdx].InstMsg, - Funds: []sdk.Coin{}, - } - - msgArgsBz, err := json.Marshal(m.Msg) - s.NoError(err) - - var funds []precompile.WasmBankCoin - fundsJson, err := m.Funds.MarshalJSON() - s.NoErrorf(err, "fundsJson: %s", fundsJson) - err = json.Unmarshal(fundsJson, &funds) - s.Require().NoError(err) - - callArgs := []any{m.Admin, m.CodeID, msgArgsBz, m.Label, funds} - input, err := embeds.SmartContract_Wasm.ABI.Pack( - string(precompile.WasmMethod_instantiate), - callArgs..., - ) - s.Require().NoError(err) - - ethTxResp, err := deps.EvmKeeper.CallContractWithInput( - deps.Ctx, deps.Sender.EthAddr, &precompile.PrecompileAddr_Wasm, true, input, - ) - s.Require().NoError(err) - s.Require().NotEmpty(ethTxResp.Ret) - - s.T().Log("Parse the response contract addr and response bytes") - var contractAddrStr string - var data []byte - err = embeds.SmartContract_Wasm.ABI.UnpackIntoInterface( - &[]any{&contractAddrStr, &data}, - string(precompile.WasmMethod_instantiate), - ethTxResp.Ret, - ) - s.Require().NoError(err) - contractAddr, err := sdk.AccAddressFromBech32(contractAddrStr) - s.NoError(err) - contracts = append(contracts, contractAddr) - } - - return contracts -} - -// DeployWasmBytecode is a setup function that stores all Wasm bytecode used in -// the test suite. -func DeployWasmBytecode( - s *suite.Suite, - ctx sdk.Context, - sender sdk.AccAddress, - nibiru *app.NibiruApp, -) (codeIds []struct { - codeId uint64 - binPath string -}, -) { - for _, pathToWasmBin := range []string{ - // nibi_stargate.wasm is a compiled version of: - // https://github.com/NibiruChain/nibiru-wasm/blob/main/contracts/nibi-stargate/src/contract.rs - "../../tokenfactory/fixture/nibi_stargate.wasm", - - // hello_world_counter.wasm is a compiled version of: - // https://github.com/NibiruChain/nibiru-wasm/tree/ec3ab9f09587a11fbdfbd4021c7617eca3912044/contracts/00-hello-world-counter - "./hello_world_counter.wasm", - - // Add other wasm bytecode here if needed... - } { - wasmBytecode, err := os.ReadFile(pathToWasmBin) - s.Require().NoError(err) - - // The "Create" fn is private on the nibiru.WasmKeeper. By placing it as the - // decorated keeper in PermissionedKeeper type, we can access "Create" as a - // public fn. - wasmPermissionedKeeper := wasmkeeper.NewDefaultPermissionKeeper(nibiru.WasmKeeper) - instantiateAccess := &wasm.AccessConfig{ - Permission: wasm.AccessTypeEverybody, - } - codeId, _, err := wasmPermissionedKeeper.Create( - ctx, sender, wasmBytecode, instantiateAccess, - ) - s.Require().NoError(err) - codeIds = append(codeIds, struct { - codeId uint64 - binPath string - }{codeId, pathToWasmBin}) - } - - return codeIds -} - func (s *WasmSuite) TestExecuteHappy() { deps := evmtest.NewTestDeps() - wasmContracts := SetupWasmContracts(&deps, &s.Suite) + wasmContracts := test.SetupWasmContracts(&deps, &s.Suite) wasmContract := wasmContracts[0] // nibi_stargate.wasm s.T().Log("Execute: create denom") @@ -173,7 +53,7 @@ func (s *WasmSuite) TestExecuteHappy() { ) s.Require().NoError(err) - ethTxResp, err := deps.EvmKeeper.CallContractWithInput( + ethTxResp, _, err := deps.EvmKeeper.CallContractWithInput( deps.Ctx, deps.Sender.EthAddr, &precompile.PrecompileAddr_Wasm, true, input, ) s.Require().NoError(err) @@ -202,7 +82,7 @@ func (s *WasmSuite) TestExecuteHappy() { callArgs..., ) s.Require().NoError(err) - ethTxResp, err = deps.EvmKeeper.CallContractWithInput( + ethTxResp, _, err = deps.EvmKeeper.CallContractWithInput( deps.Ctx, deps.Sender.EthAddr, &precompile.PrecompileAddr_Wasm, true, input, ) s.Require().NoError(err) @@ -212,178 +92,27 @@ func (s *WasmSuite) TestExecuteHappy() { ) } -// Result of QueryMsg::Count from the [hello_world_counter] Wasm contract: -// -// ```rust -// #[cw_serde] -// pub struct State { -// pub count: i64, -// pub owner: Addr, -// } -// ``` -// -// [hello_world_counter]: https://github.com/NibiruChain/nibiru-wasm/tree/ec3ab9f09587a11fbdfbd4021c7617eca3912044/contracts/00-hello-world-counter -type QueryMsgCountResp struct { - Count int64 `json:"count"` - Owner string `json:"owner"` -} - func (s *WasmSuite) TestExecuteMultiHappy() { deps := evmtest.NewTestDeps() - wasmContracts := SetupWasmContracts(&deps, &s.Suite) + wasmContracts := test.SetupWasmContracts(&deps, &s.Suite) wasmContract := wasmContracts[1] // hello_world_counter.wasm - s.assertWasmCounterState(deps, wasmContract, 0) // count = 0 - s.incrementWasmCounterWithExecuteMulti(&deps, wasmContract, 2) // count += 2 - s.assertWasmCounterState(deps, wasmContract, 2) // count = 2 + // count = 0 + test.AssertWasmCounterState(&s.Suite, deps, wasmContract, 0) + // count += 2 + test.IncrementWasmCounterWithExecuteMulti( + &s.Suite, &deps, wasmContract, 2) + // count = 2 + test.AssertWasmCounterState(&s.Suite, deps, wasmContract, 2) s.assertWasmCounterStateRaw(deps, wasmContract, 2) - s.incrementWasmCounterWithExecuteMulti(&deps, wasmContract, 67) // count += 67 - s.assertWasmCounterState(deps, wasmContract, 69) // count = 69 + // count += 67 + test.IncrementWasmCounterWithExecuteMulti( + &s.Suite, &deps, wasmContract, 67) + // count = 69 + test.AssertWasmCounterState(&s.Suite, deps, wasmContract, 69) s.assertWasmCounterStateRaw(deps, wasmContract, 69) } -// From IWasm.query of Wasm.sol: -// -// ```solidity -// function query( -// string memory contractAddr, -// bytes memory req -// ) external view returns (bytes memory response); -// ``` -func (s *WasmSuite) assertWasmCounterState( - deps evmtest.TestDeps, - wasmContract sdk.AccAddress, - wantCount int64, -) { - msgArgsBz := []byte(` - { - "count": {} - } - `) - - callArgs := []any{ - // string memory contractAddr - wasmContract.String(), - // bytes memory req - msgArgsBz, - } - input, err := embeds.SmartContract_Wasm.ABI.Pack( - string(precompile.WasmMethod_query), - callArgs..., - ) - s.Require().NoError(err) - - ethTxResp, err := deps.EvmKeeper.CallContractWithInput( - deps.Ctx, deps.Sender.EthAddr, &precompile.PrecompileAddr_Wasm, true, input, - ) - 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. - // If there were two or more return values, we'd use - // &[]any{...} - &queryResp, - string(precompile.WasmMethod_query), - ethTxResp.Ret, - ) - 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 - err = json.Unmarshal(queryResp, &wasmMsg) - s.NoError(err) - s.NoError(wasmMsg.ValidateBasic()) - var typedResp QueryMsgCountResp - err = json.Unmarshal(wasmMsg, &typedResp) - s.NoError(err) - - s.EqualValues(wantCount, typedResp.Count) - s.EqualValues(deps.Sender.NibiruAddr.String(), typedResp.Owner) -} - -// From evm/embeds/contracts/Wasm.sol: -// -// ```solidity -// struct WasmExecuteMsg { -// string contractAddr; -// bytes msgArgs; -// BankCoin[] funds; -// } -// -// /// @notice Identical to "execute", except for multiple contract calls. -// function executeMulti( -// WasmExecuteMsg[] memory executeMsgs -// ) payable external returns (bytes[] memory responses); -// ``` -// -// The increment call corresponds to the ExecuteMsg from -// the [hello_world_counter] Wasm contract: -// -// ```rust -// #[cw_serde] -// pub enum ExecuteMsg { -// Increment {}, // Increase count by 1 -// Reset { count: i64 }, // Reset to any i64 value -// } -// ``` -// -// [hello_world_counter]: https://github.com/NibiruChain/nibiru-wasm/tree/ec3ab9f09587a11fbdfbd4021c7617eca3912044/contracts/00-hello-world-counter -func (s *WasmSuite) incrementWasmCounterWithExecuteMulti( - deps *evmtest.TestDeps, - wasmContract sdk.AccAddress, - times uint, -) { - msgArgsBz := []byte(` - { - "increment": {} - } - `) - - // Parse funds argument. - var funds []precompile.WasmBankCoin // blank funds - 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) - - // The "times" arg determines the number of messages in the executeMsgs slice - executeMsgs := []struct { - 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]) - } - } - s.Require().Len(executeMsgs, int(times)) // sanity check assertion - - callArgs := []any{ - executeMsgs, - } - input, err := embeds.SmartContract_Wasm.ABI.Pack( - string(precompile.WasmMethod_executeMulti), - callArgs..., - ) - s.Require().NoError(err) - - ethTxResp, err := deps.EvmKeeper.CallContractWithInput( - deps.Ctx, deps.Sender.EthAddr, &precompile.PrecompileAddr_Wasm, true, input, - ) - s.Require().NoError(err) - s.Require().NotEmpty(ethTxResp.Ret) -} - // From IWasm.query of Wasm.sol: // // ```solidity @@ -408,7 +137,7 @@ func (s *WasmSuite) assertWasmCounterStateRaw( ) s.Require().NoError(err) - ethTxResp, err := deps.EvmKeeper.CallContractWithInput( + ethTxResp, _, err := deps.EvmKeeper.CallContractWithInput( deps.Ctx, deps.Sender.EthAddr, &precompile.PrecompileAddr_Wasm, true, input, ) s.Require().NoError(err) @@ -431,7 +160,7 @@ func (s *WasmSuite) assertWasmCounterStateRaw( s.T().Logf("wasmMsg: %s", wasmMsg) s.NoError(wasmMsg.ValidateBasic()) - var typedResp QueryMsgCountResp + var typedResp test.QueryMsgCountResp s.NoError(json.Unmarshal(wasmMsg, &typedResp)) s.EqualValues(wantCount, typedResp.Count) s.EqualValues(deps.Sender.NibiruAddr.String(), typedResp.Owner) @@ -578,11 +307,10 @@ func (s *WasmSuite) TestSadArgsExecute() { ) s.Require().NoError(err) - ethTxResp, err := deps.EvmKeeper.CallContractWithInput( + ethTxResp, _, err := deps.EvmKeeper.CallContractWithInput( deps.Ctx, deps.Sender.EthAddr, &precompile.PrecompileAddr_Wasm, true, input, ) - s.ErrorContains(err, tc.wantError) - s.Require().Nil(ethTxResp) + s.Require().ErrorContains(err, tc.wantError, "ethTxResp %v", ethTxResp) }) } } diff --git a/x/evm/statedb/config.go b/x/evm/statedb/config.go index c75cdc179..887f591c5 100644 --- a/x/evm/statedb/config.go +++ b/x/evm/statedb/config.go @@ -18,21 +18,11 @@ type TxConfig struct { LogIndex uint // the index of next log within current block } -// NewTxConfig returns a TxConfig -func NewTxConfig(bhash, thash gethcommon.Hash, txIndex, logIndex uint) TxConfig { - return TxConfig{ - BlockHash: bhash, - TxHash: thash, - TxIndex: txIndex, - LogIndex: logIndex, - } -} - // NewEmptyTxConfig construct an empty TxConfig, // used in context where there's no transaction, e.g. `eth_call`/`eth_estimateGas`. -func NewEmptyTxConfig(bhash gethcommon.Hash) TxConfig { +func NewEmptyTxConfig(blockHash gethcommon.Hash) TxConfig { return TxConfig{ - BlockHash: bhash, + BlockHash: blockHash, TxHash: gethcommon.Hash{}, TxIndex: 0, LogIndex: 0, diff --git a/x/evm/statedb/interfaces.go b/x/evm/statedb/interfaces.go index 4ef8b2862..a4c1c3b59 100644 --- a/x/evm/statedb/interfaces.go +++ b/x/evm/statedb/interfaces.go @@ -14,7 +14,7 @@ import ( // stateful precompiled contracts. type ExtStateDB interface { vm.StateDB - AppendJournalEntry(JournalEntry) + AppendJournalEntry(JournalChange) } // Keeper provide underlying storage of StateDB diff --git a/x/evm/statedb/journal.go b/x/evm/statedb/journal.go index 14bb7b1df..ac041b617 100644 --- a/x/evm/statedb/journal.go +++ b/x/evm/statedb/journal.go @@ -24,9 +24,9 @@ import ( "github.com/ethereum/go-ethereum/common" ) -// JournalEntry is a modification entry in the state change journal that can be -// Reverted on demand. -type JournalEntry interface { +// JournalChange, also called a "journal entry", is a modification entry in the +// state change journal that can be reverted on demand. +type JournalChange interface { // Revert undoes the changes introduced by this journal entry. Revert(*StateDB) @@ -38,7 +38,7 @@ type JournalEntry interface { // commit. These are tracked to be able to be reverted in the case of an execution // exception or request for reversal. type journal struct { - entries []JournalEntry // Current changes tracked by the journal + entries []JournalChange // Current changes tracked by the journal dirties map[common.Address]int // Dirty accounts and the number of changes } @@ -62,7 +62,7 @@ func (j *journal) sortedDirties() []common.Address { } // append inserts a new modification entry to the end of the change journal. -func (j *journal) append(entry JournalEntry) { +func (j *journal) append(entry JournalChange) { j.entries = append(j.entries, entry) if addr := entry.Dirtied(); addr != nil { j.dirties[*addr]++ @@ -86,58 +86,40 @@ func (j *journal) Revert(statedb *StateDB, snapshot int) { j.entries = j.entries[:snapshot] } -// length returns the current number of entries in the journal. -func (j *journal) length() int { +// Length returns the current number of entries in the journal. +func (j *journal) Length() int { return len(j.entries) } -type ( - // Changes to the account trie. - createObjectChange struct { - account *common.Address - } - resetObjectChange struct { - prev *stateObject - } - suicideChange struct { - account *common.Address - prev bool // whether account had already suicided - prevbalance *big.Int +// DirtiesCount is a test helper to inspect how many entries in the journal are +// still dirty (uncommitted). After calling [StateDB.Commit], this function should +// return zero. +func (s *StateDB) DirtiesCount() int { + dirtiesCount := 0 + for _, dirtyCount := range s.Journal.dirties { + dirtiesCount += dirtyCount } + return dirtiesCount +} - // Changes to individual accounts. - balanceChange struct { - account *common.Address - prev *big.Int - } - nonceChange struct { - account *common.Address - prev uint64 - } - storageChange struct { - account *common.Address - key, prevalue common.Hash - } - codeChange struct { - account *common.Address - prevcode, prevhash []byte - } +func (s *StateDB) Dirties() map[common.Address]int { + return s.Journal.dirties +} - // Changes to other state values. - refundChange struct { - prev uint64 - } - addLogChange struct{} +func (s *StateDB) Entries() []JournalChange { + return s.Journal.entries +} - // Changes to the access list - accessListAddAccountChange struct { - address *common.Address - } - accessListAddSlotChange struct { - address *common.Address - slot *common.Hash - } -) +// ------------------------------------------------------ +// createObjectChange + +// createObjectChange: [JournalChange] implementation for when +// a new account (called an "object" in this context) is created in state. +type createObjectChange struct { + account *common.Address +} + +var _ JournalChange = createObjectChange{} func (ch createObjectChange) Revert(s *StateDB) { delete(s.stateObjects, *ch.account) @@ -147,6 +129,18 @@ func (ch createObjectChange) Dirtied() *common.Address { return ch.account } +// ------------------------------------------------------ +// resetObjectChange + +// resetObjectChange: [JournalChange] for an account that needs its +// original state reset. This is used when an account's state is being replaced +// and we need to revert to the previous version. +type resetObjectChange struct { + prev *stateObject +} + +var _ JournalChange = resetObjectChange{} + func (ch resetObjectChange) Revert(s *StateDB) { s.setStateObject(ch.prev) } @@ -155,10 +149,21 @@ func (ch resetObjectChange) Dirtied() *common.Address { return nil } +// ------------------------------------------------------ +// suicideChange + +type suicideChange struct { + account *common.Address + prev bool // whether account had already suicided + prevbalance *big.Int +} + +var _ JournalChange = suicideChange{} + func (ch suicideChange) Revert(s *StateDB) { obj := s.getStateObject(*ch.account) if obj != nil { - obj.suicided = ch.prev + obj.Suicided = ch.prev obj.setBalance(ch.prevbalance) } } @@ -167,14 +172,37 @@ func (ch suicideChange) Dirtied() *common.Address { return ch.account } +// ------------------------------------------------------ +// balanceChange + +// balanceChange: [JournalChange] for an update to the wei balance of an account. +type balanceChange struct { + account *common.Address + prevWei *big.Int +} + +var _ JournalChange = balanceChange{} + func (ch balanceChange) Revert(s *StateDB) { - s.getStateObject(*ch.account).setBalance(ch.prev) + s.getStateObject(*ch.account).setBalance(ch.prevWei) } func (ch balanceChange) Dirtied() *common.Address { return ch.account } +// ------------------------------------------------------ +// nonceChange + +// nonceChange: [JournalChange] for an update to the nonce of an account. +// The nonce is a counter of the number of transactions sent from an account. +type nonceChange struct { + account *common.Address + prev uint64 +} + +var _ JournalChange = nonceChange{} + func (ch nonceChange) Revert(s *StateDB) { s.getStateObject(*ch.account).setNonce(ch.prev) } @@ -183,6 +211,19 @@ func (ch nonceChange) Dirtied() *common.Address { return ch.account } +// ------------------------------------------------------ +// codeChange + +// codeChange: [JournalChange] for an update to an account's code (smart contract +// bytecode). The previous code and hash for the code are stored to enable +// reversion. +type codeChange struct { + account *common.Address + prevcode, prevhash []byte +} + +var _ JournalChange = codeChange{} + func (ch codeChange) Revert(s *StateDB) { s.getStateObject(*ch.account).setCode(common.BytesToHash(ch.prevhash), ch.prevcode) } @@ -191,6 +232,18 @@ func (ch codeChange) Dirtied() *common.Address { return ch.account } +// ------------------------------------------------------ +// storageChange + +// storageChange: [JournalChange] for the modification of a single key and value +// within a contract's storage. +type storageChange struct { + account *common.Address + key, prevalue common.Hash +} + +var _ JournalChange = storageChange{} + func (ch storageChange) Revert(s *StateDB) { s.getStateObject(*ch.account).setState(ch.key, ch.prevalue) } @@ -199,6 +252,17 @@ func (ch storageChange) Dirtied() *common.Address { return ch.account } +// ------------------------------------------------------ +// refundChange + +// refundChange: [JournalChange] for the global gas refund counter. +// This tracks changes to the gas refund value during contract execution. +type refundChange struct { + prev uint64 +} + +var _ JournalChange = refundChange{} + func (ch refundChange) Revert(s *StateDB) { s.refund = ch.prev } @@ -207,6 +271,15 @@ func (ch refundChange) Dirtied() *common.Address { return nil } +// ------------------------------------------------------ +// addLogChange + +// addLogChange represents [JournalChange] for a new log addition. +// When reverted, it removes the last log from the accumulated logs list. +type addLogChange struct{} + +var _ JournalChange = addLogChange{} + func (ch addLogChange) Revert(s *StateDB) { s.logs = s.logs[:len(s.logs)-1] } @@ -215,16 +288,25 @@ func (ch addLogChange) Dirtied() *common.Address { return nil } +// ------------------------------------------------------ +// accessListAddAccountChange + +// accessListAddAccountChange represents [JournalChange] for when an address +// is added to the access list. Access lists track warm storage slots for +// gas cost calculations. +type accessListAddAccountChange struct { + address *common.Address +} + +// When an (address, slot) combination is added, it always results in two +// journal entries if the address is not already present: +// 1. `accessListAddAccountChange`: a journal change for the address +// 2. `accessListAddSlotChange`: a journal change for the (address, slot) +// combination. +// +// Thus, when reverting, we can safely delete the address, as no storage slots +// remain once the address entry is reverted. func (ch accessListAddAccountChange) Revert(s *StateDB) { - /* - One important invariant here, is that whenever a (addr, slot) is added, if the - addr is not already present, the add causes two journal entries: - - one for the address, - - one for the (address,slot) - Therefore, when unrolling the change, we can always blindly delete the - (addr) at this point, since no storage adds can remain when come upon - a single (addr) change. - */ s.accessList.DeleteAddress(*ch.address) } @@ -232,6 +314,20 @@ func (ch accessListAddAccountChange) Dirtied() *common.Address { return nil } +// ------------------------------------------------------ +// accessListAddSlotChange + +// accessListAddSlotChange: [JournalChange] implementations for +type accessListAddSlotChange struct { + address *common.Address + slot *common.Hash +} + +// accessListAddSlotChange represents a [JournalChange] for when a storage slot +// is added to an address's access list entry. This tracks individual storage +// slots that have been accessed. +var _ JournalChange = accessListAddSlotChange{} + func (ch accessListAddSlotChange) Revert(s *StateDB) { s.accessList.DeleteSlot(*ch.address, *ch.slot) } diff --git a/x/evm/statedb/journal_test.go b/x/evm/statedb/journal_test.go new file mode 100644 index 000000000..5863face5 --- /dev/null +++ b/x/evm/statedb/journal_test.go @@ -0,0 +1,181 @@ +package statedb_test + +import ( + "fmt" + "math/big" + "strings" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/core/vm" + + serverconfig "github.com/NibiruChain/nibiru/v2/app/server/config" + "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/precompile/test" + "github.com/NibiruChain/nibiru/v2/x/evm/statedb" +) + +func (s *Suite) TestPrecompileSnapshots() { + 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") + + wasmContract := test.SetupWasmContracts(&deps, &s.Suite)[1] + fmt.Printf("wasmContract: %s\n", wasmContract) + assertionsBeforeRun := func(deps *evmtest.TestDeps) { + test.AssertWasmCounterState( + &s.Suite, *deps, wasmContract, 0, + ) + } + run := func(deps *evmtest.TestDeps) *vm.EVM { + return test.IncrementWasmCounterWithExecuteMulti( + &s.Suite, deps, wasmContract, 7, + ) + } + assertionsAfterRun := func(deps *evmtest.TestDeps) { + test.AssertWasmCounterState( + &s.Suite, *deps, wasmContract, 7, + ) + } + + s.T().Log("Assert before transition") + + assertionsBeforeRun(&deps) + + deployArgs := []any{"name", "SYMBOL", uint8(18)} + deployResp, err := evmtest.DeployContract( + &deps, + embeds.SmartContract_ERC20Minter, + deployArgs..., + ) + s.Require().NoError(err, deployResp) + + contract := deployResp.ContractAddr + to, amount := deps.Sender.EthAddr, big.NewInt(69_420) + input, err := deps.EvmKeeper.ERC20().ABI.Pack("mint", to, amount) + s.Require().NoError(err) + _, evmObj, err := deps.EvmKeeper.CallContractWithInput( + deps.Ctx, deps.Sender.EthAddr, &contract, true, input, + ) + s.Require().NoError(err) + + s.Run("Populate dirty journal entries. Remove with Commit", func() { + stateDB := evmObj.StateDB.(*statedb.StateDB) + s.Equal(0, stateDB.DirtiesCount()) + + 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.DirtiesCount() != 4 { + debugDirtiesCountMismatch(stateDB, s.T()) + s.FailNow("expected 4 dirty journal changes") + } + + err = stateDB.Commit() // Dirties should be gone + s.NoError(err) + if stateDB.DirtiesCount() != 0 { + debugDirtiesCountMismatch(stateDB, s.T()) + s.FailNow("expected 0 dirty journal changes") + } + }) + + 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) + s.Require().NoError(err) + + leftoverGas := serverconfig.DefaultEthCallGasLimit + _, _, err = evmObj.Call( + vm.AccountRef(deps.Sender.EthAddr), + contract, + input, + leftoverGas, + big.NewInt(0), + ) + s.Require().NoError(err) + stateDB := evmObj.StateDB.(*statedb.StateDB) + if stateDB.DirtiesCount() != 2 { + debugDirtiesCountMismatch(stateDB, s.T()) + s.FailNow("expected 2 dirty journal changes") + } + + // The contract calling itself is invalid in this context. + // Note the comment in vm.Contract: + // + // type Contract struct { + // // CallerAddress is the result of the caller which initialized this + // // contract. However when the "call method" is delegated this value + // // needs to be initialized to that of the caller's caller. + // CallerAddress common.Address + // // ... + // } + // // + _, _, err = evmObj.Call( + vm.AccountRef(contract), + contract, + input, + leftoverGas, + big.NewInt(0), + ) + s.Require().ErrorContains(err, vm.ErrExecutionReverted.Error()) + }) + + s.Run("Precompile calls also start and end clean (no dirty changes)", func() { + evmObj = run(&deps) + assertionsAfterRun(&deps) + stateDB, ok := evmObj.StateDB.(*statedb.StateDB) + s.Require().True(ok, "error retrieving StateDB from the EVM") + if stateDB.DirtiesCount() != 0 { + debugDirtiesCountMismatch(stateDB, s.T()) + s.FailNow("expected 0 dirty journal changes") + } + }) +} + +func debugDirtiesCountMismatch(db *statedb.StateDB, t *testing.T) string { + lines := []string{} + dirties := db.Dirties() + stateObjects := db.StateObjects() + for addr, dirtyCountForAddr := range dirties { + lines = append(lines, fmt.Sprintf("Dirty addr: %s, dirtyCountForAddr=%d", addr, dirtyCountForAddr)) + + // Inspect the actual state object + maybeObj := stateObjects[addr] + if maybeObj == nil { + lines = append(lines, " no state object found!") + continue + } + obj := *maybeObj + + lines = append(lines, fmt.Sprintf(" balance: %s", obj.Balance())) + lines = append(lines, fmt.Sprintf(" suicided: %v", obj.Suicided)) + lines = append(lines, fmt.Sprintf(" dirtyCode: %v", obj.DirtyCode)) + + // Print storage state + lines = append(lines, fmt.Sprintf(" len(obj.DirtyStorage) entries: %d", len(obj.DirtyStorage))) + for k, v := range obj.DirtyStorage { + lines = append(lines, fmt.Sprintf(" key: %s, value: %s", k.Hex(), v.Hex())) + origVal := obj.OriginStorage[k] + lines = append(lines, fmt.Sprintf(" origin value: %s", origVal.Hex())) + } + } + + t.Log("debugDirtiesCountMismatch:\n", strings.Join(lines, "\n")) + return "" +} diff --git a/x/evm/statedb/state_object.go b/x/evm/statedb/state_object.go index bebbf7b40..e371beae0 100644 --- a/x/evm/statedb/state_object.go +++ b/x/evm/statedb/state_object.go @@ -115,14 +115,14 @@ type stateObject struct { code []byte // state storage - originStorage Storage - dirtyStorage Storage + OriginStorage Storage + DirtyStorage Storage address common.Address // flags - dirtyCode bool - suicided bool + DirtyCode bool + Suicided bool } // newObject creates a state object. @@ -138,8 +138,8 @@ func newObject(db *StateDB, address common.Address, account Account) *stateObjec address: address, // Reflect the micronibi (unibi) balance in wei account: account.ToWei(), - originStorage: make(Storage), - dirtyStorage: make(Storage), + OriginStorage: make(Storage), + DirtyStorage: make(Storage), } } @@ -170,9 +170,9 @@ func (s *stateObject) SubBalance(amount *big.Int) { // SetBalance update account balance. func (s *stateObject) SetBalance(amount *big.Int) { - s.db.journal.append(balanceChange{ + s.db.Journal.append(balanceChange{ account: &s.address, - prev: new(big.Int).Set(s.account.BalanceWei), + prevWei: new(big.Int).Set(s.account.BalanceWei), }) s.setBalance(amount) } @@ -212,7 +212,7 @@ func (s *stateObject) CodeSize() int { // SetCode set contract code to account func (s *stateObject) SetCode(codeHash common.Hash, code []byte) { prevcode := s.Code() - s.db.journal.append(codeChange{ + s.db.Journal.append(codeChange{ account: &s.address, prevhash: s.CodeHash(), prevcode: prevcode, @@ -223,12 +223,12 @@ func (s *stateObject) SetCode(codeHash common.Hash, code []byte) { func (s *stateObject) setCode(codeHash common.Hash, code []byte) { s.code = code s.account.CodeHash = codeHash[:] - s.dirtyCode = true + s.DirtyCode = true } // SetNonce set nonce to account func (s *stateObject) SetNonce(nonce uint64) { - s.db.journal.append(nonceChange{ + s.db.Journal.append(nonceChange{ account: &s.address, prev: s.account.Nonce, }) @@ -256,18 +256,18 @@ func (s *stateObject) Nonce() uint64 { // GetCommittedState query the committed state func (s *stateObject) GetCommittedState(key common.Hash) common.Hash { - if value, cached := s.originStorage[key]; cached { + if value, cached := s.OriginStorage[key]; cached { return value } // If no live objects are available, load it from keeper value := s.db.keeper.GetState(s.db.ctx, s.Address(), key) - s.originStorage[key] = value + s.OriginStorage[key] = value return value } // GetState query the current state (including dirty state) func (s *stateObject) GetState(key common.Hash) common.Hash { - if value, dirty := s.dirtyStorage[key]; dirty { + if value, dirty := s.DirtyStorage[key]; dirty { return value } return s.GetCommittedState(key) @@ -281,7 +281,7 @@ func (s *stateObject) SetState(key common.Hash, value common.Hash) { return } // New value is different, update and journal the change - s.db.journal.append(storageChange{ + s.db.Journal.append(storageChange{ account: &s.address, key: key, prevalue: prev, @@ -290,5 +290,5 @@ func (s *stateObject) SetState(key common.Hash, value common.Hash) { } func (s *stateObject) setState(key, value common.Hash) { - s.dirtyStorage[key] = value + s.DirtyStorage[key] = value } diff --git a/x/evm/statedb/statedb.go b/x/evm/statedb/statedb.go index 27b81a3ce..223e92edb 100644 --- a/x/evm/statedb/statedb.go +++ b/x/evm/statedb/statedb.go @@ -30,11 +30,12 @@ var _ vm.StateDB = &StateDB{} // * Accounts type StateDB struct { keeper Keeper - ctx sdk.Context + // ctx is the persistent context used for official `StateDB.Commit` calls. + ctx sdk.Context // Journal of state modifications. This is the backbone of // Snapshot and RevertToSnapshot. - journal *journal + Journal *journal validRevisions []revision nextRevisionID int @@ -58,7 +59,7 @@ func New(ctx sdk.Context, keeper Keeper, txConfig TxConfig) *StateDB { keeper: keeper, ctx: ctx, stateObjects: make(map[common.Address]*stateObject), - journal: newJournal(), + Journal: newJournal(), accessList: newAccessList(), txConfig: txConfig, @@ -77,7 +78,7 @@ func (s *StateDB) GetContext() sdk.Context { // AddLog adds a log, called by evm. func (s *StateDB) AddLog(log *gethcore.Log) { - s.journal.append(addLogChange{}) + s.Journal.append(addLogChange{}) log.TxHash = s.txConfig.TxHash log.BlockHash = s.txConfig.BlockHash @@ -93,14 +94,14 @@ func (s *StateDB) Logs() []*gethcore.Log { // AddRefund adds gas to the refund counter func (s *StateDB) AddRefund(gas uint64) { - s.journal.append(refundChange{prev: s.refund}) + s.Journal.append(refundChange{prev: s.refund}) s.refund += gas } // SubRefund removes gas from the refund counter. // This method will panic if the refund counter goes below zero func (s *StateDB) SubRefund(gas uint64) { - s.journal.append(refundChange{prev: s.refund}) + s.Journal.append(refundChange{prev: s.refund}) if gas > s.refund { panic(fmt.Sprintf("Refund counter below zero (gas: %d > refund: %d)", gas, s.refund)) } @@ -193,7 +194,7 @@ func (s *StateDB) GetRefund() uint64 { func (s *StateDB) HasSuicided(addr common.Address) bool { stateObject := s.getStateObject(addr) if stateObject != nil { - return stateObject.suicided + return stateObject.Suicided } return false } @@ -239,9 +240,9 @@ func (s *StateDB) createObject(addr common.Address) (newobj, prev *stateObject) newobj = newObject(s, addr, Account{}) if prev == nil { - s.journal.append(createObjectChange{account: &addr}) + s.Journal.append(createObjectChange{account: &addr}) } else { - s.journal.append(resetObjectChange{prev: prev}) + s.Journal.append(resetObjectChange{prev: prev}) } s.setStateObject(newobj) if prev != nil { @@ -274,7 +275,7 @@ func (s *StateDB) ForEachStorage(addr common.Address, cb func(key, value common. return nil } s.keeper.ForEachStorage(s.ctx, addr, func(key, value common.Hash) bool { - if value, dirty := so.dirtyStorage[key]; dirty { + if value, dirty := so.DirtyStorage[key]; dirty { return cb(key, value) } if len(value) > 0 { @@ -294,18 +295,25 @@ func (s *StateDB) setStateObject(object *stateObject) { */ // AddBalance adds amount to the account associated with addr. -func (s *StateDB) AddBalance(addr common.Address, amount *big.Int) { +func (s *StateDB) AddBalance(addr common.Address, wei *big.Int) { stateObject := s.getOrNewStateObject(addr) if stateObject != nil { - stateObject.AddBalance(amount) + stateObject.AddBalance(wei) } } // SubBalance subtracts amount from the account associated with addr. -func (s *StateDB) SubBalance(addr common.Address, amount *big.Int) { +func (s *StateDB) SubBalance(addr common.Address, wei *big.Int) { stateObject := s.getOrNewStateObject(addr) if stateObject != nil { - stateObject.SubBalance(amount) + stateObject.SubBalance(wei) + } +} + +func (s *StateDB) SetBalanceWei(addr common.Address, wei *big.Int) { + stateObject := s.getOrNewStateObject(addr) + if stateObject != nil { + stateObject.SetBalance(wei) } } @@ -343,12 +351,12 @@ func (s *StateDB) Suicide(addr common.Address) bool { if stateObject == nil { return false } - s.journal.append(suicideChange{ + s.Journal.append(suicideChange{ account: &addr, - prev: stateObject.suicided, + prev: stateObject.Suicided, prevbalance: new(big.Int).Set(stateObject.Balance()), }) - stateObject.suicided = true + stateObject.Suicided = true stateObject.account.BalanceWei = new(big.Int) return true @@ -388,7 +396,7 @@ func (s *StateDB) PrepareAccessList( // AddAddressToAccessList adds the given address to the access list func (s *StateDB) AddAddressToAccessList(addr common.Address) { if s.accessList.AddAddress(addr) { - s.journal.append(accessListAddAccountChange{&addr}) + s.Journal.append(accessListAddAccountChange{&addr}) } } @@ -400,10 +408,10 @@ func (s *StateDB) AddSlotToAccessList(addr common.Address, slot common.Hash) { // scope of 'address' without having the 'address' become already added // to the access list (via call-variant, create, etc). // Better safe than sorry, though - s.journal.append(accessListAddAccountChange{&addr}) + s.Journal.append(accessListAddAccountChange{&addr}) } if slotMod { - s.journal.append(accessListAddSlotChange{ + s.Journal.append(accessListAddSlotChange{ address: &addr, slot: &slot, }) @@ -424,7 +432,7 @@ func (s *StateDB) SlotInAccessList(addr common.Address, slot common.Hash) (addre func (s *StateDB) Snapshot() int { id := s.nextRevisionID s.nextRevisionID++ - s.validRevisions = append(s.validRevisions, revision{id, s.journal.length()}) + s.validRevisions = append(s.validRevisions, revision{id, s.Journal.Length()}) return id } @@ -440,7 +448,7 @@ func (s *StateDB) RevertToSnapshot(revid int) { snapshot := s.validRevisions[idx].journalIndex // Replay the journal to undo changes and remove invalidated snapshots - s.journal.Revert(s, snapshot) + s.Journal.Revert(s, snapshot) s.validRevisions = s.validRevisions[:idx] } @@ -449,31 +457,45 @@ func errorf(format string, args ...any) error { return fmt.Errorf("StateDB error: "+format, args...) } -// Commit writes the dirty states to keeper -// the StateDB object should be discarded after committed. +// Commit writes the dirty journal state changes to the EVM Keeper. The +// StateDB object cannot be reused after [Commit] has completed. A new +// object needs to be created from the EVM. func (s *StateDB) Commit() error { - for _, addr := range s.journal.sortedDirties() { - obj := s.stateObjects[addr] - if obj.suicided { - if err := s.keeper.DeleteAccount(s.ctx, obj.Address()); err != nil { - return errorf("failed to delete account: %w") + ctx := s.GetContext() + for _, addr := range s.Journal.sortedDirties() { + obj := s.getStateObject(addr) + if obj == nil { + continue + } + if obj.Suicided { + // Invariant: After [StateDB.Suicide] for some address, the + // corresponding account's state object is only available until the + // state is committed. + if err := s.keeper.DeleteAccount(ctx, obj.Address()); err != nil { + return errorf("failed to delete account: %w", err) } + delete(s.stateObjects, addr) } else { - if obj.code != nil && obj.dirtyCode { - s.keeper.SetCode(s.ctx, obj.CodeHash(), obj.code) + if obj.code != nil && obj.DirtyCode { + s.keeper.SetCode(ctx, obj.CodeHash(), obj.code) } - if err := s.keeper.SetAccount(s.ctx, obj.Address(), obj.account.ToNative()); err != nil { - return errorf("failed to set account: %w") + if err := s.keeper.SetAccount(ctx, obj.Address(), obj.account.ToNative()); err != nil { + return errorf("failed to set account: %w", err) } - for _, key := range obj.dirtyStorage.SortedKeys() { - value := obj.dirtyStorage[key] - // Skip noop changes, persist actual changes - if value == obj.originStorage[key] { + for _, key := range obj.DirtyStorage.SortedKeys() { + dirtyVal := obj.DirtyStorage[key] + // Values that match origin storage are not dirty. + if dirtyVal == obj.OriginStorage[key] { continue } - s.keeper.SetState(s.ctx, obj.Address(), key, value.Bytes()) + // Persist committed changes + s.keeper.SetState(ctx, obj.Address(), key, dirtyVal.Bytes()) + obj.OriginStorage[key] = dirtyVal } } + // Clear the dirty counts because all state changes have been + // committed. + s.Journal.dirties[addr] = 0 } return nil } diff --git a/x/evm/statedb/statedb_test.go b/x/evm/statedb/statedb_test.go index b8b4d1741..7919d3da0 100644 --- a/x/evm/statedb/statedb_test.go +++ b/x/evm/statedb/statedb_test.go @@ -513,11 +513,12 @@ func (s *Suite) TestLog() { txIdx = uint(1) logIdx = uint(1) ) - txConfig := statedb.NewTxConfig( - blockHash, - txHash, - txIdx, logIdx, - ) + txConfig := statedb.TxConfig{ + BlockHash: blockHash, + TxHash: txHash, + TxIndex: txIdx, + LogIndex: logIdx, + } deps := evmtest.NewTestDeps() db := statedb.New(deps.Ctx, &deps.App.EvmKeeper, txConfig)