dlc-messages: reject offers whose oracle event has matured - #193
Merged
Merged
Conversation
`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
force-pushed
the
fix/offer-maturity-freshness
branch
from
September 14, 2026 18:46
e4a27ae to
9dc3b6e
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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::validateonly 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::validatetakes the receiver's clock asnow_unixand rejects an offer whose closest oracle event maturity is less than or equal to it.event_maturity_epochis a unix timestamp per the oracle spec, so no block-height handling.self.time.unix_time_now()inon_offer_message, andaccept_contract_offerrepeats the comparison against the stored offer'scet_locktime(pinned to the closest maturity), since the event can mature while the offer waits for a decision.validate_offerandAcceptOfferParamstake the samenow_unix.accept_offerhas 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-managermanager_tests: the fixture offer is rejected at maturity and accepted one second before; an offer received before maturity is rejected byaccept_contract_offeronce 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, andcargo +nightly fmt --checkpass.stateless_*examples already fail onmasterwith "CET locktime must equal the closest maturity date" (fixturecet_locktime500 vs maturity 750). Unrelated to this change.馃 Generated with Claude Code