Skip to content

Commit 57f14c4

Browse files
feat: add easy track factory for deposit reserve target
1 parent 1640238 commit 57f14c4

3 files changed

Lines changed: 211 additions & 8 deletions

File tree

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
[
2+
{
3+
"inputs": [],
4+
"name": "MAX_DEPOSITS_RESERVE_TARGET",
5+
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
6+
"stateMutability": "view",
7+
"type": "function"
8+
},
9+
{
10+
"inputs": [],
11+
"name": "lido",
12+
"outputs": [{ "internalType": "address", "name": "", "type": "address" }],
13+
"stateMutability": "view",
14+
"type": "function"
15+
},
16+
{
17+
"inputs": [],
18+
"name": "trustedCaller",
19+
"outputs": [{ "internalType": "address", "name": "", "type": "address" }],
20+
"stateMutability": "view",
21+
"type": "function"
22+
},
23+
{
24+
"inputs": [
25+
{ "internalType": "address", "name": "_creator", "type": "address" },
26+
{ "internalType": "bytes", "name": "_evmScriptCallData", "type": "bytes" }
27+
],
28+
"name": "createEVMScript",
29+
"outputs": [{ "internalType": "bytes", "name": "", "type": "bytes" }],
30+
"stateMutability": "view",
31+
"type": "function"
32+
},
33+
{
34+
"inputs": [{ "internalType": "bytes", "name": "_evmScriptCallData", "type": "bytes" }],
35+
"name": "decodeEVMScriptCallData",
36+
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
37+
"stateMutability": "pure",
38+
"type": "function"
39+
}
40+
]

scripts/vote_edf_hoodi.py

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
1.83. Grant STAKING_MODULE_UNVETTING_ROLE on StakingRouter to the new DepositSecurityModule v5
1616
1.84. Revoke TOP_UP_ROLE on TopUpGateway from the old depositor bot EOA
1717
1.85. Grant TOP_UP_ROLE on TopUpGateway to the depositor bot DelegationContract
18+
# ===== Easy Track factory for deposit reserve target management by CMC =====
19+
1.86. Grant BUFFER_RESERVE_MANAGER_ROLE on Lido to the Easy Track EVMScriptExecutor
20+
2. Add SetDepositsReserveTarget factory to Easy Track with the permission to call
21+
Lido.setDepositsReserveTarget(uint256)
1822
1923
The new DSM v5 is deployed with the guardian set already moved to DelegationContracts:
2024
Stakely replaces Kiln, and the extra Lido dev team guardian is removed.
@@ -26,13 +30,19 @@
2630

2731
from utils.agent import agent_forward
2832
from utils.config import (
33+
ACL,
2934
AGENT,
35+
EASYTRACK,
36+
EASYTRACK_EVMSCRIPT_EXECUTOR,
37+
LIDO,
3038
LIDO_LOCATOR,
3139
STAKING_ROUTER,
3240
get_deployer_account,
3341
get_is_live,
3442
get_priority_fee,
3543
)
44+
from utils.easy_track import add_evmscript_factory, create_permissions
45+
from utils.permissions import encode_permission_grant
3646
from utils.dual_governance import submit_proposals
3747
from utils.ipfs import calculate_vote_ipfs_description, upload_vote_ipfs_description
3848
from utils.mainnet_fork import pass_and_exec_dao_vote
@@ -58,8 +68,16 @@
5868
# https://github.com/lidofinance/core/blob/3deade5e5f1320cb1869e5990a8372b3feab31ba/deployed-hoodi.json#L494
5969
DEPOSITOR_BOT_DELEGATION_CONTRACT = "0x25636798f6E716b2e6b7dEA8ED52a45271768D7A"
6070

71+
# Easy Track factory for deposit reserve target management by CMC,
72+
# https://research.lido.fi/t/proposal-add-easy-track-factory-for-deposit-reserve-target-management-by-cmc/11827
73+
SET_DEPOSITS_RESERVE_TARGET_FACTORY = "0x68009122a394504E8fD7fee58F92Cd73c6A60717"
74+
# Trusted caller baked into the factory: the Hoodi CMC Safe (5/9).
75+
# The forum post lists the mainnet CMC multisig instead, that address has no code on Hoodi.
76+
SET_DEPOSITS_RESERVE_TARGET_TRUSTED_CALLER = "0x84DffcfB232594975C608DE92544Ff239a24c9E9"
77+
6178
STAKING_MODULE_UNVETTING_ROLE = web3.keccak(text="STAKING_MODULE_UNVETTING_ROLE").hex()
6279
TOP_UP_ROLE = web3.keccak(text="TOP_UP_ROLE").hex()
80+
BUFFER_RESERVE_MANAGER_ROLE = "BUFFER_RESERVE_MANAGER_ROLE"
6381

6482
ORACLE_COMMITTEE_QUORUM = 6
6583

@@ -121,13 +139,18 @@ class OracleMemberMapping(NamedTuple):
121139
"and switch to the new DepositSecurityModule v5"
122140
)
123141
DG_SUBMISSION_DESCRIPTION = "1. Submit the EDF/DSM v5 upgrade to Dual Governance"
142+
ET_FACTORY_DESCRIPTION = (
143+
"2. Add SetDepositsReserveTarget factory 0x68009122a394504E8fD7fee58F92Cd73c6A60717 "
144+
"to Easy Track with the permission to call Lido.setDepositsReserveTarget(uint256)"
145+
)
124146
IPFS_DESCRIPTION = """
125147
Upgrade the Lido protocol on Hoodi to the Execution Delegation Framework (EDF) and DepositSecurityModule v5 (LIP-37).
126148
127149
1. Rotate all members of the four oracle committees (HashConsensus contracts for AccountingOracle, ValidatorsExitBusOracle, CSFeeOracle and Curated Module FeeOracle) from EOA hot keys to per-operator EDF DelegationContracts, keeping quorum 6. Items 1.1-1.80.
128150
2. Upgrade the LidoLocator implementation so it points to the new DepositSecurityModule v5. The new DSM is deployed with the guardian set already moved to DelegationContracts: Stakely replaces Kiln, and the extra Lido dev team guardian is removed. Item 1.81.
129151
3. Move STAKING_MODULE_UNVETTING_ROLE on StakingRouter from the old DepositSecurityModule to the new DepositSecurityModule v5. Items 1.82-1.83.
130152
4. Move TOP_UP_ROLE on TopUpGateway from the old depositor bot EOA to the depositor bot DelegationContract. Items 1.84-1.85.
153+
5. Enable deposit reserve target management by CMC via Easy Track: grant BUFFER_RESERVE_MANAGER_ROLE on Lido to the Easy Track EVMScriptExecutor and add the SetDepositsReserveTarget factory, limited to Lido.setDepositsReserveTarget(uint256). Item 1.86 and item 2.
131154
"""
132155

133156

@@ -234,6 +257,28 @@ def _assert_state_before_vote() -> None:
234257
new_dsm_guardians = {str(g).lower() for g in new_dsm.getGuardians()}
235258
assert new_dsm_guardians == {g.lower() for g in NEW_DSM_GUARDIANS}, "New DSM guardian set mismatch"
236259

260+
# Easy Track factory for deposit reserve target management
261+
acl = interface.ACL(ACL)
262+
buffer_reserve_manager_role = web3.keccak(text=BUFFER_RESERVE_MANAGER_ROLE)
263+
# the Agent grants the role from inside the DG proposal, so it must be its manager
264+
assert str(acl.getPermissionManager(LIDO, buffer_reserve_manager_role)).lower() == AGENT.lower(), (
265+
"Agent is not the manager of BUFFER_RESERVE_MANAGER_ROLE on Lido"
266+
)
267+
assert not acl.hasPermission(
268+
EASYTRACK_EVMSCRIPT_EXECUTOR, LIDO, buffer_reserve_manager_role
269+
), "EVMScriptExecutor already holds BUFFER_RESERVE_MANAGER_ROLE"
270+
271+
easy_track = interface.EasyTrack(EASYTRACK)
272+
registered_factories = {str(f).lower() for f in easy_track.getEVMScriptFactories()}
273+
assert SET_DEPOSITS_RESERVE_TARGET_FACTORY.lower() not in registered_factories, (
274+
"SetDepositsReserveTarget factory is already registered in Easy Track"
275+
)
276+
277+
factory = interface.SetDepositsReserveTarget(SET_DEPOSITS_RESERVE_TARGET_FACTORY)
278+
assert str(factory.trustedCaller()).lower() == SET_DEPOSITS_RESERVE_TARGET_TRUSTED_CALLER.lower(), (
279+
"SetDepositsReserveTarget factory trusted caller mismatch"
280+
)
281+
237282

238283
# ================================ Main ======================================
239284
def get_edf_upgrade_calls() -> List[Tuple[str, str]]:
@@ -304,7 +349,16 @@ def get_edf_upgrade_calls() -> List[Tuple[str, str]]:
304349
)
305350
)
306351

307-
expected_count = 2 * len(ORACLE_COMMITTEES) * len(ORACLE_MEMBER_MAPPINGS) + 5
352+
# 1.86. Grant BUFFER_RESERVE_MANAGER_ROLE on Lido to the Easy Track EVMScriptExecutor
353+
calls.append(
354+
encode_permission_grant(
355+
target_app=interface.Lido(LIDO),
356+
permission_name=BUFFER_RESERVE_MANAGER_ROLE,
357+
grant_to=EASYTRACK_EVMSCRIPT_EXECUTOR,
358+
)
359+
)
360+
361+
expected_count = 2 * len(ORACLE_COMMITTEES) * len(ORACLE_MEMBER_MAPPINGS) + 6
308362
assert len(calls) == expected_count, f"Expected {expected_count} upgrade calls, got {len(calls)}"
309363

310364
return calls
@@ -318,8 +372,19 @@ def get_dg_items() -> List[Tuple[str, str]]:
318372
def get_vote_items() -> Tuple[List[str], List[Tuple[str, str]]]:
319373
dg_call_script = submit_proposals([(get_dg_items(), DG_PROPOSAL_METADATA)])
320374

321-
vote_desc_items = [DG_SUBMISSION_DESCRIPTION]
322-
call_script_items = [dg_call_script[0]]
375+
# Easy Track admin on Hoodi is Voting, so the factory is registered
376+
# directly by the vote, not through the Agent
377+
vote_desc_items = [
378+
DG_SUBMISSION_DESCRIPTION,
379+
ET_FACTORY_DESCRIPTION,
380+
]
381+
call_script_items = [
382+
dg_call_script[0],
383+
add_evmscript_factory(
384+
factory=SET_DEPOSITS_RESERVE_TARGET_FACTORY,
385+
permissions=create_permissions(interface.Lido(LIDO), "setDepositsReserveTarget"),
386+
),
387+
]
323388

324389
return vote_desc_items, call_script_items
325390

tests/test_vote_edf_hoodi.py

Lines changed: 103 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
from typing import NamedTuple
44

5-
from brownie import chain, convert, interface, web3
5+
from brownie import accounts, chain, convert, interface, reverts, web3
6+
from eth_abi import encode as encode_abi
67
from brownie.network.event import EventDict
78
from brownie.network.transaction import TransactionReceipt
89

@@ -20,6 +21,11 @@
2021
from utils.dual_governance import PROPOSAL_STATUS
2122
from utils.test.event_validators.common import validate_events_chain
2223
from utils.test.event_validators.dual_governance import validate_dual_governance_submit_event
24+
from utils.test.event_validators.easy_track import (
25+
EVMScriptFactoryAdded,
26+
validate_evmscript_factory_added_event,
27+
)
28+
from utils.easy_track import create_permissions
2329
from utils.voting import find_metadata_by_vote_id
2430
from utils.ipfs import calculate_vote_ipfs_description, get_lido_vote_cid_from_str
2531

@@ -60,8 +66,17 @@
6066
DEPOSITOR_BOT_OLD_EOA = "0x9b186cE78Ddd6fF098b4a533Dd17a139e1FFeD76"
6167
DEPOSITOR_BOT_DELEGATION_CONTRACT = "0x25636798f6E716b2e6b7dEA8ED52a45271768D7A"
6268

69+
ACL = "0x78780e70Eae33e2935814a327f7dB6c01136cc62"
70+
LIDO = "0x3508A952176b3c15387C97BE809eaffB1982176a"
71+
EASYTRACK = "0x284D91a7D47850d21A6DEaaC6E538AC7E5E6fc2a"
72+
EASYTRACK_EVMSCRIPT_EXECUTOR = "0x79a20FD0FA36453B2F45eAbab19bfef43575Ba9E"
73+
SET_DEPOSITS_RESERVE_TARGET_FACTORY = "0x68009122a394504E8fD7fee58F92Cd73c6A60717"
74+
SET_DEPOSITS_RESERVE_TARGET_TRUSTED_CALLER = "0x84DffcfB232594975C608DE92544Ff239a24c9E9"
75+
SET_DEPOSITS_RESERVE_TARGET_MAX = 9600 * 10**18
76+
6377
STAKING_MODULE_UNVETTING_ROLE = web3.keccak(text="STAKING_MODULE_UNVETTING_ROLE").hex()
6478
TOP_UP_ROLE = web3.keccak(text="TOP_UP_ROLE").hex()
79+
BUFFER_RESERVE_MANAGER_ROLE = web3.keccak(text="BUFFER_RESERVE_MANAGER_ROLE")
6580

6681
ORACLE_COMMITTEE_QUORUM = 6
6782
NEW_DSM_VERSION = 5
@@ -127,10 +142,11 @@ class OracleMemberMapping(NamedTuple):
127142
# ============================================================================
128143
EXPECTED_VOTE_ID = None
129144
EXPECTED_DG_PROPOSAL_ID = None
130-
EXPECTED_VOTE_EVENTS_COUNT = 1
145+
EXPECTED_VOTE_EVENTS_COUNT = 2
131146
# 4 committees * 10 members * 2 (remove + add) + locator upgrade
132147
# + unvetting role revoke + grant + top-up role revoke + grant
133-
EXPECTED_DG_EVENTS_COUNT = 85
148+
# + buffer reserve manager role grant
149+
EXPECTED_DG_EVENTS_COUNT = 86
134150
IPFS_DESCRIPTION_HASH = None
135151

136152

@@ -303,6 +319,11 @@ def runtime_upgrade_context():
303319
# Cross-check the deploy addresses against the vote script copies
304320
assert NEW_DEPOSIT_SECURITY_MODULE.lower() == vote_script.NEW_DEPOSIT_SECURITY_MODULE.lower()
305321
assert NEW_LIDO_LOCATOR_IMPLEMENTATION.lower() == vote_script.NEW_LIDO_LOCATOR_IMPLEMENTATION.lower()
322+
assert SET_DEPOSITS_RESERVE_TARGET_FACTORY.lower() == vote_script.SET_DEPOSITS_RESERVE_TARGET_FACTORY.lower()
323+
assert (
324+
SET_DEPOSITS_RESERVE_TARGET_TRUSTED_CALLER.lower()
325+
== vote_script.SET_DEPOSITS_RESERVE_TARGET_TRUSTED_CALLER.lower()
326+
)
306327
for test_mapping, script_mapping in zip(ORACLE_MEMBER_MAPPINGS, vote_script.ORACLE_MEMBER_MAPPINGS):
307328
assert test_mapping.old_member.lower() == script_mapping.old_member.lower()
308329
assert test_mapping.delegation_contract.lower() == script_mapping.delegation_contract.lower()
@@ -369,6 +390,11 @@ def test_vote(
369390
# =======================================================================
370391
# ========================= Before voting checks ========================
371392
# =======================================================================
393+
acl = interface.ACL(ACL)
394+
easy_track = interface.EasyTrack(EASYTRACK)
395+
assert not acl.hasPermission(EASYTRACK_EVMSCRIPT_EXECUTOR, LIDO, BUFFER_RESERVE_MANAGER_ROLE)
396+
assert SET_DEPOSITS_RESERVE_TARGET_FACTORY not in easy_track.getEVMScriptFactories()
397+
372398
assert get_lido_vote_cid_from_str(find_metadata_by_vote_id(vote_id)) == expected_ipfs_description_hash
373399

374400
vote_tx: TransactionReceipt = helpers.execute_vote(vote_id=vote_id, accounts=accounts, dao_voting=voting)
@@ -396,6 +422,16 @@ def test_vote(
396422
proposal_calls=dual_governance_proposal_calls,
397423
)
398424

425+
# 2. Add SetDepositsReserveTarget factory to Easy Track
426+
validate_evmscript_factory_added_event(
427+
vote_events[1],
428+
EVMScriptFactoryAdded(
429+
factory_addr=SET_DEPOSITS_RESERVE_TARGET_FACTORY,
430+
permissions=create_permissions(interface.Lido(LIDO), "setDepositsReserveTarget"),
431+
),
432+
emitted_by=EASYTRACK,
433+
)
434+
399435
# =========================================================================
400436
# ======================= Execute DG Proposal =============================
401437
# =========================================================================
@@ -531,17 +567,31 @@ def test_vote(
531567
event_index += 1
532568

533569
# 1.85. Grant TOP_UP_ROLE to the depositor bot DelegationContract
534-
# (the last inner call group also carries the Agent.forward service events)
535570
validate_role_grant_event(
536571
dg_events[event_index],
537572
role_hash=TOP_UP_ROLE,
538573
account=DEPOSITOR_BOT_DELEGATION_CONTRACT,
539574
sender=AGENT,
540575
emitted_by=TOP_UP_GATEWAY,
541-
events_chain=["LogScriptCall", "RoleGranted", "ScriptResult", "Executed"],
542576
)
543577
event_index += 1
544578

579+
# 1.86. Grant BUFFER_RESERVE_MANAGER_ROLE to the Easy Track EVMScriptExecutor
580+
# (the last inner call group also carries the Agent.forward service events)
581+
validate_events_chain(
582+
[e.name for e in dg_events[event_index]],
583+
["LogScriptCall", "SetPermission", "ScriptResult", "Executed"],
584+
)
585+
set_permission_event = _single_event(dg_events[event_index], "SetPermission")
586+
assert convert.to_address(set_permission_event["entity"]) == convert.to_address(
587+
EASYTRACK_EVMSCRIPT_EXECUTOR
588+
)
589+
assert convert.to_address(set_permission_event["app"]) == convert.to_address(LIDO)
590+
assert _normalize_role(set_permission_event["role"]) == BUFFER_RESERVE_MANAGER_ROLE.hex().replace("0x", "")
591+
assert set_permission_event["allowed"] is True
592+
_assert_emitted_by(set_permission_event, ACL)
593+
event_index += 1
594+
545595
assert event_index == EXPECTED_DG_EVENTS_COUNT
546596

547597
# =========================================================================
@@ -574,6 +624,54 @@ def test_vote(
574624
else:
575625
assert after_value == before_value, f"Locator entry {name} changed unexpectedly"
576626

627+
# Easy Track factory for deposit reserve target management
628+
acl = interface.ACL(ACL)
629+
easy_track = interface.EasyTrack(EASYTRACK)
630+
factory = interface.SetDepositsReserveTarget(SET_DEPOSITS_RESERVE_TARGET_FACTORY)
631+
lido = interface.Lido(LIDO)
632+
633+
assert acl.hasPermission(EASYTRACK_EVMSCRIPT_EXECUTOR, LIDO, BUFFER_RESERVE_MANAGER_ROLE)
634+
assert SET_DEPOSITS_RESERVE_TARGET_FACTORY in easy_track.getEVMScriptFactories()
635+
assert easy_track.evmScriptFactoryPermissions(SET_DEPOSITS_RESERVE_TARGET_FACTORY) == create_permissions(
636+
lido, "setDepositsReserveTarget"
637+
)
638+
assert convert.to_address(factory.trustedCaller()) == convert.to_address(
639+
SET_DEPOSITS_RESERVE_TARGET_TRUSTED_CALLER
640+
)
641+
assert convert.to_address(factory.lido()) == convert.to_address(LIDO)
642+
assert factory.MAX_DEPOSITS_RESERVE_TARGET() == SET_DEPOSITS_RESERVE_TARGET_MAX
643+
644+
# Happy path: the granted role lets the EVMScriptExecutor move the target,
645+
# and the factory builds a script for exactly that call
646+
chain.snapshot()
647+
try:
648+
new_target = lido.getDepositsReserveTarget() + 10**18
649+
assert new_target <= SET_DEPOSITS_RESERVE_TARGET_MAX
650+
651+
# the factory builds a script for the new target and guards its limits
652+
call_data = encode_abi(["uint256"], [new_target])
653+
assert factory.decodeEVMScriptCallData(call_data) == new_target
654+
factory.createEVMScript(SET_DEPOSITS_RESERVE_TARGET_TRUSTED_CALLER, call_data)
655+
656+
with reverts("CALLER_IS_FORBIDDEN"):
657+
factory.createEVMScript(stranger, call_data)
658+
659+
with reverts("DEPOSITS_RESERVE_TARGET_TOO_HIGH"):
660+
factory.createEVMScript(
661+
SET_DEPOSITS_RESERVE_TARGET_TRUSTED_CALLER,
662+
encode_abi(["uint256"], [SET_DEPOSITS_RESERVE_TARGET_MAX + 1]),
663+
)
664+
665+
# the granted role lets the EVMScriptExecutor apply the new target
666+
executor = accounts.at(EASYTRACK_EVMSCRIPT_EXECUTOR, force=True)
667+
lido.setDepositsReserveTarget(new_target, {"from": executor})
668+
assert lido.getDepositsReserveTarget() == new_target
669+
670+
with reverts("SAME_DEPOSITS_RESERVE_TARGET"):
671+
factory.createEVMScript(SET_DEPOSITS_RESERVE_TARGET_TRUSTED_CALLER, call_data)
672+
finally:
673+
chain.revert()
674+
577675
assert not staking_router.hasRole(STAKING_MODULE_UNVETTING_ROLE, OLD_DEPOSIT_SECURITY_MODULE)
578676
assert staking_router.hasRole(STAKING_MODULE_UNVETTING_ROLE, NEW_DEPOSIT_SECURITY_MODULE)
579677
assert not top_up_gateway.hasRole(TOP_UP_ROLE, DEPOSITOR_BOT_OLD_EOA)

0 commit comments

Comments
 (0)