Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

Proxy upgrade alert

Template: proxy-upgrade-alert ยท category: security ยท risk: monitor_only

Watches an ERC1967 proxy for Upgraded(address implementation) and notifies with the new and previous implementation address. The previous one comes from rflow's durable state, written each time an upgrade fires โ€” no archive node needed.

When to use it

  • you depend on an upgradeable contract you do not control (a stablecoin, a bridge, a protocol you integrate)
  • you operate upgradeable contracts and want an independent record of every implementation change
  • as a supply-chain tripwire: an unexpected upgrade is the strongest possible signal to pause integrations

Generate it

rflow new --template proxy-upgrade-alert
# or add the workflow to an existing project:
rflow add workflow proxy-upgrade-alert

Non-interactive (CI/agents):

rflow new --template proxy-upgrade-alert --yes --output ./usdc-watch \
  --answer proxy_name=USDC --answer proxy_address=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48

The generated workflow

# recipe: partial
workflows:
  proxy-upgrade-alert:
    trigger:
      event:
        contract: USDC
        name: Upgraded
        network: ethereum
        confirmations: 0
        start_block: latest
        end_block: live
    steps:
      # the old implementation comes from durable state written on the
      # previous upgrade - 'unknown' on the very first one rflow sees
      - id: alert
        notify:
          channel: ops
          message: "PROXY UPGRADED: USDC implementation is now ${{ trigger.args.implementation }} (was ${{ state['proxy-upgrade-alert.last_impl'] if state['proxy-upgrade-alert.last_impl'] is defined else 'unknown' }}) - tx ${{ trigger.tx_hash }}"
      - id: remember
        state_set:
          key: "proxy-upgrade-alert.last_impl"
          value: "${{ trigger.args.implementation }}"
    on_failure: dead_letter

The Upgraded(address) signature is shared by OpenZeppelin transparent proxies, UUPS proxies, and older ZeppelinOS proxies (USDC included), so the default answers watch mainnet USDC out of the box.

Inputs

keytypedefault
project_namestringproxy-upgrade-alert
network / chain_id / rpc_env / rpc_urlnetwork / chain_id / env_var / stringethereum / 1 / ETH_RPC / a public RPC
proxy_name / proxy_addresscontract / addressUSDC mainnet
channelstringops

Required env vars

DATABASE_URL, the RPC env var (default ETH_RPC), TG_BOT_TOKEN, TG_CHAT_ID โ€” all listed in the generated .env.example.

Safety notes

Monitor-only: no signer:, no relayers:, no transactions. The state_set key is namespaced (proxy-upgrade-alert.last_impl) โ€” rename it if you run several proxy watchers in one project, because state keys are global across workflows. on_failure: dead_letter keeps a failed notification journaled for rflow dead-letter inspection.

Run it locally

docker compose up -d     # postgres on localhost:5448
# fill .env (RPC + telegram credentials)
rflow validate
rflow start

Rehearse without waiting for a real upgrade (dry-run โ€” state_set journals would_set and writes nothing):

rflow test proxy-upgrade-alert --fixture fixtures/upgraded.json

Production checklist

  • proxy_address is the proxy, not the implementation
  • the proxy actually emits ERC1967 Upgraded (transparent/UUPS/legacy ZeppelinOS do; some bespoke proxies use different events)
  • telegram credentials in .env, test with rflow test
  • rflow validate --preflight passes against your RPC

Common modifications

  • also watch AdminChanged / BeaconUpgraded (already in the packaged ABI): add a workflow per event
  • watch several proxies: one contracts: entry + one workflow each, with distinct state keys
  • gate an automated response on the upgrade: pause your own integration with a send_transaction step behind an approval gate