aere-contracts/contracts/raas/AereRaaSFactoryV2.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

226 lines
8.6 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 "./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);
}
}