Skip to content

dlc-messages: reject offers whose oracle event has matured - #193

Merged
bennyhodl merged 1 commit into
masterfrom
fix/offer-maturity-freshness
Sep 14, 2026
Merged

bennyhodl merged 1 commit into
masterfrom
fix/offer-maturity-freshness

Conversation

@bennyhodl

@bennyhodl bennyhodl commented Sep 10, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes the Bitcoin Red Team finding FND-002, "Accepting a DLC offer on an already-matured oracle event is a guaranteed-loss trap". OfferDlc::validate only compared the offer's locktimes with each other, so an offer over an event whose attestation was already public passed every check on the receive path and the accept path. The offering party knew the outcome before the accepting party committed collateral.

The DLC spec has no receiver-side rule for this either (Protocol.md only gives the offerer a SHOULD on cet_locktime), so this is worth proposing upstream as well.

Changes

  • OfferDlc::validate takes the receiver's clock as now_unix and rejects an offer whose closest oracle event maturity is less than or equal to it. event_maturity_epoch is a unix timestamp per the oracle spec, so no block-height handling.
  • The manager passes self.time.unix_time_now() in on_offer_message, and accept_contract_offer repeats the comparison against the stored offer's cet_locktime (pinned to the closest maturity), since the event can mature while the offer waits for a decision.
  • The stateless validate_offer and AcceptOfferParams take the same now_unix. accept_offer has no clock by design, so the caller supplies it. Examples and tests pass one second before their fixture maturity.

Testing

  • ddk-messages: existing validate tests pass a clock one second before the fixture maturity; a new test checks fresh one second before, matured at, and matured after.
  • ddk-manager manager_tests: the fixture offer is rejected at maturity and accepted one second before; an offer received before maturity is rejected by accept_contract_offer once the clock reaches maturity.
  • cargo test -p ddk-messages, cargo test -p ddk-manager --test manager_tests, cargo test -p ddk --test stateless, cargo check --all-features --tests --examples, cargo +1.89.0 clippy -- -D warnings, and cargo +nightly fmt --check pass.
  • Note: the stateless_* examples already fail on master with "CET locktime must equal the closest maturity date" (fixture cet_locktime 500 vs maturity 750). Unrelated to this change.

馃 Generated with Claude Code

`OfferDlc::validate` only compared the offer's locktimes with each other,
so an offer over an event whose attestation was already public passed
every check on the receive path and the accept path. The offering party
knew the outcome before the accepting party committed collateral
(Bitcoin Red Team finding FND-002).

`validate` now takes the receiver's clock as `now_unix` and rejects an
offer whose closest oracle event maturity is less than or equal to it.
The manager passes its `Time` provider on receipt and repeats the
comparison against the stored offer's CET locktime in
`accept_contract_offer`, since the event can mature while the offer
waits for a decision. The stateless `validate_offer` and
`AcceptOfferParams` take the same `now_unix` so callers without a
manager supply their clock.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@bennyhodl
bennyhodl force-pushed the fix/offer-maturity-freshness branch from e4a27ae to 9dc3b6e Compare September 14, 2026 18:46
@bennyhodl bennyhodl changed the title manager: reject offers whose oracle event has matured dlc-messages: reject offers whose oracle event has matured Sep 14, 2026
@bennyhodl
bennyhodl merged commit 9d825c0 into master Sep 14, 2026
138 checks passed
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