From f91e00e72d80f484a4106a947b50350fa207f1fb Mon Sep 17 00:00:00 2001 From: NindoK Date: Mon, 18 Nov 2024 18:04:22 +0100 Subject: [PATCH] Test verified commit --- src/libraries/MapWithTimeData.sol | 15 +- src/libraries/SimpleKeyRegistry32.sol | 15 +- src/middleware/Middleware.sol | 570 +++++++++++++--------- test/MapWithTimeData.t.sol | 15 +- test/mocks/MapWithTimeDataContract.sol | 15 +- test/mocks/symbiotic/DelegatorMock.sol | 19 +- test/mocks/symbiotic/OptInServiceMock.sol | 19 +- test/mocks/symbiotic/RegistryMock.sol | 15 +- test/mocks/symbiotic/VaultMock.sol | 21 +- test/unit/Middleware.t.sol | 79 +-- 10 files changed, 514 insertions(+), 269 deletions(-) diff --git a/src/libraries/MapWithTimeData.sol b/src/libraries/MapWithTimeData.sol index 934be93..9b8540c 100644 --- a/src/libraries/MapWithTimeData.sol +++ b/src/libraries/MapWithTimeData.sol @@ -1,4 +1,17 @@ -// SPDX-License-Identifier: MIT +//SPDX-License-Identifier: GPL-3.0-or-later + +// Copyright (C) Moondance Labs Ltd. +// This file is part of Tanssi. +// Tanssi is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// Tanssi is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// You should have received a copy of the GNU General Public License +// along with Tanssi. If not, see MIT pragma solidity ^0.8.25; import {Checkpoints} from "@openzeppelin/contracts/utils/structs/Checkpoints.sol"; diff --git a/src/libraries/SimpleKeyRegistry32.sol b/src/libraries/SimpleKeyRegistry32.sol index 5255d8f..0ca2e0a 100644 --- a/src/libraries/SimpleKeyRegistry32.sol +++ b/src/libraries/SimpleKeyRegistry32.sol @@ -1,4 +1,17 @@ -// SPDX-License-Identifier: MIT +//SPDX-License-Identifier: GPL-3.0-or-later + +// Copyright (C) Moondance Labs Ltd. +// This file is part of Tanssi. +// Tanssi is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// Tanssi is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// You should have received a copy of the GNU General Public License +// along with Tanssi. If not, see pragma solidity 0.8.25; import {Checkpoints} from "@openzeppelin/contracts/utils/structs/Checkpoints.sol"; diff --git a/src/middleware/Middleware.sol b/src/middleware/Middleware.sol index 05f1818..a3e4ed9 100644 --- a/src/middleware/Middleware.sol +++ b/src/middleware/Middleware.sol @@ -1,7 +1,19 @@ -// SPDX-License-Identifier: MIT +//SPDX-License-Identifier: GPL-3.0-or-later + +// Copyright (C) Moondance Labs Ltd. +// This file is part of Tanssi. +// Tanssi is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// Tanssi is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// You should have received a copy of the GNU General Public License +// along with Tanssi. If not, see pragma solidity 0.8.25; -import {console2} from "forge-std/console2.sol"; import {Time} from "@openzeppelin/contracts/utils/types/Time.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {EnumerableMap} from "@openzeppelin/contracts/utils/structs/EnumerableMap.sol"; @@ -25,55 +37,59 @@ contract Middleware is SimpleKeyRegistry32, Ownable { using MapWithTimeData for EnumerableMap.AddressToUintMap; using Subnetwork for address; - error NotOperator(); - error NotVault(); - - error OperatorNotOptedIn(); - error OperatorNotRegistred(); - error OperarorGracePeriodNotPassed(); - error OperatorAlreadyRegistred(); - - error VaultAlreadyRegistered(); - error VaultEpochTooShort(); - error VaultGracePeriodNotPassed(); - - error InvalidSubnetworksCnt(); - - error TooOldEpoch(); - error InvalidEpoch(); - - error SlashingWindowTooShort(); - error TooBigSlashAmount(); - error UnknownSlasherType(); + error Middleware__NotOperator(); + error Middleware__NotVault(); + error Middleware__OperatorNotOptedIn(); + error Middleware__OperatorNotRegistred(); + error Middleware__OperarorGracePeriodNotPassed(); + error Middleware__OperatorAlreadyRegistred(); + error Middleware__VaultAlreadyRegistered(); + error Middleware__VaultEpochTooShort(); + error Middleware__VaultGracePeriodNotPassed(); + error Middleware__InvalidSubnetworksCnt(); + error Middleware__TooOldEpoch(); + error Middleware__InvalidEpoch(); + error Middleware__SlashingWindowTooShort(); + error Middleware__TooBigSlashAmount(); + error Middleware__UnknownSlasherType(); struct ValidatorData { uint256 stake; bytes32 key; } - address public immutable NETWORK; - address public immutable OPERATOR_REGISTRY; - address public immutable VAULT_REGISTRY; - address public immutable OPERATOR_NET_OPTIN; - address public immutable OWNER; - uint48 public immutable EPOCH_DURATION; - uint48 public immutable SLASHING_WINDOW; - uint48 public immutable START_TIME; + struct SlashParams { + uint48 epochStartTs; + address vault; + address operator; + uint256 totalOperatorStake; + uint256 slashAmount; + } + + address public immutable i_network; + address public immutable i_operatorRegistry; + address public immutable i_vaultRegistry; + address public immutable i_operatorNetworkOptin; + address public immutable i_owner; + uint48 public immutable i_epochDuration; + uint48 public immutable i_slashingWindow; + + uint48 public immutable i_startTime; uint48 private constant INSTANT_SLASHER_TYPE = 0; uint48 private constant VETO_SLASHER_TYPE = 1; - uint256 public subnetworksCnt; - mapping(uint48 => uint256) public totalStakeCache; - mapping(uint48 => bool) public totalStakeCached; - mapping(uint48 => mapping(address => uint256)) public operatorStakeCache; - EnumerableMap.AddressToUintMap private operators; - EnumerableMap.AddressToUintMap private vaults; + uint256 public s_subnetworksCount; + mapping(uint48 => uint256) public s_totalStakeCache; + mapping(uint48 => bool) public s_totalStakeCached; + mapping(uint48 => mapping(address => uint256)) public s_operatorStakeCache; + EnumerableMap.AddressToUintMap private s_operators; + EnumerableMap.AddressToUintMap private s_vaults; modifier updateStakeCache( uint48 epoch ) { - if (!totalStakeCached[epoch]) { + if (!s_totalStakeCached[epoch]) { calcAndCacheStakes(epoch); } _; @@ -89,96 +105,112 @@ contract Middleware is SimpleKeyRegistry32, Ownable { uint48 _slashingWindow ) SimpleKeyRegistry32() Ownable(_owner) { if (_slashingWindow < _epochDuration) { - revert SlashingWindowTooShort(); + revert Middleware__SlashingWindowTooShort(); } - START_TIME = Time.timestamp(); - EPOCH_DURATION = _epochDuration; - NETWORK = _network; - OWNER = _owner; - OPERATOR_REGISTRY = _operatorRegistry; - VAULT_REGISTRY = _vaultRegistry; - OPERATOR_NET_OPTIN = _operatorNetOptin; - SLASHING_WINDOW = _slashingWindow; - - subnetworksCnt = 1; - } - - function getEpochStartTs( - uint48 epoch - ) public view returns (uint48 timestamp) { - return START_TIME + epoch * EPOCH_DURATION; - } - - function getEpochAtTs( - uint48 timestamp - ) public view returns (uint48 epoch) { - return (timestamp - START_TIME) / EPOCH_DURATION; - } - - function getCurrentEpoch() public view returns (uint48 epoch) { - return getEpochAtTs(Time.timestamp()); + i_startTime = Time.timestamp(); + i_epochDuration = _epochDuration; + i_network = _network; + i_owner = _owner; + i_operatorRegistry = _operatorRegistry; + i_vaultRegistry = _vaultRegistry; + i_operatorNetworkOptin = _operatorNetOptin; + i_slashingWindow = _slashingWindow; + + s_subnetworksCount = 1; } + /** + * @notice Registers a new operator with a key + * @dev Only the owner can call this function + * @param operator The operator's address + * @param key The operator's key + */ function registerOperator(address operator, bytes32 key) external onlyOwner { - if (operators.contains(operator)) { - revert OperatorAlreadyRegistred(); + if (s_operators.contains(operator)) { + revert Middleware__OperatorAlreadyRegistred(); } - if (!IRegistry(OPERATOR_REGISTRY).isEntity(operator)) { - revert NotOperator(); + if (!IRegistry(i_operatorRegistry).isEntity(operator)) { + revert Middleware__NotOperator(); } - if (!IOptInService(OPERATOR_NET_OPTIN).isOptedIn(operator, NETWORK)) { - revert OperatorNotOptedIn(); + if (!IOptInService(i_operatorNetworkOptin).isOptedIn(operator, i_network)) { + revert Middleware__OperatorNotOptedIn(); } updateKey(operator, key); - operators.add(operator); - operators.enable(operator); + s_operators.add(operator); + s_operators.enable(operator); } + /** + * @notice Updates an existing operator's key + * @dev Only the owner can call this function + * @param operator The operator's address + * @param key The new key + */ function updateOperatorKey(address operator, bytes32 key) external onlyOwner { - if (!operators.contains(operator)) { - revert OperatorNotRegistred(); + if (!s_operators.contains(operator)) { + revert Middleware__OperatorNotRegistred(); } updateKey(operator, key); } + /** + * @notice Pauses an operator + * @dev Only the owner can call this function + * @param operator The operator to pause + */ function pauseOperator( address operator ) external onlyOwner { - operators.disable(operator); + s_operators.disable(operator); } + /** + * @notice Re-enables a paused operator + * @dev Only the owner can call this function + * @param operator The operator to unpause + */ function unpauseOperator( address operator ) external onlyOwner { - operators.enable(operator); + s_operators.enable(operator); } + /** + * @notice Removes an operator after grace period + * @dev Only the owner can call this function + * @param operator The operator to unregister + */ function unregisterOperator( address operator ) external onlyOwner { - (, uint48 disabledTime) = operators.getTimes(operator); + (, uint48 disabledTime) = s_operators.getTimes(operator); - if (disabledTime == 0 || disabledTime + SLASHING_WINDOW > Time.timestamp()) { - revert OperarorGracePeriodNotPassed(); + if (disabledTime == 0 || disabledTime + i_slashingWindow > Time.timestamp()) { + revert Middleware__OperarorGracePeriodNotPassed(); } - operators.remove(operator); + s_operators.remove(operator); } + /** + * @notice Registers a new vault + * @dev Only the owner can call this function + * @param vault The vault address to register + */ function registerVault( address vault ) external onlyOwner { - if (vaults.contains(vault)) { - revert VaultAlreadyRegistered(); + if (s_vaults.contains(vault)) { + revert Middleware__VaultAlreadyRegistered(); } - if (!IRegistry(VAULT_REGISTRY).isEntity(vault)) { - revert NotVault(); + if (!IRegistry(i_vaultRegistry).isEntity(vault)) { + revert Middleware__NotVault(); } uint48 vaultEpoch = IVault(vault).epochDuration(); @@ -188,134 +220,120 @@ contract Middleware is SimpleKeyRegistry32, Ownable { vaultEpoch -= IVetoSlasher(slasher).vetoDuration(); } - if (vaultEpoch < SLASHING_WINDOW) { - revert VaultEpochTooShort(); + if (vaultEpoch < i_slashingWindow) { + revert Middleware__VaultEpochTooShort(); } - vaults.add(vault); - vaults.enable(vault); - } - - function isVaultRegistered( - address vault - ) external view returns (bool) { - return vaults.contains(vault); + s_vaults.add(vault); + s_vaults.enable(vault); } + /** + * @notice Pauses a vault + * @dev Only the owner can call this function + * @param vault The vault to pause + */ function pauseVault( address vault ) external onlyOwner { - vaults.disable(vault); + s_vaults.disable(vault); } + /** + * @notice Re-enables a paused vault + * @dev Only the owner can call this function + * @param vault The vault to unpause + */ function unpauseVault( address vault ) external onlyOwner { - vaults.enable(vault); + s_vaults.enable(vault); } + /** + * @notice Removes a vault after grace period + * @dev Only the owner can call this function + * @param vault The vault to unregister + */ function unregisterVault( address vault ) external onlyOwner { - (, uint48 disabledTime) = vaults.getTimes(vault); + (, uint48 disabledTime) = s_vaults.getTimes(vault); - if (disabledTime == 0 || disabledTime + SLASHING_WINDOW > Time.timestamp()) { - revert VaultGracePeriodNotPassed(); + if (disabledTime == 0 || disabledTime + i_slashingWindow > Time.timestamp()) { + revert Middleware__VaultGracePeriodNotPassed(); } - vaults.remove(vault); + s_vaults.remove(vault); } - function setSubnetworksCnt( - uint256 _subnetworksCnt + /** + * @notice Updates the number of subnetworks + * @dev Only the owner can call this function + * @param _subnetworksCount New subnetwork count + */ + function setSubnetworksCount( + uint256 _subnetworksCount ) external onlyOwner { - if (subnetworksCnt >= _subnetworksCnt) { - revert InvalidSubnetworksCnt(); + if (s_subnetworksCount >= _subnetworksCount) { + revert Middleware__InvalidSubnetworksCnt(); } - subnetworksCnt = _subnetworksCnt; + s_subnetworksCount = _subnetworksCount; } - function getOperatorStake(address operator, uint48 epoch) public view returns (uint256 stake) { - if (totalStakeCached[epoch]) { - return operatorStakeCache[epoch][operator]; - } - + // function submission(bytes memory payload, bytes32[] memory signatures) public updateStakeCache(getCurrentEpoch()) { + // // validate signatures + // // validate payload + // // process payload + // } + + /** + * @notice Calculates and caches stakes for an epoch + * @param epoch The epoch to calculate for + * @return totalStake The total stake amount + */ + function calcAndCacheStakes( + uint48 epoch + ) public returns (uint256 totalStake) { uint48 epochStartTs = getEpochStartTs(epoch); - for (uint256 i; i < vaults.length(); ++i) { - (address vault, uint48 enabledTime, uint48 disabledTime) = vaults.atWithTimes(i); - - // just skip the vault if it was enabled after the target epoch or not enabled - if (!_wasActiveAt(enabledTime, disabledTime, epochStartTs)) { - continue; - } - for (uint96 j = 0; j < subnetworksCnt; ++j) { - stake += IBaseDelegator(IVault(vault).delegator()).stakeAt( - NETWORK.subnetwork(j), operator, epochStartTs, new bytes(0) - ); - } + // for epoch older than SLASHING_WINDOW total stake can be invalidated (use cache) + if (epochStartTs < Time.timestamp() - i_slashingWindow) { + revert Middleware__TooOldEpoch(); } - return stake; - } - - function getTotalStake( - uint48 epoch - ) public view returns (uint256) { - if (totalStakeCached[epoch]) { - return totalStakeCache[epoch]; + if (epochStartTs > Time.timestamp()) { + revert Middleware__InvalidEpoch(); } - return _calcTotalStake(epoch); - } - - function getValidatorSet( - uint48 epoch - ) public view returns (ValidatorData[] memory validatorsData) { - uint48 epochStartTs = getEpochStartTs(epoch); - - validatorsData = new ValidatorData[](operators.length()); - uint256 valIdx = 0; - for (uint256 i; i < operators.length(); ++i) { - (address operator, uint48 enabledTime, uint48 disabledTime) = operators.atWithTimes(i); + for (uint256 i; i < s_operators.length(); ++i) { + (address operator, uint48 enabledTime, uint48 disabledTime) = s_operators.atWithTimes(i); // just skip operator if it was added after the target epoch or paused if (!_wasActiveAt(enabledTime, disabledTime, epochStartTs)) { continue; } - bytes32 key = getOperatorKeyAt(operator, epochStartTs); - if (key == bytes32(0)) { - continue; - } - - uint256 stake = getOperatorStake(operator, epoch); - - validatorsData[valIdx++] = ValidatorData(stake, key); - } + uint256 operatorStake = getOperatorStake(operator, epoch); + s_operatorStakeCache[epoch][operator] = operatorStake; - // shrink array to skip unused slots - /// @solidity memory-safe-assembly - assembly { - mstore(validatorsData, valIdx) + totalStake += operatorStake; } - } - - function submission(bytes memory payload, bytes32[] memory signatures) public updateStakeCache(getCurrentEpoch()) { - // validate signatures - // validate payload - // process payload - } - struct SlashParams { - uint48 epochStartTs; - address vault; - address operator; - uint256 totalOperatorStake; - uint256 slashAmount; + s_totalStakeCached[epoch] = true; + s_totalStakeCache[epoch] = totalStake; } + /** + * @notice Slashes an operator's stake + * @dev Only the owner can call this function + * @dev This function first updates the stake cache for the target epoch + * @param epoch The epoch number + * @param operator The operator to slash + * @param amount Amount to slash + */ + //INFO: this function can be made external. To check if it is possible to make it external function slash(uint48 epoch, address operator, uint256 amount) public onlyOwner updateStakeCache(epoch) { SlashParams memory params; params.epochStartTs = getEpochStartTs(epoch); @@ -325,20 +343,14 @@ contract Middleware is SimpleKeyRegistry32, Ownable { params.totalOperatorStake = getOperatorStake(operator, epoch); if (params.totalOperatorStake < amount) { - revert TooBigSlashAmount(); + revert Middleware__TooBigSlashAmount(); } // simple pro-rata slasher - for (uint256 i; i < vaults.length(); ++i) { - (address vault, uint48 enabledTime, uint48 disabledTime) = vaults.atWithTimes(i); - console2.log("Vault: ", vault); - console2.log("Enabled: ", enabledTime); - console2.log("Disabled: ", disabledTime); - console2.log("Epoch: ", params.epochStartTs); - console2.log("Active: ", _wasActiveAt(enabledTime, disabledTime, params.epochStartTs)); + for (uint256 i; i < s_vaults.length(); ++i) { + (address vault, uint48 enabledTime, uint48 disabledTime) = s_vaults.atWithTimes(i); // just skip the vault if it was enabled after the target epoch or not enabled if (!_wasActiveAt(enabledTime, disabledTime, params.epochStartTs)) { - console2.log("skip vault", vault); continue; } @@ -346,9 +358,14 @@ contract Middleware is SimpleKeyRegistry32, Ownable { } } + /** + * @dev Get vault stake and calculate slashing amount. + * @param vault The vault address to calculate its stake + * @param params Struct containing slashing parameters + */ function _processVaultSlashing(address vault, SlashParams memory params) private { - for (uint96 j = 0; j < subnetworksCnt; ++j) { - bytes32 subnetwork = NETWORK.subnetwork(j); + for (uint96 j = 0; j < s_subnetworksCount; ++j) { + bytes32 subnetwork = i_network.subnetwork(j); uint256 vaultStake = IBaseDelegator(IVault(vault).delegator()).stakeAt( subnetwork, params.operator, params.epochStartTs, new bytes(0) ); @@ -358,22 +375,53 @@ contract Middleware is SimpleKeyRegistry32, Ownable { } } - function calcAndCacheStakes( + /** + * @dev Slashes a vault's stake for a specific operator + * @param timestamp Time at which the epoch started + * @param vault Address of the vault to slash + * @param subnetwork Subnetwork identifier + * @param operator Address of the operator being slashed + * @param amount Amount to slash + */ + function _slashVault( + uint48 timestamp, + address vault, + bytes32 subnetwork, + address operator, + uint256 amount + ) private { + address slasher = IVault(vault).slasher(); + uint256 slasherType = IEntity(slasher).TYPE(); + if (slasherType == INSTANT_SLASHER_TYPE) { + ISlasher(slasher).slash(subnetwork, operator, amount, timestamp, new bytes(0)); + } else if (slasherType == VETO_SLASHER_TYPE) { + IVetoSlasher(slasher).requestSlash(subnetwork, operator, amount, timestamp, new bytes(0)); + } else { + revert Middleware__UnknownSlasherType(); + } + } + + /** + * @dev Calculates total stake for an epoch + * @param epoch The epoch to calculate stake for + * @return totalStake The total stake amount + */ + function _calcTotalStake( uint48 epoch - ) public returns (uint256 totalStake) { + ) private view returns (uint256 totalStake) { uint48 epochStartTs = getEpochStartTs(epoch); - // for epoch older than SLASHING_WINDOW total stake can be invalidated (use cache) - if (epochStartTs < Time.timestamp() - SLASHING_WINDOW) { - revert TooOldEpoch(); + // for epoch older than i_slashingWindow total stake can be invalidated (use cache) + if (epochStartTs < Time.timestamp() - i_slashingWindow) { + revert Middleware__TooOldEpoch(); } if (epochStartTs > Time.timestamp()) { - revert InvalidEpoch(); + revert Middleware__InvalidEpoch(); } - for (uint256 i; i < operators.length(); ++i) { - (address operator, uint48 enabledTime, uint48 disabledTime) = operators.atWithTimes(i); + for (uint256 i; i < s_operators.length(); ++i) { + (address operator, uint48 enabledTime, uint48 disabledTime) = s_operators.atWithTimes(i); // just skip operator if it was added after the target epoch or paused if (!_wasActiveAt(enabledTime, disabledTime, epochStartTs)) { @@ -381,61 +429,141 @@ contract Middleware is SimpleKeyRegistry32, Ownable { } uint256 operatorStake = getOperatorStake(operator, epoch); - operatorStakeCache[epoch][operator] = operatorStake; - totalStake += operatorStake; } + } - totalStakeCached[epoch] = true; - totalStakeCache[epoch] = totalStake; + /** + * @dev Checks if an entity was active at a specific timestamp + * @param enabledTime Time when entity was enabled + * @param disabledTime Time when entity was disabled (0 if never disabled) + * @param timestamp Timestamp to check activity for + * @return bool True if entity was active at timestamp + */ + function _wasActiveAt(uint48 enabledTime, uint48 disabledTime, uint48 timestamp) private pure returns (bool) { + return enabledTime != 0 && enabledTime <= timestamp && (disabledTime == 0 || disabledTime >= timestamp); } - function _calcTotalStake( - uint48 epoch - ) private view returns (uint256 totalStake) { + /** + * @notice Checks if a vault is registered + * @param vault The vault address to check + * @return bool True if vault is registered + */ + function isVaultRegistered( + address vault + ) external view returns (bool) { + return s_vaults.contains(vault); + } + + /** + * @notice Gets operator's stake for an epoch + * @param operator The operator address + * @param epoch The epoch number + * @return stake The operator's total stake + */ + function getOperatorStake(address operator, uint48 epoch) public view returns (uint256 stake) { + if (s_totalStakeCached[epoch]) { + return s_operatorStakeCache[epoch][operator]; + } + uint48 epochStartTs = getEpochStartTs(epoch); + for (uint256 i; i < s_vaults.length(); ++i) { + (address vault, uint48 enabledTime, uint48 disabledTime) = s_vaults.atWithTimes(i); - // for epoch older than SLASHING_WINDOW total stake can be invalidated (use cache) - if (epochStartTs < Time.timestamp() - SLASHING_WINDOW) { - revert TooOldEpoch(); + // just skip the vault if it was enabled after the target epoch or not enabled + if (!_wasActiveAt(enabledTime, disabledTime, epochStartTs)) { + continue; + } + + for (uint96 j = 0; j < s_subnetworksCount; ++j) { + stake += IBaseDelegator(IVault(vault).delegator()).stakeAt( + i_network.subnetwork(j), operator, epochStartTs, new bytes(0) + ); + } } - if (epochStartTs > Time.timestamp()) { - revert InvalidEpoch(); + return stake; + } + + /** + * @notice Gets total stake for an epoch + * @param epoch The epoch number + * @return Total stake amount + */ + function getTotalStake( + uint48 epoch + ) public view returns (uint256) { + if (s_totalStakeCached[epoch]) { + return s_totalStakeCache[epoch]; } + return _calcTotalStake(epoch); + } + + /** + * @notice Gets validator set for an epoch + * @param epoch The epoch number + * @return validatorsData Array of validator data + */ + function getValidatorSet( + uint48 epoch + ) public view returns (ValidatorData[] memory validatorsData) { + uint48 epochStartTs = getEpochStartTs(epoch); - for (uint256 i; i < operators.length(); ++i) { - (address operator, uint48 enabledTime, uint48 disabledTime) = operators.atWithTimes(i); + validatorsData = new ValidatorData[](s_operators.length()); + uint256 valIdx = 0; + + for (uint256 i; i < s_operators.length(); ++i) { + (address operator, uint48 enabledTime, uint48 disabledTime) = s_operators.atWithTimes(i); // just skip operator if it was added after the target epoch or paused if (!_wasActiveAt(enabledTime, disabledTime, epochStartTs)) { continue; } - uint256 operatorStake = getOperatorStake(operator, epoch); - totalStake += operatorStake; + bytes32 key = getOperatorKeyAt(operator, epochStartTs); + if (key == bytes32(0)) { + continue; + } + + uint256 stake = getOperatorStake(operator, epoch); + + validatorsData[valIdx++] = ValidatorData(stake, key); + } + + // shrink array to skip unused slots + /// @solidity memory-safe-assembly + assembly { + mstore(validatorsData, valIdx) } } - function _wasActiveAt(uint48 enabledTime, uint48 disabledTime, uint48 timestamp) private pure returns (bool) { - return enabledTime != 0 && enabledTime <= timestamp && (disabledTime == 0 || disabledTime >= timestamp); + /** + * @notice Gets the timestamp when an epoch starts + * @param epoch The epoch number + * @return timestamp The start time of the epoch + */ + function getEpochStartTs( + uint48 epoch + ) public view returns (uint48 timestamp) { + return i_startTime + epoch * i_epochDuration; } - function _slashVault( - uint48 timestamp, - address vault, - bytes32 subnetwork, - address operator, - uint256 amount - ) private { - address slasher = IVault(vault).slasher(); - uint256 slasherType = IEntity(slasher).TYPE(); - if (slasherType == INSTANT_SLASHER_TYPE) { - ISlasher(slasher).slash(subnetwork, operator, amount, timestamp, new bytes(0)); - } else if (slasherType == VETO_SLASHER_TYPE) { - IVetoSlasher(slasher).requestSlash(subnetwork, operator, amount, timestamp, new bytes(0)); - } else { - revert UnknownSlasherType(); - } + /** + * @notice Determines which epoch a timestamp belongs to + * @param timestamp The timestamp to check + * @return epoch The corresponding epoch number + */ + function getEpochAtTs( + uint48 timestamp + ) public view returns (uint48 epoch) { + return (timestamp - i_startTime) / i_epochDuration; + } + + /** + * @notice Gets the current epoch number + * @return epoch The current epoch + */ + function getCurrentEpoch() public view returns (uint48 epoch) { + return getEpochAtTs(Time.timestamp()); } } diff --git a/test/MapWithTimeData.t.sol b/test/MapWithTimeData.t.sol index ed66149..b848153 100644 --- a/test/MapWithTimeData.t.sol +++ b/test/MapWithTimeData.t.sol @@ -1,4 +1,17 @@ -// SPDX-License-Identifier: MIT +//SPDX-License-Identifier: GPL-3.0-or-later + +// Copyright (C) Moondance Labs Ltd. +// This file is part of Tanssi. +// Tanssi is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// Tanssi is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// You should have received a copy of the GNU General Public License +// along with Tanssi. If not, see pragma solidity 0.8.25; import {Test, console2} from "forge-std/Test.sol"; diff --git a/test/mocks/MapWithTimeDataContract.sol b/test/mocks/MapWithTimeDataContract.sol index eae5fd9..1323925 100644 --- a/test/mocks/MapWithTimeDataContract.sol +++ b/test/mocks/MapWithTimeDataContract.sol @@ -1,4 +1,17 @@ -// SPDX-License-Identifier: MIT +//SPDX-License-Identifier: GPL-3.0-or-later + +// Copyright (C) Moondance Labs Ltd. +// This file is part of Tanssi. +// Tanssi is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// Tanssi is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// You should have received a copy of the GNU General Public License +// along with Tanssi. If not, see pragma solidity 0.8.25; import {EnumerableMap} from "@openzeppelin/contracts/utils/structs/EnumerableMap.sol"; diff --git a/test/mocks/symbiotic/DelegatorMock.sol b/test/mocks/symbiotic/DelegatorMock.sol index ecf6d4a..cab0654 100644 --- a/test/mocks/symbiotic/DelegatorMock.sol +++ b/test/mocks/symbiotic/DelegatorMock.sol @@ -1,4 +1,17 @@ -// SPDX-License-Identifier: MIT +//SPDX-License-Identifier: GPL-3.0-or-later + +// Copyright (C) Moondance Labs Ltd. +// This file is part of Tanssi. +// Tanssi is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// Tanssi is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// You should have received a copy of the GNU General Public License +// along with Tanssi. If not, see pragma solidity 0.8.25; import {console2} from "forge-std/console2.sol"; @@ -26,9 +39,9 @@ contract DelegatorMock is BaseDelegator { {} function _stakeAt( - bytes32 subnetwork, + bytes32, /*subnetwork*/ address operator, - uint48 timestamp, + uint48, /*timestamp*/ bytes memory hints ) internal view override returns (uint256, bytes memory) { uint256 operatorStake = IVault(vault).activeBalanceOf(operator); diff --git a/test/mocks/symbiotic/OptInServiceMock.sol b/test/mocks/symbiotic/OptInServiceMock.sol index 7c629e4..70baed2 100644 --- a/test/mocks/symbiotic/OptInServiceMock.sol +++ b/test/mocks/symbiotic/OptInServiceMock.sol @@ -1,4 +1,17 @@ -// SPDX-License-Identifier: MIT +//SPDX-License-Identifier: GPL-3.0-or-later + +// Copyright (C) Moondance Labs Ltd. +// This file is part of Tanssi. +// Tanssi is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// Tanssi is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// You should have received a copy of the GNU General Public License +// along with Tanssi. If not, see pragma solidity 0.8.25; import {console2} from "forge-std/console2.sol"; @@ -46,7 +59,7 @@ contract OptInServiceMock is EIP712, IOptInService { _optIn(msg.sender, where); } - function optIn(address who, address where, uint48 deadline, bytes calldata signature) external { + function optIn(address who, address where, uint48, /*deadline*/ bytes calldata /*signature*/ ) external { _optIn(who, where); } @@ -56,7 +69,7 @@ contract OptInServiceMock is EIP712, IOptInService { _optOut(msg.sender, where); } - function optOut(address who, address where, uint48 deadline, bytes calldata signature) external { + function optOut(address who, address where, uint48, /*deadline*/ bytes calldata /* signature*/ ) external { _optOut(who, where); } diff --git a/test/mocks/symbiotic/RegistryMock.sol b/test/mocks/symbiotic/RegistryMock.sol index c36725d..13eed4d 100644 --- a/test/mocks/symbiotic/RegistryMock.sol +++ b/test/mocks/symbiotic/RegistryMock.sol @@ -1,4 +1,17 @@ -// SPDX-License-Identifier: MIT +//SPDX-License-Identifier: GPL-3.0-or-later + +// Copyright (C) Moondance Labs Ltd. +// This file is part of Tanssi. +// Tanssi is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// Tanssi is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// You should have received a copy of the GNU General Public License +// along with Tanssi. If not, see pragma solidity 0.8.25; import {Registry} from "@symbiotic/contracts/common/Registry.sol"; diff --git a/test/mocks/symbiotic/VaultMock.sol b/test/mocks/symbiotic/VaultMock.sol index db5c3d9..93c8874 100644 --- a/test/mocks/symbiotic/VaultMock.sol +++ b/test/mocks/symbiotic/VaultMock.sol @@ -1,4 +1,17 @@ -// SPDX-License-Identifier: MIT +//SPDX-License-Identifier: GPL-3.0-or-later + +// Copyright (C) Moondance Labs Ltd. +// This file is part of Tanssi. +// Tanssi is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// Tanssi is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// You should have received a copy of the GNU General Public License +// along with Tanssi. If not, see pragma solidity 0.8.25; import {console2} from "forge-std/console2.sol"; @@ -29,8 +42,8 @@ contract VaultMock is VaultStorage, MigratableEntity, IVault { function activeBalanceOfAt( address account, - uint48 timestamp, - bytes calldata hints + uint48, /*timestamp*/ + bytes calldata /*hints*/ ) external view returns (uint256) { return operatorStake[account]; } @@ -66,7 +79,7 @@ contract VaultMock is VaultStorage, MigratableEntity, IVault { function claimBatch(address recipient, uint256[] calldata epochs) external returns (uint256 amount) {} - function onSlash(uint256 amount, uint48 captureTimestamp) external returns (uint256 slashedAmount) { + function onSlash(uint256 amount, uint48 /*captureTimestamp*/ ) external returns (uint256 slashedAmount) { totalAtStake -= amount; slashedAmount = amount; for (uint256 i = 0; i < operators.length; i++) { diff --git a/test/unit/Middleware.t.sol b/test/unit/Middleware.t.sol index 1349c46..f94fc65 100644 --- a/test/unit/Middleware.t.sol +++ b/test/unit/Middleware.t.sol @@ -1,4 +1,17 @@ -// SPDX-License-Identifier: UNLICENSED +//SPDX-License-Identifier: GPL-3.0-or-later + +// Copyright (C) Moondance Labs Ltd. +// This file is part of Tanssi. +// Tanssi is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// Tanssi is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// You should have received a copy of the GNU General Public License +// along with Tanssi. If not, see pragma solidity ^0.8.13; import {Test, console2} from "forge-std/Test.sol"; @@ -120,7 +133,7 @@ contract MiddlewareTest is Test { uint48 SHORT_SLASHING_WINDOW_ = 99; vm.startPrank(owner); - vm.expectRevert(Middleware.SlashingWindowTooShort.selector); + vm.expectRevert(Middleware.Middleware__SlashingWindowTooShort.selector); new Middleware( address(0), @@ -197,12 +210,12 @@ contract MiddlewareTest is Test { } function testInitialState() public view { - assertEq(middleware.NETWORK(), address(network)); - assertEq(middleware.OPERATOR_REGISTRY(), address(registry)); - assertEq(middleware.VAULT_REGISTRY(), address(registry)); - assertEq(middleware.EPOCH_DURATION(), NETWORK_EPOCH_DURATION); - assertEq(middleware.SLASHING_WINDOW(), SLASHING_WINDOW); - assertEq(middleware.subnetworksCnt(), 1); + assertEq(middleware.i_network(), address(network)); + assertEq(middleware.i_operatorRegistry(), address(registry)); + assertEq(middleware.i_vaultRegistry(), address(registry)); + assertEq(middleware.i_epochDuration(), NETWORK_EPOCH_DURATION); + assertEq(middleware.i_slashingWindow(), SLASHING_WINDOW); + assertEq(middleware.s_subnetworksCount(), 1); } // ************************************************************************************************ @@ -234,7 +247,7 @@ contract MiddlewareTest is Test { vm.startPrank(owner); middleware.registerOperator(operator, OPERATOR_KEY); - vm.expectRevert(Middleware.OperatorAlreadyRegistred.selector); + vm.expectRevert(Middleware.Middleware__OperatorAlreadyRegistred.selector); middleware.registerOperator(operator, OPERATOR_KEY); vm.stopPrank(); } @@ -243,7 +256,7 @@ contract MiddlewareTest is Test { _registerOperatorToNetwork(operator, address(vault), true, false); vm.startPrank(owner); - vm.expectRevert(Middleware.NotOperator.selector); + vm.expectRevert(Middleware.Middleware__NotOperator.selector); middleware.registerOperator(owner, OPERATOR_KEY); vm.stopPrank(); } @@ -252,7 +265,7 @@ contract MiddlewareTest is Test { _registerOperatorToNetwork(operator, address(vault), false, true); vm.startPrank(owner); - vm.expectRevert(Middleware.OperatorNotOptedIn.selector); + vm.expectRevert(Middleware.Middleware__OperatorNotOptedIn.selector); middleware.registerOperator(operator, OPERATOR_KEY); vm.stopPrank(); } @@ -300,7 +313,7 @@ contract MiddlewareTest is Test { function testUpdateOperatorKeyNotRegistered() public { vm.startPrank(owner); - vm.expectRevert(Middleware.OperatorNotRegistred.selector); + vm.expectRevert(Middleware.Middleware__OperatorNotRegistred.selector); middleware.updateOperatorKey(operator, OPERATOR_KEY); vm.stopPrank(); } @@ -376,7 +389,7 @@ contract MiddlewareTest is Test { middleware.pauseOperator(operator); vm.warp(START_TIME + SLASHING_WINDOW - 1); - vm.expectRevert(Middleware.OperarorGracePeriodNotPassed.selector); + vm.expectRevert(Middleware.Middleware__OperarorGracePeriodNotPassed.selector); middleware.unregisterOperator(operator); vm.stopPrank(); } @@ -411,14 +424,14 @@ contract MiddlewareTest is Test { middleware.registerVault(address(vault)); - vm.expectRevert(Middleware.VaultAlreadyRegistered.selector); + vm.expectRevert(Middleware.Middleware__VaultAlreadyRegistered.selector); middleware.registerVault(address(vault)); vm.stopPrank(); } function testRegisterVaultNotVault() public { vm.startPrank(owner); - vm.expectRevert(Middleware.NotVault.selector); + vm.expectRevert(Middleware.Middleware__NotVault.selector); middleware.registerVault(owner); vm.stopPrank(); } @@ -429,7 +442,7 @@ contract MiddlewareTest is Test { vm.startPrank(owner); vault.setSlasher(address(vetoSlasher)); vm.store(address(vetoSlasher), bytes32(uint256(0)), bytes32(uint256(uint160(address(vault))))); - vm.expectRevert(Middleware.VaultEpochTooShort.selector); + vm.expectRevert(Middleware.Middleware__VaultEpochTooShort.selector); middleware.registerVault(address(vault)); vm.stopPrank(); } @@ -524,7 +537,7 @@ contract MiddlewareTest is Test { middleware.pauseVault(address(vault)); vm.warp(START_TIME + SLASHING_WINDOW - 1); - vm.expectRevert(Middleware.VaultGracePeriodNotPassed.selector); + vm.expectRevert(Middleware.Middleware__VaultGracePeriodNotPassed.selector); middleware.unregisterVault(address(vault)); vm.stopPrank(); } @@ -535,31 +548,31 @@ contract MiddlewareTest is Test { function testSetSubnetworksCnt() public { vm.startPrank(owner); - middleware.setSubnetworksCnt(2); - assertEq(middleware.subnetworksCnt(), 2); + middleware.setSubnetworksCount(2); + assertEq(middleware.s_subnetworksCount(), 2); vm.stopPrank(); } function testSetSubnetworksCntInvalidIfGreaterThanZero() public { vm.startPrank(owner); - middleware.setSubnetworksCnt(10); - assertEq(middleware.subnetworksCnt(), 10); + middleware.setSubnetworksCount(10); + assertEq(middleware.s_subnetworksCount(), 10); - vm.expectRevert(Middleware.InvalidSubnetworksCnt.selector); - middleware.setSubnetworksCnt(8); + vm.expectRevert(Middleware.Middleware__InvalidSubnetworksCnt.selector); + middleware.setSubnetworksCount(8); vm.stopPrank(); } function testSetSubnetworksCntInvalid() public { vm.startPrank(owner); - vm.expectRevert(Middleware.InvalidSubnetworksCnt.selector); - middleware.setSubnetworksCnt(0); + vm.expectRevert(Middleware.Middleware__InvalidSubnetworksCnt.selector); + middleware.setSubnetworksCount(0); vm.stopPrank(); } function testSetSubnetworksCntUnauthorized() public { vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, address(this))); - middleware.setSubnetworksCnt(2); + middleware.setSubnetworksCount(2); } // ************************************************************************************************ @@ -733,7 +746,7 @@ contract MiddlewareTest is Test { vm.warp(START_TIME + SLASHING_WINDOW + 1); uint48 currentEpoch = middleware.getCurrentEpoch(); vm.warp(SLASHING_WINDOW * 2 + 1); - vm.expectRevert(Middleware.TooOldEpoch.selector); + vm.expectRevert(Middleware.Middleware__TooOldEpoch.selector); middleware.getTotalStake(currentEpoch); vm.stopPrank(); } @@ -750,7 +763,7 @@ contract MiddlewareTest is Test { vm.warp(START_TIME + SLASHING_WINDOW + 1); uint48 currentEpoch = middleware.getCurrentEpoch(); vm.warp(START_TIME + SLASHING_WINDOW - 1); - vm.expectRevert(Middleware.InvalidEpoch.selector); + vm.expectRevert(Middleware.Middleware__InvalidEpoch.selector); middleware.getTotalStake(currentEpoch + 1); vm.stopPrank(); } @@ -874,7 +887,7 @@ contract MiddlewareTest is Test { vm.startPrank(owner); uint48 currentEpoch = middleware.getCurrentEpoch(); vm.warp(SLASHING_WINDOW * 2 + 1); - vm.expectRevert(Middleware.TooOldEpoch.selector); + vm.expectRevert(Middleware.Middleware__TooOldEpoch.selector); middleware.slash(currentEpoch, operator, OPERATOR_STAKE); vm.stopPrank(); } @@ -893,7 +906,7 @@ contract MiddlewareTest is Test { uint256 totalStakeCached = middleware.calcAndCacheStakes(currentEpoch); uint256 slashAmount = OPERATOR_STAKE * 2; - vm.expectRevert(Middleware.TooBigSlashAmount.selector); + vm.expectRevert(Middleware.Middleware__TooBigSlashAmount.selector); middleware.slash(currentEpoch, operator, slashAmount); uint256 totalStake = middleware.getTotalStake(currentEpoch); @@ -999,7 +1012,7 @@ contract MiddlewareTest is Test { uint48 currentEpoch = middleware.getCurrentEpoch(); uint256 slashAmount = OPERATOR_STAKE / 2; - vm.expectRevert(Middleware.UnknownSlasherType.selector); + vm.expectRevert(Middleware.Middleware__UnknownSlasherType.selector); middleware.slash(currentEpoch, operator, slashAmount); vm.stopPrank(); @@ -1044,7 +1057,7 @@ contract MiddlewareTest is Test { vm.warp(START_TIME + SLASHING_WINDOW + 1); uint48 currentEpoch = middleware.getCurrentEpoch(); vm.warp(SLASHING_WINDOW * 2 + 1); - vm.expectRevert(Middleware.TooOldEpoch.selector); + vm.expectRevert(Middleware.Middleware__TooOldEpoch.selector); middleware.calcAndCacheStakes(currentEpoch); vm.stopPrank(); } @@ -1062,7 +1075,7 @@ contract MiddlewareTest is Test { vm.warp(START_TIME + SLASHING_WINDOW + 1); uint48 currentEpoch = middleware.getCurrentEpoch(); vm.warp(START_TIME + SLASHING_WINDOW - 1); - vm.expectRevert(Middleware.InvalidEpoch.selector); + vm.expectRevert(Middleware.Middleware__InvalidEpoch.selector); middleware.calcAndCacheStakes(currentEpoch + 1); vm.stopPrank(); }