Developer guide: Building a fractionalized real-estate dApp

Developer guide: Building a fractionalized real-estate dApp

Learn how to build a real estate dApp on top of Morph Holesky testnet.

Introduction

Imagine being able to invest in a high-value real estate property without needing to shell out a huge amount of money upfront. Sounds pretty appealing, right? This is the magic of fractionalization. By dividing a property into smaller, more affordable shares, fractionalization makes it possible for just about anyone to get in on the real estate action. It’s like turning a luxury mansion into a bunch of reasonably priced slices that you can buy and sell easily, just like stocks.

Our project today, Okido Finance, takes this idea and builds a decentralized application (DApp) around it. With Okido Finance, property owners can create shares of their real estate assets and investors can buy these shares using a custom ERC20 token. This not only democratizes real estate investment but also adds a layer of liquidity, making it easier to trade shares and get in or out of investments.

In this tutorial, I’ll walk you through building the Okido Finance DApp step by step. We’ll start with setting up the development environment, move on to deploying smart contracts, and finish with designing a user-friendly UI.

By the end, you’ll have a solid grasp of how to build a decentralized real estate fractionalization platform. Whether you’re looking to create something similar or just want to learn more about these technologies, you’ll be well-equipped to dive deeper into fractionalization. Let’s get started!

Objectives

By the end of this guide, you should be able to:

  • Use the Morph starter template.
  • Create and deploy smart contracts on Morph
  • Interact with your smart contract from the frontend.

Background on Morph

Morph exemplifies the application of modular design principles within the Layer 2 (L2) solutions. By adopting a modular architecture, Morph offers unparalleled flexibility, allowing developers to tailor their apps by integrating only the necessary modules. This strategic approach not only optimizes performance by reducing complexity but also ensures that the platform can swiftly adapt to technological advancements without overhauling the entire system. Morph's dedication to modular design sets it apart from conventional L2 solutions, fostering an environment where developers can unleash their creativity to build more efficient, scalable, and innovative decentralized applications (dApps).

Read more about morph and its modular architecture


What are we building?

We are building a fractionalized real-estate dApp that allows the owner to list properties for users to acquire shares or fractions of that property.


What we'll cover:

  1. Setting up your environment
  2. Creating your smart contract
  3. Deploying your smart contract
  4. Getting started with the frontend
  5. Interacting with your smart contract from the frontend


Step 1: Setting up your environment

Morph starter kit:

This guide will be built using the Morph starter kit . This kit abstracts away, all complexities involved in setting up and configuring your project. 

  • Go to the starter kit repo and click on “use this template”. This should create a github repository for you using the template.
  • Clone the repository to your local environment and in the contracts directory, run “forge compile” and also,  “yarn install” in the frontend directory.
  • Create a “.env” file and add your private key, alongside the RPC_URL for Morph testnet. Your env file should look like this:

PRIVATE_KEY= Your-Private-Key
RPC_URL=https://rpc-quicknode-holesky.morphl2.io

  • In the frontend folder, ensure you have installed the dependencies by running “yarn”  and then create a “.env.local” file. Paste in your wallet connect project id.

NEXT_PUBLIC_PROJECT_ID= your-wallet-connect-id

Get your wallet-connect id here

  • Run “yarn dev” on your terminal and your starter kit should look like this:

Step 2: Creating your smart contract

  • Navigate to the “src” folder in the “contracts” directory.
  • Create four files. “OkidoToken.sol”, “OkidoPropertyNFT.sol”, “FractionalOwnership.sol” and “OkidoFinance.sol”

OkidoToken.sol

This is the ERC20 token that is used as the default currency for the platform. It is used in buying shares of properties. 

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

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract OkidoToken is ERC20 {
    constructor(uint256 initialSupply) ERC20("Okido Token", "OKD") {
        _mint(msg.sender, initialSupply);
    }
}


OkidoPropertyNFT.sol

This allows the platform owner to mint NFTs representing the individual properties. All properties created are managed by this contract.

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

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract OkidoPropertyNFT is ERC721 {
    uint256 private _tokenIdCounter;
    mapping(uint256 => string) private _tokenURIs;
    mapping(uint256 => bool) private _tokenExists;

    constructor() ERC721("OkidoPropertyNFT", "OKIDONFT") {}

    function mint(address to, string memory uri) external  returns (uint256) {
        _tokenIdCounter++;
        uint256 newTokenId = _tokenIdCounter;
        _mint(to, newTokenId);
            _tokenExists[newTokenId] = true;
        _setTokenURI(newTokenId, uri);
   
        return newTokenId;
    }

    function _setTokenURI(uint256 tokenId, string memory _tokenURI) internal virtual {
        require(_tokenExists[tokenId], "ERC721Metadata: URI set of nonexistent token");
        _tokenURIs[tokenId] = _tokenURI;
    }

    function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
        require(_tokenExists[tokenId], "ERC721Metadata: URI query for nonexistent token");

        string memory _tokenURI = _tokenURIs[tokenId];
        string memory base = _baseURI();

        // If there is no base URI, return the token URI.
        if (bytes(base).length == 0) {
            return _tokenURI;
        }
        // If both are set, concatenate the base URI and tokenURI (via abi.encodePacked).
        if (bytes(_tokenURI).length > 0) {
            return string(abi.encodePacked(base, _tokenURI));
        }

        return super.tokenURI(tokenId);
    }
}


FractionalOwnership.sol

This contract manages the fractional shares of a property. Each property has its own fractionalOwnership contract.

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

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract FractionalOwnership is  ERC20 {
    uint256 public totalShares;
    address public owner;

    modifier onlyOwner () {
        require(msg.sender == owner, "Not owner");

        _;
    }

    constructor(string memory name, string memory symbol, uint256 totalShares_) ERC20(name, symbol) {
        totalShares = totalShares_;
        _mint(msg.sender, totalShares_);
        owner = msg.sender;
    }

    function transferShares(address to, uint256 shares) external onlyOwner {
        _transfer(owner, to, shares);
    }
}


OkidoFinance.sol

This is the main contract of the platform and handles:

  •  Property creation by minting a new NFT and deploying a new FractionalOwnership contract.
  • Listing properties and allowing users to buy fractional shares.
  • Keeping track of user investments in properties

The code for the okidoFinance contract can be found here.


Step 3: Deploying your smart contract

  • In the contract directory, navigate to the scripts folder and paste in the script below:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Script, console2} from "forge-std/Script.sol";
import {OkidoToken} from "../src/OkidoToken.sol";
import {OkidoPropertyNFT} from "../src/OkidoPropertyNFT.sol";
import {FractionalOwnership} from "../src/FractionalOwnership.sol";
import {OkidoFinance} from "../src/OkidoFinance.sol";


contract DeployerScript is Script {
    function setUp() public {}

  function run() public returns(OkidoFinance) {
        vm.startBroadcast();
        OkidoToken token = new OkidoToken(1000000);
        OkidoPropertyNFT propertyNft = new OkidoPropertyNFT();
      OkidoFinance okidoFinance = new OkidoFinance(address(propertyNft),    address(token));

        vm.stopBroadcast();
        return okidoFinance;
    }
}

  • In your terminal, run:

source  .env

  • Deploy your contracts by running the command below in your terminal

forge script script/Deployer.s.sol --rpc-url $RPC_URL --broadcast --legacy  --private-key $PRIVATE_KEY

  • You see the output below in your terminal

  • Note down the address of the token and that of Okido finance. You can validate if an address is the token address by pasting it in the Morph testnet explorer

Step 4: Getting started on the frontend

  • In the root directory of the project, navigate to the frontend directory.
  • In the constants folder, create two files “OkidoTokenAbi.ts” & “OkidoFinanceAbi.ts” respectively.
  • To get the ABIs, head back to the contract directory and navigate to the “out” folder.
  • In the “out” folder, locate the okido finance and okido token sub-folders and copy the json object.
  • Paste the json object in the new files you created in the “constants’’ folder in the frontend directory and export it.  Ensure you delete the bytecode so you are left with just the ABI array.

You can find an example of the OkidoFinanceAbi.ts file here

  • In the index.ts file, add the addresses of the Okido token and Okido finance which we got after deploying the contract. Your index.ts file should look like this:

import { OkidoTokenAbi } from "./okidoTokenAbi";
import { okidoFinanceAbi } from "./okidoFinanceAbi";

const okidoToken = "0xd44aD4876ab7CcA5C7235DE46cbf32948F487d4D";
const okidoFinance = "0xc694c74de5daff9D7b3cdE5518cC3B1f4c8204ac";

export { OkidoTokenAbi, okidoFinanceAbi, okidoToken, okidoFinance };


Step 5: Interacting with your smart contract from the frontend

Navigate to the “app” folder in the “src” directory.

Page.tsx

  • Copy the code for this file from here

Core function & hooks

In the page.tsx file, we need to retrieve and display the list of fractionalized properties available for investment. This involves interacting with our deployed smart contract to fetch the necessary data.

const {
    data: propertyData,
  } = useReadContract({
    abi: okidoFinanceAbi,
    address: okidoFinance,
    functionName: "listProperties",
  });

  useEffect(() => {
    if (propertyData) {
      setProperties(propertyData);
    }
  }, [propertyData]);

Here's what's happening in this code:

  1. Reading from the Contract:

const {
    data: propertyData,
  } = useReadContract({
    abi: okidoFinanceAbi,
    address: okidoFinance,
    functionName: "listProperties",
  });

  • useReadContract Hook: This hook is part of the Wagmi library, which we use to interact with our smart contract.
  • abi: This stands for Application Binary Interface. It’s a JSON array that describes the contract's methods and how to interact with them. Here, okidoFinanceAbi contains the ABI of our Okido Finance contract.
  • address: The address of our deployed Okido Finance contract on the blockchain.
  • functionName: This is the name of the function we want to call on the contract. In this case, we’re calling the listProperties function, which returns the list of all fractionalized properties.
  1. Handling the retrieved data

useEffect(() => {
  if (propertyData) {
    setProperties(propertyData);
  }
}, [propertyData]);

  • useEffect Hook: This is a React hook that lets us perform side effects in functional components. Here, we use it to update our component state whenever propertyData changes.
  • Conditional Check: The if (propertyData) condition ensures that we only attempt to update the state if propertyData is not null or undefined.
  • setProperties: This function updates the state with the retrieved property data, which will then be used to render the list of properties in our DApp.

AddPropertyModal.tsx

Copy the complete code for this modal here

Core functions and hooks

In our Okido Finance DApp, handling transactions smoothly and validating user input are crucial for a seamless user experience. This section demonstrates how we manage transaction states using React hooks and notifications, and how we validate and process form data for adding new properties.

Managing Transaction States

First, let's look at how we handle different transaction states such as confirming, confirmed, and error using the useEffect hook and toast notifications.

useEffect(() => {
  if (isConfirming) {
    toast.loading("Transaction Pending");
  }
  if (isConfirmed) {
    toast.success("Transaction Successful", {
      action: {
        label: "View on Etherscan",
        onClick: () => {
          window.open(`https://explorer-testnet.morphl2.io/tx/${hash}`);
        },
      },
    });
  }
  if (error) {
    toast.error("Transaction Failed");
  }
}, [isConfirming, isConfirmed, error, hash]);

Here's what this code does:

  1. React useEffect Hook: We use useEffect to perform side effects in our component. It runs when any of the dependencies (isConfirming, isConfirmed, error, hash) change.
  2. Transaction Pending: When isConfirming is true, we show a loading toast notification indicating that the transaction is pending.
  3. Transaction Successful: When isConfirmed is true, we display a success toast notification. The notification includes an action button labelled "View on Etherscan" that opens the transaction details on the Etherscan explorer.
  4. Transaction Failed: If there's an error, we show an error toast notification.

Validating and Processing Form Data

Next, let's validate user input and process the form data to add new properties to the Okido Finance DApp.

const formSchema = z.object({
  name: z.any(),
  symbol: z.any(),
  uri: z.any(),
  totalShares: z.any(),
  pricePerShare: z.any(),
});

const form = useForm<z.infer<typeof formSchema>>({
  resolver: zodResolver(formSchema),
  defaultValues: {
    name: "",
    symbol: "",
    uri: "",
    totalShares: 0,
    pricePerShare: 0,
  },
});

  1. Form Schema: We define a schema using zod to enforce validation rules on the form data. Here, we use z.any() for simplicity, but in a real-world scenario, you should use more specific validators.
  2. useForm Hook: The useForm hook from react-hook-form initializes the form with the specified schema and default values. This helps in managing form state and validation.

Adding a New Property

Finally, let's implement the function to add a new property:

const AddProperty = async (data: z.infer<typeof formSchema>) => {
  console.log(data);
  try {
    const addPropertyTx = await writeContractAsync({
      address: okidoFinance,
      abi: okidoFinanceAbi,
      functionName: "createProperty",
      args: [
        data.name,
        data.symbol,
        data.uri,
        data.totalShares,
        data.pricePerShare,
      ],
    });

    console.log("property transaction hash:", addPropertyTx);
  } catch (err: any) {
    toast.error("Transaction Failed: " + err.message);
    console.log("Transaction Failed: " + err.message);
  }
};

  1. Form Submission: When the form is submitted, the AddProperty function is called with the validated form data.
  2. writeContractAsync: This function interacts with the Okido Finance smart contract to create a new property. It sends a transaction with the form data as arguments.
  3. Error Handling: If the transaction fails, we catch the error, show an error toast notification, and log the error message.

PropertyCard.tsx

This is a UI  component that displays the details fetched from the contract.  It is only displayed if a property has been created.

import Image from "next/image";
import { Button } from "./ui/button";
import BuySharesModal from "./BuySharesModal";

interface PropertyCardProps {
  property: any;
}

const PropertyCard: React.FC<PropertyCardProps> = ({ property }) => {
  return (
    <div className=" w-96 text-black rounded overflow-hidden shadow-lg bg-white flex-shrink-0">
      <Image
        width={600}
        height={400}
        className="w-full h-48 object-cover"
        src={property.uri}
        alt="property"
      />
      <div className="px-6 py-4">
        <div className="font-bold text-xl mb-2">
          {property.name} (id: {property.tokenId.toString()})
        </div>
        <p className="text-gray-700  font-medium  text-base">
          Price Per Share: {property.pricePerShare.toString()} OKD
        </p>
        <p className="text-gray-700 font-medium text-base">
          Total Shares: {property.totalShares.toString()}
        </p>
        <p className="text-gray-700 font-medium  text-base">
          Shares Sold: {property.sharesSold.toString()}
        </p>

        <div className="flex justify-center my-4">
          <BuySharesModal>
            <Button
              size="lg"
              className="items-center bg-slate-600 text-white hover:bg-blue-900"
            >
              Buy Shares
            </Button>
          </BuySharesModal>
        </div>
      </div>
    </div>
  );
};

export default PropertyCard;

BuySharesModal.tsx

This has a similar design and functionality with the “AddPropertyModal”. This component allows users to buy fractions or shares of listed properties. The complete code for the component can be found here.

Conclusion

In this  guide, we have gone over how to build a fractionalized real-estate dApp using the Morph starter kit. We explored setting up your environment, creating your contracts, deploying and interacting with your smart contracts from the frontend.The complete code, checkout the github repository: Okido finance dapp & you can watch a demo of the dApp  here.

If you found this guide useful, leave a comment. If you have any questions or get stuck, create an issue on github or reach out on discord.