// SPDX-License-Identifier: MIT pragma solidity 0.8.23; import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "./AereRollupSettlementV2.sol"; /** * @title AereRaaSFactoryV2 — permissionless Rollup-as-a-Service registry (bug-fixed template) * @notice Bug-fix redeploy of AereRaaSFactory (0x8C1b0018ab8C4299a75621a4BdD3cF26971B26Cd, * 0 rollups registered). Identical factory model — permissionless * registration, refundable bond, MIN_SINK_BPS floor, no admin pause / * slash / upgrade — with ONE change: it deploys AereRollupSettlementV2 * as the per-rollup settlement template instead of the buggy V1. * * AereRollupSettlementV2 fixes F6 (last-block challenge grief), * F7 (rejected-epoch finalisation freeze), the isFinalised ancestor * medium, and the settleRevenue stuck-residual low. See that contract. * * New immutable DEFAULT_DEFENSE_GRACE is threaded into every settlement * the factory deploys (per-registration override allowed, 0 → default), * mirroring DEFAULT_CHALLENGE_WINDOW. * * IMMUTABILITY at factory deploy: BOND_TOKEN, SINK, REGISTRATION_BOND, * MIN_SINK_BPS, DEFAULT_CHALLENGE_WINDOW, DEFAULT_DEFENSE_GRACE, * FOUNDATION. The factory cannot retroactively change a registered * rollup's parameters. */ contract AereRaaSFactoryV2 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; uint64 public immutable DEFAULT_DEFENSE_GRACE; // V2: default sequencer defense grace 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, uint64 defaultDefenseGrace ) { 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; DEFAULT_DEFENSE_GRACE = defaultDefenseGrace; } /* ----------------------------- registration ----------------------------- */ function registerRollup( bytes32 rollupId, address rollupOwner, address sequencer, address debtToken, uint16 sinkBps, uint16 rollupBps, uint16 sequencerBps, uint256 minChallengeBond, uint64 challengeWindow, uint64 defenseGrace, 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; if (defenseGrace == 0) defenseGrace = DEFAULT_DEFENSE_GRACE; // Pull bond. if (REGISTRATION_BOND > 0) { if (!IERC20(BOND_TOKEN).transferFrom(msg.sender, address(this), REGISTRATION_BOND)) revert TransferFailed(); } // Deploy settlement contract (V2 template). AereRollupSettlementV2.Config memory cfg = AereRollupSettlementV2.Config({ rollupId: rollupId, rollupOwner: rollupOwner, sequencer: sequencer, sink: SINK, debtToken: debtToken, challengeWindow: challengeWindow, defenseGrace: defenseGrace, sinkBps: sinkBps, rollupBps: rollupBps, sequencerBps: sequencerBps, minChallengeBond: minChallengeBond }); AereRollupSettlementV2 s = new AereRollupSettlementV2(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. Factory verifies signature and /// returns the bond. (Unchanged from V1.) 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); } }