Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions helios-chain/precompiles/ibctransfer/abi.json
Original file line number Diff line number Diff line change
@@ -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": {}
}
137 changes: 137 additions & 0 deletions helios-chain/precompiles/ibctransfer/ibctransfer.go
Original file line number Diff line number Diff line change
@@ -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
}
60 changes: 60 additions & 0 deletions helios-chain/precompiles/ibctransfer/query.go
Original file line number Diff line number Diff line change
@@ -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
}
141 changes: 141 additions & 0 deletions helios-chain/precompiles/ibctransfer/tx.go
Original file line number Diff line number Diff line change
@@ -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)
}
Loading