Skip to content

fix(kyber): bind recipient-credit checks to router-family semantics - #91

Merged
makemake-kbo merged 9 commits into
masterfrom
fix/kyber-backtest-findings
Jul 30, 2026
Merged

fix(kyber): bind recipient-credit checks to router-family semantics#91
makemake-kbo merged 9 commits into
masterfrom
fix/kyber-backtest-findings

Conversation

@makemake-kbo

@makemake-kbo makemake-kbo commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Scope

This PR addresses the Kyber assertion semantics and CI coverage identified during review. It does not claim completion of ENG-3860's seven-day replay: the previously recorded 7,143-block interval was only about 24 hours, and no findings document is included at this head. The external replay payload builder that emitted the earlier malformed constructor data is also outside this repository, so the local low-level CREATE proof has been removed rather than presented as a harness integration regression.

Changes

  • Replace the deployer-supplied router-family boolean with separate one-argument artifacts:
    • KyberOriginalMetaAggregationRouterAssertion registers swap, swapGeneric, and swapSimpleMode.
    • KyberModernMetaAggregationRouterAssertion registers only swap and swapSimpleMode; caller-controlled bit zero never disables its flat check.
  • Exclude srcToken == dstToken, whose live-valid accounting window differs because Kyber snapshots after the source debit while the assertion snapshots around the outer call.
  • Make original-family proportional-minimum classification entry-point aware:
    • direct swapSimpleMode and swap + _SIMPLE_SWAP use caller source-balance-delta accounting;
    • swapGeneric uses the native/_SHOULD_CLAIM _executeSwap accounting branch;
    • ordinary ERC-20 swap does not treat _SHOULD_CLAIM/_APPROVE_FUND as a bypass, and bit zero alone keeps the flat minimum active.
  • Split the semantic mock along the verified entry-point boundaries. Claim/approve collection exists only on swapGeneric; ordinary swap wraps route bytes in callBytes(bytes); and simple mode decodes SimpleSwapData, transfers input to first pools, calls swapSingleSequence, finalizes, and derives spent input from the caller balance delta.
  • Move the claim-mode partial-fill regression to swapGeneric with selector-prefixed raw target calldata.
  • Add faithful direct and delegated simple-mode partial-fill regressions, plus an ordinary-swap claim-flag bypass regression.
  • Keep the kyber profile in the examples PCL CI matrix.

Verification

  • FOUNDRY_PROFILE=kyber pcl test21/21 passing
  • forge fmt --check on all changed Kyber Solidity files
  • git diff --check

Comment thread examples/kyber/test/KyberMetaAggregationRouterAssertion.t.sol Outdated
Comment thread examples/kyber/BACKTEST_TRIP_FINDINGS.md Outdated
@makemake-kbo
makemake-kbo force-pushed the fix/protocol-assertion-semantic-audit branch from 704197b to 7d0ae10 Compare July 22, 2026 15:31

@fredo fredo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Request changes. In addition to the inline findings:

  • The base branch was force-updated. GitHub now reports this PR as conflicting and presents 134 changed files instead of the intended two. Please rebase or cherry-pick the unique Kyber commit onto the current base before the next review.
  • The exposed Chainstack credential is already called out in an existing inline thread; it needs to be removed and rotated.
  • The green checks do not run the Kyber profile. If this test is intended as a regression guard, please add kyber to the example CI matrix or otherwise run it in CI.

Comment thread examples/kyber/BACKTEST_TRIP_FINDINGS.md Outdated
Comment thread examples/kyber/BACKTEST_TRIP_FINDINGS.md Outdated
@makemake-kbo
makemake-kbo force-pushed the fix/kyber-backtest-findings branch 2 times, most recently from 6791fd5 to c8e54ca Compare July 22, 2026 16:25

@fredo fredo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Re-review: changes are still required.

  • The base branch was force-updated, but this head has not been rebased. GitHub reports CONFLICTING and currently presents 133 files; only c8e54ca is unique to this branch. Please replay that commit onto the current base.
  • The current force-pushed head has no Solidity CI result, and kyber is still absent from the example CI matrix. Add the profile or otherwise run this regression in CI.
  • Deleting BACKTEST_TRIP_FINDINGS.md removed the credential and stale IDs from the tip, but the PR title, description, and commit message still claim the disproven silent-zero mechanism and still say the document is rewritten. Please update them to match the actual change.

Comment thread examples/kyber/test/KyberMetaAggregationRouterAssertion.t.sol Outdated
Comment thread examples/kyber/test/KyberMetaAggregationRouterAssertion.t.sol Outdated
Base automatically changed from fix/protocol-assertion-semantic-audit to master July 24, 2026 09:59
@makemake-kbo
makemake-kbo force-pushed the fix/kyber-backtest-findings branch from c8e54ca to aab931b Compare July 24, 2026 10:02
mateo-mro
mateo-mro previously approved these changes Jul 24, 2026
@makemake-kbo
makemake-kbo requested a review from fredo July 24, 2026 10:11
…ssertion bug

The mainnet backtest trip of KyberMetaAggregationRouterAssertion was caused by
deploying the assertion with a single-arg `constructor(address)`, which silently
zeroed `originalRouterFamily_`. That disabled the `_PARTIAL_FILL` skip, so a
legitimate partial-fill order failed the flat `>= minReturnAmount` floor and
reverted. The assertion logic is correct; the fix is to deploy with the 2-arg
`constructor(address,bool)` (true for this original-family router).

- Rewrite BACKTEST_TRIP_FINDINGS.md with the confirmed root cause and disprove the
  original "false positive" candidate (0x0468…): its decoded values (credited above
  minReturn, no partial-fill flag) pass under both deployments, so it never tripped.
- Add testMinReturn_PartialFill_MisconfiguredFamilyFalse_Trips pinning the trip to
  the constructor arg: the same order passes with true and trips with false.
…r in CI

Address review feedback on the backtest-trip findings. The disputed
"silent-zero" mechanism (a single-arg constructor tail reads the missing
trailing bool as zero) is wrong: the real one-argument deployment payload
creationCode || abi.encode(address(router)) REVERTS during construction,
because the (address,bool) constructor prologue cannot ABI-decode a 32-byte
tail. Under pcl this surfaces as AssertionContractDeployFailed before the
triggered router.swap ever runs.

- Add testDeployment_OneArgPayload_RevertsDuringConstruction: builds the exact
  one-argument harness payload and asserts CREATE returns address(0) (construction
  reverted), while the correct two-argument payload constructs. This is the
  CI-safe layer to pin the regression, since the cl.assertion deploy failure is an
  uncatchable process abort rather than a Solidity revert.
- Correct the retained explicit-false test's docstring: it demonstrates a
  successfully-deployed wrong-family flag, NOT the harness bug (which never
  deploys). Cross-reference the construction-revert test for the real root cause.
- Add the kyber profile to the examples-pcl CI matrix so this regression runs on
  every PR (FOUNDRY_PROFILE=kyber pcl test).
Comment thread examples/kyber/test/KyberMetaAggregationRouterAssertion.t.sol Outdated

@fredo fredo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Re-review: the functional correction is now verified—the exact one-argument payload regression passes, the branch is mergeable, Kyber is gated in CI, and all 16 Kyber tests pass. Remaining changes:

  • Update the PR description: it still says the missing bool silently becomes zero, reports 15/15 tests, and says BACKTEST_TRIP_FINDINGS.md is rewritten even though that file is absent.
  • Restore a sanitized durable record of the seven-day replay evidence required by ENG-3860, as requested in the existing inline thread.
  • Run forge fmt; the existing formatter thread identifies the newly added assignment that is not formatter-clean.

Comment thread examples/kyber/test/KyberMetaAggregationRouterAssertion.t.sol Outdated
…mment

Run forge fmt so the two abi.encodePacked bytecode assignments are
formatter-clean (per review). Reword the originalRouterFamily_ = false
comment to describe it as a separate hypothetical wrong-family
configuration, not the backtest-harness bug (which was the malformed
one-argument constructor tail).
Restore a durable record of the seven-day mainnet replay backtest and its
triage outcome, as required by ENG-3860. The record is sanitized: it names the
assex-replay service and its archive-node requirement but carries no access
token, and it pins the toolchain-dependent assertion IDs to a reproducible
command instead of hard-coding stale hashes.

The triage outcome states the corrected root cause -- the one-argument
constructor payload reverts during construction (AssertionContractDeployFailed),
it does not silently zero the trailing bool -- consistent with the corrected
NatSpec and testDeployment_OneArgPayload_RevertsDuringConstruction.
@makemake-kbo makemake-kbo changed the title fix(kyber): resolve backtest trip as a deployment-arity bug, not an assertion bug fix(kyber): resolve backtest trip as a constructor-arity deployment bug, not an assertion bug Jul 24, 2026
@makemake-kbo

Copy link
Copy Markdown
Contributor Author

Pushed d3afa0e. Status of the outstanding review threads:

  • PR description / title — Rewritten. The disproven "missing bool silently reads as zero" mechanism is gone; the description now states the corrected root cause (the one-argument payload reverts during construction → AssertionContractDeployFailed), reports 16/16 tests, and reflects that the findings doc is present (restored, sanitized) rather than "rewritten".
  • ENG-3860 durable record (examples/kyber/BACKTEST_TRIP_FINDINGS.md) — Restored, sanitized: records the frozen 7143-block window (2548000125487144, chain 1), the early-complete/ReplayStopMatch scan outcome, and the triage outcome. No access token; the poll-only limitation on a full tx/revert tally is stated explicitly rather than fabricated.
  • Stale assertion IDs — Removed. The doc pins IDs to a reproducible forge build + cast abi-encode command instead of hard-coding toolchain-dependent hashes.
  • Corrected root-cause claim in the testtestDeployment_OneArgPayload_RevertsDuringConstruction builds the exact one-argument payload and asserts CREATE returns address(0); the explicit-false test is relabelled as a separate hypothetical wrong-family config, not the harness bug.
  • forge fmt — Clean (forge fmt --check passes on the test file).

The three red checks on the prior head (fluid, lido, protection-pcl) failed at the "Install PCL" step with a transient tar: Error is not recoverable download flake — the kyber/balancer/mellow/safe-guard jobs passed the same step. They should clear on the re-run this push triggers.

@makemake-kbo
makemake-kbo requested review from fredo and mateo-mro July 24, 2026 16:54
Comment thread examples/kyber/BACKTEST_TRIP_FINDINGS.md Outdated
mateo-mro
mateo-mro previously approved these changes Jul 27, 2026
lean-apple
lean-apple previously approved these changes Jul 27, 2026

@lean-apple lean-apple left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM

Comment thread examples/kyber/test/KyberMetaAggregationRouterAssertion.t.sol Outdated

@fredo fredo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

One blocking acceptance issue remains: ENG-3860’s seven-day replay is still missing. The sanitized report was added in d3afa0e and then removed in 7656c5d after its block window was shown to cover only about 24 hours. The current PR body nevertheless claims that a durable seven-day findings document is included. Please complete the required approximately 50,400-block replay and restore its sanitized results, or correct the PR scope/body if that requirement has changed.

The code changes otherwise look correct: the branch is mergeable with a focused two-file diff, Kyber CI is green, the local suite passes 16/16, formatting and git diff --check pass, and the constructor failure explanation matches the generated IR.

@makemake-kbo
makemake-kbo dismissed stale reviews from lean-apple and mateo-mro via 82fc0ad July 28, 2026 08:27
Comment thread examples/kyber/test/KyberMetaAggregationRouterAssertion.t.sol Outdated
mateo-mro
mateo-mro previously approved these changes Jul 28, 2026

@fredo fredo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Request changes. This supersedes my earlier conclusion that the assertion logic was otherwise correct. After comparing the fixtures with the exact deployed Kyber runtimes and reproducing the important paths on a mainnet fork, the new partial-fill regression is not a live-valid Kyber route, and the active min-return check is not a universal invariant.

1. Live-confirmed same-token false positive

assertReceiverGetsMinReturn does not measure the same balance window as Kyber. In standard swap, Kyber transfers source funds before _executeSwap snapshots the destination receiver. The assertion snapshots around the entire outer router call.

For srcToken == dstToken, with the caller also the receiver: start at 100 T, debit 10 T source, then credit 10 T output. Kyber sees 90 -> 100, calculates a return of 10, and accepts minReturnAmount = 10. The assertion sees 100 -> 100, calculates zero, and reverts. I confirmed this against the exact mainnet runtime: the router-only control succeeds and the armed call reverts with Kyber: dstReceiver credited below minReturnAmount.

At minimum, exclude srcToken == dstToken. Faithful equivalence would require reproducing the Kyber entry-point- and fee-dependent accounting boundary.

2. The family boolean is unchecked trust configuration

One deployer-supplied boolean controls both trigger registration and whether caller-controlled bit zero disables the check:

  • original + true: intended;
  • original + false: swapGeneric is omitted and genuine pro-rated fills may false-positive;
  • modern + false: intended;
  • modern + true: any caller can set otherwise-ignored bit zero and bypass the assertion.

The modern verified family has no swapGeneric, no _PARTIAL_FILL semantics, and always applies a flat minimum. The constructor validates only the shared router address, not which runtime family is deployed. Please bind configuration to code hash/chain/interface evidence, or split the two families into separate assertion artifacts, and test production triggers() for all configurations.

3. The bit-zero skip is overbroad even on the original family

For ordinary ERC-20 swap calls without _SHOULD_CLAIM, the original router can retain spentAmount == desc.amount despite bit zero being set. Its pro-rated equation then reduces to the ordinary flat minimum, but the assertion skips the call unconditionally. Thus a caller-controlled bit disables defense-in-depth coverage on paths where the flat postcondition still applies.

4. Other mock-only behavior currently presented as protocol protection

  • Fee-on-transfer output: the mock trusts executor-returned gross output. Both exact router families ignore that returndata and calculate the receiver actual balance delta, so the live router itself rejects the advertised underpayment. The assertion adds no protection in that fixture.
  • Standard swap: Kyber wraps raw route bytes in callBytes(bytes); the mock directly forwards already-selector-prefixed calldata.
  • swapSimpleMode: Kyber decodes SimpleSwapData and invokes swapSingleSequence plus final processing; the mock performs a raw executor call.
  • The zero-minimum swap fixture is unreachable because the exact swap entry point rejects zero minimum. The native-output skip fixture is likewise not a successful exact-router route.

These should either be made faithful to the verified implementation or clearly labeled as isolated assertion-branch/hypothetical compromised-runtime tests.

5. These checks are not protocol invariants

  • assertReceiverGetsMinReturn is a narrow call postcondition for distinct, non-rebasing ERC-20 accounting. On ordinary routes it mostly duplicates the Kyber guard; it is not a universal invariant.
  • The retired allowance check is not an invariant: transfer-log inference cannot determine the spender or causality. Keeping it unarmed is correct.
  • Constructor arity is deployment encoding correctness, not a protocol invariant.

Two balance snapshots also assume additive token semantics; rebasing/reflection behavior can create false positives or mask underpayment.

6. Acceptance evidence is still inconsistent

The PR body claims a durable seven-day findings document, but the head contains only the workflow and test changes. The recorded interval, 7,143 blocks (25480001 -> 25487144), is about 23h52m at the observed timestamps, not seven days. The requested approximately 50,400-block replay or a corrected scope/body is still needed.

Protocol references used for this review: Kyber official router deployment, mainnet exact-match source, and modern-family exact-match source.

Comment thread examples/kyber/test/KyberMetaAggregationRouterAssertion.t.sol
Comment thread examples/kyber/test/KyberMetaAggregationRouterAssertion.t.sol Outdated
Comment thread .github/workflows/solidity-test.yml
@makemake-kbo makemake-kbo changed the title fix(kyber): resolve backtest trip as a constructor-arity deployment bug, not an assertion bug fix(kyber): bind recipient-credit checks to router-family semantics Jul 29, 2026
@makemake-kbo

Copy link
Copy Markdown
Contributor Author

Pushed 327b9b2 and updated the PR title/body. Latest-review status:

  • Same-token false positive: excluded and covered by a live-valid debit-then-credit regression.
  • Family boolean: removed; original and modern families are separate one-argument artifacts with tested selector manifests.
  • Bit-zero bypass: original skips only genuine native/_SHOULD_CLAIM pro-rated paths; modern never treats bit zero as a bypass.
  • Mock fidelity: swap dispatch, receiver balance-delta accounting, source/snapshot ordering, conditional partial spend, and fee-on-transfer behavior now match the reviewed runtime boundaries. swapSimpleMode is explicitly scoped as an isolated branch fixture.
  • Constructor/harness claim: removed because the external payload builder is not in this repo.
  • ENG-3860 replay claim: removed; the PR explicitly states the earlier 7,143-block interval was about 24 hours and is not a completed seven-day replay.

Verification: FOUNDRY_PROFILE=kyber pcl test passes 18/18; formatter and git diff --check pass. The three latest inline threads have replies and are resolved.

Comment thread examples/kyber/src/KyberMetaAggregationRouterAssertion.sol Outdated
Comment thread examples/kyber/src/KyberMetaAggregationRouterAssertion.sol
Comment thread examples/kyber/test/KyberMetaAggregationRouterAssertion.t.sol

@fredo fredo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Re-reviewed at a061c8a. Both prior blockers are resolved: proportional minimum enforcement is now bound to the correct entry-point semantics, and the claim/partial-fill fixtures plus mocks exercise the corresponding router paths. The Kyber suite passes 21/21, all CI checks are green, and exact direct and delegated simple-mode partial-fill routes passed against the deployed router on a mainnet fork.

@makemake-kbo
makemake-kbo merged commit 119ee82 into master Jul 30, 2026
11 checks passed
@makemake-kbo
makemake-kbo deleted the fix/kyber-backtest-findings branch July 30, 2026 12: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.

4 participants