Skip to content

Commit 91e4937

Browse files
committed
feat: operator table updater pauser (#1501)
**Motivation:** Currently, `disableRoot` is called by the `owner` of the `OperatorTableCalculator`. We want to update such that it can be called by the `pauser`. This is to enable cleaner operations. **Modifications:** - Add `Pausable` to `operatorTableCalculator` - Guard `disableGlobalTableRoot` with `onlyPauser` - Guard `updateGlobalTableRoot` and `updateOperatorTable` with `whenNotPaused(index)` **Result:** Cleaner disable mechanics.
1 parent ca1d124 commit 91e4937

File tree

9 files changed

+577
-38
lines changed

9 files changed

+577
-38
lines changed

docs/multichain/destination/OperatorTableUpdater.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ Confirms a new global table root by verifying a BN254 certificate signed by the
6969
* Emits a `NewGlobalTableRoot` event
7070

7171
*Requirements*:
72+
* The contract MUST NOT be paused for global root updates
7273
* The `referenceTimestamp` MUST NOT be in the future
7374
* The `referenceTimestamp` MUST be greater than `_latestReferenceTimestamp`
7475
* The certificate's `messageHash` MUST match the expected EIP-712 hash
@@ -110,6 +111,7 @@ Updates an operator table by verifying its inclusion in a confirmed global table
110111
* Calls `ecdsaCertificateVerifier.updateOperatorTable` with the decoded operator info
111112

112113
*Requirements*:
114+
* The contract MUST NOT be paused for operator table updates
113115
* The `globalTableRoot` MUST be valid (not disabled)
114116
* The `referenceTimestamp` MUST be greater than the latest timestamp for the operator set
115117
* The merkle proof MUST verify the operator table's inclusion in the global root
@@ -174,21 +176,21 @@ Sets the stake proportion threshold required for confirming global table roots.
174176
/**
175177
* @notice Disables a global table root
176178
* @param globalTableRoot the global table root to disable
177-
* @dev Only callable by the owner of the contract
179+
* @dev Only callable by the pauser
178180
*/
179181
function disableRoot(
180182
bytes32 globalTableRoot
181183
) external;
182184
```
183185

184-
Disables a global table root, preventing further operator table updates against it. This function also prevents the `CertificateVerifier` from verifying certificates. The function is intended to prevent a malicious or invalid root from being used by downstream consumers.
186+
Disables a global table root, preventing further operator table updates against it. This function also prevents the `CertificateVerifier` from verifying certificates. The function is intended to prevent a malicious or invalid root from being used by downstream consumers. Once a root is disabled, it cannot be re-enabled.
185187

186188
*Effects*:
187189
* Sets `_isRootValid[globalTableRoot]` to `false`
188190
* Emits a `GlobalRootDisabled` event
189191

190192
*Requirements*:
191-
* Caller MUST be the `owner`
193+
* Caller MUST be the `pauser`
192194
* The `globalTableRoot` MUST exist and be currently valid
193195

194196
### `updateGenerator`
@@ -217,4 +219,4 @@ Updates the operator table for the `generator` itself. This operatorSet is a ["s
217219

218220
*Requirements*:
219221
* Caller MUST be the `owner`
220-
* Meet all requirements in [`bn254CertificateVerifier.updateOperatorTable`](../destination/CertificateVerifier.md#updateoperatortable-1)
222+
* Meet all requirements in [`bn254CertificateVerifier.updateOperatorTable`](../destination/CertificateVerifier.md#updateoperatortable-1)

pkg/bindings/BN254CertificateVerifier/binding.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/bindings/ECDSACertificateVerifier/binding.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/bindings/OperatorTableUpdater/binding.go

Lines changed: 462 additions & 16 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/contracts/interfaces/IOperatorTableUpdater.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ interface IOperatorTableUpdater is
135135
/**
136136
* @notice Disables a global table root
137137
* @param globalTableRoot the global table root to disable
138-
* @dev Only callable by the owner of the contract
138+
* @dev Only callable by the pauser
139139
*/
140140
function disableRoot(
141141
bytes32 globalTableRoot

src/contracts/multichain/OperatorTableUpdater.sol

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,17 @@ import "@openzeppelin-upgrades/contracts/proxy/utils/Initializable.sol";
55
import "@openzeppelin-upgrades/contracts/access/OwnableUpgradeable.sol";
66

77
import "../libraries/Merkle.sol";
8+
import "../permissions/Pausable.sol";
89
import "../mixins/SemVerMixin.sol";
910
import "./OperatorTableUpdaterStorage.sol";
1011

11-
contract OperatorTableUpdater is Initializable, OwnableUpgradeable, OperatorTableUpdaterStorage, SemVerMixin {
12+
contract OperatorTableUpdater is
13+
Initializable,
14+
OwnableUpgradeable,
15+
Pausable,
16+
OperatorTableUpdaterStorage,
17+
SemVerMixin
18+
{
1219
/**
1320
*
1421
* INITIALIZING FUNCTIONS
@@ -17,14 +24,20 @@ contract OperatorTableUpdater is Initializable, OwnableUpgradeable, OperatorTabl
1724
constructor(
1825
IBN254CertificateVerifier _bn254CertificateVerifier,
1926
IECDSACertificateVerifier _ecdsaCertificateVerifier,
27+
IPauserRegistry _pauserRegistry,
2028
string memory _version
21-
) OperatorTableUpdaterStorage(_bn254CertificateVerifier, _ecdsaCertificateVerifier) SemVerMixin(_version) {
29+
)
30+
OperatorTableUpdaterStorage(_bn254CertificateVerifier, _ecdsaCertificateVerifier)
31+
Pausable(_pauserRegistry)
32+
SemVerMixin(_version)
33+
{
2234
_disableInitializers();
2335
}
2436

2537
/**
2638
* @notice Initializes the OperatorTableUpdater
2739
* @param owner The owner of the OperatorTableUpdater
40+
* @param initialPausedStatus The initial paused status of the OperatorTableUpdater
2841
* @param _generator The operatorSet which certifies against global roots
2942
* @param _globalRootConfirmationThreshold The threshold, in bps, for a global root to be signed off on and updated
3043
* @param referenceTimestamp The reference timestamp for the global root confirmer set
@@ -35,13 +48,15 @@ contract OperatorTableUpdater is Initializable, OwnableUpgradeable, OperatorTabl
3548
*/
3649
function initialize(
3750
address owner,
51+
uint256 initialPausedStatus,
3852
OperatorSet calldata _generator,
3953
uint16 _globalRootConfirmationThreshold,
4054
uint32 referenceTimestamp,
4155
BN254OperatorSetInfo calldata generatorInfo,
4256
OperatorSetConfig calldata generatorConfig
4357
) external initializer {
4458
_transferOwnership(owner);
59+
_setPausedStatus(initialPausedStatus);
4560
_setGenerator(_generator);
4661
_setGlobalRootConfirmationThreshold(_globalRootConfirmationThreshold);
4762
_updateGenerator(referenceTimestamp, generatorInfo, generatorConfig);
@@ -73,7 +88,7 @@ contract OperatorTableUpdater is Initializable, OwnableUpgradeable, OperatorTabl
7388
bytes32 globalTableRoot,
7489
uint32 referenceTimestamp,
7590
uint32 referenceBlockNumber
76-
) external {
91+
) external onlyWhenNotPaused(PAUSED_GLOBAL_ROOT_UPDATE) {
7792
// Table roots can only be updated for current or past timestamps and after the latest reference timestamp
7893
require(referenceTimestamp <= block.timestamp, GlobalTableRootInFuture());
7994
require(referenceTimestamp > _latestReferenceTimestamp, GlobalTableRootStale());
@@ -109,7 +124,7 @@ contract OperatorTableUpdater is Initializable, OwnableUpgradeable, OperatorTabl
109124
uint32 operatorSetIndex,
110125
bytes calldata proof,
111126
bytes calldata operatorTableBytes
112-
) external {
127+
) external onlyWhenNotPaused(PAUSED_OPERATOR_TABLE_UPDATE) {
113128
(
114129
OperatorSet memory operatorSet,
115130
CurveType curveType,
@@ -173,7 +188,7 @@ contract OperatorTableUpdater is Initializable, OwnableUpgradeable, OperatorTabl
173188
/// @inheritdoc IOperatorTableUpdater
174189
function disableRoot(
175190
bytes32 globalTableRoot
176-
) external onlyOwner {
191+
) external onlyPauser {
177192
// Check that the root already exists and is not disabled
178193
require(_isRootValid[globalTableRoot], InvalidRoot());
179194

src/contracts/multichain/OperatorTableUpdaterStorage.sol

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ import "../interfaces/IECDSACertificateVerifier.sol";
88
abstract contract OperatorTableUpdaterStorage is IOperatorTableUpdater {
99
// Constants
1010

11+
/// @notice Index for flag that pauses calling `updateGlobalTableRoot`
12+
uint8 internal constant PAUSED_GLOBAL_ROOT_UPDATE = 0;
13+
14+
/// @notice Index for flag that pauses calling `updateOperatorTable`
15+
uint8 internal constant PAUSED_OPERATOR_TABLE_UPDATE = 1;
16+
1117
bytes32 public constant GLOBAL_TABLE_ROOT_CERT_TYPEHASH =
1218
keccak256("GlobalTableRootCert(bytes32 globalTableRoot,uint32 referenceTimestamp,uint32 referenceBlockNumber)");
1319

src/test/tree/OperatorTableUpdaterUnit.tree

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
│ └── given that the contract is not initialized
77
│ └── it should set the owner, generator, threshold, and update operator table & emit events
88
├── when confirmGlobalTableRoot is called
9+
│ ├── given that the contract is paused for global root updates
10+
│ │ └── it should revert with CurrentlyPaused
911
│ ├── given that the reference timestamp is in the future
1012
│ │ └── it should revert with GlobalTableRootInFuture
1113
│ ├── given that the reference timestamp is not greater than latest
@@ -17,6 +19,8 @@
1719
│ └── given that all parameters are valid
1820
│ └── it should update global table root, reference timestamp, block number mappings (both directions) & emit NewGlobalTableRoot event
1921
├── when updateOperatorTable is called
22+
│ ├── given that the contract is paused for operator table updates
23+
│ │ └── it should revert with CurrentlyPaused
2024
│ ├── given that the reference timestamp is not greater than operator set's latest
2125
│ │ └── it should revert with TableUpdateForPastTimestamp
2226
│ ├── given that the global table root is not valid
@@ -44,11 +48,11 @@
4448
│ └── given that the caller is owner and threshold is valid
4549
│ └── it should update the threshold & emit GlobalRootConfirmationThresholdUpdated event
4650
├── when disableRoot is called
47-
│ ├── given that the caller is not the owner
51+
│ ├── given that the caller is not the pauser
4852
│ │ └── it should revert
4953
│ ├── given that the root is invalid or doesn't exist
5054
│ │ └── it should revert with InvalidRoot
51-
│ └── given that the caller is the owner and root is valid
55+
│ └── given that the caller is the pauser and root is valid
5256
│ └── it should disable the root & emit GlobalRootDisabled event
5357
├── when updateGenerator is called
5458
│ ├── given that the caller is not the owner

src/test/unit/OperatorTableUpdaterUnit.t.sol

Lines changed: 74 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ contract OperatorTableUpdaterUnitTests is
4646
operatorTableUpdaterImplementation = new OperatorTableUpdater(
4747
IBN254CertificateVerifier(address(bn254CertificateVerifierMock)),
4848
IECDSACertificateVerifier(address(ecdsaCertificateVerifierMock)),
49+
pauserRegistry,
4950
"1.0.0"
5051
);
5152

@@ -55,6 +56,7 @@ contract OperatorTableUpdaterUnitTests is
5556
abi.encodeWithSelector(
5657
OperatorTableUpdater.initialize.selector,
5758
address(this), // owner
59+
0, // initialPausedStatus
5860
generator, // generator
5961
GLOBAL_ROOT_CONFIRMATION_THRESHOLD, // globalRootConfirmationThreshold
6062
block.timestamp - 1, // referenceTimestamp
@@ -194,6 +196,7 @@ contract OperatorTableUpdaterUnitTests_initialize is OperatorTableUpdaterUnitTes
194196
cheats.expectRevert("Initializable: contract is already initialized");
195197
operatorTableUpdater.initialize(
196198
address(this),
199+
uint(0),
197200
generator,
198201
GLOBAL_ROOT_CONFIRMATION_THRESHOLD,
199202
uint32(block.timestamp - 1),
@@ -214,6 +217,23 @@ contract OperatorTableUpdaterUnitTests_confirmGlobalTableRoot is OperatorTableUp
214217
operatorTableUpdater.confirmGlobalTableRoot(mockCertificate, bytes32(0), referenceTimestamp + 1, referenceBlockNumber);
215218
}
216219

220+
function testFuzz_revert_paused(Randomness r) public rand(r) {
221+
// Pause the confirmGlobalTableRoot functionality (bit index 0)
222+
uint pausedStatus = 1 << 0; // Set bit 0 to pause PAUSED_GLOBAL_ROOT_UPDATE
223+
cheats.prank(pauser);
224+
operatorTableUpdater.pause(pausedStatus);
225+
226+
uint32 referenceTimestamp = r.Uint32(operatorTableUpdater.getLatestReferenceTimestamp() + 1, type(uint32).max);
227+
uint32 referenceBlockNumber = r.Uint32();
228+
bytes32 globalTableRoot = bytes32(r.Uint256(1, type(uint).max));
229+
mockCertificate.messageHash =
230+
operatorTableUpdater.getGlobalTableUpdateMessageHash(globalTableRoot, referenceTimestamp, referenceBlockNumber);
231+
232+
// Try to confirm a global table root while paused
233+
cheats.expectRevert(IPausable.CurrentlyPaused.selector);
234+
operatorTableUpdater.confirmGlobalTableRoot(mockCertificate, globalTableRoot, referenceTimestamp, referenceBlockNumber);
235+
}
236+
217237
function testFuzz_revert_staleCertificate(Randomness r) public rand(r) {
218238
uint32 referenceBlockNumber = uint32(block.number);
219239
mockCertificate.messageHash =
@@ -273,6 +293,27 @@ contract OperatorTableUpdaterUnitTests_updateOperatorTable_BN254 is OperatorTabl
273293
bn254CertificateVerifierMock.setLatestReferenceTimestamp(operatorSet, referenceTimestamp);
274294
}
275295

296+
function testFuzz_BN254_revert_paused(Randomness r) public rand(r) {
297+
// Pause the updateOperatorTable functionality (bit index 1)
298+
uint pausedStatus = 1 << 1; // Set bit 1 to pause PAUSED_OPERATOR_TABLE_UPDATE
299+
cheats.prank(pauser);
300+
operatorTableUpdater.pause(pausedStatus);
301+
302+
// Generate random operatorSetInfo and operatorSetConfig
303+
BN254OperatorSetInfo memory operatorSetInfo = _generateRandomBN254OperatorSetInfo(r);
304+
bytes memory operatorSetInfoBytes = abi.encode(operatorSetInfo);
305+
OperatorSetConfig memory operatorSetConfig = _generateRandomOperatorSetConfig(r);
306+
bytes memory operatorTable = abi.encode(defaultOperatorSet, CurveType.BN254, operatorSetConfig, operatorSetInfoBytes);
307+
308+
// First create a valid root
309+
bytes32 globalTableRoot = bytes32(r.Uint256(1, type(uint).max));
310+
_updateGlobalTableRoot(globalTableRoot);
311+
312+
// Try to update operator table while paused
313+
cheats.expectRevert(IPausable.CurrentlyPaused.selector);
314+
operatorTableUpdater.updateOperatorTable(uint32(block.timestamp), globalTableRoot, 0, new bytes(0), operatorTable);
315+
}
316+
276317
function testFuzz_BN254_revert_staleTableUpdate(Randomness r) public rand(r) {
277318
uint32 referenceTimestamp = r.Uint32(uint32(block.timestamp), type(uint32).max);
278319
_setLatestReferenceTimestampBN254(defaultOperatorSet, referenceTimestamp);
@@ -394,6 +435,27 @@ contract OperatorTableUpdaterUnitTests_updateOperatorTable_ECDSA is OperatorTabl
394435
ecdsaCertificateVerifierMock.setLatestReferenceTimestamp(operatorSet, referenceTimestamp);
395436
}
396437

438+
function testFuzz_ECDSA_revert_paused(Randomness r) public rand(r) {
439+
// Pause the updateOperatorTable functionality (bit index 1)
440+
uint pausedStatus = 1 << 1; // Set bit 1 to pause PAUSED_OPERATOR_TABLE_UPDATE
441+
cheats.prank(pauser);
442+
operatorTableUpdater.pause(pausedStatus);
443+
444+
// Generate random operatorInfos and operatorSetConfig
445+
ECDSAOperatorInfo[] memory operatorInfos = _generateRandomECDSAOperatorInfos(r);
446+
bytes memory operatorInfosBytes = abi.encode(operatorInfos);
447+
OperatorSetConfig memory operatorSetConfig = _generateRandomOperatorSetConfig(r);
448+
bytes memory operatorTable = abi.encode(defaultOperatorSet, CurveType.ECDSA, operatorSetConfig, operatorInfosBytes);
449+
450+
// First create a valid root
451+
bytes32 globalTableRoot = bytes32(r.Uint256(1, type(uint).max));
452+
_updateGlobalTableRoot(globalTableRoot);
453+
454+
// Try to update operator table while paused
455+
cheats.expectRevert(IPausable.CurrentlyPaused.selector);
456+
operatorTableUpdater.updateOperatorTable(uint32(block.timestamp), globalTableRoot, 0, new bytes(0), operatorTable);
457+
}
458+
397459
function testFuzz_ECDSA_revert_rootDisabled(Randomness r) public rand(r) {
398460
// Generate random operatorSetInfo and operatorSetConfig
399461
ECDSAOperatorInfo[] memory emptyOperatorSetInfo;
@@ -661,14 +723,14 @@ contract OperatorTableUpdaterUnitTests_setGlobalRootConfirmationThreshold is Ope
661723
}
662724

663725
contract OperatorTableUpdaterUnitTests_disableRoot is OperatorTableUpdaterUnitTests {
664-
function testFuzz_revert_onlyOwner(Randomness r) public rand(r) {
726+
function testFuzz_revert_onlyPauser(Randomness r) public rand(r) {
665727
address invalidCaller = r.Address();
666-
cheats.assume(invalidCaller != address(this));
728+
cheats.assume(invalidCaller != pauser && invalidCaller != address(this));
667729
bytes32 globalTableRoot = bytes32(r.Uint256());
668730

669-
// Should revert when called by non-owner
731+
// Should revert when called by non-pauser
670732
cheats.prank(invalidCaller);
671-
cheats.expectRevert("Ownable: caller is not the owner");
733+
cheats.expectRevert(IPausable.OnlyPauser.selector);
672734
operatorTableUpdater.disableRoot(globalTableRoot);
673735
}
674736

@@ -692,7 +754,8 @@ contract OperatorTableUpdaterUnitTests_disableRoot is OperatorTableUpdaterUnitTe
692754
// Verify the root is valid
693755
assertTrue(operatorTableUpdater.isRootValid(globalTableRoot));
694756

695-
// Disable the root
757+
// Disable the root as pauser
758+
cheats.prank(pauser);
696759
cheats.expectEmit(true, true, true, true);
697760
emit GlobalRootDisabled(globalTableRoot);
698761
operatorTableUpdater.disableRoot(globalTableRoot);
@@ -750,7 +813,8 @@ contract OperatorTableUpdaterUnitTests_isRootValid is OperatorTableUpdaterUnitTe
750813
// Should now be valid
751814
assertTrue(operatorTableUpdater.isRootValid(globalTableRoot));
752815

753-
// Disable the root
816+
// Disable the root as pauser
817+
cheats.prank(pauser);
754818
operatorTableUpdater.disableRoot(globalTableRoot);
755819

756820
// Should now be invalid
@@ -773,7 +837,8 @@ contract OperatorTableUpdaterUnitTests_isRootValid is OperatorTableUpdaterUnitTe
773837
// Should be valid
774838
assertTrue(operatorTableUpdater.isRootValidByTimestamp(referenceTimestamp));
775839

776-
// Disable the root
840+
// Disable the root as pauser
841+
cheats.prank(pauser);
777842
operatorTableUpdater.disableRoot(globalTableRoot);
778843

779844
// Should now be invalid when queried by timestamp
@@ -787,7 +852,8 @@ contract OperatorTableUpdaterUnitTests_IntegrationScenarios is OperatorTableUpda
787852
bytes32 oldGlobalTableRoot = bytes32(uint(1));
788853
_updateGlobalTableRoot(oldGlobalTableRoot);
789854

790-
// Step 2: Disable the old root
855+
// Step 2: Disable the old root as pauser
856+
cheats.prank(pauser);
791857
operatorTableUpdater.disableRoot(oldGlobalTableRoot);
792858

793859
// Step 3: Set a new generator

0 commit comments

Comments
 (0)