RealEstateToken Contract

The RealEstateToken contract represents a Real World Asset (RWA). Real estate, in this instance. It is a basic example of RWA tokenization. It allows the owner/adminstrator of the project to mint tokens that represent a share in a real estate devlopment project.

The RealEstateToken is a modified ERC20 token. It overrides some of the standard behaviours of an ERC20 token. In particular, your attention is directed to the mint(), adminBurn, decimals(), and _update() functions. Essentially, the changes mean 1 token is a 1% share of the project. There are no fractional shares. Only the owner/admin of the project can mint tokens to an approved investor's account address. And, a token holder is restricted to transferring their tokens to other approved investors. Finally, given the nature of the tokens and what they are intended to represent, only the owner/admin can burn tokens. They cannot be burned by the token holder.

RealEstateToken

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

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {Ownable2Step, Ownable} from "@openzeppelin/contracts/access/Ownable2Step.sol";

// Interface for the Whitelist contract isApproved function
interface IWhiteList {
    function isApproved(
        address project,
        address investor
    ) external view returns (bool);
}

// A modified ERC20 token representing 100 shares in a real estate project
// RealEstateToken represents shares in a real estate project
// WARNING: This contract is an example only - do not use in production
contract RealEstateToken is ERC20, Ownable2Step {
    IWhiteList public whitelist;

    // Issuer's property identifier
    string public propertyId;
    // The legal jurisdiction of the real estate
    string public jurisdiction;
    // IPFS CID to legal documents (unused)
    string public metadataUri;

    // Hard cap: 100 tokens max supply. Each token = 1 whole share of the property.
    uint256 public constant MAX_SUPPLY = 100;

    mapping(address => bool) private _isShareholder;
    address[] private _shareholders;

    // The number of investors that hold tokens in the project
    uint256 public shareholderCount;

    constructor(
        string memory name,
        string memory symbol,
        address whiteListAddress,
        string memory _propertyId,
        string memory _jurisdiction,
        string memory _metadataUri,
        address projectOwner
    ) ERC20(name, symbol) Ownable(projectOwner) {
        whitelist = IWhiteList(whiteListAddress);
        propertyId = _propertyId;
        jurisdiction = _jurisdiction;
        metadataUri = _metadataUri;
    }

    // Check whether an address is an investor in this project
    // Returns true if the address is an investor, otherwise false
    function isShareholder(address account) external view returns (bool) {
        return _isShareholder[account];
    }

    // Get a list of shareholders in this project
    // Returns an array of the investor account addresses for this project
    function getShareholders() external view returns (address[] memory) {
        return _shareholders;
    }

    // Check whether an investor's address is on the whitelist
    // Returns true if the address is whitelisted, otherwise false
    function isApprovedHolder(address investor) external view returns (bool) {
        return whitelist.isApproved(address(this), investor);
    }

    // Mints new tokens to a specified address. There are 100 tokens only. Only the issuer (owner) can mint
    function mint(address to, uint256 amount) external onlyOwner {
        require(to != address(0), "Invalid recipient");

        // Investor (to) must be approved
        require(
            whitelist.isApproved(address(this), to),
            "Investor not approved for this project."
        );

        // Enforce hard cap (MAX_SUPPLY = 100 tokens)
        require(
            totalSupply() + amount <= MAX_SUPPLY,
            "Maximum shares exceeded."
        );

        _mint(to, amount);
    }

    // Admin burn. This is the only way to reduce supply.
    // Used in emergency/problem circumstances (fraud, regulatory unwind etc)
    function adminBurn(address from, uint256 amount) external onlyOwner {
        require(from != address(0), "Invalid from address.");
        _burn(from, amount);
    }

    // Override decimals to ensure 1 token = 1 whole share. No fractional shares
    function decimals() public pure override returns (uint8) {
        return 0;
    }

    // Override some behaviours of the regular _update function for this specific use case
    // Burn only allowed if msg.sender is contract owner (via adminBurn).
    // Mint and transfer sender and recipients must be whitelisted for THIS project
    function _update(
        address from,
        address to,
        uint256 value
    ) internal override {
        bool isMint = (from == address(0));
        bool isBurn = (to == address(0));

        if (isBurn) {
            require(msg.sender == owner(), "Burn restricted to administrator.");
        }

        // Whitelist enforcement
        // Sender must be approved if it's not mint
        if (!isMint) {
            // from is a real holder in transfer OR burn
            require(
                whitelist.isApproved(address(this), from),
                "Sender not approved for this project"
            );
        }

        // Recipient must be approved if it's not burn
        if (!isBurn) {
            // to is a real holder in transfer OR mint
            require(
                whitelist.isApproved(address(this), to),
                "Recipient is not approved for this project"
            );
        }

        // Snapshot balances before
        uint256 fromBalanceBefore = isMint ? 0 : balanceOf(from);
        uint256 toBalanceBefore = isBurn ? 0 : balanceOf(to);

        // Execute state update
        super._update(from, to, value);

        // Snapshot balances after transfer
        uint256 fromBalanceAfter = isMint ? 0 : balanceOf(from);
        uint256 toBalanceAfter = isBurn ? 0 : balanceOf(to);

        // Shareholder bookkeeping

        // Recipient becomes a shareholder
        if (!isBurn) {
            if (
                toBalanceBefore == 0 &&
                toBalanceAfter > 0 &&
                !_isShareholder[to]
            ) {
                _isShareholder[to] = true;
                _shareholders.push(to);
                shareholderCount += 1;
            }
        }

        // Sender ceases to be a shareholder
        if (!isMint) {
            if (
                fromBalanceBefore > 0 &&
                fromBalanceAfter == 0 &&
                _isShareholder[from]
            ) {
                _isShareholder[from] = false;
                shareholderCount -= 1;
                // We do not prune shareholders for gas saving reasons
            }
        }
    }
}

Last updated