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.
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().
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:
The spender.
ModularCompliance.canSpenderCall(spender, from, to, amount)returns true for the caller. OtherwiseSpenderNotAllowed, and the allowance is untouched.The allowance. It covers the amount, and is spent. Otherwise
ERC20InsufficientAllowance.The pause. The token is not paused. Otherwise
EnforcedPause.The freezes.
fromis not frozen, thentois not frozen. OtherwiseFrozenWallet, which names the address.The free balance. The amount is within the balance of
fromless its frozen tokens. OtherwiseERC20InsufficientBalance.Eligibility of the recipient.
TREXRegistry.isVerified(to)returns true. OtherwiseUnverifiedIdentity.The rules.
ModularCompliance.canTransfer(from, to, amount)returns true. OtherwiseComplianceNotFollowed.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.
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.
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.
MaxBalancePerIdentityModulecaps 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 asfrom. No allowance is read and the contract's code does not run. Whoever holds aMANAGEMENTor anACTIONkey 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.
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
transferand 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 passcanTransferbefore 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:
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
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.
spenderis 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
SPENDERmodules judge the spender at issuance. A refusal reverts withValidationSpenderRefused, 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
canSpenderCallis read per transfer, and the policy of each asset is known fromgetModulesByType.The contract has an ONCHAINID of type
SMART_CONTRACT, with the claims of each asset it serves.Every
transferFromis 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.