Skip to content

Commit e832983

Browse files
authored
Merge pull request #338 from lidofinance/feat/next-vote
Community staking module: limit increase + turn off EA mode; NO Acquisitions: Bridgetower is now part of Solstice Staking
2 parents f713556 + c21d336 commit e832983

19 files changed

+413
-34
lines changed

.github/actions/brownie_fork_tests/action.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ runs:
6969
shell: bash
7070
run: >
7171
poetry run
72-
brownie test -ra --network mainnet-fork
72+
brownie test -ra --network mainnet-fork --durations=20
7373
env:
7474
WEB3_INFURA_PROJECT_ID: ${{ inputs.infura }}
7575
ETHERSCAN_TOKEN: ${{ inputs.etherscan }}

archive/scripts/vote_2025_01_28.py

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
"""
2+
Voting 28/01/2025.
3+
4+
I. CSM: Enable Permissionless Phase and Increase the Share Limit
5+
1. Grant MODULE_MANAGER_ROLE on CS Module to Aragon Agent
6+
2. Activate public release mode on CS Module
7+
3. Increase the stake share limit from 1% to 2% and the priority exit threshold from 1.25% to 2.5% on CS Module
8+
4. Revoke MODULE_MANAGER_ROLE on CS Module from Aragon Agent
9+
10+
II. NO Acquisitions: Bridgetower is now part of Solstice Staking
11+
5. Rename Node Operator ID 17 from BridgeTower to Solstice
12+
13+
Vote# 183 passed & executed on 31/01/2024 at 14:57 UTC, block #21745254.
14+
15+
"""
16+
17+
import time
18+
19+
from typing import Dict, Tuple, Optional, List
20+
21+
from brownie import interface
22+
from brownie.network.transaction import TransactionReceipt
23+
from utils.voting import bake_vote_items, confirm_vote_script, create_vote
24+
from utils.ipfs import upload_vote_ipfs_description, calculate_vote_ipfs_description
25+
from utils.permissions import (
26+
encode_oz_revoke_role,
27+
encode_oz_grant_role
28+
)
29+
30+
from utils.node_operators import encode_set_node_operator_name
31+
32+
from utils.config import (
33+
get_deployer_account,
34+
contracts,
35+
get_is_live,
36+
get_priority_fee,
37+
)
38+
39+
from utils.csm import activate_public_release
40+
41+
from utils.agent import agent_forward
42+
43+
description = """
44+
1. **Transition Community Staking Module to Permissionless Phase** by activating public release and **increasing the share limit** from 1% to 2%, as [approved on Snapshot](https://snapshot.org/#/s:lido-snapshot.eth/proposal/0x7cbd5e9cb95bda9581831daf8b0e72d1ad0b068d2cbd3bda2a2f6ae378464f26). Alongside the share limit, [it is proposed](https://research.lido.fi/t/community-staking-module/5917/86) to **raise the priority exit share threshold** from 1.25% to 2.5% to maintain parameter ratios. Items 1-4.
45+
46+
2. **Rename Node Operator ID 17 from BridgeTower to Solstice** as [requested on the forum](https://research.lido.fi/t/node-operator-registry-name-reward-address-change/4170/41). Item 5.
47+
"""
48+
49+
def start_vote(tx_params: Dict[str, str], silent: bool) -> bool | list[int | TransactionReceipt | None]:
50+
"""Prepare and run voting."""
51+
voting: interface.Voting = contracts.voting
52+
csm: interface.CSModule = contracts.csm
53+
staking_router: interface.StakingRouter = contracts.staking_router
54+
csm_module_id = 3
55+
new_stake_share_limit = 200 #2%
56+
new_priority_exit_share_threshold = 250 #2.5%
57+
old_staking_module_fee = 600
58+
old_treasury_fee = 400
59+
old_max_deposits_per_block = 30
60+
old_min_deposit_block_distance = 25
61+
62+
vote_desc_items, call_script_items = zip(
63+
#
64+
# I. CSM: Enable Permissionless Phase and Increase the Share Limit
65+
#
66+
(
67+
"1. Grant MODULE_MANAGER_ROLE on CS Module to Aragon Agent",
68+
agent_forward(
69+
[
70+
encode_oz_grant_role(csm, "MODULE_MANAGER_ROLE", contracts.agent)
71+
]
72+
),
73+
),
74+
(
75+
"2. Activate public release mode on CS Module",
76+
agent_forward(
77+
[
78+
activate_public_release(csm.address)
79+
]
80+
),
81+
),
82+
(
83+
"3. Increase the stake share limit from 1% to 2% and the priority exit threshold from 1.25% to 2.5% on CS Module",
84+
agent_forward(
85+
[
86+
update_staking_module(csm_module_id, new_stake_share_limit, new_priority_exit_share_threshold,
87+
old_staking_module_fee, old_treasury_fee, old_max_deposits_per_block,
88+
old_min_deposit_block_distance)
89+
]
90+
),
91+
),
92+
(
93+
"4. Revoke MODULE_MANAGER_ROLE on CS Module from Aragon Agent",
94+
agent_forward(
95+
[
96+
encode_oz_revoke_role(csm, "MODULE_MANAGER_ROLE", revoke_from=contracts.agent)
97+
]
98+
),
99+
),
100+
#
101+
# II. NO Acquisitions: Bridgetower is now part of Solstice Staking
102+
#
103+
(
104+
"5. Rename Node Operator ID 17 from BridgeTower to Solstice",
105+
agent_forward(
106+
[
107+
encode_set_node_operator_name(
108+
id=17, name="Solstice", registry=contracts.node_operators_registry
109+
),
110+
]
111+
),
112+
),
113+
)
114+
115+
vote_items = bake_vote_items(list(vote_desc_items), list(call_script_items))
116+
117+
if silent:
118+
desc_ipfs = calculate_vote_ipfs_description(description)
119+
else:
120+
desc_ipfs = upload_vote_ipfs_description(description)
121+
122+
return confirm_vote_script(vote_items, silent, desc_ipfs) and list(
123+
create_vote(vote_items, tx_params, desc_ipfs=desc_ipfs)
124+
)
125+
126+
127+
def main():
128+
tx_params = {"from": get_deployer_account()}
129+
130+
if get_is_live():
131+
tx_params["priority_fee"] = get_priority_fee()
132+
133+
vote_id, _ = start_vote(tx_params=tx_params, silent=False)
134+
135+
vote_id >= 0 and print(f"Vote created: {vote_id}.")
136+
137+
time.sleep(5) # hack for waiting thread #2.
138+
139+
def update_staking_module(staking_module_id, stake_share_limit,
140+
priority_exit_share_threshold, staking_module_fee,
141+
treasury_fee, max_deposits_per_block,
142+
min_deposit_block_distance) -> Tuple[str, str]:
143+
return (contracts.staking_router.address, contracts.staking_router.updateStakingModule.encode_input(
144+
staking_module_id, stake_share_limit, priority_exit_share_threshold, staking_module_fee,
145+
treasury_fee, max_deposits_per_block, min_deposit_block_distance
146+
))
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
"""
2+
Tests for voting 28/01/2025.
3+
"""
4+
5+
from typing import Dict, Tuple, List, NamedTuple
6+
from scripts.vote_2025_01_28 import start_vote
7+
from brownie import interface
8+
from utils.test.tx_tracing_helpers import *
9+
from utils.config import contracts, LDO_HOLDER_ADDRESS_FOR_TESTS
10+
from utils.voting import find_metadata_by_vote_id
11+
from utils.ipfs import get_lido_vote_cid_from_str
12+
from utils.test.event_validators.csm import validate_public_release_event
13+
from utils.test.event_validators.staking_router import validate_staking_module_update_event, StakingModuleItem
14+
from utils.test.event_validators.node_operators_registry import validate_node_operator_name_set_event, NodeOperatorNameSetItem
15+
from utils.test.event_validators.permission import validate_grant_role_event, validate_revoke_role_event
16+
17+
def test_vote(helpers, accounts, vote_ids_from_env, stranger):
18+
19+
csm = interface.CSModule("0xdA7dE2ECdDfccC6c3AF10108Db212ACBBf9EA83F")
20+
staking_router = interface.StakingRouter("0xFdDf38947aFB03C621C71b06C9C70bce73f12999")
21+
node_operators_registry = interface.NodeOperatorsRegistry("0x55032650b14df07b85bF18A3a3eC8E0Af2e028d5")
22+
agent = interface.Agent("0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c")
23+
module_manager_role = "0x79dfcec784e591aafcf60db7db7b029a5c8b12aac4afd4e8c4eb740430405fa6"
24+
csm_module_id = 3
25+
new_stake_share_limit = 200 #2%
26+
new_priority_exit_share_threshold = 250
27+
new_name = "Solstice"
28+
old_name = "BridgeTower"
29+
old_stake_share_limit = 100 #1%
30+
old_priority_exit_share_threshold = 125
31+
old_staking_module_fee = 600
32+
old_treasury_fee = 400
33+
old_max_deposits_per_block = 30
34+
old_min_deposit_block_distance = 25
35+
36+
# Agent doesn't have MODULE_MANAGER_ROLE
37+
assert csm.hasRole(module_manager_role, agent) is False
38+
39+
# Public release mode is not active
40+
assert csm.publicRelease() is False
41+
42+
# Check old data
43+
assert staking_router.getStakingModule(csm_module_id)["stakeShareLimit"] == old_stake_share_limit
44+
assert staking_router.getStakingModule(csm_module_id)["priorityExitShareThreshold"] == old_priority_exit_share_threshold
45+
assert staking_router.getStakingModule(csm_module_id)["stakingModuleFee"] == old_staking_module_fee
46+
assert staking_router.getStakingModule(csm_module_id)["treasuryFee"] == old_treasury_fee
47+
assert staking_router.getStakingModule(csm_module_id)["maxDepositsPerBlock"] == old_max_deposits_per_block
48+
assert staking_router.getStakingModule(csm_module_id)["minDepositBlockDistance"] == old_min_deposit_block_distance
49+
50+
# Check old name
51+
assert node_operators_registry.getNodeOperator(17, True)["name"] == old_name
52+
53+
# START VOTE
54+
if len(vote_ids_from_env) > 0:
55+
(vote_id,) = vote_ids_from_env
56+
else:
57+
tx_params = {"from": LDO_HOLDER_ADDRESS_FOR_TESTS}
58+
vote_id, _ = start_vote(tx_params, silent=True)
59+
vote_tx = helpers.execute_vote(accounts, vote_id, contracts.voting)
60+
print(f"voteId = {vote_id}, gasUsed = {vote_tx.gas_used}")
61+
62+
#
63+
# I. CSM: Enable Permissionless Phase and Increase the Share Limit
64+
#
65+
# 2. Activate public release mode on CS Module
66+
assert csm.publicRelease() is True
67+
68+
# 3. Increase stake share limit from 1% to 2% on CS Module
69+
assert staking_router.getStakingModule(csm_module_id)["stakeShareLimit"] == new_stake_share_limit
70+
assert staking_router.getStakingModule(csm_module_id)["priorityExitShareThreshold"] == new_priority_exit_share_threshold
71+
assert staking_router.getStakingModule(csm_module_id)["stakingModuleFee"] == old_staking_module_fee
72+
assert staking_router.getStakingModule(csm_module_id)["treasuryFee"] == old_treasury_fee
73+
assert staking_router.getStakingModule(csm_module_id)["maxDepositsPerBlock"] == old_max_deposits_per_block
74+
assert staking_router.getStakingModule(csm_module_id)["minDepositBlockDistance"] == old_min_deposit_block_distance
75+
76+
# 4. Revoke MODULE_MANAGER_ROLE on CS Module from Aragon Agent
77+
assert csm.hasRole(module_manager_role, agent) is False
78+
79+
#
80+
# II. NO Acquisitions: Bridgetower is now part of Solstice Staking
81+
#
82+
# 5. Rename Node Operator ID 17 from BridgeTower to Solstice
83+
assert node_operators_registry.getNodeOperator(17, True)["name"] == new_name
84+
85+
# events
86+
display_voting_events(vote_tx)
87+
evs = group_voting_events(vote_tx)
88+
89+
metadata = find_metadata_by_vote_id(vote_id)
90+
assert get_lido_vote_cid_from_str(metadata) == "bafkreibwdmoq2ofckfsg3kgvwthyvyj3ey6zgoag4nzb2evf6djg75se6y"
91+
92+
assert count_vote_items_by_events(vote_tx, contracts.voting) == 5, "Incorrect voting items count"
93+
94+
# validate events
95+
validate_grant_role_event(evs[0], module_manager_role, agent.address, agent.address)
96+
97+
validate_public_release_event(evs[1])
98+
99+
expected_staking_module_item = StakingModuleItem(
100+
id=csm_module_id,
101+
name="Community Staking",
102+
address=None,
103+
target_share=new_stake_share_limit,
104+
module_fee=old_staking_module_fee,
105+
treasury_fee=old_treasury_fee,
106+
priority_exit_share=new_priority_exit_share_threshold,
107+
)
108+
109+
validate_staking_module_update_event(evs[2], expected_staking_module_item)
110+
111+
validate_revoke_role_event(evs[3], module_manager_role, agent.address, agent.address)
112+
113+
expected_node_operator_item = NodeOperatorNameSetItem(
114+
nodeOperatorId=17,
115+
name="Solstice",
116+
)
117+
validate_node_operator_name_set_event(evs[4], expected_node_operator_item)

configs/config_mainnet.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -353,8 +353,8 @@
353353
CS_MODULE_NAME = "Community Staking"
354354
CS_MODULE_MODULE_FEE_BP = 600
355355
CS_MODULE_TREASURY_FEE_BP = 400
356-
CS_MODULE_TARGET_SHARE_BP = 100
357-
CS_MODULE_PRIORITY_EXIT_SHARE_THRESHOLD = 125
356+
CS_MODULE_TARGET_SHARE_BP = 200
357+
CS_MODULE_PRIORITY_EXIT_SHARE_THRESHOLD = 250
358358
CS_MODULE_MAX_DEPOSITS_PER_BLOCK = 30
359359
CS_MODULE_MIN_DEPOSIT_BLOCK_DISTANCE = 25
360360

tests/acceptance/test_csm.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ def test_init_state(self, csm):
8080
assert csm.accounting() == CS_ACCOUNTING_ADDRESS
8181

8282
assert not csm.isPaused()
83-
assert not csm.publicRelease()
83+
assert csm.publicRelease()
8484

8585

8686
class TestAccounting:

tests/regression/test_accounting.py

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from tests.conftest import Helpers
88
from utils.config import contracts
99
from utils.test.deposits_helpers import fill_deposit_buffer
10-
from utils.test.helpers import ETH, GWEI, ZERO_ADDRESS, almostEqWithDiff, eth_balance
10+
from utils.test.helpers import ETH, GWEI, ZERO_ADDRESS, almostEqWithDiff, eth_balance, round_to_gwei
1111
from utils.test.oracle_report_helpers import ONE_DAY, SHARE_RATE_PRECISION, oracle_report
1212
from utils.test.csm_helpers import csm_add_node_operator, get_ea_member
1313
from utils.config import (
@@ -157,7 +157,7 @@ def test_accounting_cl_rebase_at_limits(accounting_oracle: Contract, lido: Contr
157157
pre_cl_balance = contracts.lido.getBeaconStat()[-1]
158158

159159
rebase_amount = (annual_increase_limit * ONE_DAY + 1) * pre_cl_balance // (365 * ONE_DAY) // MAX_BASIS_POINTS
160-
rebase_amount = _round_to_gwei(rebase_amount)
160+
rebase_amount = round_to_gwei(rebase_amount)
161161

162162
tx, _ = oracle_report(cl_diff=rebase_amount, exclude_vaults_balances=True)
163163
block_after_report = chain.height
@@ -946,12 +946,6 @@ def _shares_rate_from_event(tx) -> tuple[int, int]:
946946
)
947947

948948

949-
def _round_to_gwei(amount: int) -> int:
950-
"""Round amount to gwei"""
951-
952-
return amount // GWEI * GWEI
953-
954-
955949
def _drain_eth(address: str):
956950
"""Drain ETH from address"""
957951

tests/regression/test_accounting_oracle_extra_data_full_items.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
from utils.config import MAX_ITEMS_PER_EXTRA_DATA_TRANSACTION, MAX_NODE_OPERATORS_PER_EXTRA_DATA_ITEM
1616
from utils.config import contracts
17+
from utils.test.staking_router_helpers import increase_staking_module_share
1718

1819
NEW_KEYS_PER_OPERATOR = 2
1920

@@ -154,6 +155,7 @@ def test_extra_data_full_items(
154155
extraDataList=extra_data.extra_data_list,
155156
stakingModuleIdsWithNewlyExitedValidators=modules_with_exited,
156157
numExitedValidatorsByStakingModule=num_exited_validators_by_staking_module,
158+
skip_withdrawals=True,
157159
)
158160

159161
nor_distribute_reward_tx = distribute_reward(nor, stranger)
@@ -216,6 +218,7 @@ def test_extra_data_full_items(
216218
assert contracts.csm.getNodeOperatorSummary(i)["stuckValidatorsCount"] == csm_stuck[(3, i)]
217219

218220

221+
@pytest.mark.skip("This is a heavy test. Make sure to run it only if there are changes in the Staking Router or CSM contracts")
219222
def test_extra_data_most_expensive_report(extra_data_service):
220223
"""
221224
Make sure the worst report fits into the block gas limit.
@@ -233,6 +236,7 @@ def test_extra_data_most_expensive_report(extra_data_service):
233236
An estimate for 8 * 24 items:
234237
Gas used: 11850807 (39.50%)
235238
"""
239+
increase_staking_module_share(module_id=3, share_multiplier=2)
236240

237241
csm_operators_count = MAX_ITEMS_PER_EXTRA_DATA_TRANSACTION * MAX_NODE_OPERATORS_PER_EXTRA_DATA_ITEM
238242
# create or ensure there are max node operators with 1 depositable key

tests/regression/test_all_round_happy_path.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from utils.config import contracts
88
from utils.test.simple_dvt_helpers import fill_simple_dvt_ops_vetted_keys
99
from utils.balance import set_balance
10+
from utils.test.staking_router_helpers import set_staking_module_status, StakingModuleStatus
1011
from utils.test.tx_cost_helper import transaction_cost
1112

1213
def test_all_round_happy_path(accounts, stranger, steth_holder, eth_whale):
@@ -108,9 +109,15 @@ def test_all_round_happy_path(accounts, stranger, steth_holder, eth_whale):
108109

109110
assert contracts.lido.getDepositableEther() == buffered_ether_after_submit - withdrawal_unfinalized_steth
110111

112+
# pausing csm due to very high amount of keys in the queue
113+
csm_module_id = 3
114+
set_staking_module_status(csm_module_id, StakingModuleStatus.Stopped)
115+
111116
deposit_tx_nor = contracts.lido.deposit(max_deposit, curated_module_id, "0x0", {"from": dsm})
112117
deposit_tx_sdvt = contracts.lido.deposit(max_deposit, simple_dvt_module_id, "0x0", {"from": dsm})
113118

119+
set_staking_module_status(csm_module_id, StakingModuleStatus.Active)
120+
114121
buffered_ether_after_deposit = contracts.lido.getBufferedEther()
115122

116123
deposited_event_nor = deposit_tx_nor.events["StakingRouterETHDeposited"]

0 commit comments

Comments
 (0)