diff --git a/ethexe/contracts/src/Middleware.sol b/ethexe/contracts/src/Middleware.sol index af84f2ca125..4670943efe9 100644 --- a/ethexe/contracts/src/Middleware.sol +++ b/ethexe/contracts/src/Middleware.sol @@ -12,21 +12,23 @@ 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 {INetworkMiddlewareService} from "symbiotic-core/src/interfaces/service/INetworkMiddlewareService.sol"; +import {IVetoSlasher} from "symbiotic-core/src/interfaces/slasher/IVetoSlasher.sol"; +import {IMigratableEntity} from "symbiotic-core/src/interfaces/common/IMigratableEntity.sol"; import {MapWithTimeData} from "./libraries/MapWithTimeData.sol"; -// TODO: support slashing -// TODO: use camelCase for immutable variables +// TODO: document all functions and variables // TODO: implement election logic // TODO: implement forced operators removal // TODO: implement forced vaults removal // TODO: implement rewards distribution +// TODO: use hints for simbiotic calls contract Middleware { using EnumerableMap for EnumerableMap.AddressToUintMap; using MapWithTimeData for EnumerableMap.AddressToUintMap; using Subnetwork for address; - error ZeroVaultAddress(); error NotKnownVault(); error VaultWrongEpochDuration(); error UnknownCollateral(); @@ -36,57 +38,118 @@ contract Middleware { error IncorrectTimestamp(); error OperatorDoesNotExist(); error OperatorDoesNotOptIn(); + error UnsupportedHook(); + error UnsupportedBurner(); + error DelegatorNotInitialized(); + error SlasherNotInitialized(); + error IncompatibleSlasherType(); + error BurnerHookNotSupported(); + error VetoDurationTooShort(); + error VetoDurationTooLong(); + error IncompatibleVaultVersion(); + error NotRegistredVault(); + error NotRegistredOperator(); + error RoleMismatch(); + error ResolverMismatch(); + error ResolverSetDelayTooLong(); + + struct VaultSlashData { + address vault; + uint256 amount; + } + + struct SlashData { + address operator; + uint48 ts; + VaultSlashData[] vaults; + } + + struct SlashIdentifier { + address vault; + uint256 index; + } + + struct Config { + uint48 eraDuration; + uint48 minVaultEpochDuration; + uint48 operatoraGracePeriod; + uint48 vaultGracePeriod; + uint48 minVetoDuration; + uint48 minSlashExecutionDelay; + uint256 maxResolverSetEpochsDelay; + address vaultRegistry; + uint64 allowedVaultImplVersion; + uint64 vetoSlasherImplType; + address operatorRegistry; + address networkRegistry; + address networkOptIn; + address middlewareService; + address collateral; + address roleSlashRequester; + address roleSlashExecutor; + address vetoResolver; + } 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; + uint48 public immutable eraDuration; + uint48 public immutable minVaultEpochDuration; + uint48 public immutable operatoraGracePeriod; + uint48 public immutable vaultGracePeriod; + uint48 public immutable minVetoDuration; + uint48 public immutable minSlashExecutionDelay; + uint256 public immutable maxResolverSetEpochsDelay; + address public immutable vaultRegistry; + uint64 public immutable allowedVaultImplVersion; + uint64 public immutable vetoSlasherImplType; + address public immutable operatorRegistry; + address public immutable networkRegistry; + 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; 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); - + constructor(Config memory cfg) { + _validateConfiguration(cfg); + + eraDuration = cfg.eraDuration; + minVaultEpochDuration = cfg.minVaultEpochDuration; + operatoraGracePeriod = cfg.operatoraGracePeriod; + vaultGracePeriod = cfg.vaultGracePeriod; + minVetoDuration = cfg.minVetoDuration; + minSlashExecutionDelay = cfg.minSlashExecutionDelay; + maxResolverSetEpochsDelay = cfg.maxResolverSetEpochsDelay; + vaultRegistry = cfg.vaultRegistry; + allowedVaultImplVersion = cfg.allowedVaultImplVersion; + vetoSlasherImplType = cfg.vetoSlasherImplType; + operatorRegistry = cfg.operatorRegistry; + networkRegistry = cfg.networkRegistry; + networkOptIn = cfg.networkOptIn; + middlewareService = cfg.middlewareService; + collateral = cfg.collateral; + roleSlashRequester = cfg.roleSlashRequester; + roleSlashExecutor = cfg.roleSlashExecutor; + vetoResolver = cfg.vetoResolver; + + subnetwork = address(this).subnetwork(NETWORK_IDENTIFIER); + + // Presently network and middleware are the same address INetworkRegistry(networkRegistry).registerNetwork(); + INetworkMiddlewareService(middlewareService).setMiddleware(address(this)); } // TODO: Check that total stake is big enough function registerOperator() external { - if (!IRegistry(OPERATOR_REGISTRY).isEntity(msg.sender)) { + if (!IRegistry(operatorRegistry).isEntity(msg.sender)) { revert OperatorDoesNotExist(); } - if (!IOptInService(NETWORK_OPT_IN).isOptedIn(msg.sender, address(this))) { + if (!IOptInService(networkOptIn).isOptedIn(msg.sender, address(this))) { revert OperatorDoesNotOptIn(); } operators.append(msg.sender, 0); @@ -103,7 +166,7 @@ contract Middleware { function unregisterOperator(address operator) external { (, uint48 disabledTime) = operators.getTimes(operator); - if (disabledTime == 0 || Time.timestamp() < disabledTime + OPERATOR_GRACE_PERIOD) { + if (disabledTime == 0 || Time.timestamp() < disabledTime + operatoraGracePeriod) { revert OperatorGracePeriodNotPassed(); } @@ -111,28 +174,65 @@ contract Middleware { } // TODO: check vault has enough stake - // TODO: support and check slasher function registerVault(address vault) external { - if (vault == address(0)) { - revert ZeroVaultAddress(); + if (!IRegistry(vaultRegistry).isEntity(vault)) { + revert NotKnownVault(); } - if (!IRegistry(VAULT_FACTORY).isEntity(vault)) { - revert NotKnownVault(); + if (IMigratableEntity(vault).version() != allowedVaultImplVersion) { + revert IncompatibleVaultVersion(); } - if (IVault(vault).epochDuration() < VAULT_MIN_EPOCH_DURATION) { + uint48 vaultEpochDuration = IVault(vault).epochDuration(); + if (vaultEpochDuration < minVaultEpochDuration) { revert VaultWrongEpochDuration(); } - if (IVault(vault).collateral() != COLLATERAL) { + if (IVault(vault).collateral() != collateral) { revert UnknownCollateral(); } + if (!IVault(vault).isDelegatorInitialized()) { + revert DelegatorNotInitialized(); + } + + if (!IVault(vault).isSlasherInitialized()) { + revert SlasherNotInitialized(); + } + IBaseDelegator delegator = IBaseDelegator(IVault(vault).delegator()); - if (delegator.maxNetworkLimit(SUBNETWORK) != type(uint256).max) { + if (delegator.maxNetworkLimit(subnetwork) != type(uint256).max) { delegator.setMaxNetworkLimit(NETWORK_IDENTIFIER, type(uint256).max); } + _delegatorHookCheck(IBaseDelegator(delegator).hook()); + + address slasher = IVault(vault).slasher(); + if (IEntity(slasher).TYPE() != vetoSlasherImplType) { + revert IncompatibleSlasherType(); + } + if (IVetoSlasher(slasher).isBurnerHook()) { + revert BurnerHookNotSupported(); + } + uint48 vetoDuration = IVetoSlasher(slasher).vetoDuration(); + if (vetoDuration < minVetoDuration) { + revert VetoDurationTooShort(); + } + if (vetoDuration + minSlashExecutionDelay > vaultEpochDuration) { + revert VetoDurationTooLong(); + } + if (IVetoSlasher(slasher).resolverSetEpochsDelay() > maxResolverSetEpochsDelay) { + revert ResolverSetDelayTooLong(); + } + + address resolver = IVetoSlasher(slasher).resolver(subnetwork, new bytes(0)); + if (resolver == address(0)) { + IVetoSlasher(slasher).setResolver(NETWORK_IDENTIFIER, vetoResolver, new bytes(0)); + } else if (resolver != vetoResolver) { + // TODO: consider how to support this case + revert ResolverMismatch(); + } + + _burnerCheck(IVault(vault).burner()); vaults.append(vault, uint160(msg.sender)); } @@ -160,7 +260,7 @@ contract Middleware { function unregisterVault(address vault) external { (, uint48 disabledTime) = vaults.getTimes(vault); - if (disabledTime == 0 || Time.timestamp() < disabledTime + VAULT_GRACE_PERIOD) { + if (disabledTime == 0 || Time.timestamp() < disabledTime + vaultGracePeriod) { revert VaultGracePeriodNotPassed(); } @@ -181,6 +281,7 @@ contract Middleware { stake = _collectOperatorStakeFromVaultsAt(operator, ts); } + // TODO: change return siggnature function getActiveOperatorsStakeAt(uint48 ts) public view @@ -210,6 +311,40 @@ contract Middleware { } } + function requestSlash(SlashData[] calldata data) external _onlyRole(roleSlashRequester) { + for (uint256 i; i < data.length; ++i) { + SlashData calldata slashData = data[i]; + if (!operators.contains(slashData.operator)) { + revert NotRegistredOperator(); + } + + for (uint256 j; j < slashData.vaults.length; ++j) { + VaultSlashData calldata vaultData = slashData.vaults[j]; + + if (!vaults.contains(vaultData.vault)) { + revert NotRegistredVault(); + } + + address slasher = IVault(vaultData.vault).slasher(); + IVetoSlasher(slasher).requestSlash( + subnetwork, slashData.operator, vaultData.amount, slashData.ts, new bytes(0) + ); + } + } + } + + function executeSlash(SlashIdentifier[] calldata slashes) external _onlyRole(roleSlashExecutor) { + for (uint256 i; i < slashes.length; ++i) { + SlashIdentifier calldata slash = slashes[i]; + + if (!vaults.contains(slash.vault)) { + revert NotRegistredVault(); + } + + IVetoSlasher(IVault(slash.vault).slasher()).executeSlash(slash.index, new bytes(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); @@ -218,7 +353,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)); } } @@ -226,6 +361,60 @@ contract Middleware { return enabledTime != 0 && enabledTime <= ts && (disabledTime == 0 || disabledTime >= ts); } + // Supports only null hook for now + function _delegatorHookCheck(address hook) private pure { + if (hook != address(0)) { + revert UnsupportedHook(); + } + } + + // Supports only null burner for now + function _burnerCheck(address burner) private pure { + if (burner == address(0)) { + revert UnsupportedBurner(); + } + } + + function _validateConfiguration(Config memory cfg) private pure { + require(cfg.eraDuration > 0, "Era duration cannot be zero"); + + // Middleware must support cases when election for next era is made before the start of the next era, + // so the min vaults epoch duration must be bigger than `eraDuration + electionDelay`. + // The election delay is less than or equal to the era duration, so limit `2 * eraDuration` is enough. + require( + cfg.minVaultEpochDuration >= 2 * cfg.eraDuration, "Min vaults epoch duration must be bigger than 2 eras" + ); + + // 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, + "Operator grace period must be bigger than min vaults epoch duration" + ); + + // Vault 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.vaultGracePeriod >= cfg.minVaultEpochDuration, + "Vault grace period must be bigger than min vaults epoch duration" + ); + + // 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. + require(cfg.minSlashExecutionDelay > 0, "Min slash execution delay cannot be zero"); + require( + cfg.minVetoDuration + cfg.minSlashExecutionDelay <= cfg.minVaultEpochDuration, + "Veto duration and slash execution delay must be less than ot equal to min vaults epoch duration" + ); + + // 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. + require(cfg.maxResolverSetEpochsDelay >= 3, "Resolver set epochs delay must be at least 3"); + } + // 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) { @@ -233,11 +422,18 @@ contract Middleware { revert IncorrectTimestamp(); } - uint48 gracePeriod = OPERATOR_GRACE_PERIOD < VAULT_GRACE_PERIOD ? OPERATOR_GRACE_PERIOD : VAULT_GRACE_PERIOD; + uint48 gracePeriod = operatoraGracePeriod < vaultGracePeriod ? operatoraGracePeriod : vaultGracePeriod; if (ts + gracePeriod <= Time.timestamp()) { revert IncorrectTimestamp(); } _; } + + modifier _onlyRole(address role) { + if (msg.sender != role) { + revert RoleMismatch(); + } + _; + } } diff --git a/ethexe/contracts/test/Middleware.t.sol b/ethexe/contracts/test/Middleware.t.sol index ea1ba74559a..196fc04e7c0 100644 --- a/ethexe/contracts/test/Middleware.t.sol +++ b/ethexe/contracts/test/Middleware.t.sol @@ -5,14 +5,17 @@ import {EnumerableMap} from "@openzeppelin/contracts/utils/structs/EnumerableMap 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 {Vm} from "forge-std/Vm.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 {IVetoSlasher} from "symbiotic-core/src/interfaces/slasher/IVetoSlasher.sol"; +import {IBaseSlasher} from "symbiotic-core/src/interfaces/slasher/IBaseSlasher.sol"; import {Middleware} from "../src/Middleware.sol"; import {WrappedVara} from "../src/WrappedVara.sol"; @@ -28,6 +31,9 @@ contract MiddlewareTest is Test { WrappedVara public wrappedVara; function setUp() public { + // For correct simbiotic work with time artitmeticks + vm.warp(eraDuration * 100); + sym = new POCBaseTest(); sym.setUp(); @@ -39,33 +45,37 @@ contract MiddlewareTest is Test { 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) - ); + Middleware.Config memory cfg = Middleware.Config({ + eraDuration: eraDuration, + minVaultEpochDuration: eraDuration * 2, + operatoraGracePeriod: eraDuration * 2, + vaultGracePeriod: eraDuration * 2, + minVetoDuration: eraDuration / 3, + minSlashExecutionDelay: eraDuration / 3, + maxResolverSetEpochsDelay: type(uint256).max, + vaultRegistry: address(sym.vaultFactory()), + allowedVaultImplVersion: sym.vaultFactory().lastVersion(), + vetoSlasherImplType: 1, + operatorRegistry: address(sym.operatorRegistry()), + networkRegistry: address(sym.networkRegistry()), + networkOptIn: address(sym.operatorNetworkOptInService()), + middlewareService: address(sym.networkMiddlewareService()), + collateral: address(wrappedVara), + roleSlashRequester: owner, + roleSlashExecutor: owner, + vetoResolver: owner + }); + + middleware = new Middleware(cfg); } + // TODO: sync with the latest version of the middleware 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)); + assertEq(sym.networkMiddlewareService().middleware(address(middleware)), address(middleware)); } + // TODO: split to multiple tests function test_registerOperator() public { // Register operator vm.startPrank(address(0x1)); @@ -91,7 +101,7 @@ contract MiddlewareTest is Test { sym.operatorNetworkOptInService().optIn(address(middleware)); middleware.registerOperator(); - // Disable operator and the enable it + // Disable operator and then enable it middleware.disableOperator(); middleware.enableOperator(); @@ -114,6 +124,9 @@ contract MiddlewareTest is Test { middleware.unregisterOperator(address(0x2)); } + // TODO: split to multiple tests + // TODO: check vault has valid network params + // TODO: test when vault has incorrect network params function test_registerVault() public { sym.operatorRegistry().registerOperator(); address vault = _newVault(eraDuration * 2, owner); @@ -121,13 +134,9 @@ contract MiddlewareTest is Test { // 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)); + middleware.registerVault(address(0xdead)); // Try to register vault with wrong epoch duration address vault2 = _newVault(eraDuration, owner); @@ -191,92 +200,321 @@ contract MiddlewareTest is Test { middleware.unregisterVault(address(0x1)); } - function test_operatorStake() public { - address operator1 = address(0x1); - address operator2 = address(0x2); - - _registerOperator(operator1); - _registerOperator(operator2); + function test_stake() public { + (address operator1, address operator2,,, uint256 stake1, uint256 stake2) = _prepareTwoOperators(); - address vault1 = _createVaultForOperator(operator1); - address vault2 = _createVaultForOperator(operator2); + uint48 ts = uint48(vm.getBlockTimestamp() - 1); - uint256 stake1 = 1_000; - uint256 stake2 = 2_000; - uint256 stake3 = 3_000; + // Check operator stake after depositing + assertEq(middleware.getOperatorStakeAt(operator1, ts), stake1); + assertEq(middleware.getOperatorStakeAt(operator2, ts), stake2); - _depositFromInVault(owner, vault1, stake1); - _depositFromInVault(owner, vault2, stake2); + // Check active operators + (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); + } - { - // 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); - } + function test_stakeOperatorWithTwoVaults() public { + (address operator1,, address vault1,, uint256 stake1,) = _prepareTwoOperators(); // 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 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); - } + // Check after depositing to new vault + uint256 stake3 = 3_000; + _depositFromInVault(owner, vault3, stake3); + ts = uint48(vm.getBlockTimestamp()); + vm.warp(vm.getBlockTimestamp() + 1); + assertEq(middleware.getOperatorStakeAt(operator1, ts), stake1 + stake3); + + // Disable vault1 and check operator1 stake + _disableVault(operator1, vault1); + // Disable is not immediate, so we need to check for the next block ts + ts = uint48(vm.getBlockTimestamp()) + 1; + vm.warp(vm.getBlockTimestamp() + 2); + assertEq(middleware.getOperatorStakeAt(operator1, ts), 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); - } + function test_stakeDisabledOperator() public { + (address operator1, address operator2,,,, uint256 stake2) = _prepareTwoOperators(); + + // Disable operator1 and check operator1 stake is 0 + _disableOperator(operator1); + // Disable is not immediate, so we need to check for the next block ts + 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); + } - { - // 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); - } + function test_stakeTooOldTimestamp() public { + (address operator1,,,,,) = _prepareTwoOperators(); + + // 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(operator1, ts); + } + + function test_stakeCurrentTimestamp() public { + (address operator1,,,,,) = _prepareTwoOperators(); // Try to get stake for current timestamp vm.expectRevert(abi.encodeWithSelector(Middleware.IncorrectTimestamp.selector)); - middleware.getOperatorStakeAt(operator2, uint48(vm.getBlockTimestamp())); + middleware.getOperatorStakeAt(operator1, uint48(vm.getBlockTimestamp())); + } + + function test_stakeFutureTimestamp() public { + (address operator1,,,,,) = _prepareTwoOperators(); // Try to get stake for future timestamp vm.expectRevert(abi.encodeWithSelector(Middleware.IncorrectTimestamp.selector)); - middleware.getOperatorStakeAt(operator2, uint48(vm.getBlockTimestamp() + 1)); + middleware.getOperatorStakeAt(operator1, 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 test_slash() external { + (address operator1,, address vault1,, uint256 stake1,) = _prepareTwoOperators(); + + // Make slash request for operator1 in vault1 + uint256 slashIndex = _requestSlash(operator1, uint48(vm.getBlockTimestamp() - 1), vault1, 100, 0); + uint48 vetoDeadline = _vetoDeadline(IVault(vault1).slasher(), slashIndex); + assertEq(vetoDeadline, uint48(vm.getBlockTimestamp() + eraDuration / 2)); + + // Try to execute slash before veto deadline + vm.warp(vetoDeadline - 1); + vm.expectRevert(IVetoSlasher.VetoPeriodNotEnded.selector); + _executeSlash(vault1, slashIndex); + + // Execute slash when ready + vm.warp(vetoDeadline); + _executeSlash(vault1, slashIndex); + + // Check that operator1 stake is decreased + vm.warp(vetoDeadline + 1); + assertEq(middleware.getOperatorStakeAt(operator1, vetoDeadline), stake1 - 100); + + // Try to execute slash twice + vm.expectRevert(IVetoSlasher.SlashRequestCompleted.selector); + _executeSlash(vault1, slashIndex); + } + + function test_slashRequestUnknownOperator() external { + (,, address vault1,,,) = _prepareTwoOperators(); + + // Try to request slash from unknown operator + vm.warp(vm.getBlockTimestamp() + 1); + _requestSlash( + address(0xdead), uint48(vm.getBlockTimestamp() - 1), vault1, 100, Middleware.NotRegistredOperator.selector + ); + } + + function test_slashRequestUnknownVault() external { + (address operator1,,,,,) = _prepareTwoOperators(); + + // Try to request slash from unknown vault + _requestSlash( + operator1, uint48(vm.getBlockTimestamp() - 1), address(0xdead), 100, Middleware.NotRegistredVault.selector + ); + } + + function test_slashRequestOnVaultWithNoStake() external { + (address operator1,,, address vault2,,) = _prepareTwoOperators(); + + // Try to request slash on vault where it has no stake + _requestSlash( + operator1, uint48(vm.getBlockTimestamp() - 1), vault2, 10, IVetoSlasher.InsufficientSlash.selector + ); + } + + function test_slashAfterSlashPeriod() 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 slash after slash period + vm.warp(uint48(vm.getBlockTimestamp()) + IVault(vault1).epochDuration()); + vm.expectRevert(IVetoSlasher.SlashPeriodEnded.selector); + _executeSlash(vault1, slashIndex); + } + + function test_slashOneOperatorTwoVaults() external { + (address operator1,, address vault1, address vault2,,) = _prepareTwoOperators(); + + // Try request slashes for one operator, but 2 vaults + Middleware.VaultSlashData[] memory vaults = new Middleware.VaultSlashData[](2); + vaults[0] = Middleware.VaultSlashData({vault: vault1, amount: 10}); + vaults[1] = Middleware.VaultSlashData({vault: vault2, amount: 20}); + + Middleware.SlashData[] memory slashes = new Middleware.SlashData[](1); + slashes[0] = Middleware.SlashData({operator: operator1, ts: uint48(vm.getBlockTimestamp() - 1), vaults: vaults}); + + _requestSlash(slashes, IVetoSlasher.InsufficientSlash.selector); + + // Make one more vault for operator1 + address vault3 = _createVaultForOperator(operator1); + _depositFromInVault(owner, vault3, 3_000); + + vm.warp(vm.getBlockTimestamp() + 1); + + // Request slashes with correct vaults + vaults[1] = Middleware.VaultSlashData({vault: vault3, amount: 30}); + slashes[0] = Middleware.SlashData({operator: operator1, ts: uint48(vm.getBlockTimestamp() - 1), vaults: vaults}); + _requestSlash(slashes, 0); + } + + function test_slashTwoOperatorsTwoVaults() external { + (address operator1, address operator2, address vault1, address vault2,,) = _prepareTwoOperators(); + + // Request slases for 2 operators with corresponding vaults + Middleware.VaultSlashData[] memory operator1_vaults = new Middleware.VaultSlashData[](1); + operator1_vaults[0] = Middleware.VaultSlashData({vault: vault1, amount: 10}); + + Middleware.VaultSlashData[] memory operator2_vaults = new Middleware.VaultSlashData[](1); + operator2_vaults[0] = Middleware.VaultSlashData({vault: vault2, amount: 20}); + + Middleware.SlashData[] memory slashes = new Middleware.SlashData[](2); + slashes[0] = Middleware.SlashData({ + operator: operator1, + ts: uint48(vm.getBlockTimestamp() - 1), + vaults: operator1_vaults + }); + slashes[1] = Middleware.SlashData({ + operator: operator2, + ts: uint48(vm.getBlockTimestamp() - 1), + vaults: operator2_vaults + }); + + _requestSlash(slashes, 0); + } + + function test_slashVeto() external { + (address operator1,, address vault1,,,) = _prepareTwoOperators(); + + // Make slash request for operator1 in vault1 + uint256 slashIndex = _requestSlash(operator1, uint48(vm.getBlockTimestamp() - 1), vault1, 100, 0); + uint48 vetoDeadline = _vetoDeadline(IVault(vault1).slasher(), slashIndex); + + address slasher = IVault(vault1).slasher(); + + // Try to execute slash after veto deadline + vm.warp(vetoDeadline); + vm.expectRevert(IVetoSlasher.VetoPeriodEnded.selector); + IVetoSlasher(slasher).vetoSlash(slashIndex, new bytes(0)); + + // Veto slash + vm.warp(vetoDeadline - 1); + IVetoSlasher(slasher).vetoSlash(slashIndex, new bytes(0)); + + // Try to execute slash after veto is done + vm.expectRevert(IVetoSlasher.SlashRequestCompleted.selector); + IVetoSlasher(slasher).vetoSlash(slashIndex, new bytes(0)); + } + + function test_slashExecutionUnregistredVault() 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); + _executeSlash(address(0xdead), slashIndex); + } + + function _executeSlash(address vault, uint256 index) private { + Middleware.SlashIdentifier[] memory slashes = new Middleware.SlashIdentifier[](1); + slashes[0] = Middleware.SlashIdentifier({vault: vault, index: index}); + middleware.executeSlash(slashes); + } + + function _prepareTwoOperators() + private + returns (address operator1, address operator2, address vault1, address vault2, uint256 stake1, uint256 stake2) + { + operator1 = address(0x1); + operator2 = address(0x2); + + _registerOperator(operator1); + _registerOperator(operator2); + + vault1 = _createVaultForOperator(operator1); + vault2 = _createVaultForOperator(operator2); + + stake1 = 1_000; + stake2 = 2_000; + + _depositFromInVault(owner, vault1, stake1); + _depositFromInVault(owner, vault2, stake2); + + vm.warp(vm.getBlockTimestamp() + 1); + } + + function _vetoDeadline(address slasher, uint256 slash_index) private view returns (uint48) { + (,,,, uint48 vetoDeadline,) = IVetoSlasher(slasher).slashRequests(slash_index); + return vetoDeadline; + } + + function _requestSlash(address operator, uint48 ts, address vault, uint256 amount, bytes4 err) + private + returns (uint256 slashIndex) + { + Middleware.VaultSlashData[] memory vaults = new Middleware.VaultSlashData[](1); + vaults[0] = Middleware.VaultSlashData({vault: vault, amount: amount}); + + Middleware.SlashData[] memory slashes = new Middleware.SlashData[](1); + slashes[0] = Middleware.SlashData({operator: operator, ts: ts, vaults: vaults}); + + slashIndex = _requestSlash(slashes, err)[0]; + assertNotEq(slashIndex, type(uint256).max); + } + + function _requestSlash(Middleware.SlashData[] memory slashes, bytes4 err) + private + returns (uint256[] memory slashIndexes) + { + uint256 len = 0; + for (uint256 i = 0; i < slashes.length; i++) { + len += slashes[i].vaults.length; + } + + slashIndexes = new uint256[](len); + + vm.recordLogs(); + if (err != 0) { + vm.expectRevert(err); + middleware.requestSlash(slashes); + return slashIndexes; + } else { + middleware.requestSlash(slashes); + } + Vm.Log[] memory logs = vm.getRecordedLogs(); + + uint16 k = 0; + for (uint256 i = 0; i < logs.length; i++) { + Vm.Log memory log = logs[i]; + bytes32 eventSignature = log.topics[0]; + if (eventSignature == IVetoSlasher.RequestSlash.selector) { + slashIndexes[k++] = uint256(log.topics[1]); + } + } } function _disableOperator(address operator) private { @@ -320,19 +558,13 @@ 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(); } } - 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; @@ -340,7 +572,7 @@ contract MiddlewareTest is Test { (vault,,) = sym.vaultConfigurator().create( IVaultConfigurator.InitParams({ version: sym.vaultFactory().lastVersion(), - owner: owner, + owner: operator, vaultParams: abi.encode( IVault.InitParams({ collateral: address(wrappedVara), @@ -349,11 +581,11 @@ contract MiddlewareTest is Test { depositWhitelist: false, isDepositLimit: false, depositLimit: 0, - defaultAdminRoleHolder: owner, - depositWhitelistSetRoleHolder: owner, - depositorWhitelistRoleHolder: owner, - isDepositLimitSetRoleHolder: owner, - depositLimitSetRoleHolder: owner + defaultAdminRoleHolder: operator, + depositWhitelistSetRoleHolder: operator, + depositorWhitelistRoleHolder: operator, + isDepositLimitSetRoleHolder: operator, + depositLimitSetRoleHolder: operator }) ), delegatorIndex: 2, @@ -368,9 +600,15 @@ contract MiddlewareTest is Test { operator: operator }) ), - withSlasher: false, - slasherIndex: 0, - slasherParams: bytes("") + withSlasher: true, + slasherIndex: 1, + slasherParams: abi.encode( + IVetoSlasher.InitParams({ + baseParams: IBaseSlasher.BaseParams({isBurnerHook: false}), + vetoDuration: eraDuration / 2, + resolverSetEpochsDelay: 3 + }) + ) }) ); }