diff --git a/evmd/app.go b/evmd/app.go index fe494f779..2fd75014e 100644 --- a/evmd/app.go +++ b/evmd/app.go @@ -32,6 +32,7 @@ import ( "github.com/cosmos/evm/x/feemarket" feemarketkeeper "github.com/cosmos/evm/x/feemarket/keeper" feemarkettypes "github.com/cosmos/evm/x/feemarket/types" + // NOTE: override ICS20 keeper to support IBC transfers of ERC20 tokens "github.com/cosmos/evm/x/ibc/transfer" transferkeeper "github.com/cosmos/evm/x/ibc/transfer/keeper" @@ -154,7 +155,7 @@ var ( // module account permissions maccPerms = map[string][]string{ - authtypes.FeeCollectorName: nil, + authtypes.FeeCollectorName: {authtypes.Burner}, distrtypes.ModuleName: nil, ibctransfertypes.ModuleName: {authtypes.Minter, authtypes.Burner}, minttypes.ModuleName: {authtypes.Minter}, diff --git a/rpc/backend/blocks.go b/rpc/backend/blocks.go index 31b692a21..b1d552123 100644 --- a/rpc/backend/blocks.go +++ b/rpc/backend/blocks.go @@ -337,6 +337,37 @@ func (b *Backend) parseDerivedTxFromAdditionalFields( ethMsg.From = additional.Sender.Hex() return ethMsg } +func (b *Backend) parseDerivedTxFromAdditionalFieldsForTrace( + additional *rpctypes.TxResultAdditionalFields, +) *evmtypes.MsgEthereumTx { + recipient := additional.Recipient + + // for transactions before v31 this value was mistakenly used for Gas field + gas := additional.GasUsed + if additional.GasLimit != nil { + gas = *additional.GasLimit + } + t := ethtypes.NewTx(ðtypes.LegacyTx{ + Nonce: additional.Nonce, + Data: additional.Data, + Gas: gas, + To: &recipient, + GasPrice: nil, + Value: additional.Value, + V: big.NewInt(0), + R: big.NewInt(0), + S: big.NewInt(0), + }) + ethMsg := &evmtypes.MsgEthereumTx{} + err := ethMsg.FromEthereumTx(t) + if err != nil { + b.logger.Error("can not create eth msg", err.Error()) + return nil + } + ethMsg.Hash = additional.Hash.Hex() + ethMsg.From = additional.Sender.Hex() + return ethMsg +} // HeaderByNumber returns the block header identified by height. func (b *Backend) HeaderByNumber(blockNum rpctypes.BlockNumber) (*ethtypes.Header, error) { diff --git a/rpc/backend/tracing.go b/rpc/backend/tracing.go index c3885cfa3..12170509c 100644 --- a/rpc/backend/tracing.go +++ b/rpc/backend/tracing.go @@ -21,7 +21,7 @@ import ( // and returns them as a JSON object. func (b *Backend) TraceTransaction(hash common.Hash, config *evmtypes.TraceConfig) (interface{}, error) { // Get transaction by hash - transaction, _, err := b.GetTxByEthHash(hash) + transaction, additional, err := b.GetTxByEthHash(hash) if err != nil { b.logger.Debug("tx not found", "hash", hash) return nil, err @@ -49,19 +49,39 @@ func (b *Backend) TraceTransaction(hash common.Hash, config *evmtypes.TraceConfi } var predecessors []*evmtypes.MsgEthereumTx - for _, txBz := range blk.Block.Txs[:transaction.TxIndex] { - tx, err := b.clientCtx.TxConfig.TxDecoder()(txBz) + for i := 0; i < int(transaction.TxIndex); i++ { + _, txAdditional, err := b.GetTxByTxIndex(blk.Block.Height, uint(i)) if err != nil { - b.logger.Debug("failed to decode transaction in block", "height", blk.Block.Height, "error", err.Error()) + b.logger.Debug("failed to get tx by index", + "height", blk.Block.Height, + "index", i, + "error", err.Error()) continue } - for _, msg := range tx.GetMsgs() { - ethMsg, ok := msg.(*evmtypes.MsgEthereumTx) - if !ok { - continue + + if txAdditional != nil { + // Handle synthetic EVM transaction + ethMsg := b.parseDerivedTxFromAdditionalFieldsForTrace(txAdditional) + if ethMsg != nil { + predecessors = append(predecessors, ethMsg) } + continue + } - predecessors = append(predecessors, ethMsg) + // Fallback: decode as normal Cosmos tx + tx, err := b.clientCtx.TxConfig.TxDecoder()(blk.Block.Txs[i]) + if err != nil { + b.logger.Debug("failed to decode transaction in block", + "height", blk.Block.Height, + "index", i, + "error", err.Error()) + continue + } + + for _, msg := range tx.GetMsgs() { + if ethMsg, ok := msg.(*evmtypes.MsgEthereumTx); ok { + predecessors = append(predecessors, ethMsg) + } } } @@ -73,18 +93,43 @@ func (b *Backend) TraceTransaction(hash common.Hash, config *evmtypes.TraceConfi // add predecessor messages in current cosmos tx index := int(transaction.MsgIndex) // #nosec G115 + for i := 0; i < index; i++ { - ethMsg, ok := tx.GetMsgs()[i].(*evmtypes.MsgEthereumTx) - if !ok { + msg := tx.GetMsgs()[i] + // Check if it’s a normal Ethereum tx + if ethMsg, ok := msg.(*evmtypes.MsgEthereumTx); ok { + predecessors = append(predecessors, ethMsg) continue } - predecessors = append(predecessors, ethMsg) + // Fetch additional data for predecessors + _, txAdditional, err := b.GetTxByEthHashAndMsgIndex(hash, i) + if err != nil { + b.logger.Debug("failed to get tx additional info", "error", err.Error()) + continue + } + + if txAdditional != nil { + ethMsg := b.parseDerivedTxFromAdditionalFieldsForTrace(txAdditional) + if ethMsg != nil { + predecessors = append(predecessors, ethMsg) + } + } } + var ethMessage *evmtypes.MsgEthereumTx + var ok bool - ethMessage, ok := tx.GetMsgs()[transaction.MsgIndex].(*evmtypes.MsgEthereumTx) - if !ok { - b.logger.Debug("invalid transaction type", "type", fmt.Sprintf("%T", tx)) - return nil, fmt.Errorf("invalid transaction type %T", tx) + if additional == nil { + ethMessage, ok = tx.GetMsgs()[transaction.MsgIndex].(*evmtypes.MsgEthereumTx) + if !ok { + b.logger.Debug("invalid transaction type", "type", fmt.Sprintf("%T", tx.GetMsgs()[transaction.MsgIndex])) + return nil, fmt.Errorf("invalid transaction type %T", tx.GetMsgs()[transaction.MsgIndex]) + } + } else { + ethMessage = b.parseDerivedTxFromAdditionalFieldsForTrace(additional) + if ethMessage == nil { + b.logger.Error("failed to get derived eth msg from additional fields") + return nil, fmt.Errorf("failed to get derived eth msg from additional fields") + } } nc, ok := b.clientCtx.Client.(tmrpcclient.NetworkClient) diff --git a/rpc/backend/tx_info.go b/rpc/backend/tx_info.go index bf556e3a8..923feb239 100644 --- a/rpc/backend/tx_info.go +++ b/rpc/backend/tx_info.go @@ -430,6 +430,26 @@ func (b *Backend) GetTxByEthHash(hash common.Hash) (*types.TxResult, *rpctypes.T return txResult, txAdditional, nil } +func (b *Backend) GetTxByEthHashAndMsgIndex(hash common.Hash, index int) (*types.TxResult, *rpctypes.TxResultAdditionalFields, error) { + if b.indexer != nil { + txRes, err := b.indexer.GetByTxHash(hash) + if err != nil { + return nil, nil, err + } + return txRes, nil, nil + } + + // fallback to tendermint tx indexer + query := fmt.Sprintf("%s.%s='%s'", evmtypes.TypeMsgEthereumTx, evmtypes.AttributeKeyEthereumTxHash, hash.Hex()) + txResult, txAdditional, err := b.queryTendermintTxIndexer(query, func(txs *rpctypes.ParsedTxs) *rpctypes.ParsedTx { + return txs.GetTxByMsgIndex(index) + }) + if err != nil { + return nil, nil, errorsmod.Wrapf(err, "GetTxByEthHash %s", hash.Hex()) + } + return txResult, txAdditional, nil +} + // GetTxByTxIndex uses `/tx_query` to find transaction by tx index of valid ethereum txs func (b *Backend) GetTxByTxIndex(height int64, index uint) (*types.TxResult, *rpctypes.TxResultAdditionalFields, error) { int32Index := int32(index) //#nosec G115 -- checked for int overflow already diff --git a/rpc/types/events.go b/rpc/types/events.go index 97c45c239..c03f82222 100644 --- a/rpc/types/events.go +++ b/rpc/types/events.go @@ -74,6 +74,7 @@ type ParsedTx struct { Recipient common.Address Sender common.Address Nonce uint64 + GasLimit uint64 Data []byte } @@ -233,6 +234,7 @@ func ParseTxIndexerResult( GasUsed: parsedTx.GasUsed, Data: parsedTx.Data, Nonce: parsedTx.Nonce, + GasLimit: &parsedTx.GasLimit, }, nil } return &types.TxResult{ @@ -285,6 +287,7 @@ func ParseTxBlockResult( GasUsed: parsedTx.GasUsed, Data: parsedTx.Data, Nonce: parsedTx.Nonce, + GasLimit: &parsedTx.GasLimit, }, nil } return &types.TxResult{ @@ -411,6 +414,13 @@ func fillTxAttribute(tx *ParsedTx, key, value string) error { } tx.Nonce = nonce + case evmtypes.AttributeKeyTxGasLimit: + gasLimit, err := strconv.ParseUint(value, 10, 64) + if err != nil { + return err + } + tx.GasLimit = gasLimit + case evmtypes.AttributeKeyTxData: hexBytes, err := hexutil.Decode(value) if err != nil { diff --git a/rpc/types/types.go b/rpc/types/types.go index e2a3dee58..4d42ea724 100644 --- a/rpc/types/types.go +++ b/rpc/types/types.go @@ -40,6 +40,7 @@ type TxResultAdditionalFields struct { GasUsed uint64 `json:"gasUsed"` Nonce uint64 `json:"nonce"` Data []byte `json:"data"` + GasLimit *uint64 `json:"gasLimit"` } // RPCTransaction represents a transaction that will serialize to the RPC representation of a transaction diff --git a/x/vm/keeper/call_evm.go b/x/vm/keeper/call_evm.go index 286508bae..341c30d35 100644 --- a/x/vm/keeper/call_evm.go +++ b/x/vm/keeper/call_evm.go @@ -289,6 +289,7 @@ func (k Keeper) DerivedEVMCallWithData( attrs = append(attrs, sdk.NewAttribute(types.AttributeKeyTxData, hexutil.Encode(msg.Data()))) // adding nonce for more info in rpc methods in order to parse derived txs attrs = append(attrs, sdk.NewAttribute(types.AttributeKeyTxNonce, strconv.FormatUint(nonce, 10))) + attrs = append(attrs, sdk.NewAttribute(types.AttributeKeyTxGasLimit, strconv.FormatUint(gasCap, 10))) ctx.EventManager().EmitEvents(sdk.Events{ sdk.NewEvent( types.EventTypeEthereumTx, diff --git a/x/vm/keeper/grpc_query.go b/x/vm/keeper/grpc_query.go index 4c7c0028d..ec988cc1f 100644 --- a/x/vm/keeper/grpc_query.go +++ b/x/vm/keeper/grpc_query.go @@ -10,6 +10,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/common/math" "github.com/ethereum/go-ethereum/core" ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" @@ -487,10 +488,18 @@ func (k Keeper) TraceTx(c context.Context, req *types.QueryTraceTxRequest) (*typ for i, tx := range req.Predecessors { ethTx := tx.AsTransaction() - msg, err := ethTx.AsMessage(signer, cfg.BaseFee) - if err != nil { - continue + + var msg ethtypes.Message + // if tx is not unsigned, from field should be derived from signer, which can be done using AsMessage function + if !isUnsigned(ethTx) { + msg, err = ethTx.AsMessage(signer, cfg.BaseFee) + if err != nil { + continue + } + } else { + msg = unsignedTxAsMessage(common.BytesToAddress(req.Msg.GetFrom()), ethTx, cfg.BaseFee) } + txConfig.TxHash = ethTx.Hash() txConfig.TxIndex = uint(i) //nolint:gosec // G115 // won't exceed uint64 // reset gas meter for each transaction @@ -515,7 +524,7 @@ func (k Keeper) TraceTx(c context.Context, req *types.QueryTraceTxRequest) (*typ _ = json.Unmarshal([]byte(req.TraceConfig.TracerJsonConfig), &tracerConfig) } - result, _, err := k.traceTx(ctx, cfg, txConfig, signer, tx, req.TraceConfig, false, tracerConfig) + result, _, err := k.traceTx(ctx, cfg, txConfig, signer, common.BytesToAddress(req.Msg.GetFrom()), tx, req.TraceConfig, false, tracerConfig) if err != nil { // error will be returned with detail status from traceTx return nil, err @@ -582,7 +591,7 @@ func (k Keeper) TraceBlock(c context.Context, req *types.QueryTraceBlockRequest) ethTx := tx.AsTransaction() txConfig.TxHash = ethTx.Hash() txConfig.TxIndex = uint(i) //nolint:gosec // G115 // won't exceed uint64 - traceResult, logIndex, err := k.traceTx(ctx, cfg, txConfig, signer, ethTx, req.TraceConfig, true, nil) + traceResult, logIndex, err := k.traceTx(ctx, cfg, txConfig, signer, common.BytesToAddress(tx.GetFrom()), ethTx, req.TraceConfig, true, nil) if err != nil { result.Error = err.Error() } else { @@ -608,6 +617,7 @@ func (k *Keeper) traceTx( cfg *statedb.EVMConfig, txConfig statedb.TxConfig, signer ethtypes.Signer, + from common.Address, tx *ethtypes.Transaction, traceConfig *types.TraceConfig, commitMessage bool, @@ -620,9 +630,15 @@ func (k *Keeper) traceTx( err error timeout = defaultTraceTimeout ) - msg, err := tx.AsMessage(signer, cfg.BaseFee) - if err != nil { - return nil, 0, status.Error(codes.Internal, err.Error()) + var msg ethtypes.Message + // if tx is not unsigned, from field should be derived from signer, which can be done using AsMessage function + if !isUnsigned(tx) { + msg, err = tx.AsMessage(signer, cfg.BaseFee) + if err != nil { + return nil, 0, status.Error(codes.Internal, err.Error()) + } + } else { + msg = unsignedTxAsMessage(from, tx, cfg.BaseFee) } if traceConfig == nil { @@ -722,3 +738,29 @@ func (k Keeper) Config(_ context.Context, _ *types.QueryConfigRequest) (*types.Q return &types.QueryConfigResponse{Config: config}, nil } + +func isUnsigned(ethTx *ethtypes.Transaction) bool { + r, v, s := ethTx.RawSignatureValues() + + return (r == nil && v == nil && s == nil) || (r.Int64() == 0 && v.Int64() == 0 && s.Int64() == 0) +} +func unsignedTxAsMessage(fromAddress common.Address, tx *ethtypes.Transaction, baseFee *big.Int) ethtypes.Message { + gasPrice := new(big.Int).Set(tx.GasPrice()) + // If baseFee provided, set gasPrice to effectiveGasPrice. + if baseFee != nil { + gasPrice = math.BigMin(new(big.Int).Add(tx.GasTipCap(), baseFee), tx.GasFeeCap()) + } + return ethtypes.NewMessage( + fromAddress, + tx.To(), + tx.Nonce(), + tx.Value(), + tx.Gas(), + gasPrice, + tx.GasFeeCap(), + tx.GasTipCap(), + tx.Data(), + tx.AccessList(), + false, + ) +} diff --git a/x/vm/keeper/state_transition.go b/x/vm/keeper/state_transition.go index 665429a62..cef84e27b 100644 --- a/x/vm/keeper/state_transition.go +++ b/x/vm/keeper/state_transition.go @@ -18,8 +18,10 @@ import ( errorsmod "cosmossdk.io/errors" "cosmossdk.io/math" + sdkmath "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" ) // NewEVM generates a go-ethereum VM from the provided Message fields and the chain parameters @@ -243,6 +245,27 @@ func (k *Keeper) ApplyTransaction(ctx sdk.Context, tx *ethtypes.Transaction) (*t if err = k.RefundGas(ctx, msg, msg.Gas()-res.GasUsed, evmDenom); err != nil { return nil, errorsmod.Wrapf(err, "failed to refund gas leftover gas to sender %s", msg.From()) } + // burn base fee (EIP-1559): baseFee * gasUsed + if cfg.BaseFee != nil && cfg.BaseFee.Sign() > 0 && res.GasUsed > 0 { + // compute amount = baseFee * gasUsed + burnAmt := new(big.Int).Mul(cfg.BaseFee, new(big.Int).SetUint64(res.GasUsed)) + + // create coin in EVM denom + burnCoin := sdk.Coin{Denom: types.GetEVMCoinDenom(), Amount: sdkmath.NewIntFromBigInt(burnAmt)} + + // convert to extended denom (chain denom) and burn from fee collector + converted, err := types.ConvertEvmCoinDenomToExtendedDenom(burnCoin) + if err != nil { + // handle error (log and/or return). If you return, it may fail the tx; choose behavior. + k.Logger(ctx).Error("failed to convert evm denom for base fee burn", "err", err) + } else { + burnCoins := sdk.Coins{converted} + if err := k.bankWrapper.BurnCoins(ctx, authtypes.FeeCollectorName, burnCoins); err != nil { + // handle error - either log or return error. Logging avoids failing block. + k.Logger(ctx).Error("failed to burn base fee from fee collector", "err", err) + } + } + } if len(logs) > 0 { // Update transient block bloom filter diff --git a/x/vm/types/events.go b/x/vm/types/events.go index 5e2681af2..489635e41 100644 --- a/x/vm/types/events.go +++ b/x/vm/types/events.go @@ -17,6 +17,7 @@ const ( AttributeKeyTxType = "txType" AttributeKeyTxLog = "txLog" AttributeKeyTxNonce = "txNonce" + AttributeKeyTxGasLimit = "txGasLimit" AttributeKeyTxData = "txData" // tx failed in eth vm execution