Some checks failed
contracts-ci / Install (lockfile) → compile → full test suite (push) Has been cancelled
contracts-ci / Ethereum interop (EIP-2537 BLS, prague hardfork) (push) Has been cancelled
contracts-ci / PQC known-answer tests (NIST vectors) (push) Has been cancelled
contracts-ci / Coverage (scoped, with artifacts) (push) Has been cancelled
The published line and the local line had no common ancestor: the public one carried the redaction pass, the local one carried three weeks of corrections that never shipped. This commit ports the local work onto the public line, keeps every public redaction, and extends the same discretion to seven client mentions that were still named in published comments. Carried: LICENSE year and LICENSING.md; the measured burn figures replacing the deflation claim (the vault holds ~0.137 AERE of 2.8 billion, and burn is a share of validator coinbase revenue, which is zero today); 'audited' removed from next to Bouncy Castle; citation paths rewritten to published form with CITATIONS-UNRESOLVED.md remeasured 2026-08-11; VERIFY-POLICY.md; slashing and ownership comments brought down to what the code does; the AerePyth repair; the shutter test helper the tests cite; runnable package.json entries; the CI file split into a GitHub/Gitea twin pair with a real measured test-run status; and the .gitignore hardening written after a compiled artifact leaked a local path in a sibling repository. A false '2-of-3 multisig' description of the owner account is corrected to what the chain measures: an externally owned account. The self-audit findings catalog stays unpublished pending an explicit decision.
245 lines
9.9 KiB
Solidity
245 lines
9.9 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.
|
|
*
|
|
* WHAT EVERY ROLLUP STAMPED BY THIS FACTORY STILL DOES NOT GET.
|
|
* Added 2026-08-01, alongside the same warning restored in the template.
|
|
* The settlement template this factory deploys carries NO FRAUD-PROOF
|
|
* VERIFIER. Nothing in it checks whether a proposed state root is
|
|
* correct. Its challenge game is Phase 1: a challenger posts a bond and
|
|
* resolveChallenge is callable only by the sequencer, which is the party
|
|
* whose own root is being challenged; a correct challenge can be defeated
|
|
* by the accused, and the honest challenger then loses the bond. The
|
|
* permissionless resolveChallengeExpired path only covers a sequencer
|
|
* that ignores the challenge past the grace window. A rollup registered
|
|
* here is therefore secured by its sequencer's honesty plus off-chain
|
|
* cost, NOT by a proof, and must not be described as trust-minimised on
|
|
* the strength of the challenge window. Phase 2 replaces sequencer-side
|
|
* resolution with a real on-chain fraud-proof verifier.
|
|
*
|
|
* That paragraph is comments only: Solidity comments emit no
|
|
* instructions, so the executable runtime bytecode is unchanged and only
|
|
* the embedded metadata hash moves.
|
|
*
|
|
* 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);
|
|
}
|
|
}
|