From 427e6d34d23bc9918d03eaba07f2dac8b1f0e6e8 Mon Sep 17 00:00:00 2001 From: Gregory Sobol Date: Tue, 3 Dec 2024 17:55:21 +0100 Subject: [PATCH] feat(Gear.exe): impl initial election logic (#4362) --- .gitmodules | 4 + ethexe/contracts/.vscode/settings.json | 4 +- ethexe/contracts/foundry.toml | 2 + ethexe/contracts/lib/forge-std | 2 +- ethexe/contracts/lib/openzeppelin-contracts | 2 +- .../lib/openzeppelin-contracts-upgradeable | 2 +- .../lib/openzeppelin-foundry-upgrades | 2 +- ethexe/contracts/lib/symbiotic-core | 2 +- ethexe/contracts/src/Middleware.sol | 101 +++++++++++---- ethexe/contracts/test/Middleware.t.sol | 116 +++++++++++++++--- ethexe/contracts/test/Router.t.sol | 2 +- 11 files changed, 184 insertions(+), 55 deletions(-) diff --git a/.gitmodules b/.gitmodules index a2dd5d9bf4d..bf4ed3aba63 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,15 +1,19 @@ [submodule "ethexe/contracts/lib/forge-std"] path = ethexe/contracts/lib/forge-std url = https://github.com/foundry-rs/forge-std + branch = "v1" [submodule "ethexe/contracts/lib/openzeppelin-contracts"] path = ethexe/contracts/lib/openzeppelin-contracts url = https://github.com/OpenZeppelin/openzeppelin-contracts + branch = "release-v5.1" [submodule "ethexe/contracts/lib/openzeppelin-foundry-upgrades"] path = ethexe/contracts/lib/openzeppelin-foundry-upgrades url = https://github.com/OpenZeppelin/openzeppelin-foundry-upgrades + branch = "main" [submodule "ethexe/contracts/lib/openzeppelin-contracts-upgradeable"] path = ethexe/contracts/lib/openzeppelin-contracts-upgradeable url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable + branch = "release-v5.1" [submodule "ethexe/contracts/lib/symbiotic-core"] path = ethexe/contracts/lib/symbiotic-core url = https://github.com/grishasobol/symbiotic-core diff --git a/ethexe/contracts/.vscode/settings.json b/ethexe/contracts/.vscode/settings.json index 92ff8ef79c7..544c20a4fc3 100644 --- a/ethexe/contracts/.vscode/settings.json +++ b/ethexe/contracts/.vscode/settings.json @@ -6,5 +6,5 @@ "editor.defaultFormatter": "JuanBlanco.solidity" }, "solidity.formatter": "forge", - "solidity.compileUsingRemoteVersion": "v0.8.25" -} + "solidity.compileUsingRemoteVersion": "v0.8.28" +} \ No newline at end of file diff --git a/ethexe/contracts/foundry.toml b/ethexe/contracts/foundry.toml index f5cdbadb256..9bce0665b24 100644 --- a/ethexe/contracts/foundry.toml +++ b/ethexe/contracts/foundry.toml @@ -1,4 +1,5 @@ [profile.default] +solc_version = "0.8.28" src = "src" out = "out" libs = ["lib"] @@ -16,6 +17,7 @@ ignored_warnings_from = [ ] # Enable new EVM codegen via_ir = true +fs_permissions = [{access = "read-write", path = "out"}, { access = "read", path = "lib"}] [rpc_endpoints] sepolia = "${SEPOLIA_RPC_URL}" diff --git a/ethexe/contracts/lib/forge-std b/ethexe/contracts/lib/forge-std index 07263d193d6..1eea5bae12a 160000 --- a/ethexe/contracts/lib/forge-std +++ b/ethexe/contracts/lib/forge-std @@ -1 +1 @@ -Subproject commit 07263d193d621c4b2b0ce8b4d54af58f6957d97d +Subproject commit 1eea5bae12ae557d589f9f0f0edae2faa47cb262 diff --git a/ethexe/contracts/lib/openzeppelin-contracts b/ethexe/contracts/lib/openzeppelin-contracts index 4b33d326fa8..69c8def5f22 160000 --- a/ethexe/contracts/lib/openzeppelin-contracts +++ b/ethexe/contracts/lib/openzeppelin-contracts @@ -1 +1 @@ -Subproject commit 4b33d326fa818082649830b2dc8dab84419852d6 +Subproject commit 69c8def5f222ff96f2b5beff05dfba996368aa79 diff --git a/ethexe/contracts/lib/openzeppelin-contracts-upgradeable b/ethexe/contracts/lib/openzeppelin-contracts-upgradeable index 2bb98f7ae31..fa525310e45 160000 --- a/ethexe/contracts/lib/openzeppelin-contracts-upgradeable +++ b/ethexe/contracts/lib/openzeppelin-contracts-upgradeable @@ -1 +1 @@ -Subproject commit 2bb98f7ae310af7bc2554de59f8ed164f11c383e +Subproject commit fa525310e45f91eb20a6d3baa2644be8e0adba31 diff --git a/ethexe/contracts/lib/openzeppelin-foundry-upgrades b/ethexe/contracts/lib/openzeppelin-foundry-upgrades index 4cd15fc50b1..8e754fde23b 160000 --- a/ethexe/contracts/lib/openzeppelin-foundry-upgrades +++ b/ethexe/contracts/lib/openzeppelin-foundry-upgrades @@ -1 +1 @@ -Subproject commit 4cd15fc50b141c77d8cc9ff8efb44d00e841a299 +Subproject commit 8e754fde23b2b030a35bb47cd84b77dd42a44437 diff --git a/ethexe/contracts/lib/symbiotic-core b/ethexe/contracts/lib/symbiotic-core index 9cfc73f74b5..f71d470159b 160000 --- a/ethexe/contracts/lib/symbiotic-core +++ b/ethexe/contracts/lib/symbiotic-core @@ -1 +1 @@ -Subproject commit 9cfc73f74b568ad367284407d9b4dbca82a981eb +Subproject commit f71d470159b2466cfb8299d8e7b093717918f67d diff --git a/ethexe/contracts/src/Middleware.sol b/ethexe/contracts/src/Middleware.sol index 4670943efe9..ee2fd915733 100644 --- a/ethexe/contracts/src/Middleware.sol +++ b/ethexe/contracts/src/Middleware.sol @@ -18,12 +18,12 @@ import {IMigratableEntity} from "symbiotic-core/src/interfaces/common/IMigratabl import {MapWithTimeData} from "./libraries/MapWithTimeData.sol"; -// TODO: document all functions and variables -// TODO: implement election logic +// TODO (asap): document all functions and variables +// TODO (asap): implement rewards distribution +// TODO (asap): add validators commission // TODO: implement forced operators removal // TODO: implement forced vaults removal -// TODO: implement rewards distribution -// TODO: use hints for simbiotic calls +// TODO: use hints for symbiotic calls contract Middleware { using EnumerableMap for EnumerableMap.AddressToUintMap; using MapWithTimeData for EnumerableMap.AddressToUintMap; @@ -47,8 +47,8 @@ contract Middleware { error VetoDurationTooShort(); error VetoDurationTooLong(); error IncompatibleVaultVersion(); - error NotRegistredVault(); - error NotRegistredOperator(); + error NotRegisteredVault(); + error NotRegisteredOperator(); error RoleMismatch(); error ResolverMismatch(); error ResolverSetDelayTooLong(); @@ -72,7 +72,7 @@ contract Middleware { struct Config { uint48 eraDuration; uint48 minVaultEpochDuration; - uint48 operatoraGracePeriod; + uint48 operatorGracePeriod; uint48 vaultGracePeriod; uint48 minVetoDuration; uint48 minSlashExecutionDelay; @@ -94,7 +94,7 @@ contract Middleware { uint48 public immutable eraDuration; uint48 public immutable minVaultEpochDuration; - uint48 public immutable operatoraGracePeriod; + uint48 public immutable operatorGracePeriod; uint48 public immutable vaultGracePeriod; uint48 public immutable minVetoDuration; uint48 public immutable minSlashExecutionDelay; @@ -107,11 +107,12 @@ contract Middleware { address public immutable networkOptIn; address public immutable middlewareService; address public immutable collateral; - address public immutable roleSlashRequester; - address public immutable roleSlashExecutor; address public immutable vetoResolver; bytes32 public immutable subnetwork; + address public roleSlashRequester; + address public roleSlashExecutor; + EnumerableMap.AddressToUintMap private operators; EnumerableMap.AddressToUintMap private vaults; @@ -120,7 +121,7 @@ contract Middleware { eraDuration = cfg.eraDuration; minVaultEpochDuration = cfg.minVaultEpochDuration; - operatoraGracePeriod = cfg.operatoraGracePeriod; + operatorGracePeriod = cfg.operatorGracePeriod; vaultGracePeriod = cfg.vaultGracePeriod; minVetoDuration = cfg.minVetoDuration; minSlashExecutionDelay = cfg.minSlashExecutionDelay; @@ -144,6 +145,14 @@ contract Middleware { INetworkMiddlewareService(middlewareService).setMiddleware(address(this)); } + function changeSlashRequester(address newRole) external _onlyRole(roleSlashRequester) { + roleSlashRequester = newRole; + } + + function changeSlashExecutor(address newRole) external _onlyRole(roleSlashExecutor) { + roleSlashExecutor = newRole; + } + // TODO: Check that total stake is big enough function registerOperator() external { if (!IRegistry(operatorRegistry).isEntity(msg.sender)) { @@ -166,7 +175,7 @@ contract Middleware { function unregisterOperator(address operator) external { (, uint48 disabledTime) = operators.getTimes(operator); - if (disabledTime == 0 || Time.timestamp() < disabledTime + operatoraGracePeriod) { + if (disabledTime == 0 || Time.timestamp() < disabledTime + operatorGracePeriod) { revert OperatorGracePeriodNotPassed(); } @@ -267,12 +276,50 @@ contract Middleware { vaults.remove(vault); } - function getOperatorStakeAt(address operator, uint48 ts) - external - view - _validTimestamp(ts) - returns (uint256 stake) - { + function makeElectionAt(uint48 ts, uint256 maxValidators) public view returns (address[] memory) { + require(maxValidators > 0, "Max validators must be greater than zero"); + + (address[] memory activeOperators, uint256[] memory stakes) = getActiveOperatorsStakeAt(ts); + + if (activeOperators.length <= maxValidators) { + return activeOperators; + } + + // Bubble sort descending + uint256 n = activeOperators.length; + for (uint256 i = 0; i < n; i++) { + for (uint256 j = 0; j < n - 1 - i; j++) { + if (stakes[j] < stakes[j + 1]) { + (stakes[j], stakes[j + 1]) = (stakes[j + 1], stakes[j]); + (activeOperators[j], activeOperators[j + 1]) = (activeOperators[j + 1], activeOperators[j]); + } + } + } + + // Choose between validators with the same stake + uint256 sameStakeCount = 1; + uint256 lastStake = stakes[maxValidators - 1]; + for (uint256 i = maxValidators; i < activeOperators.length; i++) { + if (stakes[i] != lastStake) { + break; + } + sameStakeCount += 1; + } + + if (sameStakeCount > 1) { + // If there are multiple validators with the same stake, choose one randomly + uint256 randomIndex = uint256(keccak256(abi.encodePacked(ts))) % sameStakeCount; + activeOperators[maxValidators - 1] = activeOperators[maxValidators + randomIndex - 1]; + } + + assembly { + mstore(activeOperators, maxValidators) + } + + return activeOperators; + } + + function getOperatorStakeAt(address operator, uint48 ts) public view _validTimestamp(ts) returns (uint256 stake) { (uint48 enabledTime, uint48 disabledTime) = operators.getTimes(operator); if (!_wasActiveAt(enabledTime, disabledTime, ts)) { return 0; @@ -281,7 +328,7 @@ contract Middleware { stake = _collectOperatorStakeFromVaultsAt(operator, ts); } - // TODO: change return siggnature + // TODO: change return signature function getActiveOperatorsStakeAt(uint48 ts) public view @@ -315,14 +362,14 @@ contract Middleware { for (uint256 i; i < data.length; ++i) { SlashData calldata slashData = data[i]; if (!operators.contains(slashData.operator)) { - revert NotRegistredOperator(); + revert NotRegisteredOperator(); } for (uint256 j; j < slashData.vaults.length; ++j) { VaultSlashData calldata vaultData = slashData.vaults[j]; if (!vaults.contains(vaultData.vault)) { - revert NotRegistredVault(); + revert NotRegisteredVault(); } address slasher = IVault(vaultData.vault).slasher(); @@ -338,7 +385,7 @@ contract Middleware { SlashIdentifier calldata slash = slashes[i]; if (!vaults.contains(slash.vault)) { - revert NotRegistredVault(); + revert NotRegisteredVault(); } IVetoSlasher(IVault(slash.vault).slasher()).executeSlash(slash.index, new bytes(0)); @@ -388,7 +435,7 @@ contract Middleware { // Operator grace period cannot be smaller than minimum vaults epoch duration. // Otherwise, it would be impossible to do slash in the next era sometimes. require( - cfg.operatoraGracePeriod >= cfg.minVaultEpochDuration, + cfg.operatorGracePeriod >= cfg.minVaultEpochDuration, "Operator grace period must be bigger than min vaults epoch duration" ); @@ -402,8 +449,8 @@ contract Middleware { // Give some time for the resolvers to veto slashes. require(cfg.minVetoDuration > 0, "Veto duration cannot be zero"); - // Simbiotic guarantees that any veto slasher has veto duration less than vault epoch duration. - // But we also want to guaratie that there is some time to execute the slash. + // Symbiotic guarantees that any veto slasher has veto duration less than vault epoch duration. + // But we also want to guarantee that there is some time to execute the slash. require(cfg.minSlashExecutionDelay > 0, "Min slash execution delay cannot be zero"); require( cfg.minVetoDuration + cfg.minSlashExecutionDelay <= cfg.minVaultEpochDuration, @@ -411,7 +458,7 @@ contract Middleware { ); // In order to be able to change resolver, we need to limit max delay in epochs. - // `3` - is minimal number of epochs, which is simbiotic veto slasher impl restrictions. + // `3` - is minimal number of epochs, which is symbiotic veto slasher impl restrictions. require(cfg.maxResolverSetEpochsDelay >= 3, "Resolver set epochs delay must be at least 3"); } @@ -422,7 +469,7 @@ contract Middleware { revert IncorrectTimestamp(); } - uint48 gracePeriod = operatoraGracePeriod < vaultGracePeriod ? operatoraGracePeriod : vaultGracePeriod; + uint48 gracePeriod = operatorGracePeriod < vaultGracePeriod ? operatorGracePeriod : vaultGracePeriod; if (ts + gracePeriod <= Time.timestamp()) { revert IncorrectTimestamp(); } diff --git a/ethexe/contracts/test/Middleware.t.sol b/ethexe/contracts/test/Middleware.t.sol index a8852b1c080..ad0a4b152ee 100644 --- a/ethexe/contracts/test/Middleware.t.sol +++ b/ethexe/contracts/test/Middleware.t.sol @@ -21,26 +21,29 @@ import {Middleware} from "../src/Middleware.sol"; import {WrappedVara} from "../src/WrappedVara.sol"; import {MapWithTimeData} from "../src/libraries/MapWithTimeData.sol"; -contract MiddlewareTest is Test { +contract MiddlewareTest is Test, POCBaseTest { using MessageHashUtils for address; uint48 eraDuration = 1000; - address public owner; POCBaseTest public sym; Middleware public middleware; WrappedVara public wrappedVara; - function setUp() public { - // For correct simbiotic work with time artitmeticks + function setUp() public override { + // For correct symbiotic work with time arithmetics vm.warp(eraDuration * 100); - sym = new POCBaseTest(); - sym.setUp(); + // set up the symbiotic ecosystem + SYMBIOTIC_CORE_PROJECT_ROOT = "lib/symbiotic-core/"; + super.setUp(); - owner = address(this); + // For understanding where symbiotic ecosystem is using + sym = POCBaseTest(address(this)); wrappedVara = WrappedVara( - Upgrades.deployTransparentProxy("WrappedVara.sol", owner, abi.encodeCall(WrappedVara.initialize, (owner))) + Upgrades.deployTransparentProxy( + "WrappedVara.sol", address(0xdead), abi.encodeCall(WrappedVara.initialize, (owner)) + ) ); wrappedVara.mint(owner, 1_000_000); @@ -48,13 +51,13 @@ contract MiddlewareTest is Test { Middleware.Config memory cfg = Middleware.Config({ eraDuration: eraDuration, minVaultEpochDuration: eraDuration * 2, - operatoraGracePeriod: eraDuration * 2, + operatorGracePeriod: eraDuration * 2, vaultGracePeriod: eraDuration * 2, minVetoDuration: eraDuration / 3, minSlashExecutionDelay: eraDuration / 3, maxResolverSetEpochsDelay: type(uint256).max, vaultRegistry: address(sym.vaultFactory()), - allowedVaultImplVersion: sym.vaultFactory().lastVersion(), + allowedVaultImplVersion: 1, vetoSlasherImplType: 1, operatorRegistry: address(sym.operatorRegistry()), networkRegistry: address(sym.networkRegistry()), @@ -75,6 +78,79 @@ contract MiddlewareTest is Test { assertEq(sym.networkMiddlewareService().middleware(address(middleware)), address(middleware)); } + function test_election() public { + address[] memory operators = new address[](5); + address[] memory vaults = new address[](operators.length); + + for (uint256 i = 0; i < operators.length; i++) { + operators[i] = address(uint160(i) + 0x1000); + _createOperator(operators[i]); + vaults[i] = _createVaultForOperator(operators[i]); + } + + _depositFromInVault(owner, vaults[0], 1_000); + _depositFromInVault(owner, vaults[1], 2_000); + _depositFromInVault(owner, vaults[2], 1_000); + _depositFromInVault(owner, vaults[3], 5_000); + _depositFromInVault(owner, vaults[4], 1_000); + + vm.warp(vm.getBlockTimestamp() + 1000); + + vm.expectRevert(abi.encodeWithSelector(Middleware.IncorrectTimestamp.selector)); + middleware.makeElectionAt(uint48(vm.getBlockTimestamp()), 10); + + vm.expectRevert(); + middleware.makeElectionAt(uint48(vm.getBlockTimestamp()) - 1, 0); + + { + address[] memory res = middleware.makeElectionAt(uint48(vm.getBlockTimestamp() - 1), 1); + assertEq(res.length, 1); + assertEq(res[0], operators[3]); + } + + { + address[] memory res = middleware.makeElectionAt(uint48(vm.getBlockTimestamp() - 1), 2); + assertEq(res.length, 2); + assertEq(res[0], operators[3]); + assertEq(res[1], operators[1]); + } + + { + address[] memory res = middleware.makeElectionAt(uint48(vm.getBlockTimestamp() - 1), 3); + assertEq(res.length, 3); + assertEq(res[0], operators[3]); + assertEq(res[1], operators[1]); + assertTrue(res[2] == operators[0] || res[2] == operators[2] || res[2] == operators[4]); + } + + { + address[] memory res = middleware.makeElectionAt(uint48(vm.getBlockTimestamp() - 1), 4); + assertEq(res.length, 4); + assertEq(res[0], operators[3]); + assertEq(res[1], operators[1]); + assertTrue(res[2] == operators[0] || res[2] == operators[2] || res[2] == operators[4]); + assertTrue((res[3] == operators[3] || res[3] == operators[2] || res[3] == operators[4]) && res[3] != res[2]); + } + + { + address[] memory res = middleware.makeElectionAt(uint48(vm.getBlockTimestamp() - 1), 5); + assertEq(res.length, operators.length); + // In that case not sorted by stake + for (uint256 i; i < operators.length; i++) { + assertEq(res[i], operators[i]); + } + } + + { + address[] memory res = middleware.makeElectionAt(uint48(vm.getBlockTimestamp() - 1), 6); + assertEq(res.length, operators.length); + // In that case not sorted by stake + for (uint256 i; i < operators.length; i++) { + assertEq(res[i], operators[i]); + } + } + } + // TODO: split to multiple tests function test_registerOperator() public { // Register operator @@ -87,7 +163,7 @@ contract MiddlewareTest is Test { vm.expectRevert(abi.encodeWithSelector(MapWithTimeData.AlreadyAdded.selector)); middleware.registerOperator(); - // Try to register abother operator without registering it in symbiotic + // Try to register another operator without registering it in symbiotic vm.startPrank(address(0x2)); vm.expectRevert(abi.encodeWithSelector(Middleware.OperatorDoesNotExist.selector)); middleware.registerOperator(); @@ -321,7 +397,7 @@ contract MiddlewareTest is Test { // Try to request slash from unknown operator vm.warp(vm.getBlockTimestamp() + 1); _requestSlash( - address(0xdead), uint48(vm.getBlockTimestamp() - 1), vault1, 100, Middleware.NotRegistredOperator.selector + address(0xdead), uint48(vm.getBlockTimestamp() - 1), vault1, 100, Middleware.NotRegisteredOperator.selector ); } @@ -330,7 +406,7 @@ contract MiddlewareTest is Test { // Try to request slash from unknown vault _requestSlash( - operator1, uint48(vm.getBlockTimestamp() - 1), address(0xdead), 100, Middleware.NotRegistredVault.selector + operator1, uint48(vm.getBlockTimestamp() - 1), address(0xdead), 100, Middleware.NotRegisteredVault.selector ); } @@ -383,7 +459,7 @@ contract MiddlewareTest is Test { function test_slashTwoOperatorsTwoVaults() external { (address operator1, address operator2, address vault1, address vault2,,) = _prepareTwoOperators(); - // Request slases for 2 operators with corresponding vaults + // Request slashes for 2 operators with corresponding vaults Middleware.VaultSlashData[] memory operator1_vaults = new Middleware.VaultSlashData[](1); operator1_vaults[0] = Middleware.VaultSlashData({vault: vault1, amount: 10}); @@ -428,14 +504,14 @@ contract MiddlewareTest is Test { IVetoSlasher(slasher).vetoSlash(slashIndex, new bytes(0)); } - function test_slashExecutionUnregistredVault() external { + function test_slashExecutionUnregisteredVault() external { (address operator1,, address vault1,,,) = _prepareTwoOperators(); // Make slash request for operator1 in vault1 uint256 slashIndex = _requestSlash(operator1, uint48(vm.getBlockTimestamp() - 1), vault1, 100, 0); // Try to execute slash for unknown vault - vm.expectRevert(Middleware.NotRegistredVault.selector); + vm.expectRevert(Middleware.NotRegisteredVault.selector); _executeSlash(address(0xdead), slashIndex); } @@ -452,8 +528,8 @@ contract MiddlewareTest is Test { operator1 = address(0x1); operator2 = address(0x2); - _registerOperator(operator1); - _registerOperator(operator2); + _createOperator(operator1); + _createOperator(operator2); vault1 = _createVaultForOperator(operator1); vault2 = _createVaultForOperator(operator2); @@ -536,7 +612,7 @@ contract MiddlewareTest is Test { vm.stopPrank(); } - function _registerOperator(address operator) private { + function _createOperator(address operator) internal { vm.startPrank(operator); sym.operatorRegistry().registerOperator(); sym.operatorNetworkOptInService().optIn(address(middleware)); @@ -571,7 +647,7 @@ contract MiddlewareTest is Test { (vault,,) = sym.vaultConfigurator().create( IVaultConfigurator.InitParams({ - version: sym.vaultFactory().lastVersion(), + version: 1, owner: operator, vaultParams: abi.encode( IVault.InitParams({ diff --git a/ethexe/contracts/test/Router.t.sol b/ethexe/contracts/test/Router.t.sol index 9e685766a37..ece9a2e8b0e 100644 --- a/ethexe/contracts/test/Router.t.sol +++ b/ethexe/contracts/test/Router.t.sol @@ -75,7 +75,7 @@ contract RouterTest is Test { assertEq(router.validators(), validators); assertEq(router.signingThresholdPercentage(), 6666); - assert(router.areValidators(validators)); + assertTrue(router.areValidators(validators)); } function test_ping() public {