Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vote 2025-01-28: script and tests are ready for review #332

Merged
merged 13 commits into from
Jan 27, 2025
4 changes: 2 additions & 2 deletions configs/config_mainnet.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
iamnp marked this conversation as resolved.
Show resolved Hide resolved
CS_MODULE_MAX_DEPOSITS_PER_BLOCK = 30
CS_MODULE_MIN_DEPOSIT_BLOCK_DISTANCE = 25

Expand Down
144 changes: 144 additions & 0 deletions scripts/vote_2025_01_28.py
Original file line number Diff line number Diff line change
@@ -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
iamnp marked this conversation as resolved.
Show resolved Hide resolved

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
))
2 changes: 1 addition & 1 deletion tests/acceptance/test_csm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
51 changes: 49 additions & 2 deletions tests/regression/test_csm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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")
Expand Down
116 changes: 116 additions & 0 deletions tests/test_vote_2025_01_28.py
Original file line number Diff line number Diff line change
@@ -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)
iamnp marked this conversation as resolved.
Show resolved Hide resolved

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)
8 changes: 8 additions & 0 deletions utils/csm.py
Original file line number Diff line number Diff line change
@@ -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())
4 changes: 0 additions & 4 deletions utils/test/csm_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)):
Expand Down
7 changes: 7 additions & 0 deletions utils/test/event_validators/csm.py
Original file line number Diff line number Diff line change
@@ -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
10 changes: 6 additions & 4 deletions utils/test/event_validators/staking_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading