diff --git a/CHANGELOG.md b/CHANGELOG.md index a316c53f3..68f7f9cc8 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,18 +144,20 @@ 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 -- [#2073](https://github.com/NibiruChain/nibiru/pull/2073) - fix(evm-keeper): better utilize ERC20 metadata during FunToken creation -- [#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 -- [#2088](https://github.com/NibiruChain/nibiru/pull/2088) - refactor(evm): remove outdated comment and improper error message text +### 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 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/eth/indexer.pb.go b/eth/indexer.pb.go index 31ba6aa32..5fbf5d2af 100644 --- a/eth/indexer.pb.go +++ b/eth/indexer.pb.go @@ -27,14 +27,16 @@ const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package type TxResult struct { // height of the blockchain Height int64 `protobuf:"varint,1,opt,name=height,proto3" json:"height,omitempty"` - // tx_index of the cosmos transaction + // tx_index is the index of the block transaction. It is not the index of an + // "internal transaction" TxIndex uint32 `protobuf:"varint,2,opt,name=tx_index,json=txIndex,proto3" json:"tx_index,omitempty"` // msg_index in a batch transaction MsgIndex uint32 `protobuf:"varint,3,opt,name=msg_index,json=msgIndex,proto3" json:"msg_index,omitempty"` - // eth_tx_index is the index in the list of valid eth tx in the block, - // aka. the transaction list returned by eth_getBlock api. + // eth_tx_index is the index in the list of valid eth tx in the block. Said + // another way, it is the index of the transaction list returned by + // eth_getBlock API. EthTxIndex int32 `protobuf:"varint,4,opt,name=eth_tx_index,json=ethTxIndex,proto3" json:"eth_tx_index,omitempty"` - // failed is true if the eth transaction did not go succeed + // failed is true if the eth transaction did not succeed Failed bool `protobuf:"varint,5,opt,name=failed,proto3" json:"failed,omitempty"` // gas_used by the transaction. If it exceeds the block gas limit, // it's set to gas limit, which is what's actually deducted by ante handler. diff --git a/proto/eth/evm/v1/evm.proto b/proto/eth/evm/v1/evm.proto index 6720afff9..46893edcc 100644 --- a/proto/eth/evm/v1/evm.proto +++ b/proto/eth/evm/v1/evm.proto @@ -6,8 +6,8 @@ import "gogoproto/gogo.proto"; option go_package = "github.com/NibiruChain/nibiru/v2/x/evm"; -// FunToken is a fungible token mapping between a bank coin and a corresponding -// ERC-20 smart contract. Bank coins here refer to tokens like NIBI, IBC +// FunToken is a fungible token mapping between a Bank Coin and a corresponding +// ERC-20 smart contract. Bank Coins here refer to tokens like NIBI, IBC // coins (ICS-20), and token factory coins, which are each represented by the // "Coin" type in Golang. message FunToken { @@ -20,7 +20,7 @@ message FunToken { // bank_denom: Coin denomination in the Bank Module. string bank_denom = 2; - // True if the `FunToken` mapping was created from an existing bank coin and + // True if the `FunToken` mapping was created from an existing Bank Coin and // the ERC-20 contract gets deployed by the module account. False if the // mapping was created from an externally owned ERC-20 contract. bool is_made_from_coin = 3; diff --git a/proto/eth/evm/v1/query.proto b/proto/eth/evm/v1/query.proto index a745ae506..bef574508 100644 --- a/proto/eth/evm/v1/query.proto +++ b/proto/eth/evm/v1/query.proto @@ -314,6 +314,6 @@ message QueryFunTokenMappingResponse { option (gogoproto.equal) = false; option (gogoproto.goproto_getters) = false; - // fun_token is a mapping between the Cosmos native coin and the ERC20 contract address + // fun_token is a mapping between the Bank Coin and the ERC20 contract address eth.evm.v1.FunToken fun_token = 1; } diff --git a/proto/eth/evm/v1/tx.proto b/proto/eth/evm/v1/tx.proto index dd336183f..af185d368 100644 --- a/proto/eth/evm/v1/tx.proto +++ b/proto/eth/evm/v1/tx.proto @@ -18,13 +18,13 @@ service Msg { rpc EthereumTx(MsgEthereumTx) returns (MsgEthereumTxResponse) { option (google.api.http).post = "/nibiru/evm/v1/ethereum_tx"; }; - // UpdateParams defined a governance operation for updating the x/evm module parameters. - // The authority is hard-coded to the Cosmos SDK x/gov module account + // UpdateParams defined a governance operation for updating the x/evm module + // parameters. The authority is hard-coded to the x/gov module account rpc UpdateParams(MsgUpdateParams) returns (MsgUpdateParamsResponse); // CreateFunToken: Create a "FunToken" mapping. Either the ERC20 contract - // address can be given to create the mapping to a bank coin, or the - // denomination for a bank coin can be given to create the mapping to an ERC20. + // address can be given to create the mapping to a Bank Coin, or the + // denomination for a Bank Coin can be given to create the mapping to an ERC20. rpc CreateFunToken(MsgCreateFunToken) returns (MsgCreateFunTokenResponse); // ConvertCoinToEvm: Sends a coin with a valid "FunToken" mapping to the @@ -229,8 +229,8 @@ message MsgUpdateParams { message MsgUpdateParamsResponse {} // MsgCreateFunToken: Arguments to create a "FunToken" mapping. Either the ERC20 -// contract address can be given to create the mapping to a bank coin, or the -// denomination for a bank coin can be given to create the mapping to an ERC20. +// contract address can be given to create the mapping to a Bank Coin, or the +// denomination for a Bank Coin can be given to create the mapping to an ERC20. message MsgCreateFunToken { // Hexadecimal address of the ERC20 token to which the `FunToken` maps string from_erc20 = 1 [ @@ -250,7 +250,7 @@ message MsgCreateFunTokenResponse { eth.evm.v1.FunToken funtoken_mapping = 1 [(gogoproto.nullable) = false]; } -// MsgConvertCoinToEvm: Arguments to send a bank coin to ERC-20 representation +// MsgConvertCoinToEvm: Arguments to send a Bank Coin to ERC-20 representation message MsgConvertCoinToEvm { // Hexadecimal address of the ERC20 token to which the `FunToken` maps string to_eth_addr = 1 [ @@ -261,7 +261,7 @@ message MsgConvertCoinToEvm { // Sender: Address for the signer of the transaction. string sender = 2; - // Bank coin to get converted to ERC20 + // Bank Coin to get converted to ERC20 cosmos.base.v1beta1.Coin bank_coin = 3 [ (gogoproto.moretags) = "yaml:\"bank_coin\"", (gogoproto.nullable) = false diff --git a/proto/eth/types/v1/indexer.proto b/proto/eth/types/v1/indexer.proto index 9840795e5..df17c31f0 100644 --- a/proto/eth/types/v1/indexer.proto +++ b/proto/eth/types/v1/indexer.proto @@ -12,15 +12,17 @@ message TxResult { // height of the blockchain int64 height = 1; - // tx_index of the cosmos transaction + // tx_index is the index of the block transaction. It is not the index of an + // "internal transaction" uint32 tx_index = 2; // msg_index in a batch transaction uint32 msg_index = 3; - // eth_tx_index is the index in the list of valid eth tx in the block, - // aka. the transaction list returned by eth_getBlock api. + // eth_tx_index is the index in the list of valid eth tx in the block. Said + // another way, it is the index of the transaction list returned by + // eth_getBlock API. int32 eth_tx_index = 4; - // failed is true if the eth transaction did not go succeed + // failed is true if the eth transaction did not succeed bool failed = 5; // gas_used by the transaction. If it exceeds the block gas limit, // it's set to gas limit, which is what's actually deducted by ante handler. diff --git a/x/evm/embeds/artifacts/contracts/FunToken.sol/IFunToken.json b/x/evm/embeds/artifacts/contracts/FunToken.sol/IFunToken.json index a2bebf939..9f006c50f 100644 --- a/x/evm/embeds/artifacts/contracts/FunToken.sol/IFunToken.json +++ b/x/evm/embeds/artifacts/contracts/FunToken.sol/IFunToken.json @@ -1,6 +1,6 @@ { "_format": "hh-sol-artifact-1", - "contractName": "FunToken", + "contractName": "IFunToken", "sourceName": "contracts/FunToken.sol", "abi": [ { diff --git a/x/evm/embeds/artifacts/contracts/TestERC20MaliciousName.sol/TestERC20MaliciousName.json b/x/evm/embeds/artifacts/contracts/TestERC20MaliciousName.sol/TestERC20MaliciousName.json new file mode 100644 index 000000000..766929c8f --- /dev/null +++ b/x/evm/embeds/artifacts/contracts/TestERC20MaliciousName.sol/TestERC20MaliciousName.json @@ -0,0 +1,302 @@ +{ + "_format": "hh-sol-artifact-1", + "contractName": "TestERC20MaliciousName", + "sourceName": "contracts/TestERC20MaliciousName.sol", + "abi": [ + { + "inputs": [ + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "string", + "name": "symbol", + "type": "string" + }, + { + "internalType": "uint8", + "name": "decimals_", + "type": "uint8" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "subtractedValue", + "type": "uint256" + } + ], + "name": "decreaseAllowance", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "addedValue", + "type": "uint256" + } + ], + "name": "increaseAllowance", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } + ], + "bytecode": "0x60806040523480156200001157600080fd5b5060405162001c5638038062001c568339818101604052810190620000379190620003cc565b828281600390816200004a9190620006b1565b5080600490816200005c9190620006b1565b5050506200007b3369d3c21bcecceda10000006200008460201b60201c565b505050620008b3565b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff1603620000f6576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401620000ed90620007f9565b60405180910390fd5b6200010a60008383620001f160201b60201c565b80600260008282546200011e91906200084a565b92505081905550806000808473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055508173ffffffffffffffffffffffffffffffffffffffff16600073ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef83604051620001d1919062000896565b60405180910390a3620001ed60008383620001f660201b60201c565b5050565b505050565b505050565b6000604051905090565b600080fd5b600080fd5b600080fd5b600080fd5b6000601f19601f8301169050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b620002648262000219565b810181811067ffffffffffffffff821117156200028657620002856200022a565b5b80604052505050565b60006200029b620001fb565b9050620002a9828262000259565b919050565b600067ffffffffffffffff821115620002cc57620002cb6200022a565b5b620002d78262000219565b9050602081019050919050565b60005b8381101562000304578082015181840152602081019050620002e7565b60008484015250505050565b6000620003276200032184620002ae565b6200028f565b90508281526020810184848401111562000346576200034562000214565b5b62000353848285620002e4565b509392505050565b600082601f8301126200037357620003726200020f565b5b81516200038584826020860162000310565b91505092915050565b600060ff82169050919050565b620003a6816200038e565b8114620003b257600080fd5b50565b600081519050620003c6816200039b565b92915050565b600080600060608486031215620003e857620003e762000205565b5b600084015167ffffffffffffffff8111156200040957620004086200020a565b5b62000417868287016200035b565b935050602084015167ffffffffffffffff8111156200043b576200043a6200020a565b5b62000449868287016200035b565b92505060406200045c86828701620003b5565b9150509250925092565b600081519050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b60006002820490506001821680620004b957607f821691505b602082108103620004cf57620004ce62000471565b5b50919050565b60008190508160005260206000209050919050565b60006020601f8301049050919050565b600082821b905092915050565b600060088302620005397fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff82620004fa565b620005458683620004fa565b95508019841693508086168417925050509392505050565b6000819050919050565b6000819050919050565b6000620005926200058c62000586846200055d565b62000567565b6200055d565b9050919050565b6000819050919050565b620005ae8362000571565b620005c6620005bd8262000599565b84845462000507565b825550505050565b600090565b620005dd620005ce565b620005ea818484620005a3565b505050565b5b81811015620006125762000606600082620005d3565b600181019050620005f0565b5050565b601f82111562000661576200062b81620004d5565b6200063684620004ea565b8101602085101562000646578190505b6200065e6200065585620004ea565b830182620005ef565b50505b505050565b600082821c905092915050565b6000620006866000198460080262000666565b1980831691505092915050565b6000620006a1838362000673565b9150826002028217905092915050565b620006bc8262000466565b67ffffffffffffffff811115620006d857620006d76200022a565b5b620006e48254620004a0565b620006f182828562000616565b600060209050601f83116001811462000729576000841562000714578287015190505b62000720858262000693565b86555062000790565b601f1984166200073986620004d5565b60005b8281101562000763578489015182556001820191506020850194506020810190506200073c565b868310156200078357848901516200077f601f89168262000673565b8355505b6001600288020188555050505b505050505050565b600082825260208201905092915050565b7f45524332303a206d696e7420746f20746865207a65726f206164647265737300600082015250565b6000620007e1601f8362000798565b9150620007ee82620007a9565b602082019050919050565b600060208201905081810360008301526200081481620007d2565b9050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b600062000857826200055d565b915062000864836200055d565b92508282019050808211156200087f576200087e6200081b565b5b92915050565b62000890816200055d565b82525050565b6000602082019050620008ad600083018462000885565b92915050565b61139380620008c36000396000f3fe608060405234801561001057600080fd5b50600436106100a95760003560e01c80633950935111610071578063395093511461016857806370a082311461019857806395d89b41146101c8578063a457c2d7146101e6578063a9059cbb14610216578063dd62ed3e14610246576100a9565b806306fdde03146100ae578063095ea7b3146100cc57806318160ddd146100fc57806323b872dd1461011a578063313ce5671461014a575b600080fd5b6100b6610276565b6040516100c39190610b9f565b60405180910390f35b6100e660048036038101906100e19190610c5a565b610293565b6040516100f39190610cb5565b60405180910390f35b6101046102b6565b6040516101119190610cdf565b60405180910390f35b610134600480360381019061012f9190610cfa565b6102c0565b6040516101419190610cb5565b60405180910390f35b6101526102ef565b60405161015f9190610d69565b60405180910390f35b610182600480360381019061017d9190610c5a565b6102f8565b60405161018f9190610cb5565b60405180910390f35b6101b260048036038101906101ad9190610d84565b61032f565b6040516101bf9190610cdf565b60405180910390f35b6101d0610377565b6040516101dd9190610b9f565b60405180910390f35b61020060048036038101906101fb9190610c5a565b610409565b60405161020d9190610cb5565b60405180910390f35b610230600480360381019061022b9190610c5a565b610480565b60405161023d9190610cb5565b60405180910390f35b610260600480360381019061025b9190610db1565b6104a3565b60405161026d9190610cdf565b60405180910390f35b6060600061028261052a565b905061028c6105bc565b8091505090565b60008061029e610632565b90506102ab81858561063a565b600191505092915050565b6000600254905090565b6000806102cb610632565b90506102d8858285610803565b6102e385858561088f565b60019150509392505050565b60006012905090565b600080610303610632565b905061032481858561031585896104a3565b61031f9190610e20565b61063a565b600191505092915050565b60008060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020549050919050565b60606004805461038690610e83565b80601f01602080910402602001604051908101604052809291908181526020018280546103b290610e83565b80156103ff5780601f106103d4576101008083540402835291602001916103ff565b820191906000526020600020905b8154815290600101906020018083116103e257829003601f168201915b5050505050905090565b600080610414610632565b9050600061042282866104a3565b905083811015610467576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161045e90610f26565b60405180910390fd5b610474828686840361063a565b60019250505092915050565b60008061048b610632565b905061049881858561088f565b600191505092915050565b6000600160008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054905092915050565b60606003805461053990610e83565b80601f016020809104026020016040519081016040528092919081815260200182805461056590610e83565b80156105b25780601f10610587576101008083540402835291602001916105b2565b820191906000526020600020905b81548152906001019060200180831161059557829003601f168201915b5050505050905090565b60006001905060005b620186a081101561061d5760016002836105df9190610f46565b6105e99190610e20565b91506002826105f89190610fb7565b9150600182901b8218915067ffffffffffffffff8216915080806001019150506105c5565b506000810361062f5761062e610fe8565b5b50565b600033905090565b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff16036106a9576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016106a090611089565b60405180910390fd5b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff1603610718576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161070f9061111b565b60405180910390fd5b80600160008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508173ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925836040516107f69190610cdf565b60405180910390a3505050565b600061080f84846104a3565b90507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8114610889578181101561087b576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161087290611187565b60405180910390fd5b610888848484840361063a565b5b50505050565b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff16036108fe576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016108f590611219565b60405180910390fd5b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff160361096d576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610964906112ab565b60405180910390fd5b610978838383610b05565b60008060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020549050818110156109fe576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016109f59061133d565b60405180910390fd5b8181036000808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002081905550816000808573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055508273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef84604051610aec9190610cdf565b60405180910390a3610aff848484610b0a565b50505050565b505050565b505050565b600081519050919050565b600082825260208201905092915050565b60005b83811015610b49578082015181840152602081019050610b2e565b60008484015250505050565b6000601f19601f8301169050919050565b6000610b7182610b0f565b610b7b8185610b1a565b9350610b8b818560208601610b2b565b610b9481610b55565b840191505092915050565b60006020820190508181036000830152610bb98184610b66565b905092915050565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000610bf182610bc6565b9050919050565b610c0181610be6565b8114610c0c57600080fd5b50565b600081359050610c1e81610bf8565b92915050565b6000819050919050565b610c3781610c24565b8114610c4257600080fd5b50565b600081359050610c5481610c2e565b92915050565b60008060408385031215610c7157610c70610bc1565b5b6000610c7f85828601610c0f565b9250506020610c9085828601610c45565b9150509250929050565b60008115159050919050565b610caf81610c9a565b82525050565b6000602082019050610cca6000830184610ca6565b92915050565b610cd981610c24565b82525050565b6000602082019050610cf46000830184610cd0565b92915050565b600080600060608486031215610d1357610d12610bc1565b5b6000610d2186828701610c0f565b9350506020610d3286828701610c0f565b9250506040610d4386828701610c45565b9150509250925092565b600060ff82169050919050565b610d6381610d4d565b82525050565b6000602082019050610d7e6000830184610d5a565b92915050565b600060208284031215610d9a57610d99610bc1565b5b6000610da884828501610c0f565b91505092915050565b60008060408385031215610dc857610dc7610bc1565b5b6000610dd685828601610c0f565b9250506020610de785828601610c0f565b9150509250929050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b6000610e2b82610c24565b9150610e3683610c24565b9250828201905080821115610e4e57610e4d610df1565b5b92915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b60006002820490506001821680610e9b57607f821691505b602082108103610eae57610ead610e54565b5b50919050565b7f45524332303a2064656372656173656420616c6c6f77616e63652062656c6f7760008201527f207a65726f000000000000000000000000000000000000000000000000000000602082015250565b6000610f10602583610b1a565b9150610f1b82610eb4565b604082019050919050565b60006020820190508181036000830152610f3f81610f03565b9050919050565b6000610f5182610c24565b9150610f5c83610c24565b9250828202610f6a81610c24565b91508282048414831517610f8157610f80610df1565b5b5092915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601260045260246000fd5b6000610fc282610c24565b9150610fcd83610c24565b925082610fdd57610fdc610f88565b5b828204905092915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052600160045260246000fd5b7f45524332303a20617070726f76652066726f6d20746865207a65726f2061646460008201527f7265737300000000000000000000000000000000000000000000000000000000602082015250565b6000611073602483610b1a565b915061107e82611017565b604082019050919050565b600060208201905081810360008301526110a281611066565b9050919050565b7f45524332303a20617070726f766520746f20746865207a65726f20616464726560008201527f7373000000000000000000000000000000000000000000000000000000000000602082015250565b6000611105602283610b1a565b9150611110826110a9565b604082019050919050565b60006020820190508181036000830152611134816110f8565b9050919050565b7f45524332303a20696e73756666696369656e7420616c6c6f77616e6365000000600082015250565b6000611171601d83610b1a565b915061117c8261113b565b602082019050919050565b600060208201905081810360008301526111a081611164565b9050919050565b7f45524332303a207472616e736665722066726f6d20746865207a65726f20616460008201527f6472657373000000000000000000000000000000000000000000000000000000602082015250565b6000611203602583610b1a565b915061120e826111a7565b604082019050919050565b60006020820190508181036000830152611232816111f6565b9050919050565b7f45524332303a207472616e7366657220746f20746865207a65726f206164647260008201527f6573730000000000000000000000000000000000000000000000000000000000602082015250565b6000611295602383610b1a565b91506112a082611239565b604082019050919050565b600060208201905081810360008301526112c481611288565b9050919050565b7f45524332303a207472616e7366657220616d6f756e742065786365656473206260008201527f616c616e63650000000000000000000000000000000000000000000000000000602082015250565b6000611327602683610b1a565b9150611332826112cb565b604082019050919050565b600060208201905081810360008301526113568161131a565b905091905056fea26469706673582212205d0c992a81a43cd8431857189c1e4ae58ba867bc7888b0cc0c2e0031d6d9f3c664736f6c63430008180033", + "deployedBytecode": "0x608060405234801561001057600080fd5b50600436106100a95760003560e01c80633950935111610071578063395093511461016857806370a082311461019857806395d89b41146101c8578063a457c2d7146101e6578063a9059cbb14610216578063dd62ed3e14610246576100a9565b806306fdde03146100ae578063095ea7b3146100cc57806318160ddd146100fc57806323b872dd1461011a578063313ce5671461014a575b600080fd5b6100b6610276565b6040516100c39190610b9f565b60405180910390f35b6100e660048036038101906100e19190610c5a565b610293565b6040516100f39190610cb5565b60405180910390f35b6101046102b6565b6040516101119190610cdf565b60405180910390f35b610134600480360381019061012f9190610cfa565b6102c0565b6040516101419190610cb5565b60405180910390f35b6101526102ef565b60405161015f9190610d69565b60405180910390f35b610182600480360381019061017d9190610c5a565b6102f8565b60405161018f9190610cb5565b60405180910390f35b6101b260048036038101906101ad9190610d84565b61032f565b6040516101bf9190610cdf565b60405180910390f35b6101d0610377565b6040516101dd9190610b9f565b60405180910390f35b61020060048036038101906101fb9190610c5a565b610409565b60405161020d9190610cb5565b60405180910390f35b610230600480360381019061022b9190610c5a565b610480565b60405161023d9190610cb5565b60405180910390f35b610260600480360381019061025b9190610db1565b6104a3565b60405161026d9190610cdf565b60405180910390f35b6060600061028261052a565b905061028c6105bc565b8091505090565b60008061029e610632565b90506102ab81858561063a565b600191505092915050565b6000600254905090565b6000806102cb610632565b90506102d8858285610803565b6102e385858561088f565b60019150509392505050565b60006012905090565b600080610303610632565b905061032481858561031585896104a3565b61031f9190610e20565b61063a565b600191505092915050565b60008060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020549050919050565b60606004805461038690610e83565b80601f01602080910402602001604051908101604052809291908181526020018280546103b290610e83565b80156103ff5780601f106103d4576101008083540402835291602001916103ff565b820191906000526020600020905b8154815290600101906020018083116103e257829003601f168201915b5050505050905090565b600080610414610632565b9050600061042282866104a3565b905083811015610467576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161045e90610f26565b60405180910390fd5b610474828686840361063a565b60019250505092915050565b60008061048b610632565b905061049881858561088f565b600191505092915050565b6000600160008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054905092915050565b60606003805461053990610e83565b80601f016020809104026020016040519081016040528092919081815260200182805461056590610e83565b80156105b25780601f10610587576101008083540402835291602001916105b2565b820191906000526020600020905b81548152906001019060200180831161059557829003601f168201915b5050505050905090565b60006001905060005b620186a081101561061d5760016002836105df9190610f46565b6105e99190610e20565b91506002826105f89190610fb7565b9150600182901b8218915067ffffffffffffffff8216915080806001019150506105c5565b506000810361062f5761062e610fe8565b5b50565b600033905090565b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff16036106a9576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016106a090611089565b60405180910390fd5b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff1603610718576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161070f9061111b565b60405180910390fd5b80600160008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508173ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925836040516107f69190610cdf565b60405180910390a3505050565b600061080f84846104a3565b90507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8114610889578181101561087b576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161087290611187565b60405180910390fd5b610888848484840361063a565b5b50505050565b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff16036108fe576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016108f590611219565b60405180910390fd5b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff160361096d576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610964906112ab565b60405180910390fd5b610978838383610b05565b60008060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020549050818110156109fe576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016109f59061133d565b60405180910390fd5b8181036000808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002081905550816000808573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055508273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef84604051610aec9190610cdf565b60405180910390a3610aff848484610b0a565b50505050565b505050565b505050565b600081519050919050565b600082825260208201905092915050565b60005b83811015610b49578082015181840152602081019050610b2e565b60008484015250505050565b6000601f19601f8301169050919050565b6000610b7182610b0f565b610b7b8185610b1a565b9350610b8b818560208601610b2b565b610b9481610b55565b840191505092915050565b60006020820190508181036000830152610bb98184610b66565b905092915050565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000610bf182610bc6565b9050919050565b610c0181610be6565b8114610c0c57600080fd5b50565b600081359050610c1e81610bf8565b92915050565b6000819050919050565b610c3781610c24565b8114610c4257600080fd5b50565b600081359050610c5481610c2e565b92915050565b60008060408385031215610c7157610c70610bc1565b5b6000610c7f85828601610c0f565b9250506020610c9085828601610c45565b9150509250929050565b60008115159050919050565b610caf81610c9a565b82525050565b6000602082019050610cca6000830184610ca6565b92915050565b610cd981610c24565b82525050565b6000602082019050610cf46000830184610cd0565b92915050565b600080600060608486031215610d1357610d12610bc1565b5b6000610d2186828701610c0f565b9350506020610d3286828701610c0f565b9250506040610d4386828701610c45565b9150509250925092565b600060ff82169050919050565b610d6381610d4d565b82525050565b6000602082019050610d7e6000830184610d5a565b92915050565b600060208284031215610d9a57610d99610bc1565b5b6000610da884828501610c0f565b91505092915050565b60008060408385031215610dc857610dc7610bc1565b5b6000610dd685828601610c0f565b9250506020610de785828601610c0f565b9150509250929050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b6000610e2b82610c24565b9150610e3683610c24565b9250828201905080821115610e4e57610e4d610df1565b5b92915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b60006002820490506001821680610e9b57607f821691505b602082108103610eae57610ead610e54565b5b50919050565b7f45524332303a2064656372656173656420616c6c6f77616e63652062656c6f7760008201527f207a65726f000000000000000000000000000000000000000000000000000000602082015250565b6000610f10602583610b1a565b9150610f1b82610eb4565b604082019050919050565b60006020820190508181036000830152610f3f81610f03565b9050919050565b6000610f5182610c24565b9150610f5c83610c24565b9250828202610f6a81610c24565b91508282048414831517610f8157610f80610df1565b5b5092915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601260045260246000fd5b6000610fc282610c24565b9150610fcd83610c24565b925082610fdd57610fdc610f88565b5b828204905092915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052600160045260246000fd5b7f45524332303a20617070726f76652066726f6d20746865207a65726f2061646460008201527f7265737300000000000000000000000000000000000000000000000000000000602082015250565b6000611073602483610b1a565b915061107e82611017565b604082019050919050565b600060208201905081810360008301526110a281611066565b9050919050565b7f45524332303a20617070726f766520746f20746865207a65726f20616464726560008201527f7373000000000000000000000000000000000000000000000000000000000000602082015250565b6000611105602283610b1a565b9150611110826110a9565b604082019050919050565b60006020820190508181036000830152611134816110f8565b9050919050565b7f45524332303a20696e73756666696369656e7420616c6c6f77616e6365000000600082015250565b6000611171601d83610b1a565b915061117c8261113b565b602082019050919050565b600060208201905081810360008301526111a081611164565b9050919050565b7f45524332303a207472616e736665722066726f6d20746865207a65726f20616460008201527f6472657373000000000000000000000000000000000000000000000000000000602082015250565b6000611203602583610b1a565b915061120e826111a7565b604082019050919050565b60006020820190508181036000830152611232816111f6565b9050919050565b7f45524332303a207472616e7366657220746f20746865207a65726f206164647260008201527f6573730000000000000000000000000000000000000000000000000000000000602082015250565b6000611295602383610b1a565b91506112a082611239565b604082019050919050565b600060208201905081810360008301526112c481611288565b9050919050565b7f45524332303a207472616e7366657220616d6f756e742065786365656473206260008201527f616c616e63650000000000000000000000000000000000000000000000000000602082015250565b6000611327602683610b1a565b9150611332826112cb565b604082019050919050565b600060208201905081810360008301526113568161131a565b905091905056fea26469706673582212205d0c992a81a43cd8431857189c1e4ae58ba867bc7888b0cc0c2e0031d6d9f3c664736f6c63430008180033", + "linkReferences": {}, + "deployedLinkReferences": {} +} diff --git a/x/evm/embeds/artifacts/contracts/TestERC20MaliciousTransfer.sol/TestERC20MaliciousTransfer.json b/x/evm/embeds/artifacts/contracts/TestERC20MaliciousTransfer.sol/TestERC20MaliciousTransfer.json new file mode 100644 index 000000000..48019e35d --- /dev/null +++ b/x/evm/embeds/artifacts/contracts/TestERC20MaliciousTransfer.sol/TestERC20MaliciousTransfer.json @@ -0,0 +1,302 @@ +{ + "_format": "hh-sol-artifact-1", + "contractName": "TestERC20MaliciousTransfer", + "sourceName": "contracts/TestERC20MaliciousTransfer.sol", + "abi": [ + { + "inputs": [ + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "string", + "name": "symbol", + "type": "string" + }, + { + "internalType": "uint8", + "name": "decimals_", + "type": "uint8" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "subtractedValue", + "type": "uint256" + } + ], + "name": "decreaseAllowance", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "addedValue", + "type": "uint256" + } + ], + "name": "increaseAllowance", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } + ], + "bytecode": "0x60806040523480156200001157600080fd5b5060405162001c5538038062001c558339818101604052810190620000379190620003cc565b828281600390816200004a9190620006b1565b5080600490816200005c9190620006b1565b5050506200007b3369d3c21bcecceda10000006200008460201b60201c565b505050620008b3565b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff1603620000f6576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401620000ed90620007f9565b60405180910390fd5b6200010a60008383620001f160201b60201c565b80600260008282546200011e91906200084a565b92505081905550806000808473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055508173ffffffffffffffffffffffffffffffffffffffff16600073ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef83604051620001d1919062000896565b60405180910390a3620001ed60008383620001f660201b60201c565b5050565b505050565b505050565b6000604051905090565b600080fd5b600080fd5b600080fd5b600080fd5b6000601f19601f8301169050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b620002648262000219565b810181811067ffffffffffffffff821117156200028657620002856200022a565b5b80604052505050565b60006200029b620001fb565b9050620002a9828262000259565b919050565b600067ffffffffffffffff821115620002cc57620002cb6200022a565b5b620002d78262000219565b9050602081019050919050565b60005b8381101562000304578082015181840152602081019050620002e7565b60008484015250505050565b6000620003276200032184620002ae565b6200028f565b90508281526020810184848401111562000346576200034562000214565b5b62000353848285620002e4565b509392505050565b600082601f8301126200037357620003726200020f565b5b81516200038584826020860162000310565b91505092915050565b600060ff82169050919050565b620003a6816200038e565b8114620003b257600080fd5b50565b600081519050620003c6816200039b565b92915050565b600080600060608486031215620003e857620003e762000205565b5b600084015167ffffffffffffffff8111156200040957620004086200020a565b5b62000417868287016200035b565b935050602084015167ffffffffffffffff8111156200043b576200043a6200020a565b5b62000449868287016200035b565b92505060406200045c86828701620003b5565b9150509250925092565b600081519050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b60006002820490506001821680620004b957607f821691505b602082108103620004cf57620004ce62000471565b5b50919050565b60008190508160005260206000209050919050565b60006020601f8301049050919050565b600082821b905092915050565b600060088302620005397fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff82620004fa565b620005458683620004fa565b95508019841693508086168417925050509392505050565b6000819050919050565b6000819050919050565b6000620005926200058c62000586846200055d565b62000567565b6200055d565b9050919050565b6000819050919050565b620005ae8362000571565b620005c6620005bd8262000599565b84845462000507565b825550505050565b600090565b620005dd620005ce565b620005ea818484620005a3565b505050565b5b81811015620006125762000606600082620005d3565b600181019050620005f0565b5050565b601f82111562000661576200062b81620004d5565b6200063684620004ea565b8101602085101562000646578190505b6200065e6200065585620004ea565b830182620005ef565b50505b505050565b600082821c905092915050565b6000620006866000198460080262000666565b1980831691505092915050565b6000620006a1838362000673565b9150826002028217905092915050565b620006bc8262000466565b67ffffffffffffffff811115620006d857620006d76200022a565b5b620006e48254620004a0565b620006f182828562000616565b600060209050601f83116001811462000729576000841562000714578287015190505b62000720858262000693565b86555062000790565b601f1984166200073986620004d5565b60005b8281101562000763578489015182556001820191506020850194506020810190506200073c565b868310156200078357848901516200077f601f89168262000673565b8355505b6001600288020188555050505b505050505050565b600082825260208201905092915050565b7f45524332303a206d696e7420746f20746865207a65726f206164647265737300600082015250565b6000620007e1601f8362000798565b9150620007ee82620007a9565b602082019050919050565b600060208201905081810360008301526200081481620007d2565b9050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b600062000857826200055d565b915062000864836200055d565b92508282019050808211156200087f576200087e6200081b565b5b92915050565b62000890816200055d565b82525050565b6000602082019050620008ad600083018462000885565b92915050565b61139280620008c36000396000f3fe608060405234801561001057600080fd5b50600436106100a95760003560e01c80633950935111610071578063395093511461016857806370a082311461019857806395d89b41146101c8578063a457c2d7146101e6578063a9059cbb14610216578063dd62ed3e14610246576100a9565b806306fdde03146100ae578063095ea7b3146100cc57806318160ddd146100fc57806323b872dd1461011a578063313ce5671461014a575b600080fd5b6100b6610276565b6040516100c39190610b9e565b60405180910390f35b6100e660048036038101906100e19190610c59565b610308565b6040516100f39190610cb4565b60405180910390f35b61010461032b565b6040516101119190610cde565b60405180910390f35b610134600480360381019061012f9190610cf9565b610335565b6040516101419190610cb4565b60405180910390f35b610152610364565b60405161015f9190610d68565b60405180910390f35b610182600480360381019061017d9190610c59565b61036d565b60405161018f9190610cb4565b60405180910390f35b6101b260048036038101906101ad9190610d83565b6103a4565b6040516101bf9190610cde565b60405180910390f35b6101d06103ec565b6040516101dd9190610b9e565b60405180910390f35b61020060048036038101906101fb9190610c59565b61047e565b60405161020d9190610cb4565b60405180910390f35b610230600480360381019061022b9190610c59565b6104f5565b60405161023d9190610cb4565b60405180910390f35b610260600480360381019061025b9190610db0565b610511565b60405161026d9190610cde565b60405180910390f35b60606003805461028590610e1f565b80601f01602080910402602001604051908101604052809291908181526020018280546102b190610e1f565b80156102fe5780601f106102d3576101008083540402835291602001916102fe565b820191906000526020600020905b8154815290600101906020018083116102e157829003601f168201915b5050505050905090565b600080610313610598565b90506103208185856105a0565b600191505092915050565b6000600254905090565b600080610340610598565b905061034d858285610769565b6103588585856107f5565b60019150509392505050565b60006012905090565b600080610378610598565b905061039981858561038a8589610511565b6103949190610e7f565b6105a0565b600191505092915050565b60008060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020549050919050565b6060600480546103fb90610e1f565b80601f016020809104026020016040519081016040528092919081815260200182805461042790610e1f565b80156104745780601f1061044957610100808354040283529160200191610474565b820191906000526020600020905b81548152906001019060200180831161045757829003601f168201915b5050505050905090565b600080610489610598565b905060006104978286610511565b9050838110156104dc576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016104d390610f25565b60405180910390fd5b6104e982868684036105a0565b60019250505092915050565b60006104ff610a6b565b6105098383610ae1565b905092915050565b6000600160008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054905092915050565b600033905090565b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff160361060f576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161060690610fb7565b60405180910390fd5b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff160361067e576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161067590611049565b60405180910390fd5b80600160008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508173ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9258360405161075c9190610cde565b60405180910390a3505050565b60006107758484610511565b90507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff81146107ef57818110156107e1576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016107d8906110b5565b60405180910390fd5b6107ee84848484036105a0565b5b50505050565b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff1603610864576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161085b90611147565b60405180910390fd5b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff16036108d3576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016108ca906111d9565b60405180910390fd5b6108de838383610b04565b60008060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054905081811015610964576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161095b9061126b565b60405180910390fd5b8181036000808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002081905550816000808573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055508273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef84604051610a529190610cde565b60405180910390a3610a65848484610b09565b50505050565b60006001905060005b620186a0811015610acc576001600283610a8e919061128b565b610a989190610e7f565b9150600282610aa791906112fc565b9150600182901b8218915067ffffffffffffffff821691508080600101915050610a74565b5060008103610ade57610add61132d565b5b50565b600080610aec610598565b9050610af98185856107f5565b600191505092915050565b505050565b505050565b600081519050919050565b600082825260208201905092915050565b60005b83811015610b48578082015181840152602081019050610b2d565b60008484015250505050565b6000601f19601f8301169050919050565b6000610b7082610b0e565b610b7a8185610b19565b9350610b8a818560208601610b2a565b610b9381610b54565b840191505092915050565b60006020820190508181036000830152610bb88184610b65565b905092915050565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000610bf082610bc5565b9050919050565b610c0081610be5565b8114610c0b57600080fd5b50565b600081359050610c1d81610bf7565b92915050565b6000819050919050565b610c3681610c23565b8114610c4157600080fd5b50565b600081359050610c5381610c2d565b92915050565b60008060408385031215610c7057610c6f610bc0565b5b6000610c7e85828601610c0e565b9250506020610c8f85828601610c44565b9150509250929050565b60008115159050919050565b610cae81610c99565b82525050565b6000602082019050610cc96000830184610ca5565b92915050565b610cd881610c23565b82525050565b6000602082019050610cf36000830184610ccf565b92915050565b600080600060608486031215610d1257610d11610bc0565b5b6000610d2086828701610c0e565b9350506020610d3186828701610c0e565b9250506040610d4286828701610c44565b9150509250925092565b600060ff82169050919050565b610d6281610d4c565b82525050565b6000602082019050610d7d6000830184610d59565b92915050565b600060208284031215610d9957610d98610bc0565b5b6000610da784828501610c0e565b91505092915050565b60008060408385031215610dc757610dc6610bc0565b5b6000610dd585828601610c0e565b9250506020610de685828601610c0e565b9150509250929050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b60006002820490506001821680610e3757607f821691505b602082108103610e4a57610e49610df0565b5b50919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b6000610e8a82610c23565b9150610e9583610c23565b9250828201905080821115610ead57610eac610e50565b5b92915050565b7f45524332303a2064656372656173656420616c6c6f77616e63652062656c6f7760008201527f207a65726f000000000000000000000000000000000000000000000000000000602082015250565b6000610f0f602583610b19565b9150610f1a82610eb3565b604082019050919050565b60006020820190508181036000830152610f3e81610f02565b9050919050565b7f45524332303a20617070726f76652066726f6d20746865207a65726f2061646460008201527f7265737300000000000000000000000000000000000000000000000000000000602082015250565b6000610fa1602483610b19565b9150610fac82610f45565b604082019050919050565b60006020820190508181036000830152610fd081610f94565b9050919050565b7f45524332303a20617070726f766520746f20746865207a65726f20616464726560008201527f7373000000000000000000000000000000000000000000000000000000000000602082015250565b6000611033602283610b19565b915061103e82610fd7565b604082019050919050565b6000602082019050818103600083015261106281611026565b9050919050565b7f45524332303a20696e73756666696369656e7420616c6c6f77616e6365000000600082015250565b600061109f601d83610b19565b91506110aa82611069565b602082019050919050565b600060208201905081810360008301526110ce81611092565b9050919050565b7f45524332303a207472616e736665722066726f6d20746865207a65726f20616460008201527f6472657373000000000000000000000000000000000000000000000000000000602082015250565b6000611131602583610b19565b915061113c826110d5565b604082019050919050565b6000602082019050818103600083015261116081611124565b9050919050565b7f45524332303a207472616e7366657220746f20746865207a65726f206164647260008201527f6573730000000000000000000000000000000000000000000000000000000000602082015250565b60006111c3602383610b19565b91506111ce82611167565b604082019050919050565b600060208201905081810360008301526111f2816111b6565b9050919050565b7f45524332303a207472616e7366657220616d6f756e742065786365656473206260008201527f616c616e63650000000000000000000000000000000000000000000000000000602082015250565b6000611255602683610b19565b9150611260826111f9565b604082019050919050565b6000602082019050818103600083015261128481611248565b9050919050565b600061129682610c23565b91506112a183610c23565b92508282026112af81610c23565b915082820484148315176112c6576112c5610e50565b5b5092915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601260045260246000fd5b600061130782610c23565b915061131283610c23565b925082611322576113216112cd565b5b828204905092915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052600160045260246000fdfea2646970667358221220e34d9015349ff6af20fee44587dc5ba21b370ca2a6239a1dc6440ecaddfd47a364736f6c63430008180033", + "deployedBytecode": "0x608060405234801561001057600080fd5b50600436106100a95760003560e01c80633950935111610071578063395093511461016857806370a082311461019857806395d89b41146101c8578063a457c2d7146101e6578063a9059cbb14610216578063dd62ed3e14610246576100a9565b806306fdde03146100ae578063095ea7b3146100cc57806318160ddd146100fc57806323b872dd1461011a578063313ce5671461014a575b600080fd5b6100b6610276565b6040516100c39190610b9e565b60405180910390f35b6100e660048036038101906100e19190610c59565b610308565b6040516100f39190610cb4565b60405180910390f35b61010461032b565b6040516101119190610cde565b60405180910390f35b610134600480360381019061012f9190610cf9565b610335565b6040516101419190610cb4565b60405180910390f35b610152610364565b60405161015f9190610d68565b60405180910390f35b610182600480360381019061017d9190610c59565b61036d565b60405161018f9190610cb4565b60405180910390f35b6101b260048036038101906101ad9190610d83565b6103a4565b6040516101bf9190610cde565b60405180910390f35b6101d06103ec565b6040516101dd9190610b9e565b60405180910390f35b61020060048036038101906101fb9190610c59565b61047e565b60405161020d9190610cb4565b60405180910390f35b610230600480360381019061022b9190610c59565b6104f5565b60405161023d9190610cb4565b60405180910390f35b610260600480360381019061025b9190610db0565b610511565b60405161026d9190610cde565b60405180910390f35b60606003805461028590610e1f565b80601f01602080910402602001604051908101604052809291908181526020018280546102b190610e1f565b80156102fe5780601f106102d3576101008083540402835291602001916102fe565b820191906000526020600020905b8154815290600101906020018083116102e157829003601f168201915b5050505050905090565b600080610313610598565b90506103208185856105a0565b600191505092915050565b6000600254905090565b600080610340610598565b905061034d858285610769565b6103588585856107f5565b60019150509392505050565b60006012905090565b600080610378610598565b905061039981858561038a8589610511565b6103949190610e7f565b6105a0565b600191505092915050565b60008060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020549050919050565b6060600480546103fb90610e1f565b80601f016020809104026020016040519081016040528092919081815260200182805461042790610e1f565b80156104745780601f1061044957610100808354040283529160200191610474565b820191906000526020600020905b81548152906001019060200180831161045757829003601f168201915b5050505050905090565b600080610489610598565b905060006104978286610511565b9050838110156104dc576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016104d390610f25565b60405180910390fd5b6104e982868684036105a0565b60019250505092915050565b60006104ff610a6b565b6105098383610ae1565b905092915050565b6000600160008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054905092915050565b600033905090565b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff160361060f576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161060690610fb7565b60405180910390fd5b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff160361067e576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161067590611049565b60405180910390fd5b80600160008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508173ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9258360405161075c9190610cde565b60405180910390a3505050565b60006107758484610511565b90507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff81146107ef57818110156107e1576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016107d8906110b5565b60405180910390fd5b6107ee84848484036105a0565b5b50505050565b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff1603610864576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161085b90611147565b60405180910390fd5b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff16036108d3576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016108ca906111d9565b60405180910390fd5b6108de838383610b04565b60008060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054905081811015610964576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161095b9061126b565b60405180910390fd5b8181036000808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002081905550816000808573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055508273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef84604051610a529190610cde565b60405180910390a3610a65848484610b09565b50505050565b60006001905060005b620186a0811015610acc576001600283610a8e919061128b565b610a989190610e7f565b9150600282610aa791906112fc565b9150600182901b8218915067ffffffffffffffff821691508080600101915050610a74565b5060008103610ade57610add61132d565b5b50565b600080610aec610598565b9050610af98185856107f5565b600191505092915050565b505050565b505050565b600081519050919050565b600082825260208201905092915050565b60005b83811015610b48578082015181840152602081019050610b2d565b60008484015250505050565b6000601f19601f8301169050919050565b6000610b7082610b0e565b610b7a8185610b19565b9350610b8a818560208601610b2a565b610b9381610b54565b840191505092915050565b60006020820190508181036000830152610bb88184610b65565b905092915050565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000610bf082610bc5565b9050919050565b610c0081610be5565b8114610c0b57600080fd5b50565b600081359050610c1d81610bf7565b92915050565b6000819050919050565b610c3681610c23565b8114610c4157600080fd5b50565b600081359050610c5381610c2d565b92915050565b60008060408385031215610c7057610c6f610bc0565b5b6000610c7e85828601610c0e565b9250506020610c8f85828601610c44565b9150509250929050565b60008115159050919050565b610cae81610c99565b82525050565b6000602082019050610cc96000830184610ca5565b92915050565b610cd881610c23565b82525050565b6000602082019050610cf36000830184610ccf565b92915050565b600080600060608486031215610d1257610d11610bc0565b5b6000610d2086828701610c0e565b9350506020610d3186828701610c0e565b9250506040610d4286828701610c44565b9150509250925092565b600060ff82169050919050565b610d6281610d4c565b82525050565b6000602082019050610d7d6000830184610d59565b92915050565b600060208284031215610d9957610d98610bc0565b5b6000610da784828501610c0e565b91505092915050565b60008060408385031215610dc757610dc6610bc0565b5b6000610dd585828601610c0e565b9250506020610de685828601610c0e565b9150509250929050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b60006002820490506001821680610e3757607f821691505b602082108103610e4a57610e49610df0565b5b50919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b6000610e8a82610c23565b9150610e9583610c23565b9250828201905080821115610ead57610eac610e50565b5b92915050565b7f45524332303a2064656372656173656420616c6c6f77616e63652062656c6f7760008201527f207a65726f000000000000000000000000000000000000000000000000000000602082015250565b6000610f0f602583610b19565b9150610f1a82610eb3565b604082019050919050565b60006020820190508181036000830152610f3e81610f02565b9050919050565b7f45524332303a20617070726f76652066726f6d20746865207a65726f2061646460008201527f7265737300000000000000000000000000000000000000000000000000000000602082015250565b6000610fa1602483610b19565b9150610fac82610f45565b604082019050919050565b60006020820190508181036000830152610fd081610f94565b9050919050565b7f45524332303a20617070726f766520746f20746865207a65726f20616464726560008201527f7373000000000000000000000000000000000000000000000000000000000000602082015250565b6000611033602283610b19565b915061103e82610fd7565b604082019050919050565b6000602082019050818103600083015261106281611026565b9050919050565b7f45524332303a20696e73756666696369656e7420616c6c6f77616e6365000000600082015250565b600061109f601d83610b19565b91506110aa82611069565b602082019050919050565b600060208201905081810360008301526110ce81611092565b9050919050565b7f45524332303a207472616e736665722066726f6d20746865207a65726f20616460008201527f6472657373000000000000000000000000000000000000000000000000000000602082015250565b6000611131602583610b19565b915061113c826110d5565b604082019050919050565b6000602082019050818103600083015261116081611124565b9050919050565b7f45524332303a207472616e7366657220746f20746865207a65726f206164647260008201527f6573730000000000000000000000000000000000000000000000000000000000602082015250565b60006111c3602383610b19565b91506111ce82611167565b604082019050919050565b600060208201905081810360008301526111f2816111b6565b9050919050565b7f45524332303a207472616e7366657220616d6f756e742065786365656473206260008201527f616c616e63650000000000000000000000000000000000000000000000000000602082015250565b6000611255602683610b19565b9150611260826111f9565b604082019050919050565b6000602082019050818103600083015261128481611248565b9050919050565b600061129682610c23565b91506112a183610c23565b92508282026112af81610c23565b915082820484148315176112c6576112c5610e50565b5b5092915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601260045260246000fd5b600061130782610c23565b915061131283610c23565b925082611322576113216112cd565b5b828204905092915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052600160045260246000fdfea2646970667358221220e34d9015349ff6af20fee44587dc5ba21b370ca2a6239a1dc6440ecaddfd47a364736f6c63430008180033", + "linkReferences": {}, + "deployedLinkReferences": {} +} diff --git a/x/evm/embeds/contracts/TestERC20MaliciousName.sol b/x/evm/embeds/contracts/TestERC20MaliciousName.sol new file mode 100644 index 000000000..e6be7270a --- /dev/null +++ b/x/evm/embeds/contracts/TestERC20MaliciousName.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract TestERC20MaliciousName is ERC20 { + constructor(string memory name, string memory symbol, uint8 decimals_) + ERC20(name, symbol) { + _mint(msg.sender, 1000000 * 10**18); + } + + function name() public view virtual override returns (string memory) { + string memory actualName = super.name(); + _gasIntensiveOperation(); + return actualName; + } + + // Gas-intensive operation to simulate high computational cost + function _gasIntensiveOperation() internal pure { + uint256 result = 1; + for (uint256 i = 0; i < 100000; i++) { + result = result * 2 + 1; + result = result / 2; + result = result ^ (result << 1); + result = result & 0xFFFFFFFFFFFFFFFF; + } + // The result is not used, ensuring the compiler doesn't optimize this away + assert(result != 0); + } +} \ No newline at end of file diff --git a/x/evm/embeds/contracts/TestERC20MaliciousTransfer.sol b/x/evm/embeds/contracts/TestERC20MaliciousTransfer.sol new file mode 100644 index 000000000..07fc63699 --- /dev/null +++ b/x/evm/embeds/contracts/TestERC20MaliciousTransfer.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract TestERC20MaliciousTransfer is ERC20 { + constructor(string memory name, string memory symbol, uint8 decimals_) + ERC20(name, symbol) { + _mint(msg.sender, 1000000 * 10**18); + } + + function transfer(address recipient, uint256 amount) public virtual override returns (bool) { + _gasIntensiveOperation(); + return super.transfer(recipient, amount); + } + + // Gas-intensive operation to simulate high computational cost + function _gasIntensiveOperation() internal pure { + uint256 result = 1; + for (uint256 i = 0; i < 100000; i++) { + result = result * 2 + 1; + result = result / 2; + result = result ^ (result << 1); + result = result & 0xFFFFFFFFFFFFFFFF; + } + // The result is not used, ensuring the compiler doesn't optimize this away + assert(result != 0); + } +} \ No newline at end of file diff --git a/x/evm/embeds/embeds.go b/x/evm/embeds/embeds.go index b2535040c..5ac65655d 100644 --- a/x/evm/embeds/embeds.go +++ b/x/evm/embeds/embeds.go @@ -25,6 +25,10 @@ var ( wasmPrecompileJSON []byte //go:embed artifacts/contracts/TestERC20.sol/TestERC20.json testErc20Json []byte + //go:embed artifacts/contracts/TestERC20MaliciousName.sol/TestERC20MaliciousName.json + testErc20MaliciousNameJson []byte + //go:embed artifacts/contracts/TestERC20MaliciousTransfer.sol/TestERC20MaliciousTransfer.json + testErc20MaliciousTransferJson []byte ) var ( @@ -58,6 +62,20 @@ var ( Name: "TestERC20.sol", EmbedJSON: testErc20Json, } + // SmartContract_TestERC20MaliciousName is a test contract + // which simulates malicious ERC20 behavior by adding gas intensive operation + // for function name() intended to attack funtoken creation + SmartContract_TestERC20MaliciousName = CompiledEvmContract{ + Name: "TestERC20MaliciousName.sol", + EmbedJSON: testErc20MaliciousNameJson, + } + // SmartContract_TestERC20MaliciousTransfer is a test contract + // which simulates malicious ERC20 behavior by adding gas intensive operation + // for function transfer() intended to attack funtoken conversion from erc20 to bank coin + SmartContract_TestERC20MaliciousTransfer = CompiledEvmContract{ + Name: "TestERC20MaliciousTransfer.sol", + EmbedJSON: testErc20MaliciousTransferJson, + } ) func init() { @@ -66,6 +84,8 @@ func init() { SmartContract_Wasm.MustLoad() SmartContract_Oracle.MustLoad() SmartContract_TestERC20.MustLoad() + SmartContract_TestERC20MaliciousName.MustLoad() + SmartContract_TestERC20MaliciousTransfer.MustLoad() } type CompiledEvmContract struct { diff --git a/x/evm/embeds/embeds_test.go b/x/evm/embeds/embeds_test.go index 5aa9a020e..586ff68b9 100644 --- a/x/evm/embeds/embeds_test.go +++ b/x/evm/embeds/embeds_test.go @@ -14,5 +14,7 @@ func TestLoadContracts(t *testing.T) { embeds.SmartContract_ERC20Minter.MustLoad() embeds.SmartContract_FunToken.MustLoad() embeds.SmartContract_TestERC20.MustLoad() + embeds.SmartContract_TestERC20MaliciousName.MustLoad() + embeds.SmartContract_TestERC20MaliciousTransfer.MustLoad() }) } 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/evm.pb.go b/x/evm/evm.pb.go index fa9c6988e..59bdc4cbe 100644 --- a/x/evm/evm.pb.go +++ b/x/evm/evm.pb.go @@ -25,8 +25,8 @@ var _ = math.Inf // proto package needs to be updated. const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package -// FunToken is a fungible token mapping between a bank coin and a corresponding -// ERC-20 smart contract. Bank coins here refer to tokens like NIBI, IBC +// FunToken is a fungible token mapping between a Bank Coin and a corresponding +// ERC-20 smart contract. Bank Coins here refer to tokens like NIBI, IBC // coins (ICS-20), and token factory coins, which are each represented by the // "Coin" type in Golang. type FunToken struct { @@ -34,7 +34,7 @@ type FunToken struct { Erc20Addr github_com_NibiruChain_nibiru_v2_eth.EIP55Addr `protobuf:"bytes,1,opt,name=erc20_addr,json=erc20Addr,proto3,customtype=github.com/NibiruChain/nibiru/v2/eth.EIP55Addr" json:"erc20_addr"` // bank_denom: Coin denomination in the Bank Module. BankDenom string `protobuf:"bytes,2,opt,name=bank_denom,json=bankDenom,proto3" json:"bank_denom,omitempty"` - // True if the `FunToken` mapping was created from an existing bank coin and + // True if the `FunToken` mapping was created from an existing Bank Coin and // the ERC-20 contract gets deployed by the module account. False if the // mapping was created from an externally owned ERC-20 contract. IsMadeFromCoin bool `protobuf:"varint,3,opt,name=is_made_from_coin,json=isMadeFromCoin,proto3" json:"is_made_from_coin,omitempty"` 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 abecd80ca..10404bea4 100644 --- a/x/evm/keeper/erc20.go +++ b/x/evm/keeper/erc20.go @@ -2,7 +2,6 @@ package keeper import ( - "encoding/json" "fmt" "math/big" @@ -10,13 +9,13 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" gethabi "github.com/ethereum/go-ethereum/accounts/abi" gethcommon "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" gethcore "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" serverconfig "github.com/NibiruChain/nibiru/v2/app/server/config" + "github.com/NibiruChain/nibiru/v2/x/evm" "github.com/NibiruChain/nibiru/v2/x/evm/embeds" - "github.com/NibiruChain/nibiru/v2/x/evm/statedb" ) // ERC20 returns a mutable reference to the keeper with an ERC20 contract ABI and @@ -57,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 } /* @@ -78,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 } @@ -118,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] @@ -149,30 +150,33 @@ 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]. +// CallContractWithInput invokes a smart contract with the given [contractInput] +// or deploys a new contract. // // Parameters: // - ctx: The SDK context for the transaction. // - fromAcc: The Ethereum address of the account initiating the contract call. -// - contract: Pointer to the Ethereum address of the contract to be called. -// - commit: Boolean flag indicating whether to commit the transaction (true) or simulate it (false). +// - contract: Pointer to the Ethereum address of the contract. Nil if new +// contract is deployed. +// - commit: Boolean flag indicating whether to commit the transaction (true) +// or simulate it (false). // - contractInput: Hexadecimal-encoded bytes use as input data to the call. // // Note: This function handles both contract method calls and simulations, -// depending on the 'commit' parameter. It uses a default gas limit for -// simulations and estimates gas for actual transactions. +// depending on the 'commit' parameter. It uses a default gas limit. func (k Keeper) CallContractWithInput( ctx sdk.Context, fromAcc gethcommon.Address, contract *gethcommon.Address, commit bool, contractInput []byte, -) (evmResp *evm.MsgEthereumTxResponse, err error) { - // This is a `defer` pattern to add behavior that runs in the case that the error is - // non-nil, creating a concise way to add extra information. +) (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() { if err != nil { err = fmt.Errorf("CallContractError: %w", err) @@ -180,13 +184,9 @@ func (k Keeper) CallContractWithInput( }() nonce := k.GetAccNonce(ctx, fromAcc) + // Gas cap sufficient for all "honest" ERC20 calls without malicious (gas + // intensive) code in contracts gasLimit := serverconfig.DefaultEthCallGasLimit - gasLimit, err = computeCommitGasLimit( - commit, gasLimit, &fromAcc, contract, contractInput, k, ctx, - ) - if err != nil { - return nil, err - } unusedBigInt := big.NewInt(0) evmMsg := gethcore.NewMessage( @@ -210,70 +210,58 @@ func (k Keeper) CallContractWithInput( k.EthChainID(ctx), ) if err != nil { - return nil, errors.Wrapf(err, "failed to load evm config") - } - - txConfig := statedb.NewEmptyTxConfig(gethcommon.BytesToHash(ctx.HeaderHash())) - evmResp, err = k.ApplyEvmMsg( - ctx, evmMsg, evm.NewNoOpTracer(), commit, evmCfg, txConfig, - ) - if err != nil { - return nil, errors.Wrapf(err, "failed to apply EVM message") - } - - if evmResp.Failed() { - return nil, errors.Wrapf(err, "EVM execution failed: %s", evmResp.VmError) + err = errors.Wrapf(err, "failed to load EVM config") + return } - return evmResp, err -} - -// computeCommitGasLimit: If the transition is meant to mutate state, this -// function computes an appopriates gas limit for the call with "contractInput" -// bytes against the given contract address. -// -// ℹ️ This creates a cached context for gas estimation, which is essential -// because state transitions can occur outside of the EVM that are triggered -// by Ethereum transactions, like in the case of precompiled contract or -// custom EVM hook that runs after tx execution. Gas estimation in that case -// could mutate the "ctx" object and result in falty resulting state, so we -// must cache here to avoid this issue. -func computeCommitGasLimit( - commit bool, - gasLimit uint64, - fromAcc, contract *gethcommon.Address, - contractInput []byte, - k Keeper, - ctx sdk.Context, -) (newGasLimit uint64, err error) { - if !commit { - return gasLimit, nil - } + // Generating TxConfig with an empty tx hash as there is no actual eth tx + // sent by a user + txConfig := k.TxConfig(ctx, gethcommon.BigToHash(big.NewInt(0))) - cachedCtx, _ := ctx.CacheContext() // IMPORTANT! + // Using tmp context to not modify the state in case of evm revert + tmpCtx, commitCtx := ctx.CacheContext() - jsonArgs, err := json.Marshal(evm.JsonTxArgs{ - From: fromAcc, - To: contract, - Data: (*hexutil.Bytes)(&contractInput), - }) - if err != nil { - return gasLimit, fmt.Errorf("failed compute gas limit to marshal tx args: %w", err) - } - - gasRes, err := k.EstimateGasForEvmCallType( - sdk.WrapSDKContext(cachedCtx), - &evm.EthCallRequest{ - Args: jsonArgs, - GasCap: gasLimit, - }, - evm.CallTypeSmart, + evmResp, evmObj, err = k.ApplyEvmMsg( + tmpCtx, evmMsg, evm.NewNoOpTracer(), commit, evmCfg, txConfig, ) if err != nil { - return gasLimit, fmt.Errorf("failed to compute gas limit: %w", err) + // We don't know the actual gas used, so consuming the gas limit + k.ResetGasMeterAndConsumeGas(ctx, gasLimit) + 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() { + err = fmt.Errorf("VMError: %w", evm.NewExecErrorWithReason(evmResp.Ret)) + return + } + err = fmt.Errorf("VMError: %s", evmResp.VmError) + return + } + err = fmt.Errorf("gas required exceeds allowance (%d)", gasLimit) + return + } 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()) + 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, nil, errors.Wrap(err, "error emitting ethereum tx events") + } + blockTxIdx := uint64(txConfig.TxIndex) + 1 + k.EvmState.BlockTxIndex.Set(ctx, blockTxIdx) + } + return evmResp, evmObj, nil } - - return gasRes.Gas, nil } func (k Keeper) LoadERC20Name( 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 3eaf1462c..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{ @@ -277,6 +278,101 @@ func (s *FunTokenFromErc20Suite) TestSendFromEvmToCosmos() { s.Require().Error(err) } +// TestCreateFunTokenFromERC20MaliciousName tries to create funtoken from a contract +// with a malicious (gas intensive) name() function. +// Fun token should fail creation with "out of gas" +func (s *FunTokenFromErc20Suite) TestCreateFunTokenFromERC20MaliciousName() { + deps := evmtest.NewTestDeps() + + s.T().Log("Deploy ERC20MaliciousName") + metadata := keeper.ERC20Metadata{ + Name: "erc20name", + Symbol: "TOKEN", + Decimals: 18, + } + deployResp, err := evmtest.DeployContract( + &deps, embeds.SmartContract_TestERC20MaliciousName, + metadata.Name, metadata.Symbol, metadata.Decimals, + ) + s.Require().NoError(err) + + erc20Addr := eth.EIP55Addr{ + Address: deployResp.ContractAddr, + } + + s.T().Log("sad: CreateFunToken for ERC20 with malicious name") + s.Require().NoError(testapp.FundAccount( + deps.App.BankKeeper, + deps.Ctx, + deps.Sender.NibiruAddr, + deps.EvmKeeper.FeeForCreateFunToken(deps.Ctx), + )) + + _, err = deps.EvmKeeper.CreateFunToken( + sdk.WrapSDKContext(deps.Ctx), + &evm.MsgCreateFunToken{ + FromErc20: &erc20Addr, + Sender: deps.Sender.NibiruAddr.String(), + }, + ) + s.Require().ErrorContains(err, "gas required exceeds allowance") +} + +// TestFunTokenFromERC20MaliciousTransfer creates a funtoken from a contract +// with a malicious (gas intensive) transfer() function. +// Fun token should be created but sending from erc20 to bank should fail with out of gas +func (s *FunTokenFromErc20Suite) TestFunTokenFromERC20MaliciousTransfer() { + deps := evmtest.NewTestDeps() + + s.T().Log("Deploy ERC20MaliciousTransfer") + metadata := keeper.ERC20Metadata{ + Name: "erc20name", + Symbol: "TOKEN", + Decimals: 18, + } + deployResp, err := evmtest.DeployContract( + &deps, embeds.SmartContract_TestERC20MaliciousTransfer, + metadata.Name, metadata.Symbol, metadata.Decimals, + ) + s.Require().NoError(err) + + erc20Addr := eth.EIP55Addr{ + Address: deployResp.ContractAddr, + } + + s.T().Log("happy: CreateFunToken for ERC20 with malicious transfer") + s.Require().NoError(testapp.FundAccount( + deps.App.BankKeeper, + deps.Ctx, + deps.Sender.NibiruAddr, + deps.EvmKeeper.FeeForCreateFunToken(deps.Ctx), + )) + + _, err = deps.EvmKeeper.CreateFunToken( + sdk.WrapSDKContext(deps.Ctx), + &evm.MsgCreateFunToken{ + FromErc20: &erc20Addr, + Sender: deps.Sender.NibiruAddr.String(), + }, + ) + s.Require().NoError(err) + randomAcc := testutil.AccAddress() + + s.T().Log("send erc20 tokens to cosmos") + _, err = deps.EvmKeeper.CallContract( + deps.Ctx, + embeds.SmartContract_FunToken.ABI, + deps.Sender.EthAddr, + &precompile.PrecompileAddr_FunToken, + true, + "bankSend", + deployResp.ContractAddr, + big.NewInt(1), + randomAcc.String(), + ) + s.Require().ErrorContains(err, "gas required exceeds allowance") +} + type FunTokenFromErc20Suite struct { suite.Suite } 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 27e80a610..89a249dd3 100644 --- a/x/evm/keeper/msg_server.go +++ b/x/evm/keeper/msg_server.go @@ -31,8 +31,8 @@ var _ evm.MsgServer = &Keeper{} func (k *Keeper) EthereumTx( goCtx context.Context, txMsg *evm.MsgEthereumTx, ) (evmResp *evm.MsgEthereumTxResponse, err error) { - // This is a `defer` pattern to add behavior that runs in the case that the error is - // non-nil, creating a concise way to add extra information. + // 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 { err = fmt.Errorf("EthereumTx error: %w", err) @@ -54,15 +54,15 @@ func (k *Keeper) EthereumTx( // get the signer according to the chain rules from the config and block height signer := gethcore.MakeSigner(evmConfig.ChainConfig, big.NewInt(ctx.BlockHeight())) - msg, err := tx.AsMessage(signer, evmConfig.BaseFeeWei) + evmMsg, err := tx.AsMessage(signer, evmConfig.BaseFeeWei) if err != nil { return nil, errors.Wrap(err, "failed to return ethereum transaction as core message") } - tmpCtx, commit := ctx.CacheContext() + tmpCtx, commitCtx := ctx.CacheContext() // pass true to commit the StateDB - evmResp, err = k.ApplyEvmMsg(tmpCtx, msg, 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() @@ -70,74 +70,38 @@ func (k *Keeper) EthereumTx( return nil, errors.Wrap(err, "failed to apply ethereum core message") } - logs := evm.LogsToEthereum(evmResp.Logs) - - cumulativeGasUsed := evmResp.GasUsed - if ctx.BlockGasMeter() != nil { - limit := ctx.BlockGasMeter().Limit() - cumulativeGasUsed += ctx.BlockGasMeter().GasConsumed() - if cumulativeGasUsed > limit { - cumulativeGasUsed = limit - } - } - - var contractAddr gethcommon.Address - if msg.To() == nil { - contractAddr = crypto.CreateAddress(msg.From(), msg.Nonce()) - } - - receipt := &gethcore.Receipt{ - Type: tx.Type(), - PostState: nil, // TODO: intermediate state root - CumulativeGasUsed: cumulativeGasUsed, - Bloom: k.EvmState.CalcBloomFromLogs(ctx, logs), - Logs: logs, - TxHash: txConfig.TxHash, - ContractAddress: contractAddr, - GasUsed: evmResp.GasUsed, - BlockHash: txConfig.BlockHash, - BlockNumber: big.NewInt(ctx.BlockHeight()), - TransactionIndex: txConfig.TxIndex, - } - if !evmResp.Failed() { - receipt.Status = gethcore.ReceiptStatusSuccessful - commit() + commitCtx() } - // refund gas in order to match the Ethereum gas consumption instead of the default SDK one. + // refund gas in order to match the Ethereum gas consumption instead of the + // default SDK one. refundGas := uint64(0) - if msg.Gas() > evmResp.GasUsed { - refundGas = msg.Gas() - evmResp.GasUsed + if evmMsg.Gas() > evmResp.GasUsed { + refundGas = evmMsg.Gas() - evmResp.GasUsed } weiPerGas := txMsg.EffectiveGasPriceWeiPerGas(evmConfig.BaseFeeWei) - if err = k.RefundGas(ctx, msg.From(), refundGas, weiPerGas); err != nil { - return nil, errors.Wrapf(err, "error refunding leftover gas to sender %s", msg.From()) + if err = k.RefundGas(ctx, evmMsg.From(), refundGas, weiPerGas); err != nil { + return nil, errors.Wrapf(err, "error refunding leftover gas to sender %s", evmMsg.From()) } - if len(receipt.Logs) > 0 { - // Update transient block bloom filter - k.EvmState.BlockBloom.Set(ctx, receipt.Bloom.Bytes()) - blockLogSize := uint64(txConfig.LogIndex) + uint64(len(receipt.Logs)) - k.EvmState.BlockLogSize.Set(ctx, blockLogSize) - } + k.updateBlockBloom(ctx, evmResp, uint64(txConfig.LogIndex)) totalGasUsed, err := k.AddToBlockGasUsed(ctx, evmResp.GasUsed) if err != nil { return nil, errors.Wrap(err, "error adding transient gas used to block") } - // reset the gas meter for current cosmos transaction + // reset the gas meter for current TxMsg (EthereumTx) k.ResetGasMeterAndConsumeGas(ctx, totalGasUsed) - err = k.EmitEthereumTxEvents(ctx, tx, msg, evmResp, contractAddr) + err = k.EmitEthereumTxEvents(ctx, tx.To(), tx.Type(), evmMsg, evmResp) if err != nil { return nil, errors.Wrap(err, "error emitting ethereum tx events") } blockTxIdx := uint64(txConfig.TxIndex) + 1 k.EvmState.BlockTxIndex.Set(ctx, blockTxIdx) - return evmResp, nil } @@ -282,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() @@ -308,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 @@ -319,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, @@ -339,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 { @@ -369,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 @@ -388,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) } } @@ -397,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() @@ -417,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) { @@ -673,10 +637,10 @@ func (k Keeper) convertCoinNativeERC20( // EmitEthereumTxEvents emits all types of EVM events applicable to a particular execution case func (k *Keeper) EmitEthereumTxEvents( ctx sdk.Context, - tx *gethcore.Transaction, + recipient *gethcommon.Address, + txType uint8, msg gethcore.Message, evmResp *evm.MsgEthereumTxResponse, - contractAddr gethcommon.Address, ) error { // Typed event: eth.evm.v1.EventEthereumTx eventEthereumTx := &evm.EventEthereumTx{ @@ -687,8 +651,8 @@ func (k *Keeper) EmitEthereumTxEvents( if len(ctx.TxBytes()) > 0 { eventEthereumTx.Hash = tmbytes.HexBytes(tmtypes.Tx(ctx.TxBytes()).Hash()).String() } - if to := tx.To(); to != nil { - eventEthereumTx.Recipient = to.Hex() + if recipient != nil { + eventEthereumTx.Recipient = recipient.Hex() } if evmResp.Failed() { eventEthereumTx.EthTxFailed = evmResp.VmError @@ -715,13 +679,14 @@ func (k *Keeper) EmitEthereumTxEvents( sdk.EventTypeMessage, sdk.NewAttribute(sdk.AttributeKeyModule, evm.ModuleName), sdk.NewAttribute(sdk.AttributeKeySender, msg.From().Hex()), - sdk.NewAttribute(evm.MessageEventAttrTxType, fmt.Sprintf("%d", tx.Type())), + sdk.NewAttribute(evm.MessageEventAttrTxType, fmt.Sprintf("%d", txType)), ), ) // Emit typed events if !evmResp.Failed() { - if tx.To() == nil { // contract creation + if recipient == nil { // contract creation + contractAddr := crypto.CreateAddress(msg.From(), msg.Nonce()) _ = ctx.EventManager().EmitTypedEvent(&evm.EventContractDeployed{ Sender: msg.From().Hex(), ContractAddr: contractAddr.String(), @@ -742,3 +707,17 @@ func (k *Keeper) EmitEthereumTxEvents( return nil } + +// updateBlockBloom updates transient block bloom filter +func (k *Keeper) updateBlockBloom( + ctx sdk.Context, + evmResp *evm.MsgEthereumTxResponse, + logIndex uint64, +) { + logs := evm.LogsToEthereum(evmResp.Logs) + if len(logs) > 0 { + k.EvmState.BlockBloom.Set(ctx, k.EvmState.CalcBloomFromLogs(ctx, logs).Bytes()) + blockLogSize := logIndex + uint64(len(logs)) + k.EvmState.BlockLogSize.Set(ctx, blockLogSize) + } +} 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 091999ee3..10817c673 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) @@ -341,11 +320,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 6db1d7642..d796f8b89 100644 --- a/x/evm/precompile/wasm_test.go +++ b/x/evm/precompile/wasm_test.go @@ -4,16 +4,14 @@ 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/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" @@ -24,127 +22,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") @@ -172,7 +52,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) @@ -201,7 +81,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) @@ -211,178 +91,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 @@ -407,7 +136,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) @@ -430,7 +159,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) @@ -577,11 +306,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/query.pb.go b/x/evm/query.pb.go index a710bd10d..26db67b42 100644 --- a/x/evm/query.pb.go +++ b/x/evm/query.pb.go @@ -1198,8 +1198,9 @@ func (m *QueryBaseFeeRequest) XXX_DiscardUnknown() { var xxx_messageInfo_QueryBaseFeeRequest proto.InternalMessageInfo // QueryBaseFeeResponse returns the EIP1559 base fee. +// See https://github.com/ethereum/EIPs/blob/ba6c342c23164072adb500c3136e3ae6eabff306/EIPS/eip-1559.md. type QueryBaseFeeResponse struct { - // base_fee is the EIP1559 base fee + // base_fee is the EIP1559 base fee in units of wei. BaseFee *cosmossdk_io_math.Int `protobuf:"bytes,1,opt,name=base_fee,json=baseFee,proto3,customtype=cosmossdk.io/math.Int" json:"base_fee,omitempty"` // base_fee is the EIP1559 base fee in units of micronibi ("unibi"). BaseFeeUnibi *cosmossdk_io_math.Int `protobuf:"bytes,2,opt,name=base_fee_unibi,json=baseFeeUnibi,proto3,customtype=cosmossdk.io/math.Int" json:"base_fee_unibi,omitempty"` @@ -1239,7 +1240,8 @@ func (m *QueryBaseFeeResponse) XXX_DiscardUnknown() { var xxx_messageInfo_QueryBaseFeeResponse proto.InternalMessageInfo type QueryFunTokenMappingRequest struct { - // either the 0x contract address of the ERC-20 token or the cosmos denom + // Either the hexadecimal-encoded ERC20 contract address or denomination of the + // Bank Coin. Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"` } @@ -1277,7 +1279,7 @@ func (m *QueryFunTokenMappingRequest) XXX_DiscardUnknown() { var xxx_messageInfo_QueryFunTokenMappingRequest proto.InternalMessageInfo type QueryFunTokenMappingResponse struct { - // fun_token is a mapping between the Cosmos native coin and the ERC20 contract address + // fun_token is a mapping between the Bank Coin and the ERC20 contract address FunToken *FunToken `protobuf:"bytes,1,opt,name=fun_token,json=funToken,proto3" json:"fun_token,omitempty"` } 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) diff --git a/x/evm/tx.pb.go b/x/evm/tx.pb.go index f2082703b..f1ab92dae 100644 --- a/x/evm/tx.pb.go +++ b/x/evm/tx.pb.go @@ -475,8 +475,8 @@ func (m *MsgUpdateParamsResponse) XXX_DiscardUnknown() { var xxx_messageInfo_MsgUpdateParamsResponse proto.InternalMessageInfo // MsgCreateFunToken: Arguments to create a "FunToken" mapping. Either the ERC20 -// contract address can be given to create the mapping to a bank coin, or the -// denomination for a bank coin can be given to create the mapping to an ERC20. +// contract address can be given to create the mapping to a Bank Coin, or the +// denomination for a Bank Coin can be given to create the mapping to an ERC20. type MsgCreateFunToken struct { // Hexadecimal address of the ERC20 token to which the `FunToken` maps FromErc20 *github_com_NibiruChain_nibiru_v2_eth.EIP55Addr `protobuf:"bytes,1,opt,name=from_erc20,json=fromErc20,proto3,customtype=github.com/NibiruChain/nibiru/v2/eth.EIP55Addr" json:"from_erc20,omitempty"` @@ -578,13 +578,13 @@ func (m *MsgCreateFunTokenResponse) GetFuntokenMapping() FunToken { return FunToken{} } -// MsgConvertCoinToEvm: Arguments to send a bank coin to ERC-20 representation +// MsgConvertCoinToEvm: Arguments to send a Bank Coin to ERC-20 representation type MsgConvertCoinToEvm struct { // Hexadecimal address of the ERC20 token to which the `FunToken` maps ToEthAddr github_com_NibiruChain_nibiru_v2_eth.EIP55Addr `protobuf:"bytes,1,opt,name=to_eth_addr,json=toEthAddr,proto3,customtype=github.com/NibiruChain/nibiru/v2/eth.EIP55Addr" json:"to_eth_addr"` // Sender: Address for the signer of the transaction. Sender string `protobuf:"bytes,2,opt,name=sender,proto3" json:"sender,omitempty"` - // Bank coin to get converted to ERC20 + // Bank Coin to get converted to ERC20 BankCoin types1.Coin `protobuf:"bytes,3,opt,name=bank_coin,json=bankCoin,proto3" json:"bank_coin" yaml:"bank_coin"` } @@ -784,12 +784,12 @@ const _ = grpc.SupportPackageIsVersion4 type MsgClient interface { // EthereumTx defines a method submitting Ethereum transactions. EthereumTx(ctx context.Context, in *MsgEthereumTx, opts ...grpc.CallOption) (*MsgEthereumTxResponse, error) - // UpdateParams defined a governance operation for updating the x/evm module parameters. - // The authority is hard-coded to the Cosmos SDK x/gov module account + // UpdateParams defined a governance operation for updating the x/evm module + // parameters. The authority is hard-coded to the x/gov module account UpdateParams(ctx context.Context, in *MsgUpdateParams, opts ...grpc.CallOption) (*MsgUpdateParamsResponse, error) // CreateFunToken: Create a "FunToken" mapping. Either the ERC20 contract - // address can be given to create the mapping to a bank coin, or the - // denomination for a bank coin can be given to create the mapping to an ERC20. + // address can be given to create the mapping to a Bank Coin, or the + // denomination for a Bank Coin can be given to create the mapping to an ERC20. CreateFunToken(ctx context.Context, in *MsgCreateFunToken, opts ...grpc.CallOption) (*MsgCreateFunTokenResponse, error) // ConvertCoinToEvm: Sends a coin with a valid "FunToken" mapping to the // given recipient address ("to_eth_addr") in the corresponding ERC20 @@ -845,12 +845,12 @@ func (c *msgClient) ConvertCoinToEvm(ctx context.Context, in *MsgConvertCoinToEv type MsgServer interface { // EthereumTx defines a method submitting Ethereum transactions. EthereumTx(context.Context, *MsgEthereumTx) (*MsgEthereumTxResponse, error) - // UpdateParams defined a governance operation for updating the x/evm module parameters. - // The authority is hard-coded to the Cosmos SDK x/gov module account + // UpdateParams defined a governance operation for updating the x/evm module + // parameters. The authority is hard-coded to the x/gov module account UpdateParams(context.Context, *MsgUpdateParams) (*MsgUpdateParamsResponse, error) // CreateFunToken: Create a "FunToken" mapping. Either the ERC20 contract - // address can be given to create the mapping to a bank coin, or the - // denomination for a bank coin can be given to create the mapping to an ERC20. + // address can be given to create the mapping to a Bank Coin, or the + // denomination for a Bank Coin can be given to create the mapping to an ERC20. CreateFunToken(context.Context, *MsgCreateFunToken) (*MsgCreateFunTokenResponse, error) // ConvertCoinToEvm: Sends a coin with a valid "FunToken" mapping to the // given recipient address ("to_eth_addr") in the corresponding ERC20