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.
- You declare a custom contract type with your function ABIs (
POST /contract-types). - You write an extractor contract implementing
IExtractorand deploy it onchain. - You register the extractor with the Coordinator API (
POST /extractors), pointing to the deployed contract address. - 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>"]
}'
| Field | Required | Description |
|---|---|---|
name | Yes | Human-readable name of the contract type |
description | No | Optional description |
contract_type_functions.function_abis | Yes | Array of function ABIs the contract type covers |
extractor_ids | No | Extractors 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" }
]
}'
| Field | Required | Description |
|---|---|---|
name | Yes | Human-readable name, unique per organization |
supported_function_signatures | Yes | Function signatures the extractor parses |
outputs | Yes | Named outputs the extractor produces, in order |
onchain_extractors | No | Per-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 itsassigned_contract_types.GET /policy-engines/<ID>: the engine response includes itscontract_typesandextractor_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.