aere-contracts/contracts/raas/AereRaaSFactory.sol
Aere Network a13a649b77
Some checks are pending
contracts-ci / Install (lockfile) → compile → full test suite (push) Waiting to run
contracts-ci / PQC known-answer tests (NIST vectors) (push) Waiting to run
contracts-ci / Coverage (scoped, with artifacts) (push) Waiting to run
Initial public release
Aere Network public source. Everything here can be checked against the live
chain (chain id 2800, https://rpc.aere.network).

Scope note, stated up front rather than buried: consensus on chain 2800 is
classical secp256k1 ECDSA QBFT. The post-quantum work in this repository is at
the signature, precompile, account and transport layers. Nothing here makes the
consensus post-quantum, and no document in it should be read as claiming so.
2026-07-20 01:02:37 +03:00

228 lines
8.4 KiB
Solidity

// 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);
}
}