Complete contract dependency graph, granular freeze manager, custodian wallet management, mint/escrow, depositary separation, circuit breakers, oracle, settlement, security, data model, middleware, upgradeability, and monitoring.
Tokenize bridges traditional banking infrastructure with blockchain technology through a layered architecture.
Think of it like this: your bank already has systems (SAP, Oracle) that manage your accounts. Tokenize adds a new "layer" on top that talks to these existing systems. Your money still sits in traditional bank accounts, but now you can also use blockchain to move value faster and cheaper. It's not replacing your bank — it's making your bank's services work better.
SAP S/4HANA or Oracle Financials — handles traditional EUR deposits and customer records
SWIFT is the messaging network, not a payment rail. EUR settlement goes via TARGET2 (central bank) or STEP2 (EBA Clearing). Flow: SAP/Oracle → SWIFT message (pacs.008) → correspondent bank → TARGET2 settlement → recipient bank.
European payment infrastructure for EUR transfers — fallback for EU corridors
TraditionalKYC provider (e.g., Sumsub, Jumio) — performs initial identity verification
Node.js service that bridges off-chain data with on-chain contracts
Updates NAV based on underlying asset performance and fund valuations
Automates yield accrual and distribution to vault participants
7 deployed contracts handling payments, compliance, and vault operations
Legacy Core
Middleware
Smart Contracts
USDC deposits
Data transformation
On-chain settlement
USDC deposits
Data transformation
On-chain settlement
SAP S/4HANA and Oracle Financials are bank-internal enterprise systems. They live entirely on the bank's private infrastructure — never on the blockchain, never exposed to public networks. The diagram below shows the three-layer architecture:
Key Principle: SAP/Oracle receives payment status updates via the Middleware Service REST API. The legacy system creates ledger entries for accounting reconciliation, but it never directly interacts with the blockchain. The Middleware Service is the sole bridge — it listens to on-chain events (via WebSocket/JSON-RPC) and pushes updates to SAP/Oracle, and it receives payment instructions from SAP/Oracle and submits them to the CBPR contract.
Cross-border payments carry far more than just sender, recipient, and amount. They include invoice references, purpose of payment, HSN/tax codes, country of origin, remittance information, and regulatory data. Here's how the industry handles this:
SWIFT MT103 (single customer credit transfer) uses predefined text fields. The most important for remittance data:
Limited: 140 chars total in field :70: is insufficient for rich structured data (invoice PDFs, multiple line items, tax breakdowns).
ISO 20022 is the new global standard replacing SWIFT MT messages. Under the SWIFT CBPR+ programme, MT messages were retired for cross-border payments in November 2025. The pacs.008.001.08 message (Customer Credit Transfer) supports rich structured data with dedicated fields:
<Document>
<DocumentType>pacs.008.001.08</DocumentType>
<FIToFICstmrCdtTrf>
<GrpHdr>
<MsgId>BANK20241215001</MsgId>
<NbOfTxs>1</NbOfTxs>
<InstgAgt>...</InstgAgt>
</GrpHdr>
<CdtTrfTxInf>
<PmtId>
<InstrId>PO-2024-0415</InstrId>
<EndToEndId>INV-2024-001</EndToEndId>
</PmtId>
<Amt>
<InstdAmt Ccy="USD">1000.00</InstdAmt>
</Amt>
<XpctdExecDt>2024-12-15</XpctdExecDt>
<RmtInf>
<Strd>
<CdtrRefInf>
<Tp>
<CdOrPrtry>
<Cd>INVX</Cd>
</CdOrPrtry>
</Tp>
<Ref>INV-2024-001</Ref>
</CdtrRefInf>
<AddtlRmtInf>
Goods shipped 2024-12-01. HSN Code: 8517.70.
Country of origin: China. Tax ID: DE123456789.
PO Reference: PO-2024-0415. Net USD 1000.00.
</AddtlRmtInf>
</Strd>
</RmtInf>
<DbtrAgt>...</DbtrAgt>
<Dbtr>
<Nm>Alice Corp Inc</Nm>
<PstlAdr>...</PstlAdr>
</Dbtr>
<CdtrAgt>...</CdtrAgt>
<Cdtr>
<Nm>Bob GmbH</Nm>
<PstlAdr>...</PstlAdr>
</Cdtr>
</CdtTrfTxInf>
</FIToFICstmrCdtTrf>
</Document>
Advantage: Structured fields for invoice refs, HSN codes, country of origin, tax IDs, multiple line items. No character limits on structured data. This is what the Middleware Service receives from SAP/Oracle and what it maps to the on-chain transaction.
The industry standard across Ripple, Stellar, SWIFT GPI, and permissioned blockchains is: store a hash on-chain, store the full structured data off-chain. This pattern provides:
For B2B cross-border payments, both sender and recipient are authorized parties. The payment data is not confidential between them — both need full visibility. The hash on-chain provides integrity verification (nobody can modify the data after submission), while the off-chain storage provides richness (full structured data).
Storage options: IPFS (decentralized), AWS S3 (centralized), or the bank's own database via the Middleware Service. The dataCID (Content Identifier) or URL points to where the full data lives.
Only the hash needs to be on-chain. The full data is delivered through normal banking channels (SWIFT ISO 20022). The hash serves as a tamper-proof receipt:
// === NEW: Data hash field (add to Payment struct) ===
struct Payment {
bytes32 txId;
address sender;
address recipient;
uint256 amount;
uint256 timestamp;
uint256 corridorId;
PaymentStatus status;
// --- NEW FIELD ---
bytes32 dataHash; // keccak256 of ISO 20022 XML payload
}
// === NEW: Event for hash storage ===
event PaymentDataHashStored(
bytes32 indexed txId,
bytes32 dataHash,
uint256 timestamp
);
// === NEW: Function to store the data hash ===
// Uses Gnosis Safe multi-sig (3-of-5) for governance
function storePaymentDataHash(
bytes32 txId,
bytes32 _dataHash
) external only(GnosisSafe(address(0x1))) {
require(payments[txId].status == PaymentStatus.PENDING, "INVALID_TX");
payments[txId].dataHash = _dataHash;
emit PaymentDataHashStored(txId, _dataHash, block.timestamp);
}
// === Settlement requires multi-sig ===
function confirmSettlement(bytes32 txId, PaymentStatus status) external {
require(GnosisSafe(safeAddress).getThreshold() >= 3, "NEED_3_OF_5");
require(payments[txId].status == PaymentStatus.PENDING, "INVALID_TX");
payments[txId].status = status;
emit SettlementConfirmed(txId, status);
}
Why only a hash? The full ISO 20022 XML is delivered via SWIFT (the normal banking channel). Storing the hash on-chain creates a tamper-proof receipt that Bob's bank can use to verify the SWIFT message hasn't been altered. No IPFS, no S3, no off-chain storage needed on-chain.
This is the architecture you need to understand. One payment uses two completely separate channels simultaneously. They share the same txId as the link between them.
0x9ABC...DEF0)BIC is stored in the IdentityRegistry during KYC registration. When Bob's bank registers him, they store his BIC alongside his wallet address. Alice's frontend looks it up before initiating the payment.
// IdentityRegistry.sol — BIC stored during KYC
struct Identity {
bool isVerified;
string accreditationLevel;
string jurisdiction;
string bic; // Bob's SWIFT/BIC code
uint256 lastUpdated;
}
// When Bob registers, his bank calls:
// verifyIdentity(Bob, "Accredited", "EU", "DEUTDEFF")
// Alice's frontend looks up Bob's BIC by wallet address:
const identity = await identityRegistry.getIdentity(BobWallet);
const bic = identity.bic; // "DEUTDEFF"
// Then includes BIC in the SWIFT message as the routing address
The txId is the same identifier in both channels. It's what connects the SWIFT message to the on-chain escrow.
txId.
txId is included in the SWIFT message's EndToEndId field.
txId is the keccak256 hash of the payment parameters.
keccak256(SWIFT_XML) and stores it on-chain via storePaymentDataHash(txId, hash).
keccak256(received_SWIFT_XML) and read the hash from the CBPR contract using the txId from the SWIFT message. If they match, the data is authentic.
Key insight:
The blockchain wallet address and the SWIFT banking address are completely separate. Alice enters Bob's wallet address (blockchain) and the USDC amount. Her bank sends SWIFT to Bob's bank BIC (retrieved from IdentityRegistry). The blockchain is just an escrow + hash verification layer. The txId links both channels together. This is the same pattern used by SWIFT GPI and Ripple — parallel channels, not replacement.
The FATF Travel Rule (Recommendation 16) requires banks to exchange sender/recipient information for cross-border payments. This is not optional — it's a legal requirement enforced by regulators worldwide. Banks that don't comply face massive fines (e.g., Danske Bank paid €500M+ for AML failures).
| Jurisdiction | Threshold | Regulation | Effective |
|---|---|---|---|
| European Union | All amounts (€0+) | EU TFR 2023/1113 | December 2024 |
| United States — Bank Wires | $3,000+ | FinCEN Bank Secrecy Act | Ongoing |
| United States — VASPs | $1,000+ | FinCEN VASP Travel Rule | Ongoing |
| FATF Recommendation 16 | $1,000+ (guidance) | Non-binding baseline | Varies |
IdentityRegistry during KYC (name, address, LEI, BIC)
For EU domestic payments, banks use SEPA Instant (not SWIFT). SEPA Instant provides 24/7 settlement in under 10 seconds, with a €100,000 limit per transaction. It's the standard for EU domestic payments and is required by EU regulation.
Tokenize uses SEPA Instant for EU→EU payments (fast, cheap). For cross-border (e.g., US↔EU), we use SWIFT pacs.008 with blockchain hash verification.
Eliminates FX risk for EUR-denominated cross-border payments. Circle's EURC (Euro-pegged USDC) allows USDC holders to receive EURC directly on-chain, eliminating the need for FX conversion. The recipient's bank can then redeem EURC for EUR via SEPA Instant.
True cross-border banking requires fiat-to-fiat conversion — not USDC-to-USDC. Bob's European supplier wants EUR in their bank account, not USDC. The PvP atomic swap ensures Alice's USDC is atomically swapped for a Euro stablecoin (e.g., EURC) via an institutional AMM before settling into Bob's wallet.
Why this matters for banks: Traditional correspondent banking uses Nostro/Vostro accounts in multiple currencies. PvP eliminates the need for pre-funded Nostro accounts — the FX and settlement happen atomically, freeing up billions in trapped capital (the "pre-financing" problem that SWIFT estimates costs banks $100B+ annually).
What happens if compliance check fails mid-flight? The CBPR contract includes a reversal path: if settlement is not confirmed within the challenge period (e.g., 24 hours), the Settlement Manager (or multi-sig) can call cancelPayment(txId) to reverse the escrow and return USDC to the sender.
The Middleware Service converts the ISO 20022 XML into JSON for off-chain storage. The JSON hash is stored on-chain. Here's what the full payload looks like:
{
"document": "pacs.008.001.08",
"messageId": "BANK20241215001",
"instructionId": "PO-2024-0415",
"endToEndId": "INV-2024-001",
"amount": {
"value": 1000.00,
"currency": "USD"
},
"expectedExecutionDate": "2024-12-15",
"debtor": {
"name": "Alice Corp Inc",
"address": {
"street": "123 Wall Street",
"city": "New York",
"state": "NY",
"postalCode": "10005",
"country": "US"
},
"account": {
"routingNumber": "021000021",
"accountNumber": "123456789",
"bank": "JPMorgan Chase"
}
},
"creditor": {
"name": "Bob GmbH",
"address": {
"street": "456 Friedrichstrasse",
"city": "Berlin",
"postalCode": "10117",
"country": "DE"
},
"account": {
"iban": "DE89370400440532013000",
"bank": "Deutsche Bank AG"
}
},
"remittance": {
"invoiceNumber": "INV-2024-001",
"purchaseOrder": "PO-2024-0415",
"purposeCode": "CBFF"
},
"lineItems": [
{
"description": "Electronic Components - IC Chips",
"quantity": 100,
"unitPrice": 8.50,
"total": 850.00,
"hsnCode": "8542.31",
"countryOfOrigin": "CN"
},
{
"description": "Shipping & Handling",
"quantity": 1,
"unitPrice": 150.00,
"total": 150.00,
"hsnCode": "9999.99",
"countryOfOrigin": "US"
}
],
"tax": {
"type": "VAT",
"rate": 0.19,
"amount": 190.00,
"taxId": "DE123456789"
},
"regulatory": {
"purposeOfPayment": "Payment for imported goods Q4 2024",
"currencyOfAccount": "USD",
"currencyOfSettlement": "USD",
"localInstrument": "INST",
"categoryPurpose": "TRAD"
}
}
Hash computation:
The Middleware Service computes keccak256(canonical_ISO_20022_XML) to get the 32-byte hash, then calls storePaymentDataHash(txId, hash) on the CBPR contract. Bob's bank receives the XML via SWIFT, normalizes it to the canonical form (same serialization spec), computes the same hash, and compares it to the on-chain value to verify integrity.
Why canonical XML (not JSON)? Bob's bank receives ISO 20022 XML via SWIFT — not JSON. Both parties must hash the same byte sequence. The canonical form is defined by the ISO 20022 XML Canonicalization (xcan) spec: deterministic whitespace, sorted attributes, UTF-8 encoding. The JSON payload shown above is for developer readability only — in production, the hash is computed over the canonical XML string that Bob's bank also receives.
The bank's ERP system creates the full payment instruction with all structured data (invoice refs, line items, tax, regulatory fields).
SAP calls POST /api/payments/initiate with the full ISO 20022 payload. Middleware validates format and maps to on-chain parameters.
Middleware computes keccak256(ISO_20022_XML) to get the data hash. The full XML will be delivered via SWIFT (normal banking channel). The hash is what gets stored on-chain.
Transaction submitted with sender, recipient, amount, corridor. Compliance and corridor validation happen in the contract.
Stores the pre-computed hash on-chain. PaymentDataHashStored event emitted. This is the tamper-proof receipt.
The full payment message (with all invoice details, line items, tax data) is delivered through the SWIFT network in standard pacs.008 format. This is how banks have always exchanged payment data.
Bob's bank computes keccak256(received_SWIFT_XML) and compares it to the hash on the CBPR contract. If they match, the data is authentic — Alice's bank sent exactly what Bob received.
Right now, everything on the blockchain is public — anyone can see how much money you have and where it goes. Banks can't work with that. So for production, banks use two tricks: (1) "Zero-Knowledge Proofs" — like proving you're over 18 without showing your ID, or (2) a "private blockchain" — like a club-only version of blockchain where only invited members can see the transactions. Both keep your financial data private while still letting regulators verify everything.
Public blockchains like Sepolia are unsuitable for production banking because transaction graphs, wallet balances, and asset movements are completely public. Banks require either cryptographic privacy or network-level privacy.
ZKPs allow the platform to prove compliance without revealing transaction details. A ZK-Rollup batches transactions off-chain and posts only a cryptographic proof on-chain — proving that all transactions were valid without exposing amounts, counterparties, or purposes.
Regulators receive a "view key" that allows them to decrypt specific transactions for audit purposes — similar to how tax authorities can access bank records with proper authorization. The proof mechanism itself remains verifiable by anyone.
Prover: circom / Semaphore / aztec-connect for ZK-SNARK generation
Rollup: zkSync / Scroll / Polygon zkEVM for transaction batching
Verifier: On-chain verifier contract on Ethereum mainnet (L1)
Instead of a public chain, banks use permissioned EVM-compatible networks where every participant is KYC'd. Only authorized nodes can read/write transactions. This is how real banking consortiums operate — think of it as a "private Ethereum" where only invited banks participate.
Enterprise Ethereum variant used by JPMorgan (Onyx), Goldman Sachs, and ING. Features: private transactions via Tessera, RBAC, validator membership management.
Enterprise Ethereum with implicit state privacy. Used by central banks for CBDC pilots. Features: block encryption, private contracts, restricted block producers.
Customizable L2 chains with configurable privacy. Banks can deploy their own chain with custom consensus, privacy rules, and validator sets.
For a production banking platform, the recommended approach combines both:
Deploy on Hyperledger Besu or Quorum. All participants are pre-approved. Transactions are private by default. Best for intra-bank operations and known correspondent banking relationships. Adopted by: JPMorgan Onyx, Project Guardian (HKMA), Project Agorá (BNP Paribas).
Batch transactions on a ZK-Rollup, post proofs to Ethereum mainnet. Balances are private, compliance proofs are public. Best for cross-border payments where counterparties may not trust each other but need regulatory assurance. Adopted by: Central Bank Digital Currency (CBDC) pilots, institutional DeFi protocols.
Different banks use different blockchains — some use Ethereum, some use private networks. Chainlink CCIP is like a universal translator that lets these different blockchains talk to each other safely. It's like how international wire transfers work: your bank (on one system) sends money through a network that connects to your friend's bank (on a different system), and everything arrives correctly.
Banks will not agree on a single blockchain. The future is multi-chain — tokenized deposits on one chain, mutual funds on another, and cross-border payments bridging them. Chainlink CCIP provides secure, verifiable cross-chain messaging.
Customer deposits EUR on Ethereum L2 (e.g., Base). EUR tokens minted via MintEscrow.
Chainlink CCIP locks EUR tokens on L2 and mints wrapped representation on bank's permissioned chain.
Wrapped tokens used to purchase TMMF/TMF on the bank's consortium chain (Hyperledger Besu).
Reverse flow: redeem fund shares → burn wrapped tokens → CCIP unlocks EUR tokens on L2 → customer redeems EUR.
When a bank receives USDC that turns out to be non-compliant (e.g., from a sanctioned address), they can't just freeze the entire wallet — that would trap all legitimate funds too. Instead, only the bad amount gets frozen, like putting a lien on a specific portion of a bank account. The rest stays usable. This is how Notabene handles it in production.
Traditional token contracts only support binary freeze states: a wallet is either active or frozen. For banking, this is insufficient. A bank needs to freeze specific amounts from specific sources while keeping the rest of the balance liquid — this is the granular freeze pattern.
All outgoing transfers blocked. All incoming transfers blocked. Entire balance locked. No distinction between compliant and non-compliant funds.
Bank receives €50,000 compliant USDC + €500 non-compliant USDC from sanctioned address. Freezing the wallet locks €50,500 — the bank's own legitimate €50,000 is trapped.
Bank cannot process payroll, cannot settle trades, cannot serve customers. Regulatory compliance destroys business continuity. This is why binary freeze is unacceptable for banking.
Only €500 from the sanctioned source is frozen. The bank's own €50,000 remains fully liquid. Each frozen amount is tracked with source, reason, and timestamp.
On receiving non-compliant USDC, the FreezeManager creates a freezeEntry: {amount: 500, source: 0xSanctioned, reason: "AML_flag", timestamp: 1700000000}. The token's _updateAllowance is overridden to deduct frozen amounts from spendable balance.
When compliance team approves: unfreeze(amount, entryId). Or when funds are confiscated: confiscate(entryId, toAddress). Balance is returned to spendable or transferred to regulatory authority.
// Freeze entry — tracks individual frozen amounts
struct FreezeEntry {
uint256 amount; // Amount frozen (in smallest unit)
address source; // Source address of frozen funds
FreezeReason reason; // AML_flag, court_order, regulatory_hold
uint256 timestamp; // When freeze was applied
bytes32 referenceId; // External reference (court order #, etc.)
bool active; // Can be deactivated
}
enum FreezeReason {
AML_flag, // Automated AML alert
court_order, // Legal order to freeze
regulatory_hold, // Regulatory investigation hold
sanctions_match, // OFAC/EU sanctions list match
disputed_transaction // Payment dispute under investigation
}
// Apply granular freeze — called by compliance admin
function freeze(address target, uint256 amount, FreezeReason reason, bytes32 refId) external onlyFreezeAdmin;
// Release frozen amount back to spendable balance
function unfreeze(address target, uint256 entryId) external onlyFreezeAdmin;
// Confiscate frozen funds to regulatory address
function confiscate(uint256 entryId, address to) external onlyFreezeAdmin;
// Override transfer to deduct frozen amounts
function _updateAllowance(address owner, address spender, uint256 newAllowance, int256 delta) internal override {
uint256 frozen = totalFrozen(target);
// Spendable = balance - frozen
uint256 spendable = IERC20(target).balanceOf(target) - frozen;
require(spendable >= delta, "Insufficient spendable balance");
super._updateAllowance(owner, spender, newAllowance, delta);
}
// Query spendable balance (what matters for banking)
function spendableBalance(address target) external view returns (uint256) {
return IERC20(target).balanceOf(target) - totalFrozen(target);
}
// PermissionedToken inherits from ERC-3643 + FreezeManager
contract PermissionedToken is ERC3643, FreezeManager {
// Transfers only allowed between verified identities
// AND with sufficient spendable (non-frozen) balance
function _update(address from, address to, uint256 amount) internal override {
// 1. Check ERC-3643 identity verification
require(isVerified[from], "Sender not verified");
require(isVerified[to], "Recipient not verified");
// 2. Check granular freeze — spendable balance
require(spendableBalance(from) >= amount, "Insufficient spendable balance");
// 3. Check compliance (sanctions, whitelist)
require(complianceCheck(from, to, amount), "Compliance check failed");
super._update(from, to, amount);
}
}
Banks don't hold crypto keys themselves — they use professional custodians (like BNY Mellon, Copper, or Fireblocks) who manage keys using advanced security (MPC, HSM). Think of it like a safe deposit box: the custodian has the keys, but multiple people need to agree to open it, and every action is recorded. For the Tokenize platform, the custodian holds the actual USDC/EUR, while smart contracts control when and how funds move.
In a banking-grade tokenization platform, wallet management is separated into distinct layers: custody (holding keys), execution (signing transactions), and governance (approving actions). This separation ensures no single point of failure and provides proper audit trails.
// Custodian flow:
// 1. Smart contract emits TransferApproved event
// 2. Custodian API receives webhook
// 3. Custodian validates against internal policy engine
// 4. If approved, custodian signs transaction with HSM/MPC
// 5. Signed TX broadcast to network
// Middleware polls custodian API for execution status
const status = await custodianAPI.getTransaction(txHash);
if (status.executed) {
// Update on-chain state
await cbprContract.confirmExecution(txHash);
}
Gnosis Safe (5-of-7) for large transfers. Required for amounts above threshold. Signers: CFO, CTO, Compliance Officer, Risk Manager, + 2 external.
Automated rules: daily transfer limits, whitelist-only recipients, time-locks for large transfers, geographic restrictions, counterparty limits.
Every action logged: who approved, when, why, amount, recipient. Immutable log stored both on-chain (event) and off-chain (SIEM system).
┌─────────────┐ ┌──────────────┐ ┌───────────────┐ ┌─────────────┐
│ Bank App │ │ Middleware │ │ Custodian │ │ Blockchain │
│ (UI) │ │ (Node.js) │ │ API │ │ (EVM) │
└──────┬──────┘ └──────┬───────┘ └───────┬───────┘ └──────┬──────┘
│ │ │ │
│ 1. Initiate │ │ │
│ transfer │ │ │
│──────────────────>│ │ │
│ │ 2. Validate │ │
│ │ policy │ │
│ │────────────────────>│ │
│ │ │ 3. Check policy │
│ │ │ + AML screening│
│ │ │<───────────────────│
│ │ │ (read-only) │
│ │ 4. Policy OK │ │
│ │<────────────────────│ │
│ │ 5. Request sign │ │
│ │────────────────────>│ │
│ │ │ 6. Sign with HSM │
│ │ │ (key never │
│ │ │ leaves HSM) │
│ │ │ 7. Return sig │
│ │<────────────────────│ │
│ │ 8. Build + send TX │ │
│ │────────────────────────────────────────>│
│ │ │ │ 9. Transfer
│ │ │ │ on-chain
│ │ │ │ 10. Emit
│ │ │ │ event
│ │ 11. Poll for │ │
│ │ confirmation │ │
│ │<────────────────────────────────────────│
│ │ 12. Update status │ │
│<──────────────────│ │ │
│ 13. Show done │ │ │
Tokens can only be minted when a bank confirms that real euros are deposited in a segregated account. The Mint/Escrow contract acts as a gatekeeper: it verifies that (1) the depositor is KYC'd, (2) the deposit is confirmed by the bank's custodian, and (3) the amount matches. Only then does it allow new tokens to be created. This 1:1 backstopping is what makes tokenization trustworthy.
The mint/escrow pattern ensures that token supply always corresponds to actual fiat deposits. This is the core mechanism that makes tokenized deposits and tokenized funds truly backed — not speculative or unbacked. The escrow also handles the reverse: burning tokens to release fiat.
Customer transfers EUR to the bank's segregated custodial account at a correspondent bank. The bank's core system records the deposit.
Bank's middleware receives confirmation that funds are received and settled in the segregated account. Deposit reference is generated.
Contract verifies: (a) caller is authorized mint operator, (b) receiver is KYC'd via IdentityRegistry, (c) depositRef matches on-chain record.
_mint(tokenReceiver, amount) called. Minted event emitted with deposit reference for audit trail.
Customer initiates redemption via bank app, specifying amount and destination bank account (must be verified same owner).
Contract burns tokens from holder, verifies they are the token owner, and records the payout reference for fiat disbursement.
Bank's treasury system receives burn confirmation and initiates SEPA transfer from segregated account to customer's verified bank account.
SEPA confirmation received. Middleware updates burn record on-chain: BurnConfirmed(burnId, payoutRef, timestamp).
contract MintEscrow {
IERC3643 public token;
IdentityRegistry public identityRegistry;
address public depositary; // The entity holding fiat
struct DepositRecord {
string depositRef; // Bank's internal reference
address depositor; // Who deposited
uint256 amount; // Amount in tokens
uint256 timestamp; // When recorded
DepositStatus status; // Pending, Confirmed, Reversed
}
enum DepositStatus { Pending, Confirmed, Reversed }
// Mint — only callable by authorized operator after fiat confirmation
function mint(address receiver, uint256 amount, string calldata depositRef) external onlyMintOperator {
require(identityRegistry.isVerified(receiver), "Receiver not KYC'd");
require(amount > 0, "Must mint > 0");
// Record deposit reference for audit
depositRecords[depositRef] = DepositRecord({
depositor: receiver,
amount: amount,
timestamp: block.timestamp,
status: DepositStatus.Confirmed
});
token.mint(receiver, amount);
emit Minted(receiver, amount, depositRef);
}
// Burn — tokens destroyed, fiat released
function burn(address holder, uint256 amount, string calldata payoutRef) external {
require(holder == msg.sender || token.isApprovedForAll(holder, msg.sender), "Not authorized");
require(token.balanceOf(holder) >= amount, "Insufficient balance");
token.burn(holder, amount);
burnRecords[payoutRef] = BurnRecord({
burner: holder,
amount: amount,
timestamp: block.timestamp,
status: BurnStatus.Released
});
emit Burned(holder, amount, payoutRef);
}
// Audit: verify total supply matches total deposits
function verifyBacked() external view returns (bool) {
return token.totalSupply() == totalFiatDeposited();
}
}
In traditional banking, the entity that holds your money is not the same as the entity that manages your account. Your money sits at a custodian bank (like BNY Mellon or a central bank), while your retail bank manages your account. This separation is required by EU law (UCITS Directive, AIFMD). The same pattern applies to tokenization: the smart contract platform manages the tokens, but the actual fiat sits in segregated accounts at a depositary institution.
Under EU regulations (UCITS Directive 2014/91/EU, AIFMD 2011/61/EU), every tokenized fund MUST have an independent depositary (custodian) that holds the underlying assets separately from the fund manager. This ensures that even if the fund manager goes bankrupt, the underlying assets are protected and belong to token holders.
Investor/customer who holds tokenized assets. Has redemption rights.
Makes investment decisions. Manages the TMMF/TMF portfolio. Operates the smart contract platform.
Holds underlying assets in segregated accounts. Verifies fund manager actions. Independent from manager.
Independent verification of NAV, reserves, and compliance. Reports to regulators.
Under UCITS Art. 22 and AIFMD Art. 21, the depositary must hold all fund assets in segregated accounts that are clearly identified and separate from the depositary's own assets. In the event of the depositary's insolvency, these assets are ring-fenced and do not form part of the depositary's estate. This is why the MintEscrow contract tracks the depositary address — it's a regulatory requirement that must be verifiable on-chain.
Depositary's treasury system compares: (1) bank account balance in segregated account vs. (2) on-chain total supply. Any discrepancy triggers an alert.
Depositary signs a daily attestation: depositaryAttest(totalSupply, fiatBalance, timestamp). Anyone can verify the depositary has confirmed 1:1 backing.
If depositary detects mismatch, they can pause minting via depositaryPause(). Fund manager cannot override depositary pause. This is the nuclear option — used only for serious discrepancies.
contract DepositaryInterface {
// Depositary can pause minting if
// fiat balance != token supply
function depositaryPause(bool paused) external;
function isDepositaryPaused() external view returns (bool);
// Daily attestation
function attestFiatBacking(
uint256 onChainSupply,
uint256 fiatBalance,
bytes32 rootHash
) external returns (bool);
// Get last attestation
function lastAttestation() external view returns (
uint256 supply,
uint256 balance,
uint256 timestamp
);
}
// MintEscrow integrates depositary check
function mint(address receiver, uint256 amount, ...) external {
require(!depositary.isDepositaryPaused(), "Depositary paused");
require(token.totalSupply() + amount <=
depositary.getVerifiedFiatBalance(),
"Insufficient fiat backing");
// ... proceed with mint
}
Sometimes things go wrong — a smart contract bug is found, a key is compromised, or there's a market crisis. Circuit breakers are emergency stop mechanisms at different levels: you can pause individual functions, pause specific tokens, or pause the entire system. Think of it like circuit breakers in a power grid — you can trip one breaker for a single room without shutting down the whole building. Banks require multiple levels of emergency controls, each with different authorization requirements.
A well-designed circuit breaker system provides granular emergency controls without requiring a single point of failure. Different types of emergencies require different response levels — a smart contract bug needs a different response than a sanctioned address receiving funds.
What it does: Stops ALL state-changing operations across ALL contracts. No transfers, no mints, no burns, no updates.
Who can trigger: 5-of-7 multi-sig (governance council)
When to use: Critical smart contract vulnerability, major exploit in progress
Recovery: 5-of-7 multi-sig to resume. Minimum 24-hour delay for audit.
What it does: Pauses transfers for a specific token (e.g., non-compliant USDC variant)
Who can trigger: Compliance officer + depositary (2-of-3)
When to use: Specific token variant found to have compliance issues
Recovery: Compliance team verifies fix, lifts pause (2-of-3).
What it does: Freezes specific wallet addresses (granular, amount-level)
Who can trigger: Compliance officer (single, but logged)
When to use: OFAC sanctions match, court order, AML alert
Recovery: Compliance review + depositary approval to unfreeze.
What it does: Limits rate/amount of specific functions (e.g., max mint per hour)
Who can trigger: Auto-triggered by threshold, adjustable by ops team
When to use: Unusual volume spike, potential flash loan attack
Recovery: Auto-resumes after cooldown, or ops team resets.
contract CircuitBreaker {
enum BreakerLevel { NONE, GLOBAL, TOKEN, ADDRESS, FUNCTION }
mapping(bytes32 => BreakerLevel) private _breakers;
mapping(bytes32 => uint256) private _activatedAt;
// Multi-sig required for global pause
function triggerGlobalPause() external onlyGovernance {
_activateBreaker("GLOBAL", BreakerLevel.GLOBAL);
}
// Compliance can freeze addresses
function freezeAddress(address target, string calldata reason) external onlyCompliance {
bytes32 key = keccak256(abi.encodePacked("ADDRESS_", target));
_activateBreaker(key, BreakerLevel.ADDRESS);
emit AddressFrozen(target, reason, block.timestamp);
}
// Modifier for all contracts
modifier notPaused() {
require(!_isPaused("GLOBAL"), "Global pause active");
_;
}
// Auto-resume function-level throttles
function _activateBreaker(bytes32 key, BreakerLevel level) internal {
_breakers[key] = level;
_activatedAt[key] = block.timestamp;
if (level == BreakerLevel.FUNCTION) {
// Auto-resume after 1 hour
_scheduleResume(key, 1 hours);
}
emit BreakerTriggered(key, level, msg.sender, block.timestamp);
}
}
All the smart contracts on the platform form an interconnected system. Some contracts depend on others — like how the payment contract needs to check the identity registry before allowing a transfer. Understanding how these contracts relate to each other is essential for building, maintaining, and auditing the system. This section shows the complete architecture: every contract, every dependency, and every data flow.
┌─────────────────────────────────────────────────────────────────────────┐
│ TOKENIZE PLATFORM ARCHITECTURE │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────┐ │
│ │ FRONTEND (UI) │ ← React/Next.js dashboard │
│ └────────┬─────────┘ │
│ │ RPC (ethers.js/viem) │
│ ▼ │
│ ┌──────────────────┐ │
│ │ MIDDLEWARE │ ← Node.js service │
│ │ (API Gateway) │ │
│ └──┬──────────┬────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌────────┐ ┌────────┐ │
│ │ Oracle │ │Custodian│ ← Fireblocks/Copper API │
│ │(prices)│ │(keys) │ │
│ └───┬────┘ └───┬────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ BLOCKCHAIN LAYER (Smart Contracts) │ │
│ │ │ │
│ │ ┌──────────────────┐ ┌──────────────────┐ │ │
│ │ │ IdentityRegistry │◄────│ CorridorRegistry │ │ │
│ │ │ (KYC/AML) │ │ (Routing rules) │ │ │
│ │ └────────┬─────────┘ └──────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌──────────────────┐ ┌──────────────────┐ │ │
│ │ │ FreezeManager │ │ CircuitBreaker │ │ │
│ │ │ (Granular freeze)│ │ (Emergency ctrl) │ │ │
│ │ └────────┬─────────┘ └──────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────────────────────────────────────────────┐ │ │
│ │ │ Permissioned Token (ERC-3643) │ │ │
│ │ │ ┌────────────┐ ┌────────────┐ ┌──────────────────┐ │ │ │
│ │ │ │ CBPR │ │ MintEscrow │ │ TMMF/TMF Vaults │ │ │ │
│ │ │ │ (Payments) │ │ (Mint/Burn)│ │ (Fund management)│ │ │ │
│ │ │ └────────────┘ └────────────┘ └──────────────────┘ │ │ │
│ │ └─────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌──────────────────┐ ┌──────────────────┐ │ │
│ │ │ YieldEngine │ │ FundNAVTracker │ │ │
│ │ │ (Distribution) │ │ (NAV calculation)│ │ │
│ │ └──────────────────┘ └──────────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ All contracts depend on: IdentityRegistry (KYC check) │
│ Core token: Permissioned ERC-3643 with FreezeManager mixin │
│ Governance: Multi-sig (Gnosis Safe) for critical operations │
│ Depositary: External attestation of fiat backing │
└─────────────────────────────────────────────────────────────────────────┘
| Contract | Reads From | Writes To | Triggered By |
|---|---|---|---|
| CBPR | IdentityRegistry, CorridorRegistry | Events → Middleware | Middleware (user action) |
| PermissionedToken | IdentityRegistry, FreezeManager | Events → All | Any approved caller |
| MintEscrow | IdentityRegistry, PermissionedToken | PermissionedToken (mint/burn) | Custodian confirmation |
| TMMF/TMF | Oracle (NAV), FundNAVTracker | Events → Middleware | Oracle update + user actions |
| YieldEngine | TMMF/TMF vaults | TMMF/TMF (distribute) | Scheduled (oracle update) |
| FreezeManager | Token balances | Freeze entries mapping | Compliance admin |
| CircuitBreaker | Breaker state | Breaker state | Governance / auto-threshold |
For a bank team building the MVP, here's the recommended build order. Each block enables the next. Don't build everything at once — start with the core and add complexity incrementally.
Deliverable: KYC-gated token that can only be transferred between verified parties.
Deliverable: 1:1 backed token minting — fiat deposit triggers token mint, token burn releases fiat.
Deliverable: Cross-border payment rail with compliance checks, corridor validation, and hash-verified payment data.
Deliverable: Tokenized money market fund and mutual fund with oracle-driven NAV and yield distribution.
Deliverable: Granular freeze, emergency controls, sanctions screening, and complete audit trail. Production-ready compliance layer.
Smart contracts can't see the outside world on their own — they need oracles to bring in price data, NAV calculations, and other off-chain information. But you can't just trust any data source. Banks require multiple layers of verification: multiple price sources, time-weighted averages, and fallback mechanisms. Think of it like a bank's treasury department — they don't trust a single price quote, they get quotes from multiple dealers and use the median.
The oracle architecture is critical for fund valuation (TMMF/TMF NAV), FX conversion rates, and compliance checks. A compromised oracle could manipulate fund NAVs, causing massive financial harm. The architecture must provide tamper-evident, time-stamped price feeds with multiple verification layers.
// Chainlink-compatible oracle request
function requestPriceFeed(
bytes32 requestId,
string[] calldata pairs, // ["EUR/USD", "USD/EUR"]
uint32 validAfter, // Minimum timestamp
uint32 validBefore // Maximum timestamp
) external onlyAuthorizedRelayer {
// Oracle nodes fetch from multiple sources
// Aggregate using median + deviation check
// Sign result with their private key
// Submit to on-chain aggregator contract
}
// On-chain: verify oracle signature + check staleness
function submitPrice(
bytes32 requestId,
uint256 price,
uint256 timestamp,
bytes calldata signature
) external {
require(!isStale(timestamp, STALENESS_THRESHOLD), "Stale price");
require(verifyOracleSignature(requestId, price, signature), "Invalid signature");
prices[requestId] = PriceUpdate(price, timestamp);
}
Prices older than N minutes are rejected. For NAV updates: max 1 hour. For FX rates: max 5 minutes. Prevents replay of stale prices.
Prices deviating >5% from previous are rejected or require multi-sig approval. Prevents oracle manipulation attacks.
If oracle is down, circuit breaker pauses fund operations. Depositary can override with manual NAV (requires 2-of-3 approval).
┌─────────────┐ ┌──────────────┐ ┌───────────────┐ ┌─────────────┐
│ Fund Admin │ │ Oracle │ │ NAV Tracker │ │ TMMF/TMF │
│ (Off-chain)│ │ Network │ │ (On-chain) │ │ Contract │
└──────┬──────┘ └──────┬───────┘ └───────┬───────┘ └──────┬──────┘
│ │ │ │
│ 1. Calculate │ │ │
│ NAV from │ │ │
│ underlying │ │ │
│ assets │ │ │
│──────────────────>│ │ │
│ │ 2. Oracle nodes │ │
│ │ fetch + aggregate│ │
│ │<────────────────────│ │
│ │ 3. Signed price │ │
│ │────────────────────>│ │
│ │ │ 4. Verify: │
│ │ │ - signature │
│ │ │ - staleness │
│ │ │ - deviation │
│ │ │ 5. Update NAV │
│ │ │───────────────────>│
│ │ │ │ 6. Emit
│ │ │ │ NavUpdated
│ 7. Notify │ │ │
│ investors │ │ │
│<──────────────────│ │ │
contract PriceFeedOracle {
struct PriceUpdate {
uint256 price; // Price in 18 decimal places
uint256 timestamp; // When price was fetched
uint256 roundId; // Chainlink round ID
bool valid; // Is this price still valid?
}
mapping(bytes32 => PriceUpdate) public prices;
uint256 public constant STALENESS_THRESHOLD = 3600; // 1 hour
uint256 public constant DEVIATION_THRESHOLD = 5; // 5%
// Update price with verification
function updatePrice(
bytes32 pair,
uint256 price,
uint256 timestamp,
uint256 roundId
) external onlyOracleNode {
require(timestamp > block.timestamp - STALENESS_THRESHOLD, "Stale");
PriceUpdate storage prev = prices[pair];
if (prev.valid && prev.price > 0) {
uint256 deviation = abs(price - prev.price) * 100 / prev.price;
require(deviation <= DEVIATION_THRESHOLD, "Deviation too high");
}
prices[pair] = PriceUpdate(price, timestamp, roundId, true);
emit PriceUpdated(pair, price, timestamp);
}
// Get current price (reverts if stale)
function getCurrentPrice(bytes32 pair) external view returns (uint256) {
PriceUpdate memory p = prices[pair];
require(p.valid && block.timestamp - p.timestamp < STALENESS_THRESHOLD, "Stale");
return p.price;
}
// Fallback: depositary can set manual price in emergency
function setManualPrice(bytes32 pair, uint256 price) external onlyDepositary {
prices[pair] = PriceUpdate(price, block.timestamp, 0, true);
emit ManualPriceSet(pair, price, block.timestamp);
}
}
In banking, settlement means the actual movement of money. For Tokenize, there are TWO settlement legs happening: (1) the fiat settlement through traditional banking rails (SEPA, TARGET2) and (2) the crypto settlement on-chain (token transfers). The key is that these happen atomically — either both complete or neither does. This is called "Delivery vs Payment" (DvP) and is required by EU law for securities settlement.
Settlement architecture defines how fiat and crypto legs are coordinated to ensure atomic settlement. The platform must handle both PvP (Payment vs Payment) for FX swaps and DvP (Delivery vs Payment) for security settlements. Each has different timing, finality, and risk profiles.
USDC → EURC atomic swap for cross-border payments. Both legs settle on-chain simultaneously.
On-chain atomic swap using either: (a) ERC-8025 atomic transfer protocol, or (b) Chainlink CCIP for cross-chain atomic swaps. Both legs must succeed or both revert.
On-chain finality: ~12 seconds (Ethereum) or ~400ms (Base L2). No counterparty risk — atomic by design.
Tokenized fund subscription/redemption. Fiat deposit triggers token mint; token burn releases fiat.
Two-phase settlement: (1) Fiat leg via SEPA/TARGET2, (2) Crypto leg via smart contract. Coordinated by middleware with state machine tracking.
Fiat: T+0 (SEPA Instant) or T+1 (TARGET2). Crypto: immediate. Total: T+0 to T+1 depending on corridor.
enum SettlementState {
INITIATED, // 1. Payment initiated
FIAT_PENDING, // 2. Fiat leg in progress (SEPA/TARGET2)
FIAT_CONFIRMED, // 3. Fiat settled in segregated account
CRYPTO_PENDING, // 4. Crypto leg initiated
CRYPTO_CONFIRMED, // 5. Token minted/transferred
COMPLETE, // 6. Both legs settled
FAILED, // 7. Settlement failed
REFUNDED // 8. Funds returned on failure
}
// Settlement tracking contract
contract SettlementManager {
struct Settlement {
string settlementId;
SettlementState state;
address initiator;
uint256 amount;
string fiatRef; // SEPA/TARGET2 reference
string cryptoTxHash; // On-chain tx hash
uint256 timestamp;
string failureReason; // If state == FAILED
}
// Advance state through the machine
function advanceState(
string calldata settlementId,
SettlementState newState,
bytes calldata proof // Fiat confirmation or crypto tx
) external onlySettlementOperator {
Settlement storage s = settlements[settlementId];
require(canTransition(s.state, newState), "Invalid transition");
s.state = newState;
if (newState == SettlementState.CRYPTO_PENDING) {
s.cryptoTxHash = extractTxHash(proof);
}
if (newState == SettlementState.FAILED) {
s.failureReason = string(proof);
}
emit SettlementStateChanged(settlementId, newState);
}
// State transition matrix
function canTransition(SettlementState from, SettlementState to) internal pure returns (bool) {
// Define valid transitions
return _validTransitions[from][to];
}
}
// Atomic FX Swap — ERC-8025 style
contract AtomicFXSwap {
// Swap USDC → EURC atomically
function executeFXSwap(
address usdcToken,
address eurcToken,
uint256 usdcAmount,
uint256 minEurcAmount, // Slippage protection
uint256 deadline,
bytes calldata signature // Counterparty signature
) external {
// 1. Verify counterparty signed the terms
verifySignature(usdcAmount, minEurcAmount, deadline, signature);
// 2. Transfer USDC from sender (escrow)
IERC20(usdcToken).transferFrom(msg.sender, address(this), usdcAmount);
// 3. Transfer EURC from counterparty (escrow)
IERC20(eurcToken).transferFrom(
recoverCounterparty(signature),
msg.sender,
eurcAmount
);
// 4. Release USDC to counterparty
IERC20(usdcToken).transfer(
recoverCounterparty(signature),
usdcAmount
);
// All or nothing — if step 3 fails, step 2 is reverted
emit FXSwapExecuted(msg.sender, usdcAmount, eurcAmount);
}
}
Banks don't rely on a single security measure — they use multiple layers, like a castle with moats, drawbridges, and guards at every door. For Tokenize, this means: hardware security modules for keys, multi-signature approval for transactions, time-locks for large transfers, continuous monitoring for anomalies, and regular penetration testing. If one layer fails, others catch it.
Security architecture for a banking-grade tokenization platform must address: key management (HSM/MPC), access control (RBAC + multi-sig), transaction validation (policy engine), monitoring (SIEM + anomaly detection), and incident response (playbooks + automated containment).
┌─────────────────────────────────────────────────────────────────────────┐
│ SECURITY REQUEST FLOW │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ User Request │
│ └─> API Gateway (WAF + Rate Limit) │
│ └─> Middleware Service │
│ └─> Policy Engine (pre-flight) │
│ ├─ Check amount limits │
│ ├─ Check whitelist │
│ ├─ Check sanctions (Chainalysis/elliptic) │
│ └─ Check corridor rules │
│ └─> Custodian API (if crypto transfer) │
│ └─> HSM/MPC (signing) │
│ └─> Smart Contract (on-chain) │
│ └─> CircuitBreaker (is system paused?) │
│ └─> FreezeManager (is sender/recipient frozen?) │
│ └─> IdentityRegistry (is sender KYC'd?) │
│ └─> PermissionedToken (transfer) │
│ │
│ Post-Transaction: │
│ └─> Event Emission → Kafka → SIEM (Splunk/Elastic) │
│ └─> Anomaly Detection (ML model) │
│ └─> Alert if anomaly detected → SOC 24/7 │
└─────────────────────────────────────────────────────────────────────────┘
Not all data belongs on the blockchain. Storing data on-chain is expensive and public. The architecture separates data into three categories: (1) on-chain — critical state that needs to be tamper-proof and verifiable (balances, identities, freezes), (2) off-chain structured — operational data that needs to be queryable (payment details, audit logs), and (3) off-chain unstructured — documents and large data (KYC documents, contracts, invoices). Each category has different storage, access, and retention policies.
Data architecture for a tokenization platform must balance: on-chain verifiability vs. cost, privacy vs. transparency, queryability vs. decentralization, and regulatory retention requirements vs. data minimization principles (GDPR).
EVM state trie. Cost: ~20k gas per storage write. Indexed by Etherscan.
PostgreSQL + Kafka. Encrypted at rest. Accessible by middleware and authorized parties.
Encrypted S3 / Azure Blob. IPFS for document integrity (hash on-chain, content off-chain).
Payment Initiation:
┌─────────────────────────────────────────────────────────────────┐
│ Off-Chain (Structured) │
│ • Payment details stored in PostgreSQL (encrypted) │
│ • ISO 20022 XML generated and signed │
│ • keccak256(paymentXML) computed → hash │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ On-Chain │
│ • PaymentHash stored in CBPR contract (immutable) │
│ • PaymentHashStored event emitted → indexed │
│ • Balance updated, FreezeManager checked, Identity checked │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Off-Chain (Structured + Unstructured) │
│ • ISO 20022 XML sent via SWIFT to recipient bank │
│ • Recipient bank verifies hash matches on-chain │
│ • Settlement confirmation logged to PostgreSQL │
│ • Audit trail appended (append-only log) │
│ • KYC documents referenced by hash (not stored on-chain) │
└─────────────────────────────────────────────────────────────────┘
The middleware is the glue between the blockchain and the bank's existing systems. It receives requests from SAP/Oracle, validates them, talks to the smart contracts, and sends back results. It also listens for blockchain events and updates the bank's internal systems. Think of it as a translator that speaks both "bank" (REST APIs, ISO 20022) and "blockchain" (JSON-RPC, smart contracts).
Middleware architecture must handle: request/response translation, event listening and processing, policy enforcement, retry logic, idempotency, and graceful degradation when blockchain is unavailable. It is the single point of failure for off-chain operations and must be designed for high availability.
REST/GraphQL endpoints for frontend and legacy systems. Rate limiting, authentication, request validation.
Watches blockchain for events (PaymentHashStored, NavUpdated, AddressFrozen). Processes and stores off-chain.
Validates transactions against business rules: amount limits, whitelist, corridor rules, sanctions.
Prevents duplicate transactions. Tracks request IDs, ensures exactly-once semantics.
ethers.js/viem client. Manages connection pooling, gas estimation, transaction signing via custodian.
Fireblocks/Copper API client. Manages key derivation, transaction submission, status polling.
SAP/Oracle REST API client. Sends payment instructions, receives settlement confirmations.
ISO 20022 XML generation/parsing. SWIFT pacs.008 message creation and validation.
┌──────────────────────────────────────────────────────────────────────┐
│ MIDDLEWARE ARCHITECTURE │
│ │
│ ┌──────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ Frontend │───>│ API Gateway │───>│ Policy Engine │ │
│ │ (React) │ │ (Express) │ │ (Validation) │ │
│ └──────────┘ └──────┬───────┘ └────────┬─────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────────┐ │
│ │ Idempotency │ │ Blockchain │ │
│ │ Manager │ │ Adapter (ethers) │ │
│ └──────┬───────┘ └────────┬─────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌────────────────────────────────────────┐ │
│ │ EVENT LISTENER │ │
│ │ • Listens to blockchain events │ │
│ │ • Processes: PaymentHashStored, Nav │ │
│ │ Updated, AddressFrozen │ │
│ │ • Updates PostgreSQL + Kafka │ │
│ └─────────────────┬──────────────────────┘ │
│ │ │
│ ┌──────────┴──────────┐ │
│ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────────┐ │
│ │ PostgreSQL │ │ Kafka / Redpanda │ │
│ │ (encrypted) │ │ (event streaming)│ │
│ └──────────────┘ └──────────────────┘ │
│ │
│ External Integrations: │
│ • Custodian API (Fireblocks) • SAP/Oracle REST │
│ • SWIFT Network (pacs.008) • Chainlink Oracle │
└──────────────────────────────────────────────────────────────────────┘
Smart contracts are immutable — once deployed, they can't be changed. But banks need to fix bugs, add features, and comply with new regulations. The solution is a "proxy pattern": the actual logic lives in separate contracts, and a proxy contract forwards calls to them. This allows upgrading the logic without losing state. But upgrades must be governed — no single person should be able to change contracts unilaterally. Banks require multi-sig approval + time-locks for all upgrades.
Upgradeability is mandatory for production banking systems. The architecture must support: transparent upgradeable contracts (UUPS or transparent proxy), governance-controlled upgrades (multi-sig + time-lock), emergency pause (separate from upgrade), and full audit trail of all upgrades (what changed, who approved, when).
Proxy contract stores implementation address. All calls to proxy are forwarded to implementation. Upgrade = change implementation address (requires multi-sig).
Upgrade logic is IN the implementation contract. The _upgradeTo function is called by the proxy, but the implementation validates the caller has permission.
┌─────────────────────────────────────────────────────────────────┐
│ UPGRADE GOVERNANCE FLOW │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. Developer creates new implementation contract │
│ └─> Solidity compiler (version-pinned, deterministic) │
│ │
│ 2. Internal security review │
│ └─> Static analysis (Slither, MythX) │
│ └─> Manual code review (2+ reviewers) │
│ └─> Testnet deployment + integration tests │
│ │
│ 3. Governance proposal │
│ └─> Submit upgrade proposal to Gnosis Safe │
│ └─> Include: new bytecode hash, migration script, tests │
│ │
│ 4. Multi-sig approval (5-of-7) │
│ └─> Signers: CFO, CTO, Compliance, Risk, + 2 external │
│ └─> 24-hour time-lock before execution │
│ │
│ 5. Execution │
│ └─> Proxy.upgradeTo(newImplementation) │
│ └─> Upgrade event emitted → logged to SIEM │
│ └─> New implementation address stored on-chain │
│ │
│ 6. Post-upgrade verification │
│ └─> Automated tests run against new implementation │
│ └─> If failure: emergency rollback to previous version │
│ │
│ Audit Trail (immutable): │
│ • Proposal submitted (tx hash, timestamp, proposer) │
│ • Votes recorded (who approved, when) │
│ • Execution (tx hash, old impl, new impl) │
│ • Post-upgrade verification (pass/fail) │
└─────────────────────────────────────────────────────────────────┘
You can't manage what you can't measure. For a banking system, monitoring is not optional — it's a regulatory requirement. Every transaction, every contract call, every error must be logged, tracked, and alertable. The architecture uses three pillars: metrics (numbers and trends), logs (detailed event records), and traces (end-to-end request flows). If something goes wrong, you need to know immediately, understand what happened, and be able to prove it to regulators.
Monitoring architecture must provide: real-time alerting for critical events (failed transactions, oracle staleness, freeze events), historical audit trails for regulatory reporting, performance metrics for capacity planning, and anomaly detection for security incidents. All data must be tamper-evident (append-only logs with hashes).
Prometheus + Grafana for time-series metrics. Custom dashboards per team (ops, compliance, dev).
ELK Stack (Elasticsearch, Logstash, Kibana) or Splunk. Structured JSON logs with correlation IDs. 7-year retention (regulatory).
OpenTelemetry + Jaeger. Distributed tracing with W3C trace context. Correlates blockchain tx hashes with internal request IDs.
DORA Art. 18: Must be assessed against DORA major incident criteria. If classified as major, NBB notification required within 4 hours of classification (DORA Art. 19).
DORA (Digital Operational Resilience Act) applies to all financial entities in the EU from January 17, 2025. The Tokenize platform's incident classification maps directly to DORA's ICT incident taxonomy:
Practical implication: Any P1 event (global circuit breaker, oracle staleness >1 hour, balance reconciliation failure, smart contract exploit) must trigger internal escalation within 15 minutes and NBB notification within 4 hours of detection. The monitoring system must maintain an immutable audit log of all incident events for regulatory inspection.