Smart contracts and scripts for tokenizing real-world assets (land plots) on Arbitrum:
- RWAPermissionedERC20: share token (1 token = 1 m²,
decimals = 0) with KYC/allowlist. - RevenueDistributor: USDC revenue distributor for share token holders.
- AssetRegistry: on-chain registry of metadata and document references (URIs + hashes) per token.
- RWATokenFactory: factory that, for each land plot, deploys a
RWAPermissionedERC20and its associatedRevenueDistributor.
The project is built on Hardhat 3 + viem, with TypeScript tests using node:test and some additional Solidity tests.
| Contract | Address |
|---|---|
| RWATokenFactoryRouter | 0x0ce62220867e7df484aca7768ac30be077346803 |
| RWAPublicTokenFactory | 0xbd6ac1b582a52d39cd22ecc9501a992b1edf11f0 |
| RWAConfidentialTokenFactory | 0xa0570079ebf260648801e3271535e48c33d18102 |
| AssetRegistry | 0x8ac75a491bea0e40ce230e3be632038f4324cd4d |
| Public Token (PAP1) | 0x894cdA6feBf63aC3e4ae94e639D5D61eB9745d83 |
| Confidential Token (CAP1) | 0xA9b4F6A44d16796321f21522C1a70C7B4E97B94A |
| USDC (payout, testnet) | 0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d |
Logic verified using direct onReport calls via a temporary forwarder handshake.
- Public Token Allowlist Proof:
0x9f4dafd74e63d422f3a3ddabddc6186e8d77248b97c5a65472f4c583d7f0004b - Confidential Mint Encrypted Proof (FHE):
0x8f94450c27ee99e49f3a9d790b5986996998befea90270b86722c1bb2f47a2d5
| Contract | Address |
|---|---|
| RWATokenFactoryRouter | 0xd3e41deae71e6c81f799ca746349b8d58e83b881 |
| RWAPublicTokenFactory | 0x4f87490b7879864324d4be083d6b217994015999 |
| RWAConfidentialTokenFactory | 0xa74236c1b78d17ba1639b705053a5f0bf4ffa5a5 |
| AssetRegistry | 0xdb113e53e45f4a4ee83b87c6e58d5fc86e0122d6 |
| Public Token (PEP1) | 0xb7c22c408bb1126FE5C4B35FE7e5EE6fc69C29Da |
| Confidential Token (CEP1) | 0xc48e235465A9c04f051abf0dA6aD63Ea9B6651e5 |
| USDC (payout, testnet) | 0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238 |
- Admin / MASTERWALLET:
0xfBf9fcB06a4275DE4ba300bA0fAA8B19D048e1B2
Use Router for both public and confidential createToken and indexing. The Router is now dynamic and can link to the appropriate factories. See docs/DEPLOY.md for mainnet and env vars.
- File:
contracts/RWAPermissionedERC20.sol - Access control:
DEFAULT_ADMIN_ROLE: general configuration.ISSUER_ROLE: allowed tomint/burn.KYC_ADMIN_ROLE: canallowUser/disallowUser.PAUSER_ROLE: can pause / unpause transfers.
- Transfer policy:
- Only transfers between issuer and allowlisted investors are allowed.
mintandburnare not paused; onlytransferis affected by pause.
- Decimals:
decimalsis configurable, but for RWA deploys it is set to 0 (1 token = 1 m²).
- File:
contracts/RevenueDistributor.sol - Purpose: distribute a payout token (USDC on Arbitrum) to share token holders.
- Features:
- Pull model: issuer calls
deposit(amount)and users claim withclaim()/claimFor(user). ACC_PRECISION = 1e27to minimize rounding issues.checkpoint(user)must be called by the backend whenever a user’s share token balance changes (mint/burn/transfer).- Only issuer and KYC’d (allowlisted) users can
claim. - Reentrancy protection on
deposit,claim, andclaimFor.
- Pull model: issuer calls
- File:
contracts/AssetRegistry.sol - Access control:
REGISTRY_ADMIN_ROLE: only role allowed to update metadata and documents.
- Structures:
MetadataRef { uri, contentHash, updatedAt, version }DocumentRef { name, uri, contentHash, mimeType, gated, updatedAt, version }
- Core functions:
setMetadata(token, uri, contentHash)— stores URI + hash for a token and increments a version counter.upsertDocument(token, docId, name, uri, contentHash, mimeType, gated)— creates/updates a referenced document with versioning.getDocument(token, docId)— returns the current version of a document.
- Validations:
- Constructor and write methods ensure address parameters are not
address(0)(ZeroAddress,InvalidToken).
- Constructor and write methods ensure address parameters are not
- File:
contracts/RWATokenFactory.sol - Access control:
FACTORY_ADMIN_ROLE: allowed to create new assets (land plots).
- Constructor:
RWATokenFactory(address factoryAdmin, IERC20 payoutToken_)factoryAdminis grantedDEFAULT_ADMIN_ROLEandFACTORY_ADMIN_ROLE.payoutToken_must be the USDC on Arbitrum address.
- Main function:
createToken(name, symbol, admin, issuer, kycAdmin, pauser):- Deploys
RWAPermissionedERC20(name, symbol, 0, admin, issuer, kycAdmin, pauser). - Deploys
RevenueDistributor(admin, issuer, pauser, payoutToken, token). - Emits
TokenCreated(token, distributor, name, symbol).
- Deploys
All scripts use Hardhat + viem and assume you’ve configured network environment variables (see below).
- Responsibility: initial deployment of the “infrastructure” contracts:
AssetRegistryRWATokenFactory
- Uses:
MULTISIG_ADDRESSas admin (or theARBITRUM_PRIVATE_KEYaccount if not set).USDC_ARBITRUM(or the default0xaf88d065e77c8cC2239327C5EDb3A432268e5831).
- Output:
AssetRegistryaddress.RWATokenFactoryaddress.
Example:
ARBITRUM_RPC_URL=... \
ARBITRUM_PRIVATE_KEY=0x... \
MULTISIG_ADDRESS=0xTuMultisig \
USDC_ARBITRUM=0xaf88d065e77c8cC2239327C5EDb3A432268e5831 \
npx hardhat run scripts/deploy.ts --network arbitrum- Responsibility: create a new land asset (share token + distributor pair) using
RWATokenFactory. - Variables:
RWA_FACTORY_ADDRESS(required):RWATokenFactoryaddress.MULTISIG_ADDRESS(optional): admin/issuer/kycAdmin/pauser; if not set, the deployer is used.RWA_NAME/RWA_SYMBOL(optional): name and symbol of the new token.
- Output:
- Created
RWAPermissionedERC20address. - Associated
RevenueDistributoraddress.
- Created
Example:
ARBITRUM_RPC_URL=... \
ARBITRUM_PRIVATE_KEY=0x... \
RWA_FACTORY_ADDRESS=0xFactory \
MULTISIG_ADDRESS=0xTuMultisig \
RWA_NAME="RWA Terreno 123" \
RWA_SYMBOL="RWA123" \
npx hardhat run scripts/create-terreno.ts --network arbitrum- Responsibility: register metadata and documents for a share token in
AssetRegistry. - Variables:
ASSET_REGISTRY_ADDRESS(required):AssetRegistryaddress.RWA_TOKEN_ADDRESS(required): share token to register.METADATA_URI,METADATA_HASH(optional): main metadata.DOC_ID(bytes32, optional). If set,upsertDocumentis also called with:DOC_NAME,DOC_URI,DOC_HASH,DOC_MIME,DOC_GATED.
Minimal example (metadata only):
ASSET_REGISTRY_ADDRESS=0xRegistry \
RWA_TOKEN_ADDRESS=0xToken \
METADATA_URI="ipfs://Qm.../metadata.json" \
METADATA_HASH=0x... \
ARBITRUM_RPC_URL=... \
ARBITRUM_PRIVATE_KEY=0x... \
npx hardhat run scripts/register-asset.ts --network arbitrum- Responsibility: actions on
RevenueDistributor:ACTION=deposit: deposit USDC as issuer.ACTION=claim: claim revenue.
- Common variables:
ACTION:"deposit"or"claim".DISTRIBUTOR_ADDRESS:RevenueDistributoraddress.
- For
deposit:PAYOUT_TOKEN_ADDRESS: USDC address.DEPOSIT_AMOUNT: amount in smallest units (e.g."1000000"= 1 USDC with 6 decimals).
- For
claim:CLAIM_FOR(optional): if set and different frommsg.sender, usesclaimFor(user); otherwise usesclaim().
Deposit example:
ACTION=deposit \
DISTRIBUTOR_ADDRESS=0xDistributor \
PAYOUT_TOKEN_ADDRESS=0xUSDC \
DEPOSIT_AMOUNT=1000000 \
ARBITRUM_RPC_URL=... \
ARBITRUM_PRIVATE_KEY=0xIssuerKey \
npx hardhat run scripts/revenue-actions.ts --network arbitrumClaim example (for the investor themselves):
ACTION=claim \
DISTRIBUTOR_ADDRESS=0xDistributor \
ARBITRUM_RPC_URL=... \
ARBITRUM_PRIVATE_KEY=0xInvestorKey \
npx hardhat run scripts/revenue-actions.ts --network arbitrum- Responsibility: deploy RWAConfidentialTokenFactory and optionally create the first confidential token (FHE). Uses CoFHE; supports Arbitrum Sepolia.
- Variables:
MASTERWALLET(admin/issuer/kycAdmin/pauser of the token),USDCpayout address for the distributor (optional; script has defaults for Arbitrum Sepolia). - Output: factory address and, if created, the first
RWAConfidentialERC20address (use the latter asRWA_CONFIDENTIAL_TOKEN_ADDRESSfor minting).
Example:
npx hardhat run scripts/deploy-confidential.ts --network arbitrumSepolia- Responsibility: whitelist and mint tokens for a user on RWAConfidentialERC20. Uses mintEncrypted (amount encrypted via cofhejs in a CJS helper); balances and mint amount stay private on-chain.
- Variables:
RWA_CONFIDENTIAL_TOKEN_ADDRESS(required): address of theRWAConfidentialERC20contract (e.g. the token created bydeploy-confidential.ts).USER_TO_ALLOW_AND_MINT(required): address to whitelist and mint to.MINT_AMOUNT(optional): amount to mint (default 100).ALLOW_ONLY(optional): set to1to only whitelist, no mint.
- Network: use the network where the confidential token is deployed (e.g. arbitrumSepolia).
- Signer: must have
KYC_ADMIN_ROLE(forallowUser) andISSUER_ROLE(formintEncrypted). Same env as the network (e.g.ARBITRUM_SEPOLIA_RPC_URL,ARBITRUM_SEPOLIA_PRIVATE_KEY).
Example:
export RWA_CONFIDENTIAL_TOKEN_ADDRESS=0xe3A879a1e56FC801CaCf128eD1ddbadEc43e272a
export USER_TO_ALLOW_AND_MINT=0x...
npx hardhat run scripts/mint-confidential.ts --network arbitrumSepolia- Responsibility: grant or revoke AccessControl roles on a contract after deploy. See
docs/DEPLOY.mdfor usage.
Networks are defined in hardhat.config.ts. For Arbitrum:
- Environment variables:
ARBITRUM_RPC_URL— Arbitrum One RPC endpoint.ARBITRUM_PRIVATE_KEY— key of the account that will sign transactions (multisig delegate or operational EOA).
- Other useful values:
MULTISIG_ADDRESS— if you want to separate deployer and logical admin.USDC_ARBITRUM— USDC contract address (defaults to Circle native USDC on Arbitrum).
Quick export example:
export ARBITRUM_RPC_URL="https://arb-mainnet.g.alchemy.com/v2/..."
export ARBITRUM_PRIVATE_KEY="0x..."
export MULTISIG_ADDRESS="0x..."
export USDC_ARBITRUM="0xaf88d065e77c8cC2239327C5EDb3A432268e5831"Main tests live in test/:
RWAPermissionedERC20.tsRevenueDistributor.tsAssetRegistry.tsRWATokenFactory.ts
Run them with:
npx hardhat test(In this project we don’t split “solidity” vs “nodejs” suites; everything runs with the command above.)
Some contracts also have Solidity tests (*.t.sol) intended for Foundry.
Example (if you have Foundry installed):
forge test-
Infrastructure deploy (once)
- Run
scripts/deploy.tsto deployAssetRegistryandRWATokenFactory.
- Run
-
Create a land asset (new RWA token)
- Run
scripts/create-terreno.tswithRWA_FACTORY_ADDRESSand optionallyRWA_NAME/RWA_SYMBOL. - Store the share token and
RevenueDistributoraddresses.
- Run
-
Register land metadata & documents
- Run
scripts/register-asset.tswithASSET_REGISTRY_ADDRESSandRWA_TOKEN_ADDRESS.
- Run
-
Daily revenue operations
- Issuer/ops: use
scripts/revenue-actions.tswithACTION=depositto deposit revenue into the distributor. - Investors / backend / relayers: use
scripts/revenue-actions.tswithACTION=claim(and optionallyCLAIM_FOR) to claim.
- Issuer/ops: use
This flow matches what is described in docs/ARCHITECTURE_AND_TASKS.md and the user stories in docs/context/user_stories.md.