A compliant tokenized security has one structural feature everything else depends on: a call that can reject a transfer before it executes, consulting arbitrary external logic. Without it, nothing in any EU regime is enforceable on-chain — every rule about who may hold, who may receive, and under what conditions has nowhere to attach.
This article covers that gate and the modules immediately around it: SecurityToken, ModularCompliance, ModuleAdapter, and the narrow per-rule gates that plug into it. The wider architecture — the capability set, the module inventory, and how many instances of each you deploy — is in the EU-Compliant Tokenized Securities: Smart Contract Architecture pillar.
The hook has a name, and it is worth fixing before anything else. It is ICompliance.canTransfer(from, to, amount). SecurityToken(the token contract) calls this function on every movement and treats its answer as final, ModularCompliance(the rule engine) implements it. Each individual rule module plugs into ModularCompliance behind it. Where the sections below say the gate, that is the function they mean.
ICompliance is ERC-3643's interface, and this article assumes that choice rather than arguing it. Which standard to build on — and what conforming to one does and does not buy you — is its own article, because the answer is decided before any of the wiring below exists.
The gate is also the single easiest place in the architecture to over-build, and the mistakes are not obvious. Three of the four sections below are about what to keep off it.
1. First: check whether the anti-money-laundering rules reach you at all
The anti-money-laundering regime binds obliged entities, and the category list is specific: investment firm, credit or financial institution, payment or e-money institution, fund manager, crypto-asset service provider, crowdfunding provider.
"Issuer of securities" is not on that list.
This is a test of what license the entity holds, not what the entity actually does day to day (entity test, not a conduct test) — verify it against the operating entity's own licence, not against the activity it performs. A genuinely unlicensed pure issuer sits outside the regime and is caught only if it is independently one of the listed entity types. The moment any licence is held, it attaches in full, and once that license is held, the obligations cannot later be dropped.
This is a scoping correction, not permission to defer the module. Build the screening regardless — targeted financial sanctions obligations bind everyone irrespective of anti-money-laundering status, and that is a separate regime with its own basis. What changes is the citation. If your documentation maps a screening control to a specific anti-money-laundering Article (an 'Article-to-function map' — the document showing which law maps to which piece of code), but your entity isn't actually on the obliged-entity list, that citation is simply wrong — a defect your regulator will find when reviewing that document.
Note also that the current regime applies from 10 July 2027. Until then the operative rules are the previous directive as transposed in your Member State. The obligations survive the transition in substance; the citations do not.
2. The gate must not say why it refused
This is the constraint that surprises engineers, and a naive implementation commits a criminal offence rather than a compliance failure.
Tipping-off is prohibited: no disclosure to the customer or any third party that a suspicious-transaction report has been filed, is being prepared, or that the customer is under analysis. In most Member States it is an individual criminal offence, and it covers the preparation stage — not just a filed report.
Now consider what an on-chain block looks like from the customer's side. A transfer reverts with a named reason, or a WalletFrozen(address, reason) event lands in a public block. A specific, labeled error message (a 'typed revert reason'), or a public log entry that carries a reason, counts as a disclosure. And on a public ledger it is a disclosure to the customer and to everyone else watching.
Three rules follow, and each one has to survive contact with whatever the token standard does by default:
Two classes of refusal, and a module author picks one before writing an error.
The naive rule — one generic code for every block — is over-broad, and collapsing everything into "not compliant" throws away exactly what an audit reviewer and a support desk need and cannot recover afterwards. The line is drawn by what the block is, not by which module raised it.
- Informative. A holding period, a closed period, a missing acknowledgement, a lapsed due-diligence record, a claim the investor has not yet obtained. Each is a fact about the instrument or the record, applies to everyone in the same position, and is something the holder can cure or wait out. Those errors may name their Article and what would satisfy them. An unlock date is not a suspicion, and telling a holder when they may exit is a service.
- Opaque. A sanctions listing, a suspicion block, a probate hold, a court attachment, an operational hold pending investigation. One argument-free error, the same one for every reason in the class — not even a wallet address, because on a two-sided check that says which side failed. The moment a distinct code exists for the suspicion case, the code is the tip-off.
A module unsure which class it is in is in the opaque class. The two mistakes do not cost the same: over-disclosing is a criminal offence, under-disclosing is a support ticket.
A generic error is not enough on its own — the storage has to be generic too.
Write sanctions listings to one variable and probate holds to another, and anyone can read the contract's state and recover the reason from which variable is set, whatever the revert said. The error discloses nothing; the storage layout discloses everything. Every opaque-class stop therefore has to live in one store, behind one flag, with the reason held off-chain.
No reason code in freeze events. The reason belongs in the off-chain case file, not in an event topic anyone can index.
A suspicion block and a sanctions freeze must look identical on-chain.
You are separately required to be able to refrain from executing a suspicious transaction, and to do it without the customer learning why. Sanctions freezes are publicly justifiable; suspicion blocks are not. Same mechanism, same observable behaviour, different case file.
The interface does this work for you, which is worth knowing before you build around it.
canTransfer returns a bare boolean — pass or fail, with no room for a status code. That is an irritation for a support desk and exactly right here: the gate cannot disclose a reason to the caller even where a module author wants it to. So the interface itself makes opacity automatic because it only allows true/false, a developer can't accidentally leak a reason even if they wanted to; it isn't a rule reviewers have to keep checking for by hand.
But a bare boolean tells an informative-class holder nothing either — no unlock date, no Article, no cure.
Both classes have to coexist on one gate, and the way they do is two entry points on the same engine. canTransfer returns the boolean and is authoritative: if it says no, the movement does not happen. A second, reverting check re-runs the same module walk only after the boolean has already refused, purely to recover an explanation. Where the failing module is informative-class, the holder gets its Article and what would satisfy it. Where it is opaque-class, that module's single argument-free error is all there is to recover, so the holder learns exactly nothing — which is the requirement, not a gap.
Two properties keep that safe rather than clever. The boolean decides and the reason path never overrides it — so an explanation path that is absent, unwired or broken is a downgrade in explanation and never in enforcement. And nothing in the engine enforces the classification: an opaque-class module that reverts with a typed error compiles and runs. The class label therefore belongs on the module itself, in source, so the audit map can be read off without reading every error in the tree.
3. Claims expire, and expiry has to fail closed
Periodic due-diligence refresh runs on a documented, risk-tiered cadence — annual for high risk, three to five years for low. Separately, where due diligence cannot be completed, you must stop transacting.
Together those mean the eligibility claim carries a validity window, and the gate must treat an expired claim exactly as it treats a missing one.
The window is mandatory at the write, not per issuer.
A claim registry that accepts "no expiry" has made the refresh cadence opt-in for whoever writes the claim, and one issuer choosing zero disables the control for every wallet it attests. So the write rejects a zero or past expiry outright and refuses anything further out than a governance-set ceiling, and the read collapses an expired claim to "unasked question" rather than to a silent pass.
The failure mode otherwise is silent and one-directional: an investor onboarded in year one keeps transferring in year four on a claim nobody re-verified, and nothing on-chain signals it. There is no natural event that fires when a claim goes stale.
Claim expiry fails closed — which has an operational consequence worth pricing before you ship it. Expiry now freezes a live position rather than generating a task, so the refresh workflow needs a service-level commitment behind it. A design that fails closed without a refresh SLA has moved a compliance risk onto your support desk.
Revocation is the harder version of the same problem. When a qualified attestation is revoked, that revocation is final and must reach the gate with immediate effect on transferability. On a single token that is straightforward. Across a platform running several tokenized assets it is a fan-out to several compliance instances, and a partial propagation leaves the same investor revoked on one asset and live on another. Decide whether that propagation has to be atomic before you deploy the second asset, not after.
4. One claim that must not be on the transfer hook
Every claim so far describes a continuing attribute of the investor — verified, screened, due diligence current. Gating every transfer on those is correct. Classification is a continuing attribute too and still does not belong here: the hook does not read tier, because a per-transfer tier gate would stop a retail holder receiving on a secondary market that never asked the question. Tier is read where it decides something — the subscription path, and the covenant predicates below.
The key-information-document delivery duty is different, and wiring it into the gate breaks it in two directions at once. It is a pre-contractual event: the document was delivered in good time before this investor became bound.
Gate the subscription path, not the transfer hook. Wiring it into the gate would freeze the live position of an investor who was properly served, and would block secondary transfers the duty never reached. It belongs alongside the suitability claim, on the same path as subscription acceptance.
It is not a boolean, and it does not expire on a cadence. The claim binds the specific document version hash anchored in the document registry. An acknowledgement of version 1 is not an acknowledgement of the version 3 in force when the investor actually subscribes — so a revision invalidates outstanding acknowledgements. That is a re-issue trigger, not an expiry window. A boolean kidDelivered flag passes every test you will write and breaches the duty the first time the document is revised mid-offer.
That pattern — a covenant bound to a document version rather than a claim bound to a clock — is a capability in its own right, covered in the investor-covenants article.
5. The rule engine, its cap, and its bypass
Rules plug into the gate as individually addable, removable and upgradable modules. That modularity is not an engineering preference: your external audit needs a one-to-one map from Article to function, and a monolithic compliance function cannot produce one.
Two properties of the engine matter more than they look:
A hard cap on the module list. An unbounded list is a gas denial-of-service on the token itself, and the party who suffers is the holder trying to exit. The cap is a holder-protection control, not an optimisation.
A logged, one-module-at-a-time emergency bypass. A module that reverts blocks every transfer. That is exactly correct when the module is exercising a veto, and catastrophic when the module has a bug. Without a bypass, a defect in one narrow rule freezes the entire register — and the fix requires an upgrade, which for a disclosed contract means a supplement, regulatory approval and an investor withdrawal window before it can deploy. Weeks, to fix a bug. The bypass has to be per-module, logged, and never a global off-switch.
Which is the reason one control cannot live on this list at all. Both levers above — remove a module, bypass a module — are one governance call, and a sanctions listing binds irrespective of anything governance decides. So the wallet-stop store is read by the token directly, above the module list, on every path that moves units: an ordinary transfer, a mint, and the agent-operated forced transfer. Leave forced transfer to reach the store only through the module list and the seizure path becomes the bypass — an agent key placing units on a listed person with nothing on-chain looking wrong.
6. Adapters: narrow gates, no logic
Each rule is written against one Article and reverts with errors named after it. The rule engine, meanwhile, wants one common signature.
The wrong resolution is to widen every gate to a shared interface — that blurs the Article-to-function map the audit depends on, which is the one thing modularity was bought for. Adapt at the boundary instead. A thin abstract base presents each narrow, single-purpose gate onto the engine's interface, so the gates themselves never have to be widened.
The adapters hold no logic of their own, and that rule is worth enforcing in review. Anything decided inside an adapter is a second place a reader has to look to find the Article — which is the exact legibility problem the pattern exists to solve.
The one place adapters legitimately differ is which sides they gate, and the zero address is where a careless one breaks. A holding-period lock restricts exiting, so it binds the sender and skips mint — a burn is in scope, a mint is not. A closed-period freeze passes both sides through. A covenant gate checks the recipient on a mint and both parties on a transfer, and on a burn checks the sender only — nobody is receiving, the zero address resolves to no investor, and a check that has no valid input to test deaults to 'refuse'. Wire a receive-scoped covenant without that carve-out and every redemption, buy-back burn and maturity burn in the instrument reverts, from a rule that was never about exits.
7. What is not on the transfer hook
The most expensive mistake available here is wiring fund-structure limits — concentration, diversification, borrowing ratios — into the transfer gate. It looks right. It is wrong, for four independent reasons, any one of which is sufficient:
- A peer-to-peer transfer moves neither capital nor the net-asset-value denominator. The hook has nothing to test.
- A mint raises the denominator and lowers every concentration ratio. Vetoing subscriptions during a breach would block the remedy — the inversion that makes the control worse than nothing.
- A suspension flag marks a window in which the limit is deliberately relieved during a capital raise. It is not a trading halt. Gating mint on it is exactly backwards.
- Mint and burn take cash amounts from the subscription path, not token unit amounts. The two are not interchangeable, so the hook could not advance the state correctly even if it were wired.
Fund limits are gated by their own reverting subscription, redemption and position-recording functions. That satisfies the on-chain test perfectly well — something reverts on the state — just not through the transfer hook. Do not let an adapter be written to bridge them.
8. The topology is a star, not a chain
A common way to draw this architecture is a chain: token, then identity registry, then compliance engine. That reads as though the engine sits behind identity and can see it.
It does not. The token calls the identity registry and the rule engine independently, and the rule engine holds no identity reference at all. Every module that needs an identity attribute wires its own.
The transfer check is four reads in sequence, and none of them is nested in another: the whole-address freeze on both sides, the identity gate on each present side, the wallet-stop store, and only then the rule engine. Four references held by one caller. The rule engine is the last of them and can see none of the other three — which is why a revoked claim has to be propagated to it rather than inherited by it.
That is a fan-out, and it is the same fan-out behind the revocation question in section 3 — a revoked claim has to reach every module-held reference, not one. Drawing it as a chain hides the question rather than answering it.
9. What not to build
Five controls that look mandatory, are commonly built, and should not be:
| Do not build | Why |
|---|---|
| An on-chain cumulative due-diligence value counter | The threshold it implements governs occasional transactions — parties you have no business relationship with. Establishing a business relationship triggers full due diligence on its own with no value threshold, so every investor you onboard has already cleared it before any counter could reach any figure. It enforces nothing and adds an accumulator to the audited surface |
| The crypto-asset-service-provider onboarding trigger | This platform is never one. The instruments are financial instruments, and the crypto-asset regime expressly excludes them |
| The occasional-cash trigger | A securities subscription is not a cash transaction |
| The cash-payment prohibition | Securities are not cash |
| Travel Rule originator/beneficiary fields on the security leg | Financial instruments sit outside crypto-asset scope. The control here is due-diligence re-verification, not transfer-of-funds data |
One carve-out on the last row, and it is a live trap. Settle in e-money tokens and the Travel Rule comes back — e-money tokens are expressly treated as crypto-assets, which pulls the cash leg into full originator and beneficiary data collection with no de minimis(no minimum transaction size below which the rule doesn't apply). The security leg stays out of scope; an atomic delivery-versus-payment settles both in one transaction. That is a consequence of the settlement-asset decision, not of the token standard, and it should be priced in when that decision is made.
At a glance
| Control | On the transfer hook? | Note |
|---|---|---|
| Identity binding and the required claim set | ✅ | No identity, no transfer |
| Sanctions / politically-exposed-person freeze | ✅ | Opaque class — one argument-free refusal |
| Due-diligence claim expiry | ✅ | Informative class. Expiry mandatory at the write, capped at a governance ceiling; fails closed, so it needs a refresh SLA behind it |
| Suspicion block | ✅ | Opaque class — indistinguishable from every other stop in it |
| Investor tier | ❌ | Read where it decides something — the subscription path and the covenant predicates. A per-transfer tier gate blocks a retail holder from receiving on a market that never asked the question |
| Holding-period lock | ✅ | Sender side only — skips mint |
| Closed-period freeze | ✅ | Both sides |
| Investor covenants | ✅ | Bound to a document version, not a clock |
| Key-information delivery | ❌ | Subscription path — it is pre-contractual, not a continuing attribute |
| Fund concentration / borrowing limits | ❌ | Their own reverting functions — four reasons, any one sufficient |
| Cumulative value counters | ❌ | Do not build |
If you're building a tokenized security and need this transfer-gate architecture designed and audited end to end, see our RWA tokenization development services.
This is engineering commentary on regulatory requirements, not legal advice. Whether and how these obligations apply to a specific structure needs sign-off from counsel.



