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 β
βββββββββββ
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();
}
}
}
The Magic: Zero-Fee Oracle Integration
Notice how simple the oracle integration is:
- Import Flare's
ContractRegistry - Get FTSOv2 instance:
ContractRegistry.getFtsoV2() - 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);
}
}
How FDC Works
FDC allows smart contracts to fetch and verify any Web2 data:
- Request: Contract requests data from URL
- Attestation: ~100 independent providers fetch and attest data
- Consensus: 50%+ agreement required
- 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;
}
}
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 };
}
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
Test the complete flow:
- Generate property metadata with AI
- Upload to IPFS
- Validate with FDC (~90s)
- Create property NFT
- 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(...);
π― Key Takeaways
- Zero Oracle Fees: FTSOv2 provides real-time prices without any cost
- Decentralized Validation: FDC ensures metadata integrity without centralized APIs
- Production Ready: Clone pattern + batch operations = efficient gas usage
- 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?
- π Flare Docs
- π¬ X/Twitter Dev Community
- π Flare Official Website
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)