Skip to content

Commit 27f2123

Browse files
authored
Merge pull request #586 from lidofinance/feat/next-vote
Omnibus 20.01.2026 (#198 - Lido V3 Phase 2, CSM Share → 7.5%, Grant role to CSM Performance Oracle report weekday offset contract)
2 parents 128ea40 + 115bb38 commit 27f2123

14 files changed

+2059
-37
lines changed

.github/workflows/core_tests.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,14 @@ on:
99
- "feat/rc2"
1010
- "feat/rc1"
1111
- "feat/next-vote"
12+
- "feat/v3-phase-2"
1213
schedule:
1314
- cron: "0 0 * * TUE"
1415

1516
jobs:
1617
run-tests:
1718
name: Core repo tests in docker
18-
runs-on: [voting-private-runners]
19+
runs-on: [ubuntu-latest]
1920
timeout-minutes: 120
2021

2122
services:
@@ -35,7 +36,7 @@ jobs:
3536
- name: Run init script
3637
run: docker exec -e CORE_BRANCH tests-runner bash -c 'PYTHONPATH=$PWD make init'
3738
env:
38-
CORE_BRANCH: master
39+
CORE_BRANCH: develop
3940

4041
- name: Run node
4142
run: docker exec -e ETH_RPC_URL --detach tests-runner bash -c 'NODE_PORT=8545 make node'

.github/workflows/dual_governance_regression.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ on:
1313
- "feat/rc2"
1414
- "feat/rc1"
1515
- "feat/next-vote"
16+
- "feat/v3-phase-2"
1617
workflow_dispatch:
1718

1819
jobs:

.github/workflows/normal_vote_ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ on:
1212
jobs:
1313
run-tests-normal-1:
1414
name: Brownie fork NORMAL tests 1
15-
runs-on: [voting-private-runners]
15+
runs-on: [ubuntu-latest]
1616
timeout-minutes: 150
1717

1818
services:
@@ -36,7 +36,7 @@ jobs:
3636

3737
run-tests-normal-2:
3838
name: Brownie fork NORMAL tests 2
39-
runs-on: [voting-private-runners]
39+
runs-on: [ubuntu-latest]
4040
timeout-minutes: 150
4141

4242
services:

archive/scripts/upgrade_2026_01_20_v3_phase_2.py

Lines changed: 366 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
"""
2+
Voting 26/01/2026. Hoodi network.
3+
4+
1. Grant MANAGE_SIGNING_KEYS role for operator ID = 1 to 0xc8195bb2851d7129D9100af9d65Bd448A6dE11eF on Hoodi
5+
6+
Vote #56 passed & executed on Jan-26-2026 12:52:36 PM UTC, block 2108630.
7+
"""
8+
9+
from typing import Dict, List, Tuple
10+
11+
from utils.voting import bake_vote_items, confirm_vote_script, create_vote
12+
from utils.ipfs import upload_vote_ipfs_description, calculate_vote_ipfs_description
13+
from utils.config import get_deployer_account, get_is_live, get_priority_fee
14+
from utils.permissions import encode_permission_grant_p
15+
from utils.permission_parameters import Param, Op, ArgumentValue
16+
from utils.mainnet_fork import pass_and_exec_dao_vote
17+
18+
19+
# ============================== Addresses ===================================
20+
NEW_MANAGER_ADDRESS = "0xc8195bb2851d7129D9100af9d65Bd448A6dE11eF"
21+
TARGET_NO_REGISTRY = "0x682E94d2630846a503BDeE8b6810DF71C9806891"
22+
OPERATOR_ID = 1
23+
24+
25+
# ============================= Description ==================================
26+
IPFS_DESCRIPTION = "Grant MANAGE_SIGNING_KEYS role for operator ID = 1 to 0xc8195bb2851d7129D9100af9d65Bd448A6dE11eF on Hoodi"
27+
28+
29+
def get_vote_items() -> Tuple[List[str], List[Tuple[str, str]]]:
30+
params = [Param(0, Op.EQ, ArgumentValue(OPERATOR_ID))]
31+
32+
vote_desc_items, call_script_items = zip(
33+
(
34+
"1. Grant MANAGE_SIGNING_KEYS role for operator ID = 1 to 0xc8195bb2851d7129D9100af9d65Bd448A6dE11eF on Hoodi",
35+
encode_permission_grant_p(
36+
target_app=TARGET_NO_REGISTRY,
37+
permission_name="MANAGE_SIGNING_KEYS",
38+
grant_to=NEW_MANAGER_ADDRESS,
39+
params=params,
40+
),
41+
),
42+
)
43+
44+
return vote_desc_items, call_script_items
45+
46+
47+
def start_vote(tx_params: Dict[str, str], silent: bool = False):
48+
vote_desc_items, call_script_items = get_vote_items()
49+
vote_items = bake_vote_items(list(vote_desc_items), list(call_script_items))
50+
51+
desc_ipfs = (
52+
calculate_vote_ipfs_description(IPFS_DESCRIPTION)
53+
if silent else upload_vote_ipfs_description(IPFS_DESCRIPTION)
54+
)
55+
56+
vote_id, tx = confirm_vote_script(vote_items, silent, desc_ipfs) and list(
57+
create_vote(vote_items, tx_params, desc_ipfs=desc_ipfs)
58+
)
59+
60+
return vote_id, tx
61+
62+
63+
def main():
64+
tx_params: Dict[str, str] = {"from": get_deployer_account().address}
65+
if get_is_live():
66+
tx_params["priority_fee"] = get_priority_fee()
67+
68+
vote_id, _ = start_vote(tx_params=tx_params, silent=False)
69+
vote_id >= 0 and print(f"Vote created: {vote_id}.")
70+
71+
72+
def start_and_execute_vote_on_fork_manual():
73+
if get_is_live():
74+
raise Exception("This script is for local testing only.")
75+
76+
tx_params = {"from": get_deployer_account()}
77+
vote_id, _ = start_vote(tx_params=tx_params, silent=True)
78+
print(f"Vote created: {vote_id}.")
79+
pass_and_exec_dao_vote(int(vote_id), step_by_step=True)

archive/tests/test_2026_01_20.py

Lines changed: 1362 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
from brownie import interface, web3, reverts
2+
from brownie.network.transaction import TransactionReceipt
3+
from utils.evm_script import encode_call_script
4+
from utils.ipfs import get_lido_vote_cid_from_str
5+
from utils.permission_parameters import Param, Op, ArgumentValue
6+
from utils.test.event_validators.permission import Permission, validate_permission_grantp_event
7+
from utils.test.keys_helpers import random_pubkeys_batch, random_signatures_batch
8+
from utils.test.tx_tracing_helpers import (
9+
count_vote_items_by_events,
10+
display_voting_events,
11+
group_voting_events_from_receipt,
12+
)
13+
from utils.balance import set_balance
14+
from utils.voting import find_metadata_by_vote_id
15+
16+
from archive.scripts.vote_2026_01_26_hoodi import (
17+
start_vote,
18+
get_vote_items,
19+
)
20+
21+
22+
VOTING = "0x49B3512c44891bef83F8967d075121Bd1b07a01B"
23+
TARGET_NO_REGISTRY = "0x682E94d2630846a503BDeE8b6810DF71C9806891"
24+
ACL = "0x78780e70Eae33e2935814a327f7dB6c01136cc62"
25+
NEW_MANAGER_ADDRESS = "0xc8195bb2851d7129D9100af9d65Bd448A6dE11eF"
26+
MANAGE_SIGNING_KEYS = web3.keccak(text="MANAGE_SIGNING_KEYS").hex()
27+
OPERATOR_ID = 1
28+
EXPECTED_REWARD_ADDRESS = "0x031624fAD4E9BFC2524e7a87336C4b190E70BCA8"
29+
30+
EXPECTED_VOTE_ID = 56
31+
EXPECTED_VOTE_EVENTS_COUNT = 1
32+
IPFS_DESCRIPTION_HASH = "bafkreibcmwxupju2hx54awwjh7fpbybkmcxban5v36go4nsnrem2fwipgq"
33+
34+
35+
def test_vote(helpers, accounts, ldo_holder, vote_ids_from_env):
36+
voting = interface.Voting(VOTING)
37+
no = interface.NodeOperatorsRegistry(TARGET_NO_REGISTRY)
38+
perm_param = Param(0, Op.EQ, ArgumentValue(OPERATOR_ID))
39+
perm_param_uint = perm_param.to_uint256()
40+
41+
# =========================================================================
42+
# ======================== Identify or Create vote ========================
43+
# =========================================================================
44+
if vote_ids_from_env:
45+
vote_id = vote_ids_from_env[0]
46+
assert vote_id == EXPECTED_VOTE_ID
47+
elif voting.votesLength() > EXPECTED_VOTE_ID:
48+
vote_id = EXPECTED_VOTE_ID
49+
else:
50+
vote_id, _ = start_vote({"from": ldo_holder}, silent=True)
51+
52+
_, call_script_items = get_vote_items()
53+
onchain_script = voting.getVote(vote_id)["script"]
54+
assert str(onchain_script).lower() == encode_call_script(call_script_items).lower()
55+
56+
# =========================================================================
57+
# ============================= Execute Vote ==============================
58+
# =========================================================================
59+
is_executed = voting.getVote(vote_id)["executed"]
60+
if not is_executed:
61+
# =====================================================================
62+
# ========================= Before voting checks ======================
63+
# =====================================================================
64+
65+
# Item 1
66+
assert no.getNodeOperator(OPERATOR_ID, True)["rewardAddress"] == EXPECTED_REWARD_ADDRESS
67+
assert not no.canPerform(NEW_MANAGER_ADDRESS, MANAGE_SIGNING_KEYS, [perm_param_uint])
68+
# scenario test
69+
add_signing_keys_fails_before_vote(accounts)
70+
71+
assert get_lido_vote_cid_from_str(find_metadata_by_vote_id(vote_id)) == IPFS_DESCRIPTION_HASH
72+
73+
vote_tx: TransactionReceipt = helpers.execute_vote(vote_id=vote_id, accounts=accounts, dao_voting=voting)
74+
display_voting_events(vote_tx)
75+
vote_events = group_voting_events_from_receipt(vote_tx)
76+
77+
# =====================================================================
78+
# ========================= After voting checks =======================
79+
# =====================================================================
80+
81+
# Item 1
82+
assert no.canPerform(NEW_MANAGER_ADDRESS, MANAGE_SIGNING_KEYS, [perm_param_uint])
83+
84+
assert len(vote_events) == EXPECTED_VOTE_EVENTS_COUNT
85+
assert count_vote_items_by_events(vote_tx, voting.address) == EXPECTED_VOTE_EVENTS_COUNT
86+
87+
# events check
88+
permission = Permission(entity=NEW_MANAGER_ADDRESS, app=no, role=MANAGE_SIGNING_KEYS)
89+
validate_permission_grantp_event(vote_events[0], permission, [perm_param], emitted_by=ACL)
90+
91+
# scenario tests
92+
manager_adds_signing_keys(accounts)
93+
add_signing_keys_to_notallowed_operator_fails(accounts)
94+
95+
96+
def add_signing_keys_fails_before_vote(accounts):
97+
no = interface.SimpleDVT(TARGET_NO_REGISTRY)
98+
99+
manager = accounts.at(NEW_MANAGER_ADDRESS, force=True)
100+
set_balance(manager, 10)
101+
102+
pubkeys = random_pubkeys_batch(1)
103+
signatures = random_signatures_batch(1)
104+
105+
with reverts():
106+
no.addSigningKeys(
107+
OPERATOR_ID,
108+
1,
109+
pubkeys,
110+
signatures,
111+
{"from": manager},
112+
)
113+
114+
115+
def manager_adds_signing_keys(accounts):
116+
no = interface.SimpleDVT(TARGET_NO_REGISTRY)
117+
118+
manager = accounts.at(NEW_MANAGER_ADDRESS, force=True)
119+
set_balance(manager, 10)
120+
121+
total_keys_before = no.getTotalSigningKeyCount(OPERATOR_ID)
122+
pubkeys = random_pubkeys_batch(1)
123+
signatures = random_signatures_batch(1)
124+
125+
no.addSigningKeys(
126+
OPERATOR_ID,
127+
1,
128+
pubkeys,
129+
signatures,
130+
{"from": manager},
131+
)
132+
133+
total_keys_after = no.getTotalSigningKeyCount(OPERATOR_ID)
134+
assert total_keys_after == total_keys_before + 1
135+
136+
137+
def add_signing_keys_to_notallowed_operator_fails(accounts):
138+
no = interface.SimpleDVT(TARGET_NO_REGISTRY)
139+
140+
manager = accounts.at(NEW_MANAGER_ADDRESS, force=True)
141+
set_balance(manager, 10)
142+
143+
pubkeys = random_pubkeys_batch(1)
144+
signatures = random_signatures_batch(1)
145+
146+
with reverts():
147+
no.addSigningKeys(
148+
2, # NO id 2 - not allowed
149+
1,
150+
pubkeys,
151+
signatures,
152+
{"from": manager},
153+
)

configs/config_mainnet.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -182,8 +182,8 @@
182182
CS_MODULE_NAME = "Community Staking"
183183
CS_MODULE_MODULE_FEE_BP = 600
184184
CS_MODULE_TREASURY_FEE_BP = 400
185-
CS_MODULE_TARGET_SHARE_BP = 500
186-
CS_MODULE_PRIORITY_EXIT_SHARE_THRESHOLD = 625
185+
CS_MODULE_TARGET_SHARE_BP = 750
186+
CS_MODULE_PRIORITY_EXIT_SHARE_THRESHOLD = 900
187187
CS_MODULE_MAX_DEPOSITS_PER_BLOCK = 30
188188
CS_MODULE_MIN_DEPOSIT_BLOCK_DISTANCE = 25
189189

@@ -407,6 +407,7 @@
407407
CS_PERMISSIONLESS_GATE_ADDRESS = "0xcF33a38111d0B1246A3F38a838fb41D626B454f0"
408408
CS_VERIFIER_V2_ADDRESS = "0xdC5FE1782B6943f318E05230d688713a560063DC"
409409
CS_GATE_SEAL_V2_ADDRESS = "0xE1686C2E90eb41a48356c1cC7FaA17629af3ADB3"
410+
TWO_PHASE_FRAME_CONFIG_UPDATE = "0xb2B4DB1491cbe949ae85EfF01E0d3ee239f110C1"
410411

411412
# DualGovernance
412413
DUAL_GOVERNANCE = "0xC1db28B3301331277e307FDCfF8DE28242A4486E"
@@ -530,7 +531,7 @@
530531
OPERATOR_GRID = "0xC69685E89Cefc327b43B7234AC646451B27c544d"
531532
LAZY_ORACLE = "0x5DB427080200c235F2Ae8Cd17A7be87921f7AD6c"
532533
PREDEPOSIT_GUARANTEE = "0xF4bF42c6D6A0E38825785048124DBAD6c9eaaac3"
533-
VAULTS_ADAPTER = "0xe2DE6d2DefF15588a71849c0429101F8ca9FB14D"
534+
VAULTS_ADAPTER = "0x28F9Ac198C4E0FA6A9Ad2c2f97CB38F1A3120f27"
534535
GATE_SEAL_V3 = "0x881dAd714679A6FeaA636446A0499101375A365c"
535536
STAKING_VAULT_BEACON = "0x5FbE8cEf9CCc56ad245736D3C5bAf82ad54Ca789"
536537

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[{"inputs":[{"internalType":"address","name":"oracle","type":"address"},{"components":[{"internalType":"uint256","name":"reportsToProcessBeforeOffsetPhase","type":"uint256"},{"internalType":"uint256","name":"reportsToProcessBeforeRestorePhase","type":"uint256"},{"internalType":"uint256","name":"offsetPhaseEpochsPerFrame","type":"uint256"},{"internalType":"uint256","name":"restorePhaseFastLaneLengthSlots","type":"uint256"}],"internalType":"struct TwoPhaseFrameConfigUpdate.PhasesConfig","name":"phasesConfig","type":"tuple"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"CurrentReportMainPhaseIsNotCompleted","type":"error"},{"inputs":[],"name":"FastLanePeriodCannotBeLongerThanFrame","type":"error"},{"inputs":[],"name":"FastLaneTooShort","type":"error"},{"inputs":[],"name":"NoneOfPhasesExpired","type":"error"},{"inputs":[],"name":"OffsetPhaseNotExecuted","type":"error"},{"inputs":[],"name":"PhaseAlreadyExecuted","type":"error"},{"inputs":[{"internalType":"uint256","name":"currentSlot","type":"uint256"},{"internalType":"uint256","name":"deadlineSlot","type":"uint256"}],"name":"PhaseExpired","type":"error"},{"inputs":[{"internalType":"uint256","name":"actual","type":"uint256"},{"internalType":"uint256","name":"expected","type":"uint256"}],"name":"UnexpectedLastProcessingRefSlot","type":"error"},{"inputs":[],"name":"ZeroEpochsPerFrame","type":"error"},{"inputs":[],"name":"ZeroOracleAddress","type":"error"},{"inputs":[],"name":"ZeroReportsToEnableUpdate","type":"error"},{"anonymous":false,"inputs":[],"name":"OffsetPhaseExecuted","type":"event"},{"anonymous":false,"inputs":[],"name":"RestorePhaseExecuted","type":"event"},{"inputs":[],"name":"GENESIS_TIME","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"HASH_CONSENSUS","outputs":[{"internalType":"contract IConsensusContract","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"OFFSET_PHASE_FAST_LANE_LENGTH","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"ORACLE","outputs":[{"internalType":"contract IReportAsyncProcessor","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"SECONDS_PER_SLOT","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"SLOTS_PER_EPOCH","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"executeOffsetPhase","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"executeRestorePhase","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"getExpirationStatus","outputs":[{"internalType":"bool","name":"offsetExpired","type":"bool"},{"internalType":"bool","name":"restoreExpired","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"isReadyForOffsetPhase","outputs":[{"internalType":"bool","name":"ready","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"isReadyForRestorePhase","outputs":[{"internalType":"bool","name":"ready","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"offsetPhase","outputs":[{"internalType":"uint256","name":"expectedProcessingRefSlot","type":"uint256"},{"internalType":"uint256","name":"expirationSlot","type":"uint256"},{"internalType":"uint256","name":"epochsPerFrame","type":"uint256"},{"internalType":"uint256","name":"fastLaneLengthSlots","type":"uint256"},{"internalType":"bool","name":"executed","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"renounceRoleWhenExpired","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"restorePhase","outputs":[{"internalType":"uint256","name":"expectedProcessingRefSlot","type":"uint256"},{"internalType":"uint256","name":"expirationSlot","type":"uint256"},{"internalType":"uint256","name":"epochsPerFrame","type":"uint256"},{"internalType":"uint256","name":"fastLaneLengthSlots","type":"uint256"},{"internalType":"bool","name":"executed","type":"bool"}],"stateMutability":"view","type":"function"}]

tests/regression/test_gate_seal.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -357,10 +357,6 @@ def test_gate_seal_v3_vaults_scenario(gate_seal_committee):
357357

358358
pause_duration = gate_seal_v3.get_seal_duration_seconds()
359359

360-
# TODO remove this after PDG unpause
361-
reseal_manager_account = accounts.at(RESEAL_MANAGER, force=True)
362-
contracts.predeposit_guarantee.resume({"from": reseal_manager_account})
363-
364360
assert not contracts.vault_hub.isPaused()
365361
assert not contracts.predeposit_guarantee.isPaused()
366362

0 commit comments

Comments
 (0)