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:
- Inherits
PolicyProtectedUpgradeable— The contract calls__PolicyProtected_initduring initialization, which sets the contract owner and connects it to a PolicyEngine. - All state-changing functions are policy-protected — Every function that modifies state carries the
runPolicymodifier. The PolicyEngine evaluates all attached policies before the function body executes. - ERC-7201 namespaced storage — All token state lives in a dedicated
ComplianceTokenStoreERC3643storage 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()returnsaddress(0).onchainID()returnsaddress(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()returnsaddress(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
| Function | Modifiers | Description |
|---|---|---|
transfer(to, amount) | whenNotPaused, runPolicy | Transfer 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, runPolicy | Transfer tokens on behalf of another address using an allowance. Same frozen and balance checks as transfer. |
forcedTransfer(from, to, amount) | runPolicy | Administrative transfer that auto-unfreezes tokens if the unfrozen balance is insufficient. |
Allowances
| Function | Modifiers | Description |
|---|---|---|
approve(spender, amount) | whenNotPaused, runPolicy | Set an allowance for a spender. |
increaseAllowance(spender, addedValue) | whenNotPaused, runPolicy | Increase an existing allowance. |
decreaseAllowance(spender, subtractedValue) | whenNotPaused, runPolicy | Decrease an existing allowance. |
Minting and burning
| Function | Modifiers | Description |
|---|---|---|
mint(to, amount) | runPolicy | Create new tokens and assign them to an address. |
burn(userAddress, amount) | runPolicy | Destroy tokens from an address. Auto-unfreezes tokens if the unfrozen balance is insufficient. |
Freezing
| Function | Modifiers | Description |
|---|---|---|
setAddressFrozen(userAddress, freeze) | runPolicy | Freeze or unfreeze an entire address. A frozen address cannot send or receive tokens through regular transfers. |
freezePartialTokens(userAddress, amount) | runPolicy | Freeze a specific amount of tokens on an account. |
unfreezePartialTokens(userAddress, amount) | runPolicy | Unfreeze a previously frozen amount on an account. |
Token administration
| Function | Modifiers | Description |
|---|---|---|
pause() | runPolicy | Pause the token. All functions with whenNotPaused will revert. |
unpause() | runPolicy | Unpause the token. |
setName(name) | runPolicy | Update the token name. |
setSymbol(symbol) | runPolicy | Update 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 regulartransferandtransferFromcalls. - 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:
| Field | Type | Description |
|---|---|---|
tokenName | string | Token name. |
tokenSymbol | string | Token symbol. |
tokenDecimals | uint8 | Decimal precision for display. |
tokenPaused | bool | Whether the token is paused. |
totalSupply | uint256 | Total supply of tokens. |
balances | mapping(address => uint256) | Per-account token balances. |
allowances | mapping(address => mapping(address => uint256)) | Per-account spender allowances. |
frozen | mapping(address => bool) | Per-account address freeze flag. |
frozenTokens | mapping(address => uint256) | Per-account frozen token amounts. |
Reference implementation
The full source code for the ERC-3643 compliance token:
- ComplianceTokenERC3643.sol — Token contract implementing the
ITokeninterface with ACE policy protection. - ComplianceTokenStoreERC3643.sol — ERC-7201 namespaced storage layout.