From 050f755802c526e7fad154b0d884a8bb7c7070d9 Mon Sep 17 00:00:00 2001 From: Uday Patil Date: Mon, 15 Apr 2024 18:35:57 -0500 Subject: [PATCH] Add wasmd execute batch to precompile (#1529) * Add wasmd execute batch to precompile * Add some wasm hardhat it stuff * update script * Update deployment script * fix tests * update to add instantiate --- contracts/test/EVMPrecompileTester.js | 151 ++++++++- contracts/test/deploy_wasm_contract.sh | 26 ++ integration_test/evm_module/hardhat_test.yaml | 1 + precompiles/wasmd/Wasmd.sol | 8 + precompiles/wasmd/abi.json | 2 +- precompiles/wasmd/wasmd.go | 188 +++++++++-- precompiles/wasmd/wasmd_test.go | 302 +++++++++++++++++- 7 files changed, 635 insertions(+), 43 deletions(-) create mode 100644 contracts/test/deploy_wasm_contract.sh mode change 100755 => 100644 precompiles/wasmd/abi.json diff --git a/contracts/test/EVMPrecompileTester.js b/contracts/test/EVMPrecompileTester.js index 34551f898..c094334f8 100644 --- a/contracts/test/EVMPrecompileTester.js +++ b/contracts/test/EVMPrecompileTester.js @@ -19,15 +19,15 @@ describe("EVM Test", function () { console.log("ERC20 address is:"); console.log(contractAddress); await sleep(1000); - + // Create a signer [signer, signer2] = await ethers.getSigners(); owner = await signer.getAddress(); owner2 = await signer2.getAddress(); - + const contractABIPath = path.join(__dirname, '../../precompiles/common/erc20_abi.json'); const contractABI = require(contractABIPath); - + // Get a contract instance erc20 = new ethers.Contract(contractAddress, contractABI, signer); @@ -45,7 +45,7 @@ describe("EVM Test", function () { const receipt2 = await tx2.wait(); expect(receipt2.status).to.equal(1); }); - + it("Transfer function", async function() { const beforeBalance = await erc20.balanceOf(owner); const tx = await erc20.transfer(owner2, 1); @@ -77,13 +77,13 @@ describe("EVM Test", function () { expect(approveReceipt.status).to.equal(1); expect(await erc20.allowance(owner, owner2)).to.equal(100); - const erc20AsOwner2 = erc20.connect(signer2); + const erc20AsOwner2 = erc20.connect(signer2); + - // transfer from owner to owner2 const balanceBefore = await erc20.balanceOf(owner2); const transferFromTx = await erc20AsOwner2.transferFrom(owner, owner2, 100); - + // await sleep(3000); const transferFromReceipt = await transferFromTx.wait(); expect(transferFromReceipt.status).to.equal(1); @@ -91,17 +91,17 @@ describe("EVM Test", function () { const diff = balanceAfter - balanceBefore; expect(diff).to.equal(100); }); - + it("Balance of function", async function() { const balance = await erc20.balanceOf(owner); expect(balance).to.be.greaterThan(Number(0)); }); - + it("Name function", async function () { const name = await erc20.name() expect(name).to.equal('UATOM'); }); - + it("Symbol function", async function () { const symbol = await erc20.symbol() // expect symbol to be 'UATOM' @@ -117,17 +117,17 @@ describe("EVM Test", function () { before(async function() { govProposal = readDeploymentOutput('gov_proposal_output.txt'); await sleep(1000); - + // Create a proposal const [signer, _] = await ethers.getSigners(); owner = await signer.getAddress(); - + const contractABIPath = path.join(__dirname, '../../precompiles/gov/abi.json'); const contractABI = require(contractABIPath); // Get a contract instance gov = new ethers.Contract(GovPrecompileContract, contractABI, signer); }); - + it("Gov deposit", async function () { const depositAmount = ethers.parseEther('0.01'); const deposit = await gov.deposit(govProposal, { @@ -231,6 +231,122 @@ describe("EVM Test", function () { } }); }); + + describe("EVM Wasm Precompile Tester", function () { + const WasmPrecompileContract = '0x0000000000000000000000000000000000001002'; + before(async function() { + wasmContractAddress = readDeploymentOutput('wasm_contract_addr.txt'); + wasmCodeID = parseInt(readDeploymentOutput('wasm_code_id.txt')); + + const [signer, _] = await ethers.getSigners(); + owner = await signer.getAddress(); + + const contractABIPath = path.join(__dirname, '../../precompiles/wasmd/abi.json'); + const contractABI = require(contractABIPath); + // Get a contract instance + wasmd = new ethers.Contract(WasmPrecompileContract, contractABI, signer); + }); + + it("Wasm Precompile Instantiate", async function () { + encoder = new TextEncoder(); + + queryCountMsg = {get_count: {}}; + queryStr = JSON.stringify(queryCountMsg); + queryBz = encoder.encode(queryStr); + + instantiateMsg = {count: 2}; + instantiateStr = JSON.stringify(instantiateMsg); + instantiateBz = encoder.encode(instantiateStr); + + coins = []; + coinsStr = JSON.stringify(coins); + coinsBz = encoder.encode(coinsStr); + + instantiate = await wasmd.instantiate(wasmCodeID, "", instantiateBz, "counter-contract", coinsBz); + const receipt = await instantiate.wait(); + expect(receipt.status).to.equal(1); + // TODO: is there any way to get the instantiate results for contract address - or in events? + }); + + it("Wasm Precompile Execute", async function () { + expect(wasmContractAddress).to.not.be.empty; + encoder = new TextEncoder(); + + queryCountMsg = {get_count: {}}; + queryStr = JSON.stringify(queryCountMsg); + queryBz = encoder.encode(queryStr); + initialCountBz = await wasmd.query(wasmContractAddress, queryBz); + initialCount = parseHexToJSON(initialCountBz) + + incrementMsg = {increment: {}}; + incrementStr = JSON.stringify(incrementMsg); + incrementBz = encoder.encode(incrementStr); + + coins = []; + coinsStr = JSON.stringify(coins); + coinsBz = encoder.encode(coinsStr); + + execute = await wasmd.execute(wasmContractAddress, incrementBz, coinsBz); + const receipt = await execute.wait(); + expect(receipt.status).to.equal(1); + + finalCountBz = await wasmd.query(wasmContractAddress, queryBz); + finalCount = parseHexToJSON(finalCountBz) + expect(finalCount.count).to.equal(initialCount.count + 1); + }); + + it("Wasm Precompile Batch Execute", async function () { + expect(wasmContractAddress).to.not.be.empty; + encoder = new TextEncoder(); + + queryCountMsg = {get_count: {}}; + queryStr = JSON.stringify(queryCountMsg); + queryBz = encoder.encode(queryStr); + initialCountBz = await wasmd.query(wasmContractAddress, queryBz); + initialCount = parseHexToJSON(initialCountBz) + + incrementMsg = {increment: {}}; + incrementStr = JSON.stringify(incrementMsg); + incrementBz = encoder.encode(incrementStr); + + coins = []; + coinsStr = JSON.stringify(coins); + coinsBz = encoder.encode(coinsStr); + + executeBatch = [ + { + contractAddress: wasmContractAddress, + msg: incrementBz, + coins: coinsBz, + }, + { + contractAddress: wasmContractAddress, + msg: incrementBz, + coins: coinsBz, + }, + { + contractAddress: wasmContractAddress, + msg: incrementBz, + coins: coinsBz, + }, + { + contractAddress: wasmContractAddress, + msg: incrementBz, + coins: coinsBz, + }, + ]; + + executeBatch = await wasmd.execute_batch(executeBatch); + const receipt = await executeBatch.wait(); + expect(receipt.status).to.equal(1); + + finalCountBz = await wasmd.query(wasmContractAddress, queryBz); + finalCount = parseHexToJSON(finalCountBz) + expect(finalCount.count).to.equal(initialCount.count + 4); + }); + + }); + }); }); @@ -251,3 +367,12 @@ function readDeploymentOutput(fileName) { } return fileContent; } + +function parseHexToJSON(hexStr) { + // Remove the 0x prefix + hexStr = hexStr.slice(2); + // Convert to bytes + const bytes = Buffer.from(hexStr, 'hex'); + // Convert to JSON + return JSON.parse(bytes.toString()); +} \ No newline at end of file diff --git a/contracts/test/deploy_wasm_contract.sh b/contracts/test/deploy_wasm_contract.sh new file mode 100644 index 000000000..b35df1703 --- /dev/null +++ b/contracts/test/deploy_wasm_contract.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +seidbin=$(which ~/go/bin/seid | tr -d '"') +keyname=$(printf "12345678\n" | $seidbin keys list --output json | jq ".[0].name" | tr -d '"') +keyaddress=$(printf "12345678\n" | $seidbin keys list --output json | jq ".[0].address" | tr -d '"') +chainid=$($seidbin status | jq ".NodeInfo.network" | tr -d '"') +seihome=$(git rev-parse --show-toplevel | tr -d '"') + +cd $seihome || exit +echo "Deploying wasm counter contract" + +echo "Storing wasm counter contract" +store_result=$(printf "12345678\n" | $seidbin tx wasm store integration_test/contracts/counter_parallel.wasm -y --from="$keyname" --chain-id="$chainid" --gas=5000000 --fees=1000000usei --broadcast-mode=block --output=json) +contract_id=$(echo "$store_result" | jq -r '.logs[].events[].attributes[] | select(.key == "code_id").value') +echo "$contract_id" > contracts/wasm_code_id.txt +echo "Instantiating wasm counter contract" +instantiate_result=$(printf "12345678\n" | $seidbin tx wasm instantiate "$contract_id" '{"count": 0}' -y --no-admin --from="$keyname" --chain-id="$chainid" --gas=5000000 --fees=1000000usei --broadcast-mode=block --label=dex --output=json) +echo $instantiate_result + +contract_addr=$(echo "$instantiate_result" |jq -r 'first(.logs[].events[].attributes[] | select(.key == "_contract_address").value)') + +echo "Deployed counter contract with address:" +echo $contract_addr + +# write the contract address to wasm_contract_addr.txt +echo "$contract_addr" > contracts/wasm_contract_addr.txt \ No newline at end of file diff --git a/integration_test/evm_module/hardhat_test.yaml b/integration_test/evm_module/hardhat_test.yaml index 344396b47..14e7c153e 100644 --- a/integration_test/evm_module/hardhat_test.yaml +++ b/integration_test/evm_module/hardhat_test.yaml @@ -11,6 +11,7 @@ - cmd: bash contracts/test/get_validator_address.sh - cmd: bash contracts/test/send_gov_proposal.sh - cmd: bash contracts/test/query_oracle_data.sh + - cmd: bash contracts/test/deploy_wasm_contract.sh verifiers: - type: eval expr: RESULT == "0x1" diff --git a/precompiles/wasmd/Wasmd.sol b/precompiles/wasmd/Wasmd.sol index f932e9a32..58d685188 100644 --- a/precompiles/wasmd/Wasmd.sol +++ b/precompiles/wasmd/Wasmd.sol @@ -23,6 +23,14 @@ interface IWasmd { bytes memory coins ) payable external returns (bytes memory response); + struct ExecuteMsg { + string contractAddress; + bytes msg; + bytes coins; + } + + function execute_batch(ExecuteMsg[] memory executeMsgs) payable external returns (bytes[] memory responses); + // Queries function query(string memory contractAddress, bytes memory req) external view returns (bytes memory response); } diff --git a/precompiles/wasmd/abi.json b/precompiles/wasmd/abi.json old mode 100755 new mode 100644 index 384d896a7..c5e5b1b3f --- a/precompiles/wasmd/abi.json +++ b/precompiles/wasmd/abi.json @@ -1 +1 @@ -[{"inputs":[{"internalType":"string","name":"contractAddress","type":"string"},{"internalType":"bytes","name":"msg","type":"bytes"},{"internalType":"bytes","name":"coins","type":"bytes"}],"name":"execute","outputs":[{"internalType":"bytes","name":"response","type":"bytes"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint64","name":"codeID","type":"uint64"},{"internalType":"string","name":"admin","type":"string"},{"internalType":"bytes","name":"msg","type":"bytes"},{"internalType":"string","name":"label","type":"string"},{"internalType":"bytes","name":"coins","type":"bytes"}],"name":"instantiate","outputs":[{"internalType":"string","name":"contractAddr","type":"string"},{"internalType":"bytes","name":"data","type":"bytes"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"string","name":"contractAddress","type":"string"},{"internalType":"bytes","name":"req","type":"bytes"}],"name":"query","outputs":[{"internalType":"bytes","name":"response","type":"bytes"}],"stateMutability":"view","type":"function"}] \ No newline at end of file +[{"inputs":[{"internalType":"string","name":"contractAddress","type":"string"},{"internalType":"bytes","name":"msg","type":"bytes"},{"internalType":"bytes","name":"coins","type":"bytes"}],"name":"execute","outputs":[{"internalType":"bytes","name":"response","type":"bytes"}],"stateMutability":"payable","type":"function"},{"inputs":[{"components":[{"internalType":"string","name":"contractAddress","type":"string"},{"internalType":"bytes","name":"msg","type":"bytes"},{"internalType":"bytes","name":"coins","type":"bytes"}],"internalType":"struct IWasmd.ExecuteMsg[]","name":"executeMsgs","type":"tuple[]"}],"name":"execute_batch","outputs":[{"internalType":"bytes[]","name":"responses","type":"bytes[]"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint64","name":"codeID","type":"uint64"},{"internalType":"string","name":"admin","type":"string"},{"internalType":"bytes","name":"msg","type":"bytes"},{"internalType":"string","name":"label","type":"string"},{"internalType":"bytes","name":"coins","type":"bytes"}],"name":"instantiate","outputs":[{"internalType":"string","name":"contractAddr","type":"string"},{"internalType":"bytes","name":"data","type":"bytes"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"string","name":"contractAddress","type":"string"},{"internalType":"bytes","name":"req","type":"bytes"}],"name":"query","outputs":[{"internalType":"bytes","name":"response","type":"bytes"}],"stateMutability":"view","type":"function"}] \ No newline at end of file diff --git a/precompiles/wasmd/wasmd.go b/precompiles/wasmd/wasmd.go index ad77fc3e2..06d9db253 100644 --- a/precompiles/wasmd/wasmd.go +++ b/precompiles/wasmd/wasmd.go @@ -16,12 +16,14 @@ import ( "github.com/ethereum/go-ethereum/core/vm" pcommon "github.com/sei-protocol/sei-chain/precompiles/common" "github.com/sei-protocol/sei-chain/utils" + "github.com/sei-protocol/sei-chain/x/evm/state" ) const ( - InstantiateMethod = "instantiate" - ExecuteMethod = "execute" - QueryMethod = "query" + InstantiateMethod = "instantiate" + ExecuteMethod = "execute" + ExecuteBatchMethod = "execute_batch" + QueryMethod = "query" ) const WasmdAddress = "0x0000000000000000000000000000000000001002" @@ -42,9 +44,16 @@ type Precompile struct { wasmdViewKeeper pcommon.WasmdViewKeeper address common.Address - InstantiateID []byte - ExecuteID []byte - QueryID []byte + InstantiateID []byte + ExecuteID []byte + ExecuteBatchID []byte + QueryID []byte +} + +type ExecuteMsg struct { + ContractAddress string `json:"contractAddress"` + Msg []byte `json:"msg"` + Coins []byte `json:"coins"` } func NewPrecompile(evmKeeper pcommon.EVMKeeper, wasmdKeeper pcommon.WasmdKeeper, wasmdViewKeeper pcommon.WasmdViewKeeper, bankKeeper pcommon.BankKeeper) (*Precompile, error) { @@ -69,11 +78,13 @@ func NewPrecompile(evmKeeper pcommon.EVMKeeper, wasmdKeeper pcommon.WasmdKeeper, for name, m := range newAbi.Methods { switch name { - case "instantiate": + case InstantiateMethod: p.InstantiateID = m.ID - case "execute": + case ExecuteMethod: p.ExecuteID = m.ID - case "query": + case ExecuteBatchMethod: + p.ExecuteBatchID = m.ID + case QueryMethod: p.QueryID = m.ID } } @@ -101,6 +112,8 @@ func (Precompile) IsTransaction(method string) bool { switch method { case ExecuteMethod: return true + case ExecuteBatchMethod: + return true case InstantiateMethod: return true default: @@ -129,6 +142,8 @@ func (p Precompile) RunAndCalculateGas(evm *vm.EVM, caller common.Address, calli return p.instantiate(ctx, method, caller, callingContract, args, value, readOnly) case ExecuteMethod: return p.execute(ctx, method, caller, callingContract, args, value, readOnly) + case ExecuteBatchMethod: + return p.execute_batch(ctx, method, caller, callingContract, args, value, readOnly) case QueryMethod: return p.query(ctx, method, args, value) } @@ -187,8 +202,9 @@ func (p Precompile) instantiate(ctx sdk.Context, method *abi.Method, caller comm rerr = err return } - if !coins.AmountOf(sdk.MustGetBaseDenom()).IsZero() { - rerr = errors.New("deposit of usei must be done through the `value` field") + coinsValue := coins.AmountOf(sdk.MustGetBaseDenom()).Mul(state.SdkUseiToSweiMultiplier).BigInt() + if (value == nil && coinsValue.Sign() == 1) || (value != nil && coinsValue.Cmp(value) != 0) { + rerr = errors.New("coin amount must equal value specified") return } @@ -206,14 +222,19 @@ func (p Precompile) instantiate(ctx sdk.Context, method *abi.Method, caller comm rerr = err return } - - if value != nil { - coin, err := pcommon.HandlePaymentUsei(ctx, p.evmKeeper.GetSeiAddressOrDefault(ctx, p.address), creatorAddr, value, p.bankKeeper) + useiAmt := coins.AmountOf(sdk.MustGetBaseDenom()) + if value != nil && !useiAmt.IsZero() { + useiAmtAsWei := useiAmt.Mul(state.SdkUseiToSweiMultiplier).BigInt() + coin, err := pcommon.HandlePaymentUsei(ctx, p.evmKeeper.GetSeiAddressOrDefault(ctx, p.address), creatorAddr, useiAmtAsWei, p.bankKeeper) if err != nil { rerr = err return } - coins = coins.Add(coin) + // sanity check coin amounts match + if !coin.Amount.Equal(useiAmt) { + rerr = errors.New("mismatch between coins and payment value") + return + } } addr, data, err := p.wasmdKeeper.Instantiate(ctx, codeID, creatorAddr, adminAddr, msg, label, coins) @@ -226,6 +247,125 @@ func (p Precompile) instantiate(ctx sdk.Context, method *abi.Method, caller comm return } +func (p Precompile) execute_batch(ctx sdk.Context, method *abi.Method, caller common.Address, callingContract common.Address, args []interface{}, value *big.Int, readOnly bool) (ret []byte, remainingGas uint64, rerr error) { + defer func() { + if err := recover(); err != nil { + ret = nil + remainingGas = 0 + rerr = fmt.Errorf("%s", err) + return + } + }() + if readOnly { + rerr = errors.New("cannot call execute from staticcall") + return + } + + if err := pcommon.ValidateArgsLength(args, 1); err != nil { + rerr = err + return + } + + executeMsgs := args[0].([]struct { + ContractAddress string `json:"contractAddress"` + Msg []byte `json:"msg"` + Coins []byte `json:"coins"` + }) + + responses := make([][]byte, 0, len(executeMsgs)) + + // validate coins add up to value + validateValue := big.NewInt(0) + for i := 0; i < len(executeMsgs); i++ { + executeMsg := ExecuteMsg(executeMsgs[i]) + coinsBz := executeMsg.Coins + coins := sdk.NewCoins() + if err := json.Unmarshal(coinsBz, &coins); err != nil { + rerr = err + return + } + messageAmount := coins.AmountOf(sdk.MustGetBaseDenom()).Mul(state.SdkUseiToSweiMultiplier).BigInt() + validateValue.Add(validateValue, messageAmount) + } + // if validateValue is greater than zero, then value must be provided, and they must be equal + if (value == nil && validateValue.Sign() == 1) || (value != nil && validateValue.Cmp(value) != 0) { + rerr = errors.New("sum of coin amounts must equal value specified") + return + } + for i := 0; i < len(executeMsgs); i++ { + executeMsg := ExecuteMsg(executeMsgs[i]) + + // type assertion will always succeed because it's already validated in p.Prepare call in Run() + contractAddrStr := executeMsg.ContractAddress + if caller.Cmp(callingContract) != 0 { + erc20pointer, _, erc20exists := p.evmKeeper.GetERC20CW20Pointer(ctx, contractAddrStr) + erc721pointer, _, erc721exists := p.evmKeeper.GetERC721CW721Pointer(ctx, contractAddrStr) + if (!erc20exists || erc20pointer.Cmp(callingContract) != 0) && (!erc721exists || erc721pointer.Cmp(callingContract) != 0) { + return nil, 0, fmt.Errorf("%s is not a pointer of %s", callingContract.Hex(), contractAddrStr) + } + } + + contractAddr, err := sdk.AccAddressFromBech32(contractAddrStr) + if err != nil { + rerr = err + return + } + senderAddr := p.evmKeeper.GetSeiAddressOrDefault(ctx, caller) + msg := executeMsg.Msg + coinsBz := executeMsg.Coins + coins := sdk.NewCoins() + if err := json.Unmarshal(coinsBz, &coins); err != nil { + rerr = err + return + } + useiAmt := coins.AmountOf(sdk.MustGetBaseDenom()) + if value != nil && !useiAmt.IsZero() { + // process coin amount from the value provided + useiAmtAsWei := useiAmt.Mul(state.SdkUseiToSweiMultiplier).BigInt() + coin, err := pcommon.HandlePaymentUsei(ctx, p.evmKeeper.GetSeiAddressOrDefault(ctx, p.address), senderAddr, useiAmtAsWei, p.bankKeeper) + if err != nil { + rerr = err + return + } + value.Sub(value, useiAmtAsWei) + if value.Sign() == -1 { + rerr = errors.New("insufficient value provided for payment") + return + } + // sanity check coin amounts match + if !coin.Amount.Equal(useiAmt) { + rerr = errors.New("mismatch between coins and payment value") + return + } + } + // Run basic validation, can also just expose validateLabel and validate validateWasmCode in sei-wasmd + msgExecute := wasmtypes.MsgExecuteContract{ + Sender: senderAddr.String(), + Contract: contractAddr.String(), + Msg: msg, + Funds: coins, + } + if err := msgExecute.ValidateBasic(); err != nil { + rerr = err + return + } + + res, err := p.wasmdKeeper.Execute(ctx, contractAddr, senderAddr, msg, coins) + if err != nil { + rerr = err + return + } + responses = append(responses, res) + } + if value != nil && value.Sign() != 0 { + rerr = errors.New("value remaining after execution, must match provided amounts exactly") + return + } + ret, rerr = method.Outputs.Pack(responses) + remainingGas = pcommon.GetRemainingGas(ctx, p.evmKeeper) + return +} + func (p Precompile) execute(ctx sdk.Context, method *abi.Method, caller common.Address, callingContract common.Address, args []interface{}, value *big.Int, readOnly bool) (ret []byte, remainingGas uint64, rerr error) { defer func() { if err := recover(); err != nil { @@ -271,10 +411,12 @@ func (p Precompile) execute(ctx sdk.Context, method *abi.Method, caller common.A rerr = err return } - if !coins.AmountOf(sdk.MustGetBaseDenom()).IsZero() { - rerr = errors.New("deposit of usei must be done through the `value` field") + coinsValue := coins.AmountOf(sdk.MustGetBaseDenom()).Mul(state.SdkUseiToSweiMultiplier).BigInt() + if (value == nil && coinsValue.Sign() == 1) || (value != nil && coinsValue.Cmp(value) != 0) { + rerr = errors.New("coin amount must equal value specified") return } + // Run basic validation, can also just expose validateLabel and validate validateWasmCode in sei-wasmd msgExecute := wasmtypes.MsgExecuteContract{ Sender: senderAddr.String(), @@ -288,13 +430,19 @@ func (p Precompile) execute(ctx sdk.Context, method *abi.Method, caller common.A return } - if value != nil { - coin, err := pcommon.HandlePaymentUsei(ctx, p.evmKeeper.GetSeiAddressOrDefault(ctx, p.address), senderAddr, value, p.bankKeeper) + useiAmt := coins.AmountOf(sdk.MustGetBaseDenom()) + if value != nil && !useiAmt.IsZero() { + useiAmtAsWei := useiAmt.Mul(state.SdkUseiToSweiMultiplier).BigInt() + coin, err := pcommon.HandlePaymentUsei(ctx, p.evmKeeper.GetSeiAddressOrDefault(ctx, p.address), senderAddr, useiAmtAsWei, p.bankKeeper) if err != nil { rerr = err return } - coins = coins.Add(coin) + // sanity check coin amounts match + if !coin.Amount.Equal(useiAmt) { + rerr = errors.New("mismatch between coins and payment value") + return + } } res, err := p.wasmdKeeper.Execute(ctx, contractAddr, senderAddr, msg, coins) if err != nil { diff --git a/precompiles/wasmd/wasmd_test.go b/precompiles/wasmd/wasmd_test.go index 2676802cd..9322386fc 100644 --- a/precompiles/wasmd/wasmd_test.go +++ b/precompiles/wasmd/wasmd_test.go @@ -24,6 +24,7 @@ func TestRequiredGas(t *testing.T) { require.Nil(t, err) require.Equal(t, uint64(2000), p.RequiredGas(p.ExecuteID)) require.Equal(t, uint64(2000), p.RequiredGas(p.InstantiateID)) + require.Equal(t, uint64(2000), p.RequiredGas(p.ExecuteBatchID)) require.Equal(t, uint64(1000), p.RequiredGas(p.QueryID)) require.Equal(t, uint64(3000), p.RequiredGas([]byte{15, 15, 15, 15})) // invalid method } @@ -49,7 +50,12 @@ func TestInstantiate(t *testing.T) { require.Nil(t, err) instantiateMethod, err := p.ABI.MethodById(p.InstantiateID) require.Nil(t, err) - amtsbz, err := sdk.NewCoins().MarshalJSON() + amts := sdk.NewCoins(sdk.NewCoin("usei", sdk.NewInt(1000))) + amtsbz, err := amts.MarshalJSON() + testApp.BankKeeper.MintCoins(ctx, "evm", amts) + testApp.BankKeeper.SendCoinsFromModuleToAccount(ctx, "evm", mockAddr, amts) + testApp.BankKeeper.MintCoins(ctx, "evm", amts) + testApp.BankKeeper.SendCoinsFromModuleToAccount(ctx, "evm", mockAddr, amts) require.Nil(t, err) args, err := instantiateMethod.Inputs.Pack( codeID, @@ -63,14 +69,38 @@ func TestInstantiate(t *testing.T) { evm := vm.EVM{ StateDB: statedb, } + testApp.BankKeeper.SendCoins(ctx, mockAddr, testApp.EvmKeeper.GetSeiAddressOrDefault(ctx, common.HexToAddress(wasmd.WasmdAddress)), amts) suppliedGas := uint64(1000000) - res, g, err := p.RunAndCalculateGas(&evm, mockEVMAddr, mockEVMAddr, append(p.InstantiateID, args...), suppliedGas, nil, nil, false) + res, g, err := p.RunAndCalculateGas(&evm, mockEVMAddr, mockEVMAddr, append(p.InstantiateID, args...), suppliedGas, big.NewInt(1000_000_000_000_000), nil, false) require.Nil(t, err) outputs, err := instantiateMethod.Outputs.Unpack(res) require.Nil(t, err) require.Equal(t, 2, len(outputs)) require.Equal(t, "sei1hrpna9v7vs3stzyd4z3xf00676kf78zpe2u5ksvljswn2vnjp3yslucc3n", outputs[0].(string)) require.Empty(t, outputs[1].([]byte)) + require.Equal(t, uint64(879782), g) + + amtsbz, err = sdk.NewCoins().MarshalJSON() + require.Nil(t, err) + args, err = instantiateMethod.Inputs.Pack( + codeID, + mockAddr.String(), + []byte("{}"), + "test", + amtsbz, + ) + require.Nil(t, err) + statedb = state.NewDBImpl(ctx, &testApp.EvmKeeper, true) + evm = vm.EVM{ + StateDB: statedb, + } + res, g, err = p.RunAndCalculateGas(&evm, mockEVMAddr, mockEVMAddr, append(p.InstantiateID, args...), suppliedGas, nil, nil, false) + require.Nil(t, err) + outputs, err = instantiateMethod.Outputs.Unpack(res) + require.Nil(t, err) + require.Equal(t, 2, len(outputs)) + require.Equal(t, "sei1hrpna9v7vs3stzyd4z3xf00676kf78zpe2u5ksvljswn2vnjp3yslucc3n", outputs[0].(string)) + require.Empty(t, outputs[1].([]byte)) require.Equal(t, uint64(902838), g) // non-existent code ID @@ -126,13 +156,6 @@ func TestExecute(t *testing.T) { } suppliedGas := uint64(1000000) testApp.BankKeeper.SendCoins(ctx, mockAddr, testApp.EvmKeeper.GetSeiAddressOrDefault(ctx, common.HexToAddress(wasmd.WasmdAddress)), amts) - _, _, err = p.RunAndCalculateGas(&evm, mockEVMAddr, mockEVMAddr, append(p.ExecuteID, args...), suppliedGas, nil, nil, false) - require.NotNil(t, err) // used coins instead of `value` to send usei to the contract - - amtsbz, err = sdk.NewCoins().MarshalJSON() - require.Nil(t, err) - args, err = executeMethod.Inputs.Pack(contractAddr.String(), []byte("{\"echo\":{\"message\":\"test msg\"}}"), amtsbz) - require.Nil(t, err) res, g, err := p.RunAndCalculateGas(&evm, mockEVMAddr, mockEVMAddr, append(p.ExecuteID, args...), suppliedGas, big.NewInt(1000_000_000_000_000), nil, false) require.Nil(t, err) outputs, err := executeMethod.Outputs.Unpack(res) @@ -142,6 +165,25 @@ func TestExecute(t *testing.T) { require.Equal(t, uint64(906041), g) require.Equal(t, int64(1000), testApp.BankKeeper.GetBalance(statedb.Ctx(), contractAddr, "usei").Amount.Int64()) + amtsbz, err = sdk.NewCoins().MarshalJSON() + require.Nil(t, err) + args, err = executeMethod.Inputs.Pack(contractAddr.String(), []byte("{\"echo\":{\"message\":\"test msg\"}}"), amtsbz) + require.Nil(t, err) + _, _, err = p.RunAndCalculateGas(&evm, mockEVMAddr, mockEVMAddr, append(p.ExecuteID, args...), suppliedGas, big.NewInt(1000_000_000_000_000), nil, false) + require.NotNil(t, err) // used coins instead of `value` to send usei to the contract + + args, err = executeMethod.Inputs.Pack(contractAddr.String(), []byte("{\"echo\":{\"message\":\"test msg\"}}"), amtsbz) + require.Nil(t, err) + _, _, err = p.RunAndCalculateGas(&evm, mockEVMAddr, mockEVMAddr, append(p.ExecuteID, args...), suppliedGas, big.NewInt(1000_000_000_000_000), nil, false) + require.NotNil(t, err) + + amtsbz, err = sdk.NewCoins().MarshalJSON() + require.Nil(t, err) + args, err = executeMethod.Inputs.Pack(contractAddr.String(), []byte("{\"echo\":{\"message\":\"test msg\"}}"), amtsbz) + require.Nil(t, err) + _, _, err = p.RunAndCalculateGas(&evm, mockEVMAddr, mockEVMAddr, append(p.ExecuteID, args...), suppliedGas, big.NewInt(1000_000_000_000_000), nil, false) + require.NotNil(t, err) + // allowed delegatecall contractAddrAllowed := common.BytesToAddress([]byte("contractA")) testApp.EvmKeeper.SetERC20CW20Pointer(ctx, contractAddr.String(), contractAddrAllowed) @@ -218,3 +260,245 @@ func TestQuery(t *testing.T) { require.NotNil(t, err) require.Equal(t, uint64(0), g) } + +func TestExecuteBatchOneMessage(t *testing.T) { + testApp := app.Setup(false, false) + mockAddr, mockEVMAddr := testkeeper.MockAddressPair() + ctx := testApp.GetContextForDeliverTx([]byte{}).WithBlockTime(time.Now()) + testApp.EvmKeeper.SetAddressMapping(ctx, mockAddr, mockEVMAddr) + wasmKeeper := wasmkeeper.NewDefaultPermissionKeeper(testApp.WasmKeeper) + p, err := wasmd.NewPrecompile(&testApp.EvmKeeper, wasmKeeper, testApp.WasmKeeper, testApp.BankKeeper) + require.Nil(t, err) + code, err := os.ReadFile("../../example/cosmwasm/echo/artifacts/echo.wasm") + require.Nil(t, err) + codeID, err := wasmKeeper.Create(ctx, mockAddr, code, nil) + require.Nil(t, err) + contractAddr, _, err := wasmKeeper.Instantiate(ctx, codeID, mockAddr, mockAddr, []byte("{}"), "test", sdk.NewCoins()) + require.Nil(t, err) + + amts := sdk.NewCoins(sdk.NewCoin("usei", sdk.NewInt(1000))) + testApp.BankKeeper.MintCoins(ctx, "evm", amts) + testApp.BankKeeper.SendCoinsFromModuleToAccount(ctx, "evm", mockAddr, amts) + testApp.BankKeeper.MintCoins(ctx, "evm", amts) + testApp.BankKeeper.SendCoinsFromModuleToAccount(ctx, "evm", mockAddr, amts) + amtsbz, err := amts.MarshalJSON() + require.Nil(t, err) + executeMethod, err := p.ABI.MethodById(p.ExecuteBatchID) + require.Nil(t, err) + executeMsg := wasmd.ExecuteMsg{ + ContractAddress: contractAddr.String(), + Msg: []byte("{\"echo\":{\"message\":\"test msg\"}}"), + Coins: amtsbz, + } + args, err := executeMethod.Inputs.Pack([]wasmd.ExecuteMsg{executeMsg}) + require.Nil(t, err) + statedb := state.NewDBImpl(ctx, &testApp.EvmKeeper, true) + evm := vm.EVM{ + StateDB: statedb, + } + suppliedGas := uint64(1000000) + testApp.BankKeeper.SendCoins(ctx, mockAddr, testApp.EvmKeeper.GetSeiAddressOrDefault(ctx, common.HexToAddress(wasmd.WasmdAddress)), amts) + res, g, err := p.RunAndCalculateGas(&evm, mockEVMAddr, mockEVMAddr, append(p.ExecuteBatchID, args...), suppliedGas, big.NewInt(1000_000_000_000_000), nil, false) + require.Nil(t, err) + outputs, err := executeMethod.Outputs.Unpack(res) + require.Nil(t, err) + require.Equal(t, 1, len(outputs)) + require.Equal(t, fmt.Sprintf("received test msg from %s with 1000usei", mockAddr.String()), string((outputs[0].([][]byte))[0])) + require.Equal(t, uint64(906041), g) + require.Equal(t, int64(1000), testApp.BankKeeper.GetBalance(statedb.Ctx(), contractAddr, "usei").Amount.Int64()) + + amtsbz, err = sdk.NewCoins().MarshalJSON() + require.Nil(t, err) + executeMsg = wasmd.ExecuteMsg{ + ContractAddress: contractAddr.String(), + Msg: []byte("{\"echo\":{\"message\":\"test msg\"}}"), + Coins: amtsbz, + } + args, err = executeMethod.Inputs.Pack([]wasmd.ExecuteMsg{executeMsg}) + require.Nil(t, err) + _, _, err = p.RunAndCalculateGas(&evm, mockEVMAddr, mockEVMAddr, append(p.ExecuteBatchID, args...), suppliedGas, big.NewInt(1000_000_000_000_000), nil, false) + require.NotNil(t, err) // value and amounts not equal + + // allowed delegatecall + contractAddrAllowed := common.BytesToAddress([]byte("contractA")) + testApp.EvmKeeper.SetERC20CW20Pointer(ctx, contractAddr.String(), contractAddrAllowed) + _, _, err = p.RunAndCalculateGas(&evm, mockEVMAddr, contractAddrAllowed, append(p.ExecuteBatchID, args...), suppliedGas, nil, nil, false) + require.Nil(t, err) + + // disallowed delegatecall + contractAddrDisallowed := common.BytesToAddress([]byte("contractB")) + _, _, err = p.RunAndCalculateGas(&evm, mockEVMAddr, contractAddrDisallowed, append(p.ExecuteBatchID, args...), suppliedGas, nil, nil, false) + require.NotNil(t, err) + + // bad contract address + executeMsg = wasmd.ExecuteMsg{ + ContractAddress: mockAddr.String(), + Msg: []byte("{\"echo\":{\"message\":\"test msg\"}}"), + Coins: amtsbz, + } + args, _ = executeMethod.Inputs.Pack([]wasmd.ExecuteMsg{executeMsg}) + _, g, err = p.RunAndCalculateGas(&evm, mockEVMAddr, mockEVMAddr, append(p.ExecuteBatchID, args...), suppliedGas, nil, nil, false) + require.NotNil(t, err) + require.Equal(t, uint64(0), g) + + // bad inputs + executeMsg = wasmd.ExecuteMsg{ + ContractAddress: "not bech32", + Msg: []byte("{\"echo\":{\"message\":\"test msg\"}}"), + Coins: amtsbz, + } + args, _ = executeMethod.Inputs.Pack([]wasmd.ExecuteMsg{executeMsg}) + _, g, err = p.RunAndCalculateGas(&evm, mockEVMAddr, mockEVMAddr, append(p.ExecuteBatchID, args...), suppliedGas, nil, nil, false) + require.NotNil(t, err) + require.Equal(t, uint64(0), g) + executeMsg = wasmd.ExecuteMsg{ + ContractAddress: contractAddr.String(), + Msg: []byte("{\"echo\":{\"message\":\"test msg\"}}"), + Coins: []byte("bad coins"), + } + args, _ = executeMethod.Inputs.Pack([]wasmd.ExecuteMsg{executeMsg}) + _, g, err = p.RunAndCalculateGas(&evm, mockEVMAddr, mockEVMAddr, append(p.ExecuteBatchID, args...), suppliedGas, nil, nil, false) + require.NotNil(t, err) + require.Equal(t, uint64(0), g) +} + +func TestExecuteBatchMultipleMessages(t *testing.T) { + testApp := app.Setup(false, false) + mockAddr, mockEVMAddr := testkeeper.MockAddressPair() + ctx := testApp.GetContextForDeliverTx([]byte{}).WithBlockTime(time.Now()) + testApp.EvmKeeper.SetAddressMapping(ctx, mockAddr, mockEVMAddr) + wasmKeeper := wasmkeeper.NewDefaultPermissionKeeper(testApp.WasmKeeper) + p, err := wasmd.NewPrecompile(&testApp.EvmKeeper, wasmKeeper, testApp.WasmKeeper, testApp.BankKeeper) + require.Nil(t, err) + code, err := os.ReadFile("../../example/cosmwasm/echo/artifacts/echo.wasm") + require.Nil(t, err) + codeID, err := wasmKeeper.Create(ctx, mockAddr, code, nil) + require.Nil(t, err) + contractAddr, _, err := wasmKeeper.Instantiate(ctx, codeID, mockAddr, mockAddr, []byte("{}"), "test", sdk.NewCoins()) + require.Nil(t, err) + + amts := sdk.NewCoins(sdk.NewCoin("usei", sdk.NewInt(1000))) + largeAmts := sdk.NewCoins(sdk.NewCoin("usei", sdk.NewInt(3000))) + testApp.BankKeeper.MintCoins(ctx, "evm", sdk.NewCoins(sdk.NewCoin("usei", sdk.NewInt(13000)))) + testApp.BankKeeper.SendCoinsFromModuleToAccount(ctx, "evm", mockAddr, sdk.NewCoins(sdk.NewCoin("usei", sdk.NewInt(13000)))) + amtsbz, err := amts.MarshalJSON() + require.Nil(t, err) + executeMethod, err := p.ABI.MethodById(p.ExecuteBatchID) + require.Nil(t, err) + executeMsgWithCoinsAmt := wasmd.ExecuteMsg{ + ContractAddress: contractAddr.String(), + Msg: []byte("{\"echo\":{\"message\":\"test msg\"}}"), + Coins: amtsbz, + } + + statedb := state.NewDBImpl(ctx, &testApp.EvmKeeper, true) + evm := vm.EVM{ + StateDB: statedb, + } + suppliedGas := uint64(1000000) + err = testApp.BankKeeper.SendCoins(ctx, mockAddr, testApp.EvmKeeper.GetSeiAddressOrDefault(ctx, common.HexToAddress(wasmd.WasmdAddress)), largeAmts) + require.Nil(t, err) + args, err := executeMethod.Inputs.Pack([]wasmd.ExecuteMsg{executeMsgWithCoinsAmt, executeMsgWithCoinsAmt, executeMsgWithCoinsAmt}) + require.Nil(t, err) + res, g, err := p.RunAndCalculateGas(&evm, mockEVMAddr, mockEVMAddr, append(p.ExecuteBatchID, args...), suppliedGas, big.NewInt(3000_000_000_000_000), nil, false) + require.Nil(t, err) + outputs, err := executeMethod.Outputs.Unpack(res) + require.Nil(t, err) + require.Equal(t, 1, len(outputs)) + parsedOutputs := outputs[0].([][]byte) + require.Equal(t, fmt.Sprintf("received test msg from %s with 1000usei", mockAddr.String()), string(parsedOutputs[0])) + require.Equal(t, fmt.Sprintf("received test msg from %s with 1000usei", mockAddr.String()), string(parsedOutputs[1])) + require.Equal(t, fmt.Sprintf("received test msg from %s with 1000usei", mockAddr.String()), string(parsedOutputs[2])) + require.Equal(t, uint64(725379), g) + require.Equal(t, int64(3000), testApp.BankKeeper.GetBalance(statedb.Ctx(), contractAddr, "usei").Amount.Int64()) + + amtsbz2, err := sdk.NewCoins().MarshalJSON() + require.Nil(t, err) + executeMsgWithNoCoins := wasmd.ExecuteMsg{ + ContractAddress: contractAddr.String(), + Msg: []byte("{\"echo\":{\"message\":\"test msg\"}}"), + Coins: amtsbz2, + } + statedb = state.NewDBImpl(ctx, &testApp.EvmKeeper, true) + evm = vm.EVM{ + StateDB: statedb, + } + err = testApp.BankKeeper.SendCoins(ctx, mockAddr, testApp.EvmKeeper.GetSeiAddressOrDefault(ctx, common.HexToAddress(wasmd.WasmdAddress)), amts) + require.Nil(t, err) + args, err = executeMethod.Inputs.Pack([]wasmd.ExecuteMsg{executeMsgWithNoCoins, executeMsgWithCoinsAmt, executeMsgWithNoCoins}) + require.Nil(t, err) + res, g, err = p.RunAndCalculateGas(&evm, mockEVMAddr, mockEVMAddr, append(p.ExecuteBatchID, args...), suppliedGas, big.NewInt(1000_000_000_000_000), nil, false) + require.Nil(t, err) + outputs, err = executeMethod.Outputs.Unpack(res) + require.Nil(t, err) + require.Equal(t, 1, len(outputs)) + parsedOutputs = outputs[0].([][]byte) + require.Equal(t, fmt.Sprintf("received test msg from %s with", mockAddr.String()), string(parsedOutputs[0])) + require.Equal(t, fmt.Sprintf("received test msg from %s with 1000usei", mockAddr.String()), string(parsedOutputs[1])) + require.Equal(t, fmt.Sprintf("received test msg from %s with", mockAddr.String()), string(parsedOutputs[2])) + require.Equal(t, uint64(773900), g) + require.Equal(t, int64(1000), testApp.BankKeeper.GetBalance(statedb.Ctx(), contractAddr, "usei").Amount.Int64()) + + // allowed delegatecall + args, err = executeMethod.Inputs.Pack([]wasmd.ExecuteMsg{executeMsgWithNoCoins, executeMsgWithNoCoins}) + require.Nil(t, err) + contractAddrAllowed := common.BytesToAddress([]byte("contractA")) + testApp.EvmKeeper.SetERC20CW20Pointer(ctx, contractAddr.String(), contractAddrAllowed) + _, _, err = p.RunAndCalculateGas(&evm, mockEVMAddr, contractAddrAllowed, append(p.ExecuteBatchID, args...), suppliedGas, nil, nil, false) + require.Nil(t, err) + + // disallowed delegatecall + contractAddrDisallowed := common.BytesToAddress([]byte("contractB")) + _, _, err = p.RunAndCalculateGas(&evm, mockEVMAddr, contractAddrDisallowed, append(p.ExecuteBatchID, args...), suppliedGas, nil, nil, false) + require.NotNil(t, err) + + // bad contract address + executeMsgBadContract := wasmd.ExecuteMsg{ + ContractAddress: mockAddr.String(), + Msg: []byte("{\"echo\":{\"message\":\"test msg\"}}"), + Coins: amtsbz, + } + statedb = state.NewDBImpl(ctx, &testApp.EvmKeeper, true) + evm = vm.EVM{ + StateDB: statedb, + } + err = testApp.BankKeeper.SendCoins(ctx, mockAddr, testApp.EvmKeeper.GetSeiAddressOrDefault(ctx, common.HexToAddress(wasmd.WasmdAddress)), largeAmts) + require.Nil(t, err) + args, err = executeMethod.Inputs.Pack([]wasmd.ExecuteMsg{executeMsgWithCoinsAmt, executeMsgBadContract, executeMsgWithCoinsAmt}) + require.Nil(t, err) + _, g, err = p.RunAndCalculateGas(&evm, mockEVMAddr, mockEVMAddr, append(p.ExecuteBatchID, args...), suppliedGas, big.NewInt(3000_000_000_000_000), nil, false) + require.NotNil(t, err) + require.Equal(t, uint64(0), g) + + // bad inputs + executeMsgBadInputs := wasmd.ExecuteMsg{ + ContractAddress: "not bech32", + Msg: []byte("{\"echo\":{\"message\":\"test msg\"}}"), + Coins: amtsbz, + } + statedb = state.NewDBImpl(ctx, &testApp.EvmKeeper, true) + evm = vm.EVM{ + StateDB: statedb, + } + err = testApp.BankKeeper.SendCoins(ctx, mockAddr, testApp.EvmKeeper.GetSeiAddressOrDefault(ctx, common.HexToAddress(wasmd.WasmdAddress)), largeAmts) + require.Nil(t, err) + args, _ = executeMethod.Inputs.Pack([]wasmd.ExecuteMsg{executeMsgWithCoinsAmt, executeMsgBadInputs, executeMsgWithCoinsAmt}) + _, g, err = p.RunAndCalculateGas(&evm, mockEVMAddr, mockEVMAddr, append(p.ExecuteBatchID, args...), suppliedGas, big.NewInt(3000_000_000_000_000), nil, false) + require.NotNil(t, err) + require.Equal(t, uint64(0), g) + executeMsgBadInputCoins := wasmd.ExecuteMsg{ + ContractAddress: contractAddr.String(), + Msg: []byte("{\"echo\":{\"message\":\"test msg\"}}"), + Coins: []byte("bad coins"), + } + statedb = state.NewDBImpl(ctx, &testApp.EvmKeeper, true) + evm = vm.EVM{ + StateDB: statedb, + } + err = testApp.BankKeeper.SendCoins(ctx, mockAddr, testApp.EvmKeeper.GetSeiAddressOrDefault(ctx, common.HexToAddress(wasmd.WasmdAddress)), largeAmts) + require.Nil(t, err) + args, _ = executeMethod.Inputs.Pack([]wasmd.ExecuteMsg{executeMsgWithCoinsAmt, executeMsgBadInputCoins, executeMsgWithCoinsAmt}) + _, g, err = p.RunAndCalculateGas(&evm, mockEVMAddr, mockEVMAddr, append(p.ExecuteBatchID, args...), suppliedGas, big.NewInt(3000_000_000_000_000), nil, false) + require.NotNil(t, err) + require.Equal(t, uint64(0), g) +}