This document explains the architecture for creating compliant RWA (Real World Asset) Index Vaults that bridge ERC-4626 and ERC-3643 standards.
You are bridging two fundamentally different standards:
- ERC-4626: Permissionless, composable yield vaults
- ERC-3643: Permissioned, identity-gated securities
To achieve an "ETF-style" vault where a single ERC-4626 token represents a basket of compliant RWAs, you must solve the Identity Proxy problem: the Vault itself must be a verified entity capable of holding the underlying regulated tokens.
Unlike a standard ERC-4626 which maps 1 Asset → 1 Vault, you need a Multi-Asset Strategy logic. Since ERC-4626 strictly requires a single asset() for accounting (usually USDC), your vault acts as a "Fund Manager" that:
- Takes USDC deposits
- Splits USDC based on portfolio weights
- Swaps USDC for ERC-3643 tokens
- Holds multiple RWA tokens
- User deposits USDC into the Vault
- Vault verifies User is compliant (optional, but recommended for RWA)
- Vault splits USDC based on a defined "Portfolio Weight" (e.g., 50% Real Estate Token A, 50% Bond Token B)
- Vault swaps USDC for the specific ERC-3643 tokens (via compliant swap or direct minting)
- Institutional Payment (Dividends) arrives at the ERC-3643 token contract → claimed by Vault → distributed to Vault users
ERC-3643 tokens check the ONCHAINID of the recipient on every transfer. If your Vault contract does not have a valid identity, all buy/sell transactions will revert.
Your Vault must possess its own ONCHAINID. You cannot simply whitelist the contract address; the standard requires an identity verification.
- Deploy a standard Identity contract (from the ONCHAINID protocol)
- Set the
managementKeyto the Vault's owner/admin multisig
- Your Vault does not "sign" transactions like a user
- Instead, the RWA issuer whitelists the Vault's Address in their
IdentityRegistryunder the Vault's ONCHAINID
- Some issuers can add your Vault to a "Marketplace Whitelist" or "Agent" list
- This allows it to hold tokens without a full KYC process, treating it as a "Qualified Custodian"
Critical Requirement: You must contact the issuers of the underlying ERC-3643 tokens. They must explicitly approve your Vault contract address to hold their tokens.
When deposit(uint256 assets, address receiver) is called:
function _invest(uint256 usdcAmount) internal {
for (uint i = 0; i < portfolio.length; i++) {
address rwaToken = portfolio[i].token;
uint256 allocation = (usdcAmount * portfolio[i].weight) / 10000;
// Swap USDC for RWA Token (or Mint)
// The Vault's address must be whitelisted to receive this transfer!
compliantSwap(usdcAsset, rwaToken, allocation);
}
}Note: If secondary markets (liquidity pools) don't exist for these RWAs, this might involve:
- Calling a
mint()function on the RWA contract (if the Vault is an authorized agent) - Swapping via a permissioned DEX
The price of your Vault Share depends on the value of the underlying RWAs. You need an Oracle or a Net Asset Value (NAV) feed, because RWA tokens often don't have real-time public spot prices like ETH.
function totalAssets() public view override returns (uint256) {
uint256 totalVal = asset.balanceOf(address(this)); // Uninvested USDC
for (uint i = 0; i < portfolio.length; i++) {
uint256 rwaBalance = IERC20(portfolio[i].token).balanceOf(address(this));
uint256 price = getPrice(portfolio[i].token);
totalVal += (rwaBalance * price) / 1e18; // Normalized to USDC decimals
}
return totalVal;
}You mentioned payments sent to the ERC-3643 are "collected from the vault and distributed."
- The Institution sends USDC (or yield token) directly to the ERC-3643 token holders
- Result: Your Vault receives USDC
- Action: The Vault sees its
asset()balance increase. ThetotalAssets()goes up - User Benefit: The share price increases automatically. No extra transaction needed
- The Institution deposits funds into a
DividendDistributorcontract linked to the ERC-3643 - Action: You need a
harvest()function on your Vault - Logic:
harvest()callsclaimDividends()on the RWA token's distributor- USDC arrives in the Vault
- Vault either auto-compounds (buys more RWA tokens) or sits on cash (increasing
totalAssets())
If you want users to receive raw dividends (income) rather than accumulating value:
- You cannot use standard ERC-4626 for this (as it is designed to accumulate)
- You would need a "Rewards" module (like Synthetix StakingRewards or a split payment splitter)
- The harvested USDC is sent to a separate contract that users claim from based on their Vault Share balance
Since the underlying assets are permissioned (ERC-3643), the Vault Shares themselves might need to be permissioned.
If a non-KYC'd user buys your Vault Shares, they technically own a claim on the RWA.
- Make your Vault Share token also an ERC-3643 or compliant ERC-20 that requires whitelisting
- If the Vault is the legal "Custodian," the users might only need to pass the Vault's specific KYC, not the underlying RWA's KYC (depending on jurisdiction)
-
PraxosVaultCompliant.sol - Main vault contract
- Extends ERC-4626
- Holds multiple ERC-3643 tokens
- Implements compliance checks
- Handles dividend harvesting
-
ICompliantStrategyAdapter.sol - Interface for compliance adapter
- Handles whitelisting
- Performs compliant swaps
- Manages ONCHAINID registration
-
IPriceOracle.sol - Interface for price feeds
- Gets RWA token prices
- Used in
totalAssets()calculation
-
IDividendDistributor.sol - Interface for dividend claims
- Handles pull scenario dividends
- Claimable by vaults
-
RewardsModule.sol - Optional rewards distribution
- Distributes raw dividends to users
- Users claim based on vault share balance
- CompliantStrategyAdapter.sol - Example adapter implementation
- SimplePriceOracle.sol - Simple price oracle
- SimpleDividendDistributor.sol - Example dividend distributor
// 1. Deploy infrastructure
CompliantStrategyAdapter adapter = new CompliantStrategyAdapter();
SimplePriceOracle oracle = new SimplePriceOracle();
// 2. Deploy factory
PraxosFactoryCompliant factory = new PraxosFactoryCompliant(
address(adapter),
address(oracle)
);
// 3. Whitelist vault for RWA tokens (before creating vault)
adapter.whitelistVault(rwaToken1, vaultAddress);
adapter.whitelistVault(rwaToken2, vaultAddress);
// 4. Create vault
VaultConfig memory config = VaultConfig({
baseAsset: usdcAddress,
name: "Global Sovereign Yield Alpha",
symbol: "GSYA",
strategy: "balanced-diversified",
riskTier: 1,
targetDuration: 365 days,
assets: [rwaToken1, rwaToken2],
weights: [5000, 5000], // 50% each
dividendDistributors: [distributor1, distributor2]
});
address vault = factory.createVault(config);
// 5. Set vault identity (ONCHAINID)
PraxosVaultCompliant(vault).setVaultIdentity(vaultIdentityAddress);
// 6. Users deposit USDC
PraxosVaultCompliant(vault).deposit(1000e6, userAddress);
// 7. Harvest dividends (when available)
address[] memory tokens = [rwaToken1, rwaToken2];
PraxosVaultCompliant(vault).harvest(tokens);- Deploy ONCHAINID for your vault
- Contact RWA issuers to whitelist your vault
- Set up price oracle (Chainlink, custom oracle, or manual updates)
- Configure dividend distributors for each RWA token
- Decide on compliance level (strict vs loose) for vault shares
- Choose dividend strategy (auto-compound vs rewards module)
contracts/PraxosVaultCompliant.sol- Main compliant vaultcontracts/interfaces/ICompliantStrategyAdapter.sol- Compliance adapter interfacecontracts/interfaces/IPriceOracle.sol- Price oracle interfacecontracts/interfaces/IDividendDistributor.sol- Dividend distributor interfacecontracts/interfaces/IIdentity.sol- ONCHAINID interfacecontracts/RewardsModule.sol- Rewards distribution modulecontracts/mocks/CompliantStrategyAdapter.sol- Example adaptercontracts/mocks/SimplePriceOracle.sol- Example oraclecontracts/mocks/SimpleDividendDistributor.sol- Example distributorcontracts/PraxosFactoryCompliant.sol- Factory for compliant vaults