diff --git a/CHANGELOG.md b/CHANGELOG.md
index d09884abe..35b8ce445 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -69,6 +69,16 @@ consistent setup and dynamic gas calculations, addressing the following tickets.
- [#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
+- [#2094](https://github.com/NibiruChain/nibiru/pull/2094) - fix(evm): Following
+from the changs in #2086, this pull request implements a new `JournalChange`
+struct that saves a deep copy of the state multi store before each
+state-modifying, Nibiru-specific precompiled contract is called (`OnRunStart`).
+Additionally, we commit the `StateDB` there as well. This guarantees that the
+non-EVM and EVM state will be in sync even if there are complex, multi-step
+Ethereum transactions, such as in the case of an EthereumTx that influences the
+`StateDB`, then calls a precompile that also changes non-EVM state, and then EVM
+reverts inside of a try-catch.
+- [#2098](https://github.com/NibiruChain/nibiru/pull/2098) - test(evm): statedb tests for race conditions within funtoken precompile
#### Nibiru EVM | Before Audit 1 - 2024-10-18
diff --git a/app/keepers.go b/app/keepers.go
index 6695a6194..4c5721e83 100644
--- a/app/keepers.go
+++ b/app/keepers.go
@@ -140,6 +140,7 @@ type AppKeepers struct {
}
type privateKeepers struct {
+ bankBaseKeeper bankkeeper.BaseKeeper
capabilityKeeper *capabilitykeeper.Keeper
slashingKeeper slashingkeeper.Keeper
crisisKeeper crisiskeeper.Keeper
@@ -262,13 +263,15 @@ func (app *NibiruApp) InitKeepers(
sdk.GetConfig().GetBech32AccountAddrPrefix(),
govModuleAddr,
)
- app.BankKeeper = bankkeeper.NewBaseKeeper(
+
+ app.bankBaseKeeper = bankkeeper.NewBaseKeeper(
appCodec,
keys[banktypes.StoreKey],
app.AccountKeeper,
BlockedAddresses(),
govModuleAddr,
)
+ app.BankKeeper = app.bankBaseKeeper
app.StakingKeeper = stakingkeeper.NewKeeper(
appCodec,
keys[stakingtypes.StoreKey],
@@ -605,7 +608,7 @@ func (app *NibiruApp) initAppModules(
),
auth.NewAppModule(appCodec, app.AccountKeeper, authsims.RandomGenesisAccounts, app.GetSubspace(authtypes.ModuleName)),
vesting.NewAppModule(app.AccountKeeper, app.BankKeeper),
- bank.NewAppModule(appCodec, app.BankKeeper, app.AccountKeeper, app.GetSubspace(banktypes.ModuleName)),
+ bank.NewAppModule(appCodec, app.bankBaseKeeper, app.AccountKeeper, app.GetSubspace(banktypes.ModuleName)),
capability.NewAppModule(appCodec, *app.capabilityKeeper, false),
feegrantmodule.NewAppModule(appCodec, app.AccountKeeper, app.BankKeeper, app.FeeGrantKeeper, app.interfaceRegistry),
gov.NewAppModule(appCodec, &app.GovKeeper, app.AccountKeeper, app.BankKeeper, app.GetSubspace(govtypes.ModuleName)),
diff --git a/go.mod b/go.mod
index 158dc9b80..bc14078f3 100644
--- a/go.mod
+++ b/go.mod
@@ -244,6 +244,9 @@ require (
replace (
cosmossdk.io/api => cosmossdk.io/api v0.3.1
+ github.com/CosmWasm/wasmd => github.com/NibiruChain/wasmd v0.44.0-nibiru
+ github.com/cosmos/cosmos-sdk => github.com/NibiruChain/cosmos-sdk v0.47.11-nibiru
+
github.com/cosmos/iavl => github.com/cosmos/iavl v0.20.0
github.com/ethereum/go-ethereum => github.com/NibiruChain/go-ethereum v1.10.27-nibiru
diff --git a/go.sum b/go.sum
index 2c789f74f..213ee9c84 100644
--- a/go.sum
+++ b/go.sum
@@ -221,8 +221,6 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/ChainSafe/go-schnorrkel v1.0.0 h1:3aDA67lAykLaG1y3AOjs88dMxC88PgUuHRrLeDnvGIM=
github.com/ChainSafe/go-schnorrkel v1.0.0/go.mod h1:dpzHYVxLZcp8pjlV+O+UR8K0Hp/z7vcchBSbMBEhCw4=
-github.com/CosmWasm/wasmd v0.44.0 h1:2sbcoCAvfjCs1O0SWt53xULKjkV06dbSFthEViIC6Zg=
-github.com/CosmWasm/wasmd v0.44.0/go.mod h1:tDyYN050qUcdd7LOxGeo2e185sEShyO3nJGl2Cf59+k=
github.com/CosmWasm/wasmvm v1.5.5 h1:XlZI3xO5iUhiBqMiyzsrWEfUtk5gcBMNYIdHnsTB+NI=
github.com/CosmWasm/wasmvm v1.5.5/go.mod h1:Q0bSEtlktzh7W2hhEaifrFp1Erx11ckQZmjq8FLCyys=
github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
@@ -237,8 +235,12 @@ github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migc
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
github.com/NibiruChain/collections v0.5.0 h1:33pXpVTe1PK/tfdZlAJF1JF7AdzGNARG+iL9G/z3X7k=
github.com/NibiruChain/collections v0.5.0/go.mod h1:43L6yjuF0BMre/mw4gqn/kUOZz1c2Y3huZ/RQfBFrOQ=
+github.com/NibiruChain/cosmos-sdk v0.47.11-nibiru h1:PgFpxDe+7+OzWHs4zXlml5j2i9sGq2Zpd3ndYQG29/0=
+github.com/NibiruChain/cosmos-sdk v0.47.11-nibiru/go.mod h1:ADjORYzUQqQv/FxDi0H0K5gW/rAk1CiDR3ZKsExfJV0=
github.com/NibiruChain/go-ethereum v1.10.27-nibiru h1:o6lRFt57izoYwzN5cG8tnnBtJcaO3X7MjjN7PGGNCFg=
github.com/NibiruChain/go-ethereum v1.10.27-nibiru/go.mod h1:kvvL3nDceUcB+1qGUBAsVf5dW23RBR77fqxgx2PGNrQ=
+github.com/NibiruChain/wasmd v0.44.0-nibiru h1:b+stNdbMFsl0+o4KedXyF83qRnEpB/jCiTGZZgv2h2U=
+github.com/NibiruChain/wasmd v0.44.0-nibiru/go.mod h1:inrbdsixQ0Kdu4mFUg1u7fn3XPOEkzqieGv0H/gR0ck=
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
@@ -426,8 +428,6 @@ github.com/cosmos/cosmos-db v1.0.2 h1:hwMjozuY1OlJs/uh6vddqnk9j7VamLv+0DBlbEXbAK
github.com/cosmos/cosmos-db v1.0.2/go.mod h1:Z8IXcFJ9PqKK6BIsVOB3QXtkKoqUOp1vRvPT39kOXEA=
github.com/cosmos/cosmos-proto v1.0.0-beta.5 h1:eNcayDLpip+zVLRLYafhzLvQlSmyab+RC5W7ZfmxJLA=
github.com/cosmos/cosmos-proto v1.0.0-beta.5/go.mod h1:hQGLpiIUloJBMdQMMWb/4wRApmI9hjHH05nefC0Ojec=
-github.com/cosmos/cosmos-sdk v0.47.11 h1:0Qx7eORw0RJqPv+mvDuU8NQ1LV3nJJKJnPoYblWHolc=
-github.com/cosmos/cosmos-sdk v0.47.11/go.mod h1:ADjORYzUQqQv/FxDi0H0K5gW/rAk1CiDR3ZKsExfJV0=
github.com/cosmos/go-bip39 v0.0.0-20180819234021-555e2067c45d/go.mod h1:tSxLoYXyBmiFeKpvmq4dzayMdCjCnu8uqmCysIGBT2Y=
github.com/cosmos/go-bip39 v1.0.0 h1:pcomnQdrdH22njcAatO0yWojsUnCO3y2tNoV1cb6hHY=
github.com/cosmos/go-bip39 v1.0.0/go.mod h1:RNJv0H/pOIVgxw6KS7QeX2a0Uo0aKUlfhZ4xuwvCdJw=
diff --git a/x/evm/deps.go b/x/evm/deps.go
index 04327db06..2325def18 100644
--- a/x/evm/deps.go
+++ b/x/evm/deps.go
@@ -4,7 +4,6 @@ package evm
import (
sdk "github.com/cosmos/cosmos-sdk/types"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
- bank "github.com/cosmos/cosmos-sdk/x/bank/types"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
)
@@ -32,18 +31,6 @@ type AccountKeeper interface {
SetModuleAccount(ctx sdk.Context, macc authtypes.ModuleAccountI)
}
-// BankKeeper defines the expected interface needed to retrieve account balances.
-type BankKeeper interface {
- authtypes.BankKeeper
- GetBalance(ctx sdk.Context, addr sdk.AccAddress, denom string) sdk.Coin
- SendCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error
- MintCoins(ctx sdk.Context, moduleName string, amt sdk.Coins) error
- BurnCoins(ctx sdk.Context, moduleName string, amt sdk.Coins) error
-
- GetDenomMetaData(ctx sdk.Context, denom string) (metadata bank.Metadata, isFound bool)
- SetDenomMetaData(ctx sdk.Context, denomMetaData bank.Metadata)
-}
-
// StakingKeeper returns the historical headers kept in store.
type StakingKeeper interface {
GetHistoricalInfo(ctx sdk.Context, height int64) (stakingtypes.HistoricalInfo, bool)
diff --git a/x/evm/embeds/artifacts/contracts/FunToken.sol/IFunToken.json b/x/evm/embeds/artifacts/contracts/IFunToken.sol/IFunToken.json
similarity index 93%
rename from x/evm/embeds/artifacts/contracts/FunToken.sol/IFunToken.json
rename to x/evm/embeds/artifacts/contracts/IFunToken.sol/IFunToken.json
index 9f006c50f..6fe6e838a 100644
--- a/x/evm/embeds/artifacts/contracts/FunToken.sol/IFunToken.json
+++ b/x/evm/embeds/artifacts/contracts/IFunToken.sol/IFunToken.json
@@ -1,7 +1,7 @@
{
"_format": "hh-sol-artifact-1",
"contractName": "IFunToken",
- "sourceName": "contracts/FunToken.sol",
+ "sourceName": "contracts/IFunToken.sol",
"abi": [
{
"inputs": [
diff --git a/x/evm/embeds/artifacts/contracts/TestERC20TransferThenPrecompileSend.sol/TestERC20TransferThenPrecompileSend.json b/x/evm/embeds/artifacts/contracts/TestERC20TransferThenPrecompileSend.sol/TestERC20TransferThenPrecompileSend.json
new file mode 100644
index 000000000..234a7f943
--- /dev/null
+++ b/x/evm/embeds/artifacts/contracts/TestERC20TransferThenPrecompileSend.sol/TestERC20TransferThenPrecompileSend.json
@@ -0,0 +1,50 @@
+{
+ "_format": "hh-sol-artifact-1",
+ "contractName": "TestERC20TransferThenPrecompileSend",
+ "sourceName": "contracts/TestERC20TransferThenPrecompileSend.sol",
+ "abi": [
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "erc20_",
+ "type": "address"
+ }
+ ],
+ "stateMutability": "nonpayable",
+ "type": "constructor"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address payable",
+ "name": "transferRecipient",
+ "type": "address"
+ },
+ {
+ "internalType": "uint256",
+ "name": "transferAmount",
+ "type": "uint256"
+ },
+ {
+ "internalType": "string",
+ "name": "precompileRecipient",
+ "type": "string"
+ },
+ {
+ "internalType": "uint256",
+ "name": "precompileAmount",
+ "type": "uint256"
+ }
+ ],
+ "name": "erc20TransferThenPrecompileSend",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function"
+ }
+ ],
+ "bytecode": "0x608060405234801561001057600080fd5b50604051610974380380610974833981810160405281019061003291906100db565b806000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555050610108565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006100a88261007d565b9050919050565b6100b88161009d565b81146100c357600080fd5b50565b6000815190506100d5816100af565b92915050565b6000602082840312156100f1576100f0610078565b5b60006100ff848285016100c6565b91505092915050565b61085d806101176000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c8063264c325814610030575b600080fd5b61004a6004803603810190610045919061049f565b61004c565b005b60008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1663a9059cbb85856040518363ffffffff1660e01b81526004016100a7929190610590565b6020604051808303816000875af11580156100c6573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906100ea91906105f1565b610129576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016101209061067b565b60405180910390fd5b600061080073ffffffffffffffffffffffffffffffffffffffff1660008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1683856040516024016101799392919061072a565b6040516020818303038152906040527f03003bc5000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff838183161783525050505060405161020391906107af565b6000604051808303816000865af19150503d8060008114610240576040519150601f19603f3d011682016040523d82523d6000602084013e610245565b606091505b5050905080604051602001610259906107ec565b604051602081830303815290604052906102a9576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016102a09190610805565b60405180910390fd5b505050505050565b6000604051905090565b600080fd5b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006102f0826102c5565b9050919050565b610300816102e5565b811461030b57600080fd5b50565b60008135905061031d816102f7565b92915050565b6000819050919050565b61033681610323565b811461034157600080fd5b50565b6000813590506103538161032d565b92915050565b600080fd5b600080fd5b6000601f19601f8301169050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6103ac82610363565b810181811067ffffffffffffffff821117156103cb576103ca610374565b5b80604052505050565b60006103de6102b1565b90506103ea82826103a3565b919050565b600067ffffffffffffffff82111561040a57610409610374565b5b61041382610363565b9050602081019050919050565b82818337600083830152505050565b600061044261043d846103ef565b6103d4565b90508281526020810184848401111561045e5761045d61035e565b5b610469848285610420565b509392505050565b600082601f83011261048657610485610359565b5b813561049684826020860161042f565b91505092915050565b600080600080608085870312156104b9576104b86102bb565b5b60006104c78782880161030e565b94505060206104d887828801610344565b935050604085013567ffffffffffffffff8111156104f9576104f86102c0565b5b61050587828801610471565b925050606061051687828801610344565b91505092959194509250565b6000819050919050565b600061054761054261053d846102c5565b610522565b6102c5565b9050919050565b60006105598261052c565b9050919050565b600061056b8261054e565b9050919050565b61057b81610560565b82525050565b61058a81610323565b82525050565b60006040820190506105a56000830185610572565b6105b26020830184610581565b9392505050565b60008115159050919050565b6105ce816105b9565b81146105d957600080fd5b50565b6000815190506105eb816105c5565b92915050565b600060208284031215610607576106066102bb565b5b6000610615848285016105dc565b91505092915050565b600082825260208201905092915050565b7f4552432d3230207472616e73666572206661696c656400000000000000000000600082015250565b600061066560168361061e565b91506106708261062f565b602082019050919050565b6000602082019050818103600083015261069481610658565b9050919050565b60006106a6826102c5565b9050919050565b6106b68161069b565b82525050565b600081519050919050565b60005b838110156106e55780820151818401526020810190506106ca565b60008484015250505050565b60006106fc826106bc565b610706818561061e565b93506107168185602086016106c7565b61071f81610363565b840191505092915050565b600060608201905061073f60008301866106ad565b61074c6020830185610581565b818103604083015261075e81846106f1565b9050949350505050565b600081519050919050565b600081905092915050565b600061078982610768565b6107938185610773565b93506107a38185602086016106c7565b80840191505092915050565b60006107bb828461077e565b915081905092915050565b7f4661696c656420746f2063616c6c2062616e6b53656e64000000000000000000815250565b60006107f7826107c6565b601782019150819050919050565b6000602082019050818103600083015261081f81846106f1565b90509291505056fea26469706673582212205d952e3358213844ef60215f35b3c3adb68dbc657522a8dd683eb1c5c0f64bfa64736f6c63430008180033",
+ "deployedBytecode": "0x608060405234801561001057600080fd5b506004361061002b5760003560e01c8063264c325814610030575b600080fd5b61004a6004803603810190610045919061049f565b61004c565b005b60008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1663a9059cbb85856040518363ffffffff1660e01b81526004016100a7929190610590565b6020604051808303816000875af11580156100c6573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906100ea91906105f1565b610129576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016101209061067b565b60405180910390fd5b600061080073ffffffffffffffffffffffffffffffffffffffff1660008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1683856040516024016101799392919061072a565b6040516020818303038152906040527f03003bc5000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff838183161783525050505060405161020391906107af565b6000604051808303816000865af19150503d8060008114610240576040519150601f19603f3d011682016040523d82523d6000602084013e610245565b606091505b5050905080604051602001610259906107ec565b604051602081830303815290604052906102a9576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016102a09190610805565b60405180910390fd5b505050505050565b6000604051905090565b600080fd5b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006102f0826102c5565b9050919050565b610300816102e5565b811461030b57600080fd5b50565b60008135905061031d816102f7565b92915050565b6000819050919050565b61033681610323565b811461034157600080fd5b50565b6000813590506103538161032d565b92915050565b600080fd5b600080fd5b6000601f19601f8301169050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6103ac82610363565b810181811067ffffffffffffffff821117156103cb576103ca610374565b5b80604052505050565b60006103de6102b1565b90506103ea82826103a3565b919050565b600067ffffffffffffffff82111561040a57610409610374565b5b61041382610363565b9050602081019050919050565b82818337600083830152505050565b600061044261043d846103ef565b6103d4565b90508281526020810184848401111561045e5761045d61035e565b5b610469848285610420565b509392505050565b600082601f83011261048657610485610359565b5b813561049684826020860161042f565b91505092915050565b600080600080608085870312156104b9576104b86102bb565b5b60006104c78782880161030e565b94505060206104d887828801610344565b935050604085013567ffffffffffffffff8111156104f9576104f86102c0565b5b61050587828801610471565b925050606061051687828801610344565b91505092959194509250565b6000819050919050565b600061054761054261053d846102c5565b610522565b6102c5565b9050919050565b60006105598261052c565b9050919050565b600061056b8261054e565b9050919050565b61057b81610560565b82525050565b61058a81610323565b82525050565b60006040820190506105a56000830185610572565b6105b26020830184610581565b9392505050565b60008115159050919050565b6105ce816105b9565b81146105d957600080fd5b50565b6000815190506105eb816105c5565b92915050565b600060208284031215610607576106066102bb565b5b6000610615848285016105dc565b91505092915050565b600082825260208201905092915050565b7f4552432d3230207472616e73666572206661696c656400000000000000000000600082015250565b600061066560168361061e565b91506106708261062f565b602082019050919050565b6000602082019050818103600083015261069481610658565b9050919050565b60006106a6826102c5565b9050919050565b6106b68161069b565b82525050565b600081519050919050565b60005b838110156106e55780820151818401526020810190506106ca565b60008484015250505050565b60006106fc826106bc565b610706818561061e565b93506107168185602086016106c7565b61071f81610363565b840191505092915050565b600060608201905061073f60008301866106ad565b61074c6020830185610581565b818103604083015261075e81846106f1565b9050949350505050565b600081519050919050565b600081905092915050565b600061078982610768565b6107938185610773565b93506107a38185602086016106c7565b80840191505092915050565b60006107bb828461077e565b915081905092915050565b7f4661696c656420746f2063616c6c2062616e6b53656e64000000000000000000815250565b60006107f7826107c6565b601782019150819050919050565b6000602082019050818103600083015261081f81846106f1565b90509291505056fea26469706673582212205d952e3358213844ef60215f35b3c3adb68dbc657522a8dd683eb1c5c0f64bfa64736f6c63430008180033",
+ "linkReferences": {},
+ "deployedLinkReferences": {}
+}
diff --git a/x/evm/embeds/artifacts/contracts/TestNativeSendThenPrecompileSend.sol/TestNativeSendThenPrecompileSend.json b/x/evm/embeds/artifacts/contracts/TestNativeSendThenPrecompileSend.sol/TestNativeSendThenPrecompileSend.json
new file mode 100644
index 000000000..1d44a675c
--- /dev/null
+++ b/x/evm/embeds/artifacts/contracts/TestNativeSendThenPrecompileSend.sol/TestNativeSendThenPrecompileSend.json
@@ -0,0 +1,50 @@
+{
+ "_format": "hh-sol-artifact-1",
+ "contractName": "TestNativeSendThenPrecompileSend",
+ "sourceName": "contracts/TestNativeSendThenPrecompileSend.sol",
+ "abi": [
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "erc20_",
+ "type": "address"
+ }
+ ],
+ "stateMutability": "nonpayable",
+ "type": "constructor"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address payable",
+ "name": "nativeRecipient",
+ "type": "address"
+ },
+ {
+ "internalType": "uint256",
+ "name": "nativeAmount",
+ "type": "uint256"
+ },
+ {
+ "internalType": "string",
+ "name": "precompileRecipient",
+ "type": "string"
+ },
+ {
+ "internalType": "uint256",
+ "name": "precompileAmount",
+ "type": "uint256"
+ }
+ ],
+ "name": "nativeSendThenPrecompileSend",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function"
+ }
+ ],
+ "bytecode": "0x608060405234801561001057600080fd5b50604051610877380380610877833981810160405281019061003291906100db565b806000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555050610108565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006100a88261007d565b9050919050565b6100b88161009d565b81146100c357600080fd5b50565b6000815190506100d5816100af565b92915050565b6000602082840312156100f1576100f0610078565b5b60006100ff848285016100c6565b91505092915050565b610760806101176000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c8063a4de557414610030575b600080fd5b61004a6004803603810190610045919061043c565b61004c565b005b60008473ffffffffffffffffffffffffffffffffffffffff166108fc859081150290604051600060405180830381858888f193505050509050806100c5576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016100bc9061051c565b60405180910390fd5b600061080073ffffffffffffffffffffffffffffffffffffffff1660008054906101000a900473ffffffffffffffffffffffffffffffffffffffff168486604051602401610115939291906105da565b6040516020818303038152906040527f03003bc5000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff838183161783525050505060405161019f919061065f565b6000604051808303816000865af19150503d80600081146101dc576040519150601f19603f3d011682016040523d82523d6000602084013e6101e1565b606091505b50509050806040516020016101f5906106f3565b60405160208183030381529060405290610245576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161023c9190610708565b60405180910390fd5b50505050505050565b6000604051905090565b600080fd5b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b600061028d82610262565b9050919050565b61029d81610282565b81146102a857600080fd5b50565b6000813590506102ba81610294565b92915050565b6000819050919050565b6102d3816102c0565b81146102de57600080fd5b50565b6000813590506102f0816102ca565b92915050565b600080fd5b600080fd5b6000601f19601f8301169050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b61034982610300565b810181811067ffffffffffffffff8211171561036857610367610311565b5b80604052505050565b600061037b61024e565b90506103878282610340565b919050565b600067ffffffffffffffff8211156103a7576103a6610311565b5b6103b082610300565b9050602081019050919050565b82818337600083830152505050565b60006103df6103da8461038c565b610371565b9050828152602081018484840111156103fb576103fa6102fb565b5b6104068482856103bd565b509392505050565b600082601f830112610423576104226102f6565b5b81356104338482602086016103cc565b91505092915050565b6000806000806080858703121561045657610455610258565b5b6000610464878288016102ab565b9450506020610475878288016102e1565b935050604085013567ffffffffffffffff8111156104965761049561025d565b5b6104a28782880161040e565b92505060606104b3878288016102e1565b91505092959194509250565b600082825260208201905092915050565b7f4661696c656420746f2073656e64206e617469766520746f6b656e0000000000600082015250565b6000610506601b836104bf565b9150610511826104d0565b602082019050919050565b60006020820190508181036000830152610535816104f9565b9050919050565b600061054782610262565b9050919050565b6105578161053c565b82525050565b610566816102c0565b82525050565b600081519050919050565b60005b8381101561059557808201518184015260208101905061057a565b60008484015250505050565b60006105ac8261056c565b6105b681856104bf565b93506105c6818560208601610577565b6105cf81610300565b840191505092915050565b60006060820190506105ef600083018661054e565b6105fc602083018561055d565b818103604083015261060e81846105a1565b9050949350505050565b600081519050919050565b600081905092915050565b600061063982610618565b6106438185610623565b9350610653818560208601610577565b80840191505092915050565b600061066b828461062e565b915081905092915050565b600081905092915050565b7f4661696c656420746f2063616c6c20707265636f6d70696c652062616e6b536560008201527f6e64000000000000000000000000000000000000000000000000000000000000602082015250565b60006106dd602283610676565b91506106e882610681565b602282019050919050565b60006106fe826106d0565b9150819050919050565b6000602082019050818103600083015261072281846105a1565b90509291505056fea26469706673582212206bab9cbf21b89041a55ee0f768a9443ae5e5ebfde23fe8a91daf9d4b339427db64736f6c63430008180033",
+ "deployedBytecode": "0x608060405234801561001057600080fd5b506004361061002b5760003560e01c8063a4de557414610030575b600080fd5b61004a6004803603810190610045919061043c565b61004c565b005b60008473ffffffffffffffffffffffffffffffffffffffff166108fc859081150290604051600060405180830381858888f193505050509050806100c5576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016100bc9061051c565b60405180910390fd5b600061080073ffffffffffffffffffffffffffffffffffffffff1660008054906101000a900473ffffffffffffffffffffffffffffffffffffffff168486604051602401610115939291906105da565b6040516020818303038152906040527f03003bc5000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff838183161783525050505060405161019f919061065f565b6000604051808303816000865af19150503d80600081146101dc576040519150601f19603f3d011682016040523d82523d6000602084013e6101e1565b606091505b50509050806040516020016101f5906106f3565b60405160208183030381529060405290610245576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161023c9190610708565b60405180910390fd5b50505050505050565b6000604051905090565b600080fd5b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b600061028d82610262565b9050919050565b61029d81610282565b81146102a857600080fd5b50565b6000813590506102ba81610294565b92915050565b6000819050919050565b6102d3816102c0565b81146102de57600080fd5b50565b6000813590506102f0816102ca565b92915050565b600080fd5b600080fd5b6000601f19601f8301169050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b61034982610300565b810181811067ffffffffffffffff8211171561036857610367610311565b5b80604052505050565b600061037b61024e565b90506103878282610340565b919050565b600067ffffffffffffffff8211156103a7576103a6610311565b5b6103b082610300565b9050602081019050919050565b82818337600083830152505050565b60006103df6103da8461038c565b610371565b9050828152602081018484840111156103fb576103fa6102fb565b5b6104068482856103bd565b509392505050565b600082601f830112610423576104226102f6565b5b81356104338482602086016103cc565b91505092915050565b6000806000806080858703121561045657610455610258565b5b6000610464878288016102ab565b9450506020610475878288016102e1565b935050604085013567ffffffffffffffff8111156104965761049561025d565b5b6104a28782880161040e565b92505060606104b3878288016102e1565b91505092959194509250565b600082825260208201905092915050565b7f4661696c656420746f2073656e64206e617469766520746f6b656e0000000000600082015250565b6000610506601b836104bf565b9150610511826104d0565b602082019050919050565b60006020820190508181036000830152610535816104f9565b9050919050565b600061054782610262565b9050919050565b6105578161053c565b82525050565b610566816102c0565b82525050565b600081519050919050565b60005b8381101561059557808201518184015260208101905061057a565b60008484015250505050565b60006105ac8261056c565b6105b681856104bf565b93506105c6818560208601610577565b6105cf81610300565b840191505092915050565b60006060820190506105ef600083018661054e565b6105fc602083018561055d565b818103604083015261060e81846105a1565b9050949350505050565b600081519050919050565b600081905092915050565b600061063982610618565b6106438185610623565b9350610653818560208601610577565b80840191505092915050565b600061066b828461062e565b915081905092915050565b600081905092915050565b7f4661696c656420746f2063616c6c20707265636f6d70696c652062616e6b536560008201527f6e64000000000000000000000000000000000000000000000000000000000000602082015250565b60006106dd602283610676565b91506106e882610681565b602282019050919050565b60006106fe826106d0565b9150819050919050565b6000602082019050818103600083015261072281846105a1565b90509291505056fea26469706673582212206bab9cbf21b89041a55ee0f768a9443ae5e5ebfde23fe8a91daf9d4b339427db64736f6c63430008180033",
+ "linkReferences": {},
+ "deployedLinkReferences": {}
+}
diff --git a/x/evm/embeds/contracts/FunToken.sol b/x/evm/embeds/contracts/IFunToken.sol
similarity index 100%
rename from x/evm/embeds/contracts/FunToken.sol
rename to x/evm/embeds/contracts/IFunToken.sol
diff --git a/x/evm/embeds/contracts/TestERC20TransferThenPrecompileSend.sol b/x/evm/embeds/contracts/TestERC20TransferThenPrecompileSend.sol
new file mode 100644
index 000000000..b51d0367e
--- /dev/null
+++ b/x/evm/embeds/contracts/TestERC20TransferThenPrecompileSend.sol
@@ -0,0 +1,39 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.24;
+
+// Uncomment this line to use console.log
+// import "hardhat/console.sol";
+import "./IFunToken.sol";
+import "@openzeppelin/contracts/utils/Strings.sol";
+import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
+
+contract TestERC20TransferThenPrecompileSend {
+ address erc20;
+
+ constructor(address erc20_) {
+ erc20 = erc20_;
+ }
+
+ function erc20TransferThenPrecompileSend(
+ address payable transferRecipient,
+ uint256 transferAmount,
+ string memory precompileRecipient,
+ uint256 precompileAmount
+ ) public {
+ require(
+ ERC20(erc20).transfer(transferRecipient, transferAmount),
+ "ERC-20 transfer failed"
+ );
+
+ (bool success, ) = FUNTOKEN_PRECOMPILE_ADDRESS.call(
+ abi.encodeWithSignature(
+ "bankSend(address,uint256,string)",
+ erc20,
+ uint256(precompileAmount),
+ precompileRecipient
+ )
+ );
+
+ require(success, string.concat("Failed to call bankSend"));
+ }
+}
diff --git a/x/evm/embeds/contracts/TestNativeSendThenPrecompileSend.sol b/x/evm/embeds/contracts/TestNativeSendThenPrecompileSend.sol
new file mode 100644
index 000000000..da9cb471b
--- /dev/null
+++ b/x/evm/embeds/contracts/TestNativeSendThenPrecompileSend.sol
@@ -0,0 +1,34 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.24;
+
+import "./IFunToken.sol";
+import "@openzeppelin/contracts/utils/Strings.sol";
+
+contract TestNativeSendThenPrecompileSend {
+ address erc20;
+
+ constructor(address erc20_) {
+ erc20 = erc20_;
+ }
+
+ function nativeSendThenPrecompileSend(
+ address payable nativeRecipient,
+ uint256 nativeAmount,
+ string memory precompileRecipient,
+ uint256 precompileAmount
+ ) public {
+ bool isSent = nativeRecipient.send(nativeAmount);
+ require(isSent, "Failed to send native token");
+
+ (bool success, ) = FUNTOKEN_PRECOMPILE_ADDRESS.call(
+ abi.encodeWithSignature(
+ "bankSend(address,uint256,string)",
+ erc20,
+ precompileAmount,
+ precompileRecipient
+ )
+ );
+
+ require(success, string.concat("Failed to call precompile bankSend"));
+ }
+}
diff --git a/x/evm/embeds/embeds.go b/x/evm/embeds/embeds.go
index 5ac65655d..12b0d981c 100644
--- a/x/evm/embeds/embeds.go
+++ b/x/evm/embeds/embeds.go
@@ -19,7 +19,7 @@ var (
erc20MinterContractJSON []byte
//go:embed artifacts/contracts/IOracle.sol/IOracle.json
oracleContractJSON []byte
- //go:embed artifacts/contracts/FunToken.sol/IFunToken.json
+ //go:embed artifacts/contracts/IFunToken.sol/IFunToken.json
funtokenPrecompileJSON []byte
//go:embed artifacts/contracts/Wasm.sol/IWasm.json
wasmPrecompileJSON []byte
@@ -29,6 +29,10 @@ var (
testErc20MaliciousNameJson []byte
//go:embed artifacts/contracts/TestERC20MaliciousTransfer.sol/TestERC20MaliciousTransfer.json
testErc20MaliciousTransferJson []byte
+ //go:embed artifacts/contracts/TestERC20TransferThenPrecompileSend.sol/TestERC20TransferThenPrecompileSend.json
+ testERC20TransferThenPrecompileSendJson []byte
+ //go:embed artifacts/contracts/TestNativeSendThenPrecompileSend.sol/TestNativeSendThenPrecompileSend.json
+ testNativeSendThenPrecompileSendJson []byte
)
var (
@@ -40,10 +44,10 @@ var (
}
// SmartContract_Funtoken: Precompile contract interface for
- // "FunToken.sol". This precompile enables transfers of ERC20 tokens
+ // "IFunToken.sol". This precompile enables transfers of ERC20 tokens
// to non-EVM accounts. Only the ABI is used.
SmartContract_FunToken = CompiledEvmContract{
- Name: "FunToken.sol",
+ Name: "IFunToken.sol",
EmbedJSON: funtokenPrecompileJSON,
}
@@ -76,6 +80,22 @@ var (
Name: "TestERC20MaliciousTransfer.sol",
EmbedJSON: testErc20MaliciousTransferJson,
}
+ // SmartContract_TestNativeSendThenPrecompileSendJson is a test contract
+ // that performs two sends in a single call: a native nibi send and a precompile bankSend.
+ // It tests a race condition where the state DB commit
+ // may overwrite the state after the precompile execution, potentially causing a loss of funds.
+ SmartContract_TestNativeSendThenPrecompileSendJson = CompiledEvmContract{
+ Name: "TestNativeSendThenPrecompileSend.sol",
+ EmbedJSON: testNativeSendThenPrecompileSendJson,
+ }
+ // SmartContract_TestERC20TransferThenPrecompileSend is a test contract
+ // that performs two sends in a single call: an erc20 token transfer and a precompile bankSend.
+ // It tests a race condition where the state DB commit
+ // may overwrite the state after the precompile execution, potentially causing an infinite token mint.
+ SmartContract_TestERC20TransferThenPrecompileSend = CompiledEvmContract{
+ Name: "TestERC20TransferThenPrecompileSend.sol",
+ EmbedJSON: testERC20TransferThenPrecompileSendJson,
+ }
)
func init() {
@@ -86,6 +106,8 @@ func init() {
SmartContract_TestERC20.MustLoad()
SmartContract_TestERC20MaliciousName.MustLoad()
SmartContract_TestERC20MaliciousTransfer.MustLoad()
+ SmartContract_TestNativeSendThenPrecompileSendJson.MustLoad()
+ SmartContract_TestERC20TransferThenPrecompileSend.MustLoad()
}
type CompiledEvmContract struct {
diff --git a/x/evm/embeds/embeds_test.go b/x/evm/embeds/embeds_test.go
index 586ff68b9..94f208dbb 100644
--- a/x/evm/embeds/embeds_test.go
+++ b/x/evm/embeds/embeds_test.go
@@ -16,5 +16,7 @@ func TestLoadContracts(t *testing.T) {
embeds.SmartContract_TestERC20.MustLoad()
embeds.SmartContract_TestERC20MaliciousName.MustLoad()
embeds.SmartContract_TestERC20MaliciousTransfer.MustLoad()
+ embeds.SmartContract_TestNativeSendThenPrecompileSendJson.MustLoad()
+ embeds.SmartContract_TestERC20TransferThenPrecompileSend.MustLoad()
})
}
diff --git a/x/evm/evmtest/test_deps.go b/x/evm/evmtest/test_deps.go
index 1810b1c8f..44fb34c31 100644
--- a/x/evm/evmtest/test_deps.go
+++ b/x/evm/evmtest/test_deps.go
@@ -46,7 +46,8 @@ func NewTestDeps() TestDeps {
}
func (deps TestDeps) StateDB() *statedb.StateDB {
- return statedb.New(deps.Ctx, &deps.App.EvmKeeper,
+ return deps.EvmKeeper.NewStateDB(
+ deps.Ctx,
statedb.NewEmptyTxConfig(
gethcommon.BytesToHash(deps.Ctx.HeaderHash().Bytes()),
),
diff --git a/x/evm/evmtest/tx.go b/x/evm/evmtest/tx.go
index dde679851..6b876f16d 100644
--- a/x/evm/evmtest/tx.go
+++ b/x/evm/evmtest/tx.go
@@ -26,95 +26,6 @@ import (
"github.com/NibiruChain/nibiru/v2/x/evm/embeds"
)
-type GethTxType = uint8
-
-func TxTemplateAccessListTx() *gethcore.AccessListTx {
- return &gethcore.AccessListTx{
- GasPrice: big.NewInt(1),
- Gas: gethparams.TxGas,
- To: &gethcommon.Address{},
- Value: big.NewInt(0),
- Data: []byte{},
- }
-}
-
-func TxTemplateLegacyTx() *gethcore.LegacyTx {
- return &gethcore.LegacyTx{
- GasPrice: big.NewInt(1),
- Gas: gethparams.TxGas,
- To: &gethcommon.Address{},
- Value: big.NewInt(0),
- Data: []byte{},
- }
-}
-
-func TxTemplateDynamicFeeTx() *gethcore.DynamicFeeTx {
- return &gethcore.DynamicFeeTx{
- GasFeeCap: big.NewInt(10),
- GasTipCap: big.NewInt(2),
- Gas: gethparams.TxGas,
- To: &gethcommon.Address{},
- Value: big.NewInt(0),
- Data: []byte{},
- }
-}
-
-func NewEthTxMsgFromTxData(
- deps *TestDeps,
- txType GethTxType,
- innerTxData []byte,
- nonce uint64,
- to *gethcommon.Address,
- value *big.Int,
- gas uint64,
- accessList gethcore.AccessList,
-) (*evm.MsgEthereumTx, error) {
- if innerTxData == nil {
- innerTxData = []byte{}
- }
-
- var ethCoreTx *gethcore.Transaction
- switch txType {
- case gethcore.LegacyTxType:
- innerTx := TxTemplateLegacyTx()
- innerTx.Nonce = nonce
- innerTx.Data = innerTxData
- innerTx.To = to
- innerTx.Value = value
- innerTx.Gas = gas
- ethCoreTx = gethcore.NewTx(innerTx)
- case gethcore.AccessListTxType:
- innerTx := TxTemplateAccessListTx()
- innerTx.Nonce = nonce
- innerTx.Data = innerTxData
- innerTx.AccessList = accessList
- innerTx.To = to
- innerTx.Value = value
- innerTx.Gas = gas
- ethCoreTx = gethcore.NewTx(innerTx)
- case gethcore.DynamicFeeTxType:
- innerTx := TxTemplateDynamicFeeTx()
- innerTx.Nonce = nonce
- innerTx.Data = innerTxData
- innerTx.To = to
- innerTx.Value = value
- innerTx.Gas = gas
- innerTx.AccessList = accessList
- ethCoreTx = gethcore.NewTx(innerTx)
- default:
- return nil, fmt.Errorf(
- "received unknown tx type (%v) in NewEthTxMsgFromTxData", txType)
- }
-
- ethTxMsg := new(evm.MsgEthereumTx)
- if err := ethTxMsg.FromEthereumTx(ethCoreTx); err != nil {
- return ethTxMsg, err
- }
-
- ethTxMsg.From = deps.Sender.EthAddr.Hex()
- return ethTxMsg, ethTxMsg.Sign(deps.GethSigner(), deps.Sender.KeyringSigner)
-}
-
// ExecuteNibiTransfer executes nibi transfer
func ExecuteNibiTransfer(deps *TestDeps, t *testing.T) *evm.MsgEthereumTx {
nonce := deps.StateDB().GetNonce(deps.Sender.EthAddr)
@@ -326,6 +237,10 @@ func TransferWei(
return err
}
+// --------------------------------------------------
+// Templates
+// --------------------------------------------------
+
// ValidLegacyTx: Useful initial condition for tests
// Exported only for use in tests.
func ValidLegacyTx() *evm.LegacyTx {
@@ -342,3 +257,113 @@ func ValidLegacyTx() *evm.LegacyTx {
S: []byte{},
}
}
+
+// GethTxType represents different Ethereum transaction types as defined in
+// go-ethereum, such as Legacy, AccessList, and DynamicFee transactions.
+type GethTxType = uint8
+
+func TxTemplateAccessListTx() *gethcore.AccessListTx {
+ return &gethcore.AccessListTx{
+ GasPrice: big.NewInt(1),
+ Gas: gethparams.TxGas,
+ To: &gethcommon.Address{},
+ Value: big.NewInt(0),
+ Data: []byte{},
+ }
+}
+
+func TxTemplateLegacyTx() *gethcore.LegacyTx {
+ return &gethcore.LegacyTx{
+ GasPrice: big.NewInt(1),
+ Gas: gethparams.TxGas,
+ To: &gethcommon.Address{},
+ Value: big.NewInt(0),
+ Data: []byte{},
+ }
+}
+
+func TxTemplateDynamicFeeTx() *gethcore.DynamicFeeTx {
+ return &gethcore.DynamicFeeTx{
+ GasFeeCap: big.NewInt(10),
+ GasTipCap: big.NewInt(2),
+ Gas: gethparams.TxGas,
+ To: &gethcommon.Address{},
+ Value: big.NewInt(0),
+ Data: []byte{},
+ }
+}
+
+// NewEthTxMsgFromTxData creates an Ethereum transaction message based on
+// the specified txType (Legacy, AccessList, or DynamicFee). This function
+// populates transaction fields like nonce, recipient, value, and gas, with
+// an optional access list for AccessList and DynamicFee types. The transaction
+// is signed using the provided dependencies.
+//
+// Parameters:
+// - deps: Required dependencies including the sender address and signer.
+// - txType: Transaction type (Legacy, AccessList, or DynamicFee).
+// - innerTxData: Byte slice of transaction data (input).
+// - nonce: Transaction nonce.
+// - to: Recipient address.
+// - value: ETH value (in wei) to transfer.
+// - gas: Gas limit for the transaction.
+// - accessList: Access list for AccessList and DynamicFee types.
+//
+// Returns:
+// - *evm.MsgEthereumTx: Ethereum transaction message ready for submission.
+// - error: Any error encountered during creation or signing.
+func NewEthTxMsgFromTxData(
+ deps *TestDeps,
+ txType GethTxType,
+ innerTxData []byte,
+ nonce uint64,
+ to *gethcommon.Address,
+ value *big.Int,
+ gas uint64,
+ accessList gethcore.AccessList,
+) (*evm.MsgEthereumTx, error) {
+ if innerTxData == nil {
+ innerTxData = []byte{}
+ }
+
+ var ethCoreTx *gethcore.Transaction
+ switch txType {
+ case gethcore.LegacyTxType:
+ innerTx := TxTemplateLegacyTx()
+ innerTx.Nonce = nonce
+ innerTx.Data = innerTxData
+ innerTx.To = to
+ innerTx.Value = value
+ innerTx.Gas = gas
+ ethCoreTx = gethcore.NewTx(innerTx)
+ case gethcore.AccessListTxType:
+ innerTx := TxTemplateAccessListTx()
+ innerTx.Nonce = nonce
+ innerTx.Data = innerTxData
+ innerTx.AccessList = accessList
+ innerTx.To = to
+ innerTx.Value = value
+ innerTx.Gas = gas
+ ethCoreTx = gethcore.NewTx(innerTx)
+ case gethcore.DynamicFeeTxType:
+ innerTx := TxTemplateDynamicFeeTx()
+ innerTx.Nonce = nonce
+ innerTx.Data = innerTxData
+ innerTx.To = to
+ innerTx.Value = value
+ innerTx.Gas = gas
+ innerTx.AccessList = accessList
+ ethCoreTx = gethcore.NewTx(innerTx)
+ default:
+ return nil, fmt.Errorf(
+ "received unknown tx type (%v) in NewEthTxMsgFromTxData", txType)
+ }
+
+ ethTxMsg := new(evm.MsgEthereumTx)
+ if err := ethTxMsg.FromEthereumTx(ethCoreTx); err != nil {
+ return ethTxMsg, err
+ }
+
+ ethTxMsg.From = deps.Sender.EthAddr.Hex()
+ return ethTxMsg, ethTxMsg.Sign(deps.GethSigner(), deps.Sender.KeyringSigner)
+}
diff --git a/x/evm/keeper/erc20.go b/x/evm/keeper/erc20.go
index ddef90c2f..58b05c35c 100644
--- a/x/evm/keeper/erc20.go
+++ b/x/evm/keeper/erc20.go
@@ -248,11 +248,8 @@ func (k Keeper) CallContractWithInput(
// sent by a user
txConfig := k.TxConfig(ctx, gethcommon.BigToHash(big.NewInt(0)))
- // Using tmp context to not modify the state in case of evm revert
- tmpCtx, commitCtx := ctx.CacheContext()
-
evmResp, evmObj, err = k.ApplyEvmMsg(
- tmpCtx, evmMsg, evm.NewNoOpTracer(), commit, evmCfg, txConfig,
+ ctx, evmMsg, evm.NewNoOpTracer(), commit, evmCfg, txConfig,
)
if err != nil {
// We don't know the actual gas used, so consuming the gas limit
@@ -275,7 +272,6 @@ func (k Keeper) CallContractWithInput(
} else {
// Success, committing the state to ctx
if commit {
- commitCtx()
totalGasUsed, err := k.AddToBlockGasUsed(ctx, evmResp.GasUsed)
if err != nil {
k.ResetGasMeterAndConsumeGas(ctx, ctx.GasMeter().Limit())
diff --git a/x/evm/keeper/funtoken_from_coin_test.go b/x/evm/keeper/funtoken_from_coin_test.go
index 1f5e3a85a..3b4b17f7a 100644
--- a/x/evm/keeper/funtoken_from_coin_test.go
+++ b/x/evm/keeper/funtoken_from_coin_test.go
@@ -169,45 +169,13 @@ func (s *FunTokenFromCoinSuite) TestCreateFunTokenFromCoin() {
func (s *FunTokenFromCoinSuite) TestConvertCoinToEvmAndBack() {
deps := evmtest.NewTestDeps()
alice := evmtest.NewEthPrivAcc()
- bankDenom := "unibi"
+ bankDenom := evm.EVMBankDenom
- s.T().Log("Setup: Create a coin in the bank state")
- deps.App.BankKeeper.SetDenomMetaData(deps.Ctx, bank.Metadata{
- DenomUnits: []*bank.DenomUnit{
- {
- Denom: bankDenom,
- Exponent: 0,
- Aliases: nil,
- },
- },
- Base: bankDenom,
- Display: bankDenom,
- Name: bankDenom,
- Symbol: "TOKEN",
- })
-
- s.T().Log("Give the sender funds")
- s.Require().NoError(testapp.FundAccount(
- deps.App.BankKeeper,
- deps.Ctx,
- deps.Sender.NibiruAddr,
- deps.EvmKeeper.FeeForCreateFunToken(deps.Ctx).Add(sdk.NewCoin(bankDenom, sdk.NewInt(100))),
- ))
-
- s.T().Log("Create FunToken mapping and ERC20")
- createFunTokenResp, err := deps.EvmKeeper.CreateFunToken(
- sdk.WrapSDKContext(deps.Ctx),
- &evm.MsgCreateFunToken{
- FromBankDenom: bankDenom,
- Sender: deps.Sender.NibiruAddr.String(),
- },
- )
- s.Require().NoError(err)
-
- funTokenErc20Addr := createFunTokenResp.FuntokenMapping.Erc20Addr
+ // Initial setup
+ funTokenErc20Addr := s.fundAndCreateFunToken(deps, 100)
s.T().Log("Convert bank coin to erc-20")
- _, err = deps.EvmKeeper.ConvertCoinToEvm(
+ _, err := deps.EvmKeeper.ConvertCoinToEvm(
sdk.WrapSDKContext(deps.Ctx),
&evm.MsgConvertCoinToEvm{
Sender: deps.Sender.NibiruAddr.String(),
@@ -299,6 +267,217 @@ func (s *FunTokenFromCoinSuite) TestConvertCoinToEvmAndBack() {
s.Require().ErrorContains(err, "transfer amount exceeds balance")
}
+// TestNativeSendThenPrecompileSend
+// 1. Creates a funtoken from coin.
+// 2. Using the test contract, performs two sends in a single call: a native nibi send and a precompile bankSend.
+// It tests a race condition where the state DB commit may overwrite the state after the precompile execution,
+// potentially causing a loss of funds.
+//
+// INITIAL STATE:
+// - Test contract funds: 10 NIBI, 10 WNIBI
+// CONTRACT CALL:
+// - Sends 10 NIBI natively and 10 WNIBI -> NIBI to Alice using precompile
+// EXPECTED:
+// - Test contract funds: 0 NIBI, 0 WNIBI
+// - Alice: 20 NIBI
+// - Module account: 0 NIBI escrowed
+func (s *FunTokenFromCoinSuite) TestNativeSendThenPrecompileSend() {
+ deps := evmtest.NewTestDeps()
+ bankDenom := evm.EVMBankDenom
+
+ // Initial setup
+ funTokenErc20Addr := s.fundAndCreateFunToken(deps, 10e6)
+
+ s.T().Log("Deploy Test Contract")
+ deployResp, err := evmtest.DeployContract(
+ &deps,
+ embeds.SmartContract_TestNativeSendThenPrecompileSendJson,
+ funTokenErc20Addr.Address,
+ )
+ s.Require().NoError(err)
+
+ testContractAddr := deployResp.ContractAddr
+ testContractNibiAddr := eth.EthAddrToNibiruAddr(testContractAddr)
+
+ s.T().Log("Give the test contract 10 NIBI (native)")
+ s.Require().NoError(testapp.FundAccount(
+ deps.App.BankKeeper,
+ deps.Ctx,
+ testContractNibiAddr,
+ sdk.NewCoins(sdk.NewCoin(bankDenom, sdk.NewInt(10e6)))),
+ )
+
+ s.T().Log("Convert bank coin to erc-20: give test contract 10 WNIBI (erc20)")
+ _, err = deps.EvmKeeper.ConvertCoinToEvm(
+ sdk.WrapSDKContext(deps.Ctx),
+ &evm.MsgConvertCoinToEvm{
+ Sender: deps.Sender.NibiruAddr.String(),
+ BankCoin: sdk.NewCoin(bankDenom, sdk.NewInt(10e6)),
+ ToEthAddr: eth.EIP55Addr{Address: testContractAddr},
+ },
+ )
+ s.Require().NoError(err)
+
+ // Alice hex and Alice bech32 is the same address in different representation,
+ // so funds are expected to be available in Alice's bank wallet
+ alice := evmtest.NewEthPrivAcc()
+
+ s.T().Log("call test contract")
+ _, err = deps.EvmKeeper.CallContract(
+ deps.Ctx,
+ embeds.SmartContract_TestNativeSendThenPrecompileSendJson.ABI,
+ deps.Sender.EthAddr,
+ &testContractAddr,
+ true,
+ "nativeSendThenPrecompileSend",
+ alice.EthAddr,
+ evm.NativeToWei(big.NewInt(10e6)), // for native evm send: 18 decimals
+ alice.NibiruAddr.String(),
+ big.NewInt(10e6), // for precompile bankSend: 6 decimals
+ )
+ s.Require().NoError(err)
+
+ // Check 1: Alice has 20 NIBI in bank
+ aliceBankBalance := deps.App.BankKeeper.GetBalance(deps.Ctx, alice.NibiruAddr, bankDenom)
+ s.Require().Equal(sdk.NewInt(20e6), aliceBankBalance.Amount)
+
+ // Check 2: Alice has 0 WNIBI on ERC20
+ aliceERC20Balance, err := deps.EvmKeeper.ERC20().BalanceOf(funTokenErc20Addr.Address, alice.EthAddr, deps.Ctx)
+ s.Require().NoError(err)
+ s.Require().Zero(big.NewInt(0).Cmp(aliceERC20Balance))
+
+ // Check 3: test contract has 0 NIBI in bank
+ testContractBankBalance := deps.App.BankKeeper.GetBalance(deps.Ctx, testContractNibiAddr, bankDenom)
+ s.Require().Equal(sdk.NewInt(0), testContractBankBalance.Amount)
+
+ // Check 4: test contract has 0 WNIBI on ERC20
+ testContractERC20Balance, err := deps.EvmKeeper.ERC20().BalanceOf(funTokenErc20Addr.Address, testContractAddr, deps.Ctx)
+ s.Require().NoError(err)
+ s.Require().Zero(big.NewInt(0).Cmp(testContractERC20Balance))
+
+ // Check 5: module balance has 0 NIBI escrowed
+ moduleBalance := deps.App.BankKeeper.GetBalance(deps.Ctx, authtypes.NewModuleAddress(evm.ModuleName), bankDenom)
+ s.Require().Equal(sdk.NewInt(0), moduleBalance.Amount)
+}
+
+// TestERC20TransferThenPrecompileSend
+// 1. Creates a funtoken from coin.
+// 2. Using the test contract, performs two sends in a single call: a erc20 transfer and a precompile bankSend.
+// It tests a race condition where the state DB commit may overwrite the state after the precompile execution,
+// potentially causing an infinite minting of funds.
+//
+// INITIAL STATE:
+// - Test contract funds: 10 WNIBI
+// CONTRACT CALL:
+// - Sends 1 WNIBI to Alice using erc20 transfer and 9 WNIBI -> NIBI to Alice using precompile
+// EXPECTED:
+// - Test contract funds: 0 WNIBI
+// - Alice: 1 WNIBI, 9 NIBI
+// - Module account: 1 NIBI escrowed (which Alice holds as 1 WNIBI)
+func (s *FunTokenFromCoinSuite) TestERC20TransferThenPrecompileSend() {
+ deps := evmtest.NewTestDeps()
+ bankDenom := evm.EVMBankDenom
+
+ // Initial setup
+ funTokenErc20Addr := s.fundAndCreateFunToken(deps, 10e6)
+
+ s.T().Log("Deploy Test Contract")
+ deployResp, err := evmtest.DeployContract(
+ &deps,
+ embeds.SmartContract_TestERC20TransferThenPrecompileSend,
+ funTokenErc20Addr.Address,
+ )
+ s.Require().NoError(err)
+
+ testContractAddr := deployResp.ContractAddr
+
+ s.T().Log("Convert bank coin to erc-20: give test contract 10 WNIBI (erc20)")
+ _, err = deps.EvmKeeper.ConvertCoinToEvm(
+ sdk.WrapSDKContext(deps.Ctx),
+ &evm.MsgConvertCoinToEvm{
+ Sender: deps.Sender.NibiruAddr.String(),
+ BankCoin: sdk.NewCoin(bankDenom, sdk.NewInt(10e6)),
+ ToEthAddr: eth.EIP55Addr{Address: testContractAddr},
+ },
+ )
+ s.Require().NoError(err)
+
+ // Alice hex and Alice bech32 is the same address in different representation
+ alice := evmtest.NewEthPrivAcc()
+
+ s.T().Log("call test contract")
+ _, err = deps.EvmKeeper.CallContract(
+ deps.Ctx,
+ embeds.SmartContract_TestERC20TransferThenPrecompileSend.ABI,
+ deps.Sender.EthAddr,
+ &testContractAddr,
+ true,
+ "erc20TransferThenPrecompileSend",
+ alice.EthAddr,
+ big.NewInt(1e6), // erc20 created with 6 decimals
+ alice.NibiruAddr.String(),
+ big.NewInt(9e6), // for precompile bankSend: 6 decimals
+ )
+ s.Require().NoError(err)
+
+ // Check 1: Alice has 9 NIBI in bank
+ aliceBankBalance := deps.App.BankKeeper.GetBalance(deps.Ctx, alice.NibiruAddr, bankDenom)
+ s.Require().Equal(sdk.NewInt(9e6), aliceBankBalance.Amount)
+
+ // Check 2: Alice has 1 WNIBI on ERC20
+ aliceERC20Balance, err := deps.EvmKeeper.ERC20().BalanceOf(funTokenErc20Addr.Address, alice.EthAddr, deps.Ctx)
+ s.Require().NoError(err)
+ s.Require().Zero(big.NewInt(1e6).Cmp(aliceERC20Balance))
+
+ // Check 3: test contract has 0 WNIBI on ERC20
+ testContractERC20Balance, err := deps.EvmKeeper.ERC20().BalanceOf(funTokenErc20Addr.Address, testContractAddr, deps.Ctx)
+ s.Require().NoError(err)
+ s.Require().Zero(big.NewInt(0).Cmp(testContractERC20Balance))
+
+ // Check 4: module balance has 1 NIBI escrowed (which Alice holds as 1 WNIBI)
+ moduleBalance := deps.App.BankKeeper.GetBalance(deps.Ctx, authtypes.NewModuleAddress(evm.ModuleName), bankDenom)
+ s.Require().Equal(sdk.NewInt(1e6), moduleBalance.Amount)
+}
+
+// fundAndCreateFunToken creates initial setup for tests
+func (s *FunTokenFromCoinSuite) fundAndCreateFunToken(deps evmtest.TestDeps, unibiAmount int64) eth.EIP55Addr {
+ bankDenom := evm.EVMBankDenom
+
+ s.T().Log("Setup: Create a coin in the bank state")
+ deps.App.BankKeeper.SetDenomMetaData(deps.Ctx, bank.Metadata{
+ DenomUnits: []*bank.DenomUnit{
+ {
+ Denom: bankDenom,
+ Exponent: 0,
+ Aliases: nil,
+ },
+ },
+ Base: bankDenom,
+ Display: bankDenom,
+ Name: bankDenom,
+ Symbol: "NIBI",
+ })
+
+ s.T().Log("Give the sender funds for funtoken creation and funding test contract")
+ s.Require().NoError(testapp.FundAccount(
+ deps.App.BankKeeper,
+ deps.Ctx,
+ deps.Sender.NibiruAddr,
+ deps.EvmKeeper.FeeForCreateFunToken(deps.Ctx).Add(sdk.NewCoin(bankDenom, sdk.NewInt(unibiAmount))),
+ ))
+
+ s.T().Log("Create FunToken from coin")
+ createFunTokenResp, err := deps.EvmKeeper.CreateFunToken(
+ sdk.WrapSDKContext(deps.Ctx),
+ &evm.MsgCreateFunToken{
+ FromBankDenom: bankDenom,
+ Sender: deps.Sender.NibiruAddr.String(),
+ },
+ )
+ s.Require().NoError(err)
+ return createFunTokenResp.FuntokenMapping.Erc20Addr
+}
+
type FunTokenFromCoinSuite struct {
suite.Suite
}
diff --git a/x/evm/keeper/keeper.go b/x/evm/keeper/keeper.go
index 49ea0c9bf..dd31229fd 100644
--- a/x/evm/keeper/keeper.go
+++ b/x/evm/keeper/keeper.go
@@ -15,6 +15,7 @@ import (
"github.com/cosmos/cosmos-sdk/codec"
storetypes "github.com/cosmos/cosmos-sdk/store/types"
sdk "github.com/cosmos/cosmos-sdk/types"
+ bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper"
gethcommon "github.com/ethereum/go-ethereum/common"
"github.com/NibiruChain/nibiru/v2/app/appconst"
@@ -40,7 +41,7 @@ type Keeper struct {
// this should be the x/gov module account.
authority sdk.AccAddress
- bankKeeper evm.BankKeeper
+ bankKeeper bankkeeper.Keeper
accountKeeper evm.AccountKeeper
stakingKeeper evm.StakingKeeper
@@ -63,13 +64,14 @@ func NewKeeper(
storeKey, transientKey storetypes.StoreKey,
authority sdk.AccAddress,
accKeeper evm.AccountKeeper,
- bankKeeper evm.BankKeeper,
+ bankKeeper bankkeeper.Keeper,
stakingKeeper evm.StakingKeeper,
tracer string,
) Keeper {
if err := sdk.VerifyAddressFormat(authority); err != nil {
panic(err)
}
+
return Keeper{
cdc: cdc,
storeKey: storeKey,
diff --git a/x/evm/keeper/msg_server.go b/x/evm/keeper/msg_server.go
index cbeea3866..4587c5910 100644
--- a/x/evm/keeper/msg_server.go
+++ b/x/evm/keeper/msg_server.go
@@ -252,7 +252,7 @@ func (k *Keeper) ApplyEvmMsg(ctx sdk.Context,
vmErr error // vm errors do not effect consensus and are therefore not assigned to err
)
- stateDB := statedb.New(ctx, k, txConfig)
+ stateDB := k.NewStateDB(ctx, txConfig)
evmObj = k.NewEVM(ctx, msg, evmConfig, tracer, stateDB)
leftoverGas := msg.Gas()
diff --git a/x/evm/keeper/precompiles.go b/x/evm/keeper/precompiles.go
index 965866660..3b1a0e480 100644
--- a/x/evm/keeper/precompiles.go
+++ b/x/evm/keeper/precompiles.go
@@ -21,3 +21,7 @@ func (k *Keeper) AddPrecompiles(
}
}
}
+
+func (k *Keeper) IsPrecompile(addr gethcommon.Address) bool {
+ return k.precompiles.Has(addr)
+}
diff --git a/x/evm/keeper/statedb.go b/x/evm/keeper/statedb.go
index 6eb46f990..7aff8c02d 100644
--- a/x/evm/keeper/statedb.go
+++ b/x/evm/keeper/statedb.go
@@ -17,6 +17,13 @@ import (
var _ statedb.Keeper = &Keeper{}
+func (k *Keeper) NewStateDB(
+ ctx sdk.Context,
+ txConfig statedb.TxConfig,
+) *statedb.StateDB {
+ return statedb.New(ctx, k, txConfig)
+}
+
// ----------------------------------------------------------------------------
// StateDB Keeper implementation
// ----------------------------------------------------------------------------
@@ -65,7 +72,10 @@ func (k *Keeper) ForEachStorage(
}
}
-// SetAccBalance update account's balance, compare with current balance first, then decide to mint or burn.
+// SetAccBalance update account's balance, compare with current balance first,
+// then decide to mint or burn.
+// Implements the `statedb.Keeper` interface.
+// Only called by `StateDB.Commit()`.
func (k *Keeper) SetAccBalance(
ctx sdk.Context, addr gethcommon.Address, amountEvmDenom *big.Int,
) error {
diff --git a/x/evm/precompile/funtoken.go b/x/evm/precompile/funtoken.go
index 26c7373f0..4b1e43266 100644
--- a/x/evm/precompile/funtoken.go
+++ b/x/evm/precompile/funtoken.go
@@ -23,7 +23,7 @@ import (
var _ vm.PrecompiledContract = (*precompileFunToken)(nil)
-// Precompile address for "FunToken.sol", the contract that
+// Precompile address for "IFunToken.sol", the contract that
// enables transfers of ERC20 tokens to "nibi" addresses as bank coins
// using the ERC20's `FunToken` mapping.
var PrecompileAddr_FunToken = gethcommon.HexToAddress("0x0000000000000000000000000000000000000800")
@@ -72,8 +72,7 @@ func (p precompileFunToken) Run(
if err != nil {
return nil, err
}
- // Dirty journal entries in `StateDB` must be committed
- return bz, start.StateDB.Commit()
+ return bz, err
}
func PrecompileFunToken(keepers keepers.PublicKeepers) vm.PrecompiledContract {
diff --git a/x/evm/precompile/precompile.go b/x/evm/precompile/precompile.go
index a6bbfefc4..40d0c74b4 100644
--- a/x/evm/precompile/precompile.go
+++ b/x/evm/precompile/precompile.go
@@ -147,6 +147,8 @@ type OnRunStartResult struct {
Method *gethabi.Method
StateDB *statedb.StateDB
+
+ PrecompileJournalEntry statedb.PrecompileCalled
}
// OnRunStart prepares the execution environment for a precompiled contract call.
@@ -188,14 +190,21 @@ func OnRunStart(
err = fmt.Errorf("failed to load the sdk.Context from the EVM StateDB")
return
}
- ctx := stateDB.GetContext()
- if err = stateDB.Commit(); err != nil {
+
+ // journalEntry captures the state before precompile execution to enable
+ // proper state reversal if the call fails or if [statedb.JournalChange]
+ // is reverted in general.
+ cacheCtx, journalEntry := stateDB.CacheCtxForPrecompile(contract.Address())
+ if err = stateDB.SavePrecompileCalledJournalChange(contract.Address(), journalEntry); err != nil {
+ return res, err
+ }
+ if err = stateDB.CommitCacheCtx(); err != nil {
return res, fmt.Errorf("error committing dirty journal entries: %w", err)
}
return OnRunStartResult{
Args: args,
- Ctx: ctx,
+ Ctx: cacheCtx,
Method: method,
StateDB: stateDB,
}, nil
diff --git a/x/evm/precompile/test/export.go b/x/evm/precompile/test/export.go
index 966dd3359..28670e3e0 100644
--- a/x/evm/precompile/test/export.go
+++ b/x/evm/precompile/test/export.go
@@ -2,11 +2,14 @@ package test
import (
"encoding/json"
+ "math/big"
"os"
"os/exec"
"path"
"strings"
+ serverconfig "github.com/NibiruChain/nibiru/v2/app/server/config"
+
wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper"
wasm "github.com/CosmWasm/wasmd/x/wasm/types"
"github.com/ethereum/go-ethereum/core/vm"
@@ -267,6 +270,7 @@ func IncrementWasmCounterWithExecuteMulti(
deps *evmtest.TestDeps,
wasmContract sdk.AccAddress,
times uint,
+ finalizeTx bool,
) (evmObj *vm.EVM) {
msgArgsBz := []byte(`
{
@@ -308,9 +312,68 @@ func IncrementWasmCounterWithExecuteMulti(
s.Require().NoError(err)
ethTxResp, evmObj, err := deps.EvmKeeper.CallContractWithInput(
- deps.Ctx, deps.Sender.EthAddr, &precompile.PrecompileAddr_Wasm, true, input,
+ deps.Ctx, deps.Sender.EthAddr, &precompile.PrecompileAddr_Wasm, finalizeTx, input,
)
s.Require().NoError(err)
s.Require().NotEmpty(ethTxResp.Ret)
return evmObj
}
+
+func IncrementWasmCounterWithExecuteMultiViaVMCall(
+ s *suite.Suite,
+ deps *evmtest.TestDeps,
+ wasmContract sdk.AccAddress,
+ times uint,
+ finalizeTx bool,
+ evmObj *vm.EVM,
+) error {
+ 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)
+
+ contract := precompile.PrecompileAddr_Wasm
+ leftoverGas := serverconfig.DefaultEthCallGasLimit
+ _, _, err = evmObj.Call(
+ vm.AccountRef(deps.Sender.EthAddr),
+ contract,
+ input,
+ leftoverGas,
+ big.NewInt(0),
+ )
+ return err
+}
diff --git a/x/evm/precompile/wasm.go b/x/evm/precompile/wasm.go
index 10817c673..a7b21684c 100644
--- a/x/evm/precompile/wasm.go
+++ b/x/evm/precompile/wasm.go
@@ -62,9 +62,7 @@ func (p precompileWasm) Run(
if err != nil {
return nil, err
}
-
- // Dirty journal entries in `StateDB` must be committed
- return bz, start.StateDB.Commit()
+ return bz, err
}
type precompileWasm struct {
diff --git a/x/evm/precompile/wasm_test.go b/x/evm/precompile/wasm_test.go
index d796f8b89..dbe35b839 100644
--- a/x/evm/precompile/wasm_test.go
+++ b/x/evm/precompile/wasm_test.go
@@ -100,13 +100,13 @@ func (s *WasmSuite) TestExecuteMultiHappy() {
test.AssertWasmCounterState(&s.Suite, deps, wasmContract, 0)
// count += 2
test.IncrementWasmCounterWithExecuteMulti(
- &s.Suite, &deps, wasmContract, 2)
+ &s.Suite, &deps, wasmContract, 2, true)
// count = 2
test.AssertWasmCounterState(&s.Suite, deps, wasmContract, 2)
s.assertWasmCounterStateRaw(deps, wasmContract, 2)
// count += 67
test.IncrementWasmCounterWithExecuteMulti(
- &s.Suite, &deps, wasmContract, 67)
+ &s.Suite, &deps, wasmContract, 67, true)
// count = 69
test.AssertWasmCounterState(&s.Suite, deps, wasmContract, 69)
s.assertWasmCounterStateRaw(deps, wasmContract, 69)
diff --git a/x/evm/statedb/access_list.go b/x/evm/statedb/access_list.go
index 4513a9164..f62b45171 100644
--- a/x/evm/statedb/access_list.go
+++ b/x/evm/statedb/access_list.go
@@ -1,3 +1,5 @@
+package statedb
+
// Copyright 2020 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
@@ -14,8 +16,6 @@
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see .
-package statedb
-
import (
"github.com/ethereum/go-ethereum/common"
)
diff --git a/x/evm/statedb/config.go b/x/evm/statedb/config.go
index 887f591c5..417e480ac 100644
--- a/x/evm/statedb/config.go
+++ b/x/evm/statedb/config.go
@@ -1,6 +1,7 @@
-// Copyright (c) 2023-2024 Nibi, Inc.
package statedb
+// Copyright (c) 2023-2024 Nibi, Inc.
+
import (
"math/big"
diff --git a/x/evm/statedb/debug.go b/x/evm/statedb/debug.go
new file mode 100644
index 000000000..c2b5fb968
--- /dev/null
+++ b/x/evm/statedb/debug.go
@@ -0,0 +1,39 @@
+package statedb
+
+// Copyright (c) 2023-2024 Nibi, Inc.
+
+import (
+ "github.com/ethereum/go-ethereum/common"
+)
+
+// DebugDirtiesCount 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) DebugDirtiesCount() int {
+ dirtiesCount := 0
+ for _, dirtyCount := range s.Journal.dirties {
+ dirtiesCount += dirtyCount
+ }
+ return dirtiesCount
+}
+
+// DebugDirties is a test helper that returns the journal's dirty account changes map.
+func (s *StateDB) DebugDirties() map[common.Address]int {
+ return s.Journal.dirties
+}
+
+// DebugEntries is a test helper that returns the sequence of [JournalChange]
+// objects added during execution.
+func (s *StateDB) DebugEntries() []JournalChange {
+ return s.Journal.entries
+}
+
+// DebugStateObjects is a test helper that returns returns a copy of the
+// [StateDB.stateObjects] map.
+func (s *StateDB) DebugStateObjects() map[common.Address]*stateObject {
+ copyOfMap := make(map[common.Address]*stateObject)
+ for key, val := range s.stateObjects {
+ copyOfMap[key] = val
+ }
+ return copyOfMap
+}
diff --git a/x/evm/statedb/interfaces.go b/x/evm/statedb/interfaces.go
index a4c1c3b59..c242771ca 100644
--- a/x/evm/statedb/interfaces.go
+++ b/x/evm/statedb/interfaces.go
@@ -1,22 +1,12 @@
-// Copyright (c) 2023-2024 Nibi, Inc.
package statedb
+// Copyright (c) 2023-2024 Nibi, Inc.
+
import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/ethereum/go-ethereum/common"
- "github.com/ethereum/go-ethereum/core/vm"
)
-// ExtStateDB defines an extension to the interface provided by the go-ethereum
-// codebase to support additional state transition functionalities. In particular
-// it supports appending a new entry to the state journal through
-// AppendJournalEntry so that the state can be reverted after running
-// stateful precompiled contracts.
-type ExtStateDB interface {
- vm.StateDB
- AppendJournalEntry(JournalChange)
-}
-
// Keeper provide underlying storage of StateDB
type Keeper interface {
// GetAccount: Ethereum account getter for a [statedb.Account].
@@ -38,4 +28,6 @@ type Keeper interface {
// DeleteAccount handles contract's suicide call, clearing the balance,
// contract bytecode, contract state, and its native account.
DeleteAccount(ctx sdk.Context, addr common.Address) error
+
+ IsPrecompile(addr common.Address) bool
}
diff --git a/x/evm/statedb/journal.go b/x/evm/statedb/journal.go
index ac041b617..acbba5eab 100644
--- a/x/evm/statedb/journal.go
+++ b/x/evm/statedb/journal.go
@@ -1,3 +1,5 @@
+package statedb
+
// Copyright 2016 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
@@ -14,13 +16,13 @@
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see .
-package statedb
-
import (
"bytes"
"math/big"
"sort"
+ store "github.com/cosmos/cosmos-sdk/store/types"
+ sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/ethereum/go-ethereum/common"
)
@@ -91,25 +93,6 @@ func (j *journal) Length() int {
return len(j.entries)
}
-// 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
-}
-
-func (s *StateDB) Dirties() map[common.Address]int {
- return s.Journal.dirties
-}
-
-func (s *StateDB) Entries() []JournalChange {
- return s.Journal.entries
-}
-
// ------------------------------------------------------
// createObjectChange
@@ -335,3 +318,46 @@ func (ch accessListAddSlotChange) Revert(s *StateDB) {
func (ch accessListAddSlotChange) Dirtied() *common.Address {
return nil
}
+
+// ------------------------------------------------------
+// PrecompileSnapshotBeforeRun
+
+// PrecompileCalled: Precompiles can alter persistent storage of other
+// modules. These changes to persistent storage are not reverted by a `Revert` of
+// [JournalChange] by default, as it generally manages only changes to accounts
+// and Bank balances for ether (NIBI).
+//
+// As a workaround to make state changes from precompiles reversible, we store
+// [PrecompileCalled] snapshots that sync and record the prior state
+// of the other modules, allowing precompile calls to truly be reverted.
+//
+// As a simple example, suppose that a transaction calls a precompile.
+// 1. If the precompile changes the state in the Bank Module or Wasm module
+// 2. The call gets reverted (`revert()` in Solidity), which shoud restore the
+// state to a in-memory snapshot recorded on the StateDB journal.
+// 3. This could cause a problem where changes to the rest of the blockchain state
+// are still in effect following the reversion in the EVM state DB.
+type PrecompileCalled struct {
+ MultiStore store.CacheMultiStore
+ Events sdk.Events
+}
+
+var _ JournalChange = PrecompileCalled{}
+
+// Revert rolls back the [StateDB] cache context to the state it was in prior to
+// the precompile call. Modifications to this cache context are pushed to the
+// commit context (s.evmTxCtx) when [StateDB.Commit] is executed.
+func (ch PrecompileCalled) Revert(s *StateDB) {
+ s.cacheCtx = s.cacheCtx.WithMultiStore(ch.MultiStore)
+ // Rewrite the `writeCacheCtxFn` using the same logic as sdk.Context.CacheCtx
+ s.writeToCommitCtxFromCacheCtx = func() {
+ s.evmTxCtx.EventManager().EmitEvents(ch.Events)
+ // TODO: Check correctness of the emitted events
+ // https://github.com/NibiruChain/nibiru/issues/2096
+ ch.MultiStore.Write()
+ }
+}
+
+func (ch PrecompileCalled) Dirtied() *common.Address {
+ return nil
+}
diff --git a/x/evm/statedb/journal_test.go b/x/evm/statedb/journal_test.go
index 5863face5..6390b640f 100644
--- a/x/evm/statedb/journal_test.go
+++ b/x/evm/statedb/journal_test.go
@@ -6,10 +6,12 @@ import (
"strings"
"testing"
+ "github.com/MakeNowJust/heredoc/v2"
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"
"github.com/NibiruChain/nibiru/v2/x/common/testutil/testapp"
"github.com/NibiruChain/nibiru/v2/x/evm"
"github.com/NibiruChain/nibiru/v2/x/evm/embeds"
@@ -18,7 +20,7 @@ import (
"github.com/NibiruChain/nibiru/v2/x/evm/statedb"
)
-func (s *Suite) TestPrecompileSnapshots() {
+func (s *Suite) TestComplexJournalChanges() {
deps := evmtest.NewTestDeps()
bankDenom := evm.EVMBankDenom
s.Require().NoError(testapp.FundAccount(
@@ -32,25 +34,11 @@ func (s *Suite) TestPrecompileSnapshots() {
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)
+ test.AssertWasmCounterState(
+ &s.Suite, deps, wasmContract, 0,
+ )
deployArgs := []any{"name", "SYMBOL", uint8(18)}
deployResp, err := evmtest.DeployContract(
@@ -71,7 +59,7 @@ func (s *Suite) TestPrecompileSnapshots() {
s.Run("Populate dirty journal entries. Remove with Commit", func() {
stateDB := evmObj.StateDB.(*statedb.StateDB)
- s.Equal(0, stateDB.DirtiesCount())
+ s.Equal(0, stateDB.DebugDirtiesCount())
randomAcc := evmtest.NewEthPrivAcc().EthAddr
balDelta := evm.NativeToWei(big.NewInt(4))
@@ -81,14 +69,15 @@ func (s *Suite) TestPrecompileSnapshots() {
stateDB.AddBalance(randomAcc, balDelta)
// 1 dirties from [balanceChange]
stateDB.SubBalance(randomAcc, balDelta)
- if stateDB.DirtiesCount() != 4 {
+ if stateDB.DebugDirtiesCount() != 4 {
debugDirtiesCountMismatch(stateDB, s.T())
s.FailNow("expected 4 dirty journal changes")
}
- err = stateDB.Commit() // Dirties should be gone
+ s.T().Log("StateDB.Commit, then Dirties should be gone")
+ err = stateDB.Commit()
s.NoError(err)
- if stateDB.DirtiesCount() != 0 {
+ if stateDB.DebugDirtiesCount() != 0 {
debugDirtiesCountMismatch(stateDB, s.T())
s.FailNow("expected 0 dirty journal changes")
}
@@ -110,7 +99,7 @@ func (s *Suite) TestPrecompileSnapshots() {
)
s.Require().NoError(err)
stateDB := evmObj.StateDB.(*statedb.StateDB)
- if stateDB.DirtiesCount() != 2 {
+ if stateDB.DebugDirtiesCount() != 2 {
debugDirtiesCountMismatch(stateDB, s.T())
s.FailNow("expected 2 dirty journal changes")
}
@@ -136,22 +125,105 @@ func (s *Suite) TestPrecompileSnapshots() {
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)
+ s.Run("Precompile calls populate snapshots", func() {
+ s.T().Log("commitEvmTx=true, expect 0 dirty journal entries")
+ commitEvmTx := true
+ evmObj = test.IncrementWasmCounterWithExecuteMulti(
+ &s.Suite, &deps, wasmContract, 7, commitEvmTx,
+ )
+ // assertions after run
+ test.AssertWasmCounterState(
+ &s.Suite, deps, wasmContract, 7,
+ )
stateDB, ok := evmObj.StateDB.(*statedb.StateDB)
s.Require().True(ok, "error retrieving StateDB from the EVM")
- if stateDB.DirtiesCount() != 0 {
+ if stateDB.DebugDirtiesCount() != 0 {
+ debugDirtiesCountMismatch(stateDB, s.T())
+ s.FailNow("expected 0 dirty journal changes")
+ }
+
+ s.T().Log("commitEvmTx=false, expect dirty journal entries")
+ commitEvmTx = false
+ evmObj = test.IncrementWasmCounterWithExecuteMulti(
+ &s.Suite, &deps, wasmContract, 5, commitEvmTx,
+ )
+ stateDB, ok = evmObj.StateDB.(*statedb.StateDB)
+ s.Require().True(ok, "error retrieving StateDB from the EVM")
+
+ s.T().Log("Expect exactly 0 dirty journal entry for the precompile snapshot")
+ if stateDB.DebugDirtiesCount() != 0 {
debugDirtiesCountMismatch(stateDB, s.T())
s.FailNow("expected 0 dirty journal changes")
}
+
+ s.T().Log("Expect no change since the StateDB has not been committed")
+ test.AssertWasmCounterState(
+ &s.Suite, deps, wasmContract, 7, // 7 = 7 + 0
+ )
+
+ s.T().Log("Expect change to persist on the StateDB cacheCtx")
+ cacheCtx := stateDB.GetCacheContext()
+ s.NotNil(cacheCtx)
+ deps.Ctx = *cacheCtx
+ test.AssertWasmCounterState(
+ &s.Suite, deps, wasmContract, 12, // 12 = 7 + 5
+ )
+ // NOTE: that the [StateDB.Commit] fn has not been called yet. We're still
+ // mid-transaction.
+
+ s.T().Log("EVM revert operation should bring about the old state")
+ err = test.IncrementWasmCounterWithExecuteMultiViaVMCall(
+ &s.Suite, &deps, wasmContract, 50, commitEvmTx, evmObj,
+ )
+ stateDBPtr := evmObj.StateDB.(*statedb.StateDB)
+ s.Require().Equal(stateDB, stateDBPtr)
+ s.Require().NoError(err)
+ s.T().Log(heredoc.Doc(`At this point, 2 precompile calls have succeeded.
+One that increments the counter to 7 + 5, and another for +50.
+The StateDB has not been committed. We expect to be able to revert to both
+snapshots and see the prior states.`))
+ cacheCtx = stateDB.GetCacheContext()
+ deps.Ctx = *cacheCtx
+ test.AssertWasmCounterState(
+ &s.Suite, deps, wasmContract, 7+5+50,
+ )
+
+ errFn := common.TryCatch(func() {
+ // There were only two EVM calls.
+ // Thus, there are only 2 snapshots: 0 and 1.
+ // We should not be able to revert to a third one.
+ stateDB.RevertToSnapshot(2)
+ })
+ s.Require().ErrorContains(errFn(), "revision id 2 cannot be reverted")
+
+ stateDB.RevertToSnapshot(1)
+ cacheCtx = stateDB.GetCacheContext()
+ s.NotNil(cacheCtx)
+ deps.Ctx = *cacheCtx
+ test.AssertWasmCounterState(
+ &s.Suite, deps, wasmContract, 7+5,
+ )
+
+ stateDB.RevertToSnapshot(0)
+ cacheCtx = stateDB.GetCacheContext()
+ s.NotNil(cacheCtx)
+ deps.Ctx = *cacheCtx
+ test.AssertWasmCounterState(
+ &s.Suite, deps, wasmContract, 7, // state before precompile called
+ )
+
+ err = stateDB.Commit()
+ deps.Ctx = stateDB.GetEvmTxContext()
+ test.AssertWasmCounterState(
+ &s.Suite, deps, wasmContract, 7, // state before precompile called
+ )
})
}
func debugDirtiesCountMismatch(db *statedb.StateDB, t *testing.T) string {
lines := []string{}
- dirties := db.Dirties()
- stateObjects := db.StateObjects()
+ dirties := db.DebugDirties()
+ stateObjects := db.DebugStateObjects()
for addr, dirtyCountForAddr := range dirties {
lines = append(lines, fmt.Sprintf("Dirty addr: %s, dirtyCountForAddr=%d", addr, dirtyCountForAddr))
diff --git a/x/evm/statedb/state_object.go b/x/evm/statedb/state_object.go
index e371beae0..28ba2d85a 100644
--- a/x/evm/statedb/state_object.go
+++ b/x/evm/statedb/state_object.go
@@ -1,6 +1,7 @@
-// Copyright (c) 2023-2024 Nibi, Inc.
package statedb
+// Copyright (c) 2023-2024 Nibi, Inc.
+
import (
"bytes"
"math/big"
@@ -198,7 +199,7 @@ func (s *stateObject) Code() []byte {
if bytes.Equal(s.CodeHash(), emptyCodeHash) {
return nil
}
- code := s.db.keeper.GetCode(s.db.ctx, common.BytesToHash(s.CodeHash()))
+ code := s.db.keeper.GetCode(s.db.evmTxCtx, common.BytesToHash(s.CodeHash()))
s.code = code
return code
}
@@ -260,7 +261,7 @@ func (s *stateObject) GetCommittedState(key common.Hash) common.Hash {
return value
}
// If no live objects are available, load it from keeper
- value := s.db.keeper.GetState(s.db.ctx, s.Address(), key)
+ value := s.db.keeper.GetState(s.db.evmTxCtx, s.Address(), key)
s.OriginStorage[key] = value
return value
}
diff --git a/x/evm/statedb/statedb.go b/x/evm/statedb/statedb.go
index 223e92edb..4c79e61af 100644
--- a/x/evm/statedb/statedb.go
+++ b/x/evm/statedb/statedb.go
@@ -1,11 +1,28 @@
-// Copyright (c) 2023-2024 Nibi, Inc.
+// The "evm/statedb" package implements a go-ethereum [vm.StateDB] with state
+// management and journal changes specific to the Nibiru EVM.
+//
+// This package plays a critical role in managing the state of accounts,
+// contracts, and storage while handling atomicity, caching, and state
+// modifications. It ensures that state transitions made during the
+// execution of smart contracts are either committed or reverted based
+// on transaction outcomes.
+//
+// StateDB structs used to store anything within the state tree, including
+// accounts, contracts, and contract storage.
+// Note that Nibiru's state tree is an IAVL tree, which differs from the Merkle
+// Patricia Trie structure seen on Ethereum mainnet.
+//
+// StateDBs also take care of caching and handling nested states.
package statedb
+// Copyright (c) 2023-2024 Nibi, Inc.
+
import (
"fmt"
"math/big"
"sort"
+ store "github.com/cosmos/cosmos-sdk/store/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/ethereum/go-ethereum/common"
gethcore "github.com/ethereum/go-ethereum/core/types"
@@ -13,14 +30,6 @@ import (
"github.com/ethereum/go-ethereum/crypto"
)
-// revision is the identifier of a version of state.
-// it consists of an auto-increment id and a journal index.
-// it's safer to use than using journal index alone.
-type revision struct {
- id int
- journalIndex int
-}
-
var _ vm.StateDB = &StateDB{}
// StateDB structs within the ethereum protocol are used to store anything
@@ -30,8 +39,9 @@ var _ vm.StateDB = &StateDB{}
// * Accounts
type StateDB struct {
keeper Keeper
- // ctx is the persistent context used for official `StateDB.Commit` calls.
- ctx sdk.Context
+
+ // evmTxCtx is the persistent context used for official `StateDB.Commit` calls.
+ evmTxCtx sdk.Context
// Journal of state modifications. This is the backbone of
// Snapshot and RevertToSnapshot.
@@ -43,6 +53,21 @@ type StateDB struct {
txConfig TxConfig
+ // cacheCtx: An sdk.Context produced from the [StateDB.ctx] with the
+ // multi-store cached and a new event manager. The cached context
+ // (`cacheCtx`) is written to the persistent context (`ctx`) when
+ // `writeCacheCtx` is called.
+ cacheCtx sdk.Context
+
+ // writeToCommitCtxFromCacheCtx is the "write" function received from
+ // `s.evmTxCtx.CacheContext()`. It saves mutations on s.cacheCtx to the StateDB's
+ // commit context (s.evmTxCtx). This synchronizes the multistore and event manager
+ // of the two contexts.
+ writeToCommitCtxFromCacheCtx func()
+
+ // The number of precompiled contract calls within the current transaction
+ multistoreCacheCount uint8
+
// The refund counter, also used by state transitioning.
refund uint64
@@ -57,7 +82,7 @@ type StateDB struct {
func New(ctx sdk.Context, keeper Keeper, txConfig TxConfig) *StateDB {
return &StateDB{
keeper: keeper,
- ctx: ctx,
+ evmTxCtx: ctx,
stateObjects: make(map[common.Address]*stateObject),
Journal: newJournal(),
accessList: newAccessList(),
@@ -66,14 +91,30 @@ func New(ctx sdk.Context, keeper Keeper, txConfig TxConfig) *StateDB {
}
}
+// revision is the identifier of a version of state.
+// it consists of an auto-increment id and a journal index.
+// it's safer to use than using journal index alone.
+type revision struct {
+ id int
+ journalIndex int
+}
+
// Keeper returns the underlying `Keeper`
func (s *StateDB) Keeper() Keeper {
return s.keeper
}
-// GetContext returns the transaction Context.
-func (s *StateDB) GetContext() sdk.Context {
- return s.ctx
+// GetEvmTxContext returns the EVM transaction context.
+func (s *StateDB) GetEvmTxContext() sdk.Context {
+ return s.evmTxCtx
+}
+
+// GetCacheContext: Getter for testing purposes.
+func (s *StateDB) GetCacheContext() *sdk.Context {
+ if s.writeToCommitCtxFromCacheCtx == nil {
+ return nil
+ }
+ return &s.cacheCtx
}
// AddLog adds a log, called by evm.
@@ -212,8 +253,9 @@ func (s *StateDB) getStateObject(addr common.Address) *stateObject {
if obj := s.stateObjects[addr]; obj != nil {
return obj
}
+
// If no live objects are available, load it from keeper
- account := s.keeper.GetAccount(s.ctx, addr)
+ account := s.keeper.GetAccount(s.evmTxCtx, addr)
if account == nil {
return nil
}
@@ -274,7 +316,7 @@ func (s *StateDB) ForEachStorage(addr common.Address, cb func(key, value common.
if so == nil {
return nil
}
- s.keeper.ForEachStorage(s.ctx, addr, func(key, value common.Hash) bool {
+ s.keeper.ForEachStorage(s.evmTxCtx, addr, func(key, value common.Hash) bool {
if value, dirty := so.DirtyStorage[key]; dirty {
return cb(key, value)
}
@@ -438,6 +480,9 @@ func (s *StateDB) Snapshot() int {
// RevertToSnapshot reverts all state changes made since the given revision.
func (s *StateDB) RevertToSnapshot(revid int) {
+ fmt.Printf("len(s.validRevisions): %d\n", len(s.validRevisions))
+ fmt.Printf("s.validRevisions: %v\n", s.validRevisions)
+
// Find the snapshot in the stack of valid snapshots.
idx := sort.Search(len(s.validRevisions), func(i int) bool {
return s.validRevisions[i].id >= revid
@@ -460,11 +505,37 @@ func errorf(format string, args ...any) error {
// 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.
+//
+// cacheCtxSyncNeeded: If one of the [Nibiru-Specific Precompiled Contracts] was
+// called, a [JournalChange] of type [PrecompileSnapshotBeforeRun] gets added and
+// we branch off a cache of the commit context (s.evmTxCtx).
+//
+// [Nibiru-Specific Precompiled Contracts]: https://nibiru.fi/docs/evm/precompiles/nibiru.html
func (s *StateDB) Commit() error {
- ctx := s.GetContext()
+ if s.writeToCommitCtxFromCacheCtx != nil {
+ s.writeToCommitCtxFromCacheCtx()
+ }
+ return s.commitCtx(s.GetEvmTxContext())
+}
+
+// CommitCacheCtx is identical to [StateDB.Commit], except it:
+// (1) uses the cacheCtx of the [StateDB] and
+// (2) does not save mutations of the cacheCtx to the commit context (s.evmTxCtx).
+// The reason for (2) is that the overall EVM transaction (block, not internal)
+// is only finalized when [Commit] is called, not when [CommitCacheCtx] is
+// called.
+func (s *StateDB) CommitCacheCtx() error {
+ return s.commitCtx(s.cacheCtx)
+}
+
+// commitCtx writes the dirty journal state changes to the EVM Keeper. The
+// StateDB object cannot be reused after [commitCtx] has completed. A new
+// object needs to be created from the EVM.
+func (s *StateDB) commitCtx(ctx sdk.Context) error {
for _, addr := range s.Journal.sortedDirties() {
obj := s.getStateObject(addr)
if obj == nil {
+ s.Journal.dirties[addr] = 0
continue
}
if obj.Suicided {
@@ -493,18 +564,47 @@ func (s *StateDB) Commit() error {
obj.OriginStorage[key] = dirtyVal
}
}
- // Clear the dirty counts because all state changes have been
- // committed.
+ // TODO: UD-DEBUG: Assume clean to pretend for tests
+ // Reset the dirty count to 0 because all state changes for this dirtied
+ // address in the journal have been committed.
s.Journal.dirties[addr] = 0
}
return nil
}
-// StateObjects: Returns a copy of the [StateDB.stateObjects] map.
-func (s *StateDB) StateObjects() map[common.Address]*stateObject {
- copyOfMap := make(map[common.Address]*stateObject)
- for key, val := range s.stateObjects {
- copyOfMap[key] = val
+func (s *StateDB) CacheCtxForPrecompile(precompileAddr common.Address) (
+ sdk.Context, PrecompileCalled,
+) {
+ if s.writeToCommitCtxFromCacheCtx == nil {
+ s.cacheCtx, s.writeToCommitCtxFromCacheCtx = s.evmTxCtx.CacheContext()
+ }
+ return s.cacheCtx, PrecompileCalled{
+ MultiStore: s.cacheCtx.MultiStore().(store.CacheMultiStore).Copy(),
+ Events: s.cacheCtx.EventManager().Events(),
+ }
+}
+
+// SavePrecompileCalledJournalChange adds a snapshot of the commit multistore
+// ([PrecompileCalled]) to the [StateDB] journal at the end of
+// successful invocation of a precompiled contract. This is necessary to revert
+// intermediate states where an EVM contract augments the multistore with a
+// precompile and an inconsistency occurs between the EVM module and other
+// modules.
+//
+// See [PrecompileCalled] for more info.
+func (s *StateDB) SavePrecompileCalledJournalChange(
+ precompileAddr common.Address,
+ journalChange PrecompileCalled,
+) error {
+ s.Journal.append(journalChange)
+ s.multistoreCacheCount++
+ if s.multistoreCacheCount > maxMultistoreCacheCount {
+ return fmt.Errorf(
+ "exceeded maximum number Nibiru-specific precompiled contract calls in one transaction (%d). Called address %s",
+ maxMultistoreCacheCount, precompileAddr.Hex(),
+ )
}
- return copyOfMap
+ return nil
}
+
+const maxMultistoreCacheCount uint8 = 10