Skip to content

feat(unibtc): add Bedrock uniBTC Safe monitor and hourly state polling - #362

Merged
spalen0 merged 11 commits into
mainfrom
feat/bedrock-unibtc-monitoring
Sep 22, 2026
Merged

spalen0 merged 11 commits into
mainfrom
feat/bedrock-unibtc-monitoring

Conversation

@spalen0

@spalen0 spalen0 commented Sep 15, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Add Bedrock uniBTC as a new protocol: hourly state polling for unexpected minting, reserve-gate changes, pauses, Chainlink PoR coverage/staleness, supply-feeder drift, underfunded WBTC redemptions, and a BTC peg break.
  • Register the uniBTC ops Safe and Bedrock admin Safe in the Safe monitor so queued upgrades, role grants, and freezes are visible without event subscriptions.
  • Wire the hourly job, monitoring card, and Telegram topic placeholder.

Closes #361

Test plan

  • uv run pytest tests/test_unibtc.py tests/test_monitoring_config.py tests/test_alert_protocol_keys.py tests/test_safe_main.py
  • Confirm both Safes appear in protocols/safe/addresses.py and that TELEGRAM_TOPIC_ID_UNIBTC is set on the VPS before merge
  • First hourly run should initialize supply snapshots without a mint alert; the known ~15% feeder-vs-API gap should fire HIGH
  • Spot-check etherscan links in alert copy use full addresses

Made with Cursor

spalen0 and others added 2 commits September 15, 2026 22:32
Cover the single-EOA mint path, reserve gate, PoR, supply feeder, redemptions, and peg without event subscriptions.

Co-authored-by: Cursor <cursoragent@cursor.com>
Seven correctness fixes from the review of #362:

- Dedupe the mint alerts. A single mint stays inside the 24h lookback for
  ~20 hourly runs and re-alerted on every one. The supply level that last
  alerted is now cached; the alert repeats only once supply grows by
  another full threshold, and falling back under the threshold re-arms.
- Key the reserve-gate and pause latches on *which* fields changed rather
  than on bool(diffs). A second tampered gate field (supply-feeder hijack
  after an adequacyRatio change) or a newly paused component was
  previously swallowed by the first alert's latch.
- Replace the absolute feeder-vs-API gap check. The feeder and the Bedrock
  dashboard cover different chain sets, so the steady-state gap is ~15% --
  well over the 2% threshold -- meaning the HIGH fired on the first run and
  latched forever. The PR's own test asserted this. Now tracks the ratio
  between the two against a learned baseline and alerts on a >5% move;
  the baseline is relearned only while inside the band, so an anomalous
  reading cannot become the new normal.
- Reject non-positive uniBTC price quotes. A zero quote fell through the
  btc_usd guard and yielded a 0 ratio, i.e. a false CRITICAL depeg alert.
- Keep future-dated supply snapshots. block_timestamp moves backwards when
  the RPC pool rotates to a lagging provider, which discarded the newest
  snapshot -- the only baseline the next run had. Also warn when a mint
  window has no usable baseline instead of going silently blind.
- Report the real baseline age in mint alerts; the "1h" CRITICAL can cover
  up to 3h.
- Make check_por_stale track the live Vault feederHeartbeat as its
  docstring claims, capped at POR_STALE_SECONDS so a heartbeat widened by
  a compromised MANAGER_ROLE cannot also blind the check.

Docs in monitoring.yaml and the protocol README updated to match. The ten
new tests were verified to fail against the pre-fix code.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@spalen0
spalen0 marked this pull request as ready for review September 16, 2026 07:40
spalen0 and others added 6 commits September 16, 2026 09:46
…gaps

Both defects were introduced by the previous commit's dedupe work and were
reproduced against the real code before fixing.

Feeder ratio could drift unbounded. Re-anchoring on every in-band reading
let each sub-threshold step move the reference: four -3% steps accumulated
11.47% total drift with zero alerts. The anchor is now re-taken at most
once per FEEDER_ANCHOR_REFRESH_SECONDS (7 days), and still only from a
reading inside the band, so cumulative drift trips the alert instead of
being absorbed. The same four steps now alert on the second one.

A stale mint marker could suppress a real mint. mint_alert_due was only
called when a window delta existed, so a polling gap left the previous
alert's supply level cached. Reproduced: an 11 BTC mint alerted and pinned
the marker at 31100000000, a gap plus redemptions dropped supply back, and
a genuine fresh 10 BTC mint at 31000000000 never cleared the last +
threshold bar and alerted nothing. The helper now takes the delta as
Optional and treats a missing baseline as a re-arm, and is called
unconditionally so broken continuity always clears the marker.

Docs updated to match. The new tests were verified to fail against the
previous commit, except the mint_alert_due unit test, which passes there
by coincidence of the old bool signature -- the defect was at the call
site, which the integration-level test covers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two defects found auditing the rest of the module.

A feeder pinned at 0 was never reported. check_supply_feeder used
previous_value == 0 as its "never seen" sentinel, but 0 is also what the
cache returns for an unset key, so a feeder genuinely reporting 0 looked
like a fresh observation on every run and reset its own staleness clock.
Reproduced: 10 days of a zero feeder with the Bedrock API down produced no
alert at all, while the same loop with a normal value alerts as expected.
This is the reading that least deserves to be dropped -- a zero supply
satisfies the Vault mint gate outright, which is the unbacked-mint path
this monitor exists to catch. The staleness clock is now keyed on the
changed-at timestamp, which is unambiguous. Note the ratio check already
covers this within one run whenever the API is up; the blind spot was the
API-down fallback.

A truncated RPC batch surfaced as a bare IndexError naming no field, unlike
every other decode path here. load_state now checks the response count
first and raises with the shortfall. BATCH_CALL_COUNT is asserted against
the real batch in a test so it cannot drift from the call list.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A zero totalTokenSupply() satisfies the Vault reserve check for any mint
amount. Previously this surfaced only via the ratio check (needs the
Bedrock API) or, with the API down, the 48h staleness path. The new check
reads the on-chain value directly, fires on the first run, alerts once
while the condition holds, and re-arms on recovery.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… gap

The learned-anchor feeder check was built on a false premise. The ~15%
feeder/API gap it treated as steady state was a fault: on-chain history
shows the feeder tracked the API (ratio ~1.00) through 2026-09-12, and since
09-13 the updater 0x2C62803181243Fa99C659DE0d2A0530879a79911 writes a value
~701 uniBTC low on alternating days -- exactly BOB-uniBTC's supply. The
anchor only alerted on 09-16 because it happened to be seeded two minutes
before a bad write; seeded on a bad day it would have gone quiet on the
fault and alerted on recovery.

Feeder check: back to the absolute 2% gap specified in #361. The alert now
names the chain whose supply matches the gap ("likely omits BOB-uniBTC").

The reserve API is Bedrock's own undocumented dashboard backend (loaded by
app.bedrock.technology from their Vercel team), so it is kept, but it was
observed dropping BOB from `supplies` on 2026-09-16 09:25Z, reporting total
supply 3,845.33 instead of 4,546.70. Trusting that is worse than skipping:
PoR coverage reads 120.7% instead of 102%, and a feeder omitting the same
chain looks correct. Responses are now rejected -- skipping PoR coverage
and the feeder gap for that run, with an error message -- unless:

- `time` is at most 1h older than the pinned block;
- Ethereum, BSC, Base, BOB and Berachain (99.4% of supply) are all present
  with positive supply;
- the Ethereum entry matches block-pinned on-chain totalSupply within 1%
  (verified live: they agree to 1.8e-7).

The mainnet cross-check alone would have passed the BOB-less response,
hence the required-chain list. Dry-run against live data names BOB as the
omitted chain.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The peg had a single HIGH level at 0.98. Levels are set from a year of
DeFiLlama uniBTC/WBTC prices (1,993 4h samples, 2025-09-14 to 2026-09-16):
median 0.9945; below 0.99 9% of the time (~130 dips/yr, routine); below
0.985 ~36 dips/yr; below 0.97 only four episodes (2025-12-16, 2026-04-26,
2026-05-09/10), low 0.9616. A 0.99 trigger was considered and rejected as
noise: its 10th percentile sits at 0.9903.

Alerts fire on entering a worse band, like PoR coverage: HIGH -> CRITICAL
re-alerts, CRITICAL -> HIGH is silent, full recovery re-arms. The band
logic PoR coverage had inline is extracted into severity_band and
_alert_on_band_escalation, shared by both checks. The peg state moves to a
new UNIBTC_PEG_BAND cache key since the stored value changes meaning.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tapired

tapired commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator
  1. Mint alerts are marked as sent before delivery succeeds. mint_alert_due() writes the cached supply before send_alert(), and both the 1h and 24h markers are written before either send. If the first Telegram send fails, subsequent runs suppress both alerts. Reproduced with a +10 uniBTC supply jump: after a simulated delivery failure, the next run sends zero alerts. Could we persist each marker only after its alert is successfully sent?

  2. API validation doesn’t check total_supply against the per-chain sum. A response containing every required chain and a matching Ethereum supply still passes if the aggregate excludes BOB’s supply. This overstates PoR coverage and can hide the feeder discrepancy. Reproduced with an inconsistent fixture retaining BOB in supplies but subtracting it from total_supply. Could we reject aggregate/per-chain mismatches outside a small rounding tolerance?

…ain sum

Two findings from the PR #362 review, both reproduced before fixing.

Mint alerts were marked sent before delivery. mint_alert_due wrote the
alerted supply, and both window markers were written before either send.
send_telegram_message re-raises TelegramError on failure, so a failed
delivery aborted the run with both markers already set. Reproduced through
the real send_alert -> telegram path: a +10 uniBTC mint whose first send
failed was never delivered on the next, healthy run -- and the 24h alert was
never even attempted. mint_alert_due now only decides (and still clears on
re-arm); each marker is written right after its own send_alert returns, so a
failure retries next run, and a delivered 1h alert is not repeated when only
the 24h send failed. The module's other latches already sent first.

API total_supply was not checked against its per-chain list. The required-
chain and Ethereum safeguards inspect `supplies`, but PoR coverage and the
feeder gap consume `total_supply`, so they only protected the total when the
two agreed. A response listing every chain but whose total excluded BOB
passed validation and yielded PoR coverage 120.69% (true 102.06%) and a
feeder gap of 0.005% (true 15.43%). Responses are now rejected when the
total differs from the sum by more than 0.01 uniBTC; live responses differed
by 4.2e-7. Not yet observed live -- the observed failure dropped BOB from
both -- but the safeguards were logically incomplete without it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tapired

tapired commented Sep 20, 2026

Copy link
Copy Markdown
Collaborator

lgtm

@spalen0
spalen0 merged commit 41c97df into main Sep 22, 2026
3 checks passed
@spalen0
spalen0 deleted the feat/bedrock-unibtc-monitoring branch September 22, 2026 15:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bedrock uniBTC monitoring: Safe monitor + state polling (single-EOA mint path, supply feeder, PoR)

2 participants