// SPDX-License-Identifier: MIT pragma solidity 0.8.23; import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "./AereRollupSettlement.sol"; /** * @title AereRaaSFactory — permissionless registry for Rollup-as-a-Service * deployments on AERE * @notice Anyone can register a new rollup by: * 1. Posting a registration bond in BOND_TOKEN (refundable on * graceful exit, slashable on fraud). * 2. Choosing a rollupOwner + sequencer address. * 3. Choosing revenue splits (sink/rollup/sequencer) subject to * MIN_SINK_BPS — the factory enforces a floor so the sink * always gets at least the protocol's mandatory share. * * Factory deploys a fresh AereRollupSettlement, records the rollup, * and returns its address. Bond is held by the factory. * * IMMUTABILITY at factory deploy: * - BOND_TOKEN * - SINK * - REGISTRATION_BOND * - MIN_SINK_BPS (e.g. 1000 = 10% floor to AereSink) * - DEFAULT_CHALLENGE_WINDOW * * NO admin pause, no admin slash, no upgrade. Factory cannot * retroactively change a registered rollup's parameters. * * @dev Bond retrieval requires a Foundation-attested "graceful exit" * signature in Phase 1. Phase 2 replaces with an on-chain * no-pending-challenges + N-epoch-quiet check. */ contract AereRaaSFactory is ReentrancyGuard { /* ------------------------------- immutable ------------------------------- */ address public immutable BOND_TOKEN; address public immutable SINK; uint256 public immutable REGISTRATION_BOND; uint16 public immutable MIN_SINK_BPS; uint64 public immutable DEFAULT_CHALLENGE_WINDOW; address public immutable FOUNDATION; // signs graceful-exit attestations in Phase 1 /* --------------------------------- state -------------------------------- */ struct Rollup { bool registered; bool exited; address settlement; address rollupOwner; uint256 bond; uint64 registeredAt; string metadataUri; } /// @notice rollupId → Rollup mapping(bytes32 => Rollup) public rollups; bytes32[] public rollupIds; /* --------------------------------- events ------------------------------- */ event RollupRegistered( bytes32 indexed rollupId, address indexed rollupOwner, address indexed settlement, uint256 bond, string metadataUri ); event RollupExited(bytes32 indexed rollupId, address recipient, uint256 bondReturned); event MetadataUpdated(bytes32 indexed rollupId, string newMetadataUri); /* --------------------------------- errors ------------------------------- */ error AlreadyRegistered(); error NotRegistered(); error AlreadyExited(); error NotOwner(); error BondMismatch(); error InvalidSplits(); error SinkBpsTooLow(); error InvalidExitSig(); error TransferFailed(); error ZeroAddress(); /* ----------------------------- constructor ------------------------------ */ constructor( address bondToken, address sink, address foundation, uint256 registrationBond, uint16 minSinkBps, uint64 defaultChallengeWindow ) { if (bondToken == address(0) || sink == address(0) || foundation == address(0)) revert ZeroAddress(); BOND_TOKEN = bondToken; SINK = sink; FOUNDATION = foundation; REGISTRATION_BOND = registrationBond; MIN_SINK_BPS = minSinkBps; DEFAULT_CHALLENGE_WINDOW = defaultChallengeWindow; } /* ----------------------------- registration ----------------------------- */ function registerRollup( bytes32 rollupId, address rollupOwner, address sequencer, address debtToken, uint16 sinkBps, uint16 rollupBps, uint16 sequencerBps, uint256 minChallengeBond, uint64 challengeWindow, string calldata metadataUri ) external nonReentrant returns (address settlementAddr) { if (rollups[rollupId].registered) revert AlreadyRegistered(); if (rollupOwner == address(0) || sequencer == address(0) || debtToken == address(0)) revert ZeroAddress(); if (uint256(sinkBps) + uint256(rollupBps) + uint256(sequencerBps) != 10_000) revert InvalidSplits(); if (sinkBps < MIN_SINK_BPS) revert SinkBpsTooLow(); if (challengeWindow == 0) challengeWindow = DEFAULT_CHALLENGE_WINDOW; // Pull bond. if (REGISTRATION_BOND > 0) { if (!IERC20(BOND_TOKEN).transferFrom(msg.sender, address(this), REGISTRATION_BOND)) revert TransferFailed(); } // Deploy settlement contract. AereRollupSettlement.Config memory cfg = AereRollupSettlement.Config({ rollupId: rollupId, rollupOwner: rollupOwner, sequencer: sequencer, sink: SINK, debtToken: debtToken, challengeWindow: challengeWindow, sinkBps: sinkBps, rollupBps: rollupBps, sequencerBps: sequencerBps, minChallengeBond: minChallengeBond }); AereRollupSettlement s = new AereRollupSettlement(cfg); settlementAddr = address(s); rollups[rollupId] = Rollup({ registered: true, exited: false, settlement: settlementAddr, rollupOwner: rollupOwner, bond: REGISTRATION_BOND, registeredAt: uint64(block.timestamp), metadataUri: metadataUri }); rollupIds.push(rollupId); emit RollupRegistered(rollupId, rollupOwner, settlementAddr, REGISTRATION_BOND, metadataUri); } function updateMetadata(bytes32 rollupId, string calldata newMetadataUri) external { Rollup storage r = rollups[rollupId]; if (!r.registered) revert NotRegistered(); if (msg.sender != r.rollupOwner) revert NotOwner(); r.metadataUri = newMetadataUri; emit MetadataUpdated(rollupId, newMetadataUri); } /* -------------------------------- exit ---------------------------------- */ /// @notice Phase 1: rollupOwner submits a Foundation-signed attestation /// that the rollup has gracefully exited (no pending challenges, /// N quiet epochs since last state root). Factory verifies /// signature and returns the bond. function exitRollup( bytes32 rollupId, address recipient, uint64 attestationDeadline, bytes calldata foundationSig ) external nonReentrant { Rollup storage r = rollups[rollupId]; if (!r.registered) revert NotRegistered(); if (r.exited) revert AlreadyExited(); if (msg.sender != r.rollupOwner) revert NotOwner(); if (recipient == address(0)) revert ZeroAddress(); if (block.timestamp > attestationDeadline) revert InvalidExitSig(); bytes32 hash = keccak256(abi.encode( "AereRaaSFactory:exit", block.chainid, address(this), rollupId, recipient, attestationDeadline )); bytes32 ethHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)); address recovered = _recover(ethHash, foundationSig); if (recovered == address(0) || recovered != FOUNDATION) revert InvalidExitSig(); uint256 bond = r.bond; r.bond = 0; r.exited = true; if (bond > 0) { if (!IERC20(BOND_TOKEN).transfer(recipient, bond)) revert TransferFailed(); } emit RollupExited(rollupId, recipient, bond); } /* ---------------------------------- views ------------------------------- */ function rollupCount() external view returns (uint256) { return rollupIds.length; } /* ------------------------------- internal ------------------------------- */ function _recover(bytes32 hash, bytes calldata sig) internal pure returns (address) { if (sig.length != 65) return address(0); bytes32 r; bytes32 s; uint8 v; assembly { r := calldataload(sig.offset) s := calldataload(add(sig.offset, 32)) v := byte(0, calldataload(add(sig.offset, 64))) } if (v < 27) v += 27; return ecrecover(hash, v, r, s); } }