Skip to content

Contracts

The platform is five contract types written in Solidity (Foundry) and deployed on Base. Each has one clear responsibility. The factory deploys three of them as a set per property. All are publicly verified on the explorer.

KYCRegistry — one per platform

The single source of truth for investor eligibility.

  • Purpose: stores on-chain attestations confirming a wallet passed verification; read by every PropertyFunding before an investment.
  • Stores (per wallet): verified flag, investor-type flags, country code, expiry, status. The three type flags map to exemptions:
    • accreditedInvestor → Reg D 506(c) (US accredited)
    • nonAccreditedUS → Reg D 506(b) (US non-accredited)
    • regSEligible → Reg S (non-US)
  • Writes (ATTESTER_ROLE, the backend): issueAttestation, revokeAttestation, updateExpiry. Country gating: allowCountry, restrictCountry.
  • Views (anyone): isVerified, isEligibleInvestor, isAccredited, isNonAccreditedUS, isRegSEligible, getAttestation, isCountryRestricted, plus on-chain metadata history (pushMetadataUpdate / latestMetadata / getMetadataHistory).
  • Protections: cannot issue over an active attestation (revoke first); cannot issue with a past expiry; cannot revoke a non-existent attestation; a compromised attester key can be rotated by the admin Safe revoking ATTESTER_ROLE. Pausable.

PropertyFundingFactory — one per platform

Creates and locks down a property’s contract set.

  • Purpose: deploys PropertyToken, PropertyFunding, and ROIDistributor for a new property in one transaction.
  • createProject(...) (Platform admin only):
    1. Validates spvAdmin != spvTreasury.
    2. Validates limit logic (minInvestment ≤ maxAccredited, minInvestment ≤ maxNonAccreditedUS).
    3. Validates deadline ≤ now + 180 days (MAX_FUNDRAISING_DURATION).
    4. Deploys the three contracts.
    5. Grants MINTER_ROLE + BURNER_ROLE to PropertyFunding, and BURNER_ROLE to ROIDistributor, on the token.
    6. Revokes DEFAULT_ADMIN_ROLE from everyone — including spvAdmin and the factory itself. Token roles are frozen forever.
  • Note: the child PropertyFunding is deployed via an external library (PropertyFundingDeployer, delegatecall) so the factory stays under the EIP-170 contract-size limit while remaining the deployer.

PropertyToken (ERC-20) — one per property

On-chain proof of an investment.

  • Purpose: minted to the investor on invest(); visible in their wallet; burned on claimRefund() or ROI claim().
  • Non-transferable: wallet-to-wallet transfer/transferFrom are permanently disabled at the contract level. Mint and burn are not transfers and are unrestricted (for the authorized roles).
  • Frozen roles: after the factory finishes, no one holds DEFAULT_ADMIN_ROLE; MINTER_ROLE / BURNER_ROLE belong only to PropertyFunding and ROIDistributor and can never be changed.
  • Units: 1 token (1e18) = 1 USDC (1e6) — a 1e12 scaling factor. Total supply equals total USDC raised.

PropertyFunding — one per property

Raises and custodies investor USDC, and runs the state machine.

  • Immutable params (set at deploy): funding goal, minimum investment, deadline (≤ 180 days), spvAdmin, spvTreasury, KYCRegistry, PropertyToken, per-type investor limits, property metadata (IPFS) hash.
  • invest() checks: valid KYC attestation (not expired/revoked), investor type allowed, amount ≥ minimum, cumulative ≤ per-type cap, investor count < MAX_INVESTORS (2000), still FUNDRAISING, deadline not passed. On success: pulls USDC, mints PropertyToken, emits an event.
  • withdrawFunds() (spvAdmin, only in FUNDED): sends exactly totalRaised to the hardcoded spvTreasury; state → WITHDRAWN.
  • triggerRefund() (anyone): activates REFUNDING if the deadline passed without the goal, or if FUNDED for > 60 days without a withdrawal.
  • claimRefund() (investor): zeroes the record before transfer, burns the token, returns USDC (pull pattern, reentrancy-safe).
  • State transitions: FUNDRAISING → FUNDED → WITHDRAWN → ACTIVE → COMPLETED, with FUNDRAISING → REFUNDING (deadline) and FUNDED → REFUNDING (timeout). Other admin functions: setActive, setCompleted, pause / unpause.

ROIDistributor — one per property

Returns capital + profit to investors after completion.

  • commitDistribution(claimants[]) (spvTreasury, no USDC): takes the [wallet → amount] list and computes the Merkle root on-chain — the root is not a parameter and cannot be forged. Stores the claimant list for public viewing; rejects empty lists and duplicates; sets recoveryUnlocksAt = now + 180 days. Reversible before funding via cancelCommitment.
  • depositFunds(amount) (spvTreasury): supports multiple partial deposits; claims unlock automatically once totalDeposited == totalRequired. withdrawDeposit() returns a partial deposit if claims aren’t yet unlocked (e.g. a list error caught early).
  • claim() (investor): verifies the Merkle proof against the stored root, sets claimed before transfer (CEI / reentrancy-safe), burns the PropertyToken, sends USDC.
  • recoverFunds() (spvTreasury, after 180 days): sweeps unclaimed USDC only to the hardcoded treasury address (no address parameter).

Security highlights

  • Pull-over-push for both refunds and ROI — one bad wallet can’t block others, no batch-loop reentrancy surface.
  • Checks-Effects-Interactions on every payout (record updated before transfer).
  • Frozen token roles — mint/burn rights are set by the factory and can never be granted again.
  • Hardcoded treasury — withdrawFunds and recoverFunds can only send to the address fixed at deploy; no address parameter to manipulate.
  • On-chain Merkle root — computed inside commitDistribution, never supplied externally.
  • Time locks — 60-day withdrawal timeout (investor protection) and 180-day recovery window.
  • Immutable offering terms — goal, minimum, deadline, limits, and treasury/admin addresses are fixed at deploy and publicly verifiable.

Deployed addresses

ContractNetworkAddress
KYCRegistryBase Sepolia (testnet)0x16C7d0F3d116b225634791Afd75D26A573179173
PropertyFundingFactoryBase Sepolia (testnet)0x9822861d8A41b655aa0C0DF79017299F6283A28E
KYCRegistryBase mainnet0xABd63EE631E88a6e733c29D1ed22d3Cf61F1A651
PropertyFundingFactoryBase mainnet0xD71ef3B37Ad099308122C592B680f481678fae3f

Per-property PropertyFunding, PropertyToken, and ROIDistributor addresses are emitted in the factory’s ProjectCreated event and shown on each property’s page.