From 8db65e0aa511de6ae33d94b892ab8f701a5ba7fa Mon Sep 17 00:00:00 2001 From: nulnut <151493716+nulnut@users.noreply.github.com> Date: Mon, 19 Feb 2024 10:01:03 +0800 Subject: [PATCH] feat: attestation bridge call claim (#110) Co-authored-by: zakir <80246097+zakir-code@users.noreply.github.com> --- app/keepers/keepers.go | 8 ++ testutil/helpers/key.go | 16 +++ x/crosschain/keeper/attestation_handler.go | 12 ++ x/crosschain/keeper/keeper.go | 4 +- x/crosschain/keeper/msg_server_test.go | 64 +++++++++-- x/crosschain/keeper/relay_transfer.go | 103 +++++++++++++++++ x/crosschain/types/bridge_call.go | 123 +++++++++++++++++++++ x/crosschain/types/bridge_call_test.go | 59 ++++++++++ x/crosschain/types/events.go | 4 + x/crosschain/types/expected_keepers.go | 8 ++ x/crosschain/types/msg_validate.go | 22 +++- x/crosschain/types/msgs.go | 43 +++++++ x/evm/keeper/keeper.go | 100 +++++++++++++++++ x/tron/types/msg_validate.go | 24 +++- 14 files changed, 572 insertions(+), 18 deletions(-) create mode 100644 x/crosschain/types/bridge_call.go create mode 100644 x/crosschain/types/bridge_call_test.go diff --git a/app/keepers/keepers.go b/app/keepers/keepers.go index 0da691b02..0cef85736 100644 --- a/app/keepers/keepers.go +++ b/app/keepers/keepers.go @@ -346,6 +346,7 @@ func NewAppKeeper( appKeepers.IBCTransferKeeper, appKeepers.Erc20Keeper, appKeepers.AccountKeeper, + appKeepers.EvmKeeper, authtypes.NewModuleAddress(govtypes.ModuleName).String(), ) appKeepers.PolygonKeeper = crosschainkeeper.NewKeeper( @@ -359,6 +360,7 @@ func NewAppKeeper( appKeepers.IBCTransferKeeper, appKeepers.Erc20Keeper, appKeepers.AccountKeeper, + appKeepers.EvmKeeper, authtypes.NewModuleAddress(govtypes.ModuleName).String(), ) appKeepers.AvalancheKeeper = crosschainkeeper.NewKeeper( @@ -372,6 +374,7 @@ func NewAppKeeper( appKeepers.IBCTransferKeeper, appKeepers.Erc20Keeper, appKeepers.AccountKeeper, + appKeepers.EvmKeeper, authtypes.NewModuleAddress(govtypes.ModuleName).String(), ) appKeepers.EthKeeper = crosschainkeeper.NewKeeper( @@ -385,6 +388,7 @@ func NewAppKeeper( appKeepers.IBCTransferKeeper, appKeepers.Erc20Keeper, appKeepers.AccountKeeper, + appKeepers.EvmKeeper, authtypes.NewModuleAddress(govtypes.ModuleName).String(), ) appKeepers.ArbitrumKeeper = crosschainkeeper.NewKeeper( @@ -398,6 +402,7 @@ func NewAppKeeper( appKeepers.IBCTransferKeeper, appKeepers.Erc20Keeper, appKeepers.AccountKeeper, + appKeepers.EvmKeeper, authtypes.NewModuleAddress(govtypes.ModuleName).String(), ) appKeepers.OptimismKeeper = crosschainkeeper.NewKeeper( @@ -411,6 +416,7 @@ func NewAppKeeper( appKeepers.IBCTransferKeeper, appKeepers.Erc20Keeper, appKeepers.AccountKeeper, + appKeepers.EvmKeeper, authtypes.NewModuleAddress(govtypes.ModuleName).String(), ) appKeepers.Layer2Keeper = crosschainkeeper.NewKeeper( @@ -424,6 +430,7 @@ func NewAppKeeper( appKeepers.IBCTransferKeeper, appKeepers.Erc20Keeper, appKeepers.AccountKeeper, + appKeepers.EvmKeeper, authtypes.NewModuleAddress(govtypes.ModuleName).String(), ) appKeepers.TronKeeper = tronkeeper.NewKeeper(crosschainkeeper.NewKeeper( @@ -437,6 +444,7 @@ func NewAppKeeper( appKeepers.IBCTransferKeeper, appKeepers.Erc20Keeper, appKeepers.AccountKeeper, + appKeepers.EvmKeeper, authtypes.NewModuleAddress(govtypes.ModuleName).String(), ), appKeepers.Erc20Keeper) diff --git a/testutil/helpers/key.go b/testutil/helpers/key.go index 179b3b510..e48b82286 100644 --- a/testutil/helpers/key.go +++ b/testutil/helpers/key.go @@ -17,6 +17,8 @@ import ( "github.com/evmos/ethermint/crypto/ethsecp256k1" hd2 "github.com/evmos/ethermint/crypto/hd" tronaddress "github.com/fbsobreira/gotron-sdk/pkg/address" + + fxtypes "github.com/functionx/fx-core/v7/types" ) func NewMnemonic() string { @@ -103,6 +105,20 @@ func GenerateAddressByModule(module string) string { return addr.String() } +func AddressToBytesByModule(addr, module string) ([]byte, error) { + if module == "tron" { + tronAddr, err := tronaddress.Base58ToAddress(addr) + if err != nil { + return nil, err + } + return tronAddr.Bytes(), nil + } + if err := fxtypes.ValidateEthereumAddress(addr); err != nil { + return nil, err + } + return common.HexToAddress(addr).Bytes(), nil +} + // GenerateZeroAddressByModule generates an Ethereum or Tron zero address. func GenerateZeroAddressByModule(module string) string { addr := common.HexToAddress(common.Address{}.Hex()) diff --git a/x/crosschain/keeper/attestation_handler.go b/x/crosschain/keeper/attestation_handler.go index e9061eb2d..7be5aca30 100644 --- a/x/crosschain/keeper/attestation_handler.go +++ b/x/crosschain/keeper/attestation_handler.go @@ -53,6 +53,18 @@ func (k Keeper) AttestationHandler(ctx sdk.Context, externalClaim types.External return nil } + case *types.MsgBridgeCallClaim: + assetType, assetData, err := types.UnpackAssetType(claim.Asset) + if err != nil { + return errorsmod.Wrap(types.ErrInvalid, "asset") + } + switch assetType { + case types.AssetERC20: + return k.bridgeCallERC20Handler(ctx, assetData, claim.MustSenderBytes(), claim.MustToBytes(), + claim.MustReceiverBytes(), claim.DstChainId, claim.Message, claim.Value, claim.GasLimit, claim.EventNonce) + default: + return errorsmod.Wrap(types.ErrInvalid, "asset not support") + } case *types.MsgSendToExternalClaim: k.OutgoingTxBatchExecuted(ctx, claim.TokenContract, claim.BatchNonce) diff --git a/x/crosschain/keeper/keeper.go b/x/crosschain/keeper/keeper.go index 48995a84d..2694fbe9a 100644 --- a/x/crosschain/keeper/keeper.go +++ b/x/crosschain/keeper/keeper.go @@ -26,6 +26,7 @@ type Keeper struct { bankKeeper types.BankKeeper ibcTransferKeeper types.IBCTransferKeeper erc20Keeper types.Erc20Keeper + evmKeeper types.EVMKeeper authority string } @@ -34,7 +35,7 @@ type Keeper struct { func NewKeeper(cdc codec.BinaryCodec, moduleName string, storeKey storetypes.StoreKey, stakingKeeper types.StakingKeeper, stakingMsgServer types.StakingMsgServer, distributionKeeper types.DistributionMsgServer, bankKeeper types.BankKeeper, ibcTransferKeeper types.IBCTransferKeeper, erc20Keeper types.Erc20Keeper, ak types.AccountKeeper, - authority string, + evmKeeper types.EVMKeeper, authority string, ) Keeper { if addr := ak.GetModuleAddress(moduleName); addr == nil { panic(fmt.Sprintf("%s module account has not been set", moduleName)) @@ -51,6 +52,7 @@ func NewKeeper(cdc codec.BinaryCodec, moduleName string, storeKey storetypes.Sto bankKeeper: bankKeeper, ibcTransferKeeper: ibcTransferKeeper, erc20Keeper: erc20Keeper, + evmKeeper: evmKeeper, authority: authority, } } diff --git a/x/crosschain/keeper/msg_server_test.go b/x/crosschain/keeper/msg_server_test.go index 25c6ceaa3..488abbdb8 100644 --- a/x/crosschain/keeper/msg_server_test.go +++ b/x/crosschain/keeper/msg_server_test.go @@ -4,6 +4,7 @@ import ( "encoding/hex" "fmt" "math" + "math/big" "sort" "testing" @@ -11,6 +12,7 @@ import ( sdkmath "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" "github.com/ethereum/go-ethereum/crypto" "github.com/stretchr/testify/require" @@ -20,6 +22,7 @@ import ( "github.com/functionx/fx-core/v7/testutil/helpers" "github.com/functionx/fx-core/v7/x/crosschain/types" + erc20types "github.com/functionx/fx-core/v7/x/erc20/types" ethtypes "github.com/functionx/fx-core/v7/x/eth/types" trontypes "github.com/functionx/fx-core/v7/x/tron/types" ) @@ -1111,10 +1114,50 @@ func (suite *KeeperTestSuite) TestBridgeCallClaim() { oracleLastEventNonce := suite.Keeper().GetLastEventNonceByOracle(suite.ctx, suite.oracleAddrs[0]) require.EqualValues(suite.T(), 0, oracleLastEventNonce) - sender := helpers.GenerateAddress().String() - if suite.chainName == trontypes.ModuleName { - sender = trontypes.AddressFromHex(sender) - } + tokenContract := helpers.GenerateAddressByModule(suite.chainName) + _, err = suite.MsgServer().BridgeTokenClaim(sdk.WrapSDKContext(suite.ctx), &types.MsgBridgeTokenClaim{ + EventNonce: oracleLastEventNonce + 1, + BlockHeight: uint64(suite.ctx.BlockHeight()), + TokenContract: tokenContract, + Name: "Token Test", + Symbol: "TTT", + Decimals: 18, + BridgerAddress: suite.bridgerAddrs[0].String(), + ChannelIbc: "", + ChainName: suite.chainName, + }) + require.NoError(suite.T(), err) + + oracleLastEventNonce = suite.Keeper().GetLastEventNonceByOracle(suite.ctx, suite.oracleAddrs[0]) + require.EqualValues(suite.T(), 1, oracleLastEventNonce) + + _, err = suite.app.Erc20Keeper.RegisterCoin(sdk.WrapSDKContext(suite.ctx), &erc20types.MsgRegisterCoin{ + Authority: authtypes.NewModuleAddress(govtypes.ModuleName).String(), + Metadata: banktypes.Metadata{ + Description: "Test token", + DenomUnits: []*banktypes.DenomUnit{ + { + Denom: "ttt", + Exponent: 0, + Aliases: []string{fmt.Sprintf("%s%s", suite.chainName, tokenContract)}, + }, + { + Denom: "TTT", + Exponent: 18, + }, + }, + Base: "ttt", + Display: "TTT", + Name: "Test Token", + Symbol: "TTT", + }, + }) + require.NoError(suite.T(), err) + + tokenAddrBytes, err := helpers.AddressToBytesByModule(tokenContract, suite.chainName) + require.NoError(suite.T(), err) + assetBytes, err := types.PackERC20Asset([][]byte{tokenAddrBytes}, []*big.Int{big.NewInt(100)}) + require.NoError(suite.T(), err) testMsgs := []struct { name string @@ -1125,14 +1168,15 @@ func (suite *KeeperTestSuite) TestBridgeCallClaim() { { name: "success", msg: &types.MsgBridgeCallClaim{ - DstChainId: "123", + DstChainId: "530", EventNonce: oracleLastEventNonce + 1, - Sender: sender, - Asset: "123", - Receiver: helpers.GenerateAddress().String(), - To: helpers.GenerateAddress().String(), - Message: "123", + Sender: helpers.GenerateAddressByModule(suite.chainName), + Asset: hex.EncodeToString(assetBytes), + Receiver: helpers.GenerateAddressByModule(suite.chainName), + To: helpers.GenerateAddressByModule(suite.chainName), + Message: "", Value: sdkmath.NewInt(0), + GasLimit: 3000000, BlockHeight: 1, BridgerAddress: suite.bridgerAddrs[0].String(), ChainName: suite.chainName, diff --git a/x/crosschain/keeper/relay_transfer.go b/x/crosschain/keeper/relay_transfer.go index 6a6b883b0..d43ce4e45 100644 --- a/x/crosschain/keeper/relay_transfer.go +++ b/x/crosschain/keeper/relay_transfer.go @@ -3,9 +3,12 @@ package keeper import ( "encoding/hex" "fmt" + "math/big" + "strconv" "strings" "time" + errorsmod "cosmossdk.io/errors" sdkmath "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/bech32" @@ -103,3 +106,103 @@ func (k Keeper) transferIBCHandler(ctx sdk.Context, eventNonce uint64, receive s )) return err } + +func (k Keeper) bridgeCallERC20Handler( + ctx sdk.Context, + asset, sender, to, receiver []byte, + dstChainID, message string, + value sdkmath.Int, + gasLimit, eventNonce uint64, +) error { + tokens, amounts, err := types.UnpackERC20Asset(asset) + if err != nil { + return errorsmod.Wrap(types.ErrInvalid, "asset erc20") + } + senderAddr := common.BytesToAddress(sender) + targetCoins, err := k.bridgeCallTargetCoinsHandler(ctx, senderAddr, tokens, amounts) + if err != nil { + return err + } + + switch dstChainID { + case types.FxcoreChainID: + // convert coin to erc20 + for _, coin := range targetCoins { + // not convert FX + if coin.Denom == fxtypes.DefaultDenom { + continue + } + if err = k.transferErc20Handler(ctx, eventNonce, receiver, coin); err != nil { + return err + } + } + var toAddrPtr *common.Address + if len(to) > 0 { + toAddr := common.BytesToAddress(to) + toAddrPtr = &toAddr + } + if len(message) > 0 || toAddrPtr != nil { + k.bridgeCallEvmHandler(ctx, senderAddr, toAddrPtr, message, value, gasLimit, eventNonce) + } + default: + // not support chain, refund + } + // todo refund asset + + return nil +} + +func (k Keeper) bridgeCallTargetCoinsHandler(ctx sdk.Context, receiver common.Address, tokens []common.Address, amounts []*big.Int) (sdk.Coins, error) { + tokens, amounts = types.MergeDuplicationERC20(tokens, amounts) + targetCoins := sdk.NewCoins() + for i := 0; i < len(tokens); i++ { + bridgeToken := k.GetBridgeTokenDenom(ctx, tokens[i].String()) + if bridgeToken == nil { + return nil, errorsmod.Wrap(types.ErrInvalid, "bridge token is not exist") + } + coin := sdk.NewCoin(bridgeToken.Denom, sdkmath.NewIntFromBigInt(amounts[i])) + isOriginOrConverted := k.erc20Keeper.IsOriginOrConvertedDenom(ctx, bridgeToken.Denom) + if !isOriginOrConverted { + if err := k.bankKeeper.MintCoins(ctx, k.moduleName, sdk.NewCoins(coin)); err != nil { + return nil, errorsmod.Wrapf(err, "mint vouchers coins") + } + } + if err := k.bankKeeper.SendCoinsFromModuleToAccount(ctx, k.moduleName, receiver.Bytes(), sdk.NewCoins(coin)); err != nil { + return nil, errorsmod.Wrap(err, "transfer vouchers") + } + + targetCoin, err := k.erc20Keeper.ConvertDenomToTarget(ctx, receiver.Bytes(), coin, fxtypes.ParseFxTarget(fxtypes.ERC20Target)) + if err != nil { + return nil, errorsmod.Wrap(err, "convert to target coin") + } + targetCoins = targetCoins.Add(targetCoin) + } + return targetCoins, nil +} + +func (k Keeper) bridgeCallEvmHandler( + ctx sdk.Context, + sender common.Address, + to *common.Address, + message string, value sdkmath.Int, + gasLimit, eventNonce uint64, +) { + cacheCtx, commit := ctx.CacheContext() + txResp, err := k.evmKeeper.CallEVM(cacheCtx, sender, to, value.BigInt(), gasLimit, fxtypes.MustDecodeHex(message), true) + if err != nil { + k.Logger(ctx).Error("bridge call evm error", "nonce", eventNonce, "error", err.Error()) + attrs := []sdk.Attribute{ + sdk.NewAttribute(types.AttributeKeyEvmCallResult, strconv.FormatBool(false)), + sdk.NewAttribute(types.AttributeKeyEvmCallError, err.Error()), + } + ctx.EventManager().EmitEvents(sdk.Events{sdk.NewEvent(types.EventTypeBridgeCallEvm, attrs...)}) + return + } + // whether the tx succeeds or fails, commit with tx logs + commit() + attrs := []sdk.Attribute{sdk.NewAttribute(types.AttributeKeyEvmCallResult, strconv.FormatBool(!txResp.Failed()))} + if txResp.Failed() { + attrs = append(attrs, sdk.NewAttribute(types.AttributeKeyEvmCallError, txResp.VmError)) + } + ctx.EventManager().EmitEvents(sdk.Events{sdk.NewEvent(types.EventTypeBridgeCallEvm, attrs...)}) +} diff --git a/x/crosschain/types/bridge_call.go b/x/crosschain/types/bridge_call.go new file mode 100644 index 000000000..a34108c9f --- /dev/null +++ b/x/crosschain/types/bridge_call.go @@ -0,0 +1,123 @@ +package types + +import ( + "encoding/hex" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + + fxtypes "github.com/functionx/fx-core/v7/types" +) + +const ( + AssetERC20 = "ERC20" +) + +var FxcoreChainID = fxtypes.EIP155ChainID().String() + +var ( + TypeString, _ = abi.NewType("string", "", nil) + TypeBytes, _ = abi.NewType("bytes", "", nil) + TypeUint256Array, _ = abi.NewType("uint256[]", "", nil) +) + +var ( + assetTypeDecode = abi.Arguments{ + abi.Argument{Name: "_assetType", Type: TypeString}, + abi.Argument{Name: "_assetData", Type: TypeBytes}, + } + + erc20AssetDecode = abi.Arguments{ + abi.Argument{Name: "_tokens", Type: TypeBytes}, + abi.Argument{Name: "_amounts", Type: TypeUint256Array}, + } +) + +func UnpackAssetType(asset string) (string, []byte, error) { + assetBytes, err := hex.DecodeString(asset) + if err != nil { + return "", nil, err + } + values, err := assetTypeDecode.UnpackValues(assetBytes) + if err != nil { + return "", nil, err + } + if len(values) != 2 { + return "", nil, fmt.Errorf("invalid length") + } + assetType, ok := values[0].(string) + if !ok { + return "", nil, fmt.Errorf("invalid type type") + } + assetData, ok := values[1].([]byte) + if !ok { + return "", nil, fmt.Errorf("invalid data type") + } + return assetType, assetData, nil +} + +func UnpackERC20Asset(asset []byte) ([]common.Address, []*big.Int, error) { + values, err := erc20AssetDecode.UnpackValues(asset) + if err != nil { + return nil, nil, err + } + if len(values) != 2 { + return nil, nil, fmt.Errorf("invalid length") + } + tokenBytes, ok := values[0].([]byte) + if !ok { + return nil, nil, fmt.Errorf("invalid token type") + } + amounts, ok := values[1].([]*big.Int) + if !ok { + return nil, nil, fmt.Errorf("invalid amount type") + } + tokens := make([]common.Address, 0, len(amounts)) + for i := 0; i*common.AddressLength < len(tokenBytes); i++ { + token := tokenBytes[i*common.AddressLength : (i+1)*common.AddressLength] + tokens = append(tokens, common.BytesToAddress(token)) + } + if len(tokens) != len(amounts) { + return nil, nil, fmt.Errorf("token not match amount") + } + return tokens, amounts, nil +} + +func MergeDuplicationERC20(tokens []common.Address, amounts []*big.Int) ([]common.Address, []*big.Int) { + tokenArray := make([]common.Address, 0, len(tokens)) + amountArray := make([]*big.Int, 0, len(amounts)) + for i := 0; i < len(tokens); i++ { + found := false + j := 0 + for ; j < len(tokenArray); j++ { + if tokens[i] == tokenArray[j] { + found = true + break + } + } + if found { + amountArray[j] = new(big.Int).Add(amountArray[j], amounts[i]) + } else { + tokenArray = append(tokenArray, tokens[i]) + amountArray = append(amountArray, amounts[i]) + } + } + return tokenArray, amountArray +} + +func PackERC20Asset(tokens [][]byte, amounts []*big.Int) ([]byte, error) { + if len(tokens) != len(amounts) { + return nil, fmt.Errorf("token not match amount") + } + tokenBytes := make([]byte, 0, common.AddressLength*len(tokens)) + for _, token := range tokens { + tokenBytes = append(tokenBytes, token...) + } + assetData, err := erc20AssetDecode.Pack(tokenBytes, amounts) + if err != nil { + return nil, err + } + return assetTypeDecode.Pack("ERC20", assetData) +} diff --git a/x/crosschain/types/bridge_call_test.go b/x/crosschain/types/bridge_call_test.go new file mode 100644 index 000000000..c9afc6119 --- /dev/null +++ b/x/crosschain/types/bridge_call_test.go @@ -0,0 +1,59 @@ +package types + +import ( + "encoding/hex" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" +) + +func TestBridgeCall_UnpackAssetType(t *testing.T) { + asset := "000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000005455243323000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001" + assetType, _, err := UnpackAssetType(asset) + require.NoError(t, err) + require.Equal(t, assetType, AssetERC20) +} + +func TestBridgeCall_UnpackERC20Asset(t *testing.T) { + asset := "000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001" + assetBytes, err := hex.DecodeString(asset) + require.NoError(t, err) + tokens, amounts, err := UnpackERC20Asset(assetBytes) + require.NoError(t, err) + require.Equal(t, tokens[0].String(), common.BigToAddress(big.NewInt(1)).String()) + require.Equal(t, amounts[0].String(), "1") +} + +func TestBridgeCall_MergeDuplicationERC20(t *testing.T) { + tokens := []common.Address{ + common.BigToAddress(big.NewInt(1)), + common.BigToAddress(big.NewInt(2)), + common.BigToAddress(big.NewInt(3)), + common.BigToAddress(big.NewInt(1)), + common.BigToAddress(big.NewInt(3)), + } + expectedTokens := []common.Address{ + common.BigToAddress(big.NewInt(1)), + common.BigToAddress(big.NewInt(2)), + common.BigToAddress(big.NewInt(3)), + } + amounts := []*big.Int{ + big.NewInt(1), + big.NewInt(1), + big.NewInt(1), + big.NewInt(1), + big.NewInt(1), + } + expectedAmounts := []*big.Int{ + big.NewInt(2), + big.NewInt(1), + big.NewInt(2), + } + + newTokens, newAmounts := MergeDuplicationERC20(tokens, amounts) + + require.Equal(t, expectedTokens, newTokens) + require.Equal(t, expectedAmounts, newAmounts) +} diff --git a/x/crosschain/types/events.go b/x/crosschain/types/events.go index 837d4053d..9e7c863fc 100644 --- a/x/crosschain/types/events.go +++ b/x/crosschain/types/events.go @@ -31,4 +31,8 @@ const ( AttributeKeyIbcSourceChannel = "ibc_source_channel" EventTypeEvmTransfer = "evm_transfer" + + EventTypeBridgeCallEvm = "bridge_call_evm" + AttributeKeyEvmCallResult = "evm_call_result" + AttributeKeyEvmCallError = "evm_call_error" ) diff --git a/x/crosschain/types/expected_keepers.go b/x/crosschain/types/expected_keepers.go index 6a6e66328..0c6939a3c 100644 --- a/x/crosschain/types/expected_keepers.go +++ b/x/crosschain/types/expected_keepers.go @@ -2,6 +2,7 @@ package types import ( "context" + "math/big" sdk "github.com/cosmos/cosmos-sdk/types" bank "github.com/cosmos/cosmos-sdk/x/bank/types" @@ -9,6 +10,8 @@ import ( paramtypes "github.com/cosmos/cosmos-sdk/x/params/types" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" tranfsertypes "github.com/cosmos/ibc-go/v6/modules/apps/transfer/types" + "github.com/ethereum/go-ethereum/common" + "github.com/evmos/ethermint/x/evm/types" fxtypes "github.com/functionx/fx-core/v7/types" ) @@ -63,6 +66,11 @@ type Erc20Keeper interface { ToTargetDenom(ctx sdk.Context, denom, base string, aliases []string, fxTarget fxtypes.FxTarget) string } +// EVMKeeper defines the expected EVM keeper interface used on crosschain +type EVMKeeper interface { + CallEVM(ctx sdk.Context, from common.Address, contract *common.Address, value *big.Int, gasLimit uint64, data []byte, commit bool) (*types.MsgEthereumTxResponse, error) +} + type IBCTransferKeeper interface { Transfer(goCtx context.Context, msg *tranfsertypes.MsgTransfer) (*tranfsertypes.MsgTransferResponse, error) diff --git a/x/crosschain/types/msg_validate.go b/x/crosschain/types/msg_validate.go index eb3cdf39d..48c5778d8 100644 --- a/x/crosschain/types/msg_validate.go +++ b/x/crosschain/types/msg_validate.go @@ -5,6 +5,8 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" errortypes "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" fxtypes "github.com/functionx/fx-core/v7/types" ) @@ -201,15 +203,22 @@ func (b MsgValidate) MsgBridgeCallClaimValidate(m *MsgBridgeCallClaim) (err erro if err = fxtypes.ValidateEthereumAddress(m.Sender); err != nil { return errortypes.ErrInvalidAddress.Wrapf("invalid sender address: %s", err) } - if err = fxtypes.ValidateEthereumAddress(m.To); err != nil { - return errortypes.ErrInvalidAddress.Wrapf("invalid to contract: %s", err) + if len(m.To) > 0 { + if err = fxtypes.ValidateEthereumAddress(m.To); err != nil { + return errortypes.ErrInvalidAddress.Wrapf("invalid to contract: %s", err) + } } - if _, _, err := fxtypes.ParseAddress(m.Receiver); err != nil { + if err = fxtypes.ValidateEthereumAddress(m.Receiver); err != nil { return errortypes.ErrInvalidAddress.Wrapf("invalid receiver address: %s", err) } if m.Value.IsNil() || m.Value.IsNegative() { return errortypes.ErrInvalidRequest.Wrap("invalid value") } + if len(m.Message) > 0 { + if _, err := hexutil.Decode(m.Message); err != nil { + return errortypes.ErrInvalidRequest.Wrap("invalid message") + } + } if m.EventNonce == 0 { return errortypes.ErrInvalidRequest.Wrap("zero event nonce") } @@ -302,3 +311,10 @@ func (b MsgValidate) MsgConfirmBatchValidate(m *MsgConfirmBatch) (err error) { func (b MsgValidate) ValidateAddress(addr string) error { return fxtypes.ValidateEthereumAddress(addr) } + +func (b MsgValidate) AddressToBytes(addr string) ([]byte, error) { + if err := fxtypes.ValidateEthereumAddress(addr); err != nil { + return nil, err + } + return common.HexToAddress(addr).Bytes(), nil +} diff --git a/x/crosschain/types/msgs.go b/x/crosschain/types/msgs.go index 0cb3d0e23..649b50539 100644 --- a/x/crosschain/types/msgs.go +++ b/x/crosschain/types/msgs.go @@ -122,6 +122,7 @@ type MsgValidateBasic interface { MsgConfirmBatchValidate(m *MsgConfirmBatch) (err error) ValidateAddress(addr string) error + AddressToBytes(addr string) ([]byte, error) } var reModuleName *regexp.Regexp @@ -632,6 +633,48 @@ func (m *MsgBridgeCallClaim) ClaimHash() []byte { return tmhash.Sum([]byte(path)) } +// GetAddressBytes parse addr to bytes +func (m *MsgBridgeCallClaim) GetAddressBytes(addr string) ([]byte, error) { + if err := ValidateModuleName(m.ChainName); err != nil { + return nil, errortypes.ErrInvalidRequest.Wrap("invalid chain name") + } + if router, ok := msgValidateBasicRouter[m.ChainName]; !ok { + return nil, errortypes.ErrInvalidRequest.Wrap("unrecognized cross chain name") + } else { + return router.AddressToBytes(addr) + } +} + +// MustSenderBytes parse sender to bytes +func (m *MsgBridgeCallClaim) MustSenderBytes() []byte { + addr, err := m.GetAddressBytes(m.Sender) + if err != nil { + panic(err) + } + return addr +} + +// MustReceiverBytes parse receiver to bytes +func (m *MsgBridgeCallClaim) MustReceiverBytes() []byte { + addr, err := m.GetAddressBytes(m.Receiver) + if err != nil { + panic(err) + } + return addr +} + +// MustToBytes parse to addr to bytes +func (m *MsgBridgeCallClaim) MustToBytes() []byte { + if len(m.To) == 0 { + return []byte{} + } + addr, err := m.GetAddressBytes(m.To) + if err != nil { + panic(err) + } + return addr +} + // MsgSendToExternalClaim // GetType returns the claim type diff --git a/x/evm/keeper/keeper.go b/x/evm/keeper/keeper.go index b33f95c5a..8e43f6784 100644 --- a/x/evm/keeper/keeper.go +++ b/x/evm/keeper/keeper.go @@ -1,8 +1,10 @@ package keeper import ( + "encoding/json" "fmt" "math/big" + "strconv" errorsmod "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" @@ -12,6 +14,8 @@ import ( "github.com/ethereum/go-ethereum/core/vm" evmkeeper "github.com/evmos/ethermint/x/evm/keeper" "github.com/evmos/ethermint/x/evm/types" + tmbytes "github.com/tendermint/tendermint/libs/bytes" + tmtypes "github.com/tendermint/tendermint/types" fxserverconfig "github.com/functionx/fx-core/v7/server/config" fxtypes "github.com/functionx/fx-core/v7/types" @@ -99,3 +103,99 @@ func (k *Keeper) CallEVMWithoutGas( return res, nil } + +func (k *Keeper) CallEVM( + ctx sdk.Context, + from common.Address, + contract *common.Address, + value *big.Int, + gasLimit uint64, + data []byte, + commit bool, +) (*types.MsgEthereumTxResponse, error) { + gasMeter := ctx.GasMeter() + ctx = ctx.WithGasMeter(sdk.NewInfiniteGasMeter()) + + nonce, err := k.accountKeeper.GetSequence(ctx, from.Bytes()) + if err != nil { + return nil, err + } + params := ctx.ConsensusParams() + if params != nil && params.Block != nil && params.Block.MaxGas > 0 { + gasLimit = uint64(params.Block.MaxGas) + } + + if value == nil { + value = big.NewInt(0) + } + msg := ethtypes.NewMessage( + from, + contract, + nonce, + value, // amount + gasLimit, // gasLimit + big.NewInt(0), // gasFeeCap + big.NewInt(0), // gasTipCap + big.NewInt(0), // gasPrice + data, + ethtypes.AccessList{}, // AccessList + !commit, // isFake + ) + + res, err := k.ApplyMessage(ctx, msg, types.NewNoOpTracer(), commit) + if err != nil { + return nil, err + } + + attrs := []sdk.Attribute{ + sdk.NewAttribute(sdk.AttributeKeyAmount, value.String()), + // add event for ethereum transaction hash format + sdk.NewAttribute(types.AttributeKeyEthereumTxHash, res.Hash), + // add event for index of valid ethereum tx + sdk.NewAttribute(types.AttributeKeyTxIndex, strconv.FormatUint(0, 10)), + // add event for eth tx gas used, we can't get it from cosmos tx result when it contains multiple eth tx msgs. + sdk.NewAttribute(types.AttributeKeyTxGasUsed, strconv.FormatUint(res.GasUsed, 10)), + } + + if len(ctx.TxBytes()) > 0 { + // add event for tendermint transaction hash format + hash := tmbytes.HexBytes(tmtypes.Tx(ctx.TxBytes()).Hash()) + attrs = append(attrs, sdk.NewAttribute(types.AttributeKeyTxHash, hash.String())) + } + + attrs = append(attrs, sdk.NewAttribute(types.AttributeKeyRecipient, contract.Hex())) + + if res.Failed() { + attrs = append(attrs, sdk.NewAttribute(types.AttributeKeyEthereumTxFailed, res.VmError)) + } + + txLogAttrs := make([]sdk.Attribute, len(res.Logs)) + for i, log := range res.Logs { + value, err := json.Marshal(log) + if err != nil { + return nil, errorsmod.Wrap(err, "failed to encode log") + } + txLogAttrs[i] = sdk.NewAttribute(types.AttributeKeyTxLog, string(value)) + } + + // emit events + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + types.EventTypeEthereumTx, + attrs..., + ), + sdk.NewEvent( + types.EventTypeTxLog, + txLogAttrs..., + ), + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), + sdk.NewAttribute(sdk.AttributeKeySender, from.String()), + ), + }) + + ctx.WithGasMeter(gasMeter) + + return res, nil +} diff --git a/x/tron/types/msg_validate.go b/x/tron/types/msg_validate.go index 0d656a3d9..9c36fa5e3 100644 --- a/x/tron/types/msg_validate.go +++ b/x/tron/types/msg_validate.go @@ -5,8 +5,9 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" errortypes "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/ethereum/go-ethereum/common/hexutil" + tronaddress "github.com/fbsobreira/gotron-sdk/pkg/address" - fxtypes "github.com/functionx/fx-core/v7/types" crosschaintypes "github.com/functionx/fx-core/v7/x/crosschain/types" ) @@ -157,15 +158,22 @@ func (b TronMsgValidate) MsgBridgeCallClaimValidate(m *crosschaintypes.MsgBridge if err = ValidateTronAddress(m.Sender); err != nil { return errortypes.ErrInvalidAddress.Wrapf("invalid sender address: %s", err) } - if err = fxtypes.ValidateEthereumAddress(m.To); err != nil { - return errortypes.ErrInvalidAddress.Wrapf("invalid to contract: %s", err) + if len(m.To) > 0 { + if err = ValidateTronAddress(m.To); err != nil { + return errortypes.ErrInvalidAddress.Wrapf("invalid to contract: %s", err) + } } - if _, _, err := fxtypes.ParseAddress(m.Receiver); err != nil { + if err = ValidateTronAddress(m.Receiver); err != nil { return errortypes.ErrInvalidAddress.Wrapf("invalid receiver address: %s", err) } if m.Value.IsNil() || m.Value.IsNegative() { return errortypes.ErrInvalidRequest.Wrap("invalid value") } + if len(m.Message) > 0 { + if _, err := hexutil.Decode(m.Message); err != nil { + return errortypes.ErrInvalidRequest.Wrap("invalid message") + } + } if m.EventNonce == 0 { return errortypes.ErrInvalidRequest.Wrap("zero event nonce") } @@ -235,3 +243,11 @@ func (b TronMsgValidate) MsgConfirmBatchValidate(m *crosschaintypes.MsgConfirmBa func (b TronMsgValidate) ValidateAddress(addr string) error { return ValidateTronAddress(addr) } + +func (b TronMsgValidate) AddressToBytes(addr string) ([]byte, error) { + tronAddr, err := tronaddress.Base58ToAddress(addr) + if err != nil { + return nil, err + } + return tronAddr.Bytes(), nil +}