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.