diff --git a/contracts/modules/SSVViews.sol b/contracts/modules/SSVViews.sol index 9fd199b2..4e585042 100644 --- a/contracts/modules/SSVViews.sol +++ b/contracts/modules/SSVViews.sol @@ -84,15 +84,15 @@ contract SSVViews is ISSVViews { StorageData storage s = SSVStorage.load(); - // create the max number of masks that will be updated - uint256[] memory masks = OperatorLib.generateBlockMasks(operatorIds, false, s); - - uint256 count; - whitelistedOperatorIds = new uint64[](operatorsLength); + uint256 internalCount; + // Check whitelisting address for each operator using the internal SSV whitelisting module uint256 whitelistedMask; uint256 matchedMask; + uint256[] memory masks = OperatorLib.generateBlockMasks(operatorIds, false, s); + uint64[] memory internalWhitelistedOperatorIds = new uint64[](operatorsLength); + // Check whitelisting status for each mask for (uint256 blockIndex; blockIndex < masks.length; ++blockIndex) { // Only check blocks that have operator IDs @@ -106,15 +106,48 @@ contract SSVViews is ISSVViews { uint256 blockPointer = blockIndex << 8; for (uint256 bit; bit < 256; ++bit) { if (matchedMask & (1 << bit) != 0) { - whitelistedOperatorIds[count++] = uint64(blockPointer + bit); - if (count == operatorsLength) { - return whitelistedOperatorIds; // Early termination + internalWhitelistedOperatorIds[internalCount++] = uint64(blockPointer + bit); + if (internalCount == operatorsLength) { + return internalWhitelistedOperatorIds; // Early termination } } } } } + // Resize internalWhitelistedOperatorIds to the actual number of whitelisted operators + assembly { + mstore(internalWhitelistedOperatorIds, internalCount) + } + + // Check if pending operators use an external whitelisting contract and check whitelistedAddress using it + whitelistedOperatorIds = new uint64[](operatorsLength); + uint256 operatorIndex; + uint256 internalWhitelistIndex; + uint256 count; + + while (operatorIndex < operatorsLength) { + uint64 operatorId = operatorIds[operatorIndex]; + + // Check if operatorId is already in internalWhitelistedOperatorIds + if (internalWhitelistIndex < internalCount && operatorId == internalWhitelistedOperatorIds[internalWhitelistIndex]) { + whitelistedOperatorIds[count++] = operatorId; + ++internalWhitelistIndex; + } else { + // Check whitelisting contract + address whitelistingContract = s.operatorsWhitelist[operatorId]; + if (whitelistingContract != address(0)) { + if ( + OperatorLib.isWhitelistingContract(whitelistingContract) && + ISSVWhitelistingContract(whitelistingContract).isWhitelisted(whitelistedAddress, operatorId) + ) { + whitelistedOperatorIds[count++] = operatorId; + } + } + } + ++operatorIndex; + } + // Resize whitelistedOperatorIds to the actual number of whitelisted operators assembly { mstore(whitelistedOperatorIds, count) diff --git a/hardhat.config.ts b/hardhat.config.ts index 6cc97807..aac45bb0 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -38,6 +38,7 @@ const config: HardhatUserConfig = { enabled: true, runs: 10000, }, + evmVersion: 'cancun', }, }, ], diff --git a/test/operators/whitelist.ts b/test/operators/whitelist.ts index 2d55ad4f..bc075da5 100644 --- a/test/operators/whitelist.ts +++ b/test/operators/whitelist.ts @@ -524,7 +524,7 @@ describe('Whitelisting Operator Tests', () => { expect(await ssvViews.read.getWhitelistedOperators([[1, 2], ethers.ZeroAddress])).to.be.deep.equal([]); }); - it('Get whitelisted address for operators returns only the whitelisted operators', async () => { + it('Get whitelisted address for operators returns the whitelisted operators (only SSV whitelisting module)', async () => { const whitelistAddress = owners[4].account.address; // Register 1000 operators to have 4 bitmap blocks @@ -546,6 +546,210 @@ describe('Whitelisting Operator Tests', () => { ).to.be.deep.equal([]); }); + it('Get whitelisted address for operators returns the whitelisted operators (only externally whitelisted)', async () => { + const whitelistAddress = owners[4].account.address; + + // Register 1000 operators to have 4 bitmap blocks + await registerOperators(1, 1000, CONFIG.minimalOperatorFee); + + await ssvNetwork.write.setOperatorsWhitelistingContract( + [[100, 200, 300, 400, 500, 600, 700, 800], mockWhitelistingContractAddress], + { + account: owners[1].account, + }, + ); + + await mockWhitelistingContract.write.setWhitelistedAddress([whitelistAddress]); + + expect(await ssvViews.read.getWhitelistedOperators([[200, 400, 600, 800], whitelistAddress])).to.be.deep.equal([ + 200, 400, 600, 800, + ]); + expect( + await ssvViews.read.getWhitelistedOperators([[1, 60, 150, 200, 320, 400, 512, 715, 800, 905], whitelistAddress]), + ).to.be.deep.equal([200, 400, 800]); + expect( + await ssvViews.read.getWhitelistedOperators([[1, 60, 150, 320, 512, 715, 905], whitelistAddress]), + ).to.be.deep.equal([]); + }); + + it('Get whitelisted address for operators returns the whitelisted operators (internally and externally whitelisted)', async () => { + const whitelistAddress = owners[4].account.address; + + // Register 1000 operators to have 4 bitmap blocks + await registerOperators(1, 1000, CONFIG.minimalOperatorFee); + + // Whitelist using external whitelisting contract + await ssvNetwork.write.setOperatorsWhitelistingContract([[100, 400, 700, 800], mockWhitelistingContractAddress], { + account: owners[1].account, + }); + + await mockWhitelistingContract.write.setWhitelistedAddress([whitelistAddress]); + + // Whitelist using SSV whitelisting module + await ssvNetwork.write.setOperatosWhitelists([[200, 300, 500, 600], [whitelistAddress]], { + account: owners[1].account, + }); + + expect(await ssvViews.read.getWhitelistedOperators([[200, 400, 600, 800], whitelistAddress])).to.be.deep.equal([ + 200, 400, 600, 800, + ]); + expect( + await ssvViews.read.getWhitelistedOperators([[1, 60, 150, 200, 320, 400, 512, 715, 800, 905], whitelistAddress]), + ).to.be.deep.equal([200, 400, 800]); + expect( + await ssvViews.read.getWhitelistedOperators([[1, 60, 150, 320, 512, 715, 905], whitelistAddress]), + ).to.be.deep.equal([]); + }); + + it('Get whitelisted address for a single operator whitelisted both internally and externally', async () => { + const whitelistAddress = owners[4].account.address; + + // Register operators + await registerOperators(1, 10, CONFIG.minimalOperatorFee); + + // Whitelist using external whitelisting contract + await ssvNetwork.write.setOperatorsWhitelistingContract([[1], mockWhitelistingContractAddress], { + account: owners[1].account, + }); + + await mockWhitelistingContract.write.setWhitelistedAddress([whitelistAddress]); + + // Whitelist using SSV whitelisting module + await ssvNetwork.write.setOperatosWhitelists([[1], [whitelistAddress]], { + account: owners[1].account, + }); + + expect(await ssvViews.read.getWhitelistedOperators([[1], whitelistAddress])).to.be.deep.equal([1]); + }); + + it('Get whitelisted address for overlapping internal and external whitelisting', async () => { + const whitelistAddress = owners[4].account.address; + + // Register operators + await registerOperators(1, 10, CONFIG.minimalOperatorFee); + + // Whitelist using external whitelisting contract + await ssvNetwork.write.setOperatorsWhitelistingContract([[1, 2, 3], mockWhitelistingContractAddress], { + account: owners[1].account, + }); + + await mockWhitelistingContract.write.setWhitelistedAddress([whitelistAddress]); + + // Whitelist using SSV whitelisting module + await ssvNetwork.write.setOperatosWhitelists([[2, 3, 4], [whitelistAddress]], { + account: owners[1].account, + }); + + expect(await ssvViews.read.getWhitelistedOperators([[1, 2, 3, 4], whitelistAddress])).to.be.deep.equal([ + 1, 2, 3, 4, + ]); + }); + + it('Get whitelisted address for a list containing non-whitelisted operators', async () => { + const whitelistAddress = owners[4].account.address; + + // Register operators + await registerOperators(1, 10, CONFIG.minimalOperatorFee); + + // Whitelist using SSV whitelisting module + await ssvNetwork.write.setOperatosWhitelists([[2, 4, 6], [whitelistAddress]], { + account: owners[1].account, + }); + + expect(await ssvViews.read.getWhitelistedOperators([[1, 2, 3, 4, 5, 6, 7, 8], whitelistAddress])).to.be.deep.equal([ + 2, 4, 6, + ]); + }); + + it('Get whitelisted address for non-existent operator IDs', async () => { + const whitelistAddress = owners[4].account.address; + + // Register operators + await registerOperators(1, 10, CONFIG.minimalOperatorFee); + + // Whitelist using SSV whitelisting module + await ssvNetwork.write.setOperatosWhitelists([[2, 4, 6], [whitelistAddress]], { + account: owners[1].account, + }); + + expect(await ssvViews.read.getWhitelistedOperators([[11, 12, 13], whitelistAddress])).to.be.deep.equal([]); + }); + + it('Get whitelisted address for mixed whitelisted and non-whitelisted addresses', async () => { + const whitelistAddress1 = owners[4].account.address; + const whitelistAddress2 = owners[5].account.address; + + // Register operators + await registerOperators(1, 10, CONFIG.minimalOperatorFee); + + // Whitelist using SSV whitelisting module + await ssvNetwork.write.setOperatosWhitelists([[2, 4, 6], [whitelistAddress1]], { + account: owners[1].account, + }); + + await ssvNetwork.write.setOperatosWhitelists([[3, 5, 7], [whitelistAddress2]], { + account: owners[1].account, + }); + + expect(await ssvViews.read.getWhitelistedOperators([[1, 2, 3, 4, 5, 6, 7, 8], whitelistAddress1])).to.be.deep.equal( + [2, 4, 6], + ); + expect(await ssvViews.read.getWhitelistedOperators([[1, 2, 3, 4, 5, 6, 7, 8], whitelistAddress2])).to.be.deep.equal( + [3, 5, 7], + ); + }); + + it('Get whitelisted address for unsorted operators', async () => { + const whitelistAddress = owners[4].account.address; + + // Register operators + await registerOperators(1, 10, CONFIG.minimalOperatorFee); + + // Whitelist using SSV whitelisting module + await ssvNetwork.write.setOperatosWhitelists([[2, 4, 6], [whitelistAddress]], { + account: owners[1].account, + }); + + await expect(ssvViews.read.getWhitelistedOperators([[6, 2, 4], whitelistAddress])).to.be.rejectedWith( + 'UnsortedOperatorsList', + ); + }); + + it('Get whitelisted address for duplicate operator IDs', async () => { + const whitelistAddress = owners[4].account.address; + + // Register operators + await registerOperators(1, 10, CONFIG.minimalOperatorFee); + + // Whitelist using SSV whitelisting module + await ssvNetwork.write.setOperatosWhitelists([[2, 4, 6], [whitelistAddress]], { + account: owners[1].account, + }); + + await expect(ssvViews.read.getWhitelistedOperators([[2, 2, 4, 6, 6], whitelistAddress])).to.be.rejectedWith( + 'OperatorsListNotUnique', + ); + }); + + it('Get whitelisted address for a large number of operator IDs', async () => { + const whitelistAddress = owners[4].account.address; + + // Register a large number of operators + const largeNumber = 3000; + await registerOperators(1, largeNumber, CONFIG.minimalOperatorFee); + + let operatorIds = []; + for (let i = 1; i <= largeNumber; i++) { + operatorIds.push(i); + } + + await ssvNetwork.write.setOperatosWhitelists([operatorIds, [whitelistAddress]], { + account: owners[1].account, + }); + + expect(await ssvViews.read.getWhitelistedOperators([operatorIds, whitelistAddress])).to.be.deep.equal(operatorIds); + }); + it('Get private operator by id', async () => { await ssvNetwork.write.registerOperator([DataGenerator.publicKey(0), CONFIG.minimalOperatorFee, false], { account: owners[1].account,