Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multi-whitelisted operators [audited] #299

Merged
merged 57 commits into from
Jul 7, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
066d650
update project dependencies
mtabasco Apr 10, 2024
4206206
Merge branch 'main' into update-dependencies
mtabasco Apr 10, 2024
ed84f4c
remove not relevant files
mtabasco Apr 11, 2024
72fc9ae
upgrade github actions
mtabasco Apr 11, 2024
5109ca2
edr dependency
mtabasco Apr 11, 2024
f17d5dc
fix solidity-coverage, slither
mtabasco Apr 12, 2024
d7d6c32
sync with main
mtabasco Apr 16, 2024
4f9f291
update tests viem way
mtabasco Apr 16, 2024
9a348fc
add data structure
mtabasco Apr 16, 2024
34d6a5f
set operators whitelists
mtabasco Apr 17, 2024
f74c429
support for ERC165
mtabasco Apr 19, 2024
03e1937
refactor operators update on cluster registration
mtabasco Apr 19, 2024
1cadb43
add view functions, refactors
mtabasco Apr 23, 2024
cea5938
update validator registration flow
mtabasco Apr 24, 2024
5841234
add operator whitelisting tests
mtabasco Apr 26, 2024
65347ee
set operator public/private, add tests
mtabasco Apr 29, 2024
c61c701
added fork tests
mtabasco Apr 29, 2024
d78a91f
update docs
mtabasco Apr 30, 2024
c140c3c
upgrade to 0.8.24, gas improvements
mtabasco May 3, 2024
21fef78
CI forked tests action
mtabasco May 3, 2024
932fa69
test fix CI
mtabasco May 5, 2024
2e90a0c
explicit imports, add fork test
mtabasco May 5, 2024
38b7165
change whitelist checks priority when registering validators
mtabasco May 6, 2024
11b4e67
add reentrancy tests
mtabasco May 13, 2024
fc18035
omit to make private operator when setting whitelisting contract
mtabasco May 13, 2024
ca4633a
remove privacy setting when whitelisting and removing opertors
mtabasco May 14, 2024
736ca09
sycn with main
mtabasco May 14, 2024
fb0d810
add SSVViews.isAddressWhitelistedInWhitelistingContract
mtabasco May 14, 2024
b7cfe2f
add edge cases, improve contract-helpers
mtabasco May 15, 2024
6827f06
fix CI test
mtabasco May 15, 2024
97860c5
env settings, remove GOERLI config and references
mtabasco May 16, 2024
0ea4626
add tests, update hardhat
mtabasco May 17, 2024
f30749c
add integration tests, fix CI
mtabasco May 20, 2024
e236a8e
remove setOperatorWhitelist
mtabasco May 20, 2024
f7e1554
enhace custom errors
mtabasco May 21, 2024
1dd85de
update errors
mtabasco May 21, 2024
a196ee0
update CHANGELOG
mtabasco May 21, 2024
5e04cd4
add RELEASE_NOTES
mtabasco May 21, 2024
7e9e6ba
deploy holesky stage
mtabasco May 21, 2024
dada02c
rename bulk funcs, move operator privacy funcs
mtabasco Jun 6, 2024
d078a87
registerOperator can set privacy status
mtabasco Jun 6, 2024
632e156
update docs
mtabasco Jun 6, 2024
ed84408
deploy stage
mtabasco Jun 6, 2024
e1009fa
getWhitelistedOperators update
mtabasco Jun 7, 2024
add2434
getWhitelistedOperators legacy whitelist support, fix reduceOperatorFee
mtabasco Jun 8, 2024
c928cf2
fix CI coverage
mtabasco Jun 8, 2024
737a192
stage upgrade
mtabasco Jun 18, 2024
5979ec5
new holesky deployment
mtabasco Jun 21, 2024
2f04029
audit fixes/improvements
mtabasco Jun 24, 2024
9c3b11a
upgrade testnet metadata
mtabasco Jun 24, 2024
9ddafc0
added custom test
mtabasco Jun 24, 2024
9192795
add audit report
mtabasco Jul 1, 2024
453d75b
update CHANGELOG
mtabasco Jul 1, 2024
bac7ff4
refactor whitelist check on validator registration
mtabasco Jul 3, 2024
fe120d7
update audit reports
mtabasco Jul 4, 2024
59abd6b
unlock pragma for interfaces
mtabasco Jul 4, 2024
c71314c
update audit report
mtabasco Jul 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
add operator whitelisting tests
  • Loading branch information
mtabasco committed Apr 26, 2024
commit 5841234adbc938c6089bb4ac16c20af871a44bc5
2 changes: 2 additions & 0 deletions contracts/SSVNetwork.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import "./interfaces/ISSVOperatorsWhitelist.sol";
import "./interfaces/ISSVDAO.sol";
import "./interfaces/ISSVViews.sol";

import "./interfaces/external/ISSVWhitelistingContract.sol";

import "./libraries/Types.sol";
import "./libraries/CoreLib.sol";
import "./libraries/SSVStorage.sol";
Expand Down
3 changes: 3 additions & 0 deletions contracts/SSVNetworkViews.sol
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ contract SSVNetworkViews is UUPSUpgradeable, Ownable2StepUpgradeable, ISSVViews
) external view override returns (uint64[] memory whitelistedOperatorIds) {
return ssvNetwork.getWhitelistedOperators(operatorIds, whitelistedAddress);
}
function isWhitelistingContract(address contractAddress) external view returns (bool isWhitelistingContract) {
return ssvNetwork.isWhitelistingContract(contractAddress);
}

/***********************************/
/* Cluster External View Functions */
Expand Down
2 changes: 2 additions & 0 deletions contracts/interfaces/ISSVNetworkCore.sol
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,9 @@ interface ISSVNetworkCore {
error EmptyPublicKeysList(); // df83e679
error InvalidContractAddress(); // 0xa710429d
error AddressIsWhitelistingContract(address contractAddress); // 0x71cadba7
error InvalidWhitelistingContract(); // 0x81ed29ff
error InvalidWhitelistAddressesLength(); // 0xcbb362dc
error ZeroAddressNotAllowed(); // 0x8579befe

// legacy errors
error ValidatorAlreadyExists(); // 0x8d09a73e
Expand Down
10 changes: 5 additions & 5 deletions contracts/interfaces/ISSVOperatorsWhitelist.sol
Original file line number Diff line number Diff line change
Expand Up @@ -41,28 +41,28 @@ interface ISSVOperatorsWhitelist is ISSVNetworkCore {
/**
* @dev Emitted when the whitelist of an operator is updated.
* @param operatorId operator's ID.
* @param whitelisted operator's new whitelisted address.
* @param whitelistAddress operator's new whitelisted address.
*/
event OperatorWhitelistUpdated(uint64 indexed operatorId, address whitelisted);
event OperatorWhitelistUpdated(uint64 indexed operatorId, address whitelistAddress);

/**
* @dev Emitted when a list of adresses are whitelisted for a set of operators.
* @param operatorIds operators' IDs.
* @param whitelistAddresses operators' new whitelist addresses (EOAs or generic contracts).
*/
event OperatorMultipleWhitelistUpdated(uint64[] indexed operatorIds, address[] whitelistAddresses);
event OperatorMultipleWhitelistUpdated(uint64[] operatorIds, address[] whitelistAddresses);

/**
* @dev Emitted when a list of adresses are de-whitelisted for a set of operators.
* @param operatorIds operators' IDs.
* @param whitelistAddresses operators' list of whitelist addresses to be removed (EOAs or generic contracts).
*/
event OperatorMultipleWhitelistRemoved(uint64[] indexed operatorIds, address[] whitelistAddresses);
event OperatorMultipleWhitelistRemoved(uint64[] operatorIds, address[] whitelistAddresses);

/**
* @dev Emitted when the whitelisting contract of an operator is updated.
* @param operatorIds operators' IDs.
* @param whitelistingContract operators' new whitelisting contract address.
*/
event OperatorWhitelistingContractUpdated(uint64[] indexed operatorIds, address whitelistingContract);
event OperatorWhitelistingContractUpdated(uint64[] operatorIds, address whitelistingContract);
}
9 changes: 7 additions & 2 deletions contracts/interfaces/ISSVViews.sol
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ interface ISSVViews is ISSVNetworkCore {
/// @return fee The fee associated with the operator (SSV)
/// @return validatorCount The count of validators associated with the operator
/// @return whitelistedContract The whitelisted contract address of the operator, if any
/// @return useWhitelistedContract A boolean indicating if the operator uses a whitelisted contract
/// @return isPrivate A boolean indicating if the operator is private (uses whitelisting contract or SSV Whitelisting module)
/// @return active A boolean indicating if the operator is active
function getOperatorById(
uint64 operatorId
Expand All @@ -43,7 +43,7 @@ interface ISSVViews is ISSVNetworkCore {
uint256 fee,
uint32 validatorCount,
address whitelistedContract,
bool useWhitelistedContract,
bool isPrivate,
bool active
);

Expand All @@ -56,6 +56,11 @@ interface ISSVViews is ISSVNetworkCore {
address whitelistedAddress
) external view returns (uint64[] memory whitelistedOperatorIds);

/// @notice Checks if the given address is a whitelisting contract (implements ISSVWhitelistingContract)
/// @param contractAddress The address to check
/// @return isWhitelistingContract A boolean indicating if the address is a whitelisting contract
function isWhitelistingContract(address contractAddress) external view returns (bool isWhitelistingContract);

/// @notice Checks if the cluster can be liquidated
/// @param owner The owner address of the cluster
/// @param operatorIds The IDs of the operators in the cluster
Expand Down
2 changes: 1 addition & 1 deletion contracts/interfaces/external/ISSVWhitelistingContract.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ interface ISSVWhitelistingContract {
/// @notice Checks if the caller is whitelisted
/// @param account The account that is being checked for whitelisting
/// @param operatorId The SSV Operator Id which is being checked
function isWhitelisted(address account, uint64 operatorId) external view returns (bool);
function isWhitelisted(address account, uint256 operatorId) external view returns (bool);
}
69 changes: 45 additions & 24 deletions contracts/libraries/OperatorLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -151,32 +151,40 @@ library OperatorLib {
if (addressesLength == 0) revert ISSVNetworkCore.InvalidWhitelistAddressesLength();
if (operatorsLength == 0) revert ISSVNetworkCore.InvalidOperatorIdsLength();

ISSVNetworkCore.Operator storage operator;
for (uint256 i; i < operatorsLength; ++i) {
operator = s.operators[operatorIds[i]];

checkOwner(operator);
if (addAddresses) {
if (!operator.whitelisted) {
operator.whitelisted = true;
}
}
}

// create the max number of masks that will be updated
uint256[] memory masks = generateBlockMasks(operatorIds);

for (uint256 i = 0; i < addressesLength; ++i) {
address addr = whitelistAddresses[i];
for (uint256 i; i < addressesLength; ++i) {
address whitelistAddress = whitelistAddresses[i];
checkZeroAddress(whitelistAddress);

if (isWhitelistingContract(addr)) revert ISSVNetworkCore.AddressIsWhitelistingContract(addr);
// If whitelistAddress is a custom contract, revert also when removing
if (isWhitelistingContract(whitelistAddress))
revert ISSVNetworkCore.AddressIsWhitelistingContract(whitelistAddress);

for (uint256 blockIndex; blockIndex < masks.length; ++blockIndex) {
// only update storage for updated masks
if (masks[blockIndex] != 0) {
if (addAddresses) {
s.addressWhitelistedForOperators[addr][blockIndex] |= masks[blockIndex];
s.addressWhitelistedForOperators[whitelistAddress][blockIndex] |= masks[blockIndex];
} else {
s.addressWhitelistedForOperators[addr][blockIndex] &= ~masks[blockIndex];
s.addressWhitelistedForOperators[whitelistAddress][blockIndex] &= ~masks[blockIndex];
}
}
}
}

// TODO see the way to not iterate over operatorIds twice
for (uint256 i = 0; i < operatorsLength; ++i) {
if (!s.operators[operatorIds[i]].whitelisted) {
s.operators[operatorIds[i]].whitelisted = true;
}
}
}

function updateWhitelistingContract(
Expand All @@ -200,29 +208,42 @@ library OperatorLib {
s.operatorsWhitelist[operatorId] = address(whitelistingContract);
}

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
}

function isWhitelistingContract(address whitelistingContract) internal view returns (bool) {
// TODO create type for whitelisting contracts?
return ERC165Checker.supportsInterface(whitelistingContract, type(ISSVWhitelistingContract).interfaceId);
}

function generateBlockMasks(uint64[] calldata operatorIds) internal pure returns (uint256[] memory masks) {
uint256 blockIndex;
uint256 bitPosition;
uint64 currentOperatorId;

uint256 operatorsLength = operatorIds.length;

// create the max number of masks that will be updated
masks = new uint256[]((operatorIds[operatorsLength - 1] >> 8) + 1);

for (uint256 i = 0; i < operatorsLength; ++i) {
(blockIndex, bitPosition) = getBitmapIndexes(operatorIds[i]);
for (uint256 i; i < operatorsLength; ++i) {
currentOperatorId = operatorIds[i];

if (i > 0 && currentOperatorId <= operatorIds[i - 1]) {
if (currentOperatorId == operatorIds[i - 1]) {
revert ISSVNetworkCore.OperatorsListNotUnique();
}
revert ISSVNetworkCore.UnsortedOperatorsList();
}

(blockIndex, bitPosition) = getBitmapIndexes(currentOperatorId);

masks[blockIndex] |= (1 << bitPosition);
}
}

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
}

function checkZeroAddress(address whitelistAddress) internal pure {
if (whitelistAddress == address(0)) revert ISSVNetworkCore.ZeroAddressNotAllowed();
}

function isWhitelistingContract(address whitelistingContract) internal view returns (bool) {
return ERC165Checker.supportsInterface(whitelistingContract, type(ISSVWhitelistingContract).interfaceId);
}
}
2 changes: 2 additions & 0 deletions contracts/modules/SSVOperators.sol
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ contract SSVOperators is ISSVOperators {

function removeOperator(uint64 operatorId) external override {
StorageData storage s = SSVStorage.load();

Operator memory operator = s.operators[operatorId];
operator.checkOwner();

Expand All @@ -62,6 +63,7 @@ contract SSVOperators is ISSVOperators {
operator.snapshot.balance = 0;
operator.validatorCount = 0;
operator.fee = 0;
operator.whitelisted = false;
andrew-blox marked this conversation as resolved.
Show resolved Hide resolved

s.operators[operatorId] = operator;

Expand Down
14 changes: 9 additions & 5 deletions contracts/modules/SSVOperatorsWhitelist.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@ contract SSVOperatorsWhitelist is ISSVOperatorsWhitelist {
/*******************************/

function setOperatorWhitelist(uint64 operatorId, address whitelistAddress) external override {
StorageData storage s = SSVStorage.load();
s.operators[operatorId].checkOwner();
OperatorLib.checkZeroAddress(whitelistAddress);

if (OperatorLib.isWhitelistingContract(whitelistAddress))
revert AddressIsWhitelistingContract(whitelistAddress);

StorageData storage s = SSVStorage.load();
s.operators[operatorId].checkOwner();

// Set the bit at bitPosition for the operatorId in the corresponding uint256 blockIndex
(uint256 blockIndex, uint256 bitPosition) = OperatorLib.getBitmapIndexes(operatorId);

Expand Down Expand Up @@ -53,13 +55,16 @@ contract SSVOperatorsWhitelist is ISSVOperatorsWhitelist {
uint64[] calldata operatorIds,
ISSVWhitelistingContract whitelistingContract
) external {
// Reverts also when whitelistingContract == address(0)
if (!OperatorLib.isWhitelistingContract(address(whitelistingContract))) revert InvalidWhitelistingContract();

uint256 operatorsLength = operatorIds.length;
if (operatorsLength == 0) revert InvalidOperatorIdsLength();

StorageData storage s = SSVStorage.load();
Operator storage operator;

for (uint256 i = 0; i < operatorsLength; ++i) {
for (uint256 i; i < operatorsLength; ++i) {
uint64 operatorId = operatorIds[i];

operator = s.operators[operatorId];
Expand Down Expand Up @@ -89,12 +94,11 @@ contract SSVOperatorsWhitelist is ISSVOperatorsWhitelist {
StorageData storage s = SSVStorage.load();
Operator storage operator;

for (uint256 i = 0; i < operatorsLength; ++i) {
for (uint256 i; i < operatorsLength; ++i) {
uint64 operatorId = operatorIds[i];
operator = s.operators[operatorId];

operator.checkOwner();
operator.whitelisted = false;

s.operatorsWhitelist[operatorId] = address(0);
}
Expand Down
44 changes: 27 additions & 17 deletions contracts/modules/SSVViews.sol
Original file line number Diff line number Diff line change
Expand Up @@ -51,21 +51,27 @@ contract SSVViews is ISSVViews {

function getOperatorById(
uint64 operatorId
) external view override returns (address, uint256, uint32, address, bool, bool) {
ISSVNetworkCore.Operator memory operator = SSVStorage.load().operators[operatorId];

address whitelistedContract = SSVStorage.load().operatorsWhitelist[operatorId];
bool useWhitelistedContract = OperatorLib.isWhitelistingContract(whitelistedContract);
bool isActive = operator.snapshot.block == 0 ? false : true;

return (
operator.owner,
operator.fee.expand(),
operator.validatorCount,
whitelistedContract,
useWhitelistedContract,
isActive
);
)
external
view
override
returns (
address owner,
uint256 fee,
uint32 validatorCount,
address whitelistedContract,
bool isPrivate,
bool isActive
)
{
ISSVNetworkCore.Operator storage operator = SSVStorage.load().operators[operatorId];

owner = operator.owner;
fee = operator.fee.expand();
validatorCount = operator.validatorCount;
whitelistedContract = SSVStorage.load().operatorsWhitelist[operatorId];
isPrivate = operator.whitelisted;
isActive = operator.snapshot.block != 0;
}

function getWhitelistedOperators(
Expand All @@ -80,7 +86,7 @@ contract SSVViews is ISSVViews {
// create the max number of masks that will be updated
uint256[] memory masks = OperatorLib.generateBlockMasks(operatorIds);

uint256 count = 0;
uint256 count;
whitelistedOperatorIds = new uint64[](operatorsLength);

uint256 whitelistedMask;
Expand All @@ -97,7 +103,7 @@ contract SSVViews is ISSVViews {

// Now we need to extract operator IDs from matchedMask
uint256 blockPointer = blockIndex << 8;
for (uint256 bit = 0; bit < 256; bit++) {
for (uint256 bit; bit < 256; ++bit) {
if (matchedMask & (1 << bit) != 0) {
whitelistedOperatorIds[count++] = uint64(blockPointer + bit);
if (count == operatorsLength) {
Expand All @@ -114,6 +120,10 @@ contract SSVViews is ISSVViews {
}
}

function isWhitelistingContract(address contractAddress) external view override returns (bool) {
return OperatorLib.isWhitelistingContract(contractAddress);
}

/***********************************/
/* Cluster External View Functions */
/***********************************/
Expand Down
23 changes: 23 additions & 0 deletions contracts/test/mocks/MockWhitelistingContract.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity 0.8.18;

import "../../interfaces/external/ISSVWhitelistingContract.sol";
import "@openzeppelin/contracts/utils/introspection/ERC165.sol";

contract MockWhitelistingContract is ISSVWhitelistingContract, ERC165 {
mapping(address => bool) private whitelisted;

constructor(address[] memory whitelistedAddresses) {
for (uint i; i < whitelistedAddresses.length; ++i) {
whitelisted[whitelistedAddresses[i]] = true;
}
}

function isWhitelisted(address account, uint256) external view override returns (bool) {
return whitelisted[account];
}

function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
return interfaceId == type(ISSVWhitelistingContract).interfaceId || super.supportsInterface(interfaceId);
}
}
3 changes: 3 additions & 0 deletions test/helpers/contract-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export const initializeContract = async function () {
const ssvClustersMod = await hre.viem.deployContract('SSVClusters');
const ssvDAOMod = await hre.viem.deployContract('SSVDAO');
const ssvViewsMod = await hre.viem.deployContract('contracts/modules/SSVViews.sol:SSVViews');
const ssvWhitelistMod = await hre.viem.deployContract('SSVOperatorsWhitelist');

const ssvNetworkFactory = await ethers.getContractFactory('SSVNetwork');
const ssvNetworkProxy = await await upgrades.deployProxy(
Expand Down Expand Up @@ -138,6 +139,8 @@ export const initializeContract = async function () {

await ssvNetwork.write.updateMaximumOperatorFee([CONFIG.maximumOperatorFee as bigint]);

ssvNetwork.write.updateModule([4, await ssvWhitelistMod.address]);

for (let i = 1; i < 7; i++) {
await ssvToken.write.mint([owners[i].account.address, 10000000000000000000n]);
}
Expand Down
Loading
Loading