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/ECDSACertificateVerifier.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "@openzeppelin-upgrades/contracts/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import {OperatorSet} from "../libraries/OperatorSetLib.sol";
import "../mixins/SignatureUtilsMixin.sol";
import "./ECDSACertificateVerifierStorage.sol";
/**
* @title ECDSACertificateVerifier
* @notice Verifies ECDSA certificates across multiple operator sets
* @dev Implements ECDSA signature verification with operator information caching
*/
contract ECDSACertificateVerifier is Initializable, ECDSACertificateVerifierStorage, SignatureUtilsMixin {
using ECDSA for bytes32;
/**
* @notice Restricts access to the operator table updater
*/
modifier onlyTableUpdater() {
require(msg.sender == address(operatorTableUpdater), OnlyTableUpdater());
_;
}
/**
* @notice Constructor for the certificate verifier
* @dev Disables initializers to prevent implementation initialization
* @param _operatorTableUpdater Address authorized to update operator tables
* @param _version The version string for the SignatureUtilsMixin
*/
constructor(
IOperatorTableUpdater _operatorTableUpdater,
string memory _version
) ECDSACertificateVerifierStorage(_operatorTableUpdater) SignatureUtilsMixin(_version) {
_disableInitializers();
}
/**
*
* EXTERNAL FUNCTIONS
*
*/
///@inheritdoc IECDSACertificateVerifier
function updateOperatorTable(
OperatorSet calldata operatorSet,
uint32 referenceTimestamp,
ECDSAOperatorInfo[] calldata operatorInfos,
OperatorSetConfig calldata operatorSetConfig
) external onlyTableUpdater {
bytes32 operatorSetKey = operatorSet.key();
// Validate that the new timestamp is greater than the latest reference timestamp
require(referenceTimestamp > _latestReferenceTimestamps[operatorSetKey], TableUpdateStale());
// Store the number of operators
_numOperators[operatorSetKey][referenceTimestamp] = operatorInfos.length;
// Store each operator info in the indexed mapping
for (uint256 i = 0; i < operatorInfos.length; i++) {
_operatorInfos[operatorSetKey][referenceTimestamp][i] = operatorInfos[i];
}
_latestReferenceTimestamps[operatorSetKey] = referenceTimestamp;
_operatorSetOwners[operatorSetKey] = operatorSetConfig.owner;
_maxStalenessPeriods[operatorSetKey] = operatorSetConfig.maxStalenessPeriod;
_referenceTimestampsSet[operatorSetKey][referenceTimestamp] = true;
emit TableUpdated(operatorSet, referenceTimestamp, operatorInfos);
}
/**
*
* INTERNAL FUNCTIONS
*
*/
/**
* @notice Internal function to verify a certificate
* @param cert The certificate to verify
* @return totalSignedStakeWeights The total amount of stake that signed the certificate for each stake type
* @return signers The addresses that signed the certificate
*/
function _verifyECDSACertificate(
OperatorSet calldata operatorSet,
ECDSACertificate calldata cert
) internal view returns (uint256[] memory, address[] memory) {
bytes32 operatorSetKey = operatorSet.key();
// Assert that reference timestamp is not stale
require(
_maxStalenessPeriods[operatorSetKey] == 0
|| block.timestamp <= cert.referenceTimestamp + _maxStalenessPeriods[operatorSetKey],
CertificateStale()
);
// Assert that the reference timestamp exists
require(_referenceTimestampsSet[operatorSetKey][cert.referenceTimestamp], ReferenceTimestampDoesNotExist());
// Assert that the root that corresponds to the reference timestamp is not disabled
require(operatorTableUpdater.isRootValidByTimestamp(cert.referenceTimestamp), RootDisabled());
// Compute the EIP-712 digest for signature recovery
bytes32 signableDigest = calculateCertificateDigest(cert.referenceTimestamp, cert.messageHash);
// Parse the signatures
address[] memory signers = _parseSignatures(signableDigest, cert.sig);
// Verify that signers are operators and add their weights to the signed stakes
uint256 numStakeTypes = getTotalStakeWeights(operatorSet, cert.referenceTimestamp).length;
uint256[] memory totalSignedStakeWeights =
_processSigners(operatorSetKey, cert.referenceTimestamp, signers, numStakeTypes);
return (totalSignedStakeWeights, signers);
}
/**
* @notice Parse signatures from the concatenated signature bytes
* @param signableDigest The signable digest that was signed
* @param signatures The concatenated signatures
* @return signers Array of addresses that signed the message
* @dev Signatures must be ordered by signer address (ascending)
* @dev This does not support smart contract based signatures for multichain
*/
function _parseSignatures(
bytes32 signableDigest,
bytes memory signatures
) internal pure returns (address[] memory signers) {
// Each ECDSA signature is 65 bytes: r (32 bytes) + s (32 bytes) + v (1 byte)
require(signatures.length > 0 && signatures.length % 65 == 0, InvalidSignatureLength());
uint256 signatureCount = signatures.length / 65;
signers = new address[](signatureCount);
for (uint256 i = 0; i < signatureCount; i++) {
bytes memory signature = new bytes(65);
for (uint256 j = 0; j < 65; j++) {
signature[j] = signatures[i * 65 + j];
}
// Recover the signer
(address recovered, ECDSA.RecoverError err) = ECDSA.tryRecover(signableDigest, signature);
require(err == ECDSA.RecoverError.NoError, InvalidSignature());
// Check that signatures are ordered by signer address
require(i == 0 || recovered > signers[i - 1], SignersNotOrdered());
signers[i] = recovered;
}
return signers;
}
/**
* @notice Process the signers and add their weights to the signed stakes
* @param operatorSetKey The key of the operator set
* @param referenceTimestamp The reference timestamp of the certificate
* @param signers The signers of the certificate
* @param numStakeTypes The number of stake types
* @return totalSignedStakeWeights The total stake weight that has been signed for each stake type
*/
function _processSigners(
bytes32 operatorSetKey,
uint32 referenceTimestamp,
address[] memory signers,
uint256 numStakeTypes
) internal view returns (uint256[] memory totalSignedStakeWeights) {
uint256 operatorCount = _numOperators[operatorSetKey][referenceTimestamp];
totalSignedStakeWeights = new uint256[](numStakeTypes);
// Process each recovered signer
for (uint256 i = 0; i < signers.length; i++) {
address signer = signers[i];
// Check if this signer is an operator
bool isOperator = false;
ECDSAOperatorInfo memory operatorInfo;
for (uint256 j = 0; j < operatorCount; j++) {
operatorInfo = _operatorInfos[operatorSetKey][referenceTimestamp][j];
if (operatorInfo.pubkey == signer) {
isOperator = true;
break;
}
}
// If not an operator, the certificate is invalid
if (!isOperator) {
revert VerificationFailed();
}
// Add this operator's weights to the signed stakes
uint256[] memory weights = operatorInfo.weights;
for (uint256 j = 0; j < weights.length && j < numStakeTypes; j++) {
totalSignedStakeWeights[j] += weights[j];
}
}
}
/**
*
* VIEW FUNCTIONS
*
*/
///@inheritdoc IBaseCertificateVerifier
function getOperatorSetOwner(
OperatorSet memory operatorSet
) external view returns (address) {
bytes32 operatorSetKey = operatorSet.key();
return _operatorSetOwners[operatorSetKey];
}
///@inheritdoc IBaseCertificateVerifier
function maxOperatorTableStaleness(
OperatorSet memory operatorSet
) external view returns (uint32) {
bytes32 operatorSetKey = operatorSet.key();
return _maxStalenessPeriods[operatorSetKey];
}
///@inheritdoc IBaseCertificateVerifier
function latestReferenceTimestamp(
OperatorSet memory operatorSet
) external view returns (uint32) {
bytes32 operatorSetKey = operatorSet.key();
return _latestReferenceTimestamps[operatorSetKey];
}
///@inheritdoc IBaseCertificateVerifier
function isReferenceTimestampSet(
OperatorSet memory operatorSet,
uint32 referenceTimestamp
) external view returns (bool) {
bytes32 operatorSetKey = operatorSet.key();
return _referenceTimestampsSet[operatorSetKey][referenceTimestamp];
}
/// @inheritdoc IBaseCertificateVerifier
/// @dev This function requires the reference timestamp to be set
function getTotalStakeWeights(
OperatorSet calldata operatorSet,
uint32 referenceTimestamp
) public view returns (uint256[] memory) {
bytes32 operatorSetKey = operatorSet.key();
uint256 operatorCount = _numOperators[operatorSetKey][referenceTimestamp];
require(operatorCount > 0, OperatorCountZero());
// All weights are expected to be same length, so 0 index is used
uint256 stakeTypesCount = _operatorInfos[operatorSetKey][referenceTimestamp][0].weights.length;
uint256[] memory totalStakes = new uint256[](stakeTypesCount);
for (uint256 i = 0; i < operatorCount; i++) {
uint256[] memory weights = _operatorInfos[operatorSetKey][referenceTimestamp][i].weights;
for (uint256 j = 0; j < weights.length && j < stakeTypesCount; j++) {
totalStakes[j] += weights[j];
}
}
return totalStakes;
}
/// @inheritdoc IBaseCertificateVerifier
function getOperatorCount(
OperatorSet memory operatorSet,
uint32 referenceTimestamp
) external view returns (uint256) {
bytes32 operatorSetKey = operatorSet.key();
return _numOperators[operatorSetKey][referenceTimestamp];
}
///@inheritdoc IECDSACertificateVerifier
function verifyCertificate(
OperatorSet calldata operatorSet,
ECDSACertificate calldata cert
) external view returns (uint256[] memory, address[] memory) {
(uint256[] memory totalSignedStakeWeights, address[] memory signers) =
_verifyECDSACertificate(operatorSet, cert);
return (totalSignedStakeWeights, signers);
}
///@inheritdoc IECDSACertificateVerifier
function verifyCertificateProportion(
OperatorSet calldata operatorSet,
ECDSACertificate calldata cert,
uint16[] calldata totalStakeProportionThresholds
) external view returns (bool, address[] memory) {
(uint256[] memory signedStakes, address[] memory signers) = _verifyECDSACertificate(operatorSet, cert);
uint256[] memory totalStakes = getTotalStakeWeights(operatorSet, cert.referenceTimestamp);
require(signedStakes.length == totalStakeProportionThresholds.length, ArrayLengthMismatch());
for (uint256 i = 0; i < signedStakes.length; i++) {
uint256 threshold = (totalStakes[i] * totalStakeProportionThresholds[i]) / BPS_DENOMINATOR;
if (signedStakes[i] < threshold) {
return (false, signers);
}
}
return (true, signers);
}
///@inheritdoc IECDSACertificateVerifier
function verifyCertificateNominal(
OperatorSet calldata operatorSet,
ECDSACertificate calldata cert,
uint256[] memory totalStakeNominalThresholds
) external view returns (bool, address[] memory) {
(uint256[] memory signedStakes, address[] memory signers) = _verifyECDSACertificate(operatorSet, cert);
require(signedStakes.length == totalStakeNominalThresholds.length, ArrayLengthMismatch());
for (uint256 i = 0; i < signedStakes.length; i++) {
if (signedStakes[i] < totalStakeNominalThresholds[i]) {
return (false, signers);
}
}
return (true, signers);
}
/// @inheritdoc IECDSACertificateVerifier
function getOperatorInfos(
OperatorSet memory operatorSet,
uint32 referenceTimestamp
) external view returns (ECDSAOperatorInfo[] memory) {
bytes32 operatorSetKey = operatorSet.key();
uint32 numOperators = uint32(_numOperators[operatorSetKey][referenceTimestamp]);
ECDSAOperatorInfo[] memory operatorInfos = new ECDSAOperatorInfo[](numOperators);
for (uint256 i = 0; i < numOperators; i++) {
operatorInfos[i] = _operatorInfos[operatorSetKey][referenceTimestamp][i];
}
return operatorInfos;
}
/// @inheritdoc IECDSACertificateVerifier
function getOperatorInfo(
OperatorSet memory operatorSet,
uint32 referenceTimestamp,
uint256 operatorIndex
) external view returns (ECDSAOperatorInfo memory) {
bytes32 operatorSetKey = operatorSet.key();
require(operatorIndex < _numOperators[operatorSetKey][referenceTimestamp], IndexOutOfBounds());
return _operatorInfos[operatorSetKey][referenceTimestamp][operatorIndex];
}
/// @inheritdoc IECDSACertificateVerifier
function domainSeparator() public view override(IECDSACertificateVerifier, SignatureUtilsMixin) returns (bytes32) {
return keccak256(
abi.encode(
EIP712_DOMAIN_TYPEHASH_NO_CHAINID,
keccak256(bytes("EigenLayer")),
keccak256(bytes(_majorVersion())),
address(this)
)
);
}
/// @inheritdoc IECDSACertificateVerifier
function calculateCertificateDigestBytes(
uint32 referenceTimestamp,
bytes32 messageHash
) public view returns (bytes memory) {
bytes32 structHash = keccak256(abi.encode(ECDSA_CERTIFICATE_TYPEHASH, referenceTimestamp, messageHash));
return abi.encodePacked("\x19\x01", domainSeparator(), structHash);
}
/// @inheritdoc IECDSACertificateVerifier
function calculateCertificateDigest(uint32 referenceTimestamp, bytes32 messageHash) public view returns (bytes32) {
return keccak256(calculateCertificateDigestBytes(referenceTimestamp, messageHash));
}
}
"
},
"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-v4.9.0/contracts/utils/cryptography/ECDSA.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (utils/cryptography/ECDSA.sol)
pragma solidity ^0.8.0;
import "../Strings.sol";
/**
* @dev Elliptic Curve Digital Signature Algorithm (ECDSA) operations.
*
* These functions can be used to verify that a message was signed by the holder
* of the private keys of a given address.
*/
library ECDSA {
enum RecoverError {
NoError,
InvalidSignature,
InvalidSignatureLength,
InvalidSignatureS,
InvalidSignatureV // Deprecated in v4.8
}
function _throwError(RecoverError error) private pure {
if (error == RecoverError.NoError) {
return; // no error: do nothing
} else if (error == RecoverError.InvalidSignature) {
revert("ECDSA: invalid signature");
} else if (error == RecoverError.InvalidSignatureLength) {
revert("ECDSA: invalid signature length");
} else if (error == RecoverError.InvalidSignatureS) {
revert("ECDSA: invalid signature 's' value");
}
}
/**
* @dev Returns the address that signed a hashed message (`hash`) with
* `signature` or error string. This address can then be used for verification purposes.
*
* The `ecrecover` EVM opcode allows for malleable (non-unique) signatures:
* this function rejects them by requiring the `s` value to be in the lower
* half order, and the `v` value to be either 27 or 28.
*
* IMPORTANT: `hash` _must_ be the result of a hash operation for the
* verification to be secure: it is possible to craft signatures that
* recover to arbitrary addresses for non-hashed data. A safe way to ensure
* this is by receiving a hash of the original message (which may otherwise
* be too long), and then calling {toEthSignedMessageHash} on it.
*
* Documentation for signature generation:
* - with https://web3js.readthedocs.io/en/v1.3.4/web3-eth-accounts.html#sign[Web3.js]
* - with https://docs.ethers.io/v5/api/signer/#Signer-signMessage[ethers]
*
* _Available since v4.3._
*/
function tryRecover(bytes32 hash, bytes memory signature) internal pure returns (address, RecoverError) {
if (signature.length == 65) {
bytes32 r;
bytes32 s;
uint8 v;
// ecrecover takes the signature parameters, and the only way to get them
// currently is to use assembly.
/// @solidity memory-safe-assembly
assembly {
r := mload(add(signature, 0x20))
s := mload(add(signature, 0x40))
v := byte(0, mload(add(signature, 0x60)))
}
return tryRecover(hash, v, r, s);
} else {
return (address(0), RecoverError.InvalidSignatureLength);
}
}
/**
* @dev Returns the address that signed a hashed message (`hash`) with
* `signature`. This address can then be used for verification purposes.
*
* The `ecrecover` EVM opcode allows for malleable (non-unique) signatures:
* this function rejects them by requiring the `s` value to be in the lower
* half order, and the `v` value to be either 27 or 28.
*
* IMPORTANT: `hash` _must_ be the result of a hash operation for the
* verification to be secure: it is possible to craft signatures that
* recover to arbitrary addresses for non-hashed data. A safe way to ensure
* this is by receiving a hash of the original message (which may otherwise
* be too long), and then calling {toEthSignedMessageHash} on it.
*/
function recover(bytes32 hash, bytes memory signature) internal pure returns (address) {
(address recovered, RecoverError error) = tryRecover(hash, signature);
_throwError(error);
return recovered;
}
/**
* @dev Overload of {ECDSA-tryRecover} that receives the `r` and `vs` short-signature fields separately.
*
* See https://eips.ethereum.org/EIPS/eip-2098[EIP-2098 short signatures]
*
* _Available since v4.3._
*/
function tryRecover(bytes32 hash, bytes32 r, bytes32 vs) internal pure returns (address, RecoverError) {
bytes32 s = vs & bytes32(0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff);
uint8 v = uint8((uint256(vs) >> 255) + 27);
return tryRecover(hash, v, r, s);
}
/**
* @dev Overload of {ECDSA-recover} that receives the `r and `vs` short-signature fields separately.
*
* _Available since v4.2._
*/
function recover(bytes32 hash, bytes32 r, bytes32 vs) internal pure returns (address) {
(address recovered, RecoverError error) = tryRecover(hash, r, vs);
_throwError(error);
return recovered;
}
/**
* @dev Overload of {ECDSA-tryRecover} that receives the `v`,
* `r` and `s` signature fields separately.
*
* _Available since v4.3._
*/
function tryRecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) internal pure returns (address, RecoverError) {
// EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature
// unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines
// the valid range for s in (301): 0 < s < secp256k1n ÷ 2 + 1, and for v in (302): v ∈ {27, 28}. Most
// signatures from current libraries generate a unique signature with an s-value in the lower half order.
//
// If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value
// with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or
// vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept
// these malleable signatures as well.
if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) {
return (address(0), RecoverError.InvalidSignatureS);
}
// If the signature is valid (and not malleable), return the signer address
address signer = ecrecover(hash, v, r, s);
if (signer == address(0)) {
return (address(0), RecoverError.InvalidSignature);
}
return (signer, RecoverError.NoError);
}
/**
* @dev Overload of {ECDSA-recover} that receives the `v`,
* `r` and `s` signature fields separately.
*/
function recover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) internal pure returns (address) {
(address recovered, RecoverError error) = tryRecover(hash, v, r, s);
_throwError(error);
return recovered;
}
/**
* @dev Returns an Ethereum Signed Message, created from a `hash`. This
* produces hash corresponding to the one signed with the
* https://eth.wiki/json-rpc/API#eth_sign[`eth_sign`]
* JSON-RPC method as part of EIP-191.
*
* See {recover}.
*/
function toEthSignedMessageHash(bytes32 hash) internal pure returns (bytes32 message) {
// 32 is the length in bytes of hash,
// enforced by the type signature above
/// @solidity memory-safe-assembly
assembly {
mstore(0x00, "\x19Ethereum Signed Message:\
32")
mstore(0x1c, hash)
message := keccak256(0x00, 0x3c)
}
}
/**
* @dev Returns an Ethereum Signed Message, created from `s`. This
* produces hash corresponding to the one signed with the
* https://eth.wiki/json-rpc/API#eth_sign[`eth_sign`]
* JSON-RPC method as part of EIP-191.
*
* See {recover}.
*/
function toEthSignedMessageHash(bytes memory s) internal pure returns (bytes32) {
return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\
", Strings.toString(s.length), s));
}
/**
* @dev Returns an Ethereum Signed Typed Data, created from a
* `domainSeparator` and a `structHash`. This produces hash corresponding
* to the one signed with the
* https://eips.ethereum.org/EIPS/eip-712[`eth_signTypedData`]
* JSON-RPC method as part of EIP-712.
*
* See {recover}.
*/
function toTypedDataHash(bytes32 domainSeparator, bytes32 structHash) internal pure returns (bytes32 data) {
/// @solidity memory-safe-assembly
assembly {
let ptr := mload(0x40)
mstore(ptr, "\x19\x01")
mstore(add(ptr, 0x02), domainSeparator)
mstore(add(ptr, 0x22), structHash)
data := keccak256(ptr, 0x42)
}
}
/**
* @dev Returns an Ethereum Signed Data with intended validator, created from a
* `validator` and `data` according to the version 0 of EIP-191.
*
* See {recover}.
*/
function toDataWithIntendedValidatorHash(address validator, bytes memory data) internal pure returns (bytes32) {
return keccak256(abi.encodePacked("\x19\x00", validator, data));
}
}
"
},
"src/contracts/libraries/OperatorSetLib.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
using OperatorSetLib for OperatorSet global;
/**
* @notice An operator set identified by the AVS address and an identifier
* @param avs The address of the AVS this operator set belongs to
* @param id The unique identifier for the operator set
*/
struct OperatorSet {
address avs;
uint32 id;
}
library OperatorSetLib {
function key(
OperatorSet memory os
) internal pure returns (bytes32) {
return bytes32(abi.encodePacked(os.avs, uint96(os.id)));
}
function decode(
bytes32 _key
) internal pure returns (OperatorSet memory) {
/// forgefmt: disable-next-item
return OperatorSet({
avs: address(uint160(uint256(_key) >> 96)),
id: uint32(uint256(_key) & type(uint96).max)
});
}
}
"
},
"src/contracts/mixins/SignatureUtilsMixin.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;
import "@openzeppelin-upgrades/contracts/utils/ShortStringsUpgradeable.sol";
import "@openzeppelin-upgrades/contracts/utils/cryptography/SignatureCheckerUpgradeable.sol";
import "../interfaces/ISignatureUtilsMixin.sol";
import "./SemVerMixin.sol";
/// @dev The EIP-712 domain type hash used for computing the domain separator
/// See https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator
bytes32 constant EIP712_DOMAIN_TYPEHASH =
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");
/// @title SignatureUtilsMixin
/// @notice A mixin contract that provides utilities for validating signatures according to EIP-712 and EIP-1271 standards.
/// @dev The domain name is hardcoded to "EigenLayer". This contract implements signature validation functionality that can be
/// inherited by other contracts. The domain separator uses the major version (e.g., "v1") to maintain EIP-712
/// signature compatibility across minor and patch version updates.
abstract contract SignatureUtilsMixin is ISignatureUtilsMixin, SemVerMixin {
using SignatureCheckerUpgradeable for address;
/// @notice Initializes the contract with a semantic version string.
/// @param _version The SemVer-formatted version string (e.g., "1.1.1") to use for this contract's domain separator.
/// @dev Version should follow SemVer 2.0.0 format with 'v' prefix: vMAJOR.MINOR.PATCH.
/// Only the major version component is used in the domain separator to maintain signature compatibility
/// across minor and patch version updates.
constructor(
string memory _version
) SemVerMixin(_version) {}
/// EXTERNAL FUNCTIONS ///
/// @inheritdoc ISignatureUtilsMixin
function domainSeparator() public view virtual returns (bytes32) {
// forgefmt: disable-next-item
return
keccak256(
abi.encode(
EIP712_DOMAIN_TYPEHASH,
keccak256(bytes("EigenLayer")),
keccak256(bytes(_majorVersion())),
block.chainid,
address(this)
)
);
}
/// INTERNAL HELPERS ///
/// @notice Creates a digest that can be signed using EIP-712.
/// @dev Prepends the EIP-712 prefix ("\x19\x01") and domain separator to the input hash.
/// This follows the EIP-712 specification for creating structured data hashes.
/// See https://eips.ethereum.org/EIPS/eip-712#specification.
/// @param hash The hash of the typed data to be signed.
/// @return The complete digest that should be signed according to EIP-712.
function _calculateSignableDigest(
bytes32 hash
) internal view returns (bytes32) {
return keccak256(abi.encodePacked("\x19\x01", domainSeparator(), hash));
}
/// @notice Validates a signature against a signer and digest, with an expiry timestamp.
/// @dev Reverts if the signature is invalid or expired. Uses EIP-1271 for smart contract signers.
/// For EOA signers, validates ECDSA signatures directly.
/// For contract signers, calls isValidSignature according to EIP-1271.
/// See https://eips.ethereum.org/EIPS/eip-1271#specification.
/// @param signer The address that should have signed the digest.
/// @param signableDigest The digest that was signed, created via _calculateSignableDigest.
/// @param signature The signature bytes to validate.
/// @param expiry The timestamp after which the signature is no longer valid.
function _checkIsValidSignatureNow(
address signer,
bytes32 signableDigest,
bytes memory signature,
uint256 expiry
) internal view {
// First, check if the signature has expired by comparing the expiry timestamp
// against the current block timestamp.
require(expiry >= block.timestamp, SignatureExpired());
// Next, verify that the signature is valid for the given signer and digest.
// For EOA signers, this performs standard ECDSA signature verification.
// For contract signers, this calls the EIP-1271 isValidSignature method.
require(signer.isValidSignatureNow(signableDigest, signature), InvalidSignature());
}
}
"
},
"src/contracts/multichain/ECDSACertificateVerifierStorage.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "../interfaces/IOperatorTableUpdater.sol";
import "../interfaces/IECDSACertificateVerifier.sol";
import "../interfaces/IBaseCertificateVerifier.sol";
abstract contract ECDSACertificateVerifierStorage is IECDSACertificateVerifier {
// Constants
/// @dev Basis point unit denominator for division
uint256 internal constant BPS_DENOMINATOR = 10_000;
/// @dev EIP-712 type hash for certificate verification
bytes32 internal constant ECDSA_CERTIFICATE_TYPEHASH =
keccak256("ECDSACertificate(uint32 referenceTimestamp,bytes32 messageHash)");
/// @dev The EIP-712 domain type hash used for computing the domain separator without chainId
bytes32 internal constant EIP712_DOMAIN_TYPEHASH_NO_CHAINID =
keccak256("EIP712Domain(string name,string version,address verifyingContract)");
// Immutables
/// @dev The address that can update operator tables
IOperatorTableUpdater public immutable operatorTableUpdater;
// Mutatables
/// @dev Mapping from operatorSet key to owner address
mapping(bytes32 => address) internal _operatorSetOwners;
/// @dev Mapping from operatorSet key to maximum staleness period
mapping(bytes32 => uint32) internal _maxStalenessPeriods;
/// @dev Mapping from operatorSet key to latest reference timestamp
mapping(bytes32 => uint32) internal _latestReferenceTimestamps;
/// @dev Mapping from referenceTimestamp to the number of operators
mapping(bytes32 operatorSetKey => mapping(uint32 referenceTimestamp => uint256 numOperators)) internal _numOperators;
/// @dev Mapping from operatorSetKey to referenceTimestamp to operatorInfos
mapping(bytes32 operatorSetKey => mapping(uint32 referenceTimestamp => mapping(uint256 => ECDSAOperatorInfo)))
internal _operatorInfos;
/// @dev Mapping from operatorSetKey to referenceTimestamp to whether it has been updated
mapping(bytes32 operatorSetKey => mapping(uint32 referenceTimestamp => bool set)) internal _referenceTimestampsSet;
// Construction
constructor(
IOperatorTableUpdater _operatorTableUpdater
) {
operatorTableUpdater = _operatorTableUpdater;
}
/**
* @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[44] 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
* of certain opcodes, possibly making contracts go over the 2300 gas limit
* imposed by `transfer`, making them unable to receive funds via
* `transfer`. {sendValue} removes this limitation.
*
* https://consensys.net/diligence/blog/2019/09/stop-using-soliditys-transfer-now/[Learn more].
*
* IMPORTANT: because control is transferred to `recipient`, care must be
* taken to not create reentrancy vulnerabilities. Consider using
* {ReentrancyGuard} or the
* https://solidity.readthedocs.io/en/v0.8.0/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern].
*/
function sendValue(address payable recipient, uint256 amount) internal {
require(address(this).balance >= amount, "Address: insufficient balance");
(bool success, ) = recipient.call{value: amount}("");
require(success, "Address: unable to send value, recipient may have reverted");
}
/**
* @dev Performs a Solidity function call using a low level `call`. A
* plain `call` is an unsafe replacement for a function call: use this
* function instead.
*
* If `target` reverts with a revert reason, it is bubbled up by this
* function (like regular Solidity function calls).
*
* Returns the raw returned data. To convert to the expected return value,
* use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`].
*
* Requirements:
*
* - `target` must be a contract.
* - calling `target` with `data` must not revert.
*
* _Available since v3.1._
*/
function functionCall(address target, bytes memory data) internal returns (bytes memory) {
return functionCallWithValue(target, data, 0, "Address: low-level call failed");
}
/**
* @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], but with
* `errorMessage` as a fallback revert reason when `target` reverts.
*
* _Available since v3.1._
*/
function functionCall(
address target,
bytes memory data,
string memory errorMessage
) internal returns (bytes memory) {
return functionCallWithValue(target, data, 0, errorMessage);
}
/**
* @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
* but also transferring `value` wei to `target`.
*
* Requirements:
*
* - the calling contract must have an ETH balance of at least `value`.
* - the called Solidity function must be `payable`.
*
* _Available since v3.1._
*/
function functionCallWithValue(address target, bytes memory data, uint256 value) internal returns (bytes memory) {
return functionCallWithValue(target, data, value, "Address: low-level call with value failed");
}
/**
* @dev Same as {xref-Address-functionCallWithValue-address-bytes-uint256-}[`functionCallWithValue`], but
* with `errorMessage` as a fallback revert reason when `target` reverts.
*
* _Available since v3.1._
*/
function functionCallWithValue(
address target,
bytes memory data,
uint256 value,
string memory errorMessage
) internal returns (bytes memory) {
require(address(this).balance >= value, "Address: insufficient balance for call");
(bool success, bytes memory returndata) = target.call{value: value}(data);
return verifyCallResultFromTarget(target, success, returndata, errorMessage);
}
/**
* @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
* but performing a static call.
*
* _Available since v3.3._
*/
function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) {
return functionStaticCall(target, data, "Address: low-level static call failed");
}
/**
* @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`],
* but performing a static call.
*
* _Available since v3.3._
*/
function functionStaticCall(
address target,
bytes memory data,
string memory errorMessage
) internal view returns (bytes memory) {
(bool success, bytes memory returndata) = target.staticcall(data);
return verifyCallResultFromTarget(target, success, returndata, errorMessage);
}
/**
* @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
* but performing a delegate call.
*
* _Available since v3.4._
*/
function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) {
return functionDelegateCall(target, data, "Address: low-level delegate call failed");
}
/**
* @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`],
* but performing a delegate call.
*
* _Available since v3.4._
*/
function functionDelegateCall(
address target,
bytes memory data,
string memory errorMessage
) internal returns (bytes memory) {
(bool success, bytes memory returndata) = target.delegatecall(data);
return verifyCallResultFromTarget(target, success, returndata, errorMessage);
}
/**
* @dev Tool to verify that a low level call to smart-contract was successful, and revert (either by bubbling
* the revert reason or using the provided one) in case of unsuccessful call or if target was not a contract.
*
* _Available since v4.8._
*/
function verifyCallResultFromTarget(
address target,
bool success,
bytes memory returndata,
string memory errorMessage
) internal view returns (bytes memory) {
if (success) {
if (returndata.length == 0) {
// only check isContract if the call was successful and the return data is empty
// otherwise we already know that it was a contract
require(isContract(target), "Address: call to non-contract");
}
return returndata;
} else {
_revert(returndata, errorMessage);
}
}
/**
* @dev Tool to verify that a low level call was successful, and revert if it wasn't, either by bubbling the
* revert reason or using the provided one.
*
* _Available since v4.3._
*/
function verifyCallResult(
bool success,
bytes memory returndata,
string memory errorMessage
) internal pure returns (bytes memory) {
if (success) {
return returndata;
} else {
_revert(returndata, errorMessage);
}
}
function _revert(bytes memory returndata, string memory errorMessage) private pure {
// Look for revert reason and bubble it up if present
if (returndata.length > 0) {
// The easiest way to bubble the revert reason is using memory via assembly
/// @solidity memory-safe-assembly
assembly {
let returndata_size := mload(returndata)
revert(add(32, returndata), returndata_size)
}
} else {
revert(errorMessage);
}
}
}
"
},
"lib/openzeppelin-contracts-v4.9.0/contracts/utils/Strings.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (utils/Strings.sol)
pragma solidity ^0.8.0;
import "./math/Math.sol";
import "./math/SignedMath.sol";
/**
* @dev String operations.
*/
library Strings {
bytes16 private constant _SYMBOLS = "0123456789abcdef";
uint8 private constant _ADDRESS_LENGTH = 20;
/**
* @dev Converts a `uint256` to its ASCII `string` decimal representation.
*/
function toString(uint256 value) internal pure returns (string memory) {
unchecked {
uint256 length = Math.log10(value) + 1;
string memory buffer = new string(length);
uint256 ptr;
/// @solidity memory-safe-assembly
assembly {
ptr := add(buffer, add(32, length))
}
while (true) {
ptr--;
/// @solidity memory-safe-assembly
assembly {
mstore8(ptr, byte(mod(value, 10), _SYMBOLS))
}
value /= 10;
if (value == 0) break;
}
return buffer;
}
}
/**
* @dev Converts a `int256` to its ASCII `string` decimal representation.
*/
function toString(int256 value) internal pure returns (string memory) {
return string(abi.encodePacked(value < 0 ? "-" : "", toString(SignedMath.abs(value))));
}
/**
* @dev Converts a `uint256` to its ASCII `string` hexadecimal representation.
*/
function toHexString(uint256 value) internal pure returns (string memory) {
unchecked {
return toHexString(value, Math.log256(value) + 1);
}
}
/**
* @dev Converts a `uint256` to its ASCII `string` hexadecimal representation with fixed length.
*/
function toHexString(uint256 value, uint256 length) internal pure returns (string memory) {
bytes memory buffer = new bytes(2 * length + 2);
buffer[0] = "0";
buffer[1] = "x";
for (uint256 i = 2 * length + 1; i > 1; --i) {
buffer[i] = _SYMBOLS[value & 0xf];
value >>= 4;
}
require(value == 0, "Strings: hex length insufficient");
return string(buffer);
}
/**
* @dev Converts an `address` with fixed length of 20 bytes to its not checksummed ASCII `string` hexadecimal representation.
*/
function toHexString(address addr) internal pure returns (string memory) {
return toHexString(uint256(uint160(addr)), _ADDRESS_LENGTH);
}
/**
* @dev Returns true if the two strings are equal.
*/
function equal(string memory a, string memory b) internal pure returns (bool) {
return keccak256(bytes(a)) == keccak256(bytes(b));
}
}
"
},
"lib/openzeppelin-contracts-upgradeable-v4.9.0/contracts/utils/ShortStringsUpgradeable.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (utils/ShortStrings.sol)
pragma solidity ^0.8.8;
import "./StorageSlotUpgradeable.sol";
// | string | 0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |
// | length | 0x BB |
type ShortString is bytes32;
/**
* @dev This library provides functions to convert short memory strings
* into a `ShortString` type that can be used as an immutable variable.
*
* Strings of arbitrary length can be optimized using this library if
* they are short enough (up to 31 bytes) by packing them with their
* length (1 byte) in a single EVM word (32 bytes). Additionally, a
* fallback mechanism can be used for every other case.
*
* Usage example:
*
* ```solidity
* contract Named {
* using ShortStrings for *;
*
* ShortString private immutable _name;
* string private _nameFallback;
*
* constructor(string memory contractName) {
* _name = contractName.toShortStringWithFallback(_nameFallback);
* }
*
* function name() external view returns (string memory) {
* return _name.toStringWithFallback(_nameFallback);
* }
* }
* ```
*/
library ShortStringsUpgradeable {
// Used as an identifier for strings longer than 31 bytes.
bytes32 private constant _FALLBACK_SENTINEL = 0x00000000000000000000000000000000000000000000000000000000000000FF;
error StringTooLong(string str);
error InvalidShortString();
/**
* @dev Encode a string of at most 31 chars into a `ShortString`.
*
* This will trigger a `StringTooLong` error is the input string is too long.
*/
function toShortString(string memory str) internal pure returns (ShortString) {
bytes memory bstr = bytes(str);
if (bstr.length > 31) {
revert StringTooLong(str);
}
return ShortString.wrap(bytes32(uint256(bytes32(bstr)) | bstr.length));
}
/**
* @dev Decode a `ShortString` back to a "normal" string.
*/
function toString(ShortString sstr) internal pure returns (string memory) {
uint256 len = byteLength(sstr);
// using `new string(len)` would work locally but is not memory safe.
string memory str = new string(32);
/// @solidity memory-safe-assembly
assembly {
mstore(str, len)
mstore(add(str, 0x20), sstr)
}
return str;
}
/**
* @dev Return the length of a `ShortString`.
*/
function byteLength(ShortString sstr) internal pure returns (uint256) {
uint256 result = uint256(ShortString.unwrap(sstr)) & 0xFF;
if (result > 31) {
revert InvalidShortString();
}
return result;
}
/**
* @dev Encode a string into a `ShortString`, or write it to storage if it is too long.
*/
function toShortStringWithFallback(string memory value, string storage store) internal returns (ShortString) {
if (bytes(value).length < 32) {
return toShortString(value);
} else {
StorageSlotUpgradeable.getStringSlot(store).value = value;
return ShortString.wrap(_FALLBACK_SENTINEL);
}
}
/**
* @dev Decode a string that was encoded to `ShortString` or written to storage using {setWithFallback}.
*/
function toStringWithFallback(ShortString value, string storage store) internal pure returns (string memory) {
if (ShortString.unwrap(value) != _FALLBACK_SENTINEL) {
return toString(value);
} else {
return store;
}
}
/**
* @dev Return the length of a string that was encoded to `ShortString` or written to storage using {setWithFallback}.
*
* WARNING: This will return the "byte length" of the string. This may not reflect the actual length in terms of
* actual characters as the UTF-8 encoding of a single character can span over multiple bytes.
*/
function byteLengthWithFallback(ShortString value, string storage store) internal view returns (uint256) {
if (ShortString.unwrap(value) != _FALLBACK_SENTINEL) {
return byteLength(value);
} else {
return bytes(store).length;
}
}
}
"
},
"lib/openzeppelin-contracts-upgradeable-v4.9.0/contracts/utils/cryptography/SignatureCheckerUpgradeable.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (utils/cryptography/SignatureChecker.sol)
pragma solidity ^0.8.0;
import "./ECDSAUpgradeable.sol";
import "../../interfaces/IERC1271Upgradeable.sol";
/**
* @dev Signature verification helper that can be used instead of `ECDSA.recover` to seamlessly support both ECDSA
* signatures from externally owned accounts (EOAs) as well as ERC1271 signatures from smart contract wallets like
* Argent and Gnosis Safe.
*
* _Available since v4.1._
*/
library SignatureCheckerUpgradeable {
/**
* @dev Checks if a signature is valid for a given signer and data hash. If the signer is a smart contract, the
* signature is validated against that smart contract using ERC1271, otherwise it's validated using `ECDSA.recover`.
*
* NOTE: Unlike ECDSA signatures, contract signatures are revocable, and the outcome of this function can thus
* change through time. It could return true at block N and false at block N+1 (or the opposite).
*/
function isValidSignatureNow(address signer, bytes32 hash, bytes memory signature) internal view returns (bool) {
(address recovered, ECDSAUpgradeable.RecoverError error) = ECDSAUpgradeable.tryRecover(hash, signature);
return
(error == ECDSAUpgradeable.RecoverError.NoError && recovered == signer) ||
isValidERC1271SignatureNow(signer, hash, signature);
}
/**
* @dev Checks if a signature is valid for a given signer and data hash. The signature is validated
* against the signer smart contract using ERC1271.
*
* NOTE: Unlike ECDSA signatures, contract signatures are revocable, and the outcome of this function can thus
* change through time. It could return true at block N and false at block N+1 (or the opposite).
*/
function isValidERC1271SignatureNow(
address signer,
bytes32 hash,
bytes memory signature
) internal view returns (bool) {
(bool success, bytes memory result) = signer.staticcall(
abi.encodeWithSelector(IERC1271Upgradeable.isValidSignature.selector, hash, signature)
);
return (success &&
result.length >= 32 &&
abi.decode(result, (bytes32)) == bytes32(IERC1271Upgradeable.isValidSignature.selector));
}
}
"
},
"src/contracts/interfaces/ISignatureUtilsMixin.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity >=0.5.0;
import "./ISemVerMixin.sol";
interface ISignatureUtilsMixinErrors {
/// @notice Thrown when a signature is invalid.
error InvalidSignature();
/// @notice Thrown when a signature has expired.
error SignatureExpired();
}
interface ISignatureUtilsMixinTypes {
/// @notice Struct that bundles together a signature and an expiration time for the signature.
/// @dev Used primarily for stack management.
struct SignatureWithExpiry {
// the signature itself, formatted as a single bytes object
bytes signature;
// the expiration timestamp (UTC) of the signature
uint256 expiry;
}
/// @notice Struct that bundles together a signature, a salt for uniqueness, and an expiration time for the signature.
/// @dev Used primarily for stack management.
struct SignatureWithSaltAndExpiry {
// the signature itself, formatted as a single bytes object
bytes signature;
// the salt used to generate the signature
bytes32 salt;
// the expiration timestamp (UTC) of the signature
uint256 expiry;
}
}
/**
* @title The interface for common signature utilities.
* @author Layr Labs, Inc.
* @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service
*/
interface ISignatureUtilsMixin is ISignatureUtilsMixinErrors, ISignatureUtilsMixinTypes, ISemVerMixin {
/// @notice Computes the EIP-712 domain separator used for signature validation.
/// @dev The domain separator is computed according to EIP-712 specification, using:
/// - The hardcoded name "EigenLayer"
/// - The contract's version string
/// - The current chain ID
/// - This contract's address
/// @return The 32-byte domain separator hash used in EIP-712 structured data signing.
/// @dev See https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator.
function domainSeparator() external view returns (bytes32);
}
"
},
"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/interfaces/IOperatorTableUpdater.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity >=0.5.0;
import "../libraries/OperatorSetLib.sol";
import "./IECDSACertificateVerifier.sol";
import "./IBN254CertificateVerifier.sol";
import "./IKeyRegistrar.sol";
import "./ICrossChainRegistry.sol";
interface IOperatorTableUpdaterErrors {
/// @notice Thrown when the global table root is in the future
/// @dev Error code: 0xb4233b6a
/// @dev We enforce that reference timestamps cannot be in the future to prevent manipulation and ensure temporal consistency
error GlobalTableRootInFuture();
/// @notice Thrown when the global table root is stale
/// @dev Error code: 0x1bfd4358
/// @dev We enforce that new reference timestamps must be greater than the latest to prevent retroactive updates and maintain chronological order
error GlobalTableRootStale();
/// @notice Thrown when the table root does not match what is in the certificate
/// @dev Error code: 0x8b56642d
/// @dev We enforce that the message hash in the certificate matches the expected EIP-712 hash to prevent certificate replay attacks
error InvalidMessageHash();
/// @notice Thrown when the GlobalTableRoot update fails
/// @dev Error code: 0xc108107c
/// @dev We enforce that certificates are valid according to the confirmation threshold to prevent unauthorized global root updates
error CertificateInvalid();
/// @notice Thrown when the table has been updated for the timestamp
/// @dev Error code: 0x207617df
/// @dev We enforce that reference timestam
Submitted on: 2025-09-25 10:05:25
Comments
Log in to comment.
No comments yet.