Property Onboarding
Complete guide for property owners to tokenize property cash flows and manage yield distribution.
Who Is This For?
Property Owners - Want to raise capital by tokenizing future cash flows
Property Managers - Managing tokenized properties
Real Estate Companies - Looking to create liquid markets for their assets
Complete Onboarding Flow
( Required ) Property Information Ready
Before starting, prepare:
Property name (e.g., "Green Valley Apartment")
Token name (e.g., "Green Valley YRT")
Token symbol (e.g., "GV-YRT")
Initial supply (e.g., 1000 tokens)
Token price (e.g., 1 USDC per YRT)
Fundraising duration (e.g., 180 seconds for demo, 7,776,000 for 90 days)
Step 1: Create YRT Token
Understanding Parameters
function createSeries(
string memory _name, // "Green Valley YRT"
string memory _symbol, // "GV-YRT"
string memory _propertyName, // "Green Valley Apartment"
uint256 _initialSupply, // 1000e18 (1000 tokens)
address _underlyingToken, // USDC address
uint256 _tokenPrice, // 1e18 (1 USDC = 1 YRT)
uint256 _fundraisingDuration // 180 (seconds)
) external returns (uint256 seriesId)Duration Examples:
Demo/Testing:
- 3 minutes = 180 seconds
- 10 minutes = 600 seconds
Production:
- 30 days = 2,592,000 seconds
- 90 days = 7,776,000 seconds
- 180 days = 15,552,000 secondsExecute Creation
Using TypeScript/Frontend:
import { ethers } from "ethers";
import FactoryABI from "@/abis/YRTFactory.json";
const FACTORY_ADDRESS = "0x7698c369Cec5bFD14bFe9184ea19D644540f483b";
const USDC_ADDRESS = "0x70667aea00Fc7f087D6bFFB9De3eD95Af37140a4";
const factory = new ethers.Contract(FACTORY_ADDRESS, FactoryABI, signer);
// Create series
const tx = await factory.createSeries(
"Green Valley YRT", // Token name
"GV-YRT", // Token symbol
"Green Valley Apartment", // Property name
ethers.utils.parseUnits("1000", 18), // 1000 tokens
USDC_ADDRESS, // Underlying token
ethers.utils.parseUnits("1", 18), // 1 USDC per token
180 // 3 minutes (demo)
);
const receipt = await tx.wait();
// Get seriesId from event
const event = receipt.events?.find(e => e.event === "SeriesCreated");
const seriesId = event?.args?.seriesId;
console.log(`✅ Series created! ID: ${seriesId}`);
// Get YRT token address
const seriesInfo = await factory.seriesInfo(seriesId);
const yrtAddress = seriesInfo[0];
console.log(`YRT Token: ${yrtAddress}`);What Happens:
New YRTToken contract deployed
1000 YRT minted to your address
Period 1 automatically started
Maturity date set (now + 180 seconds)
Returns
seriesId(e.g., 1)
Step 2: Create DEX Pool
Why Create Pool?
Direct purchase is disabled. Users must buy via DEX for:
Fair price discovery
Liquidity incentives
Secondary market trading
Execute Pool Creation
import DexFactoryABI from "@/abis/OwnaFactory.json";
const DEX_FACTORY = "0xc493CC506aDF88A26cd72F45503D1DE6c3A085EF";
const dexFactory = new ethers.Contract(DEX_FACTORY, DexFactoryABI, signer);
// Create pool
const tx = await dexFactory.createPool(
yrtAddress, // YRT token from Step 1
USDC_ADDRESS, // USDC
"Green Valley Apartment" // Property name
);
const receipt = await tx.wait();
// Get pool address from event
const event = receipt.events?.find(e => e.event === "PoolCreated");
const poolAddress = event?.args?.pool;
console.log(`✅ Pool created: ${poolAddress}`);What Happens:
New OwnaPool contract deployed
Pair: YRT/USDC
Pool is empty (no liquidity yet)
Returns
poolAddress
Step 3: Add Initial Liquidity
Understanding Initial Liquidity
Initial liquidity sets the starting price:
Price = Reserve_USDC / Reserve_YRT
Example:
500 YRT + 500 USDC → Price = 1 USDC/YRT
500 YRT + 1000 USDC → Price = 2 USDC/YRT
1000 YRT + 500 USDC → Price = 0.5 USDC/YRTRecommended:
Add 50% of initial supply
Example: 1000 supply → add 500 YRT
Match with equal USDC value
Result: 1:1 starting price
Execute Liquidity Addition
import RouterABI from "@/abis/OwnaRouter.json";
const ROUTER = "0xE5b501b4d2CCD234FcD94906C561B8d5B1e4cEb7";
const router = new ethers.Contract(ROUTER, RouterABI, signer);
const yrt = new ethers.Contract(yrtAddress, ERC20_ABI, signer);
const usdc = new ethers.Contract(USDC_ADDRESS, ERC20_ABI, signer);
// Step 1: Approve tokens
await yrt.approve(ROUTER, ethers.utils.parseUnits("500", 18));
await usdc.approve(ROUTER, ethers.utils.parseUnits("500", 18));
console.log("✅ Tokens approved");
// Step 2: Add liquidity
const deadline = Math.floor(Date.now() / 1000) + 60 * 20; // 20 minutes
const tx = await router.addLiquidity(
yrtAddress,
USDC_ADDRESS,
ethers.utils.parseUnits("500", 18), // 500 YRT
ethers.utils.parseUnits("500", 18), // 500 USDC
ethers.utils.parseUnits("500", 18), // Min YRT (no slippage for first LP)
ethers.utils.parseUnits("500", 18), // Min USDC
ownerAddress, // LP tokens recipient
deadline,
"Green Valley Apartment"
);
await tx.wait();
console.log("✅ Liquidity added!");
console.log("Pool state: 500 YRT + 500 USDC");
console.log("Starting price: 1 USDC/YRT");What Happens:
500 YRT transferred to pool
500 USDC transferred to pool
You receive LP tokens
Pool is now active!
Users can start trading
Step 4: Fundraising Period
What Happens During Fundraising
Duration: 180 seconds (or configured duration)
Activities:
✅ Users can buy YRT on DEX
✅ Users can sell YRT on DEX
✅ Price changes based on supply/demand
✅ Trading volume accumulates
✅ Holder list changes dynamically
Restrictions:
❌ Cannot mint new tokens
❌ Cannot trigger snapshot yet
❌ Cannot deposit yield yetMonitor Activity
Track pool state:
const pool = new ethers.Contract(poolAddress, PoolABI, provider);
// Get reserves
const reserves = await pool.getReserves();
console.log(`Reserve YRT: ${ethers.utils.formatUnits(reserves[0], 18)}`);
console.log(`Reserve USDC: ${ethers.utils.formatUnits(reserves[1], 18)}`);
// Calculate current price
const price = reserves[1] / reserves[0];
console.log(`Current price: ${price.toFixed(4)} USDC/YRT`);Track token distribution:
const yrt = new ethers.Contract(yrtAddress, TokenABI, provider);
// Get current holders
const holdersCount = await yrt.getHoldersCount();
console.log(`Total holders: ${holdersCount}`);
// Get your balance
const balance = await yrt.balanceOf(ownerAddress);
console.log(`Your balance: ${ethers.utils.formatUnits(balance, 18)} YRT`);Wait for Maturity
// Check maturity date
const maturityDate = await yrt.periodMaturityDate(1);
const now = Math.floor(Date.now() / 1000);
const remaining = maturityDate - now;
console.log(`Time to maturity: ${remaining} seconds`);
if (remaining <= 0) {
console.log("✅ Period matured! Ready for yield deposit");
}Step 5: Deposit Yield
When to Deposit
Timing:
After maturity date reached
Before snapshot trigger
Recommended: 1-2 days before maturity for safety
Amount:
Based on property cash flow
Example: 50,000 USDC for quarterly rent collection
Execute Deposit
// Step 1: Approve USDC
const yieldAmount = ethers.utils.parseUnits("50000", 18); // 50k USDC
await usdc.approve(FACTORY_ADDRESS, yieldAmount);
console.log("✅ USDC approved");
// Step 2: Deposit yield
const tx = await factory.depositYield(
seriesId, // 1
1, // Period 1
yieldAmount // 50,000 USDC
);
await tx.wait();
console.log("✅ Yield deposited: 50,000 USDC");What Happens:
50,000 USDC transferred from your wallet to factory
Yield recorded for Period 1
Ready for distribution after snapshot
Step 6: Snapshot & Distribution
Automatic Snapshot (Chainlink)
No action required!
Chainlink Automation will:
Monitor maturity date
Trigger snapshot automatically when:
Maturity date reached ✅
Yield deposited ✅
Snapshot not taken yet ✅
Timeline:
Maturity date: Day 90, 00:00:00 UTC
Chainlink check: Every block (~2 seconds)
Snapshot trigger: Within minutes of maturityMonitor snapshot:
// Check if snapshot taken
const snapshotTaken = await yrt.isSnapshotTakenForPeriod(1);
if (snapshotTaken) {
console.log("✅ Snapshot taken!");
// Get snapshot data
const totalSupply = await yrt.getSnapshotTotalSupplyForPeriod(1);
const holders = await yrt.getSnapshotHoldersForPeriod(1);
console.log(`Snapshot total supply: ${ethers.utils.formatUnits(totalSupply, 18)}`);
console.log(`Snapshot holders: ${holders.length}`);
}Manual Distribution
After snapshot, you must trigger distribution:
import AutoDistributorABI from "@/abis/AutoDistributor.json";
const AUTO_DISTRIBUTOR = "0x8C9edAB077038B4f2e74d79663d79f3fc12Ca945";
const autoDistributor = new ethers.Contract(
AUTO_DISTRIBUTOR,
AutoDistributorABI,
signer
);
// Distribute to all snapshot holders
const tx = await autoDistributor.distributeToAllHolders(
seriesId, // 1
1 // Period 1
);
await tx.wait();
console.log("✅ Yield distributed to all holders!");What Happens:
Gets snapshot holders list (frozen at maturity)
Calculates proportional yield for each holder
Transfers USDC to each holder automatically
Users receive USDC in wallets (no claim needed)
Example Distribution:
Total Yield: 50,000 USDC
Total Supply at Snapshot: 1000 YRT
Holders:
- Alice: 250 YRT (25%) → Receives: 12,500 USDC
- Bob: 200 YRT (20%) → Receives: 10,000 USDC
- Charlie: 150 YRT (15%) → Receives: 7,500 USDC
- You (owner): 400 YRT (40%) → Receives: 20,000 USDCStep 7: Start New Period (Optional)
When to Start New Period
Typical schedule:
Period 1 (Q1): Jan-Mar → Distribute → Start Period 2
Period 2 (Q2): Apr-Jun → Distribute → Start Period 3
Period 3 (Q3): Jul-Sep → Distribute → Start Period 4
Period 4 (Q4): Oct-Dec → Distribute → End or continue
Execute New Period
// Calculate duration (90 days = 7,776,000 seconds)
const durationSeconds = 90 * 24 * 60 * 60; // 7,776,000
// Start Period 2
const tx = await factory.startNewPeriod(
seriesId, // 1
durationSeconds // 90 days
);
await tx.wait();
console.log("✅ Period 2 started!");
console.log(`Duration: 90 days (${durationSeconds} seconds)`);
// Check new maturity date
const yrt = new ethers.Contract(yrtAddress, TokenABI, provider);
const maturityDate = await yrt.periodMaturityDate(2);
console.log(`Period 2 maturity: ${new Date(maturityDate * 1000)}`);Cycle repeats:
Fundraising period (90 days)
Deposit yield
Snapshot (automatic)
Distribution
Start Period 3...
Supply Expansion
When to Expand Supply
Allowed during GAP period:
After snapshot taken
Before next period starts
Use case:
Property generates more cash flow
Want to raise more capital
Expand tokenization
Execute Mint
// Check if in GAP period
const period2Started = await yrt.currentPeriod() === 2;
const period1Snapshot = await yrt.isSnapshotTakenForPeriod(1);
if (period1Snapshot && !period2Started) {
console.log("✅ In GAP period, can mint");
// Mint 500 more tokens
const tx = await factory.mintTokens(
seriesId, // 1
ownerAddress, // Recipient
ethers.utils.parseUnits("500", 18) // 500 tokens
);
await tx.wait();
console.log("✅ Minted 500 YRT");
console.log("New total supply: 1500 YRT");
} else {
console.log("❌ Not in GAP period");
}Add minted tokens to pool:
// Add new liquidity with minted tokens
await yrt.approve(ROUTER, ethers.utils.parseUnits("500", 18));
await usdc.approve(ROUTER, ethers.utils.parseUnits("500", 18));
await router.addLiquidity(
yrtAddress,
USDC_ADDRESS,
ethers.utils.parseUnits("500", 18),
ethers.utils.parseUnits("500", 18),
0,
0,
ownerAddress,
deadline,
"Green Valley Apartment"
);
console.log("✅ New liquidity added to pool");Best Practices
Timing
Deposit yield early (1-2 days before maturity)
Ensures Chainlink can trigger snapshot
Avoids last-minute issues
Monitor Chainlink Upkeep
Check status: https://automation.chain.link/base-sepolia
Ensure LINK balance > 5 LINK
Distribute immediately after snapshot
Better UX for users
Shows professionalism
Communication
Announce periods in advance
Share maturity dates
Expected yield amounts
Distribution timeline
Update users on progress
"Yield deposited ✅"
"Snapshot taken ✅"
"Distribution complete ✅"
Transparency
Share contract addresses
Verify on BaseScan
Publish yield calculations
Risk Management
Start with demo periods (3-10 minutes)
Test full flow
Verify automation works
Fix issues before production
Use testnet first (Base Sepolia)
No real money at risk
Learn the system
Train your team
Have yield ready
Don't promise yield you can't deliver
Maintain USDC balance
Plan for periods in advance
Troubleshooting
Snapshot Not Triggered
Check:
Maturity date reached?
Yield deposited?
Chainlink Upkeep funded?
Manual trigger (if needed):
await factory.triggerSnapshotForPeriod(seriesId, periodId);Cannot Mint Tokens
Error: "Cannot mint during fundraising period"
Cause: Trying to mint during active fundraising
Solution: Wait for snapshot, then mint in GAP period
Distribution Failed
Check:
Snapshot taken?
Sufficient USDC balance in factory?
Gas limit (use batchDistribute for many holders)?
Related Documentation
Quick Start Guide - Overview for all users
User Trading Guide - Investor perspective
Snapshot System - How snapshots work
Chainlink Automation - Automatic triggers
Contract Addresses - Deployed contracts
Congratulations! Your property is now tokenized! 🎉
Need Help?
Last Updated: January 2025
Last updated
