Celo-based Tokenized Real Estate Platform

Celo-based Tokenized Real Estate Platform https://celo.academy/uploads/default/optimized/2X/7/774847da51aab93daecbab6b38197c4ba8b0bfdd_2_1024x576.jpeg
none 0.0 0

Celo-based Tokenized Real Estate Platform

Introduction

Real estate is one of the most prominent sectors ripe for blockchain disruption. Tokenization, or the process of transforming rights to a real-world asset into a digital token on a blockchain, offers unprecedented possibilities in terms of fractional ownership, accessibility, and liquidity. This tutorial will provide a hands-on guide to creating a tokenized real estate platform on the Celo blockchain. We will delve into the tokenization of real-world assets and demonstrate how to create, manage, and trade real estate-backed tokens using Solidity for smart contracts and JavaScript (React) for the frontend interface.

Prerequisites

To follow along effectively, it’s recommended that you have the following:

  1. Fundamental understanding of blockchain concepts.
  2. Prior experience with Solidity, JavaScript (ReactJS in particular), and Web3.
  3. Basic knowledge of the Celo blockchain and its Ethereum compatibility.
  4. Completion of the “Connect to Celo using hardhat” tutorial.

Requirements

Before starting, ensure you have the following tools installed:

  1. Node.js, version 12.0.1 or later.
  2. MetaMask, a blockchain wallet that will interact with our dApp.
  3. Truffle, a development framework for Ethereum.
  4. A Celo wallet with some test tokens for contract interaction.

Building the Smart Contract

Let’s start off by creating the smart contract using Solidity, a popular language for developing Ethereum-based smart contracts.

Setting up Truffle

Initialize a new Truffle project by creating a new directory and running the initialization command:

mkdir real-estate-token && cd real-estate-token
truffle init

This command creates a new Truffle project with a basic project structure.

Writing the Smart Contract

Next, create a new Solidity file in the contracts directory:

touch contracts/RealEstateToken.sol

Add this code into the smart contract

 // SPDX-License-Identifier: MIT
 pragma solidity ^0.8.0;
 
 import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
 import "@openzeppelin/contracts/access/Ownable.sol";
 import "@openzeppelin/contracts/utils/Counters.sol";
 
 contract RealEstateToken is ERC721, Ownable {
     using Counters for Counters.Counter;
     Counters.Counter private _tokenIds;
 
     struct Property {
         string uri;
         string metadata;
     }
 
     struct Sale {
         address seller;
         uint256 askingPrice;
         bool isForSale;
     }
 
     mapping(uint256 => Property) private _tokenDetails;
     mapping(uint256 => Sale) private _tokenSales;
 
     event TokenMinted(address indexed operator, uint256 indexed tokenId, string uri, string metadata);
     event TokenListedForSale(uint256 indexed tokenId, uint256 askingPrice);
 
     constructor() ERC721("RealEstateToken", "RET") {}
 
     function mintToken(string memory uri, string memory metadata) public onlyOwner returns (uint256) {
         _tokenIds.increment();
         uint256 newTokenId = _tokenIds.current();
 
         _mint(msg.sender, newTokenId);
         
         _tokenDetails[newTokenId] = Property(uri, metadata);
 
         emit TokenMinted(msg.sender, newTokenId, uri, metadata);
 
         return newTokenId;
     }
 
     function listTokenForSale(uint256 tokenId, uint256 askingPrice) public {
         require(_isApprovedOrOwner(msg.sender, tokenId), "Caller is not owner nor approved");
         require(askingPrice > 0, "Asking price must be greater than zero");
 
         _tokenSales[tokenId] = Sale(msg.sender, askingPrice, true);
 
         emit TokenListedForSale(tokenId, askingPrice);
     }
 
     function buyToken(uint256 tokenId) public payable {
         require(_tokenSales[tokenId].isForSale, "Token is not for sale");
         require(msg.value >= _tokenSales[tokenId].askingPrice, "Insufficient funds to buy the token");
 
         _transfer(_tokenSales[tokenId].seller, msg.sender, tokenId);
         _tokenSales[tokenId].seller = address(0);
         _tokenSales[tokenId].isForSale = false;
 
         (bool success, ) = _tokenSales[tokenId].seller.call{value: msg.value}("");
         require(success, "Failed to send CELO");
     }
 
     function tokenURI(uint256 tokenId) public view override returns (string memory) {
         return _tokenDetails[tokenId].uri;
     }
 
     function tokenDetails(uint256 tokenId) public view returns (Property memory) {
         return _tokenDetails[tokenId];
     }
 
     function tokenSales(uint256 tokenId) public view returns (Sale memory) {
         return _tokenSales[tokenId];
     }
 }

Here, the contract RealEstateToken inherits the ERC721 and Ownable contracts from the OpenZeppelin library. We use the Counters library to manage token IDs. We define two structs Property and Sale to store details of each token and its sale status. We use mapping to map each token ID to its details and sale status.

We emit two events TokenMinted and TokenListedForSale when a new token is minted and listed for sale.

The mintToken function mints a new token, sets its URI to the IPFS hash, and stores its details.

The listTokenForSale function allows the owner of a token to list it for sale at a specified asking price. This function requires that the token is either owned by the caller or the caller is an approved operator, and that the asking price is greater than zero.
The buyToken function allows users to purchase a token that is listed for sale, provided they send enough funds to cover the asking price. Upon successful purchase, the token is transferred from the seller to the buyer, and the sale status is updated. The funds are then sent to the seller.

The tokenDetails function retrieves the details of a specific token, and the tokenSales function retrieves the sale status of a specific token.
This contract covers the basics of minting unique tokens for real estate assets, listing them for sale, and buying them. The OpenZeppelin library provides a secure and community-vetted foundation for the contract, and the contract itself follows the ERC721 standard, ensuring compatibility with existing wallets and marketplaces.

Now that we have our smart contract, we can deploy it to the Celo network.

Compiling and Deploying the Smart Contract to the Celo Test Network

  1. Create a new file migrations/2_deploy_contracts.js and add the following code:
  const MedicalRecord = artifacts.require("RealEstateToken");
  
  module.exports = function (deployer) {
    deployer.deploy(MedicalRecord);
  };
  1. Update the truffle-config.js file to include the Celo-Alfajores network configuration:
    const ContractKit = require("@celo/contractkit");
    const Web3 = require("web3");
    
    const web3 = new Web3("https://alfajores-forno.celo-testnet.org");
    const kit = ContractKit.newKitFromWeb3(web3);
    
    // Add your private key and account address
    const privateKey = "your_private_key";
    const accountAddress = "your_account_address";
    
    kit.addAccount(privateKey);
    
    module.exports = {
      networks: {
        development: { host: "127.0.0.1", port: 7545, network_id: "*" },
        alfajores: {
          provider: kit.connection.web3.currentProvider,
          network_id: 44787,
          from: accountAddress,
          gas: 6721975,
          gasPrice: 20000000000,
        },
      },
      compilers: {
        solc: {
          version: "0.8.0",
        },
      },
    };
  1. Compile the smart contract

Run the following command to compile the smart contract:

 truffle compile
  1. Deploy the smart contract to the Celo network

Run the following command to deploy the MedicalRecord smart contract to the Celo network:

truffle migrate --network alfajores

After the deployment is successful, you will see the smart contract address in the console output.

Take note of the deployed contract address for future use.

Building the Frontend

Now that we have our token contract, let’s build a frontend to interact with it. We’ll use ReactJS for this. Start by creating a new React application:

npx create-react-app client
cd client

To interact with the Ethereum network, we’ll use web3.js. Install it by running:

npm install web3

Let’s create a component that will allow users to mint a new token. Start by creating a new component file:

touch src/components/MintForm.js

Now, open this file and import the necessary libraries:

import React, { Component } from "react";
import Web3 from "web3";

Next, let’s create our MintForm class component:

    class MintForm extends Component {
        constructor(props) {
            super(props);
            this.state = {
                account: "",
                contract: null,
                web3: null,
                buffer: null,
                ipfsHash: "",
            };
        }
    }

In the above snippet, we’re setting up the initial state of our MintForm component. We’ll manage the account from which transactions are sent, the contract we’ll interact with, a Web3 instance, a buffer to store the file that will be uploaded, and the IPFS hash of the uploaded file.
Moving forward, we need to set up our environment (Web3 and smart contract instance) when the component mounts:

    async componentDidMount() {
        await this.loadWeb3();
        await this.loadBlockchainData();
    }

The loadWeb3 method will check if a Web3 instance is injected into the browser (MetaMask injects Web3) and then set our web3 state:

    async loadWeb3() {
        if (window.ethereum) {
            window.web3 = new Web3(window.ethereum);
            await window.ethereum.enable();
        }
        else if (window.web3) {
            window.web3 = new Web3(window.web3.currentProvider);
        }
        else {
            window.alert('Non-Ethereum browser detected. Consider trying MetaMask!');
        }
        this.setState({ web3: window.web3 });
    }

The loadBlockchainData method will instantiate our contract and set the account and contract state:

    async loadBlockchainData() {
        const web3 = window.web3;
        const accounts = await web3.eth.getAccounts();
        this.setState({ account: accounts[0] });
        const networkId = await web3.eth.net.getId();
        const networkData = RealEstateToken.networks[networkId];
        if(networkData) {
            const contract = new web3.eth.Contract(RealEstateToken.abi, networkData.address);
            this.setState({ contract });
        } else {
            window.alert('RealEstateToken contract not deployed to detected network.');
        }
    }

Remember to import the RealEstateToken ABI and address.
Now, we’ll create a function to capture file input, convert it to a buffer and set our buffer state:

    captureFile = (event) => {
        event.preventDefault();
        const file = event.target.files[0];
        const reader = new window.FileReader();
        reader.readAsArrayBuffer(file);
        reader.onloadend = () => {
            this.setState({ buffer: Buffer(reader.result) });
        };
    };

We’ll also create a function to handle file submission. This function will add the file to IPFS and set our ipfsHash state:

    onSubmit = async (event) => {
        event.preventDefault();
        // save file to IPFS, return its hash#, and set hash# to state
    };

Lastly, in our render method, we’ll create a form that calls the captureFile method when a file is selected and the onSubmit method when the form is submitted:

    render() {
        return (
            <div>
                <form onSubmit={this.onSubmit}>
                    <input type='file' onChange={this.captureFile} />
                    <input type='submit' />
                </form>
            </div>
        );
    }

At this point, our MintForm component is able to capture file input and handle form submission.

This is what the complete MintForm.js script should look like:

    import React, { Component } from "react";
    import Web3 from "web3";
    import ipfs from 'ipfs-api';
    import RealEstateToken from "./RealEstateToken.json"; // Import your contract details
    
    const ipfsAPI = ipfs('ipfs.infura.io', '5001', {protocol: 'https'}); // Connect to Infura's IPFS node
    
    class MintForm extends Component {
        constructor(props) {
            super(props);
            this.state = {
                account: "",
                contract: null,
                web3: null,
                buffer: null,
                ipfsHash: "",
            };
        }
    
        async componentDidMount() {
            await this.loadWeb3();
            await this.loadBlockchainData();
        }
    
        async loadWeb3() {
            if (window.ethereum) {
                window.web3 = new Web3(window.ethereum);
                await window.ethereum.enable();
            }
            else if (window.web3) {
                window.web3 = new Web3(window.web3.currentProvider);
            }
            else {
                window.alert('Non-Ethereum browser detected. Consider trying MetaMask!');
            }
            this.setState({ web3: window.web3 });
        }
    
        async loadBlockchainData() {
            const web3 = window.web3;
            const accounts = await web3.eth.getAccounts();
            this.setState({ account: accounts[0] });
            const networkId = await web3.eth.net.getId();
            const networkData = RealEstateToken.networks[networkId];
            if(networkData) {
                const contract = new web3.eth.Contract(RealEstateToken.abi, networkData.address);
                this.setState({ contract });
            } else {
                window.alert('RealEstateToken contract not deployed to detected network.');
            }
        }
    
        captureFile = (event) => {
            event.preventDefault();
            const file = event.target.files[0];
            const reader = new window.FileReader();
            reader.readAsArrayBuffer(file);
            reader.onloadend = () => {
                this.setState({ buffer: Buffer(reader.result) });
            };
        };
    
        onSubmit = async (event) => {
            event.preventDefault();
    
            console.log('Submitting file to ipfs...')
    
            // Adding file to IPFS
            ipfsAPI.files.add(this.state.buffer, (error, result) => {
                if(error) {
                    console.error(error)
                    return
                }
    
                // setting IPFS hash to the state
                this.setState({ ipfsHash: result[0].hash });
    
                console.log('IPFS result', result)
            })
        };
    
        render() {
            return (
                <div>
                    <form onSubmit={this.onSubmit}>
                        <input type='file' onChange={this.captureFile} />
                        <input type='submit' />
                    </form>
                </div>
            );
        }
    }
    
    export default MintForm;

In the next section, we’ll create components to list all minted tokens

Listing all Minted Tokens

To showcase all the minted tokens, we’ll create a new React component. This component will interact with our smart contract to fetch and display the details of each minted token.
Start by creating a new file for this component:

    touch src/components/TokenList.js

Then, let’s define our TokenList class component. Like in the MintForm component, we’ll need to set up the initial state and Web3 environment when the component mounts.

    import React, { Component } from "react";
    import Web3 from "web3";
    
    class TokenList extends Component {
        constructor(props) {
            super(props);
            this.state = {
                account: "",
                contract: null,
                web3: null,
                tokens: []
            };
        }
    
        async componentDidMount() {
            await this.loadWeb3();
            await this.loadBlockchainData();
        }
    
        async loadWeb3() {
            // same as in the MintForm component...
        }
    
        async loadBlockchainData() {
            // same as in the MintForm component...
        }
    }

In the TokenList component’s state, we have an additional tokens array that will store the details of all the minted tokens.

Now, we’ll create a function to fetch the details of each minted token:

    async loadTokens() {
        const totalSupply = await this.state.contract.methods.totalSupply().call();
        let tokens = [];
        for (let i = 0; i < totalSupply; i++) {
            const tokenId = await this.state.contract.methods.tokenByIndex(i).call();
            const tokenURI = await this.state.contract.methods.tokenURI(tokenId).call();
            tokens.push({ tokenId, tokenURI });
        }
        this.setState({ tokens });
    }

In this function, we’re calling the totalSupply method of our ERC721 token contract to get the total number of minted tokens. We then iterate through each token using its index, fetch its id and URI, and store these details in our tokens state.
Finally, in the render method of our TokenList component, we’ll map through our tokens state and display the details of each token:

    render() {
        return (
            <div>
                {this.state.tokens.map((token, key) => {
                    return (
                        <div key={key}>
                            <h3>Token ID: {token.tokenId}</h3>
                            <p>Token URI: {token.tokenURI}</p>
                        </div>
                    );
                })}
            </div>
        );
    }

At this point, we’ve successfully created a React app where users can mint new real estate-backed tokens and view all minted tokens.

This is what your complete TokenList.js should look like:

    import React, { Component } from "react";
    import Web3 from "web3";
    import RealEstateToken from "./RealEstateToken.json";
    
    class TokenList extends Component {
        constructor(props) {
            super(props);
            this.state = {
                account: "",
                contract: null,
                web3: null,
                tokens: []
            };
        }
    
        async componentDidMount() {
            await this.loadWeb3();
            await this.loadBlockchainData();
            await this.loadTokens();
        }
    
        async loadWeb3() {
            if (window.ethereum) {
                window.web3 = new Web3(window.ethereum);
                await window.ethereum.enable();
            }
            else if (window.web3) {
                window.web3 = new Web3(window.web3.currentProvider);
            }
            else {
                window.alert('Non-Ethereum browser detected. Consider trying MetaMask!');
            }
            this.setState({ web3: window.web3 });
        }
    
        async loadBlockchainData() {
            const web3 = window.web3;
            const accounts = await web3.eth.getAccounts();
            this.setState({ account: accounts[0] });
            const networkId = await web3.eth.net.getId();
            const networkData = RealEstateToken.networks[networkId];
            if(networkData) {
                const contract = new web3.eth.Contract(RealEstateToken.abi, networkData.address);
                this.setState({ contract });
            } else {
                window.alert('RealEstateToken contract not deployed to detected network.');
            }
        }
    
        async loadTokens() {
            const totalSupply = await this.state.contract.methods.totalSupply().call();
            let tokens = [];
            for (let i = 0; i < totalSupply; i++) {
                const tokenId = await this.state.contract.methods.tokenByIndex(i).call();
                const tokenURI = await this.state.contract.methods.tokenURI(tokenId).call();
                tokens.push({ tokenId, tokenURI });
            }
            this.setState({ tokens });
        }
    
        render() {
            return (
                <div>
                    {this.state.tokens.map((token, key) => {
                        return (
                            <div key={key}>
                                <h3>Token ID: {token.tokenId}</h3>
                                <p>Token URI: {token.tokenURI}</p>
                            </div>
                        );
                    })}
                </div>
            );
        }
    }
    
    export default TokenList;

Integrating Additional Features

To make our platform more vibrant and interactive, we can incorporate several additional features such as:

Token Marketplace

This feature will allow token holders to sell their tokens to other users. To implement this feature, we will need to make modifications in both our smart contract and the frontend.
In the smart contract, we’ll need to add new functionality to allow token holders to list their tokens for sale and for buyers to purchase them.
Let’s start by defining a new struct and a mapping to keep track of the tokens listed for sale:

    struct Sale {
        uint256 tokenId;
        uint256 askingPrice;
        address payable seller;
    }
    
    mapping(uint256 => Sale) public tokenSales;

Here, each Sale consists of the tokenId, the asking price, and the address of the seller. The tokenSales mapping then maps each tokenId to its Sale.
We’ll then define a function to list a token for sale:

    function listTokenForSale(uint256 _tokenId, uint256 _askingPrice) public {
        require(_isApprovedOrOwner(_msgSender(), _tokenId), "ERC721: caller is not owner nor approved");
        tokenSales[_tokenId] = Sale(_tokenId, _askingPrice, _msgSender());
    }

In this function, we first ensure that the caller is the owner or an approved delegate of the tokenId being listed for sale. We then create a new Sale and store it in our tokenSales mapping.

We’ll also define a function to buy a token listed for sale:

    function buyToken(uint256 _tokenId) public payable {
        Sale memory sale = tokenSales[_tokenId];
        require(msg.value >= sale.askingPrice, "The amount sent is not enough");
        _transfer(sale.seller, _msgSender(), _tokenId);
        sale.seller.transfer(msg.value);
        delete tokenSales[_tokenId];
    }

In the buyToken function, we first retrieve the Sale of the tokenId being bought. We then ensure that the caller has sent enough funds to cover the asking price of the token. If they have, we transfer the token from the seller to the buyer, transfer the funds from the buyer to the seller, and delete the Sale.

In the frontend, we’ll need to add new UI components to allow users to list their tokens for sale and to display the tokens listed for sale.
Let’s start by creating a new component to list tokens for sale:

    touch src/components/ListToken.js

This component will have a form where users can enter the tokenId of the token they want to list for sale and the asking price. On form submission, it will call the listTokenForSale function in our smart contract.

Next, let’s create a component to display tokens listed for sale:

    touch src/components/SaleList.js

This component will fetch the details of each token listed for sale from the blockchain and display them. It will also have a “Buy” button that, when clicked, will call the buyToken function in our smart contract.

By incorporating these additional features, our tokenized real estate platform on the Celo blockchain will provide a more interactive and dynamic experience for users. It will also demonstrate the wide range of possibilities that tokenization of real-world assets presents.

Implementing the ListToken Component
To facilitate the process of listing a token for sale, we need to create the ListToken component. This component will have a form that allows users to enter the ID of the token they want to list for sale, as well as the asking price. On submission, it will call the listTokenForSale function we defined in our smart contract.

In your src/components folder, open the ListToken.js file and add the following code:

    import React, { Component } from 'react';
    
    class ListToken extends Component {
        constructor(props) {
            super(props);
            this.state = {
                tokenId: '',
                askingPrice: ''
            };
        }
    
        handleChange = (e) => {
            this.setState({
                [e.target.name]: e.target.value
            });
        }
    
        handleSubmit = (e) => {
            e.preventDefault();
            this.props.listTokenForSale(this.state.tokenId, this.state.askingPrice);
        }
    
        render() {
            return (
                <form onSubmit={this.handleSubmit}>
                    <label>
                        Token ID:
                        <input type='text' name='tokenId' onChange={this.handleChange} />
                    </label>
                    <label>
                        Asking Price (in CELO):
                        <input type='text' name='askingPrice' onChange={this.handleChange} />
                    </label>
                    <input type='submit' value='List token for sale' />
                </form>
            );
        }
    }
    
    export default ListToken;

In this component, we define two state variables: tokenId and askingPrice. We have a handleChange function that updates these state variables when their corresponding input fields are changed. We also have a handleSubmit function that calls the `list

Implementing the SaleList Component
The SaleList component will retrieve the details of each token listed for sale from the blockchain and display them in a list. It will also provide a “Buy” button for each listed token, allowing users to purchase tokens directly from the list.

In the src/components folder, open the SaleList.js file and add the following code:

    import React, { Component } from 'react';
    
    class SaleList extends Component {
        constructor(props) {
            super(props);
            this.state = {
                sales: []
            };
        }
    
        componentDidMount() {
            this.loadSales();
        }
    
        async loadSales() {
            const sales = [];
            for (let i = 1; i <= this.props.totalSupply; i++) {
                const sale = await this.props.contract.methods.tokenSales(i).call();
                if (sale.seller !== '0x0000000000000000000000000000000000000000') {
                    sales.push(sale);
                }
            }
            this.setState({ sales });
        }
    
        render() {
            return (
                <div>
                    {this.state.sales.map((sale, key) => {
                        return (
                            <div key={key}>
                                <p>Token ID: {sale.tokenId}</p>
                                <p>Asking Price: {this.props.web3.utils.fromWei(sale.askingPrice, 'ether')} CELO</p>
                                <button onClick={() => this.props.buyToken(sale.tokenId)}>Buy</button>
                            </div>
                        );
                    })}
                </div>
            );
        }
    }
    
    export default SaleList;

In this component, we define a state variable sales to store the details of each token listed for sale. The componentDidMount lifecycle method is used to load the details of each sale when the component is rendered.

The loadSales function retrieves the details of each token listed for sale by calling the tokenSales function from our smart contract. It checks if each token is actually listed for sale (i.e., the seller is not the zero address) and if so, adds it to the sales state variable.
The render method maps over the sales state variable and for each sale, displays the token ID, asking price (converted from wei to CELO), and a “Buy” button. The “Buy” button calls the buyToken function from our App component when clicked, passing the token ID as an argument.

Connecting Components to the App Component
To wrap everything up, we need to connect these components to the App component. This allows us to pass the necessary functions and state variables as props.

Open the App.js file and import the MintForm, ListToken, and SaleList components at the top of the file:

    import MintForm from './components/MintForm';
    import ListToken from './components/ListToken';
    import SaleList from './components/SaleList';

Then, in the render method of the App component, render these components and pass the necessary functions and state variables as props:

    render() {
        return (
            <div>
                <h1>Tokenized Real Estate Platform</h1>
                <MintForm
                    mintToken={this.mintToken}
                    captureFile={this.captureFile}
                />
                <ListToken
                    listTokenForSale={this.listTokenForSale}
                />
                <SaleList
                    contract={this.state.contract}
                    totalSupply={this.state.totalSupply}
                    web3={this.state.web3}
                    buyToken={this.buyToken}
                />
            </div>
        );
    }

With this, our platform is complete! You should now be able to mint tokens representing real estate assets, list them for sale, and buy them.

Conclusion

Congratulations on reaching this far! You’ve built a tokenized real estate platform on the Celo blockchain using Solidity, JavaScript, and React. You’ve learned how to tokenize real-world assets and create, manage, and trade these tokens on the blockchain.

Next Steps

There are many ways to improve and expand this platform. For example, you could add user authentication to restrict token minting to authorized users, implement a bidding system for token sales, or incorporate a system to verify the authenticity of the real estate assets.
You could also dive deeper into the Celo ecosystem and learn about its unique features such as the Celo Dollar (cUSD), a stablecoin pegged to the US Dollar, and the Celo Wallet, which allows users to send and receive cUSD using their phone number.

About the Author

Oluwalana is a blockchain developer and technical writer, experienced in creating decentralized applications on Ethereum and Celo platforms. With a passion for knowledge-sharing, Oluwalana has authored various tutorials and articles on blockchain and other emerging technologies. Follow me on Twitter for insights on blockchain and emerging tech. For professional inquiries, kindly connect witth me on LinkedIn and explore my work on GitHub.

References

  1. Tutorial Github Repo
  2. Celo Official Documentation
  3. React Documentation
  4. Web3.js Documentation
  5. IPFS Documentation
  6. Truffle Suite Documentation

Remember, as you continue your blockchain development journey, these resources are immensely valuable for understanding and leveraging the technologies we’ve discussed here.

Additional Learning Resources

If you’re looking to continue developing your blockchain programming skills, you might find the following resources helpful:

1. [OpenZeppelin](https://docs.openzeppelin.com/): A library for secure smart contract development. It provides implementations of standards like ERC20 and ERC721 which you can deploy as-is or extend to suit your needs.
2. [Ethernaut](https://ethernaut.openzeppelin.com/): Ethernaut is a Web3/Solidity based game that guides you through the depths of Ethereum. It's great for understanding various security considerations in smart contract development.
3. [Celo Academy](http://celo.academy): A great resource to understand and learn about Celo and how to build on Celo.
4. [CryptoZombies](https://cryptozombies.io/): An interactive code school that teaches you to write smart contracts in Solidity through building your own crypto-collectables game.

Closing Notes

And there you have it, a tokenized real estate platform on the Celo blockchain! While this tutorial is comprehensive, it is by no means the be-all and end-all of what you can accomplish in blockchain development.
Building on the Celo blockchain opens up numerous opportunities for developers to innovate and create meaningful impact. Whether it’s financial inclusion, decentralized finance (DeFi), NFTs, or beyond, there’s much to explore and build upon.
Remember, the blockchain space is continually evolving, and being a part of that journey means constant learning and adaptation. It’s an exciting field that promises significant potential for innovation, disruption, and growth.
Happy coding, and I can’t wait to see what you build on the Celo blockchain!

4 Likes

Approved for you to get started. You can manage the tutorial here by changing the category to Proposals > In Progress then Proposals > Review as you complete the tutorial. Thanks!

1 Like

Will be reviewing this in 1 or 2 days @Lanacreates

1 Like

Please attach source code repo @Lanacreates

Thank you, that has been done @ishan.pathak2711

2 Likes

Sorry I am not able to see it, can you help me to navigate @Lanacreates

2 Likes

It is added in the reference section

3 Likes

Found it, and there were many mistakes in the markdown, which I corrected it, please make sure this does not happen in the future, you can see the changes in edit section @Lanacreates

1 Like

Thank you very much, still getting a hang of this new platform, won’t happen next time.

Please what do i do next

2 Likes

No you don’t have to do anything, we will get this published as soon as possible @Lanacreates ,
And one more thing about the platform, don’t worry, the process of markdown is still same so follow that only

Thank You,
Ishan

1 Like

Nice piece :100:

Detailed piece. Thanks for this submission.