@@ -275,28 +275,45 @@ contract EscapeHatch is IEscapeHatch {
275275
276276 require (block .timestamp >= data.exitableAt, Errors.EscapeHatch__NotExitableYet (data.exitableAt, block .timestamp ));
277277
278+ // Check if this contract was the active escape hatch for the entire active period.
279+ // If not, the proposer may have been unable to fulfill duties due to governance change.
280+ Epoch firstActiveEpoch = _getFirstEpoch (_hatch);
281+ Epoch lastActiveEpoch = firstActiveEpoch + Epoch.wrap (ACTIVE_DURATION - 1 );
282+ bool wasActiveAtStart = address (ROLLUP.getEscapeHatchForEpoch (firstActiveEpoch)) == address (this );
283+ bool wasActiveAtEnd = address (ROLLUP.getEscapeHatchForEpoch (lastActiveEpoch)) == address (this );
284+ bool wasActiveEntirePeriod = wasActiveAtStart && wasActiveAtEnd;
285+
278286 bool success = true ;
279287 uint256 punishment = 0 ;
280288
281- // Check success conditions:
282- // 1. Something must have been proposed
283- if (data.lastCheckpointNumber == 0 ) {
284- success = false ;
285- }
286-
287- // 2. Proofs must have been submitted at least up to this checkpoint
288- if (success && ROLLUP.getProvenCheckpointNumber () < data.lastCheckpointNumber) {
289- success = false ;
290- }
291-
292- // 3. The checkpoint archive must still be in the chain (not pruned)
293- if (success && ROLLUP.archiveAt (data.lastCheckpointNumber) != data.lastSubmittedArchive) {
294- success = false ;
295- }
296-
297- if (! success) {
298- punishment = FAILED_HATCH_PUNISHMENT;
299- data.amount -= FAILED_HATCH_PUNISHMENT;
289+ if (! wasActiveEntirePeriod && data.lastCheckpointNumber == 0 ) {
290+ // Escape hatch was deactivated during the active window and proposer did nothing.
291+ // This is acceptable - they couldn't (or chose not to) propose during disruption.
292+ // Skip punishment, transition to EXITING.
293+ } else {
294+ // Normal validation: either was active the entire time, or proposer proposed something
295+ // (if they proposed, they're on the hook regardless of escape hatch changes,
296+ // since proofs go to the rollup directly and are unaffected by escape hatch changes).
297+
298+ // 1. Something must have been proposed
299+ if (data.lastCheckpointNumber == 0 ) {
300+ success = false ;
301+ }
302+
303+ // 2. Proofs must have been submitted at least up to this checkpoint
304+ if (success && ROLLUP.getProvenCheckpointNumber () < data.lastCheckpointNumber) {
305+ success = false ;
306+ }
307+
308+ // 3. The checkpoint archive must still be in the chain (not pruned)
309+ if (success && ROLLUP.archiveAt (data.lastCheckpointNumber) != data.lastSubmittedArchive) {
310+ success = false ;
311+ }
312+
313+ if (! success) {
314+ punishment = FAILED_HATCH_PUNISHMENT;
315+ data.amount -= FAILED_HATCH_PUNISHMENT;
316+ }
300317 }
301318
302319 data.status = Status.EXITING;
@@ -547,6 +564,14 @@ contract EscapeHatch is IEscapeHatch {
547564 * @custom:reverts EscapeHatch__SetUnstable if called before the freeze timestamp (defense in depth)
548565 */
549566 function selectCandidates () public override (IEscapeHatchCore) {
567+ // Don't select new candidates if this contract is no longer the active escape hatch.
568+ // We check the latest value rather than the epoch-stable one since we sample for the future,
569+ // so if the current differs, the future will as well.
570+ // Early return (not revert) is important because initiateExit() calls selectCandidates() internally.
571+ if (address (ROLLUP.getEscapeHatch ()) != address (this )) {
572+ return ;
573+ }
574+
550575 Hatch currentHatch = getCurrentHatch ();
551576 Hatch targetHatch = currentHatch + Hatch.wrap (LAG_IN_HATCHES);
552577
0 commit comments