Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions docs/multichain/destination/OperatorTableUpdater.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ Confirms a new global table root by verifying a BN254 certificate signed by the
* Emits a `NewGlobalTableRoot` event

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

*Requirements*:
* The contract MUST NOT be paused for operator table updates
* The `globalTableRoot` MUST be valid (not disabled)
* The `referenceTimestamp` MUST be greater than the latest timestamp for the operator set
* The merkle proof MUST verify the operator table's inclusion in the global root
Expand Down Expand Up @@ -174,21 +176,21 @@ Sets the stake proportion threshold required for confirming global table roots.
/**
* @notice Disables a global table root
* @param globalTableRoot the global table root to disable
* @dev Only callable by the owner of the contract
* @dev Only callable by the pauser
*/
function disableRoot(
bytes32 globalTableRoot
) external;
```

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.
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.

*Effects*:
* Sets `_isRootValid[globalTableRoot]` to `false`
* Emits a `GlobalRootDisabled` event

*Requirements*:
* Caller MUST be the `owner`
* Caller MUST be the `pauser`
* The `globalTableRoot` MUST exist and be currently valid

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

*Requirements*:
* Caller MUST be the `owner`
* Meet all requirements in [`bn254CertificateVerifier.updateOperatorTable`](../destination/CertificateVerifier.md#updateoperatortable-1)
* Meet all requirements in [`bn254CertificateVerifier.updateOperatorTable`](../destination/CertificateVerifier.md#updateoperatortable-1)
2 changes: 1 addition & 1 deletion pkg/bindings/BN254CertificateVerifier/binding.go

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion pkg/bindings/ECDSACertificateVerifier/binding.go

Large diffs are not rendered by default.

478 changes: 462 additions & 16 deletions pkg/bindings/OperatorTableUpdater/binding.go

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/contracts/interfaces/IOperatorTableUpdater.sol
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ interface IOperatorTableUpdater is
/**
* @notice Disables a global table root
* @param globalTableRoot the global table root to disable
* @dev Only callable by the owner of the contract
* @dev Only callable by the pauser
*/
function disableRoot(
bytes32 globalTableRoot
Expand Down
25 changes: 20 additions & 5 deletions src/contracts/multichain/OperatorTableUpdater.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,17 @@ import "@openzeppelin-upgrades/contracts/proxy/utils/Initializable.sol";
import "@openzeppelin-upgrades/contracts/access/OwnableUpgradeable.sol";

import "../libraries/Merkle.sol";
import "../permissions/Pausable.sol";
import "../mixins/SemVerMixin.sol";
import "./OperatorTableUpdaterStorage.sol";

contract OperatorTableUpdater is Initializable, OwnableUpgradeable, OperatorTableUpdaterStorage, SemVerMixin {
contract OperatorTableUpdater is
Initializable,
OwnableUpgradeable,
Pausable,
OperatorTableUpdaterStorage,
SemVerMixin
{
/**
*
* INITIALIZING FUNCTIONS
Expand All @@ -17,14 +24,20 @@ contract OperatorTableUpdater is Initializable, OwnableUpgradeable, OperatorTabl
constructor(
IBN254CertificateVerifier _bn254CertificateVerifier,
IECDSACertificateVerifier _ecdsaCertificateVerifier,
IPauserRegistry _pauserRegistry,
string memory _version
) OperatorTableUpdaterStorage(_bn254CertificateVerifier, _ecdsaCertificateVerifier) SemVerMixin(_version) {
)
OperatorTableUpdaterStorage(_bn254CertificateVerifier, _ecdsaCertificateVerifier)
Pausable(_pauserRegistry)
SemVerMixin(_version)
{
_disableInitializers();
}

/**
* @notice Initializes the OperatorTableUpdater
* @param owner The owner of the OperatorTableUpdater
* @param initialPausedStatus The initial paused status of the OperatorTableUpdater
* @param _generator The operatorSet which certifies against global roots
* @param _globalRootConfirmationThreshold The threshold, in bps, for a global root to be signed off on and updated
* @param referenceTimestamp The reference timestamp for the global root confirmer set
Expand All @@ -35,13 +48,15 @@ contract OperatorTableUpdater is Initializable, OwnableUpgradeable, OperatorTabl
*/
function initialize(
address owner,
uint256 initialPausedStatus,
OperatorSet calldata _generator,
uint16 _globalRootConfirmationThreshold,
uint32 referenceTimestamp,
BN254OperatorSetInfo calldata generatorInfo,
OperatorSetConfig calldata generatorConfig
) external initializer {
_transferOwnership(owner);
_setPausedStatus(initialPausedStatus);
_setGenerator(_generator);
_setGlobalRootConfirmationThreshold(_globalRootConfirmationThreshold);
_updateGenerator(referenceTimestamp, generatorInfo, generatorConfig);
Expand Down Expand Up @@ -73,7 +88,7 @@ contract OperatorTableUpdater is Initializable, OwnableUpgradeable, OperatorTabl
bytes32 globalTableRoot,
uint32 referenceTimestamp,
uint32 referenceBlockNumber
) external {
) external onlyWhenNotPaused(PAUSED_GLOBAL_ROOT_UPDATE) {
// Table roots can only be updated for current or past timestamps and after the latest reference timestamp
require(referenceTimestamp <= block.timestamp, GlobalTableRootInFuture());
require(referenceTimestamp > _latestReferenceTimestamp, GlobalTableRootStale());
Expand Down Expand Up @@ -109,7 +124,7 @@ contract OperatorTableUpdater is Initializable, OwnableUpgradeable, OperatorTabl
uint32 operatorSetIndex,
bytes calldata proof,
bytes calldata operatorTableBytes
) external {
) external onlyWhenNotPaused(PAUSED_OPERATOR_TABLE_UPDATE) {
(
OperatorSet memory operatorSet,
CurveType curveType,
Expand Down Expand Up @@ -173,7 +188,7 @@ contract OperatorTableUpdater is Initializable, OwnableUpgradeable, OperatorTabl
/// @inheritdoc IOperatorTableUpdater
function disableRoot(
bytes32 globalTableRoot
) external onlyOwner {
) external onlyPauser {
// Check that the root already exists and is not disabled
require(_isRootValid[globalTableRoot], InvalidRoot());

Expand Down
6 changes: 6 additions & 0 deletions src/contracts/multichain/OperatorTableUpdaterStorage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ import "../interfaces/IECDSACertificateVerifier.sol";
abstract contract OperatorTableUpdaterStorage is IOperatorTableUpdater {
// Constants

/// @notice Index for flag that pauses calling `updateGlobalTableRoot`
uint8 internal constant PAUSED_GLOBAL_ROOT_UPDATE = 0;

/// @notice Index for flag that pauses calling `updateOperatorTable`
uint8 internal constant PAUSED_OPERATOR_TABLE_UPDATE = 1;

bytes32 public constant GLOBAL_TABLE_ROOT_CERT_TYPEHASH =
keccak256("GlobalTableRootCert(bytes32 globalTableRoot,uint32 referenceTimestamp,uint32 referenceBlockNumber)");

Expand Down
8 changes: 6 additions & 2 deletions src/test/tree/OperatorTableUpdaterUnit.tree
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
│ └── given that the contract is not initialized
│ └── it should set the owner, generator, threshold, and update operator table & emit events
├── when confirmGlobalTableRoot is called
│ ├── given that the contract is paused for global root updates
│ │ └── it should revert with CurrentlyPaused
│ ├── given that the reference timestamp is in the future
│ │ └── it should revert with GlobalTableRootInFuture
│ ├── given that the reference timestamp is not greater than latest
Expand All @@ -17,6 +19,8 @@
│ └── given that all parameters are valid
│ └── it should update global table root, reference timestamp, block number mappings (both directions) & emit NewGlobalTableRoot event
├── when updateOperatorTable is called
│ ├── given that the contract is paused for operator table updates
│ │ └── it should revert with CurrentlyPaused
│ ├── given that the reference timestamp is not greater than operator set's latest
│ │ └── it should revert with TableUpdateForPastTimestamp
│ ├── given that the global table root is not valid
Expand Down Expand Up @@ -44,11 +48,11 @@
│ └── given that the caller is owner and threshold is valid
│ └── it should update the threshold & emit GlobalRootConfirmationThresholdUpdated event
├── when disableRoot is called
│ ├── given that the caller is not the owner
│ ├── given that the caller is not the pauser
│ │ └── it should revert
│ ├── given that the root is invalid or doesn't exist
│ │ └── it should revert with InvalidRoot
│ └── given that the caller is the owner and root is valid
│ └── given that the caller is the pauser and root is valid
│ └── it should disable the root & emit GlobalRootDisabled event
├── when updateGenerator is called
│ ├── given that the caller is not the owner
Expand Down
82 changes: 74 additions & 8 deletions src/test/unit/OperatorTableUpdaterUnit.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ contract OperatorTableUpdaterUnitTests is
operatorTableUpdaterImplementation = new OperatorTableUpdater(
IBN254CertificateVerifier(address(bn254CertificateVerifierMock)),
IECDSACertificateVerifier(address(ecdsaCertificateVerifierMock)),
pauserRegistry,
"1.0.0"
);

Expand All @@ -55,6 +56,7 @@ contract OperatorTableUpdaterUnitTests is
abi.encodeWithSelector(
OperatorTableUpdater.initialize.selector,
address(this), // owner
0, // initialPausedStatus
generator, // generator
GLOBAL_ROOT_CONFIRMATION_THRESHOLD, // globalRootConfirmationThreshold
block.timestamp - 1, // referenceTimestamp
Expand Down Expand Up @@ -194,6 +196,7 @@ contract OperatorTableUpdaterUnitTests_initialize is OperatorTableUpdaterUnitTes
cheats.expectRevert("Initializable: contract is already initialized");
operatorTableUpdater.initialize(
address(this),
uint(0),
generator,
GLOBAL_ROOT_CONFIRMATION_THRESHOLD,
uint32(block.timestamp - 1),
Expand All @@ -214,6 +217,23 @@ contract OperatorTableUpdaterUnitTests_confirmGlobalTableRoot is OperatorTableUp
operatorTableUpdater.confirmGlobalTableRoot(mockCertificate, bytes32(0), referenceTimestamp + 1, referenceBlockNumber);
}

function testFuzz_revert_paused(Randomness r) public rand(r) {
// Pause the confirmGlobalTableRoot functionality (bit index 0)
uint pausedStatus = 1 << 0; // Set bit 0 to pause PAUSED_GLOBAL_ROOT_UPDATE
cheats.prank(pauser);
operatorTableUpdater.pause(pausedStatus);

uint32 referenceTimestamp = r.Uint32(operatorTableUpdater.getLatestReferenceTimestamp() + 1, type(uint32).max);
uint32 referenceBlockNumber = r.Uint32();
bytes32 globalTableRoot = bytes32(r.Uint256(1, type(uint).max));
mockCertificate.messageHash =
operatorTableUpdater.getGlobalTableUpdateMessageHash(globalTableRoot, referenceTimestamp, referenceBlockNumber);

// Try to confirm a global table root while paused
cheats.expectRevert(IPausable.CurrentlyPaused.selector);
operatorTableUpdater.confirmGlobalTableRoot(mockCertificate, globalTableRoot, referenceTimestamp, referenceBlockNumber);
}

function testFuzz_revert_staleCertificate(Randomness r) public rand(r) {
uint32 referenceBlockNumber = uint32(block.number);
mockCertificate.messageHash =
Expand Down Expand Up @@ -273,6 +293,27 @@ contract OperatorTableUpdaterUnitTests_updateOperatorTable_BN254 is OperatorTabl
bn254CertificateVerifierMock.setLatestReferenceTimestamp(operatorSet, referenceTimestamp);
}

function testFuzz_BN254_revert_paused(Randomness r) public rand(r) {
// Pause the updateOperatorTable functionality (bit index 1)
uint pausedStatus = 1 << 1; // Set bit 1 to pause PAUSED_OPERATOR_TABLE_UPDATE
cheats.prank(pauser);
operatorTableUpdater.pause(pausedStatus);

// Generate random operatorSetInfo and operatorSetConfig
BN254OperatorSetInfo memory operatorSetInfo = _generateRandomBN254OperatorSetInfo(r);
bytes memory operatorSetInfoBytes = abi.encode(operatorSetInfo);
OperatorSetConfig memory operatorSetConfig = _generateRandomOperatorSetConfig(r);
bytes memory operatorTable = abi.encode(defaultOperatorSet, CurveType.BN254, operatorSetConfig, operatorSetInfoBytes);

// First create a valid root
bytes32 globalTableRoot = bytes32(r.Uint256(1, type(uint).max));
_updateGlobalTableRoot(globalTableRoot);

// Try to update operator table while paused
cheats.expectRevert(IPausable.CurrentlyPaused.selector);
operatorTableUpdater.updateOperatorTable(uint32(block.timestamp), globalTableRoot, 0, new bytes(0), operatorTable);
}

function testFuzz_BN254_revert_staleTableUpdate(Randomness r) public rand(r) {
uint32 referenceTimestamp = r.Uint32(uint32(block.timestamp), type(uint32).max);
_setLatestReferenceTimestampBN254(defaultOperatorSet, referenceTimestamp);
Expand Down Expand Up @@ -394,6 +435,27 @@ contract OperatorTableUpdaterUnitTests_updateOperatorTable_ECDSA is OperatorTabl
ecdsaCertificateVerifierMock.setLatestReferenceTimestamp(operatorSet, referenceTimestamp);
}

function testFuzz_ECDSA_revert_paused(Randomness r) public rand(r) {
// Pause the updateOperatorTable functionality (bit index 1)
uint pausedStatus = 1 << 1; // Set bit 1 to pause PAUSED_OPERATOR_TABLE_UPDATE
cheats.prank(pauser);
operatorTableUpdater.pause(pausedStatus);

// Generate random operatorInfos and operatorSetConfig
ECDSAOperatorInfo[] memory operatorInfos = _generateRandomECDSAOperatorInfos(r);
bytes memory operatorInfosBytes = abi.encode(operatorInfos);
OperatorSetConfig memory operatorSetConfig = _generateRandomOperatorSetConfig(r);
bytes memory operatorTable = abi.encode(defaultOperatorSet, CurveType.ECDSA, operatorSetConfig, operatorInfosBytes);

// First create a valid root
bytes32 globalTableRoot = bytes32(r.Uint256(1, type(uint).max));
_updateGlobalTableRoot(globalTableRoot);

// Try to update operator table while paused
cheats.expectRevert(IPausable.CurrentlyPaused.selector);
operatorTableUpdater.updateOperatorTable(uint32(block.timestamp), globalTableRoot, 0, new bytes(0), operatorTable);
}

function testFuzz_ECDSA_revert_rootDisabled(Randomness r) public rand(r) {
// Generate random operatorSetInfo and operatorSetConfig
ECDSAOperatorInfo[] memory emptyOperatorSetInfo;
Expand Down Expand Up @@ -661,14 +723,14 @@ contract OperatorTableUpdaterUnitTests_setGlobalRootConfirmationThreshold is Ope
}

contract OperatorTableUpdaterUnitTests_disableRoot is OperatorTableUpdaterUnitTests {
function testFuzz_revert_onlyOwner(Randomness r) public rand(r) {
function testFuzz_revert_onlyPauser(Randomness r) public rand(r) {
address invalidCaller = r.Address();
cheats.assume(invalidCaller != address(this));
cheats.assume(invalidCaller != pauser && invalidCaller != address(this));
bytes32 globalTableRoot = bytes32(r.Uint256());

// Should revert when called by non-owner
// Should revert when called by non-pauser
cheats.prank(invalidCaller);
cheats.expectRevert("Ownable: caller is not the owner");
cheats.expectRevert(IPausable.OnlyPauser.selector);
operatorTableUpdater.disableRoot(globalTableRoot);
}

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

// Disable the root
// Disable the root as pauser
cheats.prank(pauser);
cheats.expectEmit(true, true, true, true);
emit GlobalRootDisabled(globalTableRoot);
operatorTableUpdater.disableRoot(globalTableRoot);
Expand Down Expand Up @@ -750,7 +813,8 @@ contract OperatorTableUpdaterUnitTests_isRootValid is OperatorTableUpdaterUnitTe
// Should now be valid
assertTrue(operatorTableUpdater.isRootValid(globalTableRoot));

// Disable the root
// Disable the root as pauser
cheats.prank(pauser);
operatorTableUpdater.disableRoot(globalTableRoot);

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

// Disable the root
// Disable the root as pauser
cheats.prank(pauser);
operatorTableUpdater.disableRoot(globalTableRoot);

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

// Step 2: Disable the old root
// Step 2: Disable the old root as pauser
cheats.prank(pauser);
operatorTableUpdater.disableRoot(oldGlobalTableRoot);

// Step 3: Set a new generator
Expand Down