预编译合约是在固定地址上暴露的智能合约接口,其实现以原生 Go 代码运行。Cosmos EVM 内置了用于质押、治理、IBC 等功能的预编译合约。作为链的构建者,你可以控制哪些预编译合约启用,也可以添加自定义预编译合约。更多信息以及完整的可用预编译合约列表,请参阅预编译合约概览。

启用预编译合约

预编译合约通过 vm 模块中的 active_static_precompiles 参数启用。只有这里列出的地址才能在运行时被调用。有关所有内置预编译合约及其地址的完整列表,请参阅预编译合约概览。
  1. 在 app.go 中使用 .WithStaticPrecompiles() 将预编译合约接入 EVM keeper。标准做法是传入 precompiletypes.DefaultStaticPrecompiles(...),它包含所有内置预编译合约:
evmd/app.go
).WithStaticPrecompiles(
    precompiletypes.DefaultStaticPrecompiles(
        *app.StakingKeeper,
        app.DistrKeeper,
        app.PreciseBankKeeper,
        &app.Erc20Keeper,
        &app.TransferKeeper,
        app.IBCKeeper.ChannelKeeper,
        app.IBCKeeper.ClientKeeper,
        app.GovKeeper,
        app.SlashingKeeper,
        appCodec,
    ),
)
如果你希望使用自定义集合,可将 DefaultStaticPrecompiles 替换为你自己的构建链(见下文的添加自定义预编译合约)。
  1. 在你的创世配置中设置启用的预编译合约(evmd/genesis.go):
func NewEVMGenesisState() *evmtypes.GenesisState {
    evmGenState := evmtypes.DefaultGenesisState()

    // Enable all available precompiles
    evmGenState.Params.ActiveStaticPrecompiles = evmtypes.AvailableStaticPrecompiles
    evmGenState.Preinstalls = evmtypes.DefaultPreinstalls

    return evmGenState
}
如果只想启用特定子集,请显式传入地址。地址必须按排序顺序填写(见下文的添加自定义预编译合约):
evmGenState.Params.ActiveStaticPrecompiles = []string{
    evmtypes.StakingPrecompileAddress,      // 0x0000000000000000000000000000000000000800
    evmtypes.DistributionPrecompileAddress, // 0x0000000000000000000000000000000000000801
    evmtypes.BankPrecompileAddress,         // 0x0000000000000000000000000000000000000804
}
可用地址的完整列表定义在 x/vm/types/precompiles.go 中。更多信息请参阅预编译合约概览。 已经注册的预编译合约也可以在链上线后,通过面向 vm 模块 active_static_precompiles 参数的治理参数变更提案来启用或禁用。若要新增真正全新的自定义预编译合约,则必须执行链升级,因为其实现位于 Go 二进制中。

添加自定义预编译合约

下面的示例添加了一个有状态的 DenomSupply 预编译合约,它只包含一个 supplyOf 方法,用于直接从 Cosmos bank 模块读取代币总供应量。这个示例展示了基础预编译合约的核心模式:注入一个 Cosmos SDK keeper,并使用 RunNativeAction 在 EVM 调用中访问链上的实时状态。

1. 创建预编译合约包

创建目录 precompiles/denomsupply/,并在其中添加两个文件:
mkdir -p precompiles/denomsupply
precompiles/denomsupply/abi.json:Solidity ABI:
precompiles/denomsupply/abi.json
[
  {
    "inputs": [{"internalType": "string", "name": "denom", "type": "string"}],
    "name": "supplyOf",
    "outputs": [{"internalType": "uint256", "name": "amount", "type": "uint256"}],
    "stateMutability": "view",
    "type": "function"
  }
]
precompiles/denomsupply/denomsupply.go:实现代码:
precompiles/denomsupply/denomsupply.go
package denomsupply

import (
    "bytes"
    _ "embed"
    "fmt"

    "github.com/ethereum/go-ethereum/accounts/abi"
    "github.com/ethereum/go-ethereum/common"
    "github.com/ethereum/go-ethereum/core/vm"

    cmn "github.com/cosmos/evm/precompiles/common"
    evmtypes "github.com/cosmos/evm/x/vm/types"

    storetypes "cosmossdk.io/store/types"
    sdk "github.com/cosmos/cosmos-sdk/types"
)

var _ vm.PrecompiledContract = &Precompile{}

var (
    //go:embed abi.json
    f   []byte
    ABI abi.ABI
)

func init() {
    var err error
    ABI, err = abi.JSON(bytes.NewReader(f))
    if err != nil {
        panic(err)
    }
}

// Precompile queries the total supply of a Cosmos denomination from the bank module.
type Precompile struct {
    cmn.Precompile
    bankKeeper cmn.BankKeeper
}

func NewPrecompile(bankKeeper cmn.BankKeeper) *Precompile {
    return &Precompile{
        Precompile: cmn.Precompile{
            KvGasConfig:          storetypes.GasConfig{},
            TransientKVGasConfig: storetypes.GasConfig{},
            ContractAddress:      common.HexToAddress(evmtypes.DenomSupplyPrecompileAddress),
        },
        bankKeeper: bankKeeper,
    }
}

func (p Precompile) RequiredGas(_ []byte) uint64 {
    return 3_000
}

// Run executes the precompile inside the Cosmos EVM context.
// RunNativeAction bridges the EVM execution environment to the Cosmos SDK,
// providing an sdk.Context with access to all module state.
func (p Precompile) Run(evm *vm.EVM, contract *vm.Contract, readonly bool) ([]byte, error) {
    return p.RunNativeAction(evm, contract, func(ctx sdk.Context) ([]byte, error) {
        method, args, err := cmn.SetupABI(ABI, contract, readonly, p.IsTransaction)
        if err != nil {
            return nil, err
        }

        switch method.Name {
        case "supplyOf":
            denom, ok := args[0].(string)
            if !ok {
                return nil, fmt.Errorf("invalid argument: expected string")
            }
            coin := p.bankKeeper.GetSupply(ctx, denom)
            return method.Outputs.Pack(coin.Amount.BigInt())
        }

        return nil, fmt.Errorf("unknown method: %s", method.Name)
    })
}

// IsTransaction returns false because supplyOf is a read-only query.
func (Precompile) IsTransaction(_ *abi.Method) bool {
    return false
}
该结构体通过嵌入 cmn.Precompile 来工作,而不是直接自行处理上下文。RunNativeAction 会负责设置 SDK 上下文、管理 gas 计量,并处理快照与回滚,从而确保预编译合约调用能够正确参与 EVM 交易原子性。在闭包内部,p.bankKeeper 用于访问 bank 模块状态。

2. 注册地址

在 x/vm/types/precompiles.go 中,在常量块结束的 ) 之前添加一个常量(第 17 行),并在 AvailableStaticPrecompiles 结束的 } 之前将其追加进去(第 35 行,会因插入常量而整体下移 1 行)。地址必须按排序顺序排列。 添加常量(插入到第 17 行结束的 ) 之前):
x/vm/types/precompiles.go
	DenomSupplyPrecompileAddress   = "0x0000000000000000000000000000000000000809"
将其追加到 AvailableStaticPrecompiles 切片中(插入到第 35 行结束的 } 之前,会因前一次插入而整体下移 1 行):
x/vm/types/precompiles.go
	DenomSupplyPrecompileAddress, // appended in sorted order: 0x...0807 < 0x...0809

3. 添加构建方法

在 precompiles/types/static_precompiles.go 中,为新包添加 import(插入到第 16 行 ics02precompile 之前,位于 govprecompile 与 ics02precompile 之间):
precompiles/types/static_precompiles.go
	denomsupplyprecompile "github.com/cosmos/evm/precompiles/denomsupply"
然后在文件末尾添加 With 方法。将你的预编译合约所需的 keeper 作为参数传入:
precompiles/types/static_precompiles.go

func (s StaticPrecompiles) WithDenomSupplyPrecompile(bankKeeper cmn.BankKeeper) StaticPrecompiles {
	denomSupplyPrecompile := denomsupplyprecompile.NewPrecompile(bankKeeper)
	s[denomSupplyPrecompile.Address()] = denomSupplyPrecompile
	return s
}

4. 接入应用

在 precompiles/types/defaults.go 中,把你的方法添加到构建链里,方式是替换第 89 行的 WithSlashingPrecompile。bankKeeper 已经是 DefaultStaticPrecompiles 的参数之一:
precompiles/types/defaults.go
		WithSlashingPrecompile(slashingKeeper, bankKeeper, opts...).
		WithDenomSupplyPrecompile(bankKeeper)

5. 在创世阶段启用

由于 evmd/genesis.go 已经使用了 evmtypes.AvailableStaticPrecompiles,因此只要在步骤 2 中把你的地址加入该切片即可,无需修改 genesis.go。 如果你在本地开发中使用 local_node.sh,该脚本会通过 jq 命令硬编码预编译合约列表,而不会在运行时读取 AvailableStaticPrecompiles。请在 local_node.sh 第 244 行之前(即第 243 行 active_static_precompiles 的 jq 命令后面的空行处)插入以下内容,以追加你的地址:
local_node.sh
  jq '.app_state["evm"]["params"]["active_static_precompiles"] +=
    ["0x0000000000000000000000000000000000000809"]' \
    "$GENESIS" >"$TMP_GENESIS" && mv "$TMP_GENESIS" "$GENESIS"

6. 构建并验证

make install
在后台启动本地区块链:
bash local_node.sh -y --no-install
链启动后,调用该预编译合约,并一步拿到解码后的结果:
cast call 0x0000000000000000000000000000000000000809 \
  "supplyOf(string)(uint256)" "atest" \
  --rpc-url http://localhost:8545
# 100025807224055573593873019 [1e26]
如果成功,你应该会在输出中看到 100025807224055573593873019。
Precompiles are smart contract interfaces at fixed addresses where the implementation runs as native Go code. Cosmos EVM ships with precompiles for staking, governance, IBC, and more. As a chain builder you control which ones are active and can add your own. For more information and to see the full list of available precompiles, see the precompiles overview.

Enabling Precompiles

Precompiles are enabled via the active_static_precompiles parameter in the vm module. Only addresses listed here are callable at runtime. For the full list of built-in precompiles and their addresses, see the precompiles overview.
  1. Wire precompiles into the EVM keeper in app.go using .WithStaticPrecompiles(). The standard way is to pass precompiletypes.DefaultStaticPrecompiles(...), which includes all built-in precompiles:
evmd/app.go
).WithStaticPrecompiles(
    precompiletypes.DefaultStaticPrecompiles(
        *app.StakingKeeper,
        app.DistrKeeper,
        app.PreciseBankKeeper,
        &app.Erc20Keeper,
        &app.TransferKeeper,
        app.IBCKeeper.ChannelKeeper,
        app.IBCKeeper.ClientKeeper,
        app.GovKeeper,
        app.SlashingKeeper,
        appCodec,
    ),
)
If you want a custom set, replace DefaultStaticPrecompiles with your own builder chain (see Adding a Custom Precompile below).
  1. Set the active precompiles in your genesis configuration (evmd/genesis.go):
func NewEVMGenesisState() *evmtypes.GenesisState {
    evmGenState := evmtypes.DefaultGenesisState()

    // Enable all available precompiles
    evmGenState.Params.ActiveStaticPrecompiles = evmtypes.AvailableStaticPrecompiles
    evmGenState.Preinstalls = evmtypes.DefaultPreinstalls

    return evmGenState
}
To enable only a specific subset, pass the addresses explicitly. Addresses must be in sorted order (see Adding a Custom Precompile below):
evmGenState.Params.ActiveStaticPrecompiles = []string{
    evmtypes.StakingPrecompileAddress,      // 0x0000000000000000000000000000000000000800
    evmtypes.DistributionPrecompileAddress, // 0x0000000000000000000000000000000000000801
    evmtypes.BankPrecompileAddress,         // 0x0000000000000000000000000000000000000804
}
The full list of available addresses is defined in x/vm/types/precompiles.go. See the precompiles overview for more information. Already-registered precompiles can also be enabled or disabled after launch via a governance parameter change proposal targeting the vm module’s active_static_precompiles param. Adding a genuinely new custom precompile requires a chain upgrade, since the implementation lives in the Go binary.

Adding a Custom Precompile

The following example adds a stateful DenomSupply precompile with a single supplyOf method that reads total token supply directly from the Cosmos bank module. This demonstrates the core pattern for basic precompiles: injecting a Cosmos SDK keeper and using RunNativeAction to access live chain state from an EVM call.

1. Create the precompile package

Create a directory precompiles/denomsupply/ with two files:
mkdir -p precompiles/denomsupply
precompiles/denomsupply/abi.json — the Solidity ABI:
precompiles/denomsupply/abi.json
[
  {
    "inputs": [{"internalType": "string", "name": "denom", "type": "string"}],
    "name": "supplyOf",
    "outputs": [{"internalType": "uint256", "name": "amount", "type": "uint256"}],
    "stateMutability": "view",
    "type": "function"
  }
]
precompiles/denomsupply/denomsupply.go — the implementation:
precompiles/denomsupply/denomsupply.go
package denomsupply

import (
    "bytes"
    _ "embed"
    "fmt"

    "github.com/ethereum/go-ethereum/accounts/abi"
    "github.com/ethereum/go-ethereum/common"
    "github.com/ethereum/go-ethereum/core/vm"

    cmn "github.com/cosmos/evm/precompiles/common"
    evmtypes "github.com/cosmos/evm/x/vm/types"

    storetypes "cosmossdk.io/store/types"
    sdk "github.com/cosmos/cosmos-sdk/types"
)

var _ vm.PrecompiledContract = &Precompile{}

var (
    //go:embed abi.json
    f   []byte
    ABI abi.ABI
)

func init() {
    var err error
    ABI, err = abi.JSON(bytes.NewReader(f))
    if err != nil {
        panic(err)
    }
}

// Precompile queries the total supply of a Cosmos denomination from the bank module.
type Precompile struct {
    cmn.Precompile
    bankKeeper cmn.BankKeeper
}

func NewPrecompile(bankKeeper cmn.BankKeeper) *Precompile {
    return &Precompile{
        Precompile: cmn.Precompile{
            KvGasConfig:          storetypes.GasConfig{},
            TransientKVGasConfig: storetypes.GasConfig{},
            ContractAddress:      common.HexToAddress(evmtypes.DenomSupplyPrecompileAddress),
        },
        bankKeeper: bankKeeper,
    }
}

func (p Precompile) RequiredGas(_ []byte) uint64 {
    return 3_000
}

// Run executes the precompile inside the Cosmos EVM context.
// RunNativeAction bridges the EVM execution environment to the Cosmos SDK,
// providing an sdk.Context with access to all module state.
func (p Precompile) Run(evm *vm.EVM, contract *vm.Contract, readonly bool) ([]byte, error) {
    return p.RunNativeAction(evm, contract, func(ctx sdk.Context) ([]byte, error) {
        method, args, err := cmn.SetupABI(ABI, contract, readonly, p.IsTransaction)
        if err != nil {
            return nil, err
        }

        switch method.Name {
        case "supplyOf":
            denom, ok := args[0].(string)
            if !ok {
                return nil, fmt.Errorf("invalid argument: expected string")
            }
            coin := p.bankKeeper.GetSupply(ctx, denom)
            return method.Outputs.Pack(coin.Amount.BigInt())
        }

        return nil, fmt.Errorf("unknown method: %s", method.Name)
    })
}

// IsTransaction returns false because supplyOf is a read-only query.
func (Precompile) IsTransaction(_ *abi.Method) bool {
    return false
}
The struct embeds cmn.Precompile rather than handling context directly. RunNativeAction sets up the SDK context, manages gas metering, and handles snapshot/revert so that precompile calls participate correctly in EVM transaction atomicity. Inside the closure, p.bankKeeper provides access to the bank module’s state.

2. Register the address

In x/vm/types/precompiles.go, add a constant before the closing ) of the const block (line 17) and append it to AvailableStaticPrecompiles before its closing } (line 35, shifted +1 by the constant insert). Addresses must be in sorted order. Add the constant (inserted before the closing ) at line 17):
x/vm/types/precompiles.go
	DenomSupplyPrecompileAddress   = "0x0000000000000000000000000000000000000809"
Append it to the AvailableStaticPrecompiles slice (inserted before the closing } at line 35, shifted +1 by the previous insert):
x/vm/types/precompiles.go
	DenomSupplyPrecompileAddress, // appended in sorted order: 0x...0807 < 0x...0809

3. Add a builder method

In precompiles/types/static_precompiles.go, add the import for the new package (inserted before ics02precompile at line 16, between govprecompile and ics02precompile):
precompiles/types/static_precompiles.go
	denomsupplyprecompile "github.com/cosmos/evm/precompiles/denomsupply"
Then add the With method at the very end of the file. Pass any keepers your precompile needs as parameters:
precompiles/types/static_precompiles.go

func (s StaticPrecompiles) WithDenomSupplyPrecompile(bankKeeper cmn.BankKeeper) StaticPrecompiles {
	denomSupplyPrecompile := denomsupplyprecompile.NewPrecompile(bankKeeper)
	s[denomSupplyPrecompile.Address()] = denomSupplyPrecompile
	return s
}

4. Wire it into the app

In precompiles/types/defaults.go, add your method to the builder chain by replacing line 89 (WithSlashingPrecompile). The bankKeeper is already a parameter of DefaultStaticPrecompiles:
precompiles/types/defaults.go
		WithSlashingPrecompile(slashingKeeper, bankKeeper, opts...).
		WithDenomSupplyPrecompile(bankKeeper)

5. Activate at genesis

Because evmd/genesis.go already uses evmtypes.AvailableStaticPrecompiles, adding your address to that slice in Step 2 is sufficient — no change to genesis.go is required. If you use local_node.sh for local development, that script hardcodes the precompile list via a jq command and does not read from AvailableStaticPrecompiles at runtime. Insert the following before line 244 of local_node.sh (the blank line after the active_static_precompiles jq command at line 243) to append your address:
local_node.sh
  jq '.app_state["evm"]["params"]["active_static_precompiles"] +=
    ["0x0000000000000000000000000000000000000809"]' \
    "$GENESIS" >"$TMP_GENESIS" && mv "$TMP_GENESIS" "$GENESIS"

6. Build and verify

make install
Start a local chain in the background:
bash local_node.sh -y --no-install
Once the chain is running, call the precompile and get a decoded result in one step:
cast call 0x0000000000000000000000000000000000000809 \
  "supplyOf(string)(uint256)" "atest" \
  --rpc-url http://localhost:8545
# 100025807224055573593873019 [1e26]
When successful, you should see 100025807224055573593873019 in the output.