Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IBC transfer precompile #1459

Closed
wants to merge 16 commits into from
3 changes: 2 additions & 1 deletion app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -579,7 +579,7 @@ func New(
wasmOpts...,
)

app.EvmKeeper = *evmkeeper.NewKeeper(keys[evmtypes.StoreKey], memKeys[evmtypes.MemStoreKey], app.GetSubspace(evmtypes.ModuleName), app.BankKeeper, &app.AccountKeeper, &app.StakingKeeper)
app.EvmKeeper = *evmkeeper.NewKeeper(keys[evmtypes.StoreKey], memKeys[evmtypes.MemStoreKey], app.GetSubspace(evmtypes.ModuleName), app.BankKeeper, &app.AccountKeeper, &app.StakingKeeper, app.TransferKeeper, app.IBCKeeper, scopedTransferKeeper)
app.evmRPCConfig, err = evmrpc.ReadConfig(appOpts)
if err != nil {
panic(fmt.Sprintf("error reading EVM config due to %s", err))
Expand Down Expand Up @@ -638,6 +638,7 @@ func New(
stakingkeeper.NewMsgServerImpl(app.StakingKeeper),
app.GovKeeper,
app.DistrKeeper,
app.TransferKeeper,
); err != nil {
panic(err)
}
Expand Down
15 changes: 14 additions & 1 deletion precompiles/common/expected_keepers.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package common

import (
"context"

Check failure on line 4 in precompiles/common/expected_keepers.go

View workflow job for this annotation

GitHub Actions / lint

File is not `goimports`-ed (goimports)

sdk "github.com/cosmos/cosmos-sdk/types"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
clienttypes "github.com/cosmos/ibc-go/v3/modules/core/02-client/types"
"github.com/ethereum/go-ethereum/common"
)

Expand Down Expand Up @@ -55,3 +55,16 @@
SetWithdrawAddr(ctx sdk.Context, delegatorAddr sdk.AccAddress, withdrawAddr sdk.AccAddress) error
WithdrawDelegationRewards(ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress) (sdk.Coins, error)
}

type TransferKeeper interface {
SendTransfer(
ctx sdk.Context,
sourcePort,
sourceChannel string,
token sdk.Coin,
sender sdk.AccAddress,
receiver string,
timeoutHeight clienttypes.Height,
timeoutTimestamp uint64,
) error
}
20 changes: 20 additions & 0 deletions precompiles/ibc/IBC.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

address constant BANK_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000001008;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we udpate this to 0x0000000000000000000000000000000000001009 ? Oracle is about to be merged #1445

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated


IBC constant BANK_CONTRACT = IBC(
BANK_PRECOMPILE_ADDRESS
);

interface IBC {
// Transactions
function transfer(
address fromAddress,
address toAddress,
string memory port,
string memory channel,
string memory denom,
uint256 amount
) external returns (bool success);
}
46 changes: 46 additions & 0 deletions precompiles/ibc/abi.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
[
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

keeping formatted for readability for now (will flatten when code is post-draft)

{
"inputs": [
{
"internalType": "address",
"name": "fromAddress",
"type": "address"
},
{
"internalType": "address",
"name": "toAddress",
"type": "address"
},
{
"internalType": "string",
"name": "port",
"type": "string"
},
{
"internalType": "string",
"name": "channel",
"type": "string"
},
{
"internalType": "string",
"name": "denom",
"type": "string"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "transfer",
"outputs": [
{
"internalType": "bool",
"name": "success",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
}
]
170 changes: 170 additions & 0 deletions precompiles/ibc/ibc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
package ibc

import (
"bytes"
"embed"
"errors"

Check failure on line 6 in precompiles/ibc/ibc.go

View workflow job for this annotation

GitHub Actions / lint

File is not `goimports`-ed (goimports)
sdk "github.com/cosmos/cosmos-sdk/types"
clienttypes "github.com/cosmos/ibc-go/v3/modules/core/02-client/types"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/vm"
pcommon "github.com/sei-protocol/sei-chain/precompiles/common"
"math/big"
)

const (
TransferMethod = "transfer"
)

const (
IBCAddress = "0x0000000000000000000000000000000000001008"
)

var _ vm.PrecompiledContract = &Precompile{}

// Embed abi json file to the executable binary. Needed when importing as dependency.
//
//go:embed abi.json
var f embed.FS

func GetABI() abi.ABI {
abiBz, err := f.ReadFile("abi.json")
if err != nil {
panic(err)
}

newAbi, err := abi.JSON(bytes.NewReader(abiBz))
if err != nil {
panic(err)
}
return newAbi
}

type Precompile struct {
pcommon.Precompile
address common.Address
transferKeeper pcommon.TransferKeeper
evmKeeper pcommon.EVMKeeper

TransferID []byte
}

func NewPrecompile(transferKeeper pcommon.TransferKeeper, evmKeeper pcommon.EVMKeeper) (*Precompile, error) {
newAbi := GetABI()

p := &Precompile{
Precompile: pcommon.Precompile{ABI: newAbi},
address: common.HexToAddress(IBCAddress),
transferKeeper: transferKeeper,
evmKeeper: evmKeeper,
}

for name, m := range newAbi.Methods {
switch name {
case TransferMethod:
p.TransferID = m.ID
}
}
Comment on lines +69 to +74

Check warning

Code scanning / CodeQL

Iteration over map Warning

Iteration over map may be a possible source of non-determinism

return p, nil
}

// RequiredGas returns the required bare minimum gas to execute the precompile.
func (p Precompile) RequiredGas(input []byte) uint64 {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Kbhat1 @codchen for IBC transfer, will this have to be hardcoded as for other precompiles? Any suggested value?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is IBC gas consumption pretty constant or can vary from call-to-call quite a bit? If the latter we can report gas consumption dynamically similar to https://github.com/sei-protocol/sei-chain/blob/seiv2/precompiles/wasmd/wasmd.go#L111-L135

methodID := input[:4]

method, err := p.ABI.MethodById(methodID)
if err != nil {
// This should never happen since this method is going to fail during Run
return 0
}

return p.Precompile.RequiredGas(input, p.IsTransaction(method.Name))
}

func (p Precompile) Run(evm *vm.EVM, caller common.Address, input []byte, value *big.Int) (bz []byte, err error) {
ctx, method, args, err := p.Prepare(evm, input)
if err != nil {
return nil, err
}

switch method.Name {
case TransferMethod:
return p.transfer(ctx, method, args)
}
return
}
func (p Precompile) transfer(ctx sdk.Context, method *abi.Method, args []interface{}) ([]byte, error) {
pcommon.AssertArgsLength(args, 6)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Kbhat1 @codchen do we need to do other validations?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that from is set an arg. Can anyone trigger an IBC transfer on anyone else's behalf? If not then we should be looking at caller in https://github.com/sei-protocol/sei-chain/pull/1459/files#diff-3c39140061c990bd99f477fe4dbc73c0eaac283bfa477c1ba439016daf6fe688R87 to use as the from address instead

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


senderAddress, err := p.accAddressFromArg(ctx, args[0])
if err != nil {
return nil, err
}

receiverAddress, err := p.accAddressFromArg(ctx, args[1])
if err != nil {
return nil, err
}

port, ok := args[2].(string)
if !ok {
return nil, errors.New("port is not a string")
}

channelID, ok := args[3].(string)
if !ok {
return nil, errors.New("channelID is not a string")
}

denom := args[4].(string)
if denom == "" {
return nil, errors.New("invalid denom")
}

amount, ok := args[5].(*big.Int)
if !ok {
return nil, errors.New("amount is not a big.Int")
}

if amount.Cmp(big.NewInt(0)) == 0 {
// short circuit
return method.Outputs.Pack(true)
}

coin := sdk.Coin{
Denom: denom,
Amount: sdk.NewIntFromBigInt(amount),
}

height := clienttypes.Height{}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@codchen @Kbhat1 Not sure what values should be here...or should they be provided by the caller via ABI?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will to defer to kartik here since it's IBC domain knowledge

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's the timeout height, we can pass that in as a parameter in the transfer method and update the abi


err = p.transferKeeper.SendTransfer(ctx, port, channelID, coin, senderAddress, receiverAddress.String(), height, 0)

if err != nil {
return nil, err
}
return method.Outputs.Pack(true)
}

func (Precompile) IsTransaction(method string) bool {
switch method {
case TransferMethod:
return true
default:
return false
}
}

func (p Precompile) Address() common.Address {
return p.address
}

func (p Precompile) accAddressFromArg(ctx sdk.Context, arg interface{}) (sdk.AccAddress, error) {
addr := arg.(common.Address)
if addr == (common.Address{}) {
return nil, errors.New("invalid addr")
}
return p.evmKeeper.GetSeiAddressOrDefault(ctx, addr), nil
}
65 changes: 65 additions & 0 deletions precompiles/ibc/ibc_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package ibc_test

import (
sdk "github.com/cosmos/cosmos-sdk/types"
channeltypes "github.com/cosmos/ibc-go/v3/modules/core/04-channel/types"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/sei-protocol/sei-chain/precompiles/ibc"
testkeeper "github.com/sei-protocol/sei-chain/testutil/keeper"
"github.com/sei-protocol/sei-chain/x/evm/state"
"github.com/sei-protocol/sei-chain/x/evm/types"
"github.com/stretchr/testify/require"
tmtypes "github.com/tendermint/tendermint/proto/tendermint/types"
"math/big"
"testing"
)

func TestRun(t *testing.T) {
testApp := testkeeper.EVMTestApp
ctx := testApp.NewContext(false, tmtypes.Header{}).WithBlockHeight(2)
k := &testApp.EvmKeeper

// Setup sender addresses and environment
privKey := testkeeper.MockPrivateKey()
senderAddr, senderEVMAddr := testkeeper.PrivateKeyToAddresses(privKey)
k.SetAddressMapping(ctx, senderAddr, senderEVMAddr)
k.ScopedCapabilityKeeper().NewCapability(ctx, "capabilities/ports/port/channels/sourceChannel")
k.ChannelKeeper().SetChannel(ctx, "port", "sourceChannel", channeltypes.Channel{
State: 0,
Ordering: 0,
Counterparty: channeltypes.Counterparty{
PortId: "destinationPort",
ChannelId: "destinationChannel",
},
ConnectionHops: nil,
Version: "",
})
k.ChannelKeeper().SetNextSequenceSend(ctx, "port", "sourceChannel", 1)

err := k.BankKeeper().MintCoins(ctx, types.ModuleName, sdk.NewCoins(sdk.NewCoin("usei", sdk.NewInt(10000000))))
require.Nil(t, err)
err = k.BankKeeper().SendCoinsFromModuleToAccount(ctx, types.ModuleName, senderAddr, sdk.NewCoins(sdk.NewCoin("usei", sdk.NewInt(10000000))))
require.Nil(t, err)

// Setup receiving addresses
_, evmAddr := testkeeper.MockAddressPair()

p, err := ibc.NewPrecompile(k.TransferKeeper(), k)

require.Nil(t, err)
stateDb := state.NewDBImpl(ctx, k, true)
evm := vm.EVM{
StateDB: stateDb,
TxContext: vm.TxContext{Origin: senderEVMAddr},
}

// Precompile transfer test
send, err := p.ABI.MethodById(p.TransferID)
require.Nil(t, err)
args, err := send.Inputs.Pack(senderEVMAddr, evmAddr, "port", "sourceChannel", "usei", big.NewInt(25))
require.Nil(t, err)
_, err = p.Run(&evm, senderEVMAddr, append(p.TransferID, args...), nil)
// TODO: Fix uncomment when all dependencies are resolved
//require.Nil(t, err)

}
7 changes: 7 additions & 0 deletions precompiles/setup.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package precompiles

import (
"github.com/sei-protocol/sei-chain/precompiles/ibc"

Check failure on line 4 in precompiles/setup.go

View workflow job for this annotation

GitHub Actions / lint

File is not `goimports`-ed (goimports)
"sync"

ecommon "github.com/ethereum/go-ethereum/common"
Expand All @@ -26,6 +27,7 @@
stakingKeeper common.StakingKeeper,
govKeeper common.GovKeeper,
distrKeeper common.DistributionKeeper,
transferKeeper common.TransferKeeper,
) error {
SetupMtx.Lock()
defer SetupMtx.Unlock()
Expand Down Expand Up @@ -67,6 +69,11 @@
return err
}
addPrecompileToVM(distrp, distrp.Address())
ibcp, err := ibc.NewPrecompile(transferKeeper, evmKeeper)
if err != nil {
return err
}
addPrecompileToVM(ibcp, ibcp.Address())
Initialized = true
return nil
}
Expand Down
Loading
Loading