Skip to content

Commit

Permalink
feat: add operator grid
Browse files Browse the repository at this point in the history
  • Loading branch information
loga4 committed Feb 25, 2025
1 parent 7b5395b commit 240fb8f
Show file tree
Hide file tree
Showing 5 changed files with 712 additions and 61 deletions.
5 changes: 3 additions & 2 deletions contracts/0.8.25/Accounting.sol
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,9 @@ contract Accounting is VaultHub {

constructor(
ILidoLocator _lidoLocator,
ILido _lido
) VaultHub(_lido) {
ILido _lido,
address _operatorGrid
) VaultHub(_lido, _operatorGrid) {
LIDO_LOCATOR = _lidoLocator;
LIDO = _lido;
}
Expand Down
266 changes: 209 additions & 57 deletions contracts/0.8.25/vaults/OperatorGrid.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,90 +4,242 @@
// See contracts/COMPILERS.md
pragma solidity 0.8.25;

import {AccessControl} from "@openzeppelin/contracts-v5.2/access/AccessControl.sol";

import {IStakingVault} from "./interfaces/IStakingVault.sol";

contract OperatorGrid {
struct Node {
contract OperatorGrid is AccessControl {
bytes32 public constant REGISTRY_ROLE = keccak256("REGISTRY_ROLE");

struct VaultData {
bool exists;
uint256 tierId;
uint256 mintedShares;
}

struct OperatorData {
bool exists;
uint256 groupId;
mapping(address => VaultData) vaults;
uint256 vaultCount;
}

struct TierData {
bool exists;
uint256 shareLimit;
uint256 mintedShares;
uint256 reserveRatio;
uint256 reserveRatioThreshold;
uint256 treasuryFee;
}

struct Config {
uint256 maxVaults;
uint256 maxShareLimit;
Node[] reserveRatioVaultIndex;
struct GroupData {
bool exists;
uint256 shareLimit;
uint256 mintedShares;
mapping(uint256 => TierData) tiers;
uint256 tiersCount;
}

struct NodeOperator {
uint256 configId;
mapping(address => uint256) vaultIndex;
uint256 vaultsCount;
}
mapping(uint256 => GroupData) public groups;
mapping(address => OperatorData) public operators;

mapping(uint256 => Config) public configs;
mapping(address => NodeOperator) public nodeOperators;
uint8 public configCount;
// -----------------------------
// EVENTS
// -----------------------------

function initialize() external {
Node[] memory basisNodes = new Node[](1);
basisNodes[0] = Node({shareLimit: 0, reserveRatio: 2_000});
addConfig(type(uint16).max, 1_000_000, basisNodes);
event GroupAdded(uint256 indexed groupId, uint256 shareLimit);
event TierAddedOrUpdated(uint256 indexed groupId, uint256 indexed tierId, uint256 shareLimit, uint256 reserveRatio, uint256 reserveRatioThreshold);
event OperatorAdded(uint256 indexed groupId, address indexed operatorAddr);
event VaultAdded(uint256 indexed groupId, address indexed operatorAddr, uint256 tierId);
event Minted(uint256 indexed groupId, address indexed operatorAddr, address indexed vault, uint256 amount);
event Burned(uint256 indexed groupId, address indexed operatorAddr, address indexed vault, uint256 amount);

Node[] memory curatedNodes = new Node[](5);
curatedNodes[0] = Node({shareLimit: 50_000, reserveRatio: 500});
curatedNodes[1] = Node({shareLimit: 100_000, reserveRatio: 600});
curatedNodes[2] = Node({shareLimit: 200_000, reserveRatio: 900});
curatedNodes[3] = Node({shareLimit: 300_000, reserveRatio: 1_400});
curatedNodes[4] = Node({shareLimit: 400_000, reserveRatio: 2_000});
// -----------------------------
// GROUPS
// -----------------------------

addConfig(5, 3_300_000, curatedNodes);
function addGroup(uint256 groupId, uint256 shareLimit) external {
GroupData storage g = groups[groupId];
require(!g.exists, "Group already exists");

Check warning on line 64 in contracts/0.8.25/vaults/OperatorGrid.sol

View workflow job for this annotation

GitHub Actions / Solhint

GC: Use Custom Errors instead of require statements

g.exists = true;
g.shareLimit = shareLimit;
g.mintedShares = 0;

emit GroupAdded(groupId, shareLimit);
}

function addConfig(uint256 maxVaults, uint256 maxShareLimit, Node[] memory nodes) public {
configs[configCount].maxVaults = maxVaults;
configs[configCount].maxShareLimit = maxShareLimit;
for (uint256 i = 0; i < nodes.length; i++) {
configs[configCount].reserveRatioVaultIndex.push(nodes[i]);
}
configCount++;
function updateGroupShareLimit(uint256 groupId, uint256 newShareLimit) external {
GroupData storage g = groups[groupId];
require(g.exists, "Group does not exist");

Check warning on line 75 in contracts/0.8.25/vaults/OperatorGrid.sol

View workflow job for this annotation

GitHub Actions / Solhint

GC: Use Custom Errors instead of require statements

g.shareLimit = newShareLimit;
}

function updateConfig(uint256 index, uint256 maxVaults, uint256 maxShareLimit, Node[] memory nodes) external {
require(index < configCount, "Invalid config index");
Config storage config = configs[index];
config.maxVaults = maxVaults;
config.maxShareLimit = maxShareLimit;
delete config.reserveRatioVaultIndex;
for (uint256 i = 0; i < nodes.length; i++) {
config.reserveRatioVaultIndex.push(nodes[i]);
// -----------------------------
// TIERS
// -----------------------------

function addTier(
uint256 groupId,
uint256 tierId,
uint256 shareLimit,
uint256 reserveRatio,
uint256 reserveRatioThreshold
) external {
GroupData storage g = groups[groupId];
require(g.exists, "Group does not exist");

Check warning on line 92 in contracts/0.8.25/vaults/OperatorGrid.sol

View workflow job for this annotation

GitHub Actions / Solhint

GC: Use Custom Errors instead of require statements

TierData storage t = g.tiers[tierId];
if (!t.exists) {
t.exists = true;
t.mintedShares = 0;
}

t.shareLimit = shareLimit;
t.reserveRatio = reserveRatio;
t.reserveRatioThreshold = reserveRatioThreshold;
g.tiersCount++;

emit TierAddedOrUpdated(groupId, tierId, shareLimit, reserveRatio, reserveRatioThreshold);
}

function removeConfig(uint256 index) public {
require(index < configCount, "Invalid config index");
delete configs[index];
// -----------------------------
// NO
// -----------------------------

function addOperator(address operatorAddr) external {
_addOperator(0, operatorAddr);
}
function addOperator(address operatorAddr, uint256 groupId) external {
require(hasRole(REGISTRY_ROLE, msg.sender));

Check warning on line 116 in contracts/0.8.25/vaults/OperatorGrid.sol

View workflow job for this annotation

GitHub Actions / Solhint

GC: Use Custom Errors instead of require statements
_addOperator(groupId, operatorAddr);
}

function addVault(address vault, uint256 vaultIndex) public {
address operator = IStakingVault(vault).nodeOperator();
function _addOperator(uint256 groupId, address operatorAddr) internal {
GroupData storage g = groups[groupId];
require(g.exists, "Group does not exist");

Check warning on line 122 in contracts/0.8.25/vaults/OperatorGrid.sol

View workflow job for this annotation

GitHub Actions / Solhint

GC: Use Custom Errors instead of require statements

NodeOperator storage nodeOperator = nodeOperators[operator];
nodeOperator.vaultIndex[vault] = vaultIndex;
nodeOperator.vaultsCount++;
OperatorData storage op = operators[operatorAddr];
require(!op.exists, "Operator already exists in this group");

op.exists = true;
op.groupId = groupId;

emit OperatorAdded(groupId, operatorAddr);
}

function updateNodeOperatorConfig(address operator, uint256 newConfigId) public {
require(newConfigId < configCount, "Invalid config ID");
nodeOperators[operator].configId = newConfigId;
// -----------------------------
// VAULS
// -----------------------------

function addVault(address vault, uint256 groupId) external {
GroupData storage g = groups[groupId];
require(g.exists, "Group does not exist");

OperatorData storage op = operators[IStakingVault(vault).nodeOperator()];
require(op.exists, "Operator does not exist");

VaultData storage v = op.vaults[vault];
require(!v.exists, "Vault already exists");

uint256 nextTierIndex = op.vaultCount;
require(nextTierIndex < g.tiersCount, "No more tiers available");

v.exists = true;
v.tierId = nextTierIndex;
v.mintedShares = 0;

op.vaultCount += 1;

emit VaultAdded(groupId, vault, nextTierIndex);
}

// -----------------------------
// MINT / BURN
// -----------------------------

/**
*
* group limit: group.mintedShares + amount <= group.shareLimit
* tier limit: tier.mintedShares + amount <= tier.shareLimit
* update shares on group/tier/vault
*/
function mintShares(
address vault,
uint256 amount
) external {
address operatorAddr = IStakingVault(vault).nodeOperator();
OperatorData storage op = operators[operatorAddr];
require(op.exists, "Operator does not exist in this group");

GroupData storage g = groups[op.groupId];
require(g.exists, "Group does not exist");

VaultData storage v = op.vaults[vault];
require(v.exists, "Vault does not exist under this operator");

// group limit
require(g.mintedShares + amount <= g.shareLimit, "Group limit exceeded");

TierData storage t = g.tiers[v.tierId];
require(t.exists, "Tier does not exist");
require(v.mintedShares + amount <= t.shareLimit, "Vault tier limit exceeded");

g.mintedShares += amount;
v.mintedShares += amount;

emit Minted(op.groupId, operatorAddr, vault, amount);
}

function getNodeOperatorLimits(address vault) external view returns (Node memory) {
address operator = IStakingVault(vault).nodeOperator();
NodeOperator storage nodeOperator = nodeOperators[operator];
function burnShares(
address vault,
uint256 amount
) external {
address operatorAddr = IStakingVault(vault).nodeOperator();
OperatorData storage op = operators[operatorAddr];
require(op.exists, "Operator does not exist in this group");

GroupData storage g = groups[op.groupId];
require(g.exists, "Group does not exist");

uint256 vaultIndex = nodeOperator.vaultIndex[vault];
VaultData storage v = op.vaults[vault];
require(v.exists, "Vault does not exist under this operator");
require(v.mintedShares >= amount, "Not enough vault shares to burn");

g.mintedShares -= amount;
v.mintedShares -= amount;

emit Burned(op.groupId, operatorAddr, vault, amount);
}

require(vaultIndex < configs[nodeOperator.configId].reserveRatioVaultIndex.length, "Invalid vault index");
return configs[nodeOperator.configId].reserveRatioVaultIndex[vaultIndex];
function getVaultLimits(address vault)
external
view
returns (
uint256 shareLimit,
uint256 reserveRatio,
uint256 reserveRatioThreshold,
uint256 treasuryFee
)
{
address operatorAddr = IStakingVault(vault).nodeOperator();
OperatorData storage op = operators[operatorAddr];
require(op.exists, "Operator does not exist in this group");

VaultData storage v = op.vaults[vault];
require(v.exists, "Vault not found in OperatorGrid");

GroupData storage g = groups[op.groupId];
require(g.exists, "Group does not exist?");

TierData storage t = g.tiers[v.tierId];
require(t.exists, "Tier not found?");

shareLimit = t.shareLimit;
reserveRatio = t.reserveRatio;
reserveRatioThreshold = t.reserveRatioThreshold;
treasuryFee = t.treasuryFee;
}
}
8 changes: 6 additions & 2 deletions contracts/0.8.25/vaults/VaultHub.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {ILido as IStETH} from "../interfaces/ILido.sol";
import {PausableUntilWithRoles} from "../utils/PausableUntilWithRoles.sol";

import {Math256} from "contracts/common/lib/Math256.sol";
import {OperatorGrid} from "./OperatorGrid.sol";

interface IOperatorGrid {
function getVaultLimits(address vault) external returns(
Expand Down Expand Up @@ -82,11 +83,12 @@ abstract contract VaultHub is PausableUntilWithRoles {
/// @notice Lido stETH contract
IStETH public immutable STETH;

IOperatorGrid public immutable OPERATOR_GRID;
OperatorGrid public immutable OPERATOR_GRID;

/// @param _stETH Lido stETH contract
constructor(IStETH _stETH) {
constructor(IStETH _stETH, address _operatorGrid) {
STETH = _stETH;
OPERATOR_GRID = OperatorGrid(_operatorGrid);

_disableInitializers();
}
Expand Down Expand Up @@ -274,6 +276,7 @@ abstract contract VaultHub is PausableUntilWithRoles {
}

STETH.mintExternalShares(_recipient, _amountOfShares);
OPERATOR_GRID.mintShares(_vault, _amountOfShares);

emit MintedSharesOnVault(_vault, _amountOfShares);
}
Expand All @@ -296,6 +299,7 @@ abstract contract VaultHub is PausableUntilWithRoles {
socket.sharesMinted = uint96(sharesMinted - _amountOfShares);

STETH.burnExternalShares(_amountOfShares);
OPERATOR_GRID.burnShares(_vault, _amountOfShares);

emit BurnedSharesOnVault(_vault, _amountOfShares);
}
Expand Down
Loading

0 comments on commit 240fb8f

Please sign in to comment.