-
Notifications
You must be signed in to change notification settings - Fork 105
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
grishasobol
wants to merge
14
commits into
master
Choose a base branch
from
gsobol-symbiotic-step1
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+695
−0
Open
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
b9e86ff
append symbiotic-core submodule
grishasobol c9d30e1
append initial middleware and simple test
grishasobol d0ad837
registrations and get stake implementations
grishasobol de72ac5
append getEnabledValidatorSet and tests
grishasobol 522cb14
refactor subnetwork
grishasobol 520bf0b
enabled oeprators -> active operators
grishasobol 5092b6b
pinnedAddress -> pinnedData
grishasobol 1c1e2e2
unregisterOperator(operator)
grishasobol f348ef8
-
grishasobol 1116f9d
chore
grishasobol b058d01
chore
grishasobol 629906a
st
grishasobol 4fb9e0e
block.timestamp -> vm.getBlockTimestamp()
grishasobol 8b651e8
fix potential problem with old timstamps
grishasobol File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Submodule symbiotic-core
added at
9cfc73
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
{ | ||
_checkTimestamp(ts); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | ||
} | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
use camel case (everywhere)