DEV Community

Cover image for Building a Real Estate Tokenization dApp with Flare's Zero-Fee Oracles
Delamare
Delamare

Posted on

Building a Real Estate Tokenization dApp with Flare's Zero-Fee Oracles

Building a Real Estate Tokenization dApp with Flare's Zero-Fee Oracles

How to leverage FTSOv2 and FDC to create a production-ready RWA marketplace without oracle costs

πŸ—οΈ What We're Building

Imagine tokenizing real-world assets without paying thousands in oracle fees. Today, we'll build a complete real estate tokenization platform using Flare Network's enshrined oracles - FTSOv2 for real-time price feeds and FDC for metadata validation.

By the end of this tutorial, you'll have:

  • βœ… A property NFT system with fractional ownership
  • βœ… Real-time ETH/USD pricing without oracle fees
  • βœ… Decentralized metadata validation via IPFS
  • βœ… A production-ready marketplace with AI-generated content

Live Demo: Tokenized Real Estate on Flare

🎯 Why Flare Changes Everything for RWAs

Traditional oracle solutions charge hefty fees - Chainlink can cost $0.50-$5 per price update. For a DeFi protocol making hundreds of daily updates, that's thousands in monthly costs.

Flare's approach is radically different:

  • FTSOv2: Block-latency price feeds (every ~1.8s) with zero fees
  • FDC: Decentralized attestation for any Web2/Web3 data
  • Enshrined Security: Oracle security equals network security

Let's see this in action by building a real estate marketplace.

πŸ“‹ Prerequisites

  • Node.js 18+ and MetaMask wallet
  • Basic Solidity and React knowledge
  • Flare Coston2 testnet setup (Flare's coston2 testnet faucet here)
  • Mistral AI account for Mistral API (free tier works)
  • Pinata account for IPFS (free tier works)

🏭 Architecture Overview

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   PropertyNFT   │────▢│ PropertyFactory │◀────│  Marketplace    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚                       β”‚                         β”‚
         β”‚                   β”Œβ”€β”€β”€β–Όβ”€β”€β”€β”€β”              β”Œβ”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”
         β”‚                   β”‚ FTSOv2 β”‚              β”‚   FDC    β”‚
         β”‚                   β”‚ Oracle β”‚              β”‚Validator β”‚
         β”‚                   β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚
    β”Œβ”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”
    β”‚  IPFS   β”‚
    β”‚Metadata β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Enter fullscreen mode Exit fullscreen mode

Step 1: Setting Up the Smart Contracts

First, let's create our core contracts. The beauty of Flare is that oracle integration is just a few lines of code.

PropertyTokenFactory.sol - The Heart of Our System

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import {ContractRegistry} from "@flarenetwork/flare-periphery-contracts/coston2/ContractRegistry.sol";
import {FtsoV2Interface} from "@flarenetwork/flare-periphery-contracts/coston2/FtsoV2Interface.sol";
import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol";

contract PropertyTokenFactory is AccessControl, ReentrancyGuard {
    // Oracle interface - zero setup required!
    FtsoV2Interface private ftsoV2;

    // ETH/USD feed ID on Flare
    bytes21 private constant ETH_USD_FEED_ID = 0x014554482f55534400000000000000000000000000;

    // Fixed price per token
    uint256 public constant TOKEN_PRICE_USD = 50 * 1e18;

    constructor(address _admin, address _nftContract) {
        // Auto-initialize FTSO - it's that simple!
        ftsoV2 = ContractRegistry.getFtsoV2();

        _grantRole(DEFAULT_ADMIN_ROLE, _admin);
        nftContract = IERC721(_nftContract);

        // Deploy token template for cloning
        tokenImplementation = address(new PropertyToken());
    }

    function createPropertyToken(
        uint256 _nftId,
        uint256 _surfaceM2,
        address _propertyOwner
    ) external onlyRole(CREATOR_ROLE) returns (address token) {
        // Calculate property value: surface Γ— 1 ETH Γ— Oracle Price
        uint256 propertyValueETH = _surfaceM2 * 1 ether;

        // Get real-time ETH price - NO FEES!
        uint256 ethPriceUSD = getETHPrice();

        // Calculate tokens to mint
        uint256 valuationUSD = (propertyValueETH * ethPriceUSD) / 1e18;
        uint256 totalSupply = valuationUSD / TOKEN_PRICE_USD;

        // Deploy token clone (90% gas savings)
        token = Clones.clone(tokenImplementation);

        // Initialize with calculated supply
        PropertyToken(token).initialize(
            string.concat("RWA Property #", _toString(_nftId)),
            string.concat("RWAP", _toString(_nftId)),
            _nftId,
            address(this),
            valuationUSD,
            _propertyOwner
        );

        emit PropertyTokenCreated(_nftId, token, _propertyOwner, totalSupply);
    }

    function getETHPrice() public returns (uint256 price) {
        // Magic happens here - direct oracle access!
        try ftsoV2.getFeedById(ETH_USD_FEED_ID) returns (
            uint256 value,
            int8 decimals,
            uint64 timestamp
        ) {
            // Normalize to 18 decimals
            price = decimals < 18 
                ? value * 10**(18 - uint8(decimals))
                : value / 10**(uint8(decimals) - 18);

            emit PriceRecorded(price, timestamp);
        } catch {
            revert Factory__OracleError();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

The Magic: Zero-Fee Oracle Integration

Notice how simple the oracle integration is:

  1. Import Flare's ContractRegistry
  2. Get FTSOv2 instance: ContractRegistry.getFtsoV2()
  3. Call getFeedById() - that's it!

No API keys, no subscription fees, no complex setups. The oracle is part of the blockchain itself.

Step 2: Validating Metadata with FDC

Now let's add metadata validation using Flare Data Connector's Web2JSON attestation:

FDCValidator.sol - Ensuring Data Integrity

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {IFdcHub} from "@flarenetwork/flare-periphery-contracts/coston2/IFdcHub.sol";
import {IFdcVerification} from "@flarenetwork/flare-periphery-contracts/coston2/IFdcVerification.sol";
import {IWeb2Json} from "@flarenetwork/flare-periphery-contracts/coston2/IWeb2Json.sol";
import {ContractRegistry} from "@flarenetwork/flare-periphery-contracts/coston2/ContractRegistry.sol";
import {IFdcRequestFeeConfigurations} from "@flarenetwork/flare-periphery-contracts/coston2/IFdcRequestFeeConfigurations.sol";
import {IFlareSystemsManager} from "@flarenetwork/flare-periphery-contracts/coston2/IFlareSystemsManager.sol";

contract FDCValidator is Ownable {
    mapping(string => bool) public validatedURIs;

    string public constant PINATA_GATEWAY = "https://gateway.pinata.cloud/ipfs/";
    uint256 public constant VALIDATION_FEE = 0.01 ether;

    function validateProperty(string calldata _ipfsUri) external payable {
        if (validatedURIs[_ipfsUri]) revert AlreadyValidated();
        if (msg.value < VALIDATION_FEE) revert InsufficientFee();

        // Extract CID and build gateway URL
        string memory cid = extractCID(_ipfsUri);
        string memory gatewayUrl = string(abi.encodePacked(
            PINATA_GATEWAY,
            cid
        ));

        // Prepare Web2JSON request
        IWeb2Json.RequestBody memory web2JsonRequest = IWeb2Json.RequestBody({
            url: gatewayUrl,
            httpMethod: "GET",
            headers: '{"Content-Type": "application/json"}',
            queryParams: "{}",
            body: "{}",
            postProcessJq: ".",  // Get entire JSON object
            abiSignature: _getMetadataAbiSignature()
        });

        // Submit to FDC
        bytes memory encodedRequest = _encodeRequest(web2JsonRequest);
        IFdcHub fdcHub = ContractRegistry.getFdcHub();
        fdcHub.requestAttestation{value: msg.value}(encodedRequest);

        emit ValidationRequested(_ipfsUri, getCurrentRoundId());
    }

    function confirmValidation(
        string calldata _ipfsUri,
        IWeb2Json.Proof calldata _proof
    ) external {
        // Verify proof through FDC
        IFdcVerification verification = ContractRegistry.getFdcVerification();
        if (!verification.verifyJsonApi(_proof)) revert InvalidProof();

        // Decode and validate metadata
        (string memory name, string memory description, string memory image) = 
            abi.decode(_proof.data.responseBody.abiEncodedData, (string, string, string));

        require(bytes(name).length > 0, "Invalid metadata");

        validatedURIs[_ipfsUri] = true;
        emit ValidationCompleted(_ipfsUri);
    }
}
Enter fullscreen mode Exit fullscreen mode

How FDC Works

FDC allows smart contracts to fetch and verify any Web2 data:

  1. Request: Contract requests data from URL
  2. Attestation: ~100 independent providers fetch and attest data
  3. Consensus: 50%+ agreement required
  4. Verification: On-chain proof validation

This gives us decentralized verification of IPFS metadata - crucial for RWAs!

Step 3: Building the Marketplace

Let's tie everything together with a user-friendly marketplace:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol";
import {MerkleProof} from "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {ContractRegistry} from "@flarenetwork/flare-periphery-contracts/coston2/ContractRegistry.sol";
// import {TestFtsoV2Interface} from "@flarenetwork/flare-periphery-contracts/coston2/TestFtsoV2Interface.sol"; // Mode test
import {FtsoV2Interface} from "@flarenetwork/flare-periphery-contracts/coston2/FtsoV2Interface.sol"; // Mode prod

// components
import {FDCValidator} from "./FDCValidator.sol";

// Interfaces
interface IPropertyNFT is IERC721 {
    function safeMint(address to, string memory uri) external returns (uint256);
}

interface IPropertyTokenFactory {
    function createPropertyToken(
        uint256 nftId,
        uint256 surfaceM2,
        address propertyOwner
    ) external returns (address);

    function propertyTokens(uint256 nftId) external view returns (address);
    function grantRole(bytes32 role, address account) external;
    function CREATOR_ROLE() external view returns (bytes32);
}

contract PropertyMarketplace is AccessControl, ReentrancyGuard, Pausable {
    IPropertyNFT public immutable propertyNFT;
    IPropertyTokenFactory public immutable tokenFactory;
    FDCValidator public immutable fdcValidator;
    FtsoV2Interface private immutable ftsoV2;

    uint256 public constant TOKEN_PRICE_USD = 50 * 1e18;
    bytes21 private constant ETH_USD_FEED_ID = 0x014554482f55534400000000000000000000000000;

    function createProperty(
        string memory _tokenURI,
        uint256 _surfaceM2,
        bytes32[] calldata _ownerProof
    ) external nonReentrant whenNotPaused returns (uint256 nftId, address propertyToken) {
        // 1. Verify whitelist (Merkle proof)
        if (!_verifyMerkleProof(msg.sender, _ownerProof, merkleRootOwners)) {
            revert Marketplace__InvalidProof();
        }

        // 2. Ensure metadata is validated
        if (!fdcValidator.isValidated(_tokenURI)) {
            revert Marketplace__InvalidTokenUri();
        }

        // 3. Mint property NFT
        nftId = propertyNFT.safeMint(msg.sender, _tokenURI);

        // 4. Create fractional tokens
        propertyToken = tokenFactory.createPropertyToken(
            nftId,
            _surfaceM2,
            msg.sender
        );

        emit PropertyCreated(nftId, msg.sender, propertyToken, _surfaceM2);
    }

    function getTokenPriceInETH() public returns (uint256 priceInETH) {
        // Real-time conversion using FTSOv2
        uint256 ethPriceUSD = _getETHPrice();
        priceInETH = (TOKEN_PRICE_USD * 1e18) / ethPriceUSD;
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Frontend Integration

Here's how to integrate with the contracts using Wagmi v2:

// hooks/usePropertyCreation.js
import { useContractWrite, useWaitForTransaction } from 'wagmi';
import { parseEther } from 'viem';

export function usePropertyCreation() {
  const { writeAsync: validateProperty } = useContractWrite({
    address: FDC_VALIDATOR_ADDRESS,
    abi: FDCValidatorABI,
    functionName: 'validateProperty',
  });

  const { writeAsync: createProperty } = useContractWrite({
    address: MARKETPLACE_ADDRESS,
    abi: MarketplaceABI,
    functionName: 'createProperty',
  });

  const handlePropertyCreation = async (ipfsUri, surfaceM2, merkleProof) => {
    try {
      // Step 1: Validate metadata with FDC
      const validateTx = await validateProperty({
        args: [ipfsUri],
        value: parseEther('0.01'),
      });

      // Wait ~90-180s for FDC attestation
      await waitForAttestation(ipfsUri);

      // Step 2: Create property with validated metadata
      const createTx = await createProperty({
        args: [ipfsUri, surfaceM2, merkleProof],
      });

      return createTx;
    } catch (error) {
      console.error('Property creation failed:', error);
    }
  };

  return { handlePropertyCreation };
}
Enter fullscreen mode Exit fullscreen mode

Step 5: Testing on Coston2

Deploy and test your contracts:

# Deploy contracts
npx hardhat ignition deploy ./ignition/modules/RWADeploymentModule.js --network coston2 --verify

# Run tests
npx hardhat test --network coston2
Enter fullscreen mode Exit fullscreen mode

Test the complete flow:

  1. Generate property metadata with AI
  2. Upload to IPFS
  3. Validate with FDC (~90s)
  4. Create property NFT
  5. Trade fractional tokens

πŸš€ Production Optimizations

Gas Optimization with Clones

Using ERC-1167 minimal proxy pattern saves ~90% deployment gas:

// Instead of deploying full contract each time
PropertyToken newToken = new PropertyToken(...); // ~2M gas

// Use clone pattern
address token = Clones.clone(implementation); // ~200k gas
PropertyToken(token).initialize(...);
Enter fullscreen mode Exit fullscreen mode

🎯 Key Takeaways

  1. Zero Oracle Fees: FTSOv2 provides real-time prices without any cost
  2. Decentralized Validation: FDC ensures metadata integrity without centralized APIs
  3. Production Ready: Clone pattern + batch operations = efficient gas usage
  4. Composable: Contracts work independently or together

🏁 What's Next?

You now have a complete RWA tokenization platform! Here are some ideas to extend it:

  • Add rental income distribution
  • Implement property governance voting
  • Create cross-chain property bridges
  • Build analytics dashboard

The complete code is available at: github.com/adelamare-blockchain/Flare-Network_RWA

🀝 Join the Flare Ecosystem

Ready to build the future of RWAs?

Remember: With Flare's enshrined oracles, you're not just saving on fees - you're building on infrastructure that's as secure as the blockchain itself. Ship your RWA dApp today! 🚒


Built with ❀️ for the Flare Ambassador Program by Antoine Delamare

Top comments (0)