From fc36b39ec4f793d94173919c8704a7b470a2956f Mon Sep 17 00:00:00 2001 From: codchen Date: Tue, 13 Aug 2024 23:05:28 +0800 Subject: [PATCH] Allow CW->ERC pointers to be called through wasmd precompile (#1785) --- app/receipt_test.go | 3 +++ contracts/test/CW20toERC20PointerTest.js | 21 +++++++++++++++++ evmrpc/simulate.go | 18 ++++++++++---- precompiles/wasmd/wasmd.go | 16 ++++++++++--- precompiles/wasmd/wasmd_test.go | 1 + x/evm/keeper/evm.go | 21 +++++++++++++++-- x/evm/keeper/msg_server.go | 30 +++++++++++++++++++++++- x/evm/keeper/receipt.go | 5 ++++ 8 files changed, 105 insertions(+), 10 deletions(-) diff --git a/app/receipt_test.go b/app/receipt_test.go index ec791cef76..625818286b 100644 --- a/app/receipt_test.go +++ b/app/receipt_test.go @@ -110,10 +110,12 @@ func TestEvmEventsForCw20(t *testing.T) { tx = txBuilder.GetTx() txbz, err = testkeeper.EVMTestApp.GetTxConfig().TxEncoder()(tx) require.Nil(t, err) + sum = sha256.Sum256(txbz) res = testkeeper.EVMTestApp.DeliverTx(ctx.WithEventManager(sdk.NewEventManager()).WithTxIndex(1), abci.RequestDeliverTx{Tx: txbz}, tx, sum) require.Equal(t, uint32(0), res.Code) receipt, err = testkeeper.EVMTestApp.EvmKeeper.GetTransientReceipt(ctx, signedTx.Hash()) require.Nil(t, err) + fmt.Println(receipt.Logs) require.Equal(t, 1, len(receipt.Logs)) require.NotEmpty(t, receipt.LogsBloom) require.Equal(t, mockPointerAddr.Hex(), receipt.Logs[0].Address) @@ -225,6 +227,7 @@ func TestEvmEventsForCw721(t *testing.T) { tx = txBuilder.GetTx() txbz, err = testkeeper.EVMTestApp.GetTxConfig().TxEncoder()(tx) require.Nil(t, err) + sum = sha256.Sum256(txbz) res = testkeeper.EVMTestApp.DeliverTx(ctx.WithEventManager(sdk.NewEventManager()).WithTxIndex(1), abci.RequestDeliverTx{Tx: txbz}, tx, sum) require.Equal(t, uint32(0), res.Code) receipt, err = testkeeper.EVMTestApp.EvmKeeper.GetTransientReceipt(ctx, signedTx.Hash()) diff --git a/contracts/test/CW20toERC20PointerTest.js b/contracts/test/CW20toERC20PointerTest.js index 8c695260e9..0638aa38fe 100644 --- a/contracts/test/CW20toERC20PointerTest.js +++ b/contracts/test/CW20toERC20PointerTest.js @@ -164,6 +164,27 @@ describe("CW20 to ERC20 Pointer", function () { const balanceAfter = respAfter.data.balance; expect(balanceAfter).to.equal((parseInt(balanceBefore) - 100).toString()); }); + + it("should transfer if called through wasmd precompile", async function() { + const WasmPrecompileContract = '0x0000000000000000000000000000000000001002'; + const contractABIPath = '../../precompiles/wasmd/abi.json'; + const contractABI = require(contractABIPath); + wasmd = new ethers.Contract(WasmPrecompileContract, contractABI, accounts[0].signer); + + const encoder = new TextEncoder(); + + const transferMsg = { transfer: { recipient: accounts[1].seiAddress, amount: "100" } }; + const transferStr = JSON.stringify(transferMsg); + const transferBz = encoder.encode(transferStr); + + const coins = []; + const coinsStr = JSON.stringify(coins); + const coinsBz = encoder.encode(coinsStr); + + const response = await wasmd.execute(pointer, transferBz, coinsBz); + const receipt = await response.wait(); + expect(receipt.status).to.equal(1); + }); }); }); } diff --git a/evmrpc/simulate.go b/evmrpc/simulate.go index 428ebc0ac5..e78c500f31 100644 --- a/evmrpc/simulate.go +++ b/evmrpc/simulate.go @@ -8,6 +8,7 @@ import ( "strings" "time" + "github.com/sei-protocol/sei-chain/precompiles/wasmd" "github.com/sei-protocol/sei-chain/utils/helpers" sdk "github.com/cosmos/cosmos-sdk/types" @@ -34,6 +35,10 @@ import ( "github.com/tendermint/tendermint/rpc/coretypes" ) +type CtxIsWasmdPrecompileCallKeyType string + +const CtxIsWasmdPrecompileCallKey CtxIsWasmdPrecompileCallKeyType = "CtxIsWasmdPrecompileCallKey" + type SimulationAPI struct { backend *Backend connectionType ConnectionType @@ -66,6 +71,7 @@ func (s *SimulationAPI) CreateAccessList(ctx context.Context, args ethapi.Transa if blockNrOrHash != nil { bNrOrHash = *blockNrOrHash } + ctx = context.WithValue(ctx, CtxIsWasmdPrecompileCallKey, wasmd.IsWasmdCall(args.To)) acl, gasUsed, vmerr, err := ethapi.AccessList(ctx, s.backend, bNrOrHash, args) if err != nil { return nil, err @@ -84,6 +90,7 @@ func (s *SimulationAPI) EstimateGas(ctx context.Context, args ethapi.Transaction if blockNrOrHash != nil { bNrOrHash = *blockNrOrHash } + ctx = context.WithValue(ctx, CtxIsWasmdPrecompileCallKey, wasmd.IsWasmdCall(args.To)) estimate, err := ethapi.DoEstimateGas(ctx, s.backend, args, bNrOrHash, overrides, s.backend.RPCGasCap()) return estimate, err } @@ -104,6 +111,7 @@ func (s *SimulationAPI) Call(ctx context.Context, args ethapi.TransactionArgs, b latest := rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber) blockNrOrHash = &latest } + ctx = context.WithValue(ctx, CtxIsWasmdPrecompileCallKey, wasmd.IsWasmdCall(args.To)) callResult, err := ethapi.DoCall(ctx, s.backend, args, *blockNrOrHash, overrides, blockOverrides, s.backend.RPCEVMTimeout(), s.backend.RPCGasCap()) if err != nil { return nil, err @@ -176,11 +184,12 @@ func (b *Backend) StateAndHeaderByNumberOrHash(ctx context.Context, blockNrOrHas if err != nil { return nil, nil, err } - sdkCtx := b.ctxProvider(height) + isWasmdCall, ok := ctx.Value(CtxIsWasmdPrecompileCallKey).(bool) + sdkCtx := b.ctxProvider(height).WithIsEVM(true).WithEVMEntryViaWasmdPrecompile(ok && isWasmdCall) if err := CheckVersion(sdkCtx, b.keeper); err != nil { return nil, nil, err } - return state.NewDBImpl(b.ctxProvider(height), b.keeper, true), b.getHeader(big.NewInt(height)), nil + return state.NewDBImpl(sdkCtx, b.keeper, true), b.getHeader(big.NewInt(height)), nil } func (b *Backend) GetTransaction(ctx context.Context, txHash common.Hash) (tx *ethtypes.Transaction, blockHash common.Hash, blockNumber uint64, index uint64, err error) { @@ -291,7 +300,7 @@ func (b *Backend) StateAtTransaction(ctx context.Context, block *ethtypes.Block, // get the parent block using block.parentHash prevBlockHeight := block.Number().Int64() - 1 // Get statedb of parent block from the store - statedb := state.NewDBImpl(b.ctxProvider(prevBlockHeight), b.keeper, true) + statedb := state.NewDBImpl(b.ctxProvider(prevBlockHeight).WithIsEVM(true), b.keeper, true) if txIndex == 0 && len(block.Transactions()) == 0 { return nil, vm.BlockContext{}, statedb, emptyRelease, nil } @@ -329,6 +338,7 @@ func (b *Backend) StateAtTransaction(ctx context.Context, block *ethtypes.Block, if idx == txIndex { return tx, *blockContext, statedb, emptyRelease, nil } + statedb.WithCtx(statedb.Ctx().WithEVMEntryViaWasmdPrecompile(wasmd.IsWasmdCall(tx.To()))) // Not yet the searched for transaction, execute on top of the current state vmenv := vm.NewEVM(*blockContext, txContext, statedb, b.ChainConfig(), vm.Config{}) statedb.SetTxContext(tx.Hash(), idx) @@ -371,7 +381,7 @@ func (b *Backend) StateAtBlock(ctx context.Context, block *ethtypes.Block, reexe func (b *Backend) GetEVM(_ context.Context, msg *core.Message, stateDB vm.StateDB, _ *ethtypes.Header, vmConfig *vm.Config, blockCtx *vm.BlockContext) *vm.EVM { txContext := core.NewEVMTxContext(msg) if blockCtx == nil { - blockCtx, _ = b.keeper.GetVMBlockContext(b.ctxProvider(LatestCtxHeight), core.GasPool(b.RPCGasCap())) + blockCtx, _ = b.keeper.GetVMBlockContext(b.ctxProvider(LatestCtxHeight).WithIsEVM(true).WithEVMEntryViaWasmdPrecompile(wasmd.IsWasmdCall(msg.To)), core.GasPool(b.RPCGasCap())) } return vm.NewEVM(*blockCtx, txContext, stateDB, b.ChainConfig(), *vmConfig) } diff --git a/precompiles/wasmd/wasmd.go b/precompiles/wasmd/wasmd.go index 279132e8db..d28f00684a 100644 --- a/precompiles/wasmd/wasmd.go +++ b/precompiles/wasmd/wasmd.go @@ -27,6 +27,8 @@ const ( const WasmdAddress = "0x0000000000000000000000000000000000001002" +var Address = common.HexToAddress(WasmdAddress) + // Embed abi json file to the executable binary. Needed when importing as dependency. // //go:embed abi.json @@ -51,15 +53,19 @@ type ExecuteMsg struct { Coins []byte `json:"coins"` } +func GetABI() abi.ABI { + return pcommon.MustGetABI(f, "abi.json") +} + func NewPrecompile(evmKeeper pcommon.EVMKeeper, wasmdKeeper pcommon.WasmdKeeper, wasmdViewKeeper pcommon.WasmdViewKeeper, bankKeeper pcommon.BankKeeper) (*pcommon.DynamicGasPrecompile, error) { - newAbi := pcommon.MustGetABI(f, "abi.json") + newAbi := GetABI() executor := &PrecompileExecutor{ wasmdKeeper: wasmdKeeper, wasmdViewKeeper: wasmdViewKeeper, evmKeeper: evmKeeper, bankKeeper: bankKeeper, - address: common.HexToAddress(WasmdAddress), + address: Address, } for name, m := range newAbi.Methods { @@ -74,7 +80,7 @@ func NewPrecompile(evmKeeper pcommon.EVMKeeper, wasmdKeeper pcommon.WasmdKeeper, executor.QueryID = m.ID } } - return pcommon.NewDynamicGasPrecompile(newAbi, executor, common.HexToAddress(WasmdAddress), "wasmd"), nil + return pcommon.NewDynamicGasPrecompile(newAbi, executor, Address, "wasmd"), nil } 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) { @@ -452,3 +458,7 @@ func (p PrecompileExecutor) query(ctx sdk.Context, method *abi.Method, args []in remainingGas = pcommon.GetRemainingGas(ctx, p.evmKeeper) return } + +func IsWasmdCall(to *common.Address) bool { + return to != nil && (to.Cmp(Address) == 0) +} diff --git a/precompiles/wasmd/wasmd_test.go b/precompiles/wasmd/wasmd_test.go index b653ed14d7..1e6670c51b 100644 --- a/precompiles/wasmd/wasmd_test.go +++ b/precompiles/wasmd/wasmd_test.go @@ -154,6 +154,7 @@ func TestExecute(t *testing.T) { testApp.BankKeeper.SendCoins(ctx, mockAddr, testApp.EvmKeeper.GetSeiAddressOrDefault(ctx, common.HexToAddress(wasmd.WasmdAddress)), amts) // circular interop statedb.WithCtx(statedb.Ctx().WithIsEVM(false)) + testApp.EvmKeeper.SetCode(statedb.Ctx(), mockEVMAddr, []byte{1, 2, 3}) res, _, err := p.RunAndCalculateGas(&evm, mockEVMAddr, mockEVMAddr, append(p.GetExecutor().(*wasmd.PrecompileExecutor).ExecuteID, args...), suppliedGas, big.NewInt(1000_000_000_000_000), nil, false) require.Equal(t, "sei does not support CW->EVM->CW call pattern", string(res)) require.Equal(t, vm.ErrExecutionReverted, err) diff --git a/x/evm/keeper/evm.go b/x/evm/keeper/evm.go index 97695dc14f..1479121400 100644 --- a/x/evm/keeper/evm.go +++ b/x/evm/keeper/evm.go @@ -73,7 +73,7 @@ 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 ctx.IsEVM() { + if ctx.IsEVM() && !ctx.EVMEntryViaWasmdPrecompile() { return nil, errors.New("sei does not support EVM->CW->EVM call pattern") } if to == nil && len(data) > params.MaxInitCodeSize { @@ -87,7 +87,7 @@ func (k *Keeper) CallEVM(ctx sdk.Context, from common.Address, to *common.Addres value = val.BigInt() } // This call was not part of an existing StateTransition, so it should trigger one - executionCtx := ctx.WithGasMeter(sdk.NewInfiniteGasMeterWithMultiplier(ctx)) + executionCtx := ctx.WithGasMeter(sdk.NewInfiniteGasMeterWithMultiplier(ctx)).WithEVMEntryViaWasmdPrecompile(false) stateDB := state.NewDBImpl(executionCtx, k, false) gp := k.GetGasPool() evmMsg := &core.Message{ @@ -118,6 +118,23 @@ func (k *Keeper) CallEVM(ctx sdk.Context, from common.Address, to *common.Addres if res.Err != nil { vmErr = res.Err.Error() } + existingReceipt, err := k.GetTransientReceipt(ctx, ctx.TxSum()) + if err == nil { + for _, l := range existingReceipt.Logs { + stateDB.AddLog(ðtypes.Log{ + Address: common.HexToAddress(l.Address), + Topics: utils.Map(l.Topics, common.HexToHash), + Data: l.Data, + }) + } + if existingReceipt.VmError != "" { + vmErr = fmt.Sprintf("%s\n%s\n", existingReceipt.VmError, vmErr) + } + } + existingDeferredInfo, found := k.GetEVMTxDeferredInfo(ctx) + if found { + surplus = surplus.Add(existingDeferredInfo.Surplus) + } receipt, err := k.WriteReceipt(ctx, stateDB, evmMsg, ethtypes.LegacyTxType, ctx.TxSum(), res.UsedGas, vmErr) if err != nil { return nil, err diff --git a/x/evm/keeper/msg_server.go b/x/evm/keeper/msg_server.go index 293c62e14f..358a9f0010 100644 --- a/x/evm/keeper/msg_server.go +++ b/x/evm/keeper/msg_server.go @@ -21,6 +21,8 @@ import ( "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/precompiles/wasmd" + "github.com/sei-protocol/sei-chain/utils" "github.com/sei-protocol/sei-chain/x/evm/artifacts/erc20" "github.com/sei-protocol/sei-chain/x/evm/artifacts/erc721" "github.com/sei-protocol/sei-chain/x/evm/state" @@ -45,6 +47,11 @@ func (server msgServer) EVMTransaction(goCtx context.Context, msg *types.MsgEVMT return &types.MsgEVMTransactionResponse{}, nil } ctx := sdk.UnwrapSDKContext(goCtx) + tx, _ := msg.AsTransaction() + isWasmdPrecompileCall := wasmd.IsWasmdCall(tx.To()) + if isWasmdPrecompileCall { + ctx = ctx.WithEVMEntryViaWasmdPrecompile(true) + } // EVM has a special case here, mainly because for an EVM transaction the gas limit is set on EVM payload level, not on top-level GasWanted field // as normal transactions (because existing eth client can't). As a result EVM has its own dedicated ante handler chain. The full sequence is: @@ -56,7 +63,6 @@ func (server msgServer) EVMTransaction(goCtx context.Context, msg *types.MsgEVMT ctx = ctx.WithGasMeter(sdk.NewInfiniteGasMeterWithMultiplier(ctx)) stateDB := state.NewDBImpl(ctx, &server, false) - tx, _ := msg.AsTransaction() emsg := server.GetEVMMessage(ctx, tx, msg.Derived.SenderEVMAddr) gp := server.GetGasPool() @@ -82,6 +88,27 @@ func (server msgServer) EVMTransaction(goCtx context.Context, msg *types.MsgEVMT ) return } + extraSurplus := sdk.ZeroInt() + if isWasmdPrecompileCall { + syntheticReceipt, err := server.GetTransientReceipt(ctx, ctx.TxSum()) + if err == nil { + for _, l := range syntheticReceipt.Logs { + stateDB.AddLog(ðtypes.Log{ + Address: common.HexToAddress(l.Address), + Topics: utils.Map(l.Topics, common.HexToHash), + Data: l.Data, + }) + } + if syntheticReceipt.VmError != "" { + serverRes.VmError = fmt.Sprintf("%s\n%s\n", serverRes.VmError, syntheticReceipt.VmError) + } + server.DeleteTransientReceipt(ctx, ctx.TxSum()) + } + syntheticDeferredInfo, found := server.GetEVMTxDeferredInfo(ctx) + if found { + extraSurplus = extraSurplus.Add(syntheticDeferredInfo.Surplus) + } + } receipt, rerr := server.WriteReceipt(ctx, stateDB, emsg, uint32(tx.Type()), tx.Hash(), serverRes.GasUsed, serverRes.VmError) if rerr != nil { err = rerr @@ -118,6 +145,7 @@ func (server msgServer) EVMTransaction(goCtx context.Context, msg *types.MsgEVMT ) return } + surplus = surplus.Add(extraSurplus) bloom := ethtypes.Bloom{} bloom.SetBytes(receipt.LogsBloom) server.AppendToEvmTxDeferredInfo(ctx, bloom, tx.Hash(), surplus) diff --git a/x/evm/keeper/receipt.go b/x/evm/keeper/receipt.go index fa2c79e2b6..1f01342097 100644 --- a/x/evm/keeper/receipt.go +++ b/x/evm/keeper/receipt.go @@ -41,6 +41,11 @@ func (k *Keeper) GetTransientReceipt(ctx sdk.Context, txHash common.Hash) (*type return r, nil } +func (k *Keeper) DeleteTransientReceipt(ctx sdk.Context, txHash common.Hash) { + store := ctx.TransientStore(k.transientStoreKey) + store.Delete(types.ReceiptKey(txHash)) +} + // GetReceipt returns a data structure that stores EVM specific transaction metadata. // Many EVM applications (e.g. MetaMask) relies on being on able to query receipt // by EVM transaction hash (not Sei transaction hash) to function properly.