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));
+ }
+}