Building an ERC-3643 Compliance Token

The ComplianceTokenERC3643 implements the ERC-3643 (T-REX) IToken interface but replaces the canonical T-REX identity and compliance systems with ACE equivalents. It inherits PolicyProtectedUpgradeable, is deployed behind a proxy, and routes all state-changing functions through a PolicyEngine.

For a comparison with the ERC-20 variant and guidance on which to choose, see Building a New Contract.

What makes it ACE-compatible

The token satisfies all the requirements described in Making Your Contract ACE-Compatible:

  1. Inherits PolicyProtectedUpgradeable — The contract calls __PolicyProtected_init during initialization, which sets the contract owner and connects it to a PolicyEngine.
  2. All state-changing functions are policy-protected — Every function that modifies state carries the runPolicy modifier. The PolicyEngine evaluates all attached policies before the function body executes.
  3. ERC-7201 namespaced storage — All token state lives in a dedicated ComplianceTokenStoreERC3643 storage struct, following the ERC-7201 pattern for safe upgradeable storage.

How it differs from canonical T-REX

This implementation keeps the IToken interface that T-REX tooling and auditors expect, but swaps out the two internal subsystems for ACE equivalents:

Identity: ACE Cross-Chain Identity replaces ONCHAINID

The canonical T-REX stack uses ONCHAINID for on-chain identity claims. This implementation replaces it with ACE's Cross-Chain Identity infrastructure (IdentityRegistry and CredentialRegistry). The legacy interface stubs remain to satisfy IToken but are not functional:

  • identityRegistry() returns address(0).
  • onchainID() returns address(0).
  • setIdentityRegistry() reverts with "Not implemented".
  • setOnchainID() reverts with "Not implemented".

Identity verification is handled through ACE policies that validate credentials against the IdentityRegistry and CredentialRegistry.

Compliance: ACE Policy Management replaces ModularCompliance

The canonical T-REX stack uses ModularCompliance for transfer rules. This implementation replaces it with ACE's Policy Management system, where compliance rules are defined as policies attached to the PolicyEngine. The legacy stub remains:

  • compliance() returns address(0).
  • setCompliance() reverts with "Not implemented".

Wallet recovery not implemented

  • recoveryAddress() reverts with "Not implemented". Wallet recovery is not supported in this implementation.

Protected functions

Every state-changing function on the token is policy-protected with runPolicy, which intercepts each call and routes it through the PolicyEngine. The engine evaluates all attached policies before the function body executes. Functions that interact with user balances also carry the whenNotPaused modifier, which checks the token's pause state before proceeding.

Transfers

FunctionModifiersDescription
transfer(to, amount)whenNotPaused, runPolicyTransfer tokens from the caller to another address. Checks that neither wallet is frozen and that the sender has sufficient unfrozen balance.
transferFrom(from, to, amount)whenNotPaused, runPolicyTransfer tokens on behalf of another address using an allowance. Same frozen and balance checks as transfer.
forcedTransfer(from, to, amount)runPolicyAdministrative transfer that auto-unfreezes tokens if the unfrozen balance is insufficient.

Allowances

FunctionModifiersDescription
approve(spender, amount)whenNotPaused, runPolicySet an allowance for a spender.
increaseAllowance(spender, addedValue)whenNotPaused, runPolicyIncrease an existing allowance.
decreaseAllowance(spender, subtractedValue)whenNotPaused, runPolicyDecrease an existing allowance.

Minting and burning

Function
Modifiers
Description
mint(to, amount)runPolicyCreate new tokens and assign them to an address.
burn(userAddress, amount)runPolicyDestroy tokens from an address. Auto-unfreezes tokens if the unfrozen balance is insufficient.

Freezing

Function
Modifiers
Description
setAddressFrozen(userAddress, freeze)runPolicyFreeze or unfreeze an entire address. A frozen address cannot send or receive tokens through regular transfers.
freezePartialTokens(userAddress, amount)runPolicyFreeze a specific amount of tokens on an account.
unfreezePartialTokens(userAddress, amount)runPolicyUnfreeze a previously frozen amount on an account.

Token administration

FunctionModifiersDescription
pause()runPolicyPause the token. All functions with whenNotPaused will revert.
unpause()runPolicyUnpause the token.
setName(name)runPolicyUpdate the token name.
setSymbol(symbol)runPolicyUpdate the token symbol.

Frozen token behavior

ComplianceTokenERC3643 uses an automatic unfreezing model, following the standard T-REX approach. There are two independent freeze mechanisms:

  • Address freeze — A boolean flag (frozen[address]) that blocks an address from sending or receiving tokens through regular transfer and transferFrom calls.
  • Partial token freeze — A numeric amount (frozenTokens[address]) that restricts how many of an account's tokens can be moved. Available balance = total balance - frozen tokens.

Regular transfers check both: the wallet must not be address-frozen, and the transfer amount must not exceed the unfrozen balance.

Administrative operations auto-unfreeze. When forcedTransfer or burn is called and the unfrozen balance is insufficient, the contract automatically reduces frozenTokens by the shortfall and emits a TokensUnfrozen event. This means administrative actions are never blocked by partial frozen status — the admin has already decided the operation is necessary.

Built-in pause

The token includes a built-in pause/unpause mechanism. Both functions are policy-protected. When paused, all functions carrying the whenNotPaused modifier revert — this includes transfer, transferFrom, approve, increaseAllowance, and decreaseAllowance.

Administrative functions (mint, burn, forcedTransfer, freeze operations) do not carry whenNotPaused and remain callable while the token is paused.

Batch operations

The ERC-3643 token supports batch operations for managing large numbers of holders efficiently:

  • batchTransfer — Transfer to multiple recipients in a single transaction.
  • batchForcedTransfer — Force-transfer between multiple address pairs.
  • batchMint — Mint to multiple recipients.
  • batchBurn — Burn from multiple addresses.
  • batchSetAddressFrozen — Freeze or unfreeze multiple addresses.
  • batchFreezePartialTokens — Freeze token amounts on multiple accounts.
  • batchUnfreezePartialTokens — Unfreeze token amounts on multiple accounts.

Each batch function delegates to its single-item counterpart in a loop, so every individual operation goes through runPolicy independently.

Storage layout

All token state is stored in ComplianceTokenStoreERC3643, which uses ERC-7201 namespaced storage at a deterministic slot:

FieldTypeDescription
tokenNamestringToken name.
tokenSymbolstringToken symbol.
tokenDecimalsuint8Decimal precision for display.
tokenPausedboolWhether the token is paused.
totalSupplyuint256Total supply of tokens.
balancesmapping(address => uint256)Per-account token balances.
allowancesmapping(address => mapping(address => uint256))Per-account spender allowances.
frozenmapping(address => bool)Per-account address freeze flag.
frozenTokensmapping(address => uint256)Per-account frozen token amounts.

Reference implementation

The full source code for the ERC-3643 compliance token:

Get the latest Chainlink content straight to your inbox.