Appearance
Smart Contract Architecture
Contract relationships, roles, fund flows, and structural analysis. 7 contracts on Base Chain (OpenZeppelin). v3.0
Contract Registry
PlatformPool — Core · 1,244 lines ACTIVE
AS_POOL: receives 100% deposit, auto-mints LP, admin sends 90% to defined wallet (per pool). FUND_POOL: receives 10% reserve from Escrow via receiveReserve(). Both: yield distribution/claim/reinvest, single-stage redemption (Admin approve → reserve check → payout), lockup+penalty, stablecoin management.
Roles: DEFAULT_ADMIN VERIFIER_ROLE ORACLE_ROLE
Key: 10% auto-reserve (all pools) · NAV 24h timelock for decreases · NAV capped at $1.00 max · 4 penalty types
PlatformLPToken — ERC-20 · 186 lines ACTIVE
LP token with whitelist-gated transfers. Auto yield settlement on peer-to-peer transfers via settleYield() callback to Pool.
Roles: DEFAULT_ADMIN MINTER_ROLE (granted to Pool)
18 decimals · Pausable
PlatformKYCSoulbound — Soulbound ERC-721 · 386 lines ACTIVE
KYC/KYB verification token. 1 token per wallet. Non-transferable. Supports INDIVIDUAL and INSTITUTION levels, US person attestation, expiry, revocation.
Roles: DEFAULT_ADMIN
Verified by Pool contracts via isValidKYC() · isInstitution() · isValidKYCNonUS()
PlatformEscrow — Escrow · 359 lines ACTIVE
FUND_POOL safety layer. Holds USDC until manager confirms LP minted. releaseToFund() splits: 10% to PlatformPool (reserve via receiveReserve()), 90% to fund wallet. claimRefund() returns USDC after refund period (7 days). No yield logic — yield is unified in PlatformPool.
Roles: DEFAULT_ADMIN ESCROW_MANAGER_ROLE
Functions: deposit() releaseToFund() claimRefund() setRefundPeriod() setMinDeposit() setFundWallet()
PlatformReceiptNFT — Soulbound ERC-721 · 253 lines ACTIVE
Proof of escrow deposit. Non-transferable. Lifecycle: PENDING → MATCHED → LP_VERIFIED, or PENDING → REFUNDED (burned).
Roles: DEFAULT_ADMIN MINTER_ROLE (granted to Escrow)
Validated status transitions
PlatformPoolFactory — Factory · 205 lines ACTIVE
Unified factory + registry. createPool(type, config) deploys: Pool + LPToken always, Escrow + PlatformReceiptNFT if FUND_POOL. Single poolCounter. Auto-grants roles (MINTER_ROLE on Receipt to Escrow, ESCROW_MANAGER_ROLE to caller).
Roles: DEFAULT_ADMIN
Registry: getPoolContracts(poolId) · poolRegistry · poolCounter
Stablecoin Management
PlatformPool supports multiple stablecoins per pool. Admin uses addStablecoin(address) / removeStablecoin(address) (DEFAULT_ADMIN role) to manage accepted stablecoins at the contract level.
Each pool's accepted_currencies (DB) maps to on-chain stablecoin whitelist. Contract address per stablecoin is chain-specific — USDC on Base has a different address than USDC on Kaia.
| Function | Role | Description |
|---|---|---|
addStablecoin(address) | DEFAULT_ADMIN | Add a stablecoin to pool's accepted list |
removeStablecoin(address) | DEFAULT_ADMIN | Remove a stablecoin (no active deposits required) |
deposit(stablecoin, amount) | Investor | Deposit using an accepted stablecoin |
claimYield(stablecoin) | Investor | Claim yield in specified stablecoin |
Multi-Chain Stablecoin Addresses
Stablecoin contract addresses vary by chain. Only native (issuer-deployed) stablecoins are supported — bridged versions (e.g., USDC.e) are excluded due to depeg risk. Address lookup is done via STABLECOIN_ADDRESSES[chainId][symbol] in the frontend.
Architecture Diagram
Contract Architecture — Contracts Divided by Function/Role
Legend: 🟢 Investor actions · 🟠 Fund Manager actions · 🔴 Admin actions · 🔵 Oracle actions · ⚫ Contract-to-contract
PlatformPoolFactory — What Gets Deployed
AS_POOL gets 2 contracts. FUND_POOL gets 4 (adds Escrow + Receipt for the hold-and-release flow).
AS_POOL USDC Flow
- Investor deposits 100% USDC into PlatformPool
- LP auto-minted to investor (NAV-based pricing)
- Admin sends 90% to defined wallet, 10% stays as reserve
Legend: 🟢 Investor · 🔴 Admin · 🔵 Oracle · ⚫ Contract-to-contract
FUND_POOL USDC Flow
- Investor deposits USDC into Escrow
- PlatformReceiptNFT minted (PENDING)
- FM mints LP to investor wallet externally
- FM calls releaseToFund() — Escrow splits: 10% Pool / 90% fund wallet
Legend: 🟢 Investor · 🟠 Fund Manager · ⚫ Contract-to-contract
Fund Flows
Key Principle
Contracts are divided by function/role, not by pool type.
AS_POOL: Investor → 100% into PlatformPool (LP auto-minted) → admin sends 90% to defined wallet (per pool), 10% stays as reserve
FUND_POOL: Investor → PlatformEscrow (held until FM mints LP) → Escrow splits: 10% to PlatformPool (reserve via receiveReserve()), 90% to fund wallet
AS_POOL — Direct to PlatformPool
Deposit: Investor USDC → 100% into PlatformPool → LP tokens auto-minted to investor (NAV-based) → 10% stays as reserveBalance in Pool
Fund Deployment: Admin → transferToOperator() → 90% sent to defined wallet (can differ per pool)
Yield: Fund Manager → distributeYield(amount) → Virtual distribution (accumulatedYieldPerShare) Investor → claimYield(stablecoin) Investor → reinvest(amount) → new LP
Redemption: Investor → requestRedemption() (LP locked) Admin → approveRedemption() → reserve check → USDC payout to investor
FUND_POOL — Escrow Layer + PlatformPool
Step 1 — Escrow Hold: Investor USDC → PlatformEscrow (full amount) → PlatformReceiptNFT minted (PENDING) → USDC held in escrow (totalEscrowed)
Step 2 — Release (10/90 split): FM confirms LP tokens minted in investor wallet Fund Manager → releaseToFund(receiptId) → Receipt status: MATCHED Escrow splits directly:
- 10% → PlatformPool (reserve via
receiveReserve()) - 90% → FM's fund wallet
Refund (safety): If LP not issued within refundPeriod (7 days): Investor → claimRefund(receiptId) → Receipt burned, USDC returned
Yield: Handled by PlatformPool (same as AS_POOL)
USDC Flow — Where Funds Live at Each Stage
Roles & Access Control
All contracts use OpenZeppelin AccessControl. Roles are granted per-contract instance.
| Contract | Role | Granted To | Permissions |
|---|---|---|---|
| PlatformPool | DEFAULT_ADMIN | Platform deployer | activate/deactivate pool, approve redemption, add/remove stablecoins, set lockup/subscription/maturity, transferToOperator, pause/unpause, cancel NAV update |
VERIFIER_ROLE | Fund Manager | reject redemption, distributeYield, fundRedemption | |
ORACLE_ROLE | NAV Oracle service | updateNAV (increases instant, decreases queue 24h) | |
| PlatformEscrow | DEFAULT_ADMIN | Platform deployer | setRefundPeriod, setMinDeposit, setFundWallet, pause/unpause |
ESCROW_MANAGER | Fund Manager | releaseToFund (10/90 split to Pool reserve + fund wallet) | |
| PlatformLPToken | DEFAULT_ADMIN | Platform deployer | setPool, setWhitelist, pause/unpause |
MINTER_ROLE | PlatformPool contract | mint, burn LP tokens | |
| PlatformReceiptNFT | DEFAULT_ADMIN | PlatformPoolFactory | setBaseURI, pause/unpause |
MINTER_ROLE | PlatformEscrow contract | mint, burn, setStatus | |
| KYCSoulbound | DEFAULT_ADMIN | Platform deployer | mint KYC, revoke, burn, renew, setBaseURI, pause/unpause |
| PlatformPoolFactory | DEFAULT_ADMIN | Platform deployer | createPool, manages registry (poolCounter, poolRegistry, getPoolContracts) |
Role Assignment Flow
Resolved Issues (v2.0 → v3.0)
Issue 1: Duplicate Yield Logic ✅ RESOLVED
PlatformEscrow no longer has yield logic (accumulateYield(), claimYield() removed). All yield is unified in PlatformPool: distributeYield() + claimYield() + reinvest() for both AS_POOL and FUND_POOL.
Issue 2: Pool ID Mismatch Risk ✅ RESOLVED
PlatformEscrowFactory removed. Replaced by unified PlatformPoolFactory with single poolCounter and poolRegistry. No more separate ID counters.
Issue 3: FUND_POOL Has No 10% Reserve ✅ RESOLVED
releaseToFund() now splits: 10% sent to PlatformPool via forceApprove() + receiveReserve(), 90% sent to fund wallet via safeTransfer(). Reserve tracked by RESERVE_BPS = 1000 (10%).
Issue 4: No PoolFactory / Registry ✅ RESOLVED
PlatformPoolFactory deployed. createPool(PoolType, CreatePoolParams) deploys Pool + LPToken (AS_POOL) or Pool + LPToken + Escrow + ReceiptNFT (FUND_POOL). On-chain registry via getPoolContracts(poolId).
Issue 5: Escrow→Pool Not Connected On-Chain ✅ RESOLVED
PlatformPool now has receiveReserve(address stablecoin, uint256 amount). PlatformEscrow calls pool.receiveReserve() in releaseToFund(), updating reserveBalance and totalDeposited on-chain.
Issue 6: Docs Say "Deprecated" But Code Is Active ✅ RESOLVED
Documentation updated. PlatformEscrow correctly marked as ACTIVE (FUND_POOL safety layer). PlatformEscrowFactory removed from docs and code. backend-logic-map.html updated to reflect actual contract status.
Architecture Summary
7 Contracts — All Active
All structural issues from v2.0 have been resolved. PlatformEscrow handles hold/split-release/refund only (no yield). PlatformEscrowFactory replaced by unified PlatformPoolFactory. All yield unified in PlatformPool. Single-stage redemption (Admin approve → reserve check → payout).
| Contract | Status | Description |
|---|---|---|
| PlatformKYCSoulbound | ACTIVE | Soulbound KYC/KYB token. No changes from v2.0. |
| PlatformLPToken | ACTIVE | ERC-20 LP token with whitelist + yield settlement. No changes from v2.0. |
| PlatformReceiptNFT | ACTIVE | Soulbound receipt for escrow deposits. No changes from v2.0. |
| PlatformPool | ACTIVE | Core pool. Added receiveReserve(), single-stage redemption. NAV capped at $1.00 max. |
| PlatformEscrow | ACTIVE | FUND_POOL escrow. releaseToFund() splits 10% Pool / 90% fund wallet. No yield logic. |
| PlatformPoolFactory | ACTIVE | Unified factory + registry. Single poolCounter. |
Contract count: 7 (was 6)
- PlatformPoolFactory — unified deployer + registry (replaced PlatformEscrowFactory)
- PlatformPool — sole yield source, Escrow-aware via receiveReserve()
- PlatformLPToken — unchanged
- PlatformEscrow — hold/release/refund only, 10/90 split, no yield
- PlatformReceiptNFT — unchanged
- PlatformKYCSoulbound — unchanged