Skip to content

feat(contract-check): static checks for encrypted-type boundary rules - #341

Draft
alexshchur wants to merge 6 commits into
masterfrom
feat/contract-check-master
Draft

feat(contract-check): static checks for encrypted-type boundary rules#341
alexshchur wants to merge 6 commits into
masterfrom
feat/contract-check-master

Conversation

@alexshchur

Copy link
Copy Markdown
Contributor

Adds @cofhe/contract-check: static checks that the encrypted-type boundary conventions are actually followed in contract signatures.

Why

Encrypted values travel as handles, and a contract is normally ACL-allowed on the handles it works with. So a function accepting a raw euint64 from outside can be induced to compute on someone else's ciphertext using its own authority — the confused-deputy shape behind the confidential-deposit finding. The sharedE* / externalE* types close that hole by making a live handle unrepresentable at a trust boundary, but only if signatures use them. Nothing enforced that until now.

Rules

id severity notes
no-raw-encrypted-params error external/public must not accept raw encrypted types; follows structs and arrays
no-raw-encrypted-returns error external/public state-mutating must not return them; view/pure exempt
no-raw-shared-wrap error sharedE*.wrap/.unwrap only inside the FHE library
external-input-missing-proof error external inputs with no proof bytes can never be verified
proof-placement warning opt-in, silent by default
receive-variant warning receive*Param vs receive*FromCall must match the value's origin

Two design notes worth reviewing:

proof-placement is off by default. FHE.asEuintXX(hash, proof) and the batch verifier are both first-class, so neither arrangement is a defect and the checker accepts both. Set proofStyle: 'trailing' | 'per-value' only to pin a house style — typically so signatures match a generated client encoder. Deviations warn; they never fail a build.

receive-variant reports only what it can prove. Origin is resolved when the argument is a call expression, a parameter reference, or a local assigned exactly once. Reassigned locals, storage reads and struct members yield "unknown" and the rule stays silent, rather than guessing with a heuristic.

How it works

Reads solc build-info (Hardhat artifacts/build-info, Foundry out/build-info with --build-info) rather than parsing Solidity. The compiler has already resolved imports, aliases and inheritance, so euint64 is unambiguous no matter how it was imported. No new parser to maintain.

npx hardhat compile && npx contract-check    # exits 1 on any error finding

Also exported as a library (checkBuildInfo, checkBuildInfoFile, checkBuildInfoDir).

Open question for reviewers

Run against a real token project today and you get a large report — the confidential-token base contracts and IERC7984 itself currently take and return raw handles at public boundaries. Locally that is ~117 findings, almost all from vendored sources rather than app code.

That is the tool working correctly, but it means we need a policy before this can gate anything:

  1. those signatures become compliant as the token bases migrate, and the noise disappears on its own; or
  2. specific standard entrypoints are deliberately exempt and need an allowlist; or
  3. vendored code is excluded wholesale, and the rules only ever apply to first-party contracts.

Currently only FHE.sol and the cofhe-contracts path are exempt, overridable via libraryPaths. Which of the three we want determines what the config surface should look like.

Draft because

  • no config file yet (exclude globs + per-rule severity) — that is the piece that makes this adoptable in a repo with vendored contracts, and its shape depends on the question above
  • no Hardhat plugin yet to run the check automatically post-compile
  • receive-variant cannot see through helper functions or reassigned locals; a Slither detector would close that gap later

25 tests, no findings against real production build-info for the four newer rules.

Reads solc build-info (hardhat or foundry) and reports violations of the
shared-euint boundary conventions:

  no-raw-encrypted-params   external/public must not accept raw euint*/ebool/
                            eaddress (recurses through structs and arrays)
  no-raw-encrypted-returns  external/public state-mutating must not return raw
                            encrypted types; view/pure exempt
  no-raw-shared-wrap        sharedE*.wrap/unwrap only inside the FHE library

proof-placement and receive-variant are registered but inert, pending the
calldata convention decision and data-flow support respectively.

Ships a library API plus a contract-check CLI; consuming build-info rather than
parsing Solidity means types arrive already resolved by the compiler.
Documents each rule with the reasoning and correct/incorrect examples, the
recognised type inventory, and how to wire the check into hardhat/foundry
projects, package scripts, CI, or code.
…ment opt-in

Splits the parked proof rule in two, since the library supports both a proof
per value and one signature over a batch:

  external-input-missing-proof  error. External inputs with no proof bytes can
                                never be converted to a usable handle, which is
                                wrong under either arrangement.
  proof-placement               warning, off by default. Set proofStyle to
                                'trailing' or 'per-value' to pin a house style,
                                typically to match a generated client encoder.

Exposed as a proofStyle option and a --proof-style flag. No findings against
local production build-info, so the error rule adds no false positives.
…ysis

receive*Param verifies provenance against msg.sender; receive*FromCall against
a named callee. Swapping them reverts opaquely at runtime, so the mismatch is
worth catching statically.

Origin is resolved only where locally provable — the argument is a call
expression, a parameter reference, or a local assigned exactly once. Reassigned
locals, storage reads and struct members yield unknown and the rule stays
silent, so it reports what it can prove instead of guessing. Warning severity,
since this is a usability failure rather than a security one.

All five rules are now implemented; no findings against local production
build-info.
Exact-pin devDependencies per the pinned-deps check, add lint/check:types
scripts and eslint-config to match sibling packages, prettier-format all
sources, and add the changeset. The real-build smoke test now reads
CONTRACT_CHECK_FIXTURE instead of a hardcoded local path, so it skips cleanly
everywhere it is not set.
@changeset-bot

changeset-bot Bot commented Aug 24, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 04f77ed

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@cofhe/contract-check Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Aug 24, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cofhesdk-docs-gh-action Ready Ready Preview Aug 24, 2026 10:26am
cofhesdk-react Ready Ready Preview Aug 24, 2026 10:26am
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
cofhesdk-docs Ignored Ignored Aug 24, 2026 10:26am

Request Review

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