diff --git a/bolt-contracts/src/contracts/BoltManagerV2.sol b/bolt-contracts/src/contracts/BoltManagerV2.sol index 45ed460b4..91c4bea10 100644 --- a/bolt-contracts/src/contracts/BoltManagerV2.sol +++ b/bolt-contracts/src/contracts/BoltManagerV2.sol @@ -6,6 +6,7 @@ import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeab import {Time} from "@openzeppelin/contracts/utils/types/Time.sol"; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; import {OperatorMapWithTimeV2} from "../lib/OperatorMapWithTimeV2.sol"; import {EnumerableMapV2} from "../lib/EnumerableMapV2.sol"; @@ -215,13 +216,18 @@ contract BoltManagerV2 is IBoltManagerV2, OwnableUpgradeable, UUPSUpgradeable { // ========= OPERATOR FUNCTIONS ====== // /// @notice Registers an operator with Bolt. Only callable by a supported middleware contract. - function registerOperator(address operatorAddr, string calldata rpc) external onlyMiddleware { + function registerOperator(address operatorAddr, string calldata rpc, address curator) external onlyMiddleware { if (operators.contains(operatorAddr)) { revert OperatorAlreadyRegistered(); } + // check curator support interface (ERC165) and prevent invalid curator + if (curator != address(0) && !IERC165(curator).supportsInterface(type(ICredibleCommitmentCurationProvider).interfaceId)) { + revert InvalidCurator(); + } + // Create an already enabled operator - EnumerableMapV2.Operator memory operator = EnumerableMapV2.Operator(rpc, msg.sender, Time.timestamp()); + EnumerableMapV2.Operator memory operator = EnumerableMapV2.Operator(rpc, msg.sender, Time.timestamp(), curator); operators.set(operatorAddr, operator); } diff --git a/bolt-contracts/src/contracts/BoltSymbioticMiddlewareV2.sol b/bolt-contracts/src/contracts/BoltSymbioticMiddlewareV2.sol index 85a6bfda2..f45b52e39 100644 --- a/bolt-contracts/src/contracts/BoltSymbioticMiddlewareV2.sol +++ b/bolt-contracts/src/contracts/BoltSymbioticMiddlewareV2.sol @@ -7,7 +7,8 @@ import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; - +import {SignatureChecker} from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol"; +import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; import {IBaseDelegator} from "@symbiotic/interfaces/delegator/IBaseDelegator.sol"; import {Subnetwork} from "@symbiotic/contracts/libraries/Subnetwork.sol"; import {IVault} from "@symbiotic/interfaces/vault/IVault.sol"; @@ -29,7 +30,7 @@ import {IBoltManagerV1} from "../interfaces/IBoltManagerV1.sol"; /// See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps /// To validate the storage layout, use the Openzeppelin Foundry Upgrades toolkit. /// You can also validate manually with forge: forge inspect storage-layout --pretty -contract BoltSymbioticMiddlewareV2 is IBoltMiddlewareV1, OwnableUpgradeable, UUPSUpgradeable { +contract BoltSymbioticMiddlewareV2 is IBoltMiddlewareV1, OwnableUpgradeable, EIP712, UUPSUpgradeable { using EnumerableSet for EnumerableSet.AddressSet; using EnumerableMap for EnumerableMap.AddressToUintMap; using MapWithTimeData for EnumerableMap.AddressToUintMap; @@ -140,23 +141,17 @@ contract BoltSymbioticMiddlewareV2 is IBoltMiddlewareV1, OwnableUpgradeable, UUP NAME_HASH = keccak256("SYMBIOTIC"); } - function _authorizeUpgrade( - address newImplementation - ) internal override onlyOwner {} + function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} // ========= VIEW FUNCTIONS ========= /// @notice Get the start timestamp of an epoch. - function getEpochStartTs( - uint48 epoch - ) public view returns (uint48 timestamp) { + function getEpochStartTs(uint48 epoch) public view returns (uint48 timestamp) { return START_TIMESTAMP + epoch * parameters.EPOCH_DURATION(); } /// @notice Get the epoch at a given timestamp. - function getEpochAtTs( - uint48 timestamp - ) public view returns (uint48 epoch) { + function getEpochAtTs(uint48 timestamp) public view returns (uint48 epoch) { return (timestamp - START_TIMESTAMP) / parameters.EPOCH_DURATION(); } @@ -174,9 +169,7 @@ contract BoltSymbioticMiddlewareV2 is IBoltMiddlewareV1, OwnableUpgradeable, UUP /// @notice Allow a vault to signal opt-in to Bolt Protocol. /// @param vault The vault address to signal opt-in for. - function registerVault( - address vault - ) public onlyOwner { + function registerVault(address vault) public onlyOwner { if (vaults.contains(vault)) { revert AlreadyRegistered(); } @@ -193,9 +186,7 @@ contract BoltSymbioticMiddlewareV2 is IBoltMiddlewareV1, OwnableUpgradeable, UUP /// @notice Deregister a vault from working in Bolt Protocol. /// @param vault The vault address to deregister. - function deregisterVault( - address vault - ) public onlyOwner { + function deregisterVault(address vault) public onlyOwner { if (!vaults.contains(vault)) { revert NotRegistered(); } @@ -205,24 +196,50 @@ contract BoltSymbioticMiddlewareV2 is IBoltMiddlewareV1, OwnableUpgradeable, UUP // ========= SYMBIOTIC MIDDLEWARE LOGIC ========= - /// @notice Allow an operator to signal opt-in to Bolt Protocol. - /// msg.sender must be an operator in the Symbiotic network. function registerOperator( - string calldata rpc + address operator, + string calldata rpc, + address curator, + uint48 deadline, + bytes calldata signature ) public { - if (manager.isOperator(msg.sender)) { + if (deadline < Time.timestamp()) { + revert ExpiredSignature(); + } + + if ( + !SignatureChecker.isValidSignatureNow( + operator, + _hashTypedDataV4(keccak256(abi.encode(msg.sender, operator, rpc, curator, deadline))), + signature + ) + ) { + revert InvalidSignature(); + } + + _registerOperator(operator, rpc, curator); + } + + /// @notice Allow an operator to signal opt-in to Bolt Protocol. + /// msg.sender must be an operator in the Symbiotic network. + function registerOperator(string calldata rpc, address curator) public { + _registerOperator(msg.sender, rpc, curator); + } + + function _registerOperator(address operator, string calldata rpc, address curator) internal { + if (manager.isOperator(operator)) { revert AlreadyRegistered(); } - if (!IRegistry(OPERATOR_REGISTRY).isEntity(msg.sender)) { + if (!IRegistry(OPERATOR_REGISTRY).isEntity(operator)) { revert NotOperator(); } - if (!IOptInService(OPERATOR_NET_OPTIN).isOptedIn(msg.sender, BOLT_SYMBIOTIC_NETWORK)) { + if (!IOptInService(OPERATOR_NET_OPTIN).isOptedIn(operator, BOLT_SYMBIOTIC_NETWORK)) { revert OperatorNotOptedIn(); } - manager.registerOperator(msg.sender, rpc); + manager.registerOperator(operator, rpc, curator); } /// @notice Deregister a Symbiotic operator from working in Bolt Protocol. @@ -268,9 +285,7 @@ contract BoltSymbioticMiddlewareV2 is IBoltMiddlewareV1, OwnableUpgradeable, UUP /// @notice Check if a vault is currently enabled to work in Bolt Protocol. /// @param vault The vault address to check the enabled status for. /// @return True if the vault is enabled, false otherwise. - function isVaultEnabled( - address vault - ) public view returns (bool) { + function isVaultEnabled(address vault) public view returns (bool) { (uint48 enabledTime, uint48 disabledTime) = vaults.getTimes(vault); return enabledTime != 0 && disabledTime == 0; } @@ -280,9 +295,7 @@ contract BoltSymbioticMiddlewareV2 is IBoltMiddlewareV1, OwnableUpgradeable, UUP /// @param operator The operator address to get the collaterals and amounts staked for. /// @return collaterals The collaterals staked by the operator. /// @dev Assumes that the operator is registered and enabled. - function getOperatorCollaterals( - address operator - ) public view returns (address[] memory, uint256[] memory) { + function getOperatorCollaterals(address operator) public view returns (address[] memory, uint256[] memory) { address[] memory collateralTokens = new address[](vaults.length()); uint256[] memory amounts = new uint256[](vaults.length()); diff --git a/bolt-contracts/src/contracts/BoltValidatorsV2.sol b/bolt-contracts/src/contracts/BoltValidatorsV2.sol index 1a533c139..b7e9e41ae 100644 --- a/bolt-contracts/src/contracts/BoltValidatorsV2.sol +++ b/bolt-contracts/src/contracts/BoltValidatorsV2.sol @@ -7,8 +7,11 @@ import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeab import {BLS12381} from "../lib/bls/BLS12381.sol"; import {BLSSignatureVerifier} from "../lib/bls/BLSSignatureVerifier.sol"; import {ValidatorsLib} from "../lib/ValidatorsLib.sol"; +import {URCMerkleTree} from "../lib/URCMerkleTree.sol"; import {IBoltValidatorsV2} from "../interfaces/IBoltValidatorsV2.sol"; import {IBoltParametersV1} from "../interfaces/IBoltParametersV1.sol"; +import {IBoltManagerV2} from "../interfaces/IBoltManagerV2.sol"; +import {ICredibleCommitmentCurationProvider} from "../interfaces/ICredibleCommitmentCurationProvider.sol"; /// @title Bolt Validators /// @notice This contract is responsible for registering validators and managing their configuration @@ -26,6 +29,8 @@ contract BoltValidatorsV2 is IBoltValidatorsV2, BLSSignatureVerifier, OwnableUpg /// @notice Bolt Parameters contract. IBoltParametersV1 public parameters; + IBoltManagerV2 public manager; + /// @notice Validators (aka Blockspace providers) /// @dev This struct occupies 6 storage slots. ValidatorsLib.ValidatorSet internal VALIDATORS; @@ -65,6 +70,14 @@ contract BoltValidatorsV2 is IBoltValidatorsV2, BLSSignatureVerifier, OwnableUpg parameters = IBoltParametersV1(_parameters); } + function setManager(address _manager) external onlyOwner { + if (IBoltManagerV2(_manager).validators() != address(this)) { + revert InvalidManager(); + } + + manager = IBoltManagerV2(_manager); + } + function _authorizeUpgrade( address newImplementation ) internal override onlyOwner {} @@ -142,6 +155,11 @@ contract BoltValidatorsV2 is IBoltValidatorsV2, BLSSignatureVerifier, OwnableUpg revert InvalidBLSSignature(); } + ICredibleCommitmentCurationProvider curator = ICredibleCommitmentCurationProvider(manager.getOperatorCurator(authorizedOperator)); + if (address(curator) != address(0)) { + curator.hookOnlyApprovedRegistrationRoot(regRoot); + } + _registerValidator(hashPubkey(pubkey), authorizedOperator, maxCommittedGasLimit); } @@ -153,7 +171,7 @@ contract BoltValidatorsV2 is IBoltValidatorsV2, BLSSignatureVerifier, OwnableUpg /// @param authorizedOperator The address of the authorized operator function batchRegisterValidators( BLS12381.G1Point[] calldata pubkeys, - BLS12381.G2Point calldata signature, + BLS12381.G2Point[] calldata signatures, uint32 maxCommittedGasLimit, address authorizedOperator ) public { @@ -168,8 +186,11 @@ contract BoltValidatorsV2 is IBoltValidatorsV2, BLSSignatureVerifier, OwnableUpg // try to register the same validators bytes memory message = abi.encodePacked(block.chainid, msg.sender, expectedValidatorSequenceNumbers); - // Aggregate the pubkeys into a single pubkey to verify the aggregated signature once - BLS12381.G1Point memory aggPubkey = _aggregatePubkeys(pubkeys); + bytes32 registrationRoot = _merkleizeRegistrations(pubkeys, signatures); + + + // // Aggregate the pubkeys into a single pubkey to verify the aggregated signature once + // BLS12381.G1Point memory aggPubkey = _aggregatePubkeys(pubkeys); if (!_verifySignature(message, signature, aggPubkey)) { revert InvalidBLSSignature(); @@ -183,6 +204,19 @@ contract BoltValidatorsV2 is IBoltValidatorsV2, BLSSignatureVerifier, OwnableUpg _batchRegisterValidators(pubkeyHashes, authorizedOperator, maxCommittedGasLimit); } + function _merkleizeRegistrations(BLS12381.G1Point[] calldata pubkeys, BLS12381.G2Point[] calldata signatures,) internal returns (bytes32 registrationRoot) { + // Create leaves array with padding + bytes32[] memory leaves = new bytes32[](pubkeys.length); + + // Create leaf nodes by hashing Registration structs + for (uint256 i = 0; i < pubkeys.length; i++) { + leaves[i] = keccak256(abi.encode(pubkeys[i], signatures[i])); + } + + registrationRoot = URCMerkleTree.generateTree(leaves); + } + + /// @notice Register a batch of Validators and authorize a Collateral Provider and Operator for them /// @dev This function allows anyone to register a list of Validators. /// @param pubkeyHashes List of BLS public key hashes for the Validators to be registered diff --git a/bolt-contracts/src/interfaces/IBoltManagerV2.sol b/bolt-contracts/src/interfaces/IBoltManagerV2.sol index c267c2272..47c5c439d 100644 --- a/bolt-contracts/src/interfaces/IBoltManagerV2.sol +++ b/bolt-contracts/src/interfaces/IBoltManagerV2.sol @@ -6,6 +6,7 @@ interface IBoltManagerV2 { error OperatorAlreadyRegistered(); error OperatorNotRegistered(); error UnauthorizedMiddleware(); + error InvalidCurator(); // TODO: remove in future upgrade (unused) error InactiveOperator(); @@ -25,6 +26,9 @@ interface IBoltManagerV2 { uint256[] amounts; } + // returns the address of the validators contract + function validators() external view returns (address); + function registerOperator(address operator, string calldata rpc) external; function deregisterOperator( @@ -43,6 +47,10 @@ interface IBoltManagerV2 { address operator ) external view returns (bool); + function getOperatorCurator( + address operator + ) external view returns (address); + function getProposerStatus( bytes20 pubkeyHash ) external view returns (ProposerStatus memory status); diff --git a/bolt-contracts/src/interfaces/IBoltMiddlewareV1.sol b/bolt-contracts/src/interfaces/IBoltMiddlewareV1.sol index bd01b6ca5..3f6da23b8 100644 --- a/bolt-contracts/src/interfaces/IBoltMiddlewareV1.sol +++ b/bolt-contracts/src/interfaces/IBoltMiddlewareV1.sol @@ -10,6 +10,7 @@ interface IBoltMiddlewareV1 { error OperatorNotOptedIn(); error NotOperator(); error NotAllowed(); + error ExpiredSignature(); function NAME_HASH() external view returns (bytes32); diff --git a/bolt-contracts/src/interfaces/IBoltValidatorsV2.sol b/bolt-contracts/src/interfaces/IBoltValidatorsV2.sol index fa4fed993..f27f8074f 100644 --- a/bolt-contracts/src/interfaces/IBoltValidatorsV2.sol +++ b/bolt-contracts/src/interfaces/IBoltValidatorsV2.sol @@ -16,6 +16,7 @@ interface IBoltValidatorsV2 { error UnsafeRegistrationNotAllowed(); error UnauthorizedCaller(); error InvalidPubkey(); + error InvalidManager(); function getAllValidators() external view returns (ValidatorInfo[] memory); diff --git a/bolt-contracts/src/interfaces/ICredibleCommitmentCurationProvider.sol b/bolt-contracts/src/interfaces/ICredibleCommitmentCurationProvider.sol new file mode 100644 index 000000000..39f42d470 --- /dev/null +++ b/bolt-contracts/src/interfaces/ICredibleCommitmentCurationProvider.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +interface ICredibleCommitmentCurationProvider { + + /// @notice Method check for approval for provided registration root against + /// internal CredibleCommitmentCurationProvider logic. + function isRegistrationRootApproved(bytes32 registrationRoot) external view returns (bool); + + /// @notice Hooke used to revert if provided registration root is not approved. + function hookOnlyApprovedRegistrationRoot(bytes32 registrationRoot) external view; +} diff --git a/bolt-contracts/src/lib/EnumerableMapV2.sol b/bolt-contracts/src/lib/EnumerableMapV2.sol index b4f1a1c84..1d7e4b733 100644 --- a/bolt-contracts/src/lib/EnumerableMapV2.sol +++ b/bolt-contracts/src/lib/EnumerableMapV2.sol @@ -15,6 +15,8 @@ library EnumerableMapV2 { address middleware; // Timestamp of registration uint256 timestamp; + // Optional Curator contract address + address curator; } struct OperatorMap { diff --git a/bolt-contracts/src/lib/URCMerkleTree.sol b/bolt-contracts/src/lib/URCMerkleTree.sol new file mode 100644 index 000000000..b4476048c --- /dev/null +++ b/bolt-contracts/src/lib/URCMerkleTree.sol @@ -0,0 +1,182 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0 <0.9.0; + +/** + * @title MerkleTree + * @dev Implementation of a binary Merkle tree with proof generation and verification + */ +library URCMerkleTree { + error EmptyLeaves(); + error IndexOutOfBounds(); + error LeavesTooLarge(); + /** + * @dev Generates a complete Merkle tree from an array of leaves + * @dev The tree size is limited to 256 leaves + * @param leaves Array of leaf values + * @return bytes32 Root hash of the Merkle tree + */ + + function generateTree(bytes32[] memory leaves) internal pure returns (bytes32) { + if (leaves.length == 0) revert EmptyLeaves(); + if (leaves.length == 1) return leaves[0]; + if (leaves.length > 256) revert LeavesTooLarge(); + + uint256 _nextPowerOfTwo = nextPowerOfTwo(leaves.length); + bytes32[] memory nodes = new bytes32[](_nextPowerOfTwo); + + // Fill leaf nodes + for (uint256 i = 0; i < leaves.length; i++) { + nodes[i] = leaves[i]; + } + // Fill remaining nodes with zero + for (uint256 i = leaves.length; i < _nextPowerOfTwo; i++) { + nodes[i] = bytes32(0); + } + + // Build up the tree + uint256 n = _nextPowerOfTwo; + while (n > 1) { + for (uint256 i = 0; i < n / 2; i++) { + nodes[i] = _efficientKeccak256(nodes[2 * i], nodes[2 * i + 1]); + } + n = n / 2; + } + + return nodes[0]; + } + + /** + * @dev Generates a Merkle proof for a leaf at the given index + * @param leaves Array of leaf values + * @param index Index of the leaf to generate proof for + * @return bytes32[] Array of proof elements + */ + function generateProof(bytes32[] memory leaves, uint256 index) internal pure returns (bytes32[] memory) { + if (index >= leaves.length) revert IndexOutOfBounds(); + if (leaves.length <= 1) return new bytes32[](0); + + uint256 _nextPowerOfTwo = nextPowerOfTwo(leaves.length); + + // Calculate height of tree (log2 of next power of 2) + uint256 height = 0; + uint256 size = _nextPowerOfTwo; + while (size > 1) { + height++; + size /= 2; + } + + bytes32[] memory nodes = new bytes32[](_nextPowerOfTwo); + bytes32[] memory proof = new bytes32[](height); // <-- This is the key fix + + // Fill leaf nodes + for (uint256 i = 0; i < leaves.length; i++) { + nodes[i] = leaves[i]; + } + // Fill remaining nodes with zero + for (uint256 i = leaves.length; i < _nextPowerOfTwo; i++) { + nodes[i] = bytes32(0); + } + + uint256 proofIndex = 0; + uint256 levelSize = _nextPowerOfTwo; + uint256 currentIndex = index; + + // Build proof level by level + while (levelSize > 1) { + uint256 siblingIndex = currentIndex ^ 1; // Get sibling index + proof[proofIndex++] = nodes[siblingIndex]; + + // Calculate next level + for (uint256 i = 0; i < levelSize / 2; i++) { + nodes[i] = _efficientKeccak256(nodes[2 * i], nodes[2 * i + 1]); + } + levelSize /= 2; + currentIndex /= 2; + } + + return proof; + } + + /** + * @dev Verifies a Merkle proof for a leaf + * @param root Root hash of the Merkle tree + * @param leaf Leaf value being proved + * @param index Index of the leaf in the tree + * @param proof Array of proof elements + * @return bool True if the proof is valid, false otherwise + */ + function verifyProof(bytes32 root, bytes32 leaf, uint256 index, bytes32[] memory proof) + internal + pure + returns (bool) + { + bytes32 computedHash = leaf; + + for (uint256 i = 0; i < proof.length; i++) { + if (index % 2 == 0) { + computedHash = _efficientKeccak256(computedHash, proof[i]); + } else { + computedHash = _efficientKeccak256(proof[i], computedHash); + } + index = index / 2; + } + + return computedHash == root; + } + + /** + * @dev Verifies a Merkle proof for a leaf + * @param root Root hash of the Merkle tree + * @param leaf Leaf value being proved + * @param index Index of the leaf in the tree + * @param proof Array of proof elements + * @return bool True if the proof is valid, false otherwise + */ + function verifyProofCalldata(bytes32 root, bytes32 leaf, uint256 index, bytes32[] calldata proof) + internal + pure + returns (bool) + { + bytes32 computedHash = leaf; + + for (uint256 i = 0; i < proof.length; i++) { + if (index % 2 == 0) { + computedHash = _efficientKeccak256(computedHash, proof[i]); + } else { + computedHash = _efficientKeccak256(proof[i], computedHash); + } + index = index / 2; + } + + return computedHash == root; + } + + /** + * @dev Implementation of keccak256(abi.encode(a, b)) that doesn't allocate or expand memory. + * @dev From https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/cryptography/Hashes.sol + */ + function _efficientKeccak256(bytes32 a, bytes32 b) public pure returns (bytes32 value) { + assembly ("memory-safe") { + mstore(0x00, a) + mstore(0x20, b) + value := keccak256(0x00, 0x40) + } + } + + /** + * @dev Returns the next power of 2 for a number <= 256 + * @param x The number to find the next power of 2 for (must be <= 256) + * @return The next power of 2 + */ + function nextPowerOfTwo(uint256 x) internal pure returns (uint256) { + if (x <= 1) return 1; + if (x <= 2) return 2; + if (x <= 4) return 4; + if (x <= 8) return 8; + if (x <= 16) return 16; + if (x <= 32) return 32; + if (x <= 64) return 64; + if (x <= 128) return 128; + return 256; + } +}