For the complete documentation index, see llms.txt. This page is also available as Markdown.

Integrate a venue or an application

A venue or an application moves tokens that belong to others: it settles a trade, holds a deposit, lends against a position, or distributes to a list of holders. A T-REX token is an ERC-20, and approve, permit and transferFrom work as a contract expects. Four things differ. This page covers them for teams that build a trading venue, a settlement or escrow contract, a lending or distribution application.

In a plain ERC-20
In a T-REX token

An allowance is all a spender needs.

The asset can restrict which spenders it accepts. The spender is checked before the allowance is spent.

Any address can receive tokens, a contract included.

The recipient must be eligible. A contract that holds tokens is a holder: it has an identity with the claims the asset requires, and the rules count it.

A transfer within the balance and the allowance succeeds.

Each transfer also passes the pause, the freezes, the eligibility of the recipient and the rules of the asset, when it executes. A transfer of zero reverts.

Only a contract's own code moves the tokens it holds.

The Issuer's agents can freeze, force a transfer out of, recover or burn what a contract holds. The contract's own ONCHAINID can move it too, with identityTransfer.

Applications built on the protocol are listed in the T-REX AppStore.

Read the register

Token functions take addresses. Ownership, eligibility and the rules are evaluated on the identity an address resolves to. The registry of an asset is Token.identityRegistry(), its compliance is Token.compliance().

Question
Call

Which identity is behind this address, for this asset?

TREXRegistry.identity(address)

Can this counterparty receive the asset?

TREXRegistry.isVerified(address)

How much can this address deliver?

Token.freeBalanceOf(address): the balance less the frozen tokens

What does this identity own, over all its addresses and all chains?

ModularCompliance.positionOf(identity)

Key counterparties by identity: two addresses that resolve to one ONCHAINID are one holder, and a transfer between them changes no position. Size orders on the free balance, not on balanceOf. The parts of a balance, the wallets of an identity and their status are in Integrate a wallet or a custodian.

Execute for a holder

The holder gives the contract an allowance, with Token.approve(spender, amount) or with a signature the contract submits to Token.permit(owner, spender, value, deadline, v, r, s). Giving the allowance is plain ERC-20: the spender policy is not asked. The contract then calls Token.transferFrom(from, to, amount).

The token checks, in this order:

  1. The spender. ModularCompliance.canSpenderCall(spender, from, to, amount) returns true for the caller. Otherwise SpenderNotAllowed, and the allowance is untouched.

  2. The allowance. It covers the amount, and is spent. Otherwise ERC20InsufficientAllowance.

  3. The pause. The token is not paused. Otherwise EnforcedPause.

  4. The freezes. from is not frozen, then to is not frozen. Otherwise FrozenWallet, which names the address.

  5. The free balance. The amount is within the balance of from less its frozen tokens. Otherwise ERC20InsufficientBalance.

  6. Eligibility of the recipient. TREXRegistry.isVerified(to) returns true. Otherwise UnverifiedIdentity.

  7. The rules. ModularCompliance.canTransfer(from, to, amount) returns true. Otherwise ComplianceNotFollowed.

  8. The amount. The balances move, and the token tells the compliance, which does not record a movement of zero. Otherwise ZeroValue.

The compliance then moves the amount from the position of the sender's identity to the position of the recipient's identity. The token does not check the eligibility of the sender: a holder whose claim expired can still send. The flow is in 10 Transfer between investors.

Meet the spender policy of the asset

canSpenderCall is the whole policy. With no module of type SPENDER bound to the compliance, it returns true for every spender, as with any ERC-20. Otherwise the compliance encodes the spender as an ERC-7930 interoperable address of the reference chain and calls moduleCheckSpender on every SPENDER module, with the sender, the recipient and the amount. Every module must accept, and the answer holds for one movement. Two modules ship, and an asset binds one, both or none.

Module bound
The spender is accepted when
What the venue does

SpenderVerificationModule

TREXRegistry.isWalletVerified(spender) is true: the spender's address resolves to an identity, its link is not revoked, and the identity holds a valid claim from a trusted Claim Issuer for every topic required of its type. When the Issuer has disabled the eligibility checks of the registry, the read is true for every spender

Holds an ONCHAINID with the claims the asset requires

SpenderWhitelistModule

isSpenderAllowed(compliance, spender) is true: the spender's interoperable address is on the list of named operators the Compliance Owner keeps for this asset. The list starts empty. The spender's identity is not read

Asks the Issuer to list the contract: 26 Authorize spenders

Both reads take the spender as an ERC-7930 interoperable address in canonical form (bytes), where canSpenderCall takes the address.

An application finds the policy of an asset with two reads:

An Issuer can also bind a SPENDER module of its own, so canSpenderCall is the answer to rely on. A refused transferFrom reverts with SpenderNotAllowed(spender, from, to, value). See Identity-based control and spender compliance.

Give the contract an identity

A venue contract takes part under its own ONCHAINID, of type SMART_CONTRACT. The identity is bound to the contract's address and to no other, and it carries the claims the asset requires, like the identity of any participant. This type is created under a role granted by Network governance, never self-deployed: see Identity types and factory gating.

An Issuer can require its own set of claim topics from SMART_CONTRACT identities: TREXRegistry.getClaimTopicsForIdentityType(6) returns it, and when it is empty the default topics of getClaimTopics() apply.

What the asset checks depends on what the contract does with the tokens.

The contract
It is
The asset checks

Calls transferFrom from one holder to another and never receives tokens

A spender

The spender policy

Receives tokens: an escrow, a pool, a vault

A holder

Eligibility: isVerified on the contract's address, on every transfer to it. The rules, which count it as a holder. And the spender policy, when it pulls the tokens in with transferFrom

What follows from being a holder:

  • The contract's identity has a position. On the register, tokens in escrow belong to the identity of the escrow, not to the depositor.

  • Rules apply to that position. MaxBalancePerIdentityModule caps what one identity owns, with no exemption: all the deposits a contract holds are one position under the cap.

  • Agent actions land on its address. A freeze on it immobilizes every deposit.

  • The keys of the contract's identity command its tokens. The ONCHAINID the contract is bound to can call Token.identityTransfer(from, to, amount) with the contract's address as from. No allowance is read and the contract's code does not run. Whoever holds a MANAGEMENT or an ACTION key on that identity can move every deposit, within the checks of a transfer, so those keys are part of the security model of the escrow.

Choose a settlement pattern

Atomic delivery versus payment is a property of the application's own contract: both legs execute in one transaction, and a revert of the token leg reverts the other. The token guarantees that a transfer passes every check and is recorded, or reverts and changes nothing.

Pull from the seller to the buyer

One transferFrom(seller, buyer, amount). The contract never holds the tokens, and the position moves from the seller's identity to the buyer's.

  • Checks: the eight checks above, once, at settlement. The contract is a spender only.

  • Fails when: any of the eight fails. Nothing reserves the seller's tokens between the order and the settlement: the allowance can be lowered and the free balance can drop.

Escrow

Two transfers, with the contract holding the tokens in between.

Moment
Call
Checks

Deposit

transferFrom(seller, escrow, amount) by the contract. Or transfer(escrow, amount) by the seller, with no spender check

The escrow is the recipient: eligible, not frozen, and the rules count the deposit in its position

Release

transfer(buyer, amount) by the contract

The escrow is the sender: no spender check, not frozen, free balance sufficient. The buyer is the recipient: eligible, and within the rules

Return

transfer(seller, amount) by the contract

The same, with the seller as recipient

  • Fails when: at deposit, the escrow lacks a claim or its position reaches a cap. At release, the buyer is no longer eligible, a rule refuses the buyer, the escrow is frozen, or the asset is paused. At return, the seller lost eligibility since the deposit.

Batch

Token.batchTransfer(toList, amounts) sends from the caller's own balance to several recipients, for a contract that holds the tokens it distributes. transferFrom has no batch form.

  • Checks: every entry is a transfer and passes every check.

  • Fails when: the two lists differ in length (ArrayLengthMismatch), or one entry fails, which reverts the whole batch. Two entries to one identity can each pass canTransfer before the batch, and the second can still exceed a cap.

Check before submitting

Before a transferFrom, read what the token checks: canSpenderCall(spender, from, to, amount), allowance(from, spender), paused(), isFrozen on both addresses, freeBalanceOf(from), isVerified(to) and canTransfer(from, to, amount).

UtilityChecker.getTransferStatus(token, from, to, amount) answers all but the first two in one call. It does not know the spender or the allowance, and it is written for off-chain calls: see Pre-flight checks with UtilityChecker. A contract that checks on-chain reads canSpenderCall, isVerified and canTransfer directly.

A check is a read at one moment. The token evaluates everything again when the transfer executes. Map each error to an action:

Error
The application

SpenderNotAllowed

Stops settling the asset until its policy accepts the contract

ERC20InsufficientAllowance

Asks the holder for a new approval

EnforcedPause

Suspends settlement for the asset until Unpaused

FrozenWallet

Cancels the orders of the address the error names

UnverifiedIdentity

Cancels for this recipient until it is eligible again

ERC20InsufficientBalance, ComplianceNotFollowed

Reduces the amount, or cancels. UtilityChecker.getTransferDetails gives the largest amount each rule allows

ZeroValue

Never submits an amount of zero

What can change under an application

What changes
Effect on the application
Follow

The asset is paused

No order settles and no escrow releases

Paused(account), Unpaused(account)

An address or an amount is frozen

A frozen address neither sends nor receives. A frozen amount lowers the free balance

AddressFrozen(address, isFrozen, agent), TokensFrozen(address, amount), TokensUnfrozen(address, amount)

An agent forces a transfer out of the contract's address, recovers it to another address, or burns from it

The contract's balance decreases without its code running, and no longer matches its own accounting

A Transfer from its address that it did not make, followed by ForcedTransfer(agent) for a forced transfer, or by RecoverySuccess(lostWallet, newWallet, identity) for a recovery

The contract's ONCHAINID moves tokens out of its address

The same: the balance decreases without the contract's code running

A Transfer from its address, followed by IdentityTransfer(identity, from, to, amount)

A holder loses eligibility: a claim expires or is revoked. Or one of its addresses does: its link is revoked

The holder, or that address, can still send: sell, deposit, repay. It can no longer receive: a purchase, a release or a return to it reverts

TREXRegistry.isVerified(address), read at settlement

The contract's own claims lapse

Under SpenderVerificationModule, its transferFrom reverts. As a holder, it takes no deposit and can still release

isVerified on its own address

The Issuer changes the spender policy or a rule

transferFrom reverts with SpenderNotAllowed, and allowances are of no use. Or an amount that passed is refused

ModuleAdded(module), ModuleRemoved(module), ModuleForceRemoved(module), ModuleTypesRecorded(module, moduleTypes), ModuleInteraction(target, data) on the compliance. SpenderAllowed, SpenderDisallowed on the list module

An escrow reconciles what it owes with freeBalanceOf on its own address, and defines what happens when it holds less. A release or a return can be refused for a reason outside the contract: one blocked deposit must not block the others.

On a satellite chain

On a satellite, a transfer executes only against a validation issued on the reference chain. A validation can name a spender: the party allowed to execute the transfer on the satellite for the sender.

  • The request. ModularCompliance.requestTransferValidation(from, to, requestedMin, requestedMax, spender) is called by the identity of the sending wallet, or by a Token Agent. A venue asks to be named.

  • The spender. spender is the ERC-7930 interoperable address of the venue on the satellite, in canonical form. It is empty when the sender executes itself.

  • The check. No module runs on a satellite, so the same SPENDER modules judge the spender at issuance. A refusal reverts with ValidationSpenderRefused, and no validation is issued. Under the list policy, the Issuer lists the address the venue has on that chain.

The flow is in 31 Transfer on a satellite.

Checklist

  • canSpenderCall is read per transfer, and the policy of each asset is known from getModulesByType.

  • The contract has an ONCHAINID of type SMART_CONTRACT, with the claims of each asset it serves.

  • Every transferFrom is checked first, each error is mapped to an action, and zero is never submitted.

  • A contract that holds tokens is one holder under the rules, caps included, and a refused release or return has a way out.

  • Pause, freezes, forced transfers, recoveries, burns and changes of modules are indexed, and the keys of the contract's identity are held as tightly as the contract's own.

  • On a satellite, the venue is named as spender, with the address it has on that chain.