Skip to main content
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.