> For the complete documentation index, see [llms.txt](https://docs.t-rex.network/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.t-rex.network/build/pre-flight-checks-with-utilitychecker.md).

# Pre-flight checks with UtilityChecker

A transfer the token refuses reverts, and the holder pays for the failed transaction. `UtilityChecker` answers, before anything is signed, whether a transfer would succeed and which check would refuse it. This page is for the front-end or back-end developer who shows that answer to a holder.

## What `UtilityChecker` is

A read-only helper contract. Every function is a `view` that takes the address of a token, so one deployment answers for every T-REX token of a network.

|                     |                                                                                                                                                                                                                    |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Deployment**      | One implementation behind a `UtilityCheckerProxy` (ERC-1967, UUPS), deployed once per network by `scripts/DeployUtilityChecker.s.sol`, which records the proxy address in the deployments manifest of the network. |
| **Upgrade**         | `upgradeToAndCall`, by the owner of the proxy only: the account that deployed it, or the account it transferred ownership to. The proxy address does not change.                                                   |
| **State and roles** | It stores its owner and its implementation, and nothing about any token. It holds no role in any AccessManager, and no contract of a token suite refers to it.                                                     |

> **Trust statement.** `UtilityChecker` decides nothing: the token makes every check again when the transfer executes. Its owner can replace its code, so a wrong answer can mislead a display. It cannot move or block a token. An application that does not accept this reads the direct views listed at the end of this page.

## The interface

```solidity
import { IIdentityFactory } from "@onchain-id/solidity/contracts/factory/IIdentityFactory.sol";
import { IClaimIssuer } from "@onchain-id/solidity/contracts/interface/IClaimIssuer.sol";

interface IUtilityChecker {
    struct ComplianceCheckDetails {
        string moduleName;
        uint256 allowedAmount;
        bool pass;
    }

    struct EligibilityCheckDetails {
        IClaimIssuer issuer;
        uint256 topic;
        bool pass;
    }

    function getFreezeStatus(address _token, address _from, address _to, uint256 _amount)
        external view returns (bool _frozen, uint256 _availableBalance);

    function getTransferStatus(address _token, address _from, address _to, uint256 _amount)
        external view returns (bool _freezeStatus, bool _eligibilityStatus, bool _complianceStatus);

    function getTransferDetails(address _token, address _from, address _to, uint256 _value)
        external view returns (ComplianceCheckDetails[] memory _details);

    function getVerifiedDetails(address _token, address _userAddress)
        external view
        returns (EligibilityCheckDetails[] memory _details, IIdentityFactory.AccountStatus _walletStatus);
}
```

## The four functions

### `getTransferStatus`

The overall answer for a transfer between two holders, as opposed to a mint, a burn or a forced transfer, which pass [other checks](/the-primary-register/eligibility-and-compliance-the-two-gates.md). The three booleans are computed in this order, and all three are always computed.

| Return value         | True when                                                                                 | The token would otherwise revert with                         |
| -------------------- | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
| `_freezeStatus`      | `Token.paused()` is false, and `getFreezeStatus` returns `_frozen` false                  | `EnforcedPause`, `FrozenWallet` or `ERC20InsufficientBalance` |
| `_eligibilityStatus` | `TREXRegistry.isVerified(to)` is true. This is the call the token makes.                  | `UnverifiedIdentity`                                          |
| `_complianceStatus`  | Every entry of `getTransferDetails` has `pass` true. True when no `RULE` module is bound. | `ComplianceNotFollowed`                                       |

The token makes the same checks in the same order: pause, sender address frozen, recipient address frozen, free balance, eligibility of the recipient, rules. It stops at the first one that fails, where `getTransferStatus` reports the three groups together.

`_complianceStatus` is recomputed module by module. It is not a call to `ModularCompliance.canTransfer`, and differs from it in one case, listed under "What it does not tell you".

### `getFreezeStatus`

| Case                                                   | `_frozen`                                       | `_availableBalance`                                                                                   |
| ------------------------------------------------------ | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| `Token.isFrozen(from)` or `Token.isFrozen(to)` is true | true                                            | 0, whatever the sender holds                                                                          |
| Otherwise                                              | true when `amount` is above `_availableBalance` | `Token.balanceOf(from)` minus `Token.getFrozenTokens(from)`, the value of `Token.freeBalanceOf(from)` |

`_frozen` is therefore also true for an amount above the balance when nothing is frozen. The pause is not part of this function.

### `getVerifiedDetails`

Explains the eligibility of one address. In code order:

1. **Wallet status.** `IdentityFactory.getAccountStatus` for the address on this chain. The factory is the one `TREXRegistry.identityFactory()` names.
2. **Identity.** `TREXRegistry.identity(userAddress)`. A revoked wallet still resolves here.
3. **Required topics.** The selection `isVerified` makes: the topics defined for the type of the identity when there are any (`getClaimTopicsForIdentityType`), the default topics otherwise (`getClaimTopics`). The type comes from `IdentityFactory.identityTypeOf`.
4. **One entry per required topic,** in the order of that list. For each topic, the function goes through `getTrustedIssuersForClaimTopic(topic)`, reads the claim each issuer made on the identity, and asks the issuer `isClaimValid`.

| Case                                | The entry of the topic                                                                                                                          |
| ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| No topic is required                | The array is empty.                                                                                                                             |
| No claim from any trusted issuer    | Left empty: `issuer` is the zero address, `topic` is 0, `pass` is false. Take the topic from the list of required topics, at the same index.    |
| A claim from one trusted issuer     | `issuer` is that Claim Issuer, `topic` the topic, `pass` the answer of `isClaimValid`, or false when that call reverts.                         |
| Claims from several trusted issuers | The entry describes the last of them, in the order of `getTrustedIssuersForClaimTopic`.                                                         |
| The address resolves to no identity | The call reverts as soon as one required topic has a trusted issuer: there is no identity to read. Read `TREXRegistry.identity(address)` first. |

| `_walletStatus` | Meaning                                                                                                                                                                                                                        |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `None`          | The factory never linked this address. It can still be registered for this asset by a Registry Agent, and be eligible. Also returned for a registry that has no `identityFactory()` function. A `TREXRegistry` always has one. |
| `Active`        | The address is linked to an identity in the global identity registry. An ONCHAINID's own address reports `Active`.                                                                                                             |
| `Revoked`       | The Identity Owner revoked the link. The address cannot receive, whatever the entries say.                                                                                                                                     |

> **`isVerified` decides, `getVerifiedDetails` explains.** They differ in three cases. A revoked wallet: every entry can pass. Several trusted issuers for one topic: `isVerified` accepts any valid claim, the entry shows the last one. Eligibility checks turned off with `disableEligibilityChecks`: `isVerified` is true for every address.

### `getTransferDetails`

Explains the rules. The function reads `Token.compliance()`, takes the modules of type `RULE` with `getModulesByType`, and builds the context the compliance builds for a transfer: both identities resolved, both wallets, the exact amount, no spender. Modules that are only `SPENDER` or `TRACKER` are not listed, since the compliance never asks them for an amount. With no `RULE` module, the array is empty.

| Field           | Content                                                                                                                               |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `moduleName`    | `IModule.name()` of the module, for example `MaxBalancePerIdentityModule`.                                                            |
| `allowedAmount` | `IModule.allowedAmount(ctx)`: the largest amount this rule allows for this pair. `type(uint256).max` means no limit, 0 means refused. |
| `pass`          | `value <= allowedAmount`.                                                                                                             |

For rules that are monotonic, the largest amount every rule accepts is the smallest `allowedAmount` of the array. A rule that is not monotonic, a multiple of a unit for instance, answers `type(uint256).max` or 0 for the amount asked, and says nothing about other amounts.

## What it does not tell you

| Not covered                                                                                                                 | In the transaction                                                                                                          | Check it with                                                                                                                                                                                      |
| --------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| An amount of zero                                                                                                           | Reverts with `ZeroValue`.                                                                                                   | Refuse zero in the form.                                                                                                                                                                           |
| The allowance, for `transferFrom`                                                                                           | `ERC20InsufficientAllowance`                                                                                                | `Token.allowance(from, spender)`                                                                                                                                                                   |
| The spender policy, for `transferFrom`                                                                                      | `SpenderNotAllowed`, before the allowance is spent                                                                          | `ModularCompliance.canSpenderCall(spender, from, to, amount)`                                                                                                                                      |
| A sender with no identity, or a recipient with no identity while eligibility checks are off, while a `RULE` module is bound | `canTransfer` refuses outright: `ComplianceNotFollowed`. `getTransferDetails` still asks the modules and can report a pass. | `ModularCompliance.canTransfer(from, to, amount)`, or `TREXRegistry.identity(from)` and `TREXRegistry.identity(to)`, which must not be zero                                                        |
| The caller, for `identityTransfer`                                                                                          | `NotLinkedIdentity` when the caller is not the identity the sending address resolves to.                                    | `TREXRegistry.identity(from)`                                                                                                                                                                      |
| Positions on satellites                                                                                                     | Only the balance on the reference chain is read, and the context is the one of a native movement.                           | `Token.availableOf(wallet)`, with `wallet` the ERC-7930 interoperable address of the wallet on the satellite, and [31 Transfer on a satellite](/guides/cross-chain/31.-transfer-on-a-satellite.md) |
| What changes after the read                                                                                                 | A freeze, a revoked claim or another transfer can change the outcome.                                                       | Handle the revert as well.                                                                                                                                                                         |

## From answer to message

The messages are suggestions, in line with [Integrate a wallet or a custodian](/build/integrate-a-wallet-or-a-custodian.md).

| Outcome                                                | Revert it predicts         | Tell the holder                                                                                      | What can be done                                                                                                                                                         |
| ------------------------------------------------------ | -------------------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `Token.paused()` is true                               | `EnforcedPause`            | The asset is halted by its issuer.                                                                   | Try again later.                                                                                                                                                         |
| `Token.isFrozen(from)` or `Token.isFrozen(to)` is true | `FrozenWallet`             | This address, or the recipient's, is frozen by the issuer's agents.                                  | Contact the Issuer.                                                                                                                                                      |
| `_frozen` is true and no address is frozen             | `ERC20InsufficientBalance` | The amount exceeds the free balance. Part of the balance is frozen, or active on a satellite.        | Propose `_availableBalance`.                                                                                                                                             |
| `TREXRegistry.identity(to)` is zero                    | `UnverifiedIdentity`       | The recipient has no identity for this asset.                                                        | The recipient deploys an ONCHAINID and links its wallet: [02](/guides/identity/02.-deploy-an-onchainid.md), [07](/guides/identity/07.-link-a-wallet-to-an-onchainid.md). |
| `_walletStatus` is `Revoked`                           | `UnverifiedIdentity`       | The recipient's wallet link was revoked.                                                             | Ask the recipient for another address. A revocation is final.                                                                                                            |
| An entry with a zero `issuer`                          | `UnverifiedIdentity`       | A credential the asset requires is missing: the topic at that index.                                 | The recipient obtains it from a Claim Issuer the asset trusts: [05](/guides/claim-issuer/05.-issue-a-claim-to-an-investor.md).                                           |
| An entry with an `issuer` and `pass` false             | `UnverifiedIdentity`       | The credential from this Claim Issuer is expired, revoked or invalid.                                | The recipient renews it with that Claim Issuer.                                                                                                                          |
| `_complianceStatus` is false                           | `ComplianceNotFollowed`    | A rule of the asset refuses this amount for this pair of holders: the modules whose `pass` is false. | Propose the smallest `allowedAmount`, and check it again. If it is 0, this amount is refused: another amount can pass only if the rule is not monotonic.                 |

## A sequence for a transfer form

```mermaid
flowchart TB
    A{"Amount is zero?"} -- "yes" --> Z["Refuse in the form"]
    A -- "no" --> B["getTransferStatus(token, from, to, amount)"]
    B --> C{"All three true?"}
    C -- "yes" --> S["Let the holder sign"]
    C -- "no" --> D{"Which is false?"}
    D -- "_freezeStatus" --> F["Token.paused()<br/>getFreezeStatus, isFrozen"]
    D -- "_eligibilityStatus" --> E["TREXRegistry.identity(to)<br/>getVerifiedDetails(token, to)"]
    D -- "_complianceStatus" --> R["getTransferDetails(token, from, to, amount)"]
```

One call first, each time the recipient or the amount changes. A detail call only for a boolean that is false. For `transferFrom`, add `Token.allowance` and `canSpenderCall` to the first step.

## Calling it

Every function is a `view`. Call it with `eth_call`: no transaction, no signature, no gas paid by the holder. The functions loop over topics, issuers and modules: they are written for off-chain calls, not for a call inside a transaction.

```bash
cast call $UTILITY_CHECKER \
  "getTransferStatus(address,address,address,uint256)(bool,bool,bool)" \
  $TOKEN $FROM $TO 100 --rpc-url $RPC_URL
```

The address to call is the proxy, published with the deployments of the network. The ABI is `IUtilityChecker`, in `contracts/utils/` of the T-REX repository. Batching these reads in a multicall is the caller's choice.

## The same reads without `UtilityChecker`

| Question                       | Direct view                                       |
| ------------------------------ | ------------------------------------------------- |
| Is the asset paused?           | `Token.paused()`                                  |
| Is an address frozen?          | `Token.isFrozen(from)`, `Token.isFrozen(to)`      |
| How much can the sender send?  | `Token.freeBalanceOf(from)`                       |
| Is the recipient eligible?     | `TREXRegistry.isVerified(to)`                     |
| Do the rules allow the amount? | `ModularCompliance.canTransfer(from, to, amount)` |

The registry is `Token.identityRegistry()`, the compliance is `Token.compliance()`. These reads give the decision. `UtilityChecker` adds the reason, per topic and per module.

## Checklist

* The form calls `getTransferStatus` before the holder signs, and refuses an amount of zero itself.
* `getVerifiedDetails` is not called for an address that resolves to no identity. An empty entry is matched to its topic by index, and `_walletStatus` is read before the entries.
* A failed rule comes with a proposed amount, checked again.
* For `transferFrom`, the allowance and `canSpenderCall` are read as well.
* `TREXRegistry.identity(from)` is not zero, or `ModularCompliance.canTransfer` is read as well.
* The address used is the proxy, and a revert after a passed check is still handled.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.t-rex.network/build/pre-flight-checks-with-utilitychecker.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
