1use alloy::{
2 primitives::{Address, B256, Bytes, U256},
3 providers::Provider,
4 rpc::types::TransactionReceipt,
5};
6use anyhow::{Result, anyhow};
7use clap::ValueEnum;
8use hotshot_contract_adapter::sol_types::{
9 EspToken, FeeContract, LightClient, OpsTimelock, RewardClaim, SafeExitTimelock, StakeTable,
10 StakeTableV3,
11};
12
13use crate::{
14 Contract, Contracts, OwnableContract,
15 output::CalldataInfo,
16 proposals::multisig::{encode_generic_calldata, encode_upgrade_calldata},
17 retry_until_true,
18};
19
20#[derive(Debug, Default, Clone)]
22pub struct TimelockOperationPayload {
23 pub target: Address,
25 pub value: U256,
27 pub data: Bytes,
29 pub predecessor: B256,
31 pub salt: B256,
33 pub delay: U256,
35}
36
37#[derive(Debug, Clone, Default)]
39pub struct TimelockOperationParams {
40 pub multisig_proposer: Option<Address>,
42 pub operation_id: Option<B256>,
44 pub dry_run: bool,
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
49pub enum TimelockOperationType {
50 Schedule,
51 Execute,
52 Cancel,
53}
54
55#[derive(Debug)]
57pub enum TimelockContract {
58 OpsTimelock(Address),
59 SafeExitTimelock(Address),
60}
61
62impl TimelockContract {
63 pub async fn get_operation_id(
64 &self,
65 operation: &TimelockOperationPayload,
66 provider: &impl Provider,
67 ) -> Result<B256> {
68 match self {
69 TimelockContract::OpsTimelock(timelock_addr) => {
70 Ok(OpsTimelock::new(*timelock_addr, &provider)
71 .hashOperation(
72 operation.target,
73 operation.value,
74 operation.data.clone(),
75 operation.predecessor,
76 operation.salt,
77 )
78 .call()
79 .await?)
80 },
81 TimelockContract::SafeExitTimelock(timelock_addr) => {
82 Ok(SafeExitTimelock::new(*timelock_addr, &provider)
83 .hashOperation(
84 operation.target,
85 operation.value,
86 operation.data.clone(),
87 operation.predecessor,
88 operation.salt,
89 )
90 .call()
91 .await?)
92 },
93 }
94 }
95
96 pub async fn schedule(
97 &self,
98 operation: TimelockOperationPayload,
99 provider: &impl Provider,
100 ) -> Result<TransactionReceipt> {
101 self.call_timelock_method(TimelockOperationType::Schedule, operation, None, provider)
102 .await
103 }
104
105 pub async fn execute(
106 &self,
107 operation: TimelockOperationPayload,
108 provider: &impl Provider,
109 ) -> Result<TransactionReceipt> {
110 self.call_timelock_method(TimelockOperationType::Execute, operation, None, provider)
111 .await
112 }
113
114 pub async fn cancel(
115 &self,
116 operation_id: B256,
117 provider: &impl Provider,
118 ) -> Result<TransactionReceipt> {
119 let placeholder_operation = TimelockOperationPayload::default();
121 self.call_timelock_method(
122 TimelockOperationType::Cancel,
123 placeholder_operation,
124 Some(operation_id),
125 provider,
126 )
127 .await
128 }
129
130 async fn call_timelock_method(
132 &self,
133 method: TimelockOperationType,
134 operation: TimelockOperationPayload,
135 operation_id: Option<B256>,
136 provider: &impl Provider,
137 ) -> Result<TransactionReceipt> {
138 let pending_tx = match (self, method) {
139 (TimelockContract::OpsTimelock(addr), TimelockOperationType::Schedule) => {
140 OpsTimelock::new(*addr, &provider)
141 .schedule(
142 operation.target,
143 operation.value,
144 operation.data,
145 operation.predecessor,
146 operation.salt,
147 operation.delay,
148 )
149 .send()
150 .await?
151 },
152 (TimelockContract::SafeExitTimelock(addr), TimelockOperationType::Schedule) => {
153 SafeExitTimelock::new(*addr, &provider)
154 .schedule(
155 operation.target,
156 operation.value,
157 operation.data,
158 operation.predecessor,
159 operation.salt,
160 operation.delay,
161 )
162 .send()
163 .await?
164 },
165 (TimelockContract::OpsTimelock(addr), TimelockOperationType::Execute) => {
166 OpsTimelock::new(*addr, &provider)
167 .execute(
168 operation.target,
169 operation.value,
170 operation.data,
171 operation.predecessor,
172 operation.salt,
173 )
174 .send()
175 .await?
176 },
177 (TimelockContract::SafeExitTimelock(addr), TimelockOperationType::Execute) => {
178 SafeExitTimelock::new(*addr, &provider)
179 .execute(
180 operation.target,
181 operation.value,
182 operation.data,
183 operation.predecessor,
184 operation.salt,
185 )
186 .send()
187 .await?
188 },
189 (TimelockContract::OpsTimelock(addr), TimelockOperationType::Cancel) => {
190 OpsTimelock::new(*addr, &provider)
191 .cancel(
192 operation_id
193 .ok_or_else(|| anyhow::anyhow!("operation_id required for cancel"))?,
194 )
195 .send()
196 .await?
197 },
198 (TimelockContract::SafeExitTimelock(addr), TimelockOperationType::Cancel) => {
199 SafeExitTimelock::new(*addr, &provider)
200 .cancel(
201 operation_id
202 .ok_or_else(|| anyhow::anyhow!("operation_id required for cancel"))?,
203 )
204 .send()
205 .await?
206 },
207 };
208
209 let tx_hash = *pending_tx.tx_hash();
210 tracing::info!(%tx_hash, "waiting for tx to be mined");
211 let receipt = pending_tx.get_receipt().await?;
212 Ok(receipt)
213 }
214
215 pub async fn is_operation_pending(
216 &self,
217 operation_id: B256,
218 provider: &impl Provider,
219 ) -> Result<bool> {
220 match self {
221 TimelockContract::OpsTimelock(timelock_addr) => {
222 Ok(OpsTimelock::new(*timelock_addr, &provider)
223 .isOperationPending(operation_id)
224 .call()
225 .await?)
226 },
227 TimelockContract::SafeExitTimelock(timelock_addr) => {
228 Ok(SafeExitTimelock::new(*timelock_addr, &provider)
229 .isOperationPending(operation_id)
230 .call()
231 .await?)
232 },
233 }
234 }
235
236 pub async fn is_operation_ready(
237 &self,
238 operation_id: B256,
239 provider: &impl Provider,
240 ) -> Result<bool> {
241 match self {
242 TimelockContract::OpsTimelock(timelock_addr) => {
243 Ok(OpsTimelock::new(*timelock_addr, &provider)
244 .isOperationReady(operation_id)
245 .call()
246 .await?)
247 },
248 TimelockContract::SafeExitTimelock(timelock_addr) => {
249 Ok(SafeExitTimelock::new(*timelock_addr, &provider)
250 .isOperationReady(operation_id)
251 .call()
252 .await?)
253 },
254 }
255 }
256
257 pub async fn is_operation_done(
258 &self,
259 operation_id: B256,
260 provider: &impl Provider,
261 ) -> Result<bool> {
262 match self {
263 TimelockContract::OpsTimelock(timelock_addr) => {
264 Ok(OpsTimelock::new(*timelock_addr, &provider)
265 .isOperationDone(operation_id)
266 .call()
267 .await?)
268 },
269 TimelockContract::SafeExitTimelock(timelock_addr) => {
270 Ok(SafeExitTimelock::new(*timelock_addr, &provider)
271 .isOperationDone(operation_id)
272 .call()
273 .await?)
274 },
275 }
276 }
277
278 pub async fn is_operation_canceled(
279 &self,
280 operation_id: B256,
281 provider: &impl Provider,
282 ) -> Result<bool> {
283 let pending = self.is_operation_pending(operation_id, provider).await?;
284 let done = self.is_operation_done(operation_id, provider).await?;
285 Ok(!pending && !done)
287 }
288}
289
290pub fn derive_timelock_address_from_contract_type(
294 contract_type: OwnableContract,
295 contracts: &Contracts,
296) -> Result<Address> {
297 let timelock_type = match contract_type {
298 OwnableContract::FeeContractProxy
299 | OwnableContract::LightClientProxy
300 | OwnableContract::StakeTableProxy => Contract::OpsTimelock,
301 OwnableContract::EspTokenProxy | OwnableContract::RewardClaimProxy => {
302 Contract::SafeExitTimelock
303 },
304 };
305
306 contracts.address(timelock_type).ok_or_else(|| {
307 anyhow::anyhow!(
308 "{:?} not found in deployed contracts. Deploy it first or provide it via flag.",
309 timelock_type
310 )
311 })
312}
313
314pub async fn get_timelock_for_contract(
316 provider: &impl Provider,
317 contract_type: Contract,
318 target_addr: Address,
319) -> Result<TimelockContract> {
320 match contract_type {
321 Contract::FeeContractProxy => Ok(TimelockContract::OpsTimelock(
322 FeeContract::new(target_addr, &provider)
323 .owner()
324 .call()
325 .await?,
326 )),
327 Contract::EspTokenProxy => Ok(TimelockContract::SafeExitTimelock(
328 EspToken::new(target_addr, &provider).owner().call().await?,
329 )),
330 Contract::LightClientProxy => Ok(TimelockContract::OpsTimelock(
331 LightClient::new(target_addr, &provider)
332 .owner()
333 .call()
334 .await?,
335 )),
336 Contract::StakeTableProxy => Ok(TimelockContract::OpsTimelock(
337 StakeTable::new(target_addr, &provider)
338 .owner()
339 .call()
340 .await?,
341 )),
342 Contract::RewardClaimProxy => Ok(TimelockContract::SafeExitTimelock(
343 RewardClaim::new(target_addr, &provider)
344 .currentAdmin()
345 .call()
346 .await?,
347 )),
348 _ => anyhow::bail!(
349 "Invalid contract type for timelock get operation: {}",
350 contract_type
351 ),
352 }
353}
354
355pub async fn perform_timelock_operation(
358 provider: &impl Provider,
359 contract_type: Contract,
360 operation: TimelockOperationPayload,
361 operation_type: TimelockOperationType,
362 params: TimelockOperationParams,
363) -> Result<B256> {
364 let timelock = get_timelock_for_contract(provider, contract_type, operation.target).await?;
365 let operation_id =
368 if let (TimelockOperationType::Cancel, Some(id)) = (operation_type, params.operation_id) {
369 id
370 } else {
371 timelock.get_operation_id(&operation, &provider).await?
372 };
373
374 if let Some(multisig_proposer) = params.multisig_proposer {
375 perform_timelock_operation_via_multisig(
376 timelock,
377 operation,
378 operation_type,
379 operation_id,
380 multisig_proposer,
381 )
382 .await
383 } else {
384 perform_timelock_operation_via_eoa(
385 timelock,
386 operation,
387 operation_type,
388 operation_id,
389 provider,
390 )
391 .await
392 }
393}
394
395async fn perform_timelock_operation_via_eoa(
397 timelock: TimelockContract,
398 operation: TimelockOperationPayload,
399 operation_type: TimelockOperationType,
400 operation_id: B256,
401 provider: &impl Provider,
402) -> Result<B256> {
403 let receipt = match operation_type {
404 TimelockOperationType::Schedule => timelock.schedule(operation, &provider).await?,
405 TimelockOperationType::Execute => timelock.execute(operation, &provider).await?,
406 TimelockOperationType::Cancel => timelock.cancel(operation_id, &provider).await?,
407 };
408
409 tracing::info!(%receipt.gas_used, %receipt.transaction_hash, "tx mined");
410 if !receipt.inner.is_success() {
411 anyhow::bail!("tx failed: {:?}", receipt);
412 }
413
414 match operation_type {
416 TimelockOperationType::Schedule => {
417 let check_name = format!("Schedule operation {}", operation_id);
418 let is_scheduled = retry_until_true(&check_name, || async {
419 Ok(timelock
420 .is_operation_pending(operation_id, &provider)
421 .await?
422 || timelock.is_operation_ready(operation_id, &provider).await?)
423 })
424 .await?;
425 if !is_scheduled {
426 anyhow::bail!("tx not correctly scheduled: {}", operation_id);
427 }
428 tracing::info!("tx scheduled with id: {}", operation_id);
429 },
430 TimelockOperationType::Execute => {
431 let check_name = format!("Execute operation {}", operation_id);
432 let is_done = retry_until_true(&check_name, || async {
433 timelock.is_operation_done(operation_id, &provider).await
434 })
435 .await?;
436 if !is_done {
437 anyhow::bail!("tx not correctly executed: {}", operation_id);
438 }
439 tracing::info!("tx executed with id: {}", operation_id);
440 },
441 TimelockOperationType::Cancel => {
442 tracing::info!("tx cancelled with id: {}", operation_id);
443 },
444 }
445
446 Ok(operation_id)
447}
448
449async fn perform_timelock_operation_via_multisig(
451 timelock: TimelockContract,
452 operation: TimelockOperationPayload,
453 operation_type: TimelockOperationType,
454 operation_id: B256,
455 multisig_proposer: Address,
456) -> Result<B256> {
457 let timelock_addr = match timelock {
458 TimelockContract::OpsTimelock(addr) => addr,
459 TimelockContract::SafeExitTimelock(addr) => addr,
460 };
461
462 let (function_signature, function_args) = match operation_type {
464 TimelockOperationType::Schedule => (
465 "schedule(address target, uint256 value, bytes data, bytes32 predecessor, bytes32 \
466 salt, uint256 delay)",
467 vec![
468 operation.target.to_string(),
469 operation.value.to_string(),
470 operation.data.to_string(),
471 operation.predecessor.to_string(),
472 operation.salt.to_string(),
473 operation.delay.to_string(),
474 ],
475 ),
476 TimelockOperationType::Execute => (
477 "execute(address target, uint256 value, bytes payload, bytes32 predecessor, bytes32 \
478 salt)",
479 vec![
480 operation.target.to_string(),
481 operation.value.to_string(),
482 operation.data.to_string(),
483 operation.predecessor.to_string(),
484 operation.salt.to_string(),
485 ],
486 ),
487 TimelockOperationType::Cancel => ("cancel(bytes32 id)", vec![operation_id.to_string()]),
488 };
489
490 tracing::info!(
491 "Encoding {:?} operation calldata for timelock {}",
492 operation_type,
493 timelock_addr
494 );
495
496 let calldata =
497 encode_generic_calldata(timelock_addr, function_signature, function_args, U256::ZERO)?;
498
499 tracing::info!(
500 "Timelock {:?} operation calldata encoded. Operation ID: {}",
501 operation_type,
502 operation_id
503 );
504 tracing::info!(
505 "Multisig proposer: {}. To: {}, Data: {}",
506 multisig_proposer,
507 calldata.to,
508 calldata.data
509 );
510
511 Ok(operation_id)
512}
513
514#[derive(Clone, Debug)]
516pub struct StakeTableV3TimelockProposalParams {
517 pub salt: B256,
519 pub delay: U256,
521}
522
523pub struct StakeTableV3TimelockProposal {
528 pub schedule: CalldataInfo,
529 pub execute: CalldataInfo,
530 pub v3_impl_addr: Address,
532 pub timelock_addr: Address,
534}
535
536pub fn encode_stake_table_v3_timelock_proposal(
544 proxy_addr: Address,
545 v3_impl_addr: Address,
546 timelock_addr: Address,
547 init_data: Bytes,
548 params: &StakeTableV3TimelockProposalParams,
549) -> Result<StakeTableV3TimelockProposal> {
550 let upgrade_calldata = encode_upgrade_calldata(proxy_addr, v3_impl_addr, init_data)?;
552
553 let schedule = encode_generic_calldata(
554 timelock_addr,
555 "schedule(address target, uint256 value, bytes data, bytes32 predecessor, bytes32 salt, \
556 uint256 delay)",
557 vec![
558 proxy_addr.to_string(),
559 U256::ZERO.to_string(),
560 upgrade_calldata.data.to_string(),
561 B256::ZERO.to_string(),
562 params.salt.to_string(),
563 params.delay.to_string(),
564 ],
565 U256::ZERO,
566 )?
567 .with_description(format!(
568 "Schedule StakeTable V2 -> V3 upgrade via timelock {timelock_addr:#x} (proxy \
569 {proxy_addr:#x}, impl {v3_impl_addr:#x})"
570 ));
571
572 let execute = encode_generic_calldata(
573 timelock_addr,
574 "execute(address target, uint256 value, bytes payload, bytes32 predecessor, bytes32 salt)",
575 vec![
576 proxy_addr.to_string(),
577 U256::ZERO.to_string(),
578 upgrade_calldata.data.to_string(),
579 B256::ZERO.to_string(),
580 params.salt.to_string(),
581 ],
582 U256::ZERO,
583 )?
584 .with_description(format!(
585 "Execute StakeTable V2 -> V3 upgrade via timelock {timelock_addr:#x} (proxy \
586 {proxy_addr:#x}, impl {v3_impl_addr:#x})"
587 ));
588
589 Ok(StakeTableV3TimelockProposal {
590 schedule,
591 execute,
592 v3_impl_addr,
593 timelock_addr,
594 })
595}
596
597pub async fn upgrade_stake_table_v3_timelock_proposal(
604 provider: impl Provider,
605 contracts: &mut Contracts,
606 params: StakeTableV3TimelockProposalParams,
607) -> Result<StakeTableV3TimelockProposal> {
608 let expected_major_version: u8 = 3;
609
610 tracing::info!("Encoding StakeTableProxy -> StakeTableV3 upgrade via timelock owner");
611 let proxy_addr = contracts
612 .address(Contract::StakeTableProxy)
613 .ok_or_else(|| anyhow!("StakeTableProxy not found, can't upgrade"))?;
614
615 let proxy = StakeTableV3::new(proxy_addr, &provider);
616
617 let owner_addr = proxy.owner().call().await?;
619 let timelock_addr =
620 derive_timelock_address_from_contract_type(OwnableContract::StakeTableProxy, contracts)?;
621 if owner_addr != timelock_addr {
622 anyhow::bail!(
623 "StakeTableProxy owner {owner_addr:#x} is not the OpsTimelock {timelock_addr:#x}"
624 );
625 }
626
627 let version = proxy.getVersion().call().await?;
629 if version.majorVersion < 2 {
630 anyhow::bail!(
631 "StakeTableProxy must be at major version >= 2 to upgrade to V3, found {}",
632 version.majorVersion
633 );
634 }
635
636 let v3_impl_addr = contracts
637 .deploy(
638 Contract::StakeTableV3,
639 StakeTableV3::deploy_builder(&provider),
640 )
641 .await?;
642
643 let init_data =
645 if crate::already_initialized(&provider, proxy_addr, expected_major_version).await? {
646 tracing::info!(
647 "StakeTableProxy already initialized at V{expected_major_version}, skipping \
648 initializeV3()"
649 );
650 Bytes::new()
651 } else {
652 StakeTableV3::new(Address::ZERO, &provider)
653 .initializeV3()
654 .calldata()
655 .to_owned()
656 };
657
658 encode_stake_table_v3_timelock_proposal(
659 proxy_addr,
660 v3_impl_addr,
661 timelock_addr,
662 init_data,
663 ¶ms,
664 )
665}
666
667#[cfg(test)]
668mod tests {
669 use alloy::{
670 primitives::{Address, U256},
671 sol_types::SolCall,
672 };
673 use hotshot_contract_adapter::sol_types::OpsTimelock;
674
675 use super::*;
676
677 #[test]
681 fn test_encode_stake_table_v3_timelock_proposal() -> Result<()> {
682 let proxy_addr = Address::random();
683 let v3_impl_addr = Address::random();
684 let timelock_addr = Address::random();
685 let salt = B256::repeat_byte(0x42);
686 let delay = U256::from(3600);
687 let init_data: Bytes = StakeTableV3::initializeV3Call {}.abi_encode().into();
688
689 let proposal = encode_stake_table_v3_timelock_proposal(
690 proxy_addr,
691 v3_impl_addr,
692 timelock_addr,
693 init_data.clone(),
694 &StakeTableV3TimelockProposalParams { salt, delay },
695 )?;
696
697 assert_eq!(proposal.timelock_addr, timelock_addr);
698 assert_eq!(proposal.v3_impl_addr, v3_impl_addr);
699 assert_eq!(proposal.schedule.to, timelock_addr);
700 assert_eq!(proposal.execute.to, timelock_addr);
701 assert!(proposal.schedule.data.len() > 4);
702 assert!(proposal.execute.data.len() > 4);
703
704 let expected_inner: Bytes = StakeTableV3::upgradeToAndCallCall {
707 newImplementation: v3_impl_addr,
708 data: init_data,
709 }
710 .abi_encode()
711 .into();
712
713 let expected_schedule = OpsTimelock::scheduleCall {
714 target: proxy_addr,
715 value: U256::ZERO,
716 data: expected_inner.clone(),
717 predecessor: B256::ZERO,
718 salt,
719 delay,
720 }
721 .abi_encode();
722 assert_eq!(proposal.schedule.data.to_vec(), expected_schedule);
723
724 let expected_execute = OpsTimelock::executeCall {
725 target: proxy_addr,
726 value: U256::ZERO,
727 payload: expected_inner,
728 predecessor: B256::ZERO,
729 salt,
730 }
731 .abi_encode();
732 assert_eq!(proposal.execute.data.to_vec(), expected_execute);
733
734 Ok(())
735 }
736
737 #[test]
740 fn test_encode_stake_table_v3_timelock_proposal_already_initialized() -> Result<()> {
741 let proxy_addr = Address::random();
742 let v3_impl_addr = Address::random();
743 let timelock_addr = Address::random();
744 let salt = B256::ZERO;
745 let delay = U256::ZERO;
746
747 let proposal = encode_stake_table_v3_timelock_proposal(
748 proxy_addr,
749 v3_impl_addr,
750 timelock_addr,
751 Bytes::new(),
752 &StakeTableV3TimelockProposalParams { salt, delay },
753 )?;
754
755 let expected_inner: Bytes = StakeTableV3::upgradeToAndCallCall {
756 newImplementation: v3_impl_addr,
757 data: Bytes::new(),
758 }
759 .abi_encode()
760 .into();
761
762 let expected_schedule = OpsTimelock::scheduleCall {
763 target: proxy_addr,
764 value: U256::ZERO,
765 data: expected_inner,
766 predecessor: B256::ZERO,
767 salt,
768 delay,
769 }
770 .abi_encode();
771 assert_eq!(proposal.schedule.data.to_vec(), expected_schedule);
772
773 Ok(())
774 }
775}