diff --git a/configs/config_mainnet.py b/configs/config_mainnet.py index 1c784fe60..024f72d63 100644 --- a/configs/config_mainnet.py +++ b/configs/config_mainnet.py @@ -353,8 +353,8 @@ CS_MODULE_NAME = "Community Staking" CS_MODULE_MODULE_FEE_BP = 600 CS_MODULE_TREASURY_FEE_BP = 400 -CS_MODULE_TARGET_SHARE_BP = 100 -CS_MODULE_PRIORITY_EXIT_SHARE_THRESHOLD = 125 +CS_MODULE_TARGET_SHARE_BP = 200 +CS_MODULE_PRIORITY_EXIT_SHARE_THRESHOLD = 250 CS_MODULE_MAX_DEPOSITS_PER_BLOCK = 30 CS_MODULE_MIN_DEPOSIT_BLOCK_DISTANCE = 25 diff --git a/scripts/vote_2025_01_28.py b/scripts/vote_2025_01_28.py new file mode 100644 index 000000000..3f0ad1aa7 --- /dev/null +++ b/scripts/vote_2025_01_28.py @@ -0,0 +1,144 @@ +""" +Voting 28/01/2025. + +I. CSM: Enable Permissionless Phase and Increase the Share Limit +1. Grant MODULE_MANAGER_ROLE on CS Module to Aragon Agent +2. Activate public release mode on CS Module +3. Increase the stake share limit from 1% to 2% and the priority exit threshold from 1.25% to 2.5% on CS Module +4. Revoke MODULE_MANAGER_ROLE on CS Module from Aragon Agent + +II. NO Acquisitions: Bridgetower is now part of Solstice Staking +5. Rename Node Operator ID 17 from BridgeTower to Solstice + +""" + +import time + +from typing import Dict, Tuple, Optional, List + +from brownie import interface +from brownie.network.transaction import TransactionReceipt +from utils.voting import bake_vote_items, confirm_vote_script, create_vote +from utils.ipfs import upload_vote_ipfs_description, calculate_vote_ipfs_description +from utils.permissions import ( + encode_oz_revoke_role, + encode_oz_grant_role +) + +from utils.node_operators import encode_set_node_operator_name + +from utils.config import ( + get_deployer_account, + contracts, + get_is_live, + get_priority_fee, +) + +from utils.csm import activate_public_release + +from utils.agent import agent_forward + +description = """ +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. + +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. +""" + +def start_vote(tx_params: Dict[str, str], silent: bool) -> bool | list[int | TransactionReceipt | None]: + """Prepare and run voting.""" + voting: interface.Voting = contracts.voting + csm: interface.CSModule = contracts.csm + staking_router: interface.StakingRouter = contracts.staking_router + csm_module_id = 3 + new_stake_share_limit = 200 #2% + new_priority_exit_share_threshold = 250 #2.5% + old_staking_module_fee = 600 + old_treasury_fee = 400 + old_max_deposits_per_block = 30 + old_min_deposit_block_distance = 25 + + vote_desc_items, call_script_items = zip( + # + # I. CSM: Enable Permissionless Phase and Increase the Share Limit + # + ( + "1. Grant MODULE_MANAGER_ROLE on CS Module to Aragon Agent", + agent_forward( + [ + encode_oz_grant_role(csm, "MODULE_MANAGER_ROLE", contracts.agent) + ] + ), + ), + ( + "2. Activate public release mode on CS Module", + agent_forward( + [ + activate_public_release(csm.address) + ] + ), + ), + ( + "3. Increase the stake share limit from 1% to 2% and the priority exit threshold from 1.25% to 2.5% on CS Module", + agent_forward( + [ + update_staking_module(csm_module_id, new_stake_share_limit, new_priority_exit_share_threshold, + old_staking_module_fee, old_treasury_fee, old_max_deposits_per_block, + old_min_deposit_block_distance) + ] + ), + ), + ( + "4. Revoke MODULE_MANAGER_ROLE on CS Module from Aragon Agent", + agent_forward( + [ + encode_oz_revoke_role(csm, "MODULE_MANAGER_ROLE", revoke_from=contracts.agent) + ] + ), + ), + # + # II. NO Acquisitions: Bridgetower is now part of Solstice Staking + # + ( + "5. Rename Node Operator ID 17 from BridgeTower to Solstice", + agent_forward( + [ + encode_set_node_operator_name( + id=17, name="Solstice", registry=contracts.node_operators_registry + ), + ] + ), + ), + ) + + vote_items = bake_vote_items(list(vote_desc_items), list(call_script_items)) + + if silent: + desc_ipfs = calculate_vote_ipfs_description(description) + else: + desc_ipfs = upload_vote_ipfs_description(description) + + return confirm_vote_script(vote_items, silent, desc_ipfs) and list( + create_vote(vote_items, tx_params, desc_ipfs=desc_ipfs) + ) + + +def main(): + tx_params = {"from": get_deployer_account()} + + if get_is_live(): + tx_params["priority_fee"] = get_priority_fee() + + vote_id, _ = start_vote(tx_params=tx_params, silent=False) + + vote_id >= 0 and print(f"Vote created: {vote_id}.") + + time.sleep(5) # hack for waiting thread #2. + +def update_staking_module(staking_module_id, stake_share_limit, + priority_exit_share_threshold, staking_module_fee, + treasury_fee, max_deposits_per_block, + min_deposit_block_distance) -> Tuple[str, str]: + return (contracts.staking_router.address, contracts.staking_router.updateStakingModule.encode_input( + staking_module_id, stake_share_limit, priority_exit_share_threshold, staking_module_fee, + treasury_fee, max_deposits_per_block, min_deposit_block_distance + )) diff --git a/tests/acceptance/test_csm.py b/tests/acceptance/test_csm.py index 90cf13dc8..a473e0c50 100644 --- a/tests/acceptance/test_csm.py +++ b/tests/acceptance/test_csm.py @@ -80,7 +80,7 @@ def test_init_state(self, csm): assert csm.accounting() == CS_ACCOUNTING_ADDRESS assert not csm.isPaused() - assert not csm.publicRelease() + assert csm.publicRelease() class TestAccounting: diff --git a/tests/regression/test_csm.py b/tests/regression/test_csm.py index e6f3f266e..36bcf4290 100644 --- a/tests/regression/test_csm.py +++ b/tests/regression/test_csm.py @@ -43,6 +43,10 @@ def fee_distributor(): def fee_oracle(): return contracts.cs_fee_oracle +@pytest.fixture(scope="module") +def early_adoption(): + return contracts.cs_early_adoption + @pytest.fixture def node_operator(csm, accounting) -> int: @@ -98,8 +102,51 @@ def distribute_reward_tree(node_operator, ref_slot): @pytest.mark.parametrize("address, proof", get_ea_members()) -def test_add_ea_node_operator(csm, accounting, address, proof): - csm_add_node_operator(csm, accounting, address, proof) +def test_add_ea_node_operator(csm, accounting, early_adoption, address, proof): + no_id = csm_add_node_operator(csm, accounting, address, proof) + no = csm.getNodeOperator(no_id) + + assert no['managerAddress'] == address + assert no['rewardAddress'] == address + assert accounting.getBondCurveId(no_id) == early_adoption.CURVE_ID() + + +def test_add_node_operator_permissionless(csm, accounting, accounts): + address = accounts[8].address + no_id = csm_add_node_operator(csm, accounting, address, proof=[]) + no = csm.getNodeOperator(no_id) + + assert no['managerAddress'] == address + assert no['rewardAddress'] == address + assert accounting.getBondCurveId(no_id) == accounting.DEFAULT_BOND_CURVE_ID() + + +def test_add_node_operator_keys_more_than_limit(csm, accounting): + address, proof = get_ea_member() + keys_count = csm.MAX_SIGNING_KEYS_PER_OPERATOR_BEFORE_PUBLIC_RELEASE() + 1 + no_id = csm_add_node_operator(csm, accounting, address, proof, keys_count=keys_count) + no = csm.getNodeOperator(no_id) + + assert no["totalAddedKeys"] == keys_count + + +def test_add_node_operator_permissionless_keys_more_than_limit(csm, accounting, accounts): + keys_count = csm.MAX_SIGNING_KEYS_PER_OPERATOR_BEFORE_PUBLIC_RELEASE() + 1 + address = accounts[8].address + no_id = csm_add_node_operator(csm, accounting, address, proof=[], keys_count=keys_count) + no = csm.getNodeOperator(no_id) + + assert no["totalAddedKeys"] == keys_count + + +def test_upload_keys_more_than_limit(csm, accounting, node_operator): + no = csm.getNodeOperator(node_operator) + keys_before = no["totalAddedKeys"] + keys_count = csm.MAX_SIGNING_KEYS_PER_OPERATOR_BEFORE_PUBLIC_RELEASE() - keys_before + 1 + csm_upload_keys(csm, accounting, node_operator, keys_count) + + no = csm.getNodeOperator(node_operator) + assert no["totalAddedKeys"] == keys_count + keys_before @pytest.mark.usefixtures("pause_modules") diff --git a/tests/test_vote_2025_01_28.py b/tests/test_vote_2025_01_28.py new file mode 100644 index 000000000..f68cd0d86 --- /dev/null +++ b/tests/test_vote_2025_01_28.py @@ -0,0 +1,116 @@ +""" +Tests for voting 28/01/2025. +""" + +from typing import Dict, Tuple, List, NamedTuple +from scripts.vote_2025_01_28 import start_vote +from brownie import interface +from utils.test.tx_tracing_helpers import * +from utils.config import contracts, LDO_HOLDER_ADDRESS_FOR_TESTS +from utils.voting import find_metadata_by_vote_id +from utils.ipfs import get_lido_vote_cid_from_str +from utils.test.event_validators.csm import validate_public_release_event +from utils.test.event_validators.staking_router import validate_staking_module_update_event, StakingModuleItem +from utils.test.event_validators.node_operators_registry import validate_node_operator_name_set_event, NodeOperatorNameSetItem +from utils.test.event_validators.permission import validate_grant_role_event, validate_revoke_role_event + +def test_vote(helpers, accounts, vote_ids_from_env, stranger): + + csm = interface.CSModule("0xdA7dE2ECdDfccC6c3AF10108Db212ACBBf9EA83F") + staking_router = interface.StakingRouter("0xFdDf38947aFB03C621C71b06C9C70bce73f12999") + node_operators_registry = interface.NodeOperatorsRegistry("0x55032650b14df07b85bF18A3a3eC8E0Af2e028d5") + agent = interface.Agent("0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c") + module_manager_role = "0x79dfcec784e591aafcf60db7db7b029a5c8b12aac4afd4e8c4eb740430405fa6" + csm_module_id = 3 + new_stake_share_limit = 200 #2% + new_priority_exit_share_threshold = 250 + new_name = "Solstice" + old_name = "BridgeTower" + old_stake_share_limit = 100 #1% + old_priority_exit_share_threshold = 125 + old_staking_module_fee = 600 + old_treasury_fee = 400 + old_max_deposits_per_block = 30 + old_min_deposit_block_distance = 25 + + # Agent doesn't have MODULE_MANAGER_ROLE + assert csm.hasRole(module_manager_role, agent) is False + + # Public release mode is not active + assert csm.publicRelease() is False + + # Check old data + assert staking_router.getStakingModule(csm_module_id)["stakeShareLimit"] == old_stake_share_limit + assert staking_router.getStakingModule(csm_module_id)["priorityExitShareThreshold"] == old_priority_exit_share_threshold + assert staking_router.getStakingModule(csm_module_id)["stakingModuleFee"] == old_staking_module_fee + assert staking_router.getStakingModule(csm_module_id)["treasuryFee"] == old_treasury_fee + assert staking_router.getStakingModule(csm_module_id)["maxDepositsPerBlock"] == old_max_deposits_per_block + assert staking_router.getStakingModule(csm_module_id)["minDepositBlockDistance"] == old_min_deposit_block_distance + + # Check old name + assert node_operators_registry.getNodeOperator(17, True)["name"] == old_name + + # START VOTE + if len(vote_ids_from_env) > 0: + (vote_id,) = vote_ids_from_env + else: + tx_params = {"from": LDO_HOLDER_ADDRESS_FOR_TESTS} + vote_id, _ = start_vote(tx_params, silent=True) + vote_tx = helpers.execute_vote(accounts, vote_id, contracts.voting) + print(f"voteId = {vote_id}, gasUsed = {vote_tx.gas_used}") + + # + # I. CSM: Enable Permissionless Phase and Increase the Share Limit + # + # 2. Activate public release mode on CS Module + assert csm.publicRelease() is True + + # 3. Increase stake share limit from 1% to 2% on CS Module + assert staking_router.getStakingModule(csm_module_id)["stakeShareLimit"] == new_stake_share_limit + assert staking_router.getStakingModule(csm_module_id)["priorityExitShareThreshold"] == new_priority_exit_share_threshold + assert staking_router.getStakingModule(csm_module_id)["stakingModuleFee"] == old_staking_module_fee + assert staking_router.getStakingModule(csm_module_id)["treasuryFee"] == old_treasury_fee + assert staking_router.getStakingModule(csm_module_id)["maxDepositsPerBlock"] == old_max_deposits_per_block + assert staking_router.getStakingModule(csm_module_id)["minDepositBlockDistance"] == old_min_deposit_block_distance + + # 4. Revoke MODULE_MANAGER_ROLE on CS Module from Aragon Agent + assert csm.hasRole(module_manager_role, agent) is False + + # + # II. NO Acquisitions: Bridgetower is now part of Solstice Staking + # + # 5. Rename Node Operator ID 17 from BridgeTower to Solstice + assert node_operators_registry.getNodeOperator(17, True)["name"] == new_name + + # events + display_voting_events(vote_tx) + evs = group_voting_events(vote_tx) + + metadata = find_metadata_by_vote_id(vote_id) + assert get_lido_vote_cid_from_str(metadata) == "bafkreierrixpk7pszth7pkgau7iyhb4mxolskst62oyfat3ltfrnh355ty" + + assert count_vote_items_by_events(vote_tx, contracts.voting) == 5, "Incorrect voting items count" + + # validate events + validate_grant_role_event(evs[0], module_manager_role, agent.address, agent.address) + + validate_public_release_event(evs[1]) + + expected_staking_module_item = StakingModuleItem( + id=csm_module_id, + name="Community Staking", + address=None, + target_share=new_stake_share_limit, + module_fee=old_staking_module_fee, + treasury_fee=old_treasury_fee, + ) + + validate_staking_module_update_event(evs[2], expected_staking_module_item) + + validate_revoke_role_event(evs[3], module_manager_role, agent.address, agent.address) + + expected_node_operator_item = NodeOperatorNameSetItem( + nodeOperatorId=17, + name="Solstice", + ) + validate_node_operator_name_set_event(evs[4], expected_node_operator_item) diff --git a/utils/csm.py b/utils/csm.py new file mode 100644 index 000000000..ccf916476 --- /dev/null +++ b/utils/csm.py @@ -0,0 +1,8 @@ +from typing import Tuple +from brownie import interface + +from utils.config import contracts + +def activate_public_release(csm_address: str) -> Tuple[str, str]: + csm = interface.CSModule(csm_address) + return (csm.address, csm.activatePublicRelease.encode_input()) diff --git a/utils/test/csm_helpers.py b/utils/test/csm_helpers.py index eb374780c..a99bb0be9 100644 --- a/utils/test/csm_helpers.py +++ b/utils/test/csm_helpers.py @@ -170,10 +170,6 @@ def csm_upload_keys(csm, accounting, no_id, keys_count=5): def fill_csm_operators_with_keys(target_operators_count, keys_count): - if not contracts.csm.publicRelease(): - contracts.csm.grantRole(contracts.csm.MODULE_MANAGER_ROLE(), contracts.agent, {"from": contracts.agent}) - contracts.csm.activatePublicRelease({"from": contracts.agent}) - csm_node_operators_before = contracts.csm.getNodeOperatorsCount() added_operators_count = 0 for no_id in range(0, min(csm_node_operators_before, target_operators_count)): diff --git a/utils/test/event_validators/csm.py b/utils/test/event_validators/csm.py new file mode 100644 index 000000000..05a6b04c5 --- /dev/null +++ b/utils/test/event_validators/csm.py @@ -0,0 +1,7 @@ +from brownie.network.event import EventDict +from .common import validate_events_chain + +def validate_public_release_event(event: EventDict): + _events_chain = ['LogScriptCall', 'LogScriptCall', 'PublicRelease', 'ScriptResult'] + validate_events_chain([e.name for e in event], _events_chain) + assert event.count("PublicRelease") == 1 diff --git a/utils/test/event_validators/staking_router.py b/utils/test/event_validators/staking_router.py index 8ebc9c777..355fe8cb2 100644 --- a/utils/test/event_validators/staking_router.py +++ b/utils/test/event_validators/staking_router.py @@ -53,18 +53,20 @@ def validate_staking_module_update_event(event: EventDict, module_item: StakingM _events_chain = [ "LogScriptCall", "LogScriptCall", - "StakingModuleTargetShareSet", + "StakingModuleShareLimitSet", "StakingModuleFeesSet", + "StakingModuleMaxDepositsPerBlockSet", + "StakingModuleMinDepositBlockDistanceSet", "ScriptResult", ] validate_events_chain([e.name for e in event], _events_chain) - assert event.count("StakingModuleTargetShareSet") == 1 + assert event.count("StakingModuleShareLimitSet") == 1 assert event.count("StakingModuleFeesSet") == 1 - assert event["StakingModuleTargetShareSet"]["stakingModuleId"] == module_item.id - assert event["StakingModuleTargetShareSet"]["targetShare"] == module_item.target_share + assert event["StakingModuleShareLimitSet"]["stakingModuleId"] == module_item.id + assert event["StakingModuleShareLimitSet"]["stakeShareLimit"] == module_item.target_share assert event["StakingModuleFeesSet"]["stakingModuleId"] == module_item.id assert event["StakingModuleFeesSet"]["stakingModuleFee"] == module_item.module_fee