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
Deploy token with admin + registry.
Grant MINTER_ROLE to Offering/Lending/Distributor; grant PAUSER_ROLE to ops.
Operate: transfers/issuance flow through registry; pause if needed.
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