Skip to content

Architecture

The platform is a set of smart contracts on Base (Ethereum L2), plus an off-chain backend and website that read from and write to those contracts. The contracts are the source of truth; the off-chain components exist for verification (KYC), indexing, and a friendly UI.

On-chain components

Two contracts are deployed once for the whole platform:

  • KYCRegistry — the single source of truth for who is verified. Stores on-chain attestations: whether a wallet passed KYC, its investor type (US accredited / US non-accredited / Reg S), country, expiry, and status. Written only by the attester role wallet; read by every funding contract before an investment.
  • PropertyFundingFactory — deploys a new property’s contracts as one atomic set and locks their roles. Guarantees the three per-property contracts always appear together and correctly configured.

Three contracts are deployed per property by the factory:

  • PropertyFunding — accepts and custodies investor USDC during the raise, enforces all participation rules, and runs the project state machine.
  • PropertyToken (ERC-20) — a non-transferable proof-of-investment token, minted on invest and burned on refund or ROI claim.
  • ROIDistributor — distributes return of capital + profit to investors after completion, using an on-chain Merkle commitment.

See Contracts for each contract’s functions and security properties.

Off-chain components

  • Backend (NestJS) — runs the KYC verification integrations (Didit for identity/sanctions), writes attestations to KYCRegistry via an ATTESTER_ROLE hot wallet, indexes on-chain events, and sends notifications. It holds no investor funds and no financial authority.
  • Frontend (Next.js) — reads on-chain data and presents listings, funding progress, the invest flow, and dashboards. Investors can also bypass the UI and call the contracts directly.
  • Treasury & fiat — two Gnosis Safe multisigs per property plus a regulated fiat ramp (Coinbase Prime) handle the USDC↔USD conversion for construction (below).

Roles: separation of powers

Each property has two distinct Gnosis Safe multisigs — enforced at deploy time (spvAdmin != spvTreasury):

  • spvAdmin (Operational Safe) — drives the state machine: withdrawFunds, setActive, setCompleted. It cannot send funds to itself — withdrawals only ever go to the hardcoded treasury address.
  • spvTreasury (Financial Safe) — receives the raised USDC, converts to/from fiat, and later funds the ROI distribution. It cannot change project state.

Proposed signer set for each Safe: 3-of-4, with each signer holding a Trezor hardware wallet — no single person can move funds or change project state alone.

Lifecycle

The full flow from creating a property to returning profit.

Flow 0 — Create a property

An admin calls createProject() on the factory. In one transaction it deploys PropertyToken, PropertyFunding, and ROIDistributor, wires their roles, then revokes all admin roles — minting/burning rights are frozen forever. All project parameters (goal, minimum, deadline ≤ 180 days, investor limits, treasury/admin addresses, IPFS metadata hash) are set at deploy and immutable.

Flow 1 — Investor onboarding

Register (email, for notifications only) → KYC + Didit identity verification → the backend writes an attestation to KYCRegistry for your wallet. This is a public on-chain record that a wallet is verified, without exposing personal data.

Flow 2 — Invest

A verified investor calls invest() (via the UI or directly). PropertyFunding checks: valid KYC attestation, investor type, minimum amount, per-type cap, investor count < 2000, open state, and deadline. If all pass, USDC moves into the contract and PropertyToken is minted to the investor (1 token 1e18 = 1 USDC 1e6). Funds stay in the contract until withdrawal.

Investor wallet ── USDC ──▶ PropertyFunding ──(checks KYC, limits, deadline)──▶ KYCRegistry
└── mint ──▶ PropertyToken ──▶ investor wallet

Flow 3a — Raise succeeds

Goal met → state becomes FUNDED and investing stops. The operations Safe (spvAdmin) calls withdrawFunds(), which sends exactly totalRaised (not balanceOf) to the hardcoded spvTreasury. Treasury off-ramps via Coinbase Prime → USD → construction. State advances to ACTIVE, then COMPLETED.

PropertyFunding ──withdrawFunds() [spvAdmin]──▶ spvTreasury ──▶ Coinbase Prime ──▶ USD ──▶ build

Flow 3b — Refund (raise fails or admin is inactive)

Refunds go directly through PropertyFunding — no team action required. Anyone can call triggerRefund() when either:

  • the deadline passed without reaching the goal (FUNDRAISING → REFUNDING), or
  • the project sat in FUNDED for more than 60 days without a withdrawal (FUNDED → REFUNDING).

Each investor then calls claimRefund() themselves: the contract zeroes their record (before transfer), burns their PropertyToken, and returns their USDC.

Flow 4 — ROI distribution

After COMPLETED, proceeds return to USDC and flow back via ROIDistributor in two steps by spvTreasury:

  1. commitDistribution(claimants[]) — submits the [wallet → amount] list. The contract computes the Merkle root on-chain (the root is not an input, so it can’t be forged), stores the list.
  2. depositFunds(amount) — deposits the USDC (partial deposits allowed). Claims unlock automatically once totalDeposited reaches totalRequired. Also, contract sets a 180-day recovery clock - this intentionally done to make funds recoverable from contract by spvAdmin, in case investor loosed access to his wallet or any else life surcumstances - we keep this ability to send funds to investor off-chain, once he proves the identity & wallet ownership.

Each investor then calls claim() with a Merkle proof; the contract verifies it, marks claimed (before transfer), burns the PropertyToken, and sends USDC. Unclaimed funds after 180 days can be recovered only to the hardcoded treasury.

spvTreasury ──commitDistribution([...])──▶ ROIDistributor (root computed on-chain)
spvTreasury ──depositFunds(USDC)─────────▶ ROIDistributor (claims unlock when fully funded)
Investor ──claim()────────────────────▶ ROIDistributor ── burn ──▶ PropertyToken ── USDC ──▶ investor

Project state machine

FUNDRAISING ──(goal met)──▶ FUNDED ──(spvAdmin withdraws)──▶ WITHDRAWN ──▶ ACTIVE ──▶ COMPLETED
│ │
│ (deadline, goal missed) │ (60-day timeout, no withdrawal)
▼ ▼
────────────────▶ REFUNDING

Transparency

Every meaningful action emits an on-chain event — an immutable, cryptographically anchored log inside the transaction. Anyone can read them for free on a block explorer (Basescan), with no cooperation from the company:

  • Project created (goal, deadline, minimum, metadata hash, spvAdmin, spvTreasury)
  • KYC attestation issued / revoked
  • Investment accepted (investor, amount, timestamp, running total)
  • Raise closed (FUNDED), funds withdrawn to treasury
  • Construction started / completed
  • Distribution committed (Merkle root, investor count, total, recovery unlock)
  • Funds deposited, ROI claimed, refund triggered / claimed, recovery executed

Each PropertyFunding contract hosts list of IPFS CIDs. We will push each property updates as IPFS documents, our front-end read/parse those data directly from blockchain and represend as Timeline on project’s page.

The website aggregates this on-chain data: property listings with live funding progress and status, per-project investor lists (wallet, amount, country — never email or personal data), and platform-wide analytics. Nothing shown on the site requires trusting the company — it can all be re-derived from the chain.