diff --git a/contracts/contracts/core/ProviderRegistryV2.sol b/contracts/contracts/core/ProviderRegistryV2.sol new file mode 100644 index 000000000..310a7dfc9 --- /dev/null +++ b/contracts/contracts/core/ProviderRegistryV2.sol @@ -0,0 +1,496 @@ +// SPDX-License-Identifier: BSL 1.1 +pragma solidity 0.8.26; + +import {Ownable2StepUpgradeable} from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; +import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; +import {PreconfManager} from "./PreconfManager.sol"; +import {IProviderRegistryV2} from "../interfaces/IProviderRegistryV2.sol"; +import {ProviderRegistryStorage} from "./ProviderRegistryStorage.sol"; +import {FeePayout} from "../utils/FeePayout.sol"; +import {Errors} from "../utils/Errors.sol"; + +/// @title Provider Registry V2 +/// @notice This contract is for provider registry and staking. +/// @custom:oz-upgrades-from ProviderRegistry +contract ProviderRegistryV2 is + IProviderRegistryV2, + ProviderRegistryStorage, + Ownable2StepUpgradeable, + UUPSUpgradeable, + ReentrancyGuardUpgradeable, + PausableUpgradeable +{ + /** + * @dev Modifier to restrict a function to only be callable by the preconf manager. + */ + modifier onlyPreconfManager() { + require( + msg.sender == preconfManager, + NotPreconfContract(msg.sender, preconfManager) + ); + _; + } + + /// @dev See https://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable#initializing_the_implementation_contract + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + /** + * @dev Receive function is disabled for this contract to prevent unintended interactions. + * Should be removed from here in case the registerAndStake function becomes more complex + */ + receive() external payable { + revert Errors.InvalidReceive(); + } + + /** + * @dev Fallback function to revert all calls, ensuring no unintended interactions. + */ + fallback() external payable { + revert Errors.InvalidFallback(); + } + + /** + * @dev Initializes the contract with a minimum stake requirement. + * @param _minStake The minimum stake required for provider registration. + * @param _penaltyFeeRecipient The address that accumulates penalty fees + * @param _feePercent The fee percentage for penalty + * @param _owner Owner of the contract, explicitly needed since contract is deployed w/ create2 factory. + * @param _withdrawalDelay The withdrawal delay in milliseconds. + * @param _penaltyFeePayoutPeriod The min number of seconds (or ms on the mev-commit chain) between penalty fee payouts + */ + function initialize( + uint256 _minStake, + address _penaltyFeeRecipient, + uint256 _feePercent, + address _owner, + uint256 _withdrawalDelay, + uint256 _penaltyFeePayoutPeriod + ) external initializer { + FeePayout.initTimestampTracker( + penaltyFeeTracker, + _penaltyFeeRecipient, + _penaltyFeePayoutPeriod + ); + minStake = _minStake; + feePercent = _feePercent; + withdrawalDelay = _withdrawalDelay; + __ReentrancyGuard_init(); + __Ownable_init(_owner); + __Pausable_init(); + } + + /** + * @dev Stake more funds into the provider's stake. + */ + function stake() external payable whenNotPaused { + _stake(msg.sender); + } + + /// @dev Delegate stake to a provider. + function delegateStake(address provider) external payable whenNotPaused { + _stake(provider); + } + + /** + * @dev Slash funds from the provider and send the slashed amount to the bidder. + * @dev reenterancy not necessary but still putting here for precaution. + * @dev Note we slash the funds taking into account the residual bid percent after decay. + * @param slashAmt The amount to slash from the provider's stake. + * @param provider The address of the provider. + * @param bidder The address to transfer the slashed funds to. + */ + function slash( + uint256 slashAmt, + address provider, + address payable bidder + ) external nonReentrant onlyPreconfManager whenNotPaused { + uint256 penaltyFee = (slashAmt * feePercent) / ONE_HUNDRED_PERCENT; + uint256 bidderPortion = slashAmt; + uint256 totalSlash = bidderPortion + penaltyFee; + uint256 providerStake = providerStakes[provider]; + + if (providerStake < totalSlash) { + emit InsufficientFundsToSlash( + provider, + providerStake, + penaltyFee, + bidderPortion + ); + + uint256 leftover = providerStake; + + if (leftover < bidderPortion) { + bidderPortion = leftover; + leftover = 0; + penaltyFee = 0; + } else { + leftover -= bidderPortion; + if (leftover < penaltyFee) { + penaltyFee = leftover; + leftover = 0; + } else { + leftover -= penaltyFee; + } + } + + totalSlash = bidderPortion + penaltyFee; + } + + providerStakes[provider] = providerStake - totalSlash; + penaltyFeeTracker.accumulatedAmount += penaltyFee; + + if (FeePayout.isPayoutDueByTimestamp(penaltyFeeTracker)) { + FeePayout.transferToRecipientByTimestamp(penaltyFeeTracker); + } + + if (!payable(bidder).send(bidderPortion)) { + emit TransferToBidderFailed(bidder, bidderPortion); + bidderSlashedAmount[bidder] += bidderPortion; + } + + emit FundsSlashed(provider, totalSlash); + } + + /** + * @dev Sets the minimum stake required for registration. Can only be called by the owner. + * @param _minStake The new minimum stake amount. + */ + function setMinStake(uint256 _minStake) external onlyOwner { + minStake = _minStake; + emit MinStakeUpdated(_minStake); + } + + /** + * @dev Sets the pre-confirmations contract address. Can only be called by the owner. + * @param contractAddress The address of the pre-confirmations contract. + */ + function setPreconfManager(address contractAddress) external onlyOwner { + preconfManager = contractAddress; + emit PreconfManagerUpdated(contractAddress); + } + + /** + * @notice Sets the new fee percent + * @dev onlyOwner restriction + * @param newFeePercent this is the new fee percent + */ + function setNewFeePercent(uint256 newFeePercent) external onlyOwner { + feePercent = newFeePercent; + emit FeePercentUpdated(newFeePercent); + } + + /// @dev Sets the withdrawal delay. Can only be called by the owner. + /// @param _withdrawalDelay The new withdrawal delay in milliseconds + /// as mev-commit chain is running with milliseconds. + function setWithdrawalDelay(uint256 _withdrawalDelay) external onlyOwner { + withdrawalDelay = _withdrawalDelay; + emit WithdrawalDelayUpdated(_withdrawalDelay); + } + + /** + * @notice Sets a new penalty fee recipient + * @dev onlyOwner restriction + * @param newFeeRecipient The address of the new penalty fee recipient + */ + function setNewPenaltyFeeRecipient( + address newFeeRecipient + ) external onlyOwner { + penaltyFeeTracker.recipient = newFeeRecipient; + emit PenaltyFeeRecipientUpdated(newFeeRecipient); + } + + /// @dev Sets the fee payout period in seconds (or ms on the mev-commit chain) + /// @param _feePayoutPeriod The new fee payout period in seconds (or ms on the mev-commit chain) + function setFeePayoutPeriod( + uint256 _feePayoutPeriod + ) external onlyOwner { + penaltyFeeTracker.payoutTimePeriod = _feePayoutPeriod; + emit FeePayoutPeriodUpdated(_feePayoutPeriod); + } + + function overrideAddBLSKey( + address provider, + bytes calldata blsPublicKey + ) external onlyOwner { + require(providerRegistered[provider], ProviderNotRegistered(provider)); + require(blockBuilderBLSKeyToAddress[blsPublicKey] == address(0), BLSKeyAlreadyExists(blsPublicKey)); + eoaToBlsPubkeys[provider].push(blsPublicKey); + blockBuilderBLSKeyToAddress[blsPublicKey] = provider; + emit BLSKeyAdded(provider, blsPublicKey); + } + + function overrideRemoveBLSKey( + address provider, + bytes calldata blsPublicKey + ) external onlyOwner { + require(providerRegistered[provider], ProviderNotRegistered(provider)); + require(blockBuilderBLSKeyToAddress[blsPublicKey] == provider, BLSKeyDoesNotExist(blsPublicKey)); + bytes[] storage keys = eoaToBlsPubkeys[provider]; + uint256 length = keys.length; + for (uint256 i = 0; i < length; ++i) { + if (keccak256(keys[i]) == keccak256(blsPublicKey)) { + keys[i] = keys[length - 1]; + keys.pop(); + break; + } + } + delete blockBuilderBLSKeyToAddress[blsPublicKey]; + emit BLSKeyRemoved(provider, blsPublicKey); + } + + /// @dev Requests unstake of the staked amount. + function unstake() external whenNotPaused { + require(providerStakes[msg.sender] != 0, NoStakeToWithdraw(msg.sender)); + require( + withdrawalRequests[msg.sender] == 0, + UnstakeRequestExists(msg.sender) + ); + withdrawalRequests[msg.sender] = block.timestamp; + emit Unstake(msg.sender, block.timestamp); + } + + /// @dev Completes the withdrawal of the staked amount. + function withdraw() external nonReentrant whenNotPaused { + require( + withdrawalRequests[msg.sender] != 0, + NoUnstakeRequest(msg.sender) + ); + require( + block.timestamp >= withdrawalRequests[msg.sender] + withdrawalDelay, + DelayNotPassed( + withdrawalRequests[msg.sender], + withdrawalDelay, + block.timestamp + ) + ); + + uint256 providerStake = providerStakes[msg.sender]; + providerStakes[msg.sender] = 0; + providerRegistered[msg.sender] = false; + withdrawalRequests[msg.sender] = 0; + require(preconfManager != address(0), PreconfManagerNotSet()); + + uint256 providerPendingCommitmentsCount = PreconfManager( + payable(preconfManager) + ).commitmentsCount(msg.sender); + + require( + providerPendingCommitmentsCount == 0, + ProviderCommitmentsPending( + msg.sender, + providerPendingCommitmentsCount + ) + ); + + (bool success, ) = msg.sender.call{value: providerStake}(""); + require(success, StakeTransferFailed(msg.sender, providerStake)); + + emit Withdraw(msg.sender, providerStake); + } + + /** + * @dev Allows the bidder to withdraw the slashed amount. + */ + function withdrawSlashedAmount() external nonReentrant whenNotPaused { + require( + bidderSlashedAmount[msg.sender] != 0, + BidderAmountIsZero(msg.sender) + ); + uint256 amount = bidderSlashedAmount[msg.sender]; + bidderSlashedAmount[msg.sender] = 0; + (bool success, ) = msg.sender.call{value: amount}(""); + require(success, BidderWithdrawalTransferFailed(msg.sender, amount)); + + emit BidderWithdrawSlashedAmount(msg.sender, amount); + } + + /** + * @dev Adds a verified BLS key to the provider's account. + * @param blsPublicKey The BLS public key to be added. + * @param signature The signature (96 bytes) used for verification. + */ + function addVerifiedBLSKey( + bytes calldata blsPublicKey, + bytes calldata signature + ) external { + address provider = msg.sender; + + require(providerRegistered[provider], ProviderNotRegistered(provider)); + require( + blsPublicKey.length == 48, + PublicKeyLengthInvalid(48, blsPublicKey.length) + ); + require( + signature.length == 96, + SignatureLengthInvalid(96, signature.length) + ); + + bytes32 message = keccak256(abi.encodePacked(provider)); + + // Verify the BLS signature + bool isValid = verifySignature(blsPublicKey, message, signature); + require(isValid, BLSSignatureInvalid()); + + // Add the BLS public key to the provider's account + eoaToBlsPubkeys[provider].push(blsPublicKey); + blockBuilderBLSKeyToAddress[blsPublicKey] = provider; + + emit BLSKeyAdded(provider, blsPublicKey); + } + + /** + * @dev Manually withdraws accumulated penalty fees to the recipient + * to cover the edge case that oracle doesn't slash/reward, and funds still need to be withdrawn. + */ + function manuallyWithdrawPenaltyFee() external onlyOwner { + FeePayout.transferToRecipientByTimestamp(penaltyFeeTracker); + } + + /// @dev Allows the owner to pause the contract. + function pause() external onlyOwner { + _pause(); + } + + /// @dev Allows the owner to unpause the contract. + function unpause() external onlyOwner { + _unpause(); + } + + /** + * @dev Get provider staked amount. + * @param provider The address of the provider. + * @return The staked amount for the provider. + */ + function getProviderStake( + address provider + ) external view returns (uint256) { + return providerStakes[provider]; + } + + /// @dev Returns the BLS public keys corresponding to a provider's staked EOA address. + function getBLSKeys( + address provider + ) external view returns (bytes[] memory) { + return eoaToBlsPubkeys[provider]; + } + + /// @dev Returns the EOA address corresponding to a provider's BLS public key. + function getEoaFromBLSKey( + bytes calldata blsKey + ) external view returns (address) { + return blockBuilderBLSKeyToAddress[blsKey]; + } + + /// @return penaltyFee amount not yet transferred to recipient + function getAccumulatedPenaltyFee() external view returns (uint256) { + return penaltyFeeTracker.accumulatedAmount; + } + + /** + * @dev Register and stake function for providers. + * The validity of this key must be verified manually off-chain. + */ + function registerAndStake() public payable whenNotPaused { + _registerAndStake(msg.sender); + } + + /** + * @dev Register and stake on behalf of a provider. + * @param provider Address of the provider. + */ + function delegateRegisterAndStake( + address provider + ) public payable whenNotPaused onlyOwner { + _registerAndStake(provider); + } + + /// @dev Ensure the provider's balance is greater than minStake and no pending withdrawal + function isProviderValid(address provider) public view { + uint256 providerStake = providerStakes[provider]; + require( + providerStake >= minStake, + InsufficientStake(providerStake, minStake) + ); + require( + withdrawalRequests[provider] == 0, + PendingWithdrawalRequest(provider) + ); + } + + /// @dev View function checking if a provider is valid, returning booleans and never reverting + function areProvidersValid(address[] calldata providers) public view returns (bool[] memory) { + bool[] memory validProviders = new bool[](providers.length); + uint256 length = providers.length; + for (uint256 i = 0; i < length; ++i) { + address provider = providers[i]; + bool isRegistered = providerRegistered[provider]; + bool hasStake = providerStakes[provider] >= minStake; + bool noPendingWithdrawal = withdrawalRequests[provider] == 0; + bool hasBLSKey = eoaToBlsPubkeys[provider].length > 0; + validProviders[i] = isRegistered && hasStake && noPendingWithdrawal && hasBLSKey; + } + return validProviders; + } + + /** + * @dev Verifies a BLS signature using the precompile + * @param pubKey The public key (48 bytes G1 point) + * @param message The message hash (32 bytes) + * @param signature The signature (96 bytes G2 point) + * @return success True if verification succeeded + */ + function verifySignature( + bytes calldata pubKey, + bytes32 message, + bytes calldata signature + ) public view returns (bool) { + // Input validation + require(pubKey.length == 48, "Public key must be 48 bytes"); + require(signature.length == 96, "Signature must be 96 bytes"); + + // Concatenate inputs in required format: + // [pubkey (48 bytes) | message (32 bytes) | signature (96 bytes)] + bytes memory input = bytes.concat(pubKey, message, signature); + + // Call precompile + (bool success, bytes memory result) = address(0xf0).staticcall(input); + + // Check if call was successful + if (!success) { + return false; + } + + // If we got a result back and it's not empty, verification succeeded + return result.length > 0; + } + + function _stake(address provider) internal { + require(providerRegistered[provider], ProviderNotRegistered(provider)); + require( + withdrawalRequests[provider] == 0, + PendingWithdrawalRequest(provider) + ); + providerStakes[provider] += msg.value; + emit FundsDeposited(provider, msg.value); + } + + function _registerAndStake(address provider) internal { + require( + !providerRegistered[provider], + ProviderAlreadyRegistered(provider) + ); + require(msg.value >= minStake, InsufficientStake(msg.value, minStake)); + + providerStakes[provider] = msg.value; + providerRegistered[provider] = true; + emit ProviderRegistered(provider, msg.value); + } + + // solhint-disable-next-line no-empty-blocks + function _authorizeUpgrade(address) internal override onlyOwner {} +} diff --git a/contracts/contracts/interfaces/IProviderRegistryV2.sol b/contracts/contracts/interfaces/IProviderRegistryV2.sol new file mode 100644 index 000000000..0fa188a0e --- /dev/null +++ b/contracts/contracts/interfaces/IProviderRegistryV2.sol @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: BSL 1.1 +pragma solidity 0.8.26; + +interface IProviderRegistryV2 { + + /// @dev Event emitted when a provider is registered + event ProviderRegistered(address indexed provider, uint256 stakedAmount); + + /// @dev Event emitted when funds are deposited + event FundsDeposited(address indexed provider, uint256 amount); + + /// @dev Event emitted when funds are slashed + event FundsSlashed(address indexed provider, uint256 amount); + + /// @dev Event emitted when withdrawal is requested + event Unstake(address indexed provider, uint256 timestamp); + + /// @dev Event emitted when withdrawal is completed + event Withdraw(address indexed provider, uint256 amount); + + /// @dev Event emitted when the withdrawal delay is updated + event WithdrawalDelayUpdated(uint256 newWithdrawalDelay); + + /// @dev Event emitted when the penalty fee recipient is updated + event PenaltyFeeRecipientUpdated(address indexed newPenaltyFeeRecipient); + + /// @dev Event emitted when the fee payout period is updated + event FeePayoutPeriodUpdated(uint256 indexed newFeePayoutPeriod); + + /// @dev Event emitted when the min stake is updated + event MinStakeUpdated(uint256 indexed newMinStake); + + /// @dev Event emitted when the preconf manager is updated + event PreconfManagerUpdated(address indexed newPreconfManager); + + /// @dev Event emitted when the fee percent is updated + event FeePercentUpdated(uint256 indexed newFeePercent); + + /// @dev Event emitted when a BLS key is added + event BLSKeyAdded(address indexed provider, bytes blsPublicKey); + + /// @dev Event emitted when a BLS key is removed + event BLSKeyRemoved(address indexed provider, bytes blsPublicKey); + + /// @dev Event emitted when there are insufficient funds to slash + event InsufficientFundsToSlash( + address indexed provider, + uint256 providerStake, + uint256 penaltyFee, + uint256 bidderPortion + ); + + /// @dev Event emitted when transfer to bidder fails + event TransferToBidderFailed(address bidder, uint256 amount); + + /// @dev Event emitted when a bidder withdraws slashed amount + /// in case of transfer failure + event BidderWithdrawSlashedAmount(address bidder, uint256 amount); + + error NotPreconfContract(address sender, address preconfManager); + error NoStakeToWithdraw(address sender); + error UnstakeRequestExists(address sender); + error NoUnstakeRequest(address sender); + error DelayNotPassed(uint256 withdrawalRequestTimestamp, uint256 withdrawalDelay, uint256 currentBlockTimestamp); + error PreconfManagerNotSet(); + error ProviderCommitmentsPending(address sender, uint256 numPending); + error StakeTransferFailed(address sender, uint256 amount); + error ProviderAlreadyRegistered(address sender); + error InsufficientStake(uint256 stake, uint256 minStake); + error InvalidBLSPublicKeyLength(uint256 length, uint256 expectedLength); + error ProviderNotRegistered(address sender); + error BLSKeyAlreadyExists(bytes blsPublicKey); + error BLSKeyDoesNotExist(bytes blsPublicKey); + error AtLeastOneBLSKeyRequired(); + error PendingWithdrawalRequest(address sender); + error BidderAmountIsZero(address sender); + error BidderWithdrawalTransferFailed(address sender, uint256 amount); + error PublicKeyLengthInvalid(uint256 exp, uint256 got); + error SignatureLengthInvalid(uint256 exp, uint256 got); + error BLSSignatureInvalid(); + + function registerAndStake() external payable; + + function stake() external payable; + + function slash( + uint256 slashAmt, + address provider, + address payable bidder + ) external; + + function addVerifiedBLSKey(bytes calldata blsPublicKey, bytes calldata signature) external; + + function overrideAddBLSKey(address provider, bytes calldata blsPublicKey) external; + + function isProviderValid(address committerAddress) external view; + + function getEoaFromBLSKey(bytes calldata blsKey) external view returns (address); +} diff --git a/contracts/scripts/core/UpgradeProviderRegistry.s.sol b/contracts/scripts/core/UpgradeProviderRegistry.s.sol new file mode 100644 index 000000000..f3907aab2 --- /dev/null +++ b/contracts/scripts/core/UpgradeProviderRegistry.s.sol @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: BSL 1.1 + +// solhint-disable no-console +// solhint-disable one-contract-per-file + +pragma solidity 0.8.26; + +import {Script} from "forge-std/Script.sol"; +import {Upgrades} from "openzeppelin-foundry-upgrades/Upgrades.sol"; +import {ProviderRegistry} from "../../contracts/core/ProviderRegistry.sol"; +import {ProviderRegistryV2} from "../../contracts/core/ProviderRegistryV2.sol"; +import {console} from "forge-std/console.sol"; + +contract UpgradeProviderRegistry is Script { + + function run() external { + vm.startBroadcast(); + address proxyAddress = vm.envAddress("PROVIDER_REGISTRY_PROXY"); + console.log("Upgrading ProviderRegistry proxy at:", proxyAddress); + Upgrades.upgradeProxy(proxyAddress, "ProviderRegistryV2.sol", ""); + console.log("ProviderRegistry successfully upgraded to:", "ProviderRegistryV2.sol"); + vm.stopBroadcast(); + } +} + +contract ResetPubkeys is Script { + + function run() external { + vm.startBroadcast(); + address proxyAddress = vm.envAddress("PROVIDER_REGISTRY_PROXY"); + ProviderRegistryV2 providerRegistry = ProviderRegistryV2(payable(proxyAddress)); + + bytes[] memory pubkeys = new bytes[](33); + pubkeys[0] = hex"910f20173e75cc036eb232cf2081043d62c028c3ddae53d5cccdbdd1213a9830c5143c431ee9e401a4d3432f3c23d18c"; + pubkeys[1] = hex"a9d0a0f9059972d775a45d8377768ff20234de91a6fbacba5737ff8803807c38021b5863e5084869e695ef1d6c2fdaef"; + pubkeys[2] = hex"b26f96664274e15fb6fcda862302e47de7e0e2a6687f8349327a9846043e42596ec44af676126e2cacbdd181f548e681"; + pubkeys[3] = hex"a94a5107948363d29e6a7c476f7e2665eaa27d3d92dcba4c68a66de71c07d1286e2755af80150a3f01e39c1fe69c4ac4"; + pubkeys[4] = hex"8216e00e1dc8e15c362ce8083ad01feeb04688dd3a18998a37db1c3c8b641372398c504e1aca2cbddd87c4075482b42f"; + pubkeys[5] = hex"b4a435cf816291596fe2e405651ec8b6c80b9cc34dace3c83202ca489a833756c9a0672ebdc17f23d9d43163db1caa5d"; + pubkeys[6] = hex"95c8cc31f8d4e54eddb0603b8f12d59d466f656f374bde2073e321bdd16082d420e3eef4d62467a7ea6b83818381f742"; + pubkeys[7] = hex"b67eaa5efcfa1d17319c344e1e5167811afbfe7922a2cf01c9a361f465597a5dc3a5472bd98843bac88d2541a78eab08"; + pubkeys[8] = hex"946fcf348bbf1044a3eaa3d27f1d01397cfc4d27495a949447c95ea7ec41db9f5c4fe3b867a937623685a0462996df09"; + pubkeys[9] = hex"b0b0de6ba411630193eed5450f82a76d66501e9838fe5fbe98d4873b5de677b3a20a360302fbe094adae63bc63e4ded4"; + pubkeys[10] = hex"b9ce2753254979122cf137288efe471791893a9cf6abcd192f33515f6ca778507a51bcab84542562efc244001a7b4c55"; + pubkeys[11] = hex"8a85062118e29045e8b199723d9af519b0c071d9e7586fa89733e798536d4637f8545a169012cc1b76005d9453603273"; + pubkeys[12] = hex"8527d16cf01edcea2cbb05e27f5f61b184578ea27c4015e4533e4cebd6d53297bd004c14fe3f247a468361bf781e0069"; + pubkeys[13] = hex"b47963246adef02cd3e61cbb648c04fd99b05e28a616aef3aa7fb688c17b10d1ce9662b61a600efbdd110e93d62d5144"; + pubkeys[14] = hex"8898a80ec199dbe15a57e3ceb51389ded413d6a2ebaaac0330af7effcdd33bba70318339ec9cbc1253b30d463c4999c4"; + pubkeys[15] = hex"807a81a9873359323966feb80fcc52b6049888f885d58134bf52bf9825f89aabc723bb5bacd1d8f4dea6322a6d166535"; + pubkeys[16] = hex"88857150299287cedfbadea1ee3fb7ac121f1e4e16bef44b4a7bad35432973c4009efb90394facca3fdc0759ba70f93f"; + pubkeys[17] = hex"A32AADB23E45595FE4981114A8230128443FD5407D557DC0C158AB93BC2B88939B5A87A84B6863B0D04A4B5A2447F847"; + pubkeys[18] = hex"AE2FFC6986C9A368C5AD2D51F86DB2031D780F6AC9B2348044DEA4E3A75808B566C935099DE8B1A1609DB322F2110E7A"; + pubkeys[19] = hex"8509ECB595DA0EDA2C6FCED4E287F0510A2C2DBA5F80EE930503EF86E268D808A6DF25E397177DA06CD479771CE66840"; + pubkeys[20] = hex"94829E6F7A598A2F2DFDD9E1246D7CFDC30A626666D9419F3C147CC954507E97184C598DC109F4D05C2139C48AF6746C"; + pubkeys[21] = hex"A3523967A7955C0244910F23B7B1FC59636F03BEC437286B622815408D51389F7F6CD54617733B93926B7860E1F6AFB0"; + pubkeys[22] = hex"8226FB149BFE7B4967FFE82ECB9084FFD5BBF0303DE0B88F68FDD8297CDFFE80F611FA27BC05506B4FBA12E2EB5BC5A5"; + pubkeys[23] = hex"AF10542267816E91ADBC8F4A6754765D492534F8325F34A2E89CAA2BA45C7158F6DEAA6E7FB454EBB6F6A1495FE63DBA"; + pubkeys[24] = hex"94A076B27F294DC44B9FD44D8E2B063FB129BC85ED047DA1CEFB82D16E1A13E6B50DE31A86F5B233D1E6BBACA3C69173"; + pubkeys[25] = hex"8B39E8F6AD0A7D2C9E893459D76AC1BC7884D5343324F7639CC883590E8914C2EDB59C3751E4B4C31D466BAEC718D440"; + pubkeys[26] = hex"B255D270445AC3A52A1A97D0D8547EEEE526D649E172663438B621FF9BE4212EEAA425002AF64E4685083E871B0BD7C6"; + pubkeys[27] = hex"A0383AAD83FA40C02CEBBC89EA396AA8545A152E60B558780173CE2B81CED85D8A3858F83AE99CBCA50DDA43CCBEACA9"; + pubkeys[28] = hex"A20E2D356EEA696F4F5F8A02D8865CB3FF287A04765978D399A677198BF2A806B42DC8DBB5B5703D8238A76A6EEB6F6D"; + pubkeys[29] = hex"88A53EC4422F50238DEF5696446E06ACD076F94D73C76E51449CF82EB5312DCB8A845A6C3199CE35DE3F2B0441DEB76B"; + pubkeys[30] = hex"B435DD63A14675EA11E4EACA6BD640C70E68843CF4C8BFE3BBAF3A7EBEDC3FC53D80050E58748EC3D088A15113C6D4C6"; + pubkeys[31] = hex"947DE8EEC641942BEE15F2754F825E24A00DDD135837C5EAAD4921BD1CF18F9EBC374E1194880676DC19CB463DCA842E"; + pubkeys[32] = hex"A7A713E0275E888A06B4A95B5C0B16557CF25FE431269F54BC898287081DC4A25110EDBAFA643EA1B86C4319C8D9977E"; + + for (uint256 i = 0; i < pubkeys.length; i++) { + providerRegistry.overrideRemoveBLSKey(address(this), pubkeys[i]); + } + for (uint256 i = 0; i < pubkeys.length; i++) { + providerRegistry.overrideAddBLSKey(address(this), pubkeys[i]); + } + vm.stopBroadcast(); + } +} diff --git a/contracts/test/core/ProviderRegistryTest.sol b/contracts/test/core/ProviderRegistryTest.sol index 6933a4553..b7f5be3a2 100644 --- a/contracts/test/core/ProviderRegistryTest.sol +++ b/contracts/test/core/ProviderRegistryTest.sol @@ -10,10 +10,11 @@ import {Upgrades} from "openzeppelin-foundry-upgrades/Upgrades.sol"; import {IProviderRegistry} from "../../contracts/interfaces/IProviderRegistry.sol"; import {MockBLSVerify} from "../precompiles/BLSVerifyPreCompileMockTest.sol"; import {DepositManager} from "../../contracts/core/DepositManager.sol"; +import {ProviderRegistryV2} from "../../contracts/core/ProviderRegistryV2.sol"; contract ProviderRegistryTest is Test { uint256 public testNumber; - ProviderRegistry public providerRegistry; + ProviderRegistryV2 public providerRegistry; uint256 public feePercent; uint256 public minStake; address public provider; @@ -44,6 +45,8 @@ contract ProviderRegistryTest is Test { ); event FundsSlashed(address indexed provider, uint256 totalSlash); event TransferToBidderFailed(address indexed bidder, uint256 amount); + event BLSKeyAdded(address indexed provider, bytes blsPublicKey); + event BLSKeyRemoved(address indexed provider, bytes blsPublicKey); function setUp() public { address BLS_VERIFY_ADDRESS = address(0xf0); @@ -71,7 +74,9 @@ contract ProviderRegistryTest is Test { ) ) ); - providerRegistry = ProviderRegistry(payable(providerRegistryProxy)); + + Upgrades.upgradeProxy(providerRegistryProxy, "ProviderRegistryV2.sol", ""); + providerRegistry = ProviderRegistryV2(payable(providerRegistryProxy)); address blockTrackerProxy = Upgrades.deployUUPSProxy( "BlockTracker.sol", @@ -867,4 +872,108 @@ contract ProviderRegistryTest is Test { "Penalty fee should be 0.05 ether" ); } + + function test_OverrideAddBLSKey() public { + address testProvider = vm.addr(420); + vm.deal(testProvider, 3 ether); + vm.prank(testProvider); + providerRegistry.registerAndStake{value: 2 ether}(); + + bytes memory newBLSKey = hex"90000cddeec66a800e00b0ccbb62f12298073603f5209e812abbac7e870482e488dd1bbe533a9d44497ba8b756e1e82b"; + + vm.prank(address(this)); + vm.expectEmit(true, true, true, true); + emit BLSKeyAdded(testProvider, newBLSKey); + providerRegistry.overrideAddBLSKey(testProvider, newBLSKey); + + bytes[] memory storedBLSKeys = providerRegistry.getBLSKeys(testProvider); + assertEq(storedBLSKeys.length, 1, "Should have 1 BLS key"); + assertEq(storedBLSKeys[0], newBLSKey, "BLS key should match"); + + address mappedProvider = providerRegistry.getEoaFromBLSKey(newBLSKey); + assertEq(mappedProvider, testProvider, "BLS key should map to correct provider"); + } + + function test_RevertWhen_OverrideAddBLSKey_NotOwner() public { + address testProvider = vm.addr(421); + vm.deal(testProvider, 3 ether); + vm.prank(testProvider); + providerRegistry.registerAndStake{value: 2 ether}(); + + bytes memory newBLSKey = hex"90000cddeec66a800e00b0ccbb62f12298073603f5209e812abbac7e870482e488dd1bbe533a9d44497ba8b756e1e82b"; + + vm.prank(testProvider); + vm.expectRevert(); // Not owner + providerRegistry.overrideAddBLSKey(testProvider, newBLSKey); + } + + function test_RevertWhen_OverrideAddBLSKey_ProviderNotRegistered() public { + address unregisteredProvider = vm.addr(422); + bytes memory newBLSKey = hex"90000cddeec66a800e00b0ccbb62f12298073603f5209e812abbac7e870482e488dd1bbe533a9d44497ba8b756e1e82b"; + + vm.prank(address(this)); + vm.expectRevert(abi.encodeWithSelector(IProviderRegistry.ProviderNotRegistered.selector, unregisteredProvider)); + providerRegistry.overrideAddBLSKey(unregisteredProvider, newBLSKey); + } + + function test_OverrideAddBLSKey_MultipleKeys() public { + address testProvider = vm.addr(423); + vm.deal(testProvider, 3 ether); + vm.prank(testProvider); + vm.expectEmit(true, true, true, true); + emit ProviderRegistered(testProvider, 2 ether); + providerRegistry.registerAndStake{value: 2 ether}(); + + bytes memory firstBLSKey = hex"90000cddeec66a800e00b0ccbb62f12298073603f5209e812abbac7e870482e488dd1bbe533a9d44497ba8b756e1e82b"; + + vm.prank(address(this)); + vm.expectEmit(true, true, true, true); + emit BLSKeyAdded(testProvider, firstBLSKey); + providerRegistry.overrideAddBLSKey(testProvider, firstBLSKey); + + bytes memory secondBLSKey = hex"a0000cddeec66a800e00b0ccbb62f12298073603f5209e812abbac7e870482e488dd1bbe533a9d44497ba8b756e1e82b"; + + vm.prank(address(this)); + vm.expectEmit(true, true, true, true); + emit BLSKeyAdded(testProvider, secondBLSKey); + providerRegistry.overrideAddBLSKey(testProvider, secondBLSKey); + + bytes[] memory storedBLSKeys = providerRegistry.getBLSKeys(testProvider); + assertEq(storedBLSKeys.length, 2, "Should have 2 BLS keys"); + + assertEq(providerRegistry.getEoaFromBLSKey(firstBLSKey), testProvider, "First BLS key should map to provider"); + assertEq(providerRegistry.getEoaFromBLSKey(secondBLSKey), testProvider, "Second BLS key should map to provider"); + } + + function test_OverrideRemoveBLSKey_MultipleKeys() public { + test_OverrideAddBLSKey_MultipleKeys(); + + address testProvider = vm.addr(423); + bytes memory firstBLSKey = hex"90000cddeec66a800e00b0ccbb62f12298073603f5209e812abbac7e870482e488dd1bbe533a9d44497ba8b756e1e82b"; + bytes memory secondBLSKey = hex"a0000cddeec66a800e00b0ccbb62f12298073603f5209e812abbac7e870482e488dd1bbe533a9d44497ba8b756e1e82b"; + + bytes[] memory storedBLSKeys = providerRegistry.getBLSKeys(testProvider); + assertEq(storedBLSKeys.length, 2, "Should have 2 BLS keys before removal"); + + vm.expectEmit(true, true, true, true); + emit BLSKeyRemoved(testProvider, secondBLSKey); + vm.prank(address(this)); + providerRegistry.overrideRemoveBLSKey(testProvider, secondBLSKey); + + storedBLSKeys = providerRegistry.getBLSKeys(testProvider); + assertEq(storedBLSKeys.length, 1, "Should have 1 BLS key after removing one"); + assertEq(providerRegistry.getEoaFromBLSKey(secondBLSKey), address(0), "Removed BLS key should not map to any provider"); + + assertEq(providerRegistry.getEoaFromBLSKey(firstBLSKey), testProvider, "First BLS key should still map to provider"); + + vm.expectEmit(true, true, true, true); + emit BLSKeyRemoved(testProvider, firstBLSKey); + vm.prank(address(this)); + providerRegistry.overrideRemoveBLSKey(testProvider, firstBLSKey); + + storedBLSKeys = providerRegistry.getBLSKeys(testProvider); + assertEq(storedBLSKeys.length, 0, "Should have 0 BLS keys after removing both"); + assertEq(providerRegistry.getEoaFromBLSKey(firstBLSKey), address(0), "Removed BLS key should not map to any provider"); + assertEq(providerRegistry.getEoaFromBLSKey(secondBLSKey), address(0), "Removed BLS key should not map to any provider"); + } }