Add OGN buyback automation scripts and contracts - #2988
Conversation
Code reviewReviewed at VerdictThe Solidity is clean, minimal, and genuinely well-tested; the deploy script is correct and simulates green end-to-end (I ran it). The wiring assumptions all check out against live mainnet state. The problem is the closed-loop behavior of the weekly rate controller: with today's measured on-chain numbers it burns the entire ~2.26M OGN reward reserve in ~3 weeks and then wedges itself permanently (every subsequent run reverts) until a human intervenes via the Safe. The config docs justify the parameters with a factual claim about the current reserve that is off by ~10×, and the governance proposal text describes the opposite economic effect (rate −20%) of what the automation will actually do first (rate +95% for 3 weeks, then an erratic trickle). This needs a parameter/design pass before deploy; the contracts themselves largely don't need to change. What I verifiedLive mainnet state (via RPC, 2026-09-02):
Tests I ran (both heads): FeeSplitter 55/55 unit+fuzz, module 30/30 unit (36/36 at Code-level checks that came back clean:
F-1 — MAJOR (design/parameters): the weekly controller burns the 2.26M OGN reserve in ~3 weeks, then wedges permanentlyWhere: The action computes
Measured reality: the source holds ~15.4 days (2.26M available at 1.7/s; ~17 days as of 2026-09-03). The claim is off by ~10×, so Simulated week by week with measured values (inflow 1.845 × 0.8 post-split = 1.476 OGN/s; the action's exact clamp order; module bounds 0.25–5, step 25%, minRunway 12h):
The wedge mechanism: once the source is drained, the step limit (25%, enforced in both the action and the module) forbids proposing anything below Consequences, in order:
Note the module's runway check is working as designed here — it correctly refuses rates the source can't sustain. The controller drives into it and the step limits prevent backing out. Recommendations (combine 1 with 2 and 3):
F-2 — MEDIUM (extends @clement-ux's 🟠): verify the settle instead of tolerating its silent failureWhere: Two things to add to the original thread: in practice the silent-failure path was nearly unreachable ( F-3 — MEDIUM (endorses @clement-ux): per-call step limit ratchetsWhere: Confirmed the original issue: nothing stopped consecutive calls walking 1.7 → 5.0 in 5 txs in one block. The checkpoint design fixes it soundly: within-period calls share one baseline (reachable band F-4 — MEDIUM (ops/go-live): nothing on-chain shows the CoW bot will actually sell from the OGN harvesterWhere: The 80% leg ends at the EIP-1271 harvester, which has held zero OUSD/OETH and produced zero outgoing OUSD/OETH transfers in ≥90 days — historically the fee Safe signed CoW orders itself, so this harvester path has plausibly never run end-to-end for OUSD/OETH. Token configs exist (deploy 189), but "wired but never fed" and "not wired" are indistinguishable from chain data. If the bot (0x7aD5C91…) isn't watching this harvester's OUSD/OETH balances, fees pile up there unsold: no OGN reaches the reward source, measured inflow decays toward zero, and the rate controller winds down (or hits F-1's wedge sooner). Suggested post-deployment checklist item: confirm the CoW bot creates OUSD→OGN and OETH→OGN orders from 0x637C5093… (e.g. seed a small OUSD amount through F-5 — LOW: reorged-away fills are never pruned from the scan storeWhere: The merge dedupes by for (const fill of [...storedFills.filter((f) => f.block < fromBlock), ...fresh]) …F-6 — NIT: stale path in config docsWhere: F-7 — OBSERVATION:
|
| Finding | Status after e7b393fac |
|---|---|
| F-1 controller burns reserve then wedges | OPEN — unchanged. Script band still 1–4 days, cadence weekly, module minRunway still 12h, "~1.6 days" claim still in the docs. The checkpoint mechanics don't alter the trajectory: normal weekly runs open a fresh period each time (baseline = live rate → same ramp), and a reverted run doesn't advance the checkpoint, so wedge-state retries still re-baseline to the stuck rate and re-fail. Same table, same permanent wedge at week ~3. |
| F-2 verify the settle | RESOLVED. |
| F-3 step-limit ratcheting | RESOLVED. Compromise walk 1.7→5 now takes ~5 weeks instead of one block. |
| F-4 CoW bot wiring unproven | OPEN. |
| F-5 reorged fills never pruned | OPEN. |
F-6 stale 005 path |
OPEN. |
| F-7 JSON bounds enforced nowhere | OPEN — now with a 5th setBounds parameter to get right. |
Two new (low) observations introduced by the checkpoint design:
- N-1 —
stepPeriodSecondsexactly equals the cron cadence (both 604800s). Where:contracts/scripts/config/ogn-buyback.json:22, claim atcontracts/utils/ogn-buyback-config.js:57. "A normal run always opens a fresh period" requires this week's tx block-timestamp to land ≥ exactly 7d after last week's — with the cron firing at the same wall time weekly, inclusion jitter makes that roughly a coin flip. A "short" week measures against the stale checkpoint, clamps to last week's rate, and the deadband skips the update entirely; the following week (≈14d elapsed) refreshes and proceeds. Only a one-week delay during convergence phases, and it self-corrects, but the effective cadence becomes erratic. Fix: setstepPeriodSecondsa few hours under the cadence (e.g. ~6.9 days / 596400) — the same "backstop sits below the target" pattern the config already uses forminRunwaySeconds. - N-2 — a within-period Safe override can be walked back by the next operator call. Where:
contracts/contracts/automation/SetXOGNRewardRateModule.sol:214-243(_checkStep— within-period calls measure againstcheckpointRate, ignoring a fresher Safe-setcurrentRate). If the Safe directly sets an emergency rate mid-period (say 1.7 → 0.5 after an incident), the checkpoint still says 1.7 until the period expires, so an operator call inside the window may set anything in[1.275, 2.125]— a +155% jump from the Safe's 0.5, where the old per-call code allowed at most +25%. The window is narrow (a failed-run retry, or an N-1 "short" week), and refresh-to-live at the boundary is the deliberate mitigation, but two cheap hardenings exist: re-baseline immediately whenever the live rate is outside the checkpoint's reachable band (an out-of-band rate proves a higher authority moved it — adopt it), or document the emergency playbook as "when manually overriding the rate, also tightensetBoundsor disable the schedule/module".
Bottom line: e7b393fac cleanly resolves both of clement-ux's substantive findings with faithful tests. The major finding — the runway-band controller draining the reserve and then wedging — is fully intact, since it lives in the parameters and the governance/doc text, none of which changed. F-1 needs a decision before this deploys.
Summary
Automates the OGN buyback and xOGN reward-rate workflow without upgrading the Vault or OToken implementations.
Changes
Add a non-upgradeable
FeeSplitteras the OUSD and OETH vault fee recipient.Add
SetXOGNRewardRateModulefor bounded reward-rate automation.Add Talos actions:
feeSplitterDistribute: distributes accumulated fees daily at 12:10 UTC.setXOGNRewardRate: updates the rate weekly on Tuesday at 01:20 UTC.--dryrun.Add a shared, validated configuration for the deployment and Talos actions.
Add the corresponding schedule entries and action documentation.
Deployment
Contracts deployed:
FeeSplitterFeeSplitterSetXOGNRewardRateModuleSetXOGNRewardRateModuleThe governance proposal:
FeeSplitterfor the mainnet Timelock.FeeSplitter.FeeSplitter.No Vault or OToken implementation upgrade or storage change is required.
Post-deployment
SetXOGNRewardRateModuleon the Guardian Safe.Testing
Code Change Checklist
To be completed before internal review begins:
Internal review:
Deploy checklist
Two reviewers complete the following checklist: