From 8df573c8059506d823dcf1076c346b01c0955528 Mon Sep 17 00:00:00 2001 From: Gregory Sobol Date: Fri, 8 Nov 2024 23:35:06 +0100 Subject: [PATCH] feat(ethexe): symbiotic middleware (#4318) --- .gitmodules | 3 + ethexe/contracts/foundry.toml | 1 + ethexe/contracts/lib/symbiotic-core | 1 + ethexe/contracts/src/Middleware.sol | 243 +++++++++++ .../src/libraries/MapWithTimeData.sol | 75 ++++ ethexe/contracts/test/Middleware.t.sol | 377 ++++++++++++++++++ 6 files changed, 700 insertions(+) create mode 160000 ethexe/contracts/lib/symbiotic-core create mode 100644 ethexe/contracts/src/Middleware.sol create mode 100644 ethexe/contracts/src/libraries/MapWithTimeData.sol create mode 100644 ethexe/contracts/test/Middleware.t.sol diff --git a/.gitmodules b/.gitmodules index c389af1310c..7b68e8dbc8c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -10,3 +10,6 @@ [submodule "ethexe/contracts/lib/openzeppelin-contracts-upgradeable"] path = ethexe/contracts/lib/openzeppelin-contracts-upgradeable url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable +[submodule "ethexe/contracts/lib/symbiotic-core"] + path = ethexe/contracts/lib/symbiotic-core + url = git@github.com:grishasobol/symbiotic-core.git diff --git a/ethexe/contracts/foundry.toml b/ethexe/contracts/foundry.toml index bb0029c8438..f5cdbadb256 100644 --- a/ethexe/contracts/foundry.toml +++ b/ethexe/contracts/foundry.toml @@ -12,6 +12,7 @@ extra_output = ["storageLayout"] ignored_warnings_from = [ # Warning (3628): This contract has a payable fallback function, but no receive ether function "src/MirrorProxy.sol", + "lib/", ] # Enable new EVM codegen via_ir = true diff --git a/ethexe/contracts/lib/symbiotic-core b/ethexe/contracts/lib/symbiotic-core new file mode 160000 index 00000000000..9cfc73f74b5 --- /dev/null +++ b/ethexe/contracts/lib/symbiotic-core @@ -0,0 +1 @@ +Subproject commit 9cfc73f74b568ad367284407d9b4dbca82a981eb diff --git a/ethexe/contracts/src/Middleware.sol b/ethexe/contracts/src/Middleware.sol new file mode 100644 index 00000000000..5d8f8febe09 --- /dev/null +++ b/ethexe/contracts/src/Middleware.sol @@ -0,0 +1,243 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.26; + +import {EnumerableMap} from "@openzeppelin/contracts/utils/structs/EnumerableMap.sol"; +import {Time} from "@openzeppelin/contracts/utils/types/Time.sol"; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; + +import {Subnetwork} from "symbiotic-core/src/contracts/libraries/Subnetwork.sol"; +import {IVault} from "symbiotic-core/src/interfaces/vault/IVault.sol"; +import {IRegistry} from "symbiotic-core/src/interfaces/common/IRegistry.sol"; +import {IEntity} from "symbiotic-core/src/interfaces/common/IEntity.sol"; +import {IBaseDelegator} from "symbiotic-core/src/interfaces/delegator/IBaseDelegator.sol"; +import {INetworkRegistry} from "symbiotic-core/src/interfaces/INetworkRegistry.sol"; +import {IOptInService} from "symbiotic-core/src/interfaces/service/IOptInService.sol"; + +import {MapWithTimeData} from "./libraries/MapWithTimeData.sol"; + +// TODO: support slashing +// TODO: use camelCase for immutable variables +// TODO: implement election logic +// TODO: implement forced operators removal +// TODO: implement forced vaults removal +// TODO: implement rewards distribution +contract Middleware { + using EnumerableMap for EnumerableMap.AddressToUintMap; + using MapWithTimeData for EnumerableMap.AddressToUintMap; + using Subnetwork for address; + + error ZeroVaultAddress(); + error NotKnownVault(); + error VaultWrongEpochDuration(); + error UnknownCollateral(); + error OperatorGracePeriodNotPassed(); + error VaultGracePeriodNotPassed(); + error NotVaultOwner(); + error IncorrectTimestamp(); + error OperatorDoesNotExist(); + error OperatorDoesNotOptIn(); + + uint96 public constant NETWORK_IDENTIFIER = 0; + + uint48 public immutable ERA_DURATION; + uint48 public immutable GENESIS_TIMESTAMP; + uint48 public immutable OPERATOR_GRACE_PERIOD; + uint48 public immutable VAULT_GRACE_PERIOD; + uint48 public immutable VAULT_MIN_EPOCH_DURATION; + address public immutable VAULT_FACTORY; + address public immutable DELEGATOR_FACTORY; + address public immutable SLASHER_FACTORY; + address public immutable OPERATOR_REGISTRY; + address public immutable NETWORK_OPT_IN; + address public immutable COLLATERAL; + bytes32 public immutable SUBNETWORK; + + EnumerableMap.AddressToUintMap private operators; + EnumerableMap.AddressToUintMap private vaults; + + constructor( + uint48 eraDuration, + address vaultFactory, + address delegatorFactory, + address slasherFactory, + address operatorRegistry, + address networkRegistry, + address networkOptIn, + address collateral + ) { + ERA_DURATION = eraDuration; + GENESIS_TIMESTAMP = Time.timestamp(); + OPERATOR_GRACE_PERIOD = 2 * eraDuration; + VAULT_GRACE_PERIOD = 2 * eraDuration; + VAULT_MIN_EPOCH_DURATION = 2 * eraDuration; + VAULT_FACTORY = vaultFactory; + DELEGATOR_FACTORY = delegatorFactory; + SLASHER_FACTORY = slasherFactory; + OPERATOR_REGISTRY = operatorRegistry; + NETWORK_OPT_IN = networkOptIn; + COLLATERAL = collateral; + SUBNETWORK = address(this).subnetwork(NETWORK_IDENTIFIER); + + INetworkRegistry(networkRegistry).registerNetwork(); + } + + // TODO: Check that total stake is big enough + function registerOperator() external { + if (!IRegistry(OPERATOR_REGISTRY).isEntity(msg.sender)) { + revert OperatorDoesNotExist(); + } + if (!IOptInService(NETWORK_OPT_IN).isOptedIn(msg.sender, address(this))) { + revert OperatorDoesNotOptIn(); + } + operators.append(msg.sender, 0); + } + + function disableOperator() external { + operators.disable(msg.sender); + } + + function enableOperator() external { + operators.enable(msg.sender); + } + + function unregisterOperator(address operator) external { + (, uint48 disabledTime) = operators.getTimes(operator); + + if (disabledTime == 0 || Time.timestamp() < disabledTime + OPERATOR_GRACE_PERIOD) { + revert OperatorGracePeriodNotPassed(); + } + + operators.remove(operator); + } + + // TODO: check vault has enough stake + // TODO: support and check slasher + function registerVault(address vault) external { + if (vault == address(0)) { + revert ZeroVaultAddress(); + } + + if (!IRegistry(VAULT_FACTORY).isEntity(vault)) { + revert NotKnownVault(); + } + + if (IVault(vault).epochDuration() < VAULT_MIN_EPOCH_DURATION) { + revert VaultWrongEpochDuration(); + } + + if (IVault(vault).collateral() != COLLATERAL) { + revert UnknownCollateral(); + } + + IBaseDelegator delegator = IBaseDelegator(IVault(vault).delegator()); + if (delegator.maxNetworkLimit(SUBNETWORK) != type(uint256).max) { + delegator.setMaxNetworkLimit(NETWORK_IDENTIFIER, type(uint256).max); + } + + vaults.append(vault, uint160(msg.sender)); + } + + function disableVault(address vault) external { + address vault_owner = address(vaults.getPinnedData(vault)); + + if (vault_owner != msg.sender) { + revert NotVaultOwner(); + } + + vaults.disable(vault); + } + + function enableVault(address vault) external { + address vault_owner = address(vaults.getPinnedData(vault)); + + if (vault_owner != msg.sender) { + revert NotVaultOwner(); + } + + vaults.enable(vault); + } + + function unregisterVault(address vault) external { + (, uint48 disabledTime) = vaults.getTimes(vault); + + if (disabledTime == 0 || Time.timestamp() < disabledTime + VAULT_GRACE_PERIOD) { + revert VaultGracePeriodNotPassed(); + } + + vaults.remove(vault); + } + + function getOperatorStakeAt(address operator, uint48 ts) + external + view + _validTimestamp(ts) + returns (uint256 stake) + { + (uint48 enabledTime, uint48 disabledTime) = operators.getTimes(operator); + if (!_wasActiveAt(enabledTime, disabledTime, ts)) { + return 0; + } + + stake = _collectOperatorStakeFromVaultsAt(operator, ts); + } + + function getActiveOperatorsStakeAt(uint48 ts) + public + view + _validTimestamp(ts) + returns (address[] memory activeOperators, uint256[] memory stakes) + { + activeOperators = new address[](operators.length()); + stakes = new uint256[](operators.length()); + + uint256 operatorIdx = 0; + + for (uint256 i; i < operators.length(); ++i) { + (address operator, uint48 enabled, uint48 disabled) = operators.atWithTimes(i); + + if (!_wasActiveAt(enabled, disabled, ts)) { + continue; + } + + activeOperators[operatorIdx] = operator; + stakes[operatorIdx] = _collectOperatorStakeFromVaultsAt(operator, ts); + operatorIdx += 1; + } + + assembly { + mstore(activeOperators, operatorIdx) + mstore(stakes, operatorIdx) + } + } + + function _collectOperatorStakeFromVaultsAt(address operator, uint48 ts) private view returns (uint256 stake) { + for (uint256 i; i < vaults.length(); ++i) { + (address vault, uint48 vaultEnabledTime, uint48 vaultDisabledTime) = vaults.atWithTimes(i); + + if (!_wasActiveAt(vaultEnabledTime, vaultDisabledTime, ts)) { + continue; + } + + stake += IBaseDelegator(IVault(vault).delegator()).stakeAt(SUBNETWORK, operator, ts, new bytes(0)); + } + } + + function _wasActiveAt(uint48 enabledTime, uint48 disabledTime, uint48 ts) private pure returns (bool) { + return enabledTime != 0 && enabledTime <= ts && (disabledTime == 0 || disabledTime >= ts); + } + + // Timestamp must be always in the past, but not too far, + // so that some operators or vaults can be already unregistered. + modifier _validTimestamp(uint48 ts) { + if (ts >= Time.timestamp()) { + revert IncorrectTimestamp(); + } + + uint48 gracePeriod = OPERATOR_GRACE_PERIOD < VAULT_GRACE_PERIOD ? OPERATOR_GRACE_PERIOD : VAULT_GRACE_PERIOD; + if (ts + gracePeriod <= Time.timestamp()) { + revert IncorrectTimestamp(); + } + + _; + } +} diff --git a/ethexe/contracts/src/libraries/MapWithTimeData.sol b/ethexe/contracts/src/libraries/MapWithTimeData.sol new file mode 100644 index 00000000000..dc7a527185e --- /dev/null +++ b/ethexe/contracts/src/libraries/MapWithTimeData.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.25; + +import {Checkpoints} from "@openzeppelin/contracts/utils/structs/Checkpoints.sol"; +import {Time} from "@openzeppelin/contracts/utils/types/Time.sol"; +import {EnumerableMap} from "@openzeppelin/contracts/utils/structs/EnumerableMap.sol"; + +library MapWithTimeData { + using EnumerableMap for EnumerableMap.AddressToUintMap; + + error AlreadyAdded(); + error NotEnabled(); + error AlreadyEnabled(); + + function toInner(uint256 value) private pure returns (uint48, uint48, uint160) { + // casting to uint48 will truncate the value to 48 bits, so it's safe for this case + return (uint48(value), uint48(value >> 48), uint160(value >> 96)); + } + + function toValue(uint48 enabledTime, uint48 disabledTime, uint160 data) private pure returns (uint256) { + return uint256(enabledTime) | (uint256(disabledTime) << 48) | (uint256(data) << 96); + } + + function append(EnumerableMap.AddressToUintMap storage self, address addr, uint160 data) internal { + if (!self.set(addr, toValue(Time.timestamp(), 0, data))) { + revert AlreadyAdded(); + } + } + + function enable(EnumerableMap.AddressToUintMap storage self, address addr) internal { + (uint48 enabledTime, uint48 disabledTime, uint160 data) = toInner(self.get(addr)); + + if (enabledTime != 0 && disabledTime == 0) { + revert AlreadyEnabled(); + } + + self.set(addr, toValue(Time.timestamp(), 0, data)); + } + + function disable(EnumerableMap.AddressToUintMap storage self, address addr) internal { + (uint48 enabledTime, uint48 disabledTime, uint160 data) = toInner(self.get(addr)); + + if (enabledTime == 0 || disabledTime != 0) { + revert NotEnabled(); + } + + self.set(addr, toValue(enabledTime, Time.timestamp(), data)); + } + + function atWithTimes(EnumerableMap.AddressToUintMap storage self, uint256 idx) + internal + view + returns (address key, uint48 enabledTime, uint48 disabledTime) + { + uint256 value; + (key, value) = self.at(idx); + (enabledTime, disabledTime,) = toInner(value); + } + + function getTimes(EnumerableMap.AddressToUintMap storage self, address addr) + internal + view + returns (uint48 enabledTime, uint48 disabledTime) + { + (enabledTime, disabledTime,) = toInner(self.get(addr)); + } + + function getPinnedData(EnumerableMap.AddressToUintMap storage self, address addr) + internal + view + returns (uint160 data) + { + (,, data) = toInner(self.get(addr)); + } +} diff --git a/ethexe/contracts/test/Middleware.t.sol b/ethexe/contracts/test/Middleware.t.sol new file mode 100644 index 00000000000..ea1ba74559a --- /dev/null +++ b/ethexe/contracts/test/Middleware.t.sol @@ -0,0 +1,377 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.26; + +import {EnumerableMap} from "@openzeppelin/contracts/utils/structs/EnumerableMap.sol"; +import {Time} from "@openzeppelin/contracts/utils/types/Time.sol"; +import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; +import {Upgrades} from "openzeppelin-foundry-upgrades/Upgrades.sol"; + +import {Test, console} from "forge-std/Test.sol"; +import {NetworkRegistry} from "symbiotic-core/src/contracts/NetworkRegistry.sol"; +import {POCBaseTest} from "symbiotic-core/test/POCBase.t.sol"; +import {IVaultConfigurator} from "symbiotic-core/src/interfaces/IVaultConfigurator.sol"; +import {IVault} from "symbiotic-core/src/interfaces/vault/IVault.sol"; +import {IBaseDelegator} from "symbiotic-core/src/interfaces/delegator/IBaseDelegator.sol"; +import {IOperatorSpecificDelegator} from "symbiotic-core/src/interfaces/delegator/IOperatorSpecificDelegator.sol"; + +import {Middleware} from "../src/Middleware.sol"; +import {WrappedVara} from "../src/WrappedVara.sol"; +import {MapWithTimeData} from "../src/libraries/MapWithTimeData.sol"; + +contract MiddlewareTest is Test { + using MessageHashUtils for address; + + uint48 eraDuration = 1000; + address public owner; + POCBaseTest public sym; + Middleware public middleware; + WrappedVara public wrappedVara; + + function setUp() public { + sym = new POCBaseTest(); + sym.setUp(); + + owner = address(this); + + wrappedVara = WrappedVara( + Upgrades.deployTransparentProxy("WrappedVara.sol", owner, abi.encodeCall(WrappedVara.initialize, (owner))) + ); + + wrappedVara.mint(owner, 1_000_000); + + middleware = new Middleware( + eraDuration, + address(sym.vaultFactory()), + address(sym.delegatorFactory()), + address(sym.slasherFactory()), + address(sym.operatorRegistry()), + address(sym.networkRegistry()), + address(sym.operatorNetworkOptInService()), + address(wrappedVara) + ); + } + + function test_constructor() public view { + assertEq(uint256(middleware.ERA_DURATION()), eraDuration); + assertEq(uint256(middleware.GENESIS_TIMESTAMP()), Time.timestamp()); + assertEq(uint256(middleware.OPERATOR_GRACE_PERIOD()), eraDuration * 2); + assertEq(uint256(middleware.VAULT_GRACE_PERIOD()), eraDuration * 2); + assertEq(uint256(middleware.VAULT_MIN_EPOCH_DURATION()), eraDuration * 2); + assertEq(middleware.VAULT_FACTORY(), address(sym.vaultFactory())); + assertEq(middleware.DELEGATOR_FACTORY(), address(sym.delegatorFactory())); + assertEq(middleware.SLASHER_FACTORY(), address(sym.slasherFactory())); + assertEq(middleware.OPERATOR_REGISTRY(), address(sym.operatorRegistry())); + assertEq(middleware.COLLATERAL(), address(wrappedVara)); + + sym.networkRegistry().isEntity(address(middleware)); + } + + function test_registerOperator() public { + // Register operator + vm.startPrank(address(0x1)); + sym.operatorRegistry().registerOperator(); + sym.operatorNetworkOptInService().optIn(address(middleware)); + middleware.registerOperator(); + + // Try to register operator again + vm.expectRevert(abi.encodeWithSelector(MapWithTimeData.AlreadyAdded.selector)); + middleware.registerOperator(); + + // Try to register abother operator without registering it in symbiotic + vm.startPrank(address(0x2)); + vm.expectRevert(abi.encodeWithSelector(Middleware.OperatorDoesNotExist.selector)); + middleware.registerOperator(); + + // Try to register operator without opting in network + sym.operatorRegistry().registerOperator(); + vm.expectRevert(abi.encodeWithSelector(Middleware.OperatorDoesNotOptIn.selector)); + middleware.registerOperator(); + + // Now must be possible to register operator + sym.operatorNetworkOptInService().optIn(address(middleware)); + middleware.registerOperator(); + + // Disable operator and the enable it + middleware.disableOperator(); + middleware.enableOperator(); + + // Try to enable operator again + vm.expectRevert(abi.encodeWithSelector(MapWithTimeData.AlreadyEnabled.selector)); + middleware.enableOperator(); + + // Try to disable operator twice + middleware.disableOperator(); + vm.expectRevert(abi.encodeWithSelector(MapWithTimeData.NotEnabled.selector)); + middleware.disableOperator(); + + // Try to unregister operator - failed because operator is not disabled for enough time + vm.expectRevert(abi.encodeWithSelector(Middleware.OperatorGracePeriodNotPassed.selector)); + middleware.unregisterOperator(address(0x2)); + + // Wait for grace period and unregister operator from other address + vm.startPrank(address(0x3)); + vm.warp(vm.getBlockTimestamp() + eraDuration * 2); + middleware.unregisterOperator(address(0x2)); + } + + function test_registerVault() public { + sym.operatorRegistry().registerOperator(); + address vault = _newVault(eraDuration * 2, owner); + + // Register vault + middleware.registerVault(vault); + + // Try to register vault with zero address + vm.expectRevert(abi.encodeWithSelector(Middleware.ZeroVaultAddress.selector)); + middleware.registerVault(address(0x0)); + + // Try to register unknown vault + vm.expectRevert(abi.encodeWithSelector(Middleware.NotKnownVault.selector)); + middleware.registerVault(address(0x1)); + + // Try to register vault with wrong epoch duration + address vault2 = _newVault(eraDuration, owner); + vm.expectRevert(abi.encodeWithSelector(Middleware.VaultWrongEpochDuration.selector)); + middleware.registerVault(vault2); + + // Try to register vault with unknown collateral + address vault3 = address(sym.vault1()); + vm.expectRevert(abi.encodeWithSelector(Middleware.UnknownCollateral.selector)); + middleware.registerVault(vault3); + + // Try to enable vault once more + vm.expectRevert(abi.encodeWithSelector(MapWithTimeData.AlreadyEnabled.selector)); + middleware.enableVault(vault); + + // Try to disable vault twice + middleware.disableVault(vault); + vm.expectRevert(abi.encodeWithSelector(MapWithTimeData.NotEnabled.selector)); + middleware.disableVault(vault); + + { + vm.startPrank(address(0x1)); + + // Try to enable vault not from owner + vm.expectRevert(abi.encodeWithSelector(Middleware.NotVaultOwner.selector)); + middleware.enableVault(vault); + + // Try to disable vault not from owner + vm.expectRevert(abi.encodeWithSelector(Middleware.NotVaultOwner.selector)); + middleware.disableVault(vault); + + vm.stopPrank(); + } + + // Try to unregister vault - failed because vault is not disabled for enough time + vm.expectRevert(abi.encodeWithSelector(Middleware.VaultGracePeriodNotPassed.selector)); + middleware.unregisterVault(vault); + + // Wait for grace period and unregister vault + vm.warp(vm.getBlockTimestamp() + eraDuration * 2); + middleware.unregisterVault(vault); + + // Register vault again, disable and unregister it not by owner + middleware.registerVault(vault); + middleware.disableVault(vault); + vm.startPrank(address(0x1)); + vm.warp(vm.getBlockTimestamp() + eraDuration * 2); + middleware.unregisterVault(vault); + vm.stopPrank(); + + // Try to enable unknown vault + vm.expectRevert(abi.encodeWithSelector(EnumerableMap.EnumerableMapNonexistentKey.selector, address(0x1))); + middleware.enableVault(address(0x1)); + + // Try to disable unknown vault + vm.expectRevert(abi.encodeWithSelector(EnumerableMap.EnumerableMapNonexistentKey.selector, address(0x1))); + middleware.disableVault(address(0x1)); + + // Try to unregister unknown vault + vm.expectRevert(abi.encodeWithSelector(EnumerableMap.EnumerableMapNonexistentKey.selector, address(0x1))); + middleware.unregisterVault(address(0x1)); + } + + function test_operatorStake() public { + address operator1 = address(0x1); + address operator2 = address(0x2); + + _registerOperator(operator1); + _registerOperator(operator2); + + address vault1 = _createVaultForOperator(operator1); + address vault2 = _createVaultForOperator(operator2); + + uint256 stake1 = 1_000; + uint256 stake2 = 2_000; + uint256 stake3 = 3_000; + + _depositFromInVault(owner, vault1, stake1); + _depositFromInVault(owner, vault2, stake2); + + { + // Check operator stake after depositing + uint48 ts = uint48(vm.getBlockTimestamp()); + vm.warp(vm.getBlockTimestamp() + 1); + assertEq(middleware.getOperatorStakeAt(operator1, ts), stake1); + assertEq(middleware.getOperatorStakeAt(operator2, ts), stake2); + (address[] memory active_operators, uint256[] memory stakes) = middleware.getActiveOperatorsStakeAt(ts); + assertEq(active_operators.length, 2); + assertEq(stakes.length, 2); + assertEq(active_operators[0], operator1); + assertEq(active_operators[1], operator2); + assertEq(stakes[0], stake1); + assertEq(stakes[1], stake2); + } + + // Create one more vault for operator1 + address vault3 = _createVaultForOperator(operator1); + + { + // Check that vault creation doesn't affect operator stake without deposit + uint48 ts = uint48(vm.getBlockTimestamp()); + vm.warp(vm.getBlockTimestamp() + 1); + assertEq(middleware.getOperatorStakeAt(operator1, ts), stake1); + } + + { + // Check after depositing to new vault + _depositFromInVault(owner, vault3, stake3); + uint48 ts = uint48(vm.getBlockTimestamp()); + vm.warp(vm.getBlockTimestamp() + 1); + assertEq(middleware.getOperatorStakeAt(operator1, ts), stake1 + stake3); + } + + { + // Disable vault1 and check operator1 stake + // Disable is not immediate, so we need to check for the next block ts + _disableVault(operator1, vault1); + uint48 ts = uint48(vm.getBlockTimestamp()) + 1; + vm.warp(vm.getBlockTimestamp() + 2); + assertEq(middleware.getOperatorStakeAt(operator1, ts), stake3); + } + + { + // Disable operator1 and check operator1 stake is 0 + _disableOperator(operator1); + uint48 ts = uint48(vm.getBlockTimestamp()) + 1; + vm.warp(vm.getBlockTimestamp() + 2); + assertEq(middleware.getOperatorStakeAt(operator1, ts), 0); + + // Check that operator1 is not in active operators list + (address[] memory active_operators, uint256[] memory stakes) = middleware.getActiveOperatorsStakeAt(ts); + assertEq(active_operators.length, 1); + assertEq(stakes.length, 1); + assertEq(active_operators[0], operator2); + assertEq(stakes[0], stake2); + } + + // Try to get stake for current timestamp + vm.expectRevert(abi.encodeWithSelector(Middleware.IncorrectTimestamp.selector)); + middleware.getOperatorStakeAt(operator2, uint48(vm.getBlockTimestamp())); + + // Try to get stake for future timestamp + vm.expectRevert(abi.encodeWithSelector(Middleware.IncorrectTimestamp.selector)); + middleware.getOperatorStakeAt(operator2, uint48(vm.getBlockTimestamp() + 1)); + + // Try to get stake for too old timestamp + vm.warp(vm.getBlockTimestamp() + eraDuration * 2); + vm.expectRevert(abi.encodeWithSelector(Middleware.IncorrectTimestamp.selector)); + middleware.getOperatorStakeAt(operator2, uint48(vm.getBlockTimestamp())); + } + + function _disableOperator(address operator) private { + vm.startPrank(operator); + middleware.disableOperator(); + vm.stopPrank(); + } + + function _disableVault(address vault_owner, address vault) private { + vm.startPrank(vault_owner); + middleware.disableVault(vault); + vm.stopPrank(); + } + + function _depositFromInVault(address from, address vault, uint256 amount) private { + vm.startPrank(from); + wrappedVara.approve(vault, amount); + IVault(vault).deposit(from, amount); + vm.stopPrank(); + } + + function _registerOperator(address operator) private { + vm.startPrank(operator); + sym.operatorRegistry().registerOperator(); + sym.operatorNetworkOptInService().optIn(address(middleware)); + middleware.registerOperator(); + vm.stopPrank(); + } + + function _createVaultForOperator(address operator) private returns (address vault) { + // Create vault + vault = _newVault(eraDuration * 2, operator); + { + vm.startPrank(operator); + + // Register vault in middleware + middleware.registerVault(vault); + + // Operator opt-in vault + sym.operatorVaultOptInService().optIn(vault); + + // Set initial network limit + IOperatorSpecificDelegator(IVault(vault).delegator()).setNetworkLimit( + middleware.SUBNETWORK(), type(uint256).max + ); + + vm.stopPrank(); + } + } + + function _setNetworkLimit(address vault, address operator, uint256 limit) private { + vm.startPrank(address(operator)); + IOperatorSpecificDelegator(IVault(vault).delegator()).setNetworkLimit(middleware.SUBNETWORK(), limit); + vm.stopPrank(); + } + + function _newVault(uint48 epochDuration, address operator) private returns (address vault) { + address[] memory networkLimitSetRoleHolders = new address[](1); + networkLimitSetRoleHolders[0] = operator; + + (vault,,) = sym.vaultConfigurator().create( + IVaultConfigurator.InitParams({ + version: sym.vaultFactory().lastVersion(), + owner: owner, + vaultParams: abi.encode( + IVault.InitParams({ + collateral: address(wrappedVara), + burner: address(middleware), + epochDuration: epochDuration, + depositWhitelist: false, + isDepositLimit: false, + depositLimit: 0, + defaultAdminRoleHolder: owner, + depositWhitelistSetRoleHolder: owner, + depositorWhitelistRoleHolder: owner, + isDepositLimitSetRoleHolder: owner, + depositLimitSetRoleHolder: owner + }) + ), + delegatorIndex: 2, + delegatorParams: abi.encode( + IOperatorSpecificDelegator.InitParams({ + baseParams: IBaseDelegator.BaseParams({ + defaultAdminRoleHolder: operator, + hook: address(0), + hookSetRoleHolder: operator + }), + networkLimitSetRoleHolders: networkLimitSetRoleHolders, + operator: operator + }) + ), + withSlasher: false, + slasherIndex: 0, + slasherParams: bytes("") + }) + ); + } +}