From b9e86ff472fdd489a5b47d9f0679798582219721 Mon Sep 17 00:00:00 2001 From: Gregory Sobol Date: Thu, 24 Oct 2024 14:30:12 +0200 Subject: [PATCH 01/14] append symbiotic-core submodule --- .gitmodules | 3 +++ ethexe/contracts/lib/symbiotic-core | 1 + 2 files changed, 4 insertions(+) create mode 160000 ethexe/contracts/lib/symbiotic-core 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/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 From c9d30e15aa93be6dd68178935b76762976238c04 Mon Sep 17 00:00:00 2001 From: Gregory Sobol Date: Fri, 25 Oct 2024 13:04:09 +0200 Subject: [PATCH 02/14] append initial middleware and simple test --- ethexe/contracts/src/Middleware.sol | 34 ++++++++++++++++++++++++++ ethexe/contracts/test/Middleware.t.sol | 29 ++++++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 ethexe/contracts/src/Middleware.sol create mode 100644 ethexe/contracts/test/Middleware.t.sol diff --git a/ethexe/contracts/src/Middleware.sol b/ethexe/contracts/src/Middleware.sol new file mode 100644 index 00000000000..ac4794efa17 --- /dev/null +++ b/ethexe/contracts/src/Middleware.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.26; + +import {Time} from "@openzeppelin/contracts/utils/types/Time.sol"; + +contract Middleware { + uint48 public immutable ERA_DURATION; + uint48 public immutable GENESIS_TIMESTAMP; + address public immutable DELEGATOR_FACTORY; + address public immutable OPERATOR_SPECIFIC_DELEGATOR_TYPE_INDEX; + address public immutable SLASHER_FACTORY; + address public immutable OPERATOR_REGISTRY; + address public immutable NETWORK_REGISTRY; + address public immutable COLLATERAL; + + constructor( + uint48 eraDuration, + address delegatorFactory, + address operatorSpecificDelegatorTypeIndex, + address slasherFactory, + address operatorRegistry, + address networkRegistry, + address collateral + ) { + ERA_DURATION = eraDuration; + GENESIS_TIMESTAMP = Time.timestamp(); + DELEGATOR_FACTORY = delegatorFactory; + OPERATOR_SPECIFIC_DELEGATOR_TYPE_INDEX = operatorSpecificDelegatorTypeIndex; + SLASHER_FACTORY = slasherFactory; + OPERATOR_REGISTRY = operatorRegistry; + NETWORK_REGISTRY = networkRegistry; + COLLATERAL = collateral; + } +} diff --git a/ethexe/contracts/test/Middleware.t.sol b/ethexe/contracts/test/Middleware.t.sol new file mode 100644 index 00000000000..f2dde23ddd7 --- /dev/null +++ b/ethexe/contracts/test/Middleware.t.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.26; + +import {Middleware} from "../src/Middleware.sol"; +import {Test, console} from "forge-std/Test.sol"; +import {Time} from "@openzeppelin/contracts/utils/types/Time.sol"; +import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; + +contract MiddlewareTest is Test { + using MessageHashUtils for address; + + Middleware public middleware; + + function setUp() public { + middleware = new Middleware(100, address(0), address(0), address(0), address(0), address(0), address(0)); + } + + function test_constructor() public view { + console.log("ERA_DURATION: ", uint256(middleware.ERA_DURATION())); + assertEq(uint256(middleware.ERA_DURATION()), 100); + assertEq(uint256(middleware.GENESIS_TIMESTAMP()), Time.timestamp()); + assertEq(middleware.DELEGATOR_FACTORY(), address(0)); + assertEq(middleware.OPERATOR_SPECIFIC_DELEGATOR_TYPE_INDEX(), address(0)); + assertEq(middleware.SLASHER_FACTORY(), address(0)); + assertEq(middleware.OPERATOR_REGISTRY(), address(0)); + assertEq(middleware.NETWORK_REGISTRY(), address(0)); + assertEq(middleware.COLLATERAL(), address(0)); + } +} From d0ad83703f88deaabfee621c33dce377745a0654 Mon Sep 17 00:00:00 2001 From: Gregory Sobol Date: Tue, 29 Oct 2024 11:42:54 +0100 Subject: [PATCH 03/14] registrations and get stake implementations --- ethexe/contracts/foundry.toml | 3 +- ethexe/contracts/src/Middleware.sol | 167 +++++++++- .../src/libraries/MapWithTimeData.sol | 74 +++++ ethexe/contracts/test/Middleware.t.sol | 298 +++++++++++++++++- 4 files changed, 525 insertions(+), 17 deletions(-) create mode 100644 ethexe/contracts/src/libraries/MapWithTimeData.sol diff --git a/ethexe/contracts/foundry.toml b/ethexe/contracts/foundry.toml index bb0029c8438..6dfe199eb54 100644 --- a/ethexe/contracts/foundry.toml +++ b/ethexe/contracts/foundry.toml @@ -14,7 +14,8 @@ ignored_warnings_from = [ "src/MirrorProxy.sol", ] # Enable new EVM codegen -via_ir = true +via_ir = false +ignored_warning_paths = ["lib/"] [rpc_endpoints] sepolia = "${SEPOLIA_RPC_URL}" diff --git a/ethexe/contracts/src/Middleware.sol b/ethexe/contracts/src/Middleware.sol index ac4794efa17..822adfedf3c 100644 --- a/ethexe/contracts/src/Middleware.sol +++ b/ethexe/contracts/src/Middleware.sol @@ -1,34 +1,191 @@ // 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 {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"; + contract Middleware { + using EnumerableMap for EnumerableMap.AddressToUintMap; + using MapWithTimeData for EnumerableMap.AddressToUintMap; + using Subnetwork for address; + + error OperatorAlreadyRegistered(); + error ZeroVaultAddress(); + error NotKnownVault(); + error IncorrectDelegatorType(); + error VaultWrongEpochDuration(); + error UnknownCollateral(); + error NotEnoughStakeInVault(); + error OperatorGracePeriodNotPassed(); + error VaultGracePeriodNotPassed(); + error NotVaultOwner(); + error IncorrectTimestamp(); + error OperatorDoesNotExist(); + error OperatorDoesNotOptIn(); + uint48 public immutable ERA_DURATION; uint48 public immutable GENESIS_TIMESTAMP; + address public immutable VAULT_FACTORY; address public immutable DELEGATOR_FACTORY; - address public immutable OPERATOR_SPECIFIC_DELEGATOR_TYPE_INDEX; address public immutable SLASHER_FACTORY; address public immutable OPERATOR_REGISTRY; - address public immutable NETWORK_REGISTRY; + address public immutable NETWORK_OPT_IN; address public immutable COLLATERAL; + EnumerableMap.AddressToUintMap private operators; + EnumerableMap.AddressToUintMap private vaults; + constructor( uint48 eraDuration, + address vaultFactory, address delegatorFactory, - address operatorSpecificDelegatorTypeIndex, address slasherFactory, address operatorRegistry, address networkRegistry, + address networkOptIn, address collateral ) { ERA_DURATION = eraDuration; GENESIS_TIMESTAMP = Time.timestamp(); + VAULT_FACTORY = vaultFactory; DELEGATOR_FACTORY = delegatorFactory; - OPERATOR_SPECIFIC_DELEGATOR_TYPE_INDEX = operatorSpecificDelegatorTypeIndex; SLASHER_FACTORY = slasherFactory; OPERATOR_REGISTRY = operatorRegistry; - NETWORK_REGISTRY = networkRegistry; + NETWORK_OPT_IN = networkOptIn; COLLATERAL = collateral; + + INetworkRegistry(networkRegistry).registerNetwork(); + } + + // TODO: append total operator stake check is big enough + // TODO: append check operator is opt-in network + // TODO: append check operator is operator registry entity + 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, address(0)); + } + + function disableOperator() external { + operators.disable(msg.sender); + } + + function enableOperator() external { + operators.enable(msg.sender); + } + + function unregisterOperator() external { + (, uint48 disabledTime) = operators.getTimes(msg.sender); + + if (disabledTime == 0 || disabledTime + 2 * ERA_DURATION > Time.timestamp()) { + revert OperatorGracePeriodNotPassed(); + } + + operators.remove(msg.sender); + } + + // TODO: check vault has enough stake + function registerVault(address vault) external { + if (vault == address(0)) { + revert ZeroVaultAddress(); + } + + if (!IRegistry(VAULT_FACTORY).isEntity(vault)) { + revert NotKnownVault(); + } + + if (IVault(vault).epochDuration() < 2 * ERA_DURATION) { + revert VaultWrongEpochDuration(); + } + + if (IVault(vault).collateral() != COLLATERAL) { + revert UnknownCollateral(); + } + + IBaseDelegator(IVault(vault).delegator()).setMaxNetworkLimit(network_identifier(), type(uint256).max); + + vaults.append(vault, msg.sender); + } + + function disableVault(address vault) external { + address vault_owner = vaults.getPinnedAddress(vault); + + if (vault_owner != msg.sender) { + revert NotVaultOwner(); + } + + vaults.disable(vault); + } + + function enableVault(address vault) external { + address vault_owner = vaults.getPinnedAddress(vault); + + if (vault_owner != msg.sender) { + revert NotVaultOwner(); + } + + vaults.enable(vault); + } + + function unregisterVault(address vault) external { + (, uint48 disabledTime) = vaults.getTimes(vault); + + if (disabledTime == 0 || disabledTime + 2 * ERA_DURATION > Time.timestamp()) { + revert VaultGracePeriodNotPassed(); + } + + vaults.remove(vault); + } + + function getOperatorStakeAt(address operator, uint48 ts) external view returns (uint256 stake) { + _checkTimestampInThePast(ts); + + (uint48 enabledTime, uint48 disabledTime) = operators.getTimes(operator); + if (!_wasActiveAt(enabledTime, disabledTime, ts)) { + return 0; + } + + 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 + function _checkTimestampInThePast(uint48 ts) private view { + if (ts >= Time.timestamp()) { + revert IncorrectTimestamp(); + } + } + + function subnetwork() public view returns (bytes32) { + return address(this).subnetwork(network_identifier()); + } + + function network_identifier() public pure returns (uint96) { + return 0; } } diff --git a/ethexe/contracts/src/libraries/MapWithTimeData.sol b/ethexe/contracts/src/libraries/MapWithTimeData.sol new file mode 100644 index 00000000000..02b68276e5d --- /dev/null +++ b/ethexe/contracts/src/libraries/MapWithTimeData.sol @@ -0,0 +1,74 @@ +// 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, address) { + return (uint48(value), uint48(value >> 48), address(uint160(value >> 96))); + } + + function toValue(uint48 enabledTime, uint48 disabledTime, address pinnedAddress) private pure returns (uint256) { + return uint256(enabledTime) | (uint256(disabledTime) << 48) | (uint256(uint160(pinnedAddress)) << 96); + } + + function append(EnumerableMap.AddressToUintMap storage self, address addr, address pinnedAddress) internal { + if (!self.set(addr, toValue(Time.timestamp(), 0, pinnedAddress))) { + revert AlreadyAdded(); + } + } + + function enable(EnumerableMap.AddressToUintMap storage self, address addr) internal { + (uint48 enabledTime, uint48 disabledTime, address pinnedAddress) = toInner(self.get(addr)); + + if (enabledTime != 0 && disabledTime == 0) { + revert AlreadyEnabled(); + } + + self.set(addr, toValue(Time.timestamp(), 0, pinnedAddress)); + } + + function disable(EnumerableMap.AddressToUintMap storage self, address addr) internal { + (uint48 enabledTime, uint48 disabledTime, address pinnedAddress) = toInner(self.get(addr)); + + if (enabledTime == 0 || disabledTime != 0) { + revert NotEnabled(); + } + + self.set(addr, toValue(enabledTime, Time.timestamp(), pinnedAddress)); + } + + 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 getPinnedAddress(EnumerableMap.AddressToUintMap storage self, address addr) + internal + view + returns (address pinnedAddress) + { + (,, pinnedAddress) = toInner(self.get(addr)); + } +} diff --git a/ethexe/contracts/test/Middleware.t.sol b/ethexe/contracts/test/Middleware.t.sol index f2dde23ddd7..b91bab88a67 100644 --- a/ethexe/contracts/test/Middleware.t.sol +++ b/ethexe/contracts/test/Middleware.t.sol @@ -1,29 +1,305 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.26; -import {Middleware} from "../src/Middleware.sol"; -import {Test, console} from "forge-std/Test.sol"; +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 {IOptInService} from "symbiotic-core/src/interfaces/service/IOptInService.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 { - middleware = new Middleware(100, address(0), address(0), address(0), address(0), address(0), address(0)); + 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 { - console.log("ERA_DURATION: ", uint256(middleware.ERA_DURATION())); - assertEq(uint256(middleware.ERA_DURATION()), 100); + assertEq(uint256(middleware.ERA_DURATION()), eraDuration); assertEq(uint256(middleware.GENESIS_TIMESTAMP()), Time.timestamp()); - assertEq(middleware.DELEGATOR_FACTORY(), address(0)); - assertEq(middleware.OPERATOR_SPECIFIC_DELEGATOR_TYPE_INDEX(), address(0)); - assertEq(middleware.SLASHER_FACTORY(), address(0)); - assertEq(middleware.OPERATOR_REGISTRY(), address(0)); - assertEq(middleware.NETWORK_REGISTRY(), address(0)); - assertEq(middleware.COLLATERAL(), address(0)); + 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(); + + // Wait for grace period and unregister operator + vm.warp(block.timestamp + eraDuration * 2); + middleware.unregisterOperator(); + } + + 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(block.timestamp + 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(block.timestamp + 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); + + _depositFromInVault(owner, vault1, 1_000); + _depositFromInVault(owner, vault2, 2_000); + + uint48 ts1 = uint48(block.timestamp); + vm.warp(block.timestamp + 1); + assertEq(middleware.getOperatorStakeAt(operator1, ts1), 1_000); + assertEq(middleware.getOperatorStakeAt(operator2, ts1), 2_000); + + // Create one more vault for operator1 and check operator1 stake + address vault3 = _createVaultForOperator(operator1); + + uint48 ts2 = uint48(block.timestamp); + vm.warp(block.timestamp + 1); + assertEq(middleware.getOperatorStakeAt(operator1, ts2), 1_000); + + _depositFromInVault(owner, vault3, 3_000); + uint48 ts3 = uint48(block.timestamp); + vm.warp(block.timestamp + 1); + assertEq(middleware.getOperatorStakeAt(operator1, ts3), 4_000); + } + + 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("") + }) + ); } } From de72ac51736763e0e06294060e3479553e046ac0 Mon Sep 17 00:00:00 2001 From: Gregory Sobol Date: Tue, 29 Oct 2024 17:03:53 +0100 Subject: [PATCH 04/14] append getEnabledValidatorSet and tests --- ethexe/contracts/src/Middleware.sol | 70 ++++++++++++++++++---- ethexe/contracts/test/Middleware.t.sol | 81 ++++++++++++++++++++++---- 2 files changed, 127 insertions(+), 24 deletions(-) diff --git a/ethexe/contracts/src/Middleware.sol b/ethexe/contracts/src/Middleware.sol index 822adfedf3c..8e6a260c964 100644 --- a/ethexe/contracts/src/Middleware.sol +++ b/ethexe/contracts/src/Middleware.sol @@ -14,6 +14,7 @@ import {IOptInService} from "symbiotic-core/src/interfaces/service/IOptInService import {MapWithTimeData} from "./libraries/MapWithTimeData.sol"; +// TODO: support slashing contract Middleware { using EnumerableMap for EnumerableMap.AddressToUintMap; using MapWithTimeData for EnumerableMap.AddressToUintMap; @@ -35,6 +36,9 @@ contract Middleware { 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; @@ -57,6 +61,9 @@ contract Middleware { ) { 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; @@ -88,10 +95,11 @@ contract Middleware { operators.enable(msg.sender); } + // TODO: allow anybody to unregister operator function unregisterOperator() external { (, uint48 disabledTime) = operators.getTimes(msg.sender); - if (disabledTime == 0 || disabledTime + 2 * ERA_DURATION > Time.timestamp()) { + if (disabledTime == 0 || disabledTime + OPERATOR_GRACE_PERIOD > Time.timestamp()) { revert OperatorGracePeriodNotPassed(); } @@ -99,6 +107,7 @@ contract Middleware { } // TODO: check vault has enough stake + // TODO: support and check slasher function registerVault(address vault) external { if (vault == address(0)) { revert ZeroVaultAddress(); @@ -108,7 +117,7 @@ contract Middleware { revert NotKnownVault(); } - if (IVault(vault).epochDuration() < 2 * ERA_DURATION) { + if (IVault(vault).epochDuration() < VAULT_MIN_EPOCH_DURATION) { revert VaultWrongEpochDuration(); } @@ -116,7 +125,10 @@ contract Middleware { revert UnknownCollateral(); } - IBaseDelegator(IVault(vault).delegator()).setMaxNetworkLimit(network_identifier(), type(uint256).max); + address delegator = IVault(vault).delegator(); + if (IBaseDelegator(delegator).maxNetworkLimit(subnetwork()) != type(uint256).max) { + IBaseDelegator(delegator).setMaxNetworkLimit(network_identifier(), type(uint256).max); + } vaults.append(vault, msg.sender); } @@ -144,7 +156,7 @@ contract Middleware { function unregisterVault(address vault) external { (, uint48 disabledTime) = vaults.getTimes(vault); - if (disabledTime == 0 || disabledTime + 2 * ERA_DURATION > Time.timestamp()) { + if (disabledTime == 0 || disabledTime + VAULT_GRACE_PERIOD > Time.timestamp()) { revert VaultGracePeriodNotPassed(); } @@ -159,6 +171,48 @@ contract Middleware { return 0; } + stake = _collectOperatorStakeFromVaultsAt(operator, ts); + } + + function getEnabledOperatorsStakeAt(uint48 ts) + public + view + returns (address[] memory enabled_operators, uint256[] memory stakes) + { + _checkTimestampInThePast(ts); + + enabled_operators = 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; + } + + enabled_operators[operatorIdx] = operator; + stakes[operatorIdx] = _collectOperatorStakeFromVaultsAt(operator, ts); + operatorIdx += 1; + } + + assembly { + mstore(enabled_operators, operatorIdx) + mstore(stakes, operatorIdx) + } + } + + function subnetwork() public view returns (bytes32) { + return address(this).subnetwork(network_identifier()); + } + + function network_identifier() public pure returns (uint96) { + return 0; + } + + 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); @@ -180,12 +234,4 @@ contract Middleware { revert IncorrectTimestamp(); } } - - function subnetwork() public view returns (bytes32) { - return address(this).subnetwork(network_identifier()); - } - - function network_identifier() public pure returns (uint96) { - return 0; - } } diff --git a/ethexe/contracts/test/Middleware.t.sol b/ethexe/contracts/test/Middleware.t.sol index b91bab88a67..43a852777e2 100644 --- a/ethexe/contracts/test/Middleware.t.sol +++ b/ethexe/contracts/test/Middleware.t.sol @@ -201,22 +201,79 @@ contract MiddlewareTest is Test { _depositFromInVault(owner, vault1, 1_000); _depositFromInVault(owner, vault2, 2_000); - uint48 ts1 = uint48(block.timestamp); - vm.warp(block.timestamp + 1); - assertEq(middleware.getOperatorStakeAt(operator1, ts1), 1_000); - assertEq(middleware.getOperatorStakeAt(operator2, ts1), 2_000); + { + // Check operator stake after depositing + uint48 ts = uint48(block.timestamp); + vm.warp(block.timestamp + 1); + assertEq(middleware.getOperatorStakeAt(operator1, ts), 1_000); + assertEq(middleware.getOperatorStakeAt(operator2, ts), 2_000); + (address[] memory enabled_operators, uint256[] memory stakes) = middleware.getEnabledOperatorsStakeAt(ts); + assertEq(enabled_operators.length, 2); + assertEq(stakes.length, 2); + assertEq(enabled_operators[0], operator1); + assertEq(enabled_operators[1], operator2); + } - // Create one more vault for operator1 and check operator1 stake + // Create one more vault for operator1 address vault3 = _createVaultForOperator(operator1); - uint48 ts2 = uint48(block.timestamp); - vm.warp(block.timestamp + 1); - assertEq(middleware.getOperatorStakeAt(operator1, ts2), 1_000); + { + // Check that vault creation doesn't affect operator stake without deposit + uint48 ts = uint48(block.timestamp); + vm.warp(block.timestamp + 1); + assertEq(middleware.getOperatorStakeAt(operator1, ts), 1_000); + } + + { + // Check after depositing to new vault + _depositFromInVault(owner, vault3, 3_000); + uint48 ts = uint48(block.timestamp); + vm.warp(block.timestamp + 1); + assertEq(middleware.getOperatorStakeAt(operator1, ts), 4_000); + } + + { + // 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(block.timestamp) + 1; + vm.warp(block.timestamp + 2); + assertEq(middleware.getOperatorStakeAt(operator1, ts), 3_000); + } - _depositFromInVault(owner, vault3, 3_000); - uint48 ts3 = uint48(block.timestamp); - vm.warp(block.timestamp + 1); - assertEq(middleware.getOperatorStakeAt(operator1, ts3), 4_000); + { + // Disable operator1 and check operator1 stake is 0 + _disableOperator(operator1); + uint48 ts = uint48(block.timestamp) + 1; + vm.warp(block.timestamp + 2); + assertEq(middleware.getOperatorStakeAt(operator1, ts), 0); + + // Check that operator1 is not in enabled operators list + (address[] memory enabled_operators, uint256[] memory stakes) = middleware.getEnabledOperatorsStakeAt(ts); + assertEq(enabled_operators.length, 1); + assertEq(stakes.length, 1); + assertEq(enabled_operators[0], operator2); + } + + // Try to get stake for current timestamp + vm.expectRevert(abi.encodeWithSelector(Middleware.IncorrectTimestamp.selector)); + middleware.getOperatorStakeAt(operator2, uint48(block.timestamp)); + + // Try to get stake for future timestamp + vm.expectRevert(abi.encodeWithSelector(Middleware.IncorrectTimestamp.selector)); + middleware.getOperatorStakeAt(operator2, uint48(block.timestamp + 1)); + } + + 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 { From 522cb14fea9eebc995e64867d6e4e44ee3123af5 Mon Sep 17 00:00:00 2001 From: Gregory Sobol Date: Wed, 30 Oct 2024 11:00:42 +0100 Subject: [PATCH 05/14] refactor subnetwork --- ethexe/contracts/src/Middleware.sol | 20 ++++++-------------- ethexe/contracts/test/Middleware.t.sol | 5 ++--- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/ethexe/contracts/src/Middleware.sol b/ethexe/contracts/src/Middleware.sol index 8e6a260c964..db8ca771104 100644 --- a/ethexe/contracts/src/Middleware.sol +++ b/ethexe/contracts/src/Middleware.sol @@ -20,13 +20,10 @@ contract Middleware { using MapWithTimeData for EnumerableMap.AddressToUintMap; using Subnetwork for address; - error OperatorAlreadyRegistered(); error ZeroVaultAddress(); error NotKnownVault(); - error IncorrectDelegatorType(); error VaultWrongEpochDuration(); error UnknownCollateral(); - error NotEnoughStakeInVault(); error OperatorGracePeriodNotPassed(); error VaultGracePeriodNotPassed(); error NotVaultOwner(); @@ -45,6 +42,8 @@ contract Middleware { address public immutable OPERATOR_REGISTRY; address public immutable NETWORK_OPT_IN; address public immutable COLLATERAL; + bytes32 public immutable SUBNETWORK; + uint96 public immutable NETWORK_IDENTIFIER = 0; EnumerableMap.AddressToUintMap private operators; EnumerableMap.AddressToUintMap private vaults; @@ -70,6 +69,7 @@ contract Middleware { OPERATOR_REGISTRY = operatorRegistry; NETWORK_OPT_IN = networkOptIn; COLLATERAL = collateral; + SUBNETWORK = address(this).subnetwork(NETWORK_IDENTIFIER); INetworkRegistry(networkRegistry).registerNetwork(); } @@ -126,8 +126,8 @@ contract Middleware { } address delegator = IVault(vault).delegator(); - if (IBaseDelegator(delegator).maxNetworkLimit(subnetwork()) != type(uint256).max) { - IBaseDelegator(delegator).setMaxNetworkLimit(network_identifier(), type(uint256).max); + if (IBaseDelegator(delegator).maxNetworkLimit(SUBNETWORK) != type(uint256).max) { + IBaseDelegator(delegator).setMaxNetworkLimit(NETWORK_IDENTIFIER, type(uint256).max); } vaults.append(vault, msg.sender); @@ -204,14 +204,6 @@ contract Middleware { } } - function subnetwork() public view returns (bytes32) { - return address(this).subnetwork(network_identifier()); - } - - function network_identifier() public pure returns (uint96) { - return 0; - } - 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); @@ -220,7 +212,7 @@ contract Middleware { continue; } - stake += IBaseDelegator(IVault(vault).delegator()).stakeAt(subnetwork(), operator, ts, new bytes(0)); + stake += IBaseDelegator(IVault(vault).delegator()).stakeAt(SUBNETWORK, operator, ts, new bytes(0)); } } diff --git a/ethexe/contracts/test/Middleware.t.sol b/ethexe/contracts/test/Middleware.t.sol index 43a852777e2..73722d26253 100644 --- a/ethexe/contracts/test/Middleware.t.sol +++ b/ethexe/contracts/test/Middleware.t.sol @@ -13,7 +13,6 @@ import {IVaultConfigurator} from "symbiotic-core/src/interfaces/IVaultConfigurat 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 {IOptInService} from "symbiotic-core/src/interfaces/service/IOptInService.sol"; import {Middleware} from "../src/Middleware.sol"; import {WrappedVara} from "../src/WrappedVara.sol"; @@ -305,7 +304,7 @@ contract MiddlewareTest is Test { // Set initial network limit IOperatorSpecificDelegator(IVault(vault).delegator()).setNetworkLimit( - middleware.subnetwork(), type(uint256).max + middleware.SUBNETWORK(), type(uint256).max ); vm.stopPrank(); @@ -314,7 +313,7 @@ contract MiddlewareTest is Test { function _setNetworkLimit(address vault, address operator, uint256 limit) private { vm.startPrank(address(operator)); - IOperatorSpecificDelegator(IVault(vault).delegator()).setNetworkLimit(middleware.subnetwork(), limit); + IOperatorSpecificDelegator(IVault(vault).delegator()).setNetworkLimit(middleware.SUBNETWORK(), limit); vm.stopPrank(); } From 520bf0b5ea0145336d224d3980405840003b4220 Mon Sep 17 00:00:00 2001 From: Gregory Sobol Date: Wed, 30 Oct 2024 11:11:22 +0100 Subject: [PATCH 06/14] enabled oeprators -> active operators --- ethexe/contracts/src/Middleware.sol | 10 +++---- ethexe/contracts/test/Middleware.t.sol | 37 +++++++++++++++----------- 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/ethexe/contracts/src/Middleware.sol b/ethexe/contracts/src/Middleware.sol index db8ca771104..f9ecffa8949 100644 --- a/ethexe/contracts/src/Middleware.sol +++ b/ethexe/contracts/src/Middleware.sol @@ -174,14 +174,14 @@ contract Middleware { stake = _collectOperatorStakeFromVaultsAt(operator, ts); } - function getEnabledOperatorsStakeAt(uint48 ts) + function getActiveOperatorsStakeAt(uint48 ts) public view - returns (address[] memory enabled_operators, uint256[] memory stakes) + returns (address[] memory active_operators, uint256[] memory stakes) { _checkTimestampInThePast(ts); - enabled_operators = new address[](operators.length()); + active_operators = new address[](operators.length()); stakes = new uint256[](operators.length()); uint256 operatorIdx = 0; @@ -193,13 +193,13 @@ contract Middleware { continue; } - enabled_operators[operatorIdx] = operator; + active_operators[operatorIdx] = operator; stakes[operatorIdx] = _collectOperatorStakeFromVaultsAt(operator, ts); operatorIdx += 1; } assembly { - mstore(enabled_operators, operatorIdx) + mstore(active_operators, operatorIdx) mstore(stakes, operatorIdx) } } diff --git a/ethexe/contracts/test/Middleware.t.sol b/ethexe/contracts/test/Middleware.t.sol index 73722d26253..e852f1909cc 100644 --- a/ethexe/contracts/test/Middleware.t.sol +++ b/ethexe/contracts/test/Middleware.t.sol @@ -197,20 +197,26 @@ contract MiddlewareTest is Test { address vault1 = _createVaultForOperator(operator1); address vault2 = _createVaultForOperator(operator2); - _depositFromInVault(owner, vault1, 1_000); - _depositFromInVault(owner, vault2, 2_000); + 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(block.timestamp); vm.warp(block.timestamp + 1); - assertEq(middleware.getOperatorStakeAt(operator1, ts), 1_000); - assertEq(middleware.getOperatorStakeAt(operator2, ts), 2_000); - (address[] memory enabled_operators, uint256[] memory stakes) = middleware.getEnabledOperatorsStakeAt(ts); - assertEq(enabled_operators.length, 2); + 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(enabled_operators[0], operator1); - assertEq(enabled_operators[1], operator2); + assertEq(active_operators[0], operator1); + assertEq(active_operators[1], operator2); + assertEq(stakes[0], stake1); + assertEq(stakes[1], stake2); } // Create one more vault for operator1 @@ -220,7 +226,7 @@ contract MiddlewareTest is Test { // Check that vault creation doesn't affect operator stake without deposit uint48 ts = uint48(block.timestamp); vm.warp(block.timestamp + 1); - assertEq(middleware.getOperatorStakeAt(operator1, ts), 1_000); + assertEq(middleware.getOperatorStakeAt(operator1, ts), stake1); } { @@ -228,7 +234,7 @@ contract MiddlewareTest is Test { _depositFromInVault(owner, vault3, 3_000); uint48 ts = uint48(block.timestamp); vm.warp(block.timestamp + 1); - assertEq(middleware.getOperatorStakeAt(operator1, ts), 4_000); + assertEq(middleware.getOperatorStakeAt(operator1, ts), stake1 + stake3); } { @@ -237,7 +243,7 @@ contract MiddlewareTest is Test { _disableVault(operator1, vault1); uint48 ts = uint48(block.timestamp) + 1; vm.warp(block.timestamp + 2); - assertEq(middleware.getOperatorStakeAt(operator1, ts), 3_000); + assertEq(middleware.getOperatorStakeAt(operator1, ts), stake3); } { @@ -247,11 +253,12 @@ contract MiddlewareTest is Test { vm.warp(block.timestamp + 2); assertEq(middleware.getOperatorStakeAt(operator1, ts), 0); - // Check that operator1 is not in enabled operators list - (address[] memory enabled_operators, uint256[] memory stakes) = middleware.getEnabledOperatorsStakeAt(ts); - assertEq(enabled_operators.length, 1); + // 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(enabled_operators[0], operator2); + assertEq(active_operators[0], operator2); + assertEq(stakes[0], stake2); } // Try to get stake for current timestamp From 5092b6b7ea67501507ad9c3a7744adf2c7644772 Mon Sep 17 00:00:00 2001 From: Gregory Sobol Date: Wed, 30 Oct 2024 13:09:42 +0100 Subject: [PATCH 07/14] pinnedAddress -> pinnedData --- ethexe/contracts/src/Middleware.sol | 11 +++++--- .../src/libraries/MapWithTimeData.sol | 26 +++++++++---------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/ethexe/contracts/src/Middleware.sol b/ethexe/contracts/src/Middleware.sol index f9ecffa8949..26f8ac11e99 100644 --- a/ethexe/contracts/src/Middleware.sol +++ b/ethexe/contracts/src/Middleware.sol @@ -15,6 +15,9 @@ import {IOptInService} from "symbiotic-core/src/interfaces/service/IOptInService import {MapWithTimeData} from "./libraries/MapWithTimeData.sol"; // TODO: support slashing +// TODO: implement election logic +// TODO: implement fored operators removal +// TODO: implement forced vaults removal contract Middleware { using EnumerableMap for EnumerableMap.AddressToUintMap; using MapWithTimeData for EnumerableMap.AddressToUintMap; @@ -84,7 +87,7 @@ contract Middleware { if (!IOptInService(NETWORK_OPT_IN).isOptedIn(msg.sender, address(this))) { revert OperatorDoesNotOptIn(); } - operators.append(msg.sender, address(0)); + operators.append(msg.sender, 0); } function disableOperator() external { @@ -130,11 +133,11 @@ contract Middleware { IBaseDelegator(delegator).setMaxNetworkLimit(NETWORK_IDENTIFIER, type(uint256).max); } - vaults.append(vault, msg.sender); + vaults.append(vault, uint160(msg.sender)); } function disableVault(address vault) external { - address vault_owner = vaults.getPinnedAddress(vault); + address vault_owner = address(vaults.getPinnedData(vault)); if (vault_owner != msg.sender) { revert NotVaultOwner(); @@ -144,7 +147,7 @@ contract Middleware { } function enableVault(address vault) external { - address vault_owner = vaults.getPinnedAddress(vault); + address vault_owner = address(vaults.getPinnedData(vault)); if (vault_owner != msg.sender) { revert NotVaultOwner(); diff --git a/ethexe/contracts/src/libraries/MapWithTimeData.sol b/ethexe/contracts/src/libraries/MapWithTimeData.sol index 02b68276e5d..8f5d6d4129e 100644 --- a/ethexe/contracts/src/libraries/MapWithTimeData.sol +++ b/ethexe/contracts/src/libraries/MapWithTimeData.sol @@ -12,38 +12,38 @@ library MapWithTimeData { error NotEnabled(); error AlreadyEnabled(); - function toInner(uint256 value) private pure returns (uint48, uint48, address) { - return (uint48(value), uint48(value >> 48), address(uint160(value >> 96))); + function toInner(uint256 value) private pure returns (uint48, uint48, uint160) { + return (uint48(value), uint48(value >> 48), uint160(value >> 96)); } - function toValue(uint48 enabledTime, uint48 disabledTime, address pinnedAddress) private pure returns (uint256) { - return uint256(enabledTime) | (uint256(disabledTime) << 48) | (uint256(uint160(pinnedAddress)) << 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, address pinnedAddress) internal { - if (!self.set(addr, toValue(Time.timestamp(), 0, pinnedAddress))) { + 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, address pinnedAddress) = toInner(self.get(addr)); + (uint48 enabledTime, uint48 disabledTime, uint160 data) = toInner(self.get(addr)); if (enabledTime != 0 && disabledTime == 0) { revert AlreadyEnabled(); } - self.set(addr, toValue(Time.timestamp(), 0, pinnedAddress)); + self.set(addr, toValue(Time.timestamp(), 0, data)); } function disable(EnumerableMap.AddressToUintMap storage self, address addr) internal { - (uint48 enabledTime, uint48 disabledTime, address pinnedAddress) = toInner(self.get(addr)); + (uint48 enabledTime, uint48 disabledTime, uint160 data) = toInner(self.get(addr)); if (enabledTime == 0 || disabledTime != 0) { revert NotEnabled(); } - self.set(addr, toValue(enabledTime, Time.timestamp(), pinnedAddress)); + self.set(addr, toValue(enabledTime, Time.timestamp(), data)); } function atWithTimes(EnumerableMap.AddressToUintMap storage self, uint256 idx) @@ -64,11 +64,11 @@ library MapWithTimeData { (enabledTime, disabledTime,) = toInner(self.get(addr)); } - function getPinnedAddress(EnumerableMap.AddressToUintMap storage self, address addr) + function getPinnedData(EnumerableMap.AddressToUintMap storage self, address addr) internal view - returns (address pinnedAddress) + returns (uint160 data) { - (,, pinnedAddress) = toInner(self.get(addr)); + (,, data) = toInner(self.get(addr)); } } From 1c1e2e221f157d3231f926c8561db45e90871a4d Mon Sep 17 00:00:00 2001 From: Gregory Sobol Date: Wed, 30 Oct 2024 13:27:27 +0100 Subject: [PATCH 08/14] unregisterOperator(operator) --- ethexe/contracts/src/Middleware.sol | 7 +++---- ethexe/contracts/test/Middleware.t.sol | 7 ++++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/ethexe/contracts/src/Middleware.sol b/ethexe/contracts/src/Middleware.sol index 26f8ac11e99..ed50c2fa50f 100644 --- a/ethexe/contracts/src/Middleware.sol +++ b/ethexe/contracts/src/Middleware.sol @@ -98,15 +98,14 @@ contract Middleware { operators.enable(msg.sender); } - // TODO: allow anybody to unregister operator - function unregisterOperator() external { - (, uint48 disabledTime) = operators.getTimes(msg.sender); + function unregisterOperator(address operator) external { + (, uint48 disabledTime) = operators.getTimes(operator); if (disabledTime == 0 || disabledTime + OPERATOR_GRACE_PERIOD > Time.timestamp()) { revert OperatorGracePeriodNotPassed(); } - operators.remove(msg.sender); + operators.remove(operator); } // TODO: check vault has enough stake diff --git a/ethexe/contracts/test/Middleware.t.sol b/ethexe/contracts/test/Middleware.t.sol index e852f1909cc..fcb6294eab6 100644 --- a/ethexe/contracts/test/Middleware.t.sol +++ b/ethexe/contracts/test/Middleware.t.sol @@ -103,11 +103,12 @@ contract MiddlewareTest is Test { // Try to unregister operator - failed because operator is not disabled for enough time vm.expectRevert(abi.encodeWithSelector(Middleware.OperatorGracePeriodNotPassed.selector)); - middleware.unregisterOperator(); + middleware.unregisterOperator(address(0x2)); - // Wait for grace period and unregister operator + // Wait for grace period and unregister operator from other address + vm.startPrank(address(0x3)); vm.warp(block.timestamp + eraDuration * 2); - middleware.unregisterOperator(); + middleware.unregisterOperator(address(0x2)); } function test_registerVault() public { From f348ef8711d35cf8dcabf7240aa23f03992d5866 Mon Sep 17 00:00:00 2001 From: Gregory Sobol Date: Wed, 30 Oct 2024 13:28:15 +0100 Subject: [PATCH 09/14] - --- ethexe/contracts/src/Middleware.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ethexe/contracts/src/Middleware.sol b/ethexe/contracts/src/Middleware.sol index ed50c2fa50f..e0610f2405f 100644 --- a/ethexe/contracts/src/Middleware.sol +++ b/ethexe/contracts/src/Middleware.sol @@ -16,7 +16,7 @@ import {MapWithTimeData} from "./libraries/MapWithTimeData.sol"; // TODO: support slashing // TODO: implement election logic -// TODO: implement fored operators removal +// TODO: implement forced operators removal // TODO: implement forced vaults removal contract Middleware { using EnumerableMap for EnumerableMap.AddressToUintMap; From 1116f9d665ea8a50ed61ed16e635f8bdfffcf046 Mon Sep 17 00:00:00 2001 From: Gregory Sobol Date: Wed, 30 Oct 2024 13:32:02 +0100 Subject: [PATCH 10/14] chore --- ethexe/contracts/foundry.toml | 3 +-- ethexe/contracts/src/Middleware.sol | 4 +--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/ethexe/contracts/foundry.toml b/ethexe/contracts/foundry.toml index 6dfe199eb54..bb0029c8438 100644 --- a/ethexe/contracts/foundry.toml +++ b/ethexe/contracts/foundry.toml @@ -14,8 +14,7 @@ ignored_warnings_from = [ "src/MirrorProxy.sol", ] # Enable new EVM codegen -via_ir = false -ignored_warning_paths = ["lib/"] +via_ir = true [rpc_endpoints] sepolia = "${SEPOLIA_RPC_URL}" diff --git a/ethexe/contracts/src/Middleware.sol b/ethexe/contracts/src/Middleware.sol index e0610f2405f..7465f791274 100644 --- a/ethexe/contracts/src/Middleware.sol +++ b/ethexe/contracts/src/Middleware.sol @@ -77,9 +77,7 @@ contract Middleware { INetworkRegistry(networkRegistry).registerNetwork(); } - // TODO: append total operator stake check is big enough - // TODO: append check operator is opt-in network - // TODO: append check operator is operator registry entity + // TODO: Check that total stake is big enough function registerOperator() external { if (!IRegistry(OPERATOR_REGISTRY).isEntity(msg.sender)) { revert OperatorDoesNotExist(); From b058d01d8cc7155cfec8f89edd7e4f571d70d883 Mon Sep 17 00:00:00 2001 From: Gregory Sobol Date: Wed, 30 Oct 2024 13:34:32 +0100 Subject: [PATCH 11/14] chore --- ethexe/contracts/test/Middleware.t.sol | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ethexe/contracts/test/Middleware.t.sol b/ethexe/contracts/test/Middleware.t.sol index fcb6294eab6..c230dda60c3 100644 --- a/ethexe/contracts/test/Middleware.t.sol +++ b/ethexe/contracts/test/Middleware.t.sol @@ -54,6 +54,9 @@ contract MiddlewareTest is Test { 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())); From 629906a00be35437fc4f77222956028c7e54b225 Mon Sep 17 00:00:00 2001 From: Gregory Sobol Date: Wed, 30 Oct 2024 16:58:41 +0100 Subject: [PATCH 12/14] st --- ethexe/contracts/test/Middleware.t.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ethexe/contracts/test/Middleware.t.sol b/ethexe/contracts/test/Middleware.t.sol index c230dda60c3..ff35b86200a 100644 --- a/ethexe/contracts/test/Middleware.t.sol +++ b/ethexe/contracts/test/Middleware.t.sol @@ -55,7 +55,7 @@ contract MiddlewareTest is Test { 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_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())); @@ -235,7 +235,7 @@ contract MiddlewareTest is Test { { // Check after depositing to new vault - _depositFromInVault(owner, vault3, 3_000); + _depositFromInVault(owner, vault3, stake3); uint48 ts = uint48(block.timestamp); vm.warp(block.timestamp + 1); assertEq(middleware.getOperatorStakeAt(operator1, ts), stake1 + stake3); From 4fb9e0ea2d5d34da04889ae13a5142a7dec1ab3b Mon Sep 17 00:00:00 2001 From: Gregory Sobol Date: Thu, 31 Oct 2024 15:46:39 +0100 Subject: [PATCH 13/14] block.timestamp -> vm.getBlockTimestamp() --- ethexe/contracts/foundry.toml | 1 + ethexe/contracts/test/Middleware.t.sol | 30 +++++++++++++------------- 2 files changed, 16 insertions(+), 15 deletions(-) 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/test/Middleware.t.sol b/ethexe/contracts/test/Middleware.t.sol index ff35b86200a..ac67060a518 100644 --- a/ethexe/contracts/test/Middleware.t.sol +++ b/ethexe/contracts/test/Middleware.t.sol @@ -110,7 +110,7 @@ contract MiddlewareTest is Test { // Wait for grace period and unregister operator from other address vm.startPrank(address(0x3)); - vm.warp(block.timestamp + eraDuration * 2); + vm.warp(vm.getBlockTimestamp() + eraDuration * 2); middleware.unregisterOperator(address(0x2)); } @@ -167,14 +167,14 @@ contract MiddlewareTest is Test { middleware.unregisterVault(vault); // Wait for grace period and unregister vault - vm.warp(block.timestamp + eraDuration * 2); + 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(block.timestamp + eraDuration * 2); + vm.warp(vm.getBlockTimestamp() + eraDuration * 2); middleware.unregisterVault(vault); vm.stopPrank(); @@ -210,8 +210,8 @@ contract MiddlewareTest is Test { { // Check operator stake after depositing - uint48 ts = uint48(block.timestamp); - vm.warp(block.timestamp + 1); + 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); @@ -228,16 +228,16 @@ contract MiddlewareTest is Test { { // Check that vault creation doesn't affect operator stake without deposit - uint48 ts = uint48(block.timestamp); - vm.warp(block.timestamp + 1); + 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(block.timestamp); - vm.warp(block.timestamp + 1); + uint48 ts = uint48(vm.getBlockTimestamp()); + vm.warp(vm.getBlockTimestamp() + 1); assertEq(middleware.getOperatorStakeAt(operator1, ts), stake1 + stake3); } @@ -245,16 +245,16 @@ contract MiddlewareTest is Test { // 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(block.timestamp) + 1; - vm.warp(block.timestamp + 2); + 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(block.timestamp) + 1; - vm.warp(block.timestamp + 2); + 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 @@ -267,11 +267,11 @@ contract MiddlewareTest is Test { // Try to get stake for current timestamp vm.expectRevert(abi.encodeWithSelector(Middleware.IncorrectTimestamp.selector)); - middleware.getOperatorStakeAt(operator2, uint48(block.timestamp)); + middleware.getOperatorStakeAt(operator2, uint48(vm.getBlockTimestamp())); // Try to get stake for future timestamp vm.expectRevert(abi.encodeWithSelector(Middleware.IncorrectTimestamp.selector)); - middleware.getOperatorStakeAt(operator2, uint48(block.timestamp + 1)); + middleware.getOperatorStakeAt(operator2, uint48(vm.getBlockTimestamp() + 1)); } function _disableOperator(address operator) private { From 8b651e873114227857359b0d36675062898c8780 Mon Sep 17 00:00:00 2001 From: Gregory Sobol Date: Thu, 31 Oct 2024 20:32:43 +0100 Subject: [PATCH 14/14] fix potential problem with old timstamps --- ethexe/contracts/src/Middleware.sol | 20 +++++++++++++------ .../src/libraries/MapWithTimeData.sol | 1 + ethexe/contracts/test/Middleware.t.sol | 6 ++++++ 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/ethexe/contracts/src/Middleware.sol b/ethexe/contracts/src/Middleware.sol index 7465f791274..be0eb01c590 100644 --- a/ethexe/contracts/src/Middleware.sol +++ b/ethexe/contracts/src/Middleware.sol @@ -3,6 +3,7 @@ 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"; @@ -18,6 +19,7 @@ import {MapWithTimeData} from "./libraries/MapWithTimeData.sol"; // 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; @@ -99,7 +101,7 @@ contract Middleware { function unregisterOperator(address operator) external { (, uint48 disabledTime) = operators.getTimes(operator); - if (disabledTime == 0 || disabledTime + OPERATOR_GRACE_PERIOD > Time.timestamp()) { + if (disabledTime == 0 || Time.timestamp() < disabledTime + OPERATOR_GRACE_PERIOD) { revert OperatorGracePeriodNotPassed(); } @@ -156,7 +158,7 @@ contract Middleware { function unregisterVault(address vault) external { (, uint48 disabledTime) = vaults.getTimes(vault); - if (disabledTime == 0 || disabledTime + VAULT_GRACE_PERIOD > Time.timestamp()) { + if (disabledTime == 0 || Time.timestamp() < disabledTime + VAULT_GRACE_PERIOD) { revert VaultGracePeriodNotPassed(); } @@ -164,7 +166,7 @@ contract Middleware { } function getOperatorStakeAt(address operator, uint48 ts) external view returns (uint256 stake) { - _checkTimestampInThePast(ts); + _checkTimestamp(ts); (uint48 enabledTime, uint48 disabledTime) = operators.getTimes(operator); if (!_wasActiveAt(enabledTime, disabledTime, ts)) { @@ -179,7 +181,7 @@ contract Middleware { view returns (address[] memory active_operators, uint256[] memory stakes) { - _checkTimestampInThePast(ts); + _checkTimestamp(ts); active_operators = new address[](operators.length()); stakes = new uint256[](operators.length()); @@ -220,10 +222,16 @@ contract Middleware { return enabledTime != 0 && enabledTime <= ts && (disabledTime == 0 || disabledTime >= ts); } - // Timestamp must be always in the past - function _checkTimestampInThePast(uint48 ts) private view { + // Timestamp must be always in the past, but not too far, + // so that some operators or vaults can be already unregistered. + function _checkTimestamp(uint48 ts) private view { 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 index 8f5d6d4129e..dc7a527185e 100644 --- a/ethexe/contracts/src/libraries/MapWithTimeData.sol +++ b/ethexe/contracts/src/libraries/MapWithTimeData.sol @@ -13,6 +13,7 @@ library MapWithTimeData { 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)); } diff --git a/ethexe/contracts/test/Middleware.t.sol b/ethexe/contracts/test/Middleware.t.sol index ac67060a518..9800bfc783b 100644 --- a/ethexe/contracts/test/Middleware.t.sol +++ b/ethexe/contracts/test/Middleware.t.sol @@ -272,6 +272,12 @@ contract MiddlewareTest is Test { // 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 + uint48 ts = uint48(vm.getBlockTimestamp()); + vm.warp(vm.getBlockTimestamp() + eraDuration * 2); + vm.expectRevert(abi.encodeWithSelector(Middleware.IncorrectTimestamp.selector)); + middleware.getOperatorStakeAt(operator2, ts); } function _disableOperator(address operator) private {