SKIP TO CONTENT
Web311 minBuilding the RWA Chain / Ep. 2

The Freeze Button Stops Working: ERC-3643 Compliance Across a VM Boundary

Adam Boudjemaa
SHARE

KEY TAKEAWAYS

  • The serious failure is not the check that does not run. It is the remedy that stops existing: once the supply is escrowed at the module account, the ERC-20 sees one holder, and freeze and forced transfer have nothing left to reach.
  • My own diagram in episode 1 compressed a step. MsgConvertERC20 calls the token transfer function, so the ERC-3643 checks do fire, once, at the door. The issuer opens that door deliberately. The corridor behind it is the problem.
  • ERC-7943 (uRWA) went Final on 5 May 2026. Its one genuinely new idea is splitting account eligibility from transfer authorization. It is an interface, not a system.
  • No Final ERC covers compliance state crossing a chain or a VM boundary. Not ERC-7943, and not ERC-3643, which I helped write.
  • Verified against cosmos/evm at commit 0ae5e223 and against all 611 files in the ERC repository. No incident, no war story. I read the module.
1
HOLDER THE ERC-20 SEES
after conversion, the whole escrowed supply sits at the module account
611
ERC FILES I CHECKED
none of the Final ones covers compliance crossing a chain or a VM

A compliance agent on an ERC-3643 token has three powers that matter on the day something goes wrong. Freeze part of a balance. Freeze an address. Force a transfer out of a wallet.

All three operate on ERC-20 balances.

I spent a week reading the Cosmos EVM x/erc20 module, because episode 1 of this series made a claim I had never traced line by line myself. What I found was worse than the claim, and worse in a direction I did not expect.

When a token is converted into a Cosmos bank denomination, the converted supply is escrowed at one address: the module account. From that moment the ERC-20 contract has exactly one holder.

The agent still has all three powers. They have nothing left to point at. Freeze which balance? Force a transfer from whom? The real holders moved to a ledger the token cannot see.

That is not a check that failed to run. That is a remedy that stopped existing. If you are a regulated issuer, the remedy is the half a regulator asks about, and it is the half that is gone.

Two things before we go further. The first: episode 1 of this series drew this escape path as four steps, and one of those steps was compressed. I fix my own diagram below.

The second: I have no incident to show you. I went looking for one and what comes back is bridge hacks, which are message-verification and key-compromise failures wearing similar clothes. My evidence here is the module source at a pinned commit and the text of the standards. That is weaker than a war story and a great deal stronger than a hunch.

The remedy, not the check

Almost everyone who looks at this problem looks at the check. Did the transfer restriction run, or did it not. That is the interesting question if you are an engineer and the boring one if you are an issuer, whose question is the next one along: a regulator or a court wants an address frozen, and can you still do it.

ERC-3643 was written so the answer is yes. It gives an agent role setAddressFrozen, freezePartialTokens and forcedTransfer, and its specification requirements say in as many words that the token MUST be able to force transfers from an agent wallet. That is not a convenience bolted on the side. It is a stated requirement of the standard, because forced transfer is what regulated issuance actually needs.

Every one of those functions takes an address and locks or moves an ERC-20 balance.

Now escrow the supply at a module account. The balances the agent can reach are one entry. The balances that matter are somewhere else entirely.

What the agent can do on the EVM

Freeze a specific address. Freeze part of a specific balance. Force a transfer out of a wallet that will not cooperate. Every investor holds their own balance, so every remedy has a target.

What the agent can reach after conversion

One address, holding the entire escrowed supply, that belongs to a module rather than an investor. Freezing it freezes everyone. Force-transferring from it is meaningless. The remedy still compiles and no longer does anything useful.

I am one of five named authors of ERC-3643 (T-REX), so read this as someone describing the limits of his own work rather than scoring a point off somebody else.

Where my own diagram was wrong

Episode 1 drew the leak as four nodes: a compliant token, registered in x/erc20 as a bank denomination, moved with bank.MsgSend where the EVM checks never run, then bridged out over IBC.

The shape is right. The step count is not.

Between registered and moved there is a conversion, and the conversion is not a bookkeeping entry. MsgConvertERC20 calls the token own transfer function, inside the EVM, with the module account as recipient.

convert.go
// cosmos/evm, x/erc20/keeper/convert.go @ 0ae5e223

// Escrow tokens on module account
transferData, err := erc20.Pack("transfer", types.ModuleAddress, amount.BigInt())
if err != nil {
    return nil, err
}

res, err := k.evmKeeper.CallEVMWithData(
    ctx, stateDB, sender, &erc20Contract, transferData,
    commit, callFromPrecompile, nil,
)
erc-3643.sol
// ERC-3643: the checks that run on that very call

require(
    _tokenIdentityRegistry.isVerified(to),
    "ERC-3643: Invalid identity"
);
require(
    _tokenCompliance.canTransfer(from, to, amount),
    "ERC-3643: Compliance failure"
);

For an ERC-3643 token that means the pre-transfer checks run. isVerified fires against the module account. canTransfer fires against the offering rules. If the module account is not a verified identity in the token identity registry, the conversion reverts and nothing moves.

So compliance is not bypassed. Compliance gets exactly one shot, at the door, and it works. Which sounds like it kills the argument, and does the opposite.

Whitelisting the module account is the only way to make the token usable with Cosmos-native tooling, and making the token usable with Cosmos-native tooling is the entire reason anyone registers it. Nobody is tricked here. The issuer opens that door on purpose, for a good reason, after a meeting.

What the issuer does not see is that the door opens onto a corridor with no doors in it.

The corrected path, all five steps

Here is the whole path with the missing step restored, and with one more finding at the end that episode 1 never reached.

Step 2 deserves a note of its own. The registration handler comment is blunt: any account can permissionlessly register a native ERC20 contract to map to a Cosmos Coin. When the chain runs with permissionless registration enabled, the issuer is never consulted. Turn it off and registration takes a governance proposal instead, which is slower and public, and still not the issuer decision.

THE CORRECTED PATH
Sequence diagram: 6 participants: Holder, ERC-3643 token, x/erc20 module, x/bank, IBC, Compliance agentSequence diagram: 6 participants: Holder, ERC-3643 token, x/erc20 module, x/bank, IBC, Compliance agent. Step 1: Holder to ERC-3643 token, A normal, fully compliant EVM transfer. Step 2: Holder to x/erc20 module, MsgRegisterERC20 maps the token to a bank denomination. Step 3: x/erc20 module to ERC-3643 token, MsgConvertERC20 calls transfer(moduleAccount, amount). Step 4: x/erc20 module to x/bank, MintCoins, then SendCoinsFromModuleToAccount. Step 5: Holder to x/bank, bank.MsgSend moves it to anyone. Step 6: x/bank to IBC, ICS-20 transfer, and it leaves the chain. Step 7: ERC-3643 token to Compliance agent, balanceOf shows one holder.HOLDER > ERC-3643 TOKENA normal, fully compliant EVM transferEvery transfer restriction enforced. Nothingwrong here, and nothing wrong yet.HOLDER > X/ERC20 MODULEMsgRegisterERC20 maps the token to abank denominationPermissionless when the chain enables it.Registration alone moves nothing and callsnothing.X/ERC20 MODULE > ERC-3643 TOKENMsgConvertERC20 callstransfer(moduleAccount, amount)The step episode 1 compressed. isVerified andcanTransfer run here, and revert unless themodule account is whitelisted.X/ERC20 MODULE > X/BANKMintCoins, thenSendCoinsFromModuleToAccountThe balance is now an ordinary sdk.Coin. Theescrowed ERC-20 supply sits at the moduleaccount.HOLDER > X/BANKbank.MsgSend moves it to anyoneNo hook, no wrapper, no ante-handler inx/erc20 intercepts a bank send of a paireddenomination.X/BANK > IBCICS-20 transfer, and it leaves thechainThe EVM never saw any of it.ERC-3643 TOKEN > COMPLIANCE AGENTbalanceOf shows one holderFreeze and forced transfer have nothing toreach. This is the step nobody draws.
Seven steps, and only one of them is a compliance failure in the usual sense. Step 3 is a door the issuer opened deliberately. Steps 5 through 7 are the corridor behind it.

I read every file in x/erc20/keeper looking for the hook that would make step 5 wrong. There is not one. The only two places the module invokes an ERC-20 transfer are the two conversion paths.

That is an argument from absence, so let me bound it honestly. One module, one commit, 0ae5e223, current cosmos/evm. Chains running older Evmos forks may differ, and I have not read those. The safe generalisation is the one episode 1 already made: compliance that lives in a contract is only enforced on the paths that run that contract.

The exit is unguarded. The return trip is not.

There is a detail in the module that reads like a joke once you have been staring at it long enough.

Converting back does re-run compliance. The reverse path calls transfer from the module address to the receiver, so the token checks fire on the way home, exactly as designed.

The token guards the return trip and not the exit. And a token that left over IBC does not take the return trip. It exists as a voucher on a foreign chain, and it never needs to come home.

As for what the bridge carries, the ICS-20 packet payload is five fields, complete:

ics-020-fungible-token-transfer
interface FungibleTokenPacketData {
  denom: string
  amount: uint256
  sender: string
  receiver: string
  memo: string
}

Denomination, amount, sender, receiver, memo. No identity. No claim. No jurisdiction. No restriction flag.

That is not an oversight either. The ICS-20 specification lists permissionless token transfers with no need to whitelist connections, modules or denominations among its desired properties, and says the protocol requires no additional permissioning. It is doing precisely what it says on the label.

The memo field is the only extension point, and it is a free-form string intended for middleware. No standard assigns it compliance meaning.

Everything above assumes there is a Solidity contract somewhere that could hold the compliance logic. For a token that starts life as a native Cosmos coin, there is not. Under Single Token Representation v2, the ERC-20 face of a native coin is a stateful precompile backed directly by x/bank, not a deployed contract. There is no bytecode in which an identity registry could live, and conversion for those pairs is disabled outright.

Denominations arriving over IBC get a token pair created automatically, with no governance step and no issuer involvement. Episode 1 said whatever VM the chain adds next year. This is that sentence with a file name attached.

Where can the check actually run?

Step back from Cosmos for a second, because the specific module is not the point. The point is altitude.

Draw the bands a value transfer can pass through, and mark the one band where an ERC-3643 check exists. Every band above that mark is a path the check does not cover. New paths get added by whoever maintains the chain, on their schedule, not the issuer.

WHERE THE CHECK LIVES
Layered stack diagram: 6 layers, top to bottomLayered stack diagram: 6 layers, top to bottom. Layer 1, CONTRACT: ERC-3643 isVerified and canTransfer, setAddressFrozen, freezePartialTokens, forcedTransfer. Layer 2, EVM: Runs the contract, Entered by MsgConvertERC20, and by nothing else in the module. Layer 3, x/erc20 MODULE: Escrows the supply at one account, Mints a bank denomination, No hook on bank sends. Layer 4, x/bank: An ordinary sdk.Coin, Sends move value with no compliance code path. Layer 5, IBC / ICS-20: Five packet fields, None of them policy. Layer 6, CONSENSUS: The only altitude that covers every band above it.CONTRACTERC-3643 isVerified and canTransfersetAddressFrozen,freezePartialTokens, forcedTransferEVMRuns the contractEntered by MsgConvertERC20, and bynothing else in the moduleX/ERC20 MODULEEscrows the supply at one accountMints a bank denominationNo hook on bank sendsX/BANKAn ordinary sdk.CoinSends move value with no compliancecode pathIBC / ICS-20Five packet fieldsNone of them policyCONSENSUSThe only altitude that covers everyband above it
The compliance rule exists in exactly one band. Value can move in four of them. That gap is the whole article, and it is not fixed by writing a better contract.

This is the same discipline I apply to agents that move money: put the rule at the resource rather than at the caller, because a caller can be routed around and a resource cannot.

What ERC-7943 actually changed

Episode 1 described ERC-7943 as an active thread on Ethereum Magicians. That was accurate when I wrote it and stopped being accurate on 5 May 2026, when the repository moved it to Final. There was a press cycle later that month, which is why you will see two dates. The one that matters is the repository one.

It is not my standard. Dario Lo Buglio, Tino Martinez Molina and Mihai Colceriu wrote it, and they took it from first draft to Final in under a year.

ERC-7943 TO FINAL
1

June 2025: added

uRWA lands in the ERC repository as a draft: one interface for real-world asset compliance across several token types. It moves to Review the following month.

2

September 2025: aligned with ERC-3643

A commit titled "Align with ERC-3643 naming conventions" lands, followed by a split into separate interfaces. The compatibility is deliberate and dated.

3

January 2026: Last Call

The final comment window opens.

4

5 May 2026: Final

Commit 46745bdc flips the frontmatter from Last Call to Final and deletes the last-call deadline with it. That commit is the date to quote, not the press release three weeks later. Worth knowing why: an EIP carries no finalisation field at all, only a created date, so the repository history is the only place the day is recorded.

Draft to Final in under a year, by three people who are not me. Every date here comes from the repository history, which is the only place the day is written down.

What it standardises: one shared interface for RWA compliance across ERC-20, ERC-721, ERC-1155 and ERC-6909. Six functions, forcedTransfer, setFrozenTokens, getFrozenTokens, canSend, canReceive and canTransfer, with ERC-165 introspection and fixed interface identifiers.

The genuinely new idea is one line of that list. ERC-7943 separates whether an account may participate at all from whether a particular transfer is allowed. canSend and canReceive evaluate an account independently of any transfer parameters, which lets you express a one-way restriction: an address blocked from receiving but still permitted to send. That situation is ordinary in regulated markets and ERC-3643 has no way to say it.

I would use that split. It is the right decomposition and I wish we had made it in 2021.

One trap, because it will catch somebody. canTransfer exists in both standards and means different things. In ERC-3643 it lives on the ICompliance contract and looks at global offering rules only, in deliberate opposition to isVerified, which checks one investor eligibility. In ERC-7943 it lives on the token and must cover eligibility and frozen balance together. Same name, different contract, different scope.

ERC-3643, Final
ERC-7943, Final
What it is
A system: five interfaces, an agent role, an ownership model
An interface layer, explicitly minimal and not opinionated
Identity
A registry and its storage are required components
No registry. On-chain identity is listed as a user-supplied extension
Compliance rules
Investors per country, tokens per investor, accepted countries
None. It standardises the questions, not the answers
Account vs transfer
One check path. No way to say "may send, may not receive"
canSend and canReceive sit apart from canTransfer. This is the new idea
Token types
ERC-20 only, plus lost-key recovery
ERC-20, ERC-721, ERC-1155, ERC-6909, no recovery
Crossing a chain or a VM
Not addressed
Not addressed

If you are choosing between compliance models rather than reading about one, the comparison of ERC-3643, ERC-1400 and CMTAT is the piece that lays out the trade, and it places ERC-7943 in the same frame.

What it does not fix, including in my own standard

I grepped the whole ERC-7943 specification, 446 lines, for anything about chains. Cross-chain, cross-VM, multi-chain, bridge, interoperability, IBC, Cosmos, non-EVM, rollup. Three hits.

One is bridge traditional finance and decentralized finance, a metaphor. One is interoperability between RWA token standards. That is the lot. The Security Considerations cover access control, front-running and ordinary contract security, and say nothing about chain boundaries.

That is not a criticism. ERC-7943 is scoped as a single-contract interface and never claims to be more, and taking a swing at its authors for not solving a problem they did not scope would be cheap.

Here is the part that costs me something. I ran the same grep over ERC-3643. All 414 lines. Zero hits. Neither of the two Final RWA standards says one word about what happens when a token crosses a chain or a VM boundary, and I helped write one of them.

So I checked the rest of the corpus. I cloned the ERC repository, 611 files, highest number 8330, and selected every file mentioning both a compliance term and a chain term. Twenty-five matched. I read the plausible ones.

Standard
Status
What it actually covers
ERC-7943, uRWA
Final
Compliance for one token contract. The only chain word in it is a metaphor.
ERC-7518
Review
Its "Interop" means same-chain token wrapping through a wrapper contract. Nothing crosses a chain. The easiest trap in this list.
ERC-7802
Draft
Crosschain mint and burn, deliberately neutral on access control. Concedes the check can only run on the chain the token lives on.
ERC-8121
Draft
Can resolve a credential held on another chain, with a worked KYC example. A read mechanism, and shaped for EVM.
ERC-8262
Draft
Runs the opposite way: binds attestations to one deployment so they cannot cross chains, and names cross-chain replay as a hazard.

No Final ERC covers compliance state crossing a chain or a VM boundary. The gap episode 1 named is still open, and it has now been open long enough that two separate Final standards have shipped around it.

Is your token exposed to this?

You can work out where you stand in about a minute, and the answer is usually one of four.

CHECK YOUR OWN TOKEN
Decide: Is my compliant token exposed to this?Decision tree: Is my compliant token exposed to this? If It lives on one EVM chain, with no bank module and no bridge, then Not exposed. If It is on a Cosmos EVM chain and the module account is not a verified identity, then Blocked at the door. If You whitelisted the module account so the token would work with native tooling, then Exposed, deliberately. If You do not know whether anyone has registered your token, then Find out today.DECIDEIs my compliant token exposed tothis?IF It lives on one EVM chain, withno bank module and no bridgeNot exposedThe check and the remedy sit on thesame ledger. This whole article issomebody else problem.IF It is on a Cosmos EVM chain andthe module account is not averified identityBlocked at the doorConversion reverts on isVerified.Keep it that way, and understandthat keeping it that way costs youevery Cosmos-native integration.IF You whitelisted the moduleaccount so the token would workwith native toolingExposed, deliberatelyThe conversion is legitimate.Everything after it is outside whatyour compliance agent can see orreach.IF You do not know whether anyonehas registered your tokenFind out todayRegistration is permissionless whenthe chain enables it, and theissuer is never asked. Query thetoken pairs.
Only the first outcome is safe, and it is safe because nothing else can touch the token. The other three are decided by where the rule lives, not by how well the contract is written.

None of those four outcomes is fixed by a better contract. Three of them are decided by where the rule lives.

That is the argument episode 1 made and this is the version with the source lines in it. A compliance rule has to sit under every path that can move value, not on one of them. On the chain I am building, that means transfer restrictions understood at the protocol level rather than deployed on top of it, so the bank layer and the bridge inherit the same rule the token does. Testnet is running now. Mainnet is on track for 2026.

And if you want the honest version of my own position: I helped write a standard that solves this beautifully inside one execution environment, and says nothing at all about the second one. That is the gap. I would rather name it than have somebody else find it in production.

The full thesis, including why a purpose-built chain and not an L2, is in the first episode of this series.

FAQ

No, and that detail matters. MsgConvertERC20 calls the token own transfer function with the module account as recipient, so isVerified and canTransfer run and the conversion reverts unless that account is a verified identity. The problem is what happens next: the escrowed supply sits at a single address, bank sends carry no compliance, and the enforcement powers have nothing left to reach.

No. setAddressFrozen, freezePartialTokens and forcedTransfer all operate on ERC-20 balances, and after conversion the ERC-20 has one holder, the x/erc20 module account. The bank-layer holders are invisible to the token contract. That is a lost remedy rather than a missed check, and it is the half a regulator asks about after the fact.

No, and it does not claim to. ERC-7943 reached Final on 5 May 2026 and standardises a compliance interface for a single token contract: canSend, canReceive, canTransfer, forcedTransfer, setFrozenTokens and getFrozenTokens. Its text contains nothing about chains, bridges or virtual machines. Neither does ERC-3643, and I am one of that standard five named authors.

Not a Final one. I checked all 611 ERC files in the repository. ERC-7802 is crosschain and deliberately neutral on access control. ERC-8121 can resolve a credential held on another chain. ERC-8262 runs the other way and binds attestations to one deployment so they cannot travel. All three are Draft. ERC-7518 sounds like the answer and is not: its Interop means same-chain token wrapping, and it is Review, not Final.

Not that I can find, and I looked. The bridge incidents people reach for are message-verification and key-compromise failures, which is a different problem wearing similar clothes. What I have is the module source at a pinned commit and the standards text. My read is that permissioned RWA volume on Cosmos EVM chains is still small enough that nobody has had to learn this the expensive way.

Building the RWA Chain

Episode 2 · 2 published

Adam Boudjemaa

Adam Boudjemaa

Former CTO of Integra. Named author (1 of 5) of ERC-3643, first author of ERC-6960, co-author of ERC-7410, and co-author of ERC-8203, which is still a draft. Building production AI and regulated Web3 systems.

Enjoyed this post?

Get more like it in your inbox every Tuesday.