|
| 1 | +// SPDX-License-Identifier: BSL 1.1 |
| 2 | +pragma solidity 0.8.28; |
| 3 | + |
| 4 | +import {MevCommitBappStorage} from "./MevCommitBappStorage.sol"; |
| 5 | +import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; |
| 6 | +import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; |
| 7 | +import {OwnableUpgradeableBasedApp} from "ssv/src/middleware/modules/roles/OwnableUpgradeableBasedApp.sol"; |
| 8 | +import {TimestampOccurrence} from "../../utils/Occurrence.sol"; |
| 9 | + |
| 10 | +contract MevCommitBapp is MevCommitBappStorage, UUPSUpgradeable, PausableUpgradeable, OwnableUpgradeableBasedApp { |
| 11 | + |
| 12 | + event ValidatorRegistered(bytes indexed pubkey, address indexed registrar); |
| 13 | + event ValidatorDeregistrationRequested(bytes indexed pubkey, address indexed deregistrar); |
| 14 | + event ValidatorDeregistered(bytes indexed pubkey, address indexed deregistrar); |
| 15 | + event ValidatorFrozen(bytes indexed pubkey, address indexed freezer); |
| 16 | + event ValidatorUnfrozen(bytes indexed pubkey, address indexed unfreezer); |
| 17 | + |
| 18 | + error AlreadyWhitelisted(); |
| 19 | + error ZeroAddress(); |
| 20 | + error NotWhitelisted(); |
| 21 | + error NonWhitelistedCaller(); |
| 22 | + error NotOptedInCaller(); |
| 23 | + error ValidatorAlreadyRegistered(bytes pubkey); |
| 24 | + error ValidatorNotRegistered(bytes pubkey); |
| 25 | + error ValidatorAlreadyRequestedDeregistration(bytes pubkey); |
| 26 | + error ValidatorNotFrozen(bytes pubkey); |
| 27 | + error UnfreezeTooSoon(); |
| 28 | + error UnfreezeTransferFailed(); |
| 29 | + error RefundFailed(); |
| 30 | + error UnfreezeFeeRequired(uint256 requiredFee); |
| 31 | + |
| 32 | + // TODO: Need to address constructor pattern mismatch |
| 33 | + constructor(address _basedAppManager, address owner) OwnableUpgradeableBasedApp(_basedAppManager, owner) { |
| 34 | + _disableInitializers(); |
| 35 | + } |
| 36 | + function initialize(uint256 _unfreezePeriod, uint256 _unfreezeFee, address _unfreezeReceiver) public initializer { |
| 37 | + unfreezePeriod = _unfreezePeriod; |
| 38 | + unfreezeFee = _unfreezeFee; |
| 39 | + unfreezeReceiver = _unfreezeReceiver; |
| 40 | + __UUPSUpgradeable_init(); |
| 41 | + __Pausable_init(); |
| 42 | + } |
| 43 | + |
| 44 | + function addWhitelisted(address account) external onlyOwner { |
| 45 | + if (isWhitelisted[account]) revert AlreadyWhitelisted(); |
| 46 | + if (account == address(0)) revert ZeroAddress(); |
| 47 | + isWhitelisted[account] = true; |
| 48 | + } |
| 49 | + |
| 50 | + function removeWhitelisted(address account) external onlyOwner { |
| 51 | + if (!isWhitelisted[account]) revert NotWhitelisted(); |
| 52 | + delete isWhitelisted[account]; |
| 53 | + } |
| 54 | + |
| 55 | + function pause() external onlyOwner { |
| 56 | + _pause(); |
| 57 | + } |
| 58 | + function unpause() external onlyOwner { |
| 59 | + _unpause(); |
| 60 | + } |
| 61 | + |
| 62 | + function optInToBApp( |
| 63 | + uint32, /*strategyId*/ |
| 64 | + address[] calldata, /*tokens*/ |
| 65 | + uint32[] calldata, /*obligationPercentages*/ |
| 66 | + bytes calldata /*data*/ |
| 67 | + ) external override onlySSVBasedAppManager whenNotPaused returns (bool success) { |
| 68 | + if (!isWhitelisted[msg.sender]) revert NonWhitelistedCaller(); |
| 69 | + isOptedIn[msg.sender] = true; |
| 70 | + // Before slashing is enabled, strategies are irrelevant to mev-commit. |
| 71 | + return true; |
| 72 | + } |
| 73 | + |
| 74 | + /// After opting-in, EOAs must register L1 validator pubkeys that attest to the rules of mev-commit. |
| 75 | + function registerValidatorPubkeys(bytes[] calldata pubkeys) external whenNotPaused { |
| 76 | + if (!isWhitelisted[msg.sender]) revert NonWhitelistedCaller(); |
| 77 | + if (!isOptedIn[msg.sender]) revert NotOptedInCaller(); |
| 78 | + for (uint256 i = 0; i < pubkeys.length; i++) { |
| 79 | + bytes calldata pubkey = pubkeys[i]; |
| 80 | + if (validatorRecords[pubkey].exists) revert ValidatorAlreadyRegistered(pubkey); |
| 81 | + validatorRecords[pubkey] = ValidatorRecord({ |
| 82 | + exists: true, |
| 83 | + registrar: msg.sender, |
| 84 | + freezeOccurrence: TimestampOccurrence.Occurrence({ |
| 85 | + exists: false, |
| 86 | + timestamp: 0 |
| 87 | + }), |
| 88 | + deregRequestOccurrence: TimestampOccurrence.Occurrence({ |
| 89 | + exists: false, |
| 90 | + timestamp: 0 |
| 91 | + }) |
| 92 | + }); |
| 93 | + emit ValidatorRegistered(pubkey, msg.sender); |
| 94 | + } |
| 95 | + } |
| 96 | + |
| 97 | + function requestDeregistrations(bytes[] calldata pubkeys) external whenNotPaused { |
| 98 | + if (!isWhitelisted[msg.sender]) revert NonWhitelistedCaller(); |
| 99 | + if (!isOptedIn[msg.sender]) revert NotOptedInCaller(); |
| 100 | + for (uint256 i = 0; i < pubkeys.length; i++) { |
| 101 | + bytes calldata pubkey = pubkeys[i]; |
| 102 | + if (!validatorRecords[pubkey].exists) revert ValidatorNotRegistered(pubkey); |
| 103 | + if (validatorRecords[pubkey].deregRequestOccurrence.exists) revert ValidatorAlreadyRequestedDeregistration(pubkey); |
| 104 | + TimestampOccurrence.captureOccurrence(validatorRecords[pubkey].deregRequestOccurrence); |
| 105 | + emit ValidatorDeregistrationRequested(pubkey, msg.sender); |
| 106 | + } |
| 107 | + } |
| 108 | + |
| 109 | + function deregisterValidators(bytes[] calldata pubkeys) external whenNotPaused { |
| 110 | + if (!isWhitelisted[msg.sender]) revert NonWhitelistedCaller(); |
| 111 | + if (!isOptedIn[msg.sender]) revert NotOptedInCaller(); |
| 112 | + for (uint256 i = 0; i < pubkeys.length; i++) { |
| 113 | + bytes calldata pubkey = pubkeys[i]; |
| 114 | + delete validatorRecords[pubkey]; |
| 115 | + emit ValidatorDeregistered(pubkey, msg.sender); |
| 116 | + } |
| 117 | + } |
| 118 | + |
| 119 | + function freezeValidators(bytes[] calldata pubkeys) external onlyOwner { |
| 120 | + for (uint256 i = 0; i < pubkeys.length; i++) { |
| 121 | + bytes calldata pubkey = pubkeys[i]; |
| 122 | + if (!validatorRecords[pubkey].exists) revert ValidatorNotRegistered(pubkey); |
| 123 | + TimestampOccurrence.captureOccurrence(validatorRecords[pubkey].freezeOccurrence); |
| 124 | + emit ValidatorFrozen(pubkey, msg.sender); |
| 125 | + } |
| 126 | + } |
| 127 | + |
| 128 | + function unfreeze(bytes[] calldata valPubKeys) external payable whenNotPaused() { |
| 129 | + uint256 requiredFee = unfreezeFee * valPubKeys.length; |
| 130 | + require(msg.value >= requiredFee, UnfreezeFeeRequired(requiredFee)); |
| 131 | + uint256 len = valPubKeys.length; |
| 132 | + for (uint256 i = 0; i < len; ++i) { |
| 133 | + bytes calldata pubkey = valPubKeys[i]; |
| 134 | + ValidatorRecord storage record = validatorRecords[pubkey]; |
| 135 | + require(record.exists, ValidatorNotRegistered(pubkey)); |
| 136 | + require(record.freezeOccurrence.exists, ValidatorNotFrozen(pubkey)); |
| 137 | + require(block.timestamp > record.freezeOccurrence.timestamp + unfreezePeriod, UnfreezeTooSoon()); |
| 138 | + TimestampOccurrence.del(record.freezeOccurrence); |
| 139 | + emit ValidatorUnfrozen(pubkey, record.registrar); |
| 140 | + } |
| 141 | + (bool success, ) = unfreezeReceiver.call{value: requiredFee}(""); |
| 142 | + require(success, UnfreezeTransferFailed()); |
| 143 | + uint256 excessFee = msg.value - requiredFee; |
| 144 | + if (excessFee != 0) { |
| 145 | + (bool successRefund, ) = msg.sender.call{value: excessFee}(""); |
| 146 | + require(successRefund, RefundFailed()); |
| 147 | + } |
| 148 | + } |
| 149 | + |
| 150 | + function isValidatorOptedIn(bytes calldata pubkey) public view returns (bool) { |
| 151 | + ValidatorRecord storage record = validatorRecords[pubkey]; |
| 152 | + if (!record.exists) return false; |
| 153 | + if (!isWhitelisted[record.registrar]) return false; |
| 154 | + if (!isOptedIn[record.registrar]) return false; |
| 155 | + if (record.freezeOccurrence.exists) return false; |
| 156 | + if (record.deregRequestOccurrence.exists) return false; |
| 157 | + return true; |
| 158 | + } |
| 159 | + |
| 160 | + function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} |
| 161 | +} |
0 commit comments