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 seconds

Execute 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:

  1. New YRTToken contract deployed

  2. 1000 YRT minted to your address

  3. Period 1 automatically started

  4. Maturity date set (now + 180 seconds)

  5. 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:

  1. New OwnaPool contract deployed

  2. Pair: YRT/USDC

  3. Pool is empty (no liquidity yet)

  4. 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/YRT

Recommended:

  • 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:

  1. 500 YRT transferred to pool

  2. 500 USDC transferred to pool

  3. You receive LP tokens

  4. Pool is now active!

  5. 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 yet

Monitor 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:

  1. 50,000 USDC transferred from your wallet to factory

  2. Yield recorded for Period 1

  3. Ready for distribution after snapshot


Step 6: Snapshot & Distribution

No action required!

Chainlink Automation will:

  1. Monitor maturity date

  2. 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 maturity

Monitor 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:

  1. Gets snapshot holders list (frozen at maturity)

  2. Calculates proportional yield for each holder

  3. Transfers USDC to each holder automatically

  4. 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 USDC

Step 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:

  1. Fundraising period (90 days)

  2. Deposit yield

  3. Snapshot (automatic)

  4. Distribution

  5. 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

  1. Deposit yield early (1-2 days before maturity)

    • Ensures Chainlink can trigger snapshot

    • Avoids last-minute issues

  2. Monitor Chainlink Upkeep

    • Check status: https://automation.chain.link/base-sepolia

    • Ensure LINK balance > 5 LINK

  3. Distribute immediately after snapshot

    • Better UX for users

    • Shows professionalism

Communication

  1. Announce periods in advance

    • Share maturity dates

    • Expected yield amounts

    • Distribution timeline

  2. Update users on progress

    • "Yield deposited ✅"

    • "Snapshot taken ✅"

    • "Distribution complete ✅"

  3. Transparency

    • Share contract addresses

    • Verify on BaseScan

    • Publish yield calculations

Risk Management

  1. Start with demo periods (3-10 minutes)

    • Test full flow

    • Verify automation works

    • Fix issues before production

  2. Use testnet first (Base Sepolia)

    • No real money at risk

    • Learn the system

    • Train your team

  3. Have yield ready

    • Don't promise yield you can't deliver

    • Maintain USDC balance

    • Plan for periods in advance


Troubleshooting

Snapshot Not Triggered

Check:

  1. Maturity date reached?

  2. Yield deposited?

  3. 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:

  1. Snapshot taken?

  2. Sufficient USDC balance in factory?

  3. Gas limit (use batchDistribute for many holders)?



Congratulations! Your property is now tokenized! 🎉

Need Help?


Last Updated: January 2025

Last updated