Indexing: events and supply accounting
This page is for teams that build an indexer, a data product, a cap table or a reporting system on T-REX assets. It lists the events of each contract with their exact signatures, shows how to rebuild balances, supply, holders, positions per identity and cross-chain validations from them, and says where a view has to be read instead.
Before the tables
One suite, five emitters.
Token,TREXRegistry,IdentityRegistryStorageandModularComplianceare four proxies, named byTREXSuiteDeployed. The fifth is the suite's access manager,Token.authority(). One movement often logs on several of them.Identity events come from shared contracts.
IdentityFactory,ERC734ValidatorandKeyApprovalModuleeach serve every identity. An identity emits none of them itself: filter on the indexed identity argument.A satellite wallet is a
bytesvalue and abytes32key. The value is its ERC-7930 interoperable address, in canonical encoding. The key (walletKey,fromKey,toKey) is itskeccak256. AchainKeyis thekeccak256of the chain's ERC-7930 prefix with an empty address, andChainRegisteredmaps it back.
Events by contract
Token
Transfer(address indexed from, address indexed to, uint256 value)
A balance of the reference chain changes
Approval(address indexed owner, address indexed spender, uint256 value)
approve, permit
Paused(address account), Unpaused (same argument)
Halt, resume. A token is deployed paused
UpdatedTokenInformation(string _newName, string _newSymbol, uint8 _newDecimals, string _newVersion, address indexed _newOnchainID)
Deployment, change of name, symbol or ONCHAINID
IdentityRegistryAdded(address indexed _identityRegistry), ComplianceAdded(address indexed _compliance)
The setters only, not deployment
AddressFrozen(address indexed _userAddress, bool indexed _isFrozen, address indexed _owner)
An address is frozen or unfrozen
TokensFrozen(address indexed _userAddress, uint256 _amount), TokensUnfrozen (same arguments)
An amount is frozen, released
RecoverySuccess(address indexed _lostWallet, address indexed _newWallet, address indexed _investorOnchainID)
End of a recovery
ForcedTransfer(address indexed agent)
After the Transfer of a forced transfer
IdentityTransfer(address indexed identity, address indexed from, address indexed to, uint256 amount)
After the Transfer of an identityTransfer
ReservedForValidation(bytes32 indexed walletKey, bytes wallet, uint256 amount), ReleasedFromValidation (same arguments)
A validation reserves part of a satellite wallet, gives it back
HeldInTransit(bytes32 indexed fromKey, uint256 indexed validationId, bytes fromWallet, uint256 amount)
A burn is reported before its mint
ReturnedInTransit(bytes32 indexed walletKey, uint256 indexed validationId, bytes wallet, uint256 amount)
A held amount returns to its wallet
BridgedTransfer(bytes32 indexed fromKey, bytes32 indexed toKey, uint256 indexed validationId, bytes from, bytes to, uint256 amount)
Settlement between two satellite wallets
SettledToNative(bytes32 indexed fromKey, address indexed to, uint256 indexed validationId, bytes fromWallet, uint256 amount)
Settlement from a satellite wallet to an address of the reference chain
ChainRegistered(bytes32 indexed chainKey, bytes2 chainType, bytes chainReference)
First sight of a chain
RouteSet(bytes32 indexed chainKey, address indexed gateway), PeerSet(bytes32 indexed chainKey, bytes peer)
The gateway, the T-REX Lite address, for a chain
TrustedGatewayRegistrySet(address trustedGatewayRegistry)
Once, at deployment
ValidationRoutePinned(uint256 indexed validationId, bytes32 indexed chainKey, address gateway)
First dispatch of a validation to a chain
ProtocolMessageSent(MessageTypesLib.Message indexed messageType, bytes32 indexed chainKey, bytes32 sendId)
A message leaves for a satellite
ProtocolMessageReceived(MessageTypesLib.Message indexed messageType, bytes32 indexed chainKey, bytes32 receiveId)
A message from a satellite was processed
BurnProofReceived(bytes32 indexed originChainKey, bytes burnedWallet, address indexed nativeWallet, uint256 amount)
A burn proof arrives
Defined in EventsLib, emitted by the delegation and recall paths:
DelegatedOut(address indexed holder, bytes32 indexed toKey, bytes toWallet, uint256 amount)
After the Transfer(holder, 0x0) of a delegation-out
Recalled(bytes32 indexed fromKey, address indexed holder, bytes fromWallet, uint256 amount)
After the Transfer(0x0, holder) of a recall
MessageTypesLib.Message is a uint8 in the ABI: 0 COMPLIANCE_VALIDATION, 1 MINT_INSTRUCTION, 2 RECALL_INSTRUCTION, 3 SETTLEMENT_NOTIFICATION, 4 BURN_PROOF.
TREXRegistry
IdentityRegistered(address indexed _investorAddress, IIdentity indexed _identity), IdentityRemoved (same arguments)
An address is registered for this asset, its registration deleted
IdentityUpdated(IIdentity indexed _oldIdentity, IIdentity indexed _newIdentity)
A registration names another identity. The address is in InvestorIdentityChanged
ClaimTopicAdded(uint256 indexed _claimTopic), ClaimTopicRemoved (same argument)
A required topic is added, removed
ClaimTopicAddedForIdentityType(uint256 indexed identityType, uint256 indexed claimTopic), ClaimTopicRemovedForIdentityType (same arguments)
The same, for one type of identity
TrustedIssuerAdded(address indexed _trustedIssuer, uint256[] _claimTopics), ClaimTopicsUpdated (same arguments)
A Claim Issuer is trusted for topics, its topics replaced
TrustedIssuerRemoved(address indexed _trustedIssuer)
It is no longer trusted
EligibilityChecksDisabled(), EligibilityChecksEnabled()
Checks switched off, on. The second also at deployment
IdentityStorageSet(address indexed _identityStorage), ClaimTopicsRegistrySet(address indexed _claimTopicsRegistry), TrustedIssuersRegistrySet(address indexed _trustedIssuersRegistry)
Deployment, and a change of storage for the first. The last two carry the registry's own address
IdentityRegistryStorage
A storage can serve several registries. Its events then concern every asset wired to it.
IdentityStored(address indexed _investorAddress, IIdentity indexed _identity), IdentityUnstored (same arguments), IdentityModified(IIdentity indexed _oldIdentity, IIdentity indexed _newIdentity)
The storage side of the registration events, logged just before them
InvestorIdentityChanged(address indexed investor)
With IdentityModified: the address
IdentityOverridden(address indexed investor, IIdentity indexed globalIdentity, IIdentity indexed localIdentity)
A registration names another identity than the address's global link
IdentityOverrideReleased(address indexed investor, IIdentity indexed localIdentity, IIdentity indexed globalIdentity)
That divergence ends
IdentityRegistryBound(address indexed _identityRegistry), IdentityRegistryUnbound (same argument)
A registry is bound, unbound
CountryUpdated and CountryModified are declared by the ERC-3643 interfaces and never emitted: T-REX stores no country.
ModularCompliance
TokenBound(address _token), TokenUnbound (same argument)
Bound to its token, at deployment. Unbound
ModuleAdded(address indexed module), ModuleRemoved, ModuleForceRemoved (same argument)
A module is bound, removed, removed without being called
ModuleTypesRecorded(address indexed module, IModule.ModuleType[] moduleTypes)
A module is filed under its types
ModuleInteraction(address indexed target, bytes data)
A module setting is changed through the compliance
WalletOwnerChanged(address indexed wallet, address indexed previousIdentity, address indexed identity, uint256 amount)
The ledger moves an address's balance to the identity the registry now names
PositionUnresolved(bytes32 indexed wallet, uint256 amount)
One side of a movement resolves to no identity. On the receiving side amount is added to the gap, on the sending side it is subtracted. wallet is the address padded on the left, or the key of a satellite wallet
PositionUnderflow(address indexed identity, uint256 missing)
A debit exceeds the position: the position goes to zero and missing is subtracted from the gap
PositionFixed(address indexed from, address indexed to, uint256 amount)
The OWNER moves position. The zero address is the gap
TransferValidationIssued(uint256 indexed validationId, bytes from, bytes to, bytes spender, uint256 amountMin, uint256 amountMax, uint64 expiry, uint64 reconciliationWindow)
A validation is issued
SettlementNotified(bytes32 indexed originChainKey, uint256 indexed validationId, bytes from, bytes to, uint256 amount)
A report from a satellite reaches the compliance
ValidationLegConfirmed(uint256 indexed validationId, bytes32 indexed chainKey, uint256 amount), ValidationSettled (same arguments)
The first of two reports is recorded. Every report arrived
LateReconciliation(uint256 indexed validationId, bytes32 indexed chainKey), ReplayedSettlement (same arguments)
A report after the discard. A report for an unknown validation, for a leg already applied, or for a validation already Resolved
ValidationDiscarded(uint256 indexed validationId)
The Validation Keeper discards an expired validation
ValidationResolved(uint256 indexed validationId, uint256 returnedAmount)
The Validation Keeper gives up on a half-reported transfer
ValidationIssuancePaused(bytes32 indexed chainKey), ValidationIssuanceUnpaused (same argument)
Issuance for a chain stops, resumes
DefaultValidityWindowSet(uint64 duration), ReconciliationWindowSet(bytes32 indexed chainKey, uint64 duration)
A window is set
A module logs at its own address and serves several compliances: ComplianceBound(address indexed compliance), ComplianceUnbound (same argument), and its own settings.
Factory, versions, roles, gateways
TREXFactory
TREXSuiteDeployed(address indexed token, address registry, address irs, address mc, string salt)
A suite is deployed
IsolatedSuiteDeployed(address indexed token, ITREXImplementationAuthority.SuiteBeacons beacons)
After it, for a suite with its own beacons
Deployed(address indexed addr)
Each proxy the factory deploys: access manager, storage, registry, compliance, token
ImplementationAuthoritySet(address implementationAuthority), IdFactorySet(address idFactory), TrustedGatewayRegistrySet(address trustedGatewayRegistry)
A pointer of the factory is set
TREXImplementationAuthority
VersionPublished(Version version, ITREXImplementationAuthority.SuiteImplementations implementations), SuiteUpgraded (same arguments)
A version is archived. The shared beacons move to it
BeaconsDeployed(ITREXImplementationAuthority.SuiteBeacons beacons)
Once, at construction
TREXAccessManager
DomainCreated(uint32 indexed domainId, string name), DomainAssigned(uint32 indexed domainId, address indexed target)
A role domain is created, a contract assigned to it
TrustedGatewayRegistry
TrustedGatewaySet(address indexed gateway, bool trusted)
The network trusts a gateway, or stops
Version is a uint24. Both structs are five addresses: token, registry, storage, compliance, access manager.
IdentityFactory
IdentityDeployed(address indexed identity, bytes account, uint256 indexed identityType, address indexed deployer)
An identity is created
IdentityTypeRecorded(address indexed identity, uint256 identityType)
Same transaction: its type, recorded once
AccountLinked(bytes account, address indexed identity, address caller), AccountRevoked (same arguments)
A wallet is linked. A wallet is revoked, for good
PendingCrossChainLinkProposed(bytes account, address indexed identity, uint256 expiry, address gateway, bytes32 receiveId)
A wallet of another chain asks for a link. Nothing is linked
CrossChainLinkConfirmed(bytes account, address indexed identity)
The identity accepts. Follows the AccountLinked of that wallet
CrossChainLinkRejected(bytes account, address indexed identity, uint256 expiry)
The identity declines
Settings events, declared in IIdentityFactory: IdentityTypePolicySet, IdentityTypePolicyRemoved, IdentityTypeModulesSet, TrustedGatewaySet (another signature than the registry's), TrustedVerifierSet, BeaconInitialized, BeaconUpgraded.
ERC734Validator
It holds the keys and claims of every identity. Identity.registryModule() returns its address.
KeyAdded(address indexed account, bytes32 indexed keyHash, uint256 indexed purpose, uint256 keyType, bytes signerData, bytes clientData, address caller)
A key receives a purpose on the identity account
KeyRemoved(address indexed account, bytes32 indexed keyHash, uint256 indexed purpose, address caller)
A purpose is removed from a key
KeyUsed(address indexed account, bytes32 indexed keyHash, bytes32 userOpHash)
A user operation of account validates against the key
ClaimAdded(address indexed identity, bytes32 claimId, uint256 indexed topic, uint256 scheme, address indexed issuer, bytes signature, Structs.ClaimData data, string uri, address caller), ClaimChanged, ClaimRemoved (same arguments)
A claim is added to identity, replaced, removed
ClaimRevoked(bytes32 indexed digest, address indexed issuer, address caller)
The Claim Issuer issuer revokes a claim by its digest
ClaimAddedTo(address indexed identity, uint256 topic, bytes signature, Structs.ClaimData data, address caller)
A Claim Issuer delivered the claim itself. Follows ClaimAdded or ClaimChanged
Structs.ClaimData is (uint256 issuedAt, uint256 validUntil, bytes32 metadataHash, bytes payload). claimId is keccak256(abi.encode(issuer, topic)).
KeyApprovalModule
ExecutionRequested(address indexed account, uint256 indexed executionId, address indexed to, uint256 value, bytes data, address proposer)
A key queues a call of the identity account
Approved(address indexed account, uint256 indexed executionId, bool approved, address approver)
The call is approved or rejected
Executed(address indexed account, uint256 indexed executionId, address indexed to, uint256 value, bytes data, address executor), ExecutionFailed (same arguments)
The call ran, reverted
QueueInvalidated(address indexed account, uint256 firstValidId)
An uninstall voids the requests below firstValidId
Balances
balanceOf(address) is Transfer in minus Transfer out, with no special handling. What needs care is the meaning of a Transfer from or to the zero address.
Movement
Transfer
Same transaction, on the token
totalSupply
Mint
(0x0, to, amount)
Up
Burn
(from, 0x0, amount)
TokensUnfrozen before, if frozen tokens are taken
Down
Transfer
(from, to, amount)
IdentityTransfer after, when the identity ordered it
Same
Forced transfer
(from, to, amount)
TokensUnfrozen before if needed, ForcedTransfer after
Same
Recovery
(lostWallet, newWallet, balance)
See "Frozen amounts"
Same
Delegation-out
(holder, 0x0, amount)
DelegatedOut after
Same
Recall
(0x0, holder, amount)
Recalled after
Same
Settlement from a satellite to the reference chain
(0x0, to, amount)
SettledToNative after
Same
Transfer between satellite wallets
None
BridgedTransfer
Same
A Transfer from or to the zero address is a mint or a burn only when no DelegatedOut, Recalled or SettledToNative follows it. BurnProofReceived announces a proof. It is never a credit by itself: count a recall from the pair Transfer(0x0, holder) and Recalled only.
Supply
totalSupply() is the whole issuance: the ERC-20 supply of the reference chain plus totalBridged. Only a mint or a burn changes it.
totalBridged() is the sum of every bridged position, in-transit holds included. totalInTransit() is the part of it that no wallet holds, and inTransitOf(validationId) the amount held for one validation.
The comment on totalSupply states the trade-off: "a supply derived by summing Transfer events under-reports by totalBridged; DelegatedOut, Recalled and SettledToNative are the reconciliation for anyone deriving it that way, and totalSupply() - totalBridged() is the native float." And: "INV-7 sums the buckets to this figure and asserts that escrow is never there; INV-8 holds totalBridged to the positions, whichever transition moved it."
Supply of the reference chain
Transfer from 0x0 minus Transfer to 0x0
totalBridged
DelegatedOut minus Recalled minus SettledToNative
Bridged position of a wallet key
Up: DelegatedOut, BridgedTransfer (toKey), ReturnedInTransit. Down: Recalled, SettledToNative, HeldInTransit, and BridgedTransfer (fromKey) unless a hold exists for its validationId
inTransitOf
Set by HeldInTransit. Cleared by ReturnedInTransit or by the BridgedTransfer of that validation
reservedOf
ReservedForValidation minus ReleasedFromValidation
Identities to assert at any block, each tested as an invariant:
totalSupply()= sum ofbalanceOf+totalBridged()totalBridged()= sum ofbridgedBalanceOf+totalInTransit()getFrozenTokens(a)is at mostbalanceOf(a)balanceOfof the token's own address is zero: there is no escrow addresssum of
positionOf+positionGap()=totalSupply()
Frozen amounts
Address freeze. The last
AddressFrozengives the state._owneris the caller the token saw: the access manager when the agent acts through it, the token itself when a recovery carries the flag.Frozen amount.
TokensFrozenminusTokensUnfrozen. Free is balance minus frozen.Automatic unfreeze. A burn, a forced transfer and a recovery can take frozen tokens. The token releases what the free balance lacks and emits
TokensUnfrozenfor that shortfall before theTransfer. No agent calls an unfreeze.Recovery. In order:
TokensUnfrozen(lostWallet, frozen),Transfer,TokensFrozen(newWallet, frozen), then for a frozen lost walletAddressFrozen(lostWallet, false, token)and, unless it is frozen already,AddressFrozen(newWallet, true, token), thenRecoverySuccess. When they apply, the registry logs the registration of the new wallet first and the removal of the lost one just beforeRecoverySuccess.
Holders and positions per identity
An address resolves to an identity, for one asset, from two sources:
A registration of the asset, in its storage:
IdentityStored,IdentityModified,IdentityUnstored. It takes precedence.IdentityOverriddenmarks a registration that differs from the global link.The global record, on the
IdentityFactory:AccountLinked. An identity's own address resolves to itself fromIdentityDeployed, with noAccountLinked. AfterAccountRevokedthe wallet still resolves to its identity, so its tokens keep their owner, and it stops being eligible.
The compliance ledger keeps the position of each identity, and emits nothing when it changes in a normal movement. Its events record the exceptions: WalletOwnerChanged when the registration of an address that holds tokens changed (the position follows on the next movement through that address, so ownerOf(address) can lag the registry), PositionUnresolved and PositionUnderflow when an amount goes to the gap, PositionFixed when the OWNER repairs it.
There is no on-chain list of holders. Collect addresses from Transfer and satellite wallets from the bytes arguments of the bridged events, then group them by identity. A satellite wallet resolves through the global record only: AccountLinked, and it keeps its identity after AccountRevoked.
Identity of an address
TREXRegistry.identity(address)
Two sources and a precedence rule
Identity of a satellite wallet
TREXRegistry.resolveIdentity(bytes)
One source: the global record. A registration of the asset does not apply
Position of an identity
ModularCompliance.positionOf(identity)
No event per change
Pending amounts
pendingInOf(identity), pendingOutOf(identity)
No event names the identities. validationOf(validationId) does
Status of a validation
statusOf(validationId)
Expired is computed from the clock
Eligibility
TREXRegistry.isVerified(address)
See "Eligibility over time"
Eligibility of a satellite wallet
TREXRegistry.isWalletVerified(bytes)
The same
Cross-chain validations
The compliance emits the event that enters each status. The token logs the effect on the bridged ledger in the same transaction.
Pending
TransferValidationIssued
ReservedForValidation before it. After it, per satellite: ValidationRoutePinned, ProtocolMessageSent
AwaitingMint
ValidationLegConfirmed
ReleasedFromValidation, HeldInTransit
AwaitingBurn
ValidationLegConfirmed
Nothing
Settled
ValidationSettled
ReleasedFromValidation if the wallet was still reserved. Then BridgedTransfer, or Transfer and SettledToNative
Expired
No event: Pending past releaseAt, computed when read
Discarded
ValidationDiscarded
ReleasedFromValidation
DiscardedAwaitingMint
ValidationLegConfirmed, LateReconciliation
HeldInTransit
DiscardedAwaitingBurn
ValidationLegConfirmed, LateReconciliation
Nothing
LateReconciled
ValidationSettled, LateReconciliation
As Settled, with nothing to release
Resolved
ValidationResolved
ReturnedInTransit if the burn had arrived. Otherwise ReleasedFromValidation if the wallet was still reserved
Every report from a satellite logs
SettlementNotifiedfirst andProtocolMessageReceivedlast. A delivery that reverts logs nothing.ValidationIssuancePausedis logged with a late report whose amount breaches a rule, and with a resolution where only the mint had arrived.A replay logs
SettlementNotified,ReplayedSettlement, thenPausedon the token. Nothing is applied.
The states are explained in The dual-state lifecycle, and the keeper's operations in 35 Discard or resolve a validation.
Eligibility over time
Eligibility is not an event. isVerified computes it at each check from the identity the address resolves to, its claims, the trusted issuers and the required topics. These events can change the answer:
On the
ERC734Validator:ClaimAdded,ClaimChanged,ClaimRemovedfor the Investor's identity,ClaimRevokedby a Claim Issuer, andKeyAdded,KeyRemovedon a Claim Issuer, since a claim counts only while the key that signed it is a signing key of its issuer.On the
TREXRegistry: the claim topic and trusted issuer events,EligibilityChecksDisabled,EligibilityChecksEnabled.On the
IdentityFactoryand the storage:AccountLinked,AccountRevokedand the registration events, which change what an address resolves to.
Two changes emit nothing in the protocol: a claim reaching its validUntil, and an attestation revoked on an external system such as EAS. Re-read isVerified on a schedule, and before any report that depends on it.
Upgrades and governance
Versions.
VersionPublishedannounces,SuiteUpgradedactivates for every suite on the shared beacons. An isolated suite upgrades through the beacons listed in itsIsolatedSuiteDeployed: see 27 Upgrade a suite.Rules.
ModuleAdded,ModuleRemoved,ModuleForceRemoved,ModuleInteraction.Roles. The access manager of a suite is
Token.authority(). It emits OpenZeppelin'sRoleGrantedandRoleRevoked: see 25 Commission a suite and grant roles.Authority. Every change of access manager emits OpenZeppelin's
AuthorityUpdated(address authority)on the contract, at deployment too.transferOwnershipalso emitsOwnershipTransferred(address indexed previousOwner, address indexed newOwner).setAuthority, the path ofAccessManager.updateAuthority, emitsAuthorityUpdatedonly: index that one.
Practical notes
Discover from the factory.
TREXSuiteDeployedgives the four addresses of a suite, andToken.authority()its access manager.TREXRegistry.identityFactory()gives theIdentityFactory.Reorgs and finality are the indexer's concern. The protocol gives no confirmation signal. Pin each view call to the block of the events it complements.
Order by log index. A companion event is recognized by its position after a
Transfer.
Checklist:
Every emitter of a suite is indexed, and the shared identity contracts are filtered by identity.
Mints and burns exclude the
Transferevents paired withDelegatedOut,RecalledorSettledToNative.Supply is
totalSupply(), and the five identities are asserted.Frozen amounts include the automatic
TokensUnfrozen.Positions, pending amounts, eligibility and
Expiredare read from views.