Skip to content

improve(Disputer): configurable bond balance trigger/target for auto-wrap#3539

Open
droplet-rl wants to merge 4 commits into
masterfrom
droplet/C02SXLDUYFQ-1782981089-847529
Open

improve(Disputer): configurable bond balance trigger/target for auto-wrap#3539
droplet-rl wants to merge 4 commits into
masterfrom
droplet/C02SXLDUYFQ-1782981089-847529

Conversation

@droplet-rl

@droplet-rl droplet-rl commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Motivation

The dataworker's mainnet ABT balance monitor (warn 14 / error 2) fires long before the auto-wrap kicks in: the wrap trigger was hardcoded at 4x the HubPool bond amount (~1.8 ABT today) in the disputer watchdog, so low-balance alerts could never self-heal. Slack context: https://umaproject.slack.com/archives/C02SXLDUYFQ/p1782981089847529

Changes

  • BOND_BALANCE_TRIGGER / BOND_BALANCE_TARGET (whole bond tokens, e.g. 15 / 20) on DataworkerConfig. When set and DISPUTER_ENABLED, runDataworker() runs Disputer.validate() with these levels ahead of pending root bundle validation — i.e. the disputer owns routine bond maintenance, keeping the watchdog lean. Opt-in: deployments without the env vars (e.g. the arweave persister, which also sets DISPUTER_ENABLED) are unaffected. A top-up failure logs an error without blocking the run.
  • The watchdog is unchanged in wiring and keeps its last-resort defaults (4x/8x bond), guaranteeing it can post a dispute bond even if the disputer's maintenance hasn't run.
  • Disputer.validate() improvements (shared by both callers): top up to the target rather than re-parking at the trigger (the target multiplier was previously only used for the allowance check), and wrap partially when the native balance can't cover the full shortfall (keeping a 0.5 native reserve for gas) instead of wrapping nothing. The insufficient-balance throw is preserved for the case where even the partial wrap can't cover a single bond.
  • Documented the behavior + env vars in src/dataworker/README.md.

Config follow-up

Companion bot-configs PR sets trigger 15 / target 20 on zion-across-disputer, just above the monitor's warn threshold of 14. The env vars are inert until this change is deployed.

Testing

yarn test test/disputer.watchdog.ts — 7 passing (3 new cases: default top-up-to-target, configured trigger/target, partial wrap under low native balance).

🤖 Generated with Claude Code

…wrap

The disputer watchdog previously wrapped native token into the HubPool bond
token only when the balance fell below a hardcoded 4x bond amount (~1.8 ABT
on mainnet), topping up only to that same level. This sat below the balance
monitor's warn threshold (14 ABT), so low-balance alerts could never
self-heal, and the balance parked exactly at the trigger so every posted
bond caused another wrap.

- Add BOND_BALANCE_TRIGGER / BOND_BALANCE_TARGET (whole bond tokens) to
  DataworkerConfig, threaded into Disputer via a new bondReserve param.
  Unset values retain the existing multiplier-based defaults.
- Top up to the target level rather than the trigger (the target
  multiplier was previously only used for the allowance check).
- Wrap partially when the native balance cannot cover the full shortfall,
  keeping a 0.5 native-token reserve for gas, instead of wrapping nothing.
  The insufficient-balance throw still fires when even the partial wrap
  cannot cover a single bond.

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5170b6a746

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/dataworker/Disputer.ts Outdated
Comment on lines +69 to +70
const minBondAmount = bondReserve.trigger ?? bondAmount.mul(this.bondMultiplier.min);
let targetBondAmount = bondReserve.target ?? bondAmount.mul(this.bondMultiplier.target);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Floor custom bond reserves at the dispute bond amount

When BOND_BALANCE_TRIGGER/BOND_BALANCE_TARGET are configured below the current HubPool bondAmount, this treats the lower configured value as the minimum/allowance target. In that environment validate() can no-op with a balance or allowance below the bondAmount that HubPool.disputeRootBundle() later pulls with safeTransferFrom, so the watchdog reaches the dispute path but the dispute transaction reverts. Please reject configured levels below bondAmount or floor both thresholds at bondAmount.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed in dc3ec58. validate() now floors both the trigger and target at the HubPool bondAmount (with a warning when the configured trigger is below it), so the maintained balance/allowance always covers the safeTransferFrom in disputeRootBundle(). Flooring at runtime rather than rejecting at config parse time was chosen because bondAmount is only known after the on-chain init query and can change via HubPool.setBond(), which would otherwise turn a valid config into a crash loop. Added a regression test configuring both levels below the bond amount.

droplet-rl and others added 2 commits July 2, 2026 09:09
Per review: the watchdog should stay lean. The configured trigger/target
top-up now runs in runDataworker() when DISPUTER_ENABLED and
BOND_BALANCE_TRIGGER are set (opt-in, so other DISPUTER_ENABLED
deployments are unaffected), ahead of pending root bundle validation. A
top-up failure logs an error but doesn't block the run. The watchdog
retains its pre-existing last-resort defaults (multiples of the bond
amount).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
disputeRootBundle() pulls bondAmount from the disputer, so a configured
trigger/target below it would let validate() pass while the dispute
itself reverts. Floor both levels at bondAmount at runtime (it is only
known post-init and can change via HubPool.setBond()) and warn when the
configured trigger is below it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@droplet-rl

Copy link
Copy Markdown
Contributor Author

Addressed the Codex P2 finding in dc3ec58: Disputer.validate() now floors the configured trigger/target at the HubPool bondAmount, so the maintained balance and allowance always cover the bond that disputeRootBundle() pulls. A warning is logged when the configured trigger sits below the bond amount. Chose flooring over rejecting at config parse time since bondAmount is only known post-init and can change on-chain via HubPool.setBond(). Added a regression test; all 8 disputer watchdog tests pass, tsc and eslint clean.

Note: this was rebased onto 5eb76e9 ("move bond maintenance from watchdog to disputer flow"), which landed on the branch mid-run; doc conflicts were resolved in favor of that commit's wording. The floor applies in Disputer.validate() itself, so it covers both the disputer flow and the watchdog's defaults.

@codex review — the updated diff floors bond reserve levels at the HubPool bond amount per your feedback.

@droplet-rl

Copy link
Copy Markdown
Contributor Author

🔎 View trace

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dc3ec58e49

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/dataworker/Disputer.ts Outdated
const nativeBalance = await this.provider.getBalance(await this.signer.getAddress());
if (nativeBalance.gt(mintAmount)) {
const shortfall = targetBondAmount.sub(balance);
const available = nativeBalance.sub(this.nativeGasReserve);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clamp native reserve subtraction before wrapping

When the signer has less than the 0.5 native-token reserve and its bond-token balance is below the trigger (for example, 2x bond with the watchdog's 4x trigger), nativeBalance.sub(this.nativeGasReserve) underflows and throws before the intended insufficient-native handling. runDisputerWatchdog() calls validate() before submitting a dispute, so a signer that still has enough bond tokens for one dispute can abort solely because it cannot top up to the target; clamp the available native amount to zero when it is below the reserve.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partially correct — the abort you describe doesn't actually occur: BigNumber here is ethers' signed @ethersproject/bignumber, so nativeBalance.sub(nativeGasReserve) returns a negative value rather than throwing. The negative available then fails the gt(bnZero) guard (no mint) and falls into the intended insufficient-native handling, which only throws when the bond balance can't cover even one dispute. I added a regression test for your exact scenario (native below the 0.5 reserve, bond balance at 2x bond vs the 4x default trigger) and it passes on the unmodified code — validate() warns and proceeds.

That said, relying on signed-arithmetic semantics for correctness is fragile, so I applied the clamp anyway in d3e4094: available is now floored at zero via bnMax, which also made the minted ternary redundant. Test suite, tsc, and eslint all pass.

…ping

The negative-available case was already handled correctly (ethers
BigNumber is signed, and the gt(bnZero) guards routed a negative
mintAmount into the insufficient-native warn path), but the clamp makes
that explicit rather than a property of signed arithmetic, and lets the
redundant minted ternary go. Regression test covers a native balance
below the gas reserve with a bond balance still sufficient for one
dispute.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@droplet-rl

Copy link
Copy Markdown
Contributor Author

@codex review — d3e4094 clamps the available native amount at zero before wrapping (via bnMax) and adds a regression test for a native balance below the gas reserve. Note the underflow throw in the finding did not reproduce (ethers BigNumber is signed); the clamp was applied to make the behavior explicit rather than to fix an abort.

@droplet-rl

Copy link
Copy Markdown
Contributor Author

🔎 View trace

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Swish!

Reviewed commit: d3e409480d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@droplet-rl

Copy link
Copy Markdown
Contributor Author

Codex re-review of d3e4094 came back clean. Both findings from the earlier rounds are resolved: configured trigger/target levels are floored at the HubPool bond amount in Disputer.validate() (dc3ec58), and the available native amount is clamped at zero before wrapping (d3e4094). Ready for human review.

@droplet-rl

Copy link
Copy Markdown
Contributor Author

🔎 View trace

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.

1 participant