Description:
Multi-signature wallet contract requiring multiple confirmations for transaction execution.
Blockchain: Ethereum
Source Code: View Code On The Blockchain
Solidity Source Code:
{{
"language": "Solidity",
"sources": {
"src/contracts/multichain/OperatorTableUpdater.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "@openzeppelin-upgrades/contracts/proxy/utils/Initializable.sol";
import "@openzeppelin-upgrades/contracts/access/OwnableUpgradeable.sol";
import "@openzeppelin-upgrades/contracts/security/ReentrancyGuardUpgradeable.sol";
import "../libraries/Merkle.sol";
import "../permissions/Pausable.sol";
import "../mixins/SemVerMixin.sol";
import "../mixins/LeafCalculatorMixin.sol";
import "./OperatorTableUpdaterStorage.sol";
contract OperatorTableUpdater is
Initializable,
OwnableUpgradeable,
Pausable,
OperatorTableUpdaterStorage,
SemVerMixin,
LeafCalculatorMixin,
ReentrancyGuardUpgradeable
{
/**
*
* INITIALIZING FUNCTIONS
*
*/
constructor(
IBN254CertificateVerifier _bn254CertificateVerifier,
IECDSACertificateVerifier _ecdsaCertificateVerifier,
IPauserRegistry _pauserRegistry,
string memory _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 _initialGenerator 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 generatorInfo The operatorSetInfo for the Generator
* @dev We also update the operator table for the Generator, to begin signing off on global roots
* @dev We set the `_latestReferenceTimestamp` to the current timestamp, so that only *new* roots can be confirmed
*/
function initialize(
address _owner,
uint256 initialPausedStatus,
OperatorSet calldata _initialGenerator,
uint16 _globalRootConfirmationThreshold,
BN254OperatorSetInfo calldata generatorInfo
) external initializer {
_transferOwnership(_owner);
_setPausedStatus(initialPausedStatus);
// Set the `operatorSetConfig` for the `Generator`
_generatorConfig.maxStalenessPeriod = GENERATOR_MAX_STALENESS_PERIOD;
_generatorConfig.owner = address(this);
_updateGenerator(_initialGenerator, generatorInfo);
_setGlobalRootConfirmationThreshold(_globalRootConfirmationThreshold);
// The generator's global table root is the `GENERATOR_GLOBAL_TABLE_ROOT`.
// The constant is used to enable the call to `confirmGlobalTableRoot` to pass since the `BN254CertificateVerifier` expects the `Generator` to have a valid root associated with it.
_globalTableRoots[GENERATOR_REFERENCE_TIMESTAMP] = GENERATOR_GLOBAL_TABLE_ROOT;
_isRootValid[GENERATOR_GLOBAL_TABLE_ROOT] = true;
// Set the `latestReferenceTimestamp` so that only *new* roots can be confirmed
_latestReferenceTimestamp = uint32(block.timestamp);
}
/**
*
* ACTIONS
*
*/
/// @inheritdoc IOperatorTableUpdater
function confirmGlobalTableRoot(
BN254Certificate calldata globalTableRootCert,
bytes32 globalTableRoot,
uint32 referenceTimestamp,
uint32 referenceBlockNumber
) external onlyWhenNotPaused(PAUSED_GLOBAL_ROOT_UPDATE) nonReentrant {
// 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());
require(
globalTableRootCert.messageHash
== getGlobalTableUpdateMessageHash(globalTableRoot, referenceTimestamp, referenceBlockNumber),
InvalidMessageHash()
);
// Verify certificate by using the stake proportion thresholds
uint16[] memory stakeProportionThresholds = new uint16[](1);
stakeProportionThresholds[0] = globalRootConfirmationThreshold;
bool isValid = bn254CertificateVerifier.verifyCertificateProportion(
_generator, globalTableRootCert, stakeProportionThresholds
);
require(isValid, CertificateInvalid());
// Update the global table root & reference timestamps
_latestReferenceTimestamp = referenceTimestamp;
_referenceBlockNumbers[referenceTimestamp] = referenceBlockNumber;
_referenceTimestamps[referenceBlockNumber] = referenceTimestamp;
_globalTableRoots[referenceTimestamp] = globalTableRoot;
_isRootValid[globalTableRoot] = true;
emit NewGlobalTableRoot(referenceTimestamp, globalTableRoot);
}
/// @inheritdoc IOperatorTableUpdater
function updateOperatorTable(
uint32 referenceTimestamp,
bytes32 globalTableRoot,
uint32 operatorSetIndex,
bytes calldata proof,
bytes calldata operatorTableBytes
) external onlyWhenNotPaused(PAUSED_OPERATOR_TABLE_UPDATE) nonReentrant {
(
OperatorSet memory operatorSet,
CurveType curveType,
OperatorSetConfig memory operatorSetConfig,
bytes memory operatorTableInfo
) = _decodeOperatorTableBytes(operatorTableBytes);
// Check that the `globalTableRoot` is not disabled
require(_isRootValid[globalTableRoot], InvalidRoot());
// Check that the `operatorSet` is not the `Generator`
require(operatorSet.key() != _generator.key(), InvalidOperatorSet());
// Silently return if the `referenceTimestamp` has already been updated for the `operatorSet`
// We do this to avoid race conditions with the offchain transport of the operator table
if (
IBaseCertificateVerifier(getCertificateVerifier(curveType)).isReferenceTimestampSet(
operatorSet, referenceTimestamp
)
) {
return;
}
// Check that the `referenceTimestamp` is greater than the latest reference timestamp
require(
referenceTimestamp
> IBaseCertificateVerifier(getCertificateVerifier(curveType)).latestReferenceTimestamp(operatorSet),
TableUpdateForPastTimestamp()
);
// Check that the `globalTableRoot` matches the `referenceTimestamp`
require(_globalTableRoots[referenceTimestamp] == globalTableRoot, InvalidGlobalTableRoot());
// Verify the operator table update
_verifyMerkleInclusion({
globalTableRoot: globalTableRoot,
operatorSetIndex: operatorSetIndex,
proof: proof,
operatorSetLeafHash: calculateOperatorTableLeaf(operatorTableBytes)
});
// Update the operator table
if (curveType == CurveType.BN254) {
bn254CertificateVerifier.updateOperatorTable(
operatorSet, referenceTimestamp, _getBN254OperatorInfo(operatorTableInfo), operatorSetConfig
);
} else if (curveType == CurveType.ECDSA) {
ecdsaCertificateVerifier.updateOperatorTable(
operatorSet, referenceTimestamp, _getECDSAOperatorInfo(operatorTableInfo), operatorSetConfig
);
} else {
revert InvalidCurveType();
}
}
/**
*
* SETTERS
*
*/
/// @inheritdoc IOperatorTableUpdater
function setGlobalRootConfirmationThreshold(
uint16 bps
) external onlyOwner {
_setGlobalRootConfirmationThreshold(bps);
}
/// @inheritdoc IOperatorTableUpdater
function disableRoot(
bytes32 globalTableRoot
) external onlyPauser {
// Check that the root already exists and is not disabled
require(_isRootValid[globalTableRoot], InvalidRoot());
// Check that the root is not the generator's global table root
require(globalTableRoot != GENERATOR_GLOBAL_TABLE_ROOT, CannotDisableGeneratorRoot());
_isRootValid[globalTableRoot] = false;
emit GlobalRootDisabled(globalTableRoot);
}
/// @inheritdoc IOperatorTableUpdater
function updateGenerator(
OperatorSet calldata generator,
BN254OperatorSetInfo calldata generatorInfo
) external onlyOwner {
_updateGenerator(generator, generatorInfo);
}
/**
*
* GETTERS
*
*/
/// @inheritdoc IOperatorTableUpdater
function getGlobalTableRootByTimestamp(
uint32 referenceTimestamp
) external view returns (bytes32) {
return _globalTableRoots[referenceTimestamp];
}
/// @inheritdoc IOperatorTableUpdater
function getCurrentGlobalTableRoot() external view returns (bytes32) {
return _globalTableRoots[_latestReferenceTimestamp];
}
/// @inheritdoc IOperatorTableUpdater
function getGenerator() external view returns (OperatorSet memory) {
return _generator;
}
/// @inheritdoc IOperatorTableUpdater
function getCertificateVerifier(
CurveType curveType
) public view returns (address) {
if (curveType == CurveType.BN254) {
return address(bn254CertificateVerifier);
} else if (curveType == CurveType.ECDSA) {
return address(ecdsaCertificateVerifier);
} else {
revert InvalidCurveType();
}
}
/// @inheritdoc IOperatorTableUpdater
function getLatestReferenceTimestamp() external view returns (uint32) {
return _latestReferenceTimestamp;
}
/// @inheritdoc IOperatorTableUpdater
function getLatestReferenceBlockNumber() external view returns (uint32) {
return _referenceBlockNumbers[_latestReferenceTimestamp];
}
/// @inheritdoc IOperatorTableUpdater
function getReferenceBlockNumberByTimestamp(
uint32 referenceTimestamp
) external view returns (uint32) {
return _referenceBlockNumbers[referenceTimestamp];
}
/// @inheritdoc IOperatorTableUpdater
function getReferenceTimestampByBlockNumber(
uint32 referenceBlockNumber
) external view returns (uint32) {
return _referenceTimestamps[referenceBlockNumber];
}
/// @inheritdoc IOperatorTableUpdater
function getGlobalTableUpdateMessageHash(
bytes32 globalTableRoot,
uint32 referenceTimestamp,
uint32 referenceBlockNumber
) public pure returns (bytes32) {
return keccak256(
abi.encode(GLOBAL_TABLE_ROOT_CERT_TYPEHASH, globalTableRoot, referenceTimestamp, referenceBlockNumber)
);
}
/// @inheritdoc IOperatorTableUpdater
function getGeneratorReferenceTimestamp() external view returns (uint32) {
return IBaseCertificateVerifier(address(bn254CertificateVerifier)).latestReferenceTimestamp(_generator);
}
/// @inheritdoc IOperatorTableUpdater
function getGeneratorConfig() external view returns (OperatorSetConfig memory) {
return _generatorConfig;
}
/// @inheritdoc IOperatorTableUpdater
function isRootValid(
bytes32 globalTableRoot
) public view returns (bool) {
return _isRootValid[globalTableRoot];
}
/// @inheritdoc IOperatorTableUpdater
function isRootValidByTimestamp(
uint32 referenceTimestamp
) external view returns (bool) {
return _isRootValid[_globalTableRoots[referenceTimestamp]];
}
/// @inheritdoc IOperatorTableUpdater
function getGlobalTableUpdateSignableDigest(
bytes32 globalTableRoot,
uint32 referenceTimestamp,
uint32 referenceBlockNumber
) public view returns (bytes32) {
bytes32 messageHash = getGlobalTableUpdateMessageHash(globalTableRoot, referenceTimestamp, referenceBlockNumber);
return bn254CertificateVerifier.calculateCertificateDigest(GENERATOR_REFERENCE_TIMESTAMP, messageHash);
}
/**
*
* INTERNAL HELPERS
*
*/
/**
* @notice Verifies that the operator table update is valid by checking the `proof` against a `globalTableRoot`
* @param globalTableRoot The global table root of the operator table update
* @param operatorSetIndex The index of the operator set in the operator table
* @param proof The proof of the operator table update
* @param operatorSetLeafHash The leaf hash of the operator set
* @dev Reverts if there does not exist a `globalTableRoot` for the given `referenceTimestamp`
*/
function _verifyMerkleInclusion(
bytes32 globalTableRoot,
uint32 operatorSetIndex,
bytes calldata proof,
bytes32 operatorSetLeafHash
) internal pure {
// Verify inclusion of the operatorSet and operatorSetLeaf in the merkle tree
require(
Merkle.verifyInclusionKeccak({
proof: proof,
root: globalTableRoot,
leaf: operatorSetLeafHash,
index: operatorSetIndex
}),
InvalidOperatorSetProof()
);
}
/**
* @notice Sets the global root confirmation threshold
* @param bps The threshold, in bps, for a global root to be signed off on and updated
*/
function _setGlobalRootConfirmationThreshold(
uint16 bps
) internal {
require(bps <= MAX_BPS, InvalidConfirmationThreshold());
globalRootConfirmationThreshold = bps;
emit GlobalRootConfirmationThresholdUpdated(bps);
}
/**
* @notice Updates the `Generator` to a new operatorSet
* @param generator The operatorSet which certifies against global roots
* @param generatorInfo The operatorSetInfo for the generator
* @dev We have a separate function for updating this operatorSet since it's not transported and updated
* in the same way as the other operatorSets
* @dev Only callable by the owner of the contract
* @dev Uses GENERATOR_GLOBAL_TABLE_ROOT constant to break circular dependency for certificate verification
* @dev We ensure that there are no collisions with other reference timestamps because we expect the generator to have an initial reference timestamp of 0
* @dev The `_latestReferenceTimestamp` is not updated since this root is ONLY used for the `Generator`
* @dev The `_referenceBlockNumber` and `_referenceTimestamps` mappings are not updated since they are only used for introspection for official operatorSets
*/
function _updateGenerator(OperatorSet calldata generator, BN254OperatorSetInfo calldata generatorInfo) internal {
// Set the generator
_generator = generator;
// Get the latest reference timestamp for the Generator
uint32 referenceTimestamp = bn254CertificateVerifier.latestReferenceTimestamp(generator);
require(referenceTimestamp == 0, InvalidGenerator());
// Update the operator table for the Generator
bn254CertificateVerifier.updateOperatorTable(
generator, GENERATOR_REFERENCE_TIMESTAMP, generatorInfo, _generatorConfig
);
emit GeneratorUpdated(generator);
}
/**
* @notice Gets the operator table info from a bytes array
* @param operatorTable The bytes containing the operator table
* @return operatorSet The operator set
* @return curveType The curve type
* @return operatorSetInfo The operator set info
* @return operatorTableInfo The operator table info. This is encoded as a bytes array, and its value is dependent on the curve type, see `_getBN254OperatorInfo` and `_getECDSAOperatorInfo`
*/
function _decodeOperatorTableBytes(
bytes calldata operatorTable
)
internal
pure
returns (
OperatorSet memory operatorSet,
CurveType curveType,
OperatorSetConfig memory operatorSetInfo,
bytes memory operatorTableInfo
)
{
(operatorSet, curveType, operatorSetInfo, operatorTableInfo) =
abi.decode(operatorTable, (OperatorSet, CurveType, OperatorSetConfig, bytes));
}
/**
* @notice Gets the BN254 operator set info from a bytes array
* @param BN254OperatorSetInfoBytes The bytes containing the operator set info
* @return operatorSetInfo The BN254 operator set info
*/
function _getBN254OperatorInfo(
bytes memory BN254OperatorSetInfoBytes
) internal pure returns (BN254OperatorSetInfo memory) {
return abi.decode(BN254OperatorSetInfoBytes, (BN254OperatorSetInfo));
}
/**
* @notice Gets the ECDSA operator set info from a bytes array
* @param ECDSAOperatorInfoBytes The bytes containing the operator table info
* @return operatorSetInfo The ECDSA operator set info
*/
function _getECDSAOperatorInfo(
bytes memory ECDSAOperatorInfoBytes
) internal pure returns (ECDSAOperatorInfo[] memory) {
return abi.decode(ECDSAOperatorInfoBytes, (ECDSAOperatorInfo[]));
}
}
"
},
"lib/openzeppelin-contracts-upgradeable-v4.9.0/contracts/proxy/utils/Initializable.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (proxy/utils/Initializable.sol)
pragma solidity ^0.8.2;
import "../../utils/AddressUpgradeable.sol";
/**
* @dev This is a base contract to aid in writing upgradeable contracts, or any kind of contract that will be deployed
* behind a proxy. Since proxied contracts do not make use of a constructor, it's common to move constructor logic to an
* external initializer function, usually called `initialize`. It then becomes necessary to protect this initializer
* function so it can only be called once. The {initializer} modifier provided by this contract will have this effect.
*
* The initialization functions use a version number. Once a version number is used, it is consumed and cannot be
* reused. This mechanism prevents re-execution of each "step" but allows the creation of new initialization steps in
* case an upgrade adds a module that needs to be initialized.
*
* For example:
*
* [.hljs-theme-light.nopadding]
* ```solidity
* contract MyToken is ERC20Upgradeable {
* function initialize() initializer public {
* __ERC20_init("MyToken", "MTK");
* }
* }
*
* contract MyTokenV2 is MyToken, ERC20PermitUpgradeable {
* function initializeV2() reinitializer(2) public {
* __ERC20Permit_init("MyToken");
* }
* }
* ```
*
* TIP: To avoid leaving the proxy in an uninitialized state, the initializer function should be called as early as
* possible by providing the encoded function call as the `_data` argument to {ERC1967Proxy-constructor}.
*
* CAUTION: When used with inheritance, manual care must be taken to not invoke a parent initializer twice, or to ensure
* that all initializers are idempotent. This is not verified automatically as constructors are by Solidity.
*
* [CAUTION]
* ====
* Avoid leaving a contract uninitialized.
*
* An uninitialized contract can be taken over by an attacker. This applies to both a proxy and its implementation
* contract, which may impact the proxy. To prevent the implementation contract from being used, you should invoke
* the {_disableInitializers} function in the constructor to automatically lock it when it is deployed:
*
* [.hljs-theme-light.nopadding]
* ```
* /// @custom:oz-upgrades-unsafe-allow constructor
* constructor() {
* _disableInitializers();
* }
* ```
* ====
*/
abstract contract Initializable {
/**
* @dev Indicates that the contract has been initialized.
* @custom:oz-retyped-from bool
*/
uint8 private _initialized;
/**
* @dev Indicates that the contract is in the process of being initialized.
*/
bool private _initializing;
/**
* @dev Triggered when the contract has been initialized or reinitialized.
*/
event Initialized(uint8 version);
/**
* @dev A modifier that defines a protected initializer function that can be invoked at most once. In its scope,
* `onlyInitializing` functions can be used to initialize parent contracts.
*
* Similar to `reinitializer(1)`, except that functions marked with `initializer` can be nested in the context of a
* constructor.
*
* Emits an {Initialized} event.
*/
modifier initializer() {
bool isTopLevelCall = !_initializing;
require(
(isTopLevelCall && _initialized < 1) || (!AddressUpgradeable.isContract(address(this)) && _initialized == 1),
"Initializable: contract is already initialized"
);
_initialized = 1;
if (isTopLevelCall) {
_initializing = true;
}
_;
if (isTopLevelCall) {
_initializing = false;
emit Initialized(1);
}
}
/**
* @dev A modifier that defines a protected reinitializer function that can be invoked at most once, and only if the
* contract hasn't been initialized to a greater version before. In its scope, `onlyInitializing` functions can be
* used to initialize parent contracts.
*
* A reinitializer may be used after the original initialization step. This is essential to configure modules that
* are added through upgrades and that require initialization.
*
* When `version` is 1, this modifier is similar to `initializer`, except that functions marked with `reinitializer`
* cannot be nested. If one is invoked in the context of another, execution will revert.
*
* Note that versions can jump in increments greater than 1; this implies that if multiple reinitializers coexist in
* a contract, executing them in the right order is up to the developer or operator.
*
* WARNING: setting the version to 255 will prevent any future reinitialization.
*
* Emits an {Initialized} event.
*/
modifier reinitializer(uint8 version) {
require(!_initializing && _initialized < version, "Initializable: contract is already initialized");
_initialized = version;
_initializing = true;
_;
_initializing = false;
emit Initialized(version);
}
/**
* @dev Modifier to protect an initialization function so that it can only be invoked by functions with the
* {initializer} and {reinitializer} modifiers, directly or indirectly.
*/
modifier onlyInitializing() {
require(_initializing, "Initializable: contract is not initializing");
_;
}
/**
* @dev Locks the contract, preventing any future reinitialization. This cannot be part of an initializer call.
* Calling this in the constructor of a contract will prevent that contract from being initialized or reinitialized
* to any version. It is recommended to use this to lock implementation contracts that are designed to be called
* through proxies.
*
* Emits an {Initialized} event the first time it is successfully executed.
*/
function _disableInitializers() internal virtual {
require(!_initializing, "Initializable: contract is initializing");
if (_initialized != type(uint8).max) {
_initialized = type(uint8).max;
emit Initialized(type(uint8).max);
}
}
/**
* @dev Returns the highest version that has been initialized. See {reinitializer}.
*/
function _getInitializedVersion() internal view returns (uint8) {
return _initialized;
}
/**
* @dev Returns `true` if the contract is currently initializing. See {onlyInitializing}.
*/
function _isInitializing() internal view returns (bool) {
return _initializing;
}
}
"
},
"lib/openzeppelin-contracts-upgradeable-v4.9.0/contracts/access/OwnableUpgradeable.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (access/Ownable.sol)
pragma solidity ^0.8.0;
import "../utils/ContextUpgradeable.sol";
import "../proxy/utils/Initializable.sol";
/**
* @dev Contract module which provides a basic access control mechanism, where
* there is an account (an owner) that can be granted exclusive access to
* specific functions.
*
* By default, the owner account will be the one that deploys the contract. This
* can later be changed with {transferOwnership}.
*
* This module is used through inheritance. It will make available the modifier
* `onlyOwner`, which can be applied to your functions to restrict their use to
* the owner.
*/
abstract contract OwnableUpgradeable is Initializable, ContextUpgradeable {
address private _owner;
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
/**
* @dev Initializes the contract setting the deployer as the initial owner.
*/
function __Ownable_init() internal onlyInitializing {
__Ownable_init_unchained();
}
function __Ownable_init_unchained() internal onlyInitializing {
_transferOwnership(_msgSender());
}
/**
* @dev Throws if called by any account other than the owner.
*/
modifier onlyOwner() {
_checkOwner();
_;
}
/**
* @dev Returns the address of the current owner.
*/
function owner() public view virtual returns (address) {
return _owner;
}
/**
* @dev Throws if the sender is not the owner.
*/
function _checkOwner() internal view virtual {
require(owner() == _msgSender(), "Ownable: caller is not the owner");
}
/**
* @dev Leaves the contract without owner. It will not be possible to call
* `onlyOwner` functions. Can only be called by the current owner.
*
* NOTE: Renouncing ownership will leave the contract without an owner,
* thereby disabling any functionality that is only available to the owner.
*/
function renounceOwnership() public virtual onlyOwner {
_transferOwnership(address(0));
}
/**
* @dev Transfers ownership of the contract to a new account (`newOwner`).
* Can only be called by the current owner.
*/
function transferOwnership(address newOwner) public virtual onlyOwner {
require(newOwner != address(0), "Ownable: new owner is the zero address");
_transferOwnership(newOwner);
}
/**
* @dev Transfers ownership of the contract to a new account (`newOwner`).
* Internal function without access restriction.
*/
function _transferOwnership(address newOwner) internal virtual {
address oldOwner = _owner;
_owner = newOwner;
emit OwnershipTransferred(oldOwner, newOwner);
}
/**
* @dev This empty reserved space is put in place to allow future versions to add new
* variables without shifting down storage in the inheritance chain.
* See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
*/
uint256[49] private __gap;
}
"
},
"lib/openzeppelin-contracts-upgradeable-v4.9.0/contracts/security/ReentrancyGuardUpgradeable.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (security/ReentrancyGuard.sol)
pragma solidity ^0.8.0;
import "../proxy/utils/Initializable.sol";
/**
* @dev Contract module that helps prevent reentrant calls to a function.
*
* Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier
* available, which can be applied to functions to make sure there are no nested
* (reentrant) calls to them.
*
* Note that because there is a single `nonReentrant` guard, functions marked as
* `nonReentrant` may not call one another. This can be worked around by making
* those functions `private`, and then adding `external` `nonReentrant` entry
* points to them.
*
* TIP: If you would like to learn more about reentrancy and alternative ways
* to protect against it, check out our blog post
* https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul].
*/
abstract contract ReentrancyGuardUpgradeable is Initializable {
// Booleans are more expensive than uint256 or any type that takes up a full
// word because each write operation emits an extra SLOAD to first read the
// slot's contents, replace the bits taken up by the boolean, and then write
// back. This is the compiler's defense against contract upgrades and
// pointer aliasing, and it cannot be disabled.
// The values being non-zero value makes deployment a bit more expensive,
// but in exchange the refund on every call to nonReentrant will be lower in
// amount. Since refunds are capped to a percentage of the total
// transaction's gas, it is best to keep them low in cases like this one, to
// increase the likelihood of the full refund coming into effect.
uint256 private constant _NOT_ENTERED = 1;
uint256 private constant _ENTERED = 2;
uint256 private _status;
function __ReentrancyGuard_init() internal onlyInitializing {
__ReentrancyGuard_init_unchained();
}
function __ReentrancyGuard_init_unchained() internal onlyInitializing {
_status = _NOT_ENTERED;
}
/**
* @dev Prevents a contract from calling itself, directly or indirectly.
* Calling a `nonReentrant` function from another `nonReentrant`
* function is not supported. It is possible to prevent this from happening
* by making the `nonReentrant` function external, and making it call a
* `private` function that does the actual work.
*/
modifier nonReentrant() {
_nonReentrantBefore();
_;
_nonReentrantAfter();
}
function _nonReentrantBefore() private {
// On the first call to nonReentrant, _status will be _NOT_ENTERED
require(_status != _ENTERED, "ReentrancyGuard: reentrant call");
// Any calls to nonReentrant after this point will fail
_status = _ENTERED;
}
function _nonReentrantAfter() private {
// By storing the original value once again, a refund is triggered (see
// https://eips.ethereum.org/EIPS/eip-2200)
_status = _NOT_ENTERED;
}
/**
* @dev Returns true if the reentrancy guard is currently set to "entered", which indicates there is a
* `nonReentrant` function in the call stack.
*/
function _reentrancyGuardEntered() internal view returns (bool) {
return _status == _ENTERED;
}
/**
* @dev This empty reserved space is put in place to allow future versions to add new
* variables without shifting down storage in the inheritance chain.
* See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
*/
uint256[49] private __gap;
}
"
},
"src/contracts/libraries/Merkle.sol": {
"content": "// SPDX-License-Identifier: MIT
// Adapted from OpenZeppelin Contracts (last updated v4.8.0) (utils/cryptography/MerkleProof.sol)
pragma solidity ^0.8.0;
/**
* @dev These functions deal with verification of Merkle Tree proofs.
*
* WARNING: You should avoid using leaf values that are 64 bytes long prior to
* hashing, salt the leaves, or hash the leaves with a hash function other than
* what is used for the Merkle tree's internal nodes. This is because the
* concatenation of a sorted pair of internal nodes in the Merkle tree could
* be reinterpreted as a leaf value.
*/
library Merkle {
/// @notice Thrown when the provided proof was not a multiple of 32, or was empty for SHA256.
/// @dev Error code: 0x4dc5f6a4
error InvalidProofLength();
/// @notice Thrown when the provided index was outside the max index for the tree.
/// @dev Error code: 0x63df8171
error InvalidIndex();
/// @notice Thrown when the provided leaves' length was not a power of two.
/// @dev Error code: 0xf6558f51
error LeavesNotPowerOfTwo();
/// @notice Thrown when the provided leaves' length was 0.
/// @dev Error code: 0xbaec3d9a
error NoLeaves();
/// @notice Thrown when the provided leaves' length was insufficient.
/// @dev Error code: 0xf8ef0367
/// @dev This is used for the SHA256 Merkle tree, where the tree must have more than 1 leaf.
error NotEnoughLeaves();
/// @notice Thrown when the root is empty.
/// @dev Error code: 0x53ce4ece
/// @dev Empty roots should never be valid. We prevent them to avoid issues like the Nomad bridge attack: <https://medium.com/nomad-xyz-blog/nomad-bridge-hack-root-cause-analysis-875ad2e5aacd>
error EmptyRoot();
/**
* @notice Verifies that a given leaf is included in a Merkle tree
* @param proof The proof of inclusion for the leaf
* @param root The root of the Merkle tree
* @param leaf The leaf to verify
* @param index The index of the leaf in the Merkle tree
* @return True if the leaf is included in the Merkle tree, false otherwise
* @dev A `proof` is valid if and only if the rebuilt hash matches the root of the tree.
* @dev Reverts for:
* - InvalidProofLength: proof.length is not a multiple of 32.
* - InvalidIndex: index is not 0 at conclusion of computation (implying outside the max index for the tree).
*/
function verifyInclusionKeccak(
bytes memory proof,
bytes32 root,
bytes32 leaf,
uint256 index
) internal pure returns (bool) {
require(root != bytes32(0), EmptyRoot());
return processInclusionProofKeccak(proof, leaf, index) == root;
}
/**
* @notice Returns the rebuilt hash obtained by traversing a Merkle tree up
* from `leaf` using `proof`.
* @param proof The proof of inclusion for the leaf
* @param leaf The leaf to verify
* @param index The index of the leaf in the Merkle tree
* @return The rebuilt hash
* @dev Reverts for:
* - InvalidProofLength: proof.length is not a multiple of 32.
* - InvalidIndex: index is not 0 at conclusion of computation (implying outside the max index for the tree).
* @dev The tree is built assuming `leaf` is the 0 indexed `index`'th leaf from the bottom left of the tree.
*/
function processInclusionProofKeccak(
bytes memory proof,
bytes32 leaf,
uint256 index
) internal pure returns (bytes32) {
if (proof.length == 0) {
return leaf;
}
require(proof.length % 32 == 0, InvalidProofLength());
bytes32 computedHash = leaf;
for (uint256 i = 32; i <= proof.length; i += 32) {
if (index % 2 == 0) {
// if index is even, then computedHash is a left sibling
assembly {
mstore(0x00, computedHash)
mstore(0x20, mload(add(proof, i)))
computedHash := keccak256(0x00, 0x40)
index := div(index, 2)
}
} else {
// if index is odd, then computedHash is a right sibling
assembly {
mstore(0x00, mload(add(proof, i)))
mstore(0x20, computedHash)
computedHash := keccak256(0x00, 0x40)
index := div(index, 2)
}
}
}
// Confirm proof was fully consumed by end of computation
require(index == 0, InvalidIndex());
return computedHash;
}
/**
* @notice Verifies that a given leaf is included in a Merkle tree
* @param proof The proof of inclusion for the leaf
* @param root The root of the Merkle tree
* @param leaf The leaf to verify
* @param index The index of the leaf in the Merkle tree
* @return True if the leaf is included in the Merkle tree, false otherwise
* @dev A `proof` is valid if and only if the rebuilt hash matches the root of the tree.
* @dev Reverts for:
* - InvalidProofLength: proof.length is 0 or not a multiple of 32.
* - InvalidIndex: index is not 0 at conclusion of computation (implying outside the max index for the tree).
*/
function verifyInclusionSha256(
bytes memory proof,
bytes32 root,
bytes32 leaf,
uint256 index
) internal view returns (bool) {
require(root != bytes32(0), EmptyRoot());
return processInclusionProofSha256(proof, leaf, index) == root;
}
/**
* @notice Returns the rebuilt hash obtained by traversing a Merkle tree up
* from `leaf` using `proof`.
* @param proof The proof of inclusion for the leaf
* @param leaf The leaf to verify
* @param index The index of the leaf in the Merkle tree
* @return The rebuilt hash
* @dev Reverts for:
* - InvalidProofLength: proof.length is 0 or not a multiple of 32.
* - InvalidIndex: index is not 0 at conclusion of computation (implying outside the max index for the tree).
* @dev The tree is built assuming `leaf` is the 0 indexed `index`'th leaf from the bottom left of the tree.
*/
function processInclusionProofSha256(
bytes memory proof,
bytes32 leaf,
uint256 index
) internal view returns (bytes32) {
require(proof.length != 0 && proof.length % 32 == 0, InvalidProofLength());
bytes32[1] memory computedHash = [leaf];
for (uint256 i = 32; i <= proof.length; i += 32) {
if (index % 2 == 0) {
// if index is even, then computedHash is a left sibling
assembly {
mstore(0x00, mload(computedHash))
mstore(0x20, mload(add(proof, i)))
if iszero(staticcall(sub(gas(), 2000), 2, 0x00, 0x40, computedHash, 0x20)) { revert(0, 0) }
index := div(index, 2)
}
} else {
// if index is odd, then computedHash is a right sibling
assembly {
mstore(0x00, mload(add(proof, i)))
mstore(0x20, mload(computedHash))
if iszero(staticcall(sub(gas(), 2000), 2, 0x00, 0x40, computedHash, 0x20)) { revert(0, 0) }
index := div(index, 2)
}
}
}
// Confirm proof was fully consumed by end of computation
require(index == 0, InvalidIndex());
return computedHash[0];
}
/**
* @notice Returns the Merkle root of a tree created from a set of leaves using SHA-256 as its hash function
* @param leaves the leaves of the Merkle tree
* @return The computed Merkle root of the tree.
* @dev Reverts for:
* - NotEnoughLeaves: leaves.length is less than 2.
* - LeavesNotPowerOfTwo: leaves.length is not a power of two.
* @dev Unlike the Keccak version, this function does not allow a single-leaf tree.
*/
function merkleizeSha256(
bytes32[] memory leaves
) internal pure returns (bytes32) {
require(leaves.length > 1, NotEnoughLeaves());
require(isPowerOfTwo(leaves.length), LeavesNotPowerOfTwo());
// There are half as many nodes in the layer above the leaves
uint256 numNodesInLayer = leaves.length / 2;
// Create a layer to store the internal nodes
bytes32[] memory layer = new bytes32[](numNodesInLayer);
// Fill the layer with the pairwise hashes of the leaves
for (uint256 i = 0; i < numNodesInLayer; i++) {
layer[i] = sha256(abi.encodePacked(leaves[2 * i], leaves[2 * i + 1]));
}
// While we haven't computed the root
while (numNodesInLayer != 1) {
// The next layer above has half as many nodes
numNodesInLayer /= 2;
// Overwrite the first numNodesInLayer nodes in layer with the pairwise hashes of their children
for (uint256 i = 0; i < numNodesInLayer; i++) {
layer[i] = sha256(abi.encodePacked(layer[2 * i], layer[2 * i + 1]));
}
}
// The first node in the layer is the root
return layer[0];
}
/**
* @notice Returns the Merkle root of a tree created from a set of leaves using Keccak as its hash function
* @param leaves the leaves of the Merkle tree
* @return The computed Merkle root of the tree.
* @dev Reverts for:
* - NoLeaves: leaves.length is 0.
*/
function merkleizeKeccak(
bytes32[] memory leaves
) internal pure returns (bytes32) {
require(leaves.length > 0, NoLeaves());
uint256 numNodesInLayer;
if (!isPowerOfTwo(leaves.length)) {
// Pad to the next power of 2
numNodesInLayer = 1;
while (numNodesInLayer < leaves.length) {
numNodesInLayer *= 2;
}
} else {
numNodesInLayer = leaves.length;
}
// Create a layer to store the internal nodes
bytes32[] memory layer = new bytes32[](numNodesInLayer);
for (uint256 i = 0; i < leaves.length; i++) {
layer[i] = leaves[i];
}
// While we haven't computed the root
while (numNodesInLayer != 1) {
// The next layer above has half as many nodes
numNodesInLayer /= 2;
// Overwrite the first numNodesInLayer nodes in layer with the pairwise hashes of their children
for (uint256 i = 0; i < numNodesInLayer; i++) {
layer[i] = keccak256(abi.encodePacked(layer[2 * i], layer[2 * i + 1]));
}
}
// The first node in the layer is the root
return layer[0];
}
/**
* @notice Returns the Merkle proof for a given index in a tree created from a set of leaves using Keccak as its hash function
* @param leaves the leaves of the Merkle tree
* @param index the index of the leaf to get the proof for
* @return proof The computed Merkle proof for the leaf at index.
* @dev Reverts for:
* - InvalidIndex: index is outside the max index for the tree.
*/
function getProofKeccak(bytes32[] memory leaves, uint256 index) internal pure returns (bytes memory proof) {
require(leaves.length > 0, NoLeaves());
// TODO: very inefficient, use ZERO_HASHES
// pad to the next power of 2
uint256 numNodesInLayer = 1;
while (numNodesInLayer < leaves.length) {
numNodesInLayer *= 2;
}
bytes32[] memory layer = new bytes32[](numNodesInLayer);
for (uint256 i = 0; i < leaves.length; i++) {
layer[i] = leaves[i];
}
if (index >= layer.length) revert InvalidIndex();
// While we haven't computed the root
while (numNodesInLayer != 1) {
// Flip the least significant bit of index to get the sibling index
uint256 siblingIndex = index ^ 1;
// Add the sibling to the proof
proof = abi.encodePacked(proof, layer[siblingIndex]);
index /= 2;
// The next layer above has half as many nodes
numNodesInLayer /= 2;
// Overwrite the first numNodesInLayer nodes in layer with the pairwise hashes of their children
for (uint256 i = 0; i < numNodesInLayer; i++) {
layer[i] = keccak256(abi.encodePacked(layer[2 * i], layer[2 * i + 1]));
}
}
}
/**
* @notice Returns the Merkle proof for a given index in a tree created from a set of leaves using SHA-256 as its hash function
* @param leaves the leaves of the Merkle tree
* @param index the index of the leaf to get the proof for
* @return proof The computed Merkle proof for the leaf at index.
* @dev Reverts for:
* - NotEnoughLeaves: leaves.length is less than 2.
* @dev Unlike the Keccak version, this function does not allow a single-leaf proof.
*/
function getProofSha256(bytes32[] memory leaves, uint256 index) internal pure returns (bytes memory proof) {
require(leaves.length > 1, NotEnoughLeaves());
// TODO: very inefficient, use ZERO_HASHES
// pad to the next power of 2
uint256 numNodesInLayer = 1;
while (numNodesInLayer < leaves.length) {
numNodesInLayer *= 2;
}
bytes32[] memory layer = new bytes32[](numNodesInLayer);
for (uint256 i = 0; i < leaves.length; i++) {
layer[i] = leaves[i];
}
if (index >= layer.length) revert InvalidIndex();
// While we haven't computed the root
while (numNodesInLayer != 1) {
// Flip the least significant bit of index to get the sibling index
uint256 siblingIndex = index ^ 1;
// Add the sibling to the proof
proof = abi.encodePacked(proof, layer[siblingIndex]);
index /= 2;
// The next layer above has half as many nodes
numNodesInLayer /= 2;
// Overwrite the first numNodesInLayer nodes in layer with the pairwise hashes of their children
for (uint256 i = 0; i < numNodesInLayer; i++) {
layer[i] = sha256(abi.encodePacked(layer[2 * i], layer[2 * i + 1]));
}
}
}
/**
* @notice Returns whether the input is a power of two
* @param value the value to check
* @return True if the input is a power of two, false otherwise
*/
function isPowerOfTwo(
uint256 value
) internal pure returns (bool) {
return value != 0 && (value & (value - 1)) == 0;
}
}
"
},
"src/contracts/permissions/Pausable.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "../interfaces/IPausable.sol";
/**
* @title Adds pausability to a contract, with pausing & unpausing controlled by the `pauser` and `unpauser` of a PauserRegistry contract.
* @author Layr Labs, Inc.
* @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service
* @notice Contracts that inherit from this contract may define their own `pause` and `unpause` (and/or related) functions.
* These functions should be permissioned as "onlyPauser" which defers to a `PauserRegistry` for determining access control.
* @dev Pausability is implemented using a uint256, which allows up to 256 different single bit-flags; each bit can potentially pause different functionality.
* Inspiration for this was taken from the NearBridge design here https://etherscan.io/address/0x3FEFc5A4B1c02f21cBc8D3613643ba0635b9a873#code.
* For the `pause` and `unpause` functions we've implemented, if you pause, you can only flip (any number of) switches to on/1 (aka "paused"), and if you unpause,
* you can only flip (any number of) switches to off/0 (aka "paused").
* If you want a pauseXYZ function that just flips a single bit / "pausing flag", it will:
* 1) 'bit-wise and' (aka `&`) a flag with the current paused state (as a uint256)
* 2) update the paused state to this new value
* @dev We note as well that we have chosen to identify flags by their *bit index* as opposed to their numerical value, so, e.g. defining `DEPOSITS_PAUSED = 3`
* indicates specifically that if the *third bit* of `_paused` is flipped -- i.e. it is a '1' -- then deposits should be paused
*/
abstract contract Pausable is IPausable {
/// Constants
uint256 internal constant _UNPAUSE_ALL = 0;
uint256 internal constant _PAUSE_ALL = type(uint256).max;
/// @notice Address of the `PauserRegistry` contract that this contract defers to for determining access control (for pausing).
IPauserRegistry public immutable pauserRegistry;
/// Storage
/// @dev Do not remove, deprecated storage.
IPauserRegistry private __deprecated_pauserRegistry;
/// @dev Returns a bitmap representing the paused status of the contract.
uint256 private _paused;
/// Modifiers
/// @dev Thrown if the caller is not a valid pauser according to the pauser registry.
modifier onlyPauser() {
_checkOnlyPauser();
_;
}
/// @dev Thrown if the caller is not a valid unpauser according to the pauser registry.
modifier onlyUnpauser() {
_checkOnlyUnpauser();
_;
}
/// @dev Thrown if the contract is paused, i.e. if any of the bits in `_paused` is flipped to 1.
modifier whenNotPaused() {
_checkOnlyWhenNotPaused();
_;
}
/// @dev Thrown if the `indexed`th bit of `_paused` is 1, i.e. if the `index`th pause switch is flipped.
modifier onlyWhenNotPaused(
uint8 index
) {
_checkOnlyWhenNotPaused(index);
_;
}
function _checkOnlyPauser() internal view {
require(pauserRegistry.isPauser(msg.sender), OnlyPauser());
}
function _checkOnlyUnpauser() internal view {
require(msg.sender == pauserRegistry.unpauser(), OnlyUnpauser());
}
function _checkOnlyWhenNotPaused() internal view {
require(_paused == 0, CurrentlyPaused());
}
function _checkOnlyWhenNotPaused(
uint8 index
) internal view {
require(!paused(index), CurrentlyPaused());
}
/// Construction
constructor(
IPauserRegistry _pauserRegistry
) {
require(address(_pauserRegistry) != address(0), InputAddressZero());
pauserRegistry = _pauserRegistry;
}
/// @inheritdoc IPausable
function pause(
uint256 newPausedStatus
) external onlyPauser {
uint256 currentPausedStatus = _paused;
// verify that the `newPausedStatus` does not *unflip* any bits (i.e. doesn't unpause anything, all 1 bits remain)
require((currentPausedStatus & newPausedStatus) == currentPausedStatus, InvalidNewPausedStatus());
_setPausedStatus(newPausedStatus);
}
/// @inheritdoc IPausable
function pauseAll() external onlyPauser {
_setPausedStatus(_PAUSE_ALL);
}
/// @inheritdoc IPausable
function unpause(
uint256 newPausedStatus
) external onlyUnpauser {
uint256 currentPausedStatus = _paused;
// verify that the `newPausedStatus` does not *flip* any bits (i.e. doesn't pause anything, all 0 bits remain)
require(((~currentPausedStatus) & (~newPausedStatus)) == (~currentPausedStatus), InvalidNewPausedStatus());
_paused = newPausedStatus;
emit Unpaused(msg.sender, newPausedStatus);
}
/// @inheritdoc IPausable
function paused() public view virtual returns (uint256) {
return _paused;
}
/// @inheritdoc IPausable
function paused(
uint8 index
) public view virtual returns (bool) {
uint256 mask = 1 << index;
return ((_paused & mask) == mask);
}
/// @dev Internal helper for setting the paused status, and emitting the corresponding event.
function _setPausedStatus(
uint256 pausedStatus
) internal {
_paused = pausedStatus;
emit Paused(msg.sender, pausedStatus);
}
/**
* @dev This empty reserved space is put in place to allow future versions to add new
* variables without shifting down storage in the inheritance chain.
* See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
*/
uint256[48] private __gap;
}
"
},
"src/contracts/mixins/SemVerMixin.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;
import "../interfaces/ISemVerMixin.sol";
import "@openzeppelin-upgrades/contracts/utils/ShortStringsUpgradeable.sol";
/// @title SemVerMixin
/// @notice A mixin contract that provides semantic versioning functionality.
/// @dev Follows SemVer 2.0.0 specification (https://semver.org/).
abstract contract SemVerMixin is ISemVerMixin {
using ShortStringsUpgradeable for *;
/// @notice The semantic version string for this contract, stored as a ShortString for gas efficiency.
/// @dev Follows SemVer 2.0.0 specification (https://semver.org/).
ShortString internal immutable _VERSION;
/// @notice Initializes the contract with a semantic version string.
/// @param _version The SemVer-formatted version string (e.g., "1.2.3")
/// @dev Version should follow SemVer 2.0.0 format: MAJOR.MINOR.PATCH
constructor(
string memory _version
) {
_VERSION = _version.toShortString();
}
/// @inheritdoc ISemVerMixin
function version() public view virtual returns (string memory) {
return _VERSION.toString();
}
/// @notice Returns the major version of the contract.
/// @dev Supports single digit major versions (e.g., "1" for version "1.2.3")
/// @return The major version string (e.g., "1" for version "1.2.3")
function _majorVersion() internal view returns (string memory) {
bytes memory v = bytes(_VERSION.toString());
return string(abi.encodePacked(v[0]));
}
}
"
},
"src/contracts/mixins/LeafCalculatorMixin.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import {IOperatorTableCalculatorTypes} from "../interfaces/IOperatorTableCalculator.sol";
/**
* @title LeafCalculatorMixin
* @notice Reusable mixin for calculating operator info and operator table leaf hashes
* @dev Provides standardized leaf calculation functions for use across multiple contracts and repositories.
* This mixin centralizes the leaf hashing logic to ensure consistency across the EigenLayer ecosystem
* and maintains proper cryptographic security through salt-based domain separation.
*/
abstract contract LeafCalculatorMixin {
/// @dev Salt for operator info leaf hash calculation
/// @dev The salt is used to prevent against second preimage attacks: attacks where an
/// attacker can create a partial proof using an internal node rather than a leaf to
/// validate a proof. The salt ensures that leaves cannot be concatenated together to
/// form a valid proof, as well as reducing the likelihood of an internal node matching
/// the salt prefix.
/// @dev Value derived from keccak256("OPERATOR_INFO_LEAF_SALT") = 0x75...
/// This ensures collision resistance and semantic meaning.
uint8 public constant OPERATOR_INFO_LEAF_SALT = 0x75;
/// @dev Salt for operator table leaf hash calculation
/// @dev The salt is used to prevent against second preimage attacks: attacks where an
/// attacker can create a partial proof using an internal node rather than a leaf to
/// validate a proof. The salt ensures that leaves cannot be concatenated together to
/// form a valid proof, as well as reducing the likelihood of an internal node matching
/// the salt prefix.
/// @dev Value derived from keccak256("OPERATOR_TABLE_LEAF_SALT") = 0x8e...
/// This ensures collision resistance and semantic meaning.
uint8 public constant OPERATOR_TABLE_LEAF_SALT = 0x8e;
/**
* @notice Calculate the leaf hash for an operator info
* @param operatorInfo The BN254 operator info struct containing the operator's public key and stake weights
* @return The leaf hash (keccak256 of salt and encoded operator info)
* @dev The salt is used to prevent against second preimage attacks: attacks where an
* attacker can create a partial proof using an internal node rather than a leaf to
* validate a proof. The salt ensures that leaves cannot be concatenated together to
* form a valid proof, as well as reducing the likelihood of an internal node matching
* the salt prefix.
*
* This is a standard "domain separation" technique in Merkle tree implementations
* to ensure leaf nodes and internal nodes can never be confused with each other.
* See Section 2.1 of <https://www.rfc-editor.org/rfc/rfc9162#name-merkle-trees> for more.
*
* Uses abi.encodePacked for the salt and abi.encode for the struct to handle complex types
* (structs with dynamic arrays) while maintaining gas efficiency where possible.
*/
function calculateOperatorInfoLeaf(
IOperatorTableCalculatorTypes.BN254OperatorInfo memory operatorInfo
) public pure returns (bytes32) {
return keccak256(abi.encodePacked(OPERATOR_INFO_LEAF_SALT, abi.encode(operatorInfo)));
}
/**
* @notice Calculate the leaf hash for an operator table
* @param operatorTableBytes The encoded operator table as bytes containing operator set data
* @return The leaf hash (keccak256 of salt and operator table bytes)
* @dev The salt is used to prevent against second preimage attacks: attacks where an
* attacker can create a partial proof using an internal node rather than a leaf to
* validate a proof. The salt ensures that leaves cannot be concatenated together to
* form a valid proof, as well as reducing the likelihood of an internal node matching
* the salt prefix.
*
* This is a standard "domain separation" technique in Merkle tree implementations
* to ensure leaf nodes and internal nodes can never be confused with each other.
* See Section 2.1 of <https://www.rfc-editor.org/rfc/rfc9162#name-merkle-trees> for more.
*
* Uses abi.encodePacked for both salt and bytes for optimal gas efficiency since both
* are simple byte arrays without complex nested structures.
*/
function calculateOperatorTableLeaf(
bytes calldata operatorTableBytes
) public pure returns (bytes32) {
return keccak256(abi.encodePacked(OPERATOR_TABLE_LEAF_SALT, operatorTableBytes));
}
}
"
},
"src/contracts/multichain/OperatorTableUpdaterStorage.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "../interfaces/IOperatorTableUpdater.sol";
import "../interfaces/IBN254CertificateVerifier.sol";
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;
// OPERATOR_TABLE_LEAF_SALT is now inherited from LeafCalculatorMixin
bytes32 public constant GLOBAL_TABLE_ROOT_CERT_TYPEHASH =
keccak256("GlobalTableRootCert(bytes32 globalTableRoot,uint32 referenceTimestamp,uint32 referenceBlockNumber)");
/// @notice The maximum BPS value
uint16 public constant MAX_BPS = 10_000;
/// @notice Dummy initial global table root to break circular dependency for certificate verification
bytes32 public constant GENERATOR_GLOBAL_TABLE_ROOT = keccak256("GENERATOR_GLOBAL_TABLE_ROOT");
/// @notice The reference timestamp for the generator
uint32 public constant GENERATOR_REFERENCE_TIMESTAMP = 1;
/// @notice The `maxStalenessPeriods` for the generator
/// @dev This is set to 0 to allow certificates to always be valid, regardless of the `referenceTimestamp`
uint32 public constant GENERATOR_MAX_STALENESS_PERIOD = 0;
// Immutable Storage
/// @notice The BN254 certificate verifier
IBN254CertificateVerifier public immutable bn254CertificateVerifier;
/// @notice The ECDSA certificate verifier
IECDSACertificateVerifier public immutable ecdsaCertificateVerifier;
// Mutable Storage
/// @notice The threshold, in bps, for a global root to be signed off on and updated
uint16 public globalRootConfirmationThreshold;
/// @notice The latest reference timestamp
uint32 internal _latestReferenceTimestamp;
/// @notice The operatorSet which certifies against global roots
OperatorSet internal _generator;
/// @notice The global table roots by timestamp
mapping(uint32 timestamp => bytes32 globalTableRoot) internal _globalTableRoots;
/// @notice Mapping from latest reference timestamp to reference block number
mapping(uint32 referenceTimestamp => uint32 referenceBlockNumber) internal _referenceBlockNumbers;
/// @notice Mapping from reference block number to reference timestamp
mapping(uint32 referenceBlockNumber => uint32 referenceTimestamp) internal _referenceTimestamps;
/// @notice Mapping from global table root to validity status
mapping(bytes32 globalTableRoot => bool valid) internal _isRootValid;
/// @notice The operatorSetConfig for the generator
/// @dev The `maxStalenessPeriod` is set to `GENERATOR_MAX_STALENESS_PERIOD` to allow certificates to always be valid, regardless of the `referenceTimestamp`
/// @dev The `owner` is set to the address of the `OperatorTableUpdater`
OperatorSetConfig internal _generatorConfig;
// Constructor
constructor(
IBN254CertificateVerifier _bn254CertificateVerifier,
IECDSACertificateVerifier _ecdsaCertificateVerifier
) {
bn254CertificateVerifier = _bn254CertificateVerifier;
ecdsaCertificateVerifier = _ecdsaCertificateVerifier;
}
/**
* @dev This empty reserved space is put in place to allow future versions to add new
* variables without shifting down storage in the inheritance chain.
* See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
*/
uint256[43] private __gap;
}
"
},
"lib/openzeppelin-contracts-upgradeable-v4.9.0/contracts/utils/AddressUpgradeable.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (utils/Address.sol)
pragma solidity ^0.8.1;
/**
* @dev Collection of functions related to the address type
*/
library AddressUpgradeable {
/**
* @dev Returns true if `account` is a contract.
*
* [IMPORTANT]
* ====
* It is unsafe to assume that an address for which this function returns
* false is an externally-owned account (EOA) and not a contract.
*
* Among others, `isContract` will return false for the following
* types of addresses:
*
* - an externally-owned account
* - a contract in construction
* - an address where a contract will be created
* - an address where a contract lived, but was destroyed
*
* Furthermore, `isContract` will also return true if the target contract within
* the same transaction is already scheduled for destruction by `SELFDESTRUCT`,
* which only has an effect at the end of a transaction.
* ====
*
* [IMPORTANT]
* ====
* You shouldn't rely on `isContract` to protect against flash loan attacks!
*
* Preventing calls from contracts is highly discouraged. It breaks composability, breaks support for smart wallets
* like Gnosis Safe, and does not provide security since it can be circumvented by calling from a contract
* constructor.
* ====
*/
function isContract(address account) internal view returns (bool) {
// This method relies on extcodesize/address.code.length, which returns 0
// for contracts in construction, since the code is only stored at the end
// of the constructor execution.
return account.code.length > 0;
}
/**
* @dev Replacement for Solidity's `transfer`: sends `amount` wei to
* `recipient`, forwarding all available gas and reverting on errors.
*
* https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost
*
Submitted on: 2025-09-25 10:05:16
Comments
Log in to comment.
No comments yet.