Skip to content

refactor(security): single-source lookupKnownSpender (#780) - #827

Merged
szhygulin merged 1 commit into
mainfrom
refactor/780-single-source-known-spender
Jul 26, 2026
Merged

refactor(security): single-source lookupKnownSpender (#780)#827
szhygulin merged 1 commit into
mainfrom
refactor/780-single-source-known-spender

Conversation

@graciangabriel8

Copy link
Copy Markdown
Contributor

Closes #780

What changed and why

lookupKnownSpender was implemented twice: the canonical, exported
version in src/security/known-spenders.ts:35-51, and a local,
non-exported redefinition in src/modules/allowances/permit2.ts:195-222
with byte-for-byte identical CONTRACTS-driven loop/switch/label logic.
Two implementations of one security lookup are guaranteed to drift —
a spender allowlisted (or removed) in one copy without a matching edit
to the other — the same failure class #765 D10's single-sourcing
pattern exists to prevent.

Fix: deleted the local redefinition in permit2.ts and imported the
canonical function instead:

import { lookupKnownSpender } from "../../security/known-spenders.js";

This mirrors the import already used by src/modules/custom-call/actions.ts
and src/modules/execution/index.ts, both of which already imported the
canonical function rather than redefining it — permit2.ts was the only
holdout. The sole callsite (permit2.ts:338 pre-fix,
lookupKnownSpender(chain, downstreamSpender)) type-checks unchanged
against the shared signature (chain: SupportedChain, spender: 0x${string}``).

The CONTRACTS import in permit2.ts became unused once its only
consumer (the local lookupKnownSpender) was removed, so it's dropped
too — the doc comment on Permit2SubAllowanceRow.downstreamSpenderLabel
that references "the canonical CONTRACTS table" is unchanged and still
accurate (the shared function still reads from CONTRACTS, just from
one place now).

Falsifier test

test/780-known-spender-single-source.test.ts — two assertions:

  1. Walks every .ts file under src/ (same recursive-walk pattern as
    test/757-recipient-authorization.test.ts's "no fourth writer"
    guard) and counts function lookupKnownSpender( definitions.
    Fails on unfixed code (finds two: known-spenders.ts and
    permit2.ts). Passes after this fix (finds exactly one, in
    known-spenders.ts).
  2. Asserts permit2.ts contains an import statement for
    lookupKnownSpender from ../../security/known-spenders.js, so a
    future contributor can't satisfy assertion 1 by just renaming the
    local copy to something else while still not importing the shared
    one.

Both assertions report the offending file:line on failure for
immediate actionability.

Tests were NOT run locally — Node/npm are not installed on the
authoring machine. CI (Build & Test on Node 20 + 22) is the
authoritative check for this PR; the new test was written by matching
test/757-recipient-authorization.test.ts's directory-walk pattern
and test/presign-annotation-guard-735.test.ts's static-source-guard
style exactly, so it should run as-is under the existing vitest setup.

Existing tests updated

None. lookupKnownSpender was never exported from permit2.ts and no
existing test in test/allowances-permit2.test.ts (or elsewhere)
imports or exercises it by name — the two implementations were
identical in behavior, so there's no existing assertion whose expected
value changes.

Blast radius

One file's internal function body deleted (permit2.ts), one import
line changed, one import line dropped, no other change. The public
behavior of fetch_permit2_sub_allowances (the tool that calls the
now-shared function at the old line 338) is unchanged — the deleted
code was byte-for-byte identical to what it now calls. No schema,
export, or tool-surface change.

Residual concerns

  • I did not add the optional repo-wide lint rule the issue mentions
    ("add a test/lint guard that a second definition of a security
    lookup fails, per docs(design): consolidate #759 recipient-authorization seam design #765 D10's pattern") as a general rule covering
    every security lookup — only a guard scoped to lookupKnownSpender
    itself. Generalizing it to catch drift on other security lookups
    (allowlists, selector checks, etc.) would need enumerating which
    functions count as "a security lookup" repo-wide, which reads as a
    separate, larger decision than this point-fix; flagging it rather
    than guessing scope.
  • Per dispatch instructions I could not run npm run build, npm run lint, or npm test locally (no Node on this machine) — the diff is
    correctness-by-construction against the existing file's style and
    the CI run is the first real execution of the new test.

permit2.ts carried a byte-for-byte functional duplicate of
known-spenders.ts's lookupKnownSpender (same CONTRACTS-driven
loop/switch/label logic) — two implementations of one security lookup
guaranteed to drift, the failure class #765 D10 single-sourcing exists
to prevent.

Delete the local redefinition in permit2.ts, import the canonical
export from src/security/known-spenders.js instead (matching the
import already used by custom-call/actions.ts and execution/index.ts).
Drop the now-unused CONTRACTS import from permit2.ts along with it.

Add a regression guard (test/780-known-spender-single-source.test.ts)
that walks src/ and asserts exactly one `function lookupKnownSpender(`
definition exists, plus that permit2.ts imports rather than redefines
it.
@szhygulin
szhygulin marked this pull request as ready for review July 26, 2026 08:05
@szhygulin
szhygulin merged commit 01b5a4a into main Jul 26, 2026
3 checks passed
@szhygulin
szhygulin deleted the refactor/780-single-source-known-spender branch July 26, 2026 08:05
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 26, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

security: lookupKnownSpender implemented twice (known-spenders.ts + permit2.ts) — guaranteed drift, single-source it

2 participants