diff --git a/l1-contracts/docs/registration.md b/l1-contracts/docs/registration.md index a262a3d..eb8c9b3 100644 --- a/l1-contracts/docs/registration.md +++ b/l1-contracts/docs/registration.md @@ -152,22 +152,73 @@ sequenceDiagram ### Validator Registration Process Explanation -1. The `Operator` calls `registerValidators()` on the `UniFiAVSManager`, providing the `podOwner` address and an array of BLS public key hashes for the validators to be registered. +There are two primary paths for validator registration and incorporates robust slashing mechanisms to maintain the integrity of the network. This document explores these paths and mechanisms in detail, highlighting key nuances such as registration delays and slashing conditions. -2. The `UniFiAVSManager` checks if the operator is registered with the AVS using the `AVSDirectory`. +## Validator Registration Path 1: Registering Validators with EigenPod -3. The `UniFiAVSManager` verifies that the operator has set an OperatorCommitment. +The `registerValidators` function is used for validators associated with an EigenPod. This method ensures that validators are actively managed and verified through the EigenLayers ecosystem. -4. For each BLS public key hash in provided: - - The `UniFiAVSManager` retrieves the validator information from the `EigenPod`. - - It checks if the validator is active in the EigenPod. - - It verifies that the validator is not already registered in the UniFi AVS. - - If all checks pass, it registers the validator, associating it with the operator and storing relevant information. +```solidity +function registerValidators(address podOwner, bytes32[] calldata blsPubKeyHashes) external; +``` +Parameters: +- `podOwner`: The address of the pod owner. +- `blsPubKeyHashes`: An array of BLS public key hashes for the validators to be registered. + +Example: + +```solidity + bytes32[] memory pubKeyHashes = [0x1234..., 0x5678...]; + uniFiAVSManager.registerValidators(podOwner, pubKeyHashes); +``` +Process: +The function requires the caller to be the operator delegated to the pod owner, ensuring proper authorization. It also verifies that the operator is registered in the AVS (Active Validator Set) and checks that the validators are active and not already registered. Once these conditions are met, validators indefinitely. + +## Validator Registration Path 2: Optimistic Registration for Independent Validators + +This function is designed for independent validators, allowing for an optimistic registration process that prioritizes gas efficiency. + +```solidity +function registerValidatorsOptimistically(ValidatorRegistrationParams[] calldata validators) external; +``` + +Parameters: +- `validators`: An array of `ValidatorRegistrationParams` structs, each containing the necessary data for registration. + +Example: + +```solidity + ValidatorRegistrationParams[] memory validators = new ValidatorRegistrationParams[](1); + validators[0] = ValidatorRegistrationParams({ + blsPubKeyHash: 0x1234..., + index: 1, + salt: 123456, + expiry: block.timestamp + 1 days, + registrationSignature: BN254.G1Point({X: 0x5678..., Y: 0x9abc...}) + }); + + uniFiAVSManager.registerValidatorsOptimistically(validators); +``` + +Process: +Validators are registered without immediate validation checks, assuming validity until proven otherwise. A key feature of this method is the registration delay, which introduces a window before validators become active. This delay allows time for potential slashing of invalid registrations, enhancing network security. + +A critical feature of the optimistic registration process is the registration delay. This delay serves as a safeguard, allowing time for the network to identify and slash invalid validators before they become active. During this period, slashers can verify the validity of the registration and take action if necessary. This mechanism significantly enhances network security by preventing invalid validators from participating in the network. -5. The `UniFiAVSManager` updates the operator's validator count and resets their deregistration state if they had previously queued to deregister their operator. +To generate the `registrationSignature`, the validator needs to sign the BLS message hash using their BLS key containing the `operator`, `index`, `salt`, and `expiry`. -These checks will ensure that a validator can only be registered exactly once in the AVS, and that it can only be to the Operator whom the validator's podOwner is delegated to. +The mssage hash that needs to be signed can be fetched using the following view function: + +```solidity +function blsMessageHash(address operator, uint256 salt, uint256 expiry, uint256 index) + public + view + returns (BN254.G1Point memory) +``` +If you want to generate the hash yourself, you can use the following typehash: +`VALIDATOR_REGISTRATION_TYPEHASH = + keccak256("BN254ValidatorRegistration(address operator,bytes32 salt,uint256 expiry,uint64 index)");` # Deregistering from UniFi AVS diff --git a/l1-contracts/docs/slashing.md b/l1-contracts/docs/slashing.md index 9544688..01d5c9c 100644 --- a/l1-contracts/docs/slashing.md +++ b/l1-contracts/docs/slashing.md @@ -1,11 +1,87 @@ # Slashing Mechanism -The slashing mechanism in UniFi AVS is designed to ensure the integrity of the pre-confirmation process. It consists of two main cases: +The slashing mechanism in UniFi AVS is designed to ensure the integrity of the pre-confirmation process. It consists of three main cases: -1. Safety Faults (Breaking Pre-confirmation Promises) -2. Liveness Faults (Missed Block Slashing) +1. Invalid Validator Registration +2. Safety Faults (Breaking Pre-confirmation Promises) +3. Liveness Faults (Missed Block Slashing) -## Safety Faults +## Invalid Validator Registration +To maintain the integrity of the network, the UniFiAVSManager contract includes several mechanisms to slash operators who register invalid validators. Slashing acts as a deterrent against fraudulent or incorrect registrations. + +### Slashing Validators with Invalid Registration Signatures + +```solidity +function slashValidatorsWithInvalidSignature(ValidatorRegistrationSlashingParams[] calldata validators) external; +``` +Parameters: +- `validators`: An array of `ValidatorRegistrationSlashingParams` structs, each containing the necessary data for slashing. + +Example: +```solidity + ValidatorRegistrationSlashingParams[] memory validators = new ValidatorRegistrationSlashingParams[](1); + validators[0] = ValidatorRegistrationSlashingParams({ + pubkeyG1: BN254.G1Point({X: 0x1234..., Y: 0x5678...}), + pubkeyG2: BN254.G2Point({X: [0x9abc..., 0xdef0...], Y: [0x1234..., 0x5678...]}), + registrationSignature: BN254.G1Point({X: 0x9abc..., Y: 0xdef0...}), + expiry: block.timestamp + 1 days, + salt: 123456, + index: 1 // index of the validator + }); + + uniFiAVSManager.slashValidatorsWithInvalidSignature(validators); +``` + +Mechanism: +It checks the validity of the registration signature using BLS signature verification. If the signature is found to be invalid, the validator is slashed, and the operator is penalized. This mechanism maintains the authenticity of registrations, ensuring that only legitimate validators are part of the network. + +### Slashing Validators with Invalid Index + +```solidity +function slashValidatorsWithInvalidIndex(BeaconChainHelperLib.InclusionProof[] calldata proofs) external; +``` +Parameters: +- `proofs`: An array of `BeaconChainHelperLib.InclusionProof` structs, each containing the necessary data for slashing. + +Example: + +```solidity + BeaconChainHelperLib.InclusionProof[] memory proofs = new BeaconChainHelperLib.InclusionProof[](1); + proofs[0] = BeaconChainHelperLib.InclusionProof({ + validator: [0x1234...], + validatorIndex: 1, + // Additional proof data... + }); + + uniFiAVSManager.slashValidatorsWithInvalidIndex(proofs); +``` + +Mechanism: +This function verifies the validator's index against the provided proof. If the index does not match, the validator is slashed. This mechanism prevents the misuse of validator indices, ensuring that each index is unique and correctly assigned. + +### Slashing Validators with Invalid Public Key + +```solidity +function slashValidatorsWithInvalidPubkey(BeaconChainHelperLib.InclusionProof[] calldata proofs) external; +``` +Parameters: +- `proofs`: An array of `BeaconChainHelperLib.InclusionProof` structs, each containing the necessary data for slashing. + +Example: +```solidity + BeaconChainHelperLib.InclusionProof[] memory proofs = new BeaconChainHelperLib.InclusionProof[](1); + proofs[0] = BeaconChainHelperLib.InclusionProof({ + validator: [0x1234...], + // Additional proof data... + }); + + uniFiAVSManager.slashValidatorsWithInvalidPubkey(proofs); +``` + +Mechanism: +Similar to the previous mechanisms, this function verifies the validator's public key against the provided proof. If the public key is found to be invalid, the validator is slashed. This mechanism ensures the integrity of the validator's public key, preventing unauthorized or incorrect registrations. + +## Safety Faults (Not Implemented) Safety faults occur when a validator breaks their pre-conf promise. This category encompasses a larger design space compared to Liveness faults, including: @@ -21,7 +97,7 @@ b) Execution Pre-conf Violations: The larger design space for Safety faults allows for more complex and nuanced slashing conditions, which can be expanded and refined as the pre-confirmation ecosystem evolves. -## Liveness Faults +## Liveness Faults (Not Implemented) Liveness faults occur when: @@ -31,7 +107,7 @@ Liveness faults occur when: This mechanism ensures that validators cannot abuse the pre-confirmation system by making promises they don't intend to keep due to inactivity. -## Slashing Process +## Slashing Process for Liveness Faults and Safety Faults The slashing process involves two key components: diff --git a/l1-contracts/script/DeployEverything.s.sol b/l1-contracts/script/DeployEverything.s.sol index 9684710..3e62b2d 100644 --- a/l1-contracts/script/DeployEverything.s.sol +++ b/l1-contracts/script/DeployEverything.s.sol @@ -6,8 +6,8 @@ import { DeployUniFiAVSManager } from "script/DeployUniFiAVSManager.s.sol"; import { SetupAccess } from "script/SetupAccess.s.sol"; import { AccessManager } from "@openzeppelin/contracts/access/manager/AccessManager.sol"; import { AVSDeployment } from "script/DeploymentStructs.sol"; -import { console } from "forge-std/console.sol"; - +import { UniFiAVSDisputeManager } from "../src/UniFiAVSDisputeManager.sol"; +import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; /** * @title Deploy all protocol contracts * @author Puffer Finance @@ -15,6 +15,7 @@ import { console } from "forge-std/console.sol"; * @dev Example on how to run the script * forge script script/DeployEverything.s.sol:DeployEverything --rpc-url=$RPC_URL --sig 'run()' --broadcast */ + contract DeployEverything is BaseScript { address DAO; @@ -28,16 +29,32 @@ contract DeployEverything is BaseScript { vm.startBroadcast(_deployerPrivateKey); AccessManager accessManager = new AccessManager(_broadcaster); + + // Deploy DisputeManager + UniFiAVSDisputeManager disputeManagerImplementation = new UniFiAVSDisputeManager(); + address disputeManager = address( + new ERC1967Proxy{ salt: bytes32("UniFiAVSDisputeManager") }( + address(disputeManagerImplementation), + abi.encodeCall(UniFiAVSDisputeManager.initialize, (address(accessManager))) + ) + ); + vm.stopBroadcast(); // 1. Deploy AVSManager (address avsManagerImplementation, address avsManagerProxy) = new DeployUniFiAVSManager().run( - address(accessManager), eigenPodManager, eigenDelegationManager, avsDirectory, initialDeregistrationDelay + address(accessManager), + eigenPodManager, + eigenDelegationManager, + avsDirectory, + initialDeregistrationDelay, + disputeManager ); deployment.avsManagerImplementation = avsManagerImplementation; deployment.avsManagerProxy = avsManagerProxy; deployment.accessManager = address(accessManager); + deployment.disputeManagerProxy = disputeManager; // `anvil` in the terminal if (_localAnvil) { @@ -65,6 +82,8 @@ contract DeployEverything is BaseScript { vm.serializeAddress(obj, "avsManagerImplementation", deployment.avsManagerImplementation); vm.serializeAddress(obj, "avsManagerProxy", deployment.avsManagerProxy); vm.serializeAddress(obj, "accessManager", deployment.accessManager); + vm.serializeAddress(obj, "disputeManagerImplementation", deployment.disputeManagerImplementation); + vm.serializeAddress(obj, "disputeManagerProxy", deployment.disputeManagerProxy); vm.serializeAddress(obj, "dao", DAO); string memory finalJson = vm.serializeString(obj, "", ""); diff --git a/l1-contracts/script/DeployUniFiAVSManager.s.sol b/l1-contracts/script/DeployUniFiAVSManager.s.sol index 3a42b61..b9d2a5b 100644 --- a/l1-contracts/script/DeployUniFiAVSManager.s.sol +++ b/l1-contracts/script/DeployUniFiAVSManager.s.sol @@ -7,7 +7,7 @@ import { IEigenPodManager } from "eigenlayer/interfaces/IEigenPodManager.sol"; import { IDelegationManager } from "eigenlayer/interfaces/IDelegationManager.sol"; import { IAVSDirectory } from "eigenlayer/interfaces/IAVSDirectory.sol"; import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; -import { console } from "forge-std/console.sol"; +import { IUniFiAVSDisputeManager } from "../src/interfaces/IUniFiAVSDisputeManager.sol"; contract DeployUniFiAVSManager is BaseScript { UniFiAVSManager public uniFiAVSManagerProxy; @@ -17,11 +17,15 @@ contract DeployUniFiAVSManager is BaseScript { address eigenPodManager, address eigenDelegationManager, address avsDirectory, - uint64 initialDeregistrationDelay + uint64 initialDeregistrationDelay, + address disputeManager ) public returns (address, address) { vm.startBroadcast(_deployerPrivateKey); UniFiAVSManager uniFiAVSManagerImplementation = new UniFiAVSManager( - IEigenPodManager(eigenPodManager), IDelegationManager(eigenDelegationManager), IAVSDirectory(avsDirectory) + IEigenPodManager(eigenPodManager), + IDelegationManager(eigenDelegationManager), + IAVSDirectory(avsDirectory), + IUniFiAVSDisputeManager(disputeManager) ); uniFiAVSManagerProxy = UniFiAVSManager( diff --git a/l1-contracts/script/DeployUniFiAVSManagerWithMocks.s.sol b/l1-contracts/script/DeployUniFiAVSManagerWithMocks.s.sol index 9f422db..57f6ba7 100644 --- a/l1-contracts/script/DeployUniFiAVSManagerWithMocks.s.sol +++ b/l1-contracts/script/DeployUniFiAVSManagerWithMocks.s.sol @@ -8,11 +8,12 @@ import { IDelegationManager } from "eigenlayer/interfaces/IDelegationManager.sol import { IAVSDirectory } from "eigenlayer/interfaces/IAVSDirectory.sol"; import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import { AccessManager } from "@openzeppelin/contracts/access/manager/AccessManager.sol"; -import "forge-std/console.sol"; - import "../test/mocks/MockEigenPodManager.sol"; import "../test/mocks/MockDelegationManager.sol"; import "../test/mocks/MockAVSDirectory.sol"; +import "../src/UniFiAVSDisputeManager.sol"; +import { IUniFiAVSDisputeManager } from "../src/interfaces/IUniFiAVSDisputeManager.sol"; +import { console } from "forge-std/console.sol"; contract DeployUniFiAVSManagerWithMocks is BaseScript { UniFiAVSManager public uniFiAVSManagerProxy; @@ -21,6 +22,7 @@ contract DeployUniFiAVSManagerWithMocks is BaseScript { address eigenDelegationManager; address avsDirectory; uint64 initialDeregistrationDelay = 0; + address disputeManager; function run() public broadcast returns (address, address) { eigenPodManager = address(new MockEigenPodManager()); @@ -28,9 +30,19 @@ contract DeployUniFiAVSManagerWithMocks is BaseScript { avsDirectory = address(new MockAVSDirectory()); accessManager = new AccessManager(_broadcaster); + UniFiAVSDisputeManager disputeManagerImplementation = new UniFiAVSDisputeManager(); + disputeManager = address( + new ERC1967Proxy{ salt: bytes32("UniFiAVSDisputeManager") }( + address(disputeManagerImplementation), + abi.encodeCall(UniFiAVSDisputeManager.initialize, (address(accessManager))) + ) + ); UniFiAVSManager uniFiAVSManagerImplementation = new UniFiAVSManager( - IEigenPodManager(eigenPodManager), IDelegationManager(eigenDelegationManager), IAVSDirectory(avsDirectory) + IEigenPodManager(eigenPodManager), + IDelegationManager(eigenDelegationManager), + IAVSDirectory(avsDirectory), + IUniFiAVSDisputeManager(disputeManager) ); uniFiAVSManagerProxy = UniFiAVSManager( @@ -49,6 +61,7 @@ contract DeployUniFiAVSManagerWithMocks is BaseScript { console.log("eigenPodManager mock:", address(eigenPodManager)); console.log("eigenDelegationManager mock:", address(eigenDelegationManager)); console.log("avsDirectory mock:", address(avsDirectory)); + console.log("disputeManager mock:", address(disputeManager)); return (address(uniFiAVSManagerImplementation), address(uniFiAVSManagerProxy)); } diff --git a/l1-contracts/script/DeploymentStructs.sol b/l1-contracts/script/DeploymentStructs.sol index bbb6594..d27c701 100644 --- a/l1-contracts/script/DeploymentStructs.sol +++ b/l1-contracts/script/DeploymentStructs.sol @@ -10,4 +10,6 @@ struct AVSDeployment { address accessManager; address timelock; address dao; + address disputeManagerProxy; + address disputeManagerImplementation; } diff --git a/l1-contracts/script/Roles.sol b/l1-contracts/script/Roles.sol index 617a1a3..cc54bd9 100644 --- a/l1-contracts/script/Roles.sol +++ b/l1-contracts/script/Roles.sol @@ -30,3 +30,6 @@ uint64 constant ROLE_ID_AVS_COORDINATOR_ALLOWLISTER = 5; // Lockbox role for ETH Mainnet uint64 constant ROLE_ID_LOCKBOX = 7; + +// Role for UniFiAVSManager +uint64 constant ROLE_ID_UNIFI_AVS_MANAGER = 30; diff --git a/l1-contracts/script/SetupAccess.s.sol b/l1-contracts/script/SetupAccess.s.sol index d44ba64..54c3c85 100644 --- a/l1-contracts/script/SetupAccess.s.sol +++ b/l1-contracts/script/SetupAccess.s.sol @@ -8,14 +8,15 @@ import { AccessManager } from "@openzeppelin/contracts/access/manager/AccessMana import { Multicall } from "@openzeppelin/contracts/utils/Multicall.sol"; import { UniFiAVSManager } from "../src/UniFiAVSManager.sol"; import { AVSDeployment } from "script/DeploymentStructs.sol"; - +import { UniFiAVSDisputeManager } from "../src/UniFiAVSDisputeManager.sol"; import { ROLE_ID_OPERATIONS_MULTISIG, ROLE_ID_OPERATIONS_PAYMASTER, ROLE_ID_PUFFER_PROTOCOL, ROLE_ID_DAO, ROLE_ID_OPERATIONS_COORDINATOR, - ROLE_ID_VT_PRICER + ROLE_ID_VT_PRICER, + ROLE_ID_UNIFI_AVS_MANAGER } from "../script/Roles.sol"; contract SetupAccess is BaseScript { @@ -31,6 +32,7 @@ contract SetupAccess is BaseScript { bytes[] memory calldatas = _generateAccessCalldata({ rolesCalldatas: _grantRoles(dao), uniFiAVSManagerRoles: _setupUniFiAVSManagerRoles(), + uniFiAVSDisputeManagerRoles: _setupUniFiAVSDisputeManagerRoles(), roleLabels: _labelRoles() }); @@ -43,21 +45,26 @@ contract SetupAccess is BaseScript { function _generateAccessCalldata( bytes[] memory rolesCalldatas, bytes[] memory uniFiAVSManagerRoles, + bytes[] memory uniFiAVSDisputeManagerRoles, bytes[] memory roleLabels ) internal view returns (bytes[] memory calldatas) { - calldatas = new bytes[](4); + calldatas = new bytes[](6); calldatas[0] = rolesCalldatas[0]; + calldatas[1] = rolesCalldatas[1]; - calldatas[1] = uniFiAVSManagerRoles[0]; - calldatas[2] = uniFiAVSManagerRoles[1]; - - calldatas[3] = roleLabels[0]; + calldatas[2] = uniFiAVSManagerRoles[0]; + calldatas[3] = uniFiAVSManagerRoles[1]; + calldatas[4] = uniFiAVSDisputeManagerRoles[0]; + calldatas[5] = roleLabels[0]; } function _grantRoles(address dao) internal view returns (bytes[] memory) { - bytes[] memory calldatas = new bytes[](1); + bytes[] memory calldatas = new bytes[](2); calldatas[0] = abi.encodeWithSelector(AccessManager.grantRole.selector, ROLE_ID_DAO, dao, 0); + calldatas[1] = abi.encodeWithSelector( + AccessManager.grantRole.selector, ROLE_ID_UNIFI_AVS_MANAGER, avsDeployment.avsManagerProxy, 0 + ); return calldatas; } @@ -88,7 +95,7 @@ contract SetupAccess is BaseScript { ); bytes4[] memory publicSelectors = new bytes4[](0); - publicSelectors = new bytes4[](8); + publicSelectors = new bytes4[](12); publicSelectors[0] = UniFiAVSManager.registerOperator.selector; publicSelectors[1] = UniFiAVSManager.registerValidators.selector; publicSelectors[2] = UniFiAVSManager.startDeregisterOperator.selector; @@ -97,6 +104,10 @@ contract SetupAccess is BaseScript { publicSelectors[5] = UniFiAVSManager.setOperatorCommitment.selector; publicSelectors[6] = UniFiAVSManager.updateOperatorCommitment.selector; publicSelectors[7] = UniFiAVSManager.registerOperatorWithCommitment.selector; + publicSelectors[8] = UniFiAVSManager.registerValidatorsOptimistically.selector; + publicSelectors[9] = UniFiAVSManager.slashValidatorsWithInvalidSignature.selector; + publicSelectors[10] = UniFiAVSManager.slashValidatorsWithInvalidPubkey.selector; + publicSelectors[11] = UniFiAVSManager.slashValidatorsWithInvalidIndex.selector; calldatas[1] = abi.encodeWithSelector( AccessManager.setTargetFunctionRole.selector, @@ -107,4 +118,20 @@ contract SetupAccess is BaseScript { return calldatas; } + + function _setupUniFiAVSDisputeManagerRoles() internal view returns (bytes[] memory) { + bytes[] memory calldatas = new bytes[](1); + + bytes4[] memory selectors = new bytes4[](1); + selectors[0] = UniFiAVSDisputeManager.slashOperator.selector; + + calldatas[0] = abi.encodeWithSelector( + AccessManager.setTargetFunctionRole.selector, + address(avsDeployment.disputeManagerProxy), + selectors, + ROLE_ID_UNIFI_AVS_MANAGER + ); + + return calldatas; + } } diff --git a/l1-contracts/script/UpgradeMainnetUniFiAVS.s.sol b/l1-contracts/script/UpgradeMainnetUniFiAVS.s.sol index 63f6b4b..466923e 100644 --- a/l1-contracts/script/UpgradeMainnetUniFiAVS.s.sol +++ b/l1-contracts/script/UpgradeMainnetUniFiAVS.s.sol @@ -10,8 +10,12 @@ import { IEigenPodManager } from "eigenlayer/interfaces/IEigenPodManager.sol"; import { IDelegationManager } from "eigenlayer/interfaces/IDelegationManager.sol"; import { IAVSDirectory } from "eigenlayer/interfaces/IAVSDirectory.sol"; import { console } from "forge-std/console.sol"; -import { ROLE_ID_OPERATIONS_MULTISIG, ROLE_ID_DAO } from "./Roles.sol"; +import { ROLE_ID_OPERATIONS_MULTISIG, ROLE_ID_DAO, ROLE_ID_UNIFI_AVS_MANAGER } from "./Roles.sol"; import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import { UniFiAVSDisputeManager } from "../src/UniFiAVSDisputeManager.sol"; +import { IUniFiAVSDisputeManager } from "../src/interfaces/IUniFiAVSDisputeManager.sol"; +import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import { Multicall } from "@openzeppelin/contracts/utils/Multicall.sol"; contract UpgradeMainnetUniFiAVS is BaseScript { function run() public returns (AVSDeployment memory deployment) { @@ -25,8 +29,22 @@ contract UpgradeMainnetUniFiAVS is BaseScript { uint64 initialDeregistrationDelay = 0; AccessManager accessManager = AccessManager(accessManagerAddress); + // Deploy DisputeManager + UniFiAVSDisputeManager disputeManagerImplementation = new UniFiAVSDisputeManager(); + address disputeManager = address( + new ERC1967Proxy{ salt: bytes32("UniFiAVSDisputeManager") }( + address(disputeManagerImplementation), + abi.encodeCall(UniFiAVSDisputeManager.initialize, (address(accessManager))) + ) + ); + console.log("DisputeManager implementation:", address(disputeManagerImplementation)); + console.log("DisputeManager proxy:", disputeManager); + UniFiAVSManager uniFiAVSManagerImplementation = new UniFiAVSManager( - IEigenPodManager(eigenPodManager), IDelegationManager(eigenDelegationManager), IAVSDirectory(avsDirectory) + IEigenPodManager(eigenPodManager), + IDelegationManager(eigenDelegationManager), + IAVSDirectory(avsDirectory), + IUniFiAVSDisputeManager(disputeManager) ); console.log("UniFiAVSManager Implementation:", address(uniFiAVSManagerImplementation)); @@ -43,14 +61,36 @@ contract UpgradeMainnetUniFiAVS is BaseScript { console.log("Access control calldata:"); - bytes memory calldatas; - bytes4[] memory daoSelectors = new bytes4[](1); - daoSelectors[0] = UniFiAVSManager.setAllowlistRestakingStrategy.selector; + bytes[] memory calldatas = new bytes[](3); + bytes4[] memory uniFiAVSManagerSelectors = new bytes4[](4); + uniFiAVSManagerSelectors[0] = UniFiAVSManager.registerValidatorsOptimistically.selector; + uniFiAVSManagerSelectors[1] = UniFiAVSManager.slashValidatorsWithInvalidSignature.selector; + uniFiAVSManagerSelectors[2] = UniFiAVSManager.slashValidatorsWithInvalidPubkey.selector; + uniFiAVSManagerSelectors[3] = UniFiAVSManager.slashValidatorsWithInvalidIndex.selector; + + calldatas[0] = abi.encodeWithSelector( + AccessManager.setTargetFunctionRole.selector, + address(uniFiAVSManagerProxy), + uniFiAVSManagerSelectors, + accessManager.PUBLIC_ROLE() + ); + + calldatas[1] = abi.encodeWithSelector( + AccessManager.grantRole.selector, ROLE_ID_UNIFI_AVS_MANAGER, address(uniFiAVSManagerProxy), 0 + ); + + bytes4[] memory disputeManagerSelectors = new bytes4[](1); + disputeManagerSelectors[0] = IUniFiAVSDisputeManager.slashOperator.selector; - calldatas = abi.encodeWithSelector( - AccessManager.setTargetFunctionRole.selector, address(uniFiAVSManagerProxy), daoSelectors, ROLE_ID_DAO + calldatas[2] = abi.encodeWithSelector( + AccessManager.setTargetFunctionRole.selector, + address(disputeManager), + disputeManagerSelectors, + ROLE_ID_UNIFI_AVS_MANAGER ); - console.logBytes(calldatas); + bytes memory multicallData = abi.encodeCall(Multicall.multicall, (calldatas)); + + console.logBytes(multicallData); } } diff --git a/l1-contracts/src/UniFiAVSDisputeManager.sol b/l1-contracts/src/UniFiAVSDisputeManager.sol new file mode 100644 index 0000000..6978459 --- /dev/null +++ b/l1-contracts/src/UniFiAVSDisputeManager.sol @@ -0,0 +1,48 @@ +pragma solidity >=0.8.0 <0.9.0; + +import "./storage/UniFiAVSDisputeManagerStorage.sol"; +import "./structs/ValidatorData.sol"; +import { AccessManagedUpgradeable } from + "@openzeppelin/contracts-upgradeable/access/manager/AccessManagedUpgradeable.sol"; +import { IUniFiAVSDisputeManager } from "./interfaces/IUniFiAVSDisputeManager.sol"; +/** + * @title UniFiAVSDisputeManager + * @dev Manages disputes and slashing of operators. + */ + +contract UniFiAVSDisputeManager is IUniFiAVSDisputeManager, UniFiAVSDisputeManagerStorage, AccessManagedUpgradeable { + constructor() { + _disableInitializers(); + } + + function initialize(address accessManager) external initializer { + __AccessManaged_init(accessManager); + } + + /** + * @inheritdoc IUniFiAVSDisputeManager + * @dev restricted to the UniFiAVSManager + */ + function slashOperator(address operator, bytes32[] calldata validators, address slashingBeneficiary) + external + restricted + { + UniFiAVSDisputeStorage storage $ = _getUniFiAVSDisputeStorage(); + + for (uint256 i = 0; i < validators.length; i++) { + $.slashedOperators[operator].push( + InvalidValidator({ slashingBeneficiary: slashingBeneficiary, blsPubKeyHash: validators[i] }) + ); + } + + emit OperatorSlashed(operator, validators, slashingBeneficiary); + } + + /** + * @inheritdoc IUniFiAVSDisputeManager + */ + function isOperatorSlashed(address operator) external view returns (bool) { + UniFiAVSDisputeStorage storage $ = _getUniFiAVSDisputeStorage(); + return $.slashedOperators[operator].length > 0; + } +} diff --git a/l1-contracts/src/UniFiAVSManager.sol b/l1-contracts/src/UniFiAVSManager.sol index 6ddf9a6..867446e 100644 --- a/l1-contracts/src/UniFiAVSManager.sol +++ b/l1-contracts/src/UniFiAVSManager.sol @@ -2,6 +2,7 @@ pragma solidity >=0.8.0 <0.9.0; import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import { EIP712 } from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; import { AccessManagedUpgradeable } from "@openzeppelin/contracts-upgradeable/access/manager/AccessManagedUpgradeable.sol"; import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; @@ -12,15 +13,27 @@ import { IAVSDirectoryExtended } from "./interfaces/EigenLayer/IAVSDirectoryExte import { IDelegationManager } from "eigenlayer/interfaces/IDelegationManager.sol"; import { IEigenPodManager } from "eigenlayer/interfaces/IEigenPodManager.sol"; import { IEigenPod } from "eigenlayer/interfaces/IEigenPod.sol"; +import { BN254 } from "eigenlayer-middleware/libraries/BN254.sol"; +import { BLSSignatureCheckerLib } from "./lib/BLSSignatureCheckerLib.sol"; +import { BeaconChainHelperLib } from "./lib/BeaconChainHelperLib.sol"; import { IUniFiAVSManager } from "./interfaces/IUniFiAVSManager.sol"; -import { UniFiAVSManagerStorage } from "./UniFiAVSManagerStorage.sol"; +import { UniFiAVSManagerStorage } from "./storage/UniFiAVSManagerStorage.sol"; import "./structs/ValidatorData.sol"; import "./structs/OperatorData.sol"; - -contract UniFiAVSManager is UniFiAVSManagerStorage, IUniFiAVSManager, UUPSUpgradeable, AccessManagedUpgradeable { +import { IUniFiAVSDisputeManager } from "./interfaces/IUniFiAVSDisputeManager.sol"; + +contract UniFiAVSManager is + UniFiAVSManagerStorage, + IUniFiAVSManager, + UUPSUpgradeable, + AccessManagedUpgradeable, + EIP712 +{ using EnumerableSet for EnumerableSet.AddressSet; - address public constant BEACON_CHAIN_STRATEGY = 0xbeaC0eeEeeeeEEeEeEEEEeeEEeEeeeEeeEEBEaC0; + address private constant BEACON_CHAIN_STRATEGY = 0xbeaC0eeEeeeeEEeEeEEEEeeEEeEeeeEeeEEBEaC0; + bytes32 private constant VALIDATOR_REGISTRATION_TYPEHASH = + keccak256("BN254ValidatorRegistration(address operator,bytes32 salt,uint256 expiry,uint64 index)"); /** * @notice The EigenPodManager @@ -37,6 +50,11 @@ contract UniFiAVSManager is UniFiAVSManagerStorage, IUniFiAVSManager, UUPSUpgrad * @custom:oz-upgrades-unsafe-allow state-variable-immutable */ IAVSDirectoryExtended public immutable override AVS_DIRECTORY; + /** + * @notice The UniFiAVSDisputeManager contract + * @custom:oz-upgrades-unsafe-allow state-variable-immutable + */ + IUniFiAVSDisputeManager public immutable DISPUTE_MANAGER; /** * @dev Modifier to check if the pod is delegated to the msg.sender @@ -75,11 +93,13 @@ contract UniFiAVSManager is UniFiAVSManagerStorage, IUniFiAVSManager, UUPSUpgrad constructor( IEigenPodManager eigenPodManagerAddress, IDelegationManager eigenDelegationManagerAddress, - IAVSDirectory avsDirectoryAddress - ) { + IAVSDirectory avsDirectoryAddress, + IUniFiAVSDisputeManager disputeManagerAddress + ) EIP712("UniFiAVSManager", "1") { EIGEN_POD_MANAGER = eigenPodManagerAddress; EIGEN_DELEGATION_MANAGER = eigenDelegationManagerAddress; AVS_DIRECTORY = IAVSDirectoryExtended(address(avsDirectoryAddress)); + DISPUTE_MANAGER = disputeManagerAddress; _disableInitializers(); } @@ -153,7 +173,7 @@ contract UniFiAVSManager is UniFiAVSManagerStorage, IUniFiAVSManager, UUPSUpgrad revert ValidatorNotActive(); } - if ($.validators[blsPubKeyHash].index != 0) { + if ($.validators[blsPubKeyHash].index != 0 && $.validators[blsPubKeyHash].registeredUntil > block.number) { revert ValidatorAlreadyRegistered(); } @@ -212,6 +232,172 @@ contract UniFiAVSManager is UniFiAVSManagerStorage, IUniFiAVSManager, UUPSUpgrad $.operators[msg.sender].validatorCount -= uint128(validatorCount); } + /** + * @inheritdoc IUniFiAVSManager + * @dev Restricted in this context is like `whenNotPaused` modifier from Pausable.sol + */ + function registerValidatorsOptimistically(ValidatorRegistrationParams[] calldata validators) + external + registeredOperator(msg.sender) + restricted + { + UniFiAVSStorage storage $ = _getUniFiAVSManagerStorage(); + uint256 newValidatorCount = 0; + + for (uint256 i = 0; i < validators.length; i++) { + // Derive the BLS public key hash from pubkeyG1 + + ValidatorData storage existingValidator = $.validators[validators[i].blsPubKeyHash]; + ValidatorRegistrationData storage validatorRegistrationData = + $.validatorRegistrations[validators[i].blsPubKeyHash]; + + // Check if the validator already exists with active status + if (existingValidator.index != 0 && existingValidator.registeredUntil > block.number) { + revert ValidatorAlreadyRegistered(); + } + + // only allow re-registration if the index is not already used for a different validator + bytes32 storedBlsPubKeyHash = $.validatorIndexes[validators[i].index]; + if (storedBlsPubKeyHash != bytes32(0) && storedBlsPubKeyHash != validators[i].blsPubKeyHash) { + revert ValidatorIndexAlreadyUsed(); + } + + // Check if the registration salt has been used before + if (validatorRegistrationData.salt == validators[i].salt) { + revert SaltAlreadyUsed(); + } + + // Check if the signature is expired + if (block.timestamp > validators[i].expiry) { + revert SignatureExpired(); + } + + // Store the validator data + $.validators[validators[i].blsPubKeyHash] = ValidatorData({ + eigenPod: address(0), // Not from EigenPod + index: validators[i].index, // Store the provided index + operator: msg.sender, + registeredUntil: type(uint64).max + }); + + bytes32 registrationHash = keccak256( + abi.encodePacked( + validators[i].registrationSignature.X, + validators[i].registrationSignature.Y, + validators[i].expiry, + validators[i].salt + ) + ); + + $.validatorRegistrations[validators[i].blsPubKeyHash] = ValidatorRegistrationData({ + registrationHash: registrationHash, + salt: validators[i].salt, + registeredAt: uint64(block.number), + activeAfter: uint64(block.number) + $.registrationDelay + }); + + $.validatorIndexes[validators[i].index] = validators[i].blsPubKeyHash; + + emit ValidatorRegisteredOptimistically({ + operator: msg.sender, + blsPubKeyHash: validators[i].blsPubKeyHash, + validatorIndex: validators[i].index, + salt: validators[i].salt, + expiry: validators[i].expiry, + signature: validators[i].registrationSignature + }); + + newValidatorCount++; + } + + // Update the operator's validator count + OperatorData storage operator = $.operators[msg.sender]; + operator.validatorCount += uint128(newValidatorCount); + operator.startDeregisterOperatorBlock = 0; + } + + /** + * @inheritdoc IUniFiAVSManager + * @dev Restricted in this context is like `whenNotPaused` modifier from Pausable.sol + */ + function slashValidatorsWithInvalidSignature(ValidatorRegistrationSlashingParams[] calldata validators) + external + restricted + { + UniFiAVSStorage storage $ = _getUniFiAVSManagerStorage(); + + for (uint256 i = 0; i < validators.length; i++) { + bytes32 blsPubKeyHash = BLSSignatureCheckerLib.hashG1Point(validators[i].pubkeyG1); + + ValidatorData storage validator = $.validators[blsPubKeyHash]; + + bytes32 registrationHash = keccak256( + abi.encodePacked( + validators[i].registrationSignature.X, + validators[i].registrationSignature.Y, + validators[i].expiry, + validators[i].salt + ) + ); + + if ($.validatorRegistrations[blsPubKeyHash].registrationHash != registrationHash) { + revert InvalidRegistrationSignature(); + } + + // Calculate the hash using EIP-712 + BN254.G1Point memory messageHash = blsMessageHash({ + operator: validator.operator, + salt: validators[i].salt, + expiry: validators[i].expiry, + index: validators[i].index + }); + + // Use the stored signature for verification + bool isValid = BLSSignatureCheckerLib.isBlsSignatureValid( + validators[i].pubkeyG1, validators[i].pubkeyG2, validators[i].registrationSignature, messageHash + ); + + if (!isValid) { + _slashAndDeregisterValidator(blsPubKeyHash); + } + } + } + + /** + * @inheritdoc IUniFiAVSManager + * @dev Restricted in this context is like `whenNotPaused` modifier from Pausable.sol + */ + function slashValidatorsWithInvalidIndex(BeaconChainHelperLib.InclusionProof[] calldata proofs) + external + restricted + { + for (uint256 i = 0; i < proofs.length; i++) { + bytes32 blsPubKeyHash = proofs[i].validator[0]; + (uint64 index,) = _validateProofAndGetValidatorKeys(proofs[i], false); + + if (index != proofs[i].validatorIndex) { + _slashAndDeregisterValidator(blsPubKeyHash); + } + } + } + + /** + * @inheritdoc IUniFiAVSManager + * @dev Restricted in this context is like `whenNotPaused` modifier from Pausable.sol + */ + function slashValidatorsWithInvalidPubkey(BeaconChainHelperLib.InclusionProof[] calldata proofs) + external + restricted + { + for (uint256 i = 0; i < proofs.length; i++) { + (, bytes32 storedBlsPubKeyHash) = _validateProofAndGetValidatorKeys(proofs[i], true); + + if (storedBlsPubKeyHash != proofs[i].validator[0]) { + _slashAndDeregisterValidator(storedBlsPubKeyHash); + } + } + } + /** * @inheritdoc IUniFiAVSManager * @dev Restricted in this context is like `whenNotPaused` modifier from Pausable.sol @@ -229,6 +415,10 @@ contract UniFiAVSManager is UniFiAVSManagerStorage, IUniFiAVSManager, UUPSUpgrad revert DeregistrationAlreadyStarted(); } + if (DISPUTE_MANAGER.isOperatorSlashed(msg.sender)) { + revert OperatorSlashed(); + } + operator.startDeregisterOperatorBlock = uint64(block.number); emit OperatorDeregisterStarted(msg.sender); @@ -308,6 +498,18 @@ contract UniFiAVSManager is UniFiAVSManagerStorage, IUniFiAVSManager, UUPSUpgrad _setDeregistrationDelay(newDelay); } + /** + * @inheritdoc IUniFiAVSManager + * @dev Restricted to the DAO + */ + function setRegistrationDelay(uint64 newDelay) external restricted { + UniFiAVSStorage storage $ = _getUniFiAVSManagerStorage(); + uint64 oldDelay = $.registrationDelay; + $.registrationDelay = newDelay; + + emit RegistrationDelaySet(oldDelay, newDelay); + } + /** * @inheritdoc IUniFiAVSManager * @dev Restricted to the DAO @@ -359,6 +561,14 @@ contract UniFiAVSManager is UniFiAVSManagerStorage, IUniFiAVSManager, UUPSUpgrad return $.deregistrationDelay; } + /** + * @inheritdoc IUniFiAVSManager + */ + function getRegistrationDelay() external view returns (uint64) { + UniFiAVSStorage storage $ = _getUniFiAVSManagerStorage(); + return $.registrationDelay; + } + /** * @inheritdoc IUniFiAVSManager */ @@ -508,6 +718,40 @@ contract UniFiAVSManager is UniFiAVSManagerStorage, IUniFiAVSManager, UUPSUpgrad return address(AVS_DIRECTORY); } + /** + * @inheritdoc IUniFiAVSManager + */ + function blsMessageHash(address operator, uint256 salt, uint256 expiry, uint256 index) + public + view + returns (BN254.G1Point memory) + { + return BN254.hashToG1( + _hashTypedDataV4( + keccak256(abi.encodePacked(VALIDATOR_REGISTRATION_TYPEHASH, operator, salt, expiry, index)) + ) + ); + } + + function getValidatorRegistrationData(bytes32 blsPubKeyHash) + external + view + returns (ValidatorRegistrationData memory) + { + UniFiAVSStorage storage $ = _getUniFiAVSManagerStorage(); + return $.validatorRegistrations[blsPubKeyHash]; + } + + function getValidatorRegistrationData(uint256 validatorIndex) + external + view + returns (ValidatorRegistrationData memory) + { + UniFiAVSStorage storage $ = _getUniFiAVSManagerStorage(); + bytes32 blsPubKeyHash = $.validatorIndexes[validatorIndex]; + return $.validatorRegistrations[blsPubKeyHash]; + } + // INTERNAL FUNCTIONS function _getOperator(address operator) internal view returns (OperatorDataExtended memory) { @@ -523,7 +767,8 @@ contract UniFiAVSManager is UniFiAVSManagerStorage, IUniFiAVSManager, UUPSUpgrad startDeregisterOperatorBlock: operatorData.startDeregisterOperatorBlock, isRegistered: AVS_DIRECTORY.avsOperatorStatus(address(this), operator) == IAVSDirectory.OperatorAVSRegistrationStatus.REGISTERED, - commitmentValidAfter: operatorData.commitmentValidAfter + commitmentValidAfter: operatorData.commitmentValidAfter, + isSlashed: DISPUTE_MANAGER.isOperatorSlashed(operator) }); } @@ -531,12 +776,19 @@ contract UniFiAVSManager is UniFiAVSManagerStorage, IUniFiAVSManager, UUPSUpgrad UniFiAVSStorage storage $ = _getUniFiAVSManagerStorage(); ValidatorData memory validatorData = $.validators[blsPubKeyHash]; + ValidatorRegistrationData memory validatorRegistrationData = $.validatorRegistrations[blsPubKeyHash]; if (validatorData.index != 0) { - IEigenPod eigenPod = IEigenPod(validatorData.eigenPod); - IEigenPod.ValidatorInfo memory validatorInfo = eigenPod.validatorPubkeyHashToInfo(blsPubKeyHash); - - bool backedByStake = EIGEN_DELEGATION_MANAGER.delegatedTo(eigenPod.podOwner()) == validatorData.operator; + IEigenPod.VALIDATOR_STATUS eigenPodStatus; + bool backedByEigenPodStake; + + if (validatorData.eigenPod != address(0)) { + IEigenPod eigenPod = IEigenPod(validatorData.eigenPod); + IEigenPod.ValidatorInfo memory validatorInfo = eigenPod.validatorPubkeyHashToInfo(blsPubKeyHash); + eigenPodStatus = validatorInfo.status; + backedByEigenPodStake = + EIGEN_DELEGATION_MANAGER.delegatedTo(eigenPod.podOwner()) == validatorData.operator; + } OperatorData storage operator = $.operators[validatorData.operator]; OperatorCommitment memory activeCommitment = _getActiveCommitment(operator); @@ -544,12 +796,13 @@ contract UniFiAVSManager is UniFiAVSManagerStorage, IUniFiAVSManager, UUPSUpgrad return ValidatorDataExtended({ operator: validatorData.operator, eigenPod: validatorData.eigenPod, - validatorIndex: validatorInfo.validatorIndex, - status: validatorInfo.status, + validatorIndex: validatorData.index, + eigenPodStatus: eigenPodStatus, delegateKey: activeCommitment.delegateKey, chainIDBitMap: activeCommitment.chainIDBitMap, - backedByStake: backedByStake, + backedByEigenPodStake: backedByEigenPodStake, registered: block.number < validatorData.registeredUntil + && block.number > validatorRegistrationData.activeAfter }); } } @@ -577,5 +830,70 @@ contract UniFiAVSManager is UniFiAVSManagerStorage, IUniFiAVSManager, UUPSUpgrad emit DeregistrationDelaySet(oldDelay, newDelay); } + function _validateProofAndGetValidatorKeys( + BeaconChainHelperLib.InclusionProof calldata proof, + bool queryByProofIndex + ) internal returns (uint64, bytes32) { + UniFiAVSStorage storage $ = _getUniFiAVSManagerStorage(); + + if (!BeaconChainHelperLib.verifyValidatorExistence(proof)) { + revert InvalidValidatorProof(proof.validator[0]); + } + + bytes32 storedBlsPubKeyHash = queryByProofIndex ? $.validatorIndexes[proof.validatorIndex] : proof.validator[0]; + ValidatorData storage validator = $.validators[storedBlsPubKeyHash]; + + if (validator.eigenPod != address(0)) { + revert InvalidValidatorType(); + } + + ValidatorRegistrationData storage validatorRegistrationData = $.validatorRegistrations[storedBlsPubKeyHash]; + if (proof.timestamp < validatorRegistrationData.registeredAt || proof.timestamp > validator.registeredUntil) { + revert InvalidValidatorProof(proof.validator[0]); + } + + return (validator.index, storedBlsPubKeyHash); + } + + /** + * @dev Internal function to slash and deregister a validator + * @param blsPubKeyHash The BLS public key hash of the validator + */ + function _slashAndDeregisterValidator(bytes32 blsPubKeyHash) internal { + UniFiAVSStorage storage $ = _getUniFiAVSManagerStorage(); + ValidatorData storage validator = $.validators[blsPubKeyHash]; + address operator = validator.operator; + + uint64 validatorIndex = validator.index; + if (validatorIndex == 0) { + revert ValidatorNotFound(); + } + + // Call the dispute manager to slash the operator + bytes32[] memory validators = new bytes32[](1); + validators[0] = blsPubKeyHash; + DISPUTE_MANAGER.slashOperator(operator, validators, msg.sender); + + // Update the registeredUntil field to deregister the validator immediately + validator.registeredUntil = uint64(block.number); + + delete $.validatorRegistrations[blsPubKeyHash]; + delete $.validators[blsPubKeyHash]; + + // if the index and is pointing to the same validator, delete the index. + // this check is in place in case an EigenPod validator is registered and has used the index. + bytes32 pubkeyFromIndex = $.validatorIndexes[validatorIndex]; + if (pubkeyFromIndex == blsPubKeyHash) { + delete $.validatorIndexes[validatorIndex]; + } + + // Emit the ValidatorDeregistered event + emit ValidatorDeregistered({ operator: operator, blsPubKeyHash: blsPubKeyHash }); + + // Decrement the operator's validator count + OperatorData storage operatorData = $.operators[operator]; + operatorData.validatorCount -= 1; + } + function _authorizeUpgrade(address newImplementation) internal virtual override restricted { } } diff --git a/l1-contracts/src/interfaces/IUniFiAVSDisputeManager.sol b/l1-contracts/src/interfaces/IUniFiAVSDisputeManager.sol new file mode 100644 index 0000000..d973399 --- /dev/null +++ b/l1-contracts/src/interfaces/IUniFiAVSDisputeManager.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +/** + * @title IUniFiAVSDisputeManager + * @dev Interface for the UniFiAVSDisputeManager contract. + */ +interface IUniFiAVSDisputeManager { + /** + * @dev Emitted when an operator is slashed. + * @param operator The address of the operator that was slashed. + * @param validatorIds The array of validator IDs associated with the operator. + * @param slashingBeneficiary The address that benefits from the slashing. + */ + event OperatorSlashed(address indexed operator, bytes32[] validatorIds, address slashingBeneficiary); + + /** + * @dev Slashes an operator by storing their invalid validators and specifying a beneficiary. + * @param operator The address of the operator to be slashed. + * @param validators An array of validators associated with the operator. + * @param slashingBeneficiary The address that will benefit from the slashing. + */ + function slashOperator(address operator, bytes32[] calldata validators, address slashingBeneficiary) external; + + /** + * @dev Checks if an operator has been slashed. + * @param operator The address of the operator to check. + * @return True if the operator has been slashed, false otherwise. + */ + function isOperatorSlashed(address operator) external view returns (bool); +} diff --git a/l1-contracts/src/interfaces/IUniFiAVSManager.sol b/l1-contracts/src/interfaces/IUniFiAVSManager.sol index 01b442a..75aa7f4 100644 --- a/l1-contracts/src/interfaces/IUniFiAVSManager.sol +++ b/l1-contracts/src/interfaces/IUniFiAVSManager.sol @@ -3,9 +3,11 @@ pragma solidity >=0.8.0 <0.9.0; import { IDelegationManager } from "eigenlayer/interfaces/IDelegationManager.sol"; import { ISignatureUtils } from "eigenlayer/interfaces/ISignatureUtils.sol"; +import { BN254 } from "eigenlayer-middleware/libraries/BN254.sol"; import { IAVSDirectoryExtended } from "../interfaces/EigenLayer/IAVSDirectoryExtended.sol"; import "../structs/ValidatorData.sol"; import "../structs/OperatorData.sol"; +import { BeaconChainHelperLib } from "../lib/BeaconChainHelperLib.sol"; /** * @title IUniFiAVSManager @@ -70,6 +72,27 @@ interface IUniFiAVSManager { /// @notice Thrown when a restaking strategy allowlist update fails error RestakingStrategyAllowlistUpdateFailed(); + /// @notice Thrown when a salt is already used for a registration + error SaltAlreadyUsed(); + + /// @notice Thrown when a signature is expired + error SignatureExpired(); + + /// @notice Thrown when a validator proof is invalid + error InvalidValidatorProof(bytes32 blsPubKeyHash); + + /// @notice Thrown when a validator index is already used + error ValidatorIndexAlreadyUsed(); + + /// @notice Thrown when an operator is slashed + error OperatorSlashed(); + + /// @notice Thrown when a validator is not backed by an EigenPod + error InvalidValidatorType(); + + /// @notice Thrown when a validator registration signature is invalid + error InvalidRegistrationSignature(); + /** * @notice Emitted when a new operator is registered in the UniFi AVS. * @param operator The address of the registered operator. @@ -152,6 +175,38 @@ interface IUniFiAVSManager { */ event RestakingStrategyAllowlistUpdated(address indexed strategy, bool allowed); + /** + * @notice Emitted when a validator is slashed. + * @param operator The address of the operator managing the validator. + * @param blsPubKeyHash The BLS public key hash of the slashed validator. + */ + event ValidatorSlashed(address indexed operator, bytes32 indexed blsPubKeyHash); + + /** + * @notice Emitted when the registration delay is set. + * @param oldDelay The previous registration delay value. + * @param newDelay The new registration delay value. + */ + event RegistrationDelaySet(uint64 oldDelay, uint64 newDelay); + + /** + * @notice Emitted when a validator is registered optimistically. + * @param operator The address of the operator. + * @param blsPubKeyHash The BLS public key hash of the validator. + * @param validatorIndex The beacon chain validator index. + * @param salt The salt for the message. + * @param expiry The expiry for the message. + * @param signature The signature for the message. + */ + event ValidatorRegisteredOptimistically( + address indexed operator, + bytes32 indexed blsPubKeyHash, + uint256 validatorIndex, + uint256 salt, + uint256 expiry, + BN254.G1Point signature + ); + /** * @notice Returns the EigenPodManager contract. * @return IEigenPodManager The EigenPodManager contract. @@ -227,11 +282,18 @@ interface IUniFiAVSManager { /** * @notice Sets a new deregistration delay for operators. - * @param newDelay The new deregistration delay in seconds. + * @param newDelay The new deregistration delay in blocks. * @dev Restricted to the DAO */ function setDeregistrationDelay(uint64 newDelay) external; + /** + * @notice Sets a new registration delay for validators. + * @param newDelay The new registration delay in blocks. + * @dev Restricted to the DAO + */ + function setRegistrationDelay(uint64 newDelay) external; + /** * @notice Sets the chain ID for a specific index in the bitmap. * @param index The index in the bitmap to set. @@ -248,6 +310,29 @@ interface IUniFiAVSManager { */ function setAllowlistRestakingStrategy(address strategy, bool allowed) external; + /** + * @notice Registers validators optimistically. + * @param paramsArray The array of ValidatorRegistrationParams. + */ + function registerValidatorsOptimistically(ValidatorRegistrationParams[] calldata paramsArray) external; + + /** + * @notice Slashes validators with invalid signatures. + * @param validators The array of ValidatorRegistrationSlashingParams. + */ + function slashValidatorsWithInvalidSignature(ValidatorRegistrationSlashingParams[] calldata validators) external; + + /** + * @notice Slashes validators with invalid pubkey. + * @param proofs The inclusion proofs for each validator. + */ + function slashValidatorsWithInvalidPubkey(BeaconChainHelperLib.InclusionProof[] calldata proofs) external; + + /** + * @notice Slashes validators with invalid index. + * @param proofs The inclusion proofs for each validator. + */ + function slashValidatorsWithInvalidIndex(BeaconChainHelperLib.InclusionProof[] calldata proofs) external; /** * @notice Retrieves information about a specific operator. * @param operator The address of the operator. @@ -290,6 +375,12 @@ interface IUniFiAVSManager { */ function getDeregistrationDelay() external view returns (uint64); + /** + * @notice Retrieves the current registration delay for validators. + * @return The current registration delay in seconds. + */ + function getRegistrationDelay() external view returns (uint64); + /** * @notice Converts a bitmap to an array of chain IDs. * @param bitmap The bitmap to convert. @@ -330,4 +421,37 @@ interface IUniFiAVSManager { /// @notice Returns the EigenLayer AVSDirectory contract. function avsDirectory() external view returns (address); + + /** + * @notice Returns the BLS message hash for a validator registration. + * @param operator The address of the operator. + * @param salt The salt for the message. + * @param expiry The expiry for the message. + * @param index The index for the message. + * @return BN254.G1Point The BLS message hash. + */ + function blsMessageHash(address operator, uint256 salt, uint256 expiry, uint256 index) + external + view + returns (BN254.G1Point memory); + + /** + * @notice Retrieves the validator registration data for a given BLS public key hash. + * @param blsPubKeyHash The BLS public key hash of the validator. + * @return ValidatorRegistrationData struct containing registration data for the validator. + */ + function getValidatorRegistrationData(bytes32 blsPubKeyHash) + external + view + returns (ValidatorRegistrationData memory); + + /** + * @notice Retrieves the validator registration data for a given validator index. + * @param validatorIndex The index of the validator. + * @return ValidatorRegistrationData struct containing registration data for the validator. + */ + function getValidatorRegistrationData(uint256 validatorIndex) + external + view + returns (ValidatorRegistrationData memory); } diff --git a/l1-contracts/src/lib/BLSSignatureCheckerLib.sol b/l1-contracts/src/lib/BLSSignatureCheckerLib.sol index 557d954..eb5b180 100644 --- a/l1-contracts/src/lib/BLSSignatureCheckerLib.sol +++ b/l1-contracts/src/lib/BLSSignatureCheckerLib.sol @@ -35,4 +35,26 @@ library BLSSignatureCheckerLib { pubkeyG2 ); } + + function g1PointToBytes(BN254.G1Point memory point) internal pure returns (bytes memory) { + bytes memory result = new bytes(48); + + assembly { + // Store X coordinate + mstore(add(result, 32), mload(point)) + + // Store Y coordinate + // Set the most significant bit to 1 if Y is odd, 0 if Y is even + let y := mload(add(point, 32)) + let yMod2 := mod(y, 2) + y := or(and(y, not(shl(255, 1))), shl(255, yMod2)) + mstore(add(result, 64), y) + } + + return result; + } + + function hashG1Point(BN254.G1Point memory pk) internal pure returns (bytes32 hashedG1) { + return sha256(abi.encodePacked(g1PointToBytes(pk), bytes16(0))); + } } diff --git a/l1-contracts/src/lib/BeaconChainHelperLib.sol b/l1-contracts/src/lib/BeaconChainHelperLib.sol index 3cc765a..745dd83 100644 --- a/l1-contracts/src/lib/BeaconChainHelperLib.sol +++ b/l1-contracts/src/lib/BeaconChainHelperLib.sol @@ -1,9 +1,83 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.8.0 <0.9.0; +import { MerkleUtils } from "./MerkleUtils.sol"; + library BeaconChainHelperLib { address internal constant _BEACON_ROOT_CONTRACT = 0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02; + struct InclusionProof { + // `Chunks` of the SSZ encoded validator + bytes32[8] validator; + // Index of the validator in the beacon state validator list + uint256 validatorIndex; + // Proof of inclusion of validator in beacon state validator list + bytes32[] validatorProof; + // Root of the validator list in the beacon state + bytes32 validatorsRoot; + // Proof of inclusion of validator list in the beacon state + bytes32[] beaconStateProof; + // Root of the beacon state + bytes32 beaconStateRoot; + // Proof of inclusion of beacon state in the beacon block + bytes32[] beaconBlockProofForState; + // Proof of inclusion of the validator index in the beacon block. leave this empty if not needed. + bytes32[] beaconBlockProofForProposerIndex; + // Timestamp of the beacon block + uint256 timestamp; + } + + /// @dev The validator pub key failed verification against the pub key hash tree root in the validator chunks + error InvalidValidatorBLSPubKey(); + /// @dev The proof that the validator is a part of the validator list is invalid. + error ValidatorProofFailed(); + /// @dev The proof that the validator list is a part of the beacon state is invalid. + error BeaconStateProofFailed(); + /// @dev The proof that the beacon state is a part of the beacon block is invalid. + error BeaconBlockProofForStateFailed(); + /// @dev The proof that the actual validator index is a part of the beacon is invalid. + error BeaconBlockProofForProposerIndex(); + + function verifyValidatorExistence(InclusionProof memory inclusionProof) internal returns (bool) { + // Validator is verified against the validator list in the beacon state + bytes32 validatorHashTreeRoot = MerkleUtils.merkleize(inclusionProof.validator); + if ( + !MerkleUtils.verifyProof( + inclusionProof.validatorProof, + inclusionProof.validatorsRoot, + validatorHashTreeRoot, + inclusionProof.validatorIndex + ) + ) { + // Revert if the proof that the expected validator is a part of the validator + // list in beacon state fails + return false; + } + + if ( + !MerkleUtils.verifyProof( + inclusionProof.beaconStateProof, inclusionProof.beaconStateRoot, inclusionProof.validatorsRoot, 11 + ) + ) { + // Revert if the proof that the validator list is a part of the beacon state fails + return false; + } + + (, bytes32 beaconBlockRoot) = getRootFromTimestamp(inclusionProof.timestamp); + + // Beacon state is verified against the beacon block + if ( + !MerkleUtils.verifyProof( + inclusionProof.beaconBlockProofForState, beaconBlockRoot, inclusionProof.beaconStateRoot, 3 + ) + ) { + // Revert if the proof for the beacon state being a part of the beacon block fails + return false; + } + + return true; + } + function verifyProposerAt(uint256 timestamp, uint256 proposerIndex, bytes32[2] memory proof) internal returns (bool) @@ -16,8 +90,8 @@ library BeaconChainHelperLib { bytes32 slotAndProposerIndexNode = sha256( abi.encodePacked( - abi.encodePacked(to_little_endian_64(uint64(slot)), bytes24(0)), - abi.encodePacked(to_little_endian_64(uint64(proposerIndex)), bytes24(0)) + abi.encodePacked(MerkleUtils.to_little_endian_64(uint64(slot)), bytes24(0)), + abi.encodePacked(MerkleUtils.to_little_endian_64(uint64(proposerIndex)), bytes24(0)) ) ); @@ -28,20 +102,6 @@ library BeaconChainHelperLib { return root == beaconRootFromChain; } - function to_little_endian_64(uint64 value) internal pure returns (bytes memory ret) { - ret = new bytes(8); - bytes8 bytesValue = bytes8(value); - // Byteswapping during copying to bytes. - ret[0] = bytesValue[7]; - ret[1] = bytesValue[6]; - ret[2] = bytesValue[5]; - ret[3] = bytesValue[4]; - ret[4] = bytesValue[3]; - ret[5] = bytesValue[2]; - ret[6] = bytesValue[1]; - ret[7] = bytesValue[0]; - } - function getRootFromTimestamp(uint256 timestamp) internal returns (bool, bytes32) { (bool ret, bytes memory data) = _BEACON_ROOT_CONTRACT.call(bytes.concat(bytes32(timestamp))); return (ret, bytes32(data)); diff --git a/l1-contracts/src/lib/MerkleUtils.sol b/l1-contracts/src/lib/MerkleUtils.sol new file mode 100644 index 0000000..defdbc4 --- /dev/null +++ b/l1-contracts/src/lib/MerkleUtils.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.0 <0.9.0; + +library MerkleUtils { + uint256 internal constant CHUNKS_LENGTH = 8; + uint256 internal constant TMP_LENGTH = 4; + + function hash(bytes32 a, bytes32 b) internal pure returns (bytes32) { + return sha256(abi.encodePacked(a, b)); + } + + function merkleize(bytes32[CHUNKS_LENGTH] memory chunks) internal pure returns (bytes32) { + bytes32[] memory tmp = new bytes32[](TMP_LENGTH); + + for (uint256 i; i < CHUNKS_LENGTH; ++i) { + merge(tmp, i, chunks[i]); + } + + return tmp[TMP_LENGTH - 1]; + } + + function merge(bytes32[] memory tmp, uint256 index, bytes32 chunk) internal pure { + bytes32 h = chunk; + uint256 j = 0; + while (true) { + if (index & 1 << j == 0) { + break; + } else { + h = hash(tmp[j], h); + } + j += 1; + } + tmp[j] = h; + } + + function verifyProof(bytes32[] memory proof, bytes32 root, bytes32 leaf, uint256 leafIndex) + internal + pure + returns (bool) + { + bytes32 h = leaf; + uint256 index = leafIndex; + + for (uint256 i = 0; i < proof.length; i++) { + bytes32 proofElement = proof[i]; + + if (index % 2 == 0) { + h = sha256(bytes.concat(h, proofElement)); + } else { + h = sha256(bytes.concat(proofElement, h)); + } + + index = index / 2; + } + + return h == root; + } + + function toLittleEndian(uint256 n) internal pure returns (bytes32) { + uint256 v = n; + v = ((v & 0xFF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00) >> 8) + | ((v & 0x00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF) << 8); + v = ((v & 0xFFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000) >> 16) + | ((v & 0x0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF) << 16); + v = ((v & 0xFFFFFFFF00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF00000000) >> 32) + | ((v & 0x00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF) << 32); + v = ((v & 0xFFFFFFFFFFFFFFFF0000000000000000FFFFFFFFFFFFFFFF0000000000000000) >> 64) + | ((v & 0x0000000000000000FFFFFFFFFFFFFFFF0000000000000000FFFFFFFFFFFFFFFF) << 64); + v = (v >> 128) | (v << 128); + return bytes32(v); + } + + function to_little_endian_64(uint64 value) internal pure returns (bytes memory ret) { + ret = new bytes(8); + bytes8 bytesValue = bytes8(value); + // Byteswapping during copying to bytes. + ret[0] = bytesValue[7]; + ret[1] = bytesValue[6]; + ret[2] = bytesValue[5]; + ret[3] = bytesValue[4]; + ret[4] = bytesValue[3]; + ret[5] = bytesValue[2]; + ret[6] = bytesValue[1]; + ret[7] = bytesValue[0]; + } +} diff --git a/l1-contracts/src/storage/UniFiAVSDisputeManagerStorage.sol b/l1-contracts/src/storage/UniFiAVSDisputeManagerStorage.sol new file mode 100644 index 0000000..11f3a14 --- /dev/null +++ b/l1-contracts/src/storage/UniFiAVSDisputeManagerStorage.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import "../structs/ValidatorData.sol"; + +/** + * @title UniFiAVSDisputeManagerStorage + * @author Puffer Finance + * @custom:security-contact security@puffer.fi + */ +abstract contract UniFiAVSDisputeManagerStorage { + /** + * @dev +-----------------------------------------------------------+ + * | | + * | DO NOT CHANGE, REORDER, REMOVE EXISTING STORAGE VARIABLES | + * | | + * +-----------------------------------------------------------+ + */ + struct UniFiAVSDisputeStorage { + // Slashed operators mapping + mapping(address operator => InvalidValidator[] slashedValidators) slashedOperators; + } + + /** + * @dev Storage slot location for UniFiAVSDisputeManager + * @custom:storage-location erc7201:UniFiAVSDisputeManager.storage + */ + bytes32 private constant _STORAGE_LOCATION = 0x5637C918091F80BAF4C11B7735D9493160412E6224AE5AFB3BEEAD3699789342; + + function _getUniFiAVSDisputeStorage() internal pure returns (UniFiAVSDisputeStorage storage $) { + // solhint-disable-next-line no-inline-assembly + assembly { + $.slot := _STORAGE_LOCATION + } + } +} diff --git a/l1-contracts/src/UniFiAVSManagerStorage.sol b/l1-contracts/src/storage/UniFiAVSManagerStorage.sol similarity index 66% rename from l1-contracts/src/UniFiAVSManagerStorage.sol rename to l1-contracts/src/storage/UniFiAVSManagerStorage.sol index fc1996a..62530f6 100644 --- a/l1-contracts/src/UniFiAVSManagerStorage.sol +++ b/l1-contracts/src/storage/UniFiAVSManagerStorage.sol @@ -2,8 +2,8 @@ pragma solidity >=0.8.0 <0.9.0; import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; -import "./structs/ValidatorData.sol"; -import "./structs/OperatorData.sol"; +import "../structs/ValidatorData.sol"; +import "../structs/OperatorData.sol"; /** * @title UniFiAVSManagerStorage @@ -11,6 +11,13 @@ import "./structs/OperatorData.sol"; * @custom:security-contact security@puffer.fi */ abstract contract UniFiAVSManagerStorage { + /** + * @dev +-----------------------------------------------------------+ + * | | + * | DO NOT CHANGE, REORDER, REMOVE EXISTING STORAGE VARIABLES | + * | | + * +-----------------------------------------------------------+ + */ struct UniFiAVSStorage { mapping(bytes32 => ValidatorData) validators; mapping(uint256 => bytes32) validatorIndexes; @@ -22,6 +29,10 @@ abstract contract UniFiAVSManagerStorage { mapping(uint256 => uint8) chainIdToBitmapIndex; // Set of allowlisted restaking strategies EnumerableSet.AddressSet allowlistedRestakingStrategies; + // Mapping to store validator registration data + mapping(bytes32 => ValidatorRegistrationData) validatorRegistrations; + // Delay (in blocks) after which a validator can be considered registered + uint64 registrationDelay; } /** diff --git a/l1-contracts/src/structs/OperatorData.sol b/l1-contracts/src/structs/OperatorData.sol index 156fb73..9e59501 100644 --- a/l1-contracts/src/structs/OperatorData.sol +++ b/l1-contracts/src/structs/OperatorData.sol @@ -44,5 +44,6 @@ struct OperatorDataExtended { uint128 commitmentValidAfter; /// @notice Whether the operator is registered or not. bool isRegistered; + /// @notice Whether the operator is slashed or not. + bool isSlashed; } -// 7 bytes padding here (automatically added by the compiler) diff --git a/l1-contracts/src/structs/ValidatorData.sol b/l1-contracts/src/structs/ValidatorData.sol index 8da2907..842e17a 100644 --- a/l1-contracts/src/structs/ValidatorData.sol +++ b/l1-contracts/src/structs/ValidatorData.sol @@ -2,6 +2,7 @@ pragma solidity >=0.8.0 <0.9.0; import "eigenlayer/interfaces/IEigenPod.sol"; +import { BN254 } from "eigenlayer-middleware/libraries/BN254.sol"; /** * @title ValidatorData @@ -32,13 +33,58 @@ struct ValidatorDataExtended { /// @notice The index of the validator in the beacon chain. uint64 validatorIndex; /// @notice The current status of the validator in the EigenPod. - IEigenPod.VALIDATOR_STATUS status; + IEigenPod.VALIDATOR_STATUS eigenPodStatus; /// @notice The delegate key currently associated with the validator's operator. bytes delegateKey; /// @notice Bitmap of chain IDs the validator's operator is committed to. uint256 chainIDBitMap; /// @notice Indicates whether the validator's EigenPod is currently delegated to the operator. - bool backedByStake; + bool backedByEigenPodStake; /// @notice Indicates whether the validator is currently registered (current block < registeredUntil). bool registered; } + +/** + * @title ValidatorRegistrationData + * @notice Struct to store registration-related data for a validator. + */ +struct ValidatorRegistrationData { + bytes32 registrationHash; + uint64 salt; + uint64 registeredAt; + uint64 activeAfter; +} + +/** + * @title ValidatorRegistrationParams + * @notice Struct to store parameters for validator registration. + */ +struct ValidatorRegistrationParams { + bytes32 blsPubKeyHash; + BN254.G1Point registrationSignature; + uint64 index; + uint256 expiry; + uint64 salt; +} + +/** + * @title ValidatorRegistrationSlashingParams + * @notice Struct to store parameters for validator registration slashing. + */ +struct ValidatorRegistrationSlashingParams { + BN254.G1Point pubkeyG1; + BN254.G2Point pubkeyG2; + BN254.G1Point registrationSignature; + uint64 index; + uint256 expiry; + uint64 salt; +} + +/** + * @title InvalidValidator + * @notice Struct to store information about a slashed validator. + */ +struct InvalidValidator { + address slashingBeneficiary; + bytes32 blsPubKeyHash; +} diff --git a/l1-contracts/test/UniFiAVSManager.t.sol b/l1-contracts/test/UniFiAVSManager.t.sol index 29e67a8..75c89a4 100644 --- a/l1-contracts/test/UniFiAVSManager.t.sol +++ b/l1-contracts/test/UniFiAVSManager.t.sol @@ -14,11 +14,14 @@ import "eigenlayer-middleware/libraries/BN254.sol"; import "eigenlayer-middleware/interfaces/IBLSApkRegistry.sol"; import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; import { UnitTestHelper } from "../test/helpers/UnitTestHelper.sol"; +import { BeaconProofs } from "./fixtures/BeaconProofs.sol"; contract UniFiAVSManagerTest is UnitTestHelper { using BN254 for BN254.G1Point; using Strings for uint256; + address constant BEACON_CHAIN_STRATEGY = 0xbeaC0eeEeeeeEEeEeEEEEeeEEeEeeeEeeEEBEaC0; + bytes delegatePubKey = abi.encodePacked(uint256(1337)); // TEST HELPERS @@ -56,6 +59,33 @@ contract UniFiAVSManagerTest is UnitTestHelper { g2Point.Y[0] = abi.decode(res, (uint256)); } + function _getValidatorSignature( + uint256 validatorPrivateKey, + uint64 validatorIndex, + address operator, + uint256 salt, + uint256 expiry + ) internal returns (BN254.G1Point memory registrationSignature, bytes32 pubkeyHash) { + // Generate BLS key pair + IBLSApkRegistry.PubkeyRegistrationParams memory blsKeyPair = _generateBlsPubkeyParams(validatorPrivateKey); + + // Create ValidatorRegistrationParams + ValidatorRegistrationParams memory params; + + params.salt = uint64(salt); + params.expiry = expiry; + params.index = validatorIndex; + + // Generate a valid signature + BN254.G1Point memory messagePoint = + avsManager.blsMessageHash(operator, params.salt, params.expiry, params.index); + + registrationSignature = messagePoint.scalar_mul(validatorPrivateKey); + pubkeyHash = BLSSignatureCheckerLib.hashG1Point(blsKeyPair.pubkeyG1); + + return (registrationSignature, pubkeyHash); + } + // With ECDSA key, he sign the hash confirming that the operator wants to be registered to a certain restaking service function _getOperatorSignature( uint256 _operatorPrivateKey, @@ -230,7 +260,7 @@ contract UniFiAVSManagerTest is UnitTestHelper { ValidatorDataExtended memory validatorData = avsManager.getValidator(blsPubKeyHashes[i]); assertEq(validatorData.eigenPod, address(mockEigenPodManager.getPod(podOwner))); assertEq(validatorData.operator, operator); - assertTrue(validatorData.backedByStake); + assertTrue(validatorData.backedByEigenPodStake); } } @@ -317,6 +347,7 @@ contract UniFiAVSManagerTest is UnitTestHelper { operatorData = avsManager.getOperator(operator); assertEq(operatorData.validatorCount, 0, "all validators should be deregistered"); + vm.roll(initialBlockNumber + avsManager.getRegistrationDelay() + 1); for (uint256 i = 0; i < blsPubKeyHashes.length; i++) { ValidatorDataExtended memory validatorData = avsManager.getValidator(blsPubKeyHashes[i]); assertTrue(validatorData.registered, "Validator should be registered"); @@ -506,7 +537,7 @@ contract UniFiAVSManagerTest is UnitTestHelper { assertEq(avsManager.getDeregistrationDelay(), newDelay, "Deregistration delay should be updated"); } - function testGetValidator_BackedByStakeFalse() public { + function testGetValidator_backedByEigenPodStakeFalse() public { bytes32[] memory blsPubKeyHashes = new bytes32[](1); blsPubKeyHashes[0] = keccak256(abi.encodePacked("validator1")); @@ -524,7 +555,10 @@ contract UniFiAVSManagerTest is UnitTestHelper { ValidatorDataExtended memory validatorData = avsManager.getValidator(blsPubKeyHashes[0]); assertEq(validatorData.operator, operator); - assertFalse(validatorData.backedByStake, "backedByStake should be false when delegated to a different address"); + assertFalse( + validatorData.backedByEigenPodStake, + "backedByEigenPodStake should be false when delegated to a different address" + ); } function testSetOperatorCommitment() public { @@ -767,19 +801,19 @@ contract UniFiAVSManagerTest is UnitTestHelper { _registerOperator(); // Set shares for the operator - mockDelegationManager.setShares(operator, IStrategy(avsManager.BEACON_CHAIN_STRATEGY()), 100); + mockDelegationManager.setShares(operator, IStrategy(BEACON_CHAIN_STRATEGY), 100); address[] memory restakedStrategies = avsManager.getOperatorRestakedStrategies(operator); assertEq(restakedStrategies.length, 1, "Should return one restaked strategy"); - assertEq(restakedStrategies[0], avsManager.BEACON_CHAIN_STRATEGY(), "Should return BEACON_CHAIN_STRATEGY"); + assertEq(restakedStrategies[0], BEACON_CHAIN_STRATEGY, "Should return BEACON_CHAIN_STRATEGY"); } function testGetRestakeableStrategies() public { address[] memory restakeableStrategies = avsManager.getRestakeableStrategies(); assertEq(restakeableStrategies.length, 1, "Should return one restakeable strategy"); - assertEq(restakeableStrategies[0], avsManager.BEACON_CHAIN_STRATEGY(), "Should return BEACON_CHAIN_STRATEGY"); + assertEq(restakeableStrategies[0], BEACON_CHAIN_STRATEGY, "Should return BEACON_CHAIN_STRATEGY"); } function testIsValidatorInChainId_AfterCommitmentChange() public { @@ -867,6 +901,7 @@ contract UniFiAVSManagerTest is UnitTestHelper { vm.prank(operator); avsManager.registerValidators(podOwner, blsPubKeyHashes); + vm.roll(block.number + avsManager.getRegistrationDelay() + 1); // Deregister the first validator bytes32[] memory deregisterFirst = new bytes32[](1); @@ -913,7 +948,7 @@ contract UniFiAVSManagerTest is UnitTestHelper { // Initially, only BEACON_CHAIN_STRATEGY should be allowlisted address[] memory initialStrategies = avsManager.getRestakeableStrategies(); assertEq(initialStrategies.length, 1); - assertEq(initialStrategies[0], avsManager.BEACON_CHAIN_STRATEGY()); + assertEq(initialStrategies[0], BEACON_CHAIN_STRATEGY); // Add a new strategy vm.prank(DAO); @@ -924,10 +959,7 @@ contract UniFiAVSManagerTest is UnitTestHelper { // Check that the new strategy is added address[] memory updatedStrategies = avsManager.getRestakeableStrategies(); assertEq(updatedStrategies.length, 2); - assertTrue( - updatedStrategies[0] == avsManager.BEACON_CHAIN_STRATEGY() - || updatedStrategies[1] == avsManager.BEACON_CHAIN_STRATEGY() - ); + assertTrue(updatedStrategies[0] == BEACON_CHAIN_STRATEGY || updatedStrategies[1] == BEACON_CHAIN_STRATEGY); assertTrue(updatedStrategies[0] == newStrategy || updatedStrategies[1] == newStrategy); // Remove the new strategy @@ -939,7 +971,7 @@ contract UniFiAVSManagerTest is UnitTestHelper { // Check that the strategy is removed address[] memory finalStrategies = avsManager.getRestakeableStrategies(); assertEq(finalStrategies.length, 1); - assertEq(finalStrategies[0], avsManager.BEACON_CHAIN_STRATEGY()); + assertEq(finalStrategies[0], BEACON_CHAIN_STRATEGY); // Try to remove newStrategy (should fail) vm.prank(DAO); @@ -969,7 +1001,7 @@ contract UniFiAVSManagerTest is UnitTestHelper { vm.stopPrank(); // Set shares for the operator - mockDelegationManager.setShares(operator, IStrategy(avsManager.BEACON_CHAIN_STRATEGY()), 100); + mockDelegationManager.setShares(operator, IStrategy(BEACON_CHAIN_STRATEGY), 100); mockDelegationManager.setShares(operator, IStrategy(newStrategy1), 200); // Note: We don't set shares for newStrategy2 @@ -977,8 +1009,7 @@ contract UniFiAVSManagerTest is UnitTestHelper { assertEq(restakedStrategies.length, 2, "Should return two restaked strategies"); assertTrue( - restakedStrategies[0] == avsManager.BEACON_CHAIN_STRATEGY() - || restakedStrategies[1] == avsManager.BEACON_CHAIN_STRATEGY(), + restakedStrategies[0] == BEACON_CHAIN_STRATEGY || restakedStrategies[1] == BEACON_CHAIN_STRATEGY, "Should include BEACON_CHAIN_STRATEGY" ); assertTrue( @@ -1015,11 +1046,335 @@ contract UniFiAVSManagerTest is UnitTestHelper { avsManager.setAllowlistRestakingStrategy(newStrategy, true); // Set shares for the operator - mockDelegationManager.setShares(operator, IStrategy(avsManager.BEACON_CHAIN_STRATEGY()), 100); + mockDelegationManager.setShares(operator, IStrategy(BEACON_CHAIN_STRATEGY), 100); mockDelegationManager.setShares(operator, IStrategy(newStrategy), 200); address[] memory restakedStrategies = avsManager.getOperatorRestakedStrategies(operator); assertEq(restakedStrategies.length, 0, "Should return no restaked strategies for unregistered operator"); } + + function testRegisterValidatorsOptimistically() public { + _setupOperator(); + _registerOperator(); + + uint256 validatorPrivateKey1 = 123; + uint256 validatorPrivateKey2 = 456; + uint64 validatorIndex1 = 1; + uint64 validatorIndex2 = 2; + uint64 salt1 = 1; + uint64 salt2 = 2; + uint256 expiry = block.timestamp + 1 days; + + (BN254.G1Point memory signature1, bytes32 pubkeyHash1) = + _getValidatorSignature(validatorPrivateKey1, validatorIndex1, operator, salt1, expiry); + + (BN254.G1Point memory signature2, bytes32 pubkeyHash2) = + _getValidatorSignature(validatorPrivateKey2, validatorIndex2, operator, salt2, expiry); + + ValidatorRegistrationParams[] memory paramsArray = new ValidatorRegistrationParams[](2); + paramsArray[0] = ValidatorRegistrationParams({ + blsPubKeyHash: pubkeyHash1, + registrationSignature: signature1, + index: validatorIndex1, + expiry: expiry, + salt: salt1 + }); + + paramsArray[1] = ValidatorRegistrationParams({ + blsPubKeyHash: pubkeyHash2, + registrationSignature: signature2, + index: validatorIndex2, + expiry: expiry, + salt: salt2 + }); + + vm.prank(operator); + avsManager.registerValidatorsOptimistically(paramsArray); + + OperatorDataExtended memory operatorData = avsManager.getOperator(operator); + assertEq(operatorData.validatorCount, 2, "Validator count should be 2"); + vm.roll(block.number + avsManager.getRegistrationDelay() + 1); + + ValidatorDataExtended memory validator1 = avsManager.getValidator(pubkeyHash1); + ValidatorDataExtended memory validator2 = avsManager.getValidator(pubkeyHash2); + + assertTrue(validator1.registered, "Validator 1 should be registered"); + assertTrue(validator2.registered, "Validator 2 should be registered"); + assertEq(validator1.operator, operator, "Validator 1 should be assigned to the correct operator"); + assertEq(validator2.operator, operator, "Validator 2 should be assigned to the correct operator"); + } + + function testslashValidatorsWithInvalidSignature() public { + _setupOperator(); + _registerOperator(); + + uint256 validatorPrivateKey = 123; + uint64 validatorIndex = 1; + uint64 salt = 1; + uint256 expiry = block.timestamp + 1 days; + + (BN254.G1Point memory signature, bytes32 pubkeyHash) = + _getValidatorSignature(validatorPrivateKey, validatorIndex, operator, salt, expiry); + + ValidatorRegistrationParams[] memory paramsArray = new ValidatorRegistrationParams[](1); + paramsArray[0] = ValidatorRegistrationParams({ + blsPubKeyHash: pubkeyHash, + registrationSignature: signature, + index: validatorIndex, + expiry: expiry, + salt: salt + }); + + vm.prank(operator); + avsManager.registerValidatorsOptimistically(paramsArray); + + ValidatorRegistrationSlashingParams[] memory slashingParams = new ValidatorRegistrationSlashingParams[](1); + slashingParams[0] = ValidatorRegistrationSlashingParams({ + pubkeyG1: _generateBlsPubkeyParams(validatorPrivateKey).pubkeyG1, + pubkeyG2: _generateBlsPubkeyParams(validatorPrivateKey).pubkeyG2, + registrationSignature: signature, + index: validatorIndex, + expiry: expiry, + salt: salt + }); + + avsManager.slashValidatorsWithInvalidSignature(slashingParams); + vm.roll(block.number + avsManager.getRegistrationDelay() + 1); + + ValidatorDataExtended memory validator = avsManager.getValidator(pubkeyHash); + assertTrue(validator.registered, "Validator should still be registered after verification"); + } + + function testslashValidatorsWithInvalidSignature_InvalidSignature() public { + _setupOperator(); + _registerOperator(); + + uint256 validatorPrivateKey = 123; + uint64 validatorIndex = 1; + uint64 salt = 1; + uint256 expiry = block.timestamp + 1 days; + + (BN254.G1Point memory validSignature, bytes32 pubkeyHash) = + _getValidatorSignature(validatorPrivateKey, validatorIndex, operator, salt, expiry); + + // Create an invalid signature by using a different private key + (BN254.G1Point memory invalidSignature,) = _getValidatorSignature( + 456, // Different private key + validatorIndex, + operator, + salt, + expiry + ); + + ValidatorRegistrationParams[] memory paramsArray = new ValidatorRegistrationParams[](1); + paramsArray[0] = ValidatorRegistrationParams({ + blsPubKeyHash: pubkeyHash, + registrationSignature: invalidSignature, + index: validatorIndex, + expiry: expiry, + salt: salt + }); + + vm.prank(operator); + avsManager.registerValidatorsOptimistically(paramsArray); + + ValidatorRegistrationSlashingParams[] memory slashingParams = new ValidatorRegistrationSlashingParams[](1); + slashingParams[0] = ValidatorRegistrationSlashingParams({ + pubkeyG1: _generateBlsPubkeyParams(validatorPrivateKey).pubkeyG1, + pubkeyG2: _generateBlsPubkeyParams(validatorPrivateKey).pubkeyG2, + registrationSignature: invalidSignature, + index: validatorIndex, + expiry: expiry, + salt: salt + }); + + vm.expectEmit(true, true, false, false); + emit IUniFiAVSManager.ValidatorSlashed(operator, pubkeyHash); + + avsManager.slashValidatorsWithInvalidSignature(slashingParams); + + vm.roll(block.number + avsManager.getDeregistrationDelay() + 1); + ValidatorDataExtended memory validator = avsManager.getValidator(pubkeyHash); + assertFalse(validator.registered, "Validator should be deregistered after invalid signature verification"); + + OperatorDataExtended memory operatorData = avsManager.getOperator(operator); + assertEq(operatorData.validatorCount, 0, "Operator should have no validators after slashing"); + } + + function testVerifyValidatorOnBeaconChainValidProofAndValidator() public { + _setupOperator(); + _registerOperator(); + vm.warp(block.timestamp + 50); + // Register the validator optimistically + ValidatorRegistrationParams[] memory paramsArray = new ValidatorRegistrationParams[](1); + bytes32 pubkeyHash = sha256(abi.encodePacked(BeaconProofs.validator(), bytes16(0))); + + paramsArray[0] = ValidatorRegistrationParams({ + blsPubKeyHash: pubkeyHash, + salt: 1, + expiry: block.timestamp + 1 days, + index: uint64(BeaconProofs.validatorIndex()), + registrationSignature: _generateBlsPubkeyParams(123).pubkeyG1 // Use a dummy value for the signature + }); + + vm.prank(operator); + avsManager.registerValidatorsOptimistically(paramsArray); + + // Prepare the data for verification + bytes32[] memory blsPubKeyHashes = new bytes32[](1); + blsPubKeyHashes[0] = pubkeyHash; + + BeaconChainHelperLib.InclusionProof[] memory proofs = new BeaconChainHelperLib.InclusionProof[](1); + + proofs[0] = BeaconProofs.eip4788ValidatorInclusionProof(); + proofs[0].timestamp = block.number + avsManager.getRegistrationDelay() + 1; + // Mock the getRootFromTimestamp function to return the expected beacon block root + vm.mockCall( + address(0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02), + hex"00000000", + abi.encode(BeaconProofs.beaconBlockRoot()) + ); + + // Verify the validator on the beacon chain + avsManager.slashValidatorsWithInvalidIndex(proofs); + vm.roll(block.number + avsManager.getRegistrationDelay() + 1); + + // Check that the validator is still registered + ValidatorDataExtended memory validator = avsManager.getValidator(blsPubKeyHashes[0]); + assertTrue(validator.registered, "Validator should still be registered after successful verification"); + } + + function testSlashValidatorsWithInvalidIndex() public { + _setupOperator(); + _registerOperator(); + vm.warp(block.timestamp + 50); + // Register the validator optimistically + ValidatorRegistrationParams[] memory paramsArray = new ValidatorRegistrationParams[](1); + bytes32 pubkeyHash = sha256(abi.encodePacked(BeaconProofs.validator(), bytes16(0))); + + paramsArray[0] = ValidatorRegistrationParams({ + blsPubKeyHash: pubkeyHash, + salt: 1, + expiry: block.timestamp + 1 days, + index: 1, // Use a different index + registrationSignature: _generateBlsPubkeyParams(123).pubkeyG1 // Use a dummy value for the signature + }); + + vm.prank(operator); + avsManager.registerValidatorsOptimistically(paramsArray); + + // Prepare the data for verification + bytes32[] memory blsPubKeyHashes = new bytes32[](1); + blsPubKeyHashes[0] = pubkeyHash; + + BeaconChainHelperLib.InclusionProof[] memory proofs = new BeaconChainHelperLib.InclusionProof[](1); + proofs[0] = BeaconProofs.eip4788ValidatorInclusionProof(); + proofs[0].timestamp = block.number + avsManager.getRegistrationDelay() + 1; + // Mock the getRootFromTimestamp function to return the expected beacon block root + vm.mockCall( + address(0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02), + hex"00000000", + abi.encode(BeaconProofs.beaconBlockRoot()) + ); + + // Verify the validator on the beacon chain + avsManager.slashValidatorsWithInvalidIndex(proofs); + + // Check that the validator is still registered + ValidatorDataExtended memory validator = avsManager.getValidator(blsPubKeyHashes[0]); + assertFalse(validator.registered, "Validator should be deregistered after slashing"); + + OperatorDataExtended memory operatorData = avsManager.getOperator(operator); + assertEq(operatorData.validatorCount, 0, "Operator should have no validators after slashing"); + } + + function testSlashValidatorsWithInvalidPubkey() public { + _setupOperator(); + _registerOperator(); + vm.warp(block.timestamp + 50); + // Register the validator optimistically + ValidatorRegistrationParams[] memory paramsArray = new ValidatorRegistrationParams[](1); + bytes32 pubkeyHash = sha256(abi.encodePacked(BeaconProofs.validator(), bytes16(0))); + + paramsArray[0] = ValidatorRegistrationParams({ + blsPubKeyHash: bytes32(uint256(1234)), + salt: 1, + expiry: block.timestamp + 1 days, + index: uint64(BeaconProofs.validatorIndex()), + registrationSignature: _generateBlsPubkeyParams(123).pubkeyG1 // Use a dummy value for the signature + }); + + vm.prank(operator); + avsManager.registerValidatorsOptimistically(paramsArray); + + // Prepare the data for verification + bytes32[] memory blsPubKeyHashes = new bytes32[](1); + blsPubKeyHashes[0] = pubkeyHash; + + BeaconChainHelperLib.InclusionProof[] memory proofs = new BeaconChainHelperLib.InclusionProof[](1); + proofs[0] = BeaconProofs.eip4788ValidatorInclusionProof(); + proofs[0].timestamp = block.number + avsManager.getRegistrationDelay() + 1; + // Mock the getRootFromTimestamp function to return the expected beacon block root + vm.mockCall( + address(0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02), + hex"00000000", + abi.encode(BeaconProofs.beaconBlockRoot()) + ); + + // Verify the validator on the beacon chain + avsManager.slashValidatorsWithInvalidPubkey(proofs); + + // Check that the validator is still registered + ValidatorDataExtended memory validator = avsManager.getValidator(bytes32(uint256(1234))); + assertFalse(validator.registered, "Validator should be deregistered after slashing"); + + OperatorDataExtended memory operatorData = avsManager.getOperator(operator); + assertEq(operatorData.validatorCount, 0, "Operator should have no validators after slashing"); + } + + function testSlashValidatorsWithInvalidProof() public { + _setupOperator(); + _registerOperator(); + vm.warp(block.timestamp + 50); + // Register the validator optimistically + ValidatorRegistrationParams[] memory paramsArray = new ValidatorRegistrationParams[](1); + bytes32 pubkeyHash = sha256(abi.encodePacked(BeaconProofs.validator(), bytes16(0))); + paramsArray[0] = ValidatorRegistrationParams({ + blsPubKeyHash: pubkeyHash, + salt: 1, + expiry: block.timestamp + 1 days, + index: uint64(BeaconProofs.validatorIndex()), + registrationSignature: _generateBlsPubkeyParams(123).pubkeyG1 // Use a dummy value for the signature + }); + + vm.prank(operator); + avsManager.registerValidatorsOptimistically(paramsArray); + + // Prepare the data for verification + bytes32[] memory blsPubKeyHashes = new bytes32[](1); + blsPubKeyHashes[0] = pubkeyHash; + + BeaconChainHelperLib.InclusionProof[] memory proofs = new BeaconChainHelperLib.InclusionProof[](1); + proofs[0] = BeaconProofs.eip4788ValidatorInclusionProof(); + + // Mock the getRootFromTimestamp function to return an incorrect beacon block root + vm.mockCall( + address(0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02), + hex"00000000", + abi.encode(bytes32(uint256(1))) // Incorrect beacon block root + ); + + // Expect the verification to fail + vm.expectRevert(abi.encodeWithSelector(IUniFiAVSManager.InvalidValidatorProof.selector, pubkeyHash)); + avsManager.slashValidatorsWithInvalidPubkey(proofs); + + // Check that the validator is not slashed nor deregistered + vm.roll(block.number + avsManager.getDeregistrationDelay() + 1); + ValidatorDataExtended memory validator = avsManager.getValidator(blsPubKeyHashes[0]); + assertTrue(validator.registered, "Validator should be still registered after failed slashing"); + + OperatorDataExtended memory operatorData = avsManager.getOperator(operator); + assertEq(operatorData.validatorCount, 1, "Operator should have 1 validator after failed slashing"); + } } diff --git a/l1-contracts/test/fixtures/BeaconProofs.sol b/l1-contracts/test/fixtures/BeaconProofs.sol new file mode 100644 index 0000000..06d2b9a --- /dev/null +++ b/l1-contracts/test/fixtures/BeaconProofs.sol @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.0 <0.9.0; + +import { BeaconChainHelperLib } from "../../src/lib/BeaconChainHelperLib.sol"; + +/// @dev Data has been taken from beacon block at slot 9000000 on Ethereum mainnet +library BeaconProofs { + // validator public bls key + function validator() internal pure returns (bytes memory) { + return hex"98fb8eacf684f80712faa9354535620f94a10687c2243c0cdae7280cf6220fb64c78e49efe8eef599406b33e5aac4dd0"; + } + + function validatorsRoot() internal pure returns (bytes32) { + return 0x0ccf56d8e76d16306c6e6e78ec20c07be5fa5ae89b18873b43cc823075a5df0b; + } + + function validatorIndex() internal pure returns (uint256) { + return 912203; + } + + function beaconStateRoot() internal pure returns (bytes32) { + return 0xcd918afbe365c6dcabab551e32fae5f3f9677433876049dc035e5135122a2e7e; + } + + function beaconBlockRoot() internal pure returns (bytes32) { + return 0xcc8a36da0d5112c8dd602530ac7c7b8364edfd92cdc6f0d62365de392e8e5bb6; + } + + function validatorChunks() internal pure returns (bytes32[8] memory) { + bytes32[8] memory chunks; + chunks[0] = 0x8d7c2b324f41a1d395fc265d42c6e1293b38c33a674244cae9ac67d68367036d; // bls key hash + chunks[1] = 0x0100000000000000000000006661be71769ff00c5e403f327869505caf0b7f70; + chunks[2] = 0x0040597307000000000000000000000000000000000000000000000000000000; + chunks[3] = 0x0000000000000000000000000000000000000000000000000000000000000000; + chunks[4] = 0xe271030000000000000000000000000000000000000000000000000000000000; + chunks[5] = 0x6084030000000000000000000000000000000000000000000000000000000000; + chunks[6] = 0xffffffffffffffff000000000000000000000000000000000000000000000000; + chunks[7] = 0xffffffffffffffff000000000000000000000000000000000000000000000000; + + return chunks; + } + + function validatorProof() internal pure returns (bytes32[] memory) { + bytes32[] memory proof = new bytes32[](41); + proof[0] = 0xf5ee350215176477a7fb48aa80292de237856ad3068f46728da26aedca8a3b2b; + proof[1] = 0xfbeca4cff4f86c2ff5f1ff6808f57b12e7a6f3365d59a35c90f19715995f8be8; + proof[2] = 0x06ee0000b0cf0c0531c2a4f3368eb8df6079216bb6cca127a76e459c62058615; + proof[3] = 0x5b8c291888e7936b46e36d7b71d36c846fbfc04d48cab6beb20e23642f64ee69; + proof[4] = 0xa748ed979e88b53c303ece0946d13d2def12e003b90b562474dac1768d1d0975; + proof[5] = 0xe667bf725f0e72f47409d089248b50a9a11d08591b83374f18ed338f5c3ff964; + proof[6] = 0xd86b77a649fad1d48e109b8bc98d2a2dbc88a4b9b86c5e06878e0b980ebda3b7; + proof[7] = 0xc2db7c18d080f2b21f2c981f65414d00b0cc8542fda38233fa1c1ee33df4bbe1; + proof[8] = 0xe72e80d2ce704957f507af587e19a61ceadad2411c9728315e1f294fadae23f1; + proof[9] = 0x32f30ee3311d96e0544e2e4b0f4e1e1863d06224636ea8004e49a27280a81a11; + proof[10] = 0x89d191926d7681be7545b42b9ef95d413fbe1d8c014400c5ece8141be300b238; + proof[11] = 0x0c924ac306b692750b3285f974edf991dd4f05fff0ab3dd114430499722ff93b; + proof[12] = 0x1eb9a358bbe044159a2bed16a0b69b5b988ba0c57f2c267cfd390b3fb86fde6a; + proof[13] = 0xda60132f38fc053c26ba06136e03a861fd5e59734dc3e6cc1b69c072b9ce600a; + proof[14] = 0xcee182aa676671046ccf49213a58ef8d35e227a3adfaa146f7b71dc47c7bdd73; + proof[15] = 0xf1d0df094ceceed165886daf4c52c467710ed19a53df98ab2607629dbf7036ba; + proof[16] = 0x81917306117277e02aa4174ae73a2ec414862aced0491ec933434d9bd2279e3f; + proof[17] = 0xc562f7ffddaec138272a84b043216c1c906f68198f752ad6b80171794fcba3b5; + proof[18] = 0xcdfeaaff006b40d110ff925b18bffc36cf55543a35c84d25da0b196ea81c6029; + proof[19] = 0x8bd5e9cadc78cd0b0e0abd32a63a39596ad24e14552926bce0f6c54e39c29b99; + proof[20] = 0x6187b4f2f4b3e572fe26a6c73567ab5b1695303b0ad9dd5c9ab9679266fba2e3; + proof[21] = 0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c; + proof[22] = 0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167; + proof[23] = 0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7; + proof[24] = 0x31206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc0; + proof[25] = 0x21352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544; + proof[26] = 0x619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a46765; + proof[27] = 0x7cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4; + proof[28] = 0x848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe1; + proof[29] = 0x8869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636; + proof[30] = 0xb5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c; + proof[31] = 0x985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7; + proof[32] = 0xc6f67e02e6e4e1bdefb994c6098953f34636ba2b6ca20a4721d2b26a886722ff; + proof[33] = 0x1c9a7e5ff1cf48b4ad1582d3f4e4a1004f3b20d8c5a2b71387a4254ad933ebc5; + proof[34] = 0x2f075ae229646b6f6aed19a5e372cf295081401eb893ff599b3f9acc0c0d3e7d; + proof[35] = 0x328921deb59612076801e8cd61592107b5c67c79b846595cc6320c395b46362c; + proof[36] = 0xbfb909fdb236ad2411b4e4883810a074b840464689986c3f8a8091827e17c327; + proof[37] = 0x55d8fb3687ba3ba49f342c77f5a1f89bec83d811446e1a467139213d640b6a74; + proof[38] = 0xf7210d4f8e7e1039790e7bf4efa207555a10a6db1dd4b95da313aaa88b88fe76; + proof[39] = 0xad21b516cbc645ffe34ab5de1c8aef8cd4e7f8d2b51e8e1456adc7563cda206f; + proof[40] = 0x2821150000000000000000000000000000000000000000000000000000000000; + + return proof; + } + + function beaconStateProofForValidatorList() internal pure returns (bytes32[] memory) { + bytes32[] memory proof = new bytes32[](5); + proof[0] = 0x8c53160000000000000000000000000000000000000000000000000000000000; + proof[1] = 0xd9cb62ffd113d2a2b71b4539c54bf01587d8a2a5a7c81baa2c2ae89d245578d6; + proof[2] = 0xefbad4c97640101fc18122e8b818e8cc3c278a18e05dc601af4095d5519d834a; + proof[3] = 0x775d61d75ab0731115447847764383a42283b502eb4ed3ca7ba412ac67da5138; + proof[4] = 0xbb5cf5c0273b8d100f329ea0c78c471d0833f048c7fc264c285c3696d7aed412; + + return proof; + } + + function beaconBlockProofForBeaconState() internal pure returns (bytes32[] memory) { + bytes32[] memory proof = new bytes32[](3); + proof[0] = 0xf47de6dfa04049ce0586d989821321111d896f3cc37e40637fc226bee212e43d; + proof[1] = 0x7506bc99ed6f0e48ad0e1ded3e878dfcfe08ca4a89308910ba1941e912673258; + proof[2] = 0x00f48b46fd6aac7f8a72d8e1eed4f3b5bd244bf6242cb538ca94b44aed02857a; + + return proof; + } + + function beaconBlockProofForProposer() internal pure returns (bytes32[] memory) { + bytes32[] memory proof = new bytes32[](3); + proof[0] = 0x4054890000000000000000000000000000000000000000000000000000000000; + proof[1] = 0xd22083672621f940e26b3f1e627f8c311a3f5f0874c193b40974f244668e1372; + proof[2] = 0x00f48b46fd6aac7f8a72d8e1eed4f3b5bd244bf6242cb538ca94b44aed02857a; + + return proof; + } + + function eip4788ValidatorInclusionProof() internal view returns (BeaconChainHelperLib.InclusionProof memory) { + return BeaconChainHelperLib.InclusionProof({ + validator: validatorChunks(), + validatorIndex: validatorIndex(), + validatorProof: validatorProof(), + validatorsRoot: validatorsRoot(), + beaconStateProof: beaconStateProofForValidatorList(), + beaconStateRoot: beaconStateRoot(), + beaconBlockProofForState: beaconBlockProofForBeaconState(), + beaconBlockProofForProposerIndex: beaconBlockProofForProposer(), + timestamp: block.timestamp + }); + } +} diff --git a/l1-contracts/test/forks/UniFiAVSManagerForkTest.sol b/l1-contracts/test/forks/UniFiAVSManagerForkTest.sol index c3c07c1..261fff5 100644 --- a/l1-contracts/test/forks/UniFiAVSManagerForkTest.sol +++ b/l1-contracts/test/forks/UniFiAVSManagerForkTest.sol @@ -55,6 +55,7 @@ contract UniFiAVSManagerForkTest is Test, BaseScript { uint256 public operatorPrivateKey; address public DAO = 0xC0896ab1A8cae8c2C1d27d011eb955Cca955580d; + address constant BEACON_CHAIN_STRATEGY = 0xbeaC0eeEeeeeEEeEeEEEEeeEEeEeeeEeeEEBEaC0; function setUp() public virtual { vm.createSelectFork(vm.rpcUrl("mainnet"), 20731077); // Replace with an appropriate block number @@ -362,7 +363,7 @@ contract UniFiAVSManagerForkTest is Test, BaseScript { // Assert assertEq(restakedStrategies.length, 1, "Should have one restaked strategy"); - assertEq(restakedStrategies[0], avsManager.BEACON_CHAIN_STRATEGY(), "Should be the Beacon Chain strategy"); + assertEq(restakedStrategies[0], BEACON_CHAIN_STRATEGY, "Should be the Beacon Chain strategy"); } function test_getRestakeableStrategies() public { @@ -371,7 +372,7 @@ contract UniFiAVSManagerForkTest is Test, BaseScript { // Assert assertEq(restakeableStrategies.length, 1, "Should have one restakeable strategy"); - assertEq(restakeableStrategies[0], avsManager.BEACON_CHAIN_STRATEGY(), "Should be the Beacon Chain strategy"); + assertEq(restakeableStrategies[0], BEACON_CHAIN_STRATEGY, "Should be the Beacon Chain strategy"); } function _registerOperator() internal { diff --git a/l1-contracts/test/helpers/UnitTestHelper.sol b/l1-contracts/test/helpers/UnitTestHelper.sol index d17791d..9a70135 100644 --- a/l1-contracts/test/helpers/UnitTestHelper.sol +++ b/l1-contracts/test/helpers/UnitTestHelper.sol @@ -11,7 +11,7 @@ import "../mocks/MockDelegationManager.sol"; import "../mocks/MockAVSDirectory.sol"; import { UpgradeableBeacon } from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; import { AccessManager } from "@openzeppelin/contracts/access/manager/AccessManager.sol"; -import "forge-std/console.sol"; +import { UniFiAVSDisputeManager } from "../../src/UniFiAVSDisputeManager.sol"; contract UnitTestHelper is Test, BaseScript { address public constant ADDRESS_ZERO = address(0); @@ -28,6 +28,7 @@ contract UnitTestHelper is Test, BaseScript { MockEigenPodManager public mockEigenPodManager; MockDelegationManager public mockDelegationManager; MockAVSDirectory public mockAVSDirectory; + UniFiAVSDisputeManager public disputeManager; address public DAO = makeAddr("DAO"); address public COMMUNITY_MULTISIG = makeAddr("communityMultisig"); @@ -79,7 +80,7 @@ contract UnitTestHelper is Test, BaseScript { address(mockAVSDirectory), DEREGISTRATION_DELAY ); - + disputeManager = UniFiAVSDisputeManager(avsDeployment.disputeManagerProxy); // accessManager = AccessManager(avsDeployment.accessManager); timelock = avsDeployment.timelock; avsManager = UniFiAVSManager(avsDeployment.avsManagerProxy);