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.
228 lines
8.4 KiB
Solidity
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);
|
|
}
|
|
}
|