diff --git a/.github/workflows/consensuswarn.yml b/.github/workflows/consensuswarn.yml index 82e8919a6..b7d860760 100644 --- a/.github/workflows/consensuswarn.yml +++ b/.github/workflows/consensuswarn.yml @@ -20,7 +20,7 @@ jobs: steps: # This is used for warning when a PR touches any of the roots, or any function or method directly or indirectly called by a root - uses: actions/checkout@v4 - - uses: orijtech/consensuswarn@main + - uses: orijtech/consensuswarn@956f047a43f56021a28afdfb2a2291a20955f48d with: # example.com/pkg/path.Type.Method roots: 'github.com/ExocoreNetwork/exocore/app.ExocoreApp.DeliverTx,github.com/ExocoreNetwork/exocore/app.ExocoreApp.BeginBlocker,github.com/ExocoreNetwork/exocore/app.ExocoreApp.EndBlocker' diff --git a/precompiles/clientchains/abi.json b/precompiles/clientchains/abi.json new file mode 100644 index 000000000..eeb2160b7 --- /dev/null +++ b/precompiles/clientchains/abi.json @@ -0,0 +1,21 @@ +[ + { + "name": "getClientChains", + "type": "function", + "inputs": [], + "outputs": [ + { + "name": "success", + "type": "bool", + "internalType": "bool" + }, + { + "name": "chainIds", + "type": "uint16[]", + "internalType": "uint16[]" + } + ], + "stateMutability": "view", + "payable": false + } +] \ No newline at end of file diff --git a/precompiles/clientchains/clientchains.go b/precompiles/clientchains/clientchains.go new file mode 100644 index 000000000..adf8460ec --- /dev/null +++ b/precompiles/clientchains/clientchains.go @@ -0,0 +1,119 @@ +package clientchains + +import ( + "bytes" + "embed" + "fmt" + + assetskeeper "github.com/ExocoreNetwork/exocore/x/assets/keeper" + storetypes "github.com/cosmos/cosmos-sdk/store/types" + authzkeeper "github.com/cosmos/cosmos-sdk/x/authz/keeper" + "github.com/ethereum/go-ethereum/accounts/abi" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" + cmn "github.com/evmos/evmos/v14/precompiles/common" +) + +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 + +// Precompile defines the precompiled contract for deposit. +type Precompile struct { + cmn.Precompile + assetsKeeper assetskeeper.Keeper +} + +// NewPrecompile creates a new deposit Precompile instance as a +// PrecompiledContract interface. +func NewPrecompile( + assetsKeeper assetskeeper.Keeper, + authzKeeper authzkeeper.Keeper, +) (*Precompile, error) { + abiBz, err := f.ReadFile("abi.json") + if err != nil { + return nil, fmt.Errorf("error loading the client chains ABI %s", err) + } + + newAbi, err := abi.JSON(bytes.NewReader(abiBz)) + if err != nil { + return nil, fmt.Errorf(cmn.ErrInvalidABI, err) + } + + return &Precompile{ + Precompile: cmn.Precompile{ + ABI: newAbi, + AuthzKeeper: authzKeeper, + KvGasConfig: storetypes.KVGasConfig(), + TransientKVGasConfig: storetypes.TransientGasConfig(), + // should be configurable in the future. + ApprovalExpiration: cmn.DefaultExpirationDuration, + }, + assetsKeeper: assetsKeeper, + }, nil +} + +// Address defines the address of the client chains precompile contract. +// address: 0x0000000000000000000000000000000000000801 +func (p Precompile) Address() common.Address { + return common.HexToAddress("0x0000000000000000000000000000000000000801") +} + +// RequiredGas calculates the precompiled contract's base gas rate. +func (p Precompile) RequiredGas(input []byte) uint64 { + methodID := input[:4] + + method, err := p.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)) +} + +// Run executes the precompiled contract client chain methods defined in the ABI. +func (p Precompile) Run( + evm *vm.EVM, contract *vm.Contract, readOnly bool, +) (bz []byte, err error) { + // if the user calls instead of staticcalls, it is their problem. we don't validate that. + ctx, _, method, initialGas, args, err := p.RunSetup( + evm, contract, readOnly, p.IsTransaction, + ) + if err != nil { + return nil, err + } + + // This handles any out of gas errors that may occur during the execution of a precompile tx + // or query. It avoids panics and returns the out of gas error so the EVM can continue + // gracefully. + defer cmn.HandleGasError(ctx, contract, initialGas, &err)() + + bz, err = p.GetClientChains(ctx, method, args) + if err != nil { + ctx.Logger().Error( + "call client chains precompile error", + "module", "client chains precompile", + "err", err, + ) + return nil, err + } + + cost := ctx.GasMeter().GasConsumed() - initialGas + + if !contract.UseGas(cost) { + return nil, vm.ErrOutOfGas + } + + return bz, nil +} + +// IsTransaction checks if the given methodID corresponds to a transaction (true) +// or query (false). +func (Precompile) IsTransaction(string) bool { + // there are no transaction methods in this precompile, only queries. + return false +} diff --git a/precompiles/clientchains/clientchains.sol b/precompiles/clientchains/clientchains.sol new file mode 100644 index 000000000..e687ff062 --- /dev/null +++ b/precompiles/clientchains/clientchains.sol @@ -0,0 +1,19 @@ +pragma solidity >=0.8.17; + +/// @dev The CLIENT_CHAINS contract's address. +address constant CLIENT_CHAINS_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000801; + +/// @dev The CLIENT_CHAINS contract's instance. +IClientChains constant CLIENT_CHAINS_CONTRACT = IClientChains( + CLIENT_CHAINS_PRECOMPILE_ADDRESS +); + +/// @author Exocore Team +/// @title Client Chains Precompile Contract +/// @dev The interface through which solidity contracts will interact with ClientChains +/// @custom:address 0x0000000000000000000000000000000000000801 +interface IClientChains { + /// @dev Returns the chain indices of the client chains. + function getClientChains() external view returns (bool, uint16[] memory); +} + diff --git a/precompiles/clientchains/clientchains_test.go b/precompiles/clientchains/clientchains_test.go new file mode 100644 index 000000000..2bd3b6b1d --- /dev/null +++ b/precompiles/clientchains/clientchains_test.go @@ -0,0 +1,100 @@ +package clientchains_test + +import ( + "math/big" + + "github.com/ExocoreNetwork/exocore/app" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + evmtypes "github.com/evmos/evmos/v14/x/evm/types" +) + +func (s *ClientChainsPrecompileSuite) TestIsTransaction() { + testCases := []struct { + name string + method string + isTx bool + }{ + { + "non existant method", + "HelloFakeMethod", + false, + }, + { + "actual method", + "getClientChains", + false, + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + s.Require().Equal(s.precompile.IsTransaction(tc.method), tc.isTx) + }) + } +} + +func paddingClientChainAddress(input []byte, outputLength int) []byte { + if len(input) < outputLength { + padding := make([]byte, outputLength-len(input)) + return append(input, padding...) + } + return input +} + +func (s *ClientChainsPrecompileSuite) TestGetClientChains() { + input, err := s.precompile.Pack("getClientChains") + s.Require().NoError(err, "failed to pack input") + output, err := s.precompile.Methods["getClientChains"].Outputs.Pack(true, []uint16{101}) + s.Require().NoError(err, "failed to pack output") + s.Run("get client chains", func() { + s.SetupTest() + baseFee := s.App.FeeMarketKeeper.GetBaseFee(s.Ctx) + contract := vm.NewPrecompile( + vm.AccountRef(s.Address), + s.precompile, + big.NewInt(0), + uint64(1e6), + ) + contract.Input = input + contractAddr := contract.Address() + txArgs := evmtypes.EvmTxArgs{ + ChainID: s.App.EvmKeeper.ChainID(), + Nonce: 0, + To: &contractAddr, + Amount: nil, + GasLimit: 100000, + GasPrice: app.MainnetMinGasPrices.BigInt(), + GasFeeCap: baseFee, + GasTipCap: big.NewInt(1), + Accesses: ðtypes.AccessList{}, + } + msgEthereumTx := evmtypes.NewTx(&txArgs) + msgEthereumTx.From = s.Address.String() + err := msgEthereumTx.Sign(s.EthSigner, s.Signer) + s.Require().NoError(err, "failed to sign Ethereum message") + proposerAddress := s.Ctx.BlockHeader().ProposerAddress + cfg, err := s.App.EvmKeeper.EVMConfig( + s.Ctx, proposerAddress, s.App.EvmKeeper.ChainID(), + ) + s.Require().NoError(err, "failed to instantiate EVM config") + msg, err := msgEthereumTx.AsMessage(s.EthSigner, baseFee) + s.Require().NoError(err, "failed to instantiate Ethereum message") + evm := s.App.EvmKeeper.NewEVM( + s.Ctx, msg, cfg, nil, s.StateDB, + ) + params := s.App.EvmKeeper.GetParams(s.Ctx) + activePrecompiles := params.GetActivePrecompilesAddrs() + precompileMap := s.App.EvmKeeper.Precompiles(activePrecompiles...) + err = vm.ValidatePrecompiles(precompileMap, activePrecompiles) + s.Require().NoError(err, "invalid precompiles", activePrecompiles) + evm.WithPrecompiles(precompileMap, activePrecompiles) + bz, err := s.precompile.Run(evm, contract, true) + s.Require().NoError( + err, "expected no error when running the precompile", + ) + s.Require().Equal( + output, bz, "the return doesn't match the expected result", + ) + }) +} diff --git a/precompiles/clientchains/setup_test.go b/precompiles/clientchains/setup_test.go new file mode 100644 index 000000000..2ca76b016 --- /dev/null +++ b/precompiles/clientchains/setup_test.go @@ -0,0 +1,32 @@ +package clientchains_test + +import ( + "testing" + + "github.com/ExocoreNetwork/exocore/precompiles/clientchains" + "github.com/ExocoreNetwork/exocore/testutil" + + "github.com/stretchr/testify/suite" +) + +var s *ClientChainsPrecompileSuite + +type ClientChainsPrecompileSuite struct { + testutil.BaseTestSuite + + precompile *clientchains.Precompile +} + +func TestPrecompileTestSuite(t *testing.T) { + s = new(ClientChainsPrecompileSuite) + suite.Run(t, s) +} + +func (s *ClientChainsPrecompileSuite) SetupTest() { + s.DoSetupTest() + precompile, err := clientchains.NewPrecompile( + s.App.AssetsKeeper, s.App.AuthzKeeper, + ) + s.Require().NoError(err) + s.precompile = precompile +} diff --git a/precompiles/clientchains/tx.go b/precompiles/clientchains/tx.go new file mode 100644 index 000000000..04ed6681a --- /dev/null +++ b/precompiles/clientchains/tx.go @@ -0,0 +1,39 @@ +package clientchains + +import ( + "math" + + errorsmod "cosmossdk.io/errors" + assetstypes "github.com/ExocoreNetwork/exocore/x/assets/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/accounts/abi" +) + +func (p Precompile) GetClientChains( + ctx sdk.Context, + method *abi.Method, + args []interface{}, +) ([]byte, error) { + if len(args) > 0 { + return nil, errorsmod.Wrapf(assetstypes.ErrInvalidInput, "no input is required") + } + infos, err := p.assetsKeeper.GetAllClientChainInfo(ctx) + if err != nil { + return nil, err + } + ids := make([]uint16, 0, len(infos)) + for id := range infos { + // technically LZ supports uint32, but unfortunately all the precompiles + // based it on uint16, so we have to stick with it. + // TODO: change it to uint32 here and in other precompiles. + if id > math.MaxUint16 { + return nil, errorsmod.Wrapf( + assetstypes.ErrInvalidInput, "client chain id is too large", + ) + } + // #nosec G701 // already checked + convID := uint16(id) + ids = append(ids, convID) + } + return method.Outputs.Pack(true, ids) +} diff --git a/x/assets/types/errors.go b/x/assets/types/errors.go index bc3592d69..ae1f7b5b3 100644 --- a/x/assets/types/errors.go +++ b/x/assets/types/errors.go @@ -10,6 +10,7 @@ var ( ModuleName, 0, "there is no stored key for the input chain index", ) + ErrNoClientChainAssetKey = errorsmod.Register( ModuleName, 1, "there is no stored key for the input assetID", @@ -80,4 +81,9 @@ var ( ModuleName, 14, "the genesis data supplied is invalid", ) + + ErrInvalidInput = errorsmod.Register( + ModuleName, 15, + "the input is invalid", + ) ) diff --git a/x/evm/keeper/precompiles.go b/x/evm/keeper/precompiles.go index 7fcdf91ed..8daf2b77c 100644 --- a/x/evm/keeper/precompiles.go +++ b/x/evm/keeper/precompiles.go @@ -6,6 +6,7 @@ import ( avsManagerPrecompile "github.com/ExocoreNetwork/exocore/precompiles/avs" taskPrecompile "github.com/ExocoreNetwork/exocore/precompiles/avsTask" blsPrecompile "github.com/ExocoreNetwork/exocore/precompiles/bls" + clientchainsprecompile "github.com/ExocoreNetwork/exocore/precompiles/clientchains" delegationprecompile "github.com/ExocoreNetwork/exocore/precompiles/delegation" depositprecompile "github.com/ExocoreNetwork/exocore/precompiles/deposit" rewardPrecompile "github.com/ExocoreNetwork/exocore/precompiles/reward" @@ -68,6 +69,14 @@ func AvailablePrecompiles( } // add exoCore chain preCompiles + clientChainsPrecompile, err := clientchainsprecompile.NewPrecompile( + stakingStateKeeper, + authzKeeper, + ) + if err != nil { + panic(fmt.Errorf("failed to load client chains precompile: %w", err)) + } + depositPrecompile, err := depositprecompile.NewPrecompile( stakingStateKeeper, depositKeeper, @@ -123,6 +132,7 @@ func AvailablePrecompiles( precompiles[slashPrecompile.Address()] = slashPrecompile precompiles[rewardPrecompile.Address()] = rewardPrecompile precompiles[withdrawPrecompile.Address()] = withdrawPrecompile + precompiles[clientChainsPrecompile.Address()] = clientChainsPrecompile precompiles[depositPrecompile.Address()] = depositPrecompile precompiles[delegationPrecompile.Address()] = delegationPrecompile precompiles[avsManagerPrecompile.Address()] = avsManagerPrecompile diff --git a/x/evm/types/params.go b/x/evm/types/params.go index 3207d58f7..6021d9a4c 100644 --- a/x/evm/types/params.go +++ b/x/evm/types/params.go @@ -11,6 +11,7 @@ var ( DefaultEVMDenom = utils.BaseDenom ExocoreAvailableEVMExtensions = []string{ "0x0000000000000000000000000000000000000800", // Staking precompile + "0x0000000000000000000000000000000000000801", // client chains precompile "0x0000000000000000000000000000000000000802", // ICS20 transfer precompile "0x0000000000000000000000000000000000000804", // deposit precompile "0x0000000000000000000000000000000000000805", // delegation precompile