diff --git a/helios-chain/precompiles/ibctransfer/abi.json b/helios-chain/precompiles/ibctransfer/abi.json new file mode 100644 index 00000000..945ba32b --- /dev/null +++ b/helios-chain/precompiles/ibctransfer/abi.json @@ -0,0 +1,35 @@ +{ + "_format": "hh-sol-artifact-1", + "contractName": "IBCTransfer", + "sourceName": "solidity/precompiles/ibcTransfer/IBCTransfer.sol", + "abi": [ + { + "type": "function", + "name": "ibcTransfer", + "inputs": [ + { "name": "destinationChain", "type": "string" }, + { "name": "recipient", "type": "string" }, + { "name": "amount", "type": "uint256" }, + { "name": "denom", "type": "string" }, + { "name": "timeoutTimestamp", "type": "uint64" } + ], + "outputs": [ + { "name": "success", "type": "bool" } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "getSupportedChains", + "inputs": [], + "outputs": [ + { "name": "chains", "type": "string[]" } + ], + "stateMutability": "view" + } + ], + "bytecode": "0x", + "deployedBytecode": "0x", + "linkReferences": {}, + "deployedLinkReferences": {} + } \ No newline at end of file diff --git a/helios-chain/precompiles/ibctransfer/ibctransfer.go b/helios-chain/precompiles/ibctransfer/ibctransfer.go new file mode 100644 index 00000000..4b513f58 --- /dev/null +++ b/helios-chain/precompiles/ibctransfer/ibctransfer.go @@ -0,0 +1,137 @@ +package ibctransfer + +import ( + "embed" + "fmt" + + cmn "helios-core/helios-chain/precompiles/common" + "helios-core/helios-chain/x/evm/core/vm" + evmtypes "helios-core/helios-chain/x/evm/types" + ibckeeper "helios-core/helios-chain/x/ibc/transfer/keeper" + + storetypes "cosmossdk.io/store/types" + authzkeeper "github.com/cosmos/cosmos-sdk/x/authz/keeper" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" +) + +const ( + abiPath string = "abi.json" + TransferMethod string = "ibcTransfer" + GetSupportedChains string = "getSupportedChains" + GasIbcTransfer = 3_000_000 // Example value, adjust as needed + GasGetSupportedChains = 10_000 // Example value, adjust as needed +) + +// Embed abi json file to the executable binary. Needed when importing as dependency. +// +//go:embed abi.json +var f embed.FS + +var _ vm.PrecompiledContract = &Precompile{} + +type Precompile struct { + cmn.Precompile + ibcKeeper ibckeeper.Keeper +} + +func NewPrecompile( + ibcKeeper ibckeeper.Keeper, + authzKeeper authzkeeper.Keeper, +) (*Precompile, error) { + newABI, err := cmn.LoadABI(f, abiPath) + if err != nil { + return nil, err + } + + p := &Precompile{ + Precompile: cmn.Precompile{ + ABI: newABI, + AuthzKeeper: authzKeeper, + ApprovalExpiration: cmn.DefaultExpirationDuration, + KvGasConfig: storetypes.GasConfig{}, + TransientKVGasConfig: storetypes.GasConfig{}, + }, + ibcKeeper: ibcKeeper, + } + p.SetAddress(p.GetContractAddress()) + return p, nil +} + +func (p *Precompile) RequiredGas(input []byte) uint64 { + if len(input) < 4 { + return 0 + } + + methodID := input[:4] + method, err := p.MethodById(methodID) + if err != nil { + return 0 + } + + switch method.Name { + case TransferMethod: + return GasIbcTransfer + case "getSupportedChains": + return GasGetSupportedChains + default: + return 0 + } +} + +func (p *Precompile) Run(evm *vm.EVM, contract *vm.Contract, readOnly bool) (bz []byte, err error) { + // 1. Reject value sent to the contract (no payable) + if value := contract.Value(); value.Sign() == 1 { + return nil, fmt.Errorf("ibcTransfer precompile cannot receive funds: %s", contract.Value().String()) + } + + // 2. Setup context, stateDB, method, args, etc. + ctx, stateDB, snapshot, method, initialGas, args, err := p.RunSetup(evm, contract, readOnly, p.IsTransaction) + if err != nil { + return nil, err + } + + // 3. Defer gas error handling + defer cmn.HandleGasError(ctx, contract, initialGas, &err)() + + // 4. Dispatch to the correct handler + switch method.Name { + case "ibcTransfer": + if readOnly { + // Transfers are not allowed in read-only mode + return nil, fmt.Errorf("ibcTransfer cannot be called in read-only mode") + } + bz, err = p.TransferIBC(ctx, contract, stateDB, method, args) + case "getSupportedChains": + bz, err = p.GetSupportedChains(ctx, contract, stateDB, method, args) + default: + return nil, fmt.Errorf("unknown method: %s", method.Name) + } + + // 5. Gas accounting + cost := ctx.GasMeter().GasConsumed() - initialGas + if !contract.UseGas(cost) { + return nil, vm.ErrOutOfGas + } + + // 6. Add journal entries (if needed) + if err := p.AddJournalEntries(stateDB, snapshot); err != nil { + return nil, err + } + + return bz, err +} + +func (Precompile) IsTransaction(method *abi.Method) bool { + switch method.Name { + case TransferMethod: + return true + default: + return false + } +} + +func (p *Precompile) GetContractAddress() common.Address { + address := common.HexToAddress(evmtypes.IBCTransferPrecompileAddress) + return address +} diff --git a/helios-chain/precompiles/ibctransfer/query.go b/helios-chain/precompiles/ibctransfer/query.go new file mode 100644 index 00000000..d6c390a9 --- /dev/null +++ b/helios-chain/precompiles/ibctransfer/query.go @@ -0,0 +1,60 @@ +package ibctransfer + +import ( + "fmt" + "helios-core/helios-chain/x/evm/core/vm" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/accounts/abi" +) + +// GetSupportedChains returns a list of all unique destination chain IDs with active IBC channels. +func (p *Precompile) GetSupportedChains( + ctx sdk.Context, + contract *vm.Contract, + stateDB vm.StateDB, + method *abi.Method, + args []interface{}, +) ([]byte, error) { + chains := make(map[string]struct{}) + + // Iterate all channels + channels := p.ibcKeeper.ChannelKeeper.GetAllChannels(ctx) + for _, ch := range channels { + if ch.PortId != "transfer" { + continue + } + channel, found := p.ibcKeeper.ChannelKeeper.GetChannel(ctx, ch.PortId, ch.ChannelId) + if !found || len(channel.ConnectionHops) == 0 { + continue + } + connectionID := channel.ConnectionHops[0] + connection, found := p.ibcKeeper.ConnectionKeeper.GetConnection(ctx, connectionID) + if !found { + continue + } + clientID := connection.ClientId + clientState, found := p.ibcKeeper.ClientKeeper.GetClientState(ctx, clientID) + if !found { + continue + } + tmClientState, ok := clientState.(*ibctmtypes.ClientState) + if !ok { + continue + } + chains[tmClientState.ChainId] = struct{}{} + } + + // Convert map to slice + chainList := make([]string, 0, len(chains)) + for chainID := range chains { + chainList = append(chainList, chainID) + } + + // Pack as string[] using ABI + bz, err := method.Outputs.Pack(chainList) + if err != nil { + return nil, fmt.Errorf("failed to pack supported chains: %w", err) + } + return bz, nil +} diff --git a/helios-chain/precompiles/ibctransfer/tx.go b/helios-chain/precompiles/ibctransfer/tx.go new file mode 100644 index 00000000..3c041e48 --- /dev/null +++ b/helios-chain/precompiles/ibctransfer/tx.go @@ -0,0 +1,141 @@ +package ibctransfer + +import ( + "fmt" + "helios-core/helios-chain/x/evm/core/vm" + "math/big" + + sdk "github.com/cosmos/cosmos-sdk/types" + transfertypes "github.com/cosmos/ibc-go/v8/modules/apps/transfer/types" + clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/types" + "github.com/ethereum/go-ethereum/accounts/abi" +) + +func (p *Precompile) TransferIBC( + ctx sdk.Context, + contract *vm.Contract, + stateDB vm.StateDB, + method *abi.Method, + args []interface{}, +) (bz []byte, err error) { + // 1. Parse and validate arguments + if len(args) != 5 { + bz, packErr := method.Outputs.Pack(false) + if packErr != nil { + return nil, packErr + } + return bz, fmt.Errorf("expected 5 arguments, got %d", len(args)) + } + destinationChain, ok := args[0].(string) + if !ok { + bz, packErr := method.Outputs.Pack(false) + if packErr != nil { + return nil, packErr + } + return bz, fmt.Errorf("invalid destinationChain") + } + recipient, ok := args[1].(string) + if !ok { + bz, packErr := method.Outputs.Pack(false) + if packErr != nil { + return nil, packErr + } + return bz, fmt.Errorf("invalid recipient") + } + amount, ok := args[2].(*big.Int) + if !ok { + bz, packErr := method.Outputs.Pack(false) + if packErr != nil { + return nil, packErr + } + return bz, fmt.Errorf("invalid amount") + } + denom, ok := args[3].(string) + if !ok { + bz, packErr := method.Outputs.Pack(false) + if packErr != nil { + return nil, packErr + } + return bz, fmt.Errorf("invalid denom") + } + timeoutTimestamp, ok := args[4].(*big.Int) + if !ok { + bz, packErr := method.Outputs.Pack(false) + if packErr != nil { + return nil, packErr + } + return bz, fmt.Errorf("invalid timeoutTimestamp") + } + + // 2. Find the channel for the destination chain + channelID, err := p.findChannelForChain(ctx, destinationChain) + if err != nil { + bz, packErr := method.Outputs.Pack(false) + if packErr != nil { + return nil, packErr + } + return bz, err + } + + // 3. Build the MsgTransfer + sender := sdk.AccAddress(contract.Caller().Bytes()).String() + msg := transfertypes.NewMsgTransfer( + "transfer", // port + channelID, // channel + sdk.NewCoin(denom, sdk.NewIntFromBigInt(amount)), + sender, + recipient, + clienttypes.NewHeight(0, 0), + uint64(timeoutTimestamp.Uint64()), + "", // memo + ) + + // 4. Deliver the message + _, err = p.ibcKeeper.Transfer(ctx, msg) + if err != nil { + bz, packErr := method.Outputs.Pack(false) + if packErr != nil { + return nil, packErr + } + return bz, err + } + + // 5. Return success + bz, packErr := method.Outputs.Pack(true) + if packErr != nil { + return nil, packErr + } + return bz, nil +} + +func (p *Precompile) findChannelForChain(ctx sdk.Context, destinationChain string) (string, error) { + channels := p.ibcKeeper.ChannelKeeper.GetAllChannels(ctx) + for _, ch := range channels { + if ch.PortId != "transfer" { + continue + } + channel, found := p.ibcKeeper.ChannelKeeper.GetChannel(ctx, ch.PortId, ch.ChannelId) + if !found || len(channel.ConnectionHops) == 0 { + continue + } + connectionID := channel.ConnectionHops[0] + connection, found := p.ibcKeeper.ConnectionKeeper.GetConnection(ctx, connectionID) + if !found { + continue + } + clientID := connection.ClientId + clientState, found := p.ibcKeeper.ClientKeeper.GetClientState(ctx, clientID) + if !found { + continue + } + // Unpack the client state to get the chain ID + tmClientState, ok := clientState.(*ibctmtypes.ClientState) + if !ok { + continue + } + if tmClientState.ChainId == destinationChain { + return ch.ChannelId, nil + } + } + return "", fmt.Errorf("no channel found for chain %s", destinationChain) +} diff --git a/helios-chain/x/evm/keeper/static_precompiles.go b/helios-chain/x/evm/keeper/static_precompiles.go index 21fc12c8..0a6bea9f 100644 --- a/helios-chain/x/evm/keeper/static_precompiles.go +++ b/helios-chain/x/evm/keeper/static_precompiles.go @@ -25,6 +25,8 @@ import ( logosKeeper "helios-core/helios-chain/x/logos/keeper" stakingkeeper "helios-core/helios-chain/x/staking/keeper" + "helios-core/helios-chain/precompiles/ibctransfer" + authzkeeper "github.com/cosmos/cosmos-sdk/x/authz/keeper" bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" distributionkeeper "github.com/cosmos/cosmos-sdk/x/distribution/keeper" @@ -115,6 +117,14 @@ func NewAvailableStaticPrecompiles( panic(fmt.Errorf("failed to instantiate gov precompile: %w", err)) } + transferPrecompile, err := ibctransfer.NewPrecompile( + transferKeeper, + authzKeeper, + ) + if err != nil { + panic(fmt.Errorf("failed to instantiate IBC transfer precompile: %w", err)) + } + // Stateless precompiles precompiles[bech32Precompile.Address()] = bech32Precompile precompiles[erc20CreatorPrecompile.Address()] = erc20CreatorPrecompile @@ -129,6 +139,7 @@ func NewAvailableStaticPrecompiles( precompiles[chronosPrecompile.Address()] = chronosPrecompile precompiles[hyperionPrecompile.Address()] = hyperionPrecompile precompiles[logosPrecompile.Address()] = logosPrecompile + precompiles[transferPrecompile.Address()] = transferPrecompile return precompiles } diff --git a/helios-chain/x/evm/types/params.go b/helios-chain/x/evm/types/params.go index 3aafff87..24c2df9a 100644 --- a/helios-chain/x/evm/types/params.go +++ b/helios-chain/x/evm/types/params.go @@ -32,6 +32,7 @@ var ( ChronosPrecompileAddress, // Chronos precompile HyperionPrecompileAddress, // Hyperion precompile LogosPrecompileAddress, // Logos precompile + IBCTransferPrecompileAddress, // IBCTransfer precompile } // DefaultExtraEIPs defines the default extra EIPs to be included // On v15, EIP 3855 was enabled diff --git a/helios-chain/x/evm/types/precompiles.go b/helios-chain/x/evm/types/precompiles.go index 9ca34b03..248a4b26 100644 --- a/helios-chain/x/evm/types/precompiles.go +++ b/helios-chain/x/evm/types/precompiles.go @@ -16,6 +16,7 @@ const ( ChronosPrecompileAddress = "0x0000000000000000000000000000000000000830" HyperionPrecompileAddress = "0x0000000000000000000000000000000000000900" LogosPrecompileAddress = "0x0000000000000000000000000000000000000901" + IBCTransferPrecompileAddress = "0x0000000000000000000000000000000000000902" ) // AvailableStaticPrecompiles defines the full list of all available EVM extension addresses. @@ -35,4 +36,5 @@ var AvailableStaticPrecompiles = []string{ ChronosPrecompileAddress, HyperionPrecompileAddress, LogosPrecompileAddress, + IBCTransferPrecompileAddress, } diff --git a/helios-chain/x/ibc/transfer/keeper/keeper.go b/helios-chain/x/ibc/transfer/keeper/keeper.go index 5cbcdb1c..7bda6460 100644 --- a/helios-chain/x/ibc/transfer/keeper/keeper.go +++ b/helios-chain/x/ibc/transfer/keeper/keeper.go @@ -18,9 +18,12 @@ import ( // to be sent via IBC. type Keeper struct { *keeper.Keeper - bankKeeper types.BankKeeper - erc20Keeper types.ERC20Keeper - accountKeeper types.AccountKeeper + bankKeeper types.BankKeeper + erc20Keeper types.ERC20Keeper + accountKeeper types.AccountKeeper + channelKeeper transfertypes.ChannelKeeper + connectionKeeper transfertypes.ConnectionKeeper + clientKeeper transfertypes.ClientKeeper } // NewKeeper creates a new IBC transfer Keeper instance @@ -51,5 +54,18 @@ func NewKeeper( bankKeeper: bankKeeper, erc20Keeper: erc20Keeper, accountKeeper: accountKeeper, + channelKeeper: channelKeeper, } } + +func (k *Keeper) ChannelKeeper() transfertypes.ChannelKeeper { + return k.channelKeeper +} + +func (k *Keeper) ConnectionKeeper() transfertypes.ConnectionKeeper { + return k.connectionKeeper +} + +func (k *Keeper) ClientKeeper() transfertypes.ClientKeeper { + return k.clientKeeper +}