Skip to content

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.

FunctionRoleDescription
addStablecoin(address)DEFAULT_ADMINAdd a stablecoin to pool's accepted list
removeStablecoin(address)DEFAULT_ADMINRemove a stablecoin (no active deposits required)
deposit(stablecoin, amount)InvestorDeposit using an accepted stablecoin
claimYield(stablecoin)InvestorClaim 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

  1. Investor deposits 100% USDC into PlatformPool
  2. LP auto-minted to investor (NAV-based pricing)
  3. Admin sends 90% to defined wallet, 10% stays as reserve

Legend: 🟢 Investor · 🔴 Admin · 🔵 Oracle · ⚫ Contract-to-contract

FUND_POOL USDC Flow

  1. Investor deposits USDC into Escrow
  2. PlatformReceiptNFT minted (PENDING)
  3. FM mints LP to investor wallet externally
  4. 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.

ContractRoleGranted ToPermissions
PlatformPoolDEFAULT_ADMINPlatform deployeractivate/deactivate pool, approve redemption, add/remove stablecoins, set lockup/subscription/maturity, transferToOperator, pause/unpause, cancel NAV update
VERIFIER_ROLEFund Managerreject redemption, distributeYield, fundRedemption
ORACLE_ROLENAV Oracle serviceupdateNAV (increases instant, decreases queue 24h)
PlatformEscrowDEFAULT_ADMINPlatform deployersetRefundPeriod, setMinDeposit, setFundWallet, pause/unpause
ESCROW_MANAGERFund ManagerreleaseToFund (10/90 split to Pool reserve + fund wallet)
PlatformLPTokenDEFAULT_ADMINPlatform deployersetPool, setWhitelist, pause/unpause
MINTER_ROLEPlatformPool contractmint, burn LP tokens
PlatformReceiptNFTDEFAULT_ADMINPlatformPoolFactorysetBaseURI, pause/unpause
MINTER_ROLEPlatformEscrow contractmint, burn, setStatus
KYCSoulboundDEFAULT_ADMINPlatform deployermint KYC, revoke, burn, renew, setBaseURI, pause/unpause
PlatformPoolFactoryDEFAULT_ADMINPlatform deployercreatePool, 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).

ContractStatusDescription
PlatformKYCSoulboundACTIVESoulbound KYC/KYB token. No changes from v2.0.
PlatformLPTokenACTIVEERC-20 LP token with whitelist + yield settlement. No changes from v2.0.
PlatformReceiptNFTACTIVESoulbound receipt for escrow deposits. No changes from v2.0.
PlatformPoolACTIVECore pool. Added receiveReserve(), single-stage redemption. NAV capped at $1.00 max.
PlatformEscrowACTIVEFUND_POOL escrow. releaseToFund() splits 10% Pool / 90% fund wallet. No yield logic.
PlatformPoolFactoryACTIVEUnified 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