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

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, IdentityRegistryStorage and ModularCompliance are four proxies, named by TREXSuiteDeployed. 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, ERC734Validator and KeyApprovalModule each serve every identity. An identity emits none of them itself: filter on the indexed identity argument.

  • A satellite wallet is a bytes value and a bytes32 key. The value is its ERC-7930 interoperable address, in canonical encoding. The key (walletKey, fromKey, toKey) is its keccak256. A chainKey is the keccak256 of the chain's ERC-7930 prefix with an empty address, and ChainRegistered maps it back.

Events by contract

Token

Event
Emitted when

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:

Event
Emitted when

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

Event
Emitted when

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.

Event
Emitted when

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

Event
Emitted when

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

Emitter
Event
Emitted when

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

Event
Emitted when

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.

Event
Emitted when

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

Event
Emitted when

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."

Figure
From events

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 of balanceOf + totalBridged()

  • totalBridged() = sum of bridgedBalanceOf + totalInTransit()

  • getFrozenTokens(a) is at most balanceOf(a)

  • balanceOf of the token's own address is zero: there is no escrow address

  • sum of positionOf + positionGap() = totalSupply()

Frozen amounts

  • Address freeze. The last AddressFrozen gives the state. _owner is 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. TokensFrozen minus TokensUnfrozen. 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 TokensUnfrozen for that shortfall before the Transfer. No agent calls an unfreeze.

  • Recovery. In order: TokensUnfrozen(lostWallet, frozen), Transfer, TokensFrozen(newWallet, frozen), then for a frozen lost wallet AddressFrozen(lostWallet, false, token) and, unless it is frozen already, AddressFrozen(newWallet, true, token), then RecoverySuccess. When they apply, the registry logs the registration of the new wallet first and the removal of the lost one just before RecoverySuccess.

Holders and positions per identity

An address resolves to an identity, for one asset, from two sources:

  1. A registration of the asset, in its storage: IdentityStored, IdentityModified, IdentityUnstored. It takes precedence. IdentityOverridden marks a registration that differs from the global link.

  2. The global record, on the IdentityFactory: AccountLinked. An identity's own address resolves to itself from IdentityDeployed, with no AccountLinked. After AccountRevoked the 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.

Figure
Read at a pinned block
Why not from events

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.

Status
Entered with, on the compliance
On the token

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 SettlementNotified first and ProtocolMessageReceived last. A delivery that reverts logs nothing.

  • ValidationIssuancePaused is 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, then Paused on 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, ClaimRemoved for the Investor's identity, ClaimRevoked by a Claim Issuer, and KeyAdded, KeyRemoved on 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 IdentityFactory and the storage: AccountLinked, AccountRevoked and 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. VersionPublished announces, SuiteUpgraded activates for every suite on the shared beacons. An isolated suite upgrades through the beacons listed in its IsolatedSuiteDeployed: see 27 Upgrade a suite.

  • Rules. ModuleAdded, ModuleRemoved, ModuleForceRemoved, ModuleInteraction.

  • Roles. The access manager of a suite is Token.authority(). It emits OpenZeppelin's RoleGranted and RoleRevoked: 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. transferOwnership also emits OwnershipTransferred(address indexed previousOwner, address indexed newOwner). setAuthority, the path of AccessManager.updateAuthority, emits AuthorityUpdated only: index that one.

Practical notes

  • Discover from the factory. TREXSuiteDeployed gives the four addresses of a suite, and Token.authority() its access manager. TREXRegistry.identityFactory() gives the IdentityFactory.

  • 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 Transfer events paired with DelegatedOut, Recalled or SettledToNative.

  • Supply is totalSupply(), and the five identities are asserted.

  • Frozen amounts include the automatic TokensUnfrozen.

  • Positions, pending amounts, eligibility and Expired are read from views.