diff --git a/CHANGELOG.md b/CHANGELOG.md index aec8bba4e..626db49c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -87,6 +87,7 @@ needed to include double quotes around the hexadecimal string. - [#2172](https://github.com/NibiruChain/nibiru/pull/2172) - chore: close iterator in IterateEpochInfo - [#2173](https://github.com/NibiruChain/nibiru/pull/2173) - fix(evm): clear `StateDB` between calls - [#2177](https://github.com/NibiruChain/nibiru/pull/2177) - fix(cmd): Continue from #2127 and unwire vesting flags and logic from genaccounts.go +- [#2176](https://github.com/NibiruChain/nibiru/pull/2176) - tests(evm): add dirty state tests from code4rena audit #### Nibiru EVM | Before Audit 2 - 2024-12-06 diff --git a/x/common/testutil/testapp/test_util.go b/x/common/testutil/testapp/test_util.go index c4eecceea..d94318da1 100644 --- a/x/common/testutil/testapp/test_util.go +++ b/x/common/testutil/testapp/test_util.go @@ -16,6 +16,7 @@ import ( stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" nibiruapp "github.com/NibiruChain/nibiru/v2/app" + "github.com/NibiruChain/nibiru/v2/app/appconst" ) // GenesisStateWithSingleValidator initializes GenesisState with a single validator and genesis accounts @@ -41,7 +42,7 @@ func GenesisStateWithSingleValidator(codec codec.Codec, genesisState nibiruapp.G balances = append(balances, banktypes.Balance{ Address: acc.GetAddress().String(), - Coins: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, math.NewInt(100000000000000))), + Coins: sdk.NewCoins(sdk.NewCoin(appconst.BondDenom, math.NewIntFromUint64(1e14))), }) genesisState, err = genesisStateWithValSet(codec, genesisState, valSet, []authtypes.GenesisAccount{acc}, balances...) @@ -93,7 +94,9 @@ func genesisStateWithValSet( delegations = append(delegations, stakingtypes.NewDelegation(genAccs[0].GetAddress(), val.Address.Bytes(), math.LegacyOneDec())) } // set validators and delegations - stakingGenesis := stakingtypes.NewGenesisState(stakingtypes.DefaultParams(), validators, delegations) + stakingParams := stakingtypes.DefaultParams() + stakingParams.BondDenom = appconst.BondDenom + stakingGenesis := stakingtypes.NewGenesisState(stakingParams, validators, delegations) genesisState[stakingtypes.ModuleName] = cdc.MustMarshalJSON(stakingGenesis) totalSupply := sdk.NewCoins() @@ -104,13 +107,13 @@ func genesisStateWithValSet( for range delegations { // add delegated tokens to total supply - totalSupply = totalSupply.Add(sdk.NewCoin(sdk.DefaultBondDenom, bondAmt)) + totalSupply = totalSupply.Add(sdk.NewCoin(appconst.BondDenom, bondAmt)) } // add bonded amount to bonded pool module account balances = append(balances, banktypes.Balance{ Address: authtypes.NewModuleAddress(stakingtypes.BondedPoolName).String(), - Coins: sdk.Coins{sdk.NewCoin(sdk.DefaultBondDenom, bondAmt)}, + Coins: sdk.Coins{sdk.NewCoin(appconst.BondDenom, bondAmt)}, }) // update total supply diff --git a/x/common/testutil/testapp/testapp.go b/x/common/testutil/testapp/testapp.go index d8507b099..24f39951c 100644 --- a/x/common/testutil/testapp/testapp.go +++ b/x/common/testutil/testapp/testapp.go @@ -17,6 +17,7 @@ import ( "github.com/NibiruChain/nibiru/v2/app" "github.com/NibiruChain/nibiru/v2/app/appconst" + cryptocodec "github.com/NibiruChain/nibiru/v2/eth/crypto/codec" "github.com/NibiruChain/nibiru/v2/x/common/asset" "github.com/NibiruChain/nibiru/v2/x/common/denoms" "github.com/NibiruChain/nibiru/v2/x/common/testutil" @@ -126,6 +127,7 @@ func NewNibiruTestApp(gen app.GenesisState, baseAppOptions ...func(*baseapp.Base logger := log.NewNopLogger() encoding := app.MakeEncodingConfig() + cryptocodec.RegisterInterfaces(encoding.InterfaceRegistry) SetDefaultSudoGenesis(gen) app := app.NewNibiruApp( diff --git a/x/evm/embeds/artifacts/contracts/TestDirtyStateAttack4.sol/TestDirtyStateAttack4.json b/x/evm/embeds/artifacts/contracts/TestDirtyStateAttack4.sol/TestDirtyStateAttack4.json new file mode 100644 index 000000000..3907e082f --- /dev/null +++ b/x/evm/embeds/artifacts/contracts/TestDirtyStateAttack4.sol/TestDirtyStateAttack4.json @@ -0,0 +1,47 @@ +{ + "_format": "hh-sol-artifact-1", + "contractName": "TestDirtyStateAttack4", + "sourceName": "contracts/TestDirtyStateAttack4.sol", + "abi": [ + { + "inputs": [], + "stateMutability": "payable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "wasmAddr", + "type": "string" + }, + { + "internalType": "bytes", + "name": "msgArgs", + "type": "bytes" + } + ], + "name": "attack", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "getCounter", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } + ], + "bytecode": "0x60806040526000805561084a806100176000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c806333c889971461003b5780638ada066e14610057575b600080fd5b610055600480360381019061005091906102d6565b610075565b005b61005f6101e4565b60405161006c9190610370565b60405180910390f35b600080815480929190610087906103ba565b91905055506000600167ffffffffffffffff8111156100a9576100a8610402565b5b6040519080825280602002602001820160405280156100e257816020015b6100cf6101ed565b8152602001906001900390816100c75790505b50905060405180604001604052806040518060400160405280600581526020017f756e6962690000000000000000000000000000000000000000000000000000008152508152602001620f42408152508160008151811061014657610145610431565b5b602002602001018190525061080273ffffffffffffffffffffffffffffffffffffffff166361ffaee486868686866040518663ffffffff1660e01b8152600401610194959493929190610689565b6000604051808303816000875af11580156101b3573d6000803e3d6000fd5b505050506040513d6000823e3d601f19601f820116820180604052508101906101dc91906107cb565b505050505050565b60008054905090565b604051806040016040528060608152602001600081525090565b6000604051905090565b600080fd5b600080fd5b600080fd5b600080fd5b600080fd5b60008083601f8401126102405761023f61021b565b5b8235905067ffffffffffffffff81111561025d5761025c610220565b5b60208301915083600182028301111561027957610278610225565b5b9250929050565b60008083601f8401126102965761029561021b565b5b8235905067ffffffffffffffff8111156102b3576102b2610220565b5b6020830191508360018202830111156102cf576102ce610225565b5b9250929050565b600080600080604085870312156102f0576102ef610211565b5b600085013567ffffffffffffffff81111561030e5761030d610216565b5b61031a8782880161022a565b9450945050602085013567ffffffffffffffff81111561033d5761033c610216565b5b61034987828801610280565b925092505092959194509250565b6000819050919050565b61036a81610357565b82525050565b60006020820190506103856000830184610361565b92915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b60006103c582610357565b91507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff82036103f7576103f661038b565b5b600182019050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b600082825260208201905092915050565b82818337600083830152505050565b6000601f19601f8301169050919050565b600061049d8385610460565b93506104aa838584610471565b6104b383610480565b840190509392505050565b600082825260208201905092915050565b60006104db83856104be565b93506104e8838584610471565b6104f183610480565b840190509392505050565b600081519050919050565b600082825260208201905092915050565b6000819050602082019050919050565b600081519050919050565b600082825260208201905092915050565b60005b83811015610562578082015181840152602081019050610547565b60008484015250505050565b600061057982610528565b6105838185610533565b9350610593818560208601610544565b61059c81610480565b840191505092915050565b6105b081610357565b82525050565b600060408301600083015184820360008601526105d3828261056e565b91505060208301516105e860208601826105a7565b508091505092915050565b60006105ff83836105b6565b905092915050565b6000602082019050919050565b600061061f826104fc565b6106298185610507565b93508360208202850161063b85610518565b8060005b85811015610677578484038952815161065885826105f3565b945061066383610607565b925060208a0199505060018101905061063f565b50829750879550505050505092915050565b600060608201905081810360008301526106a4818789610491565b905081810360208301526106b98185876104cf565b905081810360408301526106cd8184610614565b90509695505050505050565b600080fd5b6106e782610480565b810181811067ffffffffffffffff8211171561070657610705610402565b5b80604052505050565b6000610719610207565b905061072582826106de565b919050565b600067ffffffffffffffff82111561074557610744610402565b5b61074e82610480565b9050602081019050919050565b600061076e6107698461072a565b61070f565b90508281526020810184848401111561078a576107896106d9565b5b610795848285610544565b509392505050565b600082601f8301126107b2576107b161021b565b5b81516107c284826020860161075b565b91505092915050565b6000602082840312156107e1576107e0610211565b5b600082015167ffffffffffffffff8111156107ff576107fe610216565b5b61080b8482850161079d565b9150509291505056fea26469706673582212208caac82ecde5bb31fb5b34bd6b569b73277f17eb9b132d65942a718f83178c9c64736f6c63430008180033", + "deployedBytecode": "0x608060405234801561001057600080fd5b50600436106100365760003560e01c806333c889971461003b5780638ada066e14610057575b600080fd5b610055600480360381019061005091906102d6565b610075565b005b61005f6101e4565b60405161006c9190610370565b60405180910390f35b600080815480929190610087906103ba565b91905055506000600167ffffffffffffffff8111156100a9576100a8610402565b5b6040519080825280602002602001820160405280156100e257816020015b6100cf6101ed565b8152602001906001900390816100c75790505b50905060405180604001604052806040518060400160405280600581526020017f756e6962690000000000000000000000000000000000000000000000000000008152508152602001620f42408152508160008151811061014657610145610431565b5b602002602001018190525061080273ffffffffffffffffffffffffffffffffffffffff166361ffaee486868686866040518663ffffffff1660e01b8152600401610194959493929190610689565b6000604051808303816000875af11580156101b3573d6000803e3d6000fd5b505050506040513d6000823e3d601f19601f820116820180604052508101906101dc91906107cb565b505050505050565b60008054905090565b604051806040016040528060608152602001600081525090565b6000604051905090565b600080fd5b600080fd5b600080fd5b600080fd5b600080fd5b60008083601f8401126102405761023f61021b565b5b8235905067ffffffffffffffff81111561025d5761025c610220565b5b60208301915083600182028301111561027957610278610225565b5b9250929050565b60008083601f8401126102965761029561021b565b5b8235905067ffffffffffffffff8111156102b3576102b2610220565b5b6020830191508360018202830111156102cf576102ce610225565b5b9250929050565b600080600080604085870312156102f0576102ef610211565b5b600085013567ffffffffffffffff81111561030e5761030d610216565b5b61031a8782880161022a565b9450945050602085013567ffffffffffffffff81111561033d5761033c610216565b5b61034987828801610280565b925092505092959194509250565b6000819050919050565b61036a81610357565b82525050565b60006020820190506103856000830184610361565b92915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b60006103c582610357565b91507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff82036103f7576103f661038b565b5b600182019050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b600082825260208201905092915050565b82818337600083830152505050565b6000601f19601f8301169050919050565b600061049d8385610460565b93506104aa838584610471565b6104b383610480565b840190509392505050565b600082825260208201905092915050565b60006104db83856104be565b93506104e8838584610471565b6104f183610480565b840190509392505050565b600081519050919050565b600082825260208201905092915050565b6000819050602082019050919050565b600081519050919050565b600082825260208201905092915050565b60005b83811015610562578082015181840152602081019050610547565b60008484015250505050565b600061057982610528565b6105838185610533565b9350610593818560208601610544565b61059c81610480565b840191505092915050565b6105b081610357565b82525050565b600060408301600083015184820360008601526105d3828261056e565b91505060208301516105e860208601826105a7565b508091505092915050565b60006105ff83836105b6565b905092915050565b6000602082019050919050565b600061061f826104fc565b6106298185610507565b93508360208202850161063b85610518565b8060005b85811015610677578484038952815161065885826105f3565b945061066383610607565b925060208a0199505060018101905061063f565b50829750879550505050505092915050565b600060608201905081810360008301526106a4818789610491565b905081810360208301526106b98185876104cf565b905081810360408301526106cd8184610614565b90509695505050505050565b600080fd5b6106e782610480565b810181811067ffffffffffffffff8211171561070657610705610402565b5b80604052505050565b6000610719610207565b905061072582826106de565b919050565b600067ffffffffffffffff82111561074557610744610402565b5b61074e82610480565b9050602081019050919050565b600061076e6107698461072a565b61070f565b90508281526020810184848401111561078a576107896106d9565b5b610795848285610544565b509392505050565b600082601f8301126107b2576107b161021b565b5b81516107c284826020860161075b565b91505092915050565b6000602082840312156107e1576107e0610211565b5b600082015167ffffffffffffffff8111156107ff576107fe610216565b5b61080b8482850161079d565b9150509291505056fea26469706673582212208caac82ecde5bb31fb5b34bd6b569b73277f17eb9b132d65942a718f83178c9c64736f6c63430008180033", + "linkReferences": {}, + "deployedLinkReferences": {} +} diff --git a/x/evm/embeds/artifacts/contracts/TestDirtyStateAttack5.sol/TestDirtyStateAttack5.json b/x/evm/embeds/artifacts/contracts/TestDirtyStateAttack5.sol/TestDirtyStateAttack5.json new file mode 100644 index 000000000..80d00a824 --- /dev/null +++ b/x/evm/embeds/artifacts/contracts/TestDirtyStateAttack5.sol/TestDirtyStateAttack5.json @@ -0,0 +1,34 @@ +{ + "_format": "hh-sol-artifact-1", + "contractName": "TestDirtyStateAttack5", + "sourceName": "contracts/TestDirtyStateAttack5.sol", + "abi": [ + { + "inputs": [], + "stateMutability": "payable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "wasmAddr", + "type": "string" + }, + { + "internalType": "bytes", + "name": "msgArgs", + "type": "bytes" + } + ], + "name": "attack", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } + ], + "bytecode": "0x6080604052610760806100136000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c806333c8899714610030575b600080fd5b61004a6004803603810190610045919061028d565b61004c565b005b6000600167ffffffffffffffff8111156100695761006861030e565b5b6040519080825280602002602001820160405280156100a257816020015b61008f6101a4565b8152602001906001900390816100875790505b50905060405180604001604052806040518060400160405280600581526020017f756e6962690000000000000000000000000000000000000000000000000000008152508152602001624c4b40815250816000815181106101065761010561033d565b5b602002602001018190525061080273ffffffffffffffffffffffffffffffffffffffff166361ffaee486868686866040518663ffffffff1660e01b815260040161015495949392919061059f565b6000604051808303816000875af1158015610173573d6000803e3d6000fd5b505050506040513d6000823e3d601f19601f8201168201806040525081019061019c91906106e1565b505050505050565b604051806040016040528060608152602001600081525090565b6000604051905090565b600080fd5b600080fd5b600080fd5b600080fd5b600080fd5b60008083601f8401126101f7576101f66101d2565b5b8235905067ffffffffffffffff811115610214576102136101d7565b5b6020830191508360018202830111156102305761022f6101dc565b5b9250929050565b60008083601f84011261024d5761024c6101d2565b5b8235905067ffffffffffffffff81111561026a576102696101d7565b5b602083019150836001820283011115610286576102856101dc565b5b9250929050565b600080600080604085870312156102a7576102a66101c8565b5b600085013567ffffffffffffffff8111156102c5576102c46101cd565b5b6102d1878288016101e1565b9450945050602085013567ffffffffffffffff8111156102f4576102f36101cd565b5b61030087828801610237565b925092505092959194509250565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b600082825260208201905092915050565b82818337600083830152505050565b6000601f19601f8301169050919050565b60006103a9838561036c565b93506103b683858461037d565b6103bf8361038c565b840190509392505050565b600082825260208201905092915050565b60006103e783856103ca565b93506103f483858461037d565b6103fd8361038c565b840190509392505050565b600081519050919050565b600082825260208201905092915050565b6000819050602082019050919050565b600081519050919050565b600082825260208201905092915050565b60005b8381101561046e578082015181840152602081019050610453565b60008484015250505050565b600061048582610434565b61048f818561043f565b935061049f818560208601610450565b6104a88161038c565b840191505092915050565b6000819050919050565b6104c6816104b3565b82525050565b600060408301600083015184820360008601526104e9828261047a565b91505060208301516104fe60208601826104bd565b508091505092915050565b600061051583836104cc565b905092915050565b6000602082019050919050565b600061053582610408565b61053f8185610413565b93508360208202850161055185610424565b8060005b8581101561058d578484038952815161056e8582610509565b94506105798361051d565b925060208a01995050600181019050610555565b50829750879550505050505092915050565b600060608201905081810360008301526105ba81878961039d565b905081810360208301526105cf8185876103db565b905081810360408301526105e3818461052a565b90509695505050505050565b600080fd5b6105fd8261038c565b810181811067ffffffffffffffff8211171561061c5761061b61030e565b5b80604052505050565b600061062f6101be565b905061063b82826105f4565b919050565b600067ffffffffffffffff82111561065b5761065a61030e565b5b6106648261038c565b9050602081019050919050565b600061068461067f84610640565b610625565b9050828152602081018484840111156106a05761069f6105ef565b5b6106ab848285610450565b509392505050565b600082601f8301126106c8576106c76101d2565b5b81516106d8848260208601610671565b91505092915050565b6000602082840312156106f7576106f66101c8565b5b600082015167ffffffffffffffff811115610715576107146101cd565b5b610721848285016106b3565b9150509291505056fea2646970667358221220035e628d5c38012c486ab00ce495fa418401de59d5a1cbfe1e48245374f7d1be64736f6c63430008180033", + "deployedBytecode": "0x608060405234801561001057600080fd5b506004361061002b5760003560e01c806333c8899714610030575b600080fd5b61004a6004803603810190610045919061028d565b61004c565b005b6000600167ffffffffffffffff8111156100695761006861030e565b5b6040519080825280602002602001820160405280156100a257816020015b61008f6101a4565b8152602001906001900390816100875790505b50905060405180604001604052806040518060400160405280600581526020017f756e6962690000000000000000000000000000000000000000000000000000008152508152602001624c4b40815250816000815181106101065761010561033d565b5b602002602001018190525061080273ffffffffffffffffffffffffffffffffffffffff166361ffaee486868686866040518663ffffffff1660e01b815260040161015495949392919061059f565b6000604051808303816000875af1158015610173573d6000803e3d6000fd5b505050506040513d6000823e3d601f19601f8201168201806040525081019061019c91906106e1565b505050505050565b604051806040016040528060608152602001600081525090565b6000604051905090565b600080fd5b600080fd5b600080fd5b600080fd5b600080fd5b60008083601f8401126101f7576101f66101d2565b5b8235905067ffffffffffffffff811115610214576102136101d7565b5b6020830191508360018202830111156102305761022f6101dc565b5b9250929050565b60008083601f84011261024d5761024c6101d2565b5b8235905067ffffffffffffffff81111561026a576102696101d7565b5b602083019150836001820283011115610286576102856101dc565b5b9250929050565b600080600080604085870312156102a7576102a66101c8565b5b600085013567ffffffffffffffff8111156102c5576102c46101cd565b5b6102d1878288016101e1565b9450945050602085013567ffffffffffffffff8111156102f4576102f36101cd565b5b61030087828801610237565b925092505092959194509250565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b600082825260208201905092915050565b82818337600083830152505050565b6000601f19601f8301169050919050565b60006103a9838561036c565b93506103b683858461037d565b6103bf8361038c565b840190509392505050565b600082825260208201905092915050565b60006103e783856103ca565b93506103f483858461037d565b6103fd8361038c565b840190509392505050565b600081519050919050565b600082825260208201905092915050565b6000819050602082019050919050565b600081519050919050565b600082825260208201905092915050565b60005b8381101561046e578082015181840152602081019050610453565b60008484015250505050565b600061048582610434565b61048f818561043f565b935061049f818560208601610450565b6104a88161038c565b840191505092915050565b6000819050919050565b6104c6816104b3565b82525050565b600060408301600083015184820360008601526104e9828261047a565b91505060208301516104fe60208601826104bd565b508091505092915050565b600061051583836104cc565b905092915050565b6000602082019050919050565b600061053582610408565b61053f8185610413565b93508360208202850161055185610424565b8060005b8581101561058d578484038952815161056e8582610509565b94506105798361051d565b925060208a01995050600181019050610555565b50829750879550505050505092915050565b600060608201905081810360008301526105ba81878961039d565b905081810360208301526105cf8185876103db565b905081810360408301526105e3818461052a565b90509695505050505050565b600080fd5b6105fd8261038c565b810181811067ffffffffffffffff8211171561061c5761061b61030e565b5b80604052505050565b600061062f6101be565b905061063b82826105f4565b919050565b600067ffffffffffffffff82111561065b5761065a61030e565b5b6106648261038c565b9050602081019050919050565b600061068461067f84610640565b610625565b9050828152602081018484840111156106a05761069f6105ef565b5b6106ab848285610450565b509392505050565b600082601f8301126106c8576106c76101d2565b5b81516106d8848260208601610671565b91505092915050565b6000602082840312156106f7576106f66101c8565b5b600082015167ffffffffffffffff811115610715576107146101cd565b5b610721848285016106b3565b9150509291505056fea2646970667358221220035e628d5c38012c486ab00ce495fa418401de59d5a1cbfe1e48245374f7d1be64736f6c63430008180033", + "linkReferences": {}, + "deployedLinkReferences": {} +} diff --git a/x/evm/embeds/contracts/TestDirtyStateAttack4.sol b/x/evm/embeds/contracts/TestDirtyStateAttack4.sol new file mode 100644 index 000000000..c9dc1d1df --- /dev/null +++ b/x/evm/embeds/contracts/TestDirtyStateAttack4.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +// Uncomment this line to use console.log +// import "hardhat/console.sol"; +import "./Wasm.sol"; +import "./NibiruEvmUtils.sol"; +import "@openzeppelin/contracts/utils/Strings.sol"; +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract TestDirtyStateAttack4 { + uint counter = 0; + + constructor() payable {} + + function attack(string calldata wasmAddr, bytes calldata msgArgs) external { + counter++; + + INibiruEvm.BankCoin[] memory funds = new INibiruEvm.BankCoin[](1); + funds[0] = INibiruEvm.BankCoin({denom: "unibi", amount: 1e6}); // 1 NIBI + + WASM_PRECOMPILE.execute(wasmAddr, msgArgs, funds); + } + + function getCounter() external view returns (uint) { + return counter; + } +} diff --git a/x/evm/embeds/contracts/TestDirtyStateAttack5.sol b/x/evm/embeds/contracts/TestDirtyStateAttack5.sol new file mode 100644 index 000000000..443738520 --- /dev/null +++ b/x/evm/embeds/contracts/TestDirtyStateAttack5.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +// Uncomment this line to use console.log +// import "hardhat/console.sol"; +import "./Wasm.sol"; +import "./NibiruEvmUtils.sol"; +import "@openzeppelin/contracts/utils/Strings.sol"; +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract TestDirtyStateAttack5 { + constructor() payable {} + + function attack(string calldata wasmAddr, bytes calldata msgArgs) external { + INibiruEvm.BankCoin[] memory funds = new INibiruEvm.BankCoin[](1); + funds[0] = INibiruEvm.BankCoin({denom: "unibi", amount: 5e6}); // 5 NIBI + + WASM_PRECOMPILE.execute(wasmAddr, msgArgs, funds); + } +} diff --git a/x/evm/embeds/embeds.go b/x/evm/embeds/embeds.go index f08e26b5d..1f0763311 100644 --- a/x/evm/embeds/embeds.go +++ b/x/evm/embeds/embeds.go @@ -47,6 +47,10 @@ var ( testMetadataBytes32 []byte //go:embed artifacts/contracts/TestPrecompileSendToBankThenERC20Transfer.sol/TestPrecompileSendToBankThenERC20Transfer.json testPrecompileSendToBankThenERC20Transfer []byte + //go:embed artifacts/contracts/TestDirtyStateAttack4.sol/TestDirtyStateAttack4.json + testDirtyStateAttack4 []byte + //go:embed artifacts/contracts/TestDirtyStateAttack5.sol/TestDirtyStateAttack5.json + testDirtyStateAttack5 []byte ) var ( @@ -118,7 +122,6 @@ var ( Name: "TestERC20TransferThenPrecompileSend.sol", EmbedJSON: testERC20TransferThenPrecompileSendJson, } - // SmartContract_TestPrecompileSelfCallRevert is a test contract // that creates another instance of itself, calls the precompile method and then force reverts. // It tests a race condition where the state DB commit @@ -150,12 +153,21 @@ var ( Name: "MKR.sol", EmbedJSON: testMetadataBytes32, } - // SmartContract_TestPrecompileSendToBankThenERC20Transfer is a test contract that sends to bank then calls ERC20 transfer SmartContract_TestPrecompileSendToBankThenERC20Transfer = CompiledEvmContract{ Name: "TestPrecompileSendToBankThenERC20Transfer.sol", EmbedJSON: testPrecompileSendToBankThenERC20Transfer, } + // SmartContract_TestDirtyStateAttack4 is a test contract that composes manual send and funtoken sendToBank with a reversion + SmartContract_TestDirtyStateAttack4 = CompiledEvmContract{ + Name: "TestDirtyStateAttack4.sol", + EmbedJSON: testDirtyStateAttack4, + } + // SmartContract_TestDirtyStateAttack5 is a test contract that calls a wasm contract with 5 NIBI + SmartContract_TestDirtyStateAttack5 = CompiledEvmContract{ + Name: "TestDirtyStateAttack5.sol", + EmbedJSON: testDirtyStateAttack5, + } ) func init() { @@ -175,6 +187,8 @@ func init() { SmartContract_TestRandom.MustLoad() SmartContract_TestBytes32Metadata.MustLoad() SmartContract_TestPrecompileSendToBankThenERC20Transfer.MustLoad() + SmartContract_TestDirtyStateAttack4.MustLoad() + SmartContract_TestDirtyStateAttack5.MustLoad() } type CompiledEvmContract struct { diff --git a/x/evm/keeper/bank_extension.go b/x/evm/keeper/bank_extension.go index aa4fe101c..587135ac2 100644 --- a/x/evm/keeper/bank_extension.go +++ b/x/evm/keeper/bank_extension.go @@ -89,6 +89,38 @@ func (bk NibiruBankKeeper) UndelegateCoins( ) } +func (bk NibiruBankKeeper) DelegateCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error { + return bk.ForceGasInvariant( + ctx, + func(ctx sdk.Context) error { + return bk.BaseKeeper.DelegateCoinsFromAccountToModule(ctx, senderAddr, recipientModule, amt) + }, + func(ctx sdk.Context) { + if findEtherBalanceChangeFromCoins(amt) { + bk.SyncStateDBWithAccount(ctx, senderAddr) + moduleBech32Addr := auth.NewModuleAddress(recipientModule) + bk.SyncStateDBWithAccount(ctx, moduleBech32Addr) + } + }, + ) +} + +func (bk NibiruBankKeeper) UndelegateCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error { + return bk.ForceGasInvariant( + ctx, + func(ctx sdk.Context) error { + return bk.BaseKeeper.UndelegateCoinsFromModuleToAccount(ctx, senderModule, recipientAddr, amt) + }, + func(ctx sdk.Context) { + if findEtherBalanceChangeFromCoins(amt) { + moduleBech32Addr := auth.NewModuleAddress(senderModule) + bk.SyncStateDBWithAccount(ctx, moduleBech32Addr) + bk.SyncStateDBWithAccount(ctx, recipientAddr) + } + }, + ) +} + func (bk NibiruBankKeeper) MintCoins( ctx sdk.Context, moduleName string, diff --git a/x/evm/keeper/funtoken_from_coin_test.go b/x/evm/keeper/funtoken_from_coin_test.go index 7a75afca6..432d3ce5b 100644 --- a/x/evm/keeper/funtoken_from_coin_test.go +++ b/x/evm/keeper/funtoken_from_coin_test.go @@ -751,7 +751,7 @@ func (s *FunTokenFromCoinSuite) TestPrecompileSendToBankThenErc20Transfer() { Account: bob.EthAddr, BalanceBank: big.NewInt(0), BalanceERC20: big.NewInt(0), - Description: "Charles has 0 NIBI / 0 WNIBI", + Description: "Bob has 0 NIBI / 0 WNIBI", }.Assert(s.T(), deps, evmObj) evmtest.FunTokenBalanceAssert{ diff --git a/x/evm/precompile/funtoken_test.go b/x/evm/precompile/funtoken_test.go index 2cd2bd34b..a5b21f60d 100644 --- a/x/evm/precompile/funtoken_test.go +++ b/x/evm/precompile/funtoken_test.go @@ -119,15 +119,14 @@ func (s *FuntokenSuite) TestHappyPath() { deps := evmtest.NewTestDeps() s.T().Log("Create FunToken mapping and ERC20") - bankDenom := "unibi" - funtoken := evmtest.CreateFunTokenForBankCoin(deps, bankDenom, &s.Suite) + funtoken := evmtest.CreateFunTokenForBankCoin(deps, evm.EVMBankDenom, &s.Suite) erc20 := funtoken.Erc20Addr.Address s.Require().NoError(testapp.FundAccount( deps.App.BankKeeper, deps.Ctx, deps.Sender.NibiruAddr, - sdk.NewCoins(sdk.NewCoin(bankDenom, sdk.NewInt(69_420))), + sdk.NewCoins(sdk.NewCoin(evm.EVMBankDenom, sdk.NewInt(69_420))), )) s.Run("IFunToken.bankBalance()", func() { @@ -152,17 +151,25 @@ func (s *FuntokenSuite) TestHappyPath() { s.Equal(deps.Sender.NibiruAddr.String(), bech32Addr) }) - _, err := deps.EvmKeeper.ConvertCoinToEvm( - sdk.WrapSDKContext(deps.Ctx), - &evm.MsgConvertCoinToEvm{ - Sender: deps.Sender.NibiruAddr.String(), - BankCoin: sdk.NewCoin(bankDenom, sdk.NewInt(69_420)), - ToEthAddr: eth.EIP55Addr{ - Address: deps.Sender.EthAddr, + s.Run("ConvertCoinToEvm", func() { + _, err := deps.EvmKeeper.ConvertCoinToEvm( + sdk.WrapSDKContext(deps.Ctx), + &evm.MsgConvertCoinToEvm{ + Sender: deps.Sender.NibiruAddr.String(), + BankCoin: sdk.NewCoin(evm.EVMBankDenom, sdk.NewInt(69_420)), + ToEthAddr: eth.EIP55Addr{ + Address: deps.Sender.EthAddr, + }, }, - }, - ) - s.Require().NoError(err) + ) + s.Require().NoError(err) + evmObj, _ := deps.NewEVM() + evmtest.AssertERC20BalanceEqualWithDescription( + s.T(), deps, evmObj, erc20, deps.Sender.EthAddr, big.NewInt(69_420), "expect 69420 balance", + ) + evmtest.AssertBankBalanceEqualWithDescription(s.T(), deps, evm.EVMBankDenom, deps.Sender.EthAddr, big.NewInt(0), "expect the sender to have zero balance") + evmtest.AssertBankBalanceEqualWithDescription(s.T(), deps, evm.EVMBankDenom, evm.EVM_MODULE_ADDRESS, big.NewInt(69_420), "expect x/evm module to escrow all tokens") + }) s.Run("Mint tokens - Fail from non-owner", func() { contractInput, err := embeds.SmartContract_ERC20Minter.ABI.Pack("mint", deps.Sender.EthAddr, big.NewInt(69_420)) @@ -187,7 +194,6 @@ func (s *FuntokenSuite) TestHappyPath() { s.NoError(err) evmObj, _ := deps.NewEVM() - ethTxResp, err := deps.EvmKeeper.CallContractWithInput( deps.Ctx, evmObj, @@ -199,15 +205,19 @@ func (s *FuntokenSuite) TestHappyPath() { ) s.Require().NoError(err) s.Require().Empty(ethTxResp.VmError) - s.True(deps.App.BankKeeper == deps.App.EvmKeeper.Bank) evmtest.AssertERC20BalanceEqualWithDescription( - s.T(), deps, evmObj, erc20, deps.Sender.EthAddr, big.NewInt(69_000), "expect 69000 balance", + s.T(), deps, evmObj, erc20, deps.Sender.EthAddr, big.NewInt(69_000), "expect 69000 balance remaining", ) evmtest.AssertERC20BalanceEqualWithDescription( s.T(), deps, evmObj, erc20, evm.EVM_MODULE_ADDRESS, big.NewInt(0), "expect 0 balance", ) - s.Require().True(deps.App.BankKeeper.GetBalance(deps.Ctx, randomAcc, funtoken.BankDenom).Amount.Equal(sdk.NewInt(420))) + evmtest.AssertBankBalanceEqualWithDescription( + s.T(), deps, evm.EVMBankDenom, eth.NibiruAddrToEthAddr(randomAcc), big.NewInt(420), "expect 420 balance", + ) + evmtest.AssertBankBalanceEqualWithDescription( + s.T(), deps, evm.EVMBankDenom, evm.EVM_MODULE_ADDRESS, big.NewInt(69_000), "expect 69000 balance", + ) s.T().Log("Parse the response contract addr and response bytes") var sentAmt *big.Int @@ -220,8 +230,9 @@ func (s *FuntokenSuite) TestHappyPath() { }) s.Run("IFuntoken.balance", func() { - contractInput, err := embeds.SmartContract_FunToken.ABI.Pack("balance", deps.Sender.EthAddr, erc20) + contractInput, err := embeds.SmartContract_FunToken.ABI.Pack(string(precompile.FunTokenMethod_balance), deps.Sender.EthAddr, erc20) s.Require().NoError(err) + evmObj, _ := deps.NewEVM() evmResp, err := deps.EvmKeeper.CallContractWithInput( deps.Ctx, @@ -363,7 +374,7 @@ func (s *FuntokenSuite) TestSendToEvm_MadeFromCoin() { s.Run("Call sendToEvm(string bankDenom, uint256 amount, string to)", func() { contractInput, err := embeds.SmartContract_FunToken.ABI.Pack( - "sendToEvm", + string(precompile.FunTokenMethod_sendToEvm), bankDenom, big.NewInt(1000), deps.Sender.EthAddr.Hex(), @@ -382,20 +393,19 @@ func (s *FuntokenSuite) TestSendToEvm_MadeFromCoin() { s.Require().NoError(err) s.Require().Empty(ethTxResp.VmError, "sendToEvm VMError") - s.T().Log("4) The response returns the actual minted/unescrowed amount") + s.T().Log("The response returns the actual minted/unescrowed amount") var amountSent *big.Int err = embeds.SmartContract_FunToken.ABI.UnpackIntoInterface( - &amountSent, "sendToEvm", ethTxResp.Ret, + &amountSent, string(precompile.FunTokenMethod_sendToEvm), ethTxResp.Ret, ) s.Require().NoError(err) s.Require().EqualValues(1000, amountSent.Int64(), "expect 1000 minted to EVM") s.T().Log("Check the user lost 1000 ulibi in bank") - bankBal := deps.App.BankKeeper.GetBalance(deps.Ctx, deps.Sender.NibiruAddr, bankDenom).Amount.BigInt() - s.EqualValues(big.NewInt(234), bankBal, "did user lose 1000 ulibi from bank?") + evmtest.AssertBankBalanceEqualWithDescription(s.T(), deps, bankDenom, deps.Sender.EthAddr, big.NewInt(234), "did user lose 1000 ulibi from bank?") - // check the evm module account balance - s.EqualValues(big.NewInt(1000), deps.App.BankKeeper.GetBalance(deps.Ctx, evm.EVM_MODULE_ADDRESS_NIBI, bankDenom).Amount.BigInt()) + s.T().Log("Check the module account has 1000 ulibi") + evmtest.AssertBankBalanceEqualWithDescription(s.T(), deps, bankDenom, evm.EVM_MODULE_ADDRESS, big.NewInt(1000), "expect 1000 balance") s.T().Log("Check the user gained 1000 in ERC20 representation") evmtest.AssertERC20BalanceEqualWithDescription(s.T(), deps, evmObj, erc20Addr, deps.Sender.EthAddr, big.NewInt(1000), "expect 1000 balance") @@ -407,16 +417,17 @@ func (s *FuntokenSuite) TestSendToEvm_MadeFromCoin() { // We'll pick a brand new random account to receive them. s.Run("Sending 400 tokens back from EVM to Cosmos bank => recipient:", func() { - recipient := testutil.AccAddress() + randomRecipient := testutil.AccAddress() + contractInput, err := embeds.SmartContract_FunToken.ABI.Pack( string(precompile.FunTokenMethod_sendToBank), erc20Addr, big.NewInt(400), - recipient.String(), + randomRecipient.String(), ) s.Require().NoError(err) - ethExResp, err := deps.EvmKeeper.CallContractWithInput( + ethTxResp, err := deps.EvmKeeper.CallContractWithInput( deps.Ctx, evmObj, deps.Sender.EthAddr, @@ -426,13 +437,13 @@ func (s *FuntokenSuite) TestSendToEvm_MadeFromCoin() { evmtest.FunTokenGasLimitSendToEvm, ) s.Require().NoError(err) - s.Require().Empty(ethExResp.VmError, "sendToBank VMError") + s.Require().Empty(ethTxResp.VmError, "sendToBank VMError") s.T().Log("Parse the returned amount from `sendToBank`") var actualSent *big.Int err = embeds.SmartContract_FunToken.ABI.UnpackIntoInterface( &actualSent, string(precompile.FunTokenMethod_sendToBank), - ethExResp.Ret, + ethTxResp.Ret, ) s.Require().NoError(err) s.Require().EqualValues(big.NewInt(400), actualSent, "expect 400 minted back to bank") @@ -450,10 +461,24 @@ func (s *FuntokenSuite) TestSendToEvm_MadeFromCoin() { ) s.T().Log("Check the bank side got 400 more") - s.Require().EqualValues(big.NewInt(400), deps.App.BankKeeper.GetBalance(deps.Ctx, recipient, bankDenom).Amount.BigInt(), "did the recipient get 400?") + evmtest.AssertBankBalanceEqualWithDescription( + s.T(), + deps, + bankDenom, + eth.NibiruAddrToEthAddr(randomRecipient), + big.NewInt(400), + "did the recipient get 400?", + ) s.T().Log("Confirm module account doesn't keep them (burn or escrow) for bank-based tokens") - s.Require().EqualValues(big.NewInt(600), deps.App.BankKeeper.GetBalance(deps.Ctx, evm.EVM_MODULE_ADDRESS[:], bankDenom).Amount.BigInt(), "module should now have 600 left escrowed") + evmtest.AssertBankBalanceEqualWithDescription( + s.T(), + deps, + bankDenom, + evm.EVM_MODULE_ADDRESS, + big.NewInt(600), + "module should now have 600 left escrowed", + ) }) } @@ -476,7 +501,6 @@ func (s *FuntokenSuite) TestSendToEvm_MadeFromERC20() { // - unescrow erc20 token deps := evmtest.NewTestDeps() - evmObj, _ := deps.NewEVM() alice := evmtest.NewEthPrivAcc() bob := evmtest.NewEthPrivAcc() @@ -492,9 +516,6 @@ func (s *FuntokenSuite) TestSendToEvm_MadeFromERC20() { s.Require().NoError(err, "failed to deploy test ERC20") erc20Addr := erc20Resp.ContractAddr - // the initial supply was sent to the deployer - evmtest.AssertERC20BalanceEqualWithDescription(s.T(), deps, evmObj, erc20Addr, deps.Sender.EthAddr, bigTokens(1_000_000), "expect nonzero balance") - // create fun token from that erc20 _, err = deps.EvmKeeper.CreateFunToken( sdk.WrapSDKContext(deps.Ctx), @@ -506,67 +527,73 @@ func (s *FuntokenSuite) TestSendToEvm_MadeFromERC20() { s.Require().NoError(err) // Transfer 500 tokens to bob => 500 * 10^18 raw - contractInput, err := embeds.SmartContract_TestERC20.ABI.Pack( - "transfer", - bob.EthAddr, - bigTokens(500), // 500 in human sense - ) - s.Require().NoError(err) - _, err = deps.EvmKeeper.CallContractWithInput( - deps.Ctx, - evmObj, - deps.Sender.EthAddr, - &erc20Addr, - true, - contractInput, - keeper.Erc20GasLimitExecute, - ) - s.Require().NoError(err) + s.Run("Transfer 500 tokens to bob", func() { + contractInput, err := embeds.SmartContract_TestERC20.ABI.Pack( + "transfer", + bob.EthAddr, + bigTokens(500), // 500 in human sense + ) + s.Require().NoError(err) + evmObj, _ := deps.NewEVM() + _, err = deps.EvmKeeper.CallContractWithInput( + deps.Ctx, + evmObj, + deps.Sender.EthAddr, + &erc20Addr, + true, + contractInput, + keeper.Erc20GasLimitExecute, + ) + s.Require().NoError(err) - // Now user should have 500 tokens => raw is 500 * 10^18 - evmtest.AssertERC20BalanceEqualWithDescription(s.T(), deps, evmObj, erc20Addr, bob.EthAddr, bigTokens(500), "expect nonzero balance") + // Now user should have 500 tokens => raw is 500 * 10^18 + evmtest.AssertERC20BalanceEqualWithDescription(s.T(), deps, evmObj, erc20Addr, bob.EthAddr, bigTokens(500), "expect nonzero balance") + }) // sendToBank: e.g. 100 tokens => 100 * 1e18 raw // expects to escrow on EVM side and mint on cosmos side - contractInput, err = embeds.SmartContract_FunToken.ABI.Pack( - string(precompile.FunTokenMethod_sendToBank), - erc20Addr, // address - bigTokens(100), - alice.NibiruAddr.String(), - ) - s.Require().NoError(err) - resp, err := deps.EvmKeeper.CallContractWithInput( - deps.Ctx, - evmObj, - bob.EthAddr, /* from */ - &precompile.PrecompileAddr_FunToken, /* to */ - true, /* commit */ - contractInput, - evmtest.FunTokenGasLimitSendToEvm, /* gasLimit */ - ) - s.Require().NoError(err) - s.Require().Empty(resp.VmError) + s.Run("send 100 tokens to alice", func() { + contractInput, err := embeds.SmartContract_FunToken.ABI.Pack( + string(precompile.FunTokenMethod_sendToBank), + erc20Addr, // address + bigTokens(100), + alice.NibiruAddr.String(), + ) + s.Require().NoError(err) + evmObj, _ := deps.NewEVM() + resp, err := deps.EvmKeeper.CallContractWithInput( + deps.Ctx, + evmObj, + bob.EthAddr, /* from */ + &precompile.PrecompileAddr_FunToken, /* to */ + true, /* commit */ + contractInput, + evmtest.FunTokenGasLimitSendToEvm, /* gasLimit */ + ) + s.Require().NoError(err) + s.Require().Empty(resp.VmError) - // Bank side should see 100 - bankBal := deps.App.BankKeeper.GetBalance(deps.Ctx, alice.NibiruAddr, "erc20/"+erc20Addr.Hex()) - s.Require().EqualValues(bigTokens(100), bankBal.Amount.BigInt()) + // Bank side should see 100 + evmtest.AssertBankBalanceEqualWithDescription(s.T(), deps, "erc20/"+erc20Addr.Hex(), alice.EthAddr, bigTokens(100), "expect 100 balance") - // Expect user to have 400 tokens => 400 * 10^18 - evmtest.AssertERC20BalanceEqualWithDescription(s.T(), deps, evmObj, erc20Addr, bob.EthAddr, bigTokens(400), "expect Bob's balance to be 400") + // Expect user to have 400 tokens => 400 * 10^18 + evmtest.AssertERC20BalanceEqualWithDescription(s.T(), deps, evmObj, erc20Addr, bob.EthAddr, bigTokens(400), "expect Bob's balance to be 400") - // 100 tokens are escrowed - evmtest.AssertERC20BalanceEqualWithDescription(s.T(), deps, evmObj, erc20Addr, evm.EVM_MODULE_ADDRESS, bigTokens(100), "expect EVM module to escrow 100 tokens") + // 100 tokens are escrowed + evmtest.AssertERC20BalanceEqualWithDescription(s.T(), deps, evmObj, erc20Addr, evm.EVM_MODULE_ADDRESS, bigTokens(100), "expect EVM module to escrow 100 tokens") + }) // Finally sendToEvm(100) -> (expects to burn on cosmos side and unescrow in the EVM side) s.Run("send 100 tokens back to Bob", func() { contractInput, err := embeds.SmartContract_FunToken.ABI.Pack( "sendToEvm", - bankBal.Denom, + "erc20/"+erc20Addr.Hex(), bigTokens(100), bob.EthAddr.Hex(), ) s.Require().NoError(err) - resp, err = deps.EvmKeeper.CallContractWithInput( + evmObj, _ := deps.NewEVM() + resp, err := deps.EvmKeeper.CallContractWithInput( deps.Ctx, evmObj, alice.EthAddr, @@ -577,25 +604,20 @@ func (s *FuntokenSuite) TestSendToEvm_MadeFromERC20() { ) s.Require().NoError(err) s.Require().Empty(resp.VmError) - }) - // no bank side left for alice - balAfter := deps.App.BankKeeper.GetBalance(deps.Ctx, alice.NibiruAddr, bankBal.Denom).Amount.BigInt() - s.Require().EqualValues(bigTokens(0), balAfter) + // no bank side left for alice + evmtest.AssertBankBalanceEqualWithDescription(s.T(), deps, "erc20/"+erc20Addr.Hex(), alice.EthAddr, bigTokens(0), "expect 0 balance") - // check bob has 500 tokens again => 500 * 1e18 - evmtest.AssertERC20BalanceEqualWithDescription(s.T(), deps, evmObj, erc20Addr, bob.EthAddr, bigTokens(500), "expect nonzero balance") + // check bob has 500 tokens again => 500 * 1e18 + evmtest.AssertERC20BalanceEqualWithDescription(s.T(), deps, evmObj, erc20Addr, bob.EthAddr, bigTokens(500), "expect nonzero balance") - // check evm module account's balance, it should have escrowed some tokens - // unescrow the tokens - evmtest.AssertERC20BalanceEqualWithDescription(s.T(), deps, evmObj, erc20Addr, evm.EVM_MODULE_ADDRESS, bigTokens(0), "expect zero balance") + // check evm module account's balance, it should have escrowed some tokens + // unescrow the tokens + evmtest.AssertERC20BalanceEqualWithDescription(s.T(), deps, evmObj, erc20Addr, evm.EVM_MODULE_ADDRESS, bigTokens(0), "expect zero balance") - // burns the bank tokens - evmBal2 := deps.App.BankKeeper.GetBalance(deps.Ctx, evm.EVM_MODULE_ADDRESS[:], bankBal.Denom).Amount.BigInt() - s.Require().EqualValues(bigTokens(0), evmBal2) - - // user has 500 tokens again => 500 * 1e18 - evmtest.AssertERC20BalanceEqualWithDescription(s.T(), deps, evmObj, erc20Addr, bob.EthAddr, bigTokens(500), "expect nonzero balance") + // burns the bank tokens + evmtest.AssertBankBalanceEqualWithDescription(s.T(), deps, "erc20/"+erc20Addr.Hex(), evm.EVM_MODULE_ADDRESS, bigTokens(0), "expect 0 balance") + }) } // FunTokenWhoAmIReturn holds the return values from the "IFuntoken.whoAmI" diff --git a/x/evm/precompile/test/bank_transfer.wasm b/x/evm/precompile/test/bank_transfer.wasm new file mode 100644 index 000000000..7f1fa8f6f Binary files /dev/null and b/x/evm/precompile/test/bank_transfer.wasm differ diff --git a/x/evm/precompile/test/counter.wasm b/x/evm/precompile/test/counter.wasm new file mode 100644 index 000000000..0e1cba209 Binary files /dev/null and b/x/evm/precompile/test/counter.wasm differ diff --git a/x/evm/precompile/test/export.go b/x/evm/precompile/test/export.go index e37c99b09..01104f0bf 100644 --- a/x/evm/precompile/test/export.go +++ b/x/evm/precompile/test/export.go @@ -2,7 +2,6 @@ package test import ( "encoding/json" - "math/big" "os" "os/exec" "path" @@ -15,7 +14,6 @@ import ( "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" @@ -30,7 +28,7 @@ const ( // SetupWasmContracts stores all Wasm bytecode and has the "deps.Sender" // instantiate each Wasm contract using the precompile. -func SetupWasmContracts(deps *evmtest.TestDeps, evmObj *vm.EVM, s *suite.Suite) ( +func SetupWasmContracts(deps *evmtest.TestDeps, s *suite.Suite) ( contracts []sdk.AccAddress, ) { wasmCodes := deployWasmBytecode(s, deps.Ctx, deps.Sender.NibiruAddr, deps.App) @@ -47,48 +45,29 @@ func SetupWasmContracts(deps *evmtest.TestDeps, evmObj *vm.EVM, s *suite.Suite) InstantiateMsg: []byte(`{"count": 0}`), Label: "https://github.com/NibiruChain/nibiru-wasm/tree/ec3ab9f09587a11fbdfbd4021c7617eca3912044/contracts/00-hello-world-counter", }, + { + InstantiateMsg: []byte(`{}`), + Label: "https://github.com/k-yang/nibiru-wasm-plus/tree/main/contracts/bank-transfer/", + }, + { + InstantiateMsg: []byte(`{}`), + Label: "https://github.com/k-yang/nibiru-wasm-plus/tree/main/contracts/staking/", + }, } for i, wasmCode := range wasmCodes { - s.T().Logf("Instantiate using Wasm precompile: %s", wasmCode.binPath) - - m := wasm.MsgInstantiateContract{ - Admin: "", - CodeID: wasmCode.codeId, - Label: instantiateArgs[i].Label, - Msg: instantiateArgs[i].InstantiateMsg, - } + s.T().Logf("Instantiate %s", wasmCode.binPath) - msgArgsBz, err := json.Marshal(m.Msg) - s.NoError(err) - - contractInput, err := embeds.SmartContract_Wasm.ABI.Pack( - string(precompile.WasmMethod_instantiate), - m.Admin, m.CodeID, msgArgsBz, m.Label, []precompile.WasmBankCoin{}, - ) - s.NoError(err) - - ethTxResp, err := deps.EvmKeeper.CallContractWithInput( + wasmPermissionedKeeper := wasmkeeper.NewDefaultPermissionKeeper(deps.App.WasmKeeper) + contractAddr, _, err := wasmPermissionedKeeper.Instantiate( deps.Ctx, - evmObj, - deps.Sender.EthAddr, - &precompile.PrecompileAddr_Wasm, - true, - contractInput, - WasmGasLimitInstantiate, - ) - - s.Require().NoError(err) - s.Require().NotEmpty(ethTxResp.Ret) - - s.T().Log("Parse the response contract addr and response bytes") - vals, err := embeds.SmartContract_Wasm.ABI.Unpack( - string(precompile.WasmMethod_instantiate), - ethTxResp.Ret, + wasmCode.codeId, + deps.Sender.NibiruAddr, + deps.Sender.NibiruAddr, + instantiateArgs[i].InstantiateMsg, + instantiateArgs[i].Label, + sdk.Coins{}, ) - s.Require().NoError(err) - - contractAddr, err := sdk.AccAddressFromBech32(vals[0].(string)) s.NoError(err) contracts = append(contracts, contractAddr) } @@ -102,7 +81,7 @@ func deployWasmBytecode( s *suite.Suite, ctx sdk.Context, sender sdk.AccAddress, - nibiru *app.NibiruApp, + app *app.NibiruApp, ) (codeIds []struct { codeId uint64 binPath string @@ -121,7 +100,15 @@ func deployWasmBytecode( // hello_world_counter.wasm is a compiled version of: // https://github.com/NibiruChain/nibiru-wasm/tree/ec3ab9f09587a11fbdfbd4021c7617eca3912044/contracts/00-hello-world-counter - "x/evm/precompile/hello_world_counter.wasm", + "x/evm/precompile/test/hello_world_counter.wasm", + + // bank_transfer.wasm is a compiled version of: + // https://github.com/k-yang/nibiru-wasm-plus/tree/main/contracts/bank-transfer/ + "x/evm/precompile/test/bank_transfer.wasm", + + // staking.wasm is a compiled version of: + // https://github.com/k-yang/nibiru-wasm-plus/tree/main/contracts/staking/ + "x/evm/precompile/test/staking.wasm", // Add other wasm bytecode here if needed... } { @@ -135,12 +122,11 @@ func deployWasmBytecode( // The "Create" fn is private on the nibiru.WasmKeeper. By placing it as the // decorated keeper in PermissionedKeeper type, we can access "Create" as a // public fn. - wasmPermissionedKeeper := wasmkeeper.NewDefaultPermissionKeeper(nibiru.WasmKeeper) - instantiateAccess := &wasm.AccessConfig{ - Permission: wasm.AccessTypeEverybody, - } + wasmPermissionedKeeper := wasmkeeper.NewDefaultPermissionKeeper(app.WasmKeeper) codeId, _, err := wasmPermissionedKeeper.Create( - ctx, sender, wasmBytecode, instantiateAccess, + ctx, sender, wasmBytecode, &wasm.AccessConfig{ + Permission: wasm.AccessTypeEverybody, + }, ) s.Require().NoError(err) codeIds = append(codeIds, struct { @@ -163,7 +149,6 @@ func deployWasmBytecode( func AssertWasmCounterState( s *suite.Suite, deps evmtest.TestDeps, - evmObj *vm.EVM, wasmContract sdk.AccAddress, wantCount int64, ) { @@ -173,6 +158,30 @@ func AssertWasmCounterState( } `) + resp, err := deps.App.WasmKeeper.QuerySmart(deps.Ctx, wasmContract, msgArgsBz) + s.Require().NoError(err) + s.Require().NotEmpty(resp) + + var typedResp QueryMsgCountResp + s.NoError(json.Unmarshal(resp, &typedResp)) + + s.EqualValues(wantCount, typedResp.Count) + s.EqualValues(deps.Sender.NibiruAddr.String(), typedResp.Owner) +} + +func AssertWasmCounterStateWithEvm( + s *suite.Suite, + deps evmtest.TestDeps, + evmObj *vm.EVM, + wasmContract sdk.AccAddress, + wantCount int64, +) { + msgArgsBz := []byte(` + { + "count": {} + } + `) + contractInput, err := embeds.SmartContract_Wasm.ABI.Pack( string(precompile.WasmMethod_query), wasmContract.String(), @@ -180,7 +189,7 @@ func AssertWasmCounterState( ) s.Require().NoError(err) - ethTxResp, err := deps.EvmKeeper.CallContractWithInput( + evmResp, err := deps.EvmKeeper.CallContractWithInput( deps.Ctx, evmObj, deps.Sender.EthAddr, @@ -190,26 +199,14 @@ func AssertWasmCounterState( WasmGasLimitQuery, ) s.Require().NoError(err) - s.Require().NotEmpty(ethTxResp.Ret) - - s.T().Log("Parse the response contract addr and response bytes") - s.T().Logf("ethTxResp.Ret: %s", ethTxResp.Ret) + s.Require().NotEmpty(evmResp.Ret) var queryResp []byte - err = embeds.SmartContract_Wasm.ABI.UnpackIntoInterface( - // Since there's only one return value, don't unpack as a slice. - // If there were two or more return values, we'd use - // &[]any{...} + s.NoError(embeds.SmartContract_Wasm.ABI.UnpackIntoInterface( &queryResp, string(precompile.WasmMethod_query), - ethTxResp.Ret, - ) - s.Require().NoError(err) - s.T().Logf("queryResp: %s", queryResp) - - var wasmMsg wasm.RawContractMessage - s.NoError(json.Unmarshal(queryResp, &wasmMsg)) - s.NoError(wasmMsg.ValidateBasic()) + evmResp.Ret, + )) var typedResp QueryMsgCountResp s.NoError(json.Unmarshal(queryResp, &typedResp)) @@ -275,36 +272,24 @@ func IncrementWasmCounterWithExecuteMulti( } `) - // 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, + for i := uint(0); i < times; i++ { + executeMsgs = append(executeMsgs, struct { + ContractAddr string `json:"contractAddr"` + MsgArgs []byte `json:"msgArgs"` + Funds []precompile.WasmBankCoin `json:"funds"` + }{wasmContract.String(), msgArgsBz, []precompile.WasmBankCoin{}}) } - input, err := embeds.SmartContract_Wasm.ABI.Pack( + + contractInput, err := embeds.SmartContract_Wasm.ABI.Pack( string(precompile.WasmMethod_executeMulti), - callArgs..., + executeMsgs, ) s.Require().NoError(err) @@ -314,60 +299,9 @@ func IncrementWasmCounterWithExecuteMulti( deps.Sender.EthAddr, &precompile.PrecompileAddr_Wasm, commit, - input, + contractInput, WasmGasLimitExecute, ) s.Require().NoError(err) s.Require().NotEmpty(ethTxResp.Ret) } - -func IncrementWasmCounterWithExecuteMultiViaVMCall( - s *suite.Suite, - deps *evmtest.TestDeps, - wasmContract sdk.AccAddress, - times int, - 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"` - }{} - for i := 0; i < times; i++ { - executeMsgs = append(executeMsgs, struct { - ContractAddr string `json:"contractAddr"` - MsgArgs []byte `json:"msgArgs"` - Funds []precompile.WasmBankCoin `json:"funds"` - }{wasmContract.String(), msgArgsBz, funds}) - } - - contractInput, err := embeds.SmartContract_Wasm.ABI.Pack( - string(precompile.WasmMethod_executeMulti), - executeMsgs, - ) - s.Require().NoError(err) - - _, _, err = evmObj.Call( - vm.AccountRef(deps.Sender.EthAddr), - precompile.PrecompileAddr_Wasm, - contractInput, - serverconfig.DefaultEthCallGasLimit, - big.NewInt(0), - ) - return err -} diff --git a/x/evm/precompile/hello_world_counter.wasm b/x/evm/precompile/test/hello_world_counter.wasm similarity index 100% rename from x/evm/precompile/hello_world_counter.wasm rename to x/evm/precompile/test/hello_world_counter.wasm diff --git a/x/evm/precompile/test/infinite_loop.wasm b/x/evm/precompile/test/infinite_loop.wasm new file mode 100644 index 000000000..d58ca4100 Binary files /dev/null and b/x/evm/precompile/test/infinite_loop.wasm differ diff --git a/x/evm/precompile/test/staking.wasm b/x/evm/precompile/test/staking.wasm new file mode 100644 index 000000000..d902c74af Binary files /dev/null and b/x/evm/precompile/test/staking.wasm differ diff --git a/x/evm/precompile/test/wasteful_gas.wasm b/x/evm/precompile/test/wasteful_gas.wasm new file mode 100644 index 000000000..873a07113 Binary files /dev/null and b/x/evm/precompile/test/wasteful_gas.wasm differ diff --git a/x/evm/precompile/wasm_test.go b/x/evm/precompile/wasm_test.go index 3bedd115b..6698e6439 100644 --- a/x/evm/precompile/wasm_test.go +++ b/x/evm/precompile/wasm_test.go @@ -6,18 +6,22 @@ import ( "math/big" "testing" + "cosmossdk.io/math" wasm "github.com/CosmWasm/wasmd/x/wasm/types" + sdk "github.com/cosmos/cosmos-sdk/types" + stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + "github.com/stretchr/testify/suite" + "github.com/NibiruChain/nibiru/v2/eth" "github.com/NibiruChain/nibiru/v2/x/common/testutil" "github.com/NibiruChain/nibiru/v2/x/common/testutil/testapp" + "github.com/NibiruChain/nibiru/v2/x/evm" "github.com/NibiruChain/nibiru/v2/x/evm/embeds" "github.com/NibiruChain/nibiru/v2/x/evm/evmtest" "github.com/NibiruChain/nibiru/v2/x/evm/precompile" "github.com/NibiruChain/nibiru/v2/x/evm/precompile/test" tokenfactory "github.com/NibiruChain/nibiru/v2/x/tokenfactory/types" - - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/stretchr/testify/suite" ) // rough gas limits for wasm execution - used in tests only @@ -34,11 +38,48 @@ func TestWasmSuite(t *testing.T) { suite.Run(t, new(WasmSuite)) } -func (s *WasmSuite) TestExecuteHappy() { +func (s *WasmSuite) TestInstantiate() { + deps := evmtest.NewTestDeps() + evmObj, _ := deps.NewEVM() + + test.SetupWasmContracts(&deps, &s.Suite) + + contractInput, err := embeds.SmartContract_Wasm.ABI.Pack( + string(precompile.WasmMethod_instantiate), + "", // admin + uint64(1), // codeId + []byte(`{}`), // instantiateMsg + "some non-empty label", // label + []precompile.WasmBankCoin{}, // funds + ) + s.Require().NoError(err) + + ethTxResp, err := deps.EvmKeeper.CallContractWithInput( + deps.Ctx, + evmObj, + deps.Sender.EthAddr, + &precompile.PrecompileAddr_Wasm, + true, + contractInput, + WasmGasLimitExecute, + ) + + s.Require().NoError(err) + s.Require().NotEmpty(ethTxResp.Ret) + + vals, err := embeds.SmartContract_Wasm.ABI.Unpack( + string(precompile.WasmMethod_instantiate), + ethTxResp.Ret, + ) + s.Require().NoError(err) + s.Require().NotEmpty(vals[0].(string)) +} + +func (s *WasmSuite) TestExecute() { deps := evmtest.NewTestDeps() evmObj, _ := deps.NewEVM() - wasmContracts := test.SetupWasmContracts(&deps, evmObj, &s.Suite) + wasmContracts := test.SetupWasmContracts(&deps, &s.Suite) wasmContract := wasmContracts[0] // nibi_stargate.wasm s.Run("create denom", func() { @@ -50,17 +91,11 @@ func (s *WasmSuite) TestExecuteHappy() { } `) - var funds []precompile.WasmBankCoin - 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) - contractInput, err := embeds.SmartContract_Wasm.ABI.Pack( string(precompile.WasmMethod_execute), wasmContract.String(), msgArgsBz, - funds, + []precompile.WasmBankCoin{}, ) s.Require().NoError(err) ethTxResp, err := deps.EvmKeeper.CallContractWithInput( @@ -76,7 +111,6 @@ func (s *WasmSuite) TestExecuteHappy() { s.Require().NotEmpty(ethTxResp.Ret) }) - s.T().Log("Execute: mint tokens") s.Run("mint tokens", func() { coinDenom := tokenfactory.TFDenom{ Creator: wasmContract.String(), @@ -98,6 +132,7 @@ func (s *WasmSuite) TestExecuteHappy() { ) s.Require().NoError(err) + evmObj, _ = deps.NewEVM() ethTxResp, err := deps.EvmKeeper.CallContractWithInput( deps.Ctx, evmObj, @@ -115,82 +150,104 @@ func (s *WasmSuite) TestExecuteHappy() { }) } -func (s *WasmSuite) TestExecuteMultiHappy() { +func (s *WasmSuite) TestExecuteMulti() { deps := evmtest.NewTestDeps() evmObj, _ := deps.NewEVM() - wasmContracts := test.SetupWasmContracts(&deps, evmObj, &s.Suite) + wasmContracts := test.SetupWasmContracts(&deps, &s.Suite) wasmContract := wasmContracts[1] // hello_world_counter.wasm // count = 0 - test.AssertWasmCounterState(&s.Suite, deps, evmObj, wasmContract, 0) + test.AssertWasmCounterState(&s.Suite, deps, wasmContract, 0) // count += 2 test.IncrementWasmCounterWithExecuteMulti( &s.Suite, &deps, evmObj, wasmContract, 2, true) // count = 2 - test.AssertWasmCounterState(&s.Suite, deps, evmObj, wasmContract, 2) - s.assertWasmCounterStateRaw(deps, wasmContract, 2) + test.AssertWasmCounterState(&s.Suite, deps, wasmContract, 2) // count += 67 test.IncrementWasmCounterWithExecuteMulti( &s.Suite, &deps, evmObj, wasmContract, 67, true) // count = 69 - test.AssertWasmCounterState(&s.Suite, deps, evmObj, wasmContract, 69) - s.assertWasmCounterStateRaw(deps, wasmContract, 69) + test.AssertWasmCounterState(&s.Suite, deps, wasmContract, 69) } -// From IWasm.query of Wasm.sol: -// -// ```solidity -// function queryRaw( -// string memory contractAddr, -// bytes memory key -// ) external view returns (bytes memory response); -// ``` -func (s *WasmSuite) assertWasmCounterStateRaw( - deps evmtest.TestDeps, - wasmContract sdk.AccAddress, - wantCount int64, -) { +func (s *WasmSuite) TestQueryRaw() { + deps := evmtest.NewTestDeps() + wasmContracts := test.SetupWasmContracts(&deps, &s.Suite) + wasmContract := wasmContracts[1] // hello_world_counter.wasm + contractInput, err := embeds.SmartContract_Wasm.ABI.Pack( string(precompile.WasmMethod_queryRaw), wasmContract.String(), []byte(`state`), ) s.Require().NoError(err) - evmObj, _ := deps.NewEVM() - ethTxResp, err := deps.EvmKeeper.CallContractWithInput( + evmObj, _ := deps.NewEVM() + queryResp, err := deps.EvmKeeper.CallContractWithInput( deps.Ctx, evmObj, deps.Sender.EthAddr, &precompile.PrecompileAddr_Wasm, - true, + false, // commit contractInput, WasmGasLimitQuery, ) - s.Require().NoError(err) - s.Require().NotEmpty(ethTxResp.Ret) - s.T().Log("Parse the response contract addr and response bytes") - s.T().Logf("ethTxResp.Ret: %s", ethTxResp.Ret) + s.Require().NoError(err) + s.Require().NotEmpty(queryResp.Ret) - var queryResp []byte + var respBz []byte err = embeds.SmartContract_Wasm.ABI.UnpackIntoInterface( - &queryResp, + &respBz, string(precompile.WasmMethod_queryRaw), - ethTxResp.Ret, + queryResp.Ret, + ) + s.Require().NoError(err, "ethTxResp: %s", queryResp) + + var typedResp test.QueryMsgCountResp + s.NoError(json.Unmarshal(respBz, &typedResp)) + s.EqualValues(0, typedResp.Count) + s.EqualValues(deps.Sender.NibiruAddr.String(), typedResp.Owner) +} + +func (s *WasmSuite) TestQuerySmart() { + deps := evmtest.NewTestDeps() + wasmContracts := test.SetupWasmContracts(&deps, &s.Suite) + wasmContract := wasmContracts[1] // hello_world_counter.wasm + + contractInput, err := embeds.SmartContract_Wasm.ABI.Pack( + string(precompile.WasmMethod_query), + wasmContract.String(), + []byte(`{"count": {}}`), ) s.Require().NoError(err) - s.T().Logf("queryResp: %s", queryResp) - var wasmMsg wasm.RawContractMessage - s.NoError(wasmMsg.UnmarshalJSON(queryResp), queryResp) - s.T().Logf("wasmMsg: %s", wasmMsg) - s.NoError(wasmMsg.ValidateBasic()) + evmObj, _ := deps.NewEVM() + queryResp, err := deps.EvmKeeper.CallContractWithInput( + deps.Ctx, + evmObj, + deps.Sender.EthAddr, + &precompile.PrecompileAddr_Wasm, + false, // commit + contractInput, + WasmGasLimitQuery, + ) + + s.Require().NoError(err) + s.Require().NotEmpty(queryResp.Ret) + + var respBz []byte + err = embeds.SmartContract_Wasm.ABI.UnpackIntoInterface( + &respBz, + string(precompile.WasmMethod_query), + queryResp.Ret, + ) + s.Require().NoError(err, "ethTxResp: %s", queryResp) var typedResp test.QueryMsgCountResp - s.NoError(json.Unmarshal(wasmMsg, &typedResp)) - s.EqualValues(wantCount, typedResp.Count) - s.EqualValues(deps.Sender.NibiruAddr.String(), typedResp.Owner) + s.Require().NoError(json.Unmarshal(respBz, &typedResp)) + s.Require().EqualValues(0, typedResp.Count) + s.Require().EqualValues(deps.Sender.NibiruAddr.String(), typedResp.Owner) } func (s *WasmSuite) TestSadArgsCount() { @@ -357,24 +414,23 @@ type WasmExecuteMsg struct { func (s *WasmSuite) TestExecuteMultiValidation() { deps := evmtest.NewTestDeps() - evmObj, _ := deps.NewEVM() s.Require().NoError(testapp.FundAccount( deps.App.BankKeeper, deps.Ctx, deps.Sender.NibiruAddr, - sdk.NewCoins(sdk.NewCoin("unibi", sdk.NewInt(100))), + sdk.NewCoins(sdk.NewCoin(evm.EVMBankDenom, sdk.NewInt(100))), )) - wasmContracts := test.SetupWasmContracts(&deps, evmObj, &s.Suite) + 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 + invalidMsgArgsBz := []byte(`{"invalid": "json"}`) // Invalid message format var emptyFunds []precompile.WasmBankCoin validFunds := []precompile.WasmBankCoin{{ - Denom: "unibi", + Denom: evm.EVMBankDenom, Amount: big.NewInt(100), }} invalidFunds := []precompile.WasmBankCoin{{ @@ -500,11 +556,11 @@ func (s *WasmSuite) TestExecuteMultiPartialExecution() { deps := evmtest.NewTestDeps() evmObj, _ := deps.NewEVM() - wasmContracts := test.SetupWasmContracts(&deps, evmObj, &s.Suite) + wasmContracts := test.SetupWasmContracts(&deps, &s.Suite) wasmContract := wasmContracts[1] // hello_world_counter.wasm // First verify initial state is 0 - test.AssertWasmCounterState(&s.Suite, deps, evmObj, wasmContract, 0) + test.AssertWasmCounterState(&s.Suite, deps, wasmContract, 0) // Create a batch where the second message will fail validation executeMsgs := []WasmExecuteMsg{ @@ -540,5 +596,177 @@ func (s *WasmSuite) TestExecuteMultiPartialExecution() { s.Require().Contains(err.Error(), "unknown variant") // Verify that no state changes occurred - test.AssertWasmCounterState(&s.Suite, deps, evmObj, wasmContract, 0) + test.AssertWasmCounterState(&s.Suite, deps, wasmContract, 0) +} + +// TestDirtyStateAttack4 +// 1. Deploy a simple wasm contract that bank transfers NIBI to a recipient (Alice) +// 2. Calls the test contract +// a. call the wasm precompile which calls the wasm contract with a bank transfer +// +// INITIAL STATE: +// - Test contract funds: 10 NIBI +// CONTRACT CALL: +// - Sends 1 NIBI to Alice via wasm precompile +// EXPECTED: +// - Test contract funds: 9 NIBI +// - Alice: 1 NIBI +func (s *WasmSuite) TestWasmPrecompileDirtyStateAttack4() { + deps := evmtest.NewTestDeps() + + wasmContracts := test.SetupWasmContracts(&deps, &s.Suite) + wasmContract := wasmContracts[2] // bank_transfer.wasm + + s.T().Log("Deploy Test Contract") + deployResp, err := evmtest.DeployContract( + &deps, + embeds.SmartContract_TestDirtyStateAttack4, + ) + s.Require().NoError(err) + testContractAddr := deployResp.ContractAddr + + s.Run("Send 10 NIBI to test contract manually", func() { + s.Require().NoError(testapp.FundAccount( + deps.App.BankKeeper, + deps.Ctx, + eth.EthAddrToNibiruAddr(testContractAddr), + sdk.NewCoins(sdk.NewCoin(evm.EVMBankDenom, sdk.NewInt(10e6))), + )) + }) + + alice := evmtest.NewEthPrivAcc() + + s.Run("call test contract", func() { + msgArgsBz := []byte(fmt.Sprintf(` + { + "bank_transfer": { + "recipient": "%s" + } + } + `, alice.NibiruAddr)) + contractInput, err := embeds.SmartContract_TestDirtyStateAttack4.ABI.Pack( + "attack", + wasmContract.String(), + msgArgsBz, + ) + s.Require().NoError(err) + + evmObj, _ := deps.NewEVM() + _, err = deps.EvmKeeper.CallContractWithInput( + deps.Ctx, + evmObj, + deps.Sender.EthAddr, + &testContractAddr, + true, + contractInput, + evmtest.FunTokenGasLimitSendToEvm, + ) + s.Require().NoError(err) + + balanceAlice := deps.App.BankKeeper.GetBalance(deps.Ctx, alice.NibiruAddr, evm.EVMBankDenom) + s.Require().Equal(balanceAlice.Amount.BigInt(), big.NewInt(1e6)) + + balanceTestContract := deps.App.BankKeeper.GetBalance(deps.Ctx, eth.EthAddrToNibiruAddr(testContractAddr), evm.EVMBankDenom) + s.Require().Equal(balanceTestContract.Amount.BigInt(), big.NewInt(9e6)) + }) +} + +// TestDirtyStateAttack5 +// 1. Deploy a simple wasm contract that stakes NIBI +// 2. Calls the test contract +// a. call the wasm precompile which calls the wasm contract that stakes 5 NIBI +// +// INITIAL STATE: +// - Test contract funds: 10 NIBI +// CONTRACT CALL: +// - Sends 5 NIBI to the wasm contract +// - The wasm contract stakes 5 NIBI +// EXPECTED: +// - Test contract funds: 5 NIBI +// - Staked NIBI from wasm contract: 5 NIBI +// - Wasm contract: 0 NIBI +func (s *WasmSuite) TestWasmPrecompileDirtyStateAttack5() { + deps := evmtest.NewTestDeps() + + wasmContracts := test.SetupWasmContracts(&deps, &s.Suite) + wasmContract := wasmContracts[3] // staking.wasm + + s.T().Log("Deploy Test Contract") + deployResp, err := evmtest.DeployContract( + &deps, + embeds.SmartContract_TestDirtyStateAttack5, + ) + s.Require().NoError(err) + testContractAddr := deployResp.ContractAddr + + s.Run("Mint 10 NIBI to test contract", func() { + s.Require().NoError(testapp.FundAccount( + deps.App.BankKeeper, + deps.Ctx, + eth.EthAddrToNibiruAddr(testContractAddr), + sdk.NewCoins(sdk.NewCoin(evm.EVMBankDenom, sdk.NewInt(10e6))), + )) + }) + + validator := evmtest.NewEthPrivAcc() + s.Run("create validator", func() { + s.Require().NoError(testapp.FundAccount( + deps.App.BankKeeper, + deps.Ctx, + validator.NibiruAddr, + sdk.NewCoins(sdk.NewCoin(evm.EVMBankDenom, sdk.NewInt(10e6))), + )) + + createValMsg, err := stakingtypes.NewMsgCreateValidator( + sdk.ValAddress(validator.NibiruAddr), + validator.PrivKey.PubKey(), + sdk.NewCoin(evm.EVMBankDenom, sdk.NewInt(10e6)), + stakingtypes.NewDescription("validator0", "", "", "", ""), + stakingtypes.NewCommissionRates(sdk.NewDec(1), sdk.NewDec(1), sdk.NewDec(1)), + math.OneInt(), + ) + s.Require().NoError(err) + + stakingMsgServer := stakingkeeper.NewMsgServerImpl(deps.App.StakingKeeper) + resp, err := stakingMsgServer.CreateValidator(deps.Ctx, createValMsg) + s.Require().NoError(err) + s.Require().NotNil(resp) + }) + + s.Run("call test contract", func() { + msgArgsBz := []byte(fmt.Sprintf(`{"run": {"validator": "%s"}}`, sdk.ValAddress(validator.NibiruAddr).String())) + contractInput, err := embeds.SmartContract_TestDirtyStateAttack5.ABI.Pack( + "attack", + wasmContract.String(), + msgArgsBz, + ) + s.Require().NoError(err) + + evmObj, _ := deps.NewEVM() + _, err = deps.EvmKeeper.CallContractWithInput( + deps.Ctx, + evmObj, + deps.Sender.EthAddr, + &testContractAddr, + true, + contractInput, + evmtest.FunTokenGasLimitSendToEvm, + ) + s.Require().NoError(err) + + testContractBalance := deps.App.BankKeeper.GetBalance(deps.Ctx, eth.EthAddrToNibiruAddr(testContractAddr), evm.EVMBankDenom) + s.Require().Equal(testContractBalance.Amount.BigInt(), big.NewInt(5e6)) + + wasmContractBalance := deps.App.BankKeeper.GetBalance(deps.Ctx, wasmContract, evm.EVMBankDenom) + s.Require().Equal(wasmContractBalance.Amount.BigInt(), big.NewInt(0)) + + delegations := deps.App.StakingKeeper.GetAllDelegatorDelegations(deps.Ctx, wasmContract) + s.Require().Equal(len(delegations), 1) + s.Require().Equal(sdk.ValAddress(validator.NibiruAddr).String(), delegations[0].ValidatorAddress) + + // after converting the wasm contract address to an eth address, check the balances + wasmContractEthAddr := eth.NibiruAddrToEthAddr(wasmContract) + balance := deps.App.BankKeeper.GetBalance(deps.Ctx, eth.EthAddrToNibiruAddr(wasmContractEthAddr), evm.EVMBankDenom) + s.Require().Equal(big.NewInt(0), balance.Amount.BigInt()) + }) } diff --git a/x/evm/statedb/debug.go b/x/evm/statedb/debug.go index c2b5fb968..3927e4a5d 100644 --- a/x/evm/statedb/debug.go +++ b/x/evm/statedb/debug.go @@ -22,12 +22,6 @@ 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 { diff --git a/x/evm/statedb/journal_test.go b/x/evm/statedb/journal_test.go index 27582e69f..e10505a00 100644 --- a/x/evm/statedb/journal_test.go +++ b/x/evm/statedb/journal_test.go @@ -161,8 +161,6 @@ func (s *Suite) TestContractCallsAnotherContract() { func (s *Suite) TestJournalReversion() { deps := evmtest.NewTestDeps() - evmObj, _ := deps.NewEVM() - stateDB := evmObj.StateDB.(*statedb.StateDB) s.Require().NoError(testapp.FundAccount( deps.App.BankKeeper, deps.Ctx, @@ -171,11 +169,12 @@ func (s *Suite) TestJournalReversion() { )) s.T().Log("Set up helloworldcounter.wasm") - wasmContracts := test.SetupWasmContracts(&deps, evmObj, &s.Suite) + wasmContracts := test.SetupWasmContracts(&deps, &s.Suite) helloWorldCounterWasm := wasmContracts[1] fmt.Printf("wasmContract: %s\n", helloWorldCounterWasm) s.T().Log("commitEvmTx=true, expect 0 dirty journal entries") + evmObj, stateDB := deps.NewEVM() test.IncrementWasmCounterWithExecuteMulti( &s.Suite, &deps, evmObj, helloWorldCounterWasm, 7, true, ) @@ -185,6 +184,7 @@ func (s *Suite) TestJournalReversion() { } s.T().Log("commitEvmTx=false, expect dirty journal entries") + evmObj, stateDB = deps.NewEVM() test.IncrementWasmCounterWithExecuteMulti( &s.Suite, &deps, evmObj, helloWorldCounterWasm, 5, false, ) @@ -194,24 +194,27 @@ func (s *Suite) TestJournalReversion() { s.FailNowf("statedb dirty count mismatch", "expected 1 dirty journal change, but instead got: %d", stateDB.DebugDirtiesCount()) } - s.T().Log("Expect to see the pending changes included") + s.T().Log("Expect to see the pending changes included in the EVM context") + test.AssertWasmCounterStateWithEvm( + &s.Suite, deps, evmObj, helloWorldCounterWasm, 7+5, + ) + s.T().Log("Expect to see the pending changes not included in cosmos ctx") test.AssertWasmCounterState( - &s.Suite, deps, evmObj, helloWorldCounterWasm, 12, // 12 = 7 + 5 + &s.Suite, deps, helloWorldCounterWasm, 7, ) // 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, helloWorldCounterWasm, 50, false, evmObj, + test.IncrementWasmCounterWithExecuteMulti( + &s.Suite, &deps, evmObj, helloWorldCounterWasm, 50, false, ) - 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.`)) - test.AssertWasmCounterState( + test.AssertWasmCounterStateWithEvm( &s.Suite, deps, evmObj, helloWorldCounterWasm, 7+5+50, ) @@ -221,21 +224,20 @@ snapshots and see the prior states.`)) }) s.Require().ErrorContains(errFn(), "revision id 9000 cannot be reverted") - stateDB.RevertToSnapshot(5) - test.AssertWasmCounterState( + stateDB.RevertToSnapshot(2) + test.AssertWasmCounterStateWithEvm( &s.Suite, deps, evmObj, helloWorldCounterWasm, 7+5, ) - stateDB.RevertToSnapshot(3) - test.AssertWasmCounterState( - &s.Suite, deps, evmObj, helloWorldCounterWasm, 7, // state before precompile called + stateDB.RevertToSnapshot(0) + test.AssertWasmCounterStateWithEvm( + &s.Suite, deps, evmObj, helloWorldCounterWasm, 7, ) - err = stateDB.Commit() - s.Require().NoError(err) + s.Require().NoError(stateDB.Commit()) s.Require().EqualValues(0, stateDB.DebugDirtiesCount()) test.AssertWasmCounterState( - &s.Suite, deps, evmObj, helloWorldCounterWasm, 7, // state before precompile called + &s.Suite, deps, helloWorldCounterWasm, 7, ) } diff --git a/x/inflation/keeper/inflation_test.go b/x/inflation/keeper/inflation_test.go index 00b8b9870..6831f656f 100644 --- a/x/inflation/keeper/inflation_test.go +++ b/x/inflation/keeper/inflation_test.go @@ -137,7 +137,7 @@ func TestGetCirculatingSupplyAndInflationRate(t *testing.T) { }{ { "no epochs per period", - sdk.TokensFromConsensusPower(400_000_000, sdk.DefaultPowerReduction), + sdk.TokensFromConsensusPower(400_000_000-100_000_001, sdk.DefaultPowerReduction), func(nibiruApp *app.NibiruApp, ctx sdk.Context) { nibiruApp.InflationKeeper.Params.Set(ctx, types.Params{ EpochsPerPeriod: 0, @@ -150,7 +150,7 @@ func TestGetCirculatingSupplyAndInflationRate(t *testing.T) { }, { "high supply", - sdk.TokensFromConsensusPower(800_000_000, sdk.DefaultPowerReduction), + sdk.TokensFromConsensusPower(800_000_000-100_000_001, sdk.DefaultPowerReduction), func(nibiruApp *app.NibiruApp, ctx sdk.Context) { params := nibiruApp.InflationKeeper.GetParams(ctx) params.InflationEnabled = true @@ -160,7 +160,7 @@ func TestGetCirculatingSupplyAndInflationRate(t *testing.T) { }, { "low supply", - sdk.TokensFromConsensusPower(400_000_000, sdk.DefaultPowerReduction), + sdk.TokensFromConsensusPower(400_000_000-100_000_001, sdk.DefaultPowerReduction), func(nibiruApp *app.NibiruApp, ctx sdk.Context) { params := nibiruApp.InflationKeeper.GetParams(ctx) params.InflationEnabled = true @@ -184,7 +184,7 @@ func TestGetCirculatingSupplyAndInflationRate(t *testing.T) { require.NoError(t, err) circulatingSupply := nibiruApp.InflationKeeper.GetCirculatingSupply(ctx, denoms.NIBI) - require.EqualValues(t, tc.supply, circulatingSupply) + require.EqualValues(t, tc.supply.Add(sdk.TokensFromConsensusPower(100_000_001, sdk.DefaultPowerReduction)), circulatingSupply) inflationRate := nibiruApp.InflationKeeper.GetInflationRate(ctx, denoms.NIBI) require.Equal(t, tc.expInflationRate, inflationRate) diff --git a/x/inflation/keeper/keeper_test.go b/x/inflation/keeper/keeper_test.go index 46f1619fc..b3f24b2ee 100644 --- a/x/inflation/keeper/keeper_test.go +++ b/x/inflation/keeper/keeper_test.go @@ -55,17 +55,17 @@ func TestBurn(t *testing.T) { ) supply := nibiruApp.BankKeeper.GetSupply(ctx, "unibi") - require.Equal(t, tc.mintCoin.Amount, supply.Amount) + require.Equal(t, tc.mintCoin.Amount.Add(sdk.TokensFromConsensusPower(100_000_001, sdk.DefaultPowerReduction)), supply.Amount) // Burn coins err := nibiruApp.InflationKeeper.Burn(ctx, sdk.NewCoins(tc.burnCoin), tc.sender) supply = nibiruApp.BankKeeper.GetSupply(ctx, "unibi") if tc.expectedErr != nil { require.EqualError(t, err, tc.expectedErr.Error()) - require.Equal(t, tc.mintCoin.Amount, supply.Amount) + require.Equal(t, tc.mintCoin.Amount.Add(sdk.TokensFromConsensusPower(100_000_001, sdk.DefaultPowerReduction)), supply.Amount) } else { require.NoError(t, err) - require.Equal(t, math.ZeroInt(), supply.Amount) + require.Equal(t, sdk.TokensFromConsensusPower(100_000_001, sdk.DefaultPowerReduction), supply.Amount) } }) } diff --git a/x/tokenfactory/keeper/msg_server_test.go b/x/tokenfactory/keeper/msg_server_test.go index 0d7ce4f04..a403a1618 100644 --- a/x/tokenfactory/keeper/msg_server_test.go +++ b/x/tokenfactory/keeper/msg_server_test.go @@ -662,7 +662,7 @@ func (s *TestSuite) TestBurnNative() { }, PostHook: func(ctx sdk.Context, bapp *app.NibiruApp) { s.Equal( - math.NewInt(0), s.app.BankKeeper.GetSupply(s.ctx, "unibi").Amount, + sdk.TokensFromConsensusPower(100_000_001, sdk.DefaultPowerReduction), s.app.BankKeeper.GetSupply(s.ctx, "unibi").Amount, ) s.Equal( @@ -696,7 +696,7 @@ func (s *TestSuite) TestBurnNative() { }, PostHook: func(ctx sdk.Context, bapp *app.NibiruApp) { s.Equal( - math.NewInt(123), s.app.BankKeeper.GetSupply(s.ctx, "unibi").Amount, + math.NewInt(123).Add(sdk.TokensFromConsensusPower(100_000_001, sdk.DefaultPowerReduction)), s.app.BankKeeper.GetSupply(s.ctx, "unibi").Amount, ) s.Equal(