From 7fb1c998518b8264520612d9cafa7c7b9c79365f Mon Sep 17 00:00:00 2001 From: Jeremy Wei Date: Mon, 27 Jan 2025 10:05:52 -0500 Subject: [PATCH 1/3] checkpoint --- contracts/test/EVMPrecompileTest.js | 20 ++++++++++++ contracts/test/lib.js | 49 +++++++++++++++++++++++++++++ precompiles/addr/addr.go | 5 +++ precompiles/addr/addr_test.go | 30 ++++++++++++++---- precompiles/bank/bank.go | 5 +++ precompiles/oracle/oracle.go | 6 ++++ 6 files changed, 109 insertions(+), 6 deletions(-) diff --git a/contracts/test/EVMPrecompileTest.js b/contracts/test/EVMPrecompileTest.js index 5f9229b93f..7d577199ad 100755 --- a/contracts/test/EVMPrecompileTest.js +++ b/contracts/test/EVMPrecompileTest.js @@ -72,8 +72,28 @@ describe("EVM Precompile Tester", function () { const seiAddr = await addr.getSeiAddr(unassociatedWallet.address); expect(seiAddr).to.not.be.null; }); + + it.only("Fails gracefully even when insufficient gas is provided", async function () { + const unassociatedWallet = hre.ethers.Wallet.createRandom(); + console.log("DEBUG: unassociatedWallet", unassociatedWallet.publicKey.slice(2)); + await expectRevert( + addr.associatePubKey(unassociatedWallet.publicKey.slice(2), {gasLimit: 52603}), + "execution reverted" + ) + const associatedAddrs = await addr.associatePubKey(unassociatedWallet.publicKey.slice(2), {gasLimit: 52603}); + const receipt = await associatedAddrs.wait(); + const hash = receipt.transactionHash; + // ensure that the transaction's trace doesn't have "panic" in it + // make http call to debug_traceTransaction + const trace = await hre.ethers.provider.send("debug_traceTransaction", [hash]); + // expect trace to not contain the word "panic" + expect(trace).to.not.have.property('panic'); + }); }); + // used to send a transaction that goes directly to SendTransaction + function sendCustomTransaction() + describe("EVM Gov Precompile Tester", function () { const GovPrecompileContract = '0x0000000000000000000000000000000000001006'; let gov; diff --git a/contracts/test/lib.js b/contracts/test/lib.js index f3ee5c8670..6047e790dd 100644 --- a/contracts/test/lib.js +++ b/contracts/test/lib.js @@ -211,6 +211,28 @@ async function createTokenFactoryTokenAndMint(name, amount, recipient, from=admi return token_denom } +async function getChainId() { + const nodeUrl = 'http://localhost:8545'; + const response = await axios.post(nodeUrl, { + method: 'eth_chainId', + params: [], + id: 1, + jsonrpc: "2.0" + }) + return response.data.result; +} + +async function getGasPrice() { + const nodeUrl = 'http://localhost:8545'; + const response = await axios.post(nodeUrl, { + method: 'eth_gasPrice', + params: [], + id: 1, + jsonrpc: "2.0" + }) + return response.data.result; +} + async function getPointerForNative(name) { const command = `seid query evm pointer NATIVE ${name} -o json` const output = await execute(command); @@ -417,6 +439,31 @@ async function deployEvmContract(name, args=[]) { return contract; } +async function sendNonSimulatedLegacyTransaction(signer, to, value, gasLimit) { + const chainId = await getChainId(); + const gasPrice = await getGasPrice(); + const nonce = await signer.getTransactionCount("pending"); + const signedTx = await signer.signTransaction({ + to: to, + value: value, + gasLimit: gasLimit, + gasPrice: gasPrice, + nonce: nonce, + maxPriorityFeePerGas: maxPriorityFeePerGas, + maxFeePerGas: maxFeePerGas, + chainId: chainId + }); + // send the transaction + const nodeUrl = 'http://localhost:8545'; + const response = await axios.post(nodeUrl, { + method: 'eth_sendRawTransaction', + params: [signedTx], + id: 1, + jsonrpc: "2.0" + }) + return response.data.result; +} + async function setupSigners(signers) { const result = [] for(let signer of signers) { @@ -526,6 +573,8 @@ module.exports = { deployWasm, instantiateWasm, createTokenFactoryTokenAndMint, + getChainId, + getGasPrice, execute, getSeiAddress, getEvmAddress, diff --git a/precompiles/addr/addr.go b/precompiles/addr/addr.go index 68dced47a8..ed4a5966ee 100644 --- a/precompiles/addr/addr.go +++ b/precompiles/addr/addr.go @@ -89,6 +89,11 @@ func (p PrecompileExecutor) RequiredGas(input []byte, method *abi.Method) uint64 } func (p PrecompileExecutor) Execute(ctx sdk.Context, method *abi.Method, _ common.Address, _ common.Address, args []interface{}, value *big.Int, readOnly bool, _ *vm.EVM, suppliedGas uint64) (ret []byte, remainingGas uint64, err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("execution reverted: %v", r) + } + }() switch method.Name { case GetSeiAddressMethod: return p.getSeiAddr(ctx, method, args, value) diff --git a/precompiles/addr/addr_test.go b/precompiles/addr/addr_test.go index 5e257b1ab7..f57852bc4a 100644 --- a/precompiles/addr/addr_test.go +++ b/precompiles/addr/addr_test.go @@ -45,11 +45,12 @@ func TestAssociatePubKey(t *testing.T) { happyPathOutput, _ := associatePubKey.Outputs.Pack(targetSeiAddress.String(), targetEvmAddress) type args struct { - evm *vm.EVM - caller common.Address - pubKey string - value *big.Int - readOnly bool + evm *vm.EVM + caller common.Address + pubKey string + value *big.Int + readOnly bool + suppliedGas uint64 } tests := []struct { name string @@ -116,6 +117,19 @@ func TestAssociatePubKey(t *testing.T) { wantErr: true, wantErrMsg: fmt.Sprintf("address %s is already associated with evm address %s", callerSeiAddress, callerEvmAddress), }, + { + name: "fails if insufficient gas provided", + args: args{ + evm: &vm.EVM{ + StateDB: state.NewDBImpl(ctx, k, true), + TxContext: vm.TxContext{Origin: callerEvmAddress}, + }, + caller: callerEvmAddress, + pubKey: targetPubKeyHex, + value: big.NewInt(0), + suppliedGas: 1, + }, + }, { name: "happy path - associates addresses if signature is correct", args: args{ @@ -141,7 +155,11 @@ func TestAssociatePubKey(t *testing.T) { require.Nil(t, err) // Make the call to associate. - ret, _, err := p.RunAndCalculateGas(tt.args.evm, tt.args.caller, tt.args.caller, append(p.GetExecutor().(*addr.PrecompileExecutor).AssociatePubKeyID, inputs...), 40000, tt.args.value, nil, tt.args.readOnly, false) + suppliedGas := uint64(40000) + if tt.args.suppliedGas != 0 { + suppliedGas = tt.args.suppliedGas + } + ret, _, err := p.RunAndCalculateGas(tt.args.evm, tt.args.caller, tt.args.caller, append(p.GetExecutor().(*addr.PrecompileExecutor).AssociatePubKeyID, inputs...), suppliedGas, tt.args.value, nil, tt.args.readOnly, false) if (err != nil) != tt.wantErr { t.Errorf("Run() error = %v, wantErr %v %v", err, tt.wantErr, string(ret)) return diff --git a/precompiles/bank/bank.go b/precompiles/bank/bank.go index 35462e89cd..760e17ef5e 100644 --- a/precompiles/bank/bank.go +++ b/precompiles/bank/bank.go @@ -103,6 +103,11 @@ func (p PrecompileExecutor) RequiredGas(input []byte, method *abi.Method) uint64 } func (p PrecompileExecutor) Execute(ctx sdk.Context, method *abi.Method, caller common.Address, callingContract common.Address, args []interface{}, value *big.Int, readOnly bool, evm *vm.EVM, suppliedGas uint64) (ret []byte, remainingGas uint64, err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("execution reverted: %v", r) + } + }() switch method.Name { case SendMethod: return p.send(ctx, caller, method, args, value, readOnly) diff --git a/precompiles/oracle/oracle.go b/precompiles/oracle/oracle.go index 8505f5589d..07f0902ed5 100644 --- a/precompiles/oracle/oracle.go +++ b/precompiles/oracle/oracle.go @@ -2,6 +2,7 @@ package oracle import ( "embed" + "fmt" "math/big" sdk "github.com/cosmos/cosmos-sdk/types" @@ -79,6 +80,11 @@ func (p PrecompileExecutor) RequiredGas(input []byte, method *abi.Method) uint64 } func (p PrecompileExecutor) Execute(ctx sdk.Context, method *abi.Method, caller common.Address, callingContract common.Address, args []interface{}, value *big.Int, readOnly bool, evm *vm.EVM, suppliedGas uint64) (bz []byte, remainingGas uint64, err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("execution reverted: %v", r) + } + }() switch method.Name { case GetExchangeRatesMethod: return p.getExchangeRates(ctx, method, args, value) From 5bf52bfad1b82af4a76c028ee884551b44936eaa Mon Sep 17 00:00:00 2001 From: Jeremy Wei Date: Mon, 27 Jan 2025 16:01:28 -0500 Subject: [PATCH 2/3] fix --- contracts/test/EVMPrecompileTest.js | 58 ++++++++++++++++++++--------- contracts/test/lib.js | 40 ++++++++------------ precompiles/addr/addr.go | 1 + precompiles/bank/bank.go | 1 + precompiles/oracle/oracle.go | 1 + 5 files changed, 58 insertions(+), 43 deletions(-) diff --git a/contracts/test/EVMPrecompileTest.js b/contracts/test/EVMPrecompileTest.js index 7d577199ad..9cf6c2b73b 100755 --- a/contracts/test/EVMPrecompileTest.js +++ b/contracts/test/EVMPrecompileTest.js @@ -4,7 +4,7 @@ const fs = require('fs'); const path = require('path'); const { expectRevert } = require('@openzeppelin/test-helpers'); -const { setupSigners, getAdmin, deployWasm, storeWasm, execute, isDocker, ABI, createTokenFactoryTokenAndMint, getSeiBalance} = require("./lib"); +const { setupSigners, getAdmin, deployWasm, storeWasm, execute, isDocker, ABI, createTokenFactoryTokenAndMint, getSeiBalance, rawHttpDebugTraceWithCallTracer} = require("./lib"); describe("EVM Precompile Tester", function () { @@ -17,6 +17,32 @@ describe("EVM Precompile Tester", function () { admin = await getAdmin(); }) + describe("EVM Bank Precompile Tester", function () { + const BankPrecompileContract = '0x0000000000000000000000000000000000001001'; + let bank; + + before(async function () { + const signer = accounts[0].signer + const contractABIPath = '../../precompiles/bank/abi.json'; + const contractABI = require(contractABIPath); + // Get a contract instance + bank = new ethers.Contract(BankPrecompileContract, contractABI, signer); + }); + + it("Fails with 'execution reverted' not 'panic occurred' when insufficient gas is provided", async function () { + try { + const bankSendTx = await bank.sendNative(accounts[1].seiAddress, {value: 1, gasLimit: 40000}); + await bankSendTx.wait(); + } catch (error) { + const txHash = error.receipt.hash + // should not get "panic occurred" + const trace = await rawHttpDebugTraceWithCallTracer(txHash) + expect(trace.result.error).to.not.include("panic") + expect(trace.result.error).to.include("execution reverted") + } + }); + }); + describe("EVM Addr Precompile Tester", function () { const AddrPrecompileContract = '0x0000000000000000000000000000000000001004'; let addr; @@ -73,27 +99,23 @@ describe("EVM Precompile Tester", function () { expect(seiAddr).to.not.be.null; }); - it.only("Fails gracefully even when insufficient gas is provided", async function () { + it("Fails with 'execution reverted' not 'panic occurred' when insufficient gas is provided", async function () { const unassociatedWallet = hre.ethers.Wallet.createRandom(); - console.log("DEBUG: unassociatedWallet", unassociatedWallet.publicKey.slice(2)); - await expectRevert( - addr.associatePubKey(unassociatedWallet.publicKey.slice(2), {gasLimit: 52603}), - "execution reverted" - ) - const associatedAddrs = await addr.associatePubKey(unassociatedWallet.publicKey.slice(2), {gasLimit: 52603}); - const receipt = await associatedAddrs.wait(); - const hash = receipt.transactionHash; - // ensure that the transaction's trace doesn't have "panic" in it - // make http call to debug_traceTransaction - const trace = await hre.ethers.provider.send("debug_traceTransaction", [hash]); - // expect trace to not contain the word "panic" - expect(trace).to.not.have.property('panic'); + try { + // provide less than gas than needed to execute the transaction + const associatedAddrs = await addr.associatePubKey(unassociatedWallet.publicKey.slice(2), {gasLimit: 52000}); + await associatedAddrs.wait(); + expect.fail("Expected an error here since we provided insufficient gas"); + } catch (error) { + const txHash = error.receipt.hash + // should not get "panic occurred" + const trace = await rawHttpDebugTraceWithCallTracer(txHash) + expect(trace.result.error).to.not.include("panic"); + expect(trace.result.error).to.include("execution reverted"); + } }); }); - // used to send a transaction that goes directly to SendTransaction - function sendCustomTransaction() - describe("EVM Gov Precompile Tester", function () { const GovPrecompileContract = '0x0000000000000000000000000000000000001006'; let gov; diff --git a/contracts/test/lib.js b/contracts/test/lib.js index 6047e790dd..e6ac3fbb75 100644 --- a/contracts/test/lib.js +++ b/contracts/test/lib.js @@ -1,5 +1,6 @@ const { exec } = require("child_process"); const {ethers} = require("hardhat"); // Importing exec from child_process +const axios = require("axios"); const adminKeyName = "admin" @@ -198,6 +199,19 @@ async function incrementPointerVersion(provider, pointerType, offset) { } } +async function rawHttpDebugTraceWithCallTracer(txHash) { + const payload = { + jsonrpc: "2.0", + method: "debug_traceTransaction", + params: [txHash, {"tracer": "callTracer"}], // The second parameter is an optional trace config object + id: 1, + }; + const response = await axios.post("http://localhost:8545", payload, { + headers: { "Content-Type": "application/json" }, + }); + return response.data; +} + async function createTokenFactoryTokenAndMint(name, amount, recipient, from=adminKeyName) { const command = `seid tx tokenfactory create-denom ${name} --from ${from} --gas=5000000 --fees=1000000usei -y --broadcast-mode block -o json` const output = await execute(command); @@ -439,31 +453,6 @@ async function deployEvmContract(name, args=[]) { return contract; } -async function sendNonSimulatedLegacyTransaction(signer, to, value, gasLimit) { - const chainId = await getChainId(); - const gasPrice = await getGasPrice(); - const nonce = await signer.getTransactionCount("pending"); - const signedTx = await signer.signTransaction({ - to: to, - value: value, - gasLimit: gasLimit, - gasPrice: gasPrice, - nonce: nonce, - maxPriorityFeePerGas: maxPriorityFeePerGas, - maxFeePerGas: maxFeePerGas, - chainId: chainId - }); - // send the transaction - const nodeUrl = 'http://localhost:8545'; - const response = await axios.post(nodeUrl, { - method: 'eth_sendRawTransaction', - params: [signedTx], - id: 1, - jsonrpc: "2.0" - }) - return response.data.result; -} - async function setupSigners(signers) { const result = [] for(let signer of signers) { @@ -579,6 +568,7 @@ module.exports = { getSeiAddress, getEvmAddress, queryWasm, + rawHttpDebugTraceWithCallTracer, executeWasm, getAdmin, setupSigners, diff --git a/precompiles/addr/addr.go b/precompiles/addr/addr.go index ed4a5966ee..d37aeb87d4 100644 --- a/precompiles/addr/addr.go +++ b/precompiles/addr/addr.go @@ -89,6 +89,7 @@ func (p PrecompileExecutor) RequiredGas(input []byte, method *abi.Method) uint64 } func (p PrecompileExecutor) Execute(ctx sdk.Context, method *abi.Method, _ common.Address, _ common.Address, args []interface{}, value *big.Int, readOnly bool, _ *vm.EVM, suppliedGas uint64) (ret []byte, remainingGas uint64, err error) { + // Needed to catch gas meter panics defer func() { if r := recover(); r != nil { err = fmt.Errorf("execution reverted: %v", r) diff --git a/precompiles/bank/bank.go b/precompiles/bank/bank.go index 760e17ef5e..cbf0f945da 100644 --- a/precompiles/bank/bank.go +++ b/precompiles/bank/bank.go @@ -103,6 +103,7 @@ func (p PrecompileExecutor) RequiredGas(input []byte, method *abi.Method) uint64 } func (p PrecompileExecutor) Execute(ctx sdk.Context, method *abi.Method, caller common.Address, callingContract common.Address, args []interface{}, value *big.Int, readOnly bool, evm *vm.EVM, suppliedGas uint64) (ret []byte, remainingGas uint64, err error) { + // Needed to catch gas meter panics defer func() { if r := recover(); r != nil { err = fmt.Errorf("execution reverted: %v", r) diff --git a/precompiles/oracle/oracle.go b/precompiles/oracle/oracle.go index 07f0902ed5..2b9adb3025 100644 --- a/precompiles/oracle/oracle.go +++ b/precompiles/oracle/oracle.go @@ -80,6 +80,7 @@ func (p PrecompileExecutor) RequiredGas(input []byte, method *abi.Method) uint64 } func (p PrecompileExecutor) Execute(ctx sdk.Context, method *abi.Method, caller common.Address, callingContract common.Address, args []interface{}, value *big.Int, readOnly bool, evm *vm.EVM, suppliedGas uint64) (bz []byte, remainingGas uint64, err error) { + // Needed to catch gas meter panics defer func() { if r := recover(); r != nil { err = fmt.Errorf("execution reverted: %v", r) From 1e3997fe1926d8df5516139501c8a138748de49d Mon Sep 17 00:00:00 2001 From: Jeremy Wei Date: Mon, 27 Jan 2025 16:11:24 -0500 Subject: [PATCH 3/3] fix --- precompiles/addr/addr_test.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/precompiles/addr/addr_test.go b/precompiles/addr/addr_test.go index f57852bc4a..4063ca3479 100644 --- a/precompiles/addr/addr_test.go +++ b/precompiles/addr/addr_test.go @@ -124,11 +124,13 @@ func TestAssociatePubKey(t *testing.T) { StateDB: state.NewDBImpl(ctx, k, true), TxContext: vm.TxContext{Origin: callerEvmAddress}, }, - caller: callerEvmAddress, - pubKey: targetPubKeyHex, - value: big.NewInt(0), + caller: callerEvmAddress, + pubKey: targetPubKeyHex, + value: big.NewInt(0), suppliedGas: 1, }, + wantErr: true, + wantErrMsg: "execution reverted: {ReadFlat}", }, { name: "happy path - associates addresses if signature is correct",