Skip to content

Commit 84f5df3

Browse files
authored
Merge pull request #634 from lidofinance/feat/next-vote
Vote: 17.06.2026
2 parents d684533 + e592911 commit 84f5df3

15 files changed

Lines changed: 1631 additions & 495 deletions

archive/scripts/upgrade_2026_06_17.py

Lines changed: 263 additions & 0 deletions
Large diffs are not rendered by default.

archive/tests/test_2026_06_17.py

Lines changed: 615 additions & 0 deletions
Large diffs are not rendered by default.

configs/config_mainnet.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@
8282
EASYTRACK_SIMPLE_DVT_SUBMIT_VALIDATOR_EXIT_REQUEST_HASHES_FACTORY = "0x58A59dDC6Aea9b1D5743D024E15DfA4badB56E37"
8383
EASYTRACK_CS_SET_VETTED_GATE_TREE_FACTORY = "0xBc5642bDD6F2a54b01A75605aAe9143525D97308"
8484

85+
# Liquidity Observation Lab (LOL) stETH AllowedRecipientsRegistry
86+
LOL_ALLOWED_RECIPIENTS_REGISTRY = "0x48c4929630099b217136b64089E8543dB0E5163a"
87+
8588
# Multisigs
8689
FINANCE_MULTISIG = "0x48F300bD3C52c7dA6aAbDE4B683dEB27d38B9ABb"
8790
L1_EMERGENCY_BRAKES_MULTISIG = "0x73b047fe6337183A454c5217241D780a932777bD"
@@ -155,7 +158,7 @@
155158
"0x637572617465642d6f6e636861696e2d76310000000000000000000000000000"
156159
)
157160
CURATED_STAKING_MODULE_OPERATORS_COUNT = 39
158-
CURATED_STAKING_MODULE_OPERATORS_ACTIVE_COUNT = 36
161+
CURATED_STAKING_MODULE_OPERATORS_ACTIVE_COUNT = 35
159162
CURATED_STAKING_MODULE_PRIORITY_EXIT_SHARE_THRESHOLD = 10_000
160163
CURATED_STAKING_MODULE_MAX_DEPOSITS_PER_BLOCK = 150
161164
CURATED_STAKING_MODULE_MIN_DEPOSITS_BLOCK_DISTANCE = 25
@@ -354,6 +357,15 @@
354357
VEB_TWG_GATE_SEAL_EXPIRY_TIMESTAMP = 1789207187
355358
VEB_TWG_SEAL_COMMITTEE = "0x8772E3a2D86B9347A2688f9bc1808A6d8917760C"
356359

360+
# CircuitBreaker
361+
CIRCUIT_BREAKER = "0x6019CB557978296BA3C08a7B73225C0975DFB2F7"
362+
CIRCUIT_BREAKER_MIN_PAUSE_DURATION = 432000 # 5 days
363+
CIRCUIT_BREAKER_MAX_PAUSE_DURATION = 5184000 # 60 days
364+
CIRCUIT_BREAKER_PAUSE_DURATION = 1814400 # 21 days
365+
CIRCUIT_BREAKER_MIN_HEARTBEAT_INTERVAL = 2592000 # 30 days
366+
CIRCUIT_BREAKER_MAX_HEARTBEAT_INTERVAL = 94608000 # 1095 days (~3 years)
367+
CIRCUIT_BREAKER_HEARTBEAT_INTERVAL = 31536000 # 1 year (365 days)
368+
357369
# Aragon Permissions test
358370
ACL_DEPLOY_BLOCK_NUMBER = 11473216
359371

interfaces/CircuitBreaker.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[{"inputs":[{"internalType":"address","name":"_admin","type":"address"},{"internalType":"uint256","name":"_minPauseDuration","type":"uint256"},{"internalType":"uint256","name":"_maxPauseDuration","type":"uint256"},{"internalType":"uint256","name":"_minHeartbeatInterval","type":"uint256"},{"internalType":"uint256","name":"_maxHeartbeatInterval","type":"uint256"},{"internalType":"uint256","name":"_initialPauseDuration","type":"uint256"},{"internalType":"uint256","name":"_initialHeartbeatInterval","type":"uint256"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"AdminZero","type":"error"},{"inputs":[],"name":"HeartbeatExpired","type":"error"},{"inputs":[],"name":"HeartbeatIntervalAboveMax","type":"error"},{"inputs":[],"name":"HeartbeatIntervalBelowMin","type":"error"},{"inputs":[],"name":"MinHeartbeatIntervalExceedsMax","type":"error"},{"inputs":[],"name":"MinHeartbeatIntervalZero","type":"error"},{"inputs":[],"name":"MinPauseDurationExceedsMax","type":"error"},{"inputs":[],"name":"MinPauseDurationZero","type":"error"},{"inputs":[],"name":"PausableZero","type":"error"},{"inputs":[],"name":"PauseDurationAboveMax","type":"error"},{"inputs":[],"name":"PauseDurationBelowMin","type":"error"},{"inputs":[],"name":"PauseFailed","type":"error"},{"inputs":[],"name":"ReentrantCall","type":"error"},{"inputs":[],"name":"SenderNotAdmin","type":"error"},{"inputs":[],"name":"SenderNotPauser","type":"error"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"admin","type":"address"},{"indexed":false,"internalType":"uint256","name":"minPauseDuration","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"maxPauseDuration","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"minHeartbeatInterval","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"maxHeartbeatInterval","type":"uint256"}],"name":"CircuitBreakerInitialized","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"previousHeartbeatInterval","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"newHeartbeatInterval","type":"uint256"}],"name":"HeartbeatIntervalUpdated","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"pauser","type":"address"},{"indexed":false,"internalType":"uint256","name":"newHeartbeatExpiry","type":"uint256"}],"name":"HeartbeatUpdated","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"previousPauseDuration","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"newPauseDuration","type":"uint256"}],"name":"PauseDurationUpdated","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"pausable","type":"address"},{"indexed":true,"internalType":"address","name":"pauser","type":"address"},{"indexed":false,"internalType":"uint256","name":"pauseDuration","type":"uint256"}],"name":"PauseTriggered","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"pausable","type":"address"},{"indexed":true,"internalType":"address","name":"previousPauser","type":"address"},{"indexed":true,"internalType":"address","name":"newPauser","type":"address"}],"name":"PauserSet","type":"event"},{"inputs":[],"name":"ADMIN","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MAX_HEARTBEAT_INTERVAL","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MAX_PAUSE_DURATION","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MIN_HEARTBEAT_INTERVAL","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MIN_PAUSE_DURATION","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_pauser","type":"address"}],"name":"getPausableCount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getPausables","outputs":[{"internalType":"address[]","name":"","type":"address[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_pausable","type":"address"}],"name":"getPauser","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"heartbeat","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"pauser","type":"address"}],"name":"heartbeatExpiry","outputs":[{"internalType":"uint256","name":"timestamp","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"heartbeatInterval","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_pauser","type":"address"}],"name":"isPauserLive","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_pausable","type":"address"}],"name":"pause","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"pauseDuration","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_pausable","type":"address"},{"internalType":"address","name":"_newPauser","type":"address"}],"name":"registerPauser","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_newHeartbeatInterval","type":"uint256"}],"name":"setHeartbeatInterval","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_newPauseDuration","type":"uint256"}],"name":"setPauseDuration","outputs":[],"stateMutability":"nonpayable","type":"function"}]
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
[
2+
{
3+
"inputs": [],
4+
"name": "PAUSE_ROLE",
5+
"outputs": [{"internalType": "bytes32", "name": "", "type": "bytes32"}],
6+
"stateMutability": "view",
7+
"type": "function"
8+
},
9+
{
10+
"inputs": [],
11+
"name": "RESUME_ROLE",
12+
"outputs": [{"internalType": "bytes32", "name": "", "type": "bytes32"}],
13+
"stateMutability": "view",
14+
"type": "function"
15+
},
16+
{
17+
"inputs": [],
18+
"name": "isPaused",
19+
"outputs": [{"internalType": "bool", "name": "", "type": "bool"}],
20+
"stateMutability": "view",
21+
"type": "function"
22+
},
23+
{
24+
"inputs": [],
25+
"name": "getResumeSinceTimestamp",
26+
"outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}],
27+
"stateMutability": "view",
28+
"type": "function"
29+
},
30+
{
31+
"inputs": [],
32+
"name": "PAUSE_INFINITELY",
33+
"outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}],
34+
"stateMutability": "view",
35+
"type": "function"
36+
},
37+
{
38+
"inputs": [{"internalType": "uint256", "name": "_duration", "type": "uint256"}],
39+
"name": "pauseFor",
40+
"outputs": [],
41+
"stateMutability": "nonpayable",
42+
"type": "function"
43+
},
44+
{
45+
"inputs": [],
46+
"name": "resume",
47+
"outputs": [],
48+
"stateMutability": "nonpayable",
49+
"type": "function"
50+
},
51+
{
52+
"inputs": [
53+
{"internalType": "bytes32", "name": "role", "type": "bytes32"},
54+
{"internalType": "address", "name": "account", "type": "address"}
55+
],
56+
"name": "hasRole",
57+
"outputs": [{"internalType": "bool", "name": "", "type": "bool"}],
58+
"stateMutability": "view",
59+
"type": "function"
60+
},
61+
{
62+
"inputs": [
63+
{"internalType": "bytes32", "name": "role", "type": "bytes32"},
64+
{"internalType": "uint256", "name": "index", "type": "uint256"}
65+
],
66+
"name": "getRoleMember",
67+
"outputs": [{"internalType": "address", "name": "", "type": "address"}],
68+
"stateMutability": "view",
69+
"type": "function"
70+
},
71+
{
72+
"inputs": [{"internalType": "bytes32", "name": "role", "type": "bytes32"}],
73+
"name": "getRoleMemberCount",
74+
"outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}],
75+
"stateMutability": "view",
76+
"type": "function"
77+
},
78+
{
79+
"inputs": [
80+
{"internalType": "bytes32", "name": "role", "type": "bytes32"},
81+
{"internalType": "address", "name": "account", "type": "address"}
82+
],
83+
"name": "grantRole",
84+
"outputs": [],
85+
"stateMutability": "nonpayable",
86+
"type": "function"
87+
},
88+
{
89+
"inputs": [
90+
{"internalType": "bytes32", "name": "role", "type": "bytes32"},
91+
{"internalType": "address", "name": "account", "type": "address"}
92+
],
93+
"name": "revokeRole",
94+
"outputs": [],
95+
"stateMutability": "nonpayable",
96+
"type": "function"
97+
},
98+
{
99+
"anonymous": false,
100+
"inputs": [
101+
{"indexed": true, "internalType": "bytes32", "name": "role", "type": "bytes32"},
102+
{"indexed": true, "internalType": "address", "name": "account", "type": "address"},
103+
{"indexed": true, "internalType": "address", "name": "sender", "type": "address"}
104+
],
105+
"name": "RoleGranted",
106+
"type": "event"
107+
},
108+
{
109+
"anonymous": false,
110+
"inputs": [
111+
{"indexed": true, "internalType": "bytes32", "name": "role", "type": "bytes32"},
112+
{"indexed": true, "internalType": "address", "name": "account", "type": "address"},
113+
{"indexed": true, "internalType": "address", "name": "sender", "type": "address"}
114+
],
115+
"name": "RoleRevoked",
116+
"type": "event"
117+
}
118+
]

tests/acceptance/test_accounting_oracle_negative.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -348,7 +348,6 @@ def test_unexpected_extra_data_item(self, extra_data_service: ExtraDataService)
348348
extra_data = extra_data_service.collect(
349349
{
350350
(1, 2): self.get_nor_operator_exited_keys(2) + 1,
351-
(1, 3): self.get_nor_operator_exited_keys(3) + 1,
352351
(1, 4): self.get_nor_operator_exited_keys(4) + 1,
353352
(1, 5): self.get_nor_operator_exited_keys(5) + 1,
354353
(1, 6): self.get_nor_operator_exited_keys(6) + 1,
@@ -367,7 +366,7 @@ def test_unexpected_extra_data_item(self, extra_data_service: ExtraDataService)
367366
"UnexpectedExtraDataItemsCount(uint256,uint256)",
368367
[
369368
extra_data.items_count - 1, # expected count
370-
extra_data.items_count - 1, # index of the item that makes the count mismatch
369+
extra_data.items_count, # received count (all items fit into a single chunk)
371370
],
372371
)
373372
):
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import pytest
2+
from brownie import interface, web3 # type: ignore
3+
4+
from utils.config import (
5+
AGENT,
6+
CIRCUIT_BREAKER,
7+
CIRCUIT_BREAKER_HEARTBEAT_INTERVAL,
8+
CIRCUIT_BREAKER_MAX_HEARTBEAT_INTERVAL,
9+
CIRCUIT_BREAKER_MAX_PAUSE_DURATION,
10+
CIRCUIT_BREAKER_MIN_HEARTBEAT_INTERVAL,
11+
CIRCUIT_BREAKER_MIN_PAUSE_DURATION,
12+
CIRCUIT_BREAKER_PAUSE_DURATION,
13+
CS_ACCOUNTING_ADDRESS,
14+
CS_EJECTOR_ADDRESS,
15+
CS_FEE_ORACLE_ADDRESS,
16+
CS_VERIFIER_V2_ADDRESS,
17+
CS_VETTED_GATE_ADDRESS,
18+
CSM_ADDRESS,
19+
CSM_COMMITTEE_MS,
20+
GATE_SEAL_COMMITTEE,
21+
PREDEPOSIT_GUARANTEE,
22+
RESEAL_MANAGER,
23+
TRIGGERABLE_WITHDRAWALS_GATEWAY,
24+
VALIDATORS_EXIT_BUS_ORACLE,
25+
VAULT_HUB,
26+
WITHDRAWAL_QUEUE,
27+
)
28+
29+
30+
EXPECTED_PAUSABLES = [
31+
(WITHDRAWAL_QUEUE, GATE_SEAL_COMMITTEE),
32+
(VALIDATORS_EXIT_BUS_ORACLE, GATE_SEAL_COMMITTEE),
33+
(TRIGGERABLE_WITHDRAWALS_GATEWAY, GATE_SEAL_COMMITTEE),
34+
(VAULT_HUB, GATE_SEAL_COMMITTEE),
35+
(PREDEPOSIT_GUARANTEE, GATE_SEAL_COMMITTEE),
36+
(CSM_ADDRESS, CSM_COMMITTEE_MS),
37+
(CS_ACCOUNTING_ADDRESS, CSM_COMMITTEE_MS),
38+
(CS_FEE_ORACLE_ADDRESS, CSM_COMMITTEE_MS),
39+
(CS_VERIFIER_V2_ADDRESS, CSM_COMMITTEE_MS),
40+
(CS_VETTED_GATE_ADDRESS, CSM_COMMITTEE_MS),
41+
(CS_EJECTOR_ADDRESS, CSM_COMMITTEE_MS),
42+
]
43+
44+
45+
@pytest.fixture(scope="module")
46+
def circuit_breaker():
47+
return interface.CircuitBreaker(CIRCUIT_BREAKER)
48+
49+
50+
def test_initial_values(circuit_breaker):
51+
assert circuit_breaker.ADMIN() == AGENT, (
52+
f"ADMIN: expected {AGENT}, got {circuit_breaker.ADMIN()}"
53+
)
54+
55+
assert circuit_breaker.MIN_PAUSE_DURATION() == CIRCUIT_BREAKER_MIN_PAUSE_DURATION, (
56+
f"MIN_PAUSE_DURATION: expected {CIRCUIT_BREAKER_MIN_PAUSE_DURATION}, got {circuit_breaker.MIN_PAUSE_DURATION()}"
57+
)
58+
assert circuit_breaker.MAX_PAUSE_DURATION() == CIRCUIT_BREAKER_MAX_PAUSE_DURATION, (
59+
f"MAX_PAUSE_DURATION: expected {CIRCUIT_BREAKER_MAX_PAUSE_DURATION}, got {circuit_breaker.MAX_PAUSE_DURATION()}"
60+
)
61+
assert circuit_breaker.MIN_HEARTBEAT_INTERVAL() == CIRCUIT_BREAKER_MIN_HEARTBEAT_INTERVAL, (
62+
f"MIN_HEARTBEAT_INTERVAL: expected {CIRCUIT_BREAKER_MIN_HEARTBEAT_INTERVAL}, got {circuit_breaker.MIN_HEARTBEAT_INTERVAL()}"
63+
)
64+
assert circuit_breaker.MAX_HEARTBEAT_INTERVAL() == CIRCUIT_BREAKER_MAX_HEARTBEAT_INTERVAL, (
65+
f"MAX_HEARTBEAT_INTERVAL: expected {CIRCUIT_BREAKER_MAX_HEARTBEAT_INTERVAL}, got {circuit_breaker.MAX_HEARTBEAT_INTERVAL()}"
66+
)
67+
assert circuit_breaker.pauseDuration() == CIRCUIT_BREAKER_PAUSE_DURATION, (
68+
f"pauseDuration: expected {CIRCUIT_BREAKER_PAUSE_DURATION}, got {circuit_breaker.pauseDuration()}"
69+
)
70+
assert circuit_breaker.heartbeatInterval() == CIRCUIT_BREAKER_HEARTBEAT_INTERVAL, (
71+
f"heartbeatInterval: expected {CIRCUIT_BREAKER_HEARTBEAT_INTERVAL}, got {circuit_breaker.heartbeatInterval()}"
72+
)
73+
74+
assert 0 < CIRCUIT_BREAKER_MIN_PAUSE_DURATION <= CIRCUIT_BREAKER_PAUSE_DURATION <= CIRCUIT_BREAKER_MAX_PAUSE_DURATION, (
75+
f"pause duration bounds broken: "
76+
f"min={CIRCUIT_BREAKER_MIN_PAUSE_DURATION}, "
77+
f"initial={CIRCUIT_BREAKER_PAUSE_DURATION}, "
78+
f"max={CIRCUIT_BREAKER_MAX_PAUSE_DURATION}"
79+
)
80+
assert 0 < CIRCUIT_BREAKER_MIN_HEARTBEAT_INTERVAL <= CIRCUIT_BREAKER_HEARTBEAT_INTERVAL <= CIRCUIT_BREAKER_MAX_HEARTBEAT_INTERVAL, (
81+
f"heartbeat interval bounds broken: "
82+
f"min={CIRCUIT_BREAKER_MIN_HEARTBEAT_INTERVAL}, "
83+
f"initial={CIRCUIT_BREAKER_HEARTBEAT_INTERVAL}, "
84+
f"max={CIRCUIT_BREAKER_MAX_HEARTBEAT_INTERVAL}"
85+
)
86+
87+
88+
def test_pausables_set(circuit_breaker):
89+
actual = sorted(addr.lower() for addr in circuit_breaker.getPausables())
90+
expected = sorted(p.lower() for p, _ in EXPECTED_PAUSABLES)
91+
assert actual == expected, (
92+
f"pausables mismatch: missing {sorted(set(expected) - set(actual))}, "
93+
f"extra {sorted(set(actual) - set(expected))}"
94+
)
95+
96+
97+
@pytest.mark.parametrize("pausable, expected_pauser", EXPECTED_PAUSABLES)
98+
def test_pauser_assignment(circuit_breaker, pausable, expected_pauser):
99+
assert circuit_breaker.getPauser(pausable).lower() == expected_pauser.lower(), (
100+
f"pauser for {pausable}: expected {expected_pauser}, got {circuit_breaker.getPauser(pausable)}"
101+
)
102+
103+
104+
def test_pausable_counts_per_pauser(circuit_breaker):
105+
expected_counts = {}
106+
for _, pauser in EXPECTED_PAUSABLES:
107+
expected_counts[pauser.lower()] = expected_counts.get(pauser.lower(), 0) + 1
108+
109+
for pauser, expected in expected_counts.items():
110+
assert circuit_breaker.getPausableCount(pauser) == expected, (
111+
f"pausable count for pauser {pauser}: expected {expected}, got {circuit_breaker.getPausableCount(pauser)}"
112+
)
113+
114+
115+
@pytest.mark.parametrize("pausable, _pauser", EXPECTED_PAUSABLES)
116+
def test_pause_role_holders(_pauser, pausable):
117+
pausable_contract = interface.IPausableUntilWithRoles(pausable)
118+
pause_role = str(pausable_contract.PAUSE_ROLE())
119+
assert pausable_contract.getRoleMemberCount(pause_role) == 2, (
120+
f"PAUSE_ROLE holder count on {pausable}: expected 2, got {pausable_contract.getRoleMemberCount(pause_role)}"
121+
)
122+
holders = {
123+
pausable_contract.getRoleMember(pause_role, 0).lower(),
124+
pausable_contract.getRoleMember(pause_role, 1).lower(),
125+
}
126+
expected_holders = {CIRCUIT_BREAKER.lower(), RESEAL_MANAGER.lower()}
127+
assert holders == expected_holders, (
128+
f"PAUSE_ROLE holders on {pausable}: expected {sorted(expected_holders)}, got {sorted(holders)}"
129+
)
130+
131+
132+
@pytest.mark.parametrize("pausable, _pauser", EXPECTED_PAUSABLES)
133+
def test_resume_role_holder(_pauser, pausable):
134+
pausable_contract = interface.IPausableUntilWithRoles(pausable)
135+
resume_role = str(pausable_contract.RESUME_ROLE())
136+
assert pausable_contract.getRoleMemberCount(resume_role) == 1, (
137+
f"RESUME_ROLE holder count on {pausable}: expected 1, got {pausable_contract.getRoleMemberCount(resume_role)}"
138+
)
139+
assert pausable_contract.getRoleMember(resume_role, 0).lower() == RESEAL_MANAGER.lower(), (
140+
f"RESUME_ROLE holder on {pausable}: expected {RESEAL_MANAGER}, got {pausable_contract.getRoleMember(resume_role, 0)}"
141+
)
142+
143+
144+
@pytest.mark.parametrize("pauser", sorted({p for _, p in EXPECTED_PAUSABLES}))
145+
def test_pauser_is_live(circuit_breaker, pauser):
146+
assert circuit_breaker.isPauserLive(pauser), f"pauser {pauser} not live"
147+
expiry = circuit_breaker.heartbeatExpiry(pauser)
148+
block_timestamp = web3.eth.get_block("latest")["timestamp"]
149+
assert expiry > block_timestamp, (
150+
f"pauser {pauser} heartbeat expired: expiry={expiry}, block.timestamp={block_timestamp}"
151+
)

0 commit comments

Comments
 (0)