diff --git a/docs/adr/0006-metadata-freeze.md b/docs/adr/0006-metadata-freeze.md index 0fc98acc..6a2dabc7 100644 --- a/docs/adr/0006-metadata-freeze.md +++ b/docs/adr/0006-metadata-freeze.md @@ -65,9 +65,12 @@ Current source references: possible after collections reference that dependency key. - `StreamCore.updateContracts(3, newContract)` can swap the dependency registry address for future reads. -- `retrieveDependencyScript` concatenates dependency chunks through dynamic - string composition. Slither tracks this as - [P0-META-001](https://github.com/6529-Collections/6529Stream/issues/9). +- `retrieveDependencyScript` renders dependency chunks with initialized + `string.concat`, and `retrieveDependencyScriptContentHash(tokenId)` exposes a + typed dependency content hash for the referenced dependency key. The hash is + segment-safe for the current registry content, but it is not a full freeze + manifest by itself because it does not pin registry identity, provenance, or + immutable version lifecycle. - `burn(collectionId, tokenId)` burns the ERC-721 token and increments `burnAmount[collectionId]`. After burn, `tokenURI(tokenId)` reverts through `_requireMinted`, while internal token mappings remain in storage. @@ -332,10 +335,16 @@ Required model: - frozen collections resolve dependency output from the pinned immutable record or from a manifest snapshot -`P0-META-001` owns the packed/dynamic composition fix. The implementation must -avoid ambiguous dynamic concatenation when hashing or proving dependency -content. Use `abi.encode`, length-prefixing, per-chunk hashes, or an equivalent -typed format instead of ambiguous packed dynamic fields. +`P0-META-001` owns the packed/dynamic composition fix and now provides typed +per-chunk and full-content hashes. The accepted hash shape uses `abi.encode`, +the dependency key, chunk count, chunk index, chunk byte length, and per-chunk +content hash so two ambiguous chunk layouts that render the same JavaScript +still produce distinct proof hashes. + +`P1-META-003` remains responsible for immutable dependency versions, +provenance, registry identity, deprecation semantics, and freeze-manifest +pinning. Release manifests must pair any dependency content hash with the +registry contract identity and accepted dependency version record. ## ERC-4906 Event Policy @@ -561,7 +570,9 @@ signal that metadata changed before freeze. ## Open Follow-Ups -- Resolve [P0-META-001](https://github.com/6529-Collections/6529Stream/issues/9). +- Keep the [P0-META-001](https://github.com/6529-Collections/6529Stream/issues/9) + typed dependency hash regression suite in place while later freeze-manifest + work builds on it. - Implement [P1-META-001](https://github.com/6529-Collections/6529Stream/issues/46). - Complete [P1-META-002](https://github.com/6529-Collections/6529Stream/issues/47). - Build [P1-META-003](https://github.com/6529-Collections/6529Stream/issues/48). diff --git a/docs/known-blockers.md b/docs/known-blockers.md index 29d24556..c8607210 100644 --- a/docs/known-blockers.md +++ b/docs/known-blockers.md @@ -52,6 +52,11 @@ contributors who start from the README. richer metadata state exposure, provider configuration runbooks, canonical core/coordinator lifecycle ownership, and full handling of weak helper randomness beyond disabling `RandomizerNXT` as a production randomizer. +- Dependency script retrieval now has segment-safe typed chunk and content + hashes, so the former packed/dynamic chunk-boundary Slither finding is fixed. + Remaining metadata blockers include golden-file metadata tests, collection + freeze manifests, immutable dependency version records, registry identity + pinning, ERC-4906 signaling, burn semantics, escaping, and size limits. - Slither high/medium findings are captured in `ops/SLITHER_BASELINE.md` and need triage before audit readiness. - Auction custody, auction bid/outbid payment, auction settlement-credit, @@ -59,8 +64,8 @@ contributors who start from the README. emergency-surplus, randomizer request lifecycle, randomizer callback validation, deterministic randomizer retry, raw-output hash storage, and randomizer reserve-boundary regressions now exist, but broader payment, - metadata, deployment, - production-governance, and invariant tests are still missing. + metadata, dependency versioning/freeze, deployment, production-governance, and + invariant tests are still missing. - Deployment scripts, manifests, and rehearsal runbooks are missing. Do not treat the current build/test smoke baseline as a security claim. diff --git a/docs/status.md b/docs/status.md index 81aac6d0..b6450cba 100644 --- a/docs/status.md +++ b/docs/status.md @@ -12,20 +12,25 @@ The current Gate A smoke baseline proves: fixed-price pull-payment credits, curator reward claim credits, and randomness lifecycle behavior. Current emergency-withdrawal target-state tests also cover explicit emergency recipients, `StreamMinter` surplus - withdrawal, and `NextGenRandomizerRNG` reserve boundaries. + withdrawal, `NextGenRandomizerRNG` reserve boundaries, and dependency-script + segment-safe content hashing. - Randomizer tests now cover request lifecycle views, callback validation, raw-output hash storage, failed post-processing state, bounded deterministic post-processing retry, and the conservative provider-migration policy that blocks lifecycle-aware provider replacement while collection requests are pending. +- Metadata encoding tests now prove dependency chunk boundaries are included in + typed content hashes while preserving the existing rendered generative script + output. - CI can run the same build/test smoke commands and publish logs. The current tests are regression tripwires, not a correctness proof. Known blockers remain tracked in `ops/ROADMAP.md`, including broader pull-payment accounting and cross-contract invariants, fuller randomizer reserve lifecycle accounting, callback-after-burn policy, canonical randomizer lifecycle -ownership, static-analysis triage, signer lifecycle operations, deployment -discipline, and the broader P0/P1 test suite. +ownership, remaining static-analysis triage, signer lifecycle operations, +dependency version/freeze manifest work, deployment discipline, and the broader +P0/P1 test suite. Contributor and security intake files exist so future work can be packaged and reviewed consistently, but they do not change the pre-audit status. diff --git a/ops/AUTONOMOUS_RUN.md b/ops/AUTONOMOUS_RUN.md index a078131a..6d1a698a 100644 --- a/ops/AUTONOMOUS_RUN.md +++ b/ops/AUTONOMOUS_RUN.md @@ -33,11 +33,11 @@ tests, security hardening, deployment discipline, and release/audit readiness. | Field | Value | | --- | --- | | Remote | `https://github.com/6529-Collections/6529Stream.git` | -| Active PR branch | `codex/randomizer-raw-output-hash` | -| Last merged PR | `https://github.com/6529-Collections/6529Stream/pull/69` | +| Active PR branch | `codex/dependency-script-safe-encoding` | +| Last merged PR | `https://github.com/6529-Collections/6529Stream/pull/70` | | Roadmap file | `ops/ROADMAP.md` | | State file | `ops/AUTONOMOUS_RUN.md` | -| Last updated | `2026-06-10 17:58 UTC` | +| Last updated | `2026-06-10 18:27 UTC` | ## Packaging Notes @@ -83,7 +83,8 @@ The queue will evolve as PRs merge and bot feedback arrives. | 26 | Block randomizer migration while requests are pending | Gate C | Implement P0-RAND-005 default ADR policy: lifecycle-aware pending counts, provider-migration guard, stale/fulfilled unblocking, tests, docs, and roadmap state updates | Merged in PR #67 | | 27 | Add failed randomness post-processing state | Gate C | Implement P0-RAND-004 failed-state path for deterministic post-processing reverts, with VRF/arRNG tests, docs, and roadmap state updates | Merged in PR #68 | | 28 | Add bounded randomness post-processing retry | Gate C | Implement P0-RAND-006 stored-seed manual retry for deterministic failed post-processing, with VRF/arRNG tests, docs, and roadmap state updates | Merged in PR #69 | -| 29 | Store raw random output hashes | Gate C | Implement P0-RAND-007 raw-output hash storage policy, domain-separated seed derivation, event/view exposure, tests, docs, and roadmap state updates | Open in PR #70; CI green, CodeRabbit clean by review comment, aggregate status stale pending | +| 29 | Store raw random output hashes | Gate C | Implement P0-RAND-007 raw-output hash storage policy, domain-separated seed derivation, event/view exposure, tests, docs, and roadmap state updates | Merged in PR #70 | +| 30 | Fix dependency script packed encoding | Gate C/Gate D | Implement P0-META-001 typed dependency chunk/content hashes, preserve rendered-script compatibility, add metadata encoding tests, and update Slither/roadmap traceability | Open in PR #71; follow-up local validation complete, post-follow-up CI pending | ## Current PR Worklog @@ -2401,9 +2402,7 @@ Outcome: ### PR #70: Store raw random output hashes (Queue Item 29) -Status: PR #70 open and merge-ready by autonomous maintainer decision; CI is -green, CodeRabbit's latest review comment is clean, and the aggregate CodeRabbit -status remains stale pending as documented in prior PR cycles. +Status: Merged. Branch: `codex/randomizer-raw-output-hash`. Pull request: `https://github.com/6529-Collections/6529Stream/pull/70`. Related issue: @@ -2515,6 +2514,122 @@ Review requests: clean, and left only non-blocking maintainability notes. Its aggregate commit status remained stale pending despite the clean review evidence. +Outcome: + +- Merged as PR #70 on `2026-06-10 18:02 UTC`. +- Merge commit: `350667fff6472e938790f0c7db5895fc3c4ddee9`. +- Latest head before merge: `f52cd8f3cf83a8c131bdbc233c4769a4ba72e3fb`. +- Issue #43 closed completed. +- GitHub CI passed on final head in run `27295440912`. +- CodeRabbit final clean comment: `4672928268`. +- Claude was not requested for this PR per user instruction; CodeRabbit was + sufficient. + +### PR #71: Fix dependency script packed encoding (Queue Item 30) + +Status: Open; CodeRabbit clean with non-blocking observations addressed in +follow-up; local follow-up validation complete, post-follow-up CI pending. +Branch: `codex/dependency-script-safe-encoding`. +Pull request: `https://github.com/6529-Collections/6529Stream/pull/71`. +Latest head before PR-state update: `457ca920cb55c9d4b75efcede714ccc1ef700a5b`. +Related issue: + +- `https://github.com/6529-Collections/6529Stream/issues/9` + +Goal: + +- Complete `P0-META-001` by eliminating the remaining first-party Slither + `encode-packed-collision` row for dependency script composition. +- Preserve the current rendered dependency script output for compatibility while + exposing typed, segment-safe chunk and content hashes for proof, indexing, and + future freeze manifests. +- Keep full dependency versioning, registry identity pinning, provenance, and + freeze-manifest semantics in the later `P1-META-003` workstream. + +Candidate files: + +- `smart-contracts/DependencyRegistry.sol` +- `smart-contracts/IDependencyRegistry.sol` +- `smart-contracts/StreamCore.sol` +- `test/StreamMetadataEncoding.t.sol` +- `docs/adr/0006-metadata-freeze.md` +- `docs/known-blockers.md` +- `docs/status.md` +- `test/README.md` +- `ops/ROADMAP.md` +- `ops/SLITHER_BASELINE.md` +- `ops/AUTONOMOUS_RUN.md` + +Initial implementation notes: + +- `DependencyRegistry` now exposes + `getDependencyScriptChunkHash(bytes32,uint256)` and + `getDependencyScriptContentHash(bytes32)`. +- Chunk hashes include `DEPENDENCY_SCRIPT_CHUNK_TYPEHASH`, chunk index, + `keccak256(bytes(chunk))`, and byte length. +- Content hashes include `DEPENDENCY_SCRIPT_CONTENT_TYPEHASH`, dependency key, + chunk count, and a folded `abi.encode` hash of all typed chunk hashes. +- `StreamCore.retrieveDependencyScript(uint256)` initializes its accumulator and + uses `string.concat` for rendering. +- `StreamCore.retrieveDependencyScriptContentHash(uint256)` exposes the + referenced dependency content hash for minted tokens. +- `test/StreamMetadataEncoding.t.sol` proves that chunks `["ab", "c"]` and + `["a", "bc"]` render the same script but produce distinct typed content + hashes, and that empty chunk hashes differ by index. + +Validation so far: + +- PR #70 merge checked locally by fast-forwarding `main` to + `350667fff6472e938790f0c7db5895fc3c4ddee9`. +- Focused `forge test --match-contract StreamMetadataEncodingTest -vvv` + passed: 2 tests, 0 failed. +- `forge fmt` ran on changed Solidity files. +- Slither delta run returned the expected remaining baseline findings while + removing the target rows: `slither_exit=-1`, `total=685`, `high=8`, + `medium=28`, `low=63`, `informational=580`, `optimization=6`, + `encode-packed-collision=0`, and `uninitialized-local=10`. +- `forge fmt --check smart-contracts\DependencyRegistry.sol + smart-contracts\IDependencyRegistry.sol smart-contracts\StreamCore.sol + test\StreamMetadataEncoding.t.sol` passed. +- Focused `forge test --match-contract StreamMetadataEncodingTest -vvv` + passed: 2 tests, 0 failed. +- `make check` passed: 173 tests, 0 failed. +- `powershell -ExecutionPolicy Bypass -File scripts\check.ps1` passed: + 173 tests, 0 failed. +- `git diff --check` passed. +- Markdown heading scan passed for the roadmap, Slither baseline, autonomous + run state, ADR 0006, status docs, known blockers, and test README. +- Traceability grep passed for `P0-META-001`, `StreamMetadataEncoding`, + dependency typehashes, dependency hash views, Slither detector rows, PR #70 + merge commit `350667fff6472e938790f0c7db5895fc3c4ddee9`, and CodeRabbit + final clean comment `4672928268`. +- Final Slither confirmation returned + `{"slither_exit":-1,"total":685,"high":8,"medium":28,"low":63,"informational":580,"optimization":6,"encode_packed_collision":0,"uninitialized_local":10,"calls_loop":8}`. +- GitHub CI passed on head `fd0b5b89d16fc0e42a839431fcae5e7edc3b399c` + in run `27297022773`. +- CodeRabbit comment `4673171581` confirmed the PR is correct and well-scoped, + with only non-blocking observations. +- Follow-up addressed the non-blocking NatSpec and zero-chunk test observations + by documenting the new public hash views and adding + `testEmptyDependencyContentHashIsDeterministic`. +- Follow-up `forge fmt --check smart-contracts\DependencyRegistry.sol + smart-contracts\StreamCore.sol test\StreamMetadataEncoding.t.sol` passed. +- Follow-up focused `forge test --match-contract StreamMetadataEncodingTest + -vvv` passed: 3 tests, 0 failed. +- Follow-up `make check` passed: 174 tests, 0 failed. +- Follow-up `powershell -ExecutionPolicy Bypass -File scripts\check.ps1` + passed: 174 tests, 0 failed. +- Follow-up Slither confirmation remained unchanged: + `{"slither_exit":-1,"total":685,"high":8,"medium":28,"low":63,"informational":580,"optimization":6,"encode_packed_collision":0,"uninitialized_local":10,"calls_loop":8}`. + +Review requests: + +- CodeRabbit requested in issue comment `4673145958`. +- CodeRabbit review comment `4673171581` reported the PR correct and + well-scoped; non-blocking observations were addressed in follow-up. +- Claude is intentionally skipped per current user instruction; use CodeRabbit + unless risk or future user instruction changes. + ## Decision Log | Time UTC | Decision | Rationale | @@ -2722,6 +2837,13 @@ Review requests: | 2026-06-10 17:52 | Address CodeRabbit PR #70 review | Add lifecycle interface request views, arRNG provider raw-word fulfillment event, stale zero-hash coverage, monotonic log helpers, retry-event documentation, and a defense-in-depth seed guard comment | | 2026-06-10 17:56 | Validate CodeRabbit PR #70 review response | Focused lifecycle/retry suites, full `make check`, Windows wrapper, formatting, diff hygiene, traceability, heading scan, and Slither baseline comparison all pass with 171 tests and unchanged high/medium counts | | 2026-06-10 17:58 | Mark PR #70 merge-ready by review evidence | GitHub CI passed on head `f8d0470b665eee2b528f95c380719014be639295`, CodeRabbit comment `4672884249` verified the fixes and marked the PR clean, and the stale aggregate pending context is documented as non-blocking | +| 2026-06-10 18:02 | Merge PR #70 | Raw-output hash storage merged as `350667fff6472e938790f0c7db5895fc3c4ddee9`; CI passed on final head `f52cd8f3cf83a8c131bdbc233c4769a4ba72e3fb`, CodeRabbit final clean comment `4672928268`, and issue #43 closed completed | +| 2026-06-10 18:05 | Select Queue Item 30 | Next open P0 Slither blocker is `P0-META-001`, a focused dependency-script `encode-packed-collision` fix with clear tests and low coupling to later metadata/freeze work | +| 2026-06-10 18:11 | Implement Queue Item 30 local draft | Added typed dependency chunk/content hashes, initialized `StreamCore` dependency script rendering, focused ambiguous-boundary tests, and Slither delta evidence showing `encode-packed-collision=0` | +| 2026-06-10 18:18 | Validate Queue Item 30 locally | Focused metadata tests, full `make check`, Windows wrapper, formatting, whitespace, heading scan, traceability grep, and final Slither confirmation pass with 173 tests and `encode-packed-collision=0` | +| 2026-06-10 18:20 | Open PR #71 | Dependency-script encoding hash fix published with full local validation evidence; CodeRabbit review will be requested on the PR-state head | +| 2026-06-10 18:21 | Request CodeRabbit PR #71 review | CodeRabbit review requested in issue comment `4673145958`; Claude intentionally skipped per current user instruction | +| 2026-06-10 18:27 | Address CodeRabbit PR #71 non-blocking observations | Added NatSpec for the new hash views, added zero-chunk dependency hash coverage, refreshed focused/full/Windows/Slither validation, and kept Slither counts unchanged | ## Resume Instructions diff --git a/ops/ROADMAP.md b/ops/ROADMAP.md index c310bf7d..18a09646 100644 --- a/ops/ROADMAP.md +++ b/ops/ROADMAP.md @@ -29,8 +29,10 @@ order. current custody models, P0-ADMIN-001 target-scoped function-admin checks cover the current protected-function surface, and P0-ADMIN-002 domain-scoped pause/emergency-recipient controls now have target-state - coverage. P0-RAND-001 randomizer request lifecycle and callback validation - now have target-state coverage for VRF and arRNG adapters. + coverage. P0-RAND-001 through P0-RAND-007 randomizer lifecycle, callback, + migration, failed-state, retry, and raw-output-hash work now have + target-state coverage for VRF and arRNG adapters. P0-META-001 dependency + script segment-safe encoding now has typed chunk/content hash coverage. - Public docs must describe actual on-chain behavior, not intended product behavior. @@ -38,7 +40,7 @@ order. | Field | Value | | --- | --- | -| Last verified | `2026-06-10 13:55 UTC` local Windows PR candidate validation; CI TBD | +| Last verified | `2026-06-10 18:18 UTC` local Windows PR candidate validation; CI TBD | | OS tested | Windows / Linux | | Foundry version | `v1.7.1` | | Solidity compiler version | `0.8.19` | @@ -51,9 +53,9 @@ order. | Area | Current status | Evidence | Required before public beta | | --- | --- | --- | --- | | Build | Passes with warnings when `forge` is invoked through the installed binary path | `forge build` | Build passes in CI and locally with warnings burned down or documented | -| Unit/integration tests | Tests cover admin guards, target-scoped function-admin permission regressions, domain-scoped pause controls, EIP-712/ERC-1271 drop authorization, auction custody and payment credits, fixed-price pull-payment credits, curator reward credits, current emergency-withdrawal boundaries, randomizer lifecycle/callback validation, and randomness/pending metadata behavior; broader P0/P1 tests are missing | `forge test -vvv` | P0 regression and integration suite exists | +| Unit/integration tests | Tests cover admin guards, target-scoped function-admin permission regressions, domain-scoped pause controls, EIP-712/ERC-1271 drop authorization, auction custody and payment credits, fixed-price pull-payment credits, curator reward credits, current emergency-withdrawal boundaries, randomizer lifecycle/callback validation, randomness/pending metadata behavior, raw-output hash storage, and dependency-script encoding hashes; broader P0/P1 tests are missing | `forge test -vvv` | P0 regression and integration suite exists | | Formatting | Fails broadly | `forge fmt --check smart-contracts` | Passing, or vendored exclusions documented | -| Static analysis | Runs with a tracked but unaccepted baseline: 686 total findings, including 9 High and 29 Medium | `slither . --config-file slither.config.json --foundry-compile-all` and `ops/SLITHER_BASELINE.md` | High/medium findings fixed, accepted, or documented | +| Static analysis | Runs with a tracked but unaccepted baseline: 685 total findings, including 8 High and 28 Medium | `slither . --config-file slither.config.json --foundry-compile-all` and `ops/SLITHER_BASELINE.md` | High/medium findings fixed, accepted, or documented | | Deployment | Missing | no meaningful `script/`/manifest process | Anvil deployment and fork rehearsal pass | | Docs | Partial README and roadmap only | manual inspection | Architecture, security, deployment, and protocol docs merged | | Release artifacts | Missing | no ABI/address/manifest release process | ABIs, manifests, checksums, and verified addresses published | @@ -1429,9 +1431,11 @@ Acceptance criteria: - Accept [`P1-META-ADR`](https://github.com/6529-Collections/6529Stream/issues/45) before metadata schema, freeze, dependency, burn, or ERC-4906 implementation. -- Resolve [`P0-META-001`](https://github.com/6529-Collections/6529Stream/issues/9) - before any production dependency-script output depends on dynamic chunk - composition. +- [`P0-META-001`](https://github.com/6529-Collections/6529Stream/issues/9) + now provides segment-safe dependency-script rendering plus typed chunk and + content hashes. Immutable dependency versions, provenance, registry identity, + and frozen collection pinning remain with + [`P1-META-003`](https://github.com/6529-Collections/6529Stream/issues/48). - Implement [`P1-META-001`](https://github.com/6529-Collections/6529Stream/issues/46): metadata schema and golden-file tests. - Implement [`P1-META-002`](https://github.com/6529-Collections/6529Stream/issues/47): @@ -1933,16 +1937,16 @@ Current capture: - Compiler: Solidity `0.8.19`. - Command: `slither . --config-file slither.config.json --foundry-compile-all --json `. - Status: baseline captured, not accepted as a CI gate. -- Result: 686 findings, including 9 High and 29 Medium. +- Result: 685 findings, including 8 High and 28 Medium. Impact summary: | Impact | Count | | --- | ---: | -| High | 9 | -| Medium | 29 | -| Low | 64 | -| Informational | 578 | +| High | 8 | +| Medium | 28 | +| Low | 63 | +| Informational | 580 | | Optimization | 6 | High/medium detector summary: @@ -1950,7 +1954,7 @@ High/medium detector summary: | Detector | Impact | Count | Primary scope | Status | Issue | Required action | | --- | --- | ---: | --- | --- | --- | --- | | `arbitrary-send-eth` | High | 0 current / 4 fixed | first-party emergency withdrawals | Fixed | [#8](https://github.com/6529-Collections/6529Stream/issues/8) | Current emergency-withdrawal surfaces are bounded: auction, fixed-price drops, curator pool, StreamMinter surplus, and conservative randomizer reserve boundary tests now exist | -| `encode-packed-collision` | High | 1 current / 2 fixed | drop authorization and dependency/script hashing | Open | [#9](https://github.com/6529-Collections/6529Stream/issues/9), [#10](https://github.com/6529-Collections/6529Stream/issues/10) | Drop authorization rows are fixed; replace dependency-script packed concatenation with typed/domain-separated encoding under `P0-META-001` | +| `encode-packed-collision` | High | 0 current / 3 fixed | drop authorization and dependency/script hashing | Fixed | [#9](https://github.com/6529-Collections/6529Stream/issues/9), [#10](https://github.com/6529-Collections/6529Stream/issues/10) | Drop authorization rows and dependency-script segment hashing are fixed; keep typed hash tests and Slither baseline traceability | | `incorrect-exp` | High | 1 | vendored `Math.mulDiv` | Needs Issue | [#11](https://github.com/6529-Collections/6529Stream/issues/11) | Confirm likely false positive against pinned upstream or replace vendored library | | `reentrancy-eth` | High | 0 current / 1 fixed | auction bidding | Fixed | [#12](https://github.com/6529-Collections/6529Stream/issues/12) | Replaced bid-path push refunds with bidder pull credits and state-before-withdrawal flow | | `suicidal` | High | 3 | test-only forced-ETH helpers | Accepted | Accepted test-only | Intentionally retained for forced-ETH accounting tests under Solidity 0.8.19 | @@ -1959,7 +1963,7 @@ High/medium detector summary: | `divide-before-multiply` | Medium | 9 | vendored math/base64 helpers | Needs Issue | [#11](https://github.com/6529-Collections/6529Stream/issues/11) | Confirm likely false positive against pinned upstream or replace vendored library | | `incorrect-equality` | Medium | 1 | test-only malleable-signature helper | Accepted | Accepted test-only | Keep scoped to test-only EIP-712 negative coverage | | `locked-ether` | Medium | 7 | test-only rejection/reentrancy/mock receivers | Accepted | Accepted test-only | Keep scoped to payment and emergency-withdrawal tests | -| `uninitialized-local` | Medium | 11 current / 1 fixed | first-party and test helper locals | Open for remaining production rows; `StreamDrops.mintDrop` fixed in `P0-AUTH-002` | [#15](https://github.com/6529-Collections/6529Stream/issues/15) | Initialize or prove Solidity zero-value intent | +| `uninitialized-local` | Medium | 10 current / 2 fixed | first-party and test helper locals | Open for remaining production rows; `StreamDrops.mintDrop` fixed in `P0-AUTH-002` and `StreamCore.retrieveDependencyScript` fixed in `P0-META-001` | [#15](https://github.com/6529-Collections/6529Stream/issues/15), [#9](https://github.com/6529-Collections/6529Stream/issues/9) | Initialize or prove Solidity zero-value intent | | `unused-return` | Medium | 1 | ERC-1271 test tuple helper | Accepted | Accepted test-only | Keep scoped to test-only assertion helper | ## Appendix B: Test Matrix @@ -1999,7 +2003,7 @@ Status values: `Missing`, `Planned`, `In Progress`, `Passing`, `Blocked`. | Collection freeze boundary | Frozen collections cannot mutate collection fields, base URI, metadata mode, scripts, dependency references, token data, image, attributes, final supply, or live-token metadata state | `test/StreamMetadataFreeze.t.sol` | Missing | [`P1-META-002`](https://github.com/6529-Collections/6529Stream/issues/47) | Gate D | TBD | | Dependency registry immutability | Dependency versions are immutable, pinned by key/version/content hash, and cannot change frozen collection output | `test/StreamDependencyRegistry.t.sol` | Missing | [`P1-META-003`](https://github.com/6529-Collections/6529Stream/issues/48) | Gate D | TBD | | ERC-4906 metadata signaling | `supportsInterface(0x49064906)` succeeds and `MetadataUpdate` / `BatchMetadataUpdate` emit only when token JSON metadata changes | `test/StreamMetadataEvents.t.sol` | Missing | [`P1-META-004`](https://github.com/6529-Collections/6529Stream/issues/49) | Gate D | TBD | -| Dependency script packed encoding | Dependency script retrieval uses safe typed concatenation/hash encoding and cannot collide across script segments | `test/StreamMetadataEncoding.t.sol` | Missing | [`P0-META-001`](https://github.com/6529-Collections/6529Stream/issues/9), [`P1-META-003`](https://github.com/6529-Collections/6529Stream/issues/48) | Gate C/Gate D | TBD | +| Dependency script packed encoding | Dependency script retrieval uses safe typed concatenation/hash encoding and cannot collide across script segments | `test/StreamMetadataEncoding.t.sol` | Passing: typed chunk/content hashes include dependency key, chunk count, chunk index, chunk byte length, and chunk content hash; ambiguous chunk splits that render the same JavaScript produce distinct content hashes while preserving rendered-script compatibility; zero-chunk dependency hashes are deterministic | [`P0-META-001`](https://github.com/6529-Collections/6529Stream/issues/9), [`P1-META-003`](https://github.com/6529-Collections/6529Stream/issues/48) | Gate C/Gate D | TBD | | Deployment redeployment rehearsal | Deployment manifests, ABI hashes, admin ceremony, signer setup, deprecation checks, and emergency redeployment rehearsal follow ADR 0007 | `test/StreamDeploymentManifest.t.sol` and `script/RehearseDeployment.s.sol` | Missing | [`P2-UPGRADE-ADR`](https://github.com/6529-Collections/6529Stream/issues/53) | Gate E/Gate G | TBD | | Mint-accounting state | Mint counters initialize and update according to the accepted drop/mint accounting design | `test/StreamMintAccounting.t.sol` | Missing | [`P0-CORE-001`](https://github.com/6529-Collections/6529Stream/issues/13) | Gate C | TBD | | Uninitialized local findings | First-party default-local behavior is explicit, removed, or covered by targeted regressions | `test/StreamInitialization.t.sol` | Missing | [`P0-INIT-001`](https://github.com/6529-Collections/6529Stream/issues/15) | Gate C | TBD | diff --git a/ops/SLITHER_BASELINE.md b/ops/SLITHER_BASELINE.md index 39b4007f..0458260c 100644 --- a/ops/SLITHER_BASELINE.md +++ b/ops/SLITHER_BASELINE.md @@ -8,7 +8,7 @@ input, not an accepted security baseline. | Field | Value | | --- | --- | | Status | Open baseline; not accepted as a CI gate | -| Last generated | `2026-06-10 13:55 UTC` | +| Last generated | `2026-06-10 18:18 UTC` | | Slither | `0.11.5` | | Solidity compiler | `0.8.19` | | solc-select | `1.2.0` | @@ -23,18 +23,18 @@ baseline. | Impact | Count | | --- | ---: | -| High | 9 | -| Medium | 29 | -| Low | 64 | -| Informational | 578 | +| High | 8 | +| Medium | 28 | +| Low | 63 | +| Informational | 580 | | Optimization | 6 | -| Total | 686 | +| Total | 685 | ## Detector Counts | Detector | Impact | Count | | --- | --- | ---: | -| `encode-packed-collision` | High | 1 | +| `encode-packed-collision` | High | 0 | | `incorrect-exp` | High | 1 | | `suicidal` | High | 3 | | `uninitialized-state` | High | 2 | @@ -42,27 +42,27 @@ baseline. | `divide-before-multiply` | Medium | 9 | | `incorrect-equality` | Medium | 1 | | `locked-ether` | Medium | 7 | -| `uninitialized-local` | Medium | 11 | +| `uninitialized-local` | Medium | 10 | | `unused-return` | Medium | 1 | -| Low-impact findings | Low | 64 | -| Informational findings | Informational | 578 | +| Low-impact findings | Low | 63 | +| Informational findings | Informational | 580 | | Optimization findings | Optimization | 6 | -Randomizer-lifecycle delta from the previous pause-control capture: - -- High, medium, and optimization counts are unchanged. -- Low findings increased by 3 and informational findings increased by 7 due to - shared randomizer lifecycle storage, events, views, request-state tests, and - low-level negative-call coverage. -- `NextGenRandomizerRNG.requestRandomWords` uses a scoped Slither suppression - for the guarded arRNG request-ID pattern: the provider returns the request ID - from an external payable call, state is recorded immediately after, and - `test/StreamRandomizerLifecycle.t.sol` proves a reentrant controller cannot - fulfill during that window. -- `arbitrary-send-eth` remains at zero findings. -- The only medium row whose description mentions randomizer-provider payment is - still the test-only `MockArrngController` `locked-ether` row in - `test/StreamEmergencyWithdraw.t.sol`; it is accepted as harness-only noise. +Dependency-script encoding delta from the previous tracked capture: + +- High findings decreased from 9 to 8 because the final + `encode-packed-collision` row is fixed. +- Medium findings decreased from 29 to 28 because + `StreamCore.retrieveDependencyScript(uint256).scripttext` is now initialized. +- `encode-packed-collision` is now zero current findings; the remaining fixed + rows are kept below as audit traceability. +- `uninitialized-local` is now 10 current findings; the + `StreamDrops.mintDrop` and `StreamCore.retrieveDependencyScript` rows are + fixed, while the broader `P0-INIT-001` workstream remains open. +- `arbitrary-send-eth` and `reentrancy-eth` remain at zero findings. +- Slither still exits non-zero because the remaining tracked baseline findings + require fixes, accepted-risk rationale, or false-positive proof before audit + readiness. ## Status Semantics @@ -86,7 +86,7 @@ GitHub work item that owns that resolution. | `arbitrary-send-eth` | 1 | `NextGenRandomizerRNG` | `emergencyWithdraw()` | first-party | Fixed in `P0-PAY-007`/`P0-PAY-008` | High | Medium | Fixed | Treats all adapter ETH as randomness reserve, exposes zero emergency-withdrawable balance, and transfers no emergency-withdrawable ETH | `test/StreamEmergencyWithdraw.t.sol` | [`P0-PAY-008`](https://github.com/6529-Collections/6529Stream/issues/8) | Gate C | TBD | | `arbitrary-send-eth` | 1 | `StreamCuratorsPool` | `emergencyWithdraw()` | first-party | Fixed in `P0-PAY-005` | High | Medium | Fixed | Bounded curator pool emergency withdrawal to surplus after local curator credits owed | `test/StreamCuratorsPool.t.sol` | [`P0-PAY-008`](https://github.com/6529-Collections/6529Stream/issues/8), [`P0-PAY-005`](https://github.com/6529-Collections/6529Stream/issues/29) | Gate C | TBD | | `arbitrary-send-eth` | 1 | `StreamMinter` | `emergencyWithdraw()` | first-party | Fixed in `P0-PAY-007`/`P0-PAY-008` | High | Medium | Fixed | Exposes `totalOwed() == 0` and withdraws only `emergencyWithdrawable()` surplus, covering forced ETH without an ordinary payable path | `test/StreamEmergencyWithdraw.t.sol` | [`P0-PAY-008`](https://github.com/6529-Collections/6529Stream/issues/8) | Gate C | TBD | -| `encode-packed-collision` | 1 | `StreamCore` | `retrieveDependencyScript(uint256)` | first-party | `smart-contracts/StreamCore.sol#L402-L408` | High | High | Open | Use typed dependency chunk encoding, versioned content hashes, and frozen dependency pinning per ADR 0006 | Encoding collision and frozen dependency regression | [`P0-META-001`](https://github.com/6529-Collections/6529Stream/issues/9) | Gate C | TBD | +| `encode-packed-collision` | 1 | `StreamCore` | `retrieveDependencyScript(uint256)` | first-party | Fixed in `P0-META-001` | High | High | Fixed | Replaced packed dynamic dependency-script composition with initialized `string.concat` rendering and typed dependency chunk/content hash views that use `abi.encode`, dependency key, chunk count, chunk index, chunk byte length, and per-chunk content hash | Ambiguous chunk-boundary and typed hash regressions in `test/StreamMetadataEncoding.t.sol` | [`P0-META-001`](https://github.com/6529-Collections/6529Stream/issues/9) | Gate C | TBD | | `encode-packed-collision` | 1 | `StreamDrops` | `retrieveMessageAndDropID(address,address,string,uint256,uint256,uint256,uint256)` | first-party | Removed in `P0-AUTH-002` | High | High | Fixed | Removed legacy packed helper; `hashDropAuthorization` now uses EIP-712 domain-separated typed data | Explicit digest, replay, wrong-domain, wrong-chain, wrong-contract, and field-substitution tests in `test/StreamDropsEIP712.t.sol` | [`P0-AUTH-002`](https://github.com/6529-Collections/6529Stream/issues/10) | Gate C | TBD | | `encode-packed-collision` | 1 | `StreamDrops` | `mintDrop(address,address,string,uint256,uint256,uint256,uint256)` | first-party | Removed in `P0-AUTH-002` | High | High | Fixed | Replaced legacy packed-hash `mintDrop` ABI with `mintDrop(DropAuthorization,string,bytes)` and storage-backed consumed/cancelled drop IDs | EOA, EIP-2098, replay, expiry, cancellation, stale-epoch, wrong-domain, wrong-chain, wrong-contract, wrong-signer, malleability, zero signer, bad quantity, and token-substitution tests in `test/StreamDropsEIP712.t.sol` | [`P0-AUTH-002`](https://github.com/6529-Collections/6529Stream/issues/10) | Gate C | TBD | | `incorrect-exp` | 1 | `Math` | `mulDiv(uint256,uint256,uint256)` | vendored | `smart-contracts/Math.sol#L55-L134` | High | Medium | Needs Issue | Likely false positive; confirm against pinned upstream OpenZeppelin or replace retained library with package-managed upstream before acceptance | Library provenance or math regression | [`P0-LIB-001`](https://github.com/6529-Collections/6529Stream/issues/11) | Gate F | TBD | @@ -108,7 +108,7 @@ GitHub work item that owns that resolution. | `uninitialized-local` | 1 | `DelegationManagementContract` | `retrieveSubDelegationStatus(...).subdelegationRights` | first-party | `smart-contracts/NFTdelegation.sol#L650` | Medium | Medium | Open | Initialize local before use or prove Solidity zero-value intent with tests/docs | Targeted regression for affected function | [`P0-INIT-001`](https://github.com/6529-Collections/6529Stream/issues/15) | Gate C | TBD | | `uninitialized-local` | 1 | `DelegationManagementContract` | `retrieveStatusOfActiveDelegator(...).status` | first-party | `smart-contracts/NFTdelegation.sol#L677` | Medium | Medium | Open | Initialize local before use or prove Solidity zero-value intent with tests/docs | Targeted regression for affected function | [`P0-INIT-001`](https://github.com/6529-Collections/6529Stream/issues/15) | Gate C | TBD | | `uninitialized-local` | 1 | `StreamCore` | `retrieveGenerativeScript(...).scripttext` | first-party | `smart-contracts/StreamCore.sol#L394` | Medium | Medium | Open | Initialize local before use or prove Solidity zero-value intent with tests/docs | Targeted regression for affected function | [`P0-INIT-001`](https://github.com/6529-Collections/6529Stream/issues/15) | Gate C | TBD | -| `uninitialized-local` | 1 | `StreamCore` | `retrieveDependencyScript(...).scripttext` | first-party | `smart-contracts/StreamCore.sol#L403` | Medium | Medium | Open | Initialize local before use or prove Solidity zero-value intent with tests/docs | Targeted regression for affected function | [`P0-INIT-001`](https://github.com/6529-Collections/6529Stream/issues/15) | Gate C | TBD | +| `uninitialized-local` | 1 | `StreamCore` | `retrieveDependencyScript(...).scripttext` | first-party | Fixed in `P0-META-001` | Medium | Medium | Fixed | Initialized the dependency-script accumulator to an empty string before concatenation and covered the rendered output through the metadata encoding regression suite | `test/StreamMetadataEncoding.t.sol` | [`P0-INIT-001`](https://github.com/6529-Collections/6529Stream/issues/15), [`P0-META-001`](https://github.com/6529-Collections/6529Stream/issues/9) | Gate C | TBD | | `uninitialized-local` | 1 | `StreamDrops` | `mintDrop(...).tokenid` | first-party | Removed in `P0-AUTH-002` | Medium | Medium | Fixed | Rewritten typed authorization path initializes branch locals explicitly; captured Slither run no longer reports `StreamDrops.mintDrop` uninitialized locals | `make slither` targeted log check plus EIP-712 and characterization tests | [`P0-INIT-001`](https://github.com/6529-Collections/6529Stream/issues/15) | Gate C | TBD | | `uninitialized-local` | 1 | `StreamMinter` | `mint(...).mintIndex` | first-party | `smart-contracts/StreamMinter.sol#L76` | Medium | Medium | Open | Initialize local before use or prove Solidity zero-value intent with tests/docs | Targeted regression for affected function | [`P0-INIT-001`](https://github.com/6529-Collections/6529Stream/issues/15) | Gate C | TBD | | `uninitialized-local` | 1 | `MockStreamMinter` | `mint(...).mintedCount` | test-only | `test/mocks/MockStreamMinter.sol#L71` | Medium | Medium | Accepted | Accepted as a test-only helper baseline | None; test-only baseline row | Accepted test-only | Gate A | TBD | diff --git a/smart-contracts/DependencyRegistry.sol b/smart-contracts/DependencyRegistry.sol index 715c8f73..ac53f2c3 100644 --- a/smart-contracts/DependencyRegistry.sol +++ b/smart-contracts/DependencyRegistry.sol @@ -13,6 +13,13 @@ pragma solidity ^0.8.19; import "./IStreamAdmins.sol"; contract DependencyRegistry { + bytes32 public constant DEPENDENCY_SCRIPT_CONTENT_TYPEHASH = keccak256( + "6529StreamDependencyScript(bytes32 dependencyNameAndVersion,uint256 chunkCount,bytes32 chunksHash)" + ); + bytes32 public constant DEPENDENCY_SCRIPT_CHUNK_TYPEHASH = keccak256( + "6529StreamDependencyScriptChunk(uint256 index,bytes32 chunkHash,uint256 byteLength)" + ); + // struct that holds a collection's info struct dependencyInfoStructure { bytes32 _collectionDependencyName; @@ -84,4 +91,55 @@ contract DependencyRegistry { { return (dependencyInfo[dependencyNameAndVersion].libraryScript[index]); } + + /// @notice Returns the typed hash of one dependency script chunk. + /// @param dependencyNameAndVersion Dependency key currently stored in the registry. + /// @param index Chunk index inside the dependency script. + /// @return The chunk hash, bound to chunk index, chunk byte length, and chunk contents. + function getDependencyScriptChunkHash(bytes32 dependencyNameAndVersion, uint256 index) + public + view + returns (bytes32) + { + return _hashDependencyScriptChunk( + index, dependencyInfo[dependencyNameAndVersion].libraryScript[index] + ); + } + + /// @notice Returns the typed content hash for the current dependency script chunks. + /// @param dependencyNameAndVersion Dependency key currently stored in the registry. + /// @return The content hash for the current chunk sequence under the dependency key. + function getDependencyScriptContentHash(bytes32 dependencyNameAndVersion) + external + view + returns (bytes32) + { + uint256 chunkCount = dependencyInfo[dependencyNameAndVersion].libraryScript.length; + bytes32 chunksHash = bytes32(0); + + for (uint256 i = 0; i < chunkCount; i++) { + chunksHash = keccak256( + abi.encode(chunksHash, getDependencyScriptChunkHash(dependencyNameAndVersion, i)) + ); + } + + return keccak256( + abi.encode( + DEPENDENCY_SCRIPT_CONTENT_TYPEHASH, dependencyNameAndVersion, chunkCount, chunksHash + ) + ); + } + + function _hashDependencyScriptChunk(uint256 index, string memory chunk) + private + pure + returns (bytes32) + { + bytes memory chunkBytes = bytes(chunk); + return keccak256( + abi.encode( + DEPENDENCY_SCRIPT_CHUNK_TYPEHASH, index, keccak256(chunkBytes), chunkBytes.length + ) + ); + } } diff --git a/smart-contracts/IDependencyRegistry.sol b/smart-contracts/IDependencyRegistry.sol index d5c36b5a..b1f3bb37 100644 --- a/smart-contracts/IDependencyRegistry.sol +++ b/smart-contracts/IDependencyRegistry.sol @@ -2,10 +2,24 @@ pragma solidity ^0.8.19; -interface IDependencyRegistry { +interface IDependencyRegistry { + function getDependencyScriptCount(bytes32 dependencyNameAndVersion) + external + view + returns (uint256); - function getDependencyScriptCount(bytes32 dependencyNameAndVersion ) external view returns (uint256); + function getDependencyScript(bytes32 dependencyNameAndVersion, uint256 index) + external + view + returns (string memory); - function getDependencyScript(bytes32 dependencyNameAndVersion, uint256 index) external view returns (string memory); + function getDependencyScriptChunkHash(bytes32 dependencyNameAndVersion, uint256 index) + external + view + returns (bytes32); -} \ No newline at end of file + function getDependencyScriptContentHash(bytes32 dependencyNameAndVersion) + external + view + returns (bytes32); +} diff --git a/smart-contracts/StreamCore.sol b/smart-contracts/StreamCore.sol index 4ceec066..08c9638c 100644 --- a/smart-contracts/StreamCore.sol +++ b/smart-contracts/StreamCore.sol @@ -719,26 +719,32 @@ contract StreamCore is ERC721Enumerable, ERC2981, Ownable { // function to retrieve on-chain dependency script function retrieveDependencyScript(uint256 tokenId) private view returns (string memory) { - string memory scripttext; + uint256 collectionId = tokenIdsToCollectionIds[tokenId]; + bytes32 dependencyNameAndVersion = collectionInfo[collectionId].collectionDependencyScript; + string memory scripttext = ""; for ( uint256 i = 0; - i - < dependencyRegistry.getDependencyScriptCount( - collectionInfo[tokenIdsToCollectionIds[tokenId]].collectionDependencyScript - ); + i < dependencyRegistry.getDependencyScriptCount(dependencyNameAndVersion); i++ ) { - scripttext = string( - abi.encodePacked( - scripttext, - dependencyRegistry.getDependencyScript( - collectionInfo[tokenIdsToCollectionIds[tokenId]].collectionDependencyScript, - i - ) - ) + scripttext = string.concat( + scripttext, dependencyRegistry.getDependencyScript(dependencyNameAndVersion, i) ); } - return string(abi.encodePacked(scripttext)); + return scripttext; + } + + /// @notice Returns the current typed dependency script content hash for a minted token. + /// @dev This is a current registry-content hash, not a freeze manifest or immutable + /// dependency-version proof. + /// @param tokenId Minted token whose collection dependency key should be resolved. + /// @return The dependency script content hash currently reported by the registry. + function retrieveDependencyScriptContentHash(uint256 tokenId) public view returns (bytes32) { + _requireMinted(tokenId); + uint256 collectionId = tokenIdsToCollectionIds[tokenId]; + return dependencyRegistry.getDependencyScriptContentHash( + collectionInfo[collectionId].collectionDependencyScript + ); } // function to retrieve the supply of a collection diff --git a/test/README.md b/test/README.md index bec0797c..1777e70c 100644 --- a/test/README.md +++ b/test/README.md @@ -119,3 +119,10 @@ only the retry-specific failure event, repeated deterministic failures remain bo `MAX_RANDOMNESS_POST_PROCESSING_RETRIES`, unauthorized retry fails, terminal fulfillment cannot be retried, and changed token-to-collection, provider, or epoch bindings fail before retry state changes in both adapters. + +Dependency script encoding now has P0-META-001 target-state coverage in +`StreamMetadataEncoding.t.sol`: ambiguous chunk boundaries that render the same +dependency JavaScript produce distinct typed content hashes, chunk hashes include +the chunk index and byte length, zero-chunk dependency hashes are deterministic, +and the existing rendered generative script output remains +compatibility-preserving. diff --git a/test/StreamMetadataEncoding.t.sol b/test/StreamMetadataEncoding.t.sol new file mode 100644 index 00000000..a0b16842 --- /dev/null +++ b/test/StreamMetadataEncoding.t.sol @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "../smart-contracts/Strings.sol"; +import "./helpers/Assertions.sol"; +import "./helpers/CharacterizationTestBase.sol"; +import "./helpers/StreamFixture.sol"; + +contract StreamMetadataEncodingTest is CharacterizationTestBase, StreamFixture { + using Assertions for bool; + using Assertions for bytes32; + using Assertions for string; + using Strings for uint256; + + bytes32 private constant DEPENDENCY_SCRIPT_CONTENT_TYPEHASH = keccak256( + "6529StreamDependencyScript(bytes32 dependencyNameAndVersion,uint256 chunkCount,bytes32 chunksHash)" + ); + bytes32 private constant DEPENDENCY_SCRIPT_CHUNK_TYPEHASH = keccak256( + "6529StreamDependencyScriptChunk(uint256 index,bytes32 chunkHash,uint256 byteLength)" + ); + + function testDependencyScriptHashSeparatesAmbiguousChunkBoundaries() public { + DeployedStream memory deployed = deployStream(address(0xBEEF), address(0xCAFE)); + bytes32 dependencyKey = bytes32(0); + uint256 tokenId = 10_000_000_000; + string[] memory firstChunks = new string[](2); + firstChunks[0] = "ab"; + firstChunks[1] = "c"; + string[] memory secondChunks = new string[](2); + secondChunks[0] = "a"; + secondChunks[1] = "bc"; + + deployed.dependencyRegistry.addDependency(dependencyKey, firstChunks); + + vm.prank(address(deployed.minter)); + deployed.core.mint(tokenId, address(0xA11CE), "1,2,3", 7, 1); + + string memory firstRenderedScript = deployed.core.retrieveGenerativeScript(tokenId); + bytes32 firstContentHash = deployed.core.retrieveDependencyScriptContentHash(tokenId); + firstRenderedScript.assertEq( + _expectedGenerativeScript( + tokenId, keccak256(abi.encode(uint256(1), tokenId, uint256(7))), "abc" + ), + "first rendered script" + ); + firstContentHash.assertEq(_contentHash(dependencyKey, firstChunks), "first content hash"); + + deployed.dependencyRegistry.addDependency(dependencyKey, secondChunks); + + deployed.core.retrieveGenerativeScript(tokenId) + .assertEq(firstRenderedScript, "rendered script compatibility changed"); + bytes32 secondContentHash = deployed.core.retrieveDependencyScriptContentHash(tokenId); + secondContentHash.assertEq(_contentHash(dependencyKey, secondChunks), "second content hash"); + (firstContentHash == secondContentHash) + .assertFalse("ambiguous dependency chunks shared hash"); + } + + function testDependencyChunkHashIncludesIndexAndLength() public { + DeployedStream memory deployed = deployStream(address(0xBEEF), address(0xCAFE)); + bytes32 dependencyKey = keccak256("empty-chunks"); + string[] memory chunks = new string[](2); + chunks[0] = ""; + chunks[1] = ""; + + deployed.dependencyRegistry.addDependency(dependencyKey, chunks); + + bytes32 firstChunkHash = + deployed.dependencyRegistry.getDependencyScriptChunkHash(dependencyKey, 0); + bytes32 secondChunkHash = + deployed.dependencyRegistry.getDependencyScriptChunkHash(dependencyKey, 1); + firstChunkHash.assertEq(_chunkHash(0, chunks[0]), "first empty chunk hash"); + secondChunkHash.assertEq(_chunkHash(1, chunks[1]), "second empty chunk hash"); + (firstChunkHash == secondChunkHash).assertFalse("empty chunks shared hash"); + } + + function testEmptyDependencyContentHashIsDeterministic() public { + DeployedStream memory deployed = deployStream(address(0xBEEF), address(0xCAFE)); + bytes32 dependencyKey = keccak256("zero-chunk-dependency"); + string[] memory chunks = new string[](0); + + deployed.dependencyRegistry.addDependency(dependencyKey, chunks); + + deployed.dependencyRegistry.getDependencyScriptContentHash(dependencyKey) + .assertEq(_contentHash(dependencyKey, chunks), "zero chunk content hash"); + } + + function _contentHash(bytes32 dependencyKey, string[] memory chunks) + private + pure + returns (bytes32) + { + bytes32 chunksHash = bytes32(0); + + for (uint256 i = 0; i < chunks.length; i++) { + chunksHash = keccak256(abi.encode(chunksHash, _chunkHash(i, chunks[i]))); + } + + return keccak256( + abi.encode(DEPENDENCY_SCRIPT_CONTENT_TYPEHASH, dependencyKey, chunks.length, chunksHash) + ); + } + + function _chunkHash(uint256 index, string memory chunk) private pure returns (bytes32) { + bytes memory chunkBytes = bytes(chunk); + return keccak256( + abi.encode( + DEPENDENCY_SCRIPT_CHUNK_TYPEHASH, index, keccak256(chunkBytes), chunkBytes.length + ) + ); + } + + function _expectedGenerativeScript(uint256 tokenId, bytes32 tokenHash, string memory dependency) + private + pure + returns (string memory) + { + return string.concat( + "let hash='", + Strings.toHexString(uint256(tokenHash), 32), + "';let tokenId=", + tokenId.toString(), + ";let tokenData=[1,2,3]", + ";let dependencyScript='", + dependency, + "';", + "function draw(){}" + ); + } +}