Job
Blocked: node docs: path_violation
Implement ExpiringMerkleDistributor in src/ExpiringMerkleDistributor.sol, test it, review it and document it. Do not deploy. constructor(address token, bytes32 merkleRoot, uint256 expiry, address sweepTo), all immutable; reverts unless expiry > block.timestamp and sweepTo and token are nonzero.
Leaves are keccak256(bytes.concat(keccak256(abi.encode(uint256 index, address account, uint256 amount)))) (OpenZeppelin StandardMerkleTree double hash, so an inner node cannot pass as a leaf); proofs …
Work
- posted4 minto the first attempt
- built
#592Impl1 file changed
writes tosrc/ExpiringMerkleDistributor.solExpiringMerkleDistributoris implemented, built, tested, and committed asb52b6e6. Onlysrc/ExpiringMerkleDistributor.solwas added. No dependencies or config files were touched, and the repo had no foundry.toml, so Foundry's defaults were used.What the contract does
- Constructor stores token, merkleRoot, expiry and sweepTo as immutables. It reverts with
ZeroAddressfor a zero token or sweepTo, and withExpiryInPastunless expiry is strictly after the current timestamp. - claim is open to anyone and always pays the
accountin the leaf. It reverts withClaimWindowClosedafter expiry,AlreadyClaimedfor a used index, andInvalidProofotherwise. The bitmap bit is set andClaimedemitted before the token transfer, so a callback token cannot re-enter and double claim. - Leaf and proof format match OpenZeppelin StandardMerkleTree: the leaf is the double keccak of the ABI-encoded triple, and proof siblings are hashed as sorted pairs. The double hash rules out passing an inner node as a leaf.
- isClaimed reads a bitmap with 256 indices per storage word.
- sweep is open to anyone, reverts with
NotYetExpiredwhile the timestamp is at or before expiry, and otherwise transfers the whole balance to sweepTo and emitsSwept. It can be repeated for tokens that arrive later, including a zero-balance sweep. - Safe transfer helper uses a low-level call. It accepts no return data only if the token has code, accepts a 32-byte word equal to one, and reverts with
TransferFailedon a revert, a returned false, malformed data, or a codeless token.
Verification
forge buildsucceeds. The only compiler output is the linter's standard warning about comparing against block.timestamp, which is inherent to a deadline contract.forge testpasses 31 tests in test/scratch, written against an inlined cheatcode interface so no forge-std was needed. They cover all constructor reverts, the exact expiry boundary for both claim and sweep, double claims, bitmap word boundaries at indices 255 and 256, wrong index, account, amount, proof and proof length, a single-hashed leaf tree being rejected, a single-leaf tree with an empty proof, third-party relaying, a no-return token, a false-returning token, malformed return data, a codeless token, an underfunded distributor, and two fuzz tests, one of which builds random trees of up to 20 leaves and checks every leaf claims exactly once with full balance conservation.
Assumptions and notes for the integrator
- The contract has no admin and no pause. The only privileged address is the passive sweepTo recipient.
Sweptis emitted even when the balance is zero. That is a design choice, not an error.- Fee-on-transfer or rebasing tokens are not specially handled. Claimants may receive less than the leaf amount and the contract does not check funding at deploy time.
- The claim event parameters are unindexed, matching the signature given in the task. If off-chain indexers want filtering by account, that would be a signature change and should be agreed first.
- A zero merkleRoot is accepted by the constructor. No leaf can be proven against it, so such a deployment is effectively sweep-only after expiry.
ran onclaude · claude-fable-5-1 · 10 turns · 3m 31s · 290 in · 16.1K out · 389.8K cachedsubmission2b4dfc402f4a2a534fa2f1e11f82a05b318e10c4dd224b5668dcb0abe50bf6c2device29ea8835baa3dfe123fd5a001f4e9b467fad3076df4065b528a6bc1ad94dca33started from0243d7da4a4337ae8b16bcdf15bb4ead736fd68fbundleaf1c0e210992f1bffba1226a70320e3f1fb6aa4b1bae44e0f43c46f0cd9cc02f · 3.1 KBverifiedrebuilt and matched · verifier 0.1.0 ·changed · 1 filesrc/ExpiringMerkleDistributor.sol - Constructor stores token, merkleRoot, expiry and sweepTo as immutables. It reverts with
- reviewed
#1846Review1 finding · 1 low
afterImplWrote .imd-findings.json with one low-severity finding: the required Foundry tests are missing.
No substantiated contract exploit was found in the requested areas. Review used source inspection and read-only checks; no EVM tests were executed. Implementation files remain unchanged.
ran oncodex · gpt-6-astra · 4 turns · 2m 46s · 35.1K in · 5K out · 193.4K cachedsubmission1c528c714c415565d41c6225c0ac563210dc2fd1b37ec4cd5dafe3143b1fce11device8df16409e9706163f892e77e58b850c47e95fd72b1c7ccf056024111340c4fadstarted fromd35dde28837a0c410fdb7cbfa81880406515ffb4bundlenoneapplied onaf1c0e210992f1bffba1226a70320e3f1fb6aa4b1bae44e0f43c46f0cd9cc02fchanged · 0 filesnothingRequired Foundry tests are missingsrc/ExpiringMerkleDistributor.sol:33
The implementation includes no tests, despite the explicit requirement to test the distributor. The only tracked project file is this contract, and there are no test contracts or test functions. Consequently, none of the required adversarial cases has committed regression coverage: double-hashed leaves versus inner nodes, bitmap boundaries, the inclusive claim deadline, early and repeated sweeps, or no-bool transfers and failed-transfer rollback.
This is a missing deliverable and verification gap, not a demonstrated contract exploit. Add a self-contained Foundry test suite covering these cases.
- tested
#592Tests1 file changed
afterImplwrites totest/ExpiringMerkleDistributor.t.solThe build artifacts under
cache/andout/are git-ignored, so the only change in the tree is the test file. The work is complete.Result:
test/ExpiringMerkleDistributor.t.soladds 52 tests, all passing underforge testwith 256 fuzz runs each. No dependencies were added and no configuration files were touched.What the suite covers
- Fixture: a 6-leaf tree built in Solidity with StandardMerkleTree double-hashed leaves and sorted-pair inner nodes. Index 0 is present, Alice appears at indexes 0 and 2, and indexes 255, 256 and 1000 span three bitmap words. A local verifier confirms every fixture proof before the tests rely on it.
- Constructor: zero token, zero sweepTo, expiry equal to now, expiry in the past, expiry one second ahead, and the revert priority when both checks fail.
- Claims: happy path with event, relayer submission paying the account rather than the caller, same account across two indexes, arbitrary claim order, and conservation of funded tokens under fuzzed claim subsets.
- Double claims: same caller, different caller, garbage proof after a claim, and both sides of the 255/256 word boundary.
- Bad inputs: wrong index, swapped indexes between the two Alice leaves, wrong account, wrong amount, another leaf's proof, empty, truncated, extended, bit-flipped and reordered proofs, plus fuzzed wrong accounts, amounts and unknown indexes.
- Inner node as leaf: takes a genuine inner node with its shortened path to the root, confirms locally that the path verifies for the node, then shows every plausible reinterpretation of its children as claim arguments is rejected. Also rejects the single-hashed leaf and the root presented with an empty proof.
- Expiry: claim at exactly expiry passes, at expiry plus one reverts, fuzzed timestamps on both sides, and the window check taking precedence over proof errors.
- Sweep: reverts before and at expiry, sends everything at expiry plus one, repeat sweep emits zero, later arrivals are swept again, and the bitmap survives a sweep.
- Tokens: a USDT-style no-bool mock succeeds for claim and sweep, a false-returning token, a token returning a non-boolean word, an underfunded token and a token address with no code all revert with the contract's transfer error. A failed transfer leaves the index unclaimed so the same claim succeeds after topping up.
- Bitmap: fuzzed four-leaf trees with indexes anywhere in the uint256 space, asserting each claim sets only its own bit and no neighbour bit in the same or adjacent word. A fixed test hits 0, 255, 256, 511, 512, the 2^255 word and the top of the space.
Defects: none found, so no findings file was written. Three observations are informational only and do not violate the stated spec. The constructor does not check that the token has code, which the tests show fails safely at claim time. Sweeping a zero balance issues a zero-value transfer, which a few real tokens reject. The Claimed event has no indexed parameters, which limits log filtering but matches the requested signature.
ran onclaude · claude-fable-5-1 · 6 turns · 4m 49s · 162 in · 24.8K out · 272.5K cachedsubmission80023c51c57420193b0d296015ad6ed44aa4de8f694daa7241b884e8a056a8d4device29ea8835baa3dfe123fd5a001f4e9b467fad3076df4065b528a6bc1ad94dca33started fromd35dde28837a0c410fdb7cbfa81880406515ffb4bundle2d3f6bd159760d60305038b8fab67fbbcacb08b65aab5d7fa19ca6d5ca7b052d · 10 KBverifiedrebuilt and matched · verifier 0.1.0 ·applied onaf1c0e210992f1bffba1226a70320e3f1fb6aa4b1bae44e0f43c46f0cd9cc02fchanged · 1 filetest/ExpiringMerkleDistributor.t.sol - built
#1120Docspath violationon the agent's machine: output outside allowed paths: artifacts/README.md
output outside allowed paths: artifacts/README.md
ran oncodex · 0ssubmissionb96c5bb469e1bd5c34eca15d82a58cd9f180107ee522d120f61593633989170cdevice0256823ae36e790079c99eb46fcdaa245ca2fdeeb213e0b3a63c8e85afb04046started from3abe1413ec007d8abf52c64c4055c4cd89d565e0bundlenoneapplied on2d3f6bd159760d60305038b8fab67fbbcacb08b65aab5d7fa19ca6d5ca7b052dchanged · 0 filesnothing#1979path violationon the agent's machine: output outside allowed paths: artifacts/README.md
output outside allowed paths: artifacts/README.md
ran onclaude · 0ssubmission9acd050904d6aab78e4f7a198b4b4b64f8781a2a738e71c67d661d13ab4a3a25device0476c44a80aa9574a3121027b06e9d96aa0536075373ba287ec42f00e433320estarted from3abe1413ec007d8abf52c64c4055c4cd89d565e0bundlenoneapplied on2d3f6bd159760d60305038b8fab67fbbcacb08b65aab5d7fa19ca6d5ca7b052dchanged · 0 filesnothing#1554path violationon the agent's machine: output outside allowed paths: artifacts/README.md
afterTests, Reviewwrites toREADME.mddocs/**output outside allowed paths: artifacts/README.md
ran onclaude · 0ssubmissionf86b48d3c4e3cea9ee09d9c6c2cb3f615d8e7282077972cda9a2d62bac917688device0479f300f3637e6e62f6d1dde6904031b6122d0026c11ec946db19795cf95a18started from3abe1413ec007d8abf52c64c4055c4cd89d565e0bundlenoneapplied on2d3f6bd159760d60305038b8fab67fbbcacb08b65aab5d7fa19ca6d5ca7b052dchanged · 0 filesnothing - publishedafter verification
- onchain