Integrate a wallet or a custodian
A T-REX token is an ERC-20. balanceOf, transfer, approve and transferFrom behave as a wallet expects, and a wallet that supports ERC-20 displays and sends the token without any change. Four things differ from a plain ERC-20, and a wallet or a custody platform that ignores them shows numbers that are not true or lets a holder sign a transaction that reverts. This page covers those four things and what to build for each.
The holder is an address.
The holder is an identity, an ONCHAINID. An address resolves to it. Several addresses can belong to one holder, and the identity can hold tokens itself.
A balance is one number.
A balance has parts: free, frozen, and, for an asset distributed on satellites, bridged and reserved.
A transfer succeeds if the balance is sufficient.
A transfer also requires an eligible recipient and a movement the rules of the asset allow.
Only the holder moves its tokens.
The Issuer's agents can freeze, force a transfer, recover and burn, under roles.
Show who holds
Token functions take addresses. Ownership and eligibility are evaluated on the identity an address resolves to.
Which identity does this address resolve to, for this asset?
TREXRegistry.identity(address)
Is this address eligible to receive this asset?
TREXRegistry.isVerified(address)
Which identity is this address linked to, for every asset?
IdentityFactory.getIdentity(account), with account the address as an ERC-7930 interoperable address
Is this wallet active, revoked, or never linked?
IdentityFactory.getAccountStatus(account)
Which wallets are actively linked to this identity?
IdentityFactory.getAccountsCount(identity) and getAccounts(identity, start, end). Revoked wallets, the identity's own address and addresses registered for one asset only are not listed.
What does this identity own in total, for this asset?
ModularCompliance.positionOf(identity)
The registry of an asset is Token.identityRegistry(), and its compliance is Token.compliance().
What to build:
Group addresses by identity. A holder with three linked wallets has one position. Show the total and the detail per address.
Treat an ONCHAINID as a holder address. It is a smart account and can hold tokens at its own address, with no wallet involved.
Show the state of each wallet. A revoked wallet still holds what it held and can no longer receive. Follow
AccountLinkedandAccountRevokedon theIdentityFactoryto keep the list of a holder's wallets, revoked ones included.
Show balances that are true
Balance
Token.balanceOf(address)
The tokens at this address on the reference chain, frozen tokens included.
Frozen
Token.getFrozenTokens(address)
The part an agent has frozen. It cannot be sent.
Free
Token.freeBalanceOf(address)
Balance minus frozen: what the address can send.
Address frozen
Token.isFrozen(address)
True when the whole address is frozen: it neither sends nor receives in a transfer between holders. An agent can still mint to it, burn from it, or force a transfer to or from it.
Asset paused
Token.paused()
True when transfers between holders are halted.
Bridged
Token.bridgedBalanceOf(wallet)
The position that is active on a satellite for this wallet. wallet is an interoperable address. These tokens are not in balanceOf on the reference chain.
Reserved
Token.reservedOf(wallet)
The part of a bridged position promised to transfers that are authorized and not settled.
Available
Token.availableOf(wallet)
Bridged minus reserved: what a new transfer on the satellite can draw on.
What to build:
Display the free balance as the spendable amount, and the frozen amount next to it. A wallet that shows
balanceOfalone lets a holder try to send tokens it cannot send.For an asset open on satellites, show where the position is. The balance on the reference chain and the bridged position on each satellite add up to what the holder owns.
ModularCompliance.positionOf(identity)gives the total.Do not read
totalSupplyas the sum of balances on the reference chain. It includes the positions that are active on satellites.
The parts of a balance are described in The balance model.
Check a transfer before sending it
A transfer the token refuses reverts. Nothing moves and the holder pays for the failed transaction. Check first, and tell the holder why.
One call
UtilityChecker is a read-only contract that answers for any token. Its answers, their edge cases and what they leave out are in Pre-flight checks with UtilityChecker.
getTransferStatus(token, from, to, amount)
Three booleans: the transfer passes the pause and freeze conditions, the recipient is eligible, the rules of the asset allow it.
getFreezeStatus(token, from, to, amount)
Whether a freeze blocks the transfer, and the balance the sender can use.
getVerifiedDetails(token, address)
One entry per required topic: whether a valid claim from a trusted issuer exists, and from which issuer. Also the status of the wallet.
getTransferDetails(token, from, to, amount)
For each rule of the asset, the largest amount it allows and whether the requested amount fits.
Or the checks themselves
Token.paused(), Token.isFrozen(from) and Token.isFrozen(to), Token.freeBalanceOf(from), TREXRegistry.isVerified(to), ModularCompliance.canTransfer(from, to, amount), and for a spender ModularCompliance.canSpenderCall(spender, from, to, amount).
What to tell the holder
EnforcedPause
The asset is halted by its issuer. Try again later.
FrozenWallet
This address, or the recipient's, is frozen by the issuer's agents.
ERC20InsufficientBalance
The amount exceeds the free balance. Part of the balance is frozen, or active on a satellite.
UnverifiedIdentity
The recipient is not eligible for this asset: it has no identity, its wallet link was revoked, or a required credential is missing, expired or revoked.
ComplianceNotFollowed
A rule of the asset refuses this amount for this pair of holders. A smaller amount may pass.
SpenderNotAllowed
The asset does not accept this spender.
ERC20InsufficientAllowance
The spender is not approved for this amount.
ZeroValue
The amount is zero. The token refuses it.
A check is a read at one moment. Eligibility and rules are evaluated again when the transfer executes, so a transfer that passed the check can still revert if something changed in between.
Send
From the wallet that holds the tokens
Token.transfer(to, amount)
The usual case.
Through a spender
Token.approve(spender, amount), then the spender calls Token.transferFrom(from, to, amount)
A venue, a settlement contract, a custodian. The asset can restrict which spenders it accepts: see 26.
With a signed approval
Token.permit(owner, spender, value, deadline, v, r, s)
The holder approves with a signature and no transaction of its own (ERC-2612).
Through the identity
Token.identityTransfer(from, to, amount), called by the ONCHAINID
The identity moves the free tokens of a wallet linked to it, with no signature from that wallet: see 24.
From the identity itself
The ONCHAINID calls Token.transfer(to, amount)
For tokens held at the identity's own address. The call is a user operation, or an execution ordered by a key of the identity.
Every way passes the same gates. The steps and the checks are in 10 Transfer between investors.
Hold for clients
A custody platform chooses how its clients appear on the register. The choice decides who the protocol treats as the holder.
Segregated
Each client has its own ONCHAINID. Each custody address is linked to the client's identity, or registered to it by the Issuer's agent when the address cannot link itself.
The client
Omnibus
One address holds for many clients. It is recorded under the identity of the custodian that operates it.
The custodian. The clients behind the position are in the custodian's own books.
What follows from the choice:
Eligibility is checked on the recorded holder. In the segregated model each client needs the claims the asset requires. In the omnibus model the custodian's identity does.
Rules count the recorded holder. A cap per investor, or a limit on the number of holders, sees an omnibus position as one holder.
Agent actions land on the recorded address. A freeze on an omnibus address immobilizes every client behind it.
A custodian acts for a client in the segregated model in one of two ways:
A custody wallet attached to the client's identity. The custodian holds the keys of the wallet and sends from it. A wallet that can sign the linking call is linked to the identity in the global identity registry, and every asset recognizes it: 07. A wallet that cannot is registered for one asset by that asset's Issuer, in the asset's own registry: 06.
What can happen to a position without the holder
The Issuer's agents act on positions under roles, and none of it needs the holder's signature. A wallet that does not follow these events shows a holder a state it cannot explain.
What happened
Event on the Token
What to show
The address was frozen or unfrozen
AddressFrozen(address, isFrozen, agent)
The holder cannot send from it or receive on it, or can again
An amount was frozen or unfrozen
TokensFrozen(address, amount), TokensUnfrozen(address, amount)
The new free balance
An agent moved tokens
Transfer(from, to, amount)
A movement the holder did not sign. A forced transfer can release frozen tokens first
The position was recovered from a lost wallet
RecoverySuccess(lostWallet, newWallet, identity)
The balance is at the new address
The identity moved tokens of a linked wallet
IdentityTransfer(identity, from, to, amount)
A movement ordered by the holder's identity
Tokens were burned
Transfer(address, 0x0, amount)
A redemption or a cancellation. A position that leaves for a satellite emits the same Transfer, together with DelegatedOut
The asset was paused or resumed
Paused(account), Unpaused(account)
Transfers are halted, or possible again
The position went to a satellite, or came back
DelegatedOut(...), Recalled(...)
Where the position is now
Two changes come from outside the token and affect what a holder can do:
A claim is revoked or expires. The holder keeps its position, can still send to an eligible recipient and can no longer receive. Read
TREXRegistry.isVerified(address).A wallet is revoked by its owner.
AccountRevoked(account, identity, caller)on theIdentityFactory. The wallet can no longer receive any asset.
On a satellite chain
On a satellite, the asset is represented by T-REX Lite, a plain ERC-20. A wallet displays and sends it like any ERC-20, with one difference: a transfer executes only against a validation that the reference chain issued for it. A holder first requests the validation, through its identity on the reference chain, then makes the transfer on the satellite. Without a validation, the transfer reverts.
What to build is in the cross-chain guides: taking a position to a satellite (30), transferring there (31), and bringing it back (32).
Checklist
The spendable amount shown is
freeBalanceOf, notbalanceOf.Addresses of one identity are grouped, and the ONCHAINID itself is treated as a holder address.
Every transfer is checked before signing, and a refusal is explained in the holder's terms.
A transfer of zero is not offered.
Freezes, forced transfers, recoveries, burns and the pause are indexed and shown.
For a custody platform: the model, segregated or omnibus, is chosen with each Issuer, since it decides who the register records as the holder.
For an asset open on satellites: the bridged and reserved parts are shown, per chain.