fix(kyber): bind recipient-credit checks to router-family semantics - #91
Conversation
704197b to
7d0ae10
Compare
fredo
left a comment
There was a problem hiding this comment.
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
kyberto the example CI matrix or otherwise run it in CI.
6791fd5 to
c8e54ca
Compare
fredo
left a comment
There was a problem hiding this comment.
Re-review: changes are still required.
- The base branch was force-updated, but this head has not been rebased. GitHub reports
CONFLICTINGand currently presents 133 files; onlyc8e54cais unique to this branch. Please replay that commit onto the current base. - The current force-pushed head has no Solidity CI result, and
kyberis still absent from the example CI matrix. Add the profile or otherwise run this regression in CI. - Deleting
BACKTEST_TRIP_FINDINGS.mdremoved 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.
c8e54ca to
aab931b
Compare
…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).
aab931b to
ab12763
Compare
fredo
left a comment
There was a problem hiding this comment.
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.mdis 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.
…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.
|
Pushed
The three red checks on the prior head ( |
fredo
left a comment
There was a problem hiding this comment.
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.
fredo
left a comment
There was a problem hiding this comment.
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:swapGenericis 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 incallBytes(bytes); the mock directly forwards already-selector-prefixed calldata. swapSimpleMode: Kyber decodesSimpleSwapDataand invokesswapSingleSequenceplus final processing; the mock performs a raw executor call.- The zero-minimum
swapfixture is unreachable because the exactswapentry 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
assertReceiverGetsMinReturnis 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.
|
Pushed
Verification: |
fredo
left a comment
There was a problem hiding this comment.
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.
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
CREATEproof has been removed rather than presented as a harness integration regression.Changes
KyberOriginalMetaAggregationRouterAssertionregistersswap,swapGeneric, andswapSimpleMode.KyberModernMetaAggregationRouterAssertionregisters onlyswapandswapSimpleMode; caller-controlled bit zero never disables its flat check.srcToken == dstToken, whose live-valid accounting window differs because Kyber snapshots after the source debit while the assertion snapshots around the outer call.swapSimpleModeandswap + _SIMPLE_SWAPuse caller source-balance-delta accounting;swapGenericuses the native/_SHOULD_CLAIM_executeSwapaccounting branch;swapdoes not treat_SHOULD_CLAIM/_APPROVE_FUNDas a bypass, and bit zero alone keeps the flat minimum active.swapGeneric; ordinaryswapwraps route bytes incallBytes(bytes); and simple mode decodesSimpleSwapData, transfers input to first pools, callsswapSingleSequence, finalizes, and derives spent input from the caller balance delta.swapGenericwith selector-prefixed raw target calldata.swapclaim-flag bypass regression.kyberprofile in the examples PCL CI matrix.Verification
FOUNDRY_PROFILE=kyber pcl test— 21/21 passingforge fmt --checkon all changed Kyber Solidity filesgit diff --check