Skip to content

Replace drop authorization with EIP-712#56

Merged
punk6529 merged 4 commits into
mainfrom
codex/eip712-drop-authorization
Jun 10, 2026
Merged

Replace drop authorization with EIP-712#56
punk6529 merged 4 commits into
mainfrom
codex/eip712-drop-authorization

Conversation

@punk6529

@punk6529 punk6529 commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Replaces the legacy signer-only packed-hash StreamDrops.mintDrop path with EIP-712 DropAuthorization typed data.
  • Adds domain-separated hashing, derived drop IDs, consumed/cancelled drop ID storage, signer epoch rotation, and per-drop cancellation.
  • Supports EOA signatures and EIP-2098 compact signatures with low-s, valid-v, and zero-recovered-signer checks.
  • Explicitly rejects contract signer addresses until ERC-1271 support lands in P0-AUTH-003 / [P0-AUTH-003] Implement ERC-1271 contract signer support #19.
  • Removes retrieveMessageAndDropID and the old mintDrop(address,address,string,uint256,uint256,uint256,uint256) execution surface.
  • Updates characterization/integration tests, adds the EIP-712 target-state suite, and refreshes roadmap, ADR, blockers, and Slither baseline rows.

Closes #10.

Review Follow-up

  • Adds an Already cancelled guard so cancelDrop cannot emit duplicate cancellation events.
  • Adds lifecycle event assertions for authorization consumption, cancellation, and signer epoch rotation.
  • Adds sale-mode negative tests for free fixed-price payer, auction payer, fixed-price auction reserve/end fields, and auction price misuse.

Validation

  • forge test --match-contract StreamDropsEIP712Test -vvv passed with 23 tests.
  • forge test --match-contract StreamAdminsTest -vvv passed with 4 tests.
  • make check passed with 47 tests and the known Solidity warning baseline.
  • powershell -ExecutionPolicy Bypass -File scripts\check.ps1 passed with 47 tests and the known Solidity warning baseline.
  • forge fmt --check smart-contracts\StreamDrops.sol test\StreamAdmins.t.sol test\StreamDropsCharacterization.t.sol test\StreamDropsIntegrationCharacterization.t.sol test\StreamDropsEIP712.t.sol test\helpers\CharacterizationTestBase.sol test\helpers\DropAuthTestHelper.sol passed.
  • git diff --check and git diff --cached --check passed.
  • rg -n "retrieveMessageAndDropID|mintDrop\\(address,address,string|dropExecuted|MintsToTxOrigin|tx\\.origin" smart-contracts test returned no matches.
  • make slither returned non-zero as expected because unrelated baseline findings remain; targeted log check confirmed the old StreamDrops packed-hash findings are gone, only StreamCore.retrieveDependencyScript remains under encode-packed-collision, and StreamDrops.mintDrop no longer appears under uninitialized-local.

Notes

Summary by CodeRabbit

  • New Features

    • Drop execution now uses EIP-712 signature authorizations (including compact signatures); legacy mint route removed; drops can be consumed/cancelled and signer epochs managed by admins.
  • Documentation

    • Clarified authorization design, phased rollout, and explicit deferral of contract-signer (ERC-1271) support until a future milestone.
  • Tests

    • Added extensive EIP-712-focused unit/integration tests and new test helpers covering valid flows, many failure modes, signature formats, and integration scenarios.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Code review skipped — your organization's overage spend limit has been reached.

Code review is billed via overage credits. To resume reviews, an organization admin can raise the monthly limit at claude.ai/admin-settings/claude-code.

Once credits are available, push a new commit or reopen this pull request to trigger a review.

@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0e9c3d9e-25fc-4e8b-ad79-d5c2618b062e

📥 Commits

Reviewing files that changed from the base of the PR and between 8dc9eaa and 89af059.

📒 Files selected for processing (5)
  • ops/AUTONOMOUS_RUN.md
  • ops/ROADMAP.md
  • smart-contracts/StreamDrops.sol
  • test/StreamDropsEIP712.t.sol
  • test/helpers/CharacterizationTestBase.sol
✅ Files skipped from review due to trivial changes (2)
  • ops/AUTONOMOUS_RUN.md
  • ops/ROADMAP.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • test/helpers/CharacterizationTestBase.sol
  • test/StreamDropsEIP712.t.sol

📝 Walkthrough

Walkthrough

Implements EIP-712 DropAuthorization in StreamDrops: signature-based minting (EOA + EIP-2098), domain-separated hashing, consumed/cancelled drop storage, signer-epoch rotation, admin epoch/cancel controls, removal of legacy packed helper, and extensive test/integration migrations validating positive and negative EIP-712 flows.

Changes

EIP-712 Drop Authorization

Layer / File(s) Summary
EIP-712 contract design and authorization data model
smart-contracts/StreamDrops.sol
Introduces EIP-712 constants and DropAuthorization struct, signerEpoch, consumed/cancelled drop mappings, and lifecycle events.
Signature verification and authorization validation logic
smart-contracts/StreamDrops.sol
mintDrop() accepts DropAuthorization, tokenData, and signature; computes EIP-712 digest, recovers EOA signer (supports 65-byte and EIP-2098 64-byte), enforces epoch/deadline/dropId/sale-mode/quantity/payer constraints, marks drop consumed, emits events, and executes fixed-price or auction paths.
Signer lifecycle and admin controls
smart-contracts/StreamDrops.sol
Constructor requires non-zero tdhSigner and initializes signerEpoch; removes legacy authorized modifier; adds admin functions incrementSignerEpoch() and cancelDrop() and emits signer/epoch events.
EIP-712 helpers and view APIs
smart-contracts/StreamDrops.sol
Adds domainSeparator, deriveDropId, hashDropAuthorization, isDropConsumed, isDropCancelled and removes legacy retrieveMessageAndDropID.
Test infra and helpers
test/helpers/CharacterizationTestBase.sol, test/helpers/DropAuthTestHelper.sol
Adds Vm cheatcodes (addr, chainId, sign) and DropAuthTestHelper to build fixed-price/auction DropAuthorization structs and produce standard, compact (EIP-2098), and malleable signatures for testing.
Comprehensive EIP-712 tests
test/StreamDropsEIP712.t.sol
New suite verifies explicit EIP-712 digest encoding, EOA and compact-signature success, and negative cases (wrong signer/domain/chain/expiry/cancelled/stale epoch/wrong dropId/invalid quantity/token-data/malleability/format/payer binding); includes ContractSignerStub for contract-signer rejection tests.
Characterization test migration
test/StreamDropsCharacterization.t.sol
Refactors to inherit DropAuthTestHelper; adds deployDrops() helper; adds domainSeparator and deriveDropId assertions; rewrites fixed-price/auction and rejection tests to use authorization+signature flows.
Integration test migration & executor wiring
test/StreamDropsIntegrationCharacterization.t.sol
Migrates integration tests to authorization/signature calls, updates low-level call patterns, auction/no-bid settlement flows, and changes AuthorizedDropExecutor.mintFixedPrice to forward (DropAuthorization, tokenData, signature) calldata to drops.mintDrop{value:msg.value}.
Admin test selector fix
test/StreamAdmins.t.sol
Updates selector derivation to use StreamAdmins.registerAdmin.selector.
Docs, roadmap, baselines, runbook
docs/adr/0001-drop-authorization.md, docs/known-blockers.md, ops/ROADMAP.md, ops/SLITHER_BASELINE.md, ops/AUTONOMOUS_RUN.md, test/README.md, README.md
ADR and ops/docs updated to define P0-AUTH-002 scope (EIP-712 EOA + EIP-2098, consumed/cancelled storage, signer epoch, contract-signer rejection until P0-AUTH-003), Slither baseline rows fixed, and runbook/roadmap entries advanced for PR #56.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant StreamDrops
  participant ECDSA
  participant MockStreamMinter
  User->>StreamDrops: call mintDrop(authorization, tokenData, signature)
  StreamDrops->>ECDSA: compute EIP-712 digest, recover signer
  ECDSA-->>StreamDrops: recovered EOA
  StreamDrops->>StreamDrops: validate authorization, mark drop consumed
  StreamDrops->>MockStreamMinter: execute fixed-price or auction mint
  MockStreamMinter-->>StreamDrops: return mint result
  StreamDrops-->>User: emit events / return result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

Poem

🐰 I signed the drop with EIP‑712 light,

domain and nonce tucked safe at night.
Compact and sturdy, the signatures hum,
Drops consumed—minting’s new drum.
Hooray! A carrot for every passing test.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Replace drop authorization with EIP-712' is concise and clearly describes the main change: replacing legacy drop authorization with EIP-712 typed data.
Linked Issues check ✅ Passed The PR implements the required work from issue #10: EIP-712 domain/typed schema, consumed-state storage, deadline/signer/recipient/payer/collection/price/quantity semantics, signer rotation, and explicit ERC-1271 rejection with comprehensive tests covering EOA, wrong signer/domain/chain, expiry, replay, malleability, and zero-address cases.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing EIP-712 drop authorization per issue #10: smart contract implementation, test suites (EIP-712 target-state, characterization, integration), documentation updates (ADR, roadmap, blockers), and operational records. No unrelated refactoring or unscoped features present.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/eip712-drop-authorization

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor Author

@claude please review this PR for correctness and security.

Focus areas:

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

The PR status has been pending on the CodeRabbit processing note since 07:11 UTC. Please complete or refresh the review for the latest head commit 175c24929ac76e5cc5cece64786201dd47063745.

@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find all relevant Solidity files
fd -e sol --full-path | head -40
echo "---"
# Read main contract
fd "StreamDrops.sol" --exec cat {}

Length of output: 17679


🏁 Script executed:

#!/bin/bash
# Read EIP-712 test file
fd "StreamDropsEIP712.t.sol" --exec cat {}

Length of output: 15142


🏁 Script executed:

#!/bin/bash
# Read test helpers
fd "DropAuthTestHelper.sol" --exec cat {}
fd "CharacterizationTestBase.sol" --exec cat {}

Length of output: 5155


🏁 Script executed:

#!/bin/bash
# Read characterization and integration test files
fd "StreamDropsCharacterization.t.sol" --exec cat {}
fd "StreamDropsIntegrationCharacterization.t.sol" --exec cat {}

Length of output: 23816


🏁 Script executed:

#!/bin/bash
# Check for any remaining packed-hash / old surface
rg -n "retrieveMessageAndDropID|mintDrop\(address,address,string|dropExecuted|MintsToTxOrigin|tx\.origin|abi\.encodePacked" --type sol
echo "---"
# Check for old mintDrop signature (7 params)
rg -n "mintDrop" --type sol

Length of output: 586


🏁 Script executed:

#!/bin/bash
# Read ADR and roadmap docs
fd "ADR" | head -10
fd "ROADMAP" | head -10
fd "BLOCKERS" | head -10
fd -e md | head -20
echo "---"
cat README.md 2>/dev/null || true

Length of output: 3102


🏁 Script executed:

#!/bin/bash
cat docs/adr/0001-drop-authorization.md
echo "==="
cat docs/known-blockers.md
echo "==="
cat ops/ROADMAP.md

Length of output: 102659


🏁 Script executed:

#!/bin/bash
# Check for old packed-hash surface and mintDrop usage
rg -n "retrieveMessageAndDropID|mintDrop|dropExecuted|MintsToTxOrigin|tx\.origin|encodePacked" smart-contracts/ test/
echo "---"
# Verify DROP_AUTHORIZATION_TYPEHASH string matches struct fields
rg -n "DROP_AUTHORIZATION_TYPEHASH\|DropAuthorization\|EIP712_DOMAIN_TYPEHASH\|DROP_ID_TYPEHASH" smart-contracts/StreamDrops.sol

Length of output: 14395


🏁 Script executed:

#!/bin/bash
# Read ADR docs and blockers
cat docs/adr/0001-drop-authorization.md 2>/dev/null || echo "not found"
cat docs/known-blockers.md 2>/dev/null || echo "not found"
# Check IStreamDrops interface
cat smart-contracts/IStreamDrops.sol

Length of output: 18637



@punk6529: Review complete for commit 175c2492. Overall this is a correct and well-scoped P0-AUTH-002 implementation. I'll cover each focus area in turn.


EIP-712 Domain and DropAuthorization Struct Hashing

Domain typehash — matches EIP-712 exactly; domainSeparator() computes dynamically using block.chainid and address(this) on every call rather than caching at construction. This is correct and defends against cross-chain replays after any future chain split. ✓

DROP_AUTHORIZATION_TYPEHASH string — I verified field by field that the type string matches the struct declaration in declaration order. All 15 fields are value types (bytes32, address, uint256, uint8), so abi.encode(DROP_AUTHORIZATION_TYPEHASH, _authorization) produces the same byte sequence as encoding each field individually. ✓

DROP_ID_TYPEHASH — correctly distinct from the authorization typehash; dropId is embedded in DropAuthorization as a pre-computed bytes32, not as an EIP-712 reference type, which is both correct and intentional. ✓

Cross-check testtestHashDropAuthorizationMatchesExplicitEip712Encoding independently replicates the digest in assembly (16 slots × 32 bytes = 0x200) and asserts equality. ✓


Replay Protection and Consume-Before-External-Call

The execution sequence in mintDrop is:

  1. tdhSigner.code.length == 0 guard (fails fast for contract signers)
  2. hashDropAuthorization + _recoverEOASigner (pure)
  3. _validateAuthorization (all require checks)
  4. consumedDropIds[dropId] = true ← state written here
  5. _emitAuthorizationConsumed ← event (no external call risk)
  6. _executeFixedPriceDrop / _executeAuctionDrop ← ETH push calls happen here

Drop state is consumed before any external ETH transfer or ERC-721 receiver hook. Checks-effects-interactions is correctly ordered. ✓


Signature Validation Policy

Check Result
65-byte ECDSA: r | s | v via calldataload ✓ correct offsets
64-byte EIP-2098: r | vs, s = vs & 0x7fff…, v = (vs >> 255) + 27
Low-s guard: uint256(s) <= SECP256K1_N_DIV_2
Valid-v guard: v == 27 || v == 28
Zero-recovered-signer guard
Wrong signer (_signer == tdhSigner)
Stale epoch (signerEpoch equality)
Expired deadline (>= block.timestamp)
Zero dropId
dropId derivation recomputed from validated signer + epoch + nonce + salt
Consumed state
Cancelled state
Payer policy (paid: payer == msg.sender; free: payer == address(0))

Tests cover: wrong signer, wrong verifying contract, wrong chain ID, expired deadline, cancelled, stale epoch, wrong dropId, quantity != 1, token-data substitution, malleable, zero-recovered-signer, invalid length, paid-payer mismatch, contract signer. ✓


ERC-1271 Boundary

require(tdhSigner.code.length == 0, "ERC1271 pending");

Checked first, before any signature processing. testContractSignerIsExplicitlyRejectedUntilErc1271Lands deploys a ContractSignerStub and confirms the call reverts. The boundary is clean and correctly documented as pending #19. ✓


Old Packed-Hash Surface

Confirmed absent from smart-contracts/StreamDrops.sol:

  • retrieveMessageAndDropID — removed ✓
  • dropExecuted — removed ✓
  • tx.origin — removed ✓
  • Old 7-parameter mintDrop ABI — removed ✓
  • The one abi.encodePacked at Line 291 is the standard "\x19\x01" EIP-712 envelope prefix, which is correct by spec ✓

The abi.encodePacked occurrences in NFTdelegation.sol, StreamCuratorsPool.sol, XRandoms.sol, etc. are pre-existing and tracked under their own Slither rows — none are in the drop authorization path. ✓


Issues Found

1. cancelDrop emits duplicate events without idempotency guard

function cancelDrop(bytes32 _dropId) public FunctionAdminRequired(this.cancelDrop.selector) {
    require(consumedDropIds[_dropId] == false, "Drop consumed");
    cancelledDropIds[_dropId] = true;           // idempotent state
    emit DropAuthorizationCancelled(_dropId, msg.sender);  // but event fires again
}

There is no require(cancelledDropIds[_dropId] == false, ...) guard. Calling cancelDrop twice on the same dropId passes the consumed check, writes the same boolean (no-op), but emits DropAuthorizationCancelled twice. Off-chain indexers treating each event as a distinct cancellation will double-count. Suggested fix:

function cancelDrop(bytes32 _dropId) public FunctionAdminRequired(this.cancelDrop.selector) {
    require(consumedDropIds[_dropId] == false, "Drop consumed");
    require(cancelledDropIds[_dropId] == false, "Already cancelled");
    cancelledDropIds[_dropId] = true;
    emit DropAuthorizationCancelled(_dropId, msg.sender);
}

2. Five test cases from ADR acceptance criteria are missing

The ADR §Test Plan lists these as required; none appear in StreamDropsEIP712.t.sol or the characterization files:

Missing test Code path exercised
Free fixed-price where payer != address(0) require(_authorization.payer == address(0), "payer") in the price == 0 branch
Auction where payer != address(0) require(_authorization.payer == address(0), "payer") in auction branch
Fixed-price with auctionReservePrice != 0 require(_authorization.auctionReservePrice == 0, "Auction price")
Fixed-price with auctionEndTime != 0 require(_authorization.auctionEndTime == 0, "Auction end")
Auction with price != 0 require(_authorization.price == 0, "Fixed price")

These are simple require paths and the production behavior is correct, but the ADR acceptance criteria explicitly lists them and they should be added for Gate C completeness.

3. No event content assertions

The ADR requires event assertions for DropAuthorizationConsumed, DropAuthorizationCancelled, and SignerEpochChanged. The tests only assert isDropConsumed storage state and isDropCancelled state — they do not verify event emission or field content. These are also part of the ADR acceptance criteria for this issue.


Pre-existing Issues (Out of Scope for This PR)

  • ETH dust from integer division in _executeFixedPriceDrop (msg.value / 2, / 4, / 4) — any msg.value not divisible by 4 traps wei in the contract. Tracked under P0-PAY-001.
  • Push payment DoS — a reverting poster, payOutAddress, or curatorsPoolAddress reverts the entire mint and the drop is not consumed (characterization tests correctly document this). Tracked under P0-PAY-001.

Test and Documentation Traceability for #10

  • ADR 0001 ## Implementation Status section is updated to reflect the P0-AUTH-002 state. ✓
  • Roadmap P0-AUTH-002 entry updated with current status. ✓
  • ops/ROADMAP.md Appendix B test matrix row for Ad hoc drop authorization updated to Passing for EOA/EIP-2098 target state. ✓
  • Slither baseline updated: encode-packed-collision for StreamDrops.mintDrop and retrieveMessageAndDropID removed; uninitialized-local for StreamDrops.mintDrop marked Fixed. ✓
  • docs/known-blockers.md updated to describe the current EIP-712 EOA state and the ERC-1271 blocker. ✓

The three findings above (duplicate-cancel event, 5 missing negative tests, missing event assertions) are the only gaps relative to the ADR acceptance criteria. Everything in the cryptographic and replay-protection layer is correct.

Copy link
Copy Markdown
Contributor Author

Follow-up verification before merge:

  • Addressed the CodeRabbit command-review findings from the earlier head:
    • added an Already cancelled guard for cancelDrop
    • added the five missing sale-mode negative tests from the ADR acceptance criteria
    • added event content assertions for DropAuthorizationConsumed, DropAuthorizationCancelled, and SignerEpochChanged
  • Latest CI on 89af0599f2e14e3ea408170727daf75cdda93b24: run 27260907183, conclusion success.
  • CodeRabbit status is success with no open review threads.
  • Claude was explicitly requested but skipped due the org overage limit; this is recorded in the PR discussion.
  • Local validation after the follow-up commit: make check and scripts\check.ps1 both passed with 47 tests.

@punk6529 punk6529 merged commit a6ae314 into main Jun 10, 2026
2 checks passed
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.

[P0-AUTH-002] Replace drop authorization with replay-safe EIP-712 typed data

1 participant