diff --git a/contracts/SSVNetwork.sol b/contracts/SSVNetwork.sol index 5d016d1b..337b95fc 100644 --- a/contracts/SSVNetwork.sol +++ b/contracts/SSVNetwork.sol @@ -154,6 +154,14 @@ contract SSVNetwork is _delegate(SSVStorage.load().ssvContracts[SSVModules.SSV_OPERATORS_WHITELIST]); } + function setOperatorsPrivateUnchecked(uint64[] calldata operatorIds) external override { + _delegate(SSVStorage.load().ssvContracts[SSVModules.SSV_OPERATORS_WHITELIST]); + } + + function setOperatorsPublicUnchecked(uint64[] calldata operatorIds) external { + _delegate(SSVStorage.load().ssvContracts[SSVModules.SSV_OPERATORS_WHITELIST]); + } + function removeOperatorsWhitelistingContract(uint64[] calldata operatorIds) external override { _delegate(SSVStorage.load().ssvContracts[SSVModules.SSV_OPERATORS_WHITELIST]); } diff --git a/contracts/interfaces/ISSVOperatorsWhitelist.sol b/contracts/interfaces/ISSVOperatorsWhitelist.sol index 98e9d1be..865853b3 100644 --- a/contracts/interfaces/ISSVOperatorsWhitelist.sol +++ b/contracts/interfaces/ISSVOperatorsWhitelist.sol @@ -38,6 +38,17 @@ interface ISSVOperatorsWhitelist is ISSVNetworkCore { /// @param operatorIds The operator IDs to remove the whitelisting contract for function removeOperatorsWhitelistingContract(uint64[] calldata operatorIds) external; + /// @notice Set the list of operators as private without checking for any whitelisting address + /// @notice The operators are considered private when registering validators + /// @param operatorIds The operator IDs to set as private + function setOperatorsPrivateUnchecked(uint64[] calldata operatorIds) external; + + /// @notice Set the list of operators as public without removing any whitelisting address + /// @notice The operators still keep its adresses whitelisted (external contract or EOAs/generic contracts) + /// @notice The operators are considered public when registering validators + /// @param operatorIds The operator IDs to set as public + function setOperatorsPublicUnchecked(uint64[] calldata operatorIds) external; + /** * @dev Emitted when the whitelist of an operator is updated. * @param operatorId operator's ID. @@ -65,4 +76,11 @@ interface ISSVOperatorsWhitelist is ISSVNetworkCore { * @param whitelistingContract operators' new whitelisting contract address. */ event OperatorWhitelistingContractUpdated(uint64[] operatorIds, address whitelistingContract); + + /** + * @dev Emitted when the operators changed its privacy status + * @param operatorIds operators' IDs. + * @param toPrivate Flag that indicates if the operators are being set to private (true) or public (false). + */ + event OperatorPrivacyStatusUpdated(uint64[] operatorIds, bool toPrivate); } diff --git a/contracts/libraries/OperatorLib.sol b/contracts/libraries/OperatorLib.sol index 80ff0183..7c78c600 100644 --- a/contracts/libraries/OperatorLib.sol +++ b/contracts/libraries/OperatorLib.sol @@ -146,10 +146,9 @@ library OperatorLib { StorageData storage s ) internal { uint256 addressesLength = whitelistAddresses.length; - uint256 operatorsLength = operatorIds.length; - if (addressesLength == 0) revert ISSVNetworkCore.InvalidWhitelistAddressesLength(); - if (operatorsLength == 0) revert ISSVNetworkCore.InvalidOperatorIdsLength(); + + uint256 operatorsLength = getOperatorsLength(operatorIds); ISSVNetworkCore.Operator storage operator; for (uint256 i; i < operatorsLength; ++i) { @@ -187,27 +186,6 @@ library OperatorLib { } } - function updateWhitelistingContract( - uint64 operatorId, - ISSVWhitelistingContract whitelistingContract, - StorageData storage s - ) internal { - checkOwner(s.operators[operatorId]); - - address currentWhitelisted = s.operatorsWhitelist[operatorId]; - - // operator already whitelisted? EOA or generic contract - if (currentWhitelisted != address(0)) { - (uint256 blockIndex, uint256 bitPosition) = OperatorLib.getBitmapIndexes(operatorId); - delete s.operatorsWhitelist[operatorId]; - s.addressWhitelistedForOperators[currentWhitelisted][blockIndex] |= (1 << bitPosition); - } else { - s.operators[operatorId].whitelisted = true; - } - - s.operatorsWhitelist[operatorId] = address(whitelistingContract); - } - function generateBlockMasks(uint64[] calldata operatorIds) internal pure returns (uint256[] memory masks) { uint256 blockIndex; uint256 bitPosition; @@ -234,6 +212,19 @@ library OperatorLib { } } + function updatePrivacyStatus(uint64[] calldata operatorIds, bool setPrivate, StorageData storage s) internal { + uint256 operatorsLength = getOperatorsLength(operatorIds); + + ISSVNetworkCore.Operator storage operator; + for (uint256 i; i < operatorsLength; ++i) { + uint64 operatorId = operatorIds[i]; + operator = s.operators[operatorId]; + checkOwner(operator); + + operator.whitelisted = setPrivate; + } + } + function getBitmapIndexes(uint64 operatorId) internal pure returns (uint256 blockIndex, uint256 bitPosition) { blockIndex = operatorId >> 8; // Equivalent to operatorId / 256 bitPosition = operatorId & 0xFF; // Equivalent to operatorId % 256 @@ -243,6 +234,11 @@ library OperatorLib { if (whitelistAddress == address(0)) revert ISSVNetworkCore.ZeroAddressNotAllowed(); } + function getOperatorsLength(uint64[] calldata operatorIds) internal pure returns (uint256 operatorsLength) { + operatorsLength = operatorIds.length; + if (operatorsLength == 0) revert ISSVNetworkCore.InvalidOperatorIdsLength(); + } + function isWhitelistingContract(address whitelistingContract) internal view returns (bool) { return ERC165Checker.supportsInterface(whitelistingContract, type(ISSVWhitelistingContract).interfaceId); } diff --git a/contracts/modules/SSVOperatorsWhitelist.sol b/contracts/modules/SSVOperatorsWhitelist.sol index 7ac763d1..6794ad25 100644 --- a/contracts/modules/SSVOperatorsWhitelist.sol +++ b/contracts/modules/SSVOperatorsWhitelist.sol @@ -58,8 +58,7 @@ contract SSVOperatorsWhitelist is ISSVOperatorsWhitelist { // Reverts also when whitelistingContract == address(0) if (!OperatorLib.isWhitelistingContract(address(whitelistingContract))) revert InvalidWhitelistingContract(); - uint256 operatorsLength = operatorIds.length; - if (operatorsLength == 0) revert InvalidOperatorIdsLength(); + uint256 operatorsLength = OperatorLib.getOperatorsLength(operatorIds); StorageData storage s = SSVStorage.load(); Operator storage operator; @@ -88,8 +87,7 @@ contract SSVOperatorsWhitelist is ISSVOperatorsWhitelist { } function removeOperatorsWhitelistingContract(uint64[] calldata operatorIds) external { - uint256 operatorsLength = operatorIds.length; - if (operatorsLength == 0) revert InvalidOperatorIdsLength(); + uint256 operatorsLength = OperatorLib.getOperatorsLength(operatorIds); StorageData storage s = SSVStorage.load(); Operator storage operator; @@ -105,4 +103,14 @@ contract SSVOperatorsWhitelist is ISSVOperatorsWhitelist { emit OperatorWhitelistingContractUpdated(operatorIds, address(0)); } + + function setOperatorsPrivateUnchecked(uint64[] calldata operatorIds) external override { + OperatorLib.updatePrivacyStatus(operatorIds, true, SSVStorage.load()); + emit OperatorPrivacyStatusUpdated(operatorIds, true); + } + + function setOperatorsPublicUnchecked(uint64[] calldata operatorIds) external override { + OperatorLib.updatePrivacyStatus(operatorIds, false, SSVStorage.load()); + emit OperatorPrivacyStatusUpdated(operatorIds, false); + } } diff --git a/hardhat.config.ts b/hardhat.config.ts index fcf7ba4c..9113fe40 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -46,6 +46,11 @@ const config: HardhatUserConfig = { ssvToken: process.env.SSVTOKEN_ADDRESS, // if empty, deploy SSV mock token } as SSVNetworkConfig, hardhat: { + forking: { + enabled: process.env.FORK_TESTING_ENABLED ? true : false, + url: "https://mainnet.infura.io/v3/1810bc4fb927499990638f8451a455e4", + blockNumber: 19621100, + }, allowUnlimitedContractSize: true, }, }, diff --git a/package.json b/package.json index 221fa6f8..c1bcd8d3 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "scripts": { "build": "npx hardhat compile", "test": "npx hardhat test", + "fork-test": "FORK_TESTING_ENABLED=true npx hardhat test", "lint": "eslint . --ext .ts", "lint:fix": "eslint --fix . --ext .ts", "solidity-coverage": "SOLIDITY_COVERAGE=true NO_GAS_ENFORCE=1 npx hardhat coverage", diff --git a/test/helpers/contract-helpers.ts b/test/helpers/contract-helpers.ts index 4f3e38e9..a30913da 100644 --- a/test/helpers/contract-helpers.ts +++ b/test/helpers/contract-helpers.ts @@ -307,6 +307,16 @@ export const reactivate = async function (ownerId: number, operatorIds: number[] return reactivatedCluster.eventsByName.ClusterReactivated[0].args; }; +export const getTransactionReceipt = async function (tx: Promise) { + const hash = await tx; + + const receipt = await publicClient.waitForTransactionReceipt({ + hash, + }); + + return receipt; +}; + async function initialize() { publicClient = await hre.viem.getPublicClient(); } diff --git a/test/helpers/gas-usage.ts b/test/helpers/gas-usage.ts index 980ac555..44674a00 100644 --- a/test/helpers/gas-usage.ts +++ b/test/helpers/gas-usage.ts @@ -1,7 +1,6 @@ -import hre from 'hardhat'; import { parseEventLogs } from 'viem'; import { expect } from 'chai'; -import { publicClient, ssvNetwork } from '../helpers/contract-helpers'; +import { ssvNetwork, getTransactionReceipt } from '../helpers/contract-helpers'; export enum GasGroup { REGISTER_OPERATOR, @@ -13,6 +12,9 @@ export enum GasGroup { REMOVE_OPERATOR_WHITELISTING_CONTRACT_10, SET_MULTIPLE_OPERATOR_WHITELIST_10_10, REMOVE_MULTIPLE_OPERATOR_WHITELIST_10_10, + SET_OPERATORS_PRIVATE_10, + SET_OPERATORS_PUBLIC_10, + DECLARE_OPERATOR_FEE, CANCEL_OPERATOR_FEE, @@ -23,8 +25,17 @@ export enum GasGroup { REGISTER_VALIDATOR_NEW_STATE, REGISTER_VALIDATOR_WITHOUT_DEPOSIT, + REGISTER_VALIDATOR_NEW_STATE_1_WHITELISTED_4, + REGISTER_VALIDATOR_NEW_STATE_4_WHITELISTED_4, + REGISTER_VALIDATOR_EXISTING_CLUSTER_4_WHITELISTED_4, + + REGISTER_VALIDATOR_NEW_STATE_1_WHITELISTING_CONTRACT_4, + REGISTER_VALIDATOR_NEW_STATE_4_WHITELISTING_CONTRACT_4, + BULK_REGISTER_10_VALIDATOR_NEW_STATE_4, BULK_REGISTER_10_VALIDATOR_EXISTING_CLUSTER_4, + BULK_REGISTER_10_VALIDATOR_1_WHITELISTING_CONTRACT_EXISTING_CLUSTER_4, + REGISTER_VALIDATOR_EXISTING_CLUSTER_7, REGISTER_VALIDATOR_NEW_STATE_7, @@ -85,13 +96,15 @@ const MAX_GAS_PER_GROUP: any = { /* REAL GAS LIMITS */ [GasGroup.REGISTER_OPERATOR]: 134500, [GasGroup.REMOVE_OPERATOR]: 70500, - [GasGroup.REMOVE_OPERATOR_WITH_WITHDRAW]: 70300, + [GasGroup.REMOVE_OPERATOR_WITH_WITHDRAW]: 70500, [GasGroup.SET_OPERATOR_WHITELIST]: 87000, [GasGroup.SET_OPERATOR_WHITELISTING_CONTRACT]: 90500, [GasGroup.SET_OPERATOR_WHITELISTING_CONTRACT_10]: 543000, [GasGroup.REMOVE_OPERATOR_WHITELISTING_CONTRACT_10]: 130000, [GasGroup.SET_MULTIPLE_OPERATOR_WHITELIST_10_10]: 590000, [GasGroup.REMOVE_MULTIPLE_OPERATOR_WHITELIST_10_10]: 173000, + [GasGroup.SET_OPERATORS_PRIVATE_10]: 313000, + [GasGroup.SET_OPERATORS_PUBLIC_10]: 114000, [GasGroup.DECLARE_OPERATOR_FEE]: 70000, [GasGroup.CANCEL_OPERATOR_FEE]: 41900, @@ -102,8 +115,15 @@ const MAX_GAS_PER_GROUP: any = { [GasGroup.REGISTER_VALIDATOR_NEW_STATE]: 236000, [GasGroup.REGISTER_VALIDATOR_WITHOUT_DEPOSIT]: 180600, + [GasGroup.REGISTER_VALIDATOR_NEW_STATE_1_WHITELISTED_4]: 240500, + [GasGroup.REGISTER_VALIDATOR_NEW_STATE_4_WHITELISTED_4]: 247000, + [GasGroup.REGISTER_VALIDATOR_EXISTING_CLUSTER_4_WHITELISTED_4]: 213500, + + [GasGroup.REGISTER_VALIDATOR_NEW_STATE_1_WHITELISTING_CONTRACT_4]: 246000, + [GasGroup.BULK_REGISTER_10_VALIDATOR_NEW_STATE_4]: 835500, [GasGroup.BULK_REGISTER_10_VALIDATOR_EXISTING_CLUSTER_4]: 818700, + [GasGroup.BULK_REGISTER_10_VALIDATOR_1_WHITELISTING_CONTRACT_EXISTING_CLUSTER_4]: 829000, [GasGroup.REGISTER_VALIDATOR_EXISTING_CLUSTER_7]: 272500, [GasGroup.REGISTER_VALIDATOR_NEW_STATE_7]: 289000, @@ -189,12 +209,11 @@ for (const group in MAX_GAS_PER_GROUP) { } export const trackGas = async function (tx: Promise, groups?: Array): Promise { - const hash = await tx; - - const receipt = await publicClient.waitForTransactionReceipt({ - hash, - }); + const receipt = await getTransactionReceipt(tx); + return await trackGasFromReceipt(receipt, groups); +}; +export const trackGasFromReceipt = async function (receipt: any, groups?: Array): Promise { const logs = parseEventLogs({ abi: ssvNetwork.abi, logs: receipt.logs, diff --git a/test/helpers/utils/test.ts b/test/helpers/utils/test.ts index c9511cbe..72ed83f8 100644 --- a/test/helpers/utils/test.ts +++ b/test/helpers/utils/test.ts @@ -35,3 +35,24 @@ export async function assertEvent(tx: Promise, eventAssertions: EventAssert } } } + +export async function assertPostTxEvent(eventAssertions: EventAssertion[], unemittedEvent?: Event) { + if (unemittedEvent) { + const events = await unemittedEvent.contract.getEvents[unemittedEvent.eventName](); + expect(events.length).to.equal(0); + } + for (const assertion of eventAssertions) { + const events = await assertion.contract.getEvents[assertion.eventName](); + if (assertion.eventLength) { + expect(events.length).to.equal(assertion.eventLength); + } + + if (assertion.argNames && assertion.argValuesList) { + for (let i = 0; i < events.length; i++) { + for (let j = 0; j < assertion.argNames.length; j++) { + expect(events[i].args[assertion.argNames[j]]).to.deep.equal(assertion.argValuesList[i][j]); + } + } + } + } +} diff --git a/test/operators/register.ts b/test/operators/register.ts index c9e2e21f..872ac7ce 100644 --- a/test/operators/register.ts +++ b/test/operators/register.ts @@ -1,10 +1,5 @@ // Declare imports -import { - owners, - initializeContract, - DataGenerator, - CONFIG, -} from '../helpers/contract-helpers'; +import { owners, initializeContract, DataGenerator, CONFIG } from '../helpers/contract-helpers'; import { assertEvent } from '../helpers/utils/test'; import { trackGas, GasGroup } from '../helpers/gas-usage'; @@ -58,7 +53,7 @@ describe('Register Operator Tests', () => { owners[1].account.address, // owner CONFIG.minimalOperatorFee, // fee 0, // validatorCount - ethers.ZeroAddress, // whitelisted + ethers.ZeroAddress, // whitelisting contract address false, // isPrivate true, // active ]); @@ -73,7 +68,7 @@ describe('Register Operator Tests', () => { ethers.ZeroAddress, // owner 0, // fee 0, // validatorCount - ethers.ZeroAddress, // whitelisted + ethers.ZeroAddress, // whitelisting contract address false, // isPrivate false, // active ]); @@ -82,7 +77,7 @@ describe('Register Operator Tests', () => { it('Get operator removed by id', async () => { await ssvNetwork.write.registerOperator([DataGenerator.publicKey(0), CONFIG.minimalOperatorFee], { account: owners[1].account, - }); + }); await ssvNetwork.write.removeOperator([1], { account: owners[1].account, }); @@ -91,16 +86,14 @@ describe('Register Operator Tests', () => { owners[1].account.address, // owner 0, // fee 0, // validatorCount - ethers.ZeroAddress, // whitelisted + ethers.ZeroAddress, // whitelisting contract address false, // isPrivate false, // active ]); }); it('Register an operator with a fee thats too low reverts "FeeTooLow"', async () => { - await expect(ssvNetwork.write.registerOperator([DataGenerator.publicKey(0), '10'])).to.be.rejectedWith( - 'FeeTooLow', - ); + await expect(ssvNetwork.write.registerOperator([DataGenerator.publicKey(0), '10'])).to.be.rejectedWith('FeeTooLow'); }); it('Register an operator with a fee thats too high reverts "FeeTooHigh"', async () => { @@ -113,12 +106,12 @@ describe('Register Operator Tests', () => { const publicKey = DataGenerator.publicKey(1); await ssvNetwork.write.registerOperator([publicKey, CONFIG.minimalOperatorFee], { account: owners[1].account, - }); + }); await expect( ssvNetwork.write.registerOperator([publicKey, CONFIG.minimalOperatorFee], { account: owners[1].account, - }) + }), ).to.be.rejectedWith('OperatorAlreadyExists'); }); }); diff --git a/test/operators/remove.ts b/test/operators/remove.ts index aaa451ac..67851dbe 100644 --- a/test/operators/remove.ts +++ b/test/operators/remove.ts @@ -65,7 +65,7 @@ describe('Remove Operator Tests', () => { owners[0].account.address, // owner 0, // fee 0, // validatorCount - ethers.ZeroAddress, // whitelisted address + ethers.ZeroAddress, // whitelisting contract address false, // isPrivate false, // active ]); @@ -118,17 +118,15 @@ describe('Remove Operator Tests', () => { }); it('Remove operator I do not own reverts "CallerNotOwner"', async () => { - await expect(ssvNetwork.write.removeOperator([1],{ - account: owners[1].account, - })).to.be.rejectedWith( - 'CallerNotOwner', - ); + await expect( + ssvNetwork.write.removeOperator([1], { + account: owners[1].account, + }), + ).to.be.rejectedWith('CallerNotOwner'); }); it('Remove same operator twice reverts "OperatorDoesNotExist"', async () => { await ssvNetwork.write.removeOperator([1]); - await expect(ssvNetwork.write.removeOperator([1])).to.be.rejectedWith( - 'OperatorDoesNotExist', - ); + await expect(ssvNetwork.write.removeOperator([1])).to.be.rejectedWith('OperatorDoesNotExist'); }); }); diff --git a/test/operators/whitelist.ts b/test/operators/whitelist.ts index 3eab64e1..cbc523cf 100644 --- a/test/operators/whitelist.ts +++ b/test/operators/whitelist.ts @@ -123,6 +123,32 @@ describe('Whitelisting Operator Tests', () => { ); }); + it('Set operators private (10 operators) gas limits', async () => { + await registerOperators(1, 10, CONFIG.minimalOperatorFee); + + await trackGas( + ssvNetwork.write.setOperatorsPrivateUnchecked([OPERATOR_IDS_10], { + account: owners[1].account, + }), + [GasGroup.SET_OPERATORS_PRIVATE_10], + ); + }); + + it('Set operators public (10 operators) gas limits', async () => { + await registerOperators(1, 10, CONFIG.minimalOperatorFee); + + await ssvNetwork.write.setOperatorsPrivateUnchecked([OPERATOR_IDS_10], { + account: owners[1].account, + }); + + await trackGas( + ssvNetwork.write.setOperatorsPublicUnchecked([OPERATOR_IDS_10], { + account: owners[1].account, + }), + [GasGroup.SET_OPERATORS_PUBLIC_10], + ); + }); + /* EVENTS */ it('Set operator whitelist (EOA) emits "OperatorWhitelistUpdated"', async () => { @@ -230,6 +256,42 @@ describe('Whitelisting Operator Tests', () => { ); }); + it('Set operators private (10 operators) emits "OperatorPrivacyStatusUpdated"', async () => { + await registerOperators(1, 10, CONFIG.minimalOperatorFee); + + await assertEvent( + ssvNetwork.write.setOperatorsPrivateUnchecked([OPERATOR_IDS_10], { + account: owners[1].account, + }), + [ + { + contract: ssvNetwork, + eventName: 'OperatorPrivacyStatusUpdated', + argNames: ['operatorIds', 'toPrivate'], + argValuesList: [[OPERATOR_IDS_10, true]], + }, + ], + ); + }); + + it('Set operators public (10 operators) emits "OperatorPrivacyStatusUpdated"', async () => { + await registerOperators(1, 10, CONFIG.minimalOperatorFee); + + await assertEvent( + ssvNetwork.write.setOperatorsPublicUnchecked([OPERATOR_IDS_10], { + account: owners[1].account, + }), + [ + { + contract: ssvNetwork, + eventName: 'OperatorPrivacyStatusUpdated', + argNames: ['operatorIds', 'toPrivate'], + argValuesList: [[OPERATOR_IDS_10, false]], + }, + ], + ); + }); + /* REVERTS */ it('Set operator whitelisted address (zero address) reverts "ZeroAddressNotAllowed"', async () => { await expect(ssvNetwork.write.setOperatorWhitelist([1, ethers.ZeroAddress])).to.be.rejectedWith( @@ -425,6 +487,34 @@ describe('Whitelisting Operator Tests', () => { ).to.be.rejectedWith('CallerNotOwner'); }); + it('Set operators private with empty operator IDs reverts "InvalidOperatorIdsLength"', async () => { + await expect(ssvNetwork.write.setOperatorsPrivateUnchecked([[]])).to.be.rejectedWith('InvalidOperatorIdsLength'); + }); + + it('Set operators public with empty operator IDs reverts "InvalidOperatorIdsLength"', async () => { + await expect(ssvNetwork.write.setOperatorsPublicUnchecked([[]])).to.be.rejectedWith('InvalidOperatorIdsLength'); + }); + + it('Non-owner set operators private reverts "CallerNotOwner"', async () => { + await registerOperators(1, 10, CONFIG.minimalOperatorFee); + + await expect( + ssvNetwork.write.setOperatorsPrivateUnchecked([OPERATOR_IDS_10], { + account: owners[2].account, + }), + ).to.be.rejectedWith('CallerNotOwner'); + }); + + it('Non-owner set operators public reverts "CallerNotOwner"', async () => { + await registerOperators(1, 10, CONFIG.minimalOperatorFee); + + await expect( + ssvNetwork.write.setOperatorsPublicUnchecked([OPERATOR_IDS_10], { + account: owners[2].account, + }), + ).to.be.rejectedWith('CallerNotOwner'); + }); + /* LOGIC */ it('Get whitelisted address for no operators returns empty list', async () => { @@ -473,7 +563,7 @@ describe('Whitelisting Operator Tests', () => { owners[1].account.address, // owner CONFIG.minimalOperatorFee, // fee 0, // validatorCount - mockWhitelistingContractAddress, // whitelisted + mockWhitelistingContractAddress, // whitelisting contract address true, // isPrivate true, // active ]); @@ -496,135 +586,48 @@ describe('Whitelisting Operator Tests', () => { owners[1].account.address, // owner 0, // fee 0, // validatorCount - ethers.ZeroAddress, // whitelisted + ethers.ZeroAddress, // whitelisting contract address false, // isPrivate false, // active ]); }); it('Check if an address is a whitelisting contract', async () => { + // whitelisting contract expect(await ssvViews.read.isWhitelistingContract([mockWhitelistingContractAddress])).to.be.true; + // EOA expect(await ssvViews.read.isWhitelistingContract([owners[1].account.address])).to.be.false; + // generic contract + expect(await ssvViews.read.isWhitelistingContract([ssvViews.address])).to.be.false; }); - /*************************/ - /* - it('Register operator emits "OperatorAdded"', async () => { - const publicKey = DataGenerator.publicKey(0); - - await assertEvent( - ssvNetwork.write.registerOperator([publicKey, CONFIG.minimalOperatorFee], { - account: owners[1].account, - }), - [ - { - contract: ssvNetwork, - eventName: 'OperatorAdded', - argNames: ['operatorId', 'owner', 'publicKey', 'fee'], - argValuesList: [[1, owners[1].account.address, publicKey, CONFIG.minimalOperatorFee]], - }, - ], - ); - }); - - it('Register operator gas limits', async () => { - await trackGas( - ssvNetwork.write.registerOperator([DataGenerator.publicKey(0), CONFIG.minimalOperatorFee], { - account: owners[1].account, - }), - [GasGroup.REGISTER_OPERATOR], - ); - }); - - it('Get operator by id', async () => { - await ssvNetwork.write.registerOperator([DataGenerator.publicKey(0), CONFIG.minimalOperatorFee], { - account: owners[1].account, - }); - - expect(await ssvViews.read.getOperatorById([1])).to.deep.equal([ - owners[1].account.address, // owner - CONFIG.minimalOperatorFee, // fee - 0, // validatorCount - ethers.ZeroAddress, // whitelisted - false, // isPrivate - true, // active - ]); - }); - - it('Get private operator by id', async () => { - await ssvNetwork.write.registerOperator([DataGenerator.publicKey(0), CONFIG.minimalOperatorFee], { - account: owners[1].account, - }); + it('Set operators private (10 operators)', async () => { + await registerOperators(1, 10, CONFIG.minimalOperatorFee); - await ssvNetwork.write.setOperatorWhitelist([1, owners[2].account.address], { + await ssvNetwork.write.setOperatorsPrivateUnchecked([OPERATOR_IDS_10], { account: owners[1].account, }); - expect(await ssvViews.read.getOperatorById([1])).to.deep.equal([ - owners[1].account.address, // owner - CONFIG.minimalOperatorFee, // fee - 0, // validatorCount - owners[2].account.address, // whitelisted - true, // isPrivate - true, // active - ]); + for (let i = 0; i < OPERATOR_IDS_10.length; i++) { + const operatorData = await ssvViews.read.getOperatorById([OPERATOR_IDS_10[i]]); + expect(operatorData[4]).to.be.true; + } }); - it('Get non-existent operator by id', async () => { - await ssvNetwork.write.registerOperator([DataGenerator.publicKey(0), CONFIG.minimalOperatorFee], { - account: owners[1].account, - }); - - expect(await ssvViews.read.getOperatorById([5])).to.deep.equal([ - ethers.ZeroAddress, // owner - 0, // fee - 0, // validatorCount - ethers.ZeroAddress, // whitelisted - false, // isPrivate - false, // active - ]); - }); + it('Set operators private (10 operators)', async () => { + await registerOperators(1, 10, CONFIG.minimalOperatorFee); - it('Get operator removed by id', async () => { - await ssvNetwork.write.registerOperator([DataGenerator.publicKey(0), CONFIG.minimalOperatorFee], { - account: owners[1].account, - }); - await ssvNetwork.write.removeOperator([1], { + await ssvNetwork.write.setOperatorsPrivateUnchecked([OPERATOR_IDS_10], { account: owners[1].account, }); - expect(await ssvViews.read.getOperatorById([1])).to.deep.equal([ - owners[1].account.address, // owner - 0, // fee - 0, // validatorCount - ethers.ZeroAddress, // whitelisted - false, // isPrivate - false, // active - ]); - }); - - it('Register an operator with a fee thats too low reverts "FeeTooLow"', async () => { - await expect(ssvNetwork.write.registerOperator([DataGenerator.publicKey(0), '10'])).to.be.rejectedWith('FeeTooLow'); - }); - - it('Register an operator with a fee thats too high reverts "FeeTooHigh"', async () => { - await expect(ssvNetwork.write.registerOperator([DataGenerator.publicKey(0), 2e14])).to.be.rejectedWith( - 'FeeTooHigh', - ); - }); - - it('Register same operator twice reverts "OperatorAlreadyExists"', async () => { - const publicKey = DataGenerator.publicKey(1); - await ssvNetwork.write.registerOperator([publicKey, CONFIG.minimalOperatorFee], { + await ssvNetwork.write.setOperatorsPublicUnchecked([OPERATOR_IDS_10], { account: owners[1].account, }); - await expect( - ssvNetwork.write.registerOperator([publicKey, CONFIG.minimalOperatorFee], { - account: owners[1].account, - }), - ).to.be.rejectedWith('OperatorAlreadyExists'); + for (let i = 0; i < OPERATOR_IDS_10.length; i++) { + const operatorData = await ssvViews.read.getOperatorById([OPERATOR_IDS_10[i]]); + expect(operatorData[4]).to.be.false; + } }); - - */ }); diff --git a/test/validators/register.ts b/test/validators/register.ts index ec6fa73b..d9619c87 100644 --- a/test/validators/register.ts +++ b/test/validators/register.ts @@ -1274,76 +1274,6 @@ describe('Register Validator Tests', () => { ).to.be.rejectedWith('InvalidOperatorIdsLength'); }); - it('Register whitelisted validator in 1 operator with 4 operators emits "ValidatorAdded"', async () => { - const result = await trackGas( - ssvNetwork.write.registerOperator([DataGenerator.publicKey(20), CONFIG.minimalOperatorFee], { - account: owners[1].account, - }), - ); - const { operatorId } = result.eventsByName.OperatorAdded[0].args; - - await ssvNetwork.write.setOperatorWhitelist([operatorId, owners[3].account.address], { - account: owners[1].account, - }); - - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { account: owners[3].account }); - - await assertEvent( - ssvNetwork.write.registerValidator( - [ - DataGenerator.publicKey(1), - [1, 2, 3, operatorId], - await DataGenerator.shares(3, 1, 4), - minDepositAmount, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }, - ], - { account: owners[3].account }, - ), - [ - { - contract: ssvNetwork, - eventName: 'ValidatorAdded', - }, - ], - ); - }); - - it('Register a non whitelisted validator reverts "CallerNotWhitelisted"', async () => { - const result = await trackGas( - ssvNetwork.write.registerOperator([DataGenerator.publicKey(22), CONFIG.minimalOperatorFee], { - account: owners[1].account, - }), - ); - const { operatorId } = result.eventsByName.OperatorAdded[0].args; - - await ssvNetwork.write.setOperatorWhitelist([operatorId, owners[3].account.address], { - account: owners[1].account, - }); - - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount]); - await expect( - ssvNetwork.write.registerValidator([ - DataGenerator.publicKey(1), - [1, 2, 3, operatorId], - await DataGenerator.shares(0, 1, 4), - minDepositAmount, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0, - active: true, - }, - ]), - ).to.be.rejectedWith('CallerNotWhitelisted'); - }); - it('Retrieve an existing validator', async () => { expect(await ssvViews.read.getValidator([owners[6].account.address, DataGenerator.publicKey(1)])).to.be.equals( true, diff --git a/test/validators/whitelist-register.ts b/test/validators/whitelist-register.ts new file mode 100644 index 00000000..1d9111e3 --- /dev/null +++ b/test/validators/whitelist-register.ts @@ -0,0 +1,450 @@ +// Declare imports +import hre from 'hardhat'; + +import { + owners, + initializeContract, + registerOperators, + bulkRegisterValidators, + DataGenerator, + getTransactionReceipt, + CONFIG, + DEFAULT_OPERATOR_IDS, +} from '../helpers/contract-helpers'; +import { assertPostTxEvent } from '../helpers/utils/test'; +import { trackGas, GasGroup, trackGasFromReceipt } from '../helpers/gas-usage'; + +import { expect } from 'chai'; + +let ssvNetwork: any, ssvViews: any, ssvToken: any, minDepositAmount: BigInt, mockWhitelistingContractAddress: any; + +describe('Register Validator Tests', () => { + beforeEach(async () => { + // Initialize contract + const metadata = await initializeContract(); + ssvNetwork = metadata.ssvNetwork; + ssvViews = metadata.ssvNetworkViews; + ssvToken = metadata.ssvToken; + + // Register operators + await registerOperators(0, 14, CONFIG.minimalOperatorFee); + + minDepositAmount = (BigInt(CONFIG.minimalBlocksBeforeLiquidation) + 2n) * CONFIG.minimalOperatorFee * 4n; + }); + + it('Register whitelisted validator in 1 operator with 4 operators emits "ValidatorAdded"/gas limits/logic', async () => { + const result = await trackGas( + ssvNetwork.write.registerOperator([DataGenerator.publicKey(20), CONFIG.minimalOperatorFee], { + account: owners[1].account, + }), + ); + const { operatorId } = result.eventsByName.OperatorAdded[0].args; + + await ssvNetwork.write.setOperatorWhitelist([operatorId, owners[3].account.address], { + account: owners[1].account, + }); + + await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { account: owners[3].account }); + + const receipt = await getTransactionReceipt( + ssvNetwork.write.registerValidator( + [ + DataGenerator.publicKey(1), + [1, 2, 3, operatorId], + await DataGenerator.shares(3, 1, 4), + minDepositAmount, + { + validatorCount: 0, + networkFeeIndex: 0, + index: 0, + balance: 0n, + active: true, + }, + ], + { account: owners[3].account }, + ), + ); + + await assertPostTxEvent([ + { + contract: ssvNetwork, + eventName: 'ValidatorAdded', + }, + ]); + + await trackGasFromReceipt(receipt, [GasGroup.REGISTER_VALIDATOR_NEW_STATE_1_WHITELISTED_4]); + + expect(await ssvViews.read.getOperatorById([operatorId])).to.deep.equal([ + owners[1].account.address, // owner + CONFIG.minimalOperatorFee, // fee + 1, // validatorCount + ethers.ZeroAddress, // whitelisting contract address + true, // isPrivate + true, // active + ]); + }); + + it('Register whitelisted validator in 4 operators in 4 operators cluster gas limits/logic', async () => { + await ssvNetwork.write.setOperatorMultipleWhitelists([DEFAULT_OPERATOR_IDS[4], [owners[3].account.address]], { + account: owners[0].account, + }); + + await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { account: owners[3].account }); + + await trackGas( + ssvNetwork.write.registerValidator( + [ + DataGenerator.publicKey(1), + DEFAULT_OPERATOR_IDS[4], + await DataGenerator.shares(3, 1, 4), + minDepositAmount, + { + validatorCount: 0, + networkFeeIndex: 0, + index: 0, + balance: 0n, + active: true, + }, + ], + { account: owners[3].account }, + ), + [GasGroup.REGISTER_VALIDATOR_NEW_STATE_4_WHITELISTED_4], + ); + + // Check totalValidatorsCount is incremented for all operators + for (let i = 0; i < DEFAULT_OPERATOR_IDS[4].length; i++) { + const operatorData = await ssvViews.read.getOperatorById([DEFAULT_OPERATOR_IDS[4][i]]); + expect(operatorData[2]).to.be.equal(1); + } + }); + + it('Register non-whitelisted validator in 1 public operator with 4 operators emits "ValidatorAdded"/logic', async () => { + await ssvNetwork.write.setOperatorMultipleWhitelists([[2], [owners[3].account.address]]); + + await ssvNetwork.write.setOperatorsPublicUnchecked([[2]]); + + await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { account: owners[3].account }); + + await ssvNetwork.write.registerValidator( + [ + DataGenerator.publicKey(1), + DEFAULT_OPERATOR_IDS[4], + await DataGenerator.shares(3, 1, 4), + minDepositAmount, + { + validatorCount: 0, + networkFeeIndex: 0, + index: 0, + balance: 0n, + active: true, + }, + ], + { account: owners[3].account }, + ); + + expect(await ssvViews.read.getOperatorById([2])).to.deep.equal([ + owners[0].account.address, // owner + CONFIG.minimalOperatorFee, // fee + 1, // validatorCount + ethers.ZeroAddress, // whitelisting contract address + false, // isPrivate + true, // active + ]); + }); + + it('Register whitelisted validator in 4 operator in 4 operators existing cluster gas limits', async () => { + await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { account: owners[3].account }); + const { eventsByName } = await trackGas( + ssvNetwork.write.registerValidator( + [ + DataGenerator.publicKey(1), + DEFAULT_OPERATOR_IDS[4], + await DataGenerator.shares(3, 1, 4), + minDepositAmount, + { + validatorCount: 0, + networkFeeIndex: 0, + index: 0, + balance: 0n, + active: true, + }, + ], + { account: owners[3].account }, + ), + ); + + const args = eventsByName.ValidatorAdded[0].args; + + await ssvNetwork.write.setOperatorMultipleWhitelists([DEFAULT_OPERATOR_IDS[4], [owners[3].account.address]]); + + await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { account: owners[3].account }); + await trackGas( + ssvNetwork.write.registerValidator( + [ + DataGenerator.publicKey(2), + DEFAULT_OPERATOR_IDS[4], + await DataGenerator.shares(3, 1, 4), + minDepositAmount, + args.cluster, + ], + { account: owners[3].account }, + ), + [GasGroup.REGISTER_VALIDATOR_EXISTING_CLUSTER_4_WHITELISTED_4], + ); + }); + + it('Register using non-authorized account for 1 operator with 4 operators cluster reverts "CallerNotWhitelisted"', async () => { + await ssvNetwork.write.setOperatorMultipleWhitelists([[3], [owners[3].account.address]], { + account: owners[0].account, + }); + + await expect( + ssvNetwork.write.registerValidator( + [ + DataGenerator.publicKey(1), + DEFAULT_OPERATOR_IDS[4], + await DataGenerator.shares(2, 1, 4), + minDepositAmount, + { + validatorCount: 0, + networkFeeIndex: 0, + index: 0, + balance: 0n, + active: true, + }, + ], + { account: owners[2].account }, + ), + ).to.be.rejectedWith('CallerNotWhitelisted'); + }); + + it('Register using non-authorized account for 1 operator with 4 operators cluster reverts "CallerNotWhitelisted"', async () => { + await ssvNetwork.write.setOperatorsPrivateUnchecked([[2]]); + + await expect( + ssvNetwork.write.registerValidator( + [ + DataGenerator.publicKey(1), + DEFAULT_OPERATOR_IDS[4], + await DataGenerator.shares(2, 1, 4), + minDepositAmount, + { + validatorCount: 0, + networkFeeIndex: 0, + index: 0, + balance: 0n, + active: true, + }, + ], + { account: owners[2].account }, + ), + ).to.be.rejectedWith('CallerNotWhitelisted'); + }); + + describe('Register using whitelisting contract', () => { + beforeEach(async () => { + // Whitelist whitelistedCaller using an external contract + const mockWhitelistingContract = await hre.viem.deployContract( + 'MockWhitelistingContract', + [[owners[3].account.address]], + { + client: owners[0].client, + }, + ); + mockWhitelistingContractAddress = await mockWhitelistingContract.address; + + // Set the whitelisting contract for operators 1,2,3,4 + await ssvNetwork.write.setOperatorsWhitelistingContract( + [DEFAULT_OPERATOR_IDS[4], mockWhitelistingContractAddress], + { + account: owners[0].account, + }, + ); + }); + + it('Register using whitelisting contract for 1 operator in 4 operators cluster gas limits/events/logic', async () => { + await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { account: owners[3].account }); + + const pk = DataGenerator.publicKey(1); + const shares = await DataGenerator.shares(3, 1, 4); + + const receipt = await getTransactionReceipt( + ssvNetwork.write.registerValidator( + [ + pk, + [4, 5, 6, 7], + shares, + minDepositAmount, + { + validatorCount: 0, + networkFeeIndex: 0, + index: 0, + balance: 0n, + active: true, + }, + ], + { account: owners[3].account }, + ), + ); + + let registeredCluster = await trackGasFromReceipt(receipt, [ + GasGroup.REGISTER_VALIDATOR_NEW_STATE_1_WHITELISTING_CONTRACT_4, + ]); + registeredCluster = registeredCluster.eventsByName.ValidatorAdded[0].args; + + await assertPostTxEvent([ + { + contract: ssvNetwork, + eventName: 'ValidatorAdded', + argNames: ['owner', 'operatorIds', 'publicKey', 'shares', 'cluster'], + argValuesList: [[owners[3].account.address, [4, 5, 6, 7], pk, shares, registeredCluster.cluster]], + }, + ]); + + expect(await ssvViews.read.getOperatorById([4])).to.deep.equal([ + owners[0].account.address, // owner + CONFIG.minimalOperatorFee, // fee + 1, // validatorCount + mockWhitelistingContractAddress, // whitelisting contract address + true, // isPrivate + true, // active + ]); + }); + + it('Bulk register 10 validators using whitelisting contract for 1 operator in 4 operators cluster gas limits/logic', async () => { + await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { account: owners[3].account }); + const { eventsByName } = await trackGas( + ssvNetwork.write.bulkRegisterValidator( + [ + [DataGenerator.publicKey(12)], + [4, 5, 6, 7], + [await DataGenerator.shares(3, 11, 4)], + minDepositAmount, + { + validatorCount: 0, + networkFeeIndex: 0, + index: 0, + balance: 0, + active: true, + }, + ], + { account: owners[3].account }, + ), + ); + + const args = eventsByName.ValidatorAdded[0].args; + + await bulkRegisterValidators(3, 10, [4, 5, 6, 7], minDepositAmount, args.cluster, [ + GasGroup.BULK_REGISTER_10_VALIDATOR_1_WHITELISTING_CONTRACT_EXISTING_CLUSTER_4, + ]); + + expect(await ssvViews.read.getOperatorById([4])).to.deep.equal([ + owners[0].account.address, // owner + CONFIG.minimalOperatorFee, // fee + 11, // validatorCount + mockWhitelistingContractAddress, // whitelisting contract address + true, // isPrivate + true, // active + ]); + }); + + it('Register using whitelisting contract for 1 public operator in 4 operators cluster', async () => { + await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { account: owners[3].account }); + + await ssvNetwork.write.setOperatorsPublicUnchecked([[4]]); + + await ssvNetwork.write.registerValidator( + [ + DataGenerator.publicKey(1), + [4, 5, 6, 7], + await DataGenerator.shares(3, 1, 4), + minDepositAmount, + { + validatorCount: 0, + networkFeeIndex: 0, + index: 0, + balance: 0n, + active: true, + }, + ], + { account: owners[3].account }, + ); + + expect(await ssvViews.read.getOperatorById([4])).to.deep.equal([ + owners[0].account.address, // owner + CONFIG.minimalOperatorFee, // fee + 1, // validatorCount + mockWhitelistingContractAddress, // whitelisting contract address + false, // isPrivate + true, // active + ]); + }); + + it('Register using whitelisting contract for 1 operator & EOA for 1 operator in 4 operators cluster', async () => { + await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { account: owners[3].account }); + + await ssvNetwork.write.setOperatorMultipleWhitelists([[6], [owners[3].account.address]]); + + await ssvNetwork.write.registerValidator( + [ + DataGenerator.publicKey(1), + [4, 5, 6, 7], + await DataGenerator.shares(3, 1, 4), + minDepositAmount, + { + validatorCount: 0, + networkFeeIndex: 0, + index: 0, + balance: 0n, + active: true, + }, + ], + { account: owners[3].account }, + ); + + expect(await ssvViews.read.getOperatorById([4])).to.deep.equal([ + owners[0].account.address, // owner + CONFIG.minimalOperatorFee, // fee + 1, // validatorCount + mockWhitelistingContractAddress, // whitelisting contract address + true, // isPrivate + true, // active + ]); + + expect(await ssvViews.read.getOperatorById([6])).to.deep.equal([ + owners[0].account.address, // owner + CONFIG.minimalOperatorFee, // fee + 1, // validatorCount + ethers.ZeroAddress, // whitelisting contract address + true, // isPrivate + true, // active + ]); + }); + + it('Register using whitelisting contract with an unauthorized account reverts "CallerNotWhitelisted"', async () => { + await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { account: owners[4].account }); + + const pk = DataGenerator.publicKey(1); + const shares = await DataGenerator.shares(4, 1, 4); + + await expect( + ssvNetwork.write.registerValidator( + [ + pk, + [4, 5, 6, 7], + shares, + minDepositAmount, + { + validatorCount: 0, + networkFeeIndex: 0, + index: 0, + balance: 0n, + active: true, + }, + ], + { account: owners[4].account }, + ), + ).to.be.rejectedWith('CallerNotWhitelisted'); + }); + }); +});