From 549632458688cc9ef91b9911ebc96d49e10f4a72 Mon Sep 17 00:00:00 2001 From: Marco Date: Tue, 22 Aug 2023 13:11:33 +0200 Subject: [PATCH] set a maximum operator fee (SSV) --- contracts/SSVNetwork.sol | 10 ++++- contracts/SSVNetworkViews.sol | 4 ++ contracts/interfaces/ISSVDAO.sol | 6 +++ contracts/interfaces/ISSVNetworkCore.sol | 1 + contracts/interfaces/ISSVViews.sol | 48 ++++++++++++++++++---- contracts/libraries/SSVStorageProtocol.sol | 2 + contracts/modules/SSVDAO.sol | 5 +++ contracts/modules/SSVOperators.sol | 8 ++++ contracts/modules/SSVViews.sol | 4 ++ contracts/test/SSVNetworkUpgrade.sol | 7 ++++ contracts/test/SSVViewsT.sol | 9 +++- test/helpers/contract-helpers.ts | 5 ++- test/helpers/gas-usage.ts | 18 ++++---- test/operators/register.ts | 7 ++++ test/operators/update-fee.ts | 37 +++++++++++++++-- 15 files changed, 147 insertions(+), 24 deletions(-) diff --git a/contracts/SSVNetwork.sol b/contracts/SSVNetwork.sol index 976b63b5..4b63019d 100644 --- a/contracts/SSVNetwork.sol +++ b/contracts/SSVNetwork.sol @@ -188,7 +188,11 @@ 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, + ISSVNetworkCore.Cluster memory cluster + ) external { _delegate(SSVStorage.load().ssvContracts[SSVModules.SSV_CLUSTERS]); } @@ -245,6 +249,10 @@ contract SSVNetwork is _delegate(SSVStorage.load().ssvContracts[SSVModules.SSV_DAO]); } + function updateMaximumOperatorFee(uint64 maxFee) external override { + _delegate(SSVStorage.load().ssvContracts[SSVModules.SSV_DAO]); + } + function getVersion() external pure override returns (string memory version) { return CoreLib.getVersion(); } diff --git a/contracts/SSVNetworkViews.sol b/contracts/SSVNetworkViews.sol index 393348a7..b731d7f8 100644 --- a/contracts/SSVNetworkViews.sol +++ b/contracts/SSVNetworkViews.sol @@ -119,6 +119,10 @@ contract SSVNetworkViews is UUPSUpgradeable, Ownable2StepUpgradeable, ISSVViews return ssvNetwork.getOperatorFeeIncreaseLimit(); } + function getMaximumOperatorFee() external view override returns (uint64 operatorMaxFee) { + return ssvNetwork.getMaximumOperatorFee(); + } + function getOperatorFeePeriods() external view diff --git a/contracts/interfaces/ISSVDAO.sol b/contracts/interfaces/ISSVDAO.sol index 359a8d51..e1330776 100644 --- a/contracts/interfaces/ISSVDAO.sol +++ b/contracts/interfaces/ISSVDAO.sol @@ -32,6 +32,10 @@ interface ISSVDAO is ISSVNetworkCore { /// @param amount The new minimum collateral amount (SSV) function updateMinimumLiquidationCollateral(uint256 amount) external; + /// @notice Updates the maximum fee an operator that uses SSV token can set + /// @param maxFee The new maximum fee (SSV) + function updateMaximumOperatorFee(uint64 maxFee) external; + event OperatorFeeIncreaseLimitUpdated(uint64 value); event DeclareOperatorFeePeriodUpdated(uint64 value); @@ -55,4 +59,6 @@ interface ISSVDAO is ISSVNetworkCore { * @param recipient The recipient address. */ event NetworkEarningsWithdrawn(uint256 value, address recipient); + + event OperatorMaximumFeeUpdated(uint64 maxFee); } diff --git a/contracts/interfaces/ISSVNetworkCore.sol b/contracts/interfaces/ISSVNetworkCore.sol index a3206e7e..401d715b 100644 --- a/contracts/interfaces/ISSVNetworkCore.sol +++ b/contracts/interfaces/ISSVNetworkCore.sol @@ -87,4 +87,5 @@ interface ISSVNetworkCore { error OperatorAlreadyExists(); // 0x289c9494 error TargetModuleDoesNotExist(); // 0x8f9195fb error MaxValueExceeded(); // 0x91aa3017 + error FeeTooHigh(); // 0xcd4e6167 } diff --git a/contracts/interfaces/ISSVViews.sol b/contracts/interfaces/ISSVViews.sol index 39cff14b..8b5c1657 100644 --- a/contracts/interfaces/ISSVViews.sol +++ b/contracts/interfaces/ISSVViews.sol @@ -14,14 +14,16 @@ interface ISSVViews is ISSVNetworkCore { /// @param operatorId The ID of the operator /// @return fee The fee associated with the operator (SSV). If the operator does not exist, the returned value is 0. function getOperatorFee(uint64 operatorId) external view returns (uint256 fee); - + /// @notice Gets the declared operator fee /// @param operatorId The ID of the operator /// @return isFeeDeclared A boolean indicating if the fee is declared /// @return fee The declared operator fee (SSV) /// @return approvalBeginTime The time when the fee approval process begins /// @return approvalEndTime The time when the fee approval process ends - function getOperatorDeclaredFee(uint64 operatorId) external view returns (bool isFeeDeclared, uint256 fee, uint64 approvalBeginTime, uint64 approvalEndTime); + function getOperatorDeclaredFee( + uint64 operatorId + ) external view returns (bool isFeeDeclared, uint256 fee, uint64 approvalBeginTime, uint64 approvalEndTime); /// @notice Gets operator details by ID /// @param operatorId The ID of the operator @@ -31,25 +33,42 @@ interface ISSVViews is ISSVNetworkCore { /// @return whitelisted The whitelisted address of the operator, if any /// @return isPrivate A boolean indicating if the operator is private /// @return active A boolean indicating if the operator is active - function getOperatorById(uint64 operatorId) external view returns (address owner, uint256 fee, uint32 validatorCount, address whitelisted, bool isPrivate, bool active); + function getOperatorById( + uint64 operatorId + ) + external + view + returns (address owner, uint256 fee, uint32 validatorCount, address whitelisted, bool isPrivate, bool active); /// @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 /// @return isLiquidatable A boolean indicating if the cluster can be liquidated - function isLiquidatable(address owner, uint64[] memory operatorIds, Cluster memory cluster) external view returns (bool isLiquidatable); + function isLiquidatable( + address owner, + uint64[] memory operatorIds, + Cluster memory cluster + ) external view returns (bool isLiquidatable); /// @notice Checks if the cluster is liquidated /// @param owner The owner address of the cluster /// @param operatorIds The IDs of the operators in the cluster /// @return isLiquidated A boolean indicating if the cluster is liquidated - function isLiquidated(address owner, uint64[] memory operatorIds, Cluster memory cluster) external view returns (bool isLiquidated); + function isLiquidated( + address owner, + uint64[] memory operatorIds, + Cluster memory cluster + ) external view returns (bool isLiquidated); /// @notice Gets the burn rate of the cluster /// @param owner The owner address of the cluster /// @param operatorIds The IDs of the operators in the cluster /// @return burnRate The burn rate of the cluster (SSV) - function getBurnRate(address owner, uint64[] memory operatorIds, Cluster memory cluster) external view returns (uint256 burnRate); + function getBurnRate( + address owner, + uint64[] memory operatorIds, + Cluster memory cluster + ) external view returns (uint256 burnRate); /// @notice Gets operator earnings /// @param operatorId The ID of the operator @@ -60,7 +79,11 @@ interface ISSVViews is ISSVNetworkCore { /// @param owner The owner address of the cluster /// @param operatorIds The IDs of the operators in the cluster /// @return balance The balance of the cluster (SSV) - function getBalance(address owner, uint64[] memory operatorIds, Cluster memory cluster) external view returns (uint256 balance); + function getBalance( + address owner, + uint64[] memory operatorIds, + Cluster memory cluster + ) external view returns (uint256 balance); /// @notice Gets the network fee /// @return networkFee The fee associated with the network (SSV) @@ -74,10 +97,17 @@ interface ISSVViews is ISSVNetworkCore { /// @return operatorMaxFeeIncrease The maximum limit of operator fee increase function getOperatorFeeIncreaseLimit() external view returns (uint64 operatorMaxFeeIncrease); + /// @notice Gets the operator maximum fee for operators that use SSV token + /// @return operatorMaxFee The maximum fee value (SSV) + function getMaximumOperatorFee() external view returns (uint64 operatorMaxFee); + /// @notice Gets the periods of operator fee declaration and execution /// @return declareOperatorFeePeriod The period for declaring operator fee /// @return executeOperatorFeePeriod The period for executing operator fee - function getOperatorFeePeriods() external view returns (uint64 declareOperatorFeePeriod, uint64 executeOperatorFeePeriod); + function getOperatorFeePeriods() + external + view + returns (uint64 declareOperatorFeePeriod, uint64 executeOperatorFeePeriod); /// @notice Gets the liquidation threshold period /// @return blocks The number of blocks for the liquidation threshold period @@ -94,4 +124,4 @@ interface ISSVViews is ISSVNetworkCore { /// @notice Gets the version of the contract /// @return version The version of the contract function getVersion() external view returns (string memory version); -} \ No newline at end of file +} diff --git a/contracts/libraries/SSVStorageProtocol.sol b/contracts/libraries/SSVStorageProtocol.sol index 81c2da7e..7c92cbfd 100644 --- a/contracts/libraries/SSVStorageProtocol.sol +++ b/contracts/libraries/SSVStorageProtocol.sol @@ -28,6 +28,8 @@ struct StorageProtocol { uint64 executeOperatorFeePeriod; /// @notice The maximum increase in operator fee that is allowed (percentage) uint64 operatorMaxFeeIncrease; + /// @notice The maximum value in operator fee that is allowed (SSV) + uint64 operatorMaxFee; } library SSVStorageProtocol { diff --git a/contracts/modules/SSVDAO.sol b/contracts/modules/SSVDAO.sol index b4c6a028..cec7a6cf 100644 --- a/contracts/modules/SSVDAO.sol +++ b/contracts/modules/SSVDAO.sol @@ -68,4 +68,9 @@ contract SSVDAO is ISSVDAO { SSVStorageProtocol.load().minimumLiquidationCollateral = amount.shrink(); emit MinimumLiquidationCollateralUpdated(amount); } + + function updateMaximumOperatorFee(uint64 maxFee) external override { + SSVStorageProtocol.load().operatorMaxFee = maxFee; + emit OperatorMaximumFeeUpdated(maxFee); + } } diff --git a/contracts/modules/SSVOperators.sol b/contracts/modules/SSVOperators.sol index 38347aeb..d6d33fc5 100644 --- a/contracts/modules/SSVOperators.sol +++ b/contracts/modules/SSVOperators.sol @@ -27,6 +27,10 @@ contract SSVOperators is ISSVOperators { if (fee != 0 && fee < MINIMAL_OPERATOR_FEE) { revert ISSVNetworkCore.FeeTooLow(); } + if (fee > SSVStorageProtocol.load().operatorMaxFee) { + revert ISSVNetworkCore.FeeTooHigh(); + } + StorageData storage s = SSVStorage.load(); bytes32 hashedPk = keccak256(publicKey); @@ -92,6 +96,8 @@ contract SSVOperators is ISSVOperators { StorageProtocol storage sp = SSVStorageProtocol.load(); if (fee != 0 && fee < MINIMAL_OPERATOR_FEE) revert FeeTooLow(); + if (fee > sp.operatorMaxFee) revert FeeTooHigh(); + uint64 operatorFee = s.operators[operatorId].fee; uint64 shrunkFee = fee.shrink(); @@ -129,6 +135,8 @@ contract SSVOperators is ISSVOperators { revert ApprovalNotWithinTimeframe(); } + if (feeChangeRequest.fee.expand() > SSVStorageProtocol.load().operatorMaxFee) revert FeeTooHigh(); + operator.updateSnapshot(); operator.fee = feeChangeRequest.fee; s.operators[operatorId] = operator; diff --git a/contracts/modules/SSVViews.sol b/contracts/modules/SSVViews.sol index a9acf885..2c6c2070 100644 --- a/contracts/modules/SSVViews.sol +++ b/contracts/modules/SSVViews.sol @@ -176,6 +176,10 @@ contract SSVViews is ISSVViews { return SSVStorageProtocol.load().operatorMaxFeeIncrease; } + function getMaximumOperatorFee() external view override returns (uint64 operatorMaxFee) { + return SSVStorageProtocol.load().operatorMaxFee; + } + function getOperatorFeePeriods() external view diff --git a/contracts/test/SSVNetworkUpgrade.sol b/contracts/test/SSVNetworkUpgrade.sol index acbbd995..c859582f 100644 --- a/contracts/test/SSVNetworkUpgrade.sol +++ b/contracts/test/SSVNetworkUpgrade.sol @@ -335,6 +335,13 @@ contract SSVNetworkUpgrade is ); } + function updateMaximumOperatorFee(uint64 maxFee) external override { + _delegateCall( + SSVStorage.load().ssvContracts[SSVModules.SSV_DAO], + abi.encodeWithSignature("updateMaximumOperatorFee(uint64)", maxFee) + ); + } + function _delegateCall(address ssvModule, bytes memory callMessage) internal returns (bytes memory) { /// @custom:oz-upgrades-unsafe-allow delegatecall (bool success, bytes memory result) = ssvModule.delegatecall(callMessage); diff --git a/contracts/test/SSVViewsT.sol b/contracts/test/SSVViewsT.sol index 57c3a297..428f595f 100644 --- a/contracts/test/SSVViewsT.sol +++ b/contracts/test/SSVViewsT.sol @@ -15,6 +15,7 @@ contract SSVViewsT is ISSVViews { using ClusterLib for Cluster; using OperatorLib for Operator; using ProtocolLib for StorageProtocol; + /*************************************/ /* Validator External View Functions */ /*************************************/ @@ -39,7 +40,9 @@ contract SSVViewsT is ISSVViews { fee = operator.fee.expand(); } - function getOperatorDeclaredFee(uint64 operatorId) external view override returns (bool feeDeclared, uint256, uint64, uint64) { + function getOperatorDeclaredFee( + uint64 operatorId + ) external view override returns (bool feeDeclared, uint256, uint64, uint64) { OperatorFeeChangeRequest memory opFeeChangeRequest = SSVStorage.load().operatorFeeChangeRequests[operatorId]; return ( @@ -176,6 +179,10 @@ contract SSVViewsT is ISSVViews { return SSVStorageProtocol.load().operatorMaxFeeIncrease; } + function getMaximumOperatorFee() external view override returns (uint64 operatorMaxFee) { + return SSVStorageProtocol.load().operatorMaxFee; + } + function getOperatorFeePeriods() external view diff --git a/test/helpers/contract-helpers.ts b/test/helpers/contract-helpers.ts index eedb34cb..85c34599 100644 --- a/test/helpers/contract-helpers.ts +++ b/test/helpers/contract-helpers.ts @@ -72,7 +72,8 @@ export const initializeContract = async () => { minimalOperatorFee: 100000000, minimalBlocksBeforeLiquidation: 100800, minimumLiquidationCollateral: 200000000, - validatorsPerOperatorLimit: 500 + validatorsPerOperatorLimit: 500, + maximumOperatorFee: 21257960000000 }; DB = { @@ -163,7 +164,7 @@ export const initializeContract = async () => { await DB.ssvToken.mint(DB.owners[5].address, '10000000000000000000'); await DB.ssvToken.mint(DB.owners[6].address, '10000000000000000000'); - // DB.ssvViews.contract = DB.ssvViews.contract.attach(DB.ssvNetwork.contract.address); + await DB.ssvNetwork.contract.updateMaximumOperatorFee(CONFIG.maximumOperatorFee); return { contract: DB.ssvNetwork.contract, owner: DB.ssvNetwork.owner, ssvToken: DB.ssvToken, ssvViews: DB.ssvViews.contract }; }; diff --git a/test/helpers/gas-usage.ts b/test/helpers/gas-usage.ts index b5ba133e..38ce3241 100644 --- a/test/helpers/gas-usage.ts +++ b/test/helpers/gas-usage.ts @@ -40,9 +40,10 @@ export enum GasGroup { NETWORK_FEE_CHANGE, WITHDRAW_NETWORK_EARNINGS, - OPERATOR_FEE_INCREASE_LIMIT, - OPERATOR_DECLARE_FEE_LIMIT, - OPERATOR_EXECUTE_FEE_LIMIT, + DAO_UPDATE_OPERATOR_FEE_INCREASE_LIMIT, + DAO_UPDATE_DECLARE_OPERATOR_FEE_PERIOD, + DAO_UPDATE_EXECUTE_OPERATOR_FEE_PERIOD, + DAO_UPDATE_OPERATOR_MAX_FEE, CHANGE_LIQUIDATION_THRESHOLD_PERIOD, CHANGE_MINIMUM_COLLATERAL @@ -50,14 +51,14 @@ export enum GasGroup { const MAX_GAS_PER_GROUP: any = { /* REAL GAS LIMITS */ - [GasGroup.REGISTER_OPERATOR]: 131890, + [GasGroup.REGISTER_OPERATOR]: 134500, [GasGroup.REMOVE_OPERATOR]: 70200, [GasGroup.REMOVE_OPERATOR_WITH_WITHDRAW]: 70200, [GasGroup.SET_OPERATOR_WHITELIST]: 84300, [GasGroup.DECLARE_OPERATOR_FEE]: 70000, [GasGroup.CANCEL_OPERATOR_FEE]: 41900, - [GasGroup.EXECUTE_OPERATOR_FEE]: 49900, + [GasGroup.EXECUTE_OPERATOR_FEE]: 52000, [GasGroup.REDUCE_OPERATOR_FEE]: 51900, [GasGroup.REGISTER_VALIDATOR_EXISTING_CLUSTER]: 202000, @@ -88,9 +89,10 @@ const MAX_GAS_PER_GROUP: any = { [GasGroup.NETWORK_FEE_CHANGE]: 45800, [GasGroup.WITHDRAW_NETWORK_EARNINGS]: 62200, - [GasGroup.OPERATOR_FEE_INCREASE_LIMIT]: 38200, - [GasGroup.OPERATOR_DECLARE_FEE_LIMIT]: 40900, - [GasGroup.OPERATOR_EXECUTE_FEE_LIMIT]: 41000, + [GasGroup.DAO_UPDATE_OPERATOR_FEE_INCREASE_LIMIT]: 38200, + [GasGroup.DAO_UPDATE_DECLARE_OPERATOR_FEE_PERIOD]: 40900, + [GasGroup.DAO_UPDATE_EXECUTE_OPERATOR_FEE_PERIOD]: 41000, + [GasGroup.DAO_UPDATE_OPERATOR_MAX_FEE]: 38500, [GasGroup.CHANGE_LIQUIDATION_THRESHOLD_PERIOD]: 41000, [GasGroup.CHANGE_MINIMUM_COLLATERAL]: 41200, diff --git a/test/operators/register.ts b/test/operators/register.ts index d17b0a74..480dea69 100644 --- a/test/operators/register.ts +++ b/test/operators/register.ts @@ -115,6 +115,13 @@ describe('Register Operator Tests', () => { )).to.be.revertedWithCustomError(ssvNetworkContract, 'FeeTooLow'); }); + it('Register an operator with a fee thats too high reverts "FeeTooHigh"', async () => { + await expect(ssvNetworkContract.registerOperator( + helpers.DataGenerator.publicKey(0), + 2e14, + )).to.be.revertedWithCustomError(ssvNetworkContract, 'FeeTooHigh'); + }); + it('Register same operator twice reverts "OperatorAlreadyExists"', async () => { const publicKey = helpers.DataGenerator.publicKey(1); await ssvNetworkContract.connect(helpers.DB.owners[1]).registerOperator( diff --git a/test/operators/update-fee.ts b/test/operators/update-fee.ts index b0d2de27..037d9254 100644 --- a/test/operators/update-fee.ts +++ b/test/operators/update-fee.ts @@ -72,6 +72,10 @@ describe('Operator Fee Tests', () => { expect(await ssvViews.getOperatorFee(12)).to.equal(0); }); + it('Get operator maximum fee limit', async () => { + expect(await ssvViews.getMaximumOperatorFee()).to.equal(helpers.CONFIG.maximumOperatorFee); + }); + it('Declare fee of operator I do not own reverts "CallerNotOwner"', async () => { await expect(ssvNetworkContract.connect(helpers.DB.owners[1]).declareOperatorFee(1, initialFee + initialFee / 10 )).to.be.revertedWithCustomError(ssvNetworkContract, 'CallerNotOwner'); @@ -111,6 +115,11 @@ describe('Operator Fee Tests', () => { )).to.be.revertedWithCustomError(ssvNetworkContract, 'FeeExceedsIncreaseLimit'); }); + it('Declare fee above the operators max fee limit reverts "FeeTooHigh"', async () => { + await expect(ssvNetworkContract.connect(helpers.DB.owners[2]).declareOperatorFee(1, 2e14 + )).to.be.revertedWithCustomError(ssvNetworkContract, 'FeeTooHigh'); + }); + it('Cancel declared fee without a pending request reverts "NoFeeDeclared"', async () => { await expect(ssvNetworkContract.connect(helpers.DB.owners[2]).cancelDeclaredOperatorFee(1 )).to.be.revertedWithCustomError(ssvNetworkContract, 'NoFeeDeclared'); @@ -173,15 +182,32 @@ describe('Operator Fee Tests', () => { expect(isFeeDeclared).to.equal(false); }); + it('Reduce maximum fee limit after declaring a fee change reverts "FeeTooHigh', async () => { + await ssvNetworkContract.connect(helpers.DB.owners[2]).declareOperatorFee(1, initialFee + initialFee / 10); + await ssvNetworkContract.updateMaximumOperatorFee(1000); + + await progressTime(helpers.CONFIG.declareOperatorFeePeriod); + + await expect(ssvNetworkContract.connect(helpers.DB.owners[2]).executeOperatorFee(1 + )).to.be.revertedWithCustomError(ssvNetworkContract, 'FeeTooHigh'); + }); + + //Dao it('DAO increase the fee emits "OperatorFeeIncreaseLimitUpdated"', async () => { await expect(ssvNetworkContract.updateOperatorFeeIncreaseLimit(1000 )).to.emit(ssvNetworkContract, 'OperatorFeeIncreaseLimitUpdated'); }); + it('DAO update the maximum operator fee emits "OperatorMaximumFeeUpdated"', async () => { + await expect(ssvNetworkContract.updateMaximumOperatorFee(2e10 + )).to.emit(ssvNetworkContract, 'OperatorMaximumFeeUpdated') + .withArgs(2e10); + }); + it('DAO increase the fee gas limits"', async () => { await trackGas(ssvNetworkContract.updateOperatorFeeIncreaseLimit(1000 - ), [GasGroup.OPERATOR_FEE_INCREASE_LIMIT]); + ), [GasGroup.DAO_UPDATE_OPERATOR_FEE_INCREASE_LIMIT]); }); it('DAO update the declare fee period emits "DeclareOperatorFeePeriodUpdated"', async () => { @@ -191,7 +217,7 @@ describe('Operator Fee Tests', () => { it('DAO update the declare fee period gas limits"', async () => { await trackGas(ssvNetworkContract.updateDeclareOperatorFeePeriod(1200 - ), [GasGroup.OPERATOR_DECLARE_FEE_LIMIT]); + ), [GasGroup.DAO_UPDATE_DECLARE_OPERATOR_FEE_PERIOD]); }); it('DAO update the execute fee period emits "ExecuteOperatorFeePeriodUpdated"', async () => { @@ -201,7 +227,12 @@ describe('Operator Fee Tests', () => { it('DAO update the execute fee period gas limits', async () => { await trackGas(ssvNetworkContract.updateExecuteOperatorFeePeriod(1200 - ), [GasGroup.OPERATOR_EXECUTE_FEE_LIMIT]); + ), [GasGroup.DAO_UPDATE_EXECUTE_OPERATOR_FEE_PERIOD]); + }); + + it('DAO update the maximum fee for operators using SSV gas limits', async () => { + await trackGas(ssvNetworkContract.updateMaximumOperatorFee(2e10 + ), [GasGroup.DAO_UPDATE_OPERATOR_MAX_FEE]); }); it('DAO get fee increase limit', async () => {