Custom Contract Types & Extractors

Any contract that routes calls through a Policy Engine can use ACE policy enforcement. Most custom integrations inherit PolicyProtected, mark protected functions with the runPolicy modifier, and connect to a Policy Engine. The platform ships built-in ERC-20, ERC-3643, and CCIP-AdvancedPoolHooks contract types with pre-built, audited extractors.

For function signatures beyond the pre-built set, you combine two building blocks:

  • A custom contract type: a declaration of your contract's functions (their ABIs) that you register with the Coordinator API.
  • A custom extractor: a contract you write and deploy that parses your function calldata into named parameters for policies.

Prerequisites

  • An API key for the Coordinator API.
  • An ACE-compatible contract with the functions you want to protect.
  • A development environment for compiling and deploying Solidity contracts (for example, Foundry or Hardhat).

How it works

A contract type describes what functions a contract has. An extractor parses those functions' calldata. When a protected function is called, the PolicyEngine looks up the extractor registered for that function's selector, calls extract(), and passes the named parameters to each attached policy.

  1. You declare a custom contract type with your function ABIs (POST /contract-types).
  2. You write an extractor contract implementing IExtractor and deploy it onchain.
  3. You register the extractor with the Coordinator API (POST /extractors), pointing to the deployed contract address.
  4. You attach the contract type and extractors to your policy engine and target.

The IExtractor interface

Your extractor contract must implement IExtractor from the chainlink-ace repository:

// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.20;

import {IExtractor} from "@chainlink/policy-management/interfaces/IExtractor.sol";
import {IPolicyEngine} from "@chainlink/policy-management/interfaces/IPolicyEngine.sol";

contract MyVaultExtractor is IExtractor {
  string public constant override typeAndVersion = "MyVaultExtractor 1.0.0";

  function extract(IPolicyEngine.Payload calldata payload)
    external
    pure
    returns (IPolicyEngine.Parameter[] memory)
  {
    // payload.selector: the function selector of the protected call
    // payload.sender:  the transaction sender
    // payload.data:    the raw calldata after the selector
    (address depositor, uint256 amount) = abi.decode(payload.data, (address, uint256));

    IPolicyEngine.Parameter[] memory result = new IPolicyEngine.Parameter[](2);
    result[0] = IPolicyEngine.Parameter(keccak256("account"), abi.encode(depositor));
    result[1] = IPolicyEngine.Parameter(keccak256("amount"), abi.encode(amount));
    return result;
  }
}

The Payload struct contains the raw call context, and Parameter is a named bytes32/bytes pair:

struct Payload {
  bytes4 selector;  // function selector of the protected call
  address sender;   // transaction sender
  bytes data;       // calldata after the selector
  bytes context;    // additional context (e.g., offchain signatures)
}

struct Parameter {
  bytes32 name;     // parameter name (keccak256 hash)
  bytes value;      // ABI-encoded value
}

For reference implementations, see the pre-built extractors in the chainlink-ace repository. ERC20TransferExtractor decodes transfer and transferFrom calldata into from, to, and amount parameters.

Create a custom contract type

Register your contract type with its function ABIs:

curl -X POST https://ace.api.chain.link/v1/contract-types \
  -H "Authorization: Apikey <API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "My Vault",
    "description": "Vault with deposit/withdraw compliance checks",
    "contract_type_functions": {
      "function_abis": [
        {
          "type": "function",
          "name": "deposit",
          "inputs": [
            { "name": "depositor", "type": "address" },
            { "name": "amount", "type": "uint256" }
          ],
          "outputs": [],
          "state_mutability": "nonpayable"
        }
      ]
    },
    "extractor_ids": ["<EXTRACTOR_ID>"]
  }'
FieldRequiredDescription
nameYesHuman-readable name of the contract type
descriptionNoOptional description
contract_type_functions.function_abisYesArray of function ABIs the contract type covers
extractor_idsNoExtractors to associate with the contract type

Function ABIs support tuple types: use type: "tuple" or "tuple[]" with a components array describing each field, and internal_type for the Solidity internal type.

To list contract types (yours plus the built-ins) or fetch one by ID:

curl https://ace.api.chain.link/v1/contract-types \
  -H "Authorization: Apikey <API_KEY>"

curl https://ace.api.chain.link/v1/contract-types/<CONTRACT_TYPE_ID> \
  -H "Authorization: Apikey <API_KEY>"

GET /contract-types accepts a policy_engine_id filter to list only the types registered on a given engine.

Register your extractor

After deploying your extractor contract onchain, register it with the Coordinator API. The onchain_extractors array maps each deployment chain to the deployed contract address:

curl -X POST https://ace.api.chain.link/v1/extractors \
  -H "Authorization: Apikey <API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "MyVaultExtractor",
    "supported_function_signatures": ["deposit(address,uint256)"],
    "outputs": [
      { "name": "account", "type": "address" },
      { "name": "amount", "type": "uint256" }
    ],
    "onchain_extractors": [
      { "chain_selector": "16015286601757825753", "address": "0xYourExtractorAddress" }
    ]
  }'
FieldRequiredDescription
nameYesHuman-readable name, unique per organization
supported_function_signaturesYesFunction signatures the extractor parses
outputsYesNamed outputs the extractor produces, in order
onchain_extractorsNoPer-chain deployment addresses of your extractor contract

The outputs you declare must match what your contract's extract() returns: same names, same order.

Attach the contract type to your engine and target

When creating (or updating) your policy engine, pass contract_type_ids alongside extractor_ids:

curl -X PUT https://ace.api.chain.link/v1/policy-engines/<POLICY_ENGINE_ID> \
  -H "Authorization: Apikey <API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "My Policy Engine",
    "contract_type_ids": ["<CONTRACT_TYPE_ID>"],
    "extractor_ids": ["<EXTRACTOR_ID>"],
    "onchain_policy_engines": [{ "chain_selector": "16015286601757825753" }]
  }'

When registering your target, pass contract_type_ids so the platform knows which contract type the target implements:

curl -X POST https://ace.api.chain.link/v1/targets \
  -H "Authorization: Apikey <API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "My Vault",
    "policy_engine_id": "<POLICY_ENGINE_ID>",
    "contract_type_ids": ["<CONTRACT_TYPE_ID>"],
    "protected_methods": ["deposit(address,uint256)"],
    "onchain_targets": [
      { "chain_selector": "16015286601757825753", "address": "0xYourVaultAddress" }
    ]
  }'

From there, the flow is the same as for built-in types: create policy instances and attach them to your protected functions with extractor_output_ids mapping your extractor's outputs to the policy's parameters.

Verify your setup

  • GET /contract-types: lists your custom types plus the built-ins; each type shows its functions and associated extractors.
  • GET /extractors: your registered extractor appears with its assigned_contract_types.
  • GET /policy-engines/<ID>: the engine response includes its contract_types and extractor_registrations.

Archive a custom contract type

Archive a contract type you no longer need with a PATCH request:

curl -X PATCH https://ace.api.chain.link/v1/contract-types/<CONTRACT_TYPE_ID> \
  -H "Authorization: Apikey <API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{ "status": "archived" }'

Chainlink maintains the built-in contract types (ERC-20, ERC-3643, and CCIP-AdvancedPoolHooks); you cannot archive them.

Use the built-in CCIP type

CCIP-AdvancedPoolHooks is a built-in contract type with a pre-built extractor for preflightCheck and postflightCheck. Do not register it as a custom contract type or create your own extractor for these functions.

See Protect CCIP Token Pools with ACE to configure the type, connect AdvancedPoolHooks, use its automatically detected target, and map hook parameters to policies.

Get the latest Chainlink content straight to your inbox.