Skip to content
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
cea6e12
feat: meta operator registry
madlabman Jan 28, 2026
44714c2
remove: unused method
madlabman Feb 2, 2026
8739d90
fix: remove CMC_ROLE
madlabman Feb 2, 2026
5ace7ee
fix: use a separate error for already assigned external operators
madlabman Feb 2, 2026
9fa3fed
fix: use sentinel value for operator group creation
madlabman Feb 2, 2026
debe86e
chore: tweaking shareData.isValue usage
madlabman Feb 2, 2026
cb2d2a9
chore: add no id to NodeOperatorAlreadyInGroup error
madlabman Feb 2, 2026
224b408
test: initial test coverage
madlabman Feb 3, 2026
d8d9914
fix: deploy scripts
madlabman Feb 3, 2026
3d142be
feat: add getOperatorsWeights
madlabman Feb 3, 2026
699604f
Merge branch 'develop' into meta-registry
madlabman Feb 4, 2026
864f53b
fix: review follow-up
madlabman Feb 6, 2026
dc9bbe8
Merge branch 'develop' into meta-registry
madlabman Feb 6, 2026
a28921c
fix: external stake denomination in 2048 ether validators
madlabman Feb 6, 2026
bad9fcf
chore: small comment on nodeOperatorWeightsUpdateCount
madlabman Feb 6, 2026
ce22194
chore: Review
dgusakov Feb 6, 2026
30d7187
fix: another review follow up
madlabman Feb 6, 2026
64d8029
Merge branch 'develop' into meta-registry
madlabman Feb 6, 2026
5ef99e0
chore: Review
dgusakov Feb 6, 2026
356da1d
fix: sunday session review follow-up
madlabman Feb 8, 2026
293c699
refactor: change logic selection for external operators
madlabman Feb 9, 2026
a808307
chore: note about external stake normalization
madlabman Feb 9, 2026
2185b4f
fix: _refreshOperatorWeight were always returning true
madlabman Feb 9, 2026
49a8fda
fix: revert in getDepositsAllocation when weights are not up to date
madlabman Feb 9, 2026
6170061
fix: zero out operator effective weight on group change
madlabman Feb 9, 2026
d0b6af2
fix: call requestFullOperatorWeightsUpdate on group operations
madlabman Feb 9, 2026
f4c49f2
chore: add comment in curated createNodeOperator
madlabman Feb 9, 2026
9f74c35
feat: emit event when all node operator weights were updated
madlabman Feb 9, 2026
4648696
test: refactor ExternalOperatorLib test
madlabman Feb 9, 2026
27d9de6
Merge branch 'develop' into meta-registry
madlabman Feb 9, 2026
2de6636
test: fix curated integration tests
madlabman Feb 9, 2026
3b9905f
chore: Review remarks and changes
dgusakov Feb 9, 2026
5171930
fix: take into account external stake in allocation
madlabman Feb 9, 2026
6fe6dcf
chore: OperatorInfo rename and events refactoring
madlabman Feb 9, 2026
fa130e5
feat: add OperatorGroupCleared event
madlabman Feb 9, 2026
d3723c6
feat: add groupInfo to OperatorGroupUpdated
madlabman Feb 9, 2026
d9f0a78
chore: small gas saving by copying struct to the memory
madlabman Feb 9, 2026
5c80c40
feat: add module address cache to the MetaRegistry
madlabman Feb 9, 2026
e0ba190
draft: changed refreshOperatorWeight wiring
madlabman Feb 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
- `test/`: Forge tests (`*.t.sol`), plus `test/fork/*` for fork/integration and deployment tests.
- `script/`: Forge scripts (deploy/verify, per-chain variants).
- `artifacts/`, `broadcast/`, `out/`, `cache/`: build, deploy, and fork outputs.
- `lib/`, `node_modules/`: dependencies; see `remappings.txt`.
- `docs/`, `digest/`: documentation and design notes.
- `gists/`: small code examples related to the module.
- `node_modules/`: dependencies; see `remappings.txt`.
- `docs/`: documentation and design notes.

## Build, Test, and Development Commands

Expand All @@ -18,21 +19,32 @@
- `just test-local`: spins up anvil fork, deploys, runs deployment+integration tests.
- `just coverage` | `just coverage-lcov`: coverage (LCOV saved; see `lcov.html`).
- Linting: `yarn lint:check` (prettier + solhint), `yarn lint:fix`, `yarn lint:solhint`.
- Diff: `git diff --no-ext-diff`

## Coding Style & Naming Conventions

- Formatting: Prettier + `prettier-plugin-solidity` (Solidity `printWidth=80`, `tabWidth=4`, spaces only).
- Linting: Solhint (`.solhint.json`) with `solhint:recommended` and `solhint-plugin-lido-csm`.
- Versions: enforce `pragma solidity 0.8.33` (`compiler-version` rule).
- Naming: contracts/libraries `CamelCase` (e.g., `CSModule`, `AssetRecovererLib`), interfaces `IName` (rule: `interface-starts-with-i`).
- The if statement body should be enclosed in a block, for example: `if (cond) { do(); }`
- Inline assembly should be well documented, preferably every non-trivial line with its own comment.
- Conventions: prefer custom errors, calldata parameters, and struct packing (gas rules). Immutable vars styled as constants.
- Keep things in one function unless composable or reusable.
- Prefer short variable names where possible. Prefer same length variable names for related things.
- Prefer early returns to else statements.
- While refactoring keep comments added from existing implementations where applicable.
- Make sure external functions in contracts and interfaces have proper natspec.
- Avoid using magic numbers, prefer re-using or defining constants.
- When last I looked, the year was 2026.

## Testing Guidelines

- Framework: Foundry/Forge with fuzzing (`fuzz.runs=256`).
- Structure: unit tests in `test/*.t.sol`; fork suites under `test/fork/*` (deployment/integration).
- Run: `just test-unit` for fast cycles; `CHAIN`/`RPC_URL` required for fork tests. Example: `export CHAIN=hoodi && export RPC_URL=<https-url>`.
- Coverage: `just coverage-lcov` produces LCOV output (commit if relevant).
- After making changes to the source code make sure you've either ran build command or unit tests.

## Commit & Pull Request Guidelines

Expand Down
2 changes: 1 addition & 1 deletion Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ import "curated.just"
default: clean deps build test-all

build *args:
forge build --skip test --force {{args}}
forge build --skip test --skip script {{args}}

clean:
forge clean
Expand Down
2 changes: 1 addition & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
[profile.default]
solc = "0.8.33"
evm_version = "osaka"
optimizer = true
optimizer_runs = 0 # REVISE IN THE FUTURE
bytecode_hash = "none" # The metadata hash removed from the bytecode (not the metadata itself).
# uncomment this to inspect storage layouts in build artifacts
# extra_output = ["storageLayout"]
verbosity = 3

src = "src"
out = "out"
Expand Down
15 changes: 1 addition & 14 deletions script/csm/DeployBase.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,6 @@ struct DeployParams {
uint256 defaultAllowedExitDelay;
uint256 defaultExitDelayFee;
uint256 defaultMaxElWithdrawalRequestFee;
uint256 defaultDepositAllocationWeight;
// VettedGate
address identifiedCommunityStakersGateManager;
uint256 identifiedCommunityStakersGateCurveId;
Expand All @@ -115,7 +114,6 @@ struct DeployParams {
uint256 identifiedCommunityStakersGateAllowedExitDelay;
uint256 identifiedCommunityStakersGateExitDelayFee;
uint256 identifiedCommunityStakersGateMaxElWithdrawalRequestFee;
uint256 identifiedCommunityStakersGateDepositAllocationWeight;
// GateSeal
address gateSealFactory;
address sealingCommittee;
Expand Down Expand Up @@ -268,9 +266,7 @@ abstract contract DeployBase is Script {
defaultAllowedExitDelay: config.defaultAllowedExitDelay,
defaultExitDelayFee: config.defaultExitDelayFee,
defaultMaxElWithdrawalRequestFee: config
.defaultMaxElWithdrawalRequestFee,
defaultDepositAllocationWeight: config
.defaultDepositAllocationWeight
.defaultMaxElWithdrawalRequestFee
})
});

Expand Down Expand Up @@ -469,15 +465,6 @@ abstract contract DeployBase is Script {
identifiedCommunityStakersGateBondCurveId,
config.identifiedCommunityStakersGateMaxElWithdrawalRequestFee
);
if (
config.identifiedCommunityStakersGateDepositAllocationWeight !=
0
) {
parametersRegistry.setDepositAllocationWeight(
identifiedCommunityStakersGateBondCurveId,
config.identifiedCommunityStakersGateDepositAllocationWeight
);
}

feeDistributor.initialize({
admin: address(deployer),
Expand Down
76 changes: 38 additions & 38 deletions script/curated/DeployBase.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { FeeOracle } from "../../src/FeeOracle.sol";
import { Verifier } from "../../src/Verifier.sol";
import { ParametersRegistry } from "../../src/ParametersRegistry.sol";
import { ExitPenalties } from "../../src/ExitPenalties.sol";
import { OperatorsData } from "../../src/OperatorsData.sol";
import { MetaOperatorRegistry } from "../../src/MetaOperatorRegistry.sol";
import { CuratedGate } from "../../src/CuratedGate.sol";
import { CuratedGateFactory } from "../../src/CuratedGateFactory.sol";

Expand Down Expand Up @@ -51,7 +51,6 @@ struct GateCurveParams {
uint256 allowedExitDelay;
uint256 exitDelayFee;
uint256 maxElWithdrawalRequestFee;
uint256 depositAllocationWeight;
}

struct CuratedGateConfig {
Expand Down Expand Up @@ -115,8 +114,6 @@ struct CuratedDeployParams {
uint256 defaultAllowedExitDelay;
uint256 defaultExitDelayFee;
uint256 defaultMaxElWithdrawalRequestFee;
uint256 defaultDepositAllocationWeight;
uint256 identifiedCommunityStakersGateDepositAllocationWeight;
// Curated gates
CuratedGateConfig[] curatedGates;
// GateSeal
Expand Down Expand Up @@ -149,7 +146,7 @@ abstract contract DeployBase is Script {
Verifier public verifier;
HashConsensus public hashConsensus;
ParametersRegistry public parametersRegistry;
OperatorsData public operatorsData;
MetaOperatorRegistry public metaRegistry;
CuratedGateFactory public curatedGateFactory;
address[] public curatedGateInstances;
address internal curatedGateImpl;
Expand Down Expand Up @@ -218,8 +215,10 @@ abstract contract DeployBase is Script {
);

accounting = Accounting(_deployProxy(deployer, address(dummyImpl)));

oracle = FeeOracle(_deployProxy(deployer, address(dummyImpl)));
metaRegistry = MetaOperatorRegistry(
_deployProxy(deployer, address(dummyImpl))
);

FeeDistributor feeDistributorImpl = new FeeDistributor({
stETH: locator.lido(),
Expand Down Expand Up @@ -277,9 +276,7 @@ abstract contract DeployBase is Script {
defaultAllowedExitDelay: config.defaultAllowedExitDelay,
defaultExitDelayFee: config.defaultExitDelayFee,
defaultMaxElWithdrawalRequestFee: config
.defaultMaxElWithdrawalRequestFee,
defaultDepositAllocationWeight: config
.defaultDepositAllocationWeight
.defaultMaxElWithdrawalRequestFee
})
});

Expand Down Expand Up @@ -386,12 +383,6 @@ abstract contract DeployBase is Script {
curveId,
params.maxElWithdrawalRequestFee
);
if (params.depositAllocationWeight != 0) {
parametersRegistry.setDepositAllocationWeight(
curveId,
params.depositAllocationWeight
);
}
}
accounting.revokeRole(
accounting.MANAGE_BOND_CURVES_ROLE(),
Expand All @@ -407,7 +398,8 @@ abstract contract DeployBase is Script {
lidoLocator: config.lidoLocatorAddress,
parametersRegistry: address(parametersRegistry),
accounting: address(accounting),
exitPenalties: address(exitPenalties)
exitPenalties: address(exitPenalties),
metaOperatorsRegistry: address(metaRegistry)
});

{
Expand All @@ -420,6 +412,20 @@ abstract contract DeployBase is Script {

curatedModule.initialize({ admin: deployer });

MetaOperatorRegistry metaRegistryImpl = new MetaOperatorRegistry(
address(curatedModule),
locator.stakingRouter()
);

{
OssifiableProxy metaRegistryProxy = OssifiableProxy(
payable(address(metaRegistry))
);
metaRegistryProxy.proxy__upgradeTo(address(metaRegistryImpl));
metaRegistryProxy.proxy__changeAdmin(config.proxyAdmin);
}
metaRegistry.initialize({ admin: deployer });

ValidatorStrikes strikesImpl = new ValidatorStrikes({
module: address(curatedModule),
oracle: address(oracle),
Expand Down Expand Up @@ -454,20 +460,8 @@ abstract contract DeployBase is Script {

strikes.initialize(deployer, address(ejector));

address operatorsDataImpl = address(
new OperatorsData(locator.stakingRouter())
);
operatorsData = OperatorsData(
_deployProxy(config.proxyAdmin, operatorsDataImpl)
);
operatorsData.initialize(deployer);

curatedGateImpl = address(
new CuratedGate(
address(curatedModule),
config.stakingModuleId,
address(operatorsData)
)
new CuratedGate(config.stakingModuleId, address(curatedModule))
);

curatedGateFactory = new CuratedGateFactory(curatedGateImpl);
Expand Down Expand Up @@ -638,12 +632,12 @@ abstract contract DeployBase is Script {
gate.revokeRole(gate.DEFAULT_ADMIN_ROLE(), deployer);
}

operatorsData.grantRole(
operatorsData.DEFAULT_ADMIN_ROLE(),
metaRegistry.grantRole(
metaRegistry.DEFAULT_ADMIN_ROLE(),
config.aragonAgent
);
operatorsData.revokeRole(
operatorsData.DEFAULT_ADMIN_ROLE(),
metaRegistry.revokeRole(
metaRegistry.DEFAULT_ADMIN_ROLE(),
deployer
);

Expand Down Expand Up @@ -687,6 +681,11 @@ abstract contract DeployBase is Script {
deployJson.set("ChainId", chainId);
deployJson.set("CuratedModule", address(curatedModule));
deployJson.set("CuratedModuleImpl", address(curatedModuleImpl));
deployJson.set("MetaOperatorRegistry", address(metaRegistry));
deployJson.set(
"MetaOperatorRegistryImpl",
address(metaRegistryImpl)
);
deployJson.set("ParametersRegistry", address(parametersRegistry));
deployJson.set(
"ParametersRegistryImpl",
Expand All @@ -705,8 +704,6 @@ abstract contract DeployBase is Script {
deployJson.set("ValidatorStrikesImpl", address(strikesImpl));
deployJson.set("HashConsensus", address(hashConsensus));
deployJson.set("Verifier", address(verifier));
deployJson.set("OperatorsData", address(operatorsData));
deployJson.set("OperatorsDataImpl", address(operatorsDataImpl));
deployJson.set("CuratedGateFactory", address(curatedGateFactory));
deployJson.set("CuratedGates", curatedGateInstances);
deployJson.set("LidoLocator", config.lidoLocatorAddress);
Expand Down Expand Up @@ -766,7 +763,10 @@ abstract contract DeployBase is Script {
address(gate)
);
}
operatorsData.grantRole(operatorsData.SETTER_ROLE(), address(gate));
metaRegistry.grantRole(
metaRegistry.SET_OPERATOR_INFO_ROLE(),
address(gate)
);
gate.grantRole(gate.PAUSE_ROLE(), config.resealManager);
gate.grantRole(gate.RESUME_ROLE(), config.resealManager);
gate.grantRole(
Expand Down Expand Up @@ -847,8 +847,8 @@ abstract contract DeployBase is Script {
parametersRegistry.DEFAULT_ADMIN_ROLE(),
config.secondAdminAddress
);
operatorsData.grantRole(
operatorsData.DEFAULT_ADMIN_ROLE(),
metaRegistry.grantRole(
metaRegistry.DEFAULT_ADMIN_ROLE(),
config.secondAdminAddress
);
for (uint256 i = 0; i < curatedGateInstances.length; i++) {
Expand Down
1 change: 0 additions & 1 deletion script/curated/DeployHoodi.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,6 @@ contract DeployHoodi is DeployBase {
primaryGate.params.allowedExitDelay = 3 days; // TODO
primaryGate.params.exitDelayFee = 0.02 ether; // TODO
primaryGate.params.maxElWithdrawalRequestFee = 0.05 ether; // TODO
primaryGate.params.depositAllocationWeight = 1; // TODO: reconsider
}

// GateSeal
Expand Down
1 change: 0 additions & 1 deletion script/curated/DeployLocalDevNet.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,6 @@ contract DeployLocalDevNet is DeployBase {
primaryGate.params.allowedExitDelay = 8 days; // TODO
primaryGate.params.exitDelayFee = 0.05 ether; // TODO
primaryGate.params.maxElWithdrawalRequestFee = 0.05 ether; // TODO
primaryGate.params.depositAllocationWeight = 1; // TODO: reconsider
}

// GateSeal
Expand Down
1 change: 0 additions & 1 deletion script/curated/DeployMainnet.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,6 @@ contract DeployMainnet is DeployBase {
primaryGate.params.allowedExitDelay = 5 days; // TODO
primaryGate.params.exitDelayFee = 0.05 ether; // TODO
primaryGate.params.maxElWithdrawalRequestFee = 0.1 ether; // TODO
primaryGate.params.depositAllocationWeight = 1; // TODO: reconsider
}

// GateSeal
Expand Down
8 changes: 0 additions & 8 deletions script/fork-helpers/SimulateVote.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -195,14 +195,6 @@ contract SimulateVote is Script, ForkHelpersCommon {
);
vm.stopBroadcast();
}
{
vm.startBroadcast(parametersRegistryAdmin);
parametersRegistry.setDefaultDepositAllocationWeight(
deployParams.defaultDepositAllocationWeight
);
vm.stopBroadcast();
}

{
OssifiableProxy oracleProxy = OssifiableProxy(
payable(deploymentConfig.oracle)
Expand Down
2 changes: 1 addition & 1 deletion src/Accounting.sol
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ contract Accounting is
) external onlyRole(SET_BOND_CURVE_ROLE) {
_onlyExistingNodeOperator(nodeOperatorId);
BondCurve._setBondCurve(nodeOperatorId, curveId);
MODULE.updateDepositableValidatorsCount(nodeOperatorId);
MODULE.onNodeOperatorBondCurveUpdated(nodeOperatorId);
}

/// @inheritdoc IAccounting
Expand Down
14 changes: 12 additions & 2 deletions src/CSModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -386,15 +386,25 @@
/// @dev The function does nothing in CSM, since the information about the operator balances is not used in the
/// module. If it becomes needed in the future, the method should be implemented and the oracle should deliver
/// the actual balances.
function updateOperatorBalances(

Check warning on line 389 in src/CSModule.sol

View workflow job for this annotation

GitHub Actions / Linters

Function order is incorrect, external function can not go after external view function (line 377)
uint256[] calldata,
uint256[] calldata,
uint256[] calldata,
uint256
) external {

Check warning on line 394 in src/CSModule.sol

View workflow job for this annotation

GitHub Actions / Linters

Code contains empty blocks
// NOTE: The function does nothing in CSM, see the docstring.
}

/// @inheritdoc IBaseModule
function onNodeOperatorBondCurveUpdated(
uint256 nodeOperatorId
) external override(IBaseModule) {
_updateDepositableValidatorsCount({
nodeOperatorId: nodeOperatorId,
incrementNonceIfUpdated: true
});
}

/// @inheritdoc IStakingModule
function getStakingModuleSummary()
external
Expand Down Expand Up @@ -467,8 +477,8 @@
uint256 nodeOperatorId,
uint256 newCount,
bool incrementNonceIfUpdated
) internal override {
super._applyDepositableValidatorsCount(
) internal override returns (bool changed) {
changed = super._applyDepositableValidatorsCount(
no,
nodeOperatorId,
newCount,
Expand Down
Loading
Loading