
Issuing tokenized Real World Assets (RWAs)—such as private credit, real estate equity, or treasury bonds—on public EVM blockchains creates an immediate conflict between permissionless decentralized finance and securities compliance. Standard ERC-20 tokens allow arbitrary transfers to unverified addresses, non-accredited investors, or sanctioned jurisdictions, breaching regulatory frameworks like SEC Reg D / Reg S. In Aurum RWA, this conflict is resolved through an end-to-end institutional architecture: pairing ERC-3643 (T-REX) on-chain Identity Registries and dynamic compliance modules (
RwaComplianceModuleCore.sol) with a NestJS operations backend enforcing dual-operator approvals, immutable terms hashing, and two-phase asynchronous redemption locks.
1. The Production Failure State: Why Standard ERC-20 Tokens Break RWA Compliance
When engineering teams first approach asset tokenization, they frequently deploy an OpenZeppelin ERC20 contract and add a simple onlyOwner mint function, assuming that off-chain KYC at onboarding is sufficient.
In regulated financial environments, this naive model causes catastrophic legal and operational failures:
[Failure Mode 1: The Secondary P2P Transfer Breach]
T0: Investor Alice (US Accredited) subscribes to 100,000 USD worth of tokenized private credit.
T1: Alice initiates a standard P2P transfer of 50,000 tokens to Bob on Uniswap or via direct transfer.
T2: Bob is a non-accredited retail investor located in a sanctioned or unauthorized jurisdiction.
Contract Reality: Standard ERC-20 transfer() executes without checking Bob's identity.
Result: The issuer is in direct breach of securities regulations. The entire issuance faces regulatory enforcement.
[Failure Mode 2: Bypassing Maximum Investor Count via Address Sybils]
T0: Regulations restrict non-accredited holders to a maximum of 99 distinct natural persons.
T1: An investor creates 10 separate EVM wallet addresses and distributes tokens across them.
Contract Reality: Simple address-level balance checks see 10 distinct holders instead of 1 natural person,
corrupting the cap-table audit trail.
[Failure Mode 3: The Double-Spend Redemption Race]
T0: Investor requests redemption of 10,000 tokens for USD bank settlement.
T1: While the issuer's compliance officer reviews the wire instructions, the investor sells those
same 10,000 tokens on a secondary OTC market.
T2: The wire transfer is released from the custodian bank, while the tokens remain in circulation.
Result: Issuer pays out fiat cash without destroying or locking the backing token shares. Direct insolvency.
To satisfy legal enforceability, token transfers must evaluate four distinct on-chain predicates on every single execution:
- Identity Verification: Is the recipient mapped to a valid
ONCHAINIDwith active KYC and asset-specific eligibility claim topics? - Jurisdiction Whitelisting: Does the recipient’s country code satisfy the asset’s active country policy (
countryMode)? - Holding Ceilings: Does the incoming transfer push the recipient’s aggregate identity balance above the single-investor limit?
- Investor Count Caps: Does the transfer cause the net count of active legal entities holding the asset to breach statutory limits?
2. On-Chain Identity & Compliance Architecture: ERC-3643 + RwaComplianceModuleCore.sol

The smart contract suite powering Aurum RWA builds on the institutional ERC-3643 (T-REX) standard, decoupling token accounting from compliance policy evaluation.
+----------------------------------------------------------------------------------------------------+
| ON-CHAIN COMPLIANCE STACK |
| |
| [ Token (ERC-3643) ] |
| | |
| v transfer(to, amount) / transferFrom() |
| +----------------------------------------------------------------------------------------------+ |
| | Token Identity Registry Check | |
| | Assert _tokenIdentityRegistry.isVerified(to) == true | |
| | Extract Claim Topics: KYC (Topic 1) + Asset Eligibility (Topic 3, chainId, tokenAddress) | |
| +----------------------------------------------------------------------------------------------+ |
| | |
| v _tokenCompliance.canTransfer(from, to, amount) | |
| +----------------------------------------------------------------------------------------------+ |
| | RwaComplianceModuleCore.sol | |
| | 1. _context(): Assert caller action matches authorized AgentController execution | |
| | 2. checkEligible(to): Verify country code against Allowlist/Blocklist | |
| | 3. _freezeCheck(from): Available Balance = balanceOf(from) - frozenTokens(from) | |
| | 4. _limits(): | |
| | - Aggregate identity balance (across all linked wallets) <= maxBalance | |
| | - Net investor count (identityCount) <= maxInvestors | |
| | - Cumulative minted <= issuanceCap | |
| +----------------------------------------------------------------------------------------------+ |
| | |
| +---> If all assertions pass: _transfer() executes -> post-transfer hook _move() |
| +---> If any assertion fails: Revert with explicit Custom Error (e.g. CountryRestricted) |
+----------------------------------------------------------------------------------------------------+
Identity and Country Mapping
In rwa-contracts/src/RwaComplianceModuleCore.sol:L14-L40, the compliance module stores both wallet-level and legal identity-level ledgers:
struct RuleConfig {
uint8 countryMode; // 0: NONE, 1: ALLOWLIST, 2: BLOCKLIST
uint16[] countries;
uint64 transferUnlockAt;
bool maxBalanceEnabled;
uint256 maxBalance;
bool maxInvestorsEnabled;
uint256 maxInvestors;
uint256 issuanceCap;
}
IRwaController public controller;
uint256 public constant MAX_COUNTRIES = 250;
uint256 public constant MAX_BATCH = 100;
bool public bound;
bool public ready;
uint64 public policyVersion;
RuleConfig private _policy;
mapping(uint16 => bool) public countryMember;
mapping(address => uint16) public identityCountry;
mapping(address => uint256) public identityBalance;
mapping(address => uint256) public walletBalance;
mapping(address => address) public walletIdentity;
uint256 public investorCount;
uint256 public lifetimeMinted;
uint256 public trackedSupply;
Notice the separation:
walletBalance[w]: Tracks tokens residing in individual cryptographic addresses.identityBalance[id]: Aggregates token holdings across all wallets associated with the same verified legal entity.investorCount: Increments only when anidentityBalancetransitions from0to non-zero, and decrements only when an entity’s total balance across all wallets reaches0. An investor splitting funds across 5 wallets counts as exactly 1 investor.
Enforcing Claim Topics and Asset Scoping
To prevent an identity approved for a high-risk equity offering from automatically passing eligibility for an institutional bond offering on the same chain, the compliance module enforces asset-scoped claim topics (src/RwaComplianceModuleCore.sol:L125-L140):
function _requiredTopicsPresent() internal view returns (bool) {
uint256 kyc = uint256(keccak256(abi.encode("RWA_CLAIM_V1", uint8(1))));
uint256 eligibility = uint256(
keccak256(abi.encode("RWA_CLAIM_V1", uint8(3), block.chainid, address(_token())))
);
uint256[] memory topics = _registry().topicsRegistry().getClaimTopics();
bool hasKyc;
bool hasEligibility;
for (uint256 i; i < topics.length; ++i) {
if (topics[i] == kyc) hasKyc = true;
if (topics[i] == eligibility) hasEligibility = true;
}
return hasKyc && hasEligibility;
}
The eligibility topic incorporates block.chainid and address(_token()). A credential issued for Asset A cannot be spoofed to trade Asset B.
3. Subscription & Issuance Workflow: Dual-Operator Gateways
In rwa-server/src/modules/subscription/service.ts:L891-L1059, the subscription and minting workflow is implemented as a strict finite state machine (FSM).
Database Schema for Subscriptions and Allocations
From rwa-server/src/migrations/1900000004000-SubscriptionMint.ts:L1-L38:
CREATE TABLE rwa_subscription (
id CHAR(36) PRIMARY KEY,
organization_id CHAR(36) NOT NULL,
offering_id CHAR(36) NOT NULL,
investor_id CHAR(36) NOT NULL,
wallet_id CHAR(36) NOT NULL,
business_key BINARY(32) NOT NULL,
token_amount BINARY(32) NOT NULL,
amount_snapshot JSON NOT NULL,
quote_id CHAR(36) NOT NULL,
terms_version_id CHAR(36) NOT NULL,
accepted_document_snapshot JSON NOT NULL,
eligibility_snapshot JSON NOT NULL,
policy_version_at_submit BIGINT UNSIGNED NOT NULL,
expires_at DATETIME(6) NOT NULL,
status VARCHAR(24) NOT NULL,
payment_status VARCHAR(24) NOT NULL DEFAULT 'UNPAID',
approval_id CHAR(36),
mint_operation_id CHAR(36),
minted_tx_hash BINARY(32),
completed_at DATETIME(6),
UNIQUE (organization_id, business_key),
UNIQUE (organization_id, mint_operation_id),
CHECK (status IN (
'SUBMITTED','COMPLIANCE_REVIEW','AWAITING_PAYMENT','PAYMENT_REVIEW',
'READY_TO_MINT','MINTING','COMPLETED','ACTION_REQUIRED','CANCELLED',
'EXPIRED','REJECTED','REFUND_PENDING','REFUNDED'
)),
CHECK (payment_status IN ('UNPAID','UNDER_REVIEW','CONFIRMED','REFUND_PENDING','REFUNDED'))
);
Self-Approval Prohibition (Maker-Checker Invariant)
To comply with SOC-2 and financial licensing mandates, an operator who submits a subscription payment cannot approve the subsequent minting action.
In rwa-server/src/modules/subscription/service.ts:L910-L935:
// Enforce Maker-Checker separation
check(String(ap.requester_id) !== a.id, 'SELF_APPROVAL_FORBIDDEN', 403);
if (ap.action_type === 'SUBSCRIPTION_MINT' && approve) {
check(
order.status === 'READY_TO_MINT' &&
order.approval_id === id &&
requestHash(this.mintSnapshot(order, o)) === b.requestHash,
'APPROVAL_STALE'
);
await this.assertMintable(order, m);
const operation = await this.mintIntent(m, order, ap);
await m.query(
'UPDATE rwa_subscription SET mint_operation_id=? WHERE organization_id=? AND id=?',
[operation, this.s.org, order.id]
);
await this.state(m, order, 'MINTING');
}
Once approved, the server dispatches a transaction to AgentController.sol:L268-L288:
function mint(
Request calldata r,
bytes32 subscriptionId,
address wallet,
uint256 q,
bytes32 approvalRef
) external role(ISSUANCE_OPERATOR) nonReentrant {
_begin(r, 2);
if (subscriptionId == bytes32(0) || subscriptionMinted[subscriptionId]) revert BusinessKeyAlreadyUsed();
if (approvalRef == bytes32(0) || q == 0) revert InvalidAmount();
Slot storage s = _slot(walletToSlot[wallet], r);
if (s.currentWallet != wallet) revert WalletEpochMismatch();
_accounting(s);
subscriptionMinted[subscriptionId] = true;
_context = ExecutionContext(2, address(0), wallet, q, r.operationId);
// Validates compliance rules (issuanceCap, countryMode, maxBalance)
module.validateTransfer(address(0), wallet, q);
token.mint(wallet, q);
delete _context;
_accounting(s);
emit SubscriptionMinted(subscriptionId, r.operationId, wallet, q);
}
4. Asynchronous Redemption & Settlement: Eliminating Double-Spends
Redemption is the most critical security boundary in an RWA system. The investor wants fiat bank wire settlement (USD/EUR) or stablecoins; the issuer must destroy the matching on-chain tokens.
Two-Phase Escrow Locking (AgentController.sol:L290-L386)
To eliminate the double-spend risk where an investor redeems tokens and simultaneously sells them before the bank wire clears, Aurum RWA enforces a Two-Phase Lock and Burn:
// Phase 1: Lock tokens in place via partial freeze before reviewing payment
function lockRedemption(
Request calldata r,
bytes32 redemptionId,
bytes32 slotId,
uint256 q,
bytes32 terms,
bytes calldata signature
) external role(LOCK_OPERATOR) nonReentrant {
_begin(r, 3);
Slot storage s = _slot(slotId, r);
if (token.paused()) revert TokenPaused();
if (token.isFrozen(s.currentWallet)) revert WalletFrozen();
module.checkEligible(s.currentWallet);
_free(s, q);
if (redemptionId == bytes32(0) || _redemptions[redemptionId].state != LockState.NONE) {
revert BusinessKeyAlreadyUsed();
}
if (terms == bytes32(0)) revert InvalidAmount();
// Verify investor EIP-712 signature over redemption terms
(address signer, ECDSA.RecoverError err) =
ECDSA.tryRecover(lockDigest(r, redemptionId, slotId, q, terms), signature);
if (signature.length != 65 || err != ECDSA.RecoverError.NoError || signer != s.currentWallet) {
revert InvalidInvestorSignature();
}
_redemptions[redemptionId] = Redemption(slotId, q, terms, LockState.LOCKED, r.operationId);
s.redemptionTotal += q;
// Tokens remain in investor wallet but are FROZEN on-chain (untransferable)
token.freezePartialTokens(s.currentWallet, q);
_accounting(s);
emit RedemptionLocked(redemptionId, slotId, r.operationId, s.currentWallet, q, terms);
}
// Phase 2: Burn locked tokens upon custodian payment confirmation
function burnRedemption(
Request calldata r,
bytes32 redemptionId,
bytes32 approvalRef
) external role(BURN_OPERATOR) nonReentrant {
_begin(r, 5);
if (approvalRef == bytes32(0)) revert InvalidAmount();
Redemption storage lock = _redemptions[redemptionId];
if (lock.state != LockState.LOCKED) revert LockNotActive();
Slot storage s = _slot(lock.slotId, r);
if (token.isFrozen(s.currentWallet)) revert WalletFrozen();
_accounting(s);
uint256 beforeBalance = token.balanceOf(s.currentWallet);
uint256 beforeSupply = token.totalSupply();
uint256 q = lock.quantity;
lock.state = LockState.BURNED;
s.redemptionTotal -= q;
_context = ExecutionContext(5, s.currentWallet, address(0), q, r.operationId);
token.unfreezePartialTokens(s.currentWallet, q);
token.burn(s.currentWallet, q);
delete _context;
_accounting(s);
if (token.balanceOf(s.currentWallet) != beforeBalance - q || token.totalSupply() != beforeSupply - q) {
revert InvalidAmount();
}
emit RedemptionBurned(redemptionId, lock.slotId, r.operationId, s.currentWallet, q);
}
Why Partial Freezing Outperforms Escrow Deposits
Many naive protocols require users to transfer() tokens to an escrow contract during redemption. This creates a tax event in many jurisdictions (beneficial ownership transfer) and breaks cap-table reporting during the pending settlement window.
By utilizing token.freezePartialTokens(wallet, amount):
- Beneficial ownership never leaves the investor’s wallet.
- The compliance module’s
_freezeCheckensures thatavailableBalance = balanceOf - frozenTokens >= transferAmount. Any attempt by the investor to transfer or sell the locked tokens reverts on-chain. - If the wire transfer fails or is rejected,
releaseRedemptionunfreezes the tokens back to the investor without requiring re-minting.
5. Regulatory Recovery and Forced Transfers
In permissionless DeFi, losing a private key means losing your assets permanently. In registered securities, institutional investors cannot legally accept permanent forfeiture due to key loss, hardware failure, or court-ordered forfeiture.
Under ERC-3643, authorized compliance officers execute recovery routines:
forcedTransfer: In case of legal sanctions, AML freezing, or court judgments, the compliance agent transfers tokens from the non-compliant address to an escrow address without requiring the sender’s private key signature.recoveryAddress: If an institutional investor proves key loss via legal documentation, the old wallet address is retired in the Identity Registry, and the exact token balance is burned from the compromised address and re-minted to the verified replacement address.
In rwa-contracts/src/RwaComplianceModuleCore.sol:L298-L315, recovery transfers execute under special execution context (action = 10):
if (_context(10, from, to, q)) {
if (
!ready || !suiteIntact() || walletIdentity[to] != address(0) || walletBalance[to] != 0
|| q != walletBalance[from]
) revert UnsafeRecoveryTarget();
_mapped(from);
_sync(to, walletIdentity[from]);
checkEligible(to);
}
The recovery destination wallet must be freshly initialized (walletBalance[to] == 0) and mapped to the same underlying legal identity, preventing identity confusion or unauthorized share diversion.
6. Concrete Verification: Foundry Test Suite
The enforcement of these compliance boundaries is verified through Foundry suites in rwa-contracts/test/Core.t.sol:
End-to-End Mint, Transfer, Lock, and Burn
Verifies that tokens can only be transferred between eligible addresses, that partial freezing blocks secondary transfer, and that burning decrements total supply:
function testEndToEndMintTransferRedemptionBurn() public {
_mint(alice, 100);
bob = vm.addr(0xB02);
bobId = _investor(bob, "e2e-b-after-mint", 826);
_transfer(alice, bob, 10);
_transfer(bob, alice, 10);
// Unverified transfer fails
vm.expectRevert();
vm.prank(alice);
token.transfer(charlie, 1);
// Lock 30 tokens for redemption
bytes32 order = _lock(alice, ALICE_KEY, 30);
_frozen(alice, 30);
// Cannot transfer frozen balance (71 > 100 - 30)
vm.expectRevert();
vm.prank(alice);
token.transfer(bob, 71);
// Exact free amount transfers successfully
_transfer(alice, bob, 70);
_transfer(bob, alice, 70);
// Burn locked tokens
controller.burnRedemption(_req(alice), order, REF);
require(token.balanceOf(alice) == 70 && token.totalSupply() == 70, "burn result");
require(controller.getRedemption(order).state == AgentController.LockState.BURNED, "completed");
_frozen(alice, 0);
}
Multi-Wallet Holding Limit Enforcement
Verifies that an investor cannot bypass the single-investor holding ceiling by splitting balances across multiple wallets:
function testMultiWalletCannotBypassMaxHolding() public {
controller.registerWallet(_req(address(0)), alice2, address(aliceId), 840);
_mint(alice, 1000);
_transfer(alice, alice2, 500);
// Minting additional tokens to wallet 2 reverts because aggregate identity balance exceeds cap
AgentController.Request memory r = _req(alice2);
vm.expectRevert(HoldingLimitExceeded.selector);
controller.mint(r, r.operationId, alice2, 1, REF);
// Secondary transfer from another user also reverts
_mint(bob, 1);
vm.expectRevert();
vm.prank(bob);
token.transfer(alice2, 1);
}
7. Architectural Comparison: RWA Implementation Models
| Evaluation Dimension | Plain ERC-20 + Blacklist | Centralized Off-Chain DB | ERC-3643 + Aurum RWA Architecture |
|---|---|---|---|
| Secondary Transfer Control | Reactive only (Too late) | Centralized, no DeFi composability | Proactive on-chain verification hook |
| Investor Identity Mapping | Single address assumption | Web2 user table | ONCHAINID with multi-wallet aggregation |
| Max Investor Cap Accounting | Address count (Sybil vulnerable) | Database query | On-chain entity count (investorCount) |
| Redemption Double-Spend Protection | Trust-based / escrow deposit | Manual hold | Two-phase EIP-712 partial freeze & burn |
| Jurisdiction Filtering | Manual geographic IP blocks | GeoIP | On-chain country bitmask allow/blocklist |
| Court-Ordered Forced Recovery | None (Permanent asset loss) | Database row update | Cryptographically audited forcedTransfer |
| Maker-Checker Governance | Single private key admin | Basic CRM roles | SOC-2 compliant dual-operator API FSM |
8. Integration & Commercial Next Steps
Tokenizing real-world financial assets demands far more than writing a smart contract. It requires an integrated operational system that binds institutional identity verification, immutable transfer compliance, and two-phase cash redemption into a unified audit trail.
The architecture outlined here—combining ERC-3643 on-chain hooks, multi-wallet entity counting, and dual-operator NestJS workflows—powers Aurum RWA.
Recommended Implementation Roadmap:
- Define Legal & Jurisdiction Rule Sets: Determine the specific SEC or regional regulatory exemptions (Reg D 506(c), Reg S, MiCA) applicable to your asset class.
- Review Asset Solutions & Workflows: Explore our companion analysis on Modeling RWA Redemption and Offchain Settlement and Designing Transfer Eligibility Checks for an RWA App.
- Explore the Working Infrastructure: Review the deployment specifications and verified workflows on the Aurum RWA Product Page or explore our Asset Solutions Overview.
- Schedule an Institutional Walkthrough: If your organization is tokenizing real estate funds, private debt facilities, or treasury instruments, book a technical demo walkthrough or request an institutional implementation proposal.