1use alloy::{
2 primitives::{Address, B256, Bytes, U256},
3 providers::Provider,
4 rpc::types::TransactionReceipt,
5};
6use anyhow::Result;
7use clap::ValueEnum;
8use hotshot_contract_adapter::sol_types::{
9 EspToken, FeeContract, LightClient, OpsTimelock, RewardClaim, SafeExitTimelock, StakeTable,
10};
11
12use crate::{
13 Contract, Contracts, OwnableContract, proposals::multisig::encode_generic_calldata,
14 retry_until_true,
15};
16
17#[derive(Debug, Default, Clone)]
19pub struct TimelockOperationPayload {
20 pub target: Address,
22 pub value: U256,
24 pub data: Bytes,
26 pub predecessor: B256,
28 pub salt: B256,
30 pub delay: U256,
32}
33
34#[derive(Debug, Clone, Default)]
36pub struct TimelockOperationParams {
37 pub multisig_proposer: Option<Address>,
39 pub operation_id: Option<B256>,
41 pub dry_run: bool,
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
46pub enum TimelockOperationType {
47 Schedule,
48 Execute,
49 Cancel,
50}
51
52#[derive(Debug)]
54pub enum TimelockContract {
55 OpsTimelock(Address),
56 SafeExitTimelock(Address),
57}
58
59impl TimelockContract {
60 pub async fn get_operation_id(
61 &self,
62 operation: &TimelockOperationPayload,
63 provider: &impl Provider,
64 ) -> Result<B256> {
65 match self {
66 TimelockContract::OpsTimelock(timelock_addr) => {
67 Ok(OpsTimelock::new(*timelock_addr, &provider)
68 .hashOperation(
69 operation.target,
70 operation.value,
71 operation.data.clone(),
72 operation.predecessor,
73 operation.salt,
74 )
75 .call()
76 .await?)
77 },
78 TimelockContract::SafeExitTimelock(timelock_addr) => {
79 Ok(SafeExitTimelock::new(*timelock_addr, &provider)
80 .hashOperation(
81 operation.target,
82 operation.value,
83 operation.data.clone(),
84 operation.predecessor,
85 operation.salt,
86 )
87 .call()
88 .await?)
89 },
90 }
91 }
92
93 pub async fn schedule(
94 &self,
95 operation: TimelockOperationPayload,
96 provider: &impl Provider,
97 ) -> Result<TransactionReceipt> {
98 self.call_timelock_method(TimelockOperationType::Schedule, operation, None, provider)
99 .await
100 }
101
102 pub async fn execute(
103 &self,
104 operation: TimelockOperationPayload,
105 provider: &impl Provider,
106 ) -> Result<TransactionReceipt> {
107 self.call_timelock_method(TimelockOperationType::Execute, operation, None, provider)
108 .await
109 }
110
111 pub async fn cancel(
112 &self,
113 operation_id: B256,
114 provider: &impl Provider,
115 ) -> Result<TransactionReceipt> {
116 let placeholder_operation = TimelockOperationPayload::default();
118 self.call_timelock_method(
119 TimelockOperationType::Cancel,
120 placeholder_operation,
121 Some(operation_id),
122 provider,
123 )
124 .await
125 }
126
127 async fn call_timelock_method(
129 &self,
130 method: TimelockOperationType,
131 operation: TimelockOperationPayload,
132 operation_id: Option<B256>,
133 provider: &impl Provider,
134 ) -> Result<TransactionReceipt> {
135 let pending_tx = match (self, method) {
136 (TimelockContract::OpsTimelock(addr), TimelockOperationType::Schedule) => {
137 OpsTimelock::new(*addr, &provider)
138 .schedule(
139 operation.target,
140 operation.value,
141 operation.data,
142 operation.predecessor,
143 operation.salt,
144 operation.delay,
145 )
146 .send()
147 .await?
148 },
149 (TimelockContract::SafeExitTimelock(addr), TimelockOperationType::Schedule) => {
150 SafeExitTimelock::new(*addr, &provider)
151 .schedule(
152 operation.target,
153 operation.value,
154 operation.data,
155 operation.predecessor,
156 operation.salt,
157 operation.delay,
158 )
159 .send()
160 .await?
161 },
162 (TimelockContract::OpsTimelock(addr), TimelockOperationType::Execute) => {
163 OpsTimelock::new(*addr, &provider)
164 .execute(
165 operation.target,
166 operation.value,
167 operation.data,
168 operation.predecessor,
169 operation.salt,
170 )
171 .send()
172 .await?
173 },
174 (TimelockContract::SafeExitTimelock(addr), TimelockOperationType::Execute) => {
175 SafeExitTimelock::new(*addr, &provider)
176 .execute(
177 operation.target,
178 operation.value,
179 operation.data,
180 operation.predecessor,
181 operation.salt,
182 )
183 .send()
184 .await?
185 },
186 (TimelockContract::OpsTimelock(addr), TimelockOperationType::Cancel) => {
187 OpsTimelock::new(*addr, &provider)
188 .cancel(
189 operation_id
190 .ok_or_else(|| anyhow::anyhow!("operation_id required for cancel"))?,
191 )
192 .send()
193 .await?
194 },
195 (TimelockContract::SafeExitTimelock(addr), TimelockOperationType::Cancel) => {
196 SafeExitTimelock::new(*addr, &provider)
197 .cancel(
198 operation_id
199 .ok_or_else(|| anyhow::anyhow!("operation_id required for cancel"))?,
200 )
201 .send()
202 .await?
203 },
204 };
205
206 let tx_hash = *pending_tx.tx_hash();
207 tracing::info!(%tx_hash, "waiting for tx to be mined");
208 let receipt = pending_tx.get_receipt().await?;
209 Ok(receipt)
210 }
211
212 pub async fn is_operation_pending(
213 &self,
214 operation_id: B256,
215 provider: &impl Provider,
216 ) -> Result<bool> {
217 match self {
218 TimelockContract::OpsTimelock(timelock_addr) => {
219 Ok(OpsTimelock::new(*timelock_addr, &provider)
220 .isOperationPending(operation_id)
221 .call()
222 .await?)
223 },
224 TimelockContract::SafeExitTimelock(timelock_addr) => {
225 Ok(SafeExitTimelock::new(*timelock_addr, &provider)
226 .isOperationPending(operation_id)
227 .call()
228 .await?)
229 },
230 }
231 }
232
233 pub async fn is_operation_ready(
234 &self,
235 operation_id: B256,
236 provider: &impl Provider,
237 ) -> Result<bool> {
238 match self {
239 TimelockContract::OpsTimelock(timelock_addr) => {
240 Ok(OpsTimelock::new(*timelock_addr, &provider)
241 .isOperationReady(operation_id)
242 .call()
243 .await?)
244 },
245 TimelockContract::SafeExitTimelock(timelock_addr) => {
246 Ok(SafeExitTimelock::new(*timelock_addr, &provider)
247 .isOperationReady(operation_id)
248 .call()
249 .await?)
250 },
251 }
252 }
253
254 pub async fn is_operation_done(
255 &self,
256 operation_id: B256,
257 provider: &impl Provider,
258 ) -> Result<bool> {
259 match self {
260 TimelockContract::OpsTimelock(timelock_addr) => {
261 Ok(OpsTimelock::new(*timelock_addr, &provider)
262 .isOperationDone(operation_id)
263 .call()
264 .await?)
265 },
266 TimelockContract::SafeExitTimelock(timelock_addr) => {
267 Ok(SafeExitTimelock::new(*timelock_addr, &provider)
268 .isOperationDone(operation_id)
269 .call()
270 .await?)
271 },
272 }
273 }
274
275 pub async fn is_operation_canceled(
276 &self,
277 operation_id: B256,
278 provider: &impl Provider,
279 ) -> Result<bool> {
280 let pending = self.is_operation_pending(operation_id, provider).await?;
281 let done = self.is_operation_done(operation_id, provider).await?;
282 Ok(!pending && !done)
284 }
285}
286
287pub fn derive_timelock_address_from_contract_type(
291 contract_type: OwnableContract,
292 contracts: &Contracts,
293) -> Result<Address> {
294 let timelock_type = match contract_type {
295 OwnableContract::FeeContractProxy
296 | OwnableContract::LightClientProxy
297 | OwnableContract::StakeTableProxy => Contract::OpsTimelock,
298 OwnableContract::EspTokenProxy | OwnableContract::RewardClaimProxy => {
299 Contract::SafeExitTimelock
300 },
301 };
302
303 contracts.address(timelock_type).ok_or_else(|| {
304 anyhow::anyhow!(
305 "{:?} not found in deployed contracts. Deploy it first or provide it via flag.",
306 timelock_type
307 )
308 })
309}
310
311pub async fn get_timelock_for_contract(
313 provider: &impl Provider,
314 contract_type: Contract,
315 target_addr: Address,
316) -> Result<TimelockContract> {
317 match contract_type {
318 Contract::FeeContractProxy => Ok(TimelockContract::OpsTimelock(
319 FeeContract::new(target_addr, &provider)
320 .owner()
321 .call()
322 .await?,
323 )),
324 Contract::EspTokenProxy => Ok(TimelockContract::SafeExitTimelock(
325 EspToken::new(target_addr, &provider).owner().call().await?,
326 )),
327 Contract::LightClientProxy => Ok(TimelockContract::OpsTimelock(
328 LightClient::new(target_addr, &provider)
329 .owner()
330 .call()
331 .await?,
332 )),
333 Contract::StakeTableProxy => Ok(TimelockContract::OpsTimelock(
334 StakeTable::new(target_addr, &provider)
335 .owner()
336 .call()
337 .await?,
338 )),
339 Contract::RewardClaimProxy => Ok(TimelockContract::SafeExitTimelock(
340 RewardClaim::new(target_addr, &provider)
341 .currentAdmin()
342 .call()
343 .await?,
344 )),
345 _ => anyhow::bail!(
346 "Invalid contract type for timelock get operation: {}",
347 contract_type
348 ),
349 }
350}
351
352pub async fn perform_timelock_operation(
355 provider: &impl Provider,
356 contract_type: Contract,
357 operation: TimelockOperationPayload,
358 operation_type: TimelockOperationType,
359 params: TimelockOperationParams,
360) -> Result<B256> {
361 let timelock = get_timelock_for_contract(provider, contract_type, operation.target).await?;
362 let operation_id =
365 if let (TimelockOperationType::Cancel, Some(id)) = (operation_type, params.operation_id) {
366 id
367 } else {
368 timelock.get_operation_id(&operation, &provider).await?
369 };
370
371 if let Some(multisig_proposer) = params.multisig_proposer {
372 perform_timelock_operation_via_multisig(
373 timelock,
374 operation,
375 operation_type,
376 operation_id,
377 multisig_proposer,
378 )
379 .await
380 } else {
381 perform_timelock_operation_via_eoa(
382 timelock,
383 operation,
384 operation_type,
385 operation_id,
386 provider,
387 )
388 .await
389 }
390}
391
392async fn perform_timelock_operation_via_eoa(
394 timelock: TimelockContract,
395 operation: TimelockOperationPayload,
396 operation_type: TimelockOperationType,
397 operation_id: B256,
398 provider: &impl Provider,
399) -> Result<B256> {
400 let receipt = match operation_type {
401 TimelockOperationType::Schedule => timelock.schedule(operation, &provider).await?,
402 TimelockOperationType::Execute => timelock.execute(operation, &provider).await?,
403 TimelockOperationType::Cancel => timelock.cancel(operation_id, &provider).await?,
404 };
405
406 tracing::info!(%receipt.gas_used, %receipt.transaction_hash, "tx mined");
407 if !receipt.inner.is_success() {
408 anyhow::bail!("tx failed: {:?}", receipt);
409 }
410
411 match operation_type {
413 TimelockOperationType::Schedule => {
414 let check_name = format!("Schedule operation {}", operation_id);
415 let is_scheduled = retry_until_true(&check_name, || async {
416 Ok(timelock
417 .is_operation_pending(operation_id, &provider)
418 .await?
419 || timelock.is_operation_ready(operation_id, &provider).await?)
420 })
421 .await?;
422 if !is_scheduled {
423 anyhow::bail!("tx not correctly scheduled: {}", operation_id);
424 }
425 tracing::info!("tx scheduled with id: {}", operation_id);
426 },
427 TimelockOperationType::Execute => {
428 let check_name = format!("Execute operation {}", operation_id);
429 let is_done = retry_until_true(&check_name, || async {
430 timelock.is_operation_done(operation_id, &provider).await
431 })
432 .await?;
433 if !is_done {
434 anyhow::bail!("tx not correctly executed: {}", operation_id);
435 }
436 tracing::info!("tx executed with id: {}", operation_id);
437 },
438 TimelockOperationType::Cancel => {
439 tracing::info!("tx cancelled with id: {}", operation_id);
440 },
441 }
442
443 Ok(operation_id)
444}
445
446async fn perform_timelock_operation_via_multisig(
448 timelock: TimelockContract,
449 operation: TimelockOperationPayload,
450 operation_type: TimelockOperationType,
451 operation_id: B256,
452 multisig_proposer: Address,
453) -> Result<B256> {
454 let timelock_addr = match timelock {
455 TimelockContract::OpsTimelock(addr) => addr,
456 TimelockContract::SafeExitTimelock(addr) => addr,
457 };
458
459 let (function_signature, function_args) = match operation_type {
461 TimelockOperationType::Schedule => (
462 "schedule(address,uint256,bytes,bytes32,bytes32,uint256)",
463 vec![
464 operation.target.to_string(),
465 operation.value.to_string(),
466 operation.data.to_string(),
467 operation.predecessor.to_string(),
468 operation.salt.to_string(),
469 operation.delay.to_string(),
470 ],
471 ),
472 TimelockOperationType::Execute => (
473 "execute(address,uint256,bytes,bytes32,bytes32)",
474 vec![
475 operation.target.to_string(),
476 operation.value.to_string(),
477 operation.data.to_string(),
478 operation.predecessor.to_string(),
479 operation.salt.to_string(),
480 ],
481 ),
482 TimelockOperationType::Cancel => ("cancel(bytes32)", vec![operation_id.to_string()]),
483 };
484
485 tracing::info!(
486 "Encoding {:?} operation calldata for timelock {}",
487 operation_type,
488 timelock_addr
489 );
490
491 let calldata =
492 encode_generic_calldata(timelock_addr, function_signature, function_args, U256::ZERO)?;
493
494 tracing::info!(
495 "Timelock {:?} operation calldata encoded. Operation ID: {}",
496 operation_type,
497 operation_id
498 );
499 tracing::info!(
500 "Multisig proposer: {}. To: {}, Data: {}",
501 multisig_proposer,
502 calldata.to,
503 calldata.data
504 );
505
506 Ok(operation_id)
507}