fix(sdk): correct validateConditionContract preflight is a no-op + docs/CONDITIONS.md uses stale 3-arg signature #99
fix(sdk): correct validateConditionContract preflight is a no-op + docs/CONDITIONS.md uses stale 3-arg signature #99sophiej-story wants to merge 8 commits into
Conversation
|
The core fix is sound: the 4-arg ABI update matches the deployed interfaces, the sentinel-probe design correctly distinguishes real implementations from catch-all fallbacks, and the two-phase logic is well-covered by the new unit tests. A few edge-case observations below. One logical gap worth noting: when the real-selector No other high-confidence issues were found. The bytecode, ABI inputs, selector values, and error-classification branches all appear correct. Review iteration 6 · Commit 0cf2cee · 2026-05-27T06:44:15Z |
| // ContractFunctionZeroDataError (EOA / no code at all) and any other | ||
| // error surface as invalid too. | ||
| const cause = e?.cause; | ||
| if (cause?.name === "ContractFunctionRevertedError" && cause.raw !== "0x") { |
There was a problem hiding this comment.
This is tighter than the old implementation, but it still treats "non-empty revert data" as proof that checkReadCondition / checkWriteCondition exists. A contract that does not implement either function but has a fallback that reverts with Error(string) or a custom error will also produce non-empty revert data here and be accepted as valid. In other words, this no longer admits every bytecode address, but it still admits unrelated contracts whose fallback reverts with payload. Since the point of the preflight is to catch misconfigured condition addresses before allocate, this remains a real false-positive path and would benefit from a regression test at minimum.
There was a problem hiding this comment.
addressed in a7e698f — added a sentinel-selector probe so contracts with payload-reverting fallbacks (Diamond Diamond_FunctionDoesNotExist(), explicit revert-string fallbacks) are now rejected with a new reason: "ambiguous-fallback" instead of silently passing. Regression tests added.
|
Re: review findings — iteration 1 follow-up (a7e698f): Both findings now addressed:
New regression tests added accordingly: undefined |
|
I think there is still one important false-negative path left in
That means the new logic now covers:
but it still does not cover:
I think the sentinel probe needs to run for the success path as well, not only for the I also do not see a regression test for this case right now — the new tests cover the variant where the real selector reverts and the sentinel returns OK, but not the more dangerous variant where the real-selector call itself returns successfully via fallback. |
Closes #95.
Fixes the two related bugs in #95: stale 3-arg condition signature in
docs/CONDITIONS.md, and avalidateConditionContractpreflight that silently accepts any contract with bytecode (including ones that don't implement the function).Summary of Changes
docs/CONDITIONS.md— every signature (Expected Interface block, parameter table, narrative, and all four example contracts:OpenCondition,OwnerCondition,TokenGateCondition,MerkleCondition) now uses the canonical 4-arg form(uint32 uuid, bytes accessAuxData, bytes conditionData, address caller). Matches theICDRReadCondition/ICDRWriteConditioninterfaces inpiplabs/storyand the deployedLicenseReadCondition/OwnerWriteConditionon Aeneid.packages/sdk/src/uploader.ts—validateConditionContractnow:0x8db3eb17for read,0x5645dbbffor write);cause.raw !== "0x"onContractFunctionRevertedError, so an empty-data dispatcher fallback revert (selector miss) is correctly classified invalid.Notes on the implementation vs. the snippet in #95
Two empirical findings during implementation that the issue snippet doesn't account for — flagging here:
Dummy
argsneed non-empty bytes, not"0x". The issue suggestsargs: [0, "0x", "0x", zeroAddr]. Probed against the canonicalLicenseReadConditionon Aeneid, that produces aContractFunctionRevertedErrorwithcause.raw === "0x"— because the body'sabi.decode(conditionData, (address, address))reverts on truncated input via a low-levelrevert(0,0). That's indistinguishable from a selector miss. This PR passes 256-byte zero buffers for eachbytesarg so the body decodes successfully and either returns a value or reverts with non-empty payload, keeping the empty-rawsignal specific to dispatcher fallback. The issue's "known caveat" about barerevert()applies more broadly than written.cause.rawis the populated field;cause.dataisundefined. The issue's snippet usescause.raw ?? cause.data. On the viem version in this repo,cause.datais undefined forContractFunctionRevertedError—cause.rawis the only relevant field. Implementation just keys offcause.raw.Test coverage
Three unit tests added in
packages/sdk/__tests__/uploader.test.ts:allocate rejects when condition contract does not implement the check function— mockscause.raw === "0x"(the dispatcher-fallback signal). This is the case the buggy preflight silently let through.allocate accepts when condition function body reverts with non-empty data— mocks a realError("demo")revert payload (0x08c379a0...). Function exists → preflight passes.allocate preflight probes condition contracts with the 4-arg signature— deep-equals the ABI inputs to pin the canonical 4-arg shape and order. Catches any drift back to a 3-arg ABI or a permuted 4-arg form that would yield a different selector.Existing tests preserved:
__tests__/uploader.test.tstests still pass (the file is now 12 tests).uploader.test.ts"default policy rejects EOA…",security.test.tsSEC-06) continue to behave identically: the EOA path producesContractFunctionZeroDataError, which falls through the new catch'snamecheck tothrow InvalidConditionContractError.skipConditionValidation: trueescape hatch are upstream of the preflight and unaffected.Known limitation
A condition contract whose function body uses a bare
revert()(no message, no custom error) produces empty revert data and would still be mis-classified as selector miss. Real contracts virtually always userequire("...")or a custom error; flagging here per #95's note.