From 16393ca8bfc0fcc84eaeb6ed50678f99c5283eb9 Mon Sep 17 00:00:00 2001 From: Oleg Nikonychev Date: Wed, 30 Oct 2024 00:59:58 +0400 Subject: [PATCH] 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")