Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ethexe): symbiotic middleware for ethexe #4318

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions ethexe/contracts/foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions ethexe/contracts/lib/symbiotic-core
Submodule symbiotic-core added at 9cfc73
237 changes: 237 additions & 0 deletions ethexe/contracts/src/Middleware.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.26;

import {EnumerableMap} from "@openzeppelin/contracts/utils/structs/EnumerableMap.sol";
import {Time} from "@openzeppelin/contracts/utils/types/Time.sol";
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";

import {Subnetwork} from "symbiotic-core/src/contracts/libraries/Subnetwork.sol";
import {IVault} from "symbiotic-core/src/interfaces/vault/IVault.sol";
import {IRegistry} from "symbiotic-core/src/interfaces/common/IRegistry.sol";
import {IEntity} from "symbiotic-core/src/interfaces/common/IEntity.sol";
import {IBaseDelegator} from "symbiotic-core/src/interfaces/delegator/IBaseDelegator.sol";
import {INetworkRegistry} from "symbiotic-core/src/interfaces/INetworkRegistry.sol";
import {IOptInService} from "symbiotic-core/src/interfaces/service/IOptInService.sol";

import {MapWithTimeData} from "./libraries/MapWithTimeData.sol";

// TODO: support slashing
// TODO: implement election logic
// TODO: implement forced operators removal
// TODO: implement forced vaults removal
// TODO: implement rewards distribution
contract Middleware {
using EnumerableMap for EnumerableMap.AddressToUintMap;
using MapWithTimeData for EnumerableMap.AddressToUintMap;
using Subnetwork for address;

error ZeroVaultAddress();
error NotKnownVault();
error VaultWrongEpochDuration();
error UnknownCollateral();
error OperatorGracePeriodNotPassed();
error VaultGracePeriodNotPassed();
error NotVaultOwner();
error IncorrectTimestamp();
error OperatorDoesNotExist();
error OperatorDoesNotOptIn();

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;
uint96 public immutable NETWORK_IDENTIFIER = 0;

EnumerableMap.AddressToUintMap private operators;
EnumerableMap.AddressToUintMap private vaults;

constructor(
uint48 eraDuration,
address vaultFactory,
address delegatorFactory,
address slasherFactory,
address operatorRegistry,
address networkRegistry,
address networkOptIn,
address collateral
) {
ERA_DURATION = eraDuration;
GENESIS_TIMESTAMP = Time.timestamp();
OPERATOR_GRACE_PERIOD = 2 * eraDuration;
VAULT_GRACE_PERIOD = 2 * eraDuration;
VAULT_MIN_EPOCH_DURATION = 2 * eraDuration;
VAULT_FACTORY = vaultFactory;
DELEGATOR_FACTORY = delegatorFactory;
SLASHER_FACTORY = slasherFactory;
OPERATOR_REGISTRY = operatorRegistry;
NETWORK_OPT_IN = networkOptIn;
COLLATERAL = collateral;
SUBNETWORK = address(this).subnetwork(NETWORK_IDENTIFIER);

INetworkRegistry(networkRegistry).registerNetwork();
}

// TODO: Check that total stake is big enough
function registerOperator() external {
if (!IRegistry(OPERATOR_REGISTRY).isEntity(msg.sender)) {
revert OperatorDoesNotExist();
}
if (!IOptInService(NETWORK_OPT_IN).isOptedIn(msg.sender, address(this))) {
revert OperatorDoesNotOptIn();
}
operators.append(msg.sender, 0);
}

function disableOperator() external {
operators.disable(msg.sender);
}

function enableOperator() external {
operators.enable(msg.sender);
}

function unregisterOperator(address operator) external {
(, uint48 disabledTime) = operators.getTimes(operator);

if (disabledTime == 0 || Time.timestamp() < disabledTime + OPERATOR_GRACE_PERIOD) {
revert OperatorGracePeriodNotPassed();
}

operators.remove(operator);
}

// TODO: check vault has enough stake
// TODO: support and check slasher
function registerVault(address vault) external {
if (vault == address(0)) {
revert ZeroVaultAddress();
}

if (!IRegistry(VAULT_FACTORY).isEntity(vault)) {
revert NotKnownVault();
}

if (IVault(vault).epochDuration() < VAULT_MIN_EPOCH_DURATION) {
revert VaultWrongEpochDuration();
}

if (IVault(vault).collateral() != COLLATERAL) {
revert UnknownCollateral();
}

address delegator = IVault(vault).delegator();
if (IBaseDelegator(delegator).maxNetworkLimit(SUBNETWORK) != type(uint256).max) {
IBaseDelegator(delegator).setMaxNetworkLimit(NETWORK_IDENTIFIER, type(uint256).max);
}

vaults.append(vault, uint160(msg.sender));
}

function disableVault(address vault) external {
address vault_owner = address(vaults.getPinnedData(vault));

if (vault_owner != msg.sender) {
revert NotVaultOwner();
}

vaults.disable(vault);
}

function enableVault(address vault) external {
address vault_owner = address(vaults.getPinnedData(vault));

if (vault_owner != msg.sender) {
revert NotVaultOwner();
}

vaults.enable(vault);
}

function unregisterVault(address vault) external {
(, uint48 disabledTime) = vaults.getTimes(vault);

if (disabledTime == 0 || Time.timestamp() < disabledTime + VAULT_GRACE_PERIOD) {
revert VaultGracePeriodNotPassed();
}

vaults.remove(vault);
}

function getOperatorStakeAt(address operator, uint48 ts) external view returns (uint256 stake) {
_checkTimestamp(ts);

(uint48 enabledTime, uint48 disabledTime) = operators.getTimes(operator);
if (!_wasActiveAt(enabledTime, disabledTime, ts)) {
return 0;
}

stake = _collectOperatorStakeFromVaultsAt(operator, ts);
}

function getActiveOperatorsStakeAt(uint48 ts)
public
view
returns (address[] memory active_operators, uint256[] memory stakes)
Copy link
Member

@StackOverflowExcept1on StackOverflowExcept1on Nov 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use camel case (everywhere)

Suggested change
returns (address[] memory active_operators, uint256[] memory stakes)
returns (address[] memory activeOperators, uint256[] memory stakes)

{
_checkTimestamp(ts);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


active_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;
}

active_operators[operatorIdx] = operator;
stakes[operatorIdx] = _collectOperatorStakeFromVaultsAt(operator, ts);
operatorIdx += 1;
}

assembly {
mstore(active_operators, operatorIdx)
mstore(stakes, operatorIdx)
}
}

function _collectOperatorStakeFromVaultsAt(address operator, uint48 ts) private view returns (uint256 stake) {
for (uint256 i; i < vaults.length(); ++i) {
(address vault, uint48 vaultEnabledTime, uint48 vaultDisabledTime) = vaults.atWithTimes(i);

if (!_wasActiveAt(vaultEnabledTime, vaultDisabledTime, ts)) {
continue;
}

stake += IBaseDelegator(IVault(vault).delegator()).stakeAt(SUBNETWORK, operator, ts, new bytes(0));
}
}

function _wasActiveAt(uint48 enabledTime, uint48 disabledTime, uint48 ts) private pure returns (bool) {
return enabledTime != 0 && enabledTime <= ts && (disabledTime == 0 || disabledTime >= ts);
}

// Timestamp must be always in the past, but not too far,
// so that some operators or vaults can be already unregistered.
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();
}
}
}
75 changes: 75 additions & 0 deletions ethexe/contracts/src/libraries/MapWithTimeData.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;

import {Checkpoints} from "@openzeppelin/contracts/utils/structs/Checkpoints.sol";
import {Time} from "@openzeppelin/contracts/utils/types/Time.sol";
import {EnumerableMap} from "@openzeppelin/contracts/utils/structs/EnumerableMap.sol";

library MapWithTimeData {
using EnumerableMap for EnumerableMap.AddressToUintMap;

error AlreadyAdded();
error NotEnabled();
error AlreadyEnabled();

function toInner(uint256 value) private pure returns (uint48, uint48, uint160) {
// casting to uint48 will truncate the value to 48 bits, so it's safe for this case
return (uint48(value), uint48(value >> 48), uint160(value >> 96));
}

function toValue(uint48 enabledTime, uint48 disabledTime, uint160 data) private pure returns (uint256) {
return uint256(enabledTime) | (uint256(disabledTime) << 48) | (uint256(data) << 96);
}

function append(EnumerableMap.AddressToUintMap storage self, address addr, uint160 data) internal {
if (!self.set(addr, toValue(Time.timestamp(), 0, data))) {
revert AlreadyAdded();
}
}

function enable(EnumerableMap.AddressToUintMap storage self, address addr) internal {
(uint48 enabledTime, uint48 disabledTime, uint160 data) = toInner(self.get(addr));

if (enabledTime != 0 && disabledTime == 0) {
revert AlreadyEnabled();
}

self.set(addr, toValue(Time.timestamp(), 0, data));
}

function disable(EnumerableMap.AddressToUintMap storage self, address addr) internal {
(uint48 enabledTime, uint48 disabledTime, uint160 data) = toInner(self.get(addr));

if (enabledTime == 0 || disabledTime != 0) {
revert NotEnabled();
}

self.set(addr, toValue(enabledTime, Time.timestamp(), data));
}

function atWithTimes(EnumerableMap.AddressToUintMap storage self, uint256 idx)
internal
view
returns (address key, uint48 enabledTime, uint48 disabledTime)
{
uint256 value;
(key, value) = self.at(idx);
(enabledTime, disabledTime,) = toInner(value);
}

function getTimes(EnumerableMap.AddressToUintMap storage self, address addr)
internal
view
returns (uint48 enabledTime, uint48 disabledTime)
{
(enabledTime, disabledTime,) = toInner(self.get(addr));
}

function getPinnedData(EnumerableMap.AddressToUintMap storage self, address addr)
internal
view
returns (uint160 data)
{
(,, data) = toInner(self.get(addr));
}
}
Loading
Loading