Own the work. Not just the walls.
A protocol for dynamic real-estate co-ownership. Cap tables rebalance as co-owners contribute real work — maintenance, upgrades, taxes, capital. Ownership tracks reality, not just day-one capital.
Live app · Architecture · Live addresses · Run it locally · Mainnet readiness · Security · Legal
Traditional fractional real estate freezes the cap table on day one. The person who wrote the biggest check owns the most, forever — even if they never lift a finger afterwards.
Stakehold takes the opposite approach. For every property, contributions — capital, renovations, maintenance, taxes — are submitted on-chain with IPFS proof, voted on by shareholders, and rebalance the cap table when executed. Rental income is paid pro-rata in ETH via a pull-pattern accumulator.
Stakehold is a platform, not a single property. Anyone can launch a property through the StakeholdFactory, which atomically deploys a paired Share token + Property governor with all permissions wired correctly.
Three honest answers — the protocol never prints ETH.
-
The factory launch fee — a flat-ETH
createPropertypayment toStakeholdFactory(treasury / protocol operator). It pays for deployment gas and compensates the platform; it is not rent. -
Rental & pass-through yield — ETH enters each property at the
StakeholdSharecontract, never atStakeholdProperty. CalldistributeYield{value: x}(or a plain transfer — both hit thereceive()hook) to stream the deposit pro-rata into the pull-pattern accumulator. ShareholdersclaimYield()to withdraw. This is indistinguishable from a fully automated on-chain system once someone has already converted off-chain rent to ETH. That conversion is the fiat rail — today it's a human with a bank account, tomorrow a Bridge.xyz / Circle / Stripe Crypto integration, but it is always outside the smart contracts. -
Not contributions — a capital contribution (invoice + IPFS hash) is an off-chain expense that mints equity (shares) after a vote, not a deposit of ETH. The co-owner already sent dollars to a contractor; the on-chain system records the claim, not the wire transfer.
Bottom line for recruiters: the contracts solve governance math, cap-table
dynamics, upgrade safety, and O(1) pull-yield. They deliberately do not
solve ACH → ETH — that is operational plumbing every tokenized-RE product
(RealT, Lofty, Roofstock) still runs through a licensed treasurer. The
frontend exposes a production-shaped rent deposit flow that calls
distributeYield so you can demonstrate the full loop on Sepolia with test
ETH.
- Four-contract architecture — Factory launches properties; each property is a
Share(ERC20Votes + yield) +Property(governor + vesting) pair, with a statelessLensaggregator for read-heavy frontends. Independent upgrade surfaces, least-authority wiring. - Dynamic cap table — Shares mint to contributors post-vote, capped at 5% per execution (
MAX_REBALANCE_BPS) to prevent silent takeovers. - Real governance, not theatre —
ERC20Votessnapshots voting power at proposal creation so late-stage share purchases can't swing votes. Small contributions auto-approve after a timelock; large ones hit quorum. - Six-month vesting cliff — Newly minted shares are locked behind a vesting grant. Discourages one-shot dilution attacks and rewards long-term co-owners.
- Pull-based yield — Rental income flows in as ETH; each holder claims independently via a MasterChef-style
accYieldPerShareaccumulator. O(1) deposits regardless of shareholder count. Yield settles inside_update, so transfers always leave both parties made whole. - Privacy by design — Public city / region on-chain; full street address, deeds, and insurance live at
legalDocsURIand are only surfaced in the UI to verified shareholders. - UUPS-upgradeable, safely — Share and Property proxies upgrade independently. The Factory and Lens are intentionally non-upgradeable so launchers always know what they're getting.
- Permissionless by default — Launching a property, submitting contributions, executing proposals, distributing yield, claiming yield — every path is permissionless. No keeper, no cron.
┌──────────────────────────────────────────────────────────────────────┐
│ StakeholdFactory (non-upgradeable) │
│ createProperty(fee) ─▶ deploys Share proxy + Property proxy │
│ wires roles, renounces self, emits event │
└──────────────┬────────────────────────────────────┬──────────────────┘
│ deploys │ deploys
▼ ▼
┌──────────────────────────┐ ┌───────────────────────────┐
│ StakeholdShare (UUPS) │◀────mints────│ StakeholdProperty (UUPS) │
│ ERC20 + Votes + Permit │ shares to │ contributions, proposals,│
│ ETH yield accumulator │ beneficiary │ vesting, rebalance math │
│ _update settles yield │ │ timelock + quorum │
└──────────────▲───────────┘ └─────────────┬─────────────┘
│ │
│ getPropertyCard ┌───────────────┐ │ getPropertyDetail
└───────────────────│ StakeholdLens │◀───┘ getUserPosition
│ (stateless) │ getUserGrants
└───────┬───────┘
│ single-call reads
▼
┌──────────────────┐
│ Frontend (UI) │
└──────────────────┘
Each property is fully isolated: its own Share token, its own cap table, its own governance parameters, its own treasury balance. The Factory is the only shared global; the Lens is just a read helper with no state.
| Contract | Address |
|---|---|
| Factory (launch new properties) | 0x2d4C7Ae731bD1c360E3f7bCBDB88CaeB1BA5f7Bf |
| Lens (read aggregator) | 0xEE4F179eB8d1fc460012CA6782860c611995d86a |
| Share implementation (UUPS logic) | 0xfb7b468780F3396b1De427aB543237303B58fe3d |
| Property implementation (UUPS logic) | 0x5951569685Cbf13CA5A6F797d2E3b10186994645 |
| Genesis property (proxy) | 0x6bAc6Ca15D70a0D1FCB5347Df3B3b2b99367BA80 |
| Genesis share token (proxy) | 0xDfd0764136f900b33cbDe0548BE5AE8C66c8edaF |
| Deployer / treasury | 0xc7f16B436594ef356751C0094F5542162f040223 |
- Network: Sepolia (chainId
11155111) - Deploy block:
10713323 - Launch fee:
0.001 ETH(configurable by factory admin) - Genesis property: Stakehold Genesis (London, UK) · token
SHG· initial supply 1,000,000 - All six contracts verified on Sepolia Etherscan
- Launcher calls
factory.createProperty{value: fee}(params)with metadata (name, city, type, token name/symbol, supply, initial holders, IPFS URIs). - Factory deploys
ERC1967Proxy(shareImpl)andERC1967Proxy(propertyImpl)back-to-back, initializing both. - Factory grants
MINTER_ROLEon the new Share to the new Property, then renounces every temporary role it held. Net result: the Property is the only minter of its Share; the Factory is a no-op from that point on. - Factory registers the pair and forwards the launch fee to the treasury. Overpayment is refunded.
- Launcher receives the initial supply and becomes the property admin (
DEFAULT_ADMIN_ROLE,PAUSER_ROLE,UPGRADER_ROLEon both proxies).
submitContribution(valueUsd, proofHash, descriptionURI)
│
▼
valueUsd ≤ threshold ?
│ │
yes │ │ no
│ │
▼ ▼
auto-approved createProposal() on StakeholdProperty
(timelock) vote window + quorum + timelock
│ │
▼ ▼
executeAutoApproved(id) executeProposal(id)
│
▼
rebalance math (capped 5%)
│
▼
createVestingGrant() — 6-month cliff
│
▼
claimVestedShares(grantId)
│
▼
share.mint() — auto self-delegate
─── parallel: rental income ───
anyone → share.distributeYield{value: ethAmt}() → accYieldPerShare += amt * 1e18 / supply
holder → share.claimYield() → ETH transfer
Contracts — Solidity 0.8.24 · OpenZeppelin Upgradeable v5 · Foundry · UUPS (EIP-1822) · ERC-1967 proxies · ERC-20 Votes + Permit Frontend — Next.js 14 App Router · TypeScript · Tailwind CSS · viem 2 · wagmi 2 · RainbowKit 2 · Chart.js · react-dropzone · sonner Infra — Vercel (frontend) · Filebase (IPFS pinning, 5 GB free tier) · Etherscan (verification)
The IPFS layer is provider-agnostic: contracts only store content-addressed hashes, so swapping pinning providers — Filebase, Pinata, 4EVERLAND, a self-hosted IPFS node — requires zero redeployment. The current default is Filebase, accessed through an IPFS-compatible RPC endpoint proxied by a Next.js API route so the access token never reaches the browser.
An earlier monolith ran into the EIP-170 24,576-byte limit as features stacked up. Rather than hack the compiler, the logic was factored into:
| Contract | Responsibility | Upgradeable? |
|---|---|---|
StakeholdFactory |
Property launchpad + registry + fee sink | No — intentional |
StakeholdShare |
ERC20 Votes + Permit + yield accumulator | Yes (UUPS) |
StakeholdProperty |
Contributions, DAO, vesting, metadata | Yes (UUPS) |
StakeholdLens |
Read aggregator for the UI | No — stateless |
Benefits: clean separation of concerns, independent upgrade paths, minimal factory attack surface, trivial read ergonomics for the frontend.
| Concern | Mitigation |
|---|---|
| Reentrancy | nonReentrant guards + CEI on every ETH transfer |
| Dilution attacks | MAX_REBALANCE_BPS = 500 (5%) per execution, 6-month cliff on minted shares |
| Flash-governance | ERC20Votes snapshots voting power at proposal creation |
| Storage collisions | uint256[50] __gap on every upgradeable implementation |
| Locked yield | Yield claims are not pausable; pause halts governance and transfers, never exits |
| Admin takeover | UPGRADER_ROLE separate from DEFAULT_ADMIN_ROLE; production wants multisig + timelock |
| Unauthorized upgrades | _authorizeUpgrade reverts without UPGRADER_ROLE |
| Unauthorized mints | MINTER_ROLE on Share is held only by its paired Property; Factory renounces after wiring |
| Cross-property contamination | Every property has its own Share/Property proxy pair — no shared state |
Stakehold is unaudited. Sepolia testnet only. Do not use with real funds.
cd contracts
forge test --summary74 tests across unit, fuzz, invariant, and upgrade-roundtrip:
StakeholdShare.t.sol— auto-delegation, mint gating, yield math, transfer-triggered settlementStakeholdProperty.t.sol— contribution flow, auto-approve, DAO vote, rebalance math, vesting, governance paramsStakeholdFactory.t.sol— constructor validation, fee forwarding, refunds, multi-property isolationStakeholdLens.t.sol— every view function across shareholder and non-shareholder casesInvariant.t.sol— stateful fuzzing asserts:sum(balanceOf) == totalSupplyafter any sequence of actionsaddress(share).balance >= sum(pending yield)totalSupplyonly grows or holds
Upgrade.t.sol— upgrade Share and Property independently, verify state preservation + role gating
- Foundry —
curl -L https://foundry.paradigm.xyz | bash && foundryup - Node 20+
- A Sepolia RPC URL
- A WalletConnect project ID
- A Filebase IPFS access token (free tier, 5 GB)
cd contracts
cp .env.example .env # SEPOLIA_RPC_URL, PRIVATE_KEY, ETHERSCAN_API_KEY
forge build
forge test -vvset -a; source .env; set +a
forge script script/DeployFactory.s.sol:DeployFactory \
--rpc-url $SEPOLIA_RPC_URL \
--broadcast \
--verify \
--etherscan-api-key $ETHERSCAN_API_KEY \
-vvvvThis deploys the Share implementation, Property implementation, Factory, Lens, and one Genesis property in a single broadcast. Customize the genesis property with env vars:
GENESIS_DISPLAY_NAME="Brooklyn Brownstone" \
GENESIS_CITY="Brooklyn, NY" \
GENESIS_TOKEN_SYMBOL="BKB" \
GENESIS_VALUE_USD=1500000000000 \
LAUNCH_FEE_WEI=1000000000000000 \
forge script script/DeployFactory.s.sol:DeployFactory …Addresses are written to contracts/deployments/latest.json.
cd frontend
npm install
cp .env.example .env.local
# paste factory + lens addresses into NEXT_PUBLIC_FACTORY_ADDRESS / NEXT_PUBLIC_LENS_ADDRESS
npm run devVisit http://localhost:3000, connect a Sepolia-funded wallet, browse an existing property, or launch your own.
# Upgrade the Share implementation for a given proxy
PROXY_ADDRESS=0xShareProxy \
PROXY_KIND=share \
forge script script/Upgrade.s.sol:Upgrade --rpc-url $SEPOLIA_RPC_URL --broadcast --verify -vvvv
# Upgrade the Property implementation
PROXY_ADDRESS=0xPropertyProxy \
PROXY_KIND=property \
forge script script/Upgrade.s.sol:Upgrade --rpc-url $SEPOLIA_RPC_URL --broadcast --verify -vvvvEach proxy upgrades independently. The Factory and Lens are immutable by design — to change their behaviour, deploy a new one and point the UI at it.
adaptive-coownership/
├── contracts/ Foundry project
│ ├── src/
│ │ ├── StakeholdFactory.sol Launchpad + registry + fee sink
│ │ ├── StakeholdShare.sol ERC20Votes + Permit + yield accumulator
│ │ ├── StakeholdProperty.sol Governance + vesting + metadata
│ │ └── StakeholdLens.sol Read aggregator for the UI
│ ├── test/
│ │ ├── Base.t.sol Shared harness
│ │ ├── StakeholdShare.t.sol
│ │ ├── StakeholdProperty.t.sol
│ │ ├── StakeholdFactory.t.sol
│ │ ├── StakeholdLens.t.sol
│ │ ├── Invariant.t.sol
│ │ └── Upgrade.t.sol
│ ├── script/
│ │ ├── DeployFactory.s.sol Canonical deployment
│ │ └── Upgrade.s.sol UUPS upgrade (Share | Property)
│ └── foundry.toml
│
└── frontend/ Next.js 14 App Router + TS + Tailwind
├── app/
│ ├── page.tsx Home — hero + featured + list
│ ├── properties/ Browse every property
│ ├── launch/ Launch a new property via factory
│ ├── portfolio/ Your holdings across properties
│ ├── p/[address]/ Per-property dashboard + subroutes
│ │ ├── page.tsx Overview (stats, ownership, activity)
│ │ ├── contribute/ IPFS upload → submitContribution
│ │ ├── proposals/ Vote + execute
│ │ ├── yield/ Claim / deposit ETH
│ │ └── rebalance/ Vesting grants + preview
│ ├── about/ User-facing docs, guides, FAQ
│ └── api/ipfs/route.ts Filebase proxy (token stays on server)
├── components/ Logo · Header · PropertyCard · TxButton …
├── hooks/ useProperty · useProperties · useUserPosition …
└── lib/
├── abis/ Auto-generated TS ABIs (share, property, factory, lens)
├── contracts.ts Address wiring + typed configs
├── wagmi.ts wagmi config (Sepolia)
├── ipfs.ts Gateway helper (provider-agnostic)
└── format.ts Number, USD, ETH, duration formatters
- Factory-based multi-property launchpad
- Per-property isolated Share + Property proxies
- Privacy-aware metadata (public city, gated legal docs)
- Read aggregator (Lens) for frontend efficiency
- Provider-agnostic IPFS pinning (Filebase today, zero-migration swap)
- In-app admin console (rotate legal docs, pause, governance params, role grants)
- In-app shareholder actions (transfer shares, delegate votes)
- Subgraph / Ponder indexer for historical analytics at scale
- Secondary market for share transfers (AMM pool per property)
- On-chain valuation oracle (currently admin-submitted)
- Timelock + Safe multisig for
UPGRADER_ROLEon mainnet - Full security audit before mainnet
Stakehold is deployed to Sepolia only. The gap between "works on Sepolia" and "can hold real capital on mainnet" is non-trivial; the checklist below is the real list we'd burn down before any mainnet deployment. Items marked ✅ are in place today; items marked ◻ are deliberate follow-ups.
Contracts
- ✅ UUPS proxies with
_authorizeUpgraderole-gated - ✅ Reentrancy guards + checks-effects-interactions on every ETH path
- ✅
__gapreserved on every upgradeable implementation - ✅ Pausable without locking users out of earned yield
- ✅ 74+ unit / fuzz / invariant / upgrade-roundtrip tests
- ◻ External audit (Trail of Bits, Spearbit, OpenZeppelin, etc.)
- ◻ Formal verification of critical invariants (balances ≤ supply, ETH ≥ pending yield)
- ◻ Immunefi bug bounty funded before launch
Governance & access control
- ✅ Separate
DEFAULT_ADMIN_ROLE,PAUSER_ROLE,UPGRADER_ROLE - ✅ Factory renounces all roles atomically after launch
- ✅ In-app role-grant UI to hand control to a multisig without Etherscan calls
- ◻ Safe multisig (3-of-5 minimum) as admin on every launched property
- ◻ OpenZeppelin
TimelockControllerin front ofUPGRADER_ROLE(48h minimum) - ◻ Renounce launcher EOA after verified multisig handoff
Valuations & oracles
- ◻ Chainlink (or equivalent) price feed for ETH/USD conversions at yield deposit
- ◻ Attested valuation oracle for
propertyValueUsd(signed by ≥ 2 independent appraisers; current field is admin-settable) - ◻ Circuit breaker on rebalance math if valuation changes more than N% per epoch
Monitoring & ops
- ◻ OpenZeppelin Defender sentinels for: paused state changes, role grants, proposal executions, large yield deposits, upgrade calls
- ◻ Tenderly alerts on revert spikes + gas anomalies
- ◻ On-call runbook (who flips the pause, who signs multisig, who rotates keys)
- ◻ Subgraph + analytics so shareholders can audit rebalance math historically
Frontend / off-chain
- ✅ Server-side IPFS proxy keeps pinning credentials off the browser
- ✅ Provider-agnostic gateway selection
- ✅ Graceful rendering when metadata URIs are missing or malformed
- ◻ Second pinning provider + automatic failover
- ◻ CSP, subresource integrity, and rate-limited
/api/ipfs - ◻ Replace WalletConnect v2 with SIWE session management for admin paths
Legal & regulatory — full treatment in LEGAL.md
- ◻ Entity wrapper per property (LLC / Series LLC / DST / Wyoming DAO-LLC / DUNA) holding the recorded deed
- ◻ Securities registration (Reg D 506(b)/(c), Reg A+, Reg CF, or non-US equivalents)
- ◻ Transfer-restriction module (ERC-3643 / ERC-1400) + registered transfer agent
- ◻ KYC / AML allowlist on share transfers for security-classified properties
- ◻ Operating agreement with a recognised on-chain proposals clause binding the LLC manager to on-chain votes
- ◻ Annual K-1 / 1099 pipeline keyed to verified identity
- ◻ Licensed property-management agreement (broker licensing where required)
- ◻ Terms of service, privacy policy, offering documents
Responsible-disclosure policy lives in SECURITY.md. Full legal-stack walkthrough in LEGAL.md.
MIT — see LICENSE.