Modular compliance
The rules of an asset are enforced by its ModularCompliance contract, which the token consults on every movement. Each rule is a module. An Issuer composes the rule set its asset requires by binding the corresponding modules, and the compliance evaluates the composed set at the moment of each movement, against the complete distribution of the asset.
This page describes how modules are typed, called, bound and removed. What the compliance gate decides is walked through in Eligibility and compliance: the two gates.
What the compliance is
One ModularCompliance serves one token. It has four jobs.
Answer the token before a movement: is it allowed, and may this spender execute it
This page
Keep each identity's total position
Call the modules that want to be told after a movement
This page
Issue and settle the validations for movements on satellite chains
Cross-chain: the distributed register
Three types of module
A module declares what it is, and the compliance calls it only for that.
RULE
allowedAmount(ctx)
The largest amount it allows for this movement
Before a transfer or a mint. When a cross-chain validation is issued.
SPENDER
moduleCheckSpender(ctx)
Whether this spender may execute this movement
Before an allowance is spent in transferFrom. When a validation names a spender.
TRACKER
afterTransfer(ctx)
Nothing. It records what it needs.
After every movement the register records.
A module can declare one, two or all three types. The compliance keeps one list per type, so each call goes to exactly the modules that answer it. A token with no SPENDER module pays nothing for the spender check, and a token with no RULE module allows every movement that passes the eligibility gate.
The context
Every call to a module carries the same structure, filled in once by the compliance.
compliance
The compliance asking. A module reads the ledger and its own settings from this address.
fromIdentity, toIdentity
The identities of the sender and the recipient, already resolved. Zero for the absent side of a mint or a burn.
fromWallet, toWallet
The addresses involved, as keys: an address of the reference chain, or the key of a satellite wallet.
amountMin, amountMax
The range of the movement. Equal for a movement on the reference chain. A range when a cross-chain validation is being issued.
isIssuance
True while a cross-chain validation is being issued.
spender
Who executes on the sender's behalf, as an interoperable address. Filled in the calls to SPENDER modules only. Empty in the calls to RULE and TRACKER modules, and when the sender executes itself.
Two consequences for whoever writes a module:
A module never resolves an identity. Both arrive resolved. Every module reasons about the same holders.
One convention covers every case. A mint has no sender, a burn has no recipient, and the same function handles a movement on the reference chain and the issuance of a validation for a satellite.
How a rule answers
A RULE module does not say yes or no. It says how much: the largest amount it allows to move. No limit is the maximum value. A refusal is zero. The compliance keeps the smallest answer over all bound rules.
That single answer serves both situations.
A movement on the reference chain
The amount must not exceed the smallest answer.
The issuance of a cross-chain validation
The requested range is narrowed to the smallest answer. If nothing is left of the range, the validation is refused.
A rule must be monotonic: a smaller amount is never less acceptable than a larger one. Caps, floors, limits and allow lists all are. A rule that is not, such as "a multiple of 100", evaluates the amount itself and answers all or nothing, and refuses a validation whose range is not a single amount.
A rule is read-only. The compliance calls it in a way that makes any write revert. A rule that needs to remember something also declares TRACKER and records it after the movement.
The modules that ship
MaxBalancePerIdentityModule
RULE
A cap on what one identity may own, over all its addresses and all chains. It counts the identity's position and what open validations already promise to it.
setMaxBalance(max)
SpenderVerificationModule
SPENDER
The spender must be eligible for the asset, like a holder.
None
SpenderWhitelistModule
SPENDER
The spender must be on a list of operators named by the Issuer. The list starts empty, so every spender is refused until one is listed.
allowSpender(spender), disallowSpender(spender)
The cap is a complete example of a rule over the global distribution. It limits no burn and no movement between two addresses of one identity. The same logic covers a transfer on the reference chain, the issuance of a validation, two validations competing for the same room, and a settlement that arrives late.
The two spender modules are described in Identity-based control and spender compliance.
Other rules can be written on the same interface: a limit on the number of holders, a restriction by jurisdiction, a limit on supply, a lock-up. The modules of earlier versions of T-REX do not run on this interface, which changed in depth: each rule is implemented anew for it, and the three modules above are the ones that exist so far. Some rules need a check against data held off-chain. Those rely on Chainlink ACE. Writing a module is covered under Build.
Binding and removing modules
All of these require the OWNER role on the compliance.
addModule(module)
Binds a module.
addAndSetModule(module, calls)
Binds a module and configures it in the same transaction, with up to five calls.
callModuleFunction(callData, module)
Configures a bound module. A module accepts settings only from a compliance it is bound to.
removeModule(module)
Unbinds a module.
forceRemoveModule(module)
Removes a module without calling it.
resyncModuleTypes(module)
Re-reads the types a module declares, after the module was upgraded.
getModules() lists the bound modules, getModulesByType(type) those of one type, and isModuleBound(module) answers for one.
The rules of binding:
At most 25 modules per compliance.
A module declares at least one type, and each type once. A module that declares nothing would never be called, and is refused.
A module can refuse a compliance. A module that needs a precondition says so, and the binding fails until the precondition is met. The three modules that ship bind anywhere.
A module's types are read when it is bound. If an upgrade of the module changes them, the change takes effect when the
OWNERcallsresyncModuleTypes.
Settings belong to a binding
A module is one contract that serves many compliances. It keeps its settings per compliance, and ties them to the current binding.
Unbinding discards the settings. A module removed and bound again comes back with an empty list or an unset cap.
Do not remove and re-add a module to refresh it. Use
resyncModuleTypes.Bind a restrictive module with its settings.
SpenderWhitelistModulerefuses every spender until one is listed. Binding it withaddAndSetModulelists the operators in the same transaction, so there is no interval in whichtransferFromis blocked for everyone.
When a module misbehaves
A TRACKER module reverts when told of a movement
The movement reverts. A module that cannot record a movement has to stop it.
Fix the cause, or remove the module.
A module reverts when it is unbound
removeModule reverts and the module stays bound.
forceRemoveModule, which does not call the module. The same module address cannot be bound to this compliance again. A new deployment of it can.
A faulty module cannot hold a token hostage.
Who controls a module's code
A module sits behind its own upgradeable proxy and has its own authority, separate from the token suite that binds it.
Trust statement. Whoever can upgrade a module can change the rule for every compliance bound to it. The shared modules are deployed and governed by Network governance. An Issuer that wants sole control over a rule deploys its own instance of the module.
Roles
OWNER
Binding, configuring and removing modules. Repairing a position.
COMPLIANCE_MANAGER
The settings of cross-chain validations: validity window, reconciliation windows, issuance pause per chain.
AGENT
Requesting a cross-chain validation on behalf of a holder.
VALIDATION_KEEPER
Discarding expired validations and resolving stuck ones. The T-REX Network runs the keeper under this role.
Last updated