bug(SuperchainConfig): extend() can re-activate an expired pause, bypassing Stage 1 requirement
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 78/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Quiet
- Tech stack
- solidity
- Domain
- blockchain, security
Research direction
Start with SuperchainConfig.extend(), pause(), and paused(), then inspect the existing SuperchainConfig tests around guardian pauses and PAUSE_EXPIRY. Add coverage for extending an expired pause and verify it reverts with SuperchainConfig_NotAlreadyPaused; done means expired pauses cannot be reactivated through extend().
Written by the indexing model from the issue text.
Description
Summary
SuperchainConfig.extend() allows the guardian to re-activate an expired pause by resetting its timestamp, bypassing the Stage 1 Decentralization requirement that the guardian must explicitly unpause() before pause() can be called again.
Root Cause
pause() correctly enforces the invariant (lines 91–96):
// "intentionally prevents re-pausing even after a pause has expired
// (when paused() returns false but the timestamp is still non-zero).
// This is a Stage 1 Decentralization requirement."
if (pauseTimestamps[_identifier] != 0) revert SuperchainConfig_AlreadyPaused(_identifier);
But extend() uses a weaker check (lines 122–125):
// Only reverts when timestamp == 0 (never paused), NOT when expired
if (pauseTimestamps[_identifier] == 0) revert SuperchainConfig_NotAlreadyPaused(_identifier);
pauseTimestamps[_identifier] = block.timestamp; // re-activates expired pause!
An expired pause has pauseTimestamps[id] != 0 AND paused(id) == false. It passes the extend() check but would correctly revert pause().
Attack Scenario
- Guardian calls
pause(id)→pauseTimestamps[id] = T - 3 months pass →
paused(id)returnsfalse(expired), butpauseTimestamps[id] = T ≠ 0 pause(id)→ reverts ✅ (Stage 1 enforced)extend(id)→ succeeds ❌ →pauseTimestamps[id] = now→ system re-paused without the required unpause cycle
Missing Test
No test covers extend() called after a pause expires:
function test_extend_expiredPause_shouldRevert() external {
_pauseAsGuardian(address(this));
vm.warp(block.timestamp + PAUSE_EXPIRY + 1);
// paused() is false — pause has expired
assertFalse(superchainConfig.paused(address(this)));
// extend() should revert here but currently succeeds
vm.prank(superchainConfig.guardian());
vm.expectRevert(
abi.encodeWithSelector(ISuperchainConfig.SuperchainConfig_NotAlreadyPaused.selector, address(this))
);
superchainConfig.extend(address(this)); // BUG: does not revert
}
Fix
function extend(address _identifier) external {
_assertOnlyGuardian();
// Use paused() instead of raw timestamp check to catch expired pauses
if (!paused(_identifier)) {
revert SuperchainConfig_NotAlreadyPaused(_identifier);
}
pauseTimestamps[_identifier] = block.timestamp;
emit PauseExtended(_identifier);
}
- Dominant language
- Solidity
- Stars
- 327
- Forks
- 245
- Avg merge
- 20h 41m
- Merged PRs (30d)
- 22
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from base/contracts
-
Difficulty 1/5 1-3 hours Newbie friendliness 86/100
-
Difficulty 3/5 1-2 days Newbie friendliness 78/100
-
Difficulty 3/5 1-2 days Newbie friendliness 48/100
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
MystenLabs/sui#28056 · 1 comment ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
filecoin-project/solstice#76 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
CypherBoxLLC/Cypher-Box#283 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
blinklabs-io/bursa#904 ·