From 16393ca8bfc0fcc84eaeb6ed50678f99c5283eb9 Mon Sep 17 00:00:00 2001 From: Oleg Nikonychev Date: Wed, 30 Oct 2024 00:59:58 +0400 Subject: [PATCH 1/5] test(evm): statedb tests for race conditions within funtoken precompile (#2098) * test(evm): statedb tests for race conditions within funtoken precompile * chore: changelog update --- CHANGELOG.md | 1 + .../IFunToken.json | 2 +- .../TestERC20TransferThenPrecompileSend.json | 50 ++++ .../TestNativeSendThenPrecompileSend.json | 50 ++++ .../contracts/{FunToken.sol => IFunToken.sol} | 0 .../TestERC20TransferThenPrecompileSend.sol | 39 +++ .../TestNativeSendThenPrecompileSend.sol | 34 +++ x/evm/embeds/embeds.go | 28 +- x/evm/embeds/embeds_test.go | 2 + x/evm/keeper/funtoken_from_coin_test.go | 251 +++++++++++++++--- x/evm/precompile/funtoken.go | 2 +- 11 files changed, 418 insertions(+), 41 deletions(-) rename x/evm/embeds/artifacts/contracts/{FunToken.sol => IFunToken.sol}/IFunToken.json (93%) create mode 100644 x/evm/embeds/artifacts/contracts/TestERC20TransferThenPrecompileSend.sol/TestERC20TransferThenPrecompileSend.json create mode 100644 x/evm/embeds/artifacts/contracts/TestNativeSendThenPrecompileSend.sol/TestNativeSendThenPrecompileSend.json rename x/evm/embeds/contracts/{FunToken.sol => IFunToken.sol} (100%) create mode 100644 x/evm/embeds/contracts/TestERC20TransferThenPrecompileSend.sol create mode 100644 x/evm/embeds/contracts/TestNativeSendThenPrecompileSend.sol diff --git a/CHANGELOG.md b/CHANGELOG.md index 821b29e13..41cf28c19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,6 +69,7 @@ consistent setup and dynamic gas calculations, addressing the following tickets. - [#2088](https://github.com/NibiruChain/nibiru/pull/2088) - refactor(evm): remove outdated comment and improper error message text - [#2089](https://github.com/NibiruChain/nibiru/pull/2089) - better handling of gas consumption within erc20 contract execution - [#2091](https://github.com/NibiruChain/nibiru/pull/2091) - feat(evm): add fun token creation fee validation +- [#2098](https://github.com/NibiruChain/nibiru/pull/2098) - test(evm): statedb tests for race conditions within funtoken precompile #### Nibiru EVM | Before Audit 1 - 2024-10-18 diff --git a/x/evm/embeds/artifacts/contracts/FunToken.sol/IFunToken.json b/x/evm/embeds/artifacts/contracts/IFunToken.sol/IFunToken.json similarity index 93% rename from x/evm/embeds/artifacts/contracts/FunToken.sol/IFunToken.json rename to x/evm/embeds/artifacts/contracts/IFunToken.sol/IFunToken.json index 9f006c50f..6fe6e838a 100644 --- a/x/evm/embeds/artifacts/contracts/FunToken.sol/IFunToken.json +++ b/x/evm/embeds/artifacts/contracts/IFunToken.sol/IFunToken.json @@ -1,7 +1,7 @@ { "_format": "hh-sol-artifact-1", "contractName": "IFunToken", - "sourceName": "contracts/FunToken.sol", + "sourceName": "contracts/IFunToken.sol", "abi": [ { "inputs": [ diff --git a/x/evm/embeds/artifacts/contracts/TestERC20TransferThenPrecompileSend.sol/TestERC20TransferThenPrecompileSend.json b/x/evm/embeds/artifacts/contracts/TestERC20TransferThenPrecompileSend.sol/TestERC20TransferThenPrecompileSend.json new file mode 100644 index 000000000..234a7f943 --- /dev/null +++ b/x/evm/embeds/artifacts/contracts/TestERC20TransferThenPrecompileSend.sol/TestERC20TransferThenPrecompileSend.json @@ -0,0 +1,50 @@ +{ + "_format": "hh-sol-artifact-1", + "contractName": "TestERC20TransferThenPrecompileSend", + "sourceName": "contracts/TestERC20TransferThenPrecompileSend.sol", + "abi": [ + { + "inputs": [ + { + "internalType": "address", + "name": "erc20_", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "address payable", + "name": "transferRecipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "transferAmount", + "type": "uint256" + }, + { + "internalType": "string", + "name": "precompileRecipient", + "type": "string" + }, + { + "internalType": "uint256", + "name": "precompileAmount", + "type": "uint256" + } + ], + "name": "erc20TransferThenPrecompileSend", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } + ], + "bytecode": "0x608060405234801561001057600080fd5b50604051610974380380610974833981810160405281019061003291906100db565b806000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555050610108565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006100a88261007d565b9050919050565b6100b88161009d565b81146100c357600080fd5b50565b6000815190506100d5816100af565b92915050565b6000602082840312156100f1576100f0610078565b5b60006100ff848285016100c6565b91505092915050565b61085d806101176000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c8063264c325814610030575b600080fd5b61004a6004803603810190610045919061049f565b61004c565b005b60008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1663a9059cbb85856040518363ffffffff1660e01b81526004016100a7929190610590565b6020604051808303816000875af11580156100c6573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906100ea91906105f1565b610129576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016101209061067b565b60405180910390fd5b600061080073ffffffffffffffffffffffffffffffffffffffff1660008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1683856040516024016101799392919061072a565b6040516020818303038152906040527f03003bc5000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff838183161783525050505060405161020391906107af565b6000604051808303816000865af19150503d8060008114610240576040519150601f19603f3d011682016040523d82523d6000602084013e610245565b606091505b5050905080604051602001610259906107ec565b604051602081830303815290604052906102a9576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016102a09190610805565b60405180910390fd5b505050505050565b6000604051905090565b600080fd5b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006102f0826102c5565b9050919050565b610300816102e5565b811461030b57600080fd5b50565b60008135905061031d816102f7565b92915050565b6000819050919050565b61033681610323565b811461034157600080fd5b50565b6000813590506103538161032d565b92915050565b600080fd5b600080fd5b6000601f19601f8301169050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6103ac82610363565b810181811067ffffffffffffffff821117156103cb576103ca610374565b5b80604052505050565b60006103de6102b1565b90506103ea82826103a3565b919050565b600067ffffffffffffffff82111561040a57610409610374565b5b61041382610363565b9050602081019050919050565b82818337600083830152505050565b600061044261043d846103ef565b6103d4565b90508281526020810184848401111561045e5761045d61035e565b5b610469848285610420565b509392505050565b600082601f83011261048657610485610359565b5b813561049684826020860161042f565b91505092915050565b600080600080608085870312156104b9576104b86102bb565b5b60006104c78782880161030e565b94505060206104d887828801610344565b935050604085013567ffffffffffffffff8111156104f9576104f86102c0565b5b61050587828801610471565b925050606061051687828801610344565b91505092959194509250565b6000819050919050565b600061054761054261053d846102c5565b610522565b6102c5565b9050919050565b60006105598261052c565b9050919050565b600061056b8261054e565b9050919050565b61057b81610560565b82525050565b61058a81610323565b82525050565b60006040820190506105a56000830185610572565b6105b26020830184610581565b9392505050565b60008115159050919050565b6105ce816105b9565b81146105d957600080fd5b50565b6000815190506105eb816105c5565b92915050565b600060208284031215610607576106066102bb565b5b6000610615848285016105dc565b91505092915050565b600082825260208201905092915050565b7f4552432d3230207472616e73666572206661696c656400000000000000000000600082015250565b600061066560168361061e565b91506106708261062f565b602082019050919050565b6000602082019050818103600083015261069481610658565b9050919050565b60006106a6826102c5565b9050919050565b6106b68161069b565b82525050565b600081519050919050565b60005b838110156106e55780820151818401526020810190506106ca565b60008484015250505050565b60006106fc826106bc565b610706818561061e565b93506107168185602086016106c7565b61071f81610363565b840191505092915050565b600060608201905061073f60008301866106ad565b61074c6020830185610581565b818103604083015261075e81846106f1565b9050949350505050565b600081519050919050565b600081905092915050565b600061078982610768565b6107938185610773565b93506107a38185602086016106c7565b80840191505092915050565b60006107bb828461077e565b915081905092915050565b7f4661696c656420746f2063616c6c2062616e6b53656e64000000000000000000815250565b60006107f7826107c6565b601782019150819050919050565b6000602082019050818103600083015261081f81846106f1565b90509291505056fea26469706673582212205d952e3358213844ef60215f35b3c3adb68dbc657522a8dd683eb1c5c0f64bfa64736f6c63430008180033", + "deployedBytecode": "0x608060405234801561001057600080fd5b506004361061002b5760003560e01c8063264c325814610030575b600080fd5b61004a6004803603810190610045919061049f565b61004c565b005b60008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1663a9059cbb85856040518363ffffffff1660e01b81526004016100a7929190610590565b6020604051808303816000875af11580156100c6573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906100ea91906105f1565b610129576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016101209061067b565b60405180910390fd5b600061080073ffffffffffffffffffffffffffffffffffffffff1660008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1683856040516024016101799392919061072a565b6040516020818303038152906040527f03003bc5000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff838183161783525050505060405161020391906107af565b6000604051808303816000865af19150503d8060008114610240576040519150601f19603f3d011682016040523d82523d6000602084013e610245565b606091505b5050905080604051602001610259906107ec565b604051602081830303815290604052906102a9576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016102a09190610805565b60405180910390fd5b505050505050565b6000604051905090565b600080fd5b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006102f0826102c5565b9050919050565b610300816102e5565b811461030b57600080fd5b50565b60008135905061031d816102f7565b92915050565b6000819050919050565b61033681610323565b811461034157600080fd5b50565b6000813590506103538161032d565b92915050565b600080fd5b600080fd5b6000601f19601f8301169050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6103ac82610363565b810181811067ffffffffffffffff821117156103cb576103ca610374565b5b80604052505050565b60006103de6102b1565b90506103ea82826103a3565b919050565b600067ffffffffffffffff82111561040a57610409610374565b5b61041382610363565b9050602081019050919050565b82818337600083830152505050565b600061044261043d846103ef565b6103d4565b90508281526020810184848401111561045e5761045d61035e565b5b610469848285610420565b509392505050565b600082601f83011261048657610485610359565b5b813561049684826020860161042f565b91505092915050565b600080600080608085870312156104b9576104b86102bb565b5b60006104c78782880161030e565b94505060206104d887828801610344565b935050604085013567ffffffffffffffff8111156104f9576104f86102c0565b5b61050587828801610471565b925050606061051687828801610344565b91505092959194509250565b6000819050919050565b600061054761054261053d846102c5565b610522565b6102c5565b9050919050565b60006105598261052c565b9050919050565b600061056b8261054e565b9050919050565b61057b81610560565b82525050565b61058a81610323565b82525050565b60006040820190506105a56000830185610572565b6105b26020830184610581565b9392505050565b60008115159050919050565b6105ce816105b9565b81146105d957600080fd5b50565b6000815190506105eb816105c5565b92915050565b600060208284031215610607576106066102bb565b5b6000610615848285016105dc565b91505092915050565b600082825260208201905092915050565b7f4552432d3230207472616e73666572206661696c656400000000000000000000600082015250565b600061066560168361061e565b91506106708261062f565b602082019050919050565b6000602082019050818103600083015261069481610658565b9050919050565b60006106a6826102c5565b9050919050565b6106b68161069b565b82525050565b600081519050919050565b60005b838110156106e55780820151818401526020810190506106ca565b60008484015250505050565b60006106fc826106bc565b610706818561061e565b93506107168185602086016106c7565b61071f81610363565b840191505092915050565b600060608201905061073f60008301866106ad565b61074c6020830185610581565b818103604083015261075e81846106f1565b9050949350505050565b600081519050919050565b600081905092915050565b600061078982610768565b6107938185610773565b93506107a38185602086016106c7565b80840191505092915050565b60006107bb828461077e565b915081905092915050565b7f4661696c656420746f2063616c6c2062616e6b53656e64000000000000000000815250565b60006107f7826107c6565b601782019150819050919050565b6000602082019050818103600083015261081f81846106f1565b90509291505056fea26469706673582212205d952e3358213844ef60215f35b3c3adb68dbc657522a8dd683eb1c5c0f64bfa64736f6c63430008180033", + "linkReferences": {}, + "deployedLinkReferences": {} +} diff --git a/x/evm/embeds/artifacts/contracts/TestNativeSendThenPrecompileSend.sol/TestNativeSendThenPrecompileSend.json b/x/evm/embeds/artifacts/contracts/TestNativeSendThenPrecompileSend.sol/TestNativeSendThenPrecompileSend.json new file mode 100644 index 000000000..1d44a675c --- /dev/null +++ b/x/evm/embeds/artifacts/contracts/TestNativeSendThenPrecompileSend.sol/TestNativeSendThenPrecompileSend.json @@ -0,0 +1,50 @@ +{ + "_format": "hh-sol-artifact-1", + "contractName": "TestNativeSendThenPrecompileSend", + "sourceName": "contracts/TestNativeSendThenPrecompileSend.sol", + "abi": [ + { + "inputs": [ + { + "internalType": "address", + "name": "erc20_", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "address payable", + "name": "nativeRecipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "nativeAmount", + "type": "uint256" + }, + { + "internalType": "string", + "name": "precompileRecipient", + "type": "string" + }, + { + "internalType": "uint256", + "name": "precompileAmount", + "type": "uint256" + } + ], + "name": "nativeSendThenPrecompileSend", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } + ], + "bytecode": "0x608060405234801561001057600080fd5b50604051610877380380610877833981810160405281019061003291906100db565b806000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555050610108565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006100a88261007d565b9050919050565b6100b88161009d565b81146100c357600080fd5b50565b6000815190506100d5816100af565b92915050565b6000602082840312156100f1576100f0610078565b5b60006100ff848285016100c6565b91505092915050565b610760806101176000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c8063a4de557414610030575b600080fd5b61004a6004803603810190610045919061043c565b61004c565b005b60008473ffffffffffffffffffffffffffffffffffffffff166108fc859081150290604051600060405180830381858888f193505050509050806100c5576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016100bc9061051c565b60405180910390fd5b600061080073ffffffffffffffffffffffffffffffffffffffff1660008054906101000a900473ffffffffffffffffffffffffffffffffffffffff168486604051602401610115939291906105da565b6040516020818303038152906040527f03003bc5000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff838183161783525050505060405161019f919061065f565b6000604051808303816000865af19150503d80600081146101dc576040519150601f19603f3d011682016040523d82523d6000602084013e6101e1565b606091505b50509050806040516020016101f5906106f3565b60405160208183030381529060405290610245576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161023c9190610708565b60405180910390fd5b50505050505050565b6000604051905090565b600080fd5b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b600061028d82610262565b9050919050565b61029d81610282565b81146102a857600080fd5b50565b6000813590506102ba81610294565b92915050565b6000819050919050565b6102d3816102c0565b81146102de57600080fd5b50565b6000813590506102f0816102ca565b92915050565b600080fd5b600080fd5b6000601f19601f8301169050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b61034982610300565b810181811067ffffffffffffffff8211171561036857610367610311565b5b80604052505050565b600061037b61024e565b90506103878282610340565b919050565b600067ffffffffffffffff8211156103a7576103a6610311565b5b6103b082610300565b9050602081019050919050565b82818337600083830152505050565b60006103df6103da8461038c565b610371565b9050828152602081018484840111156103fb576103fa6102fb565b5b6104068482856103bd565b509392505050565b600082601f830112610423576104226102f6565b5b81356104338482602086016103cc565b91505092915050565b6000806000806080858703121561045657610455610258565b5b6000610464878288016102ab565b9450506020610475878288016102e1565b935050604085013567ffffffffffffffff8111156104965761049561025d565b5b6104a28782880161040e565b92505060606104b3878288016102e1565b91505092959194509250565b600082825260208201905092915050565b7f4661696c656420746f2073656e64206e617469766520746f6b656e0000000000600082015250565b6000610506601b836104bf565b9150610511826104d0565b602082019050919050565b60006020820190508181036000830152610535816104f9565b9050919050565b600061054782610262565b9050919050565b6105578161053c565b82525050565b610566816102c0565b82525050565b600081519050919050565b60005b8381101561059557808201518184015260208101905061057a565b60008484015250505050565b60006105ac8261056c565b6105b681856104bf565b93506105c6818560208601610577565b6105cf81610300565b840191505092915050565b60006060820190506105ef600083018661054e565b6105fc602083018561055d565b818103604083015261060e81846105a1565b9050949350505050565b600081519050919050565b600081905092915050565b600061063982610618565b6106438185610623565b9350610653818560208601610577565b80840191505092915050565b600061066b828461062e565b915081905092915050565b600081905092915050565b7f4661696c656420746f2063616c6c20707265636f6d70696c652062616e6b536560008201527f6e64000000000000000000000000000000000000000000000000000000000000602082015250565b60006106dd602283610676565b91506106e882610681565b602282019050919050565b60006106fe826106d0565b9150819050919050565b6000602082019050818103600083015261072281846105a1565b90509291505056fea26469706673582212206bab9cbf21b89041a55ee0f768a9443ae5e5ebfde23fe8a91daf9d4b339427db64736f6c63430008180033", + "deployedBytecode": "0x608060405234801561001057600080fd5b506004361061002b5760003560e01c8063a4de557414610030575b600080fd5b61004a6004803603810190610045919061043c565b61004c565b005b60008473ffffffffffffffffffffffffffffffffffffffff166108fc859081150290604051600060405180830381858888f193505050509050806100c5576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016100bc9061051c565b60405180910390fd5b600061080073ffffffffffffffffffffffffffffffffffffffff1660008054906101000a900473ffffffffffffffffffffffffffffffffffffffff168486604051602401610115939291906105da565b6040516020818303038152906040527f03003bc5000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff838183161783525050505060405161019f919061065f565b6000604051808303816000865af19150503d80600081146101dc576040519150601f19603f3d011682016040523d82523d6000602084013e6101e1565b606091505b50509050806040516020016101f5906106f3565b60405160208183030381529060405290610245576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161023c9190610708565b60405180910390fd5b50505050505050565b6000604051905090565b600080fd5b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b600061028d82610262565b9050919050565b61029d81610282565b81146102a857600080fd5b50565b6000813590506102ba81610294565b92915050565b6000819050919050565b6102d3816102c0565b81146102de57600080fd5b50565b6000813590506102f0816102ca565b92915050565b600080fd5b600080fd5b6000601f19601f8301169050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b61034982610300565b810181811067ffffffffffffffff8211171561036857610367610311565b5b80604052505050565b600061037b61024e565b90506103878282610340565b919050565b600067ffffffffffffffff8211156103a7576103a6610311565b5b6103b082610300565b9050602081019050919050565b82818337600083830152505050565b60006103df6103da8461038c565b610371565b9050828152602081018484840111156103fb576103fa6102fb565b5b6104068482856103bd565b509392505050565b600082601f830112610423576104226102f6565b5b81356104338482602086016103cc565b91505092915050565b6000806000806080858703121561045657610455610258565b5b6000610464878288016102ab565b9450506020610475878288016102e1565b935050604085013567ffffffffffffffff8111156104965761049561025d565b5b6104a28782880161040e565b92505060606104b3878288016102e1565b91505092959194509250565b600082825260208201905092915050565b7f4661696c656420746f2073656e64206e617469766520746f6b656e0000000000600082015250565b6000610506601b836104bf565b9150610511826104d0565b602082019050919050565b60006020820190508181036000830152610535816104f9565b9050919050565b600061054782610262565b9050919050565b6105578161053c565b82525050565b610566816102c0565b82525050565b600081519050919050565b60005b8381101561059557808201518184015260208101905061057a565b60008484015250505050565b60006105ac8261056c565b6105b681856104bf565b93506105c6818560208601610577565b6105cf81610300565b840191505092915050565b60006060820190506105ef600083018661054e565b6105fc602083018561055d565b818103604083015261060e81846105a1565b9050949350505050565b600081519050919050565b600081905092915050565b600061063982610618565b6106438185610623565b9350610653818560208601610577565b80840191505092915050565b600061066b828461062e565b915081905092915050565b600081905092915050565b7f4661696c656420746f2063616c6c20707265636f6d70696c652062616e6b536560008201527f6e64000000000000000000000000000000000000000000000000000000000000602082015250565b60006106dd602283610676565b91506106e882610681565b602282019050919050565b60006106fe826106d0565b9150819050919050565b6000602082019050818103600083015261072281846105a1565b90509291505056fea26469706673582212206bab9cbf21b89041a55ee0f768a9443ae5e5ebfde23fe8a91daf9d4b339427db64736f6c63430008180033", + "linkReferences": {}, + "deployedLinkReferences": {} +} diff --git a/x/evm/embeds/contracts/FunToken.sol b/x/evm/embeds/contracts/IFunToken.sol similarity index 100% rename from x/evm/embeds/contracts/FunToken.sol rename to x/evm/embeds/contracts/IFunToken.sol diff --git a/x/evm/embeds/contracts/TestERC20TransferThenPrecompileSend.sol b/x/evm/embeds/contracts/TestERC20TransferThenPrecompileSend.sol new file mode 100644 index 000000000..b51d0367e --- /dev/null +++ b/x/evm/embeds/contracts/TestERC20TransferThenPrecompileSend.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +// Uncomment this line to use console.log +// import "hardhat/console.sol"; +import "./IFunToken.sol"; +import "@openzeppelin/contracts/utils/Strings.sol"; +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract TestERC20TransferThenPrecompileSend { + address erc20; + + constructor(address erc20_) { + erc20 = erc20_; + } + + function erc20TransferThenPrecompileSend( + address payable transferRecipient, + uint256 transferAmount, + string memory precompileRecipient, + uint256 precompileAmount + ) public { + require( + ERC20(erc20).transfer(transferRecipient, transferAmount), + "ERC-20 transfer failed" + ); + + (bool success, ) = FUNTOKEN_PRECOMPILE_ADDRESS.call( + abi.encodeWithSignature( + "bankSend(address,uint256,string)", + erc20, + uint256(precompileAmount), + precompileRecipient + ) + ); + + require(success, string.concat("Failed to call bankSend")); + } +} diff --git a/x/evm/embeds/contracts/TestNativeSendThenPrecompileSend.sol b/x/evm/embeds/contracts/TestNativeSendThenPrecompileSend.sol new file mode 100644 index 000000000..da9cb471b --- /dev/null +++ b/x/evm/embeds/contracts/TestNativeSendThenPrecompileSend.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import "./IFunToken.sol"; +import "@openzeppelin/contracts/utils/Strings.sol"; + +contract TestNativeSendThenPrecompileSend { + address erc20; + + constructor(address erc20_) { + erc20 = erc20_; + } + + function nativeSendThenPrecompileSend( + address payable nativeRecipient, + uint256 nativeAmount, + string memory precompileRecipient, + uint256 precompileAmount + ) public { + bool isSent = nativeRecipient.send(nativeAmount); + require(isSent, "Failed to send native token"); + + (bool success, ) = FUNTOKEN_PRECOMPILE_ADDRESS.call( + abi.encodeWithSignature( + "bankSend(address,uint256,string)", + erc20, + precompileAmount, + precompileRecipient + ) + ); + + require(success, string.concat("Failed to call precompile bankSend")); + } +} diff --git a/x/evm/embeds/embeds.go b/x/evm/embeds/embeds.go index 5ac65655d..12b0d981c 100644 --- a/x/evm/embeds/embeds.go +++ b/x/evm/embeds/embeds.go @@ -19,7 +19,7 @@ var ( erc20MinterContractJSON []byte //go:embed artifacts/contracts/IOracle.sol/IOracle.json oracleContractJSON []byte - //go:embed artifacts/contracts/FunToken.sol/IFunToken.json + //go:embed artifacts/contracts/IFunToken.sol/IFunToken.json funtokenPrecompileJSON []byte //go:embed artifacts/contracts/Wasm.sol/IWasm.json wasmPrecompileJSON []byte @@ -29,6 +29,10 @@ var ( testErc20MaliciousNameJson []byte //go:embed artifacts/contracts/TestERC20MaliciousTransfer.sol/TestERC20MaliciousTransfer.json testErc20MaliciousTransferJson []byte + //go:embed artifacts/contracts/TestERC20TransferThenPrecompileSend.sol/TestERC20TransferThenPrecompileSend.json + testERC20TransferThenPrecompileSendJson []byte + //go:embed artifacts/contracts/TestNativeSendThenPrecompileSend.sol/TestNativeSendThenPrecompileSend.json + testNativeSendThenPrecompileSendJson []byte ) var ( @@ -40,10 +44,10 @@ var ( } // SmartContract_Funtoken: Precompile contract interface for - // "FunToken.sol". This precompile enables transfers of ERC20 tokens + // "IFunToken.sol". This precompile enables transfers of ERC20 tokens // to non-EVM accounts. Only the ABI is used. SmartContract_FunToken = CompiledEvmContract{ - Name: "FunToken.sol", + Name: "IFunToken.sol", EmbedJSON: funtokenPrecompileJSON, } @@ -76,6 +80,22 @@ var ( Name: "TestERC20MaliciousTransfer.sol", EmbedJSON: testErc20MaliciousTransferJson, } + // SmartContract_TestNativeSendThenPrecompileSendJson is a test contract + // that performs two sends in a single call: a native nibi send and a precompile bankSend. + // It tests a race condition where the state DB commit + // may overwrite the state after the precompile execution, potentially causing a loss of funds. + SmartContract_TestNativeSendThenPrecompileSendJson = CompiledEvmContract{ + Name: "TestNativeSendThenPrecompileSend.sol", + EmbedJSON: testNativeSendThenPrecompileSendJson, + } + // SmartContract_TestERC20TransferThenPrecompileSend is a test contract + // that performs two sends in a single call: an erc20 token transfer and a precompile bankSend. + // It tests a race condition where the state DB commit + // may overwrite the state after the precompile execution, potentially causing an infinite token mint. + SmartContract_TestERC20TransferThenPrecompileSend = CompiledEvmContract{ + Name: "TestERC20TransferThenPrecompileSend.sol", + EmbedJSON: testERC20TransferThenPrecompileSendJson, + } ) func init() { @@ -86,6 +106,8 @@ func init() { SmartContract_TestERC20.MustLoad() SmartContract_TestERC20MaliciousName.MustLoad() SmartContract_TestERC20MaliciousTransfer.MustLoad() + SmartContract_TestNativeSendThenPrecompileSendJson.MustLoad() + SmartContract_TestERC20TransferThenPrecompileSend.MustLoad() } type CompiledEvmContract struct { diff --git a/x/evm/embeds/embeds_test.go b/x/evm/embeds/embeds_test.go index 586ff68b9..94f208dbb 100644 --- a/x/evm/embeds/embeds_test.go +++ b/x/evm/embeds/embeds_test.go @@ -16,5 +16,7 @@ func TestLoadContracts(t *testing.T) { embeds.SmartContract_TestERC20.MustLoad() embeds.SmartContract_TestERC20MaliciousName.MustLoad() embeds.SmartContract_TestERC20MaliciousTransfer.MustLoad() + embeds.SmartContract_TestNativeSendThenPrecompileSendJson.MustLoad() + embeds.SmartContract_TestERC20TransferThenPrecompileSend.MustLoad() }) } diff --git a/x/evm/keeper/funtoken_from_coin_test.go b/x/evm/keeper/funtoken_from_coin_test.go index 1f5e3a85a..3b4b17f7a 100644 --- a/x/evm/keeper/funtoken_from_coin_test.go +++ b/x/evm/keeper/funtoken_from_coin_test.go @@ -169,45 +169,13 @@ func (s *FunTokenFromCoinSuite) TestCreateFunTokenFromCoin() { func (s *FunTokenFromCoinSuite) TestConvertCoinToEvmAndBack() { deps := evmtest.NewTestDeps() alice := evmtest.NewEthPrivAcc() - bankDenom := "unibi" + bankDenom := evm.EVMBankDenom - s.T().Log("Setup: Create a coin in the bank state") - deps.App.BankKeeper.SetDenomMetaData(deps.Ctx, bank.Metadata{ - DenomUnits: []*bank.DenomUnit{ - { - Denom: bankDenom, - Exponent: 0, - Aliases: nil, - }, - }, - Base: bankDenom, - Display: bankDenom, - Name: bankDenom, - Symbol: "TOKEN", - }) - - s.T().Log("Give the sender funds") - s.Require().NoError(testapp.FundAccount( - deps.App.BankKeeper, - deps.Ctx, - deps.Sender.NibiruAddr, - deps.EvmKeeper.FeeForCreateFunToken(deps.Ctx).Add(sdk.NewCoin(bankDenom, sdk.NewInt(100))), - )) - - s.T().Log("Create FunToken mapping and ERC20") - createFunTokenResp, err := deps.EvmKeeper.CreateFunToken( - sdk.WrapSDKContext(deps.Ctx), - &evm.MsgCreateFunToken{ - FromBankDenom: bankDenom, - Sender: deps.Sender.NibiruAddr.String(), - }, - ) - s.Require().NoError(err) - - funTokenErc20Addr := createFunTokenResp.FuntokenMapping.Erc20Addr + // Initial setup + funTokenErc20Addr := s.fundAndCreateFunToken(deps, 100) s.T().Log("Convert bank coin to erc-20") - _, err = deps.EvmKeeper.ConvertCoinToEvm( + _, err := deps.EvmKeeper.ConvertCoinToEvm( sdk.WrapSDKContext(deps.Ctx), &evm.MsgConvertCoinToEvm{ Sender: deps.Sender.NibiruAddr.String(), @@ -299,6 +267,217 @@ func (s *FunTokenFromCoinSuite) TestConvertCoinToEvmAndBack() { s.Require().ErrorContains(err, "transfer amount exceeds balance") } +// TestNativeSendThenPrecompileSend +// 1. Creates a funtoken from coin. +// 2. Using the test contract, performs two sends in a single call: a native nibi send and a precompile bankSend. +// It tests a race condition where the state DB commit may overwrite the state after the precompile execution, +// potentially causing a loss of funds. +// +// INITIAL STATE: +// - Test contract funds: 10 NIBI, 10 WNIBI +// CONTRACT CALL: +// - Sends 10 NIBI natively and 10 WNIBI -> NIBI to Alice using precompile +// EXPECTED: +// - Test contract funds: 0 NIBI, 0 WNIBI +// - Alice: 20 NIBI +// - Module account: 0 NIBI escrowed +func (s *FunTokenFromCoinSuite) TestNativeSendThenPrecompileSend() { + deps := evmtest.NewTestDeps() + bankDenom := evm.EVMBankDenom + + // Initial setup + funTokenErc20Addr := s.fundAndCreateFunToken(deps, 10e6) + + s.T().Log("Deploy Test Contract") + deployResp, err := evmtest.DeployContract( + &deps, + embeds.SmartContract_TestNativeSendThenPrecompileSendJson, + funTokenErc20Addr.Address, + ) + s.Require().NoError(err) + + testContractAddr := deployResp.ContractAddr + testContractNibiAddr := eth.EthAddrToNibiruAddr(testContractAddr) + + s.T().Log("Give the test contract 10 NIBI (native)") + s.Require().NoError(testapp.FundAccount( + deps.App.BankKeeper, + deps.Ctx, + testContractNibiAddr, + sdk.NewCoins(sdk.NewCoin(bankDenom, sdk.NewInt(10e6)))), + ) + + s.T().Log("Convert bank coin to erc-20: give test contract 10 WNIBI (erc20)") + _, err = deps.EvmKeeper.ConvertCoinToEvm( + sdk.WrapSDKContext(deps.Ctx), + &evm.MsgConvertCoinToEvm{ + Sender: deps.Sender.NibiruAddr.String(), + BankCoin: sdk.NewCoin(bankDenom, sdk.NewInt(10e6)), + ToEthAddr: eth.EIP55Addr{Address: testContractAddr}, + }, + ) + s.Require().NoError(err) + + // Alice hex and Alice bech32 is the same address in different representation, + // so funds are expected to be available in Alice's bank wallet + alice := evmtest.NewEthPrivAcc() + + s.T().Log("call test contract") + _, err = deps.EvmKeeper.CallContract( + deps.Ctx, + embeds.SmartContract_TestNativeSendThenPrecompileSendJson.ABI, + deps.Sender.EthAddr, + &testContractAddr, + true, + "nativeSendThenPrecompileSend", + alice.EthAddr, + evm.NativeToWei(big.NewInt(10e6)), // for native evm send: 18 decimals + alice.NibiruAddr.String(), + big.NewInt(10e6), // for precompile bankSend: 6 decimals + ) + s.Require().NoError(err) + + // Check 1: Alice has 20 NIBI in bank + aliceBankBalance := deps.App.BankKeeper.GetBalance(deps.Ctx, alice.NibiruAddr, bankDenom) + s.Require().Equal(sdk.NewInt(20e6), aliceBankBalance.Amount) + + // Check 2: Alice has 0 WNIBI on ERC20 + aliceERC20Balance, err := deps.EvmKeeper.ERC20().BalanceOf(funTokenErc20Addr.Address, alice.EthAddr, deps.Ctx) + s.Require().NoError(err) + s.Require().Zero(big.NewInt(0).Cmp(aliceERC20Balance)) + + // Check 3: test contract has 0 NIBI in bank + testContractBankBalance := deps.App.BankKeeper.GetBalance(deps.Ctx, testContractNibiAddr, bankDenom) + s.Require().Equal(sdk.NewInt(0), testContractBankBalance.Amount) + + // Check 4: test contract has 0 WNIBI on ERC20 + testContractERC20Balance, err := deps.EvmKeeper.ERC20().BalanceOf(funTokenErc20Addr.Address, testContractAddr, deps.Ctx) + s.Require().NoError(err) + s.Require().Zero(big.NewInt(0).Cmp(testContractERC20Balance)) + + // Check 5: module balance has 0 NIBI escrowed + moduleBalance := deps.App.BankKeeper.GetBalance(deps.Ctx, authtypes.NewModuleAddress(evm.ModuleName), bankDenom) + s.Require().Equal(sdk.NewInt(0), moduleBalance.Amount) +} + +// TestERC20TransferThenPrecompileSend +// 1. Creates a funtoken from coin. +// 2. Using the test contract, performs two sends in a single call: a erc20 transfer and a precompile bankSend. +// It tests a race condition where the state DB commit may overwrite the state after the precompile execution, +// potentially causing an infinite minting of funds. +// +// INITIAL STATE: +// - Test contract funds: 10 WNIBI +// CONTRACT CALL: +// - Sends 1 WNIBI to Alice using erc20 transfer and 9 WNIBI -> NIBI to Alice using precompile +// EXPECTED: +// - Test contract funds: 0 WNIBI +// - Alice: 1 WNIBI, 9 NIBI +// - Module account: 1 NIBI escrowed (which Alice holds as 1 WNIBI) +func (s *FunTokenFromCoinSuite) TestERC20TransferThenPrecompileSend() { + deps := evmtest.NewTestDeps() + bankDenom := evm.EVMBankDenom + + // Initial setup + funTokenErc20Addr := s.fundAndCreateFunToken(deps, 10e6) + + s.T().Log("Deploy Test Contract") + deployResp, err := evmtest.DeployContract( + &deps, + embeds.SmartContract_TestERC20TransferThenPrecompileSend, + funTokenErc20Addr.Address, + ) + s.Require().NoError(err) + + testContractAddr := deployResp.ContractAddr + + s.T().Log("Convert bank coin to erc-20: give test contract 10 WNIBI (erc20)") + _, err = deps.EvmKeeper.ConvertCoinToEvm( + sdk.WrapSDKContext(deps.Ctx), + &evm.MsgConvertCoinToEvm{ + Sender: deps.Sender.NibiruAddr.String(), + BankCoin: sdk.NewCoin(bankDenom, sdk.NewInt(10e6)), + ToEthAddr: eth.EIP55Addr{Address: testContractAddr}, + }, + ) + s.Require().NoError(err) + + // Alice hex and Alice bech32 is the same address in different representation + alice := evmtest.NewEthPrivAcc() + + s.T().Log("call test contract") + _, err = deps.EvmKeeper.CallContract( + deps.Ctx, + embeds.SmartContract_TestERC20TransferThenPrecompileSend.ABI, + deps.Sender.EthAddr, + &testContractAddr, + true, + "erc20TransferThenPrecompileSend", + alice.EthAddr, + big.NewInt(1e6), // erc20 created with 6 decimals + alice.NibiruAddr.String(), + big.NewInt(9e6), // for precompile bankSend: 6 decimals + ) + s.Require().NoError(err) + + // Check 1: Alice has 9 NIBI in bank + aliceBankBalance := deps.App.BankKeeper.GetBalance(deps.Ctx, alice.NibiruAddr, bankDenom) + s.Require().Equal(sdk.NewInt(9e6), aliceBankBalance.Amount) + + // Check 2: Alice has 1 WNIBI on ERC20 + aliceERC20Balance, err := deps.EvmKeeper.ERC20().BalanceOf(funTokenErc20Addr.Address, alice.EthAddr, deps.Ctx) + s.Require().NoError(err) + s.Require().Zero(big.NewInt(1e6).Cmp(aliceERC20Balance)) + + // Check 3: test contract has 0 WNIBI on ERC20 + testContractERC20Balance, err := deps.EvmKeeper.ERC20().BalanceOf(funTokenErc20Addr.Address, testContractAddr, deps.Ctx) + s.Require().NoError(err) + s.Require().Zero(big.NewInt(0).Cmp(testContractERC20Balance)) + + // Check 4: module balance has 1 NIBI escrowed (which Alice holds as 1 WNIBI) + moduleBalance := deps.App.BankKeeper.GetBalance(deps.Ctx, authtypes.NewModuleAddress(evm.ModuleName), bankDenom) + s.Require().Equal(sdk.NewInt(1e6), moduleBalance.Amount) +} + +// fundAndCreateFunToken creates initial setup for tests +func (s *FunTokenFromCoinSuite) fundAndCreateFunToken(deps evmtest.TestDeps, unibiAmount int64) eth.EIP55Addr { + bankDenom := evm.EVMBankDenom + + s.T().Log("Setup: Create a coin in the bank state") + deps.App.BankKeeper.SetDenomMetaData(deps.Ctx, bank.Metadata{ + DenomUnits: []*bank.DenomUnit{ + { + Denom: bankDenom, + Exponent: 0, + Aliases: nil, + }, + }, + Base: bankDenom, + Display: bankDenom, + Name: bankDenom, + Symbol: "NIBI", + }) + + s.T().Log("Give the sender funds for funtoken creation and funding test contract") + s.Require().NoError(testapp.FundAccount( + deps.App.BankKeeper, + deps.Ctx, + deps.Sender.NibiruAddr, + deps.EvmKeeper.FeeForCreateFunToken(deps.Ctx).Add(sdk.NewCoin(bankDenom, sdk.NewInt(unibiAmount))), + )) + + s.T().Log("Create FunToken from coin") + createFunTokenResp, err := deps.EvmKeeper.CreateFunToken( + sdk.WrapSDKContext(deps.Ctx), + &evm.MsgCreateFunToken{ + FromBankDenom: bankDenom, + Sender: deps.Sender.NibiruAddr.String(), + }, + ) + s.Require().NoError(err) + return createFunTokenResp.FuntokenMapping.Erc20Addr +} + type FunTokenFromCoinSuite struct { suite.Suite } diff --git a/x/evm/precompile/funtoken.go b/x/evm/precompile/funtoken.go index 5c585c2e9..a937d111f 100644 --- a/x/evm/precompile/funtoken.go +++ b/x/evm/precompile/funtoken.go @@ -23,7 +23,7 @@ import ( var _ vm.PrecompiledContract = (*precompileFunToken)(nil) -// Precompile address for "FunToken.sol", the contract that +// Precompile address for "IFunToken.sol", the contract that // enables transfers of ERC20 tokens to "nibi" addresses as bank coins // using the ERC20's `FunToken` mapping. var PrecompileAddr_FunToken = gethcommon.HexToAddress("0x0000000000000000000000000000000000000800") From f3cbcaec58f23c54f6b75204b0c4009856b47250 Mon Sep 17 00:00:00 2001 From: Unique Divine <51418232+Unique-Divine@users.noreply.github.com> Date: Tue, 29 Oct 2024 16:19:03 -0500 Subject: [PATCH 2/5] fix(evm): StateDB multistore cache for precompile reversion and a safer Nibiru bank keeper that respects the EVM (#2094) * statedb: add cacheing for multistore before precompile runs * messy, working first version that allows for precompile reversion * wip!: Save checkpoint. 1. Created NibiruBankKeeper with safety around NIBI transfers inside of EthereumTx. 2. The "PrecompileCalled" JournalChange now has a propery implementation and a strong test case to show that reverting the precompile calls works as intended. 3. Remove unneeded functions created for testing with low-level struct fields. * chore: changelog * finalize bank keeper changes * revert to previous commit 7f904a07ac4d5555d8c088411024fc50ff65d085 * fix strange ignored file issue * remove new bank keeper * chore: comments from self-review --- CHANGELOG.md | 9 ++ app/keepers.go | 7 +- go.mod | 3 + go.sum | 8 +- x/evm/deps.go | 13 -- x/evm/evmtest/test_deps.go | 3 +- x/evm/evmtest/tx.go | 203 ++++++++++++++++++-------------- x/evm/keeper/erc20.go | 6 +- x/evm/keeper/keeper.go | 6 +- x/evm/keeper/msg_server.go | 2 +- x/evm/keeper/precompiles.go | 4 + x/evm/keeper/statedb.go | 12 +- x/evm/precompile/funtoken.go | 3 +- x/evm/precompile/precompile.go | 15 ++- x/evm/precompile/test/export.go | 65 +++++++++- x/evm/precompile/wasm.go | 4 +- x/evm/precompile/wasm_test.go | 4 +- x/evm/statedb/access_list.go | 4 +- x/evm/statedb/config.go | 3 +- x/evm/statedb/debug.go | 39 ++++++ x/evm/statedb/interfaces.go | 16 +-- x/evm/statedb/journal.go | 68 +++++++---- x/evm/statedb/journal_test.go | 130 +++++++++++++++----- x/evm/statedb/state_object.go | 7 +- x/evm/statedb/statedb.go | 152 ++++++++++++++++++++---- 25 files changed, 563 insertions(+), 223 deletions(-) create mode 100644 x/evm/statedb/debug.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 41cf28c19..c99138718 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,6 +69,15 @@ consistent setup and dynamic gas calculations, addressing the following tickets. - [#2088](https://github.com/NibiruChain/nibiru/pull/2088) - refactor(evm): remove outdated comment and improper error message text - [#2089](https://github.com/NibiruChain/nibiru/pull/2089) - better handling of gas consumption within erc20 contract execution - [#2091](https://github.com/NibiruChain/nibiru/pull/2091) - feat(evm): add fun token creation fee validation +- [#2094](https://github.com/NibiruChain/nibiru/pull/2094) - fix(evm): Following +from the changs in #2086, this pull request implements a new `JournalChange` +struct that saves a deep copy of the state multi store before each +state-modifying, Nibiru-specific precompiled contract is called (`OnRunStart`). +Additionally, we commit the `StateDB` there as well. This guarantees that the +non-EVM and EVM state will be in sync even if there are complex, multi-step +Ethereum transactions, such as in the case of an EthereumTx that influences the +`StateDB`, then calls a precompile that also changes non-EVM state, and then EVM +reverts inside of a try-catch. - [#2098](https://github.com/NibiruChain/nibiru/pull/2098) - test(evm): statedb tests for race conditions within funtoken precompile #### Nibiru EVM | Before Audit 1 - 2024-10-18 diff --git a/app/keepers.go b/app/keepers.go index 6695a6194..4c5721e83 100644 --- a/app/keepers.go +++ b/app/keepers.go @@ -140,6 +140,7 @@ type AppKeepers struct { } type privateKeepers struct { + bankBaseKeeper bankkeeper.BaseKeeper capabilityKeeper *capabilitykeeper.Keeper slashingKeeper slashingkeeper.Keeper crisisKeeper crisiskeeper.Keeper @@ -262,13 +263,15 @@ func (app *NibiruApp) InitKeepers( sdk.GetConfig().GetBech32AccountAddrPrefix(), govModuleAddr, ) - app.BankKeeper = bankkeeper.NewBaseKeeper( + + app.bankBaseKeeper = bankkeeper.NewBaseKeeper( appCodec, keys[banktypes.StoreKey], app.AccountKeeper, BlockedAddresses(), govModuleAddr, ) + app.BankKeeper = app.bankBaseKeeper app.StakingKeeper = stakingkeeper.NewKeeper( appCodec, keys[stakingtypes.StoreKey], @@ -605,7 +608,7 @@ func (app *NibiruApp) initAppModules( ), auth.NewAppModule(appCodec, app.AccountKeeper, authsims.RandomGenesisAccounts, app.GetSubspace(authtypes.ModuleName)), vesting.NewAppModule(app.AccountKeeper, app.BankKeeper), - bank.NewAppModule(appCodec, app.BankKeeper, app.AccountKeeper, app.GetSubspace(banktypes.ModuleName)), + bank.NewAppModule(appCodec, app.bankBaseKeeper, app.AccountKeeper, app.GetSubspace(banktypes.ModuleName)), capability.NewAppModule(appCodec, *app.capabilityKeeper, false), feegrantmodule.NewAppModule(appCodec, app.AccountKeeper, app.BankKeeper, app.FeeGrantKeeper, app.interfaceRegistry), gov.NewAppModule(appCodec, &app.GovKeeper, app.AccountKeeper, app.BankKeeper, app.GetSubspace(govtypes.ModuleName)), diff --git a/go.mod b/go.mod index 158dc9b80..bc14078f3 100644 --- a/go.mod +++ b/go.mod @@ -244,6 +244,9 @@ require ( replace ( cosmossdk.io/api => cosmossdk.io/api v0.3.1 + github.com/CosmWasm/wasmd => github.com/NibiruChain/wasmd v0.44.0-nibiru + github.com/cosmos/cosmos-sdk => github.com/NibiruChain/cosmos-sdk v0.47.11-nibiru + github.com/cosmos/iavl => github.com/cosmos/iavl v0.20.0 github.com/ethereum/go-ethereum => github.com/NibiruChain/go-ethereum v1.10.27-nibiru diff --git a/go.sum b/go.sum index 2c789f74f..213ee9c84 100644 --- a/go.sum +++ b/go.sum @@ -221,8 +221,6 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/ChainSafe/go-schnorrkel v1.0.0 h1:3aDA67lAykLaG1y3AOjs88dMxC88PgUuHRrLeDnvGIM= github.com/ChainSafe/go-schnorrkel v1.0.0/go.mod h1:dpzHYVxLZcp8pjlV+O+UR8K0Hp/z7vcchBSbMBEhCw4= -github.com/CosmWasm/wasmd v0.44.0 h1:2sbcoCAvfjCs1O0SWt53xULKjkV06dbSFthEViIC6Zg= -github.com/CosmWasm/wasmd v0.44.0/go.mod h1:tDyYN050qUcdd7LOxGeo2e185sEShyO3nJGl2Cf59+k= github.com/CosmWasm/wasmvm v1.5.5 h1:XlZI3xO5iUhiBqMiyzsrWEfUtk5gcBMNYIdHnsTB+NI= github.com/CosmWasm/wasmvm v1.5.5/go.mod h1:Q0bSEtlktzh7W2hhEaifrFp1Erx11ckQZmjq8FLCyys= github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= @@ -237,8 +235,12 @@ github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migc github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/NibiruChain/collections v0.5.0 h1:33pXpVTe1PK/tfdZlAJF1JF7AdzGNARG+iL9G/z3X7k= github.com/NibiruChain/collections v0.5.0/go.mod h1:43L6yjuF0BMre/mw4gqn/kUOZz1c2Y3huZ/RQfBFrOQ= +github.com/NibiruChain/cosmos-sdk v0.47.11-nibiru h1:PgFpxDe+7+OzWHs4zXlml5j2i9sGq2Zpd3ndYQG29/0= +github.com/NibiruChain/cosmos-sdk v0.47.11-nibiru/go.mod h1:ADjORYzUQqQv/FxDi0H0K5gW/rAk1CiDR3ZKsExfJV0= github.com/NibiruChain/go-ethereum v1.10.27-nibiru h1:o6lRFt57izoYwzN5cG8tnnBtJcaO3X7MjjN7PGGNCFg= github.com/NibiruChain/go-ethereum v1.10.27-nibiru/go.mod h1:kvvL3nDceUcB+1qGUBAsVf5dW23RBR77fqxgx2PGNrQ= +github.com/NibiruChain/wasmd v0.44.0-nibiru h1:b+stNdbMFsl0+o4KedXyF83qRnEpB/jCiTGZZgv2h2U= +github.com/NibiruChain/wasmd v0.44.0-nibiru/go.mod h1:inrbdsixQ0Kdu4mFUg1u7fn3XPOEkzqieGv0H/gR0ck= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= @@ -426,8 +428,6 @@ github.com/cosmos/cosmos-db v1.0.2 h1:hwMjozuY1OlJs/uh6vddqnk9j7VamLv+0DBlbEXbAK github.com/cosmos/cosmos-db v1.0.2/go.mod h1:Z8IXcFJ9PqKK6BIsVOB3QXtkKoqUOp1vRvPT39kOXEA= github.com/cosmos/cosmos-proto v1.0.0-beta.5 h1:eNcayDLpip+zVLRLYafhzLvQlSmyab+RC5W7ZfmxJLA= github.com/cosmos/cosmos-proto v1.0.0-beta.5/go.mod h1:hQGLpiIUloJBMdQMMWb/4wRApmI9hjHH05nefC0Ojec= -github.com/cosmos/cosmos-sdk v0.47.11 h1:0Qx7eORw0RJqPv+mvDuU8NQ1LV3nJJKJnPoYblWHolc= -github.com/cosmos/cosmos-sdk v0.47.11/go.mod h1:ADjORYzUQqQv/FxDi0H0K5gW/rAk1CiDR3ZKsExfJV0= github.com/cosmos/go-bip39 v0.0.0-20180819234021-555e2067c45d/go.mod h1:tSxLoYXyBmiFeKpvmq4dzayMdCjCnu8uqmCysIGBT2Y= github.com/cosmos/go-bip39 v1.0.0 h1:pcomnQdrdH22njcAatO0yWojsUnCO3y2tNoV1cb6hHY= github.com/cosmos/go-bip39 v1.0.0/go.mod h1:RNJv0H/pOIVgxw6KS7QeX2a0Uo0aKUlfhZ4xuwvCdJw= diff --git a/x/evm/deps.go b/x/evm/deps.go index 04327db06..2325def18 100644 --- a/x/evm/deps.go +++ b/x/evm/deps.go @@ -4,7 +4,6 @@ package evm import ( sdk "github.com/cosmos/cosmos-sdk/types" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" - bank "github.com/cosmos/cosmos-sdk/x/bank/types" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" ) @@ -32,18 +31,6 @@ type AccountKeeper interface { SetModuleAccount(ctx sdk.Context, macc authtypes.ModuleAccountI) } -// BankKeeper defines the expected interface needed to retrieve account balances. -type BankKeeper interface { - authtypes.BankKeeper - GetBalance(ctx sdk.Context, addr sdk.AccAddress, denom string) sdk.Coin - SendCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error - MintCoins(ctx sdk.Context, moduleName string, amt sdk.Coins) error - BurnCoins(ctx sdk.Context, moduleName string, amt sdk.Coins) error - - GetDenomMetaData(ctx sdk.Context, denom string) (metadata bank.Metadata, isFound bool) - SetDenomMetaData(ctx sdk.Context, denomMetaData bank.Metadata) -} - // StakingKeeper returns the historical headers kept in store. type StakingKeeper interface { GetHistoricalInfo(ctx sdk.Context, height int64) (stakingtypes.HistoricalInfo, bool) diff --git a/x/evm/evmtest/test_deps.go b/x/evm/evmtest/test_deps.go index 1810b1c8f..44fb34c31 100644 --- a/x/evm/evmtest/test_deps.go +++ b/x/evm/evmtest/test_deps.go @@ -46,7 +46,8 @@ func NewTestDeps() TestDeps { } func (deps TestDeps) StateDB() *statedb.StateDB { - return statedb.New(deps.Ctx, &deps.App.EvmKeeper, + return deps.EvmKeeper.NewStateDB( + deps.Ctx, statedb.NewEmptyTxConfig( gethcommon.BytesToHash(deps.Ctx.HeaderHash().Bytes()), ), diff --git a/x/evm/evmtest/tx.go b/x/evm/evmtest/tx.go index dde679851..6b876f16d 100644 --- a/x/evm/evmtest/tx.go +++ b/x/evm/evmtest/tx.go @@ -26,95 +26,6 @@ import ( "github.com/NibiruChain/nibiru/v2/x/evm/embeds" ) -type GethTxType = uint8 - -func TxTemplateAccessListTx() *gethcore.AccessListTx { - return &gethcore.AccessListTx{ - GasPrice: big.NewInt(1), - Gas: gethparams.TxGas, - To: &gethcommon.Address{}, - Value: big.NewInt(0), - Data: []byte{}, - } -} - -func TxTemplateLegacyTx() *gethcore.LegacyTx { - return &gethcore.LegacyTx{ - GasPrice: big.NewInt(1), - Gas: gethparams.TxGas, - To: &gethcommon.Address{}, - Value: big.NewInt(0), - Data: []byte{}, - } -} - -func TxTemplateDynamicFeeTx() *gethcore.DynamicFeeTx { - return &gethcore.DynamicFeeTx{ - GasFeeCap: big.NewInt(10), - GasTipCap: big.NewInt(2), - Gas: gethparams.TxGas, - To: &gethcommon.Address{}, - Value: big.NewInt(0), - Data: []byte{}, - } -} - -func NewEthTxMsgFromTxData( - deps *TestDeps, - txType GethTxType, - innerTxData []byte, - nonce uint64, - to *gethcommon.Address, - value *big.Int, - gas uint64, - accessList gethcore.AccessList, -) (*evm.MsgEthereumTx, error) { - if innerTxData == nil { - innerTxData = []byte{} - } - - var ethCoreTx *gethcore.Transaction - switch txType { - case gethcore.LegacyTxType: - innerTx := TxTemplateLegacyTx() - innerTx.Nonce = nonce - innerTx.Data = innerTxData - innerTx.To = to - innerTx.Value = value - innerTx.Gas = gas - ethCoreTx = gethcore.NewTx(innerTx) - case gethcore.AccessListTxType: - innerTx := TxTemplateAccessListTx() - innerTx.Nonce = nonce - innerTx.Data = innerTxData - innerTx.AccessList = accessList - innerTx.To = to - innerTx.Value = value - innerTx.Gas = gas - ethCoreTx = gethcore.NewTx(innerTx) - case gethcore.DynamicFeeTxType: - innerTx := TxTemplateDynamicFeeTx() - innerTx.Nonce = nonce - innerTx.Data = innerTxData - innerTx.To = to - innerTx.Value = value - innerTx.Gas = gas - innerTx.AccessList = accessList - ethCoreTx = gethcore.NewTx(innerTx) - default: - return nil, fmt.Errorf( - "received unknown tx type (%v) in NewEthTxMsgFromTxData", txType) - } - - ethTxMsg := new(evm.MsgEthereumTx) - if err := ethTxMsg.FromEthereumTx(ethCoreTx); err != nil { - return ethTxMsg, err - } - - ethTxMsg.From = deps.Sender.EthAddr.Hex() - return ethTxMsg, ethTxMsg.Sign(deps.GethSigner(), deps.Sender.KeyringSigner) -} - // ExecuteNibiTransfer executes nibi transfer func ExecuteNibiTransfer(deps *TestDeps, t *testing.T) *evm.MsgEthereumTx { nonce := deps.StateDB().GetNonce(deps.Sender.EthAddr) @@ -326,6 +237,10 @@ func TransferWei( return err } +// -------------------------------------------------- +// Templates +// -------------------------------------------------- + // ValidLegacyTx: Useful initial condition for tests // Exported only for use in tests. func ValidLegacyTx() *evm.LegacyTx { @@ -342,3 +257,113 @@ func ValidLegacyTx() *evm.LegacyTx { S: []byte{}, } } + +// GethTxType represents different Ethereum transaction types as defined in +// go-ethereum, such as Legacy, AccessList, and DynamicFee transactions. +type GethTxType = uint8 + +func TxTemplateAccessListTx() *gethcore.AccessListTx { + return &gethcore.AccessListTx{ + GasPrice: big.NewInt(1), + Gas: gethparams.TxGas, + To: &gethcommon.Address{}, + Value: big.NewInt(0), + Data: []byte{}, + } +} + +func TxTemplateLegacyTx() *gethcore.LegacyTx { + return &gethcore.LegacyTx{ + GasPrice: big.NewInt(1), + Gas: gethparams.TxGas, + To: &gethcommon.Address{}, + Value: big.NewInt(0), + Data: []byte{}, + } +} + +func TxTemplateDynamicFeeTx() *gethcore.DynamicFeeTx { + return &gethcore.DynamicFeeTx{ + GasFeeCap: big.NewInt(10), + GasTipCap: big.NewInt(2), + Gas: gethparams.TxGas, + To: &gethcommon.Address{}, + Value: big.NewInt(0), + Data: []byte{}, + } +} + +// NewEthTxMsgFromTxData creates an Ethereum transaction message based on +// the specified txType (Legacy, AccessList, or DynamicFee). This function +// populates transaction fields like nonce, recipient, value, and gas, with +// an optional access list for AccessList and DynamicFee types. The transaction +// is signed using the provided dependencies. +// +// Parameters: +// - deps: Required dependencies including the sender address and signer. +// - txType: Transaction type (Legacy, AccessList, or DynamicFee). +// - innerTxData: Byte slice of transaction data (input). +// - nonce: Transaction nonce. +// - to: Recipient address. +// - value: ETH value (in wei) to transfer. +// - gas: Gas limit for the transaction. +// - accessList: Access list for AccessList and DynamicFee types. +// +// Returns: +// - *evm.MsgEthereumTx: Ethereum transaction message ready for submission. +// - error: Any error encountered during creation or signing. +func NewEthTxMsgFromTxData( + deps *TestDeps, + txType GethTxType, + innerTxData []byte, + nonce uint64, + to *gethcommon.Address, + value *big.Int, + gas uint64, + accessList gethcore.AccessList, +) (*evm.MsgEthereumTx, error) { + if innerTxData == nil { + innerTxData = []byte{} + } + + var ethCoreTx *gethcore.Transaction + switch txType { + case gethcore.LegacyTxType: + innerTx := TxTemplateLegacyTx() + innerTx.Nonce = nonce + innerTx.Data = innerTxData + innerTx.To = to + innerTx.Value = value + innerTx.Gas = gas + ethCoreTx = gethcore.NewTx(innerTx) + case gethcore.AccessListTxType: + innerTx := TxTemplateAccessListTx() + innerTx.Nonce = nonce + innerTx.Data = innerTxData + innerTx.AccessList = accessList + innerTx.To = to + innerTx.Value = value + innerTx.Gas = gas + ethCoreTx = gethcore.NewTx(innerTx) + case gethcore.DynamicFeeTxType: + innerTx := TxTemplateDynamicFeeTx() + innerTx.Nonce = nonce + innerTx.Data = innerTxData + innerTx.To = to + innerTx.Value = value + innerTx.Gas = gas + innerTx.AccessList = accessList + ethCoreTx = gethcore.NewTx(innerTx) + default: + return nil, fmt.Errorf( + "received unknown tx type (%v) in NewEthTxMsgFromTxData", txType) + } + + ethTxMsg := new(evm.MsgEthereumTx) + if err := ethTxMsg.FromEthereumTx(ethCoreTx); err != nil { + return ethTxMsg, err + } + + ethTxMsg.From = deps.Sender.EthAddr.Hex() + return ethTxMsg, ethTxMsg.Sign(deps.GethSigner(), deps.Sender.KeyringSigner) +} diff --git a/x/evm/keeper/erc20.go b/x/evm/keeper/erc20.go index 10404bea4..328452ecf 100644 --- a/x/evm/keeper/erc20.go +++ b/x/evm/keeper/erc20.go @@ -218,11 +218,8 @@ func (k Keeper) CallContractWithInput( // sent by a user txConfig := k.TxConfig(ctx, gethcommon.BigToHash(big.NewInt(0))) - // Using tmp context to not modify the state in case of evm revert - tmpCtx, commitCtx := ctx.CacheContext() - evmResp, evmObj, err = k.ApplyEvmMsg( - tmpCtx, evmMsg, evm.NewNoOpTracer(), commit, evmCfg, txConfig, + ctx, evmMsg, evm.NewNoOpTracer(), commit, evmCfg, txConfig, ) if err != nil { // We don't know the actual gas used, so consuming the gas limit @@ -245,7 +242,6 @@ func (k Keeper) CallContractWithInput( } else { // Success, committing the state to ctx if commit { - commitCtx() totalGasUsed, err := k.AddToBlockGasUsed(ctx, evmResp.GasUsed) if err != nil { k.ResetGasMeterAndConsumeGas(ctx, ctx.GasMeter().Limit()) diff --git a/x/evm/keeper/keeper.go b/x/evm/keeper/keeper.go index 49ea0c9bf..dd31229fd 100644 --- a/x/evm/keeper/keeper.go +++ b/x/evm/keeper/keeper.go @@ -15,6 +15,7 @@ import ( "github.com/cosmos/cosmos-sdk/codec" storetypes "github.com/cosmos/cosmos-sdk/store/types" sdk "github.com/cosmos/cosmos-sdk/types" + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" gethcommon "github.com/ethereum/go-ethereum/common" "github.com/NibiruChain/nibiru/v2/app/appconst" @@ -40,7 +41,7 @@ type Keeper struct { // this should be the x/gov module account. authority sdk.AccAddress - bankKeeper evm.BankKeeper + bankKeeper bankkeeper.Keeper accountKeeper evm.AccountKeeper stakingKeeper evm.StakingKeeper @@ -63,13 +64,14 @@ func NewKeeper( storeKey, transientKey storetypes.StoreKey, authority sdk.AccAddress, accKeeper evm.AccountKeeper, - bankKeeper evm.BankKeeper, + bankKeeper bankkeeper.Keeper, stakingKeeper evm.StakingKeeper, tracer string, ) Keeper { if err := sdk.VerifyAddressFormat(authority); err != nil { panic(err) } + return Keeper{ cdc: cdc, storeKey: storeKey, diff --git a/x/evm/keeper/msg_server.go b/x/evm/keeper/msg_server.go index 89a249dd3..be8abb236 100644 --- a/x/evm/keeper/msg_server.go +++ b/x/evm/keeper/msg_server.go @@ -252,7 +252,7 @@ func (k *Keeper) ApplyEvmMsg(ctx sdk.Context, vmErr error // vm errors do not effect consensus and are therefore not assigned to err ) - stateDB := statedb.New(ctx, k, txConfig) + stateDB := k.NewStateDB(ctx, txConfig) evmObj = k.NewEVM(ctx, msg, evmConfig, tracer, stateDB) leftoverGas := msg.Gas() diff --git a/x/evm/keeper/precompiles.go b/x/evm/keeper/precompiles.go index 965866660..3b1a0e480 100644 --- a/x/evm/keeper/precompiles.go +++ b/x/evm/keeper/precompiles.go @@ -21,3 +21,7 @@ func (k *Keeper) AddPrecompiles( } } } + +func (k *Keeper) IsPrecompile(addr gethcommon.Address) bool { + return k.precompiles.Has(addr) +} diff --git a/x/evm/keeper/statedb.go b/x/evm/keeper/statedb.go index 6eb46f990..7aff8c02d 100644 --- a/x/evm/keeper/statedb.go +++ b/x/evm/keeper/statedb.go @@ -17,6 +17,13 @@ import ( var _ statedb.Keeper = &Keeper{} +func (k *Keeper) NewStateDB( + ctx sdk.Context, + txConfig statedb.TxConfig, +) *statedb.StateDB { + return statedb.New(ctx, k, txConfig) +} + // ---------------------------------------------------------------------------- // StateDB Keeper implementation // ---------------------------------------------------------------------------- @@ -65,7 +72,10 @@ func (k *Keeper) ForEachStorage( } } -// SetAccBalance update account's balance, compare with current balance first, then decide to mint or burn. +// SetAccBalance update account's balance, compare with current balance first, +// then decide to mint or burn. +// Implements the `statedb.Keeper` interface. +// Only called by `StateDB.Commit()`. func (k *Keeper) SetAccBalance( ctx sdk.Context, addr gethcommon.Address, amountEvmDenom *big.Int, ) error { diff --git a/x/evm/precompile/funtoken.go b/x/evm/precompile/funtoken.go index a937d111f..69e756ed3 100644 --- a/x/evm/precompile/funtoken.go +++ b/x/evm/precompile/funtoken.go @@ -72,8 +72,7 @@ func (p precompileFunToken) Run( if err != nil { return nil, err } - // Dirty journal entries in `StateDB` must be committed - return bz, start.StateDB.Commit() + return bz, err } func PrecompileFunToken(keepers keepers.PublicKeepers) vm.PrecompiledContract { diff --git a/x/evm/precompile/precompile.go b/x/evm/precompile/precompile.go index a6bbfefc4..40d0c74b4 100644 --- a/x/evm/precompile/precompile.go +++ b/x/evm/precompile/precompile.go @@ -147,6 +147,8 @@ type OnRunStartResult struct { Method *gethabi.Method StateDB *statedb.StateDB + + PrecompileJournalEntry statedb.PrecompileCalled } // OnRunStart prepares the execution environment for a precompiled contract call. @@ -188,14 +190,21 @@ func OnRunStart( err = fmt.Errorf("failed to load the sdk.Context from the EVM StateDB") return } - ctx := stateDB.GetContext() - if err = stateDB.Commit(); err != nil { + + // journalEntry captures the state before precompile execution to enable + // proper state reversal if the call fails or if [statedb.JournalChange] + // is reverted in general. + cacheCtx, journalEntry := stateDB.CacheCtxForPrecompile(contract.Address()) + if err = stateDB.SavePrecompileCalledJournalChange(contract.Address(), journalEntry); err != nil { + return res, err + } + if err = stateDB.CommitCacheCtx(); err != nil { return res, fmt.Errorf("error committing dirty journal entries: %w", err) } return OnRunStartResult{ Args: args, - Ctx: ctx, + Ctx: cacheCtx, Method: method, StateDB: stateDB, }, nil diff --git a/x/evm/precompile/test/export.go b/x/evm/precompile/test/export.go index 966dd3359..28670e3e0 100644 --- a/x/evm/precompile/test/export.go +++ b/x/evm/precompile/test/export.go @@ -2,11 +2,14 @@ package test import ( "encoding/json" + "math/big" "os" "os/exec" "path" "strings" + serverconfig "github.com/NibiruChain/nibiru/v2/app/server/config" + wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" wasm "github.com/CosmWasm/wasmd/x/wasm/types" "github.com/ethereum/go-ethereum/core/vm" @@ -267,6 +270,7 @@ func IncrementWasmCounterWithExecuteMulti( deps *evmtest.TestDeps, wasmContract sdk.AccAddress, times uint, + finalizeTx bool, ) (evmObj *vm.EVM) { msgArgsBz := []byte(` { @@ -308,9 +312,68 @@ func IncrementWasmCounterWithExecuteMulti( s.Require().NoError(err) ethTxResp, evmObj, err := deps.EvmKeeper.CallContractWithInput( - deps.Ctx, deps.Sender.EthAddr, &precompile.PrecompileAddr_Wasm, true, input, + deps.Ctx, deps.Sender.EthAddr, &precompile.PrecompileAddr_Wasm, finalizeTx, input, ) s.Require().NoError(err) s.Require().NotEmpty(ethTxResp.Ret) return evmObj } + +func IncrementWasmCounterWithExecuteMultiViaVMCall( + s *suite.Suite, + deps *evmtest.TestDeps, + wasmContract sdk.AccAddress, + times uint, + finalizeTx bool, + evmObj *vm.EVM, +) error { + msgArgsBz := []byte(` + { + "increment": {} + } + `) + + // Parse funds argument. + var funds []precompile.WasmBankCoin // blank funds + fundsJson, err := json.Marshal(funds) + s.NoErrorf(err, "fundsJson: %s", fundsJson) + err = json.Unmarshal(fundsJson, &funds) + s.Require().NoError(err, "fundsJson %s, funds %s", fundsJson, funds) + + // The "times" arg determines the number of messages in the executeMsgs slice + executeMsgs := []struct { + ContractAddr string `json:"contractAddr"` + MsgArgs []byte `json:"msgArgs"` + Funds []precompile.WasmBankCoin `json:"funds"` + }{ + {wasmContract.String(), msgArgsBz, funds}, + } + if times == 0 { + executeMsgs = executeMsgs[:0] // force empty + } else { + for i := uint(1); i < times; i++ { + executeMsgs = append(executeMsgs, executeMsgs[0]) + } + } + s.Require().Len(executeMsgs, int(times)) // sanity check assertion + + callArgs := []any{ + executeMsgs, + } + input, err := embeds.SmartContract_Wasm.ABI.Pack( + string(precompile.WasmMethod_executeMulti), + callArgs..., + ) + s.Require().NoError(err) + + contract := precompile.PrecompileAddr_Wasm + leftoverGas := serverconfig.DefaultEthCallGasLimit + _, _, err = evmObj.Call( + vm.AccountRef(deps.Sender.EthAddr), + contract, + input, + leftoverGas, + big.NewInt(0), + ) + return err +} diff --git a/x/evm/precompile/wasm.go b/x/evm/precompile/wasm.go index 10817c673..a7b21684c 100644 --- a/x/evm/precompile/wasm.go +++ b/x/evm/precompile/wasm.go @@ -62,9 +62,7 @@ func (p precompileWasm) Run( if err != nil { return nil, err } - - // Dirty journal entries in `StateDB` must be committed - return bz, start.StateDB.Commit() + return bz, err } type precompileWasm struct { diff --git a/x/evm/precompile/wasm_test.go b/x/evm/precompile/wasm_test.go index d796f8b89..dbe35b839 100644 --- a/x/evm/precompile/wasm_test.go +++ b/x/evm/precompile/wasm_test.go @@ -100,13 +100,13 @@ func (s *WasmSuite) TestExecuteMultiHappy() { test.AssertWasmCounterState(&s.Suite, deps, wasmContract, 0) // count += 2 test.IncrementWasmCounterWithExecuteMulti( - &s.Suite, &deps, wasmContract, 2) + &s.Suite, &deps, wasmContract, 2, true) // count = 2 test.AssertWasmCounterState(&s.Suite, deps, wasmContract, 2) s.assertWasmCounterStateRaw(deps, wasmContract, 2) // count += 67 test.IncrementWasmCounterWithExecuteMulti( - &s.Suite, &deps, wasmContract, 67) + &s.Suite, &deps, wasmContract, 67, true) // count = 69 test.AssertWasmCounterState(&s.Suite, deps, wasmContract, 69) s.assertWasmCounterStateRaw(deps, wasmContract, 69) diff --git a/x/evm/statedb/access_list.go b/x/evm/statedb/access_list.go index 4513a9164..f62b45171 100644 --- a/x/evm/statedb/access_list.go +++ b/x/evm/statedb/access_list.go @@ -1,3 +1,5 @@ +package statedb + // Copyright 2020 The go-ethereum Authors // This file is part of the go-ethereum library. // @@ -14,8 +16,6 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . -package statedb - import ( "github.com/ethereum/go-ethereum/common" ) diff --git a/x/evm/statedb/config.go b/x/evm/statedb/config.go index 887f591c5..417e480ac 100644 --- a/x/evm/statedb/config.go +++ b/x/evm/statedb/config.go @@ -1,6 +1,7 @@ -// Copyright (c) 2023-2024 Nibi, Inc. package statedb +// Copyright (c) 2023-2024 Nibi, Inc. + import ( "math/big" diff --git a/x/evm/statedb/debug.go b/x/evm/statedb/debug.go new file mode 100644 index 000000000..c2b5fb968 --- /dev/null +++ b/x/evm/statedb/debug.go @@ -0,0 +1,39 @@ +package statedb + +// Copyright (c) 2023-2024 Nibi, Inc. + +import ( + "github.com/ethereum/go-ethereum/common" +) + +// DebugDirtiesCount is a test helper to inspect how many entries in the journal +// are still dirty (uncommitted). After calling [StateDB.Commit], this function +// should return zero. +func (s *StateDB) DebugDirtiesCount() int { + dirtiesCount := 0 + for _, dirtyCount := range s.Journal.dirties { + dirtiesCount += dirtyCount + } + return dirtiesCount +} + +// DebugDirties is a test helper that returns the journal's dirty account changes map. +func (s *StateDB) DebugDirties() map[common.Address]int { + return s.Journal.dirties +} + +// DebugEntries is a test helper that returns the sequence of [JournalChange] +// objects added during execution. +func (s *StateDB) DebugEntries() []JournalChange { + return s.Journal.entries +} + +// DebugStateObjects is a test helper that returns returns a copy of the +// [StateDB.stateObjects] map. +func (s *StateDB) DebugStateObjects() map[common.Address]*stateObject { + copyOfMap := make(map[common.Address]*stateObject) + for key, val := range s.stateObjects { + copyOfMap[key] = val + } + return copyOfMap +} diff --git a/x/evm/statedb/interfaces.go b/x/evm/statedb/interfaces.go index a4c1c3b59..c242771ca 100644 --- a/x/evm/statedb/interfaces.go +++ b/x/evm/statedb/interfaces.go @@ -1,22 +1,12 @@ -// Copyright (c) 2023-2024 Nibi, Inc. package statedb +// Copyright (c) 2023-2024 Nibi, Inc. + import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/vm" ) -// ExtStateDB defines an extension to the interface provided by the go-ethereum -// codebase to support additional state transition functionalities. In particular -// it supports appending a new entry to the state journal through -// AppendJournalEntry so that the state can be reverted after running -// stateful precompiled contracts. -type ExtStateDB interface { - vm.StateDB - AppendJournalEntry(JournalChange) -} - // Keeper provide underlying storage of StateDB type Keeper interface { // GetAccount: Ethereum account getter for a [statedb.Account]. @@ -38,4 +28,6 @@ type Keeper interface { // DeleteAccount handles contract's suicide call, clearing the balance, // contract bytecode, contract state, and its native account. DeleteAccount(ctx sdk.Context, addr common.Address) error + + IsPrecompile(addr common.Address) bool } diff --git a/x/evm/statedb/journal.go b/x/evm/statedb/journal.go index ac041b617..acbba5eab 100644 --- a/x/evm/statedb/journal.go +++ b/x/evm/statedb/journal.go @@ -1,3 +1,5 @@ +package statedb + // Copyright 2016 The go-ethereum Authors // This file is part of the go-ethereum library. // @@ -14,13 +16,13 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . -package statedb - import ( "bytes" "math/big" "sort" + store "github.com/cosmos/cosmos-sdk/store/types" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/ethereum/go-ethereum/common" ) @@ -91,25 +93,6 @@ func (j *journal) Length() int { return len(j.entries) } -// DirtiesCount is a test helper to inspect how many entries in the journal are -// still dirty (uncommitted). After calling [StateDB.Commit], this function should -// return zero. -func (s *StateDB) DirtiesCount() int { - dirtiesCount := 0 - for _, dirtyCount := range s.Journal.dirties { - dirtiesCount += dirtyCount - } - return dirtiesCount -} - -func (s *StateDB) Dirties() map[common.Address]int { - return s.Journal.dirties -} - -func (s *StateDB) Entries() []JournalChange { - return s.Journal.entries -} - // ------------------------------------------------------ // createObjectChange @@ -335,3 +318,46 @@ func (ch accessListAddSlotChange) Revert(s *StateDB) { func (ch accessListAddSlotChange) Dirtied() *common.Address { return nil } + +// ------------------------------------------------------ +// PrecompileSnapshotBeforeRun + +// PrecompileCalled: Precompiles can alter persistent storage of other +// modules. These changes to persistent storage are not reverted by a `Revert` of +// [JournalChange] by default, as it generally manages only changes to accounts +// and Bank balances for ether (NIBI). +// +// As a workaround to make state changes from precompiles reversible, we store +// [PrecompileCalled] snapshots that sync and record the prior state +// of the other modules, allowing precompile calls to truly be reverted. +// +// As a simple example, suppose that a transaction calls a precompile. +// 1. If the precompile changes the state in the Bank Module or Wasm module +// 2. The call gets reverted (`revert()` in Solidity), which shoud restore the +// state to a in-memory snapshot recorded on the StateDB journal. +// 3. This could cause a problem where changes to the rest of the blockchain state +// are still in effect following the reversion in the EVM state DB. +type PrecompileCalled struct { + MultiStore store.CacheMultiStore + Events sdk.Events +} + +var _ JournalChange = PrecompileCalled{} + +// Revert rolls back the [StateDB] cache context to the state it was in prior to +// the precompile call. Modifications to this cache context are pushed to the +// commit context (s.evmTxCtx) when [StateDB.Commit] is executed. +func (ch PrecompileCalled) Revert(s *StateDB) { + s.cacheCtx = s.cacheCtx.WithMultiStore(ch.MultiStore) + // Rewrite the `writeCacheCtxFn` using the same logic as sdk.Context.CacheCtx + s.writeToCommitCtxFromCacheCtx = func() { + s.evmTxCtx.EventManager().EmitEvents(ch.Events) + // TODO: Check correctness of the emitted events + // https://github.com/NibiruChain/nibiru/issues/2096 + ch.MultiStore.Write() + } +} + +func (ch PrecompileCalled) Dirtied() *common.Address { + return nil +} diff --git a/x/evm/statedb/journal_test.go b/x/evm/statedb/journal_test.go index 5863face5..6390b640f 100644 --- a/x/evm/statedb/journal_test.go +++ b/x/evm/statedb/journal_test.go @@ -6,10 +6,12 @@ import ( "strings" "testing" + "github.com/MakeNowJust/heredoc/v2" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/ethereum/go-ethereum/core/vm" serverconfig "github.com/NibiruChain/nibiru/v2/app/server/config" + "github.com/NibiruChain/nibiru/v2/x/common" "github.com/NibiruChain/nibiru/v2/x/common/testutil/testapp" "github.com/NibiruChain/nibiru/v2/x/evm" "github.com/NibiruChain/nibiru/v2/x/evm/embeds" @@ -18,7 +20,7 @@ import ( "github.com/NibiruChain/nibiru/v2/x/evm/statedb" ) -func (s *Suite) TestPrecompileSnapshots() { +func (s *Suite) TestComplexJournalChanges() { deps := evmtest.NewTestDeps() bankDenom := evm.EVMBankDenom s.Require().NoError(testapp.FundAccount( @@ -32,25 +34,11 @@ func (s *Suite) TestPrecompileSnapshots() { wasmContract := test.SetupWasmContracts(&deps, &s.Suite)[1] fmt.Printf("wasmContract: %s\n", wasmContract) - assertionsBeforeRun := func(deps *evmtest.TestDeps) { - test.AssertWasmCounterState( - &s.Suite, *deps, wasmContract, 0, - ) - } - run := func(deps *evmtest.TestDeps) *vm.EVM { - return test.IncrementWasmCounterWithExecuteMulti( - &s.Suite, deps, wasmContract, 7, - ) - } - assertionsAfterRun := func(deps *evmtest.TestDeps) { - test.AssertWasmCounterState( - &s.Suite, *deps, wasmContract, 7, - ) - } s.T().Log("Assert before transition") - - assertionsBeforeRun(&deps) + test.AssertWasmCounterState( + &s.Suite, deps, wasmContract, 0, + ) deployArgs := []any{"name", "SYMBOL", uint8(18)} deployResp, err := evmtest.DeployContract( @@ -71,7 +59,7 @@ func (s *Suite) TestPrecompileSnapshots() { s.Run("Populate dirty journal entries. Remove with Commit", func() { stateDB := evmObj.StateDB.(*statedb.StateDB) - s.Equal(0, stateDB.DirtiesCount()) + s.Equal(0, stateDB.DebugDirtiesCount()) randomAcc := evmtest.NewEthPrivAcc().EthAddr balDelta := evm.NativeToWei(big.NewInt(4)) @@ -81,14 +69,15 @@ func (s *Suite) TestPrecompileSnapshots() { stateDB.AddBalance(randomAcc, balDelta) // 1 dirties from [balanceChange] stateDB.SubBalance(randomAcc, balDelta) - if stateDB.DirtiesCount() != 4 { + if stateDB.DebugDirtiesCount() != 4 { debugDirtiesCountMismatch(stateDB, s.T()) s.FailNow("expected 4 dirty journal changes") } - err = stateDB.Commit() // Dirties should be gone + s.T().Log("StateDB.Commit, then Dirties should be gone") + err = stateDB.Commit() s.NoError(err) - if stateDB.DirtiesCount() != 0 { + if stateDB.DebugDirtiesCount() != 0 { debugDirtiesCountMismatch(stateDB, s.T()) s.FailNow("expected 0 dirty journal changes") } @@ -110,7 +99,7 @@ func (s *Suite) TestPrecompileSnapshots() { ) s.Require().NoError(err) stateDB := evmObj.StateDB.(*statedb.StateDB) - if stateDB.DirtiesCount() != 2 { + if stateDB.DebugDirtiesCount() != 2 { debugDirtiesCountMismatch(stateDB, s.T()) s.FailNow("expected 2 dirty journal changes") } @@ -136,22 +125,105 @@ func (s *Suite) TestPrecompileSnapshots() { s.Require().ErrorContains(err, vm.ErrExecutionReverted.Error()) }) - s.Run("Precompile calls also start and end clean (no dirty changes)", func() { - evmObj = run(&deps) - assertionsAfterRun(&deps) + s.Run("Precompile calls populate snapshots", func() { + s.T().Log("commitEvmTx=true, expect 0 dirty journal entries") + commitEvmTx := true + evmObj = test.IncrementWasmCounterWithExecuteMulti( + &s.Suite, &deps, wasmContract, 7, commitEvmTx, + ) + // assertions after run + test.AssertWasmCounterState( + &s.Suite, deps, wasmContract, 7, + ) stateDB, ok := evmObj.StateDB.(*statedb.StateDB) s.Require().True(ok, "error retrieving StateDB from the EVM") - if stateDB.DirtiesCount() != 0 { + if stateDB.DebugDirtiesCount() != 0 { + debugDirtiesCountMismatch(stateDB, s.T()) + s.FailNow("expected 0 dirty journal changes") + } + + s.T().Log("commitEvmTx=false, expect dirty journal entries") + commitEvmTx = false + evmObj = test.IncrementWasmCounterWithExecuteMulti( + &s.Suite, &deps, wasmContract, 5, commitEvmTx, + ) + stateDB, ok = evmObj.StateDB.(*statedb.StateDB) + s.Require().True(ok, "error retrieving StateDB from the EVM") + + s.T().Log("Expect exactly 0 dirty journal entry for the precompile snapshot") + if stateDB.DebugDirtiesCount() != 0 { debugDirtiesCountMismatch(stateDB, s.T()) s.FailNow("expected 0 dirty journal changes") } + + s.T().Log("Expect no change since the StateDB has not been committed") + test.AssertWasmCounterState( + &s.Suite, deps, wasmContract, 7, // 7 = 7 + 0 + ) + + s.T().Log("Expect change to persist on the StateDB cacheCtx") + cacheCtx := stateDB.GetCacheContext() + s.NotNil(cacheCtx) + deps.Ctx = *cacheCtx + test.AssertWasmCounterState( + &s.Suite, deps, wasmContract, 12, // 12 = 7 + 5 + ) + // NOTE: that the [StateDB.Commit] fn has not been called yet. We're still + // mid-transaction. + + s.T().Log("EVM revert operation should bring about the old state") + err = test.IncrementWasmCounterWithExecuteMultiViaVMCall( + &s.Suite, &deps, wasmContract, 50, commitEvmTx, evmObj, + ) + stateDBPtr := evmObj.StateDB.(*statedb.StateDB) + s.Require().Equal(stateDB, stateDBPtr) + s.Require().NoError(err) + s.T().Log(heredoc.Doc(`At this point, 2 precompile calls have succeeded. +One that increments the counter to 7 + 5, and another for +50. +The StateDB has not been committed. We expect to be able to revert to both +snapshots and see the prior states.`)) + cacheCtx = stateDB.GetCacheContext() + deps.Ctx = *cacheCtx + test.AssertWasmCounterState( + &s.Suite, deps, wasmContract, 7+5+50, + ) + + errFn := common.TryCatch(func() { + // There were only two EVM calls. + // Thus, there are only 2 snapshots: 0 and 1. + // We should not be able to revert to a third one. + stateDB.RevertToSnapshot(2) + }) + s.Require().ErrorContains(errFn(), "revision id 2 cannot be reverted") + + stateDB.RevertToSnapshot(1) + cacheCtx = stateDB.GetCacheContext() + s.NotNil(cacheCtx) + deps.Ctx = *cacheCtx + test.AssertWasmCounterState( + &s.Suite, deps, wasmContract, 7+5, + ) + + stateDB.RevertToSnapshot(0) + cacheCtx = stateDB.GetCacheContext() + s.NotNil(cacheCtx) + deps.Ctx = *cacheCtx + test.AssertWasmCounterState( + &s.Suite, deps, wasmContract, 7, // state before precompile called + ) + + err = stateDB.Commit() + deps.Ctx = stateDB.GetEvmTxContext() + test.AssertWasmCounterState( + &s.Suite, deps, wasmContract, 7, // state before precompile called + ) }) } func debugDirtiesCountMismatch(db *statedb.StateDB, t *testing.T) string { lines := []string{} - dirties := db.Dirties() - stateObjects := db.StateObjects() + dirties := db.DebugDirties() + stateObjects := db.DebugStateObjects() for addr, dirtyCountForAddr := range dirties { lines = append(lines, fmt.Sprintf("Dirty addr: %s, dirtyCountForAddr=%d", addr, dirtyCountForAddr)) diff --git a/x/evm/statedb/state_object.go b/x/evm/statedb/state_object.go index e371beae0..28ba2d85a 100644 --- a/x/evm/statedb/state_object.go +++ b/x/evm/statedb/state_object.go @@ -1,6 +1,7 @@ -// Copyright (c) 2023-2024 Nibi, Inc. package statedb +// Copyright (c) 2023-2024 Nibi, Inc. + import ( "bytes" "math/big" @@ -198,7 +199,7 @@ func (s *stateObject) Code() []byte { if bytes.Equal(s.CodeHash(), emptyCodeHash) { return nil } - code := s.db.keeper.GetCode(s.db.ctx, common.BytesToHash(s.CodeHash())) + code := s.db.keeper.GetCode(s.db.evmTxCtx, common.BytesToHash(s.CodeHash())) s.code = code return code } @@ -260,7 +261,7 @@ func (s *stateObject) GetCommittedState(key common.Hash) common.Hash { return value } // If no live objects are available, load it from keeper - value := s.db.keeper.GetState(s.db.ctx, s.Address(), key) + value := s.db.keeper.GetState(s.db.evmTxCtx, s.Address(), key) s.OriginStorage[key] = value return value } diff --git a/x/evm/statedb/statedb.go b/x/evm/statedb/statedb.go index 223e92edb..4c79e61af 100644 --- a/x/evm/statedb/statedb.go +++ b/x/evm/statedb/statedb.go @@ -1,11 +1,28 @@ -// Copyright (c) 2023-2024 Nibi, Inc. +// The "evm/statedb" package implements a go-ethereum [vm.StateDB] with state +// management and journal changes specific to the Nibiru EVM. +// +// This package plays a critical role in managing the state of accounts, +// contracts, and storage while handling atomicity, caching, and state +// modifications. It ensures that state transitions made during the +// execution of smart contracts are either committed or reverted based +// on transaction outcomes. +// +// StateDB structs used to store anything within the state tree, including +// accounts, contracts, and contract storage. +// Note that Nibiru's state tree is an IAVL tree, which differs from the Merkle +// Patricia Trie structure seen on Ethereum mainnet. +// +// StateDBs also take care of caching and handling nested states. package statedb +// Copyright (c) 2023-2024 Nibi, Inc. + import ( "fmt" "math/big" "sort" + store "github.com/cosmos/cosmos-sdk/store/types" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/ethereum/go-ethereum/common" gethcore "github.com/ethereum/go-ethereum/core/types" @@ -13,14 +30,6 @@ import ( "github.com/ethereum/go-ethereum/crypto" ) -// revision is the identifier of a version of state. -// it consists of an auto-increment id and a journal index. -// it's safer to use than using journal index alone. -type revision struct { - id int - journalIndex int -} - var _ vm.StateDB = &StateDB{} // StateDB structs within the ethereum protocol are used to store anything @@ -30,8 +39,9 @@ var _ vm.StateDB = &StateDB{} // * Accounts type StateDB struct { keeper Keeper - // ctx is the persistent context used for official `StateDB.Commit` calls. - ctx sdk.Context + + // evmTxCtx is the persistent context used for official `StateDB.Commit` calls. + evmTxCtx sdk.Context // Journal of state modifications. This is the backbone of // Snapshot and RevertToSnapshot. @@ -43,6 +53,21 @@ type StateDB struct { txConfig TxConfig + // cacheCtx: An sdk.Context produced from the [StateDB.ctx] with the + // multi-store cached and a new event manager. The cached context + // (`cacheCtx`) is written to the persistent context (`ctx`) when + // `writeCacheCtx` is called. + cacheCtx sdk.Context + + // writeToCommitCtxFromCacheCtx is the "write" function received from + // `s.evmTxCtx.CacheContext()`. It saves mutations on s.cacheCtx to the StateDB's + // commit context (s.evmTxCtx). This synchronizes the multistore and event manager + // of the two contexts. + writeToCommitCtxFromCacheCtx func() + + // The number of precompiled contract calls within the current transaction + multistoreCacheCount uint8 + // The refund counter, also used by state transitioning. refund uint64 @@ -57,7 +82,7 @@ type StateDB struct { func New(ctx sdk.Context, keeper Keeper, txConfig TxConfig) *StateDB { return &StateDB{ keeper: keeper, - ctx: ctx, + evmTxCtx: ctx, stateObjects: make(map[common.Address]*stateObject), Journal: newJournal(), accessList: newAccessList(), @@ -66,14 +91,30 @@ func New(ctx sdk.Context, keeper Keeper, txConfig TxConfig) *StateDB { } } +// revision is the identifier of a version of state. +// it consists of an auto-increment id and a journal index. +// it's safer to use than using journal index alone. +type revision struct { + id int + journalIndex int +} + // Keeper returns the underlying `Keeper` func (s *StateDB) Keeper() Keeper { return s.keeper } -// GetContext returns the transaction Context. -func (s *StateDB) GetContext() sdk.Context { - return s.ctx +// GetEvmTxContext returns the EVM transaction context. +func (s *StateDB) GetEvmTxContext() sdk.Context { + return s.evmTxCtx +} + +// GetCacheContext: Getter for testing purposes. +func (s *StateDB) GetCacheContext() *sdk.Context { + if s.writeToCommitCtxFromCacheCtx == nil { + return nil + } + return &s.cacheCtx } // AddLog adds a log, called by evm. @@ -212,8 +253,9 @@ func (s *StateDB) getStateObject(addr common.Address) *stateObject { if obj := s.stateObjects[addr]; obj != nil { return obj } + // If no live objects are available, load it from keeper - account := s.keeper.GetAccount(s.ctx, addr) + account := s.keeper.GetAccount(s.evmTxCtx, addr) if account == nil { return nil } @@ -274,7 +316,7 @@ func (s *StateDB) ForEachStorage(addr common.Address, cb func(key, value common. if so == nil { return nil } - s.keeper.ForEachStorage(s.ctx, addr, func(key, value common.Hash) bool { + s.keeper.ForEachStorage(s.evmTxCtx, addr, func(key, value common.Hash) bool { if value, dirty := so.DirtyStorage[key]; dirty { return cb(key, value) } @@ -438,6 +480,9 @@ func (s *StateDB) Snapshot() int { // RevertToSnapshot reverts all state changes made since the given revision. func (s *StateDB) RevertToSnapshot(revid int) { + fmt.Printf("len(s.validRevisions): %d\n", len(s.validRevisions)) + fmt.Printf("s.validRevisions: %v\n", s.validRevisions) + // Find the snapshot in the stack of valid snapshots. idx := sort.Search(len(s.validRevisions), func(i int) bool { return s.validRevisions[i].id >= revid @@ -460,11 +505,37 @@ func errorf(format string, args ...any) error { // Commit writes the dirty journal state changes to the EVM Keeper. The // StateDB object cannot be reused after [Commit] has completed. A new // object needs to be created from the EVM. +// +// cacheCtxSyncNeeded: If one of the [Nibiru-Specific Precompiled Contracts] was +// called, a [JournalChange] of type [PrecompileSnapshotBeforeRun] gets added and +// we branch off a cache of the commit context (s.evmTxCtx). +// +// [Nibiru-Specific Precompiled Contracts]: https://nibiru.fi/docs/evm/precompiles/nibiru.html func (s *StateDB) Commit() error { - ctx := s.GetContext() + if s.writeToCommitCtxFromCacheCtx != nil { + s.writeToCommitCtxFromCacheCtx() + } + return s.commitCtx(s.GetEvmTxContext()) +} + +// CommitCacheCtx is identical to [StateDB.Commit], except it: +// (1) uses the cacheCtx of the [StateDB] and +// (2) does not save mutations of the cacheCtx to the commit context (s.evmTxCtx). +// The reason for (2) is that the overall EVM transaction (block, not internal) +// is only finalized when [Commit] is called, not when [CommitCacheCtx] is +// called. +func (s *StateDB) CommitCacheCtx() error { + return s.commitCtx(s.cacheCtx) +} + +// commitCtx writes the dirty journal state changes to the EVM Keeper. The +// StateDB object cannot be reused after [commitCtx] has completed. A new +// object needs to be created from the EVM. +func (s *StateDB) commitCtx(ctx sdk.Context) error { for _, addr := range s.Journal.sortedDirties() { obj := s.getStateObject(addr) if obj == nil { + s.Journal.dirties[addr] = 0 continue } if obj.Suicided { @@ -493,18 +564,47 @@ func (s *StateDB) Commit() error { obj.OriginStorage[key] = dirtyVal } } - // Clear the dirty counts because all state changes have been - // committed. + // TODO: UD-DEBUG: Assume clean to pretend for tests + // Reset the dirty count to 0 because all state changes for this dirtied + // address in the journal have been committed. s.Journal.dirties[addr] = 0 } return nil } -// StateObjects: Returns a copy of the [StateDB.stateObjects] map. -func (s *StateDB) StateObjects() map[common.Address]*stateObject { - copyOfMap := make(map[common.Address]*stateObject) - for key, val := range s.stateObjects { - copyOfMap[key] = val +func (s *StateDB) CacheCtxForPrecompile(precompileAddr common.Address) ( + sdk.Context, PrecompileCalled, +) { + if s.writeToCommitCtxFromCacheCtx == nil { + s.cacheCtx, s.writeToCommitCtxFromCacheCtx = s.evmTxCtx.CacheContext() + } + return s.cacheCtx, PrecompileCalled{ + MultiStore: s.cacheCtx.MultiStore().(store.CacheMultiStore).Copy(), + Events: s.cacheCtx.EventManager().Events(), + } +} + +// SavePrecompileCalledJournalChange adds a snapshot of the commit multistore +// ([PrecompileCalled]) to the [StateDB] journal at the end of +// successful invocation of a precompiled contract. This is necessary to revert +// intermediate states where an EVM contract augments the multistore with a +// precompile and an inconsistency occurs between the EVM module and other +// modules. +// +// See [PrecompileCalled] for more info. +func (s *StateDB) SavePrecompileCalledJournalChange( + precompileAddr common.Address, + journalChange PrecompileCalled, +) error { + s.Journal.append(journalChange) + s.multistoreCacheCount++ + if s.multistoreCacheCount > maxMultistoreCacheCount { + return fmt.Errorf( + "exceeded maximum number Nibiru-specific precompiled contract calls in one transaction (%d). Called address %s", + maxMultistoreCacheCount, precompileAddr.Hex(), + ) } - return copyOfMap + return nil } + +const maxMultistoreCacheCount uint8 = 10 From 9d0dbd5edb108bb69f844219c46cb7c30f71154a Mon Sep 17 00:00:00 2001 From: Matthias <97468149+matthiasmatt@users.noreply.github.com> Date: Wed, 30 Oct 2024 12:17:40 +0100 Subject: [PATCH 3/5] fix(evm): transfer for native to erc20 (#2090) * feat: fix transfer for native to erc20 * chore: changelog * feat: check transfer occurred by ensuring new balance is higher than previous one * feat: fix the 0 check * fix: fix error message * feat: move transfer logic to erc20.go * chore: changelog * fix: make the FunToken precompile return the sent amount --------- Co-authored-by: Unique-Divine --- CHANGELOG.md | 4 + .../contracts/IFunToken.sol/IFunToken.json | 8 +- .../TestERC20TransferThenPrecompileSend.json | 4 +- .../TestNativeSendThenPrecompileSend.json | 4 +- x/evm/embeds/contracts/IFunToken.sol | 17 ++- x/evm/keeper/erc20.go | 40 ++++++- x/evm/keeper/erc20_test.go | 15 ++- x/evm/keeper/msg_server.go | 100 +++++++----------- x/evm/precompile/funtoken.go | 11 +- x/evm/precompile/funtoken_test.go | 22 +++- 10 files changed, 134 insertions(+), 91 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c99138718..61dc41b07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -79,6 +79,10 @@ Ethereum transactions, such as in the case of an EthereumTx that influences the `StateDB`, then calls a precompile that also changes non-EVM state, and then EVM reverts inside of a try-catch. - [#2098](https://github.com/NibiruChain/nibiru/pull/2098) - test(evm): statedb tests for race conditions within funtoken precompile +- [#2090](https://github.com/NibiruChain/nibiru/pull/2090) - fix(evm): Account +for (1) ERC20 transfers with tokens that return false success values instead of +throwing an error and (2) ERC20 transfers with other operations that don't bring +about the expected resulting balance for the transfer recipient. #### Nibiru EVM | Before Audit 1 - 2024-10-18 diff --git a/x/evm/embeds/artifacts/contracts/IFunToken.sol/IFunToken.json b/x/evm/embeds/artifacts/contracts/IFunToken.sol/IFunToken.json index 6fe6e838a..882e04696 100644 --- a/x/evm/embeds/artifacts/contracts/IFunToken.sol/IFunToken.json +++ b/x/evm/embeds/artifacts/contracts/IFunToken.sol/IFunToken.json @@ -22,7 +22,13 @@ } ], "name": "bankSend", - "outputs": [], + "outputs": [ + { + "internalType": "uint256", + "name": "sentAmount", + "type": "uint256" + } + ], "stateMutability": "nonpayable", "type": "function" } diff --git a/x/evm/embeds/artifacts/contracts/TestERC20TransferThenPrecompileSend.sol/TestERC20TransferThenPrecompileSend.json b/x/evm/embeds/artifacts/contracts/TestERC20TransferThenPrecompileSend.sol/TestERC20TransferThenPrecompileSend.json index 234a7f943..2d58f750e 100644 --- a/x/evm/embeds/artifacts/contracts/TestERC20TransferThenPrecompileSend.sol/TestERC20TransferThenPrecompileSend.json +++ b/x/evm/embeds/artifacts/contracts/TestERC20TransferThenPrecompileSend.sol/TestERC20TransferThenPrecompileSend.json @@ -43,8 +43,8 @@ "type": "function" } ], - "bytecode": "0x608060405234801561001057600080fd5b50604051610974380380610974833981810160405281019061003291906100db565b806000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555050610108565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006100a88261007d565b9050919050565b6100b88161009d565b81146100c357600080fd5b50565b6000815190506100d5816100af565b92915050565b6000602082840312156100f1576100f0610078565b5b60006100ff848285016100c6565b91505092915050565b61085d806101176000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c8063264c325814610030575b600080fd5b61004a6004803603810190610045919061049f565b61004c565b005b60008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1663a9059cbb85856040518363ffffffff1660e01b81526004016100a7929190610590565b6020604051808303816000875af11580156100c6573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906100ea91906105f1565b610129576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016101209061067b565b60405180910390fd5b600061080073ffffffffffffffffffffffffffffffffffffffff1660008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1683856040516024016101799392919061072a565b6040516020818303038152906040527f03003bc5000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff838183161783525050505060405161020391906107af565b6000604051808303816000865af19150503d8060008114610240576040519150601f19603f3d011682016040523d82523d6000602084013e610245565b606091505b5050905080604051602001610259906107ec565b604051602081830303815290604052906102a9576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016102a09190610805565b60405180910390fd5b505050505050565b6000604051905090565b600080fd5b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006102f0826102c5565b9050919050565b610300816102e5565b811461030b57600080fd5b50565b60008135905061031d816102f7565b92915050565b6000819050919050565b61033681610323565b811461034157600080fd5b50565b6000813590506103538161032d565b92915050565b600080fd5b600080fd5b6000601f19601f8301169050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6103ac82610363565b810181811067ffffffffffffffff821117156103cb576103ca610374565b5b80604052505050565b60006103de6102b1565b90506103ea82826103a3565b919050565b600067ffffffffffffffff82111561040a57610409610374565b5b61041382610363565b9050602081019050919050565b82818337600083830152505050565b600061044261043d846103ef565b6103d4565b90508281526020810184848401111561045e5761045d61035e565b5b610469848285610420565b509392505050565b600082601f83011261048657610485610359565b5b813561049684826020860161042f565b91505092915050565b600080600080608085870312156104b9576104b86102bb565b5b60006104c78782880161030e565b94505060206104d887828801610344565b935050604085013567ffffffffffffffff8111156104f9576104f86102c0565b5b61050587828801610471565b925050606061051687828801610344565b91505092959194509250565b6000819050919050565b600061054761054261053d846102c5565b610522565b6102c5565b9050919050565b60006105598261052c565b9050919050565b600061056b8261054e565b9050919050565b61057b81610560565b82525050565b61058a81610323565b82525050565b60006040820190506105a56000830185610572565b6105b26020830184610581565b9392505050565b60008115159050919050565b6105ce816105b9565b81146105d957600080fd5b50565b6000815190506105eb816105c5565b92915050565b600060208284031215610607576106066102bb565b5b6000610615848285016105dc565b91505092915050565b600082825260208201905092915050565b7f4552432d3230207472616e73666572206661696c656400000000000000000000600082015250565b600061066560168361061e565b91506106708261062f565b602082019050919050565b6000602082019050818103600083015261069481610658565b9050919050565b60006106a6826102c5565b9050919050565b6106b68161069b565b82525050565b600081519050919050565b60005b838110156106e55780820151818401526020810190506106ca565b60008484015250505050565b60006106fc826106bc565b610706818561061e565b93506107168185602086016106c7565b61071f81610363565b840191505092915050565b600060608201905061073f60008301866106ad565b61074c6020830185610581565b818103604083015261075e81846106f1565b9050949350505050565b600081519050919050565b600081905092915050565b600061078982610768565b6107938185610773565b93506107a38185602086016106c7565b80840191505092915050565b60006107bb828461077e565b915081905092915050565b7f4661696c656420746f2063616c6c2062616e6b53656e64000000000000000000815250565b60006107f7826107c6565b601782019150819050919050565b6000602082019050818103600083015261081f81846106f1565b90509291505056fea26469706673582212205d952e3358213844ef60215f35b3c3adb68dbc657522a8dd683eb1c5c0f64bfa64736f6c63430008180033", - "deployedBytecode": "0x608060405234801561001057600080fd5b506004361061002b5760003560e01c8063264c325814610030575b600080fd5b61004a6004803603810190610045919061049f565b61004c565b005b60008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1663a9059cbb85856040518363ffffffff1660e01b81526004016100a7929190610590565b6020604051808303816000875af11580156100c6573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906100ea91906105f1565b610129576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016101209061067b565b60405180910390fd5b600061080073ffffffffffffffffffffffffffffffffffffffff1660008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1683856040516024016101799392919061072a565b6040516020818303038152906040527f03003bc5000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff838183161783525050505060405161020391906107af565b6000604051808303816000865af19150503d8060008114610240576040519150601f19603f3d011682016040523d82523d6000602084013e610245565b606091505b5050905080604051602001610259906107ec565b604051602081830303815290604052906102a9576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016102a09190610805565b60405180910390fd5b505050505050565b6000604051905090565b600080fd5b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006102f0826102c5565b9050919050565b610300816102e5565b811461030b57600080fd5b50565b60008135905061031d816102f7565b92915050565b6000819050919050565b61033681610323565b811461034157600080fd5b50565b6000813590506103538161032d565b92915050565b600080fd5b600080fd5b6000601f19601f8301169050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6103ac82610363565b810181811067ffffffffffffffff821117156103cb576103ca610374565b5b80604052505050565b60006103de6102b1565b90506103ea82826103a3565b919050565b600067ffffffffffffffff82111561040a57610409610374565b5b61041382610363565b9050602081019050919050565b82818337600083830152505050565b600061044261043d846103ef565b6103d4565b90508281526020810184848401111561045e5761045d61035e565b5b610469848285610420565b509392505050565b600082601f83011261048657610485610359565b5b813561049684826020860161042f565b91505092915050565b600080600080608085870312156104b9576104b86102bb565b5b60006104c78782880161030e565b94505060206104d887828801610344565b935050604085013567ffffffffffffffff8111156104f9576104f86102c0565b5b61050587828801610471565b925050606061051687828801610344565b91505092959194509250565b6000819050919050565b600061054761054261053d846102c5565b610522565b6102c5565b9050919050565b60006105598261052c565b9050919050565b600061056b8261054e565b9050919050565b61057b81610560565b82525050565b61058a81610323565b82525050565b60006040820190506105a56000830185610572565b6105b26020830184610581565b9392505050565b60008115159050919050565b6105ce816105b9565b81146105d957600080fd5b50565b6000815190506105eb816105c5565b92915050565b600060208284031215610607576106066102bb565b5b6000610615848285016105dc565b91505092915050565b600082825260208201905092915050565b7f4552432d3230207472616e73666572206661696c656400000000000000000000600082015250565b600061066560168361061e565b91506106708261062f565b602082019050919050565b6000602082019050818103600083015261069481610658565b9050919050565b60006106a6826102c5565b9050919050565b6106b68161069b565b82525050565b600081519050919050565b60005b838110156106e55780820151818401526020810190506106ca565b60008484015250505050565b60006106fc826106bc565b610706818561061e565b93506107168185602086016106c7565b61071f81610363565b840191505092915050565b600060608201905061073f60008301866106ad565b61074c6020830185610581565b818103604083015261075e81846106f1565b9050949350505050565b600081519050919050565b600081905092915050565b600061078982610768565b6107938185610773565b93506107a38185602086016106c7565b80840191505092915050565b60006107bb828461077e565b915081905092915050565b7f4661696c656420746f2063616c6c2062616e6b53656e64000000000000000000815250565b60006107f7826107c6565b601782019150819050919050565b6000602082019050818103600083015261081f81846106f1565b90509291505056fea26469706673582212205d952e3358213844ef60215f35b3c3adb68dbc657522a8dd683eb1c5c0f64bfa64736f6c63430008180033", + "bytecode": "0x608060405234801561001057600080fd5b50604051610974380380610974833981810160405281019061003291906100db565b806000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555050610108565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006100a88261007d565b9050919050565b6100b88161009d565b81146100c357600080fd5b50565b6000815190506100d5816100af565b92915050565b6000602082840312156100f1576100f0610078565b5b60006100ff848285016100c6565b91505092915050565b61085d806101176000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c8063264c325814610030575b600080fd5b61004a6004803603810190610045919061049f565b61004c565b005b60008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1663a9059cbb85856040518363ffffffff1660e01b81526004016100a7929190610590565b6020604051808303816000875af11580156100c6573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906100ea91906105f1565b610129576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016101209061067b565b60405180910390fd5b600061080073ffffffffffffffffffffffffffffffffffffffff1660008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1683856040516024016101799392919061072a565b6040516020818303038152906040527f03003bc5000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff838183161783525050505060405161020391906107af565b6000604051808303816000865af19150503d8060008114610240576040519150601f19603f3d011682016040523d82523d6000602084013e610245565b606091505b5050905080604051602001610259906107ec565b604051602081830303815290604052906102a9576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016102a09190610805565b60405180910390fd5b505050505050565b6000604051905090565b600080fd5b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006102f0826102c5565b9050919050565b610300816102e5565b811461030b57600080fd5b50565b60008135905061031d816102f7565b92915050565b6000819050919050565b61033681610323565b811461034157600080fd5b50565b6000813590506103538161032d565b92915050565b600080fd5b600080fd5b6000601f19601f8301169050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6103ac82610363565b810181811067ffffffffffffffff821117156103cb576103ca610374565b5b80604052505050565b60006103de6102b1565b90506103ea82826103a3565b919050565b600067ffffffffffffffff82111561040a57610409610374565b5b61041382610363565b9050602081019050919050565b82818337600083830152505050565b600061044261043d846103ef565b6103d4565b90508281526020810184848401111561045e5761045d61035e565b5b610469848285610420565b509392505050565b600082601f83011261048657610485610359565b5b813561049684826020860161042f565b91505092915050565b600080600080608085870312156104b9576104b86102bb565b5b60006104c78782880161030e565b94505060206104d887828801610344565b935050604085013567ffffffffffffffff8111156104f9576104f86102c0565b5b61050587828801610471565b925050606061051687828801610344565b91505092959194509250565b6000819050919050565b600061054761054261053d846102c5565b610522565b6102c5565b9050919050565b60006105598261052c565b9050919050565b600061056b8261054e565b9050919050565b61057b81610560565b82525050565b61058a81610323565b82525050565b60006040820190506105a56000830185610572565b6105b26020830184610581565b9392505050565b60008115159050919050565b6105ce816105b9565b81146105d957600080fd5b50565b6000815190506105eb816105c5565b92915050565b600060208284031215610607576106066102bb565b5b6000610615848285016105dc565b91505092915050565b600082825260208201905092915050565b7f4552432d3230207472616e73666572206661696c656400000000000000000000600082015250565b600061066560168361061e565b91506106708261062f565b602082019050919050565b6000602082019050818103600083015261069481610658565b9050919050565b60006106a6826102c5565b9050919050565b6106b68161069b565b82525050565b600081519050919050565b60005b838110156106e55780820151818401526020810190506106ca565b60008484015250505050565b60006106fc826106bc565b610706818561061e565b93506107168185602086016106c7565b61071f81610363565b840191505092915050565b600060608201905061073f60008301866106ad565b61074c6020830185610581565b818103604083015261075e81846106f1565b9050949350505050565b600081519050919050565b600081905092915050565b600061078982610768565b6107938185610773565b93506107a38185602086016106c7565b80840191505092915050565b60006107bb828461077e565b915081905092915050565b7f4661696c656420746f2063616c6c2062616e6b53656e64000000000000000000815250565b60006107f7826107c6565b601782019150819050919050565b6000602082019050818103600083015261081f81846106f1565b90509291505056fea26469706673582212203da3b9141c515078ba917a5b792ee843396b82d45e2ef60e0d6307c236e8c30664736f6c63430008180033", + "deployedBytecode": "0x608060405234801561001057600080fd5b506004361061002b5760003560e01c8063264c325814610030575b600080fd5b61004a6004803603810190610045919061049f565b61004c565b005b60008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1663a9059cbb85856040518363ffffffff1660e01b81526004016100a7929190610590565b6020604051808303816000875af11580156100c6573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906100ea91906105f1565b610129576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016101209061067b565b60405180910390fd5b600061080073ffffffffffffffffffffffffffffffffffffffff1660008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1683856040516024016101799392919061072a565b6040516020818303038152906040527f03003bc5000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff838183161783525050505060405161020391906107af565b6000604051808303816000865af19150503d8060008114610240576040519150601f19603f3d011682016040523d82523d6000602084013e610245565b606091505b5050905080604051602001610259906107ec565b604051602081830303815290604052906102a9576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016102a09190610805565b60405180910390fd5b505050505050565b6000604051905090565b600080fd5b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006102f0826102c5565b9050919050565b610300816102e5565b811461030b57600080fd5b50565b60008135905061031d816102f7565b92915050565b6000819050919050565b61033681610323565b811461034157600080fd5b50565b6000813590506103538161032d565b92915050565b600080fd5b600080fd5b6000601f19601f8301169050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6103ac82610363565b810181811067ffffffffffffffff821117156103cb576103ca610374565b5b80604052505050565b60006103de6102b1565b90506103ea82826103a3565b919050565b600067ffffffffffffffff82111561040a57610409610374565b5b61041382610363565b9050602081019050919050565b82818337600083830152505050565b600061044261043d846103ef565b6103d4565b90508281526020810184848401111561045e5761045d61035e565b5b610469848285610420565b509392505050565b600082601f83011261048657610485610359565b5b813561049684826020860161042f565b91505092915050565b600080600080608085870312156104b9576104b86102bb565b5b60006104c78782880161030e565b94505060206104d887828801610344565b935050604085013567ffffffffffffffff8111156104f9576104f86102c0565b5b61050587828801610471565b925050606061051687828801610344565b91505092959194509250565b6000819050919050565b600061054761054261053d846102c5565b610522565b6102c5565b9050919050565b60006105598261052c565b9050919050565b600061056b8261054e565b9050919050565b61057b81610560565b82525050565b61058a81610323565b82525050565b60006040820190506105a56000830185610572565b6105b26020830184610581565b9392505050565b60008115159050919050565b6105ce816105b9565b81146105d957600080fd5b50565b6000815190506105eb816105c5565b92915050565b600060208284031215610607576106066102bb565b5b6000610615848285016105dc565b91505092915050565b600082825260208201905092915050565b7f4552432d3230207472616e73666572206661696c656400000000000000000000600082015250565b600061066560168361061e565b91506106708261062f565b602082019050919050565b6000602082019050818103600083015261069481610658565b9050919050565b60006106a6826102c5565b9050919050565b6106b68161069b565b82525050565b600081519050919050565b60005b838110156106e55780820151818401526020810190506106ca565b60008484015250505050565b60006106fc826106bc565b610706818561061e565b93506107168185602086016106c7565b61071f81610363565b840191505092915050565b600060608201905061073f60008301866106ad565b61074c6020830185610581565b818103604083015261075e81846106f1565b9050949350505050565b600081519050919050565b600081905092915050565b600061078982610768565b6107938185610773565b93506107a38185602086016106c7565b80840191505092915050565b60006107bb828461077e565b915081905092915050565b7f4661696c656420746f2063616c6c2062616e6b53656e64000000000000000000815250565b60006107f7826107c6565b601782019150819050919050565b6000602082019050818103600083015261081f81846106f1565b90509291505056fea26469706673582212203da3b9141c515078ba917a5b792ee843396b82d45e2ef60e0d6307c236e8c30664736f6c63430008180033", "linkReferences": {}, "deployedLinkReferences": {} } diff --git a/x/evm/embeds/artifacts/contracts/TestNativeSendThenPrecompileSend.sol/TestNativeSendThenPrecompileSend.json b/x/evm/embeds/artifacts/contracts/TestNativeSendThenPrecompileSend.sol/TestNativeSendThenPrecompileSend.json index 1d44a675c..5b0046040 100644 --- a/x/evm/embeds/artifacts/contracts/TestNativeSendThenPrecompileSend.sol/TestNativeSendThenPrecompileSend.json +++ b/x/evm/embeds/artifacts/contracts/TestNativeSendThenPrecompileSend.sol/TestNativeSendThenPrecompileSend.json @@ -43,8 +43,8 @@ "type": "function" } ], - "bytecode": "0x608060405234801561001057600080fd5b50604051610877380380610877833981810160405281019061003291906100db565b806000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555050610108565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006100a88261007d565b9050919050565b6100b88161009d565b81146100c357600080fd5b50565b6000815190506100d5816100af565b92915050565b6000602082840312156100f1576100f0610078565b5b60006100ff848285016100c6565b91505092915050565b610760806101176000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c8063a4de557414610030575b600080fd5b61004a6004803603810190610045919061043c565b61004c565b005b60008473ffffffffffffffffffffffffffffffffffffffff166108fc859081150290604051600060405180830381858888f193505050509050806100c5576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016100bc9061051c565b60405180910390fd5b600061080073ffffffffffffffffffffffffffffffffffffffff1660008054906101000a900473ffffffffffffffffffffffffffffffffffffffff168486604051602401610115939291906105da565b6040516020818303038152906040527f03003bc5000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff838183161783525050505060405161019f919061065f565b6000604051808303816000865af19150503d80600081146101dc576040519150601f19603f3d011682016040523d82523d6000602084013e6101e1565b606091505b50509050806040516020016101f5906106f3565b60405160208183030381529060405290610245576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161023c9190610708565b60405180910390fd5b50505050505050565b6000604051905090565b600080fd5b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b600061028d82610262565b9050919050565b61029d81610282565b81146102a857600080fd5b50565b6000813590506102ba81610294565b92915050565b6000819050919050565b6102d3816102c0565b81146102de57600080fd5b50565b6000813590506102f0816102ca565b92915050565b600080fd5b600080fd5b6000601f19601f8301169050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b61034982610300565b810181811067ffffffffffffffff8211171561036857610367610311565b5b80604052505050565b600061037b61024e565b90506103878282610340565b919050565b600067ffffffffffffffff8211156103a7576103a6610311565b5b6103b082610300565b9050602081019050919050565b82818337600083830152505050565b60006103df6103da8461038c565b610371565b9050828152602081018484840111156103fb576103fa6102fb565b5b6104068482856103bd565b509392505050565b600082601f830112610423576104226102f6565b5b81356104338482602086016103cc565b91505092915050565b6000806000806080858703121561045657610455610258565b5b6000610464878288016102ab565b9450506020610475878288016102e1565b935050604085013567ffffffffffffffff8111156104965761049561025d565b5b6104a28782880161040e565b92505060606104b3878288016102e1565b91505092959194509250565b600082825260208201905092915050565b7f4661696c656420746f2073656e64206e617469766520746f6b656e0000000000600082015250565b6000610506601b836104bf565b9150610511826104d0565b602082019050919050565b60006020820190508181036000830152610535816104f9565b9050919050565b600061054782610262565b9050919050565b6105578161053c565b82525050565b610566816102c0565b82525050565b600081519050919050565b60005b8381101561059557808201518184015260208101905061057a565b60008484015250505050565b60006105ac8261056c565b6105b681856104bf565b93506105c6818560208601610577565b6105cf81610300565b840191505092915050565b60006060820190506105ef600083018661054e565b6105fc602083018561055d565b818103604083015261060e81846105a1565b9050949350505050565b600081519050919050565b600081905092915050565b600061063982610618565b6106438185610623565b9350610653818560208601610577565b80840191505092915050565b600061066b828461062e565b915081905092915050565b600081905092915050565b7f4661696c656420746f2063616c6c20707265636f6d70696c652062616e6b536560008201527f6e64000000000000000000000000000000000000000000000000000000000000602082015250565b60006106dd602283610676565b91506106e882610681565b602282019050919050565b60006106fe826106d0565b9150819050919050565b6000602082019050818103600083015261072281846105a1565b90509291505056fea26469706673582212206bab9cbf21b89041a55ee0f768a9443ae5e5ebfde23fe8a91daf9d4b339427db64736f6c63430008180033", - "deployedBytecode": "0x608060405234801561001057600080fd5b506004361061002b5760003560e01c8063a4de557414610030575b600080fd5b61004a6004803603810190610045919061043c565b61004c565b005b60008473ffffffffffffffffffffffffffffffffffffffff166108fc859081150290604051600060405180830381858888f193505050509050806100c5576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016100bc9061051c565b60405180910390fd5b600061080073ffffffffffffffffffffffffffffffffffffffff1660008054906101000a900473ffffffffffffffffffffffffffffffffffffffff168486604051602401610115939291906105da565b6040516020818303038152906040527f03003bc5000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff838183161783525050505060405161019f919061065f565b6000604051808303816000865af19150503d80600081146101dc576040519150601f19603f3d011682016040523d82523d6000602084013e6101e1565b606091505b50509050806040516020016101f5906106f3565b60405160208183030381529060405290610245576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161023c9190610708565b60405180910390fd5b50505050505050565b6000604051905090565b600080fd5b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b600061028d82610262565b9050919050565b61029d81610282565b81146102a857600080fd5b50565b6000813590506102ba81610294565b92915050565b6000819050919050565b6102d3816102c0565b81146102de57600080fd5b50565b6000813590506102f0816102ca565b92915050565b600080fd5b600080fd5b6000601f19601f8301169050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b61034982610300565b810181811067ffffffffffffffff8211171561036857610367610311565b5b80604052505050565b600061037b61024e565b90506103878282610340565b919050565b600067ffffffffffffffff8211156103a7576103a6610311565b5b6103b082610300565b9050602081019050919050565b82818337600083830152505050565b60006103df6103da8461038c565b610371565b9050828152602081018484840111156103fb576103fa6102fb565b5b6104068482856103bd565b509392505050565b600082601f830112610423576104226102f6565b5b81356104338482602086016103cc565b91505092915050565b6000806000806080858703121561045657610455610258565b5b6000610464878288016102ab565b9450506020610475878288016102e1565b935050604085013567ffffffffffffffff8111156104965761049561025d565b5b6104a28782880161040e565b92505060606104b3878288016102e1565b91505092959194509250565b600082825260208201905092915050565b7f4661696c656420746f2073656e64206e617469766520746f6b656e0000000000600082015250565b6000610506601b836104bf565b9150610511826104d0565b602082019050919050565b60006020820190508181036000830152610535816104f9565b9050919050565b600061054782610262565b9050919050565b6105578161053c565b82525050565b610566816102c0565b82525050565b600081519050919050565b60005b8381101561059557808201518184015260208101905061057a565b60008484015250505050565b60006105ac8261056c565b6105b681856104bf565b93506105c6818560208601610577565b6105cf81610300565b840191505092915050565b60006060820190506105ef600083018661054e565b6105fc602083018561055d565b818103604083015261060e81846105a1565b9050949350505050565b600081519050919050565b600081905092915050565b600061063982610618565b6106438185610623565b9350610653818560208601610577565b80840191505092915050565b600061066b828461062e565b915081905092915050565b600081905092915050565b7f4661696c656420746f2063616c6c20707265636f6d70696c652062616e6b536560008201527f6e64000000000000000000000000000000000000000000000000000000000000602082015250565b60006106dd602283610676565b91506106e882610681565b602282019050919050565b60006106fe826106d0565b9150819050919050565b6000602082019050818103600083015261072281846105a1565b90509291505056fea26469706673582212206bab9cbf21b89041a55ee0f768a9443ae5e5ebfde23fe8a91daf9d4b339427db64736f6c63430008180033", + "bytecode": "0x608060405234801561001057600080fd5b50604051610877380380610877833981810160405281019061003291906100db565b806000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555050610108565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006100a88261007d565b9050919050565b6100b88161009d565b81146100c357600080fd5b50565b6000815190506100d5816100af565b92915050565b6000602082840312156100f1576100f0610078565b5b60006100ff848285016100c6565b91505092915050565b610760806101176000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c8063a4de557414610030575b600080fd5b61004a6004803603810190610045919061043c565b61004c565b005b60008473ffffffffffffffffffffffffffffffffffffffff166108fc859081150290604051600060405180830381858888f193505050509050806100c5576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016100bc9061051c565b60405180910390fd5b600061080073ffffffffffffffffffffffffffffffffffffffff1660008054906101000a900473ffffffffffffffffffffffffffffffffffffffff168486604051602401610115939291906105da565b6040516020818303038152906040527f03003bc5000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff838183161783525050505060405161019f919061065f565b6000604051808303816000865af19150503d80600081146101dc576040519150601f19603f3d011682016040523d82523d6000602084013e6101e1565b606091505b50509050806040516020016101f5906106f3565b60405160208183030381529060405290610245576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161023c9190610708565b60405180910390fd5b50505050505050565b6000604051905090565b600080fd5b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b600061028d82610262565b9050919050565b61029d81610282565b81146102a857600080fd5b50565b6000813590506102ba81610294565b92915050565b6000819050919050565b6102d3816102c0565b81146102de57600080fd5b50565b6000813590506102f0816102ca565b92915050565b600080fd5b600080fd5b6000601f19601f8301169050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b61034982610300565b810181811067ffffffffffffffff8211171561036857610367610311565b5b80604052505050565b600061037b61024e565b90506103878282610340565b919050565b600067ffffffffffffffff8211156103a7576103a6610311565b5b6103b082610300565b9050602081019050919050565b82818337600083830152505050565b60006103df6103da8461038c565b610371565b9050828152602081018484840111156103fb576103fa6102fb565b5b6104068482856103bd565b509392505050565b600082601f830112610423576104226102f6565b5b81356104338482602086016103cc565b91505092915050565b6000806000806080858703121561045657610455610258565b5b6000610464878288016102ab565b9450506020610475878288016102e1565b935050604085013567ffffffffffffffff8111156104965761049561025d565b5b6104a28782880161040e565b92505060606104b3878288016102e1565b91505092959194509250565b600082825260208201905092915050565b7f4661696c656420746f2073656e64206e617469766520746f6b656e0000000000600082015250565b6000610506601b836104bf565b9150610511826104d0565b602082019050919050565b60006020820190508181036000830152610535816104f9565b9050919050565b600061054782610262565b9050919050565b6105578161053c565b82525050565b610566816102c0565b82525050565b600081519050919050565b60005b8381101561059557808201518184015260208101905061057a565b60008484015250505050565b60006105ac8261056c565b6105b681856104bf565b93506105c6818560208601610577565b6105cf81610300565b840191505092915050565b60006060820190506105ef600083018661054e565b6105fc602083018561055d565b818103604083015261060e81846105a1565b9050949350505050565b600081519050919050565b600081905092915050565b600061063982610618565b6106438185610623565b9350610653818560208601610577565b80840191505092915050565b600061066b828461062e565b915081905092915050565b600081905092915050565b7f4661696c656420746f2063616c6c20707265636f6d70696c652062616e6b536560008201527f6e64000000000000000000000000000000000000000000000000000000000000602082015250565b60006106dd602283610676565b91506106e882610681565b602282019050919050565b60006106fe826106d0565b9150819050919050565b6000602082019050818103600083015261072281846105a1565b90509291505056fea2646970667358221220bd148fba67bf9e1966835ecfba5be560625fcf8c88f7890050149168488a782364736f6c63430008180033", + "deployedBytecode": "0x608060405234801561001057600080fd5b506004361061002b5760003560e01c8063a4de557414610030575b600080fd5b61004a6004803603810190610045919061043c565b61004c565b005b60008473ffffffffffffffffffffffffffffffffffffffff166108fc859081150290604051600060405180830381858888f193505050509050806100c5576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016100bc9061051c565b60405180910390fd5b600061080073ffffffffffffffffffffffffffffffffffffffff1660008054906101000a900473ffffffffffffffffffffffffffffffffffffffff168486604051602401610115939291906105da565b6040516020818303038152906040527f03003bc5000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff838183161783525050505060405161019f919061065f565b6000604051808303816000865af19150503d80600081146101dc576040519150601f19603f3d011682016040523d82523d6000602084013e6101e1565b606091505b50509050806040516020016101f5906106f3565b60405160208183030381529060405290610245576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161023c9190610708565b60405180910390fd5b50505050505050565b6000604051905090565b600080fd5b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b600061028d82610262565b9050919050565b61029d81610282565b81146102a857600080fd5b50565b6000813590506102ba81610294565b92915050565b6000819050919050565b6102d3816102c0565b81146102de57600080fd5b50565b6000813590506102f0816102ca565b92915050565b600080fd5b600080fd5b6000601f19601f8301169050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b61034982610300565b810181811067ffffffffffffffff8211171561036857610367610311565b5b80604052505050565b600061037b61024e565b90506103878282610340565b919050565b600067ffffffffffffffff8211156103a7576103a6610311565b5b6103b082610300565b9050602081019050919050565b82818337600083830152505050565b60006103df6103da8461038c565b610371565b9050828152602081018484840111156103fb576103fa6102fb565b5b6104068482856103bd565b509392505050565b600082601f830112610423576104226102f6565b5b81356104338482602086016103cc565b91505092915050565b6000806000806080858703121561045657610455610258565b5b6000610464878288016102ab565b9450506020610475878288016102e1565b935050604085013567ffffffffffffffff8111156104965761049561025d565b5b6104a28782880161040e565b92505060606104b3878288016102e1565b91505092959194509250565b600082825260208201905092915050565b7f4661696c656420746f2073656e64206e617469766520746f6b656e0000000000600082015250565b6000610506601b836104bf565b9150610511826104d0565b602082019050919050565b60006020820190508181036000830152610535816104f9565b9050919050565b600061054782610262565b9050919050565b6105578161053c565b82525050565b610566816102c0565b82525050565b600081519050919050565b60005b8381101561059557808201518184015260208101905061057a565b60008484015250505050565b60006105ac8261056c565b6105b681856104bf565b93506105c6818560208601610577565b6105cf81610300565b840191505092915050565b60006060820190506105ef600083018661054e565b6105fc602083018561055d565b818103604083015261060e81846105a1565b9050949350505050565b600081519050919050565b600081905092915050565b600061063982610618565b6106438185610623565b9350610653818560208601610577565b80840191505092915050565b600061066b828461062e565b915081905092915050565b600081905092915050565b7f4661696c656420746f2063616c6c20707265636f6d70696c652062616e6b536560008201527f6e64000000000000000000000000000000000000000000000000000000000000602082015250565b60006106dd602283610676565b91506106e882610681565b602282019050919050565b60006106fe826106d0565b9150819050919050565b6000602082019050818103600083015261072281846105a1565b90509291505056fea2646970667358221220bd148fba67bf9e1966835ecfba5be560625fcf8c88f7890050149168488a782364736f6c63430008180033", "linkReferences": {}, "deployedLinkReferences": {} } diff --git a/x/evm/embeds/contracts/IFunToken.sol b/x/evm/embeds/contracts/IFunToken.sol index 73fb0ed7f..b571c0d00 100644 --- a/x/evm/embeds/contracts/IFunToken.sol +++ b/x/evm/embeds/contracts/IFunToken.sol @@ -5,11 +5,18 @@ pragma solidity >=0.8.19; /// coins to a Nibiru bech32 address using the "FunToken" mapping between the /// ERC20 and bank. interface IFunToken { - /// @dev bankSend sends ERC20 tokens as coins to a Nibiru base account - /// @param erc20 the address of the ERC20 token contract - /// @param amount the amount of tokens to send - /// @param to the receiving Nibiru base account address as a string - function bankSend(address erc20, uint256 amount, string memory to) external; + /// @dev bankSend sends ERC20 tokens as coins to a Nibiru base account + /// @param erc20 - the address of the ERC20 token contract + /// @param amount - the amount of tokens to send + /// @param to - the receiving Nibiru base account address as a string + /// @return sentAmount - amount of tokens received by the recipient. This may + /// not be equal to `amount` if the corresponding ERC20 contract has a fee or + /// deduction on transfer. + function bankSend( + address erc20, + uint256 amount, + string memory to + ) external returns (uint256 sentAmount); } address constant FUNTOKEN_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000800; diff --git a/x/evm/keeper/erc20.go b/x/evm/keeper/erc20.go index 328452ecf..06d2e5ab3 100644 --- a/x/evm/keeper/erc20.go +++ b/x/evm/keeper/erc20.go @@ -73,23 +73,53 @@ Transfer implements "ERC20.transfer" func (e erc20Calls) Transfer( contract, from, to gethcommon.Address, amount *big.Int, ctx sdk.Context, -) (out bool, err error) { +) (balanceIncrease *big.Int, err error) { + recipientBalanceBefore, err := e.BalanceOf(contract, to, ctx) + if err != nil { + return balanceIncrease, errors.Wrap(err, "failed to retrieve recipient balance") + } + input, err := e.ABI.Pack("transfer", to, amount) if err != nil { - return false, fmt.Errorf("failed to pack ABI args: %w", err) + return balanceIncrease, fmt.Errorf("failed to pack ABI args: %w", err) } + resp, _, err := e.CallContractWithInput(ctx, from, &contract, true, input) if err != nil { - return false, err + return balanceIncrease, err } var erc20Bool ERC20Bool err = e.ABI.UnpackIntoInterface(&erc20Bool, "transfer", resp.Ret) if err != nil { - return false, err + return balanceIncrease, err + } + + // Handle the case of success=false: https://github.com/NibiruChain/nibiru/issues/2080 + success := erc20Bool.Value + if !success { + return balanceIncrease, fmt.Errorf("transfer executed but returned success=false") + } + + recipientBalanceAfter, err := e.BalanceOf(contract, to, ctx) + if err != nil { + return balanceIncrease, errors.Wrap(err, "failed to retrieve recipient balance") + } + + balanceIncrease = new(big.Int).Sub(recipientBalanceAfter, recipientBalanceBefore) + + // For flexibility with fee on transfer tokens and other types of deductions, + // we cannot assume that the amount received by the recipient is equal to + // the call "amount". Instead, verify that the recipient got tokens and + // return the amount. + if balanceIncrease.Sign() <= 0 { + return balanceIncrease, fmt.Errorf( + "amount of ERC20 tokens received MUST be positive: the balance of recipient %s would've changed by %v for token %s", + to.Hex(), balanceIncrease.String(), contract.Hex(), + ) } - return erc20Bool.Value, nil + return balanceIncrease, err } // BalanceOf retrieves the balance of an ERC20 token for a specific account. diff --git a/x/evm/keeper/erc20_test.go b/x/evm/keeper/erc20_test.go index 4b1dc10fa..c45ed10a1 100644 --- a/x/evm/keeper/erc20_test.go +++ b/x/evm/keeper/erc20_test.go @@ -34,7 +34,8 @@ func (s *Suite) TestERC20Calls() { s.T().Log("Transfer - Not enough funds") { - _, err := deps.EvmKeeper.ERC20().Transfer(contract, deps.Sender.EthAddr, evm.EVM_MODULE_ADDRESS, big.NewInt(9_420), deps.Ctx) + amt := big.NewInt(9_420) + _, err := deps.EvmKeeper.ERC20().Transfer(contract, deps.Sender.EthAddr, evm.EVM_MODULE_ADDRESS, amt, deps.Ctx) s.ErrorContains(err, "ERC20: transfer amount exceeds balance") // balances unchanged evmtest.AssertERC20BalanceEqual(s.T(), deps, contract, deps.Sender.EthAddr, big.NewInt(0)) @@ -43,10 +44,16 @@ func (s *Suite) TestERC20Calls() { s.T().Log("Transfer - Success (sanity check)") { - _, err := deps.EvmKeeper.ERC20().Transfer(contract, evm.EVM_MODULE_ADDRESS, deps.Sender.EthAddr, big.NewInt(9_420), deps.Ctx) + amt := big.NewInt(9_420) + sentAmt, err := deps.EvmKeeper.ERC20().Transfer( + contract, evm.EVM_MODULE_ADDRESS, deps.Sender.EthAddr, amt, deps.Ctx, + ) s.Require().NoError(err) - evmtest.AssertERC20BalanceEqual(s.T(), deps, contract, deps.Sender.EthAddr, big.NewInt(9_420)) - evmtest.AssertERC20BalanceEqual(s.T(), deps, contract, evm.EVM_MODULE_ADDRESS, big.NewInt(60_000)) + evmtest.AssertERC20BalanceEqual( + s.T(), deps, contract, deps.Sender.EthAddr, big.NewInt(9_420)) + evmtest.AssertERC20BalanceEqual( + s.T(), deps, contract, evm.EVM_MODULE_ADDRESS, big.NewInt(60_000)) + s.Require().Equal(sentAmt.String(), amt.String()) } s.T().Log("Burn tokens - Allowed as non-owner") diff --git a/x/evm/keeper/msg_server.go b/x/evm/keeper/msg_server.go index be8abb236..ec13c0709 100644 --- a/x/evm/keeper/msg_server.go +++ b/x/evm/keeper/msg_server.go @@ -491,22 +491,27 @@ func (k *Keeper) ConvertCoinToEvm( fungibleTokenMapping := funTokens[0] if fungibleTokenMapping.IsMadeFromCoin { - return k.convertCoinNativeCoin(ctx, sender, msg.ToEthAddr.Address, msg.BankCoin, fungibleTokenMapping) + return k.convertCoinToEvmBornCoin( + ctx, sender, msg.ToEthAddr.Address, msg.BankCoin, fungibleTokenMapping, + ) } else { - return k.convertCoinNativeERC20(ctx, sender, msg.ToEthAddr.Address, msg.BankCoin, fungibleTokenMapping) + return k.convertCoinToEvmBornERC20( + ctx, sender, msg.ToEthAddr.Address, msg.BankCoin, fungibleTokenMapping, + ) } } -// Converts a native coin to an ERC20 token. -// EVM module owns the ERC-20 contract and can mint the ERC-20 tokens. -func (k Keeper) convertCoinNativeCoin( +// Converts Bank Coins for FunToken mapping that was born from a coin +// (IsMadeFromCoin=true) into the ERC20 tokens. EVM module owns the ERC-20 +// contract and can mint the ERC-20 tokens. +func (k Keeper) convertCoinToEvmBornCoin( ctx sdk.Context, sender sdk.AccAddress, recipient gethcommon.Address, coin sdk.Coin, funTokenMapping evm.FunToken, ) (*evm.MsgConvertCoinToEvmResponse, error) { - // Step 1: Escrow bank coins with EVM module account + // Step 1: Send Bank Coins to the EVM module err := k.bankKeeper.SendCoinsFromAccountToModule(ctx, sender, evm.ModuleName, sdk.NewCoins(coin)) if err != nil { return nil, errors.Wrap(err, "failed to send coins to module account") @@ -514,7 +519,7 @@ func (k Keeper) convertCoinNativeCoin( erc20Addr := funTokenMapping.Erc20Addr.Address - // Step 2: mint ERC-20 tokens for recipient + // Step 2: Mint ERC20 tokens to the recipient evmResp, err := k.CallContract( ctx, embeds.SmartContract_ERC20Minter.ABI, @@ -542,10 +547,11 @@ func (k Keeper) convertCoinNativeCoin( return &evm.MsgConvertCoinToEvmResponse{}, nil } -// Converts a coin that was originally an ERC20 token, and that was converted to a bank coin, back to an ERC20 token. -// EVM module does not own the ERC-20 contract and cannot mint the ERC-20 tokens. -// EVM module has escrowed tokens in the first conversion from ERC-20 to bank coin. -func (k Keeper) convertCoinNativeERC20( +// Converts a coin that was originally an ERC20 token, and that was converted to +// a bank coin, back to an ERC20 token. EVM module does not own the ERC-20 +// contract and cannot mint the ERC-20 tokens. EVM module has escrowed tokens in +// the first conversion from ERC-20 to bank coin. +func (k Keeper) convertCoinToEvmBornERC20( ctx sdk.Context, sender sdk.AccAddress, recipient gethcommon.Address, @@ -553,44 +559,26 @@ func (k Keeper) convertCoinNativeERC20( funTokenMapping evm.FunToken, ) (*evm.MsgConvertCoinToEvmResponse, error) { erc20Addr := funTokenMapping.Erc20Addr.Address - - recipientBalanceBefore, err := k.ERC20().BalanceOf(erc20Addr, recipient, ctx) - if err != nil { - return nil, errors.Wrap(err, "failed to retrieve balance") - } - if recipientBalanceBefore == nil { - return nil, fmt.Errorf("failed to retrieve balance, balance is nil") - } - - // Escrow Coins on module account + // 1 | Caller transfers Bank Coins to be converted to ERC20 tokens. if err := k.bankKeeper.SendCoinsFromAccountToModule( ctx, sender, evm.ModuleName, sdk.NewCoins(coin), ); err != nil { - return nil, errors.Wrap(err, "failed to escrow coins") + return nil, errors.Wrap(err, "error sending Bank Coins to the EVM") } - // verify that the EVM module account has enough escrowed ERC-20 to transfer - // should never fail, because the coins were minted from the escrowed tokens, but check just in case - evmModuleBalance, err := k.ERC20().BalanceOf( - erc20Addr, - evm.EVM_MODULE_ADDRESS, - ctx, - ) - if err != nil { - return nil, errors.Wrap(err, "failed to retrieve balance") - } - if evmModuleBalance == nil { - return nil, fmt.Errorf("failed to retrieve balance, balance is nil") - } - if evmModuleBalance.Cmp(coin.Amount.BigInt()) < 0 { - return nil, fmt.Errorf("insufficient balance in EVM module account") - } - - // unescrow ERC-20 tokens from EVM module address - res, err := k.ERC20().Transfer( + // 2 | EVM sends ERC20 tokens to the "to" account. + // This should never fail due to the EVM account lacking ERc20 fund because + // the an account must have sent the EVM module ERC20 tokens in the mapping + // in order to create the coins originally. + // + // Said another way, if an asset is created as an ERC20 and some amount is + // converted to its Bank Coin representation, a balance of the ERC20 is left + // inside the EVM module account in order to convert the coins back to + // ERC20s. + actualSentAmount, err := k.ERC20().Transfer( erc20Addr, evm.EVM_MODULE_ADDRESS, recipient, @@ -598,37 +586,25 @@ func (k Keeper) convertCoinNativeERC20( ctx, ) if err != nil { - return nil, errors.Wrap(err, "failed to transfer ERC20 tokens") - } - if !res { - return nil, fmt.Errorf("failed to transfer ERC20 tokens") - } - - // Check expected Receiver balance after transfer execution - recipientBalanceAfter, err := k.ERC20().BalanceOf(erc20Addr, recipient, ctx) - if err != nil { - return nil, errors.Wrap(err, "failed to retrieve balance") - } - if recipientBalanceAfter == nil { - return nil, fmt.Errorf("failed to retrieve balance, balance is nil") + return nil, errors.Wrap(err, "failed to transfer ERC-20 tokens") } - expectedFinalBalance := big.NewInt(0).Add(recipientBalanceBefore, coin.Amount.BigInt()) - if r := recipientBalanceAfter.Cmp(expectedFinalBalance); r != 0 { - return nil, fmt.Errorf("expected balance after transfer to be %s, got %s", expectedFinalBalance, recipientBalanceAfter) - } - - // Burn escrowed Coins - err = k.bankKeeper.BurnCoins(ctx, evm.ModuleName, sdk.NewCoins(coin)) + // 3 | In the FunToken ERC20 → BC conversion process that preceded this + // TxMsg, the Bank Coins were minted. Consequently, to preserve an invariant + // on the sum of the FunToken's bank and ERC20 supply, we burn the coins here + // in the BC → ERC20 conversion. + burnCoin := sdk.NewCoin(coin.Denom, sdk.NewIntFromBigInt(actualSentAmount)) + err = k.bankKeeper.BurnCoins(ctx, evm.ModuleName, sdk.NewCoins(burnCoin)) if err != nil { return nil, errors.Wrap(err, "failed to burn coins") } + // Emit event with the actual amount received _ = ctx.EventManager().EmitTypedEvent(&evm.EventConvertCoinToEvm{ Sender: sender.String(), Erc20ContractAddress: funTokenMapping.Erc20Addr.String(), ToEthAddr: recipient.String(), - BankCoin: coin, + BankCoin: burnCoin, }) return &evm.MsgConvertCoinToEvmResponse{}, nil diff --git a/x/evm/precompile/funtoken.go b/x/evm/precompile/funtoken.go index 69e756ed3..e9ccaee82 100644 --- a/x/evm/precompile/funtoken.go +++ b/x/evm/precompile/funtoken.go @@ -143,20 +143,19 @@ func (p precompileFunToken) bankSend( // Caller transfers ERC20 to the EVM account transferTo := evm.EVM_MODULE_ADDRESS - _, err = p.evmKeeper.ERC20().Transfer(erc20, caller, transferTo, amount, ctx) + gotAmount, err := p.evmKeeper.ERC20().Transfer(erc20, caller, transferTo, amount, ctx) if err != nil { - return nil, fmt.Errorf("failed to send from caller to the EVM account: %w", err) + return nil, fmt.Errorf("error in ERC20.transfer from caller to EVM account: %w", err) } // EVM account mints FunToken.BankDenom to module account - amt := math.NewIntFromBigInt(amount) - coinToSend := sdk.NewCoin(funtoken.BankDenom, amt) + coinToSend := sdk.NewCoin(funtoken.BankDenom, math.NewIntFromBigInt(gotAmount)) 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. // Since we're sending them away and want accurate total supply tracking, the // tokens need to be burned. - _, err = p.evmKeeper.ERC20().Burn(erc20, evm.EVM_MODULE_ADDRESS, amount, ctx) + _, err = p.evmKeeper.ERC20().Burn(erc20, evm.EVM_MODULE_ADDRESS, gotAmount, ctx) if err != nil { err = fmt.Errorf("ERC20.Burn: %w", err) return @@ -187,7 +186,7 @@ func (p precompileFunToken) bankSend( // TODO: UD-DEBUG: feat: Emit EVM events - return method.Outputs.Pack() + return method.Outputs.Pack(gotAmount) } func SafeMintCoins( diff --git a/x/evm/precompile/funtoken_test.go b/x/evm/precompile/funtoken_test.go index dd5176fb3..fc51a47f5 100644 --- a/x/evm/precompile/funtoken_test.go +++ b/x/evm/precompile/funtoken_test.go @@ -126,18 +126,32 @@ func (s *FuntokenSuite) TestHappyPath() { input, err := embeds.SmartContract_FunToken.ABI.Pack(string(precompile.FunTokenMethod_BankSend), callArgs...) s.NoError(err) - _, resp, err := evmtest.CallContractTx( + _, ethTxResp, err := evmtest.CallContractTx( &deps, precompile.PrecompileAddr_FunToken, input, deps.Sender, ) s.Require().NoError(err) - s.Require().Empty(resp.VmError) + s.Require().Empty(ethTxResp.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)) + evmtest.AssertERC20BalanceEqual( + s.T(), deps, erc20, deps.Sender.EthAddr, big.NewInt(69_000), + ) + evmtest.AssertERC20BalanceEqual( + s.T(), deps, erc20, evm.EVM_MODULE_ADDRESS, big.NewInt(0), + ) s.Equal(sdk.NewInt(420).String(), deps.App.BankKeeper.GetBalance(deps.Ctx, randomAcc, funtoken.BankDenom).Amount.String(), ) + + s.T().Log("Parse the response contract addr and response bytes") + var sentAmt *big.Int + err = embeds.SmartContract_FunToken.ABI.UnpackIntoInterface( + &sentAmt, + string(precompile.FunTokenMethod_BankSend), + ethTxResp.Ret, + ) + s.NoError(err) + s.Require().Equal("420", sentAmt.String()) } From 7b7beb7b53a537a8ad1462cf8ca5520e21e4978c Mon Sep 17 00:00:00 2001 From: Matthias <97468149+matthiasmatt@users.noreply.github.com> Date: Wed, 30 Oct 2024 15:09:25 +0100 Subject: [PATCH 4/5] feat: add validation for multi message execution wasm (#2092) * feat: add validation for multi message execution wasm * chore: changelog * merge conflicts + simplify --------- Co-authored-by: Unique-Divine --- CHANGELOG.md | 1 + x/evm/precompile/wasm.go | 12 ++- x/evm/precompile/wasm_test.go | 183 ++++++++++++++++++++++++++++++++++ x/evm/statedb/statedb.go | 3 - 4 files changed, 193 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 61dc41b07..23dcb15d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -83,6 +83,7 @@ reverts inside of a try-catch. for (1) ERC20 transfers with tokens that return false success values instead of throwing an error and (2) ERC20 transfers with other operations that don't bring about the expected resulting balance for the transfer recipient. +- [#2092](https://github.com/NibiruChain/nibiru/pull/2092) - feat(evm): add validation for wasm multi message execution #### Nibiru EVM | Before Audit 1 - 2024-10-18 diff --git a/x/evm/precompile/wasm.go b/x/evm/precompile/wasm.go index a7b21684c..f19511cb9 100644 --- a/x/evm/precompile/wasm.go +++ b/x/evm/precompile/wasm.go @@ -10,6 +10,7 @@ import ( "github.com/NibiruChain/nibiru/v2/x/evm/embeds" wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" + wasm "github.com/CosmWasm/wasmd/x/wasm/types" gethabi "github.com/ethereum/go-ethereum/accounts/abi" gethcommon "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/vm" @@ -275,10 +276,15 @@ func (p precompileWasm) executeMulti( callerBech32 := eth.EthAddrToNibiruAddr(caller) var responses [][]byte - for _, m := range wasmExecMsgs { + for i, m := range wasmExecMsgs { wasmContract, e := sdk.AccAddressFromBech32(m.ContractAddr) if e != nil { - err = fmt.Errorf("Execute failed: %w", e) + err = fmt.Errorf("Execute failed at index %d: %w", i, e) + return + } + msgArgsCopy := wasm.RawContractMessage(m.MsgArgs) + if e := msgArgsCopy.ValidateBasic(); e != nil { + err = fmt.Errorf("Execute failed at index %d: error parsing msg args: %w", i, e) return } var funds sdk.Coins @@ -290,7 +296,7 @@ func (p precompileWasm) executeMulti( } respBz, e := p.Wasm.Execute(ctx, wasmContract, callerBech32, m.MsgArgs, funds) if e != nil { - err = e + err = fmt.Errorf("Execute failed at index %d: %w", i, e) return } responses = append(responses, respBz) diff --git a/x/evm/precompile/wasm_test.go b/x/evm/precompile/wasm_test.go index dbe35b839..72c45046c 100644 --- a/x/evm/precompile/wasm_test.go +++ b/x/evm/precompile/wasm_test.go @@ -8,6 +8,7 @@ import ( wasm "github.com/CosmWasm/wasmd/x/wasm/types" "github.com/NibiruChain/nibiru/v2/x/common/testutil" + "github.com/NibiruChain/nibiru/v2/x/common/testutil/testapp" "github.com/NibiruChain/nibiru/v2/x/evm/embeds" "github.com/NibiruChain/nibiru/v2/x/evm/evmtest" "github.com/NibiruChain/nibiru/v2/x/evm/precompile" @@ -313,3 +314,185 @@ func (s *WasmSuite) TestSadArgsExecute() { }) } } + +type WasmExecuteMsg struct { + ContractAddr string `json:"contractAddr"` + MsgArgs []byte `json:"msgArgs"` + Funds []precompile.WasmBankCoin `json:"funds"` +} + +func (s *WasmSuite) TestExecuteMultiValidation() { + deps := evmtest.NewTestDeps() + + s.Require().NoError(testapp.FundAccount( + deps.App.BankKeeper, + deps.Ctx, + deps.Sender.NibiruAddr, + sdk.NewCoins(sdk.NewCoin("unibi", sdk.NewInt(100))), + )) + + wasmContracts := test.SetupWasmContracts(&deps, &s.Suite) + wasmContract := wasmContracts[1] // hello_world_counter.wasm + + invalidMsgArgsBz := []byte(`{"invalid": "json"}`) // Invalid message format + validMsgArgsBz := []byte(`{"increment": {}}`) // Valid increment message + + var emptyFunds []precompile.WasmBankCoin + validFunds := []precompile.WasmBankCoin{{ + Denom: "unibi", + Amount: big.NewInt(100), + }} + invalidFunds := []precompile.WasmBankCoin{{ + Denom: "invalid!denom", + Amount: big.NewInt(100), + }} + + testCases := []struct { + name string + executeMsgs []WasmExecuteMsg + wantError string + }{ + { + name: "valid - single message", + executeMsgs: []WasmExecuteMsg{ + { + ContractAddr: wasmContract.String(), + MsgArgs: validMsgArgsBz, + Funds: emptyFunds, + }, + }, + wantError: "", + }, + { + name: "valid - multiple messages", + executeMsgs: []WasmExecuteMsg{ + { + ContractAddr: wasmContract.String(), + MsgArgs: validMsgArgsBz, + Funds: validFunds, + }, + { + ContractAddr: wasmContract.String(), + MsgArgs: validMsgArgsBz, + Funds: emptyFunds, + }, + }, + wantError: "", + }, + { + name: "invalid - malformed contract address", + executeMsgs: []WasmExecuteMsg{ + { + ContractAddr: "invalid-address", + MsgArgs: validMsgArgsBz, + Funds: emptyFunds, + }, + }, + wantError: "decoding bech32 failed", + }, + { + name: "invalid - malformed message args", + executeMsgs: []WasmExecuteMsg{ + { + ContractAddr: wasmContract.String(), + MsgArgs: invalidMsgArgsBz, + Funds: emptyFunds, + }, + }, + wantError: "unknown variant", + }, + { + name: "invalid - malformed funds", + executeMsgs: []WasmExecuteMsg{ + { + ContractAddr: wasmContract.String(), + MsgArgs: validMsgArgsBz, + Funds: invalidFunds, + }, + }, + wantError: "invalid coins", + }, + { + name: "invalid - second message fails validation", + executeMsgs: []WasmExecuteMsg{ + { + ContractAddr: wasmContract.String(), + MsgArgs: validMsgArgsBz, + Funds: emptyFunds, + }, + { + ContractAddr: wasmContract.String(), + MsgArgs: invalidMsgArgsBz, + Funds: emptyFunds, + }, + }, + wantError: "unknown variant", + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + callArgs := []any{tc.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, + ) + + if tc.wantError != "" { + s.Require().ErrorContains(err, tc.wantError) + return + } + s.Require().NoError(err) + s.NotNil(ethTxResp) + s.NotEmpty(ethTxResp.Ret) + }) + } +} + +// TestExecuteMultiPartialExecution ensures that no state changes occur if any message +// in the batch fails validation +func (s *WasmSuite) TestExecuteMultiPartialExecution() { + deps := evmtest.NewTestDeps() + wasmContracts := test.SetupWasmContracts(&deps, &s.Suite) + wasmContract := wasmContracts[1] // hello_world_counter.wasm + + // First verify initial state is 0 + test.AssertWasmCounterState(&s.Suite, deps, wasmContract, 0) + + // Create a batch where the second message will fail validation + executeMsgs := []WasmExecuteMsg{ + { + ContractAddr: wasmContract.String(), + MsgArgs: []byte(`{"increment": {}}`), + Funds: []precompile.WasmBankCoin{}, + }, + { + ContractAddr: wasmContract.String(), + MsgArgs: []byte(`{"invalid": "json"}`), // This will fail validation + Funds: []precompile.WasmBankCoin{}, + }, + } + + 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, + ) + + // Verify that the call failed + s.Require().Error(err, "ethTxResp: ", ethTxResp) + s.Require().Contains(err.Error(), "unknown variant") + + // Verify that no state changes occurred + test.AssertWasmCounterState(&s.Suite, deps, wasmContract, 0) +} diff --git a/x/evm/statedb/statedb.go b/x/evm/statedb/statedb.go index 4c79e61af..a411660f9 100644 --- a/x/evm/statedb/statedb.go +++ b/x/evm/statedb/statedb.go @@ -480,9 +480,6 @@ func (s *StateDB) Snapshot() int { // RevertToSnapshot reverts all state changes made since the given revision. func (s *StateDB) RevertToSnapshot(revid int) { - fmt.Printf("len(s.validRevisions): %d\n", len(s.validRevisions)) - fmt.Printf("s.validRevisions: %v\n", s.validRevisions) - // Find the snapshot in the stack of valid snapshots. idx := sort.Search(len(s.validRevisions), func(i int) bool { return s.validRevisions[i].id >= revid From 0afb08f403baffc6e61046f3ad0774f64e7d14c4 Mon Sep 17 00:00:00 2001 From: Kevin Yang <5478483+k-yang@users.noreply.github.com> Date: Wed, 30 Oct 2024 21:48:29 -0700 Subject: [PATCH 5/5] refactor: general code style cleanups on precompile and statedb journal (#2100) * statedb: add cacheing for multistore before precompile runs * messy, working first version that allows for precompile reversion * wip!: Save checkpoint. 1. Created NibiruBankKeeper with safety around NIBI transfers inside of EthereumTx. 2. The "PrecompileCalled" JournalChange now has a propery implementation and a strong test case to show that reverting the precompile calls works as intended. 3. Remove unneeded functions created for testing with low-level struct fields. * chore: changelog * finalize bank keeper changes * revert to previous commit 7f904a07ac4d5555d8c088411024fc50ff65d085 * fix strange ignored file issue * remove new bank keeper * chore: comments from self-review * refactor: clean up test code * refactor: clean up parse fund args * refactor: clean up common precompile functions * refactor: clean up precompile onrunstart * refactor: clean up precompile onrunstart * refactor: clean up oracle precompile * fix: import * Delete .gitignore * Update CHANGELOG.md * refactor: replace interface{} with any * refactor: add ABI() precompile fn * refactor: replace assertNotReadonlyTx --------- Co-authored-by: Unique-Divine --- CHANGELOG.md | 7 +-- app/server/config/server_config.go | 2 +- cmd/nibid/cmd/base64.go | 2 +- cmd/nibid/cmd/decode_base64.go | 8 +-- eth/eip712/eip712_legacy.go | 10 ++-- eth/eip712/encoding_legacy.go | 4 +- eth/eip712/message.go | 4 +- eth/rpc/backend/blocks.go | 8 +-- eth/rpc/backend/node_info.go | 4 +- eth/rpc/backend/tracing.go | 8 +-- eth/rpc/backend/utils.go | 2 +- eth/rpc/rpc.go | 6 +-- eth/rpc/rpcapi/debugapi/api.go | 4 +- eth/rpc/rpcapi/eth_api.go | 20 ++++---- eth/rpc/rpcapi/eth_filters_api.go | 2 +- eth/rpc/rpcapi/websockets.go | 34 ++++++------- x/common/testutil/nullify.go | 2 +- x/common/testutil/testnetwork/logger.go | 4 +- x/devgas/v1/types/params.go | 6 +-- x/epochs/types/identifier.go | 2 +- x/evm/errors.go | 2 +- x/evm/keeper/grpc_query.go | 4 +- x/evm/params.go | 4 +- x/evm/precompile/.gitignore | 17 ------- x/evm/precompile/errors.go | 16 +++--- x/evm/precompile/funtoken.go | 28 ++++------- x/evm/precompile/oracle.go | 21 ++++---- x/evm/precompile/precompile.go | 43 ++++++++-------- x/evm/precompile/test/export.go | 63 +++++++++-------------- x/evm/precompile/wasm.go | 57 +++++++++++---------- x/evm/precompile/wasm_parse.go | 66 +++++++++++-------------- x/evm/statedb/journal_test.go | 27 +++++----- x/evm/statedb/statedb.go | 7 ++- x/evm/vmtracer.go | 4 +- x/inflation/types/params.go | 12 ++--- x/oracle/types/hash.go | 2 +- x/tokenfactory/types/codec.go | 2 +- 37 files changed, 230 insertions(+), 284 deletions(-) delete mode 100644 x/evm/precompile/.gitignore diff --git a/CHANGELOG.md b/CHANGELOG.md index 23dcb15d1..88fa93ee9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,9 +63,9 @@ 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 @@ -163,6 +163,7 @@ about the expected resulting balance for the transfer recipient. - [#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 +- [#2100](https://github.com/NibiruChain/nibiru/pull/2100) - refactor: cleanup statedb and precompile sections ### State Machine Breaking (Other) diff --git a/app/server/config/server_config.go b/app/server/config/server_config.go index b72638ed2..f393983a9 100644 --- a/app/server/config/server_config.go +++ b/app/server/config/server_config.go @@ -174,7 +174,7 @@ type TLSConfig struct { // AppConfig helps to override default appConfig template and configs. // return "", nil if no custom configuration is required for the application. -func AppConfig(denom string) (string, interface{}) { +func AppConfig(denom string) (string, any) { // Optionally allow the chain developer to overwrite the SDK's default // server config. customAppConfig := DefaultConfig() diff --git a/cmd/nibid/cmd/base64.go b/cmd/nibid/cmd/base64.go index 5f235d3ea..ba5685aff 100644 --- a/cmd/nibid/cmd/base64.go +++ b/cmd/nibid/cmd/base64.go @@ -33,7 +33,7 @@ func GetBuildWasmMsg() *cobra.Command { Value string `json:"value,omitempty"` } - js, err := json.Marshal(map[string]interface{}{ + js, err := json.Marshal(map[string]any{ "stargate": stargateMessage{ TypeURL: anyMsg.TypeUrl, Value: base64.StdEncoding.EncodeToString(anyMsg.Value), diff --git a/cmd/nibid/cmd/decode_base64.go b/cmd/nibid/cmd/decode_base64.go index 3de3706f2..8625070c3 100644 --- a/cmd/nibid/cmd/decode_base64.go +++ b/cmd/nibid/cmd/decode_base64.go @@ -27,7 +27,7 @@ import ( // from the provided JSON data. // - err error: An error object, which is nil if the operation is successful. func YieldStargateMsgs(jsonBz []byte) (sgMsgs []wasmvm.StargateMsg, err error) { - var data interface{} + var data any if err := json.Unmarshal(jsonBz, &data); err != nil { return sgMsgs, err } @@ -50,7 +50,7 @@ func YieldStargateMsgs(jsonBz []byte) (sgMsgs []wasmvm.StargateMsg, err error) { // encoded base 64 string. func parseStargateMsgs(jsonData any, msgs *[]wasmvm.StargateMsg) { switch v := jsonData.(type) { - case map[string]interface{}: + case map[string]any: if typeURL, ok := v["type_url"].(string); ok { if value, ok := v["value"].(string); ok { *msgs = append(*msgs, wasmvm.StargateMsg{ @@ -62,7 +62,7 @@ func parseStargateMsgs(jsonData any, msgs *[]wasmvm.StargateMsg) { for _, value := range v { parseStargateMsgs(value, msgs) } - case []interface{}: + case []any: for _, value := range v { parseStargateMsgs(value, msgs) } @@ -93,7 +93,7 @@ func DecodeBase64StargateMsgs( ) (newSgMsgs []StargateMsgDecoded, err error) { codec := clientCtx.Codec - var data interface{} + var data any if err := json.Unmarshal(jsonBz, &data); err != nil { return newSgMsgs, fmt.Errorf( "failed to decode stargate msgs due to invalid JSON: %w", err) diff --git a/eth/eip712/eip712_legacy.go b/eth/eip712/eip712_legacy.go index c1d0b1b30..b50f10e5f 100644 --- a/eth/eip712/eip712_legacy.go +++ b/eth/eip712/eip712_legacy.go @@ -38,7 +38,7 @@ func LegacyWrapTxToTypedData( data []byte, feeDelegation *FeeDelegationOptions, ) (apitypes.TypedData, error) { - txData := make(map[string]interface{}) + txData := make(map[string]any) if err := json.Unmarshal(data, &txData); err != nil { return apitypes.TypedData{}, errorsmod.Wrap(errortypes.ErrJSONUnmarshal, "failed to JSON unmarshal data") @@ -58,7 +58,7 @@ func LegacyWrapTxToTypedData( } if feeDelegation != nil { - feeInfo, ok := txData["fee"].(map[string]interface{}) + feeInfo, ok := txData["fee"].(map[string]any) if !ok { return apitypes.TypedData{}, errorsmod.Wrap(errortypes.ErrInvalidType, "cannot parse fee from tx data") } @@ -139,7 +139,7 @@ func extractMsgTypes(cdc codectypes.AnyUnpacker, msgTypeName string, msg sdk.Msg return rootTypes, nil } -func walkFields(cdc codectypes.AnyUnpacker, typeMap apitypes.Types, rootType string, in interface{}) (err error) { +func walkFields(cdc codectypes.AnyUnpacker, typeMap apitypes.Types, rootType string, in any) (err error) { defer doRecover(&err) t := reflect.TypeOf(in) @@ -161,8 +161,8 @@ func walkFields(cdc codectypes.AnyUnpacker, typeMap apitypes.Types, rootType str } type CosmosAnyWrapper struct { - Type string `json:"type"` - Value interface{} `json:"value"` + Type string `json:"type"` + Value any `json:"value"` } // legacyTraverseFields: Recursively inspects the fields of a given diff --git a/eth/eip712/encoding_legacy.go b/eth/eip712/encoding_legacy.go index 805eb48dd..23f943eaf 100644 --- a/eth/eip712/encoding_legacy.go +++ b/eth/eip712/encoding_legacy.go @@ -17,8 +17,8 @@ import ( ) type aminoMessage struct { - Type string `json:"type"` - Value interface{} `json:"value"` + Type string `json:"type"` + Value any `json:"value"` } // LegacyGetEIP712BytesForMsg returns the EIP-712 object bytes for the given SignDoc bytes by decoding the bytes into diff --git a/eth/eip712/message.go b/eth/eip712/message.go index 6a8577c22..176d90b56 100644 --- a/eth/eip712/message.go +++ b/eth/eip712/message.go @@ -14,7 +14,7 @@ import ( type eip712MessagePayload struct { payload gjson.Result numPayloadMsgs int - message map[string]interface{} + message map[string]any } const ( @@ -34,7 +34,7 @@ func createEIP712MessagePayload(data []byte) (eip712MessagePayload, error) { return eip712MessagePayload{}, errorsmod.Wrap(err, "failed to flatten payload JSON messages") } - message, ok := payload.Value().(map[string]interface{}) + message, ok := payload.Value().(map[string]any) if !ok { return eip712MessagePayload{}, errorsmod.Wrap(errortypes.ErrInvalidType, "failed to parse JSON as map") } diff --git a/eth/rpc/backend/blocks.go b/eth/rpc/backend/blocks.go index a64e73e2a..7bc5f4b97 100644 --- a/eth/rpc/backend/blocks.go +++ b/eth/rpc/backend/blocks.go @@ -57,7 +57,7 @@ func (b *Backend) BlockNumber() (hexutil.Uint64, error) { // GetBlockByNumber returns the JSON-RPC compatible Ethereum block identified by // block number. Depending on fullTx it either returns the full transaction // objects or if false only the hashes of the transactions. -func (b *Backend) GetBlockByNumber(blockNum rpc.BlockNumber, fullTx bool) (map[string]interface{}, error) { +func (b *Backend) GetBlockByNumber(blockNum rpc.BlockNumber, fullTx bool) (map[string]any, error) { resBlock, err := b.TendermintBlockByNumber(blockNum) if err != nil { return nil, nil @@ -85,7 +85,7 @@ func (b *Backend) GetBlockByNumber(blockNum rpc.BlockNumber, fullTx bool) (map[s // GetBlockByHash returns the JSON-RPC compatible Ethereum block identified by // hash. -func (b *Backend) GetBlockByHash(hash gethcommon.Hash, fullTx bool) (map[string]interface{}, error) { +func (b *Backend) GetBlockByHash(hash gethcommon.Hash, fullTx bool) (map[string]any, error) { resBlock, err := b.TendermintBlockByHash(hash) if err != nil { return nil, err @@ -348,8 +348,8 @@ func (b *Backend) RPCBlockFromTendermintBlock( resBlock *tmrpctypes.ResultBlock, blockRes *tmrpctypes.ResultBlockResults, fullTx bool, -) (map[string]interface{}, error) { - ethRPCTxs := []interface{}{} +) (map[string]any, error) { + ethRPCTxs := []any{} block := resBlock.Block baseFeeWei, err := b.BaseFeeWei(blockRes) diff --git a/eth/rpc/backend/node_info.go b/eth/rpc/backend/node_info.go index 402578dd5..e95488b2a 100644 --- a/eth/rpc/backend/node_info.go +++ b/eth/rpc/backend/node_info.go @@ -39,7 +39,7 @@ func (b *Backend) Accounts() ([]gethcommon.Address, error) { // - highestBlock: block number of the highest block header this node has received from peers // - pulledStates: number of state entries processed until now // - knownStates: number of known state entries that still need to be pulled -func (b *Backend) Syncing() (interface{}, error) { +func (b *Backend) Syncing() (any, error) { status, err := b.clientCtx.Client.Status(b.ctx) if err != nil { return false, err @@ -49,7 +49,7 @@ func (b *Backend) Syncing() (interface{}, error) { return false, nil } - return map[string]interface{}{ + return map[string]any{ "startingBlock": hexutil.Uint64(status.SyncInfo.EarliestBlockHeight), "currentBlock": hexutil.Uint64(status.SyncInfo.LatestBlockHeight), // "highestBlock": nil, // NA diff --git a/eth/rpc/backend/tracing.go b/eth/rpc/backend/tracing.go index 4a20557cd..94bb8fbba 100644 --- a/eth/rpc/backend/tracing.go +++ b/eth/rpc/backend/tracing.go @@ -18,7 +18,7 @@ import ( // TraceTransaction returns the structured logs created during the execution of EVM // and returns them as a JSON object. -func (b *Backend) TraceTransaction(hash gethcommon.Hash, config *evm.TraceConfig) (interface{}, error) { +func (b *Backend) TraceTransaction(hash gethcommon.Hash, config *evm.TraceConfig) (any, error) { // Get transaction by hash transaction, err := b.GetTxByEthHash(hash) if err != nil { @@ -124,7 +124,7 @@ func (b *Backend) TraceTransaction(hash gethcommon.Hash, config *evm.TraceConfig // Response format is unknown due to custom tracer config param // More information can be found here https://geth.ethereum.org/docs/dapp/tracing-filtered - var decodedResult interface{} + var decodedResult any err = json.Unmarshal(traceResult.Data, &decodedResult) if err != nil { return nil, err @@ -219,7 +219,7 @@ func (b *Backend) TraceCall( txArgs evm.JsonTxArgs, contextBlock rpc.BlockNumber, config *evm.TraceConfig, -) (interface{}, error) { +) (any, error) { blk, err := b.TendermintBlockByNumber(contextBlock) if err != nil { b.logger.Debug("block not found", "contextBlock", contextBlock) @@ -253,7 +253,7 @@ func (b *Backend) TraceCall( if err != nil { return nil, err } - var decodedResult interface{} + var decodedResult any err = json.Unmarshal(traceResult.Data, &decodedResult) if err != nil { return nil, err diff --git a/eth/rpc/backend/utils.go b/eth/rpc/backend/utils.go index 23ce2f410..bb689bcce 100644 --- a/eth/rpc/backend/utils.go +++ b/eth/rpc/backend/utils.go @@ -109,7 +109,7 @@ func (b *Backend) getAccountNonce(accAddr common.Address, pending bool, height i // See eth_feeHistory method for more details of the return format. func (b *Backend) retrieveEVMTxFeesFromBlock( tendermintBlock *tmrpctypes.ResultBlock, - ethBlock *map[string]interface{}, + ethBlock *map[string]any, rewardPercentiles []float64, tendermintBlockResult *tmrpctypes.ResultBlockResults, targetOneFeeHistory *rpc.OneFeeHistory, diff --git a/eth/rpc/rpc.go b/eth/rpc/rpc.go index de5a97991..c6c0891de 100644 --- a/eth/rpc/rpc.go +++ b/eth/rpc/rpc.go @@ -114,9 +114,9 @@ func BlockMaxGasFromConsensusParams( // transactions. func FormatBlock( header tmtypes.Header, size int, gasLimit int64, - gasUsed *big.Int, transactions []interface{}, bloom gethcore.Bloom, + gasUsed *big.Int, transactions []any, bloom gethcore.Bloom, validatorAddr gethcommon.Address, baseFee *big.Int, -) map[string]interface{} { +) map[string]any { var transactionsRoot gethcommon.Hash if len(transactions) == 0 { transactionsRoot = gethcore.EmptyRootHash @@ -124,7 +124,7 @@ func FormatBlock( transactionsRoot = gethcommon.BytesToHash(header.DataHash) } - result := map[string]interface{}{ + result := map[string]any{ "number": hexutil.Uint64(header.Height), "hash": hexutil.Bytes(header.Hash()), "parentHash": gethcommon.BytesToHash(header.LastBlockID.Hash.Bytes()), diff --git a/eth/rpc/rpcapi/debugapi/api.go b/eth/rpc/rpcapi/debugapi/api.go index c70673273..7ce3fa345 100644 --- a/eth/rpc/rpcapi/debugapi/api.go +++ b/eth/rpc/rpcapi/debugapi/api.go @@ -65,7 +65,7 @@ func NewImplDebugAPI( // TraceTransaction returns the structured logs created during the execution of EVM // and returns them as a JSON object. -func (a *DebugAPI) TraceTransaction(hash common.Hash, config *evm.TraceConfig) (interface{}, error) { +func (a *DebugAPI) TraceTransaction(hash common.Hash, config *evm.TraceConfig) (any, error) { a.logger.Debug("debug_traceTransaction", "hash", hash) return a.backend.TraceTransaction(hash, config) } @@ -115,7 +115,7 @@ func (a *DebugAPI) TraceCall( args evm.JsonTxArgs, blockNrOrHash rpc.BlockNumberOrHash, config *evm.TraceConfig, -) (interface{}, error) { +) (any, error) { a.logger.Debug("debug_traceCall", args.String(), "block number or hash", blockNrOrHash) // Get Tendermint Block diff --git a/eth/rpc/rpcapi/eth_api.go b/eth/rpc/rpcapi/eth_api.go index 62590347f..3ada30358 100644 --- a/eth/rpc/rpcapi/eth_api.go +++ b/eth/rpc/rpcapi/eth_api.go @@ -30,8 +30,8 @@ type IEthAPI interface { // // Retrieves information from a particular block in the blockchain. BlockNumber() (hexutil.Uint64, error) - GetBlockByNumber(ethBlockNum rpc.BlockNumber, fullTx bool) (map[string]interface{}, error) - GetBlockByHash(hash common.Hash, fullTx bool) (map[string]interface{}, error) + GetBlockByNumber(ethBlockNum rpc.BlockNumber, fullTx bool) (map[string]any, error) + GetBlockByHash(hash common.Hash, fullTx bool) (map[string]any, error) GetBlockTransactionCountByHash(hash common.Hash) *hexutil.Uint GetBlockTransactionCountByNumber(blockNum rpc.BlockNumber) *hexutil.Uint @@ -97,15 +97,15 @@ type IEthAPI interface { // and replaced by a canonical block instead. GetUncleByBlockHashAndIndex( hash common.Hash, idx hexutil.Uint, - ) map[string]interface{} + ) map[string]any GetUncleByBlockNumberAndIndex( number, idx hexutil.Uint, - ) map[string]interface{} + ) map[string]any GetUncleCountByBlockHash(hash common.Hash) hexutil.Uint GetUncleCountByBlockNumber(blockNum rpc.BlockNumber) hexutil.Uint // Other - Syncing() (interface{}, error) + Syncing() (any, error) GetTransactionLogs(txHash common.Hash) ([]*gethcore.Log, error) FillTransaction( args evm.JsonTxArgs, @@ -144,13 +144,13 @@ func (e *EthAPI) BlockNumber() (hexutil.Uint64, error) { } // GetBlockByNumber returns the block identified by number. -func (e *EthAPI) GetBlockByNumber(ethBlockNum rpc.BlockNumber, fullTx bool) (map[string]interface{}, error) { +func (e *EthAPI) GetBlockByNumber(ethBlockNum rpc.BlockNumber, fullTx bool) (map[string]any, error) { e.logger.Debug("eth_getBlockByNumber", "number", ethBlockNum, "full", fullTx) return e.backend.GetBlockByNumber(ethBlockNum, fullTx) } // GetBlockByHash returns the block identified by hash. -func (e *EthAPI) GetBlockByHash(hash common.Hash, fullTx bool) (map[string]interface{}, error) { +func (e *EthAPI) GetBlockByHash(hash common.Hash, fullTx bool) (map[string]any, error) { e.logger.Debug("eth_getBlockByHash", "hash", hash.Hex(), "full", fullTx) return e.backend.GetBlockByHash(hash, fullTx) } @@ -359,7 +359,7 @@ func (e *EthAPI) ChainId() (*hexutil.Big, error) { //nolint // Always returns nil. func (e *EthAPI) GetUncleByBlockHashAndIndex( _ common.Hash, _ hexutil.Uint, -) map[string]interface{} { +) map[string]any { return nil } @@ -367,7 +367,7 @@ func (e *EthAPI) GetUncleByBlockHashAndIndex( // index. Always returns nil. func (e *EthAPI) GetUncleByBlockNumberAndIndex( _, _ hexutil.Uint, -) map[string]interface{} { +) map[string]any { return nil } @@ -396,7 +396,7 @@ func (e *EthAPI) GetUncleCountByBlockNumber(_ rpc.BlockNumber) hexutil.Uint { // - highestBlock: block number of the highest block header this node has received from peers // - pulledStates: number of state entries processed until now // - knownStates: number of known state entries that still need to be pulled -func (e *EthAPI) Syncing() (interface{}, error) { +func (e *EthAPI) Syncing() (any, error) { e.logger.Debug("eth_syncing") return e.backend.Syncing() } diff --git a/eth/rpc/rpcapi/eth_filters_api.go b/eth/rpc/rpcapi/eth_filters_api.go index 782014bb5..eca8dd27c 100644 --- a/eth/rpc/rpcapi/eth_filters_api.go +++ b/eth/rpc/rpcapi/eth_filters_api.go @@ -621,7 +621,7 @@ func (api *FiltersAPI) GetFilterLogs(ctx context.Context, id gethrpc.ID) ([]*get // // This function implements the "eth_getFilterChanges" JSON-RPC service method. // https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getfilterchanges -func (api *FiltersAPI) GetFilterChanges(id gethrpc.ID) (interface{}, error) { +func (api *FiltersAPI) GetFilterChanges(id gethrpc.ID) (any, error) { api.logger.Debug("eth_getFilterChanges") api.filtersMu.Lock() defer api.filtersMu.Unlock() diff --git a/eth/rpc/rpcapi/websockets.go b/eth/rpc/rpcapi/websockets.go index 68fb7eb9e..530dd39d7 100644 --- a/eth/rpc/rpcapi/websockets.go +++ b/eth/rpc/rpcapi/websockets.go @@ -41,9 +41,9 @@ type WebsocketsServer interface { } type SubscriptionResponseJSON struct { - Jsonrpc string `json:"jsonrpc"` - Result interface{} `json:"result"` - ID float64 `json:"id"` + Jsonrpc string `json:"jsonrpc"` + Result any `json:"result"` + ID float64 `json:"id"` } type SubscriptionNotification struct { @@ -53,8 +53,8 @@ type SubscriptionNotification struct { } type SubscriptionResult struct { - Subscription gethrpc.ID `json:"subscription"` - Result interface{} `json:"result"` + Subscription gethrpc.ID `json:"subscription"` + Result any `json:"result"` } type ErrorResponseJSON struct { @@ -157,7 +157,7 @@ type wsConn struct { mux *sync.Mutex } -func (w *wsConn) WriteJSON(v interface{}) error { +func (w *wsConn) WriteJSON(v any) error { w.mux.Lock() defer w.mux.Unlock() @@ -203,7 +203,7 @@ func (s *websocketsServer) readLoop(wsConn *wsConn) { continue } - var msg map[string]interface{} + var msg map[string]any if err = json.Unmarshal(mb, &msg); err != nil { s.sendErrResponse(wsConn, err.Error()) continue @@ -299,8 +299,8 @@ func (s *websocketsServer) readLoop(wsConn *wsConn) { } // tcpGetAndSendResponse sends error response to client if params is invalid -func (s *websocketsServer) getParamsAndCheckValid(msg map[string]interface{}, wsConn *wsConn) ([]interface{}, bool) { - params, ok := msg["params"].([]interface{}) +func (s *websocketsServer) getParamsAndCheckValid(msg map[string]any, wsConn *wsConn) ([]any, bool) { + params, ok := msg["params"].([]any) if !ok { s.sendErrResponse(wsConn, "invalid parameters") return nil, false @@ -336,7 +336,7 @@ func (s *websocketsServer) tcpGetAndSendResponse(wsConn *wsConn, mb []byte) erro return errors.Wrap(err, "could not read body from response") } - var wsSend interface{} + var wsSend any err = json.Unmarshal(body, &wsSend) if err != nil { return errors.Wrap(err, "failed to unmarshal rest-server response") @@ -362,7 +362,7 @@ func newPubSubAPI(clientCtx client.Context, logger log.Logger, tmWSClient *rpccl } } -func (api *pubSubAPI) subscribe(wsConn *wsConn, subID gethrpc.ID, params []interface{}) (pubsub.UnsubscribeFunc, error) { +func (api *pubSubAPI) subscribe(wsConn *wsConn, subID gethrpc.ID, params []any) (pubsub.UnsubscribeFunc, error) { method, ok := params[0].(string) if !ok { return nil, errors.New("invalid parameters") @@ -462,11 +462,11 @@ func try(fn func(), l log.Logger, desc string) { fn() } -func (api *pubSubAPI) subscribeLogs(wsConn *wsConn, subID gethrpc.ID, extra interface{}) (pubsub.UnsubscribeFunc, error) { +func (api *pubSubAPI) subscribeLogs(wsConn *wsConn, subID gethrpc.ID, extra any) (pubsub.UnsubscribeFunc, error) { crit := filters.FilterCriteria{} if extra != nil { - params, ok := extra.(map[string]interface{}) + params, ok := extra.(map[string]any) if !ok { err := errors.New("invalid criteria") api.logger.Debug("invalid criteria", "type", fmt.Sprintf("%T", extra)) @@ -475,7 +475,7 @@ func (api *pubSubAPI) subscribeLogs(wsConn *wsConn, subID gethrpc.ID, extra inte if params["address"] != nil { address, isString := params["address"].(string) - addresses, isSlice := params["address"].([]interface{}) + addresses, isSlice := params["address"].([]any) if !isString && !isSlice { err := errors.New("invalid addresses; must be address or array of addresses") api.logger.Debug("invalid addresses", "type", fmt.Sprintf("%T", params["address"])) @@ -502,7 +502,7 @@ func (api *pubSubAPI) subscribeLogs(wsConn *wsConn, subID gethrpc.ID, extra inte } if params["topics"] != nil { - topics, ok := params["topics"].([]interface{}) + topics, ok := params["topics"].([]any) if !ok { err := errors.Errorf("invalid topics: %s", topics) api.logger.Error("invalid topics", "type", fmt.Sprintf("%T", topics)) @@ -511,7 +511,7 @@ func (api *pubSubAPI) subscribeLogs(wsConn *wsConn, subID gethrpc.ID, extra inte crit.Topics = make([][]common.Hash, len(topics)) - addCritTopic := func(topicIdx int, topic interface{}) error { + addCritTopic := func(topicIdx int, topic any) error { tstr, ok := topic.(string) if !ok { err := errors.Errorf("invalid topic: %s", topic) @@ -538,7 +538,7 @@ func (api *pubSubAPI) subscribeLogs(wsConn *wsConn, subID gethrpc.ID, extra inte } // in case we actually have a list of subtopics - subtopicsList, ok := subtopics.([]interface{}) + subtopicsList, ok := subtopics.([]any) if !ok { err := errors.New("invalid subtopics") api.logger.Error("invalid subtopic", "type", fmt.Sprintf("%T", subtopics)) diff --git a/x/common/testutil/nullify.go b/x/common/testutil/nullify.go index 202b59e6f..96fc340f6 100644 --- a/x/common/testutil/nullify.go +++ b/x/common/testutil/nullify.go @@ -16,7 +16,7 @@ var ( // Fill analyze all struct fields and slices with // reflection and initialize the nil and empty slices, // structs, and pointers. -func Fill(x interface{}) interface{} { +func Fill(x any) any { v := reflect.Indirect(reflect.ValueOf(x)) switch v.Kind() { case reflect.Slice: diff --git a/x/common/testutil/testnetwork/logger.go b/x/common/testutil/testnetwork/logger.go index dfdf56d97..7b1206a94 100644 --- a/x/common/testutil/testnetwork/logger.go +++ b/x/common/testutil/testnetwork/logger.go @@ -11,8 +11,8 @@ import ( // Typically, a `testing.T` struct is used as the logger for both the "Network" // and corresponding "Validators". type Logger interface { - Log(args ...interface{}) - Logf(format string, args ...interface{}) + Log(args ...any) + Logf(format string, args ...any) } var _ Logger = (*testing.T)(nil) diff --git a/x/devgas/v1/types/params.go b/x/devgas/v1/types/params.go index 433aa4d43..532436c73 100644 --- a/x/devgas/v1/types/params.go +++ b/x/devgas/v1/types/params.go @@ -28,7 +28,7 @@ func DefaultParams() ModuleParams { } } -func validateBool(i interface{}) error { +func validateBool(i any) error { _, ok := i.(bool) if !ok { return fmt.Errorf("invalid parameter type: %T", i) @@ -37,7 +37,7 @@ func validateBool(i interface{}) error { return nil } -func validateShares(i interface{}) error { +func validateShares(i any) error { v, ok := i.(sdk.Dec) if !ok { @@ -59,7 +59,7 @@ func validateShares(i interface{}) error { return nil } -func validateArray(i interface{}) error { +func validateArray(i any) error { _, ok := i.([]string) if !ok { return fmt.Errorf("invalid parameter type: %T", i) diff --git a/x/epochs/types/identifier.go b/x/epochs/types/identifier.go index 3dd3012cb..714b9dd66 100644 --- a/x/epochs/types/identifier.go +++ b/x/epochs/types/identifier.go @@ -20,7 +20,7 @@ const ( // ValidateEpochIdentifierInterface performs a stateless // validation of the epoch ID interface. -func ValidateEpochIdentifierInterface(i interface{}) error { +func ValidateEpochIdentifierInterface(i any) error { v, ok := i.(string) if !ok { return fmt.Errorf("invalid parameter type: %T", i) diff --git a/x/evm/errors.go b/x/evm/errors.go index 9fc6722ac..87d5c747b 100644 --- a/x/evm/errors.go +++ b/x/evm/errors.go @@ -106,6 +106,6 @@ func (e *RevertError) ErrorCode() int { } // ErrorData returns the hex encoded revert reason. -func (e *RevertError) ErrorData() interface{} { +func (e *RevertError) ErrorData() any { return e.reason } diff --git a/x/evm/keeper/grpc_query.go b/x/evm/keeper/grpc_query.go index 9cc9290e9..4e13254a9 100644 --- a/x/evm/keeper/grpc_query.go +++ b/x/evm/keeper/grpc_query.go @@ -736,7 +736,7 @@ func (k *Keeper) TraceEthTxMsg( traceConfig *evm.TraceConfig, commitMessage bool, tracerJSONConfig json.RawMessage, -) (*interface{}, uint, error) { +) (*any, uint, error) { // Assemble the structured logger or the JavaScript tracer var ( tracer tracers.Tracer @@ -805,7 +805,7 @@ func (k *Keeper) TraceEthTxMsg( return nil, 0, grpcstatus.Error(grpccodes.Internal, err.Error()) } - var result interface{} + var result any result, err = tracer.GetResult() if err != nil { return nil, 0, grpcstatus.Error(grpccodes.Internal, err.Error()) diff --git a/x/evm/params.go b/x/evm/params.go index 66d31e035..e0211d0bb 100644 --- a/x/evm/params.go +++ b/x/evm/params.go @@ -35,7 +35,7 @@ func DefaultParams() Params { } // validateChannels checks if channels ids are valid -func validateChannels(i interface{}) error { +func validateChannels(i any) error { channels, ok := i.([]string) if !ok { return fmt.Errorf("invalid parameter type: %T", i) @@ -76,7 +76,7 @@ func (p Params) IsEVMChannel(channel string) bool { return slices.Contains(p.EVMChannels, channel) } -func validateEIPs(i interface{}) error { +func validateEIPs(i any) error { eips, ok := i.([]int64) if !ok { return fmt.Errorf("invalid EIP slice type: %T", i) diff --git a/x/evm/precompile/.gitignore b/x/evm/precompile/.gitignore deleted file mode 100644 index e8c12ff4f..000000000 --- a/x/evm/precompile/.gitignore +++ /dev/null @@ -1,17 +0,0 @@ -node_modules -.env - -# Hardhat files -/cache -/artifacts - -# TypeChain files -/typechain -/typechain-types - -# solidity-coverage files -/coverage -/coverage.json - -# Hardhat Ignition default folder for deployments against a local node -ignition/deployments/chain-31337 diff --git a/x/evm/precompile/errors.go b/x/evm/precompile/errors.go index a95989b71..438ffa750 100644 --- a/x/evm/precompile/errors.go +++ b/x/evm/precompile/errors.go @@ -1,7 +1,6 @@ package precompile import ( - "errors" "fmt" "reflect" @@ -9,6 +8,13 @@ import ( "github.com/ethereum/go-ethereum/core/vm" ) +func assertNotReadonlyTx(readOnly bool, method *gethabi.Method) error { + if readOnly { + return fmt.Errorf("method %s cannot be called in a read-only context (e.g. staticcall)", method.Name) + } + return nil +} + // 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. @@ -34,14 +40,6 @@ func ErrMethodCalled(method *gethabi.Method, wrapped error) error { return fmt.Errorf("%s method called: %w", method.Name, wrapped) } -// Check required for transactions but not needed for queries -func assertNotReadonlyTx(readOnly bool, isTx bool) error { - if readOnly && isTx { - return errors.New("cannot write state from staticcall (a read-only call)") - } - return nil -} - // assertContractQuery checks if a contract call is a valid query operation. This // function verifies that no funds (wei) are being sent with the query, as query // operations should be read-only and not involve any value transfer. diff --git a/x/evm/precompile/funtoken.go b/x/evm/precompile/funtoken.go index e9ccaee82..1d50d99ab 100644 --- a/x/evm/precompile/funtoken.go +++ b/x/evm/precompile/funtoken.go @@ -3,7 +3,6 @@ package precompile import ( "fmt" "math/big" - "sync" "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" @@ -32,21 +31,19 @@ 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) { - return RequiredGas(input, p.ABI()) + return requiredGas(input, p.ABI()) +} + +func (p precompileFunToken) ABI() *gethabi.ABI { + return embeds.SmartContract_FunToken.ABI } const ( FunTokenMethod_BankSend PrecompileMethod = "bankSend" ) -type PrecompileMethod string - // Run runs the precompiled contract func (p precompileFunToken) Run( evm *vm.EVM, contract *vm.Contract, readonly bool, @@ -54,7 +51,7 @@ func (p precompileFunToken) Run( defer func() { err = ErrPrecompileRun(err, p) }() - start, err := OnRunStart(evm, contract, p.ABI()) + start, err := OnRunStart(evm, contract.Input, p.ABI()) if err != nil { return nil, err } @@ -87,8 +84,6 @@ type precompileFunToken struct { evmKeeper evmkeeper.Keeper } -var executionGuard sync.Mutex - // bankSend: Implements "IFunToken.bankSend" // // The "args" populate the following function signature in Solidity: @@ -105,15 +100,10 @@ func (p precompileFunToken) bankSend( caller gethcommon.Address, 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 - } - if !executionGuard.TryLock() { - return nil, fmt.Errorf("bankSend is already in progress") + ctx, method, args := start.CacheCtx, start.Method, start.Args + if err := assertNotReadonlyTx(readOnly, method); err != nil { + return nil, err } - defer executionGuard.Unlock() erc20, amount, to, err := p.decomposeBankSendArgs(args) if err != nil { diff --git a/x/evm/precompile/oracle.go b/x/evm/precompile/oracle.go index fb0b2981b..f59b93822 100644 --- a/x/evm/precompile/oracle.go +++ b/x/evm/precompile/oracle.go @@ -24,7 +24,11 @@ func (p precompileOracle) Address() gethcommon.Address { } func (p precompileOracle) RequiredGas(input []byte) (gasPrice uint64) { - return RequiredGas(input, embeds.SmartContract_Oracle.ABI) + return requiredGas(input, p.ABI()) +} + +func (p precompileOracle) ABI() *gethabi.ABI { + return embeds.SmartContract_Oracle.ABI } const ( @@ -38,21 +42,19 @@ func (p precompileOracle) Run( defer func() { err = ErrPrecompileRun(err, p) }() - res, err := OnRunStart(evm, contract, embeds.SmartContract_Oracle.ABI) + startResult, err := OnRunStart(evm, contract.Input, p.ABI()) if err != nil { return nil, err } - method, args, ctx := res.Method, res.Args, res.Ctx + method, args, ctx := startResult.Method, startResult.Args, startResult.CacheCtx switch PrecompileMethod(method.Name) { case OracleMethod_queryExchangeRate: - bz, err = p.queryExchangeRate(ctx, method, args, readonly) + return p.queryExchangeRate(ctx, method, args) default: err = fmt.Errorf("invalid method called with name \"%s\"", method.Name) return } - - return } func PrecompileOracle(keepers keepers.PublicKeepers) vm.PrecompiledContract { @@ -68,10 +70,9 @@ type precompileOracle struct { func (p precompileOracle) queryExchangeRate( ctx sdk.Context, method *gethabi.Method, - args []interface{}, - readOnly bool, + args []any, ) (bz []byte, err error) { - pair, err := p.decomposeQueryExchangeRateArgs(args) + pair, err := p.parseQueryExchangeRateArgs(args) if err != nil { return nil, err } @@ -88,7 +89,7 @@ func (p precompileOracle) queryExchangeRate( return method.Outputs.Pack(price.String()) } -func (p precompileOracle) decomposeQueryExchangeRateArgs(args []any) ( +func (p precompileOracle) parseQueryExchangeRateArgs(args []any) ( pair string, err error, ) { diff --git a/x/evm/precompile/precompile.go b/x/evm/precompile/precompile.go index 40d0c74b4..234a27f4b 100644 --- a/x/evm/precompile/precompile.go +++ b/x/evm/precompile/precompile.go @@ -85,15 +85,15 @@ func methodById(abi *gethabi.ABI, sigdata []byte) (*gethabi.Method, error) { return nil, fmt.Errorf("no method with id: %#x", sigdata[:4]) } -func DecomposeInput( +func decomposeInput( abi *gethabi.ABI, input []byte, -) (method *gethabi.Method, args []interface{}, err error) { +) (method *gethabi.Method, args []any, err error) { // ABI method IDs are exactly 4 bytes according to "gethabi.ABI.MethodByID". if len(input) < 4 { - readableBz := collections.HumanizeBytes(input) - err = fmt.Errorf("input \"%s\" too short to extract method ID (less than 4 bytes)", readableBz) + err = fmt.Errorf("input \"%s\" too short to extract method ID (less than 4 bytes)", collections.HumanizeBytes(input)) return } + method, err = methodById(abi, input[:4]) if err != nil { err = fmt.Errorf("unable to parse ABI method by its 4-byte ID: %w", err) @@ -109,8 +109,8 @@ func DecomposeInput( return method, args, nil } -func RequiredGas(input []byte, abi *gethabi.ABI) uint64 { - method, _, err := DecomposeInput(abi, input) +func requiredGas(input []byte, abi *gethabi.ABI) uint64 { + method, err := methodById(abi, input[:4]) if err != nil { // It's appropriate to return a reasonable default here // because the error from DecomposeInput will be handled automatically by @@ -122,26 +122,27 @@ func RequiredGas(input []byte, abi *gethabi.ABI) uint64 { // Map access could panic. We know that it won't panic because all methods // are in the map, which is verified by unit tests. - methodIsTx := precompileMethodIsTxMap[PrecompileMethod(method.Name)] var costPerByte, costFlat uint64 - if methodIsTx { + if isMutation[PrecompileMethod(method.Name)] { costPerByte, costFlat = gasCfg.WriteCostPerByte, gasCfg.WriteCostFlat } else { costPerByte, costFlat = gasCfg.ReadCostPerByte, gasCfg.ReadCostFlat } - argsBzLen := uint64(len(input[4:])) - return (costPerByte * argsBzLen) + costFlat + // Calculate the total gas required based on the input size and flat cost + return (costPerByte * uint64(len(input[4:]))) + costFlat } +type PrecompileMethod string + 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 + // CacheCtx 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 + CacheCtx sdk.Context // Method is the ABI method for the precompiled contract call. Method *gethabi.Method @@ -178,9 +179,9 @@ type OnRunStartResult struct { // } // ``` func OnRunStart( - evm *vm.EVM, contract *vm.Contract, abi *gethabi.ABI, + evm *vm.EVM, contractInput []byte, abi *gethabi.ABI, ) (res OnRunStartResult, err error) { - method, args, err := DecomposeInput(abi, contract.Input) + method, args, err := decomposeInput(abi, contractInput) if err != nil { return res, err } @@ -194,8 +195,8 @@ func OnRunStart( // journalEntry captures the state before precompile execution to enable // proper state reversal if the call fails or if [statedb.JournalChange] // is reverted in general. - cacheCtx, journalEntry := stateDB.CacheCtxForPrecompile(contract.Address()) - if err = stateDB.SavePrecompileCalledJournalChange(contract.Address(), journalEntry); err != nil { + cacheCtx, journalEntry := stateDB.CacheCtxForPrecompile() + if err = stateDB.SavePrecompileCalledJournalChange(journalEntry); err != nil { return res, err } if err = stateDB.CommitCacheCtx(); err != nil { @@ -203,14 +204,14 @@ func OnRunStart( } return OnRunStartResult{ - Args: args, - Ctx: cacheCtx, - Method: method, - StateDB: stateDB, + Args: args, + CacheCtx: cacheCtx, + Method: method, + StateDB: stateDB, }, nil } -var precompileMethodIsTxMap map[PrecompileMethod]bool = map[PrecompileMethod]bool{ +var isMutation map[PrecompileMethod]bool = map[PrecompileMethod]bool{ WasmMethod_execute: true, WasmMethod_instantiate: true, WasmMethod_executeMulti: true, diff --git a/x/evm/precompile/test/export.go b/x/evm/precompile/test/export.go index 28670e3e0..24700ce85 100644 --- a/x/evm/precompile/test/export.go +++ b/x/evm/precompile/test/export.go @@ -8,20 +8,17 @@ import ( "path" "strings" - serverconfig "github.com/NibiruChain/nibiru/v2/app/server/config" - wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" wasm "github.com/CosmWasm/wasmd/x/wasm/types" - "github.com/ethereum/go-ethereum/core/vm" - sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/core/vm" "github.com/stretchr/testify/suite" "github.com/NibiruChain/nibiru/v2/app" + serverconfig "github.com/NibiruChain/nibiru/v2/app/server/config" "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" @@ -31,68 +28,52 @@ func SetupWasmContracts(deps *evmtest.TestDeps, s *suite.Suite) ( ) { wasmCodes := DeployWasmBytecode(s, deps.Ctx, deps.Sender.NibiruAddr, deps.App) - otherArgs := []struct { - InstMsg []byte - Label string + instantiateArgs := []struct { + InstantiateMsg []byte + Label string }{ { - InstMsg: []byte("{}"), - Label: "https://github.com/NibiruChain/nibiru-wasm/blob/main/contracts/nibi-stargate/src/contract.rs", + InstantiateMsg: []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", + InstantiateMsg: []byte(`{"count": 0}`), + Label: "https://github.com/NibiruChain/nibiru-wasm/tree/ec3ab9f09587a11fbdfbd4021c7617eca3912044/contracts/00-hello-world-counter", }, } - for wasmCodeIdx, wasmCode := range wasmCodes { + for i, 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{}, + Label: instantiateArgs[i].Label, + Msg: instantiateArgs[i].InstantiateMsg, } 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} + callArgs := []any{m.Admin, m.CodeID, msgArgsBz, m.Label, []precompile.WasmBankCoin{}} input, err := embeds.SmartContract_Wasm.ABI.Pack( string(precompile.WasmMethod_instantiate), callArgs..., ) s.Require().NoError(err) - ethTxResp, evmObj, err := deps.EvmKeeper.CallContractWithInput( + ethTxResp, _, 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, - ) + vals, err := embeds.SmartContract_Wasm.ABI.Unpack(string(precompile.WasmMethod_instantiate), ethTxResp.Ret) s.Require().NoError(err) - contractAddr, err := sdk.AccAddressFromBech32(contractAddrStr) + + contractAddr, err := sdk.AccAddressFromBech32(vals[0].(string)) s.NoError(err) contracts = append(contracts, contractAddr) } @@ -118,7 +99,7 @@ func DeployWasmBytecode( rootPathBz, err := exec.Command("go", "list", "-m", "-f", "{{.Dir}}").Output() s.Require().NoError(err) rootPath := strings.Trim(string(rootPathBz), "\n") - for _, pathToWasmBin := range []string{ + for _, wasmFile := 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", @@ -129,11 +110,11 @@ func DeployWasmBytecode( // Add other wasm bytecode here if needed... } { - pathToWasmBin = path.Join(string(rootPath), pathToWasmBin) - wasmBytecode, err := os.ReadFile(pathToWasmBin) + binPath := path.Join(rootPath, wasmFile) + wasmBytecode, err := os.ReadFile(binPath) s.Require().NoErrorf( err, - "rootPath %s, pathToWasmBin %s", rootPath, pathToWasmBin, + "path %s, pathToWasmBin %s", binPath, ) // The "Create" fn is private on the nibiru.WasmKeeper. By placing it as the @@ -150,7 +131,7 @@ func DeployWasmBytecode( codeIds = append(codeIds, struct { codeId uint64 binPath string - }{codeId, pathToWasmBin}) + }{codeId, binPath}) } return codeIds diff --git a/x/evm/precompile/wasm.go b/x/evm/precompile/wasm.go index f19511cb9..cddfc488e 100644 --- a/x/evm/precompile/wasm.go +++ b/x/evm/precompile/wasm.go @@ -37,27 +37,26 @@ func (p precompileWasm) Run( defer func() { err = ErrPrecompileRun(err, p) }() - start, err := OnRunStart(evm, contract, p.ABI()) + startResult, err := OnRunStart(evm, contract.Input, p.ABI()) if err != nil { return nil, err } - method := start.Method - switch PrecompileMethod(method.Name) { + switch PrecompileMethod(startResult.Method.Name) { case WasmMethod_execute: - bz, err = p.execute(start, contract.CallerAddress, readonly) + bz, err = p.execute(startResult, contract.CallerAddress, readonly) case WasmMethod_query: - bz, err = p.query(start, contract) + bz, err = p.query(startResult, contract) case WasmMethod_instantiate: - bz, err = p.instantiate(start, contract.CallerAddress, readonly) + bz, err = p.instantiate(startResult, contract.CallerAddress, readonly) case WasmMethod_executeMulti: - bz, err = p.executeMulti(start, contract.CallerAddress, readonly) + bz, err = p.executeMulti(startResult, contract.CallerAddress, readonly) case WasmMethod_queryRaw: - bz, err = p.queryRaw(start, contract) + bz, err = p.queryRaw(startResult, 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) + err = fmt.Errorf("invalid method called with name \"%s\"", startResult.Method.Name) return } if err != nil { @@ -74,13 +73,13 @@ func (p precompileWasm) Address() gethcommon.Address { return PrecompileAddr_Wasm } -func (p precompileWasm) ABI() *gethabi.ABI { - return embeds.SmartContract_Wasm.ABI -} - // RequiredGas calculates the cost of calling the precompile in gas units. func (p precompileWasm) RequiredGas(input []byte) (gasCost uint64) { - return RequiredGas(input, p.ABI()) + return requiredGas(input, p.ABI()) +} + +func (p precompileWasm) ABI() *gethabi.ABI { + return embeds.SmartContract_Wasm.ABI } // Wasm: A struct embedding keepers for read and write operations in Wasm, such @@ -123,23 +122,22 @@ func (p precompileWasm) execute( caller gethcommon.Address, readOnly bool, ) (bz []byte, err error) { - method, args, ctx := start.Method, start.Args, start.Ctx + method, args, ctx := start.Method, start.Args, start.CacheCtx defer func() { if err != nil { err = ErrMethodCalled(method, err) } }() - - if err := assertNotReadonlyTx(readOnly, true); err != nil { - return bz, err + if err := assertNotReadonlyTx(readOnly, method); err != nil { + return nil, err } - wasmContract, msgArgs, funds, err := p.parseExecuteArgs(args) + + wasmContract, msgArgsBz, funds, err := p.parseExecuteArgs(args) if err != nil { err = ErrInvalidArgs(err) return } - callerBech32 := eth.EthAddrToNibiruAddr(caller) - data, err := p.Wasm.Execute(ctx, wasmContract, callerBech32, msgArgs, funds) + data, err := p.Wasm.Execute(ctx, wasmContract, eth.EthAddrToNibiruAddr(caller), msgArgsBz, funds) if err != nil { return } @@ -162,7 +160,7 @@ func (p precompileWasm) query( start OnRunStartResult, contract *vm.Contract, ) (bz []byte, err error) { - method, args, ctx := start.Method, start.Args, start.Ctx + method, args, ctx := start.Method, start.Args, start.CacheCtx defer func() { if err != nil { err = ErrMethodCalled(method, err) @@ -171,6 +169,7 @@ func (p precompileWasm) query( if err := assertContractQuery(contract); err != nil { return bz, err } + wasmContract, req, err := p.parseQueryArgs(args) if err != nil { err = ErrInvalidArgs(err) @@ -207,14 +206,14 @@ func (p precompileWasm) instantiate( caller gethcommon.Address, readOnly bool, ) (bz []byte, err error) { - method, args, ctx := start.Method, start.Args, start.Ctx + method, args, ctx := start.Method, start.Args, start.CacheCtx defer func() { if err != nil { err = ErrMethodCalled(method, err) } }() - if err := assertNotReadonlyTx(readOnly, true); err != nil { - return bz, err + if err := assertNotReadonlyTx(readOnly, method); err != nil { + return nil, err } callerBech32 := eth.EthAddrToNibiruAddr(caller) @@ -258,14 +257,14 @@ func (p precompileWasm) executeMulti( caller gethcommon.Address, readOnly bool, ) (bz []byte, err error) { - method, args, ctx := start.Method, start.Args, start.Ctx + method, args, ctx := start.Method, start.Args, start.CacheCtx defer func() { if err != nil { err = ErrMethodCalled(method, err) } }() - if err := assertNotReadonlyTx(readOnly, true); err != nil { - return bz, err + if err := assertNotReadonlyTx(readOnly, method); err != nil { + return nil, err } wasmExecMsgs, err := p.parseExecuteMultiArgs(args) @@ -327,7 +326,7 @@ func (p precompileWasm) queryRaw( start OnRunStartResult, contract *vm.Contract, ) (bz []byte, err error) { - method, args, ctx := start.Method, start.Args, start.Ctx + method, args, ctx := start.Method, start.Args, start.CacheCtx defer func() { if err != nil { err = ErrMethodCalled(method, err) diff --git a/x/evm/precompile/wasm_parse.go b/x/evm/precompile/wasm_parse.go index 80d950622..97a532753 100644 --- a/x/evm/precompile/wasm_parse.go +++ b/x/evm/precompile/wasm_parse.go @@ -16,51 +16,38 @@ type WasmBankCoin struct { Amount *big.Int `json:"amount"` } -func parseSdkCoins(unparsed []struct { - Denom string `json:"denom"` - Amount *big.Int `json:"amount"` -}, -) sdk.Coins { - parsed := sdk.Coins{} - for _, coin := range unparsed { - parsed = append( - parsed, - // Favor the sdk.Coin constructor over sdk.NewCoin because sdk.NewCoin - // is not panic-safe. Validation will be handled when the coin is used - // as an argument during the execution of a transaction. - sdk.Coin{ - Denom: coin.Denom, - Amount: sdk.NewIntFromBigInt(coin.Amount), - }, - ) - } - return parsed -} - // Parses [sdk.Coins] from a "BankCoin[]" solidity argument: // // ```solidity // BankCoin[] memory funds // ``` func parseFundsArg(arg any) (funds sdk.Coins, err error) { - bankCoinsUnparsed, ok := arg.([]struct { + if arg == nil { + return funds, nil + } + + raw, ok := arg.([]struct { Denom string `json:"denom"` Amount *big.Int `json:"amount"` }) - switch { - case arg == nil: - bankCoinsUnparsed = []struct { - Denom string `json:"denom"` - Amount *big.Int `json:"amount"` - }{} - case !ok: - err = ErrArgTypeValidation("BankCoin[] funds", arg) - return - case ok: - // Type assertion succeeded + + if !ok { + return funds, ErrArgTypeValidation("BankCoin[] funds", arg) } - funds = parseSdkCoins(bankCoinsUnparsed) - return + + for _, coin := range raw { + funds = append( + funds, + // Favor the sdk.Coin constructor over sdk.NewCoin because sdk.NewCoin + // is not panic-safe. Validation will be handled when the coin is used + // as an argument during the execution of a transaction. + sdk.Coin{ + Denom: coin.Denom, + Amount: sdk.NewIntFromBigInt(coin.Amount), + }, + ) + } + return funds, nil } // Parses [sdk.AccAddress] from a "string" solidity argument: @@ -148,6 +135,7 @@ func (p precompileWasm) parseExecuteArgs(args []any) ( return } + // contract address argIdx := 0 contractAddrStr, ok := args[argIdx].(string) if !ok { @@ -162,6 +150,7 @@ func (p precompileWasm) parseExecuteArgs(args []any) ( return } + // msg args argIdx++ msgArgs, ok = args[argIdx].([]byte) if !ok { @@ -174,6 +163,7 @@ func (p precompileWasm) parseExecuteArgs(args []any) ( return } + // funds argIdx++ funds, e := parseFundsArg(args[argIdx]) if e != nil { @@ -202,7 +192,11 @@ func (p precompileWasm) parseQueryArgs(args []any) ( } argsIdx++ - reqBz := args[argsIdx].([]byte) + reqBz, ok := args[argsIdx].([]byte) + if !ok { + err = ErrArgTypeValidation("bytes req", args[argsIdx]) + return + } req = wasm.RawContractMessage(reqBz) if e := req.ValidateBasic(); e != nil { err = e diff --git a/x/evm/statedb/journal_test.go b/x/evm/statedb/journal_test.go index 6390b640f..c1c74110c 100644 --- a/x/evm/statedb/journal_test.go +++ b/x/evm/statedb/journal_test.go @@ -31,13 +31,12 @@ func (s *Suite) TestComplexJournalChanges() { )) s.T().Log("Set up helloworldcounter.wasm") - - wasmContract := test.SetupWasmContracts(&deps, &s.Suite)[1] - fmt.Printf("wasmContract: %s\n", wasmContract) + helloWorldCounterWasm := test.SetupWasmContracts(&deps, &s.Suite)[1] + fmt.Printf("wasmContract: %s\n", helloWorldCounterWasm) s.T().Log("Assert before transition") test.AssertWasmCounterState( - &s.Suite, deps, wasmContract, 0, + &s.Suite, deps, helloWorldCounterWasm, 0, ) deployArgs := []any{"name", "SYMBOL", uint8(18)} @@ -129,11 +128,11 @@ func (s *Suite) TestComplexJournalChanges() { s.T().Log("commitEvmTx=true, expect 0 dirty journal entries") commitEvmTx := true evmObj = test.IncrementWasmCounterWithExecuteMulti( - &s.Suite, &deps, wasmContract, 7, commitEvmTx, + &s.Suite, &deps, helloWorldCounterWasm, 7, commitEvmTx, ) // assertions after run test.AssertWasmCounterState( - &s.Suite, deps, wasmContract, 7, + &s.Suite, deps, helloWorldCounterWasm, 7, ) stateDB, ok := evmObj.StateDB.(*statedb.StateDB) s.Require().True(ok, "error retrieving StateDB from the EVM") @@ -145,7 +144,7 @@ func (s *Suite) TestComplexJournalChanges() { s.T().Log("commitEvmTx=false, expect dirty journal entries") commitEvmTx = false evmObj = test.IncrementWasmCounterWithExecuteMulti( - &s.Suite, &deps, wasmContract, 5, commitEvmTx, + &s.Suite, &deps, helloWorldCounterWasm, 5, commitEvmTx, ) stateDB, ok = evmObj.StateDB.(*statedb.StateDB) s.Require().True(ok, "error retrieving StateDB from the EVM") @@ -158,7 +157,7 @@ func (s *Suite) TestComplexJournalChanges() { s.T().Log("Expect no change since the StateDB has not been committed") test.AssertWasmCounterState( - &s.Suite, deps, wasmContract, 7, // 7 = 7 + 0 + &s.Suite, deps, helloWorldCounterWasm, 7, // 7 = 7 + 0 ) s.T().Log("Expect change to persist on the StateDB cacheCtx") @@ -166,14 +165,14 @@ func (s *Suite) TestComplexJournalChanges() { s.NotNil(cacheCtx) deps.Ctx = *cacheCtx test.AssertWasmCounterState( - &s.Suite, deps, wasmContract, 12, // 12 = 7 + 5 + &s.Suite, deps, helloWorldCounterWasm, 12, // 12 = 7 + 5 ) // NOTE: that the [StateDB.Commit] fn has not been called yet. We're still // mid-transaction. s.T().Log("EVM revert operation should bring about the old state") err = test.IncrementWasmCounterWithExecuteMultiViaVMCall( - &s.Suite, &deps, wasmContract, 50, commitEvmTx, evmObj, + &s.Suite, &deps, helloWorldCounterWasm, 50, commitEvmTx, evmObj, ) stateDBPtr := evmObj.StateDB.(*statedb.StateDB) s.Require().Equal(stateDB, stateDBPtr) @@ -185,7 +184,7 @@ snapshots and see the prior states.`)) cacheCtx = stateDB.GetCacheContext() deps.Ctx = *cacheCtx test.AssertWasmCounterState( - &s.Suite, deps, wasmContract, 7+5+50, + &s.Suite, deps, helloWorldCounterWasm, 7+5+50, ) errFn := common.TryCatch(func() { @@ -201,7 +200,7 @@ snapshots and see the prior states.`)) s.NotNil(cacheCtx) deps.Ctx = *cacheCtx test.AssertWasmCounterState( - &s.Suite, deps, wasmContract, 7+5, + &s.Suite, deps, helloWorldCounterWasm, 7+5, ) stateDB.RevertToSnapshot(0) @@ -209,13 +208,13 @@ snapshots and see the prior states.`)) s.NotNil(cacheCtx) deps.Ctx = *cacheCtx test.AssertWasmCounterState( - &s.Suite, deps, wasmContract, 7, // state before precompile called + &s.Suite, deps, helloWorldCounterWasm, 7, // state before precompile called ) err = stateDB.Commit() deps.Ctx = stateDB.GetEvmTxContext() test.AssertWasmCounterState( - &s.Suite, deps, wasmContract, 7, // state before precompile called + &s.Suite, deps, helloWorldCounterWasm, 7, // state before precompile called ) }) } diff --git a/x/evm/statedb/statedb.go b/x/evm/statedb/statedb.go index a411660f9..4e93b127a 100644 --- a/x/evm/statedb/statedb.go +++ b/x/evm/statedb/statedb.go @@ -569,7 +569,7 @@ func (s *StateDB) commitCtx(ctx sdk.Context) error { return nil } -func (s *StateDB) CacheCtxForPrecompile(precompileAddr common.Address) ( +func (s *StateDB) CacheCtxForPrecompile() ( sdk.Context, PrecompileCalled, ) { if s.writeToCommitCtxFromCacheCtx == nil { @@ -590,15 +590,14 @@ func (s *StateDB) CacheCtxForPrecompile(precompileAddr common.Address) ( // // See [PrecompileCalled] for more info. func (s *StateDB) SavePrecompileCalledJournalChange( - precompileAddr common.Address, journalChange PrecompileCalled, ) error { s.Journal.append(journalChange) s.multistoreCacheCount++ if s.multistoreCacheCount > maxMultistoreCacheCount { return fmt.Errorf( - "exceeded maximum number Nibiru-specific precompiled contract calls in one transaction (%d). Called address %s", - maxMultistoreCacheCount, precompileAddr.Hex(), + "exceeded maximum number Nibiru-specific precompiled contract calls in one transaction (%d).", + maxMultistoreCacheCount, ) } return nil diff --git a/x/evm/vmtracer.go b/x/evm/vmtracer.go index 2a20f2b01..2078bdfe7 100644 --- a/x/evm/vmtracer.go +++ b/x/evm/vmtracer.go @@ -51,8 +51,8 @@ func NewTracer(tracer string, msg core.Message, cfg *params.ChainConfig, height // TxTraceResult is the result of a single transaction trace during a block trace. type TxTraceResult struct { - Result interface{} `json:"result,omitempty"` // Trace results produced by the tracer - Error string `json:"error,omitempty"` // Trace failure produced by the tracer + Result any `json:"result,omitempty"` // Trace results produced by the tracer + Error string `json:"error,omitempty"` // Trace failure produced by the tracer } var _ vm.EVMLogger = &NoOpTracer{} diff --git a/x/inflation/types/params.go b/x/inflation/types/params.go index 1ca46623b..6b95bbb7c 100644 --- a/x/inflation/types/params.go +++ b/x/inflation/types/params.go @@ -71,7 +71,7 @@ func DefaultParams() Params { } } -func validatePolynomialFactors(i interface{}) error { +func validatePolynomialFactors(i any) error { v, ok := i.([]sdk.Dec) if !ok { return fmt.Errorf("invalid parameter type: %T", i) @@ -83,7 +83,7 @@ func validatePolynomialFactors(i interface{}) error { return nil } -func validateInflationDistribution(i interface{}) error { +func validateInflationDistribution(i any) error { v, ok := i.(InflationDistribution) if !ok { return fmt.Errorf("invalid parameter type: %T", i) @@ -109,7 +109,7 @@ func validateInflationDistribution(i interface{}) error { return nil } -func validateBool(i interface{}) error { +func validateBool(i any) error { _, ok := i.(bool) if !ok { return fmt.Errorf("invalid parameter type: %T", i) @@ -118,7 +118,7 @@ func validateBool(i interface{}) error { return nil } -func validateUint64(i interface{}) error { +func validateUint64(i any) error { _, ok := i.(uint64) if !ok { return fmt.Errorf("invalid genesis state type: %T", i) @@ -126,7 +126,7 @@ func validateUint64(i interface{}) error { return nil } -func validateEpochsPerPeriod(i interface{}) error { +func validateEpochsPerPeriod(i any) error { val, ok := i.(uint64) if !ok { return fmt.Errorf("invalid parameter type: %T", i) @@ -139,7 +139,7 @@ func validateEpochsPerPeriod(i interface{}) error { return nil } -func validatePeriodsPerYear(i interface{}) error { +func validatePeriodsPerYear(i any) error { val, ok := i.(uint64) if !ok { return fmt.Errorf("invalid parameter type: %T", i) diff --git a/x/oracle/types/hash.go b/x/oracle/types/hash.go index b9d816ecc..a828a6414 100644 --- a/x/oracle/types/hash.go +++ b/x/oracle/types/hash.go @@ -99,7 +99,7 @@ func (h AggregateVoteHash) MarshalJSON() ([]byte, error) { } // MarshalYAML marshals to YAML using Bech32. -func (h AggregateVoteHash) MarshalYAML() (interface{}, error) { +func (h AggregateVoteHash) MarshalYAML() (any, error) { return h.String(), nil } diff --git a/x/tokenfactory/types/codec.go b/x/tokenfactory/types/codec.go index e0e639c0d..c20e55ad4 100644 --- a/x/tokenfactory/types/codec.go +++ b/x/tokenfactory/types/codec.go @@ -58,7 +58,7 @@ func TX_MSG_TYPE_URLS() []string { // Amino JSON serialization and EIP-712 compatibility. func RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) { for _, ele := range []struct { - MsgType interface{} + MsgType any Name string }{ {&MsgCreateDenom{}, "nibiru/tokenfactory/create-denom"},