OfferingFactory.sol

Overview

OfferingFactory standardizes deployment of RealOffering instances. It emits a detailed OfferingDeployed event so off-chain systems can track new sales. The basic version uses plain CREATE; you can adapt to CREATE2 or clones if you want deterministic addresses or lower gas.


Responsibilities

  • Construct RealOffering with canonical parameters.

  • Emit metadata so indexers/frontends discover and render the sale.


Data Flow

deployer/issuer
   └─ deployOffering(issuer, token, payToken, rate, softCap, hardCap, start, end, min, max)
          └─ new RealOffering(...)
                 └─ emit OfferingDeployed(offering, issuer, token, payToken, rate, softCap, hardCap, start, end, min, max)

Role of the Factory in the System

  • Centralizes sale creation rules and defaults (e.g., guardrails on caps/durations if you add them).

  • Plays well with token factories: deploy token → deploy offering → grant roles (scripted).


Deterministic Deployment (Optional)

If your ops require predictable offering addresses (e.g., to grant token roles before deployment), switch to CREATE2:

  • Compute salt = keccak256(abi.encode(issuer, token, start, end))

  • Use a deployer helper to create2 with constructor args

  • Expose predictAddress(...) to frontends/scripts


Security Considerations

  • If you only want approved issuers, add AccessControl to the factory and restrict deployOffering.

  • Validate all constructor args (non-zero addresses, start < end, hardCap ≥ softCap, etc.). The example RealOffering already enforces key invariants.


Ops Runbook (Typical)

  1. Deploy token via RealTokenFactory.

  2. Deploy offering via OfferingFactory.

  3. Grant MINTER_ROLE on the token to the new offering address.

  4. Publish offering metadata (rate, caps, window) to your UI/indexer.


Minimal Usage Snippet

address off = OfferingFactory(factory).deployOffering(
  issuer,
  token,
  usdc,              // or address(0) for ETH
  100e18,            // 1 USDC → 100 fractions
  100_000e6,         // soft cap
  1_000_000e6,       // hard cap
  uint64(startTs),
  uint64(endTs),
  100e6,             // min per wallet
  10_000e6           // max per wallet
);

// grant minter to the offering so claim() can mint
RealFractionalToken(token).grantRole(MINTER_ROLE, off);

End-to-End Sequence (Primary Sale)

[Issuer/Ops]
  ├─ deploy token (RealTokenFactory)
  ├─ deploy offering (OfferingFactory)
  ├─ grant MINTER_ROLE to offering on token
  └─ announce sale

[Buyer]
  ├─ approve USDC to offering
  ├─ buy(amountIn)  ─────────────────────────────► offering
  │                        contributed[buyer]+=amountIn
  │                        purchased[buyer]+=amountIn*rate/1e18
  │                        totalRaised+=amountIn
  └─ wait for end/finalize

[Issuer]
  └─ finalize() ─────────────────────────────────► offering
                           if totalRaised<softCap: refundsEnabled=true
                           else: transfer funds to issuer

[Buyer]
  ├─ (success) claim() ───► offering → token.mint(buyer, claimable)
  └─ (failure) claimRefund() → offering returns funds

Extension Hooks & Variants

  • Immediate minting vs claim-later:

    The provided RealOffering mints on claim after finalize (safe and common).

    If you prefer immediate minting at purchase time, remove claim accounting and mint in buy(); you’ll need a reverse path for refunds (burn or escrow) if the sale fails.

  • Multi-asset or tiered pricing:

    Keep RealOffering single-asset and single-price; model tiers by deploying multiple offerings with different rate and windows, or write a thin router that dispatches buy() to the correct offering.

  • KYC at payment token level:

    If payment tokens are permissioned, add an optional registry check for the pay token leg as well.

  • Operator fees:

    If the sale requires a fee, skim from totalRaised on finalize or add a separate fee sink during buy().


OfferingFactory.sol

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

import {RealOffering} from "./RealOffering.sol";

/// @title OfferingFactory
/// @notice Deploys new primary-sale contracts (RealOffering) for fractionalized RWAs.
/// @dev Simple factory using CREATE. If you want deterministic addresses, adapt to CREATE2.
contract OfferingFactory {
    /// @notice Emitted when a new offering is deployed.
    event OfferingDeployed(
        address indexed offering,
        address indexed issuer,
        address indexed token,
        address payToken,
        uint256 rate,
        uint256 softCap,
        uint256 hardCap,
        uint64  start,
        uint64  end,
        uint256 minPerWallet,
        uint256 maxPerWallet
    );

    /// @notice Deploy a new primary sale (RealOffering) instance.
    /// @param issuer        Address that will receive funds on successful finalize
    /// @param token         Fractional token address (offering must have MINTER_ROLE on it)
    /// @param payToken      ERC-20 payment token (use address(0) to accept native ETH)
    /// @param rate          Fractions per 1 payment unit (scaled by 1e18)
    /// @param softCap       Minimum total raised (payment units) for success
    /// @param hardCap       Maximum total raised (payment units)
    /// @param start         Sale start timestamp (UTC seconds)
    /// @param end           Sale end timestamp (UTC seconds)
    /// @param minPerWallet  Minimum contribution per wallet (payment units)
    /// @param maxPerWallet  Maximum contribution per wallet (payment units)
    /// @return offering Address of the newly deployed RealOffering
    function deployOffering(
        address issuer,
        address token,
        address payToken,
        uint256 rate,
        uint256 softCap,
        uint256 hardCap,
        uint64  start,
        uint64  end,
        uint256 minPerWallet,
        uint256 maxPerWallet
    ) external returns (address offering) {
        RealOffering o = new RealOffering(
            issuer,
            token,
            payToken,
            rate,
            softCap,
            hardCap,
            start,
            end,
            minPerWallet,
            maxPerWallet
        );
        offering = address(o);

        emit OfferingDeployed(
            offering,
            issuer,
            token,
            payToken,
            rate,
            softCap,
            hardCap,
            start,
            end,
            minPerWallet,
            maxPerWallet
        );
    }
}

Last updated