From de765df2cdf38f563b6cff38beb3ee95b86cf6c6 Mon Sep 17 00:00:00 2001 From: Marco Date: Wed, 16 Aug 2023 16:49:34 +0200 Subject: [PATCH] tokenized cluster support --- contracts/SSVNetwork.sol | 17 +- contracts/interfaces/ISSVClusters.sol | 47 ++- contracts/interfaces/ISSVNetwork.sol | 2 - contracts/interfaces/ISSVNetworkCore.sol | 10 + contracts/interfaces/ISSVOperators.sol | 2 +- contracts/libraries/ClusterLib.sol | 340 ++++++++++++++++++++- contracts/libraries/CoreLib.sol | 11 +- contracts/libraries/SSVStorage.sol | 4 +- contracts/modules/SSVClusters.sol | 357 ++++++++++++----------- contracts/modules/SSVOperators.sol | 14 +- contracts/test/SSVNetworkUpgrade.sol | 11 +- 11 files changed, 616 insertions(+), 199 deletions(-) diff --git a/contracts/SSVNetwork.sol b/contracts/SSVNetwork.sol index 976b63b5..e11b822e 100644 --- a/contracts/SSVNetwork.sol +++ b/contracts/SSVNetwork.sol @@ -118,7 +118,11 @@ contract SSVNetwork is /* Operator External Functions */ /*******************************/ - function registerOperator(bytes calldata publicKey, uint256 fee) external override returns (uint64 id) { + function registerOperator( + bytes calldata publicKey, + uint256 fee, + IERC20 feeToken + ) external override returns (uint64 id) { if (!RegisterAuth.load().authorization[msg.sender].registerOperator) revert NotAuthorized(); _delegate(SSVStorage.load().ssvContracts[SSVModules.SSV_OPERATORS]); @@ -173,6 +177,8 @@ contract SSVNetwork is uint64[] memory operatorIds, bytes calldata sharesData, uint256 amount, + IERC20 feeToken, + uint256 ssvAmount, ISSVNetworkCore.Cluster memory cluster ) external override { if (!RegisterAuth.load().authorization[msg.sender].registerValidator) revert NotAuthorized(); @@ -188,13 +194,19 @@ contract SSVNetwork is _delegate(SSVStorage.load().ssvContracts[SSVModules.SSV_CLUSTERS]); } - function liquidate(address clusterOwner, uint64[] calldata operatorIds, ISSVNetworkCore.Cluster memory cluster) external { + function liquidate( + address clusterOwner, + uint64[] calldata operatorIds, + IERC20 feeToken, + ISSVNetworkCore.Cluster memory cluster + ) external { _delegate(SSVStorage.load().ssvContracts[SSVModules.SSV_CLUSTERS]); } function reactivate( uint64[] calldata operatorIds, uint256 amount, + IERC20 feeToken, ISSVNetworkCore.Cluster memory cluster ) external override { _delegate(SSVStorage.load().ssvContracts[SSVModules.SSV_CLUSTERS]); @@ -204,6 +216,7 @@ contract SSVNetwork is address clusterOwner, uint64[] calldata operatorIds, uint256 amount, + IERC20 feeToken, ISSVNetworkCore.Cluster memory cluster ) external override { _delegate(SSVStorage.load().ssvContracts[SSVModules.SSV_CLUSTERS]); diff --git a/contracts/interfaces/ISSVClusters.sol b/contracts/interfaces/ISSVClusters.sol index 9910e9fd..3585d8a7 100644 --- a/contracts/interfaces/ISSVClusters.sol +++ b/contracts/interfaces/ISSVClusters.sol @@ -15,6 +15,8 @@ interface ISSVClusters is ISSVNetworkCore { uint64[] memory operatorIds, bytes calldata sharesData, uint256 amount, + IERC20 feeToken, + uint256 ssvAmount, Cluster memory cluster ) external; @@ -32,6 +34,8 @@ interface ISSVClusters is ISSVNetworkCore { /// @param owner The owner of the cluster /// @param operatorIds Array of IDs of operators managing the cluster /// @param cluster Cluster to be liquidated + function liquidate(address owner, uint64[] memory operatorIds, IERC20 feeToken, Cluster memory cluster) external; + function liquidate(address owner, uint64[] memory operatorIds, Cluster memory cluster) external; /// @notice Reactivates a cluster @@ -40,16 +44,37 @@ interface ISSVClusters is ISSVNetworkCore { /// @param cluster Cluster to be reactivated function reactivate(uint64[] memory operatorIds, uint256 amount, Cluster memory cluster) external; + function reactivate( + uint64[] calldata operatorIds, + uint256 feeAmount, + IERC20 feeToken, + uint256 ssvAmount, + Cluster memory cluster + ) external; + /******************************/ /* Balance External Functions */ /******************************/ /// @notice Deposits tokens into a cluster - /// @param owner The owner of the cluster + /// @param clusterOwner The owner of the cluster /// @param operatorIds Array of IDs of operators managing the cluster - /// @param amount Amount of SSV tokens to be deposited + /// @param ssvAmount Amount of SSV tokens to be deposited /// @param cluster Cluster where the deposit will be made - function deposit(address owner, uint64[] memory operatorIds, uint256 amount, Cluster memory cluster) external; + function depositClusterBalance( + address clusterOwner, + uint64[] calldata operatorIds, + uint256 ssvAmount, + Cluster memory cluster + ) external; + + function depositClusterBalance( + address clusterOwner, + uint64[] calldata operatorIds, + uint256 feeAmount, + IERC20 feeToken, + Cluster memory cluster + ) external; /// @notice Withdraws tokens from a cluster /// @param operatorIds Array of IDs of operators managing the cluster @@ -80,5 +105,19 @@ interface ISSVClusters is ISSVNetworkCore { event ClusterWithdrawn(address indexed owner, uint64[] operatorIds, uint256 value, Cluster cluster); - event ClusterDeposited(address indexed owner, uint64[] operatorIds, uint256 value, Cluster cluster); + event ClusterDeposited( + address indexed owner, + uint64[] operatorIds, + IERC20 feeToken, + uint256 value, + Cluster cluster + ); + + event ClusterTokenDeposited( + address indexed owner, + uint64[] operatorIds, + IERC20 feeToken, + uint256 value, + Cluster cluster + ); } diff --git a/contracts/interfaces/ISSVNetwork.sol b/contracts/interfaces/ISSVNetwork.sol index de6e5e73..9a032b11 100644 --- a/contracts/interfaces/ISSVNetwork.sol +++ b/contracts/interfaces/ISSVNetwork.sol @@ -9,8 +9,6 @@ import "./ISSVViews.sol"; import {SSVModules} from "../libraries/SSVStorage.sol"; -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - interface ISSVNetwork { function initialize( IERC20 token_, diff --git a/contracts/interfaces/ISSVNetworkCore.sol b/contracts/interfaces/ISSVNetworkCore.sol index a3206e7e..43f0d492 100644 --- a/contracts/interfaces/ISSVNetworkCore.sol +++ b/contracts/interfaces/ISSVNetworkCore.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.18; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + interface ISSVNetworkCore { /***********/ /* Structs */ @@ -28,6 +30,8 @@ interface ISSVNetworkCore { bool whitelisted; /// @dev The state snapshot of the operator Snapshot snapshot; + /// @dev The token used for fees + IERC20 feeToken; } /// @notice Represents a request to change an operator's fee @@ -54,6 +58,11 @@ interface ISSVNetworkCore { uint256 balance; } + struct Account { + /// @dev Total account validators + uint32 validatorCount; + uint64 ssvBalance; + } /**********/ /* Errors */ /**********/ @@ -87,4 +96,5 @@ interface ISSVNetworkCore { error OperatorAlreadyExists(); // 0x289c9494 error TargetModuleDoesNotExist(); // 0x8f9195fb error MaxValueExceeded(); // 0x91aa3017 + error FeeTokenMismatch(); } diff --git a/contracts/interfaces/ISSVOperators.sol b/contracts/interfaces/ISSVOperators.sol index c77c2329..ab40df94 100644 --- a/contracts/interfaces/ISSVOperators.sol +++ b/contracts/interfaces/ISSVOperators.sol @@ -7,7 +7,7 @@ interface ISSVOperators is ISSVNetworkCore { /// @notice Registers a new operator /// @param publicKey The public key of the operator /// @param fee The operator's fee (SSV) - function registerOperator(bytes calldata publicKey, uint256 fee) external returns (uint64); + function registerOperator(bytes calldata publicKey, uint256 fee, IERC20 feeToken) external returns (uint64); /// @notice Removes an existing operator /// @param operatorId The ID of the operator to be removed diff --git a/contracts/libraries/ClusterLib.sol b/contracts/libraries/ClusterLib.sol index ea0bb36f..5a61ae60 100644 --- a/contracts/libraries/ClusterLib.sol +++ b/contracts/libraries/ClusterLib.sol @@ -3,45 +3,112 @@ pragma solidity 0.8.18; import "../interfaces/ISSVNetworkCore.sol"; import "./SSVStorage.sol"; +import "./SSVStorageProtocol.sol"; +import "./CoreLib.sol"; +import "./OperatorLib.sol"; +import "./ProtocolLib.sol"; import "./Types.sol"; library ClusterLib { using Types64 for uint64; + using Types256 for uint256; + using OperatorLib for ISSVNetworkCore.Operator; + using ProtocolLib for StorageProtocol; - function updateBalance( + function updateClusterBalance( ISSVNetworkCore.Cluster memory cluster, uint64 newIndex, - uint64 currentNetworkFeeIndex - ) internal pure { - uint64 networkFee = uint64(currentNetworkFeeIndex - cluster.networkFeeIndex) * cluster.validatorCount; - uint64 usage = (newIndex - cluster.index) * cluster.validatorCount + networkFee; + uint64 currentNetworkFeeIndex, + ISSVNetworkCore.Account memory account, + bool isTokenFee + ) internal pure returns (uint64 clusterNetworkFee) { + uint64 networkUsage = uint64(currentNetworkFeeIndex - cluster.networkFeeIndex) * cluster.validatorCount; + uint64 operatorsUsage = (newIndex - cluster.index) * cluster.validatorCount; + + if (isTokenFee) { + clusterNetworkFee = updateAccountBalance(networkUsage, account); + } else { + clusterNetworkFee = updateNetworkFeeBalance(cluster, networkUsage, account); + } + + cluster.balance = operatorsUsage.expand() > cluster.balance ? 0 : cluster.balance - operatorsUsage.expand(); + } + + /* + function updateBalance(ISSVNetworkCore.Cluster memory cluster, uint64 newIndex) internal pure { + //uint64 networkFee = uint64(currentNetworkFeeIndex - cluster.networkFeeIndex) * cluster.validatorCount; + uint64 usage = (newIndex - cluster.index) * cluster.validatorCount; cluster.balance = usage.expand() > cluster.balance ? 0 : cluster.balance - usage.expand(); } +*/ + function updateNetworkFeeBalance( + ISSVNetworkCore.Cluster memory cluster, + uint64 networkUsage, + ISSVNetworkCore.Account memory account + ) internal pure returns (uint64 clusterNetworkFee) { + // for legacy clusters check if the account has SSV balance to cover network fees + if (account.ssvBalance != 0) { + clusterNetworkFee = updateAccountBalance(networkUsage, account); + } else { + // if not, take the network fee from the cluster + cluster.balance = networkUsage.expand() > cluster.balance ? 0 : cluster.balance - networkUsage.expand(); + } + } + + function updateAccountBalance( + uint64 networkUsage, + ISSVNetworkCore.Account memory account + ) internal pure returns (uint64 clusterNetworkFee) { + if (networkUsage > account.ssvBalance) { + clusterNetworkFee = account.ssvBalance; + account.ssvBalance = 0; + } else { + clusterNetworkFee = networkUsage; + account.ssvBalance -= networkUsage; + } + } function isLiquidatable( ISSVNetworkCore.Cluster memory cluster, uint64 burnRate, uint64 networkFee, uint64 minimumBlocksBeforeLiquidation, - uint64 minimumLiquidationCollateral + uint64 minimumLiquidationCollateral, + ISSVNetworkCore.Account memory account, + bool isTokenFee ) internal pure returns (bool) { - if (cluster.balance < minimumLiquidationCollateral.expand()) return true; - uint64 liquidationThreshold = minimumBlocksBeforeLiquidation * (burnRate + networkFee) * cluster.validatorCount; + uint64 networkLiquidationThreshold = minimumBlocksBeforeLiquidation * networkFee * cluster.validatorCount; + if (isTokenFee) { + // for new clusters, check only at the account level + if (account.ssvBalance < minimumLiquidationCollateral && account.ssvBalance < networkLiquidationThreshold) { + return true; + } + } else if ( + // for legacy clusters, check first at account level and then at cluster level + account.ssvBalance < minimumLiquidationCollateral && + account.ssvBalance < networkLiquidationThreshold && + cluster.balance < minimumLiquidationCollateral && + cluster.balance < networkLiquidationThreshold + ) { + return true; + } + + // Operators' fee check always against the cluster + // if (cluster.balance < minimumLiquidationCollateral.expand()) return true; // TODO collateral by token type? + uint64 operatorsLiquidationThreshold = minimumBlocksBeforeLiquidation * (burnRate) * cluster.validatorCount; - return cluster.balance < liquidationThreshold.expand(); + return cluster.balance < operatorsLiquidationThreshold.expand(); } function validateClusterIsNotLiquidated(ISSVNetworkCore.Cluster memory cluster) internal pure { if (!cluster.active) revert ISSVNetworkCore.ClusterIsLiquidated(); } - function validateHashedCluster( + function validateCluster( ISSVNetworkCore.Cluster memory cluster, - address owner, - uint64[] memory operatorIds, + bytes32 hashedCluster, StorageData storage s ) internal view returns (bytes32) { - bytes32 hashedCluster = keccak256(abi.encodePacked(owner, operatorIds)); bytes32 hashedClusterData = hashClusterData(cluster); bytes32 clusterData = s.clusters[hashedCluster]; @@ -57,9 +124,11 @@ library ClusterLib { function updateClusterData( ISSVNetworkCore.Cluster memory cluster, uint64 clusterIndex, - uint64 currentNetworkFeeIndex + uint64 currentNetworkFeeIndex, + ISSVNetworkCore.Account memory account, + bool isTokenFee ) internal pure { - updateBalance(cluster, clusterIndex, currentNetworkFeeIndex); + updateClusterBalance(cluster, clusterIndex, currentNetworkFeeIndex, account, isTokenFee); cluster.index = clusterIndex; cluster.networkFeeIndex = currentNetworkFeeIndex; } @@ -76,4 +145,245 @@ library ClusterLib { ) ); } + + function validateClusterOnRegistration( + ISSVNetworkCore.Cluster memory cluster, + bytes32 hashedCluster, + StorageData storage s + ) internal view returns (bytes32) { + bytes32 clusterData = s.clusters[hashedCluster]; + if (clusterData == bytes32(0)) { + if ( + cluster.validatorCount != 0 || + cluster.networkFeeIndex != 0 || + cluster.index != 0 || + cluster.balance != 0 || + !cluster.active + ) { + revert ISSVNetworkCore.IncorrectClusterState(); + } + } else if (clusterData != hashClusterData(cluster)) { + revert ISSVNetworkCore.IncorrectClusterState(); + } else { + validateClusterIsNotLiquidated(cluster); + } + return hashedCluster; + } + + function updateClusterOnRegistration( + ISSVNetworkCore.Cluster memory cluster, + uint256 feeAmount, + uint256 operatorsLength, + uint64[] memory operatorIds, + IERC20 feeToken, + ISSVNetworkCore.Account memory accountData, + StorageData storage s, + StorageProtocol storage sp, + bool isTokenFee + ) internal returns (uint64 burnRate) { + cluster.balance += feeAmount; + + if (cluster.active) { + uint64 clusterIndex; + + for (uint256 i; i < operatorsLength; ) { + uint64 operatorId = operatorIds[i]; + { + if (i + 1 < operatorsLength) { + if (operatorId > operatorIds[i + 1]) { + revert ISSVNetworkCore.UnsortedOperatorsList(); + } else if (operatorId == operatorIds[i + 1]) { + revert ISSVNetworkCore.OperatorsListNotUnique(); + } + } + } + + ISSVNetworkCore.Operator memory operator = s.operators[operatorId]; + if (operator.snapshot.block == 0) { + revert ISSVNetworkCore.OperatorDoesNotExist(); + } + + // check if the deposit token is the same as operator's fee token + if (isTokenFee && operator.feeToken != feeToken) { + revert ISSVNetworkCore.FeeTokenMismatch(); + } + if (operator.whitelisted) { + address whitelisted = s.operatorsWhitelist[operatorId]; + if (whitelisted != address(0) && whitelisted != msg.sender) { + revert ISSVNetworkCore.CallerNotWhitelisted(); + } + } + operator.updateSnapshot(); + if (++operator.validatorCount > sp.validatorsPerOperatorLimit) { + revert ISSVNetworkCore.ExceedValidatorLimit(); + } + clusterIndex += operator.snapshot.index; + burnRate += operator.fee; + + s.operators[operatorId] = operator; + + unchecked { + ++i; + } + } + updateClusterData(cluster, clusterIndex, sp.currentNetworkFeeIndex()); + if (isTokenFee) { + updateAccountBalance(cluster, sp.currentNetworkFeeIndex(), accountData); + } + sp.updateDAO(true, 1); + } + + ++cluster.validatorCount; + } + + function deposit( + ISSVNetworkCore.Cluster memory cluster, + bytes32 hashedCluster, + IERC20 feeToken, + uint256 feeAmount + ) internal { + StorageData storage s = SSVStorage.load(); + + validateCluster(cluster, hashedCluster, s); + + cluster.balance += feeAmount; + + s.clusters[hashedCluster] = hashClusterData(cluster); + + CoreLib.deposit(feeAmount, feeToken); + } + + function liquidateCluster( + ISSVNetworkCore.Cluster memory cluster, + address clusterOwner, + uint64[] memory operatorIds, + bytes32 hashedCluster, + IERC20 feeToken, + bool isTokenFee + ) internal { + StorageData storage s = SSVStorage.load(); + + validateCluster(cluster, hashedCluster, s); + validateClusterIsNotLiquidated(cluster); + + StorageProtocol storage sp = SSVStorageProtocol.load(); + + (uint64 clusterIndex, uint64 burnRate) = OperatorLib.updateOperators( + operatorIds, + false, + cluster.validatorCount, + s + ); + + bytes32 account = keccak256(abi.encodePacked(clusterOwner)); + ISSVNetworkCore.Account memory accountData = s.accounts[account]; + + uint64 clusterNetworkFee = updateClusterBalance( + cluster, + clusterIndex, + sp.currentNetworkFeeIndex(), + accountData, + isTokenFee + ); + + if ( + clusterOwner != msg.sender && + !isLiquidatable( + cluster, + burnRate, + sp.networkFee, + sp.minimumBlocksBeforeLiquidation, + sp.minimumLiquidationCollateral, + accountData, + isTokenFee + ) + ) { + revert ISSVNetworkCore.ClusterNotLiquidatable(); + } + + sp.updateDAO(false, cluster.validatorCount); + + uint256 balanceLiquidatable; + + if (cluster.balance != 0) { + balanceLiquidatable = cluster.balance; + cluster.balance = 0; + } + cluster.index = 0; + cluster.networkFeeIndex = 0; + cluster.active = false; + + s.clusters[hashedCluster] = hashClusterData(cluster); + + // accountData.validatorCount -= cluster.validatorCount; + s.accounts[account] = accountData; + + if (balanceLiquidatable != 0) { + CoreLib.transferBalance(msg.sender, balanceLiquidatable, feeToken); + } + + if (clusterNetworkFee != 0) { + CoreLib.transferBalance(msg.sender, clusterNetworkFee, s.token); + } + } + + function reactivateCluster( + ISSVNetworkCore.Cluster memory cluster, + bytes32 hashedCluster, + uint64[] calldata operatorIds, + uint256 feeAmount, + IERC20 feeToken, + uint256 ssvAmount, + bool isTokenFee + ) internal { + StorageData storage s = SSVStorage.load(); + + validateCluster(cluster, hashedCluster, s); + if (cluster.active) revert ISSVNetworkCore.ClusterAlreadyEnabled(); + + StorageProtocol storage sp = SSVStorageProtocol.load(); + + (uint64 clusterIndex, uint64 burnRate) = OperatorLib.updateOperators( + operatorIds, + true, + cluster.validatorCount, + s + ); + + cluster.balance += feeAmount; + cluster.active = true; + cluster.index = clusterIndex; + cluster.networkFeeIndex = sp.currentNetworkFeeIndex(); + + sp.updateDAO(true, cluster.validatorCount); + + bytes32 account = keccak256(abi.encodePacked(msg.sender)); + ISSVNetworkCore.Account memory accountData = s.accounts[account]; + accountData.ssvBalance += ssvAmount.shrink(); + + if ( + isLiquidatable( + cluster, + burnRate, + sp.networkFee, + sp.minimumBlocksBeforeLiquidation, + sp.minimumLiquidationCollateral, + accountData, + isTokenFee + ) + ) { + revert ISSVNetworkCore.InsufficientBalance(); + } + + s.clusters[hashedCluster] = hashClusterData(cluster); + + if (feeAmount > 0) { + CoreLib.deposit(feeAmount, feeToken); + } + + if (ssvAmount > 0) { + s.accounts[account] = accountData; + CoreLib.deposit(ssvAmount, s.token); + } + } } diff --git a/contracts/libraries/CoreLib.sol b/contracts/libraries/CoreLib.sol index e8d31429..af04865b 100644 --- a/contracts/libraries/CoreLib.sol +++ b/contracts/libraries/CoreLib.sol @@ -10,14 +10,17 @@ library CoreLib { return "v1.0.0.rc3"; } - function transferBalance(address to, uint256 amount) internal { - if (!SSVStorage.load().token.transfer(to, amount)) { + function transferBalance(address to, uint256 amount, IERC20 token) internal { + IERC20 targetToken; + targetToken = token == IERC20(address(0)) ? SSVStorage.load().token : token; + + if (!targetToken.transfer(to, amount)) { revert ISSVNetworkCore.TokenTransferFailed(); } } - function deposit(uint256 amount) internal { - if (!SSVStorage.load().token.transferFrom(msg.sender, address(this), amount)) { + function deposit(uint256 amount, IERC20 token) internal { + if (!token.transferFrom(msg.sender, address(this), amount)) { revert ISSVNetworkCore.TokenTransferFailed(); } } diff --git a/contracts/libraries/SSVStorage.sol b/contracts/libraries/SSVStorage.sol index 1ddd79aa..43a3d90f 100644 --- a/contracts/libraries/SSVStorage.sol +++ b/contracts/libraries/SSVStorage.sol @@ -33,10 +33,12 @@ struct StorageData { IERC20 token; /// @notice Counter keeping track of the last Operator ID issued Counters.Counter lastOperatorId; + /// @notice Maps each account's owner to its corresponding account data + mapping(bytes32 => ISSVNetworkCore.Account) accounts; } library SSVStorage { - uint256 constant private SSV_STORAGE_POSITION = uint256(keccak256("ssv.network.storage.main")) - 1; + uint256 private constant SSV_STORAGE_POSITION = uint256(keccak256("ssv.network.storage.main")) - 1; function load() internal pure returns (StorageData storage sd) { uint256 position = SSV_STORAGE_POSITION; diff --git a/contracts/modules/SSVClusters.sol b/contracts/modules/SSVClusters.sol index 8d1df9aa..9728e299 100644 --- a/contracts/modules/SSVClusters.sol +++ b/contracts/modules/SSVClusters.sol @@ -13,117 +13,131 @@ contract SSVClusters is ISSVClusters { using ClusterLib for Cluster; using OperatorLib for Operator; using ProtocolLib for StorageProtocol; + using Types256 for uint256; + uint64 private constant MIN_OPERATORS_LENGTH = 4; uint64 private constant MAX_OPERATORS_LENGTH = 13; uint64 private constant MODULO_OPERATORS_LENGTH = 3; uint64 private constant PUBLIC_KEY_LENGTH = 48; + // legacy cluster function registerValidator( bytes calldata publicKey, uint64[] memory operatorIds, bytes calldata sharesData, - uint256 amount, + uint256 feeAmount, Cluster memory cluster - ) external override { + ) external { StorageData storage s = SSVStorage.load(); StorageProtocol storage sp = SSVStorageProtocol.load(); uint256 operatorsLength = operatorIds.length; - { - if ( - operatorsLength < MIN_OPERATORS_LENGTH || - operatorsLength > MAX_OPERATORS_LENGTH || - operatorsLength % MODULO_OPERATORS_LENGTH != 1 - ) { - revert InvalidOperatorIdsLength(); - } - if (publicKey.length != PUBLIC_KEY_LENGTH) revert InvalidPublicKeyLength(); + if ( + operatorsLength < MIN_OPERATORS_LENGTH || + operatorsLength > MAX_OPERATORS_LENGTH || + operatorsLength % MODULO_OPERATORS_LENGTH != 1 + ) { + revert InvalidOperatorIdsLength(); + } - bytes32 hashedPk = keccak256(abi.encodePacked(publicKey, msg.sender)); + registerValidatorPublicKey(publicKey, operatorIds, s); - if (s.validatorPKs[hashedPk] != bytes32(0)) { - revert ValidatorAlreadyExists(); - } + bytes32 hashedCluster = cluster.validateClusterOnRegistration( + keccak256(abi.encodePacked(msg.sender, operatorIds)), + s + ); - s.validatorPKs[hashedPk] = bytes32(uint256(keccak256(abi.encodePacked(operatorIds))) | uint256(0x01)); // set LSB to 1 - } - bytes32 hashedCluster = keccak256(abi.encodePacked(msg.sender, operatorIds)); + bytes32 account = keccak256(abi.encodePacked(msg.sender)); + Account memory accountData = s.accounts[account]; - { - bytes32 clusterData = s.clusters[hashedCluster]; - if (clusterData == bytes32(0)) { - if ( - cluster.validatorCount != 0 || - cluster.networkFeeIndex != 0 || - cluster.index != 0 || - cluster.balance != 0 || - !cluster.active - ) { - revert IncorrectClusterState(); - } - } else if (clusterData != cluster.hashClusterData()) { - revert IncorrectClusterState(); - } else { - cluster.validateClusterIsNotLiquidated(); - } + uint64 burnRate = cluster.updateClusterOnRegistration( + feeAmount, + operatorsLength, + operatorIds, + IERC20(address(0)), + accountData, + s, + sp, + false + ); + + if ( + cluster.isLiquidatable( + burnRate, + sp.networkFee, + sp.minimumBlocksBeforeLiquidation, + sp.minimumLiquidationCollateral, + accountData, + false + ) + ) { + revert InsufficientBalance(); } - cluster.balance += amount; + s.clusters[hashedCluster] = cluster.hashClusterData(); - uint64 burnRate; + if (feeAmount != 0) { + CoreLib.deposit(feeAmount, s.token); + } - if (cluster.active) { - uint64 clusterIndex; + emit ValidatorAdded(msg.sender, operatorIds, publicKey, sharesData, cluster); + } - for (uint256 i; i < operatorsLength; ) { - uint64 operatorId = operatorIds[i]; - { - if (i + 1 < operatorsLength) { - if (operatorId > operatorIds[i + 1]) { - revert UnsortedOperatorsList(); - } else if (operatorId == operatorIds[i + 1]) { - revert OperatorsListNotUnique(); - } - } - } + // token cluster + function registerValidator( + bytes calldata publicKey, + uint64[] memory operatorIds, + bytes calldata sharesData, + uint256 feeAmount, + IERC20 feeToken, + uint256 ssvAmount, + Cluster memory cluster + ) external override { + StorageData storage s = SSVStorage.load(); + StorageProtocol storage sp = SSVStorageProtocol.load(); - Operator memory operator = s.operators[operatorId]; - if (operator.snapshot.block == 0) { - revert OperatorDoesNotExist(); - } - if (operator.whitelisted) { - address whitelisted = s.operatorsWhitelist[operatorId]; - if (whitelisted != address(0) && whitelisted != msg.sender) { - revert CallerNotWhitelisted(); - } - } - operator.updateSnapshot(); - if (++operator.validatorCount > sp.validatorsPerOperatorLimit) { - revert ExceedValidatorLimit(); - } - clusterIndex += operator.snapshot.index; - burnRate += operator.fee; + uint256 operatorsLength = operatorIds.length; - s.operators[operatorId] = operator; + if ( + operatorsLength < MIN_OPERATORS_LENGTH || + operatorsLength > MAX_OPERATORS_LENGTH || + operatorsLength % MODULO_OPERATORS_LENGTH != 1 + ) { + revert InvalidOperatorIdsLength(); + } - unchecked { - ++i; - } - } - cluster.updateClusterData(clusterIndex, sp.currentNetworkFeeIndex()); + registerValidatorPublicKey(publicKey, operatorIds, s); - sp.updateDAO(true, 1); - } + bytes32 hashedCluster = cluster.validateClusterOnRegistration( + keccak256(abi.encodePacked(msg.sender, operatorIds, feeToken)), + s + ); + + bytes32 account = keccak256(abi.encodePacked(msg.sender)); + Account memory accountData = s.accounts[account]; + ++accountData.validatorCount; + accountData.ssvBalance += ssvAmount.shrink(); - ++cluster.validatorCount; + uint64 burnRate = cluster.updateClusterOnRegistration( + feeAmount, + operatorsLength, + operatorIds, + feeToken, + accountData, + s, + sp, + true + ); if ( cluster.isLiquidatable( burnRate, sp.networkFee, sp.minimumBlocksBeforeLiquidation, - sp.minimumLiquidationCollateral + sp.minimumLiquidationCollateral, + accountData, + true ) ) { revert InsufficientBalance(); @@ -131,8 +145,12 @@ contract SSVClusters is ISSVClusters { s.clusters[hashedCluster] = cluster.hashClusterData(); - if (amount != 0) { - CoreLib.deposit(amount); + if (feeAmount != 0) { + CoreLib.deposit(feeAmount, s.token); + } + + if (ssvAmount != 0) { + CoreLib.deposit(feeAmount, s.token); } emit ValidatorAdded(msg.sender, operatorIds, publicKey, sharesData, cluster); @@ -160,7 +178,7 @@ contract SSVClusters is ISSVClusters { revert IncorrectValidatorState(); } - bytes32 hashedCluster = cluster.validateHashedCluster(msg.sender, operatorIds, s); + bytes32 hashedCluster = cluster.validateCluster(msg.sender, operatorIds, s); { if (cluster.active) { @@ -179,118 +197,115 @@ contract SSVClusters is ISSVClusters { s.clusters[hashedCluster] = cluster.hashClusterData(); + // not sure about the usage of Account.validatorCount for now + // --s.accounts[keccak256(abi.encodePacked(msg.sender))].validatorCount; + emit ValidatorRemoved(msg.sender, operatorIds, publicKey, cluster); } + // legacy clusters function liquidate(address clusterOwner, uint64[] memory operatorIds, Cluster memory cluster) external override { - StorageData storage s = SSVStorage.load(); - - bytes32 hashedCluster = cluster.validateHashedCluster(clusterOwner, operatorIds, s); - cluster.validateClusterIsNotLiquidated(); - - StorageProtocol storage sp = SSVStorageProtocol.load(); - - (uint64 clusterIndex, uint64 burnRate) = OperatorLib.updateOperators( + cluster.liquidateCluster( + clusterOwner, operatorIds, - false, - cluster.validatorCount, - s + keccak256(abi.encodePacked(msg.sender, operatorIds)), + SSVStorage.load().token, + false ); - cluster.updateBalance(clusterIndex, sp.currentNetworkFeeIndex()); - - uint256 balanceLiquidatable; - - if ( - clusterOwner != msg.sender && - !cluster.isLiquidatable( - burnRate, - sp.networkFee, - sp.minimumBlocksBeforeLiquidation, - sp.minimumLiquidationCollateral - ) - ) { - revert ClusterNotLiquidatable(); - } - - sp.updateDAO(false, cluster.validatorCount); - - if (cluster.balance != 0) { - balanceLiquidatable = cluster.balance; - cluster.balance = 0; - } - cluster.index = 0; - cluster.networkFeeIndex = 0; - cluster.active = false; - - s.clusters[hashedCluster] = cluster.hashClusterData(); - - if (balanceLiquidatable != 0) { - CoreLib.transferBalance(msg.sender, balanceLiquidatable); - } - emit ClusterLiquidated(clusterOwner, operatorIds, cluster); } - function reactivate(uint64[] calldata operatorIds, uint256 amount, Cluster memory cluster) external override { - StorageData storage s = SSVStorage.load(); - - bytes32 hashedCluster = cluster.validateHashedCluster(msg.sender, operatorIds, s); - if (cluster.active) revert ClusterAlreadyEnabled(); + // token cluster + function liquidate( + address clusterOwner, + uint64[] memory operatorIds, + IERC20 feeToken, + Cluster memory cluster + ) external override { + cluster.liquidateCluster( + clusterOwner, + operatorIds, + keccak256(abi.encodePacked(msg.sender, operatorIds, feeToken)), + feeToken, + true + ); - StorageProtocol storage sp = SSVStorageProtocol.load(); + emit ClusterLiquidated(clusterOwner, operatorIds, cluster); + } - (uint64 clusterIndex, uint64 burnRate) = OperatorLib.updateOperators( + // legacy cluster + function reactivate(uint64[] calldata operatorIds, uint256 ssvAmount, Cluster memory cluster) external override { + cluster.reactivateCluster( + keccak256(abi.encodePacked(msg.sender, operatorIds)), operatorIds, - true, - cluster.validatorCount, - s + feeAmount, + feeToken, + ssvAmount, + true ); + emit ClusterReactivated(msg.sender, operatorIds, cluster); + } - cluster.balance += amount; - cluster.active = true; - cluster.index = clusterIndex; - cluster.networkFeeIndex = sp.currentNetworkFeeIndex(); - - sp.updateDAO(true, cluster.validatorCount); + // token cluster + function reactivate( + uint64[] calldata operatorIds, + uint256 feeAmount, + IERC20 feeToken, + uint256 ssvAmount, + Cluster memory cluster + ) external override { + cluster.reactivateCluster( + keccak256(abi.encodePacked(msg.sender, operatorIds, feeToken)), + operatorIds, + feeAmount, + feeToken, + ssvAmount, + true + ); + emit ClusterReactivated(msg.sender, operatorIds, cluster); + } - if ( - cluster.isLiquidatable( - burnRate, - sp.networkFee, - sp.minimumBlocksBeforeLiquidation, - sp.minimumLiquidationCollateral - ) - ) { - revert InsufficientBalance(); - } + function depositNetworkFees(address accountOwner, uint256 ssvAmount) external { + StorageData storage s = SSVStorage.load(); - s.clusters[hashedCluster] = cluster.hashClusterData(); + bytes32 account = keccak256(abi.encodePacked(msg.sender)); + Account memory accountData = s.accounts[account]; - if (amount > 0) { - CoreLib.deposit(amount); - } + accountData.ssvBalance += ssvAmount; - emit ClusterReactivated(msg.sender, operatorIds, cluster); + CoreLib.deposit(ssvAmount, s.token); } - function deposit( + // legacy cluster + function depositClusterBalance( address clusterOwner, uint64[] calldata operatorIds, - uint256 amount, + uint256 ssvAmount, Cluster memory cluster ) external override { - StorageData storage s = SSVStorage.load(); - - bytes32 hashedCluster = cluster.validateHashedCluster(clusterOwner, operatorIds, s); - - cluster.balance += amount; + cluster.deposit(keccak256(abi.encodePacked(clusterOwner, operatorIds)), SSVStorage.load().token, ssvAmount); - s.clusters[hashedCluster] = cluster.hashClusterData(); + emit ClusterDeposited(clusterOwner, operatorIds, ssvAmount, cluster); + } - CoreLib.deposit(amount); + // token cluster + function depositClusterBalance( + address clusterOwner, + uint64[] calldata operatorIds, + uint256 feeAmount, + IERC20 feeToken, + Cluster memory cluster + ) external override { + // add check to match feeToken with Operator.feeToken + cluster.deposit( + clusterOwner, + keccak256(abi.encodePacked(clusterOwner, operatorIds, feeToken)), + feeToken, + feeAmount + ); - emit ClusterDeposited(clusterOwner, operatorIds, amount, cluster); + emit ClusterTokenDeposited(clusterOwner, operatorIds, feeToken, feeAmount, cluster); } function withdraw(uint64[] calldata operatorIds, uint256 amount, Cluster memory cluster) external override { @@ -344,4 +359,20 @@ contract SSVClusters is ISSVClusters { emit ClusterWithdrawn(msg.sender, operatorIds, amount, cluster); } + + function registerValidatorPublicKey( + bytes calldata publicKey, + uint64[] memory operatorIds, + StorageData storage s + ) private { + if (publicKey.length != PUBLIC_KEY_LENGTH) revert ISSVNetworkCore.InvalidPublicKeyLength(); + + bytes32 hashedPk = keccak256(abi.encodePacked(publicKey, msg.sender)); + + if (s.validatorPKs[hashedPk] != bytes32(0)) { + revert ISSVNetworkCore.ValidatorAlreadyExists(); + } + + s.validatorPKs[hashedPk] = bytes32(uint256(keccak256(abi.encodePacked(operatorIds))) | uint256(0x01)); // set LSB to 1 + } } diff --git a/contracts/modules/SSVOperators.sol b/contracts/modules/SSVOperators.sol index 38347aeb..05f38600 100644 --- a/contracts/modules/SSVOperators.sol +++ b/contracts/modules/SSVOperators.sol @@ -8,6 +8,7 @@ import "../libraries/SSVStorageProtocol.sol"; import "../libraries/OperatorLib.sol"; import "../libraries/CoreLib.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/utils/Counters.sol"; contract SSVOperators is ISSVOperators { @@ -23,7 +24,7 @@ contract SSVOperators is ISSVOperators { /* Operator External Functions */ /*******************************/ - function registerOperator(bytes calldata publicKey, uint256 fee) external override returns (uint64 id) { + function registerOperator(bytes calldata publicKey, uint256 fee, IERC20 feeToken) external override returns (uint64 id) { if (fee != 0 && fee < MINIMAL_OPERATOR_FEE) { revert ISSVNetworkCore.FeeTooLow(); } @@ -39,7 +40,8 @@ contract SSVOperators is ISSVOperators { snapshot: ISSVNetworkCore.Snapshot({block: uint32(block.number), index: 0, balance: 0}), validatorCount: 0, fee: fee.shrink(), - whitelisted: false + whitelisted: false, + feeToken: feeToken }); s.operatorsPKs[hashedPk] = id; @@ -66,7 +68,7 @@ contract SSVOperators is ISSVOperators { } if (currentBalance > 0) { - _transferOperatorBalanceUnsafe(operatorId, currentBalance.expand()); + _transferOperatorBalanceUnsafe(operatorId, currentBalance.expand(), operator.feeToken); } emit OperatorRemoved(operatorId); } @@ -197,11 +199,11 @@ contract SSVOperators is ISSVOperators { s.operators[operatorId] = operator; - _transferOperatorBalanceUnsafe(operatorId, shrunkWithdrawn.expand()); + _transferOperatorBalanceUnsafe(operatorId, shrunkWithdrawn.expand(), s.token); } - function _transferOperatorBalanceUnsafe(uint64 operatorId, uint256 amount) private { - CoreLib.transferBalance(msg.sender, amount); + function _transferOperatorBalanceUnsafe(uint64 operatorId, uint256 amount, IERC20 feeToken) private { + CoreLib.transferBalance(msg.sender, amount, feeToken); emit OperatorWithdrawn(msg.sender, operatorId, amount); } } diff --git a/contracts/test/SSVNetworkUpgrade.sol b/contracts/test/SSVNetworkUpgrade.sol index acbbd995..4596a720 100644 --- a/contracts/test/SSVNetworkUpgrade.sol +++ b/contracts/test/SSVNetworkUpgrade.sol @@ -193,6 +193,8 @@ contract SSVNetworkUpgrade is uint64[] memory operatorIds, bytes calldata sharesData, uint256 amount, + IERC20 feeToken, + uint256 ssvAmount, ISSVNetworkCore.Cluster memory cluster ) external override { _delegateCall( @@ -224,7 +226,12 @@ contract SSVNetworkUpgrade is ); } - function liquidate(address owner, uint64[] calldata operatorIds, ISSVNetworkCore.Cluster memory cluster) external { + function liquidate( + address owner, + uint64[] calldata operatorIds, + IERC20 feeToken, + ISSVNetworkCore.Cluster memory cluster + ) external { _delegateCall( SSVStorage.load().ssvContracts[SSVModules.SSV_CLUSTERS], abi.encodeWithSignature( @@ -239,6 +246,7 @@ contract SSVNetworkUpgrade is function reactivate( uint64[] calldata operatorIds, uint256 amount, + IERC20 feeToken, ISSVNetworkCore.Cluster memory cluster ) external override { _delegateCall( @@ -256,6 +264,7 @@ contract SSVNetworkUpgrade is address owner, uint64[] calldata operatorIds, uint256 amount, + IERC20 feeToken, ISSVNetworkCore.Cluster memory cluster ) external override { _delegateCall(