diff --git a/.gitignore b/.gitignore index 024b78b..433cb74 100644 --- a/.gitignore +++ b/.gitignore @@ -12,5 +12,6 @@ docs/ # Dotenv file .env +.env.local .history/ \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index 8ca8be8..7ea3667 100644 --- a/.gitmodules +++ b/.gitmodules @@ -10,3 +10,6 @@ [submodule "lib/core"] path = lib/core url = https://github.com/symbioticfi/core +[submodule "lib/foundry-devops"] + path = lib/foundry-devops + url = https://github.com/Cyfrin/foundry-devops diff --git a/Makefile b/Makefile index f449974..455695f 100644 --- a/Makefile +++ b/Makefile @@ -3,8 +3,7 @@ .PHONY: all test clean deploy fund help install snapshot format anvil DEFAULT_ANVIL_KEY := 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 -DEFAULT_OWNER := 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 -DEFAULT_VAULT_CONFIGURATOR := 0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f # This works since every run is seeded + all: clean remove install update build @@ -12,16 +11,25 @@ all: clean remove install update build clean :; forge clean # Remove modules -remove :; rm -rf .gitmodules && rm -rf .git/modules/* && rm -rf lib && touch .gitmodules && git add . && git commit -m "modules" +remove :; rm -rf .gitmodules && rm -rf .git/modules/* && rm -rf lib && touch .gitmodules -install :; forge install foundry-rs/forge-std@v1.8.2 --no-commit && forge install openzeppelin/openzeppelin-contracts@v5.0.2 --no-commit && forge install openzeppelin/openzeppelin-contracts-upgradeable@v5.0.2 --no-commit && forge install symbioticfi/core --no-commit +install :; forge install foundry-rs/forge-std@v1.8.2 --no-commit && \ + forge install openzeppelin/openzeppelin-contracts@v5.0.2 --no-commit && \ + forge install openzeppelin/openzeppelin-contracts-upgradeable@v5.0.2 --no-commit && \ + forge install symbioticfi/core --no-commit && \ + forge install symbioticfi/rewards --no-commit && \ + forge install Cyfrin/foundry-devops --no-commit # Update Dependencies update:; forge update build:; forge build -test :; forge test +test :; forge test + +testv :; forge test -vvvv + +coverage :; forge coverage snapshot :; forge snapshot @@ -31,41 +39,13 @@ anvil :; anvil -m 'test test test test test test test test test test test junk' NETWORK_ARGS := --rpc-url http://localhost:8545 --private-key $(DEFAULT_ANVIL_KEY) --broadcast -deploy-symbiotic: +deploy: @echo "🚀 Deploying contracts..." - @echo "📡 Deploying Core..." - @forge script lib/core/script/deploy/Core.s.sol:CoreScript $(DEFAULT_OWNER) --sig "run(address)" $(NETWORK_ARGS) - @echo "✅ Core deployment completed" - - @echo "📡 Deploying NetworkRegistry..." - @forge script lib/core/script/deploy/NetworkRegistry.s.sol:NetworkRegistryScript $(NETWORK_ARGS) - @echo "✅ NetworkRegistry deployment completed" - - @echo "📡 Deploying MetadataService..." - @forge script lib/core/script/deploy/MetadataService.s.sol:MetadataServiceScript ${DEFAULT_OWNER} --sig "run(address)" $(NETWORK_ARGS) - @echo "✅ MetadataService deployment completed" - - @echo "📡 Deploying NetworkMiddlewareService..." - @forge script lib/core/script/deploy/NetworkMiddlewareService.s.sol:NetworkMiddlewareServiceScript ${DEFAULT_OWNER} --sig "run(address)" $(NETWORK_ARGS) - @echo "✅ NetworkMiddlewareService deployment completed" - - @echo "📡 Deploying OptInService..." - @forge script lib/core/script/deploy/OptInService.s.sol:OptInServiceScript ${DEFAULT_OWNER} ${DEFAULT_OWNER} "test" --sig "run(address,address,string)" $(NETWORK_ARGS) - @echo "✅ OptInService deployment completed" - - @echo "📡 Deploying OperatorRegistry..." - @forge script lib/core/script/deploy/OperatorRegistry.s.sol:OperatorRegistryScript $(NETWORK_ARGS) - @echo "✅ OperatorRegistry deployment completed" - - @echo "📡 Deploying VaultFactory..." - @forge script lib/core/script/deploy/VaultFactory.s.sol:VaultFactoryScript ${DEFAULT_OWNER} --sig "run(address)" ${NETWORK_ARGS} - @echo "✅ VaultFactory deployment completed" - - @echo "📡 Deploying Vault..." - @forge script lib/core/script/deploy/Vault.s.sol:VaultScript -vvvv ${DEFAULT_VAULT_CONFIGURATOR} ${DEFAULT_OWNER} ${DEFAULT_VAULT_CONFIGURATOR} 1 false 0 0 false 0 0 --sig "run(address,address,address,uint48,bool,uint256,uint64,bool,uint64,uint48)" ${NETWORK_ARGS} - @echo "✅ Vault deployment completed" + @echo "📡 Deploying Collateral..." + @forge script script/DeployCollateral.s.sol:DeployCollateral ${NETWORK_ARGS} + @echo "✅ Collateral deployment completed" - @echo "📡 Deploying VaultTokenized..." - @forge script lib/core/script/deploy/VaultTokenized.s.sol:VaultTokenizedScript ${DEFAULT_VAULT_CONFIGURATOR} ${DEFAULT_OWNER} ${DEFAULT_VAULT_CONFIGURATOR} 1 false 0 Test TEST 0 false 0 0 --sig "run(address,address,address,uint48,bool,uint256,string,string,uint64,bool,uint64,uint48)" ${NETWORK_ARGS} - @echo "✅ VaultTokenized deployment completed" \ No newline at end of file + @echo "📡 Deploying Symbiotic..." + @forge script script/DeploySymbiotic.s.sol ${NETWORK_ARGS} + @echo "✅ Symbiotic deployment completed" diff --git a/foundry.toml b/foundry.toml index 0ada5ce..5b254d1 100644 --- a/foundry.toml +++ b/foundry.toml @@ -9,7 +9,7 @@ gas_reports = ["*"] # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options -remappings = ['@openzeppelin/contracts=lib/openzeppelin-contracts/contracts','@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/', '@symbiotic/=lib/core/src/'] +remappings = ['@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/','@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/', '@symbiotic/=lib/core/src/', '@symbiotic-rewards=lib/rewards/src/'] [rpc_endpoints] mainnet = "${ETH_RPC_URL}" diff --git a/lib/core b/lib/core index f38f1b1..5061e30 160000 --- a/lib/core +++ b/lib/core @@ -1 +1 @@ -Subproject commit f38f1b16b8207dcff55d681a0d5ba28c66e785c8 +Subproject commit 5061e30ba6866680d3a9a2b1a4c52f6cb95dc9fc diff --git a/lib/foundry-devops b/lib/foundry-devops new file mode 160000 index 0000000..47393d0 --- /dev/null +++ b/lib/foundry-devops @@ -0,0 +1 @@ +Subproject commit 47393d0a85ad9f6aa127ba2aed2bf9a7a7488bcf diff --git a/script/NetworkSetup.s.sol b/script/NetworkSetup.s.sol deleted file mode 100644 index c8855b6..0000000 --- a/script/NetworkSetup.s.sol +++ /dev/null @@ -1,32 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity 0.8.25; - -import {Script} from "forge-std/Script.sol"; -import {SimpleMiddleware} from "src/SimpleMiddleware.sol"; -import {IRegistry} from "@symbiotic/interfaces/common/IRegistry.sol"; -import {INetworkRegistry} from "@symbiotic/interfaces/INetworkRegistry.sol"; -import {IOperatorRegistry} from "@symbiotic/interfaces/IOperatorRegistry.sol"; -import {IOptInService} from "@symbiotic/interfaces/service/IOptInService.sol"; -import {IVault} from "@symbiotic/interfaces/vault/IVault.sol"; -import {IBaseDelegator} from "@symbiotic/interfaces/delegator/IBaseDelegator.sol"; - -contract NetworkSetup is Script { - function run( - address networkRegistry, - address[] memory vaults, - uint256 subnetworksCnt, - uint256[][] calldata networkLimits - ) external { - require(vaults.length == networkLimits.length, "inconsistent length"); - vm.startBroadcast(); - INetworkRegistry(networkRegistry).registerNetwork(); - for (uint256 i = 0; i < vaults.length; ++i) { - require(subnetworksCnt == networkLimits[i].length, "inconsistent length"); - address delegator = IVault(vaults[i]).delegator(); - for (uint96 j = 0; j < subnetworksCnt; ++j) { - IBaseDelegator(delegator).setMaxNetworkLimit(j, networkLimits[i][j]); - } - } - vm.stopBroadcast(); - } -} diff --git a/script/Setup.s.sol b/script/Setup.s.sol deleted file mode 100644 index d14729f..0000000 --- a/script/Setup.s.sol +++ /dev/null @@ -1,38 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity 0.8.25; - -import {Script} from "forge-std/Script.sol"; -import {SimpleMiddleware} from "src/SimpleMiddleware.sol"; - -contract Setup is Script { - function run( - address network, - address owner, - uint48 epochDuration, - address[] memory vaults, - address[] memory operators, - bytes32[] memory keys, - address operatorRegistry, - address vaultRegistry, - address operatorNetworkOptIn - ) external { - require(operators.length == keys.length, "inconsistent length"); - vm.startBroadcast(); - - uint48 minSlashingWindow = epochDuration; // we dont use this - - SimpleMiddleware middleware = new SimpleMiddleware( - network, operatorRegistry, vaultRegistry, operatorNetworkOptIn, owner, epochDuration, minSlashingWindow - ); - - for (uint256 i = 0; i < vaults.length; ++i) { - middleware.registerVault(vaults[i]); - } - - for (uint256 i = 0; i < operators.length; ++i) { - middleware.registerOperator(operators[i], keys[i]); - } - - vm.stopBroadcast(); - } -} diff --git a/src/SimpleMiddleware.sol b/src/SimpleMiddleware.sol deleted file mode 100644 index 3517d79..0000000 --- a/src/SimpleMiddleware.sol +++ /dev/null @@ -1,419 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.25; - -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"; - -import {IRegistry} from "@symbiotic/interfaces/common/IRegistry.sol"; -import {IEntity} from "@symbiotic/interfaces/common/IEntity.sol"; -import {IVault} from "@symbiotic/interfaces/vault/IVault.sol"; -import {IBaseDelegator} from "@symbiotic/interfaces/delegator/IBaseDelegator.sol"; -import {IBaseSlasher} from "@symbiotic/interfaces/slasher/IBaseSlasher.sol"; -import {IOptInService} from "@symbiotic/interfaces/service/IOptInService.sol"; -import {IEntity} from "@symbiotic/interfaces/common/IEntity.sol"; -import {ISlasher} from "@symbiotic/interfaces/slasher/ISlasher.sol"; -import {IVetoSlasher} from "@symbiotic/interfaces/slasher/IVetoSlasher.sol"; -import {Subnetwork} from "@symbiotic/contracts/libraries/Subnetwork.sol"; - -import {SimpleKeyRegistry32} from "./SimpleKeyRegistry32.sol"; -import {MapWithTimeData} from "./libraries/MapWithTimeData.sol"; - -contract SimpleMiddleware is SimpleKeyRegistry32, Ownable { - using EnumerableMap for EnumerableMap.AddressToUintMap; - using MapWithTimeData for EnumerableMap.AddressToUintMap; - using Subnetwork for address; - - error NotOperator(); - error NotVault(); - - error OperatorNotOptedIn(); - error OperatorNotRegistred(); - error OperarorGracePeriodNotPassed(); - error OperatorAlreadyRegistred(); - - error VaultAlreadyRegistred(); - error VaultEpochTooShort(); - error VaultGracePeriodNotPassed(); - - error InvalidSubnetworksCnt(); - - error TooOldEpoch(); - error InvalidEpoch(); - - error SlashingWindowTooShort(); - error TooBigSlashAmount(); - error 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; - - 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; - - modifier updateStakeCache( - uint48 epoch - ) { - if (!totalStakeCached[epoch]) { - calcAndCacheStakes(epoch); - } - _; - } - - constructor( - address _network, - address _operatorRegistry, - address _vaultRegistry, - address _operatorNetOptin, - address _owner, - uint48 _epochDuration, - uint48 _slashingWindow - ) SimpleKeyRegistry32() Ownable(_owner) { - if (_slashingWindow < _epochDuration) { - revert 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()); - } - - function registerOperator(address operator, bytes32 key) external onlyOwner { - if (operators.contains(operator)) { - revert OperatorAlreadyRegistred(); - } - - if (!IRegistry(OPERATOR_REGISTRY).isEntity(operator)) { - revert NotOperator(); - } - - if (!IOptInService(OPERATOR_NET_OPTIN).isOptedIn(operator, NETWORK)) { - revert OperatorNotOptedIn(); - } - - updateKey(operator, key); - - operators.add(operator); - operators.enable(operator); - } - - function updateOperatorKey(address operator, bytes32 key) external onlyOwner { - if (!operators.contains(operator)) { - revert OperatorNotRegistred(); - } - - updateKey(operator, key); - } - - function pauseOperator( - address operator - ) external onlyOwner { - operators.disable(operator); - } - - function unpauseOperator( - address operator - ) external onlyOwner { - operators.enable(operator); - } - - function unregisterOperator( - address operator - ) external onlyOwner { - (, uint48 disabledTime) = operators.getTimes(operator); - - if (disabledTime == 0 || disabledTime + SLASHING_WINDOW > Time.timestamp()) { - revert OperarorGracePeriodNotPassed(); - } - - operators.remove(operator); - } - - function registerVault( - address vault - ) external onlyOwner { - if (vaults.contains(vault)) { - revert VaultAlreadyRegistred(); - } - - if (!IRegistry(VAULT_REGISTRY).isEntity(vault)) { - revert NotVault(); - } - - uint48 vaultEpoch = IVault(vault).epochDuration(); - - address slasher = IVault(vault).slasher(); - if (slasher != address(0) && IEntity(slasher).TYPE() == VETO_SLASHER_TYPE) { - vaultEpoch -= IVetoSlasher(slasher).vetoDuration(); - } - - if (vaultEpoch < SLASHING_WINDOW) { - revert VaultEpochTooShort(); - } - - vaults.add(vault); - vaults.enable(vault); - } - - function pauseVault( - address vault - ) external onlyOwner { - vaults.disable(vault); - } - - function unpauseVault( - address vault - ) external onlyOwner { - vaults.enable(vault); - } - - function unregisterVault( - address vault - ) external onlyOwner { - (, uint48 disabledTime) = vaults.getTimes(vault); - - if (disabledTime == 0 || disabledTime + SLASHING_WINDOW > Time.timestamp()) { - revert VaultGracePeriodNotPassed(); - } - - vaults.remove(vault); - } - - function setSubnetworksCnt( - uint256 _subnetworksCnt - ) external onlyOwner { - if (subnetworksCnt >= _subnetworksCnt) { - revert InvalidSubnetworksCnt(); - } - - subnetworksCnt = _subnetworksCnt; - } - - function getOperatorStake(address operator, uint48 epoch) public view returns (uint256 stake) { - if (totalStakeCached[epoch]) { - return operatorStakeCache[epoch][operator]; - } - - 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) - ); - } - } - - return stake; - } - - function getTotalStake( - uint48 epoch - ) public view returns (uint256) { - if (totalStakeCached[epoch]) { - return totalStakeCache[epoch]; - } - 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); - - // 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); - } - - // shrink array to skip unused slots - /// @solidity memory-safe-assembly - assembly { - mstore(validatorsData, valIdx) - } - } - - function submission(bytes memory payload, bytes32[] memory signatures) public updateStakeCache(getCurrentEpoch()) { - // validate signatures - // validate payload - // process payload - } - - // just for example, our devnets don't support slashing - function slash(uint48 epoch, address operator, uint256 amount) public onlyOwner updateStakeCache(epoch) { - uint48 epochStartTs = getEpochStartTs(epoch); - - if (epochStartTs < Time.timestamp() - SLASHING_WINDOW) { - revert TooOldEpoch(); - } - - uint256 totalOperatorStake = getOperatorStake(operator, epoch); - - if (totalOperatorStake < amount) { - revert TooBigSlashAmount(); - } - - // simple pro-rata slasher - for (uint256 i; i < vaults.length(); ++i) { - (address vault, uint48 enabledTime, uint48 disabledTime) = operators.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) { - bytes32 subnetwork = NETWORK.subnetwork(j); - uint256 vaultStake = - IBaseDelegator(IVault(vault).delegator()).stakeAt(subnetwork, operator, epochStartTs, new bytes(0)); - - _slashVault(epochStartTs, vault, subnetwork, operator, amount * vaultStake / totalOperatorStake); - } - } - } - - function calcAndCacheStakes( - uint48 epoch - ) public 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(); - } - - if (epochStartTs > Time.timestamp()) { - revert InvalidEpoch(); - } - - for (uint256 i; i < operators.length(); ++i) { - (address operator, uint48 enabledTime, uint48 disabledTime) = 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); - operatorStakeCache[epoch][operator] = operatorStake; - - totalStake += operatorStake; - } - - totalStakeCached[epoch] = true; - totalStakeCache[epoch] = totalStake; - } - - function _calcTotalStake( - uint48 epoch - ) 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(); - } - - if (epochStartTs > Time.timestamp()) { - revert InvalidEpoch(); - } - - for (uint256 i; i < operators.length(); ++i) { - (address operator, uint48 enabledTime, uint48 disabledTime) = 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; - } - } - - function _wasActiveAt(uint48 enabledTime, uint48 disabledTime, uint48 timestamp) private pure returns (bool) { - return enabledTime != 0 && enabledTime <= timestamp && (disabledTime == 0 || disabledTime >= timestamp); - } - - 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(); - } - } -} 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/SimpleKeyRegistry32.sol b/src/libraries/SimpleKeyRegistry32.sol similarity index 69% rename from src/SimpleKeyRegistry32.sol rename to src/libraries/SimpleKeyRegistry32.sol index 5255d8f..0ca2e0a 100644 --- a/src/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 new file mode 100644 index 0000000..a3e4ed9 --- /dev/null +++ b/src/middleware/Middleware.sol @@ -0,0 +1,569 @@ +//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 {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"; + +import {IRegistry} from "@symbiotic/interfaces/common/IRegistry.sol"; +import {IEntity} from "@symbiotic/interfaces/common/IEntity.sol"; +import {IVault} from "@symbiotic/interfaces/vault/IVault.sol"; +import {IBaseDelegator} from "@symbiotic/interfaces/delegator/IBaseDelegator.sol"; +import {IBaseSlasher} from "@symbiotic/interfaces/slasher/IBaseSlasher.sol"; +import {IOptInService} from "@symbiotic/interfaces/service/IOptInService.sol"; +import {IEntity} from "@symbiotic/interfaces/common/IEntity.sol"; +import {ISlasher} from "@symbiotic/interfaces/slasher/ISlasher.sol"; +import {IVetoSlasher} from "@symbiotic/interfaces/slasher/IVetoSlasher.sol"; +import {Subnetwork} from "@symbiotic/contracts/libraries/Subnetwork.sol"; + +import {SimpleKeyRegistry32} from "../libraries/SimpleKeyRegistry32.sol"; +import {MapWithTimeData} from "../libraries/MapWithTimeData.sol"; + +contract Middleware is SimpleKeyRegistry32, Ownable { + using EnumerableMap for EnumerableMap.AddressToUintMap; + using MapWithTimeData for EnumerableMap.AddressToUintMap; + using Subnetwork for address; + + 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; + } + + 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 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 (!s_totalStakeCached[epoch]) { + calcAndCacheStakes(epoch); + } + _; + } + + constructor( + address _network, + address _operatorRegistry, + address _vaultRegistry, + address _operatorNetOptin, + address _owner, + uint48 _epochDuration, + uint48 _slashingWindow + ) SimpleKeyRegistry32() Ownable(_owner) { + if (_slashingWindow < _epochDuration) { + revert Middleware__SlashingWindowTooShort(); + } + 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 (s_operators.contains(operator)) { + revert Middleware__OperatorAlreadyRegistred(); + } + + if (!IRegistry(i_operatorRegistry).isEntity(operator)) { + revert Middleware__NotOperator(); + } + + if (!IOptInService(i_operatorNetworkOptin).isOptedIn(operator, i_network)) { + revert Middleware__OperatorNotOptedIn(); + } + + updateKey(operator, key); + + 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 (!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 { + 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 { + 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) = s_operators.getTimes(operator); + + if (disabledTime == 0 || disabledTime + i_slashingWindow > Time.timestamp()) { + revert Middleware__OperarorGracePeriodNotPassed(); + } + + 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 (s_vaults.contains(vault)) { + revert Middleware__VaultAlreadyRegistered(); + } + + if (!IRegistry(i_vaultRegistry).isEntity(vault)) { + revert Middleware__NotVault(); + } + + uint48 vaultEpoch = IVault(vault).epochDuration(); + + address slasher = IVault(vault).slasher(); + if (slasher != address(0) && IEntity(slasher).TYPE() == VETO_SLASHER_TYPE) { + vaultEpoch -= IVetoSlasher(slasher).vetoDuration(); + } + + if (vaultEpoch < i_slashingWindow) { + revert Middleware__VaultEpochTooShort(); + } + + 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 { + 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 { + 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) = s_vaults.getTimes(vault); + + if (disabledTime == 0 || disabledTime + i_slashingWindow > Time.timestamp()) { + revert Middleware__VaultGracePeriodNotPassed(); + } + + s_vaults.remove(vault); + } + + /** + * @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 (s_subnetworksCount >= _subnetworksCount) { + revert Middleware__InvalidSubnetworksCnt(); + } + + s_subnetworksCount = _subnetworksCount; + } + + // 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 epoch older than SLASHING_WINDOW total stake can be invalidated (use cache) + if (epochStartTs < Time.timestamp() - i_slashingWindow) { + revert Middleware__TooOldEpoch(); + } + + if (epochStartTs > Time.timestamp()) { + revert Middleware__InvalidEpoch(); + } + + 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); + s_operatorStakeCache[epoch][operator] = operatorStake; + + totalStake += operatorStake; + } + + 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); + params.operator = operator; + params.slashAmount = amount; + + params.totalOperatorStake = getOperatorStake(operator, epoch); + + if (params.totalOperatorStake < amount) { + revert Middleware__TooBigSlashAmount(); + } + + // simple pro-rata slasher + 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)) { + continue; + } + + _processVaultSlashing(vault, params); + } + } + + /** + * @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 < s_subnetworksCount; ++j) { + bytes32 subnetwork = i_network.subnetwork(j); + uint256 vaultStake = IBaseDelegator(IVault(vault).delegator()).stakeAt( + subnetwork, params.operator, params.epochStartTs, new bytes(0) + ); + + uint256 slashAmount = (params.slashAmount * vaultStake) / params.totalOperatorStake; + _slashVault(params.epochStartTs, vault, subnetwork, params.operator, slashAmount); + } + } + + /** + * @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 + ) private view returns (uint256 totalStake) { + uint48 epochStartTs = getEpochStartTs(epoch); + + // 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 Middleware__InvalidEpoch(); + } + + 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; + } + } + + /** + * @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); + } + + /** + * @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); + + // 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) + ); + } + } + + 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); + + 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; + } + + 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) + } + } + + /** + * @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; + } + + /** + * @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 new file mode 100644 index 0000000..cab0654 --- /dev/null +++ b/test/mocks/symbiotic/DelegatorMock.sol @@ -0,0 +1,50 @@ +//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 {BaseDelegator} from "@symbiotic/contracts/delegator/BaseDelegator.sol"; +import {Entity} from "@symbiotic/contracts/common/Entity.sol"; +import {IVault} from "@symbiotic/interfaces/vault/IVault.sol"; + +contract DelegatorMock is BaseDelegator { + constructor( + address networkRegistry, + address vaultFactory, + address operatorVaultOptInService, + address operatorNetworkOptInService, + address delegatorFactory, + uint64 entityType + ) + BaseDelegator( + networkRegistry, + vaultFactory, + operatorVaultOptInService, + operatorNetworkOptInService, + delegatorFactory, + entityType + ) + {} + + function _stakeAt( + bytes32, /*subnetwork*/ + address operator, + uint48, /*timestamp*/ + bytes memory hints + ) internal view override returns (uint256, bytes memory) { + uint256 operatorStake = IVault(vault).activeBalanceOf(operator); + return (hints.length > 0 ? (0, bytes("0xrandomData")) : (operatorStake, bytes(""))); + } +} diff --git a/test/mocks/symbiotic/OptInServiceMock.sol b/test/mocks/symbiotic/OptInServiceMock.sol new file mode 100644 index 0000000..70baed2 --- /dev/null +++ b/test/mocks/symbiotic/OptInServiceMock.sol @@ -0,0 +1,105 @@ +//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 {IOptInService} from "@symbiotic/interfaces/service/IOptInService.sol"; + +import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; +import {Checkpoints} from "@symbiotic/contracts/libraries/Checkpoints.sol"; + +import {Time} from "@openzeppelin/contracts/utils/types/Time.sol"; + +contract OptInServiceMock is EIP712, IOptInService { + using Checkpoints for Checkpoints.Trace208; + + address public immutable WHO_REGISTRY; + address public immutable WHERE_REGISTRY; + + mapping(address who => mapping(address where => uint256 nonce)) public nonces; + + mapping(address who => mapping(address where => Checkpoints.Trace208 value)) internal _isOptedIn; + + constructor(address whoRegistry, address whereRegistry, string memory name) EIP712(name, "1") { + WHO_REGISTRY = whoRegistry; + WHERE_REGISTRY = whereRegistry; + } + + function test() public {} + + function isOptedInAt( + address who, + address where, + uint48 timestamp, + bytes calldata hint + ) external view returns (bool) { + return _isOptedIn[who][where].upperLookupRecent(timestamp, hint) == 1; + } + + function isOptedIn(address who, address where) public view returns (bool) { + return _isOptedIn[who][where].latest() == 1; + } + + function optIn( + address where + ) external { + _optIn(msg.sender, where); + } + + function optIn(address who, address where, uint48, /*deadline*/ bytes calldata /*signature*/ ) external { + _optIn(who, where); + } + + function optOut( + address where + ) external { + _optOut(msg.sender, where); + } + + function optOut(address who, address where, uint48, /*deadline*/ bytes calldata /* signature*/ ) external { + _optOut(who, where); + } + + function increaseNonce( + address where + ) external { + _increaseNonce(msg.sender, where); + } + + function _optIn(address who, address where) internal { + _isOptedIn[who][where].push(Time.timestamp(), 1); + + _increaseNonce(who, where); + + emit OptIn(who, where); + } + + function _optOut(address who, address where) internal { + _isOptedIn[who][where].push(Time.timestamp(), 0); + + _increaseNonce(who, where); + + emit OptOut(who, where); + } + + function _increaseNonce(address who, address where) internal { + unchecked { + ++nonces[who][where]; + } + + emit IncreaseNonce(who, where); + } +} diff --git a/test/mocks/symbiotic/RegistryMock.sol b/test/mocks/symbiotic/RegistryMock.sol new file mode 100644 index 0000000..13eed4d --- /dev/null +++ b/test/mocks/symbiotic/RegistryMock.sol @@ -0,0 +1,24 @@ +//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"; +import {Entity} from "@symbiotic/contracts/common/Entity.sol"; + +contract RegistryMock is Registry { + function register() external { + _addEntity(msg.sender); + } +} diff --git a/test/mocks/symbiotic/VaultMock.sol b/test/mocks/symbiotic/VaultMock.sol new file mode 100644 index 0000000..6bfa43c --- /dev/null +++ b/test/mocks/symbiotic/VaultMock.sol @@ -0,0 +1,124 @@ +//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 {IVault} from "@symbiotic/interfaces/vault/IVault.sol"; +import {VaultStorage} from "@symbiotic/contracts/vault/VaultStorage.sol"; +import {IVaultStorage} from "@symbiotic/interfaces/vault/IVaultStorage.sol"; +import {MigratableEntity} from "@symbiotic/contracts/common/MigratableEntity.sol"; + +import {Entity} from "@symbiotic/contracts/common/Entity.sol"; + +contract VaultMock is VaultStorage, MigratableEntity, IVault { + uint256 public totalAtStake; + + mapping(address => uint256) public operatorStake; + address[] public operators; + + constructor( + address delegatorFactory, + address slasherFactory, + address vaultFactory + ) VaultStorage(delegatorFactory, slasherFactory) MigratableEntity(vaultFactory) {} + + function test() public {} + + function isInitialized() external view returns (bool) {} + + function totalStake() external view returns (uint256) {} + + function activeBalanceOfAt( + address account, + uint48, /*timestamp*/ + bytes calldata /*hints*/ + ) external view returns (uint256) { + return operatorStake[account]; + } + + function activeBalanceOf( + address account + ) external view returns (uint256) { + return operatorStake[account]; + } + + function withdrawalsOf(uint256 epoch, address account) external view returns (uint256) {} + + function slashableBalanceOf( + address account + ) external view returns (uint256) {} + + function deposit( + address onBehalfOf, + uint256 amount + ) external returns (uint256 depositedAmount, uint256 mintedShares) { + operatorStake[onBehalfOf] += amount; + operators.push(onBehalfOf); + totalAtStake += amount; + depositedAmount = amount; + mintedShares = amount; + } + + function withdraw(address claimer, uint256 amount) external returns (uint256 burnedShares, uint256 mintedShares) {} + + function redeem(address claimer, uint256 shares) external returns (uint256 withdrawnAssets, uint256 mintedShares) {} + + function claim(address recipient, uint256 epoch) external returns (uint256 amount) {} + + function claimBatch(address recipient, uint256[] calldata epochs) external returns (uint256 amount) {} + + function onSlash(uint256 amount, uint48 /*captureTimestamp */ ) external returns (uint256 slashedAmount) { + totalAtStake -= amount; + slashedAmount = amount; + for (uint256 i = 0; i < operators.length; i++) { + if (operatorStake[operators[i]] >= amount) { + operatorStake[operators[i]] -= amount; + break; + } + } + } + + function setDepositWhitelist( + bool status + ) external {} + + function setDepositorWhitelistStatus(address account, bool status) external {} + + function setIsDepositLimit( + bool status + ) external {} + + function setDepositLimit( + uint256 limit + ) external {} + + function setDelegator( + address delegator_ + ) external nonReentrant { + delegator = delegator_; + + isDelegatorInitialized = true; + + emit SetDelegator(delegator_); + } + + function setSlasher( + address slasher_ + ) external nonReentrant { + isSlasherInitialized = true; + slasher = slasher_; + emit SetSlasher(slasher_); + } +} diff --git a/test/unit/Middleware.t.sol b/test/unit/Middleware.t.sol new file mode 100644 index 0000000..f94fc65 --- /dev/null +++ b/test/unit/Middleware.t.sol @@ -0,0 +1,1123 @@ +//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"; + +//************************************************************************************************** +// SYMBIOTIC +//************************************************************************************************** +import {OptInService} from "@symbiotic/contracts/service/OptInService.sol"; +import {NetworkMiddlewareService} from "@symbiotic/contracts/service/NetworkMiddlewareService.sol"; +import {DelegatorFactory} from "@symbiotic/contracts/DelegatorFactory.sol"; +import {SlasherFactory} from "@symbiotic/contracts/SlasherFactory.sol"; +import {VaultFactory} from "@symbiotic/contracts/VaultFactory.sol"; +import {Slasher} from "@symbiotic/contracts/slasher/Slasher.sol"; +import {VetoSlasher} from "@symbiotic/contracts/slasher/VetoSlasher.sol"; +import {Subnetwork} from "@symbiotic/contracts/libraries/Subnetwork.sol"; +import {NetworkMiddlewareService} from "@symbiotic/contracts/service/NetworkMiddlewareService.sol"; + +//************************************************************************************************** +// OPENZEPPELIN +//************************************************************************************************** +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; + +import {Middleware} from "../../src/middleware/Middleware.sol"; +import {SimpleKeyRegistry32} from "../../src/libraries/SimpleKeyRegistry32.sol"; + +import {DelegatorMock} from "../mocks/symbiotic/DelegatorMock.sol"; +import {OptInServiceMock} from "../mocks/symbiotic/OptInServiceMock.sol"; +import {RegistryMock} from "../mocks/symbiotic/RegistryMock.sol"; +import {VaultMock} from "../mocks/symbiotic/VaultMock.sol"; + +contract MiddlewareTest is Test { + using Subnetwork for address; + + uint48 public constant NETWORK_EPOCH_DURATION = 6 days; + uint48 public constant SLASHING_WINDOW = 7 days; + uint256 public constant OPERATOR_STAKE = 10 ether; + uint256 public constant OPERATOR_INITIAL_BALANCE = 1000 ether; + uint256 public constant MIN_SLASHING_WINDOW = 1 days; + bytes32 public constant OPERATOR_KEY = bytes32(uint256(1)); + + uint48 public constant START_TIME = 1; + + address network = makeAddr("network"); + address vaultFactory = makeAddr("vaultFactory"); + address slasherFactory = makeAddr("vaultFactory"); + address delegatorFactory = makeAddr("delegatorFactory"); + + address owner = makeAddr("owner"); + address operator = makeAddr("operator"); + OptInServiceMock operatorNetworkOptInServiceMock; + OptInServiceMock operatorVaultOptInServiceMock; + DelegatorMock delegator; + Middleware middleware; + RegistryMock registry; + VaultMock vault; + Slasher slasher; + VetoSlasher vetoSlasher; + Slasher slasherWithBadType; + + function setUp() public { + vm.startPrank(owner); + + registry = new RegistryMock(); + operatorNetworkOptInServiceMock = + new OptInServiceMock(address(registry), address(registry), "OperatorNetworkOptInService"); + + operatorVaultOptInServiceMock = + new OptInServiceMock(address(registry), address(vaultFactory), "OperatorVaultOptInService"); + + NetworkMiddlewareService networkMiddlewareService = new NetworkMiddlewareService(address(registry)); + + delegator = new DelegatorMock( + address(registry), + vaultFactory, + address(operatorVaultOptInServiceMock), + address(operatorNetworkOptInServiceMock), + delegatorFactory, + 0 + ); + slasher = new Slasher(vaultFactory, address(networkMiddlewareService), slasherFactory, 0); + slasherWithBadType = new Slasher(vaultFactory, address(networkMiddlewareService), slasherFactory, 2); + vetoSlasher = + new VetoSlasher(vaultFactory, address(networkMiddlewareService), address(registry), slasherFactory, 1); + + vault = new VaultMock(delegatorFactory, slasherFactory, vaultFactory); + vault.setDelegator(address(delegator)); + + vm.store(address(delegator), bytes32(uint256(0)), bytes32(uint256(uint160(address(vault))))); + middleware = new Middleware( + address(network), + address(registry), + address(registry), + address(operatorNetworkOptInServiceMock), + owner, + NETWORK_EPOCH_DURATION, + SLASHING_WINDOW + ); + + vm.startPrank(network); + registry.register(); + networkMiddlewareService.setMiddleware(address(middleware)); + vm.stopPrank(); + } + + function _registerOperatorToNetwork(address _operator, address _vault, bool skipRegister, bool skipOptIn) public { + vm.startPrank(_operator); + if (!skipRegister) { + registry.register(); + } + if (!skipOptIn) { + operatorNetworkOptInServiceMock.optIn(network); + operatorVaultOptInServiceMock.optIn(address(_vault)); + } + vm.stopPrank(); + } + + function testConstructorFailsWithInvalidSlashingWindow() public { + uint48 EPOCH_DURATION_ = 100; + uint48 SHORT_SLASHING_WINDOW_ = 99; + + vm.startPrank(owner); + vm.expectRevert(Middleware.Middleware__SlashingWindowTooShort.selector); + + new Middleware( + address(0), + address(0), + address(0), + address(0), + owner, + EPOCH_DURATION_, + SHORT_SLASHING_WINDOW_ // slashing window less than epoch duration + ); + + vm.stopPrank(); + } + + function testGetEpochStartTs() public view { + // Test first epoch + assertEq(middleware.getEpochStartTs(0), START_TIME); + + // Test subsequent epochs + assertEq(middleware.getEpochStartTs(1), START_TIME + NETWORK_EPOCH_DURATION); + assertEq(middleware.getEpochStartTs(2), START_TIME + 2 * NETWORK_EPOCH_DURATION); + + // Test large epoch number + uint48 largeEpoch = 1000; + assertEq(middleware.getEpochStartTs(largeEpoch), START_TIME + largeEpoch * NETWORK_EPOCH_DURATION); + } + + function testGetEpochAtTs() public view { + // Test start time + assertEq(middleware.getEpochAtTs(uint48(START_TIME)), 0); + + // Test middle of first epoch + assertEq(middleware.getEpochAtTs(uint48(START_TIME + NETWORK_EPOCH_DURATION / 2)), 0); + + // Test exact epoch boundaries + assertEq(middleware.getEpochAtTs(uint48(START_TIME + NETWORK_EPOCH_DURATION)), 1); + + assertEq(middleware.getEpochAtTs(uint48(START_TIME + 2 * NETWORK_EPOCH_DURATION)), 2); + + // Test random time in later epoch + uint48 randomOffset = 1000; + assertEq(middleware.getEpochAtTs(uint48(START_TIME + randomOffset)), randomOffset / NETWORK_EPOCH_DURATION); + } + + function testGetCurrentEpoch() public { + // Test at start + assertEq(middleware.getCurrentEpoch(), 0); + + // Test after some time has passed + vm.warp(START_TIME + NETWORK_EPOCH_DURATION * 2 / 3); + assertEq(middleware.getCurrentEpoch(), 0); + + // Test at exact epoch boundary + vm.warp(START_TIME + NETWORK_EPOCH_DURATION + 1); + assertEq(middleware.getCurrentEpoch(), 1); + + // Test in middle of later epoch + vm.warp(START_TIME + 5 * NETWORK_EPOCH_DURATION + NETWORK_EPOCH_DURATION / 2); + assertEq(middleware.getCurrentEpoch(), 5); + } + + function _registerVaultToNetwork(address _vault, bool skipRegister, uint256 slashingWindowReduction) public { + bytes32 slotValue = vm.load(address(_vault), bytes32(uint256(1))); + uint256 newValue = uint256(SLASHING_WINDOW - slashingWindowReduction) << (26 * 8); + bytes32 mask = bytes32(~(uint256(type(uint48).max) << (26 * 8))); + bytes32 newSlotValue = (slotValue & mask) | bytes32(newValue); + + vm.store(address(_vault), bytes32(uint256(1)), newSlotValue); + vm.startPrank(_vault); + if (!skipRegister) { + registry.register(); + } + vm.stopPrank(); + } + + function testInitialState() public view { + 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); + } + + // ************************************************************************************************ + // * REGISTER OPERATOR + // ************************************************************************************************ + function testRegisterOperator() public { + _registerOperatorToNetwork(operator, address(vault), false, false); + + vm.startPrank(owner); + middleware.registerOperator(operator, OPERATOR_KEY); + + // Get validator set for current epoch + uint48 currentEpoch = middleware.getCurrentEpoch(); + Middleware.ValidatorData[] memory validators = middleware.getValidatorSet(currentEpoch); + + assertEq(validators.length, 1); + assertEq(validators[0].key, OPERATOR_KEY); + vm.stopPrank(); + } + + function testRegisterOperatorUnauthorized() public { + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, address(this))); + middleware.registerOperator(operator, OPERATOR_KEY); + } + + function testRegisterOperatorAlreadyRegistered() public { + _registerOperatorToNetwork(operator, address(vault), false, false); + + vm.startPrank(owner); + middleware.registerOperator(operator, OPERATOR_KEY); + + vm.expectRevert(Middleware.Middleware__OperatorAlreadyRegistred.selector); + middleware.registerOperator(operator, OPERATOR_KEY); + vm.stopPrank(); + } + + function testRegisterOperatorNotOperator() public { + _registerOperatorToNetwork(operator, address(vault), true, false); + + vm.startPrank(owner); + vm.expectRevert(Middleware.Middleware__NotOperator.selector); + middleware.registerOperator(owner, OPERATOR_KEY); + vm.stopPrank(); + } + + function testRegisterOperatorNotOptedIn() public { + _registerOperatorToNetwork(operator, address(vault), false, true); + + vm.startPrank(owner); + vm.expectRevert(Middleware.Middleware__OperatorNotOptedIn.selector); + middleware.registerOperator(operator, OPERATOR_KEY); + vm.stopPrank(); + } + + function testRegisterOperatorWithSameKeyAsOtherOperator() public { + _registerOperatorToNetwork(operator, address(vault), false, false); + address operator2 = makeAddr("operator2"); + _registerOperatorToNetwork(operator2, address(vault), false, false); + + vm.startPrank(owner); + + middleware.registerOperator(operator, OPERATOR_KEY); + vm.expectRevert(SimpleKeyRegistry32.DuplicateKey.selector); + middleware.registerOperator(operator2, OPERATOR_KEY); + + vm.stopPrank(); + } + + // ************************************************************************************************ + // * UPDATE OPERATOR KEY + // ************************************************************************************************ + + function testUpdateOperatorKey() public { + _registerOperatorToNetwork(operator, address(vault), false, false); + + vm.startPrank(owner); + middleware.registerOperator(operator, OPERATOR_KEY); + + bytes32 newKey = bytes32(uint256(2)); + middleware.updateOperatorKey(operator, newKey); + + // Get validator set for current epoch + uint48 currentEpoch = middleware.getCurrentEpoch(); + Middleware.ValidatorData[] memory validators = middleware.getValidatorSet(currentEpoch); + + assertEq(validators.length, 1); + assertEq(validators[0].key, newKey); + vm.stopPrank(); + } + + function testUpdateOperatorKeyUnauthorized() public { + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, address(this))); + middleware.updateOperatorKey(operator, OPERATOR_KEY); + } + + function testUpdateOperatorKeyNotRegistered() public { + vm.startPrank(owner); + vm.expectRevert(Middleware.Middleware__OperatorNotRegistred.selector); + middleware.updateOperatorKey(operator, OPERATOR_KEY); + vm.stopPrank(); + } + + // ************************************************************************************************ + // * PAUSE OPERATOR + // ************************************************************************************************ + function testPauseOperator() public { + _registerOperatorToNetwork(operator, address(vault), false, false); + + vm.startPrank(owner); + middleware.registerOperator(operator, OPERATOR_KEY); + + middleware.pauseOperator(operator); + vm.stopPrank(); + } + + function testPauseOperatorUnauthorized() public { + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, address(this))); + middleware.pauseOperator(operator); + } + + // ************************************************************************************************ + // * UNPAUSE OPERATOR + // ************************************************************************************************ + function testUnpauseOperator() public { + _registerOperatorToNetwork(operator, address(vault), false, false); + + vm.startPrank(owner); + middleware.registerOperator(operator, OPERATOR_KEY); + + middleware.pauseOperator(operator); + middleware.unpauseOperator(operator); + vm.stopPrank(); + } + + function testUnpauseOperatorUnauthorized() public { + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, address(this))); + middleware.unpauseOperator(operator); + } + + // ************************************************************************************************ + // * UNREGISTER OPERATOR + // ************************************************************************************************ + function testUnregisterOperator() public { + _registerOperatorToNetwork(operator, address(vault), false, false); + + vm.startPrank(owner); + middleware.registerOperator(operator, OPERATOR_KEY); + + middleware.pauseOperator(operator); + vm.warp(START_TIME + SLASHING_WINDOW + 1); + middleware.unregisterOperator(operator); + + // Get validator set for current epoch + uint48 currentEpoch = middleware.getCurrentEpoch(); + Middleware.ValidatorData[] memory validators = middleware.getValidatorSet(currentEpoch); + + assertEq(validators.length, 0); + vm.stopPrank(); + } + + function testUnregisterOperatorUnauthorized() public { + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, address(this))); + middleware.unregisterOperator(operator); + } + + function testUnregisterOperatorGracePeriodNotPassed() public { + _registerOperatorToNetwork(operator, address(vault), false, false); + + vm.startPrank(owner); + middleware.registerOperator(operator, OPERATOR_KEY); + + middleware.pauseOperator(operator); + vm.warp(START_TIME + SLASHING_WINDOW - 1); + vm.expectRevert(Middleware.Middleware__OperarorGracePeriodNotPassed.selector); + middleware.unregisterOperator(operator); + vm.stopPrank(); + } + + // ************************************************************************************************ + // * REGISTER VAULT + // ************************************************************************************************ + + function testRegisterVault() public { + _registerVaultToNetwork(address(vault), false, 0); + + vm.startPrank(owner); + vault.setSlasher(address(slasher)); + vm.store(address(slasher), bytes32(uint256(0)), bytes32(uint256(uint160(address(vault))))); + middleware.registerVault(address(vault)); + + assertEq(middleware.isVaultRegistered(address(vault)), true); + vm.stopPrank(); + } + + function testRegisterVaultUnauthorized() public { + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, address(this))); + middleware.registerVault(address(vault)); + } + + function testRegisterVaultAlreadyRegistered() public { + _registerVaultToNetwork(address(vault), false, 0); + + vm.startPrank(owner); + vault.setSlasher(address(slasher)); + vm.store(address(slasher), bytes32(uint256(0)), bytes32(uint256(uint160(address(vault))))); + + middleware.registerVault(address(vault)); + + vm.expectRevert(Middleware.Middleware__VaultAlreadyRegistered.selector); + middleware.registerVault(address(vault)); + vm.stopPrank(); + } + + function testRegisterVaultNotVault() public { + vm.startPrank(owner); + vm.expectRevert(Middleware.Middleware__NotVault.selector); + middleware.registerVault(owner); + vm.stopPrank(); + } + + function testRegisterVaultEpochTooShort() public { + _registerVaultToNetwork(address(vault), false, 1); + + vm.startPrank(owner); + vault.setSlasher(address(vetoSlasher)); + vm.store(address(vetoSlasher), bytes32(uint256(0)), bytes32(uint256(uint160(address(vault))))); + vm.expectRevert(Middleware.Middleware__VaultEpochTooShort.selector); + middleware.registerVault(address(vault)); + vm.stopPrank(); + } + + function testRegisterVaultWithVetoSlasher() public { + _registerVaultToNetwork(address(vault), false, 0); + + vm.startPrank(owner); + vault.setSlasher(address(vetoSlasher)); + vm.store(address(vetoSlasher), bytes32(uint256(0)), bytes32(uint256(uint160(address(vault))))); + middleware.registerVault(address(vault)); + + assertEq(middleware.isVaultRegistered(address(vault)), true); + vm.stopPrank(); + } + + // ************************************************************************************************ + // * PAUSE VAULT + // ************************************************************************************************ + + function testPauseVault() public { + _registerVaultToNetwork(address(vault), false, 0); + + vm.startPrank(owner); + vault.setSlasher(address(slasher)); + vm.store(address(slasher), bytes32(uint256(0)), bytes32(uint256(uint160(address(vault))))); + middleware.registerVault(address(vault)); + + middleware.pauseVault(address(vault)); + vm.stopPrank(); + } + + function testPauseVaultUnauthorized() public { + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, address(this))); + middleware.pauseVault(address(vault)); + } + + // ************************************************************************************************ + // * UNPAUSE VAULT + // ************************************************************************************************ + + function testUnpauseVault() public { + _registerVaultToNetwork(address(vault), false, 0); + + vm.startPrank(owner); + vault.setSlasher(address(slasher)); + vm.store(address(slasher), bytes32(uint256(0)), bytes32(uint256(uint160(address(vault))))); + middleware.registerVault(address(vault)); + + middleware.pauseVault(address(vault)); + middleware.unpauseVault(address(vault)); + vm.stopPrank(); + } + + function testUnpauseVaultUnauthorized() public { + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, address(this))); + middleware.unpauseVault(address(vault)); + } + + // ************************************************************************************************ + // * UNREGISTER VAULT + // ************************************************************************************************ + + function testUnregisterVault() public { + _registerVaultToNetwork(address(vault), false, 0); + + vm.startPrank(owner); + vault.setSlasher(address(slasher)); + vm.store(address(slasher), bytes32(uint256(0)), bytes32(uint256(uint160(address(vault))))); + middleware.registerVault(address(vault)); + + middleware.pauseVault(address(vault)); + vm.warp(START_TIME + SLASHING_WINDOW + 1); + middleware.unregisterVault(address(vault)); + + assertEq(middleware.isVaultRegistered(address(vault)), false); + vm.stopPrank(); + } + + function testUnregisterVaultUnauthorized() public { + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, address(this))); + middleware.unregisterVault(address(vault)); + } + + function testUnregisterVaultGracePeriodNotPassed() public { + _registerVaultToNetwork(address(vault), false, 0); + + vm.startPrank(owner); + vault.setSlasher(address(slasher)); + vm.store(address(slasher), bytes32(uint256(0)), bytes32(uint256(uint160(address(vault))))); + middleware.registerVault(address(vault)); + + middleware.pauseVault(address(vault)); + vm.warp(START_TIME + SLASHING_WINDOW - 1); + vm.expectRevert(Middleware.Middleware__VaultGracePeriodNotPassed.selector); + middleware.unregisterVault(address(vault)); + vm.stopPrank(); + } + + // ************************************************************************************************ + // * SET SUBNETWORKS COUNT + // ************************************************************************************************ + + function testSetSubnetworksCnt() public { + vm.startPrank(owner); + middleware.setSubnetworksCount(2); + assertEq(middleware.s_subnetworksCount(), 2); + vm.stopPrank(); + } + + function testSetSubnetworksCntInvalidIfGreaterThanZero() public { + vm.startPrank(owner); + middleware.setSubnetworksCount(10); + assertEq(middleware.s_subnetworksCount(), 10); + + vm.expectRevert(Middleware.Middleware__InvalidSubnetworksCnt.selector); + middleware.setSubnetworksCount(8); + vm.stopPrank(); + } + + function testSetSubnetworksCntInvalid() public { + vm.startPrank(owner); + vm.expectRevert(Middleware.Middleware__InvalidSubnetworksCnt.selector); + middleware.setSubnetworksCount(0); + vm.stopPrank(); + } + + function testSetSubnetworksCntUnauthorized() public { + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, address(this))); + middleware.setSubnetworksCount(2); + } + + // ************************************************************************************************ + // * GET OPERATOR STAKE + // ************************************************************************************************ + + function testGetOperatorStake() public { + _registerOperatorToNetwork(operator, address(vault), false, false); + _registerVaultToNetwork(address(vault), false, 0); + + vm.startPrank(owner); + middleware.registerOperator(operator, OPERATOR_KEY); + vault.setSlasher(address(slasher)); + vm.store(address(slasher), bytes32(uint256(0)), bytes32(uint256(uint160(address(vault))))); + middleware.registerVault(address(vault)); + + vm.startPrank(operator); + vault.deposit(operator, OPERATOR_STAKE); + + uint48 currentEpoch = middleware.getCurrentEpoch(); + uint256 stake = middleware.getOperatorStake(operator, currentEpoch); + + assertEq(stake, OPERATOR_STAKE); + vm.stopPrank(); + } + + function testGetOperatorStakeIsSameForEachEpoch() public { + _registerOperatorToNetwork(operator, address(vault), false, false); + _registerVaultToNetwork(address(vault), false, 0); + + vm.startPrank(owner); + middleware.registerOperator(operator, OPERATOR_KEY); + vault.setSlasher(address(slasher)); + vm.store(address(slasher), bytes32(uint256(0)), bytes32(uint256(uint160(address(vault))))); + middleware.registerVault(address(vault)); + vm.startPrank(operator); + vault.deposit(operator, OPERATOR_STAKE); + + vm.startPrank(owner); + uint48 currentEpoch = middleware.getCurrentEpoch(); + uint256 stake = middleware.getOperatorStake(operator, currentEpoch); + + assertEq(stake, OPERATOR_STAKE); + + vm.warp(START_TIME + NETWORK_EPOCH_DURATION + 1); + stake = middleware.getOperatorStake(operator, currentEpoch); + assertEq(stake, OPERATOR_STAKE); + vm.stopPrank(); + } + + function testGetOperatorStakeIsZeroIfNotRegisteredToVault() public { + _registerOperatorToNetwork(operator, address(vault), false, false); + _registerVaultToNetwork(address(vault), false, 0); + + vm.startPrank(owner); + middleware.registerOperator(operator, OPERATOR_KEY); + uint48 currentEpoch = middleware.getCurrentEpoch(); + uint256 stake = middleware.getOperatorStake(operator, currentEpoch); + + assertEq(stake, 0); + + vm.warp(START_TIME + NETWORK_EPOCH_DURATION + 1); + stake = middleware.getOperatorStake(operator, currentEpoch); + assertEq(stake, 0); + vm.stopPrank(); + } + + function testGetOperatorStakeCached() public { + _registerOperatorToNetwork(operator, address(vault), false, false); + _registerVaultToNetwork(address(vault), false, 0); + + vm.startPrank(owner); + middleware.registerOperator(operator, OPERATOR_KEY); + vault.setSlasher(address(slasher)); + vm.store(address(slasher), bytes32(uint256(0)), bytes32(uint256(uint160(address(vault))))); + middleware.registerVault(address(vault)); + + vm.startPrank(operator); + vault.deposit(operator, OPERATOR_STAKE); + + vm.startPrank(owner); + vm.warp(START_TIME + SLASHING_WINDOW + 1); //We need this otherwise underflow in the first IF + uint48 currentEpoch = middleware.getCurrentEpoch(); + uint256 totalStakeCached = middleware.calcAndCacheStakes(currentEpoch); + + uint256 stake = middleware.getOperatorStake(operator, currentEpoch); + + assertEq(stake, totalStakeCached); + assertEq(stake, OPERATOR_STAKE); + vm.stopPrank(); + } + + function testGetOperatorStakeButOperatorNotActive() public { + address operatorUnregistered = address(1); + _registerOperatorToNetwork(operatorUnregistered, address(vault), false, false); + _registerVaultToNetwork(address(vault), false, 0); + + vm.startPrank(owner); + middleware.registerOperator(operatorUnregistered, OPERATOR_KEY); + vault.setSlasher(address(slasher)); + vm.store(address(slasher), bytes32(uint256(0)), bytes32(uint256(uint160(address(vault))))); + middleware.registerVault(address(vault)); + middleware.pauseVault(address(vault)); + vm.warp(START_TIME + SLASHING_WINDOW + 1); + uint48 currentEpoch = middleware.getCurrentEpoch(); + uint256 stake = middleware.getOperatorStake(operatorUnregistered, currentEpoch); + assertEq(stake, 0); + vm.stopPrank(); + } + + // ************************************************************************************************ + // * GET TOTAL STAKE + // ************************************************************************************************ + + function testGetTotalStake() public { + _registerOperatorToNetwork(operator, address(vault), false, false); + _registerVaultToNetwork(address(vault), false, 0); + + vm.startPrank(owner); + middleware.registerOperator(operator, OPERATOR_KEY); + vault.setSlasher(address(slasher)); + vm.store(address(slasher), bytes32(uint256(0)), bytes32(uint256(uint160(address(vault))))); + middleware.registerVault(address(vault)); + + vm.startPrank(operator); + vault.deposit(operator, OPERATOR_STAKE); + + vm.startPrank(owner); + vm.warp(START_TIME + SLASHING_WINDOW + 1); + uint48 currentEpoch = middleware.getCurrentEpoch(); + uint256 totalStake = middleware.getTotalStake(currentEpoch); + + assertEq(totalStake, OPERATOR_STAKE); + vm.stopPrank(); + } + + function testGetTotalStakeCached() public { + _registerOperatorToNetwork(operator, address(vault), false, false); + _registerVaultToNetwork(address(vault), false, 0); + + vm.startPrank(owner); + middleware.registerOperator(operator, OPERATOR_KEY); + vault.setSlasher(address(slasher)); + vm.store(address(slasher), bytes32(uint256(0)), bytes32(uint256(uint160(address(vault))))); + middleware.registerVault(address(vault)); + + vm.startPrank(operator); + vault.deposit(operator, OPERATOR_STAKE); + + vm.startPrank(owner); + vm.warp(START_TIME + SLASHING_WINDOW + 1); //We need this otherwise underflow in the first IF + uint48 currentEpoch = middleware.getCurrentEpoch(); + uint256 totalStakeCached = middleware.calcAndCacheStakes(currentEpoch); + + uint256 totalStake = middleware.getTotalStake(currentEpoch); + + assertEq(totalStake, totalStakeCached); + assertEq(totalStake, OPERATOR_STAKE); + vm.stopPrank(); + } + + function testGetTotalStakeEpochTooOld() public { + _registerOperatorToNetwork(operator, address(vault), false, false); + _registerVaultToNetwork(address(vault), false, 0); + + vm.startPrank(owner); + middleware.registerOperator(operator, OPERATOR_KEY); + vault.setSlasher(address(slasher)); + vm.store(address(slasher), bytes32(uint256(0)), bytes32(uint256(uint160(address(vault))))); + middleware.registerVault(address(vault)); + vm.warp(START_TIME + SLASHING_WINDOW + 1); + uint48 currentEpoch = middleware.getCurrentEpoch(); + vm.warp(SLASHING_WINDOW * 2 + 1); + vm.expectRevert(Middleware.Middleware__TooOldEpoch.selector); + middleware.getTotalStake(currentEpoch); + vm.stopPrank(); + } + + function testGetTotalStakeEpochInvalid() public { + _registerOperatorToNetwork(operator, address(vault), false, false); + _registerVaultToNetwork(address(vault), false, 0); + + vm.startPrank(owner); + middleware.registerOperator(operator, OPERATOR_KEY); + vault.setSlasher(address(slasher)); + vm.store(address(slasher), bytes32(uint256(0)), bytes32(uint256(uint160(address(vault))))); + middleware.registerVault(address(vault)); + vm.warp(START_TIME + SLASHING_WINDOW + 1); + uint48 currentEpoch = middleware.getCurrentEpoch(); + vm.warp(START_TIME + SLASHING_WINDOW - 1); + vm.expectRevert(Middleware.Middleware__InvalidEpoch.selector); + middleware.getTotalStake(currentEpoch + 1); + vm.stopPrank(); + } + + function testGetTotalStakeButOperatorNotActive() public { + _registerOperatorToNetwork(operator, address(vault), false, false); + _registerVaultToNetwork(address(vault), false, 0); + + vm.startPrank(owner); + middleware.registerOperator(operator, OPERATOR_KEY); + vault.setSlasher(address(slasher)); + vm.store(address(slasher), bytes32(uint256(0)), bytes32(uint256(uint160(address(vault))))); + middleware.registerVault(address(vault)); + middleware.pauseOperator(operator); + vm.warp(START_TIME + SLASHING_WINDOW + 1); + uint48 currentEpoch = middleware.getCurrentEpoch(); + uint256 totalStake = middleware.getTotalStake(currentEpoch); + assertEq(totalStake, 0); + vm.stopPrank(); + } + // ************************************************************************************************ + // * GET VALIDATOR SET + // ************************************************************************************************ + + function testGetValidatorSet() public { + _registerOperatorToNetwork(operator, address(vault), false, false); + _registerVaultToNetwork(address(vault), false, 0); + + vm.startPrank(owner); + middleware.registerOperator(operator, OPERATOR_KEY); + vault.setSlasher(address(slasher)); + vm.store(address(slasher), bytes32(uint256(0)), bytes32(uint256(uint160(address(vault))))); + middleware.registerVault(address(vault)); + + vm.startPrank(operator); + vault.deposit(operator, OPERATOR_STAKE); + + vm.startPrank(owner); + uint48 currentEpoch = middleware.getCurrentEpoch(); + Middleware.ValidatorData[] memory validators = middleware.getValidatorSet(currentEpoch); + + assertEq(validators.length, 1); + assertEq(validators[0].key, OPERATOR_KEY); + assertEq(validators[0].stake, OPERATOR_STAKE); + vm.stopPrank(); + } + + function testGetValidatorSetButOperatorNotActive() public { + _registerOperatorToNetwork(operator, address(vault), false, false); + _registerVaultToNetwork(address(vault), false, 0); + + vm.startPrank(owner); + middleware.registerOperator(operator, OPERATOR_KEY); + vault.setSlasher(address(slasher)); + vm.store(address(slasher), bytes32(uint256(0)), bytes32(uint256(uint160(address(vault))))); + middleware.registerVault(address(vault)); + middleware.pauseOperator(operator); + vm.warp(START_TIME + SLASHING_WINDOW + 1); + uint48 currentEpoch = middleware.getCurrentEpoch(); + Middleware.ValidatorData[] memory validators = middleware.getValidatorSet(currentEpoch); + assertEq(validators.length, 0); + + vm.stopPrank(); + } + + function testGetValidatorSetButOperatorHasNullKey() public { + _registerOperatorToNetwork(operator, address(vault), false, false); + _registerVaultToNetwork(address(vault), false, 0); + + vm.startPrank(owner); + middleware.registerOperator(operator, bytes32(0)); + vault.setSlasher(address(slasher)); + vm.store(address(slasher), bytes32(uint256(0)), bytes32(uint256(uint160(address(vault))))); + middleware.registerVault(address(vault)); + vm.warp(START_TIME + SLASHING_WINDOW + 1); + uint48 currentEpoch = middleware.getCurrentEpoch(); + Middleware.ValidatorData[] memory validators = middleware.getValidatorSet(currentEpoch); + assertEq(validators.length, 0); + + vm.stopPrank(); + } + + // ************************************************************************************************ + // * GET VALIDATOR SET + // ************************************************************************************************ + + function testSlash() public { + _registerOperatorToNetwork(operator, address(vault), false, false); + _registerVaultToNetwork(address(vault), false, 0); + + vm.startPrank(owner); + middleware.registerOperator(operator, OPERATOR_KEY); + vault.setSlasher(address(slasher)); + vm.store(address(slasher), bytes32(uint256(0)), bytes32(uint256(uint160(address(vault))))); + middleware.registerVault(address(vault)); + + vm.startPrank(operator); + vault.deposit(operator, OPERATOR_STAKE); + + vm.startPrank(owner); + vm.warp(START_TIME + SLASHING_WINDOW + 1); + uint48 currentEpoch = middleware.getCurrentEpoch(); + uint256 totalStakeCached = middleware.calcAndCacheStakes(currentEpoch); + + uint256 slashAmount = OPERATOR_STAKE / 2; + middleware.slash(currentEpoch, operator, slashAmount); + + vm.warp(SLASHING_WINDOW * 2 + 1); + currentEpoch = middleware.getCurrentEpoch(); + uint256 totalStake = middleware.getTotalStake(currentEpoch); + assertEq(totalStake, totalStakeCached - slashAmount); + vm.stopPrank(); + } + + function testSlashUnauthorized() public { + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, address(this))); + middleware.slash(0, operator, 0); + } + + function testSlashEpochTooOld() public { + vm.startPrank(owner); + uint48 currentEpoch = middleware.getCurrentEpoch(); + vm.warp(SLASHING_WINDOW * 2 + 1); + vm.expectRevert(Middleware.Middleware__TooOldEpoch.selector); + middleware.slash(currentEpoch, operator, OPERATOR_STAKE); + vm.stopPrank(); + } + + function testSlashTooBigSlashAmount() public { + _registerOperatorToNetwork(operator, address(vault), false, false); + _registerVaultToNetwork(address(vault), false, 0); + + vm.startPrank(owner); + middleware.registerOperator(operator, OPERATOR_KEY); + vault.setSlasher(address(slasher)); + vm.store(address(slasher), bytes32(uint256(0)), bytes32(uint256(uint160(address(vault))))); + middleware.registerVault(address(vault)); + vm.warp(START_TIME + SLASHING_WINDOW + 1); + uint48 currentEpoch = middleware.getCurrentEpoch(); + uint256 totalStakeCached = middleware.calcAndCacheStakes(currentEpoch); + + uint256 slashAmount = OPERATOR_STAKE * 2; + vm.expectRevert(Middleware.Middleware__TooBigSlashAmount.selector); + middleware.slash(currentEpoch, operator, slashAmount); + + uint256 totalStake = middleware.getTotalStake(currentEpoch); + assertEq(totalStake, totalStakeCached); + vm.stopPrank(); + } + + function testSlashWithNoActiveVault() public { + _registerOperatorToNetwork(operator, address(vault), false, false); + _registerVaultToNetwork(address(vault), false, 0); + + vm.startPrank(owner); + middleware.registerOperator(operator, OPERATOR_KEY); + vault.setSlasher(address(slasher)); + vm.store(address(slasher), bytes32(uint256(0)), bytes32(uint256(uint160(address(vault))))); + middleware.registerVault(address(vault)); + + //Creating another vault to have stake on another vault + VaultMock vault2 = new VaultMock(delegatorFactory, slasherFactory, vaultFactory); + DelegatorMock delegator2 = new DelegatorMock( + address(registry), + vaultFactory, + address(operatorVaultOptInServiceMock), + address(operatorNetworkOptInServiceMock), + delegatorFactory, + 0 + ); + _registerOperatorToNetwork(operator, address(vault2), false, false); + _registerVaultToNetwork(address(vault2), false, 0); + + vm.startPrank(owner); + vault2.setDelegator(address(delegator2)); + vm.store(address(delegator2), bytes32(uint256(0)), bytes32(uint256(uint160(address(vault))))); + + vault2.setSlasher(address(slasher)); + vm.store(address(slasher), bytes32(uint256(0)), bytes32(uint256(uint160(address(vault2))))); + + middleware.registerVault(address(vault2)); + + vault.setSlasher(address(slasher)); + vm.store(address(slasher), bytes32(uint256(0)), bytes32(uint256(uint160(address(vault))))); + + vm.startPrank(operator); + vault.deposit(operator, OPERATOR_STAKE); + + vm.startPrank(owner); + middleware.pauseVault(address(vault)); + vm.warp(START_TIME + SLASHING_WINDOW + 1); + uint48 currentEpoch = middleware.getCurrentEpoch(); + + uint256 slashAmount = OPERATOR_STAKE / 2; + middleware.slash(currentEpoch, operator, slashAmount); + + vm.warp(SLASHING_WINDOW * 2 + 1); + currentEpoch = middleware.getCurrentEpoch(); + uint256 totalStake = middleware.getTotalStake(currentEpoch); + assertEq(totalStake, OPERATOR_STAKE / 2); //Because it slashes the operator everywhere, but the operator has stake only in vault2, since the first vault is paused + vm.stopPrank(); + } + + function testSlashWithVetoSlasher() public { + _registerOperatorToNetwork(operator, address(vault), false, false); + _registerVaultToNetwork(address(vault), false, 0); + + vm.startPrank(owner); + middleware.registerOperator(operator, OPERATOR_KEY); + vault.setSlasher(address(vetoSlasher)); + vm.store(address(vetoSlasher), bytes32(uint256(0)), bytes32(uint256(uint160(address(vault))))); + middleware.registerVault(address(vault)); + + vm.startPrank(operator); + vault.deposit(operator, OPERATOR_STAKE); + + vm.startPrank(owner); + vm.warp(START_TIME + SLASHING_WINDOW + 1); + uint48 currentEpoch = middleware.getCurrentEpoch(); + + uint256 slashAmount = OPERATOR_STAKE / 2; + middleware.slash(currentEpoch, operator, slashAmount); + + vm.warp(SLASHING_WINDOW * 2 + 1); + currentEpoch = middleware.getCurrentEpoch(); + uint256 totalStake = middleware.getTotalStake(currentEpoch); + assertEq(totalStake, OPERATOR_STAKE); + vm.stopPrank(); + } + + function testSlashWithSlasherWrongType() public { + _registerOperatorToNetwork(operator, address(vault), false, false); + _registerVaultToNetwork(address(vault), false, 0); + + vm.startPrank(owner); + middleware.registerOperator(operator, OPERATOR_KEY); + vault.setSlasher(address(slasherWithBadType)); + vm.store(address(slasherWithBadType), bytes32(uint256(0)), bytes32(uint256(uint160(address(vault))))); + middleware.registerVault(address(vault)); + + vm.startPrank(operator); + vault.deposit(operator, OPERATOR_STAKE); + + vm.startPrank(owner); + vm.warp(START_TIME + SLASHING_WINDOW + 1); + uint48 currentEpoch = middleware.getCurrentEpoch(); + + uint256 slashAmount = OPERATOR_STAKE / 2; + vm.expectRevert(Middleware.Middleware__UnknownSlasherType.selector); + middleware.slash(currentEpoch, operator, slashAmount); + + vm.stopPrank(); + } + + // ************************************************************************************************ + // * CALC AND CACHE STAKES + // ************************************************************************************************ + + function testCalcAndCacheStakes() public { + _registerOperatorToNetwork(operator, address(vault), false, false); + _registerVaultToNetwork(address(vault), false, 0); + + vm.startPrank(owner); + middleware.registerOperator(operator, OPERATOR_KEY); + vault.setSlasher(address(slasher)); + vm.store(address(slasher), bytes32(uint256(0)), bytes32(uint256(uint160(address(vault))))); + middleware.registerVault(address(vault)); + + vm.startPrank(operator); + vault.deposit(operator, OPERATOR_STAKE); + + vm.startPrank(owner); + vm.warp(START_TIME + SLASHING_WINDOW + 1); + uint48 currentEpoch = middleware.getCurrentEpoch(); + uint256 totalStake = middleware.calcAndCacheStakes(currentEpoch); + + assertEq(totalStake, OPERATOR_STAKE); + vm.stopPrank(); + } + + function testCalcAndCacheStakesEpochTooOld() public { + _registerOperatorToNetwork(operator, address(vault), false, false); + _registerVaultToNetwork(address(vault), false, 0); + + vm.startPrank(owner); + middleware.registerOperator(operator, OPERATOR_KEY); + vault.setSlasher(address(slasher)); + vm.store(address(slasher), bytes32(uint256(0)), bytes32(uint256(uint160(address(vault))))); + middleware.registerVault(address(vault)); + + vm.warp(START_TIME + SLASHING_WINDOW + 1); + uint48 currentEpoch = middleware.getCurrentEpoch(); + vm.warp(SLASHING_WINDOW * 2 + 1); + vm.expectRevert(Middleware.Middleware__TooOldEpoch.selector); + middleware.calcAndCacheStakes(currentEpoch); + vm.stopPrank(); + } + + function testCalcAndCacheStakesEpochInvalid() public { + _registerOperatorToNetwork(operator, address(vault), false, false); + _registerVaultToNetwork(address(vault), false, 0); + + vm.startPrank(owner); + middleware.registerOperator(operator, OPERATOR_KEY); + vault.setSlasher(address(slasher)); + vm.store(address(slasher), bytes32(uint256(0)), bytes32(uint256(uint160(address(vault))))); + + middleware.registerVault(address(vault)); + vm.warp(START_TIME + SLASHING_WINDOW + 1); + uint48 currentEpoch = middleware.getCurrentEpoch(); + vm.warp(START_TIME + SLASHING_WINDOW - 1); + vm.expectRevert(Middleware.Middleware__InvalidEpoch.selector); + middleware.calcAndCacheStakes(currentEpoch + 1); + vm.stopPrank(); + } + + function testCalcAndCacheStakesButOperatorNotActive() public { + _registerOperatorToNetwork(operator, address(vault), false, false); + _registerVaultToNetwork(address(vault), false, 0); + + vm.startPrank(owner); + middleware.registerOperator(operator, OPERATOR_KEY); + vault.setSlasher(address(slasher)); + vm.store(address(slasher), bytes32(uint256(0)), bytes32(uint256(uint160(address(vault))))); + + middleware.registerVault(address(vault)); + middleware.pauseOperator(operator); + vm.warp(START_TIME + SLASHING_WINDOW + 1); + uint48 currentEpoch = middleware.getCurrentEpoch(); + uint256 totalStake = middleware.calcAndCacheStakes(currentEpoch); + assertEq(totalStake, 0); + vm.stopPrank(); + } + + // ************************************************************************************************ + // * SIMPLE KEY REGISTRY 32 + // ************************************************************************************************ + + function testSimpleKeyRegistryHistoricalKeyLookup() public { + uint48 timestamp1 = uint48(block.timestamp); + _registerOperatorToNetwork(operator, address(vault), false, false); + + vm.startPrank(owner); + middleware.registerOperator(operator, OPERATOR_KEY); + vm.warp(block.timestamp + 1 days); + + assertEq(middleware.getOperatorKeyAt(operator, timestamp1), OPERATOR_KEY); + assertEq(middleware.getCurrentOperatorKey(operator), OPERATOR_KEY); + vm.stopPrank(); + } + + function testSimpleKeyRegistryEmptyStates() public view { + assertEq(middleware.getCurrentOperatorKey(operator), bytes32(0)); + assertEq(middleware.getOperatorByKey(OPERATOR_KEY), address(0)); + assertEq(middleware.getOperatorKeyAt(operator, uint48(block.timestamp) + 10 days), bytes32(0)); + } +}