From 49eb8b3d205d4a73c5e2b2bb3a433f886279028b Mon Sep 17 00:00:00 2001 From: Uday Patil Date: Mon, 22 Apr 2024 14:26:42 -0500 Subject: [PATCH 01/31] Fix distr integration test (#1581) * Add pause prior to distr queries to reduce flakiness * Update sleep --- integration_test/distribution_module/rewards.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/integration_test/distribution_module/rewards.yaml b/integration_test/distribution_module/rewards.yaml index b24ba81e08..5bc59dba19 100644 --- a/integration_test/distribution_module/rewards.yaml +++ b/integration_test/distribution_module/rewards.yaml @@ -11,6 +11,8 @@ env: REWARDS_START # Simple tx to increase rewards - cmd: printf "12345678\n" | seid tx bank send $NODE_ADMIN_ACC $DISTRIBUTION_TEST_ACC 1sei -b block --fees 2000usei --chain-id sei -y + # Wait a couple seconds before querying to reduce likelihood of flaky test results + - cmd: sleep 1 # Get rewards after tx - cmd: seid q distribution rewards $NODE_ADMIN_ACC -o json | jq -r ".total[0].amount | tonumber" env: REWARDS_AFTER_TX From 6a30166ce96608315e6fab46bd8ffa4cd94f50f9 Mon Sep 17 00:00:00 2001 From: Uday Patil Date: Mon, 22 Apr 2024 17:11:38 -0500 Subject: [PATCH 02/31] Update init script to enable seidb and occ (#1583) * Update init script to enable seidb and occ * Also enable state store for local chain --- scripts/initialize_local_chain.sh | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/scripts/initialize_local_chain.sh b/scripts/initialize_local_chain.sh index 08b3d6b012..68773c0a93 100755 --- a/scripts/initialize_local_chain.sh +++ b/scripts/initialize_local_chain.sh @@ -70,6 +70,18 @@ END_DATE_5DAYS=$($PYTHON_CMD -c "from datetime import datetime, timedelta; print cat ~/.sei/config/genesis.json | jq --arg start_date "$START_DATE" --arg end_date "$END_DATE_3DAYS" '.app_state["mint"]["params"]["token_release_schedule"]=[{"start_date": $start_date, "end_date": $end_date, "token_release_amount": "999999999999"}]' > ~/.sei/config/tmp_genesis.json && mv ~/.sei/config/tmp_genesis.json ~/.sei/config/genesis.json cat ~/.sei/config/genesis.json | jq --arg start_date "$END_DATE_3DAYS" --arg end_date "$END_DATE_5DAYS" '.app_state["mint"]["params"]["token_release_schedule"] += [{"start_date": $start_date, "end_date": $end_date, "token_release_amount": "999999999999"}]' > ~/.sei/config/tmp_genesis.json && mv ~/.sei/config/tmp_genesis.json ~/.sei/config/genesis.json +if [ ! -z "$2" ]; then + APP_TOML_PATH="$2" +else + APP_TOML_PATH="$HOME/.sei/config/app.toml" +fi +# Enable OCC and SeiDB +sed -i.bak -e 's/# concurrency-workers = .*/concurrency-workers = 500/' $APP_TOML_PATH +sed -i.bak -e 's/occ-enabled = .*/occ-enabled = true/' $APP_TOML_PATH +sed -i.bak -e 's/sc-enable = .*/sc-enable = true/' $APP_TOML_PATH +sed -i.bak -e 's/ss-enable = .*/ss-enable = true/' $APP_TOML_PATH + + # set block time to 2s if [ ! -z "$1" ]; then CONFIG_PATH="$1" From 5b895909d216a1173d69b25a7bfff2a9c862d1d0 Mon Sep 17 00:00:00 2001 From: Steven Landers Date: Mon, 22 Apr 2024 19:53:19 -0400 Subject: [PATCH 03/31] [EVM] Add integration test for cw20 pointer (#1576) * add integration test * remove excess json debug file * we're going to need these * rename test * permissions to execute... * try a make install * update command to check for docker * try injecting 12345678 * put pipe in right place * make paths smarter for docker * fix quotes * add check back * fix docker fallback * add debug * trigger associate tx before other steps * minimal changes for now * cleanup * add execute actions * comment out failing test for now --- .github/workflows/integration-test.yml | 21 ++- contracts/hardhat.config.js | 12 +- contracts/test/CW20toERC20PointerTest.js | 106 ++++++++++++ ...interTest.js => ERC20toCW20PointerTest.js} | 4 +- ...ilityTester.js => EVMCompatabilityTest.js} | 0 ...ecompileTester.js => EVMPrecompileTest.js} | 0 contracts/test/lib.js | 163 ++++++++++++++++++ contracts/wasm/cwerc721.wasm | Bin 0 -> 255724 bytes .../scripts/evm_interoperability_tests.sh | 3 + .../evm_module/scripts/evm_tests.sh | 4 + scripts/hardhat.sh | 4 - 11 files changed, 298 insertions(+), 19 deletions(-) create mode 100644 contracts/test/CW20toERC20PointerTest.js rename contracts/test/{CW20ERC20PointerTest.js => ERC20toCW20PointerTest.js} (99%) rename contracts/test/{EVMCompatabilityTester.js => EVMCompatabilityTest.js} (100%) rename contracts/test/{EVMPrecompileTester.js => EVMPrecompileTest.js} (100%) create mode 100644 contracts/test/lib.js create mode 100644 contracts/wasm/cwerc721.wasm create mode 100755 integration_test/evm_module/scripts/evm_interoperability_tests.sh create mode 100755 integration_test/evm_module/scripts/evm_tests.sh delete mode 100755 scripts/hardhat.sh diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index c7d5145508..398b390e8a 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -105,18 +105,25 @@ jobs: ], }, { - name: "Hardhat tests", + name: "SeiDB State Store", + scripts: [ + "docker exec sei-node-0 integration_test/contracts/deploy_wasm_contracts.sh", + "docker exec sei-node-0 integration_test/contracts/create_tokenfactory_denoms.sh", + "python3 integration_test/scripts/runner.py integration_test/seidb/state_store_test.yaml", + ] + }, + { + name: "EVM Module", scripts: [ "python3 integration_test/scripts/runner.py integration_test/evm_module/hardhat_test.yaml", - "./scripts/hardhat.sh" + "./integration_test/evm_module/scripts/evm_tests.sh", ] }, { - name: "SeiDB State Store", + name: "EVM Interoperability", scripts: [ - "docker exec sei-node-0 integration_test/contracts/deploy_wasm_contracts.sh", - "docker exec sei-node-0 integration_test/contracts/create_tokenfactory_denoms.sh", - "python3 integration_test/scripts/runner.py integration_test/seidb/state_store_test.yaml", + "python3 integration_test/scripts/runner.py integration_test/evm_module/hardhat_test.yaml", + "./integration_test/evm_module/scripts/evm_interoperability_tests.sh" ] }, ] @@ -127,7 +134,7 @@ jobs: python-version: '3.10' - uses: actions/setup-node@v2 with: - node-version: '16' + node-version: '20' - name: Pyyaml run: | diff --git a/contracts/hardhat.config.js b/contracts/hardhat.config.js index 5160e48ffd..9bf1ab2b58 100644 --- a/contracts/hardhat.config.js +++ b/contracts/hardhat.config.js @@ -22,15 +22,15 @@ module.exports = { address: ["0xF87A299e6bC7bEba58dbBe5a5Aa21d49bCD16D52"], accounts: ["0x57acb95d82739866a5c29e40b0aa2590742ae50425b7dd5b5d279a986370189e"], // Replace with your private key }, - sei: { - url: "https://evm-devnet.seinetwork.io", // Replace with your JSON-RPC URL - address: ["0x07dc55085b721947d5c1645a07929eac9f1cc750"], - accounts: ["0x57acb95d82739866a5c29e40b0aa2590742ae50425b7dd5b5d279a986370189e"], // Replace with your private key - }, seilocal: { url: "http://127.0.0.1:8545", address: ["0xF87A299e6bC7bEba58dbBe5a5Aa21d49bCD16D52", "0x70997970C51812dc3A010C7d01b50e0d17dc79C8"], - accounts: ["0x57acb95d82739866a5c29e40b0aa2590742ae50425b7dd5b5d279a986370189e", "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d"], // Replace with your private key + accounts: ["0x57acb95d82739866a5c29e40b0aa2590742ae50425b7dd5b5d279a986370189e", "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d"], + }, + devnet: { + url: "https://evm-rpc.arctic-1.seinetwork.io/", + address: ["0xF87A299e6bC7bEba58dbBe5a5Aa21d49bCD16D52"], + accounts: ["0x57acb95d82739866a5c29e40b0aa2590742ae50425b7dd5b5d279a986370189e", "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d"], } }, }; diff --git a/contracts/test/CW20toERC20PointerTest.js b/contracts/test/CW20toERC20PointerTest.js new file mode 100644 index 0000000000..b7af86de30 --- /dev/null +++ b/contracts/test/CW20toERC20PointerTest.js @@ -0,0 +1,106 @@ +const {fundAddress, storeWasm, instantiateWasm, getSeiAddress, getAdmin, queryWasm, executeWasm, deployEvmContract, setupSigners, + getEvmAddress +} = require("./lib") +const { expect } = require("chai"); +const {getAdminAddress} = require("@openzeppelin/upgrades-core"); + +const CW20_POINTER_WASM = "../example/cosmwasm/cw20/artifacts/cwerc20.wasm"; +describe("CW20 to ERC20 Pointer", function () { + let accounts; + let testToken; + let cw20Pointer; + let admin; + + async function setBalance(addr, balance) { + const resp = await testToken.setBalance(addr, balance) + await resp.wait() + } + + before(async function () { + accounts = await setupSigners(await hre.ethers.getSigners()) + + // deploy TestToken + testToken = await deployEvmContract("TestToken", ["TEST", "TEST"]) + const tokenAddr = await testToken.getAddress() + await setBalance(accounts[0].evmAddress, 1000000000000) + await setBalance(accounts[1].evmAddress, 1000000000000) + + // give admin balance + admin = await getAdmin() + await setBalance(admin.evmAddress, 1000000000000) + + const codeId = await storeWasm(CW20_POINTER_WASM) + cw20Pointer = await instantiateWasm(codeId, accounts[0].seiAddress, "cw20-erc20", {erc20_address: tokenAddr }) + }) + + async function assertUnsupported(addr, operation, args) { + try { + await queryWasm(addr, operation, args); + // If the promise resolves, force the test to fail + expect.fail(`Expected rejection: address=${addr} operation=${operation} args=${JSON.stringify(args)}`); + } catch (error) { + expect(error.message).to.include("ERC20 does not support"); + } + } + + describe("query", function(){ + it("should return token_info", async function(){ + const result = await queryWasm(cw20Pointer, "token_info", {}) + expect(result).to.deep.equal({data:{name:"TEST",symbol:"TEST",decimals:18,total_supply:"3000000000000"}}) + }) + + it("should return balance", async function(){ + const result = await queryWasm(cw20Pointer, "balance", {address: accounts[0].seiAddress}) + expect(result).to.deep.equal({ data: { balance: '1000000000000' } }) + }) + + it("should return allowance", async function(){ + const result = await queryWasm(cw20Pointer, "allowance", {owner: accounts[0].seiAddress, spender: accounts[0].seiAddress}) + expect(result).to.deep.equal({ data: { allowance: '0', expires: { never: {} } } }) + }) + + it("should throw exception on unsupported endpoints", async function() { + await assertUnsupported(cw20Pointer, "minter", {}) + await assertUnsupported(cw20Pointer, "marketing_info", {}) + await assertUnsupported(cw20Pointer, "download_logo", {}) + await assertUnsupported(cw20Pointer, "all_allowances", { owner: accounts[0].seiAddress }) + await assertUnsupported(cw20Pointer, "all_accounts", {}) + }); + }) + + + describe("execute", function() { + it("should transfer token", async function() { + const respBefore = await queryWasm(cw20Pointer, "balance", {address: accounts[1].seiAddress}) + const balanceBefore = respBefore.data.balance; + + await executeWasm(cw20Pointer, { transfer: { recipient: accounts[1].seiAddress, amount: "100" } }); + const respAfter = await queryWasm(cw20Pointer, "balance", {address: accounts[1].seiAddress}) + const balanceAfter = respAfter.data.balance; + + expect(balanceAfter).to.equal((parseInt(balanceBefore) + 100).toString()) + }); + + //TODO: other execute methods + + // it("should increase and decrease allowance for a spender", async function() { + // const spender = accounts[1].seiAddress + // const result = await executeWasm(cw20Pointer, { increase_allowance: { spender: spender, amount: "300" } }); + // console.log(result) + // + // let allowance = await queryWasm(cw20Pointer, "allowance", { owner: admin.seiAddress, spender: spender }); + // console.log(allowance) + // expect(allowance.data.allowance).to.equal("300"); + // + // const result2 = await executeWasm(cw20Pointer, { decrease_allowance: { spender: spender, amount: "300" } }); + // console.log(result2) + // + // allowance = await queryWasm(cw20Pointer, "allowance", { owner: admin.seiAddress, spender: spender }); + // console.log(allowance) + // expect(allowance.data.allowance).to.equal("0"); + // }); + + }) + + +}) \ No newline at end of file diff --git a/contracts/test/CW20ERC20PointerTest.js b/contracts/test/ERC20toCW20PointerTest.js similarity index 99% rename from contracts/test/CW20ERC20PointerTest.js rename to contracts/test/ERC20toCW20PointerTest.js index ca1c360c88..c740876bae 100644 --- a/contracts/test/CW20ERC20PointerTest.js +++ b/contracts/test/ERC20toCW20PointerTest.js @@ -4,7 +4,7 @@ const { exec } = require("child_process"); // Importing exec from child_process const { cons } = require("fp-ts/lib/NonEmptyArray2v"); // Run instructions -// Should be run on a local chain using: `npx hardhat test --network seilocal test/CW20ERC20PointerTest.js` +// Should be run on a local chain using: `npx hardhat test --network seilocal test/ERC20toCW20PointerTest.js` async function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); @@ -27,7 +27,7 @@ const secondAnvilPk = "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603 const secondAnvilWallet = new ethers.Wallet(secondAnvilPk); const secondAnvilSigner = secondAnvilWallet.connect(ethers.provider); -describe("CW20ERC20PointerTest", function () { +describe("ERC20 to CW20 Pointer", function () { let adminAddrSei; let contractAddress; let deployerAddrETH; diff --git a/contracts/test/EVMCompatabilityTester.js b/contracts/test/EVMCompatabilityTest.js similarity index 100% rename from contracts/test/EVMCompatabilityTester.js rename to contracts/test/EVMCompatabilityTest.js diff --git a/contracts/test/EVMPrecompileTester.js b/contracts/test/EVMPrecompileTest.js similarity index 100% rename from contracts/test/EVMPrecompileTester.js rename to contracts/test/EVMPrecompileTest.js diff --git a/contracts/test/lib.js b/contracts/test/lib.js new file mode 100644 index 0000000000..cfabf311a1 --- /dev/null +++ b/contracts/test/lib.js @@ -0,0 +1,163 @@ +const { exec } = require("child_process"); // Importing exec from child_process + +async function fundAddress(addr) { + return await execute(`seid tx evm send ${addr} 10000000000000000000 --from admin`); +} + +async function getAdmin() { + await associateAdmin() + const seiAddress = await getAdminSeiAddress() + const evmAddress = await getEvmAddress(seiAddress) + return { + seiAddress, + evmAddress + } +} + +async function getAdminSeiAddress() { + return (await execute(`seid keys show admin -a`)).trim() +} + +async function associateAdmin() { + try { + const result = await execute(`seid tx evm associate-address --from admin`) + console.log(result) + return result + }catch(e){ + console.log("skipping associate") + } +} + +function getEventAttribute(response, type, attribute) { + if(!response.logs || response.logs.length === 0) { + throw new Error("logs not returned") + } + + for(let evt of response.logs[0].events) { + if(evt.type === type) { + for(let att of evt.attributes) { + if(att.key === attribute) { + return att.value; + } + } + } + } + throw new Error("attribute not found") +} + +async function storeWasm(path) { + const command = `seid tx wasm store ${path} --from admin --gas=5000000 --fees=1000000usei -y --broadcast-mode block -o json` + const output = await execute(command); + const response = JSON.parse(output) + return getEventAttribute(response, "store_code", "code_id") +} + +async function instantiateWasm(codeId, adminAddr, label, args = {}) { + const jsonString = JSON.stringify(args).replace(/"/g, '\\"'); + const command = `seid tx wasm instantiate ${codeId} "${jsonString}" --label ${label} --admin ${adminAddr} --from admin --gas=5000000 --fees=1000000usei -y --broadcast-mode block -o json`; + const output = await execute(command); + const response = JSON.parse(output); + return getEventAttribute(response, "instantiate", "_contract_address"); +} + + +async function getSeiAddress(evmAddress) { + const command = `seid q evm sei-addr ${evmAddress} -o json` + const output = await execute(command); + const response = JSON.parse(output) + return response.sei_address +} + +async function getEvmAddress(seiAddress) { + const command = `seid q evm evm-addr ${seiAddress} -o json` + const output = await execute(command); + const response = JSON.parse(output) + return response.evm_address +} + + +async function deployEvmContract(name, args=[]) { + const Contract = await ethers.getContractFactory(name); + const contract = await Contract.deploy(...args); + await contract.waitForDeployment() + return contract; +} + +async function setupSigners(signers) { + const result = [] + for(let signer of signers) { + const evmAddress = await signer.getAddress(); + await fundAddress(evmAddress); + const resp = await signer.sendTransaction({ + to: evmAddress, + value: 0 + }); + await resp.wait() + const seiAddress = await getSeiAddress(evmAddress); + result.push({ + seiAddress, + evmAddress, + signer, + }) + } + return result; +} + +async function queryWasm(contractAddress, operation, args={}){ + const jsonString = JSON.stringify({ [operation]: args }).replace(/"/g, '\\"'); + const command = `seid query wasm contract-state smart ${contractAddress} "${jsonString}" --output json`; + const output = await execute(command); + return JSON.parse(output) +} + +async function executeWasm(contractAddress, msg, args = {}, coins = "0usei") { + const jsonString = JSON.stringify(msg).replace(/"/g, '\\"'); // Properly escape JSON string + const command = `seid tx wasm execute ${contractAddress} "${jsonString}" --amount ${coins} --from admin --gas=5000000 --fees=1000000usei -y --broadcast-mode block -o json`; + const output = await execute(command); + return JSON.parse(output); +} + +async function execute(command) { + return new Promise((resolve, reject) => { + // Check if the Docker container 'sei-node-0' is running + exec("docker ps --filter 'name=sei-node-0' --format '{{.Names}}'", (error, stdout, stderr) => { + if (stdout.includes('sei-node-0')) { + // The container is running, modify the command to execute inside Docker + command = command.replace(/\.\.\//g, "/sei-protocol/sei-chain/"); + const dockerCommand = `docker exec sei-node-0 /bin/bash -c 'export PATH=$PATH:/root/go/bin:/root/.foundry/bin && printf "12345678\\n" | ${command}'`; + execCommand(dockerCommand, resolve, reject); + } else { + // The container is not running, execute command normally + execCommand(command, resolve, reject); + } + }); + }); +} + +function execCommand(command, resolve, reject) { + exec(command, (error, stdout, stderr) => { + if (error) { + reject(error); + return; + } + if (stderr) { + reject(new Error(stderr)); + return; + } + resolve(stdout); + }); +} + +module.exports = { + fundAddress, + storeWasm, + instantiateWasm, + execute, + getSeiAddress, + getEvmAddress, + queryWasm, + executeWasm, + getAdmin, + setupSigners, + deployEvmContract, +}; \ No newline at end of file diff --git a/contracts/wasm/cwerc721.wasm b/contracts/wasm/cwerc721.wasm new file mode 100644 index 0000000000000000000000000000000000000000..318ada77bc67874fa3f7386116c2d75127549b30 GIT binary patch literal 255724 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 literal 0 HcmV?d00001 diff --git a/integration_test/evm_module/scripts/evm_interoperability_tests.sh b/integration_test/evm_module/scripts/evm_interoperability_tests.sh new file mode 100755 index 0000000000..1efcb6e2bd --- /dev/null +++ b/integration_test/evm_module/scripts/evm_interoperability_tests.sh @@ -0,0 +1,3 @@ +cd contracts +npm ci +npx hardhat test --network seilocal test/CW20toERC20PointerTest.js diff --git a/integration_test/evm_module/scripts/evm_tests.sh b/integration_test/evm_module/scripts/evm_tests.sh new file mode 100755 index 0000000000..dedc6f2f8c --- /dev/null +++ b/integration_test/evm_module/scripts/evm_tests.sh @@ -0,0 +1,4 @@ +cd contracts +npm ci +npx hardhat test --network seilocal test/EVMCompatabilityTest.js +npx hardhat test --network seilocal test/EVMPrecompileTest.js diff --git a/scripts/hardhat.sh b/scripts/hardhat.sh deleted file mode 100755 index d38f62b1de..0000000000 --- a/scripts/hardhat.sh +++ /dev/null @@ -1,4 +0,0 @@ -cd contracts -npm ci -npx hardhat test --network seilocal test/EVMCompatabilityTester.js -npx hardhat test --network seilocal test/EVMPrecompileTester.js From 76d8c1e7ae032052c6ac3c3573b0dfa7556ec549 Mon Sep 17 00:00:00 2001 From: codchen Date: Tue, 23 Apr 2024 22:54:28 +0800 Subject: [PATCH 04/31] Add EVM keeper unit tests (#1587) Add keeper unit test --- x/evm/keeper/address_test.go | 22 ++++++++++++++++------ x/evm/keeper/coinbase_test.go | 21 +++++++++++++++++++++ x/evm/keeper/genesis_test.go | 18 ++++++++++++++++++ x/evm/keeper/nonce_test.go | 16 ++++++++++++++++ x/evm/keeper/receipt_test.go | 21 +++++++++++++++++++++ x/evm/keeper/tx_test.go | 20 ++++++++++++++++++++ x/evm/keeper/whitelist_test.go | 14 ++++++++++++++ 7 files changed, 126 insertions(+), 6 deletions(-) create mode 100644 x/evm/keeper/coinbase_test.go create mode 100644 x/evm/keeper/genesis_test.go create mode 100644 x/evm/keeper/nonce_test.go create mode 100644 x/evm/keeper/receipt_test.go create mode 100644 x/evm/keeper/tx_test.go create mode 100644 x/evm/keeper/whitelist_test.go diff --git a/x/evm/keeper/address_test.go b/x/evm/keeper/address_test.go index 6c511fcc84..825dfb5658 100644 --- a/x/evm/keeper/address_test.go +++ b/x/evm/keeper/address_test.go @@ -1,6 +1,7 @@ package keeper_test import ( + "bytes" "testing" "github.com/sei-protocol/sei-chain/testutil/keeper" @@ -10,15 +11,15 @@ import ( func TestSetGetAddressMapping(t *testing.T) { k, ctx := keeper.MockEVMKeeper() seiAddr, evmAddr := keeper.MockAddressPair() - foundEVM, ok := k.GetEVMAddress(ctx, seiAddr) + _, ok := k.GetEVMAddress(ctx, seiAddr) require.False(t, ok) - foundSei, ok := k.GetSeiAddress(ctx, evmAddr) + _, ok = k.GetSeiAddress(ctx, evmAddr) require.False(t, ok) k.SetAddressMapping(ctx, seiAddr, evmAddr) - foundEVM, ok = k.GetEVMAddress(ctx, seiAddr) + foundEVM, ok := k.GetEVMAddress(ctx, seiAddr) require.True(t, ok) require.Equal(t, evmAddr, foundEVM) - foundSei, ok = k.GetSeiAddress(ctx, evmAddr) + foundSei, ok := k.GetSeiAddress(ctx, evmAddr) require.True(t, ok) require.Equal(t, seiAddr, foundSei) require.Equal(t, seiAddr, k.AccountKeeper().GetAccount(ctx, seiAddr).GetAddress()) @@ -35,8 +36,17 @@ func TestDeleteAddressMapping(t *testing.T) { require.True(t, ok) require.Equal(t, seiAddr, foundSei) k.DeleteAddressMapping(ctx, seiAddr, evmAddr) - foundEVM, ok = k.GetEVMAddress(ctx, seiAddr) + _, ok = k.GetEVMAddress(ctx, seiAddr) require.False(t, ok) - foundSei, ok = k.GetSeiAddress(ctx, evmAddr) + _, ok = k.GetSeiAddress(ctx, evmAddr) require.False(t, ok) } + +func TestGetAddressOrDefault(t *testing.T) { + k, ctx := keeper.MockEVMKeeper() + seiAddr, evmAddr := keeper.MockAddressPair() + defaultEvmAddr := k.GetEVMAddressOrDefault(ctx, seiAddr) + require.True(t, bytes.Equal(seiAddr, defaultEvmAddr[:])) + defaultSeiAddr := k.GetSeiAddressOrDefault(ctx, evmAddr) + require.True(t, bytes.Equal(defaultSeiAddr, evmAddr[:])) +} diff --git a/x/evm/keeper/coinbase_test.go b/x/evm/keeper/coinbase_test.go new file mode 100644 index 0000000000..1b61929dbe --- /dev/null +++ b/x/evm/keeper/coinbase_test.go @@ -0,0 +1,21 @@ +package keeper_test + +import ( + "testing" + + keepertest "github.com/sei-protocol/sei-chain/testutil/keeper" + "github.com/sei-protocol/sei-chain/x/evm/keeper" + "github.com/stretchr/testify/require" +) + +func TestGetFeeCollectorAddress(t *testing.T) { + k, ctx := keepertest.MockEVMKeeper() + addr, err := k.GetFeeCollectorAddress(ctx) + require.Nil(t, err) + expected := k.GetEVMAddressOrDefault(ctx, k.AccountKeeper().GetModuleAddress("fee_collector")) + require.Equal(t, expected.Hex(), addr.Hex()) +} + +func TestGetCoinbaseAddress(t *testing.T) { + require.Equal(t, "0x27F7B8B8B5A4e71E8E9aA671f4e4031E3773303F", keeper.GetCoinbaseAddress().Hex()) +} diff --git a/x/evm/keeper/genesis_test.go b/x/evm/keeper/genesis_test.go new file mode 100644 index 0000000000..7567a5777d --- /dev/null +++ b/x/evm/keeper/genesis_test.go @@ -0,0 +1,18 @@ +package keeper_test + +import ( + "bytes" + "testing" + + keepertest "github.com/sei-protocol/sei-chain/testutil/keeper" + "github.com/sei-protocol/sei-chain/x/evm/keeper" + "github.com/stretchr/testify/require" +) + +func TestInitGenesis(t *testing.T) { + k, ctx := keepertest.MockEVMKeeper() // this would call `InitGenesis` + // coinbase address must be associated + coinbaseSeiAddr, associated := k.GetSeiAddress(ctx, keeper.GetCoinbaseAddress()) + require.True(t, associated) + require.True(t, bytes.Equal(coinbaseSeiAddr, k.AccountKeeper().GetModuleAddress("fee_collector"))) +} diff --git a/x/evm/keeper/nonce_test.go b/x/evm/keeper/nonce_test.go new file mode 100644 index 0000000000..32c87ba5fa --- /dev/null +++ b/x/evm/keeper/nonce_test.go @@ -0,0 +1,16 @@ +package keeper_test + +import ( + "testing" + + keepertest "github.com/sei-protocol/sei-chain/testutil/keeper" + "github.com/stretchr/testify/require" +) + +func TestNonce(t *testing.T) { + k, ctx := keepertest.MockEVMKeeper() + _, evmAddr := keepertest.MockAddressPair() + require.Equal(t, uint64(0), k.GetNonce(ctx, evmAddr)) + k.SetNonce(ctx, evmAddr, 1) + require.Equal(t, uint64(1), k.GetNonce(ctx, evmAddr)) +} diff --git a/x/evm/keeper/receipt_test.go b/x/evm/keeper/receipt_test.go new file mode 100644 index 0000000000..fc1971d8f9 --- /dev/null +++ b/x/evm/keeper/receipt_test.go @@ -0,0 +1,21 @@ +package keeper_test + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common" + keepertest "github.com/sei-protocol/sei-chain/testutil/keeper" + "github.com/sei-protocol/sei-chain/x/evm/types" + "github.com/stretchr/testify/require" +) + +func TestReceipt(t *testing.T) { + k, ctx := keepertest.MockEVMKeeper() + txHash := common.HexToHash("0x0750333eac0be1203864220893d8080dd8a8fd7a2ed098dfd92a718c99d437f2") + _, err := k.GetReceipt(ctx, txHash) + require.NotNil(t, err) + k.SetReceipt(ctx, txHash, &types.Receipt{TxHashHex: txHash.Hex()}) + r, err := k.GetReceipt(ctx, txHash) + require.Nil(t, err) + require.Equal(t, txHash.Hex(), r.TxHashHex) +} diff --git a/x/evm/keeper/tx_test.go b/x/evm/keeper/tx_test.go new file mode 100644 index 0000000000..5c96fd45db --- /dev/null +++ b/x/evm/keeper/tx_test.go @@ -0,0 +1,20 @@ +package keeper_test + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common" + keepertest "github.com/sei-protocol/sei-chain/testutil/keeper" + "github.com/stretchr/testify/require" +) + +func TestTxHashesOnHeight(t *testing.T) { + k, ctx := keepertest.MockEVMKeeper() + require.Empty(t, k.GetTxHashesOnHeight(ctx, 1234)) + hashes := []common.Hash{ + common.HexToHash("0x0750333eac0be1203864220893d8080dd8a8fd7a2ed098dfd92a718c99d437f2"), + common.HexToHash("0x6f0c1476adb51b1646ff35433b410f1e9c326bd6428f90acf39d0bb1a312bc50"), + } + k.SetTxHashesOnHeight(ctx, 1234, hashes) + require.Equal(t, hashes, k.GetTxHashesOnHeight(ctx, 1234)) +} diff --git a/x/evm/keeper/whitelist_test.go b/x/evm/keeper/whitelist_test.go new file mode 100644 index 0000000000..f25db274b8 --- /dev/null +++ b/x/evm/keeper/whitelist_test.go @@ -0,0 +1,14 @@ +package keeper_test + +import ( + "testing" + + keepertest "github.com/sei-protocol/sei-chain/testutil/keeper" + "github.com/stretchr/testify/require" +) + +func TestWhitelist(t *testing.T) { + k, ctx := keepertest.MockEVMKeeper() + require.True(t, k.IsCWCodeHashWhitelistedForEVMDelegateCall(ctx, k.WhitelistedCwCodeHashesForDelegateCall(ctx)[0])) + require.False(t, k.IsCWCodeHashWhitelistedForEVMDelegateCall(ctx, []byte("1"))) +} From 32a1b3282fa532d4e832c89b682b24afd49e67f8 Mon Sep 17 00:00:00 2001 From: codchen Date: Tue, 23 Apr 2024 23:04:32 +0800 Subject: [PATCH 05/31] Only consider spendable coins when transferring in EVM (#1586) --- x/evm/module.go | 6 +++--- x/evm/module_test.go | 27 +++++++++++++++++++++++++++ x/evm/state/balance.go | 27 +++++++++++++++++---------- x/evm/state/balance_test.go | 1 + 4 files changed, 48 insertions(+), 13 deletions(-) diff --git a/x/evm/module.go b/x/evm/module.go index 2e1ec56066..122ae4d0a2 100644 --- a/x/evm/module.go +++ b/x/evm/module.go @@ -225,10 +225,10 @@ func (am AppModule) EndBlock(ctx sdk.Context, _ abci.RequestEndBlock) []abci.Val } idx := deferredInfo.TxIndx coinbaseAddress := state.GetCoinbaseAddress(idx) - balance := am.keeper.BankKeeper().GetBalance(ctx, coinbaseAddress, denom) + balance := am.keeper.BankKeeper().SpendableCoins(ctx, coinbaseAddress).AmountOf(denom) weiBalance := am.keeper.BankKeeper().GetWeiBalance(ctx, coinbaseAddress) - if !balance.Amount.IsZero() || !weiBalance.IsZero() { - if err := am.keeper.BankKeeper().SendCoinsAndWei(ctx, coinbaseAddress, coinbase, balance.Amount, weiBalance); err != nil { + if !balance.IsZero() || !weiBalance.IsZero() { + if err := am.keeper.BankKeeper().SendCoinsAndWei(ctx, coinbaseAddress, coinbase, balance, weiBalance); err != nil { panic(err) } } diff --git a/x/evm/module_test.go b/x/evm/module_test.go index 108603d854..f35fc90014 100644 --- a/x/evm/module_test.go +++ b/x/evm/module_test.go @@ -1,11 +1,14 @@ package evm_test import ( + "math" "math/big" "testing" sdk "github.com/cosmos/cosmos-sdk/types" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/cosmos/cosmos-sdk/x/auth/vesting" + vestingtypes "github.com/cosmos/cosmos-sdk/x/auth/vesting/types" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/tracing" ethtypes "github.com/ethereum/go-ethereum/core/types" @@ -74,4 +77,28 @@ func TestABCI(t *testing.T) { receipt, err := k.GetReceipt(ctx, common.Hash{1}) require.Nil(t, err) require.Equal(t, receipt.VmError, "test error") + + // fourth block with locked tokens in coinbase address + m.BeginBlock(ctx, abci.RequestBeginBlock{}) + coinbase := state.GetCoinbaseAddress(2) + vms := vesting.NewMsgServerImpl(*k.AccountKeeper(), k.BankKeeper()) + _, err = vms.CreateVestingAccount(sdk.WrapSDKContext(ctx), &vestingtypes.MsgCreateVestingAccount{ + FromAddress: sdk.AccAddress(evmAddr1[:]).String(), + ToAddress: coinbase.String(), + Amount: sdk.NewCoins(sdk.NewCoin("usei", sdk.OneInt())), + EndTime: math.MaxInt64, + }) + require.Nil(t, err) + s = state.NewDBImpl(ctx.WithTxIndex(2), k, false) + s.SubBalance(evmAddr1, big.NewInt(2000000000000), tracing.BalanceChangeUnspecified) + s.AddBalance(evmAddr2, big.NewInt(1000000000000), tracing.BalanceChangeUnspecified) + s.AddBalance(feeCollectorAddr, big.NewInt(1000000000000), tracing.BalanceChangeUnspecified) + surplus, err = s.Finalize() + require.Nil(t, err) + k.AppendToEvmTxDeferredInfo(ctx.WithTxIndex(2), ethtypes.Bloom{}, common.Hash{}, surplus) + k.SetTxResults([]*abci.ExecTxResult{{Code: 0}, {Code: 0}, {Code: 0}}) + require.Equal(t, sdk.OneInt(), k.BankKeeper().SpendableCoins(ctx, coinbase).AmountOf("usei")) + m.EndBlock(ctx, abci.RequestEndBlock{}) // should not crash + require.Equal(t, sdk.OneInt(), k.BankKeeper().GetBalance(ctx, coinbase, "usei").Amount) + require.Equal(t, sdk.ZeroInt(), k.BankKeeper().SpendableCoins(ctx, coinbase).AmountOf("usei")) } diff --git a/x/evm/state/balance.go b/x/evm/state/balance.go index 493c8e4956..7c48729523 100644 --- a/x/evm/state/balance.go +++ b/x/evm/state/balance.go @@ -28,12 +28,14 @@ func (s *DBImpl) SubBalance(evmAddr common.Address, amt *big.Int, reason tracing usei, wei := SplitUseiWeiAmount(amt) addr := s.getSeiAddress(evmAddr) - s.err = s.k.BankKeeper().SubUnlockedCoins(ctx, addr, sdk.NewCoins(sdk.NewCoin(s.k.GetBaseDenom(s.ctx), usei)), true) - if s.err != nil { + err := s.k.BankKeeper().SubUnlockedCoins(ctx, addr, sdk.NewCoins(sdk.NewCoin(s.k.GetBaseDenom(s.ctx), usei)), true) + if err != nil { + s.err = err return } - s.err = s.k.BankKeeper().SubWei(ctx, addr, wei) - if s.err != nil { + err = s.k.BankKeeper().SubWei(ctx, addr, wei) + if err != nil { + s.err = err return } @@ -66,12 +68,14 @@ func (s *DBImpl) AddBalance(evmAddr common.Address, amt *big.Int, reason tracing usei, wei := SplitUseiWeiAmount(amt) addr := s.getSeiAddress(evmAddr) - s.err = s.k.BankKeeper().AddCoins(ctx, addr, sdk.NewCoins(sdk.NewCoin(s.k.GetBaseDenom(s.ctx), usei)), true) - if s.err != nil { + err := s.k.BankKeeper().AddCoins(ctx, addr, sdk.NewCoins(sdk.NewCoin(s.k.GetBaseDenom(s.ctx), usei)), true) + if err != nil { + s.err = err return } - s.err = s.k.BankKeeper().AddWei(ctx, addr, wei) - if s.err != nil { + err = s.k.BankKeeper().AddWei(ctx, addr, wei) + if err != nil { + s.err = err return } @@ -88,7 +92,7 @@ func (s *DBImpl) AddBalance(evmAddr common.Address, amt *big.Int, reason tracing func (s *DBImpl) GetBalance(evmAddr common.Address) *big.Int { s.k.PrepareReplayedAddr(s.ctx, evmAddr) - usei := s.k.BankKeeper().GetBalance(s.ctx, s.getSeiAddress(evmAddr), s.k.GetBaseDenom(s.ctx)).Amount + usei := s.k.BankKeeper().SpendableCoins(s.ctx, s.getSeiAddress(evmAddr)).AmountOf(s.k.GetBaseDenom(s.ctx)) wei := s.k.BankKeeper().GetWeiBalance(s.ctx, s.getSeiAddress(evmAddr)) return usei.Mul(SdkUseiToSweiMultiplier).Add(wei).BigInt() } @@ -124,5 +128,8 @@ func (s *DBImpl) getSeiAddress(evmAddr common.Address) sdk.AccAddress { func (s *DBImpl) send(from sdk.AccAddress, to sdk.AccAddress, amt *big.Int) { usei, wei := SplitUseiWeiAmount(amt) - s.err = s.k.BankKeeper().SendCoinsAndWei(s.ctx, from, to, usei, wei) + err := s.k.BankKeeper().SendCoinsAndWei(s.ctx, from, to, usei, wei) + if err != nil { + s.err = err + } } diff --git a/x/evm/state/balance_test.go b/x/evm/state/balance_test.go index d732b2fb30..33aa29b325 100644 --- a/x/evm/state/balance_test.go +++ b/x/evm/state/balance_test.go @@ -69,6 +69,7 @@ func TestSubBalance(t *testing.T) { db.SubBalance(evmAddr2, big.NewInt(10000000000000), tracing.BalanceChangeUnspecified) require.NotNil(t, db.Err()) + db.WithErr(nil) _, evmAddr3 := testkeeper.MockAddressPair() db.SelfDestruct(evmAddr3) db.SubBalance(evmAddr2, big.NewInt(5000000000000), tracing.BalanceChangeUnspecified) From a42726e1c4d25927e787cc5b1be3a1a555d92668 Mon Sep 17 00:00:00 2001 From: codchen Date: Tue, 23 Apr 2024 23:20:05 +0800 Subject: [PATCH 06/31] Invoke StateTransition for initial EVM entrance (#1585) --- contracts/test/CW20toERC20PointerTest.js | 32 +++---- contracts/test/lib.js | 9 ++ x/evm/keeper/evm.go | 108 +++++++++++++++-------- x/evm/keeper/msg_server.go | 6 +- x/evm/module.go | 6 +- 5 files changed, 101 insertions(+), 60 deletions(-) diff --git a/contracts/test/CW20toERC20PointerTest.js b/contracts/test/CW20toERC20PointerTest.js index b7af86de30..dc6e95f0f0 100644 --- a/contracts/test/CW20toERC20PointerTest.js +++ b/contracts/test/CW20toERC20PointerTest.js @@ -83,22 +83,22 @@ describe("CW20 to ERC20 Pointer", function () { //TODO: other execute methods - // it("should increase and decrease allowance for a spender", async function() { - // const spender = accounts[1].seiAddress - // const result = await executeWasm(cw20Pointer, { increase_allowance: { spender: spender, amount: "300" } }); - // console.log(result) - // - // let allowance = await queryWasm(cw20Pointer, "allowance", { owner: admin.seiAddress, spender: spender }); - // console.log(allowance) - // expect(allowance.data.allowance).to.equal("300"); - // - // const result2 = await executeWasm(cw20Pointer, { decrease_allowance: { spender: spender, amount: "300" } }); - // console.log(result2) - // - // allowance = await queryWasm(cw20Pointer, "allowance", { owner: admin.seiAddress, spender: spender }); - // console.log(allowance) - // expect(allowance.data.allowance).to.equal("0"); - // }); + it("should increase and decrease allowance for a spender", async function() { + const spender = accounts[1].seiAddress + const result = await executeWasm(cw20Pointer, { increase_allowance: { spender: spender, amount: "300" } }); + console.log(result) + + let allowance = await queryWasm(cw20Pointer, "allowance", { owner: admin.seiAddress, spender: spender }); + console.log(allowance) + expect(allowance.data.allowance).to.equal("300"); + + const result2 = await executeWasm(cw20Pointer, { decrease_allowance: { spender: spender, amount: "300" } }); + console.log(result2) + + allowance = await queryWasm(cw20Pointer, "allowance", { owner: admin.seiAddress, spender: spender }); + console.log(allowance) + expect(allowance.data.allowance).to.equal("0"); + }); }) diff --git a/contracts/test/lib.js b/contracts/test/lib.js index cfabf311a1..0d05ef8ddb 100644 --- a/contracts/test/lib.js +++ b/contracts/test/lib.js @@ -1,5 +1,13 @@ const { exec } = require("child_process"); // Importing exec from child_process +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +async function delay() { + await sleep(1000) +} + async function fundAddress(addr) { return await execute(`seid tx evm send ${addr} 10000000000000000000 --from admin`); } @@ -88,6 +96,7 @@ async function setupSigners(signers) { for(let signer of signers) { const evmAddress = await signer.getAddress(); await fundAddress(evmAddress); + await delay() const resp = await signer.sendTransaction({ to: evmAddress, value: 0 diff --git a/x/evm/keeper/evm.go b/x/evm/keeper/evm.go index 6db6c419ad..bb56693d53 100644 --- a/x/evm/keeper/evm.go +++ b/x/evm/keeper/evm.go @@ -8,6 +8,8 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" "github.com/sei-protocol/sei-chain/utils" "github.com/sei-protocol/sei-chain/x/evm/state" @@ -28,7 +30,7 @@ func (k *Keeper) HandleInternalEVMCall(ctx sdk.Context, req *types.MsgInternalEV if err != nil { return nil, err } - ret, err := k.CallEVM(ctx, senderAddr, to, req.Value, req.Data) + ret, err := k.CallEVM(ctx, k.GetEVMAddressOrDefault(ctx, senderAddr), to, req.Value, req.Data) if err != nil { return nil, err } @@ -51,30 +53,56 @@ func (k *Keeper) HandleInternalEVMDelegateCall(ctx sdk.Context, req *types.MsgIn } // delegatecall caller must be associated; otherwise any state change on EVM contract will be lost // after they asssociate. - _, found := k.GetEVMAddress(ctx, senderAddr) + senderEvmAddr, found := k.GetEVMAddress(ctx, senderAddr) if !found { return nil, fmt.Errorf("sender %s is not associated", req.Sender) } - ret, err := k.CallEVM(ctx, senderAddr, to, &zeroInt, req.Data) + ret, err := k.CallEVM(ctx, senderEvmAddr, to, &zeroInt, req.Data) if err != nil { return nil, err } return &sdk.Result{Data: ret}, nil } -func (k *Keeper) CallEVM(ctx sdk.Context, from sdk.AccAddress, to *common.Address, val *sdk.Int, data []byte) (retdata []byte, reterr error) { - evm, finalizer, err := k.getOrCreateEVM(ctx, from) - if err != nil { - return nil, err - } - defer func() { - if finalizer != nil { - if err := finalizer(); err != nil { - reterr = err - return - } +func (k *Keeper) CallEVM(ctx sdk.Context, from common.Address, to *common.Address, val *sdk.Int, data []byte) (retdata []byte, reterr error) { + evm := types.GetCtxEVM(ctx) + if evm == nil { + // This call was not part of an existing StateTransition, so it should trigger one + executionCtx := ctx.WithGasMeter(sdk.NewInfiniteGasMeter()) + stateDB := state.NewDBImpl(executionCtx, k, false) + gp := k.GetGasPool() + value := utils.Big0 + if val != nil { + value = val.BigInt() + } + evmMsg := &core.Message{ + Nonce: stateDB.GetNonce(from), // replay attack is prevented by the AccountSequence number set on the CW transaction that triggered this call + GasLimit: k.getEvmGasLimitFromCtx(ctx), + GasPrice: utils.Big0, // fees are already paid on the CW transaction + GasFeeCap: utils.Big0, + GasTipCap: utils.Big0, + To: to, + Value: value, + Data: data, + SkipAccountChecks: false, + From: from, + } + res, err := k.applyEVMMessage(ctx, evmMsg, stateDB, gp) + if err != nil { + return nil, err } - }() + k.consumeEvmGas(ctx, res.UsedGas) + if res.Err != nil { + return nil, res.Err + } + surplus, err := stateDB.Finalize() + if err != nil { + return nil, err + } + k.AppendToEvmTxDeferredInfo(ctx, ethtypes.Bloom{}, ethtypes.EmptyTxsHash, surplus) + return res.ReturnData, nil + } + // This call is part of an existing StateTransition, so directly invoking `Call` var f EVMCallFunc if to == nil { // contract creation @@ -91,57 +119,61 @@ func (k *Keeper) CallEVM(ctx sdk.Context, from sdk.AccAddress, to *common.Addres } func (k *Keeper) StaticCallEVM(ctx sdk.Context, from sdk.AccAddress, to *common.Address, data []byte) ([]byte, error) { - evm, _, err := k.getOrCreateEVM(ctx, from) + evm, err := k.getOrCreateEVM(ctx, from) if err != nil { return nil, err } - return k.callEVM(ctx, from, to, nil, data, func(caller vm.ContractRef, addr *common.Address, input []byte, gas uint64, _ *big.Int) ([]byte, uint64, error) { + return k.callEVM(ctx, k.GetEVMAddressOrDefault(ctx, from), to, nil, data, func(caller vm.ContractRef, addr *common.Address, input []byte, gas uint64, _ *big.Int) ([]byte, uint64, error) { return evm.StaticCall(caller, *addr, input, gas) }) } -func (k *Keeper) callEVM(ctx sdk.Context, from sdk.AccAddress, to *common.Address, val *sdk.Int, data []byte, f EVMCallFunc) ([]byte, error) { - sender := k.GetEVMAddressOrDefault(ctx, from) - seiGasRemaining := ctx.GasMeter().Limit() - ctx.GasMeter().GasConsumedToLimit() - if ctx.GasMeter().Limit() <= 0 { - // infinite gas meter (used in queries) - seiGasRemaining = math.MaxUint64 - } - multiplier := k.GetPriorityNormalizer(ctx) - evmGasRemaining := sdk.NewDecFromInt(sdk.NewIntFromUint64(seiGasRemaining)).Quo(multiplier).TruncateInt().BigInt() - if evmGasRemaining.Cmp(MaxUint64BigInt) > 0 { - evmGasRemaining = MaxUint64BigInt - } +func (k *Keeper) callEVM(ctx sdk.Context, from common.Address, to *common.Address, val *sdk.Int, data []byte, f EVMCallFunc) ([]byte, error) { + evmGasLimit := k.getEvmGasLimitFromCtx(ctx) value := utils.Big0 if val != nil { value = val.BigInt() } - ret, leftoverGas, err := f(vm.AccountRef(sender), to, data, evmGasRemaining.Uint64(), value) - ctx.GasMeter().ConsumeGas(ctx.GasMeter().Limit()-sdk.NewDecFromInt(sdk.NewIntFromUint64(leftoverGas)).Mul(multiplier).TruncateInt().Uint64(), "call EVM") + ret, leftoverGas, err := f(vm.AccountRef(from), to, data, evmGasLimit, value) + k.consumeEvmGas(ctx, evmGasLimit-leftoverGas) if err != nil { return nil, err } return ret, nil } -func (k *Keeper) getOrCreateEVM(ctx sdk.Context, from sdk.AccAddress) (*vm.EVM, func() error, error) { +// only used for StaticCalls +func (k *Keeper) getOrCreateEVM(ctx sdk.Context, from sdk.AccAddress) (*vm.EVM, error) { evm := types.GetCtxEVM(ctx) if evm != nil { - return evm, nil, nil + return evm, nil } executionCtx := ctx.WithGasMeter(sdk.NewInfiniteGasMeter()) stateDB := state.NewDBImpl(executionCtx, k, false) gp := k.GetGasPool() blockCtx, err := k.GetVMBlockContext(executionCtx, gp) if err != nil { - return nil, nil, err + return nil, err } cfg := types.DefaultChainConfig().EthereumConfig(k.ChainID()) txCtx := vm.TxContext{Origin: k.GetEVMAddressOrDefault(ctx, from)} evm = vm.NewEVM(*blockCtx, txCtx, stateDB, cfg, vm.Config{}) stateDB.SetEVM(evm) - return evm, func() error { - _, err := stateDB.Finalize() - return err - }, nil + return evm, nil +} + +func (k *Keeper) getEvmGasLimitFromCtx(ctx sdk.Context) uint64 { + seiGasRemaining := ctx.GasMeter().Limit() - ctx.GasMeter().GasConsumedToLimit() + if ctx.GasMeter().Limit() <= 0 { + return math.MaxUint64 + } + evmGasBig := sdk.NewDecFromInt(sdk.NewIntFromUint64(seiGasRemaining)).Quo(k.GetPriorityNormalizer(ctx)).TruncateInt().BigInt() + if evmGasBig.Cmp(MaxUint64BigInt) > 0 { + evmGasBig = MaxUint64BigInt + } + return evmGasBig.Uint64() +} + +func (k *Keeper) consumeEvmGas(ctx sdk.Context, usedEvmGas uint64) { + ctx.GasMeter().ConsumeGas(sdk.NewDecFromInt(sdk.NewIntFromUint64(usedEvmGas)).Mul(k.GetPriorityNormalizer(ctx)).TruncateInt().Uint64(), "call EVM") } diff --git a/x/evm/keeper/msg_server.go b/x/evm/keeper/msg_server.go index 29ad789fad..370ab4f6d8 100644 --- a/x/evm/keeper/msg_server.go +++ b/x/evm/keeper/msg_server.go @@ -191,12 +191,12 @@ func (k *Keeper) GetEVMMessage(ctx sdk.Context, tx *ethtypes.Transaction, sender return msg } -func (server msgServer) applyEVMMessage(ctx sdk.Context, msg *core.Message, stateDB *state.DBImpl, gp core.GasPool) (*core.ExecutionResult, error) { - blockCtx, err := server.GetVMBlockContext(ctx, gp) +func (k Keeper) applyEVMMessage(ctx sdk.Context, msg *core.Message, stateDB *state.DBImpl, gp core.GasPool) (*core.ExecutionResult, error) { + blockCtx, err := k.GetVMBlockContext(ctx, gp) if err != nil { return nil, err } - cfg := types.DefaultChainConfig().EthereumConfig(server.ChainID()) + cfg := types.DefaultChainConfig().EthereumConfig(k.ChainID()) txCtx := core.NewEVMTxContext(msg) evmInstance := vm.NewEVM(*blockCtx, txCtx, stateDB, cfg, vm.Config{}) stateDB.SetEVM(evmInstance) diff --git a/x/evm/module.go b/x/evm/module.go index 122ae4d0a2..5c1c7bc3ca 100644 --- a/x/evm/module.go +++ b/x/evm/module.go @@ -215,7 +215,7 @@ func (am AppModule) EndBlock(ctx sdk.Context, _ abci.RequestEndBlock) []abci.Val denom := am.keeper.GetBaseDenom(ctx) surplus := utils.Sdk0 for _, deferredInfo := range evmTxDeferredInfoList { - if deferredInfo.Error != "" { + if deferredInfo.Error != "" && deferredInfo.TxHash.Cmp(ethtypes.EmptyTxsHash) != 0 { _ = am.keeper.SetReceipt(ctx, deferredInfo.TxHash, &types.Receipt{ TxHashHex: deferredInfo.TxHash.Hex(), TransactionIndex: uint32(deferredInfo.TxIndx), @@ -229,7 +229,7 @@ func (am AppModule) EndBlock(ctx sdk.Context, _ abci.RequestEndBlock) []abci.Val weiBalance := am.keeper.BankKeeper().GetWeiBalance(ctx, coinbaseAddress) if !balance.IsZero() || !weiBalance.IsZero() { if err := am.keeper.BankKeeper().SendCoinsAndWei(ctx, coinbaseAddress, coinbase, balance, weiBalance); err != nil { - panic(err) + ctx.Logger().Error(fmt.Sprintf("failed to send usei surplus from %s to coinbase account due to %s", coinbaseAddress.String(), err)) } } surplus = surplus.Add(deferredInfo.Surplus) @@ -245,7 +245,7 @@ func (am AppModule) EndBlock(ctx sdk.Context, _ abci.RequestEndBlock) []abci.Val ctx.Logger().Error("failed to send wei surplus of %s to EVM module account", surplusWei) } } - am.keeper.SetTxHashesOnHeight(ctx, ctx.BlockHeight(), utils.Map(evmTxDeferredInfoList, func(i keeper.EvmTxDeferredInfo) common.Hash { return i.TxHash })) + am.keeper.SetTxHashesOnHeight(ctx, ctx.BlockHeight(), utils.Filter(utils.Map(evmTxDeferredInfoList, func(i keeper.EvmTxDeferredInfo) common.Hash { return i.TxHash }), func(h common.Hash) bool { return h.Cmp(ethtypes.EmptyTxsHash) != 0 })) am.keeper.SetBlockBloom(ctx, ctx.BlockHeight(), utils.Map(evmTxDeferredInfoList, func(i keeper.EvmTxDeferredInfo) ethtypes.Bloom { return i.TxBloom })) return []abci.ValidatorUpdate{} } From 17ba75750373e22192586f87d4d83060f60d5eb0 Mon Sep 17 00:00:00 2001 From: Yiming Zang <50607998+yzang2019@users.noreply.github.com> Date: Wed, 24 Apr 2024 11:51:41 +0800 Subject: [PATCH 07/31] Add unit test for x/evm (#1588) * Add unit test for x/evm * Fix lint * Fix unit test * Revert import --- x/evm/genesis_test.go | 21 +++++++++++++++++++++ x/evm/module.go | 1 - x/evm/module_test.go | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 x/evm/genesis_test.go diff --git a/x/evm/genesis_test.go b/x/evm/genesis_test.go new file mode 100644 index 0000000000..e2b8151291 --- /dev/null +++ b/x/evm/genesis_test.go @@ -0,0 +1,21 @@ +package evm_test + +import ( + "testing" + + testkeeper "github.com/sei-protocol/sei-chain/testutil/keeper" + "github.com/sei-protocol/sei-chain/x/evm" + "github.com/sei-protocol/sei-chain/x/evm/types" + "github.com/stretchr/testify/assert" +) + +func TestExportGenesis(t *testing.T) { + keeper, ctx := testkeeper.MockEVMKeeper() + genesis := evm.ExportGenesis(ctx, keeper) + assert.NoError(t, genesis.Validate()) + param := genesis.GetParams() + assert.Equal(t, types.DefaultParams().PriorityNormalizer, param.PriorityNormalizer) + assert.Equal(t, types.DefaultParams().BaseFeePerGas, param.BaseFeePerGas) + assert.Equal(t, types.DefaultParams().MinimumFeePerGas, param.MinimumFeePerGas) + assert.Equal(t, types.DefaultParams().WhitelistedCwCodeHashesForDelegateCall, param.WhitelistedCwCodeHashesForDelegateCall) +} diff --git a/x/evm/module.go b/x/evm/module.go index 5c1c7bc3ca..6af1d9cc39 100644 --- a/x/evm/module.go +++ b/x/evm/module.go @@ -6,7 +6,6 @@ import ( "math" // this line is used by starport scaffolding # 1 - "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" ethtypes "github.com/ethereum/go-ethereum/core/types" diff --git a/x/evm/module_test.go b/x/evm/module_test.go index f35fc90014..8456c5cba9 100644 --- a/x/evm/module_test.go +++ b/x/evm/module_test.go @@ -16,10 +16,44 @@ import ( "github.com/sei-protocol/sei-chain/x/evm" "github.com/sei-protocol/sei-chain/x/evm/state" "github.com/sei-protocol/sei-chain/x/evm/types" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" abci "github.com/tendermint/tendermint/abci/types" ) +func TestModuleName(t *testing.T) { + k, _ := testkeeper.MockEVMKeeper() + module := evm.NewAppModule(nil, k) + assert.Equal(t, "evm", module.Name()) +} + +func TestModuleRoute(t *testing.T) { + k, _ := testkeeper.MockEVMKeeper() + module := evm.NewAppModule(nil, k) + assert.Equal(t, "evm", module.Route().Path()) + assert.Equal(t, false, module.Route().Empty()) +} + +func TestQuerierRoute(t *testing.T) { + k, _ := testkeeper.MockEVMKeeper() + module := evm.NewAppModule(nil, k) + assert.Equal(t, "evm", module.QuerierRoute()) +} + +func TestModuleExportGenesis(t *testing.T) { + k, ctx := testkeeper.MockEVMKeeper() + module := evm.NewAppModule(nil, k) + jsonMsg := module.ExportGenesis(ctx, types.ModuleCdc) + jsonStr := string(jsonMsg) + assert.Equal(t, "{\"params\":{\"priority_normalizer\":\"1.000000000000000000\",\"base_fee_per_gas\":\"0.000000000000000000\",\"minimum_fee_per_gas\":\"1000000000.000000000000000000\",\"whitelisted_cw_code_hashes_for_delegate_call\":[\"ol1416zS7kfMOcIk4WL+ebU+a75u0qVujAqGWT6+YQI=\",\"lM3Zw+hcJvfOxDwjv7SzsrLXGgqNhcWN8S/+wHQf68g=\"]},\"address_associations\":[]}", jsonStr) +} + +func TestConsensusVersion(t *testing.T) { + k, _ := testkeeper.MockEVMKeeper() + module := evm.NewAppModule(nil, k) + assert.Equal(t, uint64(5), module.ConsensusVersion()) +} + func TestABCI(t *testing.T) { k, ctx := testkeeper.MockEVMKeeper() _, evmAddr1 := testkeeper.MockAddressPair() From 36b729031abe232e7eaea370e76f8e42c1cf0c7e Mon Sep 17 00:00:00 2001 From: Uday Patil Date: Tue, 23 Apr 2024 23:26:17 -0500 Subject: [PATCH 08/31] Refactor EVM chain ID to refer to constant map instead of config (#1589) * Refactor EVM chain ID to refer to constant map instead of config * update test * update test * update tests * update default config * update test * revert default chain ID --- aclmapping/evm/mappings_test.go | 4 +-- app/ante_test.go | 2 +- app/app.go | 7 +--- app/app_test.go | 3 +- cmd/seid/cmd/root.go | 7 ---- evmrpc/info.go | 2 +- evmrpc/net.go | 2 +- evmrpc/send.go | 2 +- evmrpc/send_test.go | 4 +-- evmrpc/setup_test.go | 4 +-- evmrpc/simulate.go | 3 +- evmrpc/simulate_test.go | 8 ++--- evmrpc/tx.go | 2 +- evmrpc/txpool.go | 2 +- precompiles/bank/bank_test.go | 2 +- precompiles/distribution/distribution_test.go | 2 +- precompiles/gov/gov_test.go | 4 +-- precompiles/pointer/pointer_test.go | 2 +- precompiles/staking/staking_test.go | 4 +-- x/evm/ante/fee.go | 2 +- x/evm/ante/fee_test.go | 2 +- x/evm/ante/preprocess_test.go | 2 +- x/evm/ante/sig_test.go | 4 +-- x/evm/artifacts/native/artifacts_test.go | 2 +- x/evm/client/wasm/query_test.go | 2 +- x/evm/config/config.go | 34 +++++++------------ x/evm/keeper/evm.go | 2 +- x/evm/keeper/keeper.go | 5 +-- x/evm/keeper/keeper_test.go | 13 +++++-- x/evm/keeper/msg_server.go | 2 +- x/evm/keeper/msg_server_test.go | 12 +++---- x/evm/keeper/params.go | 7 ++-- x/evm/module.go | 2 +- x/evm/state/accesslist_test.go | 2 +- x/evm/types/message_evm_transaction_test.go | 2 +- 35 files changed, 76 insertions(+), 85 deletions(-) diff --git a/aclmapping/evm/mappings_test.go b/aclmapping/evm/mappings_test.go index 512d8d215d..fb56460aa1 100644 --- a/aclmapping/evm/mappings_test.go +++ b/aclmapping/evm/mappings_test.go @@ -80,9 +80,9 @@ func (suite *KeeperTestSuite) buildSendMsgTo(to common.Address, amt *big.Int) *t To: &to, Value: amt, Data: []byte(""), - ChainID: suite.App.EvmKeeper.ChainID(), + ChainID: suite.App.EvmKeeper.ChainID(suite.Ctx), } - ethCfg := types.DefaultChainConfig().EthereumConfig(suite.App.EvmKeeper.ChainID()) + ethCfg := types.DefaultChainConfig().EthereumConfig(suite.App.EvmKeeper.ChainID(suite.Ctx)) signer := ethtypes.MakeSigner(ethCfg, big.NewInt(suite.Ctx.BlockHeight()), uint64(suite.Ctx.BlockTime().Unix())) tx := ethtypes.NewTx(&txData) tx, err := ethtypes.SignTx(tx, signer, suite.sender) diff --git a/app/ante_test.go b/app/ante_test.go index 687e34649b..608b97deeb 100644 --- a/app/ante_test.go +++ b/app/ante_test.go @@ -236,7 +236,7 @@ func TestEvmAnteErrorHandler(t *testing.T) { Data: []byte{}, Nonce: 1, // will cause ante error } - chainID := testkeeper.EVMTestApp.EvmKeeper.ChainID() + chainID := testkeeper.EVMTestApp.EvmKeeper.ChainID(ctx) chainCfg := evmtypes.DefaultChainConfig() ethCfg := chainCfg.EthereumConfig(chainID) blockNum := big.NewInt(ctx.BlockHeight()) diff --git a/app/app.go b/app/app.go index fc9fa4c434..2f58e85219 100644 --- a/app/app.go +++ b/app/app.go @@ -122,7 +122,6 @@ import ( "github.com/sei-protocol/sei-chain/x/evm" evmante "github.com/sei-protocol/sei-chain/x/evm/ante" "github.com/sei-protocol/sei-chain/x/evm/blocktest" - evmconfig "github.com/sei-protocol/sei-chain/x/evm/config" evmkeeper "github.com/sei-protocol/sei-chain/x/evm/keeper" "github.com/sei-protocol/sei-chain/x/evm/querier" "github.com/sei-protocol/sei-chain/x/evm/replay" @@ -588,13 +587,9 @@ func New( wasmOpts..., ) - evmConfig, err := evmconfig.ReadConfig(appOpts) - if err != nil { - panic(fmt.Sprintf("error reading EVM config due to %s", err)) - } app.EvmKeeper = *evmkeeper.NewKeeper(keys[evmtypes.StoreKey], memKeys[evmtypes.MemStoreKey], app.GetSubspace(evmtypes.ModuleName), app.BankKeeper, &app.AccountKeeper, &app.StakingKeeper, - app.TransferKeeper, wasmkeeper.NewDefaultPermissionKeeper(app.WasmKeeper), &evmConfig) + app.TransferKeeper, wasmkeeper.NewDefaultPermissionKeeper(app.WasmKeeper)) app.evmRPCConfig, err = evmrpc.ReadConfig(appOpts) if err != nil { panic(fmt.Sprintf("error reading EVM config due to %s", err)) diff --git a/app/app_test.go b/app/app_test.go index ec48c50a0d..bd0edb69b8 100644 --- a/app/app_test.go +++ b/app/app_test.go @@ -21,6 +21,7 @@ import ( "github.com/sei-protocol/sei-chain/app" testkeeper "github.com/sei-protocol/sei-chain/testutil/keeper" dextypes "github.com/sei-protocol/sei-chain/x/dex/types" + "github.com/sei-protocol/sei-chain/x/evm/config" evmtypes "github.com/sei-protocol/sei-chain/x/evm/types" "github.com/sei-protocol/sei-chain/x/evm/types/ethtx" oracletypes "github.com/sei-protocol/sei-chain/x/oracle/types" @@ -372,7 +373,7 @@ func TestDecodeTransactionsConcurrently(t *testing.T) { Data: []byte("abc"), } chainCfg := evmtypes.DefaultChainConfig() - ethCfg := chainCfg.EthereumConfig(big.NewInt(713715)) + ethCfg := chainCfg.EthereumConfig(big.NewInt(config.DefaultChainID)) signer := ethtypes.MakeSigner(ethCfg, big.NewInt(1), uint64(123)) tx, err := ethtypes.SignTx(ethtypes.NewTx(&txData), signer, key) ethtxdata, _ := ethtx.NewTxDataFromTx(tx) diff --git a/cmd/seid/cmd/root.go b/cmd/seid/cmd/root.go index 331da423ba..495d30270b 100644 --- a/cmd/seid/cmd/root.go +++ b/cmd/seid/cmd/root.go @@ -39,7 +39,6 @@ import ( "github.com/sei-protocol/sei-chain/evmrpc" "github.com/sei-protocol/sei-chain/tools" "github.com/sei-protocol/sei-chain/x/evm/blocktest" - evmconfig "github.com/sei-protocol/sei-chain/x/evm/config" "github.com/sei-protocol/sei-chain/x/evm/querier" "github.com/sei-protocol/sei-chain/x/evm/replay" "github.com/spf13/cast" @@ -382,8 +381,6 @@ func initAppConfig() (string, interface{}) { ETHBlockTest blocktest.Config `mapstructure:"eth_block_test"` EvmQuery querier.Config `mapstructure:"evm_query"` - - EvmModule evmconfig.Config `mapstructure:"evm_module"` } // Optionally allow the chain developer to overwrite the SDK's default @@ -427,7 +424,6 @@ func initAppConfig() (string, interface{}) { ETHReplay: replay.DefaultConfig, ETHBlockTest: blocktest.DefaultConfig, EvmQuery: querier.DefaultConfig, - EvmModule: evmconfig.DefaultConfig, } customAppTemplate := serverconfig.DefaultConfigTemplate + ` @@ -516,9 +512,6 @@ eth_blocktest_test_data_path = "{{ .ETHBlockTest.TestDataPath }}" [evm_query] evm_query_gas_limit = {{ .EvmQuery.GasLimit }} - -[evm_module] -evm_chain_id = {{ .EvmModule.ChainID }} ` return customAppTemplate, customAppConfig diff --git a/evmrpc/info.go b/evmrpc/info.go index b6f71e2191..e165cc3cf4 100644 --- a/evmrpc/info.go +++ b/evmrpc/info.go @@ -46,7 +46,7 @@ func (i *InfoAPI) BlockNumber() hexutil.Uint64 { func (i *InfoAPI) ChainId() *hexutil.Big { startTime := time.Now() defer recordMetrics("eth_ChainId", startTime, true) - return (*hexutil.Big)(i.keeper.ChainID()) + return (*hexutil.Big)(i.keeper.ChainID(i.ctxProvider(LatestCtxHeight))) } func (i *InfoAPI) Coinbase() (common.Address, error) { diff --git a/evmrpc/net.go b/evmrpc/net.go index a32f31c530..9bc603cc84 100644 --- a/evmrpc/net.go +++ b/evmrpc/net.go @@ -23,5 +23,5 @@ func NewNetAPI(tmClient rpcclient.Client, k *keeper.Keeper, ctxProvider func(int func (i *NetAPI) Version() string { startTime := time.Now() defer recordMetrics("net_version", startTime, true) - return fmt.Sprintf("%d", i.keeper.ChainID().Uint64()) + return fmt.Sprintf("%d", i.keeper.ChainID(i.ctxProvider(LatestCtxHeight)).Uint64()) } diff --git a/evmrpc/send.go b/evmrpc/send.go index 264df5b5e2..ec388d84c4 100644 --- a/evmrpc/send.go +++ b/evmrpc/send.go @@ -134,7 +134,7 @@ func (s *SendAPI) signTransaction(unsignedTx *ethtypes.Transaction, from string) if !ok { return nil, errors.New("from address does not have hosted key") } - chainId := s.keeper.ChainID() + chainId := s.keeper.ChainID(s.ctxProvider(LatestCtxHeight)) signer := ethtypes.LatestSignerForChainID(chainId) return ethtypes.SignTx(unsignedTx, signer, privKey) } diff --git a/evmrpc/send_test.go b/evmrpc/send_test.go index 6a4da68893..2f86cc7904 100644 --- a/evmrpc/send_test.go +++ b/evmrpc/send_test.go @@ -33,14 +33,14 @@ func TestSendRawTransaction(t *testing.T) { To: &to, Value: big.NewInt(1000), Data: []byte("abc"), - ChainID: EVMKeeper.ChainID(), + ChainID: EVMKeeper.ChainID(Ctx), } mnemonic := "fish mention unlock february marble dove vintage sand hub ordinary fade found inject room embark supply fabric improve spike stem give current similar glimpse" derivedPriv, _ := hd.Secp256k1.Derive()(mnemonic, "", "") privKey := hd.Secp256k1.Generate()(derivedPriv) testPrivHex := hex.EncodeToString(privKey.Bytes()) key, _ := crypto.HexToECDSA(testPrivHex) - ethCfg := types.DefaultChainConfig().EthereumConfig(EVMKeeper.ChainID()) + ethCfg := types.DefaultChainConfig().EthereumConfig(EVMKeeper.ChainID(Ctx)) signer := ethtypes.MakeSigner(ethCfg, big.NewInt(Ctx.BlockHeight()), uint64(Ctx.BlockTime().Unix())) tx := ethtypes.NewTx(&txData) tx, err := ethtypes.SignTx(tx, signer, key) diff --git a/evmrpc/setup_test.go b/evmrpc/setup_test.go index fa0fffbb02..dd086b82b7 100644 --- a/evmrpc/setup_test.go +++ b/evmrpc/setup_test.go @@ -451,7 +451,7 @@ func init() { } func generateTxData() { - chainId := big.NewInt(config.DefaultConfig.ChainID) + chainId := big.NewInt(config.DefaultChainID) to := common.HexToAddress("010203") var txBuilder1, txBuilder1_5, txBuilder2, txBuilder3, txBuilder4 client.TxBuilder txBuilder1, tx1 = buildTx(ethtypes.DynamicFeeTx{ @@ -574,7 +574,7 @@ func generateTxData() { } func buildTx(txData ethtypes.DynamicFeeTx) (client.TxBuilder, *ethtypes.Transaction) { - chainId := big.NewInt(config.DefaultConfig.ChainID) + chainId := big.NewInt(config.DefaultChainID) mnemonic := "fish mention unlock february marble dove vintage sand hub ordinary fade found inject room embark supply fabric improve spike stem give current similar glimpse" derivedPriv, _ := hd.Secp256k1.Derive()(mnemonic, "", "") privKey := hd.Secp256k1.Generate()(derivedPriv) diff --git a/evmrpc/simulate.go b/evmrpc/simulate.go index 5fc664297e..8e82ef1b3c 100644 --- a/evmrpc/simulate.go +++ b/evmrpc/simulate.go @@ -243,7 +243,8 @@ func (b *Backend) RPCGasCap() uint64 { return b.config.GasCap } func (b *Backend) RPCEVMTimeout() time.Duration { return b.config.EVMTimeout } func (b *Backend) ChainConfig() *params.ChainConfig { - return types.DefaultChainConfig().EthereumConfig(b.keeper.ChainID()) + ctx := b.ctxProvider(LatestCtxHeight) + return types.DefaultChainConfig().EthereumConfig(b.keeper.ChainID(ctx)) } func (b *Backend) GetPoolNonce(_ context.Context, addr common.Address) (uint64, error) { diff --git a/evmrpc/simulate_test.go b/evmrpc/simulate_test.go index fb9e13dfc8..28b5a06f03 100644 --- a/evmrpc/simulate_test.go +++ b/evmrpc/simulate_test.go @@ -26,7 +26,7 @@ func TestEstimateGas(t *testing.T) { "to": to.Hex(), "value": "0x10", "nonce": "0x1", - "chainId": fmt.Sprintf("%#x", EVMKeeper.ChainID()), + "chainId": fmt.Sprintf("%#x", EVMKeeper.ChainID(Ctx)), } amts := sdk.NewCoins(sdk.NewCoin(EVMKeeper.GetBaseDenom(Ctx), sdk.NewInt(20))) EVMKeeper.BankKeeper().MintCoins(Ctx, types.ModuleName, amts) @@ -57,7 +57,7 @@ func TestEstimateGas(t *testing.T) { "to": contractAddr.Hex(), "value": "0x0", "nonce": "0x2", - "chainId": fmt.Sprintf("%#x", EVMKeeper.ChainID()), + "chainId": fmt.Sprintf("%#x", EVMKeeper.ChainID(Ctx)), "input": fmt.Sprintf("%#x", input), } resObj = sendRequestGood(t, "estimateGas", txArgs, nil, map[string]interface{}{}) @@ -86,7 +86,7 @@ func TestCreateAccessList(t *testing.T) { "to": contractAddr.Hex(), "value": "0x0", "nonce": "0x1", - "chainId": fmt.Sprintf("%#x", EVMKeeper.ChainID()), + "chainId": fmt.Sprintf("%#x", EVMKeeper.ChainID(Ctx)), "input": fmt.Sprintf("%#x", input), } amts := sdk.NewCoins(sdk.NewCoin(EVMKeeper.GetBaseDenom(Ctx), sdk.NewInt(20))) @@ -122,7 +122,7 @@ func TestCall(t *testing.T) { "to": contractAddr.Hex(), "value": "0x0", "nonce": "0x2", - "chainId": fmt.Sprintf("%#x", EVMKeeper.ChainID()), + "chainId": fmt.Sprintf("%#x", EVMKeeper.ChainID(Ctx)), "input": fmt.Sprintf("%#x", input), } resObj := sendRequestGood(t, "call", txArgs, nil, map[string]interface{}{}, map[string]interface{}{}) diff --git a/evmrpc/tx.go b/evmrpc/tx.go index 9f59bdda3c..3863506be7 100644 --- a/evmrpc/tx.go +++ b/evmrpc/tx.go @@ -105,7 +105,7 @@ func (t *TransactionAPI) GetTransactionByHash(ctx context.Context, hash common.H etx := getEthTxForTxBz(tx, t.txConfig.TxDecoder()) if etx != nil && etx.Hash() == hash { signer := ethtypes.MakeSigner( - types.DefaultChainConfig().EthereumConfig(t.keeper.ChainID()), + types.DefaultChainConfig().EthereumConfig(t.keeper.ChainID(sdkCtx)), big.NewInt(sdkCtx.BlockHeight()), uint64(sdkCtx.BlockTime().Unix()), ) diff --git a/evmrpc/txpool.go b/evmrpc/txpool.go index 802bdbf7b6..48f8f8f160 100644 --- a/evmrpc/txpool.go +++ b/evmrpc/txpool.go @@ -46,7 +46,7 @@ func (t *TxPoolAPI) Content(ctx context.Context) (result map[string]map[string]m sdkCtx := t.ctxProvider(LatestCtxHeight) signer := ethtypes.MakeSigner( - types.DefaultChainConfig().EthereumConfig(t.keeper.ChainID()), + types.DefaultChainConfig().EthereumConfig(t.keeper.ChainID(sdkCtx)), big.NewInt(sdkCtx.BlockHeight()), uint64(sdkCtx.BlockTime().Unix()), ) diff --git a/precompiles/bank/bank_test.go b/precompiles/bank/bank_test.go index 8552b3fd38..d8ad3e9867 100644 --- a/precompiles/bank/bank_test.go +++ b/precompiles/bank/bank_test.go @@ -113,7 +113,7 @@ func TestRun(t *testing.T) { Data: argsNative, Nonce: 0, } - chainID := k.ChainID() + chainID := k.ChainID(ctx) chainCfg := types.DefaultChainConfig() ethCfg := chainCfg.EthereumConfig(chainID) blockNum := big.NewInt(ctx.BlockHeight()) diff --git a/precompiles/distribution/distribution_test.go b/precompiles/distribution/distribution_test.go index 3d27d57b28..1790961250 100644 --- a/precompiles/distribution/distribution_test.go +++ b/precompiles/distribution/distribution_test.go @@ -55,7 +55,7 @@ func TestWithdraw(t *testing.T) { Data: args, Nonce: 0, } - chainID := k.ChainID() + chainID := k.ChainID(ctx) chainCfg := evmtypes.DefaultChainConfig() ethCfg := chainCfg.EthereumConfig(chainID) blockNum := big.NewInt(ctx.BlockHeight()) diff --git a/precompiles/gov/gov_test.go b/precompiles/gov/gov_test.go index d122e1d798..9e77c36afd 100644 --- a/precompiles/gov/gov_test.go +++ b/precompiles/gov/gov_test.go @@ -45,7 +45,7 @@ func TestVoteDeposit(t *testing.T) { Data: args, Nonce: 0, } - chainID := k.ChainID() + chainID := k.ChainID(ctx) chainCfg := evmtypes.DefaultChainConfig() ethCfg := chainCfg.EthereumConfig(chainID) blockNum := big.NewInt(ctx.BlockHeight()) @@ -90,7 +90,7 @@ func TestVoteDeposit(t *testing.T) { Data: args, Nonce: 0, } - chainID := k.ChainID() + chainID := k.ChainID(ctx) chainCfg := evmtypes.DefaultChainConfig() ethCfg := chainCfg.EthereumConfig(chainID) blockNum := big.NewInt(ctx.BlockHeight()) diff --git a/precompiles/pointer/pointer_test.go b/precompiles/pointer/pointer_test.go index eb893454fd..f621a66c39 100644 --- a/precompiles/pointer/pointer_test.go +++ b/precompiles/pointer/pointer_test.go @@ -24,7 +24,7 @@ func TestAddNative(t *testing.T) { ctx := testApp.GetContextForDeliverTx([]byte{}).WithBlockTime(time.Now()) _, caller := testkeeper.MockAddressPair() suppliedGas := uint64(10000000) - cfg := types.DefaultChainConfig().EthereumConfig(testApp.EvmKeeper.ChainID()) + cfg := types.DefaultChainConfig().EthereumConfig(testApp.EvmKeeper.ChainID(ctx)) // token has no metadata m, err := p.ABI.MethodById(p.AddNativePointerID) diff --git a/precompiles/staking/staking_test.go b/precompiles/staking/staking_test.go index 4f32535902..682dbcf2b3 100644 --- a/precompiles/staking/staking_test.go +++ b/precompiles/staking/staking_test.go @@ -53,7 +53,7 @@ func TestStaking(t *testing.T) { Data: args, Nonce: 0, } - chainID := k.ChainID() + chainID := k.ChainID(ctx) chainCfg := evmtypes.DefaultChainConfig() ethCfg := chainCfg.EthereumConfig(chainID) blockNum := big.NewInt(ctx.BlockHeight()) @@ -162,7 +162,7 @@ func TestStakingError(t *testing.T) { Data: args, Nonce: 0, } - chainID := k.ChainID() + chainID := k.ChainID(ctx) chainCfg := evmtypes.DefaultChainConfig() ethCfg := chainCfg.EthereumConfig(chainID) blockNum := big.NewInt(ctx.BlockHeight()) diff --git a/x/evm/ante/fee.go b/x/evm/ante/fee.go index 10988e0b33..1e64bd920a 100644 --- a/x/evm/ante/fee.go +++ b/x/evm/ante/fee.go @@ -66,7 +66,7 @@ func (fc EVMFeeCheckDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate b if err != nil { return ctx, err } - cfg := evmtypes.DefaultChainConfig().EthereumConfig(fc.evmKeeper.ChainID()) + cfg := evmtypes.DefaultChainConfig().EthereumConfig(fc.evmKeeper.ChainID(ctx)) txCtx := core.NewEVMTxContext(emsg) evmInstance := vm.NewEVM(*blockCtx, txCtx, stateDB, cfg, vm.Config{}) stateDB.SetEVM(evmInstance) diff --git a/x/evm/ante/fee_test.go b/x/evm/ante/fee_test.go index 23f780d3dd..8b47c980e1 100644 --- a/x/evm/ante/fee_test.go +++ b/x/evm/ante/fee_test.go @@ -27,7 +27,7 @@ func TestEVMFeeCheckDecorator(t *testing.T) { key, _ := crypto.HexToECDSA(testPrivHex) to := new(common.Address) copy(to[:], []byte("0x1234567890abcdef1234567890abcdef12345678")) - chainID := k.ChainID() + chainID := k.ChainID(ctx) txData := ethtypes.DynamicFeeTx{ Nonce: 0, GasFeeCap: big.NewInt(10000000000000), diff --git a/x/evm/ante/preprocess_test.go b/x/evm/ante/preprocess_test.go index 9ec2f23a6f..bec4ea6850 100644 --- a/x/evm/ante/preprocess_test.go +++ b/x/evm/ante/preprocess_test.go @@ -44,7 +44,7 @@ func TestPreprocessAnteHandler(t *testing.T) { Value: big.NewInt(1000), Data: []byte("abc"), } - chainID := k.ChainID() + chainID := k.ChainID(ctx) chainCfg := types.DefaultChainConfig() ethCfg := chainCfg.EthereumConfig(chainID) blockNum := big.NewInt(ctx.BlockHeight()) diff --git a/x/evm/ante/sig_test.go b/x/evm/ante/sig_test.go index db63952280..22bae527aa 100644 --- a/x/evm/ante/sig_test.go +++ b/x/evm/ante/sig_test.go @@ -33,7 +33,7 @@ func TestEVMSigVerifyDecorator(t *testing.T) { Value: big.NewInt(1000), Data: []byte("abc"), } - chainID := k.ChainID() + chainID := k.ChainID(ctx) chainCfg := types.DefaultChainConfig() ethCfg := chainCfg.EthereumConfig(chainID) blockNum := big.NewInt(ctx.BlockHeight()) @@ -116,7 +116,7 @@ func TestSigVerifyPendingTransaction(t *testing.T) { Value: big.NewInt(1000), Data: []byte("abc"), } - chainID := k.ChainID() + chainID := k.ChainID(ctx) chainCfg := types.DefaultChainConfig() ethCfg := chainCfg.EthereumConfig(chainID) blockNum := big.NewInt(ctx.BlockHeight()) diff --git a/x/evm/artifacts/native/artifacts_test.go b/x/evm/artifacts/native/artifacts_test.go index 88f15e6e3f..e023def9b2 100644 --- a/x/evm/artifacts/native/artifacts_test.go +++ b/x/evm/artifacts/native/artifacts_test.go @@ -41,7 +41,7 @@ func TestSimple(t *testing.T) { Data: contractData, Nonce: 0, } - chainID := k.ChainID() + chainID := k.ChainID(ctx) chainCfg := types.DefaultChainConfig() ethCfg := chainCfg.EthereumConfig(chainID) blockNum := big.NewInt(ctx.BlockHeight()) diff --git a/x/evm/client/wasm/query_test.go b/x/evm/client/wasm/query_test.go index f295e4aaf3..536f1caf04 100644 --- a/x/evm/client/wasm/query_test.go +++ b/x/evm/client/wasm/query_test.go @@ -154,7 +154,7 @@ func TestHandleStaticCall(t *testing.T) { Data: bz, Nonce: 0, } - chainID := k.ChainID() + chainID := k.ChainID(ctx) chainCfg := evmtypes.DefaultChainConfig() ethCfg := chainCfg.EthereumConfig(chainID) blockNum := big.NewInt(ctx.BlockHeight()) diff --git a/x/evm/config/config.go b/x/evm/config/config.go index 8722424c26..c0535da0f7 100644 --- a/x/evm/config/config.go +++ b/x/evm/config/config.go @@ -1,29 +1,21 @@ package config -import ( - servertypes "github.com/cosmos/cosmos-sdk/server/types" - "github.com/spf13/cast" -) +import "math/big" -type Config struct { - ChainID int64 `mapstructure:"evm_chain_id"` -} +const DefaultChainID = int64(713715) -var DefaultConfig = Config{ - ChainID: 713715, +// ChainIDMapping is a mapping of cosmos chain IDs to their respective chain IDs. +var ChainIDMapping = map[string]int64{ + // pacific-1 chain ID == 0x531 + "pacific-1": int64(1329), + // atlantic-2 chain ID == 0x530 + "atlantic-2": int64(1328), + "arctic-1": int64(713715), } -const ( - flagChainID = "evm_module.evm_chain_id" -) - -func ReadConfig(opts servertypes.AppOptions) (Config, error) { - cfg := DefaultConfig // copy - var err error - if v := opts.Get(flagChainID); v != nil { - if cfg.ChainID, err = cast.ToInt64E(v); err != nil { - return cfg, err - } +func GetEVMChainID(cosmosChainID string) *big.Int { + if evmChainID, ok := ChainIDMapping[cosmosChainID]; ok { + return big.NewInt(evmChainID) } - return cfg, nil + return big.NewInt(DefaultChainID) } diff --git a/x/evm/keeper/evm.go b/x/evm/keeper/evm.go index bb56693d53..c2664bb868 100644 --- a/x/evm/keeper/evm.go +++ b/x/evm/keeper/evm.go @@ -155,7 +155,7 @@ func (k *Keeper) getOrCreateEVM(ctx sdk.Context, from sdk.AccAddress) (*vm.EVM, if err != nil { return nil, err } - cfg := types.DefaultChainConfig().EthereumConfig(k.ChainID()) + cfg := types.DefaultChainConfig().EthereumConfig(k.ChainID(ctx)) txCtx := vm.TxContext{Origin: k.GetEVMAddressOrDefault(ctx, from)} evm = vm.NewEVM(*blockCtx, txCtx, stateDB, cfg, vm.Config{}) stateDB.SetEVM(evm) diff --git a/x/evm/keeper/keeper.go b/x/evm/keeper/keeper.go index 426ae61655..2097328431 100644 --- a/x/evm/keeper/keeper.go +++ b/x/evm/keeper/keeper.go @@ -33,7 +33,6 @@ import ( "github.com/sei-protocol/sei-chain/utils" "github.com/sei-protocol/sei-chain/x/evm/blocktest" - "github.com/sei-protocol/sei-chain/x/evm/config" "github.com/sei-protocol/sei-chain/x/evm/querier" "github.com/sei-protocol/sei-chain/x/evm/replay" "github.com/sei-protocol/sei-chain/x/evm/state" @@ -60,7 +59,6 @@ type Keeper struct { pendingTxs map[string][]*PendingTx keyToNonce map[tmtypes.TxKey]*AddressNoncePair - Config *config.Config QueryConfig *querier.Config // only used during ETH replay. Not used in chain critical path. @@ -117,7 +115,7 @@ func (ctx *ReplayChainContext) GetHeader(hash common.Hash, number uint64) *ethty func NewKeeper( storeKey sdk.StoreKey, memStoreKey sdk.StoreKey, paramstore paramtypes.Subspace, bankKeeper bankkeeper.Keeper, accountKeeper *authkeeper.AccountKeeper, stakingKeeper *stakingkeeper.Keeper, - transferKeeper ibctransferkeeper.Keeper, wasmKeeper *wasmkeeper.PermissionedKeeper, conf *config.Config) *Keeper { + transferKeeper ibctransferkeeper.Keeper, wasmKeeper *wasmkeeper.PermissionedKeeper) *Keeper { if !paramstore.HasKeyTable() { paramstore = paramstore.WithKeyTable(types.ParamKeyTable()) } @@ -135,7 +133,6 @@ func NewKeeper( cachedFeeCollectorAddressMtx: &sync.RWMutex{}, keyToNonce: make(map[tmtypes.TxKey]*AddressNoncePair), deferredInfo: &sync.Map{}, - Config: conf, } return k } diff --git a/x/evm/keeper/keeper_test.go b/x/evm/keeper/keeper_test.go index 5df83bd7ee..1dbd34b563 100644 --- a/x/evm/keeper/keeper_test.go +++ b/x/evm/keeper/keeper_test.go @@ -31,8 +31,17 @@ func TestPurgePrefixNotHang(t *testing.T) { } func TestGetChainID(t *testing.T) { - k, _ := keeper.MockEVMKeeper() - require.Equal(t, config.DefaultConfig.ChainID, k.ChainID().Int64()) + k, ctx := keeper.MockEVMKeeper() + require.Equal(t, config.DefaultChainID, k.ChainID(ctx).Int64()) + + ctx = ctx.WithChainID("pacific-1") + require.Equal(t, int64(1329), k.ChainID(ctx).Int64()) + + ctx = ctx.WithChainID("atlantic-2") + require.Equal(t, int64(1328), k.ChainID(ctx).Int64()) + + ctx = ctx.WithChainID("arctic-1") + require.Equal(t, int64(713715), k.ChainID(ctx).Int64()) } func TestGetVMBlockContext(t *testing.T) { diff --git a/x/evm/keeper/msg_server.go b/x/evm/keeper/msg_server.go index 370ab4f6d8..d295730d5c 100644 --- a/x/evm/keeper/msg_server.go +++ b/x/evm/keeper/msg_server.go @@ -196,7 +196,7 @@ func (k Keeper) applyEVMMessage(ctx sdk.Context, msg *core.Message, stateDB *sta if err != nil { return nil, err } - cfg := types.DefaultChainConfig().EthereumConfig(k.ChainID()) + cfg := types.DefaultChainConfig().EthereumConfig(k.ChainID(ctx)) txCtx := core.NewEVMTxContext(msg) evmInstance := vm.NewEVM(*blockCtx, txCtx, stateDB, cfg, vm.Config{}) stateDB.SetEVM(evmInstance) diff --git a/x/evm/keeper/msg_server_test.go b/x/evm/keeper/msg_server_test.go index 99823375a1..5d1fc24eae 100644 --- a/x/evm/keeper/msg_server_test.go +++ b/x/evm/keeper/msg_server_test.go @@ -55,7 +55,7 @@ func TestEVMTransaction(t *testing.T) { Data: bz, Nonce: 0, } - chainID := k.ChainID() + chainID := k.ChainID(ctx) chainCfg := types.DefaultChainConfig() ethCfg := chainCfg.EthereumConfig(chainID) blockNum := big.NewInt(ctx.BlockHeight()) @@ -144,7 +144,7 @@ func TestEVMTransactionError(t *testing.T) { Data: []byte("123090321920390920123"), // gibberish data Nonce: 0, } - chainID := k.ChainID() + chainID := k.ChainID(ctx) chainCfg := types.DefaultChainConfig() ethCfg := chainCfg.EthereumConfig(chainID) blockNum := big.NewInt(ctx.BlockHeight()) @@ -198,7 +198,7 @@ func TestEVMTransactionInsufficientGas(t *testing.T) { Data: bz, Nonce: 0, } - chainID := k.ChainID() + chainID := k.ChainID(ctx) chainCfg := types.DefaultChainConfig() ethCfg := chainCfg.EthereumConfig(chainID) blockNum := big.NewInt(ctx.BlockHeight()) @@ -246,7 +246,7 @@ func TestEVMDynamicFeeTransaction(t *testing.T) { Data: bz, Nonce: 0, } - chainID := k.ChainID() + chainID := k.ChainID(ctx) chainCfg := types.DefaultChainConfig() ethCfg := chainCfg.EthereumConfig(chainID) blockNum := big.NewInt(ctx.BlockHeight()) @@ -303,7 +303,7 @@ func TestEVMPrecompiles(t *testing.T) { Data: bz, Nonce: 0, } - chainID := k.ChainID() + chainID := k.ChainID(ctx) chainCfg := types.DefaultChainConfig() ethCfg := chainCfg.EthereumConfig(chainID) blockNum := big.NewInt(ctx.BlockHeight()) @@ -418,7 +418,7 @@ func TestEVMBlockEnv(t *testing.T) { Data: bz, Nonce: 0, } - chainID := k.ChainID() + chainID := k.ChainID(ctx) chainCfg := types.DefaultChainConfig() ethCfg := chainCfg.EthereumConfig(chainID) blockNum := big.NewInt(ctx.BlockHeight()) diff --git a/x/evm/keeper/params.go b/x/evm/keeper/params.go index 98107d36a3..211e387d45 100644 --- a/x/evm/keeper/params.go +++ b/x/evm/keeper/params.go @@ -5,6 +5,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/sei-protocol/sei-chain/utils" + "github.com/sei-protocol/sei-chain/x/evm/config" "github.com/sei-protocol/sei-chain/x/evm/types" ) @@ -36,12 +37,14 @@ func (k *Keeper) GetMinimumFeePerGas(ctx sdk.Context) sdk.Dec { return k.GetParams(ctx).MinimumFeePerGas } -func (k *Keeper) ChainID() *big.Int { +func (k *Keeper) ChainID(ctx sdk.Context) *big.Int { if k.EthReplayConfig.Enabled || k.EthBlockTestConfig.Enabled { // replay is for eth mainnet so always return 1 return utils.Big1 } - return big.NewInt(k.Config.ChainID) + // return mapped chain ID + return config.GetEVMChainID(ctx.ChainID()) + } func (k *Keeper) WhitelistedCwCodeHashesForDelegateCall(ctx sdk.Context) [][]byte { diff --git a/x/evm/module.go b/x/evm/module.go index 6af1d9cc39..8286973b54 100644 --- a/x/evm/module.go +++ b/x/evm/module.go @@ -183,7 +183,7 @@ func (am AppModule) BeginBlock(ctx sdk.Context, _ abci.RequestBeginBlock) { panic(err) } statedb := state.NewDBImpl(ctx, am.keeper, false) - vmenv := vm.NewEVM(*blockCtx, vm.TxContext{}, statedb, types.DefaultChainConfig().EthereumConfig(am.keeper.ChainID()), vm.Config{}) + vmenv := vm.NewEVM(*blockCtx, vm.TxContext{}, statedb, types.DefaultChainConfig().EthereumConfig(am.keeper.ChainID(ctx)), vm.Config{}) core.ProcessBeaconBlockRoot(*beaconRoot, vmenv, statedb) _, err = statedb.Finalize() if err != nil { diff --git a/x/evm/state/accesslist_test.go b/x/evm/state/accesslist_test.go index dc455f73f6..9b6fe1e476 100644 --- a/x/evm/state/accesslist_test.go +++ b/x/evm/state/accesslist_test.go @@ -77,7 +77,7 @@ func TestPrepare(t *testing.T) { }, }, } - shanghai := params.Rules{ChainID: k.ChainID(), IsShanghai: true} + shanghai := params.Rules{ChainID: k.ChainID(ctx), IsShanghai: true} statedb.Prepare( shanghai, sender, coinbase, &dest, precompiles, txaccesses, ) diff --git a/x/evm/types/message_evm_transaction_test.go b/x/evm/types/message_evm_transaction_test.go index bf780e25ee..2c50096103 100644 --- a/x/evm/types/message_evm_transaction_test.go +++ b/x/evm/types/message_evm_transaction_test.go @@ -36,7 +36,7 @@ func TestIsNotAssociate(t *testing.T) { func TestAsTransaction(t *testing.T) { k, ctx := testkeeper.MockEVMKeeper() - chainID := k.ChainID() + chainID := k.ChainID(ctx) chainCfg := types.DefaultChainConfig() ethCfg := chainCfg.EthereumConfig(chainID) blockNum := big.NewInt(ctx.BlockHeight()) From e80e074f7e2f452063177095b2f7aaa25ad480b7 Mon Sep 17 00:00:00 2001 From: Uday Patil Date: Wed, 24 Apr 2024 08:52:21 -0500 Subject: [PATCH 09/31] Disallow negative value passed into internal EVM calls (#1590) * Disallow negative value passed into internal EVM calls * Update defensive checks when calling EVM internally --- x/evm/keeper/evm.go | 16 +++++++++--- x/evm/keeper/evm_test.go | 53 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 4 deletions(-) diff --git a/x/evm/keeper/evm.go b/x/evm/keeper/evm.go index c2664bb868..032be4e96c 100644 --- a/x/evm/keeper/evm.go +++ b/x/evm/keeper/evm.go @@ -7,10 +7,12 @@ import ( "math/big" sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/params" "github.com/sei-protocol/sei-chain/utils" "github.com/sei-protocol/sei-chain/x/evm/state" "github.com/sei-protocol/sei-chain/x/evm/types" @@ -65,16 +67,22 @@ func (k *Keeper) HandleInternalEVMDelegateCall(ctx sdk.Context, req *types.MsgIn } func (k *Keeper) CallEVM(ctx sdk.Context, from common.Address, to *common.Address, val *sdk.Int, data []byte) (retdata []byte, reterr error) { + if to == nil && len(data) > params.MaxInitCodeSize { + return nil, fmt.Errorf("%w: code size %v, limit %v", core.ErrMaxInitCodeSizeExceeded, len(data), params.MaxInitCodeSize) + } + value := utils.Big0 + if val != nil { + if val.IsNegative() { + return nil, sdkerrors.ErrInvalidCoins + } + value = val.BigInt() + } evm := types.GetCtxEVM(ctx) if evm == nil { // This call was not part of an existing StateTransition, so it should trigger one executionCtx := ctx.WithGasMeter(sdk.NewInfiniteGasMeter()) stateDB := state.NewDBImpl(executionCtx, k, false) gp := k.GetGasPool() - value := utils.Big0 - if val != nil { - value = val.BigInt() - } evmMsg := &core.Message{ Nonce: stateDB.GetNonce(from), // replay attack is prevented by the AccountSequence number set on the CW transaction that triggered this call GasLimit: k.getEvmGasLimitFromCtx(ctx), diff --git a/x/evm/keeper/evm_test.go b/x/evm/keeper/evm_test.go index 940e246ec7..a7e7fc024d 100644 --- a/x/evm/keeper/evm_test.go +++ b/x/evm/keeper/evm_test.go @@ -6,6 +6,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/params" testkeeper "github.com/sei-protocol/sei-chain/testutil/keeper" "github.com/sei-protocol/sei-chain/x/evm/artifacts/native" "github.com/sei-protocol/sei-chain/x/evm/types" @@ -115,3 +116,55 @@ func TestStaticCall(t *testing.T) { require.Equal(t, 1, len(decoded)) require.Equal(t, big.NewInt(int64(2000)), decoded[0].(*big.Int)) } + +func TestNegativeTransfer(t *testing.T) { + steal_amount := int64(1_000_000_000_000) + + k := testkeeper.EVMTestApp.EvmKeeper + ctx := testkeeper.EVMTestApp.NewContext(false, tmtypes.Header{}).WithBlockHeight(2) + attackerAddr, attackerEvmAddr := testkeeper.MockAddressPair() + victimAddr, victimEvmAddr := testkeeper.MockAddressPair() + + // associate addrs + k.SetAddressMapping(ctx, attackerAddr, attackerEvmAddr) + k.SetAddressMapping(ctx, victimAddr, victimEvmAddr) + + // mint some funds to victim + amt := sdk.NewCoins(sdk.NewCoin(k.GetBaseDenom(ctx), sdk.NewInt(steal_amount))) + require.Nil(t, k.BankKeeper().MintCoins(ctx, types.ModuleName, sdk.NewCoins(sdk.NewCoin(k.GetBaseDenom(ctx), sdk.NewInt(steal_amount))))) + require.Nil(t, k.BankKeeper().SendCoinsFromModuleToAccount(ctx, types.ModuleName, victimAddr, amt)) + + // construct attack payload + val := sdk.NewInt(steal_amount).Mul(sdk.NewInt(steal_amount * -1)) + req := &types.MsgInternalEVMCall{ + Sender: attackerAddr.String(), + Data: []byte{}, + Value: &val, + To: victimEvmAddr.Hex(), + } + + // pre verification + preAttackerBal := testkeeper.EVMTestApp.BankKeeper.GetBalance(ctx, attackerAddr, k.GetBaseDenom(ctx)).Amount.Int64() + preVictimBal := testkeeper.EVMTestApp.BankKeeper.GetBalance(ctx, victimAddr, k.GetBaseDenom(ctx)).Amount.Int64() + require.Zero(t, preAttackerBal) + require.Equal(t, steal_amount, preVictimBal) + + _, err := k.HandleInternalEVMCall(ctx, req) + require.ErrorContains(t, err, "invalid coins") + + // post verification + postAttackerBal := testkeeper.EVMTestApp.BankKeeper.GetBalance(ctx, attackerAddr, k.GetBaseDenom(ctx)).Amount.Int64() + postVictimBal := testkeeper.EVMTestApp.BankKeeper.GetBalance(ctx, victimAddr, k.GetBaseDenom(ctx)).Amount.Int64() + require.Zero(t, postAttackerBal) + require.Equal(t, steal_amount, postVictimBal) + + zeroVal := sdk.NewInt(0) + req2 := &types.MsgInternalEVMCall{ + Sender: attackerAddr.String(), + Data: make([]byte, params.MaxInitCodeSize+1), + Value: &zeroVal, + } + + _, err = k.HandleInternalEVMCall(ctx, req2) + require.ErrorContains(t, err, "max initcode size exceeded") +} From 14ce99704f3f863d3487f389bc4aa9e13ff0c883 Mon Sep 17 00:00:00 2001 From: Jeremy Wei Date: Wed, 24 Apr 2024 10:22:54 -0400 Subject: [PATCH 10/31] ETH Replay - add flag to disable contract state checks (#1580) * eth replay - add flag to disable contract state checks * go fmt --- app/eth_replay.go | 4 +++- cmd/seid/cmd/root.go | 1 + x/evm/replay/config.go | 26 +++++++++++++++++--------- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/app/eth_replay.go b/app/eth_replay.go index 80b045068a..d63a3f1ddf 100644 --- a/app/eth_replay.go +++ b/app/eth_replay.go @@ -92,7 +92,9 @@ func Replay(a *App) { a.Logger().Info(fmt.Sprintf("Verifying tx %s", tx.Hash().Hex())) if tx.To() != nil { a.EvmKeeper.VerifyBalance(ctx, *tx.To()) - a.EvmKeeper.VerifyState(ctx, *tx.To()) + if a.EvmKeeper.EthReplayConfig.ContractStateChecks { + a.EvmKeeper.VerifyState(ctx, *tx.To()) + } } a.EvmKeeper.VerifyTxResult(ctx, tx.Hash()) } diff --git a/cmd/seid/cmd/root.go b/cmd/seid/cmd/root.go index 495d30270b..be53376715 100644 --- a/cmd/seid/cmd/root.go +++ b/cmd/seid/cmd/root.go @@ -505,6 +505,7 @@ max_blocks_for_log = {{ .EVM.MaxBlocksForLog }} eth_replay_enabled = {{ .ETHReplay.Enabled }} eth_rpc = "{{ .ETHReplay.EthRPC }}" eth_data_dir = "{{ .ETHReplay.EthDataDir }}" +eth_replay_contract_state_checks = {{ .ETHReplay.ContractStateChecks }} [eth_blocktest] eth_blocktest_enabled = {{ .ETHBlockTest.Enabled }} diff --git a/x/evm/replay/config.go b/x/evm/replay/config.go index 638c868adf..f718e1a565 100644 --- a/x/evm/replay/config.go +++ b/x/evm/replay/config.go @@ -6,21 +6,24 @@ import ( ) type Config struct { - Enabled bool `mapstructure:"eth_replay_enabled"` - EthRPC string `mapstructure:"eth_rpc"` - EthDataDir string `mapstructure:"eth_data_dir"` + Enabled bool `mapstructure:"eth_replay_enabled"` + EthRPC string `mapstructure:"eth_rpc"` + EthDataDir string `mapstructure:"eth_data_dir"` + ContractStateChecks bool `mapstructure:"contract_state_checks"` } var DefaultConfig = Config{ - Enabled: false, - EthRPC: "http://44.234.105.54:18545", - EthDataDir: "/root/.ethereum/chaindata", + Enabled: false, + EthRPC: "http://44.234.105.54:18545", + EthDataDir: "/root/.ethereum/chaindata", + ContractStateChecks: false, } const ( - flagEnabled = "eth_replay.eth_replay_enabled" - flagEthRPC = "eth_replay.eth_rpc" - flagEthDataDir = "eth_replay.eth_data_dir" + flagEnabled = "eth_replay.eth_replay_enabled" + flagEthRPC = "eth_replay.eth_rpc" + flagEthDataDir = "eth_replay.eth_data_dir" + flagContractStateChecks = "eth_replay.contract_state_checks" ) func ReadConfig(opts servertypes.AppOptions) (Config, error) { @@ -41,5 +44,10 @@ func ReadConfig(opts servertypes.AppOptions) (Config, error) { return cfg, err } } + if v := opts.Get(flagContractStateChecks); v != nil { + if cfg.ContractStateChecks, err = cast.ToBoolE(v); err != nil { + return cfg, err + } + } return cfg, nil } From c2b4604352e07e207a5e04b8a7a6b6ad4887a4e4 Mon Sep 17 00:00:00 2001 From: Steven Landers Date: Wed, 24 Apr 2024 18:17:43 -0400 Subject: [PATCH 11/31] [EVM] Add erc20 to cw20 pointer tests (#1593) * add erc20 to cw20 pointer tests * cleanup --- contracts/test/CW20toERC20PointerTest.js | 12 +- contracts/test/ERC20toCW20PointerTest.js | 474 +++++------------- contracts/test/lib.js | 38 +- .../scripts/evm_interoperability_tests.sh | 1 + x/evm/client/cli/tx.go | 10 +- 5 files changed, 172 insertions(+), 363 deletions(-) diff --git a/contracts/test/CW20toERC20PointerTest.js b/contracts/test/CW20toERC20PointerTest.js index dc6e95f0f0..6ea2bed7e8 100644 --- a/contracts/test/CW20toERC20PointerTest.js +++ b/contracts/test/CW20toERC20PointerTest.js @@ -85,18 +85,14 @@ describe("CW20 to ERC20 Pointer", function () { it("should increase and decrease allowance for a spender", async function() { const spender = accounts[1].seiAddress - const result = await executeWasm(cw20Pointer, { increase_allowance: { spender: spender, amount: "300" } }); - console.log(result) - + await executeWasm(cw20Pointer, { increase_allowance: { spender: spender, amount: "300" } }); + let allowance = await queryWasm(cw20Pointer, "allowance", { owner: admin.seiAddress, spender: spender }); - console.log(allowance) expect(allowance.data.allowance).to.equal("300"); - const result2 = await executeWasm(cw20Pointer, { decrease_allowance: { spender: spender, amount: "300" } }); - console.log(result2) - + await executeWasm(cw20Pointer, { decrease_allowance: { spender: spender, amount: "300" } }); + allowance = await queryWasm(cw20Pointer, "allowance", { owner: admin.seiAddress, spender: spender }); - console.log(allowance) expect(allowance.data.allowance).to.equal("0"); }); diff --git a/contracts/test/ERC20toCW20PointerTest.js b/contracts/test/ERC20toCW20PointerTest.js index c740876bae..b750ce9985 100644 --- a/contracts/test/ERC20toCW20PointerTest.js +++ b/contracts/test/ERC20toCW20PointerTest.js @@ -1,401 +1,187 @@ -const { expect } = require("chai"); -const {isBigNumber} = require("hardhat/common"); -const { exec } = require("child_process"); // Importing exec from child_process -const { cons } = require("fp-ts/lib/NonEmptyArray2v"); - -// Run instructions -// Should be run on a local chain using: `npx hardhat test --network seilocal test/ERC20toCW20PointerTest.js` - -async function sleep(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); -} - -async function delay() { - await sleep(3000) -} - -function debug(msg) { - //console.log(msg) -} - -const CW20_BASE_WASM_LOCATION = "wasm/cw20_base.wasm"; -const secondAnvilAddrETH = "0x70997970C51812dc3A010C7d01b50e0d17dc79C8"; -const secondAnvilAddrSEI = "sei1cjzphr67dug28rw9ueewrqllmxlqe5f0awulvy"; -const thirdAnvilAddrETH = "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC"; -const thirdAnvilAddrSEI = "sei183zvmhdk4yq0526cthffncpaztay9yauk6y0ue" -const secondAnvilPk = "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d"; // Replace with the spender's private key -const secondAnvilWallet = new ethers.Wallet(secondAnvilPk); -const secondAnvilSigner = secondAnvilWallet.connect(ethers.provider); +const {setupSigners, deployErc20PointerForCw20, getAdmin, storeWasm, instantiateWasm} = require("./lib"); +const {expect} = require("chai"); + +const CW20_BASE_WASM_LOCATION = "../contracts/wasm/cw20_base.wasm"; + +const erc20Abi = [ + "function name() view returns (string)", + "function symbol() view returns (string)", + "function decimals() view returns (uint8)", + "function totalSupply() view returns (uint256)", + "function balanceOf(address owner) view returns (uint256 balance)", + "function transfer(address to, uint amount) returns (bool)", + "function allowance(address owner, address spender) view returns (uint256)", + "function approve(address spender, uint256 value) returns (bool)", + "function transferFrom(address from, address to, uint value) returns (bool)" +]; + describe("ERC20 to CW20 Pointer", function () { - let adminAddrSei; - let contractAddress; - let deployerAddrETH; - let deployerAddrSEI; - let cW20ERC20Pointer; + let accounts; + let pointer; + let cw20Address; + let admin; before(async function () { - // fund addresses with SEI - console.log("funding addresses with SEI...") - await fundwithSei(deployerAddrETH); - await fundwithSei(secondAnvilAddrETH); - await fundwithSei(thirdAnvilAddrETH); - - let signers = await hre.ethers.getSigners(); - const deployer = signers[0]; - deployerAddrETH = await deployer.getAddress(); - deployerAddrSEI = await getSeiAddr(deployerAddrETH); - console.log("deployer address ETH = ", deployerAddrETH); - console.log("deployer address SEI = ", deployerAddrSEI); - - console.log("deploying wasm...") - let codeId = await deployWasm(); - console.log(`codeId: ${codeId}`); - console.log("getting admin addr...") - adminAddrSei = await getAdmin(); - console.log(`seid admin address: ${adminAddrSei}`); - console.log("instantiating wasm...") - contractAddress = await instantiateWasm(codeId, deployerAddrSEI); - console.log(`CW20 Sei contract address: ${contractAddress}`) - console.log( - "Deploying contracts with the account:", - deployerAddrETH - ); - - await delay(); - const CW20ERC20Pointer = await ethers.getContractFactory("CW20ERC20Pointer"); - await delay(); - console.log("deploying cw20 erc20 pointer...") - cW20ERC20Pointer = await CW20ERC20Pointer.deploy(contractAddress, "BTOK", "TOK"); - await cW20ERC20Pointer.waitForDeployment(); - console.log("CW20ERC20Pointer address = ", cW20ERC20Pointer.target) - }); + accounts = await setupSigners(await hre.ethers.getSigners()) + admin = await getAdmin() + + const codeId = await storeWasm(CW20_BASE_WASM_LOCATION) + cw20Address = await instantiateWasm(codeId, accounts[0].seiAddress, "cw20", { + name: "Test", + symbol: "TEST", + decimals: 6, + initial_balances: [ + { address: admin.seiAddress, amount: "1000000" }, + { address: accounts[0].seiAddress, amount: "2000000"}, + { address: accounts[1].seiAddress, amount: "3000000"} + ], + mint: { + "minter": admin.seiAddress, "cap": "99900000000" + } + }) - describe("name", function () { - it("name should work", async function () { - const name = await cW20ERC20Pointer.name(); - console.log(`Name: ${name}`); - expect(name).to.equal("BTOK"); - }); - }); + // deploy TestToken + const pointerAddr = await deployErc20PointerForCw20(hre.ethers.provider, cw20Address) + const contract = new hre.ethers.Contract(pointerAddr, erc20Abi, hre.ethers.provider); + pointer = contract.connect(accounts[0].signer) + }) - describe("symbol", function () { - it("symbol should work", async function () { - const symbol = await cW20ERC20Pointer.symbol(); - console.log(`Symbol: ${symbol}`); - expect(symbol).to.equal("TOK"); // Replace "TOK" with the expected symbol + describe("read", function(){ + it("get name", async function () { + const name = await pointer.name(); + expect(name).to.equal("Test"); }); - }); - describe("decimals", function () { - it("decimals should work", async function () { - const decimals = await cW20ERC20Pointer.decimals(); - console.log(`Decimals: ${decimals}`); - expect(Number(decimals)).to.be.greaterThan(0); + it("get symbol", async function () { + const symbol = await pointer.symbol(); + expect(symbol).to.equal("TEST"); }); - }); - describe("balanceOf", function () { - it("balanceOf should work", async function () { - let addressToCheck = secondAnvilAddrETH; - console.log(`addressToCheck: ${addressToCheck}`); - let secondAnvilAddrBalance = await cW20ERC20Pointer.balanceOf(addressToCheck); - console.log(`Balance of ${addressToCheck}: ${secondAnvilAddrBalance}`); // without this line the test fails more frequently - expect(Number(secondAnvilAddrBalance)).to.be.greaterThan(0); + it("get decimals", async function () { + const decimals = await pointer.decimals(); + expect(Number(decimals)).to.equal(6); }); - }); - describe("totalSupply", function () { - it("totalSupply should work", async function () { - let totalSupply = await cW20ERC20Pointer.totalSupply(); - console.log(`Total supply: ${totalSupply}`); - // expect total supply to be great than 0 - expect(Number(totalSupply)).to.be.greaterThan(0); + it("get balanceOf", async function () { + expect(await pointer.balanceOf(admin.evmAddress)).to.equal(1000000) + expect(await pointer.balanceOf(accounts[0].evmAddress)).to.equal(2000000); + expect(await pointer.balanceOf(accounts[1].evmAddress)).to.equal(3000000); }); - }); - describe("allowance", function () { - it("increase allowance should work", async function () { - let owner = deployerAddrETH; // Replace with the owner's address - let spender = deployerAddrETH; // Replace with the spender's address - let allowance = await cW20ERC20Pointer.allowance(owner, spender); - console.log(`Allowance for ${spender} from ${owner}: ${allowance}`); - expect(Number(allowance)).to.equal(0); // Replace with the expected allowance + it("get totalSupply", async function () { + expect(await pointer.totalSupply()).to.equal(6000000); }); - }); - describe("approve", function () { - it("increasing approval should work", async function () { - let spender = secondAnvilAddrETH; - let amount = 1000000; // Replace with the amount to approve - const tx = await cW20ERC20Pointer.approve(spender, amount); - await tx.wait(); - const allowance = await cW20ERC20Pointer.allowance(deployerAddrETH, spender); - console.log(`Allowance for ${spender} from ${deployerAddrETH}: ${allowance}`); - expect(Number(allowance)).to.equal(amount); + it("get allowance", async function () { + expect(await pointer.allowance(accounts[0].evmAddress, accounts[1].evmAddress)).to.equal(0); }); + }) - it("decreasing approval should work", async function () { - let spender = secondAnvilAddrETH; - let amount = 10; // Replace with the amount to approve + describe("transfer()", function () { + it("should transfer", async function () { + let sender = accounts[0]; + let recipient = accounts[1]; - // check that current allowance is greater than amount - const currentAllowance = await cW20ERC20Pointer.allowance(deployerAddrETH, spender); - expect(Number(currentAllowance)).to.be.greaterThan(amount); + expect(await pointer.balanceOf(sender.evmAddress)).to.equal(2000000); + expect(await pointer.balanceOf(recipient.evmAddress)).to.equal(3000000); - // decrease allowance - const tx = await cW20ERC20Pointer.approve(spender, amount); + const tx = await pointer.transfer(recipient.evmAddress, 1); await tx.wait(); - const allowance = await cW20ERC20Pointer.allowance(deployerAddrETH, spender); - console.log(`Allowance for ${spender} from ${deployerAddrETH}: ${allowance}`); - expect(Number(allowance)).to.equal(amount); - }); - }); - describe("transfer", function () { - it("transfer should work", async function () { - let recipient = secondAnvilAddrETH; - let amount = 8; // Replace with the amount to transfer + expect(await pointer.balanceOf(sender.evmAddress)).to.equal(1999999); + expect(await pointer.balanceOf(recipient.evmAddress)).to.equal(3000001); - // check that balanceOf sender address has enough ERC20s to send - let balanceOfDeployer = await cW20ERC20Pointer.balanceOf(deployerAddrETH); - expect(Number(balanceOfDeployer)).to.be.greaterThan(amount); - console.log("transfer: deployerAddr balance = ", balanceOfDeployer); + const cleanupTx = await pointer.connect(recipient.signer).transfer(sender.evmAddress, 1) + await cleanupTx.wait(); + }); - // capture recipient balance before the transfer - let balanceOfRecipientBefore = await cW20ERC20Pointer.balanceOf(recipient); - console.log("transfer: recipient balance before = ", balanceOfRecipientBefore); + it("should fail transfer() if sender has insufficient balance", async function () { + let recipient = accounts[1]; + await expect(pointer.transfer(recipient.evmAddress, 20000000)).to.be.revertedWith("CosmWasm execute failed"); + }); + }); - // do the transfer - const tx = await cW20ERC20Pointer.transfer(recipient, amount); + describe("approve()", function () { + it("should approve", async function () { + const owner = accounts[0].evmAddress; + const spender = accounts[1].evmAddress; + const tx = await pointer.approve(spender, 1000000); await tx.wait(); - - // compare recipient balance before and after the transfer - let balanceOfRecipientAfter = await cW20ERC20Pointer.balanceOf(recipient); - let diff = balanceOfRecipientAfter - balanceOfRecipientBefore; - expect(diff).to.equal(amount); + const allowance = await pointer.allowance(owner, spender); + expect(Number(allowance)).to.equal(1000000); }); - it("transfer should fail if sender has insufficient balance", async function () { - const balanceOfDeployer = await cW20ERC20Pointer.balanceOf(deployerAddrETH); - - const recipient = secondAnvilAddrETH; - const amount = balanceOfDeployer + BigInt(1); // This should be more than the sender's balance - - await expect(cW20ERC20Pointer.transfer(recipient, amount)).to.be.revertedWith("CosmWasm execute failed"); + it("should lower approval", async function () { + const owner = accounts[0].evmAddress; + const spender = accounts[1].evmAddress; + const tx = await pointer.approve(spender, 0); + await tx.wait(); + const allowance = await pointer.allowance(owner, spender); + expect(Number(allowance)).to.equal(0); }); }); - describe("transferFrom", function () { - it("transferFrom should work", async function () { - const amountToTransfer = 10; - const spender = secondAnvilAddrETH; - const recipient = thirdAnvilAddrETH; - // check balanceOf deployer - console.log("transferFrom: checking balanceOf deployer...") - const balanceOfDeployer = await cW20ERC20Pointer.balanceOf(deployerAddrETH); - expect(Number(balanceOfDeployer)).to.be.greaterThanOrEqual(amountToTransfer); - - // give allowance of deployer to spender (third party) - console.log("transferFrom: doing approve...") - const tx = await cW20ERC20Pointer.approve(spender, amountToTransfer); - await tx.wait(); - // check allownce of deployer to spender - console.log("transferFrom: checking allowance...") - const allowanceBefore = await cW20ERC20Pointer.allowance(deployerAddrETH, spender); - expect(Number(allowanceBefore)).to.be.greaterThanOrEqual(amountToTransfer); + describe("transferFrom()", function () { + it("should transferFrom", async function () { + const recipient = admin; + const owner = accounts[0]; + const spender = accounts[1]; + const amountToTransfer = 10; - // check that spender has gas - console.log("transferFrom: checking spender has gas...") - const spenderGas = await ethers.provider.getBalance(spender); - expect(Number(spenderGas)).to.be.greaterThan(0); - console.log("transferFrom: spender gas = ", spenderGas) + // capture balances before + const recipientBalanceBefore = await pointer.balanceOf(recipient.evmAddress); + const ownerBalanceBefore = await pointer.balanceOf(owner.evmAddress); + expect(Number(ownerBalanceBefore)).to.be.greaterThanOrEqual(amountToTransfer); - // capture recipient balance before transfer - console.log("transferFrom: checking balanceOf recipient before transfer...") - const balanceOfRecipientBefore = await cW20ERC20Pointer.balanceOf(recipient); + // approve the amount + const tx = await pointer.approve(spender.evmAddress, amountToTransfer); + await tx.wait(); + const allowanceBefore = await pointer.allowance(owner.evmAddress, spender.evmAddress); + expect(Number(allowanceBefore)).to.be.greaterThanOrEqual(amountToTransfer); - // check balanceOf sender (deployerAddr) to ensure it went down - const balanceOfSenderBefore = await cW20ERC20Pointer.balanceOf(deployerAddrETH); + // transfer + const tfTx = await pointer.connect(spender.signer).transferFrom(owner.evmAddress, recipient.evmAddress, amountToTransfer); + const receipt = await tfTx.wait(); - // have deployer transferFrom spender to recipient - console.log("transferFrom: doing actual transferFrom...") - const tfTx = await cW20ERC20Pointer.connect(secondAnvilSigner).transferFrom(deployerAddrETH, recipient, amountToTransfer); - await tfTx.wait(); + // capture balances after + const recipientBalanceAfter = await pointer.balanceOf(recipient.evmAddress); + const ownerBalanceAfter = await pointer.balanceOf(owner.evmAddress); // check balance diff to ensure transfer went through - console.log("transferFrom: checking balanceOf recipient after transfer...") - const balanceOfRecipientAfter = await cW20ERC20Pointer.balanceOf(recipient); - const diff = balanceOfRecipientAfter - balanceOfRecipientBefore; + const diff = recipientBalanceAfter - recipientBalanceBefore; expect(diff).to.equal(amountToTransfer); // check balanceOf sender (deployerAddr) to ensure it went down - const balanceOfSenderAfter = await cW20ERC20Pointer.balanceOf(deployerAddrETH); - const diff2 = balanceOfSenderBefore - balanceOfSenderAfter; + const diff2 = ownerBalanceBefore - ownerBalanceAfter; expect(diff2).to.equal(amountToTransfer); // check that allowance has gone down by amountToTransfer - const allowanceAfter = await cW20ERC20Pointer.allowance(deployerAddrETH, spender); + const allowanceAfter = await pointer.allowance(owner.evmAddress, spender.evmAddress); expect(Number(allowanceBefore) - Number(allowanceAfter)).to.equal(amountToTransfer); }); - it("transferFrom should fail if sender has insufficient balance", async function() { - const fromAddrBalance = await cW20ERC20Pointer.balanceOf(deployerAddrETH); - const spender = secondAnvilAddrETH; - const recipient = thirdAnvilAddrETH; - - // try to transfer more than the balance - const amountToTransfer = fromAddrBalance + BigInt(1); + it("should fail transferFrom() if sender has insufficient balance", async function () { + const recipient = admin; + const owner = accounts[0]; + const spender = accounts[1]; - const tx = await cW20ERC20Pointer.approve(spender, amountToTransfer); + const tx = await pointer.approve(spender.evmAddress, 999999999); await tx.wait(); - await expect(cW20ERC20Pointer.connect(secondAnvilSigner).transferFrom(deployerAddrETH, recipient, amountToTransfer)) - .to.be.revertedWith("CosmWasm execute failed"); + await expect(pointer.connect(spender.signer).transferFrom(owner.evmAddress, recipient.evmAddress, 999999999)).to.be.revertedWith("CosmWasm execute failed"); }); - it("transferFrom should fail if proper allowance not given", async function () { - const fromAddrBalance = await cW20ERC20Pointer.balanceOf(deployerAddrETH); - const spender = secondAnvilAddrETH; - const recipient = thirdAnvilAddrETH; + it("should fail transferFrom() if allowance is too low", async function () { + const recipient = admin; + const owner = accounts[0]; + const spender = accounts[1]; - const amountToTransfer = 1 - - // set approval to 0 - const tx = await cW20ERC20Pointer.approve(spender, 0); + const tx = await pointer.approve(spender.evmAddress, 10); await tx.wait(); - await expect(cW20ERC20Pointer.connect(secondAnvilSigner).transferFrom(deployerAddrETH, recipient, amountToTransfer)) - .to.be.revertedWith("CosmWasm execute failed"); - }); - - }); -}); - -async function getSeiAddr(ethAddr) { - let seiAddr = await new Promise((resolve, reject) => { - exec(`seid q evm sei-addr ${ethAddr}`, (error, stdout, stderr) => { - if (error) { - console.log(`error: ${error.message}`); - reject(error); - return; - } - if (stderr) { - console.log(`stderr: ${stderr}`); - reject(new Error(stderr)); - return; - } - debug(`stdout: ${stdout}`) - resolve(stdout.trim()); - }); - }); - return seiAddr.replace("sei_address: ", "");; -} - -async function fundwithSei(deployerAddress) { - // Wrap the exec function in a Promise - await new Promise((resolve, reject) => { - exec(`seid tx evm send ${deployerAddress} 10000000000000000000 --from admin`, (error, stdout, stderr) => { - if (error) { - console.log(`error: ${error.message}`); - reject(error); - return; - } - if (stderr) { - console.log(`stderr: ${stderr}`); - reject(new Error(stderr)); - return; - } - debug(`stdout: ${stdout}`) - resolve(); - }); - }); -} - -async function deployWasm() { - // Wrap the exec function in a Promise - let codeId = await new Promise((resolve, reject) => { - exec(`seid tx wasm store ${CW20_BASE_WASM_LOCATION} --from admin --gas=5000000 --fees=1000000usei -y --broadcast-mode block`, (error, stdout, stderr) => { - if (error) { - console.log(`error: ${error.message}`); - reject(error); - return; - } - if (stderr) { - console.log(`stderr: ${stderr}`); - reject(new Error(stderr)); - return; - } - debug(`stdout: ${stdout}`) - - // Regular expression to find the 'code_id' value - const regex = /key: code_id\s+value: "(\d+)"/; - - // Searching for the pattern in the string - const match = stdout.match(regex); - - let cId = null; - if (match && match[1]) { - // The captured group is the code_id value - cId = match[1]; - } - - console.log(`cId: ${cId}`); - resolve(cId); - }); - }); - - return codeId; -} - -async function getAdmin() { - // Wrap the exec function in a Promise - let adminAddr = await new Promise((resolve, reject) => { - exec(`seid keys show admin -a`, (error, stdout, stderr) => { - if (error) { - console.log(`error: ${error.message}`); - reject(error); - return; - } - if (stderr) { - console.log(`stderr: ${stderr}`); - reject(new Error(stderr)); - return; - } - debug(`stdout: ${stdout}`) - resolve(stdout.trim()); - }); - }); - return adminAddr; -} - -async function instantiateWasm(codeId, adminAddr) { - // Wrap the exec function in a Promise - console.log("instantiateWasm: will fund admin addr = ", adminAddr); - console.log("instantiateWasm: will fund secondAnvilAddr = ", secondAnvilAddrSEI); - let contractAddress = await new Promise((resolve, reject) => { - const cmd = `seid tx wasm instantiate ${codeId} '{ "name": "BTOK", "symbol": "BTOK", "decimals": 6, "initial_balances": [ { "address": "${adminAddr}", "amount": "1000000" }, { "address": "${secondAnvilAddrSEI}", "amount": "1000000"} ], "mint": { "minter": "${adminAddr}", "cap": "99900000000" } }' --label cw20-test --admin ${adminAddr} --from admin --gas=5000000 --fees=1000000usei -y --broadcast-mode block`; - exec(cmd, (error, stdout, stderr) => { - if (error) { - console.log(`error: ${error.message}`); - reject(error); - return; - } - if (stderr) { - console.log(`stderr: ${stderr}`); - reject(new Error(stderr)); - return; - } - debug(`stdout: ${stdout}`) - const regex = /_contract_address\s*value:\s*(\w+)/; - const match = stdout.match(regex); - if (match && match[1]) { - resolve(match[1]); - } else { - reject(new Error('Contract address not found')); - } + await expect(pointer.connect(spender.signer).transferFrom(owner.evmAddress, recipient.evmAddress, 20)).to.be.revertedWith("CosmWasm execute failed"); }); }); - return contractAddress; -} +}) \ No newline at end of file diff --git a/contracts/test/lib.js b/contracts/test/lib.js index 0d05ef8ddb..3c32f317c4 100644 --- a/contracts/test/lib.js +++ b/contracts/test/lib.js @@ -1,5 +1,7 @@ const { exec } = require("child_process"); // Importing exec from child_process +const adminKeyName = "admin" + function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } @@ -9,7 +11,7 @@ async function delay() { } async function fundAddress(addr) { - return await execute(`seid tx evm send ${addr} 10000000000000000000 --from admin`); + return await execute(`seid tx evm send ${addr} 10000000000000000000 --from ${adminKeyName}`); } async function getAdmin() { @@ -23,14 +25,12 @@ async function getAdmin() { } async function getAdminSeiAddress() { - return (await execute(`seid keys show admin -a`)).trim() + return (await execute(`seid keys show ${adminKeyName} -a`)).trim() } async function associateAdmin() { try { - const result = await execute(`seid tx evm associate-address --from admin`) - console.log(result) - return result + return await execute(`seid tx evm associate-address --from ${adminKeyName}`) }catch(e){ console.log("skipping associate") } @@ -54,15 +54,36 @@ function getEventAttribute(response, type, attribute) { } async function storeWasm(path) { - const command = `seid tx wasm store ${path} --from admin --gas=5000000 --fees=1000000usei -y --broadcast-mode block -o json` + const command = `seid tx wasm store ${path} --from ${adminKeyName} --gas=5000000 --fees=1000000usei -y --broadcast-mode block -o json` const output = await execute(command); const response = JSON.parse(output) return getEventAttribute(response, "store_code", "code_id") } +async function getPointerForCw20(cw20Address) { + const command = `seid query evm pointer CW20 ${cw20Address} -o json` + const output = await execute(command); + return JSON.parse(output); +} + +async function deployErc20PointerForCw20(provider, cw20Address) { + const command = `seid tx evm call-precompile pointer addCW20Pointer ${cw20Address} --from=admin -b block` + const output = await execute(command); + const txHash = output.replace(/.*0x/, "0x").trim() + let attempt = 0; + while(attempt < 10) { + const receipt = await provider.getTransactionReceipt(txHash); + if(receipt) { + return (await getPointerForCw20(cw20Address)).pointer + } + await sleep(500) + attempt++ + } + throw new Error("contract deployment failed") +} async function instantiateWasm(codeId, adminAddr, label, args = {}) { const jsonString = JSON.stringify(args).replace(/"/g, '\\"'); - const command = `seid tx wasm instantiate ${codeId} "${jsonString}" --label ${label} --admin ${adminAddr} --from admin --gas=5000000 --fees=1000000usei -y --broadcast-mode block -o json`; + const command = `seid tx wasm instantiate ${codeId} "${jsonString}" --label ${label} --admin ${adminAddr} --from ${adminKeyName} --gas=5000000 --fees=1000000usei -y --broadcast-mode block -o json`; const output = await execute(command); const response = JSON.parse(output); return getEventAttribute(response, "instantiate", "_contract_address"); @@ -121,7 +142,7 @@ async function queryWasm(contractAddress, operation, args={}){ async function executeWasm(contractAddress, msg, args = {}, coins = "0usei") { const jsonString = JSON.stringify(msg).replace(/"/g, '\\"'); // Properly escape JSON string - const command = `seid tx wasm execute ${contractAddress} "${jsonString}" --amount ${coins} --from admin --gas=5000000 --fees=1000000usei -y --broadcast-mode block -o json`; + const command = `seid tx wasm execute ${contractAddress} "${jsonString}" --amount ${coins} --from ${adminKeyName} --gas=5000000 --fees=1000000usei -y --broadcast-mode block -o json`; const output = await execute(command); return JSON.parse(output); } @@ -169,4 +190,5 @@ module.exports = { getAdmin, setupSigners, deployEvmContract, + deployErc20PointerForCw20, }; \ No newline at end of file diff --git a/integration_test/evm_module/scripts/evm_interoperability_tests.sh b/integration_test/evm_module/scripts/evm_interoperability_tests.sh index 1efcb6e2bd..529d1ba841 100755 --- a/integration_test/evm_module/scripts/evm_interoperability_tests.sh +++ b/integration_test/evm_module/scripts/evm_interoperability_tests.sh @@ -1,3 +1,4 @@ cd contracts npm ci npx hardhat test --network seilocal test/CW20toERC20PointerTest.js +npx hardhat test --network seilocal test/ERC20toCW20PointerTest.js diff --git a/x/evm/client/cli/tx.go b/x/evm/client/cli/tx.go index 5787e978de..3a88080d4c 100644 --- a/x/evm/client/cli/tx.go +++ b/x/evm/client/cli/tx.go @@ -464,9 +464,13 @@ func CmdCallPrecompile() *cobra.Command { if err != nil { return err } - valueBig, success := new(big.Int).SetString(value, 10) - if !success || valueBig.Cmp(utils.Big0) < 0 { - return fmt.Errorf("%s is not a valid value. Must be a decimal nonnegative integer", value) + + valueBig := big.NewInt(0) + if value != "" { + valueBig, success := new(big.Int).SetString(value, 10) + if !success || valueBig.Cmp(utils.Big0) < 0 { + return fmt.Errorf("%s is not a valid value. Must be a decimal nonnegative integer", value) + } } txData, err := getTxData(cmd) From 55414550ee4586a8a06a2f080f8f3e1e551c47ed Mon Sep 17 00:00:00 2001 From: Yiming Zang <50607998+yzang2019@users.noreply.github.com> Date: Thu, 25 Apr 2024 10:29:37 +0800 Subject: [PATCH 12/31] Fix loadtest client TPS calculation logic (#1596) --- loadtest/main.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/loadtest/main.go b/loadtest/main.go index d38c56eed7..1b094892d1 100644 --- a/loadtest/main.go +++ b/loadtest/main.go @@ -268,12 +268,13 @@ func printStats( totalProduced += atomic.LoadInt64(producedCountPerMsgType[msg_type]) } - var tps float64 + var totalTps float64 = 0 for msgType := range sentCountPerMsgType { sentCount := atomic.LoadInt64(sentCountPerMsgType[msgType]) prevTotalSent := atomic.LoadInt64(prevSentPerCounterPerMsgType[msgType]) //nolint:gosec - tps = float64(sentCount-prevTotalSent) / elapsed.Seconds() + tps := float64(sentCount-prevTotalSent) / elapsed.Seconds() + totalTps += tps defer metrics.SetThroughputMetricByType("tps", float32(tps), msgType) } @@ -292,7 +293,7 @@ func printStats( fmt.Printf("Unable to calculate stats, not enough data. Skipping...\n") } else { avgDuration := totalDuration.Milliseconds() / int64(len(blockTimes)-1) - fmt.Printf("High Level - Time Elapsed: %v, Produced: %d, Sent: %d, TPS: %f, Avg Block Time: %d ms\nBlock Heights %v\n\n", elapsed, totalProduced, totalSent, tps, avgDuration, blockHeights) + fmt.Printf("High Level - Time Elapsed: %v, Produced: %d, Sent: %d, TPS: %f, Avg Block Time: %d ms\nBlock Heights %v\n\n", elapsed, totalProduced, totalSent, totalTps, avgDuration, blockHeights) } } From 89b9a0722cce57d165cc4901ce581d0f9c63a775 Mon Sep 17 00:00:00 2001 From: codchen Date: Thu, 25 Apr 2024 11:17:45 +0800 Subject: [PATCH 13/31] Add CLI to register CW->ERC pointers (#1591) --- x/evm/client/cli/native_tx.go | 29 +++++++++++++++++++++++++++++ x/evm/client/cli/tx.go | 1 + 2 files changed, 30 insertions(+) diff --git a/x/evm/client/cli/native_tx.go b/x/evm/client/cli/native_tx.go index f61d9a5e8a..1800350416 100644 --- a/x/evm/client/cli/native_tx.go +++ b/x/evm/client/cli/native_tx.go @@ -45,3 +45,32 @@ func NativeSendTxCmd() *cobra.Command { return cmd } + +func NativeRegisterPointerCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "register-pointer [pointer type] [erc address]", + Short: `Register a CosmWasm pointer for an ERC20/721 contract. Pointer type is either ERC20 or ERC721`, + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + msg := &types.MsgRegisterPointer{ + Sender: clientCtx.GetFromAddress().String(), + PointerType: types.PointerType(types.PointerType_value[args[0]]), + ErcAddress: args[1], + } + if err := msg.ValidateBasic(); err != nil { + return err + } + + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) + }, + } + + flags.AddTxFlagsToCmd(cmd) + + return cmd +} diff --git a/x/evm/client/cli/tx.go b/x/evm/client/cli/tx.go index 3a88080d4c..37844437c8 100644 --- a/x/evm/client/cli/tx.go +++ b/x/evm/client/cli/tx.go @@ -62,6 +62,7 @@ func GetTxCmd() *cobra.Command { cmd.AddCommand(CmdERC20Send()) cmd.AddCommand(CmdCallPrecompile()) cmd.AddCommand(NativeSendTxCmd()) + cmd.AddCommand(NativeRegisterPointerCmd()) cmd.AddCommand(NewAddERCNativePointerProposalTxCmd()) cmd.AddCommand(NewAddERCCW20PointerProposalTxCmd()) cmd.AddCommand(NewAddERCCW721PointerProposalTxCmd()) From 2879145b991e9887ee825c1c3672bb5b16e710ea Mon Sep 17 00:00:00 2001 From: codchen Date: Thu, 25 Apr 2024 20:58:47 +0800 Subject: [PATCH 14/31] Inherit multipler from original gas meter when creating new gas meter (#1597) --- go.mod | 2 +- go.sum | 4 ++-- precompiles/ibc/ibc.go | 2 +- precompiles/wasmd/wasmd.go | 2 +- utils/gas.go | 22 ++++++++++++++++++ utils/gas_test.go | 32 ++++++++++++++++++++++++++ x/dex/contract/abci.go | 3 ++- x/dex/keeper/abci/end_block_deposit.go | 2 +- x/dex/keeper/contract.go | 2 +- x/dex/keeper/utils/wasm.go | 2 +- x/dex/utils/gas.go | 14 ----------- x/evm/ante/gas.go | 3 ++- x/evm/ante/preprocess.go | 2 +- x/evm/keeper/evm.go | 4 ++-- x/evm/keeper/grpc_query.go | 3 ++- x/evm/keeper/msg_server.go | 2 +- 16 files changed, 72 insertions(+), 29 deletions(-) create mode 100644 utils/gas.go create mode 100644 utils/gas_test.go delete mode 100644 x/dex/utils/gas.go diff --git a/go.mod b/go.mod index d9d11677a3..d9ee0e2d35 100644 --- a/go.mod +++ b/go.mod @@ -346,7 +346,7 @@ require ( replace ( github.com/CosmWasm/wasmd => github.com/sei-protocol/sei-wasmd v0.1.1 github.com/confio/ics23/go => github.com/cosmos/cosmos-sdk/ics23/go v0.8.0 - github.com/cosmos/cosmos-sdk => github.com/sei-protocol/sei-cosmos v0.3.1 + github.com/cosmos/cosmos-sdk => github.com/sei-protocol/sei-cosmos v0.3.2 github.com/cosmos/iavl => github.com/sei-protocol/sei-iavl v0.1.9 github.com/cosmos/ibc-go/v3 => github.com/sei-protocol/sei-ibc-go/v3 v3.3.0 github.com/ethereum/go-ethereum => github.com/sei-protocol/go-ethereum v1.13.5-sei-15 diff --git a/go.sum b/go.sum index ad3f0d014d..51ac47286e 100644 --- a/go.sum +++ b/go.sum @@ -1347,8 +1347,8 @@ github.com/sei-protocol/go-ethereum v1.13.5-sei-15 h1:VSFQrbWnSDCPCQzsYDW3k07EP3 github.com/sei-protocol/go-ethereum v1.13.5-sei-15/go.mod h1:kcRZmuzRn1lVejiFNTz4l4W7imnpq1bDAnuKS/RyhbQ= github.com/sei-protocol/goutils v0.0.2 h1:Bfa7Sv+4CVLNM20QcpvGb81B8C5HkQC/kW1CQpIbXDA= github.com/sei-protocol/goutils v0.0.2/go.mod h1:iYE2DuJfEnM+APPehr2gOUXfuLuPsVxorcDO+Tzq9q8= -github.com/sei-protocol/sei-cosmos v0.3.1 h1:9+jWsdoN/LIw9xnAAlBLVtlQAiEqf4CMQrpsYfDjH1g= -github.com/sei-protocol/sei-cosmos v0.3.1/go.mod h1:y86cNKDsiwsbhSoZZVCqGLUY8t6q9sUZl58kpaspI4Q= +github.com/sei-protocol/sei-cosmos v0.3.2 h1:bkPjY/SdDDmWqG6EmN987jvMjKP+5YVrzWdq/xcD/PM= +github.com/sei-protocol/sei-cosmos v0.3.2/go.mod h1:y86cNKDsiwsbhSoZZVCqGLUY8t6q9sUZl58kpaspI4Q= github.com/sei-protocol/sei-db v0.0.35 h1:BNHv0gtKE4J5kq1Mhxt9dpop3lI4W2I5WurgWYIYa4E= github.com/sei-protocol/sei-db v0.0.35/go.mod h1:F/ZKZA8HJPcUzSZPA8yt6pfwlGriJ4RDR4eHKSGLStI= github.com/sei-protocol/sei-iavl v0.1.9 h1:y4mVYftxLNRs6533zl7N0/Ch+CzRQc04JDfHolIxgBE= diff --git a/precompiles/ibc/ibc.go b/precompiles/ibc/ibc.go index a103e45aa5..0f0a8c1293 100644 --- a/precompiles/ibc/ibc.go +++ b/precompiles/ibc/ibc.go @@ -111,7 +111,7 @@ func (p Precompile) RunAndCalculateGas(evm *vm.EVM, caller common.Address, calli if gasLimitBigInt.Cmp(utils.BigMaxU64) > 0 { gasLimitBigInt = utils.BigMaxU64 } - ctx = ctx.WithGasMeter(sdk.NewGasMeter(gasLimitBigInt.Uint64())) + ctx = ctx.WithGasMeter(utils.NewGasMeterWithMultiplier(ctx, gasLimitBigInt.Uint64())) switch method.Name { case TransferMethod: diff --git a/precompiles/wasmd/wasmd.go b/precompiles/wasmd/wasmd.go index 226ae9637e..1ed01e5f9d 100644 --- a/precompiles/wasmd/wasmd.go +++ b/precompiles/wasmd/wasmd.go @@ -139,7 +139,7 @@ func (p Precompile) RunAndCalculateGas(evm *vm.EVM, caller common.Address, calli if gasLimitBigInt.Cmp(utils.BigMaxU64) > 0 { gasLimitBigInt = utils.BigMaxU64 } - ctx = ctx.WithGasMeter(sdk.NewGasMeter(gasLimitBigInt.Uint64())) + ctx = ctx.WithGasMeter(utils.NewGasMeterWithMultiplier(ctx, gasLimitBigInt.Uint64())) switch method.Name { case InstantiateMethod: diff --git a/utils/gas.go b/utils/gas.go new file mode 100644 index 0000000000..23b9511e9b --- /dev/null +++ b/utils/gas.go @@ -0,0 +1,22 @@ +package utils + +import ( + sdkstoretypes "github.com/cosmos/cosmos-sdk/store/types" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func NewGasMeterWithMultiplier(ctx sdk.Context, limit uint64) sdk.GasMeter { + if ctx.GasMeter() == nil { + return sdk.NewGasMeter(limit) + } + n, d := ctx.GasMeter().Multiplier() + return sdkstoretypes.NewMultiplierGasMeter(limit, n, d) +} + +func NewInfiniteGasMeterWithMultiplier(ctx sdk.Context) sdk.GasMeter { + if ctx.GasMeter() == nil { + return sdk.NewInfiniteGasMeter() + } + n, d := ctx.GasMeter().Multiplier() + return sdkstoretypes.NewInfiniteMultiplierGasMeter(n, d) +} diff --git a/utils/gas_test.go b/utils/gas_test.go new file mode 100644 index 0000000000..9080a7c64b --- /dev/null +++ b/utils/gas_test.go @@ -0,0 +1,32 @@ +package utils_test + +import ( + "testing" + + sdkstoretypes "github.com/cosmos/cosmos-sdk/store/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/sei-protocol/sei-chain/utils" + "github.com/stretchr/testify/require" +) + +func TestNewGasMeterWithMultiplier(t *testing.T) { + ctx := sdk.Context{} + n, d := utils.NewGasMeterWithMultiplier(ctx, 10).Multiplier() + require.Equal(t, uint64(1), n) + require.Equal(t, uint64(1), d) + ctx = ctx.WithGasMeter(sdkstoretypes.NewMultiplierGasMeter(10, 3, 4)) + n, d = utils.NewGasMeterWithMultiplier(ctx, 10).Multiplier() + require.Equal(t, uint64(3), n) + require.Equal(t, uint64(4), d) +} + +func TestNewInfiniteGasMeterWithMultiplier(t *testing.T) { + ctx := sdk.Context{} + n, d := utils.NewInfiniteGasMeterWithMultiplier(ctx).Multiplier() + require.Equal(t, uint64(1), n) + require.Equal(t, uint64(1), d) + ctx = ctx.WithGasMeter(sdkstoretypes.NewMultiplierGasMeter(10, 3, 4)) + n, d = utils.NewInfiniteGasMeterWithMultiplier(ctx).Multiplier() + require.Equal(t, uint64(3), n) + require.Equal(t, uint64(4), d) +} diff --git a/x/dex/contract/abci.go b/x/dex/contract/abci.go index 126f76d3a9..10d6d942ab 100644 --- a/x/dex/contract/abci.go +++ b/x/dex/contract/abci.go @@ -9,6 +9,7 @@ import ( "github.com/cosmos/cosmos-sdk/telemetry" "github.com/cosmos/cosmos-sdk/utils/tracing" + "github.com/sei-protocol/sei-chain/utils" "github.com/sei-protocol/sei-chain/utils/logging" sdk "github.com/cosmos/cosmos-sdk/types" @@ -162,7 +163,7 @@ func decorateContextForContract(ctx sdk.Context, contractInfo types.ContractInfo whitelistedStore := multi.NewStore(ctx.MultiStore(), GetWhitelistMap(contractInfo.ContractAddr)) newEventManager := sdk.NewEventManager() return ctx.WithContext(goCtx).WithMultiStore(whitelistedStore).WithEventManager(newEventManager).WithGasMeter( - seisync.NewGasWrapper(sdk.NewInfiniteGasMeter()), + seisync.NewGasWrapper(utils.NewInfiniteGasMeterWithMultiplier(ctx)), ) } diff --git a/x/dex/keeper/abci/end_block_deposit.go b/x/dex/keeper/abci/end_block_deposit.go index 4bbda57cf5..c0216e4826 100644 --- a/x/dex/keeper/abci/end_block_deposit.go +++ b/x/dex/keeper/abci/end_block_deposit.go @@ -23,7 +23,7 @@ func (w KeeperWrapper) HandleEBDeposit(ctx context.Context, sdkCtx sdk.Context, if msg.IsEmpty() { return nil } - _, err := utils.CallContractSudo(sdkCtx, w.Keeper, contractAddr, msg, dexutils.ZeroUserProvidedGas) // deposit + _, err := utils.CallContractSudo(sdkCtx, w.Keeper, contractAddr, msg, 0) // deposit if err != nil { sdkCtx.Logger().Error(fmt.Sprintf("Error during deposit: %s", err.Error())) return err diff --git a/x/dex/keeper/contract.go b/x/dex/keeper/contract.go index 892c7049c2..cec9f7065d 100644 --- a/x/dex/keeper/contract.go +++ b/x/dex/keeper/contract.go @@ -55,7 +55,7 @@ func (k Keeper) GetContract(ctx sdk.Context, contractAddr string) (types.Contrac } func (k Keeper) GetContractWithoutGasCharge(ctx sdk.Context, contractAddr string) (types.ContractInfoV2, error) { - return k.GetContract(ctx.WithGasMeter(sdk.NewInfiniteGasMeter()), contractAddr) + return k.GetContract(ctx.WithGasMeter(utils.NewInfiniteGasMeterWithMultiplier(ctx)), contractAddr) } func (k Keeper) GetContractGasLimit(ctx sdk.Context, contractAddr sdk.AccAddress) (uint64, error) { diff --git a/x/dex/keeper/utils/wasm.go b/x/dex/keeper/utils/wasm.go index 3ffbdb8911..be17ae868e 100644 --- a/x/dex/keeper/utils/wasm.go +++ b/x/dex/keeper/utils/wasm.go @@ -46,7 +46,7 @@ func sudo(sdkCtx sdk.Context, k *keeper.Keeper, contractAddress sdk.AccAddress, if err != nil { return nil, 0, err } - tmpCtx := sdkCtx.WithGasMeter(sdk.NewGasMeter(gasLimit)) + tmpCtx := sdkCtx.WithGasMeter(utils.NewGasMeterWithMultiplier(sdkCtx, gasLimit)) data, err := sudoWithoutOutOfGasPanic(tmpCtx, k, contractAddress, wasmMsg, msgType) gasConsumed := tmpCtx.GasMeter().GasConsumed() sdkCtx.EventManager().EmitEvent( diff --git a/x/dex/utils/gas.go b/x/dex/utils/gas.go deleted file mode 100644 index e66eba9a9e..0000000000 --- a/x/dex/utils/gas.go +++ /dev/null @@ -1,14 +0,0 @@ -package utils - -import ( - sdk "github.com/cosmos/cosmos-sdk/types" -) - -const ZeroUserProvidedGas = 0 - -func GetGasMeterForLimit(limit uint64) sdk.GasMeter { - if limit == 0 { - return sdk.NewInfiniteGasMeter() - } - return sdk.NewGasMeter(limit) -} diff --git a/x/evm/ante/gas.go b/x/evm/ante/gas.go index 5a04f2f51c..75dab3806b 100644 --- a/x/evm/ante/gas.go +++ b/x/evm/ante/gas.go @@ -2,6 +2,7 @@ package ante import ( sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/sei-protocol/sei-chain/utils" evmkeeper "github.com/sei-protocol/sei-chain/x/evm/keeper" evmtypes "github.com/sei-protocol/sei-chain/x/evm/types" ) @@ -23,6 +24,6 @@ func (gl GasLimitDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool } adjustedGasLimit := gl.evmKeeper.GetPriorityNormalizer(ctx).MulInt64(int64(txData.GetGas())) - ctx = ctx.WithGasMeter(sdk.NewGasMeter(adjustedGasLimit.TruncateInt().Uint64())) + ctx = ctx.WithGasMeter(utils.NewGasMeterWithMultiplier(ctx, adjustedGasLimit.TruncateInt().Uint64())) return next(ctx, tx, simulate) } diff --git a/x/evm/ante/preprocess.go b/x/evm/ante/preprocess.go index 30bbd9c1e4..15eb9aaa4a 100644 --- a/x/evm/ante/preprocess.go +++ b/x/evm/ante/preprocess.go @@ -61,7 +61,7 @@ func (p *EVMPreprocessDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate } // use infinite gas meter for EVM transaction because EVM handles gas checking from within - ctx = ctx.WithGasMeter(sdk.NewInfiniteGasMeter()) + ctx = ctx.WithGasMeter(utils.NewInfiniteGasMeterWithMultiplier(ctx)) derived := msg.Derived seiAddr := derived.SenderSeiAddr diff --git a/x/evm/keeper/evm.go b/x/evm/keeper/evm.go index 032be4e96c..60f57a8462 100644 --- a/x/evm/keeper/evm.go +++ b/x/evm/keeper/evm.go @@ -80,7 +80,7 @@ func (k *Keeper) CallEVM(ctx sdk.Context, from common.Address, to *common.Addres evm := types.GetCtxEVM(ctx) if evm == nil { // This call was not part of an existing StateTransition, so it should trigger one - executionCtx := ctx.WithGasMeter(sdk.NewInfiniteGasMeter()) + executionCtx := ctx.WithGasMeter(utils.NewInfiniteGasMeterWithMultiplier(ctx)) stateDB := state.NewDBImpl(executionCtx, k, false) gp := k.GetGasPool() evmMsg := &core.Message{ @@ -156,7 +156,7 @@ func (k *Keeper) getOrCreateEVM(ctx sdk.Context, from sdk.AccAddress) (*vm.EVM, if evm != nil { return evm, nil } - executionCtx := ctx.WithGasMeter(sdk.NewInfiniteGasMeter()) + executionCtx := ctx.WithGasMeter(utils.NewInfiniteGasMeterWithMultiplier(ctx)) stateDB := state.NewDBImpl(executionCtx, k, false) gp := k.GetGasPool() blockCtx, err := k.GetVMBlockContext(executionCtx, gp) diff --git a/x/evm/keeper/grpc_query.go b/x/evm/keeper/grpc_query.go index 3c0905ef0e..f5f55c6d65 100644 --- a/x/evm/keeper/grpc_query.go +++ b/x/evm/keeper/grpc_query.go @@ -7,6 +7,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" "github.com/ethereum/go-ethereum/common" + "github.com/sei-protocol/sei-chain/utils" "github.com/sei-protocol/sei-chain/x/evm/types" ) @@ -59,7 +60,7 @@ func (q Querier) StaticCall(c context.Context, req *types.QueryStaticCallRequest return nil, errors.New("cannot use static call to create contracts") } if ctx.GasMeter().Limit() == 0 { - ctx = ctx.WithGasMeter(sdk.NewGasMeter(q.QueryConfig.GasLimit)) + ctx = ctx.WithGasMeter(utils.NewGasMeterWithMultiplier(ctx, q.QueryConfig.GasLimit)) } to := common.HexToAddress(req.To) res, err := q.Keeper.StaticCallEVM(ctx, q.Keeper.AccountKeeper().GetModuleAddress(types.ModuleName), &to, req.Data) diff --git a/x/evm/keeper/msg_server.go b/x/evm/keeper/msg_server.go index d295730d5c..77f533f458 100644 --- a/x/evm/keeper/msg_server.go +++ b/x/evm/keeper/msg_server.go @@ -55,7 +55,7 @@ func (server msgServer) EVMTransaction(goCtx context.Context, msg *types.MsgEVMT // 3. At the beginning of message server (here), gas meter is set to infinite again, because EVM internal logic will then take over and manage out-of-gas scenarios. // 4. At the end of message server, gas consumed by EVM is adjusted to Sei's unit and counted in the original gas meter, because that original gas meter will be used to count towards block gas after message server returns originalGasMeter := ctx.GasMeter() - ctx = ctx.WithGasMeter(sdk.NewInfiniteGasMeter()) + ctx = ctx.WithGasMeter(utils.NewInfiniteGasMeterWithMultiplier(ctx)) stateDB := state.NewDBImpl(ctx, &server, false) stateDB.AddSurplus(msg.Derived.AnteSurplus) From a4e4bcb451f2b2a8e0ee1ad889bdfa5090e02d10 Mon Sep 17 00:00:00 2001 From: Uday Patil Date: Thu, 25 Apr 2024 09:44:32 -0500 Subject: [PATCH 15/31] Refactor oracle spam counter to use mem store instead of in memory map (#1594) * Refactor oracle spam counter to use mem store instead of in memory map * Add unit test --- app/app.go | 4 ++-- x/oracle/ante.go | 19 ++++++------------- x/oracle/keeper/keeper.go | 23 ++++++++++++++++++++++- x/oracle/keeper/keeper_test.go | 15 +++++++++++++++ x/oracle/keeper/test_utils.go | 3 +++ x/oracle/types/keys.go | 9 +++++++++ 6 files changed, 57 insertions(+), 16 deletions(-) diff --git a/app/app.go b/app/app.go index 2f58e85219..193222311e 100644 --- a/app/app.go +++ b/app/app.go @@ -416,7 +416,7 @@ func New( // this line is used by starport scaffolding # stargate/app/storeKey ) tkeys := sdk.NewTransientStoreKeys(paramstypes.TStoreKey) - memKeys := sdk.NewMemoryStoreKeys(capabilitytypes.MemStoreKey, dexmoduletypes.MemStoreKey, banktypes.DeferredCacheStoreKey, evmtypes.MemStoreKey) + memKeys := sdk.NewMemoryStoreKeys(capabilitytypes.MemStoreKey, dexmoduletypes.MemStoreKey, banktypes.DeferredCacheStoreKey, evmtypes.MemStoreKey, oracletypes.MemStoreKey) app := &App{ BaseApp: bApp, @@ -511,7 +511,7 @@ func New( app.EvidenceKeeper = *evidenceKeeper app.OracleKeeper = oraclekeeper.NewKeeper( - appCodec, keys[oracletypes.StoreKey], app.GetSubspace(oracletypes.ModuleName), + appCodec, keys[oracletypes.StoreKey], memKeys[oracletypes.MemStoreKey], app.GetSubspace(oracletypes.ModuleName), app.AccountKeeper, app.BankKeeper, app.DistrKeeper, &stakingKeeper, distrtypes.ModuleName, ) diff --git a/x/oracle/ante.go b/x/oracle/ante.go index a6519f3beb..2f355b5411 100644 --- a/x/oracle/ante.go +++ b/x/oracle/ante.go @@ -3,7 +3,6 @@ package oracle import ( "encoding/hex" "fmt" - "sync" sdk "github.com/cosmos/cosmos-sdk/types" sdkacltypes "github.com/cosmos/cosmos-sdk/types/accesscontrol" @@ -17,17 +16,13 @@ import ( // SpammingPreventionDecorator will check if the transaction's gas is smaller than // configured hard cap type SpammingPreventionDecorator struct { - oracleKeeper keeper.Keeper - oracleVoteMap map[string]int64 - mu *sync.Mutex + oracleKeeper keeper.Keeper } // NewSpammingPreventionDecorator returns new spamming prevention decorator instance func NewSpammingPreventionDecorator(oracleKeeper keeper.Keeper) SpammingPreventionDecorator { return SpammingPreventionDecorator{ - oracleKeeper: oracleKeeper, - oracleVoteMap: make(map[string]int64), - mu: &sync.Mutex{}, + oracleKeeper: oracleKeeper, } } @@ -87,9 +82,6 @@ func (spd SpammingPreventionDecorator) AnteDeps(txDeps []sdkacltypes.AccessOpera // CheckOracleSpamming check whether the msgs are spamming purpose or not func (spd SpammingPreventionDecorator) CheckOracleSpamming(ctx sdk.Context, msgs []sdk.Msg) error { - spd.mu.Lock() - defer spd.mu.Unlock() - curHeight := ctx.BlockHeight() for _, msg := range msgs { switch msg := msg.(type) { @@ -108,11 +100,12 @@ func (spd SpammingPreventionDecorator) CheckOracleSpamming(ctx sdk.Context, msgs if err != nil { return err } - if lastSubmittedHeight, ok := spd.oracleVoteMap[msg.Validator]; ok && lastSubmittedHeight == curHeight { + + spamPreventionCounterHeight := spd.oracleKeeper.GetSpamPreventionCounter(ctx, valAddr) + if spamPreventionCounterHeight == curHeight { return sdkerrors.Wrap(sdkerrors.ErrAlreadyExists, fmt.Sprintf("the validator has already submitted a vote at the current height=%d", curHeight)) } - - spd.oracleVoteMap[msg.Validator] = curHeight + spd.oracleKeeper.SetSpamPreventionCounter(ctx, valAddr) continue default: return nil diff --git a/x/oracle/keeper/keeper.go b/x/oracle/keeper/keeper.go index 285bee8f82..f01c0ed67a 100755 --- a/x/oracle/keeper/keeper.go +++ b/x/oracle/keeper/keeper.go @@ -22,6 +22,7 @@ import ( type Keeper struct { cdc codec.BinaryCodec storeKey sdk.StoreKey + memKey sdk.StoreKey paramSpace paramstypes.Subspace accountKeeper types.AccountKeeper @@ -33,7 +34,7 @@ type Keeper struct { } // NewKeeper constructs a new keeper for oracle -func NewKeeper(cdc codec.BinaryCodec, storeKey sdk.StoreKey, +func NewKeeper(cdc codec.BinaryCodec, storeKey sdk.StoreKey, memKey sdk.StoreKey, paramspace paramstypes.Subspace, accountKeeper types.AccountKeeper, bankKeeper types.BankKeeper, distrKeeper types.DistributionKeeper, stakingKeeper types.StakingKeeper, distrName string, @@ -51,6 +52,7 @@ func NewKeeper(cdc codec.BinaryCodec, storeKey sdk.StoreKey, return Keeper{ cdc: cdc, storeKey: storeKey, + memKey: memKey, paramSpace: paramspace, accountKeeper: accountKeeper, bankKeeper: bankKeeper, @@ -571,3 +573,22 @@ func (k Keeper) ValidateLookbackSeconds(ctx sdk.Context, lookbackSeconds uint64) return nil } + +func (k Keeper) GetSpamPreventionCounter(ctx sdk.Context, validatorAddr sdk.ValAddress) int64 { + store := ctx.KVStore(k.memKey) + bz := store.Get(types.GetSpamPreventionCounterKey(validatorAddr)) + if bz == nil { + return -1 + } + + return int64(sdk.BigEndianToUint64(bz)) +} + +func (k Keeper) SetSpamPreventionCounter(ctx sdk.Context, validatorAddr sdk.ValAddress) { + store := ctx.KVStore(k.memKey) + + height := ctx.BlockHeight() + bz := sdk.Uint64ToBigEndian(uint64(height)) + + store.Set(types.GetSpamPreventionCounterKey(validatorAddr), bz) +} diff --git a/x/oracle/keeper/keeper_test.go b/x/oracle/keeper/keeper_test.go index 9b545a58ab..3b491d0e29 100755 --- a/x/oracle/keeper/keeper_test.go +++ b/x/oracle/keeper/keeper_test.go @@ -813,3 +813,18 @@ func TestCalculateTwapsWithUnsupportedDenom(t *testing.T) { require.Error(t, err) require.Equal(t, types.ErrInvalidTwapLookback, err) } + +func TestSpamPreventionCounter(t *testing.T) { + input := CreateTestInput(t) + + // verify value == -1 when not set + require.Equal(t, int64(-1), input.OracleKeeper.GetSpamPreventionCounter(input.Ctx, sdk.ValAddress(Addrs[0]))) + + input.Ctx = input.Ctx.WithBlockHeight(3) + + input.OracleKeeper.SetSpamPreventionCounter(input.Ctx, sdk.ValAddress(Addrs[0])) + // verify counter value correct when set + require.Equal(t, int64(3), input.OracleKeeper.GetSpamPreventionCounter(input.Ctx, sdk.ValAddress(Addrs[0]))) + // verify value == -1 for a different address + require.Equal(t, int64(-1), input.OracleKeeper.GetSpamPreventionCounter(input.Ctx, sdk.ValAddress(Addrs[1]))) +} diff --git a/x/oracle/keeper/test_utils.go b/x/oracle/keeper/test_utils.go index ffaa5062c1..b8b49ec536 100644 --- a/x/oracle/keeper/test_utils.go +++ b/x/oracle/keeper/test_utils.go @@ -138,6 +138,7 @@ func CreateTestInput(t *testing.T) TestInput { keyParams := sdk.NewKVStoreKey(paramstypes.StoreKey) tKeyParams := sdk.NewTransientStoreKey(paramstypes.TStoreKey) keyOracle := sdk.NewKVStoreKey(types.StoreKey) + memKeys := sdk.NewMemoryStoreKeys(types.MemStoreKey) keyStaking := sdk.NewKVStoreKey(stakingtypes.StoreKey) keyDistr := sdk.NewKVStoreKey(distrtypes.StoreKey) @@ -152,6 +153,7 @@ func CreateTestInput(t *testing.T) TestInput { ms.MountStoreWithDB(tKeyParams, sdk.StoreTypeTransient, db) ms.MountStoreWithDB(keyParams, sdk.StoreTypeIAVL, db) ms.MountStoreWithDB(keyOracle, sdk.StoreTypeIAVL, db) + ms.MountStoreWithDB(memKeys[types.MemStoreKey], sdk.StoreTypeMemory, nil) ms.MountStoreWithDB(keyStaking, sdk.StoreTypeIAVL, db) ms.MountStoreWithDB(keyDistr, sdk.StoreTypeIAVL, db) @@ -230,6 +232,7 @@ func CreateTestInput(t *testing.T) TestInput { keeper := NewKeeper( appCodec, keyOracle, + memKeys[types.MemStoreKey], paramsKeeper.Subspace(types.ModuleName), accountKeeper, bankKeeper, diff --git a/x/oracle/types/keys.go b/x/oracle/types/keys.go index d42dc339c8..7f62197dde 100755 --- a/x/oracle/types/keys.go +++ b/x/oracle/types/keys.go @@ -14,6 +14,9 @@ const ( // StoreKey is the string store representation StoreKey = ModuleName + // StoreKey is the string store representation + MemStoreKey = "oracle_mem" + // RouterKey is the msg router key for the oracle module RouterKey = ModuleName @@ -44,6 +47,7 @@ var ( AggregateExchangeRateVoteKey = []byte{0x05} // prefix for each key to a aggregate vote VoteTargetKey = []byte{0x06} // prefix for each key to a vote target PriceSnapshotKey = []byte{0x07} // key for price snapshots history + SpamPreventionCounter = []byte{0x08} // key for spam prevention counter ) // GetExchangeRateKey - stored by *denom* @@ -66,6 +70,11 @@ func GetAggregateExchangeRateVoteKey(v sdk.ValAddress) []byte { return append(AggregateExchangeRateVoteKey, address.MustLengthPrefix(v)...) } +// GetSpamPreventionCounterKey - stored by *Validator* address +func GetSpamPreventionCounterKey(v sdk.ValAddress) []byte { + return append(SpamPreventionCounter, address.MustLengthPrefix(v)...) +} + func GetVoteTargetKey(d string) []byte { return append(VoteTargetKey, []byte(d)...) } From 3dd9e13b9590eaa989eba2645882acc84135aadc Mon Sep 17 00:00:00 2001 From: Kartik Bhat Date: Thu, 25 Apr 2024 11:06:52 -0400 Subject: [PATCH 16/31] Add Evm Receipt Status Metrics (#1595) --- x/evm/keeper/msg_server.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/x/evm/keeper/msg_server.go b/x/evm/keeper/msg_server.go index 77f533f458..8a318a694e 100644 --- a/x/evm/keeper/msg_server.go +++ b/x/evm/keeper/msg_server.go @@ -98,6 +98,14 @@ func (server msgServer) EVMTransaction(goCtx context.Context, msg *types.MsgEVMT ) return } + + // Add metrics for receipt status + if receipt.Status == uint32(ethtypes.ReceiptStatusFailed) { + telemetry.IncrCounter(1, "receipt", "status", "failed") + } else { + telemetry.IncrCounter(1, "receipt", "status", "success") + } + surplus, ferr := stateDB.Finalize() if ferr != nil { err = ferr @@ -110,7 +118,6 @@ func (server msgServer) EVMTransaction(goCtx context.Context, msg *types.MsgEVMT telemetry.NewLabel("type", err.Error()), }, ) - return } bloom := ethtypes.Bloom{} From 3c6e9f6a54cc8c630504f1bf94001460f1ec6e8c Mon Sep 17 00:00:00 2001 From: Uday Patil Date: Thu, 25 Apr 2024 13:06:10 -0500 Subject: [PATCH 17/31] V5.1.0 upgrade (#1582) * Add v5.1.0 upgrade * Add todo * remove todo --- app/app.go | 9 +++++++++ app/upgrades.go | 1 + 2 files changed, 10 insertions(+) diff --git a/app/app.go b/app/app.go index 193222311e..f7b6585f5c 100644 --- a/app/app.go +++ b/app/app.go @@ -1001,6 +1001,15 @@ func (app *App) SetStoreUpgradeHandlers() { // configure store loader that checks if version == upgradeHeight and applies store upgrades app.SetStoreLoader(upgradetypes.UpgradeStoreLoader(upgradeInfo.Height, &storeUpgrades)) } + + if upgradeInfo.Name == "v5.1.0" && !app.UpgradeKeeper.IsSkipHeight(upgradeInfo.Height) { + storeUpgrades := storetypes.StoreUpgrades{ + Added: []string{evmtypes.StoreKey}, + } + + // configure store loader that checks if version == upgradeHeight and applies store upgrades + app.SetStoreLoader(upgradetypes.UpgradeStoreLoader(upgradeInfo.Height, &storeUpgrades)) + } } // AppName returns the name of the App diff --git a/app/upgrades.go b/app/upgrades.go index c62c628362..5c1f471e70 100644 --- a/app/upgrades.go +++ b/app/upgrades.go @@ -92,6 +92,7 @@ var upgradesList = []string{ "v4.2.0-evm-devnet", "v5.0.0", "v5.0.1", + "v5.1.0", } // if there is an override list, use that instead, for integration tests From 997cbd51d1a8fedd4e992b4ba0c54e1256aa0fa0 Mon Sep 17 00:00:00 2001 From: codchen Date: Fri, 26 Apr 2024 18:51:21 +0800 Subject: [PATCH 18/31] Do not call `WriteStateToCommitAndGetWorkingHash` in FinalizeBlocker during replay (#1601) --- app/app.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/app.go b/app/app.go index f7b6585f5c..0eacd4282c 100644 --- a/app/app.go +++ b/app/app.go @@ -1119,6 +1119,9 @@ func (app *App) FinalizeBlocker(ctx sdk.Context, req *abci.RequestFinalizeBlock) if !app.optimisticProcessingInfo.Aborted && bytes.Equal(app.optimisticProcessingInfo.Hash, req.Hash) { metrics.IncrementOptimisticProcessingCounter(true) app.SetProcessProposalStateToCommit() + if app.EvmKeeper.EthReplayConfig.Enabled || app.EvmKeeper.EthBlockTestConfig.Enabled { + return &abci.ResponseFinalizeBlock{}, nil + } appHash := app.WriteStateToCommitAndGetWorkingHash() resp := app.getFinalizeBlockResponse(appHash, app.optimisticProcessingInfo.Events, app.optimisticProcessingInfo.TxRes, app.optimisticProcessingInfo.EndBlockResp) return &resp, nil @@ -1131,6 +1134,9 @@ func (app *App) FinalizeBlocker(ctx sdk.Context, req *abci.RequestFinalizeBlock) events, txResults, endBlockResp, _ := app.ProcessBlock(ctx, req.Txs, req, req.DecidedLastCommit) app.SetDeliverStateToCommit() + if app.EvmKeeper.EthReplayConfig.Enabled || app.EvmKeeper.EthBlockTestConfig.Enabled { + return &abci.ResponseFinalizeBlock{}, nil + } appHash := app.WriteStateToCommitAndGetWorkingHash() resp := app.getFinalizeBlockResponse(appHash, events, txResults, endBlockResp) return &resp, nil From 86b441ed7ac23b876f2db869bf215809d82687fc Mon Sep 17 00:00:00 2001 From: Steven Landers Date: Fri, 26 Apr 2024 14:30:54 -0400 Subject: [PATCH 19/31] [EVM] Add cw721 to erc721 tests (#1598) * add cw721 to erc721 query tests * add execution tests (including one that's failing) * use admin instead of accounts[0] * add extra cases --- contracts/src/ERC721.sol | 37 +++++ contracts/test/CW721toERC721PointerTest.js | 148 ++++++++++++++++++ contracts/test/lib.js | 8 +- .../scripts/evm_interoperability_tests.sh | 1 + 4 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 contracts/test/CW721toERC721PointerTest.js diff --git a/contracts/src/ERC721.sol b/contracts/src/ERC721.sol index a4c083f2a4..c7e89c2655 100644 --- a/contracts/src/ERC721.sol +++ b/contracts/src/ERC721.sol @@ -177,6 +177,43 @@ contract ERC721 is IERC721 { } contract MyNFT is ERC721 { + + // needed to concatenate the token ID to the URI + function uintToString(uint _num) internal pure returns (string memory) { + if (_num == 0) { + return "0"; + } + uint j = _num; + uint len; + while (j != 0) { + len++; + j /= 10; + } + bytes memory bstr = new bytes(len); + uint k = len; + while (_num != 0) { + k = k-1; + uint8 temp = (48 + uint8(_num - _num / 10 * 10)); + bytes1 b1 = bytes1(temp); + bstr[k] = b1; + _num /= 10; + } + return string(bstr); + } + + function name() external pure returns (string memory _name) { + return "MyNFT"; + } + + function symbol() external pure returns (string memory _symbol) { + return "MYNFT"; + } + + function tokenURI(uint256 tokenId) external pure returns (string memory) { + string memory numAsString = uintToString(tokenId); + return string(abi.encodePacked("https://sei.io/token/", numAsString)); + } + function mint(address to, uint id) external { _mint(to, id); } diff --git a/contracts/test/CW721toERC721PointerTest.js b/contracts/test/CW721toERC721PointerTest.js new file mode 100644 index 0000000000..492b861d3d --- /dev/null +++ b/contracts/test/CW721toERC721PointerTest.js @@ -0,0 +1,148 @@ +const {setupSigners, deployEvmContract, getAdmin, deployWasm, executeWasm, queryWasm} = require("./lib"); +const {expect} = require("chai"); + +const CW721_POINTER_WASM = "../example/cosmwasm/cw721/artifacts/cwerc721.wasm"; + +describe("CW721 to ERC721 Pointer", function () { + let accounts; + let erc721; + let pointer; + let admin; + + before(async function () { + accounts = await setupSigners(await hre.ethers.getSigners()) + erc721 = await deployEvmContract("MyNFT") + admin = await getAdmin() + pointer = await deployWasm(CW721_POINTER_WASM, + accounts[0].seiAddress, + "cw721-erc721", + {erc721_address: await erc721.getAddress() } + ) + + await (await erc721.mint(accounts[0].evmAddress, 1)).wait() + await (await erc721.mint(accounts[1].evmAddress, 2)).wait() + await (await erc721.mint(admin.evmAddress, 3)).wait() + + await (await erc721.approve(accounts[1].evmAddress, 1)).wait(); + await (await erc721.setApprovalForAll(admin.evmAddress, true)).wait(); + }) + + describe("query", function(){ + + it("should query the owner of a token", async function () { + const result = await queryWasm(pointer, "owner_of", { token_id: "1" }); + expect(result).to.deep.equal({data:{ + owner:accounts[0].seiAddress, + approvals:[ + {spender:accounts[1].seiAddress,expires:{never:{}}} + ] + }}); + }); + + it("should confirm an approval exists for a specific spender and token", async function () { + const result = await queryWasm(pointer, "approval", { token_id: "1", spender: accounts[1].seiAddress }); + expect(result).to.deep.equal({data:{ + approval:{spender:accounts[1].seiAddress, expires:{never:{}}} + }}); + }); + + it("should list all approvals for a token", async function () { + const result = await queryWasm(pointer, "approvals", { token_id: "1" }); + expect(result).to.deep.equal({data:{ + approvals:[ + {spender: accounts[1].seiAddress, expires:{never:{}}} + ]}}); + }); + + it("should verify if an operator is approved for all tokens of an owner", async function () { + const result = await queryWasm(pointer, "operator", { owner: accounts[0].seiAddress, operator: admin.seiAddress }); + expect(result).to.deep.equal({ + data: { + approval: { + spender: admin.seiAddress, + expires: {never:{}} + } + } + }); + }); + + it("should retrieve contract information", async function () { + const result = await queryWasm(pointer, "contract_info", {}); + expect(result).to.deep.equal({data:{name:"MyNFT",symbol:"MYNFT"}}); + }); + + it("should fetch NFT info based on token ID", async function () { + const result = await queryWasm(pointer, "nft_info", { token_id: "1" }); + expect(result).to.deep.equal({ data: { token_uri: 'https://sei.io/token/1', extension: '' } }); + }); + + it("should fetch all information about an NFT", async function () { + const result = await queryWasm(pointer, "all_nft_info", { token_id: "1" }); + expect(result).to.deep.equal({ + data: { + access: { + owner: accounts[0].seiAddress, + approvals: [ + { + spender: accounts[1].seiAddress, + expires: { + never: {} + } + } + ] + }, + info: { + token_uri: "https://sei.io/token/1", + extension: "" + } + } + }); + }); + + }) + + describe("execute operations", function () { + it("should transfer an NFT to another address", async function () { + await executeWasm(pointer, { transfer_nft: { recipient: accounts[1].seiAddress, token_id: "3" }}); + const ownerResult = await queryWasm(pointer, "owner_of", { token_id: "3" }); + expect(ownerResult).to.deep.equal({ data: { owner: accounts[1].seiAddress, approvals: [] } }); + await (await erc721.connect(accounts[1].signer).transferFrom(accounts[1].evmAddress, admin.evmAddress, 3)).wait(); + const ownerResult2 = await queryWasm(pointer, "owner_of", { token_id: "3" }); + expect(ownerResult2).to.deep.equal({ data: { owner: admin.seiAddress, approvals: [] } }); + }); + + it("should not transfer an NFT if not owned", async function () { + await executeWasm(pointer, { transfer_nft: { recipient: accounts[1].seiAddress, token_id: "2" }}); + const ownerResult = await queryWasm(pointer, "owner_of", { token_id: "2" }); + expect(ownerResult).to.deep.equal({ data: { owner: accounts[1].seiAddress, approvals: [] } }); + }); + + it("should approve a spender for a specific token", async function () { + // Approve accounts[1] to manage token ID 3 + await executeWasm(pointer, { approve: { spender: accounts[1].seiAddress, token_id: "3" }}); + const approvalResult = await queryWasm(pointer, "approval", { token_id: "3", spender: accounts[1].seiAddress }); + expect(approvalResult).to.deep.equal({ data: { approval: { spender: accounts[1].seiAddress, expires: { never: {} } } } }); + // allowed to transfer (does not revert) + await (await erc721.connect(accounts[1].signer).transferFrom(admin.evmAddress, accounts[1].evmAddress, 3)).wait(); + // transfer back to try with approval revocation (has to go back to admin first) + await (await erc721.connect(accounts[1].signer).transferFrom(accounts[1].evmAddress, admin.evmAddress, 3)).wait(); + + // Revoke approval to reset the state + await executeWasm(pointer, { revoke: { spender: accounts[1].seiAddress, token_id: "3" }}); + const result = await queryWasm(pointer, "approvals", { token_id: "3" }); + expect(result).to.deep.equal({data: { approvals:[]}}); + + // no longer allowed to transfer + await expect(erc721.connect(accounts[1].signer).transferFrom(admin.evmAddress, accounts[1].evmAddress, 3)).to.be.revertedWith("not authorized") + }); + + it("should set an operator for all tokens of an owner", async function () { + await executeWasm(pointer, { approve_all: { operator: accounts[1].seiAddress }}); + expect(await erc721.isApprovedForAll(admin.evmAddress, accounts[1].evmAddress)).to.be.true + await executeWasm(pointer, { revoke_all: { operator: accounts[1].seiAddress }}); + expect(await erc721.isApprovedForAll(admin.evmAddress, accounts[1].evmAddress)).to.be.false; + }); + + }); + +}) \ No newline at end of file diff --git a/contracts/test/lib.js b/contracts/test/lib.js index 3c32f317c4..c59a3dd619 100644 --- a/contracts/test/lib.js +++ b/contracts/test/lib.js @@ -81,6 +81,11 @@ async function deployErc20PointerForCw20(provider, cw20Address) { throw new Error("contract deployment failed") } +async function deployWasm(path, adminAddr, label, args = {}) { + const codeId = await storeWasm(path) + return await instantiateWasm(codeId, adminAddr, label, args) +} + async function instantiateWasm(codeId, adminAddr, label, args = {}) { const jsonString = JSON.stringify(args).replace(/"/g, '\\"'); const command = `seid tx wasm instantiate ${codeId} "${jsonString}" --label ${label} --admin ${adminAddr} --from ${adminKeyName} --gas=5000000 --fees=1000000usei -y --broadcast-mode block -o json`; @@ -140,7 +145,7 @@ async function queryWasm(contractAddress, operation, args={}){ return JSON.parse(output) } -async function executeWasm(contractAddress, msg, args = {}, coins = "0usei") { +async function executeWasm(contractAddress, msg, coins = "0usei") { const jsonString = JSON.stringify(msg).replace(/"/g, '\\"'); // Properly escape JSON string const command = `seid tx wasm execute ${contractAddress} "${jsonString}" --amount ${coins} --from ${adminKeyName} --gas=5000000 --fees=1000000usei -y --broadcast-mode block -o json`; const output = await execute(command); @@ -181,6 +186,7 @@ function execCommand(command, resolve, reject) { module.exports = { fundAddress, storeWasm, + deployWasm, instantiateWasm, execute, getSeiAddress, diff --git a/integration_test/evm_module/scripts/evm_interoperability_tests.sh b/integration_test/evm_module/scripts/evm_interoperability_tests.sh index 529d1ba841..37aa948cf7 100755 --- a/integration_test/evm_module/scripts/evm_interoperability_tests.sh +++ b/integration_test/evm_module/scripts/evm_interoperability_tests.sh @@ -2,3 +2,4 @@ cd contracts npm ci npx hardhat test --network seilocal test/CW20toERC20PointerTest.js npx hardhat test --network seilocal test/ERC20toCW20PointerTest.js +npx hardhat test --network seilocal test/CW721toERC721PointerTest.js From 28c621d9288eef991d0d85e34fb64dd0cb61da16 Mon Sep 17 00:00:00 2001 From: Kartik Bhat Date: Mon, 29 Apr 2024 10:00:14 -0400 Subject: [PATCH 20/31] Remove Non-Multiplier gas Meter (#1600) * Remove Non-Multiplier gas Meter * Update go mod * Bump sei-cosmos sei-wasmd --- app/antedecorators/gas.go | 6 +----- go.mod | 4 ++-- go.sum | 8 ++++---- precompiles/ibc/ibc.go | 2 +- precompiles/wasmd/wasmd.go | 2 +- utils/gas.go | 22 ---------------------- utils/gas_test.go | 32 -------------------------------- x/dex/contract/abci.go | 3 +-- x/dex/keeper/contract.go | 2 +- x/dex/keeper/contract_test.go | 6 +++--- x/dex/keeper/utils/wasm.go | 2 +- x/evm/ante/gas.go | 3 +-- x/evm/ante/preprocess.go | 2 +- x/evm/keeper/evm.go | 4 ++-- x/evm/keeper/grpc_query.go | 3 +-- x/evm/keeper/msg_server.go | 2 +- 16 files changed, 21 insertions(+), 82 deletions(-) delete mode 100644 utils/gas.go delete mode 100644 utils/gas_test.go diff --git a/app/antedecorators/gas.go b/app/antedecorators/gas.go index 944449c1a1..64a9eb3216 100644 --- a/app/antedecorators/gas.go +++ b/app/antedecorators/gas.go @@ -8,14 +8,10 @@ import ( func GetGasMeterSetter(pk paramskeeper.Keeper) func(bool, sdk.Context, uint64, sdk.Tx) sdk.Context { return func(simulate bool, ctx sdk.Context, gasLimit uint64, tx sdk.Tx) sdk.Context { - if ctx.BlockHeight() == 0 { - return ctx.WithGasMeter(sdk.NewInfiniteGasMeter()) - } - cosmosGasParams := pk.GetCosmosGasParams(ctx) // In simulation, still use multiplier but with infinite gas limit - if simulate { + if simulate || ctx.BlockHeight() == 0 { return ctx.WithGasMeter(types.NewInfiniteMultiplierGasMeter(cosmosGasParams.CosmosGasMultiplierNumerator, cosmosGasParams.CosmosGasMultiplierDenominator)) } diff --git a/go.mod b/go.mod index d9ee0e2d35..bdc924a4f3 100644 --- a/go.mod +++ b/go.mod @@ -344,9 +344,9 @@ require ( ) replace ( - github.com/CosmWasm/wasmd => github.com/sei-protocol/sei-wasmd v0.1.1 + github.com/CosmWasm/wasmd => github.com/sei-protocol/sei-wasmd v0.1.2 github.com/confio/ics23/go => github.com/cosmos/cosmos-sdk/ics23/go v0.8.0 - github.com/cosmos/cosmos-sdk => github.com/sei-protocol/sei-cosmos v0.3.2 + github.com/cosmos/cosmos-sdk => github.com/sei-protocol/sei-cosmos v0.3.3 github.com/cosmos/iavl => github.com/sei-protocol/sei-iavl v0.1.9 github.com/cosmos/ibc-go/v3 => github.com/sei-protocol/sei-ibc-go/v3 v3.3.0 github.com/ethereum/go-ethereum => github.com/sei-protocol/go-ethereum v1.13.5-sei-15 diff --git a/go.sum b/go.sum index 51ac47286e..1f7ab3164a 100644 --- a/go.sum +++ b/go.sum @@ -1347,8 +1347,8 @@ github.com/sei-protocol/go-ethereum v1.13.5-sei-15 h1:VSFQrbWnSDCPCQzsYDW3k07EP3 github.com/sei-protocol/go-ethereum v1.13.5-sei-15/go.mod h1:kcRZmuzRn1lVejiFNTz4l4W7imnpq1bDAnuKS/RyhbQ= github.com/sei-protocol/goutils v0.0.2 h1:Bfa7Sv+4CVLNM20QcpvGb81B8C5HkQC/kW1CQpIbXDA= github.com/sei-protocol/goutils v0.0.2/go.mod h1:iYE2DuJfEnM+APPehr2gOUXfuLuPsVxorcDO+Tzq9q8= -github.com/sei-protocol/sei-cosmos v0.3.2 h1:bkPjY/SdDDmWqG6EmN987jvMjKP+5YVrzWdq/xcD/PM= -github.com/sei-protocol/sei-cosmos v0.3.2/go.mod h1:y86cNKDsiwsbhSoZZVCqGLUY8t6q9sUZl58kpaspI4Q= +github.com/sei-protocol/sei-cosmos v0.3.3 h1:Ml6tsfQWIVxuoYq9dQKmUVOtIXWXanV+dEMzOtin1Bo= +github.com/sei-protocol/sei-cosmos v0.3.3/go.mod h1:y86cNKDsiwsbhSoZZVCqGLUY8t6q9sUZl58kpaspI4Q= github.com/sei-protocol/sei-db v0.0.35 h1:BNHv0gtKE4J5kq1Mhxt9dpop3lI4W2I5WurgWYIYa4E= github.com/sei-protocol/sei-db v0.0.35/go.mod h1:F/ZKZA8HJPcUzSZPA8yt6pfwlGriJ4RDR4eHKSGLStI= github.com/sei-protocol/sei-iavl v0.1.9 h1:y4mVYftxLNRs6533zl7N0/Ch+CzRQc04JDfHolIxgBE= @@ -1359,8 +1359,8 @@ github.com/sei-protocol/sei-tendermint v0.3.0 h1:tzxuZeYgXy2wPhYXaX9vrc3dmzGE4oT github.com/sei-protocol/sei-tendermint v0.3.0/go.mod h1:4LSlJdhl3nf3OmohliwRNUFLOB1XWlrmSodrIP7fLh4= github.com/sei-protocol/sei-tm-db v0.0.5 h1:3WONKdSXEqdZZeLuWYfK5hP37TJpfaUa13vAyAlvaQY= github.com/sei-protocol/sei-tm-db v0.0.5/go.mod h1:Cpa6rGyczgthq7/0pI31jys2Fw0Nfrc+/jKdP1prVqY= -github.com/sei-protocol/sei-wasmd v0.1.1 h1:szcBq0ECW5uP3MldoSzTLoIloIoDghOFRBum0K/7MLU= -github.com/sei-protocol/sei-wasmd v0.1.1/go.mod h1:CNF8PPkDFU0I/Lzw2+DbaNQhyxQj309ljOq7Waxj3fk= +github.com/sei-protocol/sei-wasmd v0.1.2 h1:CHAQGWq0htMsJVo+4CWqz1uCCAMg6bTm1OUlD1mRZ5o= +github.com/sei-protocol/sei-wasmd v0.1.2/go.mod h1:CNF8PPkDFU0I/Lzw2+DbaNQhyxQj309ljOq7Waxj3fk= github.com/sei-protocol/tm-db v0.0.4 h1:7Y4EU62Xzzg6wKAHEotm7SXQR0aPLcGhKHkh3qd0tnk= github.com/sei-protocol/tm-db v0.0.4/go.mod h1:PWsIWOTwdwC7Ow/GUvx8HgUJTO691pBuorIQD8JvwAs= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= diff --git a/precompiles/ibc/ibc.go b/precompiles/ibc/ibc.go index 0f0a8c1293..c6f1921d47 100644 --- a/precompiles/ibc/ibc.go +++ b/precompiles/ibc/ibc.go @@ -111,7 +111,7 @@ func (p Precompile) RunAndCalculateGas(evm *vm.EVM, caller common.Address, calli if gasLimitBigInt.Cmp(utils.BigMaxU64) > 0 { gasLimitBigInt = utils.BigMaxU64 } - ctx = ctx.WithGasMeter(utils.NewGasMeterWithMultiplier(ctx, gasLimitBigInt.Uint64())) + ctx = ctx.WithGasMeter(sdk.NewGasMeterWithMultiplier(ctx, gasLimitBigInt.Uint64())) switch method.Name { case TransferMethod: diff --git a/precompiles/wasmd/wasmd.go b/precompiles/wasmd/wasmd.go index 1ed01e5f9d..52926a26fa 100644 --- a/precompiles/wasmd/wasmd.go +++ b/precompiles/wasmd/wasmd.go @@ -139,7 +139,7 @@ func (p Precompile) RunAndCalculateGas(evm *vm.EVM, caller common.Address, calli if gasLimitBigInt.Cmp(utils.BigMaxU64) > 0 { gasLimitBigInt = utils.BigMaxU64 } - ctx = ctx.WithGasMeter(utils.NewGasMeterWithMultiplier(ctx, gasLimitBigInt.Uint64())) + ctx = ctx.WithGasMeter(sdk.NewGasMeterWithMultiplier(ctx, gasLimitBigInt.Uint64())) switch method.Name { case InstantiateMethod: diff --git a/utils/gas.go b/utils/gas.go deleted file mode 100644 index 23b9511e9b..0000000000 --- a/utils/gas.go +++ /dev/null @@ -1,22 +0,0 @@ -package utils - -import ( - sdkstoretypes "github.com/cosmos/cosmos-sdk/store/types" - sdk "github.com/cosmos/cosmos-sdk/types" -) - -func NewGasMeterWithMultiplier(ctx sdk.Context, limit uint64) sdk.GasMeter { - if ctx.GasMeter() == nil { - return sdk.NewGasMeter(limit) - } - n, d := ctx.GasMeter().Multiplier() - return sdkstoretypes.NewMultiplierGasMeter(limit, n, d) -} - -func NewInfiniteGasMeterWithMultiplier(ctx sdk.Context) sdk.GasMeter { - if ctx.GasMeter() == nil { - return sdk.NewInfiniteGasMeter() - } - n, d := ctx.GasMeter().Multiplier() - return sdkstoretypes.NewInfiniteMultiplierGasMeter(n, d) -} diff --git a/utils/gas_test.go b/utils/gas_test.go deleted file mode 100644 index 9080a7c64b..0000000000 --- a/utils/gas_test.go +++ /dev/null @@ -1,32 +0,0 @@ -package utils_test - -import ( - "testing" - - sdkstoretypes "github.com/cosmos/cosmos-sdk/store/types" - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/sei-protocol/sei-chain/utils" - "github.com/stretchr/testify/require" -) - -func TestNewGasMeterWithMultiplier(t *testing.T) { - ctx := sdk.Context{} - n, d := utils.NewGasMeterWithMultiplier(ctx, 10).Multiplier() - require.Equal(t, uint64(1), n) - require.Equal(t, uint64(1), d) - ctx = ctx.WithGasMeter(sdkstoretypes.NewMultiplierGasMeter(10, 3, 4)) - n, d = utils.NewGasMeterWithMultiplier(ctx, 10).Multiplier() - require.Equal(t, uint64(3), n) - require.Equal(t, uint64(4), d) -} - -func TestNewInfiniteGasMeterWithMultiplier(t *testing.T) { - ctx := sdk.Context{} - n, d := utils.NewInfiniteGasMeterWithMultiplier(ctx).Multiplier() - require.Equal(t, uint64(1), n) - require.Equal(t, uint64(1), d) - ctx = ctx.WithGasMeter(sdkstoretypes.NewMultiplierGasMeter(10, 3, 4)) - n, d = utils.NewInfiniteGasMeterWithMultiplier(ctx).Multiplier() - require.Equal(t, uint64(3), n) - require.Equal(t, uint64(4), d) -} diff --git a/x/dex/contract/abci.go b/x/dex/contract/abci.go index 10d6d942ab..712c01c67c 100644 --- a/x/dex/contract/abci.go +++ b/x/dex/contract/abci.go @@ -9,7 +9,6 @@ import ( "github.com/cosmos/cosmos-sdk/telemetry" "github.com/cosmos/cosmos-sdk/utils/tracing" - "github.com/sei-protocol/sei-chain/utils" "github.com/sei-protocol/sei-chain/utils/logging" sdk "github.com/cosmos/cosmos-sdk/types" @@ -163,7 +162,7 @@ func decorateContextForContract(ctx sdk.Context, contractInfo types.ContractInfo whitelistedStore := multi.NewStore(ctx.MultiStore(), GetWhitelistMap(contractInfo.ContractAddr)) newEventManager := sdk.NewEventManager() return ctx.WithContext(goCtx).WithMultiStore(whitelistedStore).WithEventManager(newEventManager).WithGasMeter( - seisync.NewGasWrapper(utils.NewInfiniteGasMeterWithMultiplier(ctx)), + seisync.NewGasWrapper(sdk.NewInfiniteGasMeterWithMultiplier(ctx)), ) } diff --git a/x/dex/keeper/contract.go b/x/dex/keeper/contract.go index cec9f7065d..dd25962546 100644 --- a/x/dex/keeper/contract.go +++ b/x/dex/keeper/contract.go @@ -55,7 +55,7 @@ func (k Keeper) GetContract(ctx sdk.Context, contractAddr string) (types.Contrac } func (k Keeper) GetContractWithoutGasCharge(ctx sdk.Context, contractAddr string) (types.ContractInfoV2, error) { - return k.GetContract(ctx.WithGasMeter(utils.NewInfiniteGasMeterWithMultiplier(ctx)), contractAddr) + return k.GetContract(ctx.WithGasMeter(sdk.NewInfiniteGasMeterWithMultiplier(ctx)), contractAddr) } func (k Keeper) GetContractGasLimit(ctx sdk.Context, contractAddr sdk.AccAddress) (uint64, error) { diff --git a/x/dex/keeper/contract_test.go b/x/dex/keeper/contract_test.go index ca88ff5945..8dae31c24f 100644 --- a/x/dex/keeper/contract_test.go +++ b/x/dex/keeper/contract_test.go @@ -308,7 +308,7 @@ func TestGetContractWithoutGasCharge(t *testing.T) { RentBalance: 1000000, }) // regular gas meter case - ctx = ctx.WithGasMeter(sdk.NewGasMeter(10000)) + ctx = ctx.WithGasMeter(sdk.NewGasMeterWithMultiplier(ctx, 10000)) contract, err := keeper.GetContractWithoutGasCharge(ctx, keepertest.TestContract) require.Nil(t, err) require.Equal(t, keepertest.TestContract, contract.ContractAddr) @@ -316,7 +316,7 @@ func TestGetContractWithoutGasCharge(t *testing.T) { require.Equal(t, uint64(10000), ctx.GasMeter().Limit()) // regular gas meter out of gas case - ctx = ctx.WithGasMeter(sdk.NewGasMeter(1)) + ctx = ctx.WithGasMeter(sdk.NewGasMeterWithMultiplier(ctx, 1)) contract, err = keeper.GetContractWithoutGasCharge(ctx, keepertest.TestContract) require.Nil(t, err) require.Equal(t, keepertest.TestContract, contract.ContractAddr) @@ -324,7 +324,7 @@ func TestGetContractWithoutGasCharge(t *testing.T) { require.Equal(t, uint64(1), ctx.GasMeter().Limit()) // infinite gas meter case - ctx = ctx.WithGasMeter(sdk.NewInfiniteGasMeter()) + ctx = ctx.WithGasMeter(sdk.NewInfiniteGasMeterWithMultiplier(ctx)) contract, err = keeper.GetContractWithoutGasCharge(ctx, keepertest.TestContract) require.Nil(t, err) require.Equal(t, keepertest.TestContract, contract.ContractAddr) diff --git a/x/dex/keeper/utils/wasm.go b/x/dex/keeper/utils/wasm.go index be17ae868e..8cc3354550 100644 --- a/x/dex/keeper/utils/wasm.go +++ b/x/dex/keeper/utils/wasm.go @@ -46,7 +46,7 @@ func sudo(sdkCtx sdk.Context, k *keeper.Keeper, contractAddress sdk.AccAddress, if err != nil { return nil, 0, err } - tmpCtx := sdkCtx.WithGasMeter(utils.NewGasMeterWithMultiplier(sdkCtx, gasLimit)) + tmpCtx := sdkCtx.WithGasMeter(sdk.NewGasMeterWithMultiplier(sdkCtx, gasLimit)) data, err := sudoWithoutOutOfGasPanic(tmpCtx, k, contractAddress, wasmMsg, msgType) gasConsumed := tmpCtx.GasMeter().GasConsumed() sdkCtx.EventManager().EmitEvent( diff --git a/x/evm/ante/gas.go b/x/evm/ante/gas.go index 75dab3806b..e4524fa57f 100644 --- a/x/evm/ante/gas.go +++ b/x/evm/ante/gas.go @@ -2,7 +2,6 @@ package ante import ( sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/sei-protocol/sei-chain/utils" evmkeeper "github.com/sei-protocol/sei-chain/x/evm/keeper" evmtypes "github.com/sei-protocol/sei-chain/x/evm/types" ) @@ -24,6 +23,6 @@ func (gl GasLimitDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool } adjustedGasLimit := gl.evmKeeper.GetPriorityNormalizer(ctx).MulInt64(int64(txData.GetGas())) - ctx = ctx.WithGasMeter(utils.NewGasMeterWithMultiplier(ctx, adjustedGasLimit.TruncateInt().Uint64())) + ctx = ctx.WithGasMeter(sdk.NewGasMeterWithMultiplier(ctx, adjustedGasLimit.TruncateInt().Uint64())) return next(ctx, tx, simulate) } diff --git a/x/evm/ante/preprocess.go b/x/evm/ante/preprocess.go index 15eb9aaa4a..e8f3ff13b3 100644 --- a/x/evm/ante/preprocess.go +++ b/x/evm/ante/preprocess.go @@ -61,7 +61,7 @@ func (p *EVMPreprocessDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate } // use infinite gas meter for EVM transaction because EVM handles gas checking from within - ctx = ctx.WithGasMeter(utils.NewInfiniteGasMeterWithMultiplier(ctx)) + ctx = ctx.WithGasMeter(sdk.NewInfiniteGasMeterWithMultiplier(ctx)) derived := msg.Derived seiAddr := derived.SenderSeiAddr diff --git a/x/evm/keeper/evm.go b/x/evm/keeper/evm.go index 60f57a8462..15b6b388ad 100644 --- a/x/evm/keeper/evm.go +++ b/x/evm/keeper/evm.go @@ -80,7 +80,7 @@ func (k *Keeper) CallEVM(ctx sdk.Context, from common.Address, to *common.Addres evm := types.GetCtxEVM(ctx) if evm == nil { // This call was not part of an existing StateTransition, so it should trigger one - executionCtx := ctx.WithGasMeter(utils.NewInfiniteGasMeterWithMultiplier(ctx)) + executionCtx := ctx.WithGasMeter(sdk.NewInfiniteGasMeterWithMultiplier(ctx)) stateDB := state.NewDBImpl(executionCtx, k, false) gp := k.GetGasPool() evmMsg := &core.Message{ @@ -156,7 +156,7 @@ func (k *Keeper) getOrCreateEVM(ctx sdk.Context, from sdk.AccAddress) (*vm.EVM, if evm != nil { return evm, nil } - executionCtx := ctx.WithGasMeter(utils.NewInfiniteGasMeterWithMultiplier(ctx)) + executionCtx := ctx.WithGasMeter(sdk.NewInfiniteGasMeterWithMultiplier(ctx)) stateDB := state.NewDBImpl(executionCtx, k, false) gp := k.GetGasPool() blockCtx, err := k.GetVMBlockContext(executionCtx, gp) diff --git a/x/evm/keeper/grpc_query.go b/x/evm/keeper/grpc_query.go index f5f55c6d65..451e2998e1 100644 --- a/x/evm/keeper/grpc_query.go +++ b/x/evm/keeper/grpc_query.go @@ -7,7 +7,6 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" "github.com/ethereum/go-ethereum/common" - "github.com/sei-protocol/sei-chain/utils" "github.com/sei-protocol/sei-chain/x/evm/types" ) @@ -60,7 +59,7 @@ func (q Querier) StaticCall(c context.Context, req *types.QueryStaticCallRequest return nil, errors.New("cannot use static call to create contracts") } if ctx.GasMeter().Limit() == 0 { - ctx = ctx.WithGasMeter(utils.NewGasMeterWithMultiplier(ctx, q.QueryConfig.GasLimit)) + ctx = ctx.WithGasMeter(sdk.NewGasMeterWithMultiplier(ctx, q.QueryConfig.GasLimit)) } to := common.HexToAddress(req.To) res, err := q.Keeper.StaticCallEVM(ctx, q.Keeper.AccountKeeper().GetModuleAddress(types.ModuleName), &to, req.Data) diff --git a/x/evm/keeper/msg_server.go b/x/evm/keeper/msg_server.go index 8a318a694e..61aef70747 100644 --- a/x/evm/keeper/msg_server.go +++ b/x/evm/keeper/msg_server.go @@ -55,7 +55,7 @@ func (server msgServer) EVMTransaction(goCtx context.Context, msg *types.MsgEVMT // 3. At the beginning of message server (here), gas meter is set to infinite again, because EVM internal logic will then take over and manage out-of-gas scenarios. // 4. At the end of message server, gas consumed by EVM is adjusted to Sei's unit and counted in the original gas meter, because that original gas meter will be used to count towards block gas after message server returns originalGasMeter := ctx.GasMeter() - ctx = ctx.WithGasMeter(utils.NewInfiniteGasMeterWithMultiplier(ctx)) + ctx = ctx.WithGasMeter(sdk.NewInfiniteGasMeterWithMultiplier(ctx)) stateDB := state.NewDBImpl(ctx, &server, false) stateDB.AddSurplus(msg.Derived.AnteSurplus) From 9df725ef6655e1588ae60c5ff6f2b3dc63fa03b3 Mon Sep 17 00:00:00 2001 From: Philip Su Date: Mon, 29 Apr 2024 10:38:33 -0700 Subject: [PATCH 21/31] Skip evm antehandler on non-secp signatures (#1606) * debug * debug * skip non-secp signatures fevm antehandler * fix log * fix log --- x/evm/ante/preprocess.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x/evm/ante/preprocess.go b/x/evm/ante/preprocess.go index e8f3ff13b3..f906d95d74 100644 --- a/x/evm/ante/preprocess.go +++ b/x/evm/ante/preprocess.go @@ -358,7 +358,7 @@ func (p *EVMAddressDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bo } pk, err := btcec.ParsePubKey(acc.GetPubKey().Bytes(), btcec.S256()) if err != nil { - ctx.Logger().Error(fmt.Sprintf("failed to parse pubkey for %s", err)) + ctx.Logger().Debug(fmt.Sprintf("failed to parse pubkey for %s, likely due to the fact that it isn't on secp256k1 curve", acc.GetPubKey()), "err", err) continue } evmAddr, err := pubkeyToEVMAddress(pk.SerializeUncompressed()) From 5b9e0254ce8fca8e1fbc241f1d82f31a47879f7d Mon Sep 17 00:00:00 2001 From: Philip Su Date: Mon, 29 Apr 2024 10:53:40 -0700 Subject: [PATCH 22/31] Disallow using sr25519 addresses for evm send and other funcs (#1605) * debug * debug * debug * goimport --- evmrpc/utils.go | 6 ++++++ x/evm/client/cli/tx.go | 8 ++++++++ 2 files changed, 14 insertions(+) diff --git a/evmrpc/utils.go b/evmrpc/utils.go index 0e7a974129..f39bf9b3c5 100644 --- a/evmrpc/utils.go +++ b/evmrpc/utils.go @@ -8,6 +8,8 @@ import ( "math/big" "time" + "github.com/cosmos/cosmos-sdk/crypto/hd" + "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/config" "github.com/cosmos/cosmos-sdk/codec/legacy" @@ -197,6 +199,10 @@ func getAddressPrivKeyMap(kb keyring.Keyring) map[string]*ecdsa.PrivateKey { // will only show local key continue } + if localInfo.GetAlgo() != hd.Secp256k1Type { + fmt.Printf("Skipping address %s because it isn't signed with secp256k1\n", localInfo.Name) + continue + } priv, err := legacy.PrivKeyFromBytes([]byte(localInfo.PrivKeyArmor)) if err != nil { continue diff --git a/x/evm/client/cli/tx.go b/x/evm/client/cli/tx.go index 37844437c8..93e52710de 100644 --- a/x/evm/client/cli/tx.go +++ b/x/evm/client/cli/tx.go @@ -14,6 +14,8 @@ import ( "strconv" "strings" + "github.com/cosmos/cosmos-sdk/crypto/hd" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/crypto" @@ -95,6 +97,9 @@ func CmdAssociateAddress() *cobra.Command { if !ok { return errors.New("can only associate address for local keys") } + if localInfo.GetAlgo() != hd.Secp256k1Type { + return errors.New("can only use addresses using secp256k1") + } priv, err := legacy.PrivKeyFromBytes([]byte(localInfo.PrivKeyArmor)) if err != nil { return err @@ -585,6 +590,9 @@ func getPrivateKey(cmd *cobra.Command) (*ecdsa.PrivateKey, error) { if !ok { return nil, errors.New("can only associate address for local keys") } + if localInfo.GetAlgo() != hd.Secp256k1Type { + return nil, errors.New("can only use addresses using secp256k1") + } priv, err := legacy.PrivKeyFromBytes([]byte(localInfo.PrivKeyArmor)) if err != nil { return nil, err From 4dd716ef976a3f05e16fe91e44bdbc837ae728a3 Mon Sep 17 00:00:00 2001 From: Philip Su Date: Mon, 29 Apr 2024 20:07:29 -0700 Subject: [PATCH 23/31] Bump sei cosmos (#1607) * debug * debug * debug * goimport * Bump cosmos --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index bdc924a4f3..6a4fdd5d1f 100644 --- a/go.mod +++ b/go.mod @@ -346,7 +346,7 @@ require ( replace ( github.com/CosmWasm/wasmd => github.com/sei-protocol/sei-wasmd v0.1.2 github.com/confio/ics23/go => github.com/cosmos/cosmos-sdk/ics23/go v0.8.0 - github.com/cosmos/cosmos-sdk => github.com/sei-protocol/sei-cosmos v0.3.3 + github.com/cosmos/cosmos-sdk => github.com/sei-protocol/sei-cosmos v0.3.4 github.com/cosmos/iavl => github.com/sei-protocol/sei-iavl v0.1.9 github.com/cosmos/ibc-go/v3 => github.com/sei-protocol/sei-ibc-go/v3 v3.3.0 github.com/ethereum/go-ethereum => github.com/sei-protocol/go-ethereum v1.13.5-sei-15 diff --git a/go.sum b/go.sum index 1f7ab3164a..271d08a655 100644 --- a/go.sum +++ b/go.sum @@ -1347,8 +1347,8 @@ github.com/sei-protocol/go-ethereum v1.13.5-sei-15 h1:VSFQrbWnSDCPCQzsYDW3k07EP3 github.com/sei-protocol/go-ethereum v1.13.5-sei-15/go.mod h1:kcRZmuzRn1lVejiFNTz4l4W7imnpq1bDAnuKS/RyhbQ= github.com/sei-protocol/goutils v0.0.2 h1:Bfa7Sv+4CVLNM20QcpvGb81B8C5HkQC/kW1CQpIbXDA= github.com/sei-protocol/goutils v0.0.2/go.mod h1:iYE2DuJfEnM+APPehr2gOUXfuLuPsVxorcDO+Tzq9q8= -github.com/sei-protocol/sei-cosmos v0.3.3 h1:Ml6tsfQWIVxuoYq9dQKmUVOtIXWXanV+dEMzOtin1Bo= -github.com/sei-protocol/sei-cosmos v0.3.3/go.mod h1:y86cNKDsiwsbhSoZZVCqGLUY8t6q9sUZl58kpaspI4Q= +github.com/sei-protocol/sei-cosmos v0.3.4 h1:bWgG/+J6xZmnEq74tjKzk1v9Rou0+m9o6fRS6Vkqpns= +github.com/sei-protocol/sei-cosmos v0.3.4/go.mod h1:y86cNKDsiwsbhSoZZVCqGLUY8t6q9sUZl58kpaspI4Q= github.com/sei-protocol/sei-db v0.0.35 h1:BNHv0gtKE4J5kq1Mhxt9dpop3lI4W2I5WurgWYIYa4E= github.com/sei-protocol/sei-db v0.0.35/go.mod h1:F/ZKZA8HJPcUzSZPA8yt6pfwlGriJ4RDR4eHKSGLStI= github.com/sei-protocol/sei-iavl v0.1.9 h1:y4mVYftxLNRs6533zl7N0/Ch+CzRQc04JDfHolIxgBE= From dd2e61d834aca643b7e5d6ba7d13dd8ff8282348 Mon Sep 17 00:00:00 2001 From: Steven Landers Date: Tue, 30 Apr 2024 00:52:19 -0400 Subject: [PATCH 24/31] [EVM] fix mempool removal panic (#1608) fix mempool removal panic --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 6a4fdd5d1f..367513047d 100644 --- a/go.mod +++ b/go.mod @@ -346,7 +346,7 @@ require ( replace ( github.com/CosmWasm/wasmd => github.com/sei-protocol/sei-wasmd v0.1.2 github.com/confio/ics23/go => github.com/cosmos/cosmos-sdk/ics23/go v0.8.0 - github.com/cosmos/cosmos-sdk => github.com/sei-protocol/sei-cosmos v0.3.4 + github.com/cosmos/cosmos-sdk => github.com/sei-protocol/sei-cosmos v0.3.5 github.com/cosmos/iavl => github.com/sei-protocol/sei-iavl v0.1.9 github.com/cosmos/ibc-go/v3 => github.com/sei-protocol/sei-ibc-go/v3 v3.3.0 github.com/ethereum/go-ethereum => github.com/sei-protocol/go-ethereum v1.13.5-sei-15 @@ -354,7 +354,7 @@ replace ( github.com/sei-protocol/sei-db => github.com/sei-protocol/sei-db v0.0.35 // Latest goleveldb is broken, we have to stick to this version github.com/syndtr/goleveldb => github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 - github.com/tendermint/tendermint => github.com/sei-protocol/sei-tendermint v0.3.0 + github.com/tendermint/tendermint => github.com/sei-protocol/sei-tendermint v0.3.1 github.com/tendermint/tm-db => github.com/sei-protocol/tm-db v0.0.4 google.golang.org/grpc => google.golang.org/grpc v1.33.2 ) diff --git a/go.sum b/go.sum index 271d08a655..2eb3139da6 100644 --- a/go.sum +++ b/go.sum @@ -1347,16 +1347,16 @@ github.com/sei-protocol/go-ethereum v1.13.5-sei-15 h1:VSFQrbWnSDCPCQzsYDW3k07EP3 github.com/sei-protocol/go-ethereum v1.13.5-sei-15/go.mod h1:kcRZmuzRn1lVejiFNTz4l4W7imnpq1bDAnuKS/RyhbQ= github.com/sei-protocol/goutils v0.0.2 h1:Bfa7Sv+4CVLNM20QcpvGb81B8C5HkQC/kW1CQpIbXDA= github.com/sei-protocol/goutils v0.0.2/go.mod h1:iYE2DuJfEnM+APPehr2gOUXfuLuPsVxorcDO+Tzq9q8= -github.com/sei-protocol/sei-cosmos v0.3.4 h1:bWgG/+J6xZmnEq74tjKzk1v9Rou0+m9o6fRS6Vkqpns= -github.com/sei-protocol/sei-cosmos v0.3.4/go.mod h1:y86cNKDsiwsbhSoZZVCqGLUY8t6q9sUZl58kpaspI4Q= +github.com/sei-protocol/sei-cosmos v0.3.5 h1:ibWj4uM3YeKLaGKfl1oWj36nkJo/2aZSyS7xo8Ixh6E= +github.com/sei-protocol/sei-cosmos v0.3.5/go.mod h1:imKzUdlLFKj8H39Ej9dICT+HZkx0rgEPsVm0PPb59kc= github.com/sei-protocol/sei-db v0.0.35 h1:BNHv0gtKE4J5kq1Mhxt9dpop3lI4W2I5WurgWYIYa4E= github.com/sei-protocol/sei-db v0.0.35/go.mod h1:F/ZKZA8HJPcUzSZPA8yt6pfwlGriJ4RDR4eHKSGLStI= github.com/sei-protocol/sei-iavl v0.1.9 h1:y4mVYftxLNRs6533zl7N0/Ch+CzRQc04JDfHolIxgBE= github.com/sei-protocol/sei-iavl v0.1.9/go.mod h1:7PfkEVT5dcoQE+s/9KWdoXJ8VVVP1QpYYPLdxlkSXFk= github.com/sei-protocol/sei-ibc-go/v3 v3.3.0 h1:/mjpTuCSEVDJ51nUDSHU92N0bRSwt49r1rmdC/lqgp8= github.com/sei-protocol/sei-ibc-go/v3 v3.3.0/go.mod h1:VwB/vWu4ysT5DN2aF78d17LYmx3omSAdq6gpKvM7XRA= -github.com/sei-protocol/sei-tendermint v0.3.0 h1:tzxuZeYgXy2wPhYXaX9vrc3dmzGE4oTWfxaCs5YJ3GA= -github.com/sei-protocol/sei-tendermint v0.3.0/go.mod h1:4LSlJdhl3nf3OmohliwRNUFLOB1XWlrmSodrIP7fLh4= +github.com/sei-protocol/sei-tendermint v0.3.1 h1:CzlJqFCFBoh0qlzUNc4n3HVNGD9FX+bGTKOyHkgMd4U= +github.com/sei-protocol/sei-tendermint v0.3.1/go.mod h1:4LSlJdhl3nf3OmohliwRNUFLOB1XWlrmSodrIP7fLh4= github.com/sei-protocol/sei-tm-db v0.0.5 h1:3WONKdSXEqdZZeLuWYfK5hP37TJpfaUa13vAyAlvaQY= github.com/sei-protocol/sei-tm-db v0.0.5/go.mod h1:Cpa6rGyczgthq7/0pI31jys2Fw0Nfrc+/jKdP1prVqY= github.com/sei-protocol/sei-wasmd v0.1.2 h1:CHAQGWq0htMsJVo+4CWqz1uCCAMg6bTm1OUlD1mRZ5o= From 954bd219a90d5edefa1c911a7b280ac52815a3b2 Mon Sep 17 00:00:00 2001 From: Jeremy Wei Date: Tue, 30 Apr 2024 11:16:26 -0400 Subject: [PATCH 25/31] evm/client/wasm/query unit tests (#1574) * check to see if any tests touch these funcs fix test not working getting write protection error on TestHandleERC20TokenInfo write protection err some cleanup very simplified contract add executable executable works executable works with complex erc20 refactor deploy contract out token info done finished rest of functions cleanups * remove comments in ERC20 and ERC721 * cleanup * fix --- example/contracts/erc20/ERC20.abi | 1 + example/contracts/erc20/ERC20.bin | 1 + example/contracts/erc20/ERC20.go | 738 +++++++++++++++++++++++ example/contracts/erc20/ERC20.sol | 54 ++ example/contracts/erc20/IERC20.abi | 1 + example/contracts/erc20/IERC20.bin | 0 example/contracts/erc20/README.md | 6 + example/contracts/erc721/DummyERC721.abi | 1 + example/contracts/erc721/DummyERC721.bin | 1 + example/contracts/erc721/ERC721.sol | 93 +++ example/contracts/erc721/README.md | 6 + x/evm/client/wasm/query_test.go | 192 +++++- 12 files changed, 1075 insertions(+), 19 deletions(-) create mode 100644 example/contracts/erc20/ERC20.abi create mode 100644 example/contracts/erc20/ERC20.bin create mode 100644 example/contracts/erc20/ERC20.go create mode 100644 example/contracts/erc20/ERC20.sol create mode 100644 example/contracts/erc20/IERC20.abi create mode 100644 example/contracts/erc20/IERC20.bin create mode 100644 example/contracts/erc20/README.md create mode 100644 example/contracts/erc721/DummyERC721.abi create mode 100644 example/contracts/erc721/DummyERC721.bin create mode 100644 example/contracts/erc721/ERC721.sol create mode 100644 example/contracts/erc721/README.md diff --git a/example/contracts/erc20/ERC20.abi b/example/contracts/erc20/ERC20.abi new file mode 100644 index 0000000000..50dd4f6b0b --- /dev/null +++ b/example/contracts/erc20/ERC20.abi @@ -0,0 +1 @@ +[{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"spender","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Transfer","type":"event"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"allowance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"approve","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"decimals","outputs":[{"internalType":"uint8","name":"","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transfer","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transferFrom","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"}] \ No newline at end of file diff --git a/example/contracts/erc20/ERC20.bin b/example/contracts/erc20/ERC20.bin new file mode 100644 index 0000000000..51fff6490f --- /dev/null +++ b/example/contracts/erc20/ERC20.bin @@ -0,0 +1 @@ +60806040526040518060400160405280600581526020017f45524332300000000000000000000000000000000000000000000000000000008152505f908162000049919062000323565b506040518060400160405280600581526020017f45524332300000000000000000000000000000000000000000000000000000008152506001908162000090919062000323565b50601260025f6101000a81548160ff021916908360ff160217905550348015620000b8575f80fd5b5062000407565b5f81519050919050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b7f4e487b71000000000000000000000000000000000000000000000000000000005f52602260045260245ffd5b5f60028204905060018216806200013b57607f821691505b602082108103620001515762000150620000f6565b5b50919050565b5f819050815f5260205f209050919050565b5f6020601f8301049050919050565b5f82821b905092915050565b5f60088302620001b57fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8262000178565b620001c1868362000178565b95508019841693508086168417925050509392505050565b5f819050919050565b5f819050919050565b5f6200020b62000205620001ff84620001d9565b620001e2565b620001d9565b9050919050565b5f819050919050565b6200022683620001eb565b6200023e620002358262000212565b84845462000184565b825550505050565b5f90565b6200025462000246565b620002618184846200021b565b505050565b5b8181101562000288576200027c5f826200024a565b60018101905062000267565b5050565b601f821115620002d757620002a18162000157565b620002ac8462000169565b81016020851015620002bc578190505b620002d4620002cb8562000169565b83018262000266565b50505b505050565b5f82821c905092915050565b5f620002f95f1984600802620002dc565b1980831691505092915050565b5f620003138383620002e8565b9150826002028217905092915050565b6200032e82620000bf565b67ffffffffffffffff8111156200034a5762000349620000c9565b5b62000356825462000123565b620003638282856200028c565b5f60209050601f83116001811462000399575f841562000384578287015190505b62000390858262000306565b865550620003ff565b601f198416620003a98662000157565b5f5b82811015620003d257848901518255600182019150602085019450602081019050620003ab565b86831015620003f25784890151620003ee601f891682620002e8565b8355505b6001600288020188555050505b505050505050565b610dd280620004155f395ff3fe608060405234801561000f575f80fd5b5060043610610091575f3560e01c8063313ce56711610064578063313ce5671461013157806370a082311461014f57806395d89b411461017f578063a9059cbb1461019d578063dd62ed3e146101cd57610091565b806306fdde0314610095578063095ea7b3146100b357806318160ddd146100e357806323b872dd14610101575b5f80fd5b61009d6101fd565b6040516100aa9190610959565b60405180910390f35b6100cd60048036038101906100c89190610a0a565b610288565b6040516100da9190610a62565b60405180910390f35b6100eb610375565b6040516100f89190610a8a565b60405180910390f35b61011b60048036038101906101169190610aa3565b610386565b6040516101289190610a62565b60405180910390f35b610139610666565b6040516101469190610b0e565b60405180910390f35b61016960048036038101906101649190610b27565b610678565b6040516101769190610a8a565b60405180910390f35b61018761068d565b6040516101949190610959565b60405180910390f35b6101b760048036038101906101b29190610a0a565b610719565b6040516101c49190610a62565b60405180910390f35b6101e760048036038101906101e29190610b52565b6108af565b6040516101f49190610a8a565b60405180910390f35b5f805461020990610bbd565b80601f016020809104026020016040519081016040528092919081815260200182805461023590610bbd565b80156102805780601f1061025757610100808354040283529160200191610280565b820191905f5260205f20905b81548152906001019060200180831161026357829003601f168201915b505050505081565b5f8160045f3373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f8573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f20819055508273ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925846040516103639190610a8a565b60405180910390a36001905092915050565b5f69d3c21bcecceda1000000905090565b5f8160035f8673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f20541015610407576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016103fe90610c5d565b60405180910390fd5b8160045f8673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f3373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205410156104c2576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016104b990610ceb565b60405180910390fd5b8160035f8673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f82825461050e9190610d36565b925050819055508160035f8573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f8282546105619190610d69565b925050819055508160045f8673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f3373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f8282546105ef9190610d36565b925050819055508273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef846040516106539190610a8a565b60405180910390a3600190509392505050565b60025f9054906101000a900460ff1681565b6003602052805f5260405f205f915090505481565b6001805461069a90610bbd565b80601f01602080910402602001604051908101604052809291908181526020018280546106c690610bbd565b80156107115780601f106106e857610100808354040283529160200191610711565b820191905f5260205f20905b8154815290600101906020018083116106f457829003601f168201915b505050505081565b5f8160035f3373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f2054101561079a576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161079190610c5d565b60405180910390fd5b8160035f3373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f8282546107e69190610d36565b925050819055508160035f8573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f8282546108399190610d69565b925050819055508273ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef8460405161089d9190610a8a565b60405180910390a36001905092915050565b6004602052815f5260405f20602052805f5260405f205f91509150505481565b5f81519050919050565b5f82825260208201905092915050565b5f5b838110156109065780820151818401526020810190506108eb565b5f8484015250505050565b5f601f19601f8301169050919050565b5f61092b826108cf565b61093581856108d9565b93506109458185602086016108e9565b61094e81610911565b840191505092915050565b5f6020820190508181035f8301526109718184610921565b905092915050565b5f80fd5b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f6109a68261097d565b9050919050565b6109b68161099c565b81146109c0575f80fd5b50565b5f813590506109d1816109ad565b92915050565b5f819050919050565b6109e9816109d7565b81146109f3575f80fd5b50565b5f81359050610a04816109e0565b92915050565b5f8060408385031215610a2057610a1f610979565b5b5f610a2d858286016109c3565b9250506020610a3e858286016109f6565b9150509250929050565b5f8115159050919050565b610a5c81610a48565b82525050565b5f602082019050610a755f830184610a53565b92915050565b610a84816109d7565b82525050565b5f602082019050610a9d5f830184610a7b565b92915050565b5f805f60608486031215610aba57610ab9610979565b5b5f610ac7868287016109c3565b9350506020610ad8868287016109c3565b9250506040610ae9868287016109f6565b9150509250925092565b5f60ff82169050919050565b610b0881610af3565b82525050565b5f602082019050610b215f830184610aff565b92915050565b5f60208284031215610b3c57610b3b610979565b5b5f610b49848285016109c3565b91505092915050565b5f8060408385031215610b6857610b67610979565b5b5f610b75858286016109c3565b9250506020610b86858286016109c3565b9150509250929050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52602260045260245ffd5b5f6002820490506001821680610bd457607f821691505b602082108103610be757610be6610b90565b5b50919050565b7f45524332303a207472616e7366657220616d6f756e74206578636565647320625f8201527f616c616e63650000000000000000000000000000000000000000000000000000602082015250565b5f610c476026836108d9565b9150610c5282610bed565b604082019050919050565b5f6020820190508181035f830152610c7481610c3b565b9050919050565b7f45524332303a207472616e7366657220616d6f756e74206578636565647320615f8201527f6c6c6f77616e6365000000000000000000000000000000000000000000000000602082015250565b5f610cd56028836108d9565b9150610ce082610c7b565b604082019050919050565b5f6020820190508181035f830152610d0281610cc9565b9050919050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f610d40826109d7565b9150610d4b836109d7565b9250828203905081811115610d6357610d62610d09565b5b92915050565b5f610d73826109d7565b9150610d7e836109d7565b9250828201905080821115610d9657610d95610d09565b5b9291505056fea2646970667358221220279345e78aa756df0f261c165289eeb756e8e65e3a8d7e4ab176637da3e5f52564736f6c63430008140033 \ No newline at end of file diff --git a/example/contracts/erc20/ERC20.go b/example/contracts/erc20/ERC20.go new file mode 100644 index 0000000000..77c5cfbc2d --- /dev/null +++ b/example/contracts/erc20/ERC20.go @@ -0,0 +1,738 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package erc20 + +import ( + "errors" + "math/big" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" +) + +// Reference imports to suppress errors if they are not otherwise used. +var ( + _ = errors.New + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription + _ = abi.ConvertType +) + +// Erc20MetaData contains all meta data concerning the Erc20 contract. +var Erc20MetaData = &bind.MetaData{ + ABI: "[{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"spender\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"Approval\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"Transfer\",\"type\":\"event\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"name\":\"allowance\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"spender\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"approve\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"name\":\"balanceOf\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"decimals\",\"outputs\":[{\"internalType\":\"uint8\",\"name\":\"\",\"type\":\"uint8\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"name\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"\",\"type\":\"string\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"symbol\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"\",\"type\":\"string\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"totalSupply\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"pure\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"transfer\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"transferFrom\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]", +} + +// Erc20ABI is the input ABI used to generate the binding from. +// Deprecated: Use Erc20MetaData.ABI instead. +var Erc20ABI = Erc20MetaData.ABI + +// Erc20 is an auto generated Go binding around an Ethereum contract. +type Erc20 struct { + Erc20Caller // Read-only binding to the contract + Erc20Transactor // Write-only binding to the contract + Erc20Filterer // Log filterer for contract events +} + +// Erc20Caller is an auto generated read-only Go binding around an Ethereum contract. +type Erc20Caller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// Erc20Transactor is an auto generated write-only Go binding around an Ethereum contract. +type Erc20Transactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// Erc20Filterer is an auto generated log filtering Go binding around an Ethereum contract events. +type Erc20Filterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// Erc20Session is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type Erc20Session struct { + Contract *Erc20 // Generic contract binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// Erc20CallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type Erc20CallerSession struct { + Contract *Erc20Caller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// Erc20TransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type Erc20TransactorSession struct { + Contract *Erc20Transactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// Erc20Raw is an auto generated low-level Go binding around an Ethereum contract. +type Erc20Raw struct { + Contract *Erc20 // Generic contract binding to access the raw methods on +} + +// Erc20CallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type Erc20CallerRaw struct { + Contract *Erc20Caller // Generic read-only contract binding to access the raw methods on +} + +// Erc20TransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type Erc20TransactorRaw struct { + Contract *Erc20Transactor // Generic write-only contract binding to access the raw methods on +} + +// NewErc20 creates a new instance of Erc20, bound to a specific deployed contract. +func NewErc20(address common.Address, backend bind.ContractBackend) (*Erc20, error) { + contract, err := bindErc20(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &Erc20{Erc20Caller: Erc20Caller{contract: contract}, Erc20Transactor: Erc20Transactor{contract: contract}, Erc20Filterer: Erc20Filterer{contract: contract}}, nil +} + +// NewErc20Caller creates a new read-only instance of Erc20, bound to a specific deployed contract. +func NewErc20Caller(address common.Address, caller bind.ContractCaller) (*Erc20Caller, error) { + contract, err := bindErc20(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &Erc20Caller{contract: contract}, nil +} + +// NewErc20Transactor creates a new write-only instance of Erc20, bound to a specific deployed contract. +func NewErc20Transactor(address common.Address, transactor bind.ContractTransactor) (*Erc20Transactor, error) { + contract, err := bindErc20(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &Erc20Transactor{contract: contract}, nil +} + +// NewErc20Filterer creates a new log filterer instance of Erc20, bound to a specific deployed contract. +func NewErc20Filterer(address common.Address, filterer bind.ContractFilterer) (*Erc20Filterer, error) { + contract, err := bindErc20(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &Erc20Filterer{contract: contract}, nil +} + +// bindErc20 binds a generic wrapper to an already deployed contract. +func bindErc20(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := Erc20MetaData.GetAbi() + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, *parsed, caller, transactor, filterer), nil +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_Erc20 *Erc20Raw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _Erc20.Contract.Erc20Caller.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_Erc20 *Erc20Raw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Erc20.Contract.Erc20Transactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_Erc20 *Erc20Raw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _Erc20.Contract.Erc20Transactor.contract.Transact(opts, method, params...) +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_Erc20 *Erc20CallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _Erc20.Contract.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_Erc20 *Erc20TransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Erc20.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_Erc20 *Erc20TransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _Erc20.Contract.contract.Transact(opts, method, params...) +} + +// Allowance is a free data retrieval call binding the contract method 0xdd62ed3e. +// +// Solidity: function allowance(address , address ) view returns(uint256) +func (_Erc20 *Erc20Caller) Allowance(opts *bind.CallOpts, arg0 common.Address, arg1 common.Address) (*big.Int, error) { + var out []interface{} + err := _Erc20.contract.Call(opts, &out, "allowance", arg0, arg1) + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// Allowance is a free data retrieval call binding the contract method 0xdd62ed3e. +// +// Solidity: function allowance(address , address ) view returns(uint256) +func (_Erc20 *Erc20Session) Allowance(arg0 common.Address, arg1 common.Address) (*big.Int, error) { + return _Erc20.Contract.Allowance(&_Erc20.CallOpts, arg0, arg1) +} + +// Allowance is a free data retrieval call binding the contract method 0xdd62ed3e. +// +// Solidity: function allowance(address , address ) view returns(uint256) +func (_Erc20 *Erc20CallerSession) Allowance(arg0 common.Address, arg1 common.Address) (*big.Int, error) { + return _Erc20.Contract.Allowance(&_Erc20.CallOpts, arg0, arg1) +} + +// BalanceOf is a free data retrieval call binding the contract method 0x70a08231. +// +// Solidity: function balanceOf(address ) view returns(uint256) +func (_Erc20 *Erc20Caller) BalanceOf(opts *bind.CallOpts, arg0 common.Address) (*big.Int, error) { + var out []interface{} + err := _Erc20.contract.Call(opts, &out, "balanceOf", arg0) + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// BalanceOf is a free data retrieval call binding the contract method 0x70a08231. +// +// Solidity: function balanceOf(address ) view returns(uint256) +func (_Erc20 *Erc20Session) BalanceOf(arg0 common.Address) (*big.Int, error) { + return _Erc20.Contract.BalanceOf(&_Erc20.CallOpts, arg0) +} + +// BalanceOf is a free data retrieval call binding the contract method 0x70a08231. +// +// Solidity: function balanceOf(address ) view returns(uint256) +func (_Erc20 *Erc20CallerSession) BalanceOf(arg0 common.Address) (*big.Int, error) { + return _Erc20.Contract.BalanceOf(&_Erc20.CallOpts, arg0) +} + +// Decimals is a free data retrieval call binding the contract method 0x313ce567. +// +// Solidity: function decimals() view returns(uint8) +func (_Erc20 *Erc20Caller) Decimals(opts *bind.CallOpts) (uint8, error) { + var out []interface{} + err := _Erc20.contract.Call(opts, &out, "decimals") + + if err != nil { + return *new(uint8), err + } + + out0 := *abi.ConvertType(out[0], new(uint8)).(*uint8) + + return out0, err + +} + +// Decimals is a free data retrieval call binding the contract method 0x313ce567. +// +// Solidity: function decimals() view returns(uint8) +func (_Erc20 *Erc20Session) Decimals() (uint8, error) { + return _Erc20.Contract.Decimals(&_Erc20.CallOpts) +} + +// Decimals is a free data retrieval call binding the contract method 0x313ce567. +// +// Solidity: function decimals() view returns(uint8) +func (_Erc20 *Erc20CallerSession) Decimals() (uint8, error) { + return _Erc20.Contract.Decimals(&_Erc20.CallOpts) +} + +// Name is a free data retrieval call binding the contract method 0x06fdde03. +// +// Solidity: function name() view returns(string) +func (_Erc20 *Erc20Caller) Name(opts *bind.CallOpts) (string, error) { + var out []interface{} + err := _Erc20.contract.Call(opts, &out, "name") + + if err != nil { + return *new(string), err + } + + out0 := *abi.ConvertType(out[0], new(string)).(*string) + + return out0, err + +} + +// Name is a free data retrieval call binding the contract method 0x06fdde03. +// +// Solidity: function name() view returns(string) +func (_Erc20 *Erc20Session) Name() (string, error) { + return _Erc20.Contract.Name(&_Erc20.CallOpts) +} + +// Name is a free data retrieval call binding the contract method 0x06fdde03. +// +// Solidity: function name() view returns(string) +func (_Erc20 *Erc20CallerSession) Name() (string, error) { + return _Erc20.Contract.Name(&_Erc20.CallOpts) +} + +// Symbol is a free data retrieval call binding the contract method 0x95d89b41. +// +// Solidity: function symbol() view returns(string) +func (_Erc20 *Erc20Caller) Symbol(opts *bind.CallOpts) (string, error) { + var out []interface{} + err := _Erc20.contract.Call(opts, &out, "symbol") + + if err != nil { + return *new(string), err + } + + out0 := *abi.ConvertType(out[0], new(string)).(*string) + + return out0, err + +} + +// Symbol is a free data retrieval call binding the contract method 0x95d89b41. +// +// Solidity: function symbol() view returns(string) +func (_Erc20 *Erc20Session) Symbol() (string, error) { + return _Erc20.Contract.Symbol(&_Erc20.CallOpts) +} + +// Symbol is a free data retrieval call binding the contract method 0x95d89b41. +// +// Solidity: function symbol() view returns(string) +func (_Erc20 *Erc20CallerSession) Symbol() (string, error) { + return _Erc20.Contract.Symbol(&_Erc20.CallOpts) +} + +// TotalSupply is a free data retrieval call binding the contract method 0x18160ddd. +// +// Solidity: function totalSupply() pure returns(uint256) +func (_Erc20 *Erc20Caller) TotalSupply(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _Erc20.contract.Call(opts, &out, "totalSupply") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// TotalSupply is a free data retrieval call binding the contract method 0x18160ddd. +// +// Solidity: function totalSupply() pure returns(uint256) +func (_Erc20 *Erc20Session) TotalSupply() (*big.Int, error) { + return _Erc20.Contract.TotalSupply(&_Erc20.CallOpts) +} + +// TotalSupply is a free data retrieval call binding the contract method 0x18160ddd. +// +// Solidity: function totalSupply() pure returns(uint256) +func (_Erc20 *Erc20CallerSession) TotalSupply() (*big.Int, error) { + return _Erc20.Contract.TotalSupply(&_Erc20.CallOpts) +} + +// Approve is a paid mutator transaction binding the contract method 0x095ea7b3. +// +// Solidity: function approve(address spender, uint256 value) returns(bool) +func (_Erc20 *Erc20Transactor) Approve(opts *bind.TransactOpts, spender common.Address, value *big.Int) (*types.Transaction, error) { + return _Erc20.contract.Transact(opts, "approve", spender, value) +} + +// Approve is a paid mutator transaction binding the contract method 0x095ea7b3. +// +// Solidity: function approve(address spender, uint256 value) returns(bool) +func (_Erc20 *Erc20Session) Approve(spender common.Address, value *big.Int) (*types.Transaction, error) { + return _Erc20.Contract.Approve(&_Erc20.TransactOpts, spender, value) +} + +// Approve is a paid mutator transaction binding the contract method 0x095ea7b3. +// +// Solidity: function approve(address spender, uint256 value) returns(bool) +func (_Erc20 *Erc20TransactorSession) Approve(spender common.Address, value *big.Int) (*types.Transaction, error) { + return _Erc20.Contract.Approve(&_Erc20.TransactOpts, spender, value) +} + +// Transfer is a paid mutator transaction binding the contract method 0xa9059cbb. +// +// Solidity: function transfer(address to, uint256 value) returns(bool) +func (_Erc20 *Erc20Transactor) Transfer(opts *bind.TransactOpts, to common.Address, value *big.Int) (*types.Transaction, error) { + return _Erc20.contract.Transact(opts, "transfer", to, value) +} + +// Transfer is a paid mutator transaction binding the contract method 0xa9059cbb. +// +// Solidity: function transfer(address to, uint256 value) returns(bool) +func (_Erc20 *Erc20Session) Transfer(to common.Address, value *big.Int) (*types.Transaction, error) { + return _Erc20.Contract.Transfer(&_Erc20.TransactOpts, to, value) +} + +// Transfer is a paid mutator transaction binding the contract method 0xa9059cbb. +// +// Solidity: function transfer(address to, uint256 value) returns(bool) +func (_Erc20 *Erc20TransactorSession) Transfer(to common.Address, value *big.Int) (*types.Transaction, error) { + return _Erc20.Contract.Transfer(&_Erc20.TransactOpts, to, value) +} + +// TransferFrom is a paid mutator transaction binding the contract method 0x23b872dd. +// +// Solidity: function transferFrom(address from, address to, uint256 value) returns(bool) +func (_Erc20 *Erc20Transactor) TransferFrom(opts *bind.TransactOpts, from common.Address, to common.Address, value *big.Int) (*types.Transaction, error) { + return _Erc20.contract.Transact(opts, "transferFrom", from, to, value) +} + +// TransferFrom is a paid mutator transaction binding the contract method 0x23b872dd. +// +// Solidity: function transferFrom(address from, address to, uint256 value) returns(bool) +func (_Erc20 *Erc20Session) TransferFrom(from common.Address, to common.Address, value *big.Int) (*types.Transaction, error) { + return _Erc20.Contract.TransferFrom(&_Erc20.TransactOpts, from, to, value) +} + +// TransferFrom is a paid mutator transaction binding the contract method 0x23b872dd. +// +// Solidity: function transferFrom(address from, address to, uint256 value) returns(bool) +func (_Erc20 *Erc20TransactorSession) TransferFrom(from common.Address, to common.Address, value *big.Int) (*types.Transaction, error) { + return _Erc20.Contract.TransferFrom(&_Erc20.TransactOpts, from, to, value) +} + +// Erc20ApprovalIterator is returned from FilterApproval and is used to iterate over the raw logs and unpacked data for Approval events raised by the Erc20 contract. +type Erc20ApprovalIterator struct { + Event *Erc20Approval // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *Erc20ApprovalIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(Erc20Approval) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(Erc20Approval) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *Erc20ApprovalIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *Erc20ApprovalIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// Erc20Approval represents a Approval event raised by the Erc20 contract. +type Erc20Approval struct { + Owner common.Address + Spender common.Address + Value *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterApproval is a free log retrieval operation binding the contract event 0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925. +// +// Solidity: event Approval(address indexed owner, address indexed spender, uint256 value) +func (_Erc20 *Erc20Filterer) FilterApproval(opts *bind.FilterOpts, owner []common.Address, spender []common.Address) (*Erc20ApprovalIterator, error) { + + var ownerRule []interface{} + for _, ownerItem := range owner { + ownerRule = append(ownerRule, ownerItem) + } + var spenderRule []interface{} + for _, spenderItem := range spender { + spenderRule = append(spenderRule, spenderItem) + } + + logs, sub, err := _Erc20.contract.FilterLogs(opts, "Approval", ownerRule, spenderRule) + if err != nil { + return nil, err + } + return &Erc20ApprovalIterator{contract: _Erc20.contract, event: "Approval", logs: logs, sub: sub}, nil +} + +// WatchApproval is a free log subscription operation binding the contract event 0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925. +// +// Solidity: event Approval(address indexed owner, address indexed spender, uint256 value) +func (_Erc20 *Erc20Filterer) WatchApproval(opts *bind.WatchOpts, sink chan<- *Erc20Approval, owner []common.Address, spender []common.Address) (event.Subscription, error) { + + var ownerRule []interface{} + for _, ownerItem := range owner { + ownerRule = append(ownerRule, ownerItem) + } + var spenderRule []interface{} + for _, spenderItem := range spender { + spenderRule = append(spenderRule, spenderItem) + } + + logs, sub, err := _Erc20.contract.WatchLogs(opts, "Approval", ownerRule, spenderRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(Erc20Approval) + if err := _Erc20.contract.UnpackLog(event, "Approval", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseApproval is a log parse operation binding the contract event 0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925. +// +// Solidity: event Approval(address indexed owner, address indexed spender, uint256 value) +func (_Erc20 *Erc20Filterer) ParseApproval(log types.Log) (*Erc20Approval, error) { + event := new(Erc20Approval) + if err := _Erc20.contract.UnpackLog(event, "Approval", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// Erc20TransferIterator is returned from FilterTransfer and is used to iterate over the raw logs and unpacked data for Transfer events raised by the Erc20 contract. +type Erc20TransferIterator struct { + Event *Erc20Transfer // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *Erc20TransferIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(Erc20Transfer) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(Erc20Transfer) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *Erc20TransferIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *Erc20TransferIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// Erc20Transfer represents a Transfer event raised by the Erc20 contract. +type Erc20Transfer struct { + From common.Address + To common.Address + Value *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterTransfer is a free log retrieval operation binding the contract event 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef. +// +// Solidity: event Transfer(address indexed from, address indexed to, uint256 value) +func (_Erc20 *Erc20Filterer) FilterTransfer(opts *bind.FilterOpts, from []common.Address, to []common.Address) (*Erc20TransferIterator, error) { + + var fromRule []interface{} + for _, fromItem := range from { + fromRule = append(fromRule, fromItem) + } + var toRule []interface{} + for _, toItem := range to { + toRule = append(toRule, toItem) + } + + logs, sub, err := _Erc20.contract.FilterLogs(opts, "Transfer", fromRule, toRule) + if err != nil { + return nil, err + } + return &Erc20TransferIterator{contract: _Erc20.contract, event: "Transfer", logs: logs, sub: sub}, nil +} + +// WatchTransfer is a free log subscription operation binding the contract event 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef. +// +// Solidity: event Transfer(address indexed from, address indexed to, uint256 value) +func (_Erc20 *Erc20Filterer) WatchTransfer(opts *bind.WatchOpts, sink chan<- *Erc20Transfer, from []common.Address, to []common.Address) (event.Subscription, error) { + + var fromRule []interface{} + for _, fromItem := range from { + fromRule = append(fromRule, fromItem) + } + var toRule []interface{} + for _, toItem := range to { + toRule = append(toRule, toItem) + } + + logs, sub, err := _Erc20.contract.WatchLogs(opts, "Transfer", fromRule, toRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(Erc20Transfer) + if err := _Erc20.contract.UnpackLog(event, "Transfer", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseTransfer is a log parse operation binding the contract event 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef. +// +// Solidity: event Transfer(address indexed from, address indexed to, uint256 value) +func (_Erc20 *Erc20Filterer) ParseTransfer(log types.Log) (*Erc20Transfer, error) { + event := new(Erc20Transfer) + if err := _Erc20.contract.UnpackLog(event, "Transfer", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} diff --git a/example/contracts/erc20/ERC20.sol b/example/contracts/erc20/ERC20.sol new file mode 100644 index 0000000000..d553da200e --- /dev/null +++ b/example/contracts/erc20/ERC20.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/IERC20.sol) + +pragma solidity ^0.8.20; + +interface IERC20 { + event Transfer(address indexed from, address indexed to, uint256 value); + event Approval(address indexed owner, address indexed spender, uint256 value); + + function totalSupply() external view returns (uint256); + function balanceOf(address account) external view returns (uint256); + function transfer(address to, uint256 value) external returns (bool); + function allowance(address owner, address spender) external view returns (uint256); + function approve(address spender, uint256 value) external returns (bool); + function transferFrom(address from, address to, uint256 value) external returns (bool); +} + +// NOTE: NOT A REAL IMPLEMENTATION -- DO NOT USE IN PROD +contract ERC20 is IERC20 { + string public name = "ERC20"; + string public symbol = "ERC20"; + uint8 public decimals = 18; + + mapping(address => uint256) public balanceOf; + mapping(address => mapping(address => uint256)) public allowance; + + function totalSupply() external pure override returns (uint256) { + return 1000000 * 10 ** 18; + } + + function transfer(address to, uint256 value) external returns (bool) { + require(balanceOf[msg.sender] >= value, "ERC20: transfer amount exceeds balance"); + balanceOf[msg.sender] -= value; + balanceOf[to] += value; + emit Transfer(msg.sender, to, value); + return true; + } + + function approve(address spender, uint256 value) external returns (bool) { + allowance[msg.sender][spender] = value; + emit Approval(msg.sender, spender, value); + return true; + } + + function transferFrom(address from, address to, uint256 value) external returns (bool) { + require(balanceOf[from] >= value, "ERC20: transfer amount exceeds balance"); + require(allowance[from][msg.sender] >= value, "ERC20: transfer amount exceeds allowance"); + balanceOf[from] -= value; + balanceOf[to] += value; + allowance[from][msg.sender] -= value; + emit Transfer(from, to, value); + return true; + } +} diff --git a/example/contracts/erc20/IERC20.abi b/example/contracts/erc20/IERC20.abi new file mode 100644 index 0000000000..d5e4ff2299 --- /dev/null +++ b/example/contracts/erc20/IERC20.abi @@ -0,0 +1 @@ +[{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"spender","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Transfer","type":"event"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"spender","type":"address"}],"name":"allowance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"approve","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transfer","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transferFrom","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"}] \ No newline at end of file diff --git a/example/contracts/erc20/IERC20.bin b/example/contracts/erc20/IERC20.bin new file mode 100644 index 0000000000..e69de29bb2 diff --git a/example/contracts/erc20/README.md b/example/contracts/erc20/README.md new file mode 100644 index 0000000000..849e19eeb0 --- /dev/null +++ b/example/contracts/erc20/README.md @@ -0,0 +1,6 @@ +To regenerate these files, run: +``` +solc --bin -o example/contracts/erc20 example/contracts/erc20/ERC20.sol --overwrite +solc --abi -o example/contracts/erc20 example/contracts/erc20/ERC20.sol --overwrite +abigen --abi=example/contracts/erc20/ERC20.abi --pkg=erc20 --out=example/contracts/erc20/ERC20.go +``` \ No newline at end of file diff --git a/example/contracts/erc721/DummyERC721.abi b/example/contracts/erc721/DummyERC721.abi new file mode 100644 index 0000000000..9395a3fc51 --- /dev/null +++ b/example/contracts/erc721/DummyERC721.abi @@ -0,0 +1 @@ +[{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"approved","type":"address"},{"indexed":true,"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"operator","type":"address"},{"indexed":false,"internalType":"bool","name":"approved","type":"bool"}],"name":"ApprovalForAll","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":true,"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"Transfer","type":"event"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"approve","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"balance","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"getApproved","outputs":[{"internalType":"address","name":"operator","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"operator","type":"address"}],"name":"isApprovedForAll","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"ownerOf","outputs":[{"internalType":"address","name":"owner","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"randomAddress","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"safeTransferFrom","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"safeTransferFrom","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"operator","type":"address"},{"internalType":"bool","name":"approved","type":"bool"}],"name":"setApprovalForAll","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes4","name":"interfaceId","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"tokenURI","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"transferFrom","outputs":[],"stateMutability":"nonpayable","type":"function"}] \ No newline at end of file diff --git a/example/contracts/erc721/DummyERC721.bin b/example/contracts/erc721/DummyERC721.bin new file mode 100644 index 0000000000..faddd7a16f --- /dev/null +++ b/example/contracts/erc721/DummyERC721.bin @@ -0,0 +1 @@ +608060405273f39fd6e51aad88f6f4ce6ab8827279cfffb9226660045f6101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550348015610063575f80fd5b5061091f806100715f395ff3fe608060405234801561000f575f80fd5b50600436106100e8575f3560e01c806370a082311161008a578063b88d4fde11610064578063b88d4fde14610258578063c87b56dd14610274578063d5bee9f5146102a4578063e985e9c5146102c2576100e8565b806370a08231146101ee57806395d89b411461021e578063a22cb4651461023c576100e8565b8063095ea7b3116100c6578063095ea7b31461016a57806323b872dd1461018657806342842e0e146101a25780636352211e146101be576100e8565b806301ffc9a7146100ec57806306fdde031461011c578063081812fc1461013a575b5f80fd5b61010660048036038101906101019190610495565b6102f2565b60405161011391906104da565b60405180910390f35b6101246102fc565b604051610131919061057d565b60405180910390f35b610154600480360381019061014f91906105d0565b610339565b604051610161919061063a565b60405180910390f35b610184600480360381019061017f919061067d565b61033f565b005b6101a0600480360381019061019b91906106bb565b610343565b005b6101bc60048036038101906101b791906106bb565b610348565b005b6101d860048036038101906101d391906105d0565b61034d565b6040516101e5919061063a565b60405180910390f35b6102086004803603810190610203919061070b565b610377565b6040516102159190610745565b60405180910390f35b610226610381565b604051610233919061057d565b60405180910390f35b61025660048036038101906102519190610788565b6103be565b005b610272600480360381019061026d9190610827565b6103c2565b005b61028e600480360381019061028991906105d0565b6103c9565b60405161029b919061057d565b60405180910390f35b6102ac610408565b6040516102b9919061063a565b60405180910390f35b6102dc60048036038101906102d791906108ab565b61042d565b6040516102e991906104da565b60405180910390f35b5f60019050919050565b60606040518060400160405280600b81526020017f44756d6d79455243373231000000000000000000000000000000000000000000815250905090565b5f919050565b5050565b505050565b505050565b5f60045f9054906101000a900473ffffffffffffffffffffffffffffffffffffffff169050919050565b5f60329050919050565b60606040518060400160405280600581526020017f44554d4d59000000000000000000000000000000000000000000000000000000815250905090565b5050565b5050505050565b60606040518060400160405280601381526020017f68747470733a2f2f6578616d706c652e636f6d000000000000000000000000008152509050919050565b60045f9054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b5f6001905092915050565b5f80fd5b5f80fd5b5f7fffffffff0000000000000000000000000000000000000000000000000000000082169050919050565b61047481610440565b811461047e575f80fd5b50565b5f8135905061048f8161046b565b92915050565b5f602082840312156104aa576104a9610438565b5b5f6104b784828501610481565b91505092915050565b5f8115159050919050565b6104d4816104c0565b82525050565b5f6020820190506104ed5f8301846104cb565b92915050565b5f81519050919050565b5f82825260208201905092915050565b5f5b8381101561052a57808201518184015260208101905061050f565b5f8484015250505050565b5f601f19601f8301169050919050565b5f61054f826104f3565b61055981856104fd565b935061056981856020860161050d565b61057281610535565b840191505092915050565b5f6020820190508181035f8301526105958184610545565b905092915050565b5f819050919050565b6105af8161059d565b81146105b9575f80fd5b50565b5f813590506105ca816105a6565b92915050565b5f602082840312156105e5576105e4610438565b5b5f6105f2848285016105bc565b91505092915050565b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f610624826105fb565b9050919050565b6106348161061a565b82525050565b5f60208201905061064d5f83018461062b565b92915050565b61065c8161061a565b8114610666575f80fd5b50565b5f8135905061067781610653565b92915050565b5f806040838503121561069357610692610438565b5b5f6106a085828601610669565b92505060206106b1858286016105bc565b9150509250929050565b5f805f606084860312156106d2576106d1610438565b5b5f6106df86828701610669565b93505060206106f086828701610669565b9250506040610701868287016105bc565b9150509250925092565b5f602082840312156107205761071f610438565b5b5f61072d84828501610669565b91505092915050565b61073f8161059d565b82525050565b5f6020820190506107585f830184610736565b92915050565b610767816104c0565b8114610771575f80fd5b50565b5f813590506107828161075e565b92915050565b5f806040838503121561079e5761079d610438565b5b5f6107ab85828601610669565b92505060206107bc85828601610774565b9150509250929050565b5f80fd5b5f80fd5b5f80fd5b5f8083601f8401126107e7576107e66107c6565b5b8235905067ffffffffffffffff811115610804576108036107ca565b5b6020830191508360018202830111156108205761081f6107ce565b5b9250929050565b5f805f805f608086880312156108405761083f610438565b5b5f61084d88828901610669565b955050602061085e88828901610669565b945050604061086f888289016105bc565b935050606086013567ffffffffffffffff8111156108905761088f61043c565b5b61089c888289016107d2565b92509250509295509295909350565b5f80604083850312156108c1576108c0610438565b5b5f6108ce85828601610669565b92505060206108df85828601610669565b915050925092905056fea2646970667358221220cac0547d8cdc2d72600e74bf9296eee8caaa1ec4c78594f3b8947dd3ec2baef364736f6c63430008140033 \ No newline at end of file diff --git a/example/contracts/erc721/ERC721.sol b/example/contracts/erc721/ERC721.sol new file mode 100644 index 0000000000..93044d8acc --- /dev/null +++ b/example/contracts/erc721/ERC721.sol @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC721/IERC721.sol) + +pragma solidity ^0.8.20; + +interface IERC165 { + function supportsInterface(bytes4 interfaceId) external view returns (bool); +} + +interface IERC721Receiver { + function onERC721Received( + address operator, + address from, + uint256 tokenId, + bytes calldata data + ) external returns (bytes4); +} + +interface IERC721 is IERC165 { + event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); + event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId); + event ApprovalForAll(address indexed owner, address indexed operator, bool approved); + + function balanceOf(address owner) external view returns (uint256 balance); + function ownerOf(uint256 tokenId) external view returns (address owner); + function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external; + function safeTransferFrom(address from, address to, uint256 tokenId) external; + function transferFrom(address from, address to, uint256 tokenId) external; + function approve(address to, uint256 tokenId) external; + function setApprovalForAll(address operator, bool approved) external; + function getApproved(uint256 tokenId) external view returns (address operator); + function isApprovedForAll(address owner, address operator) external view returns (bool); +} + +// NOT A REAL IMPLEMENTATION -- DO NOT USE IN PROD +contract DummyERC721 is IERC721 { + mapping(uint256 => address) private _tokenOwners; + mapping(address => uint256) private _tokenBalances; + mapping(uint256 => address) private _tokenApprovals; + mapping(address => mapping(address => bool)) private _operatorApprovals; + + address public randomAddress = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266; + + function name() external pure returns (string memory) { + return "DummyERC721"; + } + + function symbol() external pure returns (string memory) { + return "DUMMY"; + } + + function supportsInterface(bytes4 interfaceId) external view returns (bool) { + return true; + } + + function balanceOf(address owner) external view override returns (uint256 balance) { + return 50; + } + + function ownerOf(uint256 tokenId) public view override returns (address owner) { + return randomAddress; + } + + function tokenURI(uint256 tokenId) public view returns (string memory) { + return "https://example.com"; + } + + function transferFrom(address from, address to, uint256 tokenId) public override {} + + function safeTransferFrom(address from, address to, uint256 tokenId) public override {} + + function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) public override {} + + function approve(address to, uint256 tokenId) external override {} + + function getApproved(uint256 tokenId) public view override returns (address operator) {} + + function setApprovalForAll(address operator, bool approved) external override { } + + function isApprovedForAll(address owner, address operator) public view override returns (bool) { + return true; + } + + function _exists(uint256 tokenId) internal view returns (bool) { + return true; + } + + function _transfer(address from, address to, uint256 tokenId) internal {} + + function _checkOnERC721Received(address from, address to, uint256 tokenId, bytes memory data) internal returns (bool) { + return true; + } +} diff --git a/example/contracts/erc721/README.md b/example/contracts/erc721/README.md new file mode 100644 index 0000000000..e8fadec53c --- /dev/null +++ b/example/contracts/erc721/README.md @@ -0,0 +1,6 @@ +To regenerate these files, run: +``` +solc --bin -o example/contracts/erc721 example/contracts/erc721/ERC721.sol --overwrite +solc --abi -o example/contracts/erc721 example/contracts/erc721/ERC721.sol --overwrite +abigen --abi=example/contracts/erc721/ERC721.abi --pkg=erc721 --out=example/contracts/erc721/ERC721.go +``` \ No newline at end of file diff --git a/x/evm/client/wasm/query_test.go b/x/evm/client/wasm/query_test.go index 536f1caf04..6ef307da93 100644 --- a/x/evm/client/wasm/query_test.go +++ b/x/evm/client/wasm/query_test.go @@ -10,6 +10,7 @@ import ( "testing" cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/tx/signing" ethabi "github.com/ethereum/go-ethereum/accounts/abi" @@ -65,12 +66,27 @@ func TestERC20TransferPayload(t *testing.T) { addr1, e1 := testkeeper.MockAddressPair() k.SetAddressMapping(ctx, addr1, e1) h := wasm.NewEVMQueryHandler(k) - value := sdk.NewInt(500) + value := types.NewInt(500) res, err := h.HandleERC20TransferPayload(ctx, addr1.String(), &value) require.Nil(t, err) require.NotEmpty(t, res) } +func TestHandleERC20TokenInfo(t *testing.T) { + k, ctx := testkeeper.MockEVMKeeper() + privKey := testkeeper.MockPrivateKey() + res, _ := deployContract(t, ctx, k, "../../../../example/contracts/erc20/ERC20.bin", privKey) + addr1, e1 := testkeeper.MockAddressPair() + k.SetAddressMapping(ctx, addr1, e1) + receipt, err := k.GetReceipt(ctx, common.HexToHash(res.Hash)) + require.Nil(t, err) + contractAddr := common.HexToAddress(receipt.ContractAddress) + h := wasm.NewEVMQueryHandler(k) + tokenInfo, err := h.HandleERC20TokenInfo(ctx, contractAddr.String(), addr1.String()) + require.Nil(t, err) + require.Equal(t, string(tokenInfo), "{\"name\":\"ERC20\",\"symbol\":\"ERC20\",\"decimals\":18,\"total_supply\":\"1000000000000000000000000\"}") +} + func TestERC20TransferFromPayload(t *testing.T) { k, ctx := testkeeper.MockEVMKeeper() addr1, e1 := testkeeper.MockAddressPair() @@ -78,7 +94,7 @@ func TestERC20TransferFromPayload(t *testing.T) { k.SetAddressMapping(ctx, addr1, e1) k.SetAddressMapping(ctx, addr2, e2) h := wasm.NewEVMQueryHandler(k) - value := sdk.NewInt(500) + value := types.NewInt(500) res, err := h.HandleERC20TransferFromPayload(ctx, addr1.String(), addr2.String(), &value) require.Nil(t, err) require.NotEmpty(t, res) @@ -89,12 +105,126 @@ func TestERC20ApprovePayload(t *testing.T) { addr1, e1 := testkeeper.MockAddressPair() k.SetAddressMapping(ctx, addr1, e1) h := wasm.NewEVMQueryHandler(k) - value := sdk.NewInt(500) + value := types.NewInt(500) res, err := h.HandleERC20ApprovePayload(ctx, addr1.String(), &value) require.Nil(t, err) require.NotEmpty(t, res) } +func TestHandleERC20Balance(t *testing.T) { + k, ctx := testkeeper.MockEVMKeeper() + privKey := testkeeper.MockPrivateKey() + res, _ := deployContract(t, ctx, k, "../../../../example/contracts/erc20/ERC20.bin", privKey) + addr1, e1 := testkeeper.MockAddressPair() + k.SetAddressMapping(ctx, addr1, e1) + receipt, err := k.GetReceipt(ctx, common.HexToHash(res.Hash)) + require.Nil(t, err) + contractAddr := common.HexToAddress(receipt.ContractAddress) + h := wasm.NewEVMQueryHandler(k) + res2, err := h.HandleERC20Balance(ctx, contractAddr.String(), addr1.String()) + require.Nil(t, err) + require.Equal(t, string(res2), "{\"balance\":\"0\"}") + require.NotEmpty(t, res2) +} + +func TestHandleERC721Owner(t *testing.T) { + k, ctx := testkeeper.MockEVMKeeper() + privKey := testkeeper.MockPrivateKey() + res, _ := deployContract(t, ctx, k, "../../../../example/contracts/erc721/DummyERC721.bin", privKey) + addr1, e1 := testkeeper.MockAddressPair() + k.SetAddressMapping(ctx, addr1, e1) + receipt, err := k.GetReceipt(ctx, common.HexToHash(res.Hash)) + require.Nil(t, err) + contractAddr := common.HexToAddress(receipt.ContractAddress) + h := wasm.NewEVMQueryHandler(k) + res2, err := h.HandleERC721Owner(ctx, addr1.String(), contractAddr.String(), "1") + require.Nil(t, err) + require.NotEmpty(t, res2) +} + +func TestHandleERC20Allowance(t *testing.T) { + k, ctx := testkeeper.MockEVMKeeper() + privKey := testkeeper.MockPrivateKey() + res, _ := deployContract(t, ctx, k, "../../../../example/contracts/erc20/ERC20.bin", privKey) + addr1, e1 := testkeeper.MockAddressPair() + addr2, e2 := testkeeper.MockAddressPair() + k.SetAddressMapping(ctx, addr1, e1) + k.SetAddressMapping(ctx, addr2, e2) + receipt, err := k.GetReceipt(ctx, common.HexToHash(res.Hash)) + require.Nil(t, err) + contractAddr := common.HexToAddress(receipt.ContractAddress) + h := wasm.NewEVMQueryHandler(k) + res2, err := h.HandleERC20Allowance(ctx, contractAddr.String(), addr1.String(), addr2.String()) + require.Nil(t, err) + require.NotEmpty(t, res2) + require.Equal(t, string(res2), "{\"allowance\":\"0\"}") +} + +func TestHandleERC721Approved(t *testing.T) { + k, ctx := testkeeper.MockEVMKeeper() + privKey := testkeeper.MockPrivateKey() + res, _ := deployContract(t, ctx, k, "../../../../example/contracts/erc721/DummyERC721.bin", privKey) + addr1, e1 := testkeeper.MockAddressPair() + addr2, e2 := testkeeper.MockAddressPair() + k.SetAddressMapping(ctx, addr1, e1) + k.SetAddressMapping(ctx, addr2, e2) + receipt, err := k.GetReceipt(ctx, common.HexToHash(res.Hash)) + require.Nil(t, err) + contractAddr := common.HexToAddress(receipt.ContractAddress) + h := wasm.NewEVMQueryHandler(k) + res2, err := h.HandleERC721Approved(ctx, addr1.String(), contractAddr.String(), "1") + require.Nil(t, err) + require.NotEmpty(t, res2) +} + +func TestHandleERC721IsApprovedForAll(t *testing.T) { + k, ctx := testkeeper.MockEVMKeeper() + privKey := testkeeper.MockPrivateKey() + res, _ := deployContract(t, ctx, k, "../../../../example/contracts/erc721/DummyERC721.bin", privKey) + addr1, e1 := testkeeper.MockAddressPair() + addr2, e2 := testkeeper.MockAddressPair() + k.SetAddressMapping(ctx, addr1, e1) + k.SetAddressMapping(ctx, addr2, e2) + receipt, err := k.GetReceipt(ctx, common.HexToHash(res.Hash)) + require.Nil(t, err) + contractAddr := common.HexToAddress(receipt.ContractAddress) + h := wasm.NewEVMQueryHandler(k) + res2, err := h.HandleERC721IsApprovedForAll(ctx, addr1.String(), contractAddr.String(), addr2.String(), addr2.String()) + require.Nil(t, err) + require.NotEmpty(t, res2) +} + +func TestHandleERC721NameSymbol(t *testing.T) { + k, ctx := testkeeper.MockEVMKeeper() + privKey := testkeeper.MockPrivateKey() + res, _ := deployContract(t, ctx, k, "../../../../example/contracts/erc721/DummyERC721.bin", privKey) + addr1, e1 := testkeeper.MockAddressPair() + k.SetAddressMapping(ctx, addr1, e1) + receipt, err := k.GetReceipt(ctx, common.HexToHash(res.Hash)) + require.Nil(t, err) + contractAddr := common.HexToAddress(receipt.ContractAddress) + h := wasm.NewEVMQueryHandler(k) + res2, err := h.HandleERC721NameSymbol(ctx, addr1.String(), contractAddr.String()) + require.Nil(t, err) + require.NotEmpty(t, res2) + require.Equal(t, string(res2), "{\"name\":\"DummyERC721\",\"symbol\":\"DUMMY\"}") +} + +func TestHandleERC721TokenURI(t *testing.T) { + k, ctx := testkeeper.MockEVMKeeper() + privKey := testkeeper.MockPrivateKey() + res, _ := deployContract(t, ctx, k, "../../../../example/contracts/erc721/DummyERC721.bin", privKey) + addr1, e1 := testkeeper.MockAddressPair() + k.SetAddressMapping(ctx, addr1, e1) + receipt, err := k.GetReceipt(ctx, common.HexToHash(res.Hash)) + require.Nil(t, err) + contractAddr := common.HexToAddress(receipt.ContractAddress) + h := wasm.NewEVMQueryHandler(k) + res2, err := h.HandleERC721Uri(ctx, addr1.String(), contractAddr.String(), "1") + require.Nil(t, err) + require.NotEmpty(t, res2) +} + func TestGetAddress(t *testing.T) { k, ctx := testkeeper.MockEVMKeeper() seiAddr1, evmAddr1 := testkeeper.MockAddressPair() @@ -137,18 +267,14 @@ func (tx mockTx) GetSigners() []sdk.AccAddress { return tx.si func (tx mockTx) GetPubKeys() ([]cryptotypes.PubKey, error) { return nil, nil } func (tx mockTx) GetSignaturesV2() ([]signing.SignatureV2, error) { return nil, nil } -func TestHandleStaticCall(t *testing.T) { - k, ctx := testkeeper.MockEVMKeeper() - code, err := os.ReadFile("../../../../example/contracts/simplestorage/SimpleStorage.bin") +func deployContract(t *testing.T, ctx sdk.Context, k *keeper.Keeper, path string, privKey cryptotypes.PrivKey) (*evmtypes.MsgEVMTransactionResponse, evmtypes.MsgServer) { + code, err := os.ReadFile(path) require.Nil(t, err) bz, err := hex.DecodeString(string(code)) require.Nil(t, err) - privKey := testkeeper.MockPrivateKey() - testPrivHex := hex.EncodeToString(privKey.Bytes()) - key, _ := crypto.HexToECDSA(testPrivHex) txData := ethtypes.LegacyTx{ GasPrice: big.NewInt(1000000000000), - Gas: 200000, + Gas: 2000000, To: nil, Value: big.NewInt(0), Data: bz, @@ -159,6 +285,8 @@ func TestHandleStaticCall(t *testing.T) { ethCfg := chainCfg.EthereumConfig(chainID) blockNum := big.NewInt(ctx.BlockHeight()) signer := ethtypes.MakeSigner(ethCfg, blockNum, uint64(ctx.BlockTime().Unix())) + testPrivHex := hex.EncodeToString(privKey.Bytes()) + key, _ := crypto.HexToECDSA(testPrivHex) tx, err := ethtypes.SignTx(ethtypes.NewTx(&txData), signer, key) require.Nil(t, err) txwrapper, err := ethtx.NewLegacyTx(tx) @@ -167,13 +295,13 @@ func TestHandleStaticCall(t *testing.T) { require.Nil(t, err) _, evmAddr := testkeeper.PrivateKeyToAddresses(privKey) - amt := sdk.NewCoins(sdk.NewCoin(k.GetBaseDenom(ctx), sdk.NewInt(1000000))) - k.BankKeeper().MintCoins(ctx, evmtypes.ModuleName, sdk.NewCoins(sdk.NewCoin(k.GetBaseDenom(ctx), sdk.NewInt(1000000)))) + + amt := sdk.NewCoins(sdk.NewCoin(k.GetBaseDenom(ctx), sdk.NewInt(100000000))) + k.BankKeeper().MintCoins(ctx, evmtypes.ModuleName, sdk.NewCoins(sdk.NewCoin(k.GetBaseDenom(ctx), sdk.NewInt(100000000)))) k.BankKeeper().SendCoinsFromModuleToAccount(ctx, evmtypes.ModuleName, evmAddr[:], amt) msgServer := keeper.NewMsgServerImpl(k) - // Deploy Simple Storage contract ante.Preprocess(ctx, req) ctx, err = ante.NewEVMFeeCheckDecorator(k).AnteHandle(ctx, mockTx{msgs: []sdk.Msg{req}}, false, func(sdk.Context, sdk.Tx, bool) (sdk.Context, error) { return ctx, nil @@ -181,11 +309,37 @@ func TestHandleStaticCall(t *testing.T) { require.Nil(t, err) res, err := msgServer.EVMTransaction(sdk.WrapSDKContext(ctx), req) require.Nil(t, err) + + receipt, err := k.GetReceipt(ctx, common.HexToHash(res.Hash)) + require.Nil(t, err) + if receipt.Status != 1 { + t.Fatalf("receipt status is not 1, got %d, vmerror = %s", receipt.Status, receipt.VmError) + } + return res, msgServer +} + +func createSigner(k *keeper.Keeper, ctx sdk.Context) ethtypes.Signer { + chainID := k.ChainID(ctx) + chainCfg := evmtypes.DefaultChainConfig() + ethCfg := chainCfg.EthereumConfig(chainID) + blockNum := big.NewInt(ctx.BlockHeight()) + signer := ethtypes.MakeSigner(ethCfg, blockNum, uint64(ctx.BlockTime().Unix())) + return signer +} + +func TestHandleStaticCall(t *testing.T) { + k, ctx := testkeeper.MockEVMKeeper() + privKey := testkeeper.MockPrivateKey() + _, evmAddr := testkeeper.PrivateKeyToAddresses(privKey) + testPrivHex := hex.EncodeToString(privKey.Bytes()) + key, _ := crypto.HexToECDSA(testPrivHex) + signer := createSigner(k, ctx) + res, msgServer := deployContract(t, ctx, k, "../../../../example/contracts/simplestorage/SimpleStorage.bin", privKey) require.LessOrEqual(t, res.GasUsed, uint64(200000)) require.Empty(t, res.VmError) require.NotEmpty(t, res.ReturnData) require.NotEmpty(t, res.Hash) - require.Equal(t, uint64(1000000)-res.GasUsed, k.BankKeeper().GetBalance(ctx, sdk.AccAddress(evmAddr[:]), "usei").Amount.Uint64()) + require.Equal(t, uint64(100000000)-res.GasUsed, k.BankKeeper().GetBalance(ctx, sdk.AccAddress(evmAddr[:]), "usei").Amount.Uint64()) require.Equal(t, res.GasUsed, k.BankKeeper().GetBalance(ctx, state.GetCoinbaseAddress(ctx.TxIndex()), k.GetBaseDenom(ctx)).Amount.Uint64()) receipt, err := k.GetReceipt(ctx, common.HexToHash(res.Hash)) require.Nil(t, err) @@ -196,9 +350,9 @@ func TestHandleStaticCall(t *testing.T) { contractAddr := common.HexToAddress(receipt.ContractAddress) abi, err := simplestorage.SimplestorageMetaData.GetAbi() require.Nil(t, err) - bz, err = abi.Pack("set", big.NewInt(20)) + bz, err := abi.Pack("set", big.NewInt(20)) require.Nil(t, err) - txData = ethtypes.LegacyTx{ + txData := ethtypes.LegacyTx{ GasPrice: big.NewInt(1000000000000), Gas: 200000, To: &contractAddr, @@ -206,11 +360,11 @@ func TestHandleStaticCall(t *testing.T) { Data: bz, Nonce: 1, } - tx, err = ethtypes.SignTx(ethtypes.NewTx(&txData), signer, key) + tx, err := ethtypes.SignTx(ethtypes.NewTx(&txData), signer, key) require.Nil(t, err) - txwrapper, err = ethtx.NewLegacyTx(tx) + txwrapper, err := ethtx.NewLegacyTx(tx) require.Nil(t, err) - req, err = evmtypes.NewMsgEVMTransaction(txwrapper) + req, err := evmtypes.NewMsgEVMTransaction(txwrapper) require.Nil(t, err) ante.Preprocess(ctx, req) ctx, err = ante.NewEVMFeeCheckDecorator(k).AnteHandle(ctx, mockTx{msgs: []sdk.Msg{req}}, false, func(sdk.Context, sdk.Tx, bool) (sdk.Context, error) { From 03adf59e15a2f5a90b4c535f1f0e8fdc47cf4226 Mon Sep 17 00:00:00 2001 From: Jeremy Wei Date: Tue, 30 Apr 2024 12:19:31 -0400 Subject: [PATCH 26/31] Add ERC721 to CW721 Pointer Tests (#1602) * add cw721 to erc721 query tests * add execution tests (including one that's failing) * wip * able to instantiate and read name * fix * failing getApproved() * transferfrom wroks * added more tests * fix * remove comments --------- Co-authored-by: Steven Landers --- contracts/src/CW721ERC721Pointer.sol | 4 +- contracts/test/ERC721toCW721PointerTest.js | 148 ++++++++++++++++++ contracts/test/lib.js | 25 ++- contracts/wasm/cw721_base.wasm | Bin 0 -> 377539 bytes .../scripts/evm_interoperability_tests.sh | 1 + 5 files changed, 175 insertions(+), 3 deletions(-) create mode 100644 contracts/test/ERC721toCW721PointerTest.js create mode 100755 contracts/wasm/cw721_base.wasm diff --git a/contracts/src/CW721ERC721Pointer.sol b/contracts/src/CW721ERC721Pointer.sol index a8c614e189..259a135737 100644 --- a/contracts/src/CW721ERC721Pointer.sol +++ b/contracts/src/CW721ERC721Pointer.sol @@ -99,8 +99,8 @@ contract CW721ERC721Pointer is ERC721 { string memory spender = _formatPayload("spender", _doubleQuotes(AddrPrecompile.getSeiAddr(approved))); string memory tId = _formatPayload("token_id", _doubleQuotes(Strings.toString(tokenId))); string memory req = _curlyBrace(_formatPayload("approve", _curlyBrace(_join(spender, tId, ",")))); - _execute(bytes(req)); - emit Approval(ownerOf(tokenId), approved, tokenId); + _execute(bytes(req)); + emit Approval(ownerOf(tokenId), approved, tokenId); } function setApprovalForAll(address operator, bool approved) public override { diff --git a/contracts/test/ERC721toCW721PointerTest.js b/contracts/test/ERC721toCW721PointerTest.js new file mode 100644 index 0000000000..89a9f94d4c --- /dev/null +++ b/contracts/test/ERC721toCW721PointerTest.js @@ -0,0 +1,148 @@ +const {setupSigners, deployErc721PointerForCw721, getAdmin, deployWasm, executeWasm} = require("./lib"); +const {expect} = require("chai"); + +const CW721_BASE_WASM_LOCATION = "../contracts/wasm/cw721_base.wasm"; + +const erc721Abi = [ + "event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId)", + "event Transfer(address indexed from, address indexed to, uint256 indexed tokenId)", + "event ApprovalForAll(address indexed owner, address indexed operator, bool approved)", + "function name() view returns (string)", + "function symbol() view returns (string)", + "function totalSupply() view returns (uint256)", + "function tokenURI(uint256 tokenId) view returns (string)", + "function balanceOf(address owner) view returns (uint256 balance)", + "function ownerOf(uint256 tokenId) view returns (address owner)", + "function getApproved(uint256 tokenId) view returns (address operator)", + "function isApprovedForAll(address owner, address operator) view returns (bool)", + "function approve(address to, uint256 tokenId) returns (bool)", + "function setApprovalForAll(address operator, bool _approved) returns (bool)", + "function transferFrom(address from, address to, uint256 tokenId) returns (bool)", + "function safeTransferFrom(address from, address to, uint256 tokenId) returns (bool)", + "function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) returns (bool)" +]; + +describe("ERC721 to CW721 Pointer", function () { + let accounts; + let pointerAcc0; + let pointerAcc1; + let cw721Address; + let admin; + + before(async function () { + accounts = await setupSigners(await hre.ethers.getSigners()) + admin = await getAdmin() + + cw721Address = await deployWasm(CW721_BASE_WASM_LOCATION, admin.seiAddress, "cw721", { + name: "Test", + symbol: "TEST", + minter: admin.seiAddress + }) + + await executeWasm(cw721Address, { mint : { token_id : "1", owner : admin.seiAddress, token_uri: "token uri 1"}}); + await executeWasm(cw721Address, { mint : { token_id : "2", owner : accounts[0].seiAddress, token_uri: "token uri 2"}}); + await executeWasm(cw721Address, { mint : { token_id : "3", owner : accounts[1].seiAddress, token_uri: "token uri 3"}}); + + const pointerAddr = await deployErc721PointerForCw721(hre.ethers.provider, cw721Address) + const contract = new hre.ethers.Contract(pointerAddr, erc721Abi, hre.ethers.provider); + pointerAcc0 = contract.connect(accounts[0].signer) + pointerAcc1 = contract.connect(accounts[1].signer) + }) + + describe("read", function(){ + it("get name", async function () { + const name = await pointerAcc0.name(); + expect(name).to.equal("Test"); + }); + + it("get symbol", async function () { + const symbol = await pointerAcc0.symbol(); + expect(symbol).to.equal("TEST"); + }); + + it("owner of", async function () { + const owner = await pointerAcc0.ownerOf(1); + expect(owner).to.equal(admin.evmAddress); + }); + + it("token uri", async function () { + const uri = await pointerAcc0.tokenURI(1); + expect(uri).to.equal("token uri 1"); + }); + + it("balance of", async function () { + const balance = await pointerAcc0.balanceOf(admin.evmAddress); + expect(balance).to.equal(1); + }); + + it("get approved", async function () { + const approved = await pointerAcc0.getApproved(1); + expect(approved).to.equal("0x0000000000000000000000000000000000000000"); + }); + + it("is approved for all", async function () { + const approved = await pointerAcc0.isApprovedForAll(admin.evmAddress, admin.evmAddress); + expect(approved).to.equal(false); + }); + }) + + describe("write", function(){ + it("approve", async function () { + const approvedTxResp = await pointerAcc0.approve(accounts[1].evmAddress, 2) + await approvedTxResp.wait() + const approved = await pointerAcc0.getApproved(2); + expect(approved).to.equal(accounts[1].evmAddress); + + await expect(approvedTxResp) + .to.emit(pointerAcc0, 'Approval') + .withArgs(accounts[0].evmAddress, accounts[1].evmAddress, 2); + }); + + it("cannot approve token you don't own", async function () { + await expect(pointerAcc0.approve(accounts[1].evmAddress, 1)).to.be.reverted; + }); + + it("transfer from", async function () { + // accounts[0] should transfer token id 2 to accounts[1] + await mine(pointerAcc0.approve(accounts[1].evmAddress, 2)); + transferTxResp = await pointerAcc1.transferFrom(accounts[0].evmAddress, accounts[1].evmAddress, 2); + await transferTxResp.wait(); + await expect(transferTxResp) + .to.emit(pointerAcc0, 'Transfer') + .withArgs(accounts[0].evmAddress, accounts[1].evmAddress, 2); + const balance0 = await pointerAcc0.balanceOf(accounts[0].evmAddress); + expect(balance0).to.equal(0); + const balance1 = await pointerAcc0.balanceOf(accounts[1].evmAddress); + expect(balance1).to.equal(2); + + // return token id 2 back to accounts[0] using safe transfer from + await mine(pointerAcc1.approve(accounts[0].evmAddress, 2)); + await mine(pointerAcc1.safeTransferFrom(accounts[1].evmAddress, accounts[0].evmAddress, 2)); + const balance0After = await pointerAcc0.balanceOf(accounts[0].evmAddress); + expect(balance0After).to.equal(1); + }); + + it("cannot transfer token you don't own", async function () { + await expect(pointerAcc0.transferFrom(accounts[0].evmAddress, accounts[1].evmAddress, 3)).to.be.reverted; + }); + + it("set approval for all", async function () { + const setApprovalForAllTxResp = await pointerAcc0.setApprovalForAll(accounts[1].evmAddress, true); + await setApprovalForAllTxResp.wait(); + await expect(setApprovalForAllTxResp) + .to.emit(pointerAcc0, 'ApprovalForAll') + .withArgs(accounts[0].evmAddress, accounts[1].evmAddress, true); + const approved = await pointerAcc0.isApprovedForAll(accounts[0].evmAddress, accounts[1].evmAddress); + expect(approved).to.equal(true); + + // test revoking approval + await mine(pointerAcc0.setApprovalForAll(accounts[1].evmAddress, false)); + const approvedAfter = await pointerAcc0.isApprovedForAll(accounts[0].evmAddress, accounts[1].evmAddress); + expect(approvedAfter).to.equal(false); + }); + }) +}) + +async function mine(action) { + await (await action).wait() +} diff --git a/contracts/test/lib.js b/contracts/test/lib.js index c59a3dd619..e409e436a8 100644 --- a/contracts/test/lib.js +++ b/contracts/test/lib.js @@ -65,6 +65,12 @@ async function getPointerForCw20(cw20Address) { return JSON.parse(output); } +async function getPointerForCw721(cw721Address) { + const command = `seid query evm pointer CW721 ${cw721Address} -o json` + const output = await execute(command); + return JSON.parse(output); +} + async function deployErc20PointerForCw20(provider, cw20Address) { const command = `seid tx evm call-precompile pointer addCW20Pointer ${cw20Address} --from=admin -b block` const output = await execute(command); @@ -81,6 +87,22 @@ async function deployErc20PointerForCw20(provider, cw20Address) { throw new Error("contract deployment failed") } +async function deployErc721PointerForCw721(provider, cw721Address) { + const command = `seid tx evm call-precompile pointer addCW721Pointer ${cw721Address} --from=admin -b block` + const output = await execute(command); + const txHash = output.replace(/.*0x/, "0x").trim() + let attempt = 0; + while(attempt < 10) { + const receipt = await provider.getTransactionReceipt(txHash); + if(receipt) { + return (await getPointerForCw721(cw721Address)).pointer + } + await sleep(500) + attempt++ + } + throw new Error("contract deployment failed") +} + async function deployWasm(path, adminAddr, label, args = {}) { const codeId = await storeWasm(path) return await instantiateWasm(codeId, adminAddr, label, args) @@ -197,4 +219,5 @@ module.exports = { setupSigners, deployEvmContract, deployErc20PointerForCw20, -}; \ No newline at end of file + deployErc721PointerForCw721, +}; diff --git a/contracts/wasm/cw721_base.wasm b/contracts/wasm/cw721_base.wasm new file mode 100755 index 0000000000000000000000000000000000000000..09c700df88f5cd62d3727d16ce98ad2ec58bdc6e GIT binary patch literal 377539 zcmeFa3$$isdFQ!r-{suDsyaXw6tKUKGM+PwT9zHGDv{3At_2Dsvw}VL44LILg;FgX z5TPs`?XjVh(x4#`4Js(zlEjFK67iO3k1-X45tSq)Z35{SQ%0gBLB$RxOl#u&{?GHi zd++buC@yw7YlYIY_qX?ZdEV##Jnv;E7r)|VX_6%A8?#gQW!GMt?CV}@|0VkhU6XyC z@H4kxJ^Q8SDgW2A^jg*9Z+dNdjs9HgkFW8X*ChM;p-$a6&nDtC9!}Ty*IvUb>z7s% zYo7-W0Z%5Ig2}!_9bF?xifg#=D?rX&gp%8-rjE+YwSMZBM{0yihtBCfdsjA>@4xa@ zor_=m@&gBxuI_u6y!fI6`!BvEnbfa7zy9w7mmS=nB>FjFFJJcZSM6^Kzv3kqUzzk` z)hqYE`k+^xx%iSx4qWuAi?6usl8dQt&26w}RCxK7m%W5H|89TMS4}T_=__A$@s;(n z!7KK^IV_~MHXzU0!eSDj1tzxb6e_0ddS{mT6ZUUSh)F1`4&D}nc|owszRlcFe6 z{+Giy_FtF3oh-{bMOG9^;SagjO{bGmzLWR+oz6@`y}anmQZvsFcwjEzrhHo+kxAdNiU-F*8$$ z*AxDv^pfYDET?{7U8!|}qTYK&mgkhB3sPw^ zp`m`F-ynwn)bBBN(^CcVi?XlwQI{)xOR`X^J#{_FKl>G5CDORq|Qhku-V|LRv> zc3}T2E~2^1FT3)k$+xo&?PmvGs%q_rXbzfv`2ikoy6j4R{M(CPvR?xDipySpp7s*B#%Iq>qU|F5SlemMQr^yBG=(mT_Sr5{N@n%Mem4DF`rGMU>0hP~rQb=vmHu`5aQe;k+vy*s-%I~4J(OLSy&*fCy)}D%c4PLl z+0EJ8v$tjM%x=owk=>U4$Lu}X&t*TK{Ym!0>{qfovVX`vko{)yr`e~nPiMcL{lD3N z%D#~OarTp)`?EjGzMlO>_Lc01z@;}c1Oa7<%pXXoAkLF*= z|6Bg`{G0h--kE&=7mj;N=hi$eu08D#|0Pv<`KYta zZp(BbONQNSPqwE_wlB`a7>tD{ku=VhntEN1hgVpp!B(=?)|Nm=a5 z3Ld2VQq6M7$uRZxG|gMo$^5aCMd(OHn8b%N%B%F?s3_Bt|3CkvQCjsucHi!{$DsjtmK&(3U~O9F^Hvw;B5G=$Aut)+~i4*u3~n?dbiP7B;5>Q|M$`%;ik zXR6${(I(aFsPpRQ&ZK3c{_-*z{MuoVl}rsj;aAGm<9skpv(`BC_BhLIX#_0iwMLry zNHa#7F+Ms~$9C5qsbL4KXr#G7oEIF;Nb@k#oUs;V-i$QYNK@e|j#T3_M3-3@X}-ot z8Ssjc=FLbyJ)ITTX4eFXJDOZR3WIf#a}q_7DjG!x) z=!ijihRCcCxlUQMMXo42?KV2Jp?aMla=eZWf(jG4&gE5C(xO+rAT8E^Qy0EV7LvPP zcjQpAGfCjUd`SkM()Ei=o~|F0# z!D8Za=k$f_0`cBL_CoPK6uv)M+MXCgdgHLHfDmdCn;Nxg8!dzRJ)9x?q^=2MLtdBA z4D|=me^zubL|$-?=(t>PWPwaV7PA-bLM}juNU^GL@px_Fs%joNYdUucg`E%)?aX`*;DPIQ2vNGO_gK8yFYV~N&Vl*m%(+~#D+fh-!-#Y5=%KJcs8nLIi zOKpcBuZ||sBX}QiX0((?TGqpC8M3oCm2VOHI`S6CJ@~flEqo`GW6ZztdVa!uI03>1 zg;9TlUlEs6h#!T!74{CjH@F>BF1l4mA{28|)%|L^+Qn0bd*hAz@sR!wj=FV!Uk5vv zMmr}+yKdCo&h?CQqMhr>&h=dp>V7m)axvGkS55>Gu|gA5cySYwioWa|&ysRtg`ML? z+s^fB_te_JJ+*p>gt?ZyjtvS+YO;U1yOzAFUCU*4g}vxnS}_NS7Sy*l;xw;}`9ZHf1XAB*S9XVKN?t{aeo_>&Cea^6- z?*R~obv5q$ z3ZGp}uF(KSl{|0o1&u+#4rXvNUE~l+6%)6sd9Gkf!q6?di`f~wvUy3|V-!&VG(adt z1Bx|56Z50V+QGJ`Njce;r*6Z^Q=cbE(eUZRV-f|wW0r0;D^Wx3&+IZ;25N*j5F)}Iye^|ZX$;tl6F_#PKDpMeONFrBOP%ma@kGi|^(lwAh1(OZN ztUe$n=k3hOdYHl0IjueFp{bO!hp9J3mDA(|(#t|t?o1|l4AC-KAaJm=4tiY0Vwt8h z3@k_|FeIX~zz}u|8ojcXC!~22a5Hqs-u!t1y5+$Or+jMiC)%llxQmwyi}Nbw`=AdHBZ3zE$v)NH$#}g zADMF2FHEK(lPY)~ZT);kt)F+uFAe$`Fok4>4fN62IbJu*?m+(KRNp4557wf(c;?|% zHU5Ke&?+kbBP1~!Q7Zq*MK;DW@_r)EEDJz9vZ;B--RBn1Sk>C6dXkY1Y7`c7wHwvV zc!EKC1rnJeg*jXP#QdmN>%ZVn^crb)UEs!e>bB)H>;AKqCbJ}k+Hw;@0;p}J&CEgZ zD42+!Svkbdt(=*#CvKtY)X49SQE10Mr|4ucg=>zR3C>EZtty7Ymsaz3rxb;`7DG81rs;=W?kpWQ zOfdFqYtq9n5ZjYYB_l_c-@G2>|zIaOofzD~8KK_QGZ*xs*c?5l29FTaHSV+!bCTtIOX3U;#;;zm1 z#Nd}2cegm7@-^#6NmLw`RbNg_?9>uTMag29+b{X8#Enddp5m`7#&L1&OP2N1WOfuX z@t};7nDU6;i1IYXbc3Z~noAJ`^)Rvt851#_-x#2$P!w9{RfxzGGgxe>35(9bsvip) z2*m-*aJ%Xgfo+DRDPr6~ho(TBH`2un-B4CD!&Jf|;p25{L@FkXF!lzs8t?RyR~1_+ z^=5CuHlFQ}mMj*4ZN+_X6;mO)yR>mmy(i}~xEqC(RZdIqN5C9Pql!IAO^qv{K~Set z>%IGErd}(iu(*!iC{fIXxrY2j^T%EYCEMo}W9M(PhtYR+rtwIbC)P2X(8VxvZS~vFt(+ zf!eqLNKgb2R%xhx{33wh?@`3A9qky0_}jr@We8>vlf`@=pk&X=E*;KgAc7?CD8|4S zl+z8L&+g4%Fq2ZmIa&cg6M6N7c-88FMlCIoU0x@MmP!igH$5 zRg`nexD^6A*_#h%El`>%_7=n0DYZqL6jeuJMheR!f>fd7aubV z)q32Gk4a6m9(Us7Lbz+aKx#>CAR)Cz6Knw@O^>1m6HkR4Yb~XDtycDU`pRN0sip=g zDL*uN9s*Pg&Z+B*Y|uo$EtBo^kdElrYGYbzgI3qo9IDW(GFUjWdx5@VQih=|KG*dXqe@}DlJYP9pL zuV;OkpI$jB3{K^0Wy-x%7t1Ljp(U16Xe>bJ$^6jWlrTP_b|y*Kw#*NY2YNiGKZA0v z?zfz|V{iG!a`ukH!;O;1Df>CCpXF>d^E?D=X1HOh+(7mo(U&O!i)~Q}SYv7hnKNvZ zLxHiXJ|Ka-6%8<<+2Nv`)a-CkPD%R}bNf9y&0(u_M+|dyt|wB zqtkP=aoyT-iT$gJ!TY3vqX)19W&q10CMm=fgOdjDmDZ|A=Y@jUaZ?h)h7PmsxHt-o+-gg0qcy;mb+Z-fcsE;PH#2oN&^;5~>1Hu`vl`ayQD48_ z5PvCx5aO5jV3wgwW%F{>RVB~FM~ROCm8x%j<&z)iD%+OrJ&^GF zR^-sVK&HrtsdV|hAdm-2=g!NR)ecdPSGV`4RX&v`O!D0p8dPU2?fn^^9t}?!pVEHY zs4L%EC9BibkfAYvkmG}!f%W%yVKyhit7I=0^3V?FIIXw{3>_3oP?6^70Wt{j`5H=* z#t1AX*HAxJdh%wRx6#3&gjpS9&9F|xxG&y|MdG~(8}H*Zka@wY88ahMDE;wPyx4@k zQ8vnSjA;NBJzR5&%&;xONNYY4(4}qKn|tjLrBuo~dfG8; zm*MlUJ+q`Y3mn_9c$C5P?L-n$NiGhOP`OuDp7|sj z=Q5;SfSOT>1xbM7F-M-|eRzDj9xHXeb%W$ANwN?%k^g9@ggZ%BvL(PU@2HSMuF2SI za^+LkXHDcwUZe=zQW)U|zXX@yn2KVZKni|r9(7^=WJnxgi|o*s`9U(y(Ngqe0J_!u z6(FLEpI-fd|MIKx0D2I7q|fu}x@z8YjcmI2nh=DK4I^au(FB zUJ3bA6>G~IiOpfQreNaw4f-;4gzWPQi4_2BdPk8t+Rc04 zOo?h(J^E8B`FSsCb#J!1pY+_4Q184iIB*Dxw>b!aE@+SEJp_HqSOYVK&GN%IP_ z!Kcma*OP;T-)>Ak9kH%Jt3pg;F{^EfJmvEuN2$OBafp_0{BKLMMJGv8GkZn#FSUsM;^LrweB|KK;&7fMAdA^7<@2W5K#i?R@fLfexRi%&CjvEw|R6xFkz z&n3HK@8mV3?(H})O+#gOZ+ZRg=uSFf^|U%&_zNEvfPB_YycjOdDQ?ri@*=zp-xo zlaXU@N{7n1T(36^*^O{*=s5LV5#G5N`ufmyHq2<9(0WL?j5pw5sKlF6oi)(9!W_IL7j(F(P|-kD3$tH$R12bS#{j=v{;um3t9pSyWd#-KTETnBxgGu z@}5?fI;lE?cMnKQgAvR!H}&O)x&;kM1y!6 zgm=vhdn*zDD_W7U39TI0v~ql40W#JnRD_Br4Nu_ZeW|UNq{=grKg3Vs(VexleZ2-9ugFHJ6gdTQhS!J&z7C?gmY%*%gy?8Lb*A1K0Pl(1(1(mo<0Ry zwTSN_av&emJ(lavbb1y7og<64emNKbU+|onLD?GI^guG}@^+W7Wa3lHme|167oMS0 z%RD0)mKu~B#Q9zf$^Vte3)g3o&gagU0f!cHE@NAus&3+j%tuqPTJTHtsF1?$G%@DD z)CBo)NG6KbEdL?z$7@;X*%o`_N~I3`*w}=js5OlJ8f7INc&VvC*mQexnf3cE7A9)> zWi%kxSoN0oNGs%n-->KiHwlin$iu(~L0O%s?%?+=t{DY=l;-8o11{qw=N*Z0rw?zu zoi_L=l@sMQ4Nodhyd6d+Z_pDD^~6JJj$aKQmDzA}xn+35^yE~z#Z>kd=_>jwPu!J% zLTKNzv&B>ap_(6n&W89XrprO^P7}-$I?;xei@J7YxyT6=l!0$KZ&5Cx$+$e~x8(F6 zLoPw)tdKd|Mx>-IDK~3MAt_H#9vH=-DTSmwUQ-H)wqwE2-X&JPQF$J++OVg;0<;r5*poD(6$}9`iqm6)hLCMxW_Xv{V$?%}I zMd{(iCUAd(QiC*d{!AspLAk9mCnk?Ez^HNEz(T!oi-n*CjwCPQ_o;4yD z$VfC!>H?4iA9az8X5C@xj^tqYG$8nU5V;X2)mq94<6;Xw%rfHfX*SG_0gpq&WAI0x zHodV81pYg1)c)vAnK+1OAse2e>kXOWYTaS;V0{@LxvX0a1hc409cII=<;hj=dA6kC zleGw0fSG2XtF?e@s20){Eg(>GKGb-0 zYyuqsRAaZG3Auyb2Syw9WOQfcg!XAN%|k~2Bg0bxWoWXlC?|qc z`*Psw=>b=#23#4=z!dTN^JY*f+cb@sm&0?A!s#0B(1PmRjGyy;nJCQajd%->*uC(+ zlbE19RX)ur<*9L8?$bEU#&oFnDe91Y4GByoK+Hq^v%)r_-T8BOEMJJ;Dr{} z&|iyt7jP&xa+PB-EEjJdK3ywAGSqN7JjIRE7-9p{8z~cj^Ov=0iqyX$8;M&0^~P)j z&otGZ%TgX0fCY|TSjj0G|0z42;D%mPoC>X>Pfc5wy+|b%?9@b>9Ve1g*J1o{S{&EV7aa>dZq9~_&XPmsowVUEDjE@B%h2e6Cr`d&syrE0$2@jVmTlwk*!QQy)AagiI)t51D@$Su zWz80}gefae_3i#3j};*0qLw*ixb@ZMGii56TMm2N2sXrie>C7N;EAdcCd3I6#beZ$v1?ZSbG6Kn3}K{gGW90Va8&w=>LsJ zU|KggmM*#~n&*sWm3($3w>`Pq474ILhFSXuj}eh6?JQ*dn0cm>^W5)2dNriP^vjE^bjXZaF-v@T|Yc;Xc_fVsuaX^B4k}UcWP2^0}9( zhtg(G_Sgn*EO03qqE=s6qMuMNVU(7Y*m zoXIm%_uV+Je`B*o8uTg(OWiYf$zK0&nQc!lgk_C~fS;c~VLoPF{o=Jtv!@kl7v!$} zUnlKiMf2+4ERp6(;(r~UX`vR7*c_BMEYT3}(8t6u&xI^`v#iGQG?IchrIVCFXGKKN zB_u$@L>BeZA=$O<3D#29TXRTSkfj7oQn7Ee!^Jn;RqRR9)VQ3^Of(5NQEgdIP4aVV ziPdWKJSM;E{wihQ(-2mmy$_|qCDtewvcqZE;z4>LG=IJ4QN^=*sATV)xGqYF9M4Pv zPYVbPJwUK$VSC~s&Yz$`TceiSl7<*DGmz*T4`O?=m70UsX$79Z0lHicyDXgpZWhd8 zji8I63|s>QF7(1aovIb*G{x#Sjg6y`_EB2wr^lq5eWec~l*rNarQ@xETwnvieydAj zb~6XxGjq_ORB?Ab8V>a!5u8~l%?QpN<&j_2z#eN4fQX^Vo?@sq7=Xrwel-SYccZQS1WL;-Lh4<}Y?x0P2Ci@@CIQpJx=nLVl zRc(M0dGm%ZuUCHD7*hB#PUwmgSa7VYBi2}d*lriuFpR!R56~%dSpiC=5}kMQG2gvz zQjwYMB#=aF?G7HzNc;fPOc1Y$XReZcgvNSGI-6x6@?DH-!0aI#4elEOB3R?qGm`y6 z=u0gg0=Em<6%y`+>{1PDdveKeQi#mZ#1V6Pya-QlSupgW0V1L^5KY39S*w~+T^xLp z!3Seg(Q5OFivY1-Qn5-o@!m|TOcAZFeh5hdNaV^)!kAJ3afCJCYMR@&eA#OM)f<9Z zp&=M>S&=M0!k& z-8Pc03rI|%JAJAnb5rIiFKESz7Kyx;;^W-J)iL(N#55ik$B}n(66r!2FXm|_ovGF< zp(L<<~ zkC2;?5Z#E${45NE0Z+PKpd_%v0woX-S3pkPFjolg`L%gmGu+xnMnMUGFkqk}7h3$E z@PDc4`owU>Pn&PT!b&6Knkb)}GCFoEXv&<%U~U@wRYi@1!$KiAhA=kg=4w#C%8jLzOWvNeI#@IkBJQ5t(*-nTcb z?Ji#2QZo&BBzbuCdPdNI5@1XZncT1amazw@8STxQ`R978Op=C4^=6prVS8c=6}214 zm-X9NgRdrO5FWxfNoisjy?$V-so>4!9(ES9tRP6lD>jqSBIe$3YNjwz&uA2JMGB8D zHld1nd6$D)xZHX6#5`qRq*2*C1!KE#)N}id>Fh-Ikn@8;WiC+6O|$00dC38I!N^YH zVG*h(DKMV$ZCOHv-!Nf|F1Tfmum(VWQs~ATIM+MS2*hrho9G)33<7Vw2(C`yj;uRY z5m;)fntN56!m*!KqgtBI#Us7Ozkr2{s9bJ!4_96rD}k{8cAg(txhWm>%sr5?)jF|T zUw7mX)EoSR0(ut_y^HtNkjsI+nH(sltd29&PK=~GrZ*uGhjkA>9Fq57 z?hfFYZ}N=f4ZPc){G=yWfpRg&JDSw9!w8l#22lh;f;I?2Y++FY>Fx#QLswll#KYTr z=$h+>wvh{-wzYPdoWd5TIepwUG@-{iG_0BPFRb>do1t; z3k(&JxQa{XImy+!SUTbr+;Nl=ar9z-)oK7wT7FHkizMj(@`<8qRv07)%`{6_Y0Yx# za5=sA5R0AXu4A54yZdTPeM%I}_Gp}KeP;SePMBt_B0d5?Y)^t)%Q>YCS%`Nuxmzpd zNAuP18YD?Au_c- z*url+zsSFlJM{#+c8HSIar$#-=jm#vPzJ00@Xyozoo-s^XYOU)c;opsyeJej|HK&z zYKYkkH~d@-rb^krNSVM_wTarMn8~I%bhb*-jxe3r5JQt5T6l$Vf;C%R`buESztAZq zOh7%nwUD`^Ag5FDQeS&1c&a`%otGX&O_$T zlysv>#juBXb}VZxUvq3ga*fd+yswB}DMKT)KjF4as%6%zHb5Q0o)(QsJJ%!3v!%Wk zvOUPSraCn&h04ZDtk;SbDGXilvX+%1Qz2m>lzYqOPzj9CnwcXMqd9Ua!;NtwWI%wk zvv*}@Xso6@izY7K;h<DQO^t6S(N9p zTj<`Na89B$L*a2F7Dz5VA%X2E^SIG=dNS({wam_0+{gkE516LjUF6yUICk)6BT@*o z@3k|mO2Di4*c#CJWD-|MVNatDGkr4I{rTVM#aU5`=UTz5XMzBYE4t`BVP&E#}%~8rmKgKBC zE@_l4brLDHe3T`l{M}nL%C9<~_%I_v4X#y)LE3Gc7@kVRWqXVqw7@njFv{eQ)B4!*FCl%tKu?n#r!yD@ zQk>b@2TUJK{FsA}n89cOX!ViW(+0;dGU}=lA9JYKVd<~>?bD~x-J$N@Z+_u--u=#p z{`ez*W2X|R5#&JV5jKO^&`vnTx>q03xg3UYSezQjjH{fF;4u<9?NfIH|FqG86KE74 zP6i&cEVt$fu1~MfHp?#VKK9))$Si?&cN_u7+-ie|ZYO+$2fVA>xg2%{e$5kzRVQ_d z1qV_DzL>0~K`$f?JPFdk6EGH!KlV{fR`*>mY2X7Fkyp1TtZ;~M_XKy~U(BH9eh+d1 zjaNC){AKpN`crF#orKkeoN0+R^h46ybr-U$EDq1NCkJcp?zgxbuBf?t1w%Lw;5nNN z7;`j-)gCq1#Q}1|Cbb;cdslXe&)qLC6`5EWHpXQYoN+JnAzorL`eNFH41|uwv~0c$ zOuIq?g7KtG&(yvT>7Qs`)4L(f^sd_zMiYu z1QxX+zxP?XX|f!Ma9wtia}92qS65IdBjv^I@Yt$bZlvt1t?W(4G+tKo{^$pFtZuf` z>EPo6O*^WV!=F}f4)CYz!cXM24*U_IG58H8c-)~viq?h-l_rCumJ0>r@#>S7!cwf8BAAQ&K~tDPBJ7W%PtdL+xe9*3@E&2iR$z*xWA zc2y#tX}JaYu}W6UO)0AD%}pUb9aH_0rEv!35ZSs$Geo!t##U_yKz;?JY3a_-i%Nr? znN!IY!^p9yYepmmP5?__+<9hze!{NlBzXIbhLebmC@OF#@?-7DCYlpd>-f8DE7SDw z$zuCY4comjMWeGKE=7wxadf+)F`1vuvos+HjnesDlSw{BQ~e;^<*Q0(AjJ$jGHU4r z#mltIo6H8!M8(4O;0gsZ8Txhw^NzZPsTVt>X-pqQP^~x(_Lm0lkO~4Dk0tKp{nnJi zY1XeU$3a^Cfwi?Q@+EoXbR^+^|TJoZZMKhtd6sGkH;{Uh@@8sBt`i-c^yQY zv=R|Xb%!BB=6p3%BUTKO`FIeao_@H9sK>GvB2HQd5hp%b;r)H1Z5mi7^Zqx%YuL$Um6FKX|+KKTkStRiXuX!Dz2TgpXWj-UjeqD#_()F3Ffk;+d~u zU}e+Hdnl?1R$Bdy0l4Kcz-pWQm5QYilC{B-qqC|v0#IgXIk2T|XiuBoCU9MuJKW2( zy8Hx;2qDbMV$pS9*Zf%E`q&+qKCZ2c!mM%YA~>*81Zz(W4%MrhU(^3u zQ@Sh+u0CmfP7#K!3U}=F@tGPi(mKL@Oyg9KYnz(db#U{2myJSS^1EtjaC6%(Kb~ed zH|K3lSaHtG=5+vQshG$FN%eaMhO%8xAu`p|4;PvBSR9!#HVZlzqNP&0kPu?8>h9G&oFmCnaa>5PZwa;BaNkGbtP+Yu6p znF16viI}&Uzs&Sob&K8UJla*cFW6nJ5hG4{ZwO9qletn$x#oVW_gWikc+OE7v{jJm zZ>O~oaD(>sR1mMDVUBR;)rYOm>8IAo)3V?vJI?j_R6VpX_vLX7%*LMNB=G+*s=KHVP$p?XdOnbm;mhUe=Oj11k=i0>!T7qx-*c z9n{(>5aXy#tNR=%&ai3Aiba>?uq^PGj+RUey5~`;k zE(z9?$zJItwuE0=f9%*;BZWE;e>Wu#oG!#=WM%ptJ} zSE`t7{7|0YlSxEVKl*%$2FsVyQhnbc?F-i+Bng(sZM6XqcL<%TfBW`&R^o@gEl zEc2aMd8!-23>(W%R)z-4@?40D6}!{o3)i8m1{%D=mUAM{$*Z+`Oo5Mf7KcFamkb-6 zI9Ywl?v5#`(27@(Y(W{j{ltoN-zNhVv==S_9#bxNg;z%!0xA5t%K?Ek*YWp7oRwfK z!|Ad7-4)r7Z!UDl?PxerWb-I^kX%J(v(wgFj^ z;^>!yw!{IiNf}E!Y(|aOQT(W@O6B14D%XsfUKL?R&7`$e_iw-Q`+knR6>{-y)wj7b zy^3FGTwXs+|5_1E|F&{L8)pkmaPvQ)5AM9~5Hm8CiU2tbI2K8( zKd>TMC{o9EO_8knq7~_dB6WPx6v?aKup-l5M0yOi^g!iUbdoixxSCay?#w!)gVYu? zKX+!}$4b@gBOx14lXw!o<_q&b)G*-OW^z+|%W~s&z!8c&q^O6RxVu8pzYb3mXJ{Uv zrJ&O%rJzNhr;_LF%jsf>_~rCvJTscFl7+-vV|~D29wyf;?r+K~(p-sEeQG$d+-Qo@ zSZnM<+9D|vhnqYf1p4AX=a0Hpt|ju-pS!~b73@1g%@@v@YHhT)O0IN@LW9lJrHUPc z)5Fr>7)hwcUm6Hf+5XMjF6%^Lh_!N`vih>(Ap&Cx-JLd;qlBqIee)S3V$+0L;sO`i zOrbrV(GnM1@xQjhL}=H3iMX!)5~*7Ar2zF{-6Vc>5vj#;h4fI}(y7bjb3#Wpt(=N< ziwqL#DM>39I=$X-SWg?bI~{+uMyDNLAF+J?b7iN@?XEWH@lLdW%v6>}IQ}ZAHKuEq z4clgdR}Q-wE;we6b@@^$*$$Q&{C&Nq0=Evf&^N^ap35Sj*cjm zV|^U!A6Ni$Og?t#R_j*caxL{rmjL2ROrF&0l73uWGO_mRlEviDWH9uh7S?H)UiyX} zM3^Sx`jC3f%HvyI5-emn;hzr+e&}-5RFuaJ?vQ0P&O*s(%g7-=ee>AnX@1za49m;d zC%if?_x{%}{N~r*@$LJ5?OS1anRXN#6J^bEpC^PmSJdTTcHFrrst3)|Yc0wlo4D^T zcP^SDY4trT+*$5iG)1!N5pQp~bI}yZt8cekS>{~SVuJBm<_l=bBrKky6zRI8nb6;SM)&ckz;ZX>C>>MoFxtt|)tQR%!gR8G!{;9B; zk&@B*;R~kgtUK5$-IY~#vbt^4H*3S*bXr)5>(ow{3nD*=CG44oZ$431m#_-6+g2^K zsAN3Kgcagpomw}J9)Twp9 zUTC2mPr#GN!xOUtQpe&v@m8n$>98gyTXjv00cPr|Nky@X9M>YcGPfZNZ?iU7FsdH3 z?3C-6AEW&(&dL<0g!6K5ZC=J;ugAw_}Bn8lGsCK9Y0e&92||5NtyFe_Ux`;E;6?tL6WrDW6#6$c2&b_iUlmPhC*#VOwOLXkGPMpUrSfzu6dhsM&-9#dgG9Tnb{@BK2lF zYE>T?{D|Dz7;Nh*S=I-2xv;TSzZn+U`KQ>os2LqxMIEGPwm&267gIRQ($(n@H;B7e@Eanf+hTIU=2h?kI_#R7@+R2d9 z+N;I!zKwZ4ibuSfdEQ6Rz*^qU~vIuxw z6{>*LRn2PN5QwSUh}yCg<1v!Fy2rj(UKcof7rQJ?CeHBh;vp70q`2Z4nI3<`wL*BT z#&^E+eKdBT?XKhKQ3dJY!+;VmeeV^+0e>%MK&kzs+T`atw;88Kr!sul%{HT952dl| z-=@Lq55U{O0}@t$uaO_B74^He6dU;*P3u#Scdk6y4#*-0g)c3I13Gl@U41@BeWr^x zQid}+&=k059hXfW$Aijz-*9{fd#Ry#+K$fS@F(?->+@~LH3ZWpvvZp**JAtlPu=eE z-OB9Ml~>6BwTIa9A-t@HVMDgQQ==ESh4Q;s{|7N~%}0~*Y_rL*QQLMthmG1yY4&ZS zcBjfYIm==zdyi7&M2U{hZfo{MbCgAEpRpfe!9G!)Z)TsTi5=QOWYp#;C>?7t!wDVM zlI?vR+nik={Gj3Xeg2R3#?CYA@VEuUjri8B&Dk8Zf!Ap>ij%tDln$q}J-Xv05_W29 zZ}l`y`}ZXHX1dN@acwiD@5*GUPK24PZnxRtZu3!@l*0l*NR^?#CSxAeU!Ps>d~YfUQ&bOM{qk{?1Yw-0^x)fJ}<5GGh)KVH%H;|GXp>9zHESx^c|$HZ_4#Xlp3HMe zRybUCh3H0X4)o{)qI_=AaCH z22#M?YCfeMd8h63H`#ak?ChB<1R>{G zH+}h;h>u;-k2G+W52pq&2q$7-?o7&1X2i)|TM7izfZzlisaq?C;M9le9SY)M=@%W` zOYy@Fvwij{t8R#Uf}j$Klq+C{NuA!nu?HeF+x6HUS@W}hAtY0Xa;Y86pbaZ^sVH7v zsyoy7^9(dHCwTmRh>tCvuw#cqO=`d*bz}ih@WUfHv}OU?IIZpYJ{c*uWqeQ?j+Rx! zP=&+mAOL)=XP8V_n>(>~w1CoK&fi6ITw+5=#q#mBpSNxd<==g0=!OEM$&PGv#ij$^ zCWo_%uGT249>rn?7imUrCk2?ZC|gIlHNsjOCVVa6B>wsrfZU)%FI0D7%m^%)6PBu2 zJbFZ5{m`Kr8~@i*oL`=OyBu9#1(IVZ{o(!d~B%fD4Xqx zpH2ez`ROD`ygp&TF-a8I6pdjpd|f|1t81V4(2?X=Lo=zY#^EHb-f)Au7Lb#*<%XW2 zS09lMBw;X=L&@5J=vhaT=n+NC;?*Df*q|MMPR7Z>f3mf_E|liF@U0e3L%1;=9;ddI z9g>nMkF&Fvy5+oGk1vnAolT7Mx3d~XjVSJ>MrkCU_2DNoRu@*VN$JQsMBLDHq1SQ3}8xac4*4ww+K zCotw*l-~=d1{oA=>hx~Ab`XV0s{shh)&@YYEU6Wc=nM<2mdBRuGc`R8N?k|e-~qKK z;rwg49AhP|-8PZhpj`qkh(IMB&ww4Phf!;4Q-{Yi+FGBsgMUpAx;FI_+kqZw;++oY z<44=D=rP)M$e3`9qU@>c2+7?s-D1ZhA;{!0hpd)xi8Vdr z5^IXaCDt@ertPd(SkfGMst=nTKlDYeNjYPFBUmMnvXem6`rn`gNZs1;{oWT0AhB*~ z2Pt;QZ`%oj=Fh046aBB2^0zp(v9C`8B`}^DR~8_QAq9h_SF*5T5B-_dpC0=_qm2h< z2UO-f*l8OPzojoVBHfzRxI#PdO;}-@KA+N%Y)Kokc#W~y8VDQeP5MnVeq)Lz|Mi;* zh^vye;F<9GP^cKr`WP8pmf}HjV%U^v5zaPbpESCi>NFuNOnLzFbL=Askd$mGU}l>yAN2S7HFk0#n46Kv}*< zODa!G`HGh4T+D=$X*ksh!?Zx_Y{)hj#(QKlKcG*V+o|gWAha`^v4d6-M^Vn%X|X-A zs)FDevN}$<&<}*ffK(5-NXpfR!6coAJWk9+4eFc0c+@JVc5p2tK0;H~T@b?9@mX2& zW9GeBJ4y>=XCb%H;YD`b{y8%nnfu7&Snc~yY7{cw71M$>)ifYeuz zF0|a_mJ7{Uin1K6C)F{JUR^RYUj3j0hxR67CQ>7Cn7UmbG?d%pEgDb)=&I4I?38eT z2Cf;`g;>@%sD%3TGM&vitKcuIzDCcsd?3>?s23vw@TpGbdXaqXhMm33nw?)O+rN;# znnlW7;Kvpmg|V>5fI{C!*5bZdGrW!tJ?h3srG06ILlPp^gF(I!^iD$}xh9MGzTx~{ zY^uTtQ483a5JAfe7oac-Nc?J2#}2`l1G5|YVhbjyZy9d6uR0;xJ=0df{Xo2e_%*o`lFq`9l9>SE+HRF+k`|AU#vx;-3r=Z zmBs8V&um%DWpsy=&EuCBtF|l>#5~N7aiN`qk23Y|2bwhK+a5HjAYvaE&is9m6f@>) zF@ta9@xTXJ_5GNdB8;?$uwN_mmfuPOZb@&muTOA4|udPwi zVrY-zR*_36?TBMY&i0xdq!srIU+t02L7njia@&3koUlDf%wC!H(}>%NDJUNlyHG2abq zK`JF(Ef2{=k{Ahovaac{m6iPNvwNGc`WgT4-~rS}&>hO#_Wx1bcli$^&dr7*+|}91 zhb=IsK-)hMX7o`qKhH2GA2qt|$#+t-U}HxV;||e4bQm72o2;WS`}Q(ARB!A{KJ~|Y zqVhKdK=VgKw~Ys(<2?&wobAb1Q=A48{rXlm7$U!p8zGDw^;?ok)x&-}gGHN_&Efw{ zdg@pAK+nN z_iJGK3L*D@%rI2h`w#OohoC;qm1Z-ZY#$uA}{n}SP`3txI%KaaHgac!hC!LdT zl5>&EAOEfBY;x7kq{de540)bhy))bwIs+ET-z2cs^5^uOpli8z;(SbAQBEB2V}3dA zs89qqn9a3AQT*WLXAKGcCYcF_?O0DS?LC>fCYVJl>l06A9id21 z+eiFGL~(FW#6h3S&P?6{FX?#8&t}g_e%kIA^Se3D`9`~E-vVWBuzL}fa65A?>fCTw z+dl!SiEvuA$&Xna!9VU{+@;Sb+IO2x%;2dy%lB|-!Ioz5t-Ro*tZ3p9nlPaFMl1l4 zt->hdoK>n5vE|9TjWaW{gtJv;JOywoAiebG!!^p#?=6HHL5n)!!TmT^$aO*)w z9;?}&;d`p%UhHPAorLNOq8`*_$RQERa8XP$t16f+^5K7tBS+kDPA8*O$@-fZ2A z(B)i9gKer_yENNL^8^*9SnnMcDuwwN(_fn`k;I&O^~fK5`~zL0VRY9f;jwG#3{&c7P(25QW$-i>K3_S4vDz;l3JYb~d5*U37Z z6Q8#9Xp7gWbzo%l!P240l5%b~N0GVOB_+rcx1bR%b#&zduG9eTFL>V)dMJ-?D!tpZ zUD0T}<&{pf_>d;blXhnJOE>WejW6kn^K`GSeE05dUAJ&VWoEMy-d}iZv}Q$>il9L# zV&9CC&415wRST1tc>~z3fk`Y!15k5f5rQAJagW*p4C@v!tZh=+w5>eZV{~uncIVDo zxdcA6m`(URy1qJmAZS#I!Q-_?JsNB=aSLE9W)Gk_@dVwYe#lHm?b8D*)&dR4n9P<$ zWVoK(%2WddoDm?Hf92kF_=GFPHq6lH7+{7`y{|}h*S2Q0d?N7H%U?!gmzoU=PTOv%ILG8g$O65fw#zrM#a>P2i*_15FBPzDQN6eeU%n0 zrD;(3P$%neR-g3D$@)4p;n~mU5}QJwE|puBpR7MnD~tJNKUx2{a&zqPwWBTNah$BL z34+biO(q%m7V<|n|M|z^3;s0zd;o3=7 z&G11v&v{z*VKIM@GGO=k5KEfS)S_22NZ*YBe%K<`L6W0gLICvrC^=r=Ikk>O;Z&>TW|Q7 zPQkD#^pA8HZTSA8(Pr*Uki`2ri!VRtw+b*n3#lPOD)cdm7XFT*Q;p;ZYEwOB_LFlQHYIEo2#*dl%R7CVbezz_`EUT!pF0wPG*fV=GiN!H#Wwr+?NBjp^#(vYE_JB?m0Qf4NPK{ zmM=#%daAI8?3*u#>~B>g7LA&rPRgz1@8o#%d@G)#FnDU!ku%b*?lQaI`GmjeV_3?ieh_mF(19Jt#CraXPzfLd4d+; zSg;fs^@Kb2PF^!&mYjJJpFCG+&+#slE4#xJr1!e8miN6hI-%_TIQDpDOR$jw9S#MW z*EfdE{XQ@}1r|No?6MLtIOW!^(R#?1{7kdxMsi%yqat~$vY0@OWD^(>xfy?AR<0Jxgz8@ zt~MKSay#Ugjj#aQ-n)%3wu@|@X&$-FLwEXs!Q}Sj!$2a>SOPN_qyCdyQ9oyvDC(zB z6ZM~Liw3IqH_iG$-ya6*ez}`^b@EGNsY|Qf0JqXun48ry79sG5Pv_m|n@6sK8$sZY zn9U=ltgx1`2nTM~t1l6GV`L>_tPm0LDusfvST*RLH`OC!v2~fTh=I15ixZ^vSm|vk zU^5mG7B?2mYE8*@_bj56aft706ZY z3MYM1sqIeJF(_uh>X{kvE#<+HZk>N^qK!2&h14%7{?ssB{MIwAp=6ur?FU= zV~UPBJ=1-xu)9HnK*N$!1#esSE?j>*5!Du3>b1GtayvoF*5MFjwN;xYQ$Q*rLORao zz)C*u2U6kXz8MUMFmy)o%Y?Cf&FXT-J<$mGpeOcjHa+3GmK|2t2vNN@Rh$c( zE?dhzf0$j`$DuqT(!?(@QilsBd;>xUFO^k2AiM+9nZJ^7MG0Djxb& zF%hPwcL~AzT!_F}-l>#X!tg{yte!fjnWes$~k9uK)yh%G% zqFM1&s)^6W$g*rcVtjgBO2Xn)wC2#9f`x3G*4;545;&}h8C>_eLo8enTJ(hFA^xOf zNPJJF8Iomfw332lmzsAz4tF@^R_g?&+;;-Qr}Hfx5RC9`+r>gkKD?>d8btWMvdIuB zBHJq<8gU0g_2uJ|WtduRAT6|z&9jKcA8?@){n7#g0a|~I>2+yZPa+R4AN3sJXj0*o z-ZcU?Ck?*!dInkc7P4u22!EKR`L%cs#D;P5INRgv19N?r+7c`*atec$QI~B1J>j#|8O3di zzoX(Nd7vrc21Zcq0476mg&5(>Z0rT*JFzU~guxU*>BDW*JaS0*W)gp1HUK}u_L zJPY=mchfv(w>(XN<#_<>J+dF3CqvkSYVOC$~kF}m_6G=gA4XKj(nSnH(s5(fU% zze%{Juz&qqEbc$%nonN~HIecfcesD5KDN1;WtKqkN5A}s|I>LDONlyijh#FuJ}4J0 zytqP0K;@W&^N;qi&9CKo-!_g(2E#%$+ZXEoyiwj)9Y+r6T&|-1QARw;Frh!Xckr2p z1GG)7WDg^MHFc^35=azq)j7QIk2s-YkG1+?f~YtzIp;uwtB zN|6~>L-Wke*bslrsP}1c!VFbdhoU=Q-9G`B;6P|IuWW-Dhq*E>l4f5rm=}zG2eVB` zOL4ip+I%H|4YeA7CKuJRa%PFi=dDtxzM@(U^Fu9%#0+keJrT>qOl$ z1NCKk9vEQK)5eKeJ2)yBJOAhNv5-R~u{-1KY&mbcIh1MG3@s5*$U4 z&^fS%56QA%9K!-6S~)Ht!lrZ)R7yamCTA!jKvJQ-54~Cw?zUf6h|bRsg+5ir{E=ZqNIYaa!R2?r8xl!8e7F0;cO zkQUePMzunJJT6B)EU{J=Xvvar#n*{(y`!CG35<-%JR-^ETJmW8mus2!1Uj~>of32e zv*&|btSgKbAAFV`(2je`4)lSi$Tx= zEKD6d8MP%Qs?{owVYIFO=~TR$vGzJz$GFXSoB0OuUGxipwz^(YH2B8oW=&0gD zymm_eH|2p_yH9;Q9`>!C44}rV74wB{DoerB}f{(n0j-E z0HwGlSOo3HXjG$?&W2@}TulMenW^o;-BJR^*uCw^Ynh9*W$;o^C|%G_U2Xt(WKUj+6zpUzpx$dBU?mjFCeVHu>E4AX|KWIa8Hjfo~{EcbVQsSfBuIL zV6F0lRzpktpzqKhKY-AoDtn9hg#rZ4TlSuj{QKu5+qvTb2NG$&&*>#bC=Qymoc9A! ziH8TD7Y<6se>y)c@oIY0*p)lJ@>;9TY!3js>tL@_?Zc%mZZdEV(|ivlVn}KJxR$}f z)wS+TfqIxiSawfg98Icas)XX$<2+5Yw|LP!L7^*7b3~-mQwCJ)bXT-b9c>(;GnP)M zac;Xp!^+@DA-ppPB`wdKq;uoc!+9eI54Vjp5Gz~WSZGLV7CON+8>UyZ(D{W zOe+AC)gz+e=VrIl;N9@8PoLJv?tyInhv#DzxCPD5$GpIOqXo6ZtslX_PhJoWg)X>l zzv$c98CJtM*?II7X-IFUVPj$N++BI;4EeI>q+7*dS4hIHuC8HwewJW#++xcif{7M6 zghDDs42uKL7({=D9}p$2e;``9%aoWSG>=R%i|tMV zqGavZv)GRPi0wewY>#kUxD$b*YU7r<*+B769-kIb^F562=?A^U=1gfePO8&+Xw33z zj~<_R`$E(@PF>WXZv0;>rVjz_dw=eThwfN*ng9!xPGdIHDFbqvw4Fra*};SB;6k}PC|83Zj+Px8xML;xOTKIV zCc}zj$WOT-uW}}%0B~hMIJHxLg#$j(23*s_CfZAWrhP}74SW>THD`pJh@IoBSMeNQ z$4p(940N)X+@WK$bdIkSfX)~HxE{z!v#NEgg2(#2ys~o-^)?=MzpUZ-p=c|DCJ(fi z%`T^!v$Oy;lD35+SJ@93QKRypB$1bvsp5zc;c0I49A%(*!N|3(kUK#!0&z95SKv&A z4Iav%7~?rw&WWAs5jo|ESv<58mU>V|$G8}_b7)b7B@+@~7WLSDet*Ns?9HjZZ*Aa} zV#CzQqcYCsJ@H@?GnT2DKC4&2v1CO$FFAanBmiYXBAON^v&CIcMz$Qx}J`k-kdwR9BU19wR5)f|3f+Da$m>_bEt_(9K1aCa39QajN`a%!7m zsRy%fSAJB)JwnTt_96+Uj+k|QeYQn36*Y z2dkOZCgmX=+HF8_q%4?pguRMJbauze(FX4G-8!A?>i=v`$n%k%;YS)Mr&13KXU?*Y zsBHP&On<`00K`;7@TWC_I$a{D(o@9SK6@m8i~$jc$Q=U(I2F=g!LxxT;b^#61Q^GP z2pbRA!O{=1v>#n(61g#Hpx_>pWZ5>xKuTrv$g%*8;rZ?^gGW29mUkRUCWTm%oI;9; zKUseOAO-#%syw>N1W=l$Ap2s65ayPw%3B$@E*i9B>HAFdo(LJZRFiy`fs-LHElYw* zNf}N3pg9;2X$tM>5KNjxaBpw^ilxv5rn{sWI6!{@D5Qk=G4Elo%xx<#q^6|rl-_B3 zx*0lV8*FkYQFe#X9JV~oXAg^W{K+HA;KUpct=1W!I=6GgN_^hyd?e3`W@;A_Kt z+HIh_%uVkETdqJOG$@#aOb*KK!VdJ+se0{m1B1e(ICUY1VDitT#q8j+FchSUui-}qvbCJhAPC9wtR@Qy& zk!BeX(5^^lNjW2(Jw<7@N!M+Wu4s!JD=GeoiCg#tMyGb2T_@C{>aOz_K79$+wl`GjXd`@@0?5RmJPsa}cUfh{%A(f)WnMG0qPh9Hjr6RZWnyD%n zrxRp8%ohEU%@)~2RWS%ZlsDD%0JGKj^;_`{xNIH@^V~3J%|J)_T=jD3A%42J<|Hd7hToP0BoT zqEE0E>sIx&SfRm{Oiq-c2B7$e zbU-n)rv-h6X)zYwYJN=9Vi_P$+iBH;NYi45!tj`((oC2ZqfbdrnD+~_fSNlr=R;mc zJuS9MC7e1{3DaU~BsRyin9lyL-z?N`j^MsCli$3G(_);W&$O73ZCT|$Rz&L$!m!Zv z;uz5C(_(aO^I56%xx7HuvO2Ei1I1}E%WJckvduar?8w3@&+#HUVrvFL3+P-1T%WAk z@{M>brgm4?85Z8Mq##T{erH?so5XW5CkU`>%!-MQ>(7d*?%1r@Q@1JsDbNyyn#

k>-QUQ-^w>vmoWI!t-kDg#_Yx2CNn2_{e&^Y)Yn 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ν;Yr)`^ssx0Wzjpa1QbYy1n}jC$?OM0t%`An| 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≤p@ekjV_$L%2 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 literal 0 HcmV?d00001 diff --git a/integration_test/evm_module/scripts/evm_interoperability_tests.sh b/integration_test/evm_module/scripts/evm_interoperability_tests.sh index 37aa948cf7..707991d57a 100755 --- a/integration_test/evm_module/scripts/evm_interoperability_tests.sh +++ b/integration_test/evm_module/scripts/evm_interoperability_tests.sh @@ -3,3 +3,4 @@ npm ci npx hardhat test --network seilocal test/CW20toERC20PointerTest.js npx hardhat test --network seilocal test/ERC20toCW20PointerTest.js npx hardhat test --network seilocal test/CW721toERC721PointerTest.js +npx hardhat test --network seilocal test/ERC721toCW721PointerTest.js From 58550c475de0d0b6727b99eff244cc915622a6f1 Mon Sep 17 00:00:00 2001 From: Philip Su Date: Wed, 1 May 2024 10:16:18 -0700 Subject: [PATCH 27/31] Allow big ints to be passed into erc20-send (#1610) * Allow big ints to be passed into erc20-send * lint --- x/evm/client/cli/tx.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/x/evm/client/cli/tx.go b/x/evm/client/cli/tx.go index 93e52710de..6cc9d3ce3d 100644 --- a/x/evm/client/cli/tx.go +++ b/x/evm/client/cli/tx.go @@ -11,7 +11,6 @@ import ( "math/big" "net/http" "os" - "strconv" "strings" "github.com/cosmos/cosmos-sdk/crypto/hd" @@ -374,15 +373,15 @@ func CmdERC20Send() *cobra.Command { RunE: func(cmd *cobra.Command, args []string) (err error) { contract := common.HexToAddress(args[0]) recipient := common.HexToAddress(args[1]) - amt, err := strconv.ParseUint(args[2], 10, 64) - if err != nil { - return err + amt, ok := new(big.Int).SetString(args[2], 10) + if !ok { + return fmt.Errorf("unable to parse amount: %s", args[2]) } abi, err := native.NativeMetaData.GetAbi() if err != nil { return err } - payload, err := abi.Pack("transfer", recipient, new(big.Int).SetUint64(amt)) + payload, err := abi.Pack("transfer", recipient, amt) if err != nil { return err } From 717925e06ef052cc85fbc315b7e7784166b95a20 Mon Sep 17 00:00:00 2001 From: Kartik Bhat Date: Wed, 1 May 2024 15:14:07 -0400 Subject: [PATCH 28/31] Add SeiV2 config migration (#1612) --- docs/migration/seiv2_config_migration.md | 1080 ++++++++++++++++++++++ 1 file changed, 1080 insertions(+) create mode 100644 docs/migration/seiv2_config_migration.md diff --git a/docs/migration/seiv2_config_migration.md b/docs/migration/seiv2_config_migration.md new file mode 100644 index 0000000000..a8c8ea73c1 --- /dev/null +++ b/docs/migration/seiv2_config_migration.md @@ -0,0 +1,1080 @@ +# Sei V2 Config Migration Guide + +## Intro +SeiV2 introduces a parallelized EVM along with a host of other features to the Sei blockchain +This doc is meant to help node operators with migrating their configs when running on SeiV2. + + +## Overview +We will focus particularly on the `config.toml` and `app.toml`. +For the `config.toml`, there are only minimal changes required adding in some params to the +mempool config. For the `app.toml`, we will make sure to enable seidb and add in a whole section for evm. + +# Config.toml + +## Step 1: Add Extra Mempool Configurations + +Add the following params to the `Mempool Configuration Option` section + +```bash +pending-size = 5000 + +max-pending-txs-bytes = 1073741824 + +pending-ttl-duration = "0s" + +pending-ttl-num-blocks = 0 +``` + + +## Full Example SeiV2 config.toml + +```bash +# This is a TOML config file. +# For more information, see https://github.com/toml-lang/toml + +# NOTE: Any path below can be absolute (e.g. "/var/myawesomeapp/data") or +# relative to the home directory (e.g. "data"). The home directory is +# "$HOME/.tendermint" by default, but could be changed via $TMHOME env variable +# or --home cmd flag. + +####################################################################### +### Main Base Config Options ### +####################################################################### + +# TCP or UNIX socket address of the ABCI application, +# or the name of an ABCI application compiled in with the Tendermint binary +proxy-app = "tcp://127.0.0.1:26658" + +# A custom human readable name for this node +moniker = "demo" + +# Mode of Node: full | validator | seed +# * validator node +# - all reactors +# - with priv_validator_key.json, priv_validator_state.json +# * full node +# - all reactors +# - No priv_validator_key.json, priv_validator_state.json +# * seed node +# - only P2P, PEX Reactor +# - No priv_validator_key.json, priv_validator_state.json +mode = "validator" + +# Database backend: goleveldb | cleveldb | boltdb | rocksdb | badgerdb +# * goleveldb (github.com/syndtr/goleveldb - most popular implementation) +# - pure go +# - stable +# * cleveldb (uses levigo wrapper) +# - fast +# - requires gcc +# - use cleveldb build tag (go build -tags cleveldb) +# * boltdb (uses etcd's fork of bolt - github.com/etcd-io/bbolt) +# - EXPERIMENTAL +# - may be faster is some use-cases (random reads - indexer) +# - use boltdb build tag (go build -tags boltdb) +# * rocksdb (uses github.com/tecbot/gorocksdb) +# - EXPERIMENTAL +# - requires gcc +# - use rocksdb build tag (go build -tags rocksdb) +# * badgerdb (uses github.com/dgraph-io/badger) +# - EXPERIMENTAL +# - use badgerdb build tag (go build -tags badgerdb) +db-backend = "goleveldb" + +# Database directory +db-dir = "data" + +# Output level for logging, including package level options +log-level = "info" + +# Output format: 'plain' (colored text) or 'json' +log-format = "plain" + +##### additional base config options ##### + +# Path to the JSON file containing the initial validator set and other meta data +genesis-file = "config/genesis.json" + +# Path to the JSON file containing the private key to use for node authentication in the p2p protocol +node-key-file = "config/node_key.json" + +# Mechanism to connect to the ABCI application: socket | grpc +abci = "socket" + +# If true, query the ABCI app on connecting to a new peer +# so the app can decide if we should keep the connection or not +filter-peers = false + + +####################################################### +### Priv Validator Configuration ### +####################################################### +[priv-validator] + +# Path to the JSON file containing the private key to use as a validator in the consensus protocol +key-file = "config/priv_validator_key.json" + +# Path to the JSON file containing the last sign state of a validator +state-file = "data/priv_validator_state.json" + +# TCP or UNIX socket address for Tendermint to listen on for +# connections from an external PrivValidator process +# when the listenAddr is prefixed with grpc instead of tcp it will use the gRPC Client +laddr = "" + +# Path to the client certificate generated while creating needed files for secure connection. +# If a remote validator address is provided but no certificate, the connection will be insecure +client-certificate-file = "" + +# Client key generated while creating certificates for secure connection +client-key-file = "" + +# Path to the Root Certificate Authority used to sign both client and server certificates +root-ca-file = "" + + +####################################################################### +### Advanced Configuration Options ### +####################################################################### + +####################################################### +### RPC Server Configuration Options ### +####################################################### +[rpc] + +# TCP or UNIX socket address for the RPC server to listen on +laddr = "tcp://127.0.0.1:26657" + +# A list of origins a cross-domain request can be executed from +# Default value '[]' disables cors support +# Use '["*"]' to allow any origin +cors-allowed-origins = [] + +# A list of methods the client is allowed to use with cross-domain requests +cors-allowed-methods = ["HEAD", "GET", "POST", ] + +# A list of non simple headers the client is allowed to use with cross-domain requests +cors-allowed-headers = ["Origin", "Accept", "Content-Type", "X-Requested-With", "X-Server-Time", ] + +# Activate unsafe RPC commands like /dial-seeds and /unsafe-flush-mempool +unsafe = false + +# Maximum number of simultaneous connections (including WebSocket). +# If you want to accept a larger number than the default, make sure +# you increase your OS limits. +# 0 - unlimited. +# Should be < {ulimit -Sn} - {MaxNumInboundPeers} - {MaxNumOutboundPeers} - {N of wal, db and other open files} +# 1024 - 40 - 10 - 50 = 924 = ~900 +max-open-connections = 900 + +# Maximum number of unique clientIDs that can /subscribe +# If you're using /broadcast_tx_commit, set to the estimated maximum number +# of broadcast_tx_commit calls per block. +max-subscription-clients = 100 + +# Maximum number of unique queries a given client can /subscribe to +# If you're using a Local RPC client and /broadcast_tx_commit, set this +# to the estimated maximum number of broadcast_tx_commit calls per block. +max-subscriptions-per-client = 5 + +# If true, disable the websocket interface to the RPC service. This has +# the effect of disabling the /subscribe, /unsubscribe, and /unsubscribe_all +# methods for event subscription. +# +# EXPERIMENTAL: This setting will be removed in Tendermint v0.37. +experimental-disable-websocket = false + +# The time window size for the event log. All events up to this long before +# the latest (up to EventLogMaxItems) will be available for subscribers to +# fetch via the /events method. If 0 (the default) the event log and the +# /events RPC method are disabled. +event-log-window-size = "30s" + +# The maxiumum number of events that may be retained by the event log. If +# this value is 0, no upper limit is set. Otherwise, items in excess of +# this number will be discarded from the event log. +# +# Warning: This setting is a safety valve. Setting it too low may cause +# subscribers to miss events. Try to choose a value higher than the +# maximum worst-case expected event load within the chosen window size in +# ordinary operation. +# +# For example, if the window size is 10 minutes and the node typically +# averages 1000 events per ten minutes, but with occasional known spikes of +# up to 2000, choose a value > 2000. +event-log-max-items = 0 + +# How long to wait for a tx to be committed during /broadcast_tx_commit. +# WARNING: Using a value larger than 10s will result in increasing the +# global HTTP write timeout, which applies to all connections and endpoints. +# See https://github.com/tendermint/tendermint/issues/3435 +timeout-broadcast-tx-commit = "10s" + +# Maximum size of request body, in bytes +max-body-bytes = 1000000 + +# Maximum size of request header, in bytes +max-header-bytes = 1048576 + +# The path to a file containing certificate that is used to create the HTTPS server. +# Might be either absolute path or path related to Tendermint's config directory. +# If the certificate is signed by a certificate authority, +# the certFile should be the concatenation of the server's certificate, any intermediates, +# and the CA's certificate. +# NOTE: both tls-cert-file and tls-key-file must be present for Tendermint to create HTTPS server. +# Otherwise, HTTP server is run. +tls-cert-file = "" + +# The path to a file containing matching private key that is used to create the HTTPS server. +# Might be either absolute path or path related to Tendermint's config directory. +# NOTE: both tls-cert-file and tls-key-file must be present for Tendermint to create HTTPS server. +# Otherwise, HTTP server is run. +tls-key-file = "" + +# pprof listen address (https://golang.org/pkg/net/http/pprof) +pprof-laddr = "localhost:6060" + +####################################################### +### P2P Configuration Options ### +####################################################### +[p2p] + +# Select the p2p internal queue +queue-type = "simple-priority" + +# Address to listen for incoming connections +laddr = "tcp://0.0.0.0:26656" + +# Address to advertise to peers for them to dial +# If empty, will use the same port as the laddr, +# and will introspect on the listener or use UPnP +# to figure out the address. ip and port are required +# example: 159.89.10.97:26656 +external-address = "" + +# Comma separated list of peers to be added to the peer store +# on startup. Either BootstrapPeers or PersistentPeers are +# needed for peer discovery +bootstrap-peers = "" + +# Comma separated list of nodes to keep persistent connections to +persistent-peers = "" + +# UPNP port forwarding +upnp = false + +# Maximum number of connections (inbound and outbound). +max-connections = 200 + +# Rate limits the number of incoming connection attempts per IP address. +max-incoming-connection-attempts = 100 + +# Set true to enable the peer-exchange reactor +pex = true + +# Comma separated list of peer IDs to keep private (will not be gossiped to other peers) +# Warning: IPs will be exposed at /net_info, for more information https://github.com/tendermint/tendermint/issues/3055 +private-peer-ids = "" + +# Toggle to disable guard against peers connecting from the same ip. +allow-duplicate-ip = false + +# Peer connection configuration. +handshake-timeout = "20s" +dial-timeout = "3s" + +# Time to wait before flushing messages out on the connection +# TODO: Remove once MConnConnection is removed. +flush-throttle-timeout = "10ms" + +# Maximum size of a message packet payload, in bytes +# TODO: Remove once MConnConnection is removed. +max-packet-msg-payload-size = 1000000 + +# Rate at which packets can be sent, in bytes/second +# TODO: Remove once MConnConnection is removed. +send-rate = 20480000 + +# Rate at which packets can be received, in bytes/second +# TODO: Remove once MConnConnection is removed. +recv-rate = 20480000 + +# List of node IDs, to which a connection will be (re)established ignoring any existing limits +unconditional-peer-ids = "" + + +####################################################### +### Mempool Configuration Option ### +####################################################### +[mempool] + +# recheck has been moved from a config option to a global +# consensus param in v0.36 +# See https://github.com/tendermint/tendermint/issues/8244 for more information. + +# Set true to broadcast transactions in the mempool to other nodes +broadcast = true + +# Maximum number of transactions in the mempool +size = 1000 + +# Limit the total size of all txs in the mempool. +# This only accounts for raw transactions (e.g. given 1MB transactions and +# max-txs-bytes=5MB, mempool will only accept 5 transactions). +max-txs-bytes = 10737418240 + +# Size of the cache (used to filter transactions we saw earlier) in transactions +cache-size = 10000 + +# Do not remove invalid transactions from the cache (default: false) +# Set to true if it's not possible for any invalid transaction to become valid +# again in the future. +keep-invalid-txs-in-cache = false + +# Maximum size of a single transaction. +# NOTE: the max size of a tx transmitted over the network is {max-tx-bytes}. +max-tx-bytes = 2048576 + +# Maximum size of a batch of transactions to send to a peer +# Including space needed by encoding (one varint per transaction). +# XXX: Unused due to https://github.com/tendermint/tendermint/issues/5796 +max-batch-bytes = 0 + +# ttl-duration, if non-zero, defines the maximum amount of time a transaction +# can exist for in the mempool. +# +# Note, if ttl-num-blocks is also defined, a transaction will be removed if it +# has existed in the mempool at least ttl-num-blocks number of blocks or if it's +# insertion time into the mempool is beyond ttl-duration. +ttl-duration = "30s" + +# ttl-num-blocks, if non-zero, defines the maximum number of blocks a transaction +# can exist for in the mempool. +# +# Note, if ttl-duration is also defined, a transaction will be removed if it +# has existed in the mempool at least ttl-num-blocks number of blocks or if +# it's insertion time into the mempool is beyond ttl-duration. +ttl-num-blocks = 100 + +tx-notify-threshold = 0 + +check-tx-error-blacklist-enabled = false + +check-tx-error-threshold = 0 + +pending-size = 5000 + +max-pending-txs-bytes = 1073741824 + +pending-ttl-duration = "0s" + +pending-ttl-num-blocks = 0 + +####################################################### +### State Sync Configuration Options ### +####################################################### +[statesync] +# State sync rapidly bootstraps a new node by discovering, fetching, and restoring a state machine +# snapshot from peers instead of fetching and replaying historical blocks. Requires some peers in +# the network to take and serve state machine snapshots. State sync is not attempted if the node +# has any local state (LastBlockHeight > 0). The node will have a truncated block history, +# starting from the height of the snapshot. +enable = false + +# State sync uses light client verification to verify state. This can be done either through the +# P2P layer or RPC layer. Set this to true to use the P2P layer. If false (default), RPC layer +# will be used. +use-p2p = false + +# If using RPC, at least two addresses need to be provided. They should be compatible with net.Dial, +# for example: "host.example.com:2125" +rpc-servers = "" + +# The hash and height of a trusted block. Must be within the trust-period. +trust-height = 0 +trust-hash = "" + +# The trust period should be set so that Tendermint can detect and gossip misbehavior before +# it is considered expired. For chains based on the Cosmos SDK, one day less than the unbonding +# period should suffice. +trust-period = "168h0m0s" + +# Backfill sequentially fetches after state sync completes, verifies and stores light blocks in reverse order. +# backfill-blocks means it will keep reverse fetching up to backfill-blocks number of blocks behind state sync position +# backfill-duration means it will keep fetching up to backfill-duration old time +# The actual backfill process will take at backfill-blocks as priority: +# - If backfill-blocks is set, use backfill-blocks to backfill +# - If backfill-blocks is not set to be greater than 0, use backfill-duration to backfill +backfill-blocks = "0" +backfill-duration = "0s" + +# Time to spend discovering snapshots before initiating a restore. +discovery-time = "15s" + +# Temporary directory for state sync snapshot chunks, defaults to os.TempDir(). +# The synchronizer will create a new, randomly named directory within this directory +# and remove it when the sync is complete. +temp-dir = "" + +# The timeout duration before re-requesting a chunk, possibly from a different +# peer (default: 15 seconds). +chunk-request-timeout = "15s" + +# The number of concurrent chunk and block fetchers to run (default: 4). +fetchers = "4" + +verify-light-block-timeout = "1m0s" + +blacklist-ttl = "5m0s" + +####################################################### +### Consensus Configuration Options ### +####################################################### +[consensus] + +wal-file = "data/cs.wal/wal" + +# How many blocks to look back to check existence of the node's consensus votes before joining consensus +# When non-zero, the node will panic upon restart +# if the same consensus key was used to sign {double-sign-check-height} last blocks. +# So, validators should stop the state machine, wait for some blocks, and then restart the state machine to avoid panic. +double-sign-check-height = 0 + +# EmptyBlocks mode and possible interval between empty blocks +create-empty-blocks = true +create-empty-blocks-interval = "0s" + +# Only gossip hashes, not the actual data +gossip-tx-key-only = "true" + +# Reactor sleep duration parameters +peer-gossip-sleep-duration = "100ms" +peer-query-maj23-sleep-duration = "2s" + +### Unsafe Timeout Overrides ### + +# These fields provide temporary overrides for the Timeout consensus parameters. +# Use of these parameters is strongly discouraged. Using these parameters may have serious +# liveness implications for the validator and for the chain. +# +# These fields will be removed from the configuration file in the v0.37 release of Tendermint. +# For additional information, see ADR-74: +# https://github.com/tendermint/tendermint/blob/master/docs/architecture/adr-074-timeout-params.md + +# This field provides an unsafe override of the Propose timeout consensus parameter. +# This field configures how long the consensus engine will wait for a proposal block before prevoting nil. +# If this field is set to a value greater than 0, it will take effect. +unsafe-propose-timeout-override = "2s" + +# This field provides an unsafe override of the ProposeDelta timeout consensus parameter. +# This field configures how much the propose timeout increases with each round. +# If this field is set to a value greater than 0, it will take effect. +unsafe-propose-timeout-delta-override = "2s" + +# This field provides an unsafe override of the Vote timeout consensus parameter. +# This field configures how long the consensus engine will wait after +# receiving +2/3 votes in a round. +# If this field is set to a value greater than 0, it will take effect. +unsafe-vote-timeout-override = "2s" + +# This field provides an unsafe override of the VoteDelta timeout consensus parameter. +# This field configures how much the vote timeout increases with each round. +# If this field is set to a value greater than 0, it will take effect. +unsafe-vote-timeout-delta-override = "2s" + +# This field provides an unsafe override of the Commit timeout consensus parameter. +# This field configures how long the consensus engine will wait after receiving +# +2/3 precommits before beginning the next height. +# If this field is set to a value greater than 0, it will take effect. +unsafe-commit-timeout-override = "2s" + +# This field provides an unsafe override of the BypassCommitTimeout consensus parameter. +# This field configures if the consensus engine will wait for the full Commit timeout +# before proceeding to the next height. +# If this field is set to true, the consensus engine will proceed to the next height +# as soon as the node has gathered votes from all of the validators on the network. +# unsafe-bypass-commit-timeout-override = + +####################################################### +### Transaction Indexer Configuration Options ### +####################################################### +[tx-index] + +# The backend database list to back the indexer. +# If list contains "null" or "", meaning no indexer service will be used. +# +# The application will set which txs to index. In some cases a node operator will be able +# to decide which txs to index based on configuration set in the application. +# +# Options: +# 1) "null" (default) - no indexer services. +# 2) "kv" - a simple indexer backed by key-value storage (see DBBackend) +# 3) "psql" - the indexer services backed by PostgreSQL. +# When "kv" or "psql" is chosen "tx.height" and "tx.hash" will always be indexed. +indexer = ["kv"] + +# The PostgreSQL connection configuration, the connection format: +# postgresql://:@:/? +psql-conn = "" + +####################################################### +### Instrumentation Configuration Options ### +####################################################### +[instrumentation] + +# When true, Prometheus metrics are served under /metrics on +# PrometheusListenAddr. +# Check out the documentation for the list of available metrics. +prometheus = true + +# Address to listen for Prometheus collector(s) connections +prometheus-listen-addr = ":26660" + +# Maximum number of simultaneous connections. +# If you want to accept a larger number than the default, make sure +# you increase your OS limits. +# 0 - unlimited. +max-open-connections = 3 + +# Instrumentation namespace +namespace = "tendermint" + +####################################################### +### SelfRemediation Configuration Options ### +####################################################### +[self-remediation] + +# If the node has no p2p peers available then trigger a restart +# Set to 0 to disable +p2p-no-peers-available-window-seconds = 0 + +# If node has no peers for statesync after a period of time then restart +# Set to 0 to disable +statesync-no-peers-available-window-seconds = 0 + +# Threshold for how far back the node can be behind the current block height before triggering a restart +# Set to 0 to disable +blocks-behind-threshold = 0 + +# How often to check if node is behind +blocks-behind-check-interval = 60 + +# Cooldown between each restart +restart-cooldown-seconds = 600 + +[db-sync] +db-sync-enable = "false" +snapshot-interval = "0" +snapshot-directory = "" +snapshot-worker-count = "16" +timeout-in-seconds = "1200" +no-file-sleep-in-seconds = "1" +file-worker-count = "32" +file-worker-timeout = "30" +trust-height = "0" +trust-hash = "" +trust-period = "24h0m0s" +verify-light-block-timeout = "1m0s" +blacklist-ttl = "5m0s" +``` + +# App.toml + +## Step 1: Enable OCC + +Alter the following in the `Base Configuration` section to enable OCC: + +```bash +concurrency-workers = 500 +occ-enabled = true +``` + +## Step 2: Enable SeiDB + +Alter the following: + +```bash +ss-enable = true +ss-db-directory = " +``` + +This is just for validator nodes. Reference `seidb_migration.md` for more information on enabling state store for rpc nodes. + +## Step 3: Add EVM Section + +This is the largest edit involved for the configs. Add the following whole sections for `evm`, `eth_replay`, `eth_blocktest` +and `evm_query` at the bottom of the `app.toml`: + +```bash +[evm] +# controls whether an HTTP EVM server is enabled +http_enabled = true +http_port = 8545 + +# controls whether a websocket server is enabled +ws_enabled = true +ws_port = 8546 + +# ReadTimeout is the maximum duration for reading the entire +# request, including the body. +# Because ReadTimeout does not let Handlers make per-request +# decisions on each request body's acceptable deadline or +# upload rate, most users will prefer to use +# ReadHeaderTimeout. It is valid to use them both. +read_timeout = "30s" + +# ReadHeaderTimeout is the amount of time allowed to read +# request headers. The connection's read deadline is reset +# after reading the headers and the Handler can decide what +# is considered too slow for the body. If ReadHeaderTimeout +# is zero, the value of ReadTimeout is used. If both are +# zero, there is no timeout. +read_header_timeout = "30s" + +# WriteTimeout is the maximum duration before timing out +# writes of the response. It is reset whenever a new +# request's header is read. Like ReadTimeout, it does not +# let Handlers make decisions on a per-request basis. +write_timeout = "30s" + +# IdleTimeout is the maximum amount of time to wait for the +# next request when keep-alives are enabled. If IdleTimeout +# is zero, the value of ReadTimeout is used. If both are +# zero, ReadHeaderTimeout is used. +idle_timeout = "2m0s" + +# Maximum gas limit for simulation +simulation_gas_limit = 10000000 + +# Timeout for EVM call in simulation +simulation_evm_timeout = "1m0s" + +# list of CORS allowed origins, separated by comma +cors_origins = "*" + +# list of WS origins, separated by comma +ws_origins = "*" + +# timeout for filters +filter_timeout = "2m0s" + +# checkTx timeout for sig verify +checktx_timeout = "5s" + +# controls whether to have txns go through one by one +slow = false + +# Deny list defines list of methods that EVM RPC should fail fast +deny_list = [] + +# max number of logs returned if block range is open-ended +max_log_no_block = 10000 + +# max number of blocks to query logs for +max_blocks_for_log = 2000 + +[eth_replay] +eth_replay_enabled = false +eth_rpc = "http://44.234.105.54:18545" +eth_data_dir = "/root/.ethereum/chaindata" +eth_replay_contract_state_checks = false + +[eth_blocktest] +eth_blocktest_enabled = false +eth_blocktest_test_data_path = "~/testdata/" + +[evm_query] +evm_query_gas_limit = 300000 +``` + +## Full Example SeiV2 app.toml + +```bash +# This is a TOML config file. +# For more information, see https://github.com/toml-lang/toml + +############################################################################### +### Base Configuration ### +############################################################################### + +# The minimum gas prices a validator is willing to accept for processing a +# transaction. A transaction's fees must meet the minimum of any denomination +# specified in this config (e.g. 0.25token1;0.0001token2). +minimum-gas-prices = "0.02usei" + +# Pruning Strategies: +# - default: Keep the recent 362880 blocks and prune is triggered every 10 blocks +# - nothing: all historic states will be saved, nothing will be deleted (i.e. archiving node) +# - everything: all saved states will be deleted, storing only the recent 2 blocks; pruning at every block +# - custom: allow pruning options to be manually specified through 'pruning-keep-recent' and 'pruning-interval' +# Pruning strategy is completely ignored when seidb is enabled +pruning = "default" + +# These are applied if and only if the pruning strategy is custom, and seidb is not enabled +pruning-keep-recent = "0" +pruning-keep-every = "0" +pruning-interval = "3467" + +# HaltHeight contains a non-zero block height at which a node will gracefully +# halt and shutdown that can be used to assist upgrades and testing. +# +# Note: Commitment of state will be attempted on the corresponding block. +halt-height = 0 + +# HaltTime contains a non-zero minimum block time (in Unix seconds) at which +# a node will gracefully halt and shutdown that can be used to assist upgrades +# and testing. +# +# Note: Commitment of state will be attempted on the corresponding block. +halt-time = 0 + +# MinRetainBlocks defines the minimum block height offset from the current +# block being committed, such that all blocks past this offset are pruned +# from Tendermint. It is used as part of the process of determining the +# ResponseCommit.RetainHeight value during ABCI Commit. A value of 0 indicates +# that no blocks should be pruned. +# +# This configuration value is only responsible for pruning Tendermint blocks. +# It has no bearing on application state pruning which is determined by the +# "pruning-*" configurations. +# +# Note: Tendermint block pruning is dependant on this parameter in conunction +# with the unbonding (safety threshold) period, state pruning and state sync +# snapshot parameters to determine the correct minimum value of +# ResponseCommit.RetainHeight. +min-retain-blocks = 0 + +# InterBlockCache enables inter-block caching. +inter-block-cache = true + +# IndexEvents defines the set of events in the form {eventType}.{attributeKey}, +# which informs Tendermint what to index. If empty, all events will be indexed. +# +# Example: +# ["message.sender", "message.recipient"] +index-events = [] + +# IavlCacheSize set the size of the iavl tree cache. +# Default cache size is 50mb. +iavl-cache-size = 781250 + +# IAVLDisableFastNode enables or disables the fast node feature of IAVL. +# Default is true. +iavl-disable-fastnode = true + +# CompactionInterval sets (in seconds) the interval between forced levelDB +# compaction. A value of 0 means no forced levelDB. +# Default is 0. +compaction-interval = 0 + +# deprecated +no-versioning = false + +# Whether to store orphan data (to-be-deleted data pointers) outside the main +# application LevelDB +separate-orphan-storage = false + +# if separate-orphan-storage is true, how many versions of orphan data to keep +separate-orphan-versions-to-keep = 0 + +# if separate-orphan-storage is true, how many orphans to store in each file +num-orphan-per-file = 0 + +# if separate-orphan-storage is true, where to store orphan data +orphan-dir = "" + +# concurrency-workers defines how many workers to run for concurrent transaction execution +concurrency-workers = 500 + +# occ-enabled defines whether OCC is enabled or not for transaction execution +occ-enabled = true + +############################################################################### +### Telemetry Configuration ### +############################################################################### + +[telemetry] + +# Prefixed with keys to separate services. +service-name = "" + +# Enabled enables the application telemetry functionality. When enabled, +# an in-memory sink is also enabled by default. Operators may also enabled +# other sinks such as Prometheus. +enabled = true + +# Enable prefixing gauge values with hostname. +enable-hostname = false + +# Enable adding hostname to labels. +enable-hostname-label = false + +# Enable adding service to labels. +enable-service-label = false + +# PrometheusRetentionTime, when positive, enables a Prometheus metrics sink. +prometheus-retention-time = 60 + +# GlobalLabels defines a global set of name/value label tuples applied to all +# metrics emitted using the wrapper functions defined in telemetry package. +# +# Example: +# [["chain_id", "cosmoshub-1"]] +global-labels = [ +] + +############################################################################### +### API Configuration ### +############################################################################### + +[api] + +# Enable defines if the API server should be enabled. +enable = true + +# Swagger defines if swagger documentation should automatically be registered. +swagger = false + +# Address defines the API server to listen on. +address = "tcp://0.0.0.0:1317" + +# MaxOpenConnections defines the number of maximum open connections. +max-open-connections = 1000 + +# RPCReadTimeout defines the Tendermint RPC read timeout (in seconds). +rpc-read-timeout = 10 + +# RPCWriteTimeout defines the Tendermint RPC write timeout (in seconds). +rpc-write-timeout = 0 + +# RPCMaxBodyBytes defines the Tendermint maximum response body (in bytes). +rpc-max-body-bytes = 1000000 + +# EnableUnsafeCORS defines if CORS should be enabled (unsafe - use it at your own risk). +enabled-unsafe-cors = false + +############################################################################### +### Rosetta Configuration ### +############################################################################### + +[rosetta] + +# Enable defines if the Rosetta API server should be enabled. +enable = false + +# Address defines the Rosetta API server to listen on. +address = ":8080" + +# Network defines the name of the blockchain that will be returned by Rosetta. +blockchain = "app" + +# Network defines the name of the network that will be returned by Rosetta. +network = "network" + +# Retries defines the number of retries when connecting to the node before failing. +retries = 3 + +# Offline defines if Rosetta server should run in offline mode. +offline = false + +############################################################################### +### gRPC Configuration ### +############################################################################### + +[grpc] + +# Enable defines if the gRPC server should be enabled. +enable = true + +# Address defines the gRPC server address to bind to. +address = "0.0.0.0:9090" + +############################################################################### +### gRPC Web Configuration ### +############################################################################### + +[grpc-web] + +# GRPCWebEnable defines if the gRPC-web should be enabled. +# NOTE: gRPC must also be enabled, otherwise, this configuration is a no-op. +enable = true + +# Address defines the gRPC-web server address to bind to. +address = "0.0.0.0:9091" + +# EnableUnsafeCORS defines if CORS should be enabled (unsafe - use it at your own risk). +enable-unsafe-cors = false + +############################################################################### +### State Sync Configuration ### +############################################################################### + +# State sync snapshots allow other nodes to rapidly join the network without replaying historical +# blocks, instead downloading and applying a snapshot of the application state at a given height. +[state-sync] + +# snapshot-interval specifies the block interval at which local state sync snapshots are +# taken (0 to disable). Must be a multiple of pruning-keep-every. +snapshot-interval = 0 + +# snapshot-keep-recent specifies the number of recent snapshots to keep and serve (0 to keep all). +snapshot-keep-recent = 2 + +# snapshot-directory sets the directory for where state sync snapshots are persisted. +# default is emtpy which will then store under the app home directory same as before. +snapshot-directory = "" + + +############################################################################# +### SeiDB Configuration ### +############################################################################# + +[state-commit] +# Enable defines if the SeiDB should be enabled to override existing IAVL db backend. +sc-enable = true + +# Defines the SC store directory, if not explicitly set, default to application home directory +sc-directory = "" + +# ZeroCopy defines if memiavl should return slices pointing to mmap-ed buffers directly (zero-copy), +# the zero-copied slices must not be retained beyond current block's execution. +# the sdk address cache will be disabled if zero-copy is enabled. +sc-zero-copy = false + +# AsyncCommitBuffer defines the size of asynchronous commit queue, this greatly improve block catching-up +# performance, setting to 0 means synchronous commit. +sc-async-commit-buffer = 100 + +# KeepRecent defines how many state-commit snapshots (besides the latest one) to keep +# defaults to 1 to make sure ibc relayers work. +sc-keep-recent = 1 + +# SnapshotInterval defines the block interval the snapshot is taken, default to 10000 blocks. +sc-snapshot-interval = 10000 + +# SnapshotWriterLimit defines the max concurrency for taking commit store snapshot +sc-snapshot-writer-limit = 0 + +[state-store] +# Enable defines whether the state-store should be enabled for storing historical data. +# Supporting historical queries or exporting state snapshot requires setting this to true +# This config only take effect when SeiDB is enabled (sc-enable = true +ss-enable = true + +# Defines the directory to store the state store db files +# If not explicitly set, default to application home directory +ss-db-directory = "" + +# DBBackend defines the backend database used for state-store. +# Supported backends: pebbledb, rocksdb +# defaults to pebbledb (recommended) +ss-backend = "pebbledb" + +# AsyncWriteBuffer defines the async queue length for commits to be applied to State Store +# Set <= 0 for synchronous writes, which means commits also need to wait for data to be persisted in State Store. +# defaults to 100 for asynchronous writes +ss-async-write-buffer = 100 + +# KeepRecent defines the number of versions to keep in state store +# Setting it to 0 means keep everything +# Default to keep the last 100,000 blocks +ss-keep-recent = 100000 + +# PruneInterval defines the minimum interval in seconds + some random delay to trigger SS pruning. +# It is recommended to trigger pruning less frequently with a large interval. +# default to 600 seconds +ss-prune-interval = 600 + +# ImportNumWorkers defines the concurrency for state sync import +# defaults to 1 +ss-import-num-workers = 1 + + +[wasm] +# This is the maximum sdk gas (wasm and storage) that we allow for any x/wasm "smart" queries +query_gas_limit = 300000 +# This is the number of wasm vm instances we keep cached in memory for speed-up +# Warning: this is currently unstable and may lead to crashes, best to keep for 0 unless testing locally +lru_size = 0 + +[evm] +# controls whether an HTTP EVM server is enabled +http_enabled = true +http_port = 8545 + +# controls whether a websocket server is enabled +ws_enabled = true +ws_port = 8546 + +# ReadTimeout is the maximum duration for reading the entire +# request, including the body. +# Because ReadTimeout does not let Handlers make per-request +# decisions on each request body's acceptable deadline or +# upload rate, most users will prefer to use +# ReadHeaderTimeout. It is valid to use them both. +read_timeout = "30s" + +# ReadHeaderTimeout is the amount of time allowed to read +# request headers. The connection's read deadline is reset +# after reading the headers and the Handler can decide what +# is considered too slow for the body. If ReadHeaderTimeout +# is zero, the value of ReadTimeout is used. If both are +# zero, there is no timeout. +read_header_timeout = "30s" + +# WriteTimeout is the maximum duration before timing out +# writes of the response. It is reset whenever a new +# request's header is read. Like ReadTimeout, it does not +# let Handlers make decisions on a per-request basis. +write_timeout = "30s" + +# IdleTimeout is the maximum amount of time to wait for the +# next request when keep-alives are enabled. If IdleTimeout +# is zero, the value of ReadTimeout is used. If both are +# zero, ReadHeaderTimeout is used. +idle_timeout = "2m0s" + +# Maximum gas limit for simulation +simulation_gas_limit = 10000000 + +# Timeout for EVM call in simulation +simulation_evm_timeout = "1m0s" + +# list of CORS allowed origins, separated by comma +cors_origins = "*" + +# list of WS origins, separated by comma +ws_origins = "*" + +# timeout for filters +filter_timeout = "2m0s" + +# checkTx timeout for sig verify +checktx_timeout = "5s" + +# controls whether to have txns go through one by one +slow = false + +# Deny list defines list of methods that EVM RPC should fail fast +deny_list = [] + +# max number of logs returned if block range is open-ended +max_log_no_block = 10000 + +# max number of blocks to query logs for +max_blocks_for_log = 2000 + +[eth_replay] +eth_replay_enabled = false +eth_rpc = "http://44.234.105.54:18545" +eth_data_dir = "/root/.ethereum/chaindata" +eth_replay_contract_state_checks = false + +[eth_blocktest] +eth_blocktest_enabled = false +eth_blocktest_test_data_path = "~/testdata/" + +[evm_query] +evm_query_gas_limit = 300000 +``` From 96467247001552d0f2d5e8bca2d9cb410f11f74b Mon Sep 17 00:00:00 2001 From: codchen Date: Thu, 2 May 2024 04:28:36 +0800 Subject: [PATCH 29/31] Add association logic in simulate (#1609) * Add association logic in simulate * associate for traceBlock --- evmrpc/setup_test.go | 3 +++ evmrpc/simulate.go | 33 +++++++++++++++++++++++++++++++++ go.mod | 2 +- go.sum | 4 ++-- x/evm/ante/preprocess.go | 6 +++--- 5 files changed, 42 insertions(+), 6 deletions(-) diff --git a/evmrpc/setup_test.go b/evmrpc/setup_test.go index dd086b82b7..60414d79a5 100644 --- a/evmrpc/setup_test.go +++ b/evmrpc/setup_test.go @@ -571,6 +571,9 @@ func generateTxData() { ChainID: chainId, }) UnconfirmedTx = unconfirmedTxBuilder.GetTx() + + tracerTestTxFrom := common.HexToAddress("0x5b4eba929f3811980f5ae0c5d04fa200f837df4e") + EVMKeeper.SetAddressMapping(Ctx, sdk.AccAddress(tracerTestTxFrom[:]), tracerTestTxFrom) } func buildTx(txData ethtypes.DynamicFeeTx) (client.TxBuilder, *ethtypes.Transaction) { diff --git a/evmrpc/simulate.go b/evmrpc/simulate.go index 8e82ef1b3c..971c215427 100644 --- a/evmrpc/simulate.go +++ b/evmrpc/simulate.go @@ -23,6 +23,7 @@ import ( "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rpc" "github.com/sei-protocol/sei-chain/utils" + "github.com/sei-protocol/sei-chain/x/evm/ante" "github.com/sei-protocol/sei-chain/x/evm/keeper" "github.com/sei-protocol/sei-chain/x/evm/state" "github.com/sei-protocol/sei-chain/x/evm/types" @@ -288,6 +289,20 @@ func (b *Backend) StateAtTransaction(ctx context.Context, block *ethtypes.Block, // set block context time as of the block time (block time is the time of the CURRENT block) blockContext.Time = block.Time() + // set address association for the sender if not present. Note that here we take the shortcut + // of querying from the latest height with the assumption that if this tx has been processed + // at all then its association must be present in the latest height + _, associated := b.keeper.GetSeiAddress(statedb.Ctx(), msg.From) + if !associated { + seiAddr, associatedNow := b.keeper.GetSeiAddress(b.ctxProvider(LatestCtxHeight), msg.From) + if !associatedNow { + return nil, vm.BlockContext{}, nil, nil, fmt.Errorf("address %s is not associated in the latest height", msg.From.Hex()) + } + if err := ante.NewEVMPreprocessDecorator(b.keeper, b.keeper.AccountKeeper()).AssociateAddresses(statedb.Ctx(), seiAddr, msg.From, nil); err != nil { + return nil, vm.BlockContext{}, nil, nil, err + } + } + if idx == txIndex { return tx, *blockContext, statedb, emptyRelease, nil } @@ -307,6 +322,24 @@ func (b *Backend) StateAtTransaction(ctx context.Context, block *ethtypes.Block, func (b *Backend) StateAtBlock(ctx context.Context, block *ethtypes.Block, reexec uint64, base vm.StateDB, readOnly bool, preferDisk bool) (vm.StateDB, tracers.StateReleaseFunc, error) { emptyRelease := func() {} statedb := state.NewDBImpl(b.ctxProvider(block.Number().Int64()-1), b.keeper, true) + signer := ethtypes.MakeSigner(b.ChainConfig(), block.Number(), block.Time()) + for _, tx := range block.Transactions() { + msg, _ := core.TransactionToMessage(tx, signer, block.BaseFee()) + + // set address association for the sender if not present. Note that here we take the shortcut + // of querying from the latest height with the assumption that if this tx has been processed + // at all then its association must be present in the latest height + _, associated := b.keeper.GetSeiAddress(statedb.Ctx(), msg.From) + if !associated { + seiAddr, associatedNow := b.keeper.GetSeiAddress(b.ctxProvider(LatestCtxHeight), msg.From) + if !associatedNow { + return nil, emptyRelease, fmt.Errorf("address %s is not associated in the latest height", msg.From.Hex()) + } + if err := ante.NewEVMPreprocessDecorator(b.keeper, b.keeper.AccountKeeper()).AssociateAddresses(statedb.Ctx(), seiAddr, msg.From, nil); err != nil { + return nil, emptyRelease, err + } + } + } return statedb, emptyRelease, nil } diff --git a/go.mod b/go.mod index 367513047d..593860f763 100644 --- a/go.mod +++ b/go.mod @@ -349,7 +349,7 @@ replace ( github.com/cosmos/cosmos-sdk => github.com/sei-protocol/sei-cosmos v0.3.5 github.com/cosmos/iavl => github.com/sei-protocol/sei-iavl v0.1.9 github.com/cosmos/ibc-go/v3 => github.com/sei-protocol/sei-ibc-go/v3 v3.3.0 - github.com/ethereum/go-ethereum => github.com/sei-protocol/go-ethereum v1.13.5-sei-15 + github.com/ethereum/go-ethereum => github.com/sei-protocol/go-ethereum v1.13.5-sei-16 github.com/gogo/protobuf => github.com/regen-network/protobuf v1.3.3-alpha.regen.1 github.com/sei-protocol/sei-db => github.com/sei-protocol/sei-db v0.0.35 // Latest goleveldb is broken, we have to stick to this version diff --git a/go.sum b/go.sum index 2eb3139da6..ad47b2af98 100644 --- a/go.sum +++ b/go.sum @@ -1343,8 +1343,8 @@ github.com/seccomp/libseccomp-golang v0.9.2-0.20220502022130-f33da4d89646/go.mod github.com/securego/gosec/v2 v2.11.0 h1:+PDkpzR41OI2jrw1q6AdXZCbsNGNGT7pQjal0H0cArI= github.com/securego/gosec/v2 v2.11.0/go.mod h1:SX8bptShuG8reGC0XS09+a4H2BoWSJi+fscA+Pulbpo= github.com/segmentio/fasthash v1.0.3/go.mod h1:waKX8l2N8yckOgmSsXJi7x1ZfdKZ4x7KRMzBtS3oedY= -github.com/sei-protocol/go-ethereum v1.13.5-sei-15 h1:VSFQrbWnSDCPCQzsYDW3k07EP3yPZb+4xAcED9kSKpg= -github.com/sei-protocol/go-ethereum v1.13.5-sei-15/go.mod h1:kcRZmuzRn1lVejiFNTz4l4W7imnpq1bDAnuKS/RyhbQ= +github.com/sei-protocol/go-ethereum v1.13.5-sei-16 h1:bPQw44//5XHDZWfwO98g2Hie5HguxYZY+AiRsYMBdVg= +github.com/sei-protocol/go-ethereum v1.13.5-sei-16/go.mod h1:kcRZmuzRn1lVejiFNTz4l4W7imnpq1bDAnuKS/RyhbQ= github.com/sei-protocol/goutils v0.0.2 h1:Bfa7Sv+4CVLNM20QcpvGb81B8C5HkQC/kW1CQpIbXDA= github.com/sei-protocol/goutils v0.0.2/go.mod h1:iYE2DuJfEnM+APPehr2gOUXfuLuPsVxorcDO+Tzq9q8= github.com/sei-protocol/sei-cosmos v0.3.5 h1:ibWj4uM3YeKLaGKfl1oWj36nkJo/2aZSyS7xo8Ixh6E= diff --git a/x/evm/ante/preprocess.go b/x/evm/ante/preprocess.go index f906d95d74..bb0e1d0a26 100644 --- a/x/evm/ante/preprocess.go +++ b/x/evm/ante/preprocess.go @@ -88,7 +88,7 @@ func (p *EVMPreprocessDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate return ctx, sdkerrors.Wrap(sdkerrors.ErrInsufficientFunds, "account needs to have at least 1Sei to force association") } } - if err := p.associateAddresses(ctx, seiAddr, evmAddr, pubkey); err != nil { + if err := p.AssociateAddresses(ctx, seiAddr, evmAddr, pubkey); err != nil { return ctx, err } return ctx.WithPriority(antedecorators.EVMAssociatePriority), nil // short-circuit without calling next @@ -96,7 +96,7 @@ func (p *EVMPreprocessDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate // noop; for readability } else { // not associatedTx and not already associated - if err := p.associateAddresses(ctx, seiAddr, evmAddr, pubkey); err != nil { + if err := p.AssociateAddresses(ctx, seiAddr, evmAddr, pubkey); err != nil { return ctx, err } if p.evmKeeper.EthReplayConfig.Enabled { @@ -107,7 +107,7 @@ func (p *EVMPreprocessDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate return next(ctx, tx, simulate) } -func (p *EVMPreprocessDecorator) associateAddresses(ctx sdk.Context, seiAddr sdk.AccAddress, evmAddr common.Address, pubkey cryptotypes.PubKey) error { +func (p *EVMPreprocessDecorator) AssociateAddresses(ctx sdk.Context, seiAddr sdk.AccAddress, evmAddr common.Address, pubkey cryptotypes.PubKey) error { p.evmKeeper.SetAddressMapping(ctx, seiAddr, evmAddr) if acc := p.accountKeeper.GetAccount(ctx, seiAddr); acc.GetPubKey() == nil { if err := acc.SetPubKey(pubkey); err != nil { From 5ddc34966ddff9319809587e14ca03f85a948a56 Mon Sep 17 00:00:00 2001 From: codchen Date: Thu, 2 May 2024 04:37:53 +0800 Subject: [PATCH 30/31] optimize WS newhead (#1615) --- evmrpc/filter.go | 18 ++++++--- evmrpc/server.go | 2 +- evmrpc/setup_test.go | 3 ++ evmrpc/subscribe.go | 81 +++++++++++++++++++++++++++++----------- evmrpc/subscribe_test.go | 1 + 5 files changed, 76 insertions(+), 29 deletions(-) diff --git a/evmrpc/filter.go b/evmrpc/filter.go index d58bd853b5..0cc3590848 100644 --- a/evmrpc/filter.go +++ b/evmrpc/filter.go @@ -98,7 +98,8 @@ func (a *FilterAPI) timeoutLoop(timeout time.Duration) { func (a *FilterAPI) NewFilter( _ context.Context, crit filters.FilterCriteria, -) (ethrpc.ID, error) { +) (id ethrpc.ID, err error) { + defer recordMetrics("eth_newFilter", time.Now(), err == nil) a.filtersMu.Lock() defer a.filtersMu.Unlock() curFilterID := ethrpc.NewID() @@ -113,7 +114,8 @@ func (a *FilterAPI) NewFilter( func (a *FilterAPI) NewBlockFilter( _ context.Context, -) (ethrpc.ID, error) { +) (id ethrpc.ID, err error) { + defer recordMetrics("eth_newBlockFilter", time.Now(), err == nil) a.filtersMu.Lock() defer a.filtersMu.Unlock() curFilterID := ethrpc.NewID() @@ -128,7 +130,8 @@ func (a *FilterAPI) NewBlockFilter( func (a *FilterAPI) GetFilterChanges( ctx context.Context, filterID ethrpc.ID, -) (interface{}, error) { +) (res interface{}, err error) { + defer recordMetrics("eth_getFilterChanges", time.Now(), err == nil) a.filtersMu.Lock() defer a.filtersMu.Unlock() filter, ok := a.filters[filterID] @@ -178,7 +181,8 @@ func (a *FilterAPI) GetFilterChanges( func (a *FilterAPI) GetFilterLogs( ctx context.Context, filterID ethrpc.ID, -) ([]*ethtypes.Log, error) { +) (res []*ethtypes.Log, err error) { + defer recordMetrics("eth_getFilterLogs", time.Now(), err == nil) a.filtersMu.Lock() defer a.filtersMu.Unlock() filter, ok := a.filters[filterID] @@ -206,7 +210,8 @@ func (a *FilterAPI) GetFilterLogs( func (a *FilterAPI) GetLogs( ctx context.Context, crit filters.FilterCriteria, -) ([]*ethtypes.Log, error) { +) (res []*ethtypes.Log, err error) { + defer recordMetrics("eth_getLogs", time.Now(), err == nil) logs, _, err := a.logFetcher.GetLogsByFilters(ctx, crit, 0) return logs, err } @@ -252,7 +257,8 @@ func (a *FilterAPI) getBlockHeadersAfter( func (a *FilterAPI) UninstallFilter( _ context.Context, filterID ethrpc.ID, -) bool { +) (res bool) { + defer recordMetrics("eth_uninstallFilter", time.Now(), res) a.filtersMu.Lock() defer a.filtersMu.Unlock() _, found := a.filters[filterID] diff --git a/evmrpc/server.go b/evmrpc/server.go index a0deb45803..b69a23bdeb 100644 --- a/evmrpc/server.go +++ b/evmrpc/server.go @@ -154,7 +154,7 @@ func NewEVMWebSocketServer( }, { Namespace: "eth", - Service: NewSubscriptionAPI(tmClient, &LogFetcher{tmClient: tmClient, k: k, ctxProvider: ctxProvider}, &SubscriptionConfig{subscriptionCapacity: 100}), + Service: NewSubscriptionAPI(tmClient, &LogFetcher{tmClient: tmClient, k: k, ctxProvider: ctxProvider}, &SubscriptionConfig{subscriptionCapacity: 100}, &FilterConfig{timeout: config.FilterTimeout, maxLog: config.MaxLogNoBlock, maxBlock: config.MaxBlocksForLog}), }, { Namespace: "web3", diff --git a/evmrpc/setup_test.go b/evmrpc/setup_test.go index 60414d79a5..21ea2d1804 100644 --- a/evmrpc/setup_test.go +++ b/evmrpc/setup_test.go @@ -80,6 +80,8 @@ var MockBlockID = tmtypes.BlockID{ Hash: bytes.HexBytes(mustHexToBytes("0000000000000000000000000000000000000000000000000000000000000001")), } +var NewHeadsCalled = make(chan struct{}) + type MockClient struct { mock.Client } @@ -271,6 +273,7 @@ func (c *MockClient) Subscribe(ctx context.Context, subscriber string, query str if query == "tm.event = 'NewBlockHeader'" { resCh := make(chan coretypes.ResultEvent, 5) go func() { + <-NewHeadsCalled for i := uint64(0); i < 5; i++ { resCh <- coretypes.ResultEvent{ SubscriptionID: subscriber, diff --git a/evmrpc/subscribe.go b/evmrpc/subscribe.go index 696aa2ff89..6789bc1d88 100644 --- a/evmrpc/subscribe.go +++ b/evmrpc/subscribe.go @@ -25,61 +25,98 @@ type SubscriptionAPI struct { subscriptionManager *SubscriptionManager subscriptonConfig *SubscriptionConfig - logFetcher *LogFetcher + logFetcher *LogFetcher + newHeadListenersMtx *sync.Mutex + newHeadListeners map[rpc.ID]chan map[string]interface{} } type SubscriptionConfig struct { subscriptionCapacity int } -func NewSubscriptionAPI(tmClient rpcclient.Client, logFetcher *LogFetcher, subscriptionConfig *SubscriptionConfig) *SubscriptionAPI { - logFetcher.filterConfig = &FilterConfig{} - return &SubscriptionAPI{ +func NewSubscriptionAPI(tmClient rpcclient.Client, logFetcher *LogFetcher, subscriptionConfig *SubscriptionConfig, filterConfig *FilterConfig) *SubscriptionAPI { + logFetcher.filterConfig = filterConfig + api := &SubscriptionAPI{ tmClient: tmClient, subscriptionManager: NewSubscriptionManager(tmClient), subscriptonConfig: subscriptionConfig, logFetcher: logFetcher, + newHeadListenersMtx: &sync.Mutex{}, + newHeadListeners: make(map[rpc.ID]chan map[string]interface{}), } + id, subCh, err := api.subscriptionManager.Subscribe(context.Background(), NewHeadQueryBuilder(), api.subscriptonConfig.subscriptionCapacity) + if err != nil { + panic(err) + } + go func() { + defer func() { + _ = api.subscriptionManager.Unsubscribe(context.Background(), id) + }() + for { + res := <-subCh + ethHeader, err := encodeTmHeader(res.Data.(tmtypes.EventDataNewBlockHeader)) + if err != nil { + fmt.Printf("error encoding new head event %#v due to %s\n", res.Data, err) + continue + } + api.newHeadListenersMtx.Lock() + for _, c := range api.newHeadListeners { + c := c + go func() { + defer func() { + // if the channel is already closed, sending to it will panic + if err := recover(); err != nil { + return + } + }() + c <- ethHeader + }() + } + api.newHeadListenersMtx.Unlock() + } + }() + return api } -func (a *SubscriptionAPI) NewHeads(ctx context.Context) (*rpc.Subscription, error) { +func (a *SubscriptionAPI) NewHeads(ctx context.Context) (s *rpc.Subscription, err error) { + defer recordMetrics("eth_newHeads", time.Now(), err == nil) notifier, supported := rpc.NotifierFromContext(ctx) if !supported { return &rpc.Subscription{}, rpc.ErrNotificationsUnsupported } rpcSub := notifier.CreateSubscription() - subscriberID, subCh, err := a.subscriptionManager.Subscribe(context.Background(), NewHeadQueryBuilder(), a.subscriptonConfig.subscriptionCapacity) - if err != nil { - return nil, err - } + listener := make(chan map[string]interface{}) go func() { - defer func() { - _ = a.subscriptionManager.Unsubscribe(context.Background(), subscriberID) - }() + OUTER: for { select { - case res := <-subCh: - ethHeader, err := encodeTmHeader(res.Data.(tmtypes.EventDataNewBlockHeader)) + case res := <-listener: + err = notifier.Notify(rpcSub.ID, res) if err != nil { - return - } - err = notifier.Notify(rpcSub.ID, ethHeader) - if err != nil { - return + break OUTER } case <-rpcSub.Err(): - return + break OUTER case <-notifier.Closed(): - return + break OUTER } } + a.newHeadListenersMtx.Lock() + defer a.newHeadListenersMtx.Unlock() + delete(a.newHeadListeners, rpcSub.ID) + close(listener) }() + a.newHeadListenersMtx.Lock() + defer a.newHeadListenersMtx.Unlock() + a.newHeadListeners[rpcSub.ID] = listener + return rpcSub, nil } -func (a *SubscriptionAPI) Logs(ctx context.Context, filter *filters.FilterCriteria) (*rpc.Subscription, error) { +func (a *SubscriptionAPI) Logs(ctx context.Context, filter *filters.FilterCriteria) (s *rpc.Subscription, err error) { + defer recordMetrics("eth_logs", time.Now(), err == nil) notifier, supported := rpc.NotifierFromContext(ctx) if !supported { return &rpc.Subscription{}, rpc.ErrNotificationsUnsupported diff --git a/evmrpc/subscribe_test.go b/evmrpc/subscribe_test.go index d999482599..f64e44fc80 100644 --- a/evmrpc/subscribe_test.go +++ b/evmrpc/subscribe_test.go @@ -15,6 +15,7 @@ import ( func TestSubscribeNewHeads(t *testing.T) { t.Parallel() recvCh, done := sendWSRequestGood(t, "subscribe", "newHeads") + NewHeadsCalled <- struct{}{} defer func() { done <- struct{}{} }() receivedSubMsg := false From 7f1037fa254d9b497101e37740bfe44b613aaff7 Mon Sep 17 00:00:00 2001 From: Kartik Bhat Date: Wed, 1 May 2024 17:42:51 -0400 Subject: [PATCH 31/31] Bump SeiDB version (#1613) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 593860f763..778cb9657b 100644 --- a/go.mod +++ b/go.mod @@ -351,7 +351,7 @@ replace ( github.com/cosmos/ibc-go/v3 => github.com/sei-protocol/sei-ibc-go/v3 v3.3.0 github.com/ethereum/go-ethereum => github.com/sei-protocol/go-ethereum v1.13.5-sei-16 github.com/gogo/protobuf => github.com/regen-network/protobuf v1.3.3-alpha.regen.1 - github.com/sei-protocol/sei-db => github.com/sei-protocol/sei-db v0.0.35 + github.com/sei-protocol/sei-db => github.com/sei-protocol/sei-db v0.0.36 // Latest goleveldb is broken, we have to stick to this version github.com/syndtr/goleveldb => github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 github.com/tendermint/tendermint => github.com/sei-protocol/sei-tendermint v0.3.1 diff --git a/go.sum b/go.sum index ad47b2af98..d4c27f7a2f 100644 --- a/go.sum +++ b/go.sum @@ -1349,8 +1349,8 @@ github.com/sei-protocol/goutils v0.0.2 h1:Bfa7Sv+4CVLNM20QcpvGb81B8C5HkQC/kW1CQp github.com/sei-protocol/goutils v0.0.2/go.mod h1:iYE2DuJfEnM+APPehr2gOUXfuLuPsVxorcDO+Tzq9q8= github.com/sei-protocol/sei-cosmos v0.3.5 h1:ibWj4uM3YeKLaGKfl1oWj36nkJo/2aZSyS7xo8Ixh6E= github.com/sei-protocol/sei-cosmos v0.3.5/go.mod h1:imKzUdlLFKj8H39Ej9dICT+HZkx0rgEPsVm0PPb59kc= -github.com/sei-protocol/sei-db v0.0.35 h1:BNHv0gtKE4J5kq1Mhxt9dpop3lI4W2I5WurgWYIYa4E= -github.com/sei-protocol/sei-db v0.0.35/go.mod h1:F/ZKZA8HJPcUzSZPA8yt6pfwlGriJ4RDR4eHKSGLStI= +github.com/sei-protocol/sei-db v0.0.36 h1:Qg8MlO/4btECyAB/XrbEexhpaS7OmYsrs9IUYULf+bY= +github.com/sei-protocol/sei-db v0.0.36/go.mod h1:F/ZKZA8HJPcUzSZPA8yt6pfwlGriJ4RDR4eHKSGLStI= github.com/sei-protocol/sei-iavl v0.1.9 h1:y4mVYftxLNRs6533zl7N0/Ch+CzRQc04JDfHolIxgBE= github.com/sei-protocol/sei-iavl v0.1.9/go.mod h1:7PfkEVT5dcoQE+s/9KWdoXJ8VVVP1QpYYPLdxlkSXFk= github.com/sei-protocol/sei-ibc-go/v3 v3.3.0 h1:/mjpTuCSEVDJ51nUDSHU92N0bRSwt49r1rmdC/lqgp8=