Monitors all timelock contract types (TimelockController, Aave, Compound, Puffer, Lido, Maple) and sends Telegram alerts to protocol-specific channels.
- Queries the Envio GraphQL indexer (
ENVIO_GRAPHQL_URL) for newTimelockEventevents across all monitored timelocks (all types). - Groups events by
operationIdso batch operations (scheduleBatch) are sent as a single alert. - Routes each alert to the correct Telegram channel based on the protocol mapping.
- Stores the latest processed
blockTimestampincache-id.txt(key:TIMELOCK_LAST_TS) to avoid duplicate alerts between runs.
The script runs hourly via GitHub Actions.
The script queries the unified TimelockEvent type from the Envio indexer. The query fetches all timelock types (TimelockController, Aave, Compound, Puffer, Lido, Maple) for monitored addresses.
query GetTimelockEvents($limit: Int!, $sinceTs: Int!, $addresses: [String!]!) {
TimelockEvent(
where: {
timelockAddress: { _in: $addresses }
blockTimestamp: { _gt: $sinceTs }
}
order_by: { blockTimestamp: asc, blockNumber: asc, logIndex: asc }
limit: $limit
) {
id
timelockAddress
timelockType
eventName
chainId
blockNumber
blockTimestamp
transactionHash
operationId
index
target
value
data
predecessor
delay
signature
creator
metadata
votesFor
votesAgainst
}
}The TimelockEvent type includes fields that vary by timelock type:
Common fields (all types):
id- Unique identifier:${chainId}_${blockNumber}_${logIndex}timelockAddress- Address of the timelock contracttimelockType- Type discriminator:"TimelockController","Aave","Compound","Puffer","Lido", or"Maple"eventName- Original event name (e.g.,"CallScheduled","ProposalQueued","QueueTransaction", etc.)chainId- Chain ID (1 for Mainnet, 8453 for Base, etc.)blockNumber- Block number where the event was emittedblockTimestamp- Unix timestamp of the blocktransactionHash- Transaction hashoperationId- Unified identifier for the queued operation
Type-specific fields:
- TimelockController:
target,value,data,delay(relative seconds),predecessor,index - Aave:
votesFor,votesAgainst,operationId(proposalId) - Compound:
target,value,data,delay(absolute timestamp/eta),signature,operationId(txHash) - Puffer:
target,data,delay(absolute timestamp/lockedUntil),operationId(txHash) - Lido:
creator,metadata,operationId(voteId) - Maple:
delay(absolute timestamp/delayedUntil),operationId(proposalId)
For complete field mapping details, see detils.md.
-
Add the address to the Envio indexer config. The address must be indexed before this script can query events for it. Open the Envio config.yaml, add the address under the correct chain's
TimelockControllercontract list, and deploy the updated indexer. -
Add a
TimelockConfigentry intimelock_alerts.pyin theTIMELOCK_LISTlist:
TimelockConfig("0xabcdef...lowercase_address", 1, "PROTOCOL_NAME", "Human Readable Label"),Parameters:
- address: Timelock contract address, must be lowercase.
- chain_id: Chain ID (
1for Mainnet,8453for Base, etc.). Must match the network in the Envio config. - protocol: Protocol identifier used for Telegram routing. Maps to
TELEGRAM_CHAT_ID_{PROTOCOL}andTELEGRAM_BOT_TOKEN_{PROTOCOL}env variables. Falls back toTELEGRAM_BOT_TOKEN_DEFAULTif no protocol-specific bot token exists. - label: Human-readable name shown in the alert message.
- If the chain is new, make sure it exists in
utils/chains.py(Chainenum andEXPLORER_URLSdict). - If the protocol needs a dedicated Telegram channel, add
TELEGRAM_CHAT_ID_{PROTOCOL}and optionallyTELEGRAM_BOT_TOKEN_{PROTOCOL}to the environment and GitHub Actions secrets.
The alert format varies by timelock type:
TimelockController/Compound/Puffer:
⏰ TIMELOCK: New Operation Scheduled
🅿️ Protocol: LRT
📋 Timelock: EtherFi Timelock
🔗 Chain: Mainnet
📌 Type: TimelockController
📝 Event: CallScheduled
⏳ Delay: 2d
🎯 Target: 0x1234...
📝 Function: 0xabcdef12
🔗 Tx: https://etherscan.io/tx/0x...
Aave:
⏰ TIMELOCK: New Operation Scheduled
🅿️ Protocol: AAVE
📋 Timelock: Aave Timelock
🔗 Chain: Mainnet
📌 Type: Aave
📝 Event: ProposalQueued
✅ Votes For: 12345
❌ Votes Against: 6789
🆔 Proposal ID: 42
🔗 Tx: https://etherscan.io/tx/0x...
Lido:
⏰ TIMELOCK: New Operation Scheduled
🅿️ Protocol: LIDO
📋 Timelock: Lido DAO
🔗 Chain: Mainnet
📌 Type: Lido
📝 Event: StartVote
👤 Creator: 0x1234...
📄 Metadata: ipfs://...
🆔 Vote ID: 123
🔗 Tx: https://etherscan.io/tx/0x...
For batch operations (scheduleBatch), all calls are included in a single message with --- Call N --- separators.
uv run timelock/timelock_alerts.pyOptional flags:
--limit— max events to fetch per run (default:100)--since-seconds— fallback lookback window when no cache exists (default:43200/ 12h)--no-cache— disable caching, always use--since-secondslookback--protocol— filter to a specific protocol, case-insensitive (e.g.--protocol MAPLE)--log-level— set log verbosity:DEBUG,INFO,WARNING,ERROR(default:WARNING)
The script stores the latest processed blockTimestamp in cache-id.txt under key TIMELOCK_LAST_TS. This value is universal across chains (unlike block numbers) so a single cache entry covers all monitored timelocks. On the first run (or with --no-cache), it falls back to querying events from the last 12 hours.
For comprehensive information about the unified TimelockEvent schema, including field mappings for all supported timelock types (TimelockController, Aave, Compound, Puffer, Lido, Maple), see detils.md.