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:
- Fundamental understanding of blockchain concepts.
- Prior experience with Solidity, JavaScript (ReactJS in particular), and Web3.
- Basic knowledge of the Celo blockchain and its Ethereum compatibility.
- Completion of the “Connect to Celo using hardhat” tutorial.
Requirements
Before starting, ensure you have the following tools installed:
- Node.js, version 12.0.1 or later.
- MetaMask, a blockchain wallet that will interact with our dApp.
- Truffle, a development framework for Ethereum.
- 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
- 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);
};
- 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",
},
},
};
- Compile the smart contract
Run the following command to compile the smart contract:
truffle compile
- 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
- Tutorial Github Repo
- Celo Official Documentation
- React Documentation
- Web3.js Documentation
- IPFS Documentation
- 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!