RealFractionalToken.sol

Overview

RealFractionalToken is an ERC-20 representing fractional ownership of a single real-world asset series. It’s intentionally “thin” and delegates eligibility/transfer policy to an external Compliance Registry. The token adds AccessControl roles and Pausable controls, and routes every state change through a compliance hook.


Responsibilities

  • Standard ERC-20 balance and allowance accounting.

  • Compliance gate on mint/transfer/burn via _update override.

  • Role-gated mint and burn for lifecycle operations.

  • Pause all transfers during corporate actions or incidents.

  • Optional rotation of the compliance registry.


Key Components and Storage

  • IComplianceRegistry compliance; – external policy engine:

    • isTransferAllowed(token, from, to, amount) -> bool

  • Roles (AccessControl):

    • DEFAULT_ADMIN_ROLE – manage roles.

    • REGISTRY_ADMIN_ROLE – rotate compliance registry.

    • PAUSER_ROLE – pause/unpause.

    • MINTER_ROLE / BURNER_ROLE – controlled issuance/redemptions.

  • Inherits:

    • ERC20, ERC20Permit (EIP-2612), AccessControl, Pausable.


Control Flow (Core Hooks)

All ERC-20 state changes (transfer, mint, burn) funnel through OZ v5’s _update:

function _update(address from, address to, uint256 value) internal override whenNotPaused {
    _checkCompliance(from, to, value); // external call to compliance
    super._update(from, to, value);
}
  • Mint: treated as (from == address(0)); still runs _checkCompliance so the registry can block minting to ineligible recipients.

  • Burn: (to == address(0)); registry may allow/block based on policy.


Interactions With Other Modules

  • Primary Market (RealOffering): gets MINTER_ROLE to mint fractions to buyers (or on claim).

  • Secondary Market (P2PRouter): reads permissioned()/isEligible() (if you expose them) via a compliance adapter; router enforces eligibility before routing trades.

  • Yield Distribution (RealStakingDistributor): stakers approve & transfer tokens to distributor; distributor does not bypass compliance (transfers still checked).

  • Lending (RealLending): borrowers approve transfers for collateral deposit; liquidations transfer collateral subject to compliance.


Compliance Registry (Pluggable)

  • Encapsulates business rules (KYC, sanctions, geo-fencing, per-investor limits, global freezes).

  • Swappable via setComplianceRegistry() under REGISTRY_ADMIN_ROLE.

  • Keep registry stateless per transfer or cache minimal read-only state (e.g., whitelists) to avoid reentrancy and reduce gas.

Example isTransferAllowed Contract Boundary

// Called on every mint/transfer/burn
require(
  compliance.isTransferAllowed(address(this), from, to, amount),
  "REAL: compliance block"
);

Tip: include mints/burns in your policy (treat 0x0 legs explicitly).


Lifecycle and State Transitions

  1. Deploy token with admin + registry.

  2. Grant MINTER_ROLE to Offering/Lending/Distributor; grant PAUSER_ROLE to ops.

  3. Operate: transfers/issuance flow through registry; pause if needed.

  4. Rotate registry or roles as policy evolves.


Extensibility

  • Fees: if you need per-transfer fee routing, add a treasury and skim inside _update after compliance passes.

  • Timelocks: add per-account/global timelock checks before super._update.

  • Snapshots: plug OZ ERC20Snapshot for off-chain accounting/voting.


Security Considerations

  • Registry is a privileged dependency—audit it. Avoid external calls other than a view; never let it call back into the token.

  • Use role separation: issuer ops vs. protocol ops vs. registry admin.

  • Consider pausing on critical incidents or bridge events.

  • Add fuzz tests for _update invariants (sum of balances, pause behavior, registry flips).


Gas Notes

  • Compliance check adds an external call; keep registry reads O(1) by using mappings/bitmaps.

  • Use ERC20Permit to reduce “approve then call” flows.


Minimal Usage Examples

// grant roles
token.grantRole(MINTER_ROLE, offering);
token.grantRole(PAUSER_ROLE, ops);

// mint during offering finalize/claim
token.mint(buyer, fractions);

// pause during migration
token.pause();

RealFractionalToken.sol

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

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol";

interface IComplianceRegistry {
    /// @notice Returns true if a transfer is allowed for (token, from, to, amount)
    function isTransferAllowed(address token, address from, address to, uint256 amount) external view returns (bool);
}

/// @title RealFractionalToken
/// @notice ERC-20 for fractional RWA with compliance hooks, roles, and pausability.
/// @dev Designed for use with a pluggable compliance registry. OZ v5 compatible.
contract RealFractionalToken is ERC20, ERC20Permit, AccessControl, Pausable {
    // --- Roles ---
    bytes32 public constant PAUSER_ROLE          = keccak256("PAUSER_ROLE");
    bytes32 public constant MINTER_ROLE          = keccak256("MINTER_ROLE");
    bytes32 public constant BURNER_ROLE          = keccak256("BURNER_ROLE");
    bytes32 public constant REGISTRY_ADMIN_ROLE  = keccak256("REGISTRY_ADMIN_ROLE");

    // --- Compliance Registry ---
    IComplianceRegistry public compliance;

    // --- Events ---
    event ComplianceRegistryUpdated(address indexed oldRegistry, address indexed newRegistry);

    // --- Constructor ---
    constructor(
        string memory name_,
        string memory symbol_,
        address admin_,
        address complianceRegistry_
    ) ERC20(name_, symbol_) ERC20Permit(name_) {
        _grantRole(DEFAULT_ADMIN_ROLE, admin_);
        _grantRole(PAUSER_ROLE, admin_);
        _grantRole(MINTER_ROLE, admin_);
        _grantRole(BURNER_ROLE, admin_);
        _grantRole(REGISTRY_ADMIN_ROLE, admin_);

        compliance = IComplianceRegistry(complianceRegistry_);
    }

    // --- Admin ---
    function setComplianceRegistry(address registry) external onlyRole(REGISTRY_ADMIN_ROLE) {
        emit ComplianceRegistryUpdated(address(compliance), registry);
        compliance = IComplianceRegistry(registry);
    }

    function pause() external onlyRole(PAUSER_ROLE) {
        _pause();
    }

    function unpause() external onlyRole(PAUSER_ROLE) {
        _unpause();
    }

    // --- Mint / Burn ---
    /// @notice Mint tokens; also run compliance check (treat mint as from=address(0))
    function mint(address to, uint256 amount) external onlyRole(MINTER_ROLE) {
        _checkCompliance(address(0), to, amount);
        _mint(to, amount);
    }

    /// @notice Burn tokens from an address; caller must hold BURNER_ROLE
    function burn(address from, uint256 amount) external onlyRole(BURNER_ROLE) {
        _burn(from, amount);
    }

    // --- ERC20 Hook Overrides ---
    /// @dev OZ v5 `ERC20` funnels transfer/mint/burn through `_update`.
    function _update(address from, address to, uint256 value) internal override whenNotPaused {
        _checkCompliance(from, to, value);
        super._update(from, to, value);
    }

    // --- Internal ---
    function _checkCompliance(address from, address to, uint256 amount) internal view {
        // If registry is unset, allow by default
        if (address(compliance) != address(0)) {
            require(
                compliance.isTransferAllowed(address(this), from, to, amount),
                "REAL: transfer blocked by compliance"
            );
        }
    }
}

Last updated