Skip to content

FundForwarderV2: dual-source routing, batch deployment, TLV chain scanner#7

Open
Ramarti wants to merge 7 commits into
mainfrom
feat/fund-forwarder-v2
Open

FundForwarderV2: dual-source routing, batch deployment, TLV chain scanner#7
Ramarti wants to merge 7 commits into
mainfrom
feat/fund-forwarder-v2

Conversation

@Ramarti

@Ramarti Ramarti commented Jul 22, 2026

Copy link
Copy Markdown

Summary

Adds the fund-forwarder side of the TLV Extension to 2028 (design doc, code change reference). The deployed V1 contracts are untouched; new implementations are separate files.

Contracts

  • FundForwarderV2.sol — two authorized source hashes (original TLV + 2026-08-13 TLV) and one target (the user's 2028 TLV). receive() forwards the full msg.value of every source transfer to the target; non-source transfers stay in the EOA and the balance is never swept. The V1 tradingDesk/forwardThreshold split logic is removed (equivalent to the reference's tradingDesk=0, threshold=0 configuration, minus the now-unused withdrawn() read).
  • FundForwarderFactoryV2.sol — CREATE3 salt keyed on the beneficiary hash: one forwarder per user, shared by both source vaults. deployForwarder(beneficiaryHash, sourceVaultHash, secondarySourceVaultHash, target) verifies target.beneficiary() == beneficiaryHash (TargetBeneficiaryMismatch). computeForwarderAddress(sourceVault) keeps the V1 signature and reads the beneficiary hash from the vault; computeForwarderAddressByBeneficiary predicts before any vault exists; isForwarderDeployed returns false (V1 reverted) when nothing is deployed.
  • interfaces/ISourceVaultV2.solbeneficiary() → bytes32.

Secondary source is always required (matches the code change reference): forwarders deploy once per beneficiary and cannot be redeployed, so an accidental zero secondary would be a permanent silent misconfiguration. Both contracts revert InvalidSecondarySourceVaultHash on zero; users without a 2026-08-13 TLV deploy with the explicit inert sentinel noSecondarySentinel(beneficiaryHash) = keccak256("TLV2028_NO_SECONDARY" ‖ beneficiaryHash), which can never match a real sender hash.

Deployment scripts (JSON-driven)

  • script/DeployFundForwarderFactoryV2.s.sol — deploys the V2 factory (--sig "run(bytes32)" <fresh salt>).
  • script/DeployFundForwardersBatch.s.sol — consumes the DeployVaults2028 output from the timelock-vest-vault repo, derives source hashes (zero aug2026Vault in the JSON → the no-secondary sentinel on-chain), verifies each target vault's beneficiary, deploys via CREATE3, and writes the batch enriched with forwarder per entry. Idempotent: already-deployed forwarders are config-verified and skipped.

Offchain scanner (offchain/, TypeScript + viem)

Discovers every deployed TLV on Story with no trusted input list: VaultCreated logs from the TLV factories, ForwarderDeployed logs from the V1 forwarder factories (several exist; newest record per source vault wins) plus a per-factory prediction cross-check, and Blockscout verified-contract search. Candidates are validated with multicall-batched getters at a pinned block, bucketed by cliff date, grouped per beneficiary hash, and classified into the design-doc user states with review flags (selfTargetForwarder, moreThanTwoSources, multipleOriginalVaults, …). suggestedAllocation2028 sums the aug-2026 remaining plus every original vault not covered by it. Outputs a status report and the deploy-input JSON for the vault batch; optional address,name CSV attaches labels.

Factory addresses and scan findings are intentionally not in this repo — see the internal runbook.

Docs

docs/TLV-2028-Extension.md — economic parameters (old vs new schedules, forwarder economics change, staking rewards claimable from deployment) and the deployment runbook. Addresses live in the internal runbook.

Tests

74 Foundry tests (V1 suites untouched; new FundForwarderV2.t.sol, FundForwarderFactoryV2.t.sol, DeployFundForwardersBatch.t.sol) + 26 vitest tests, tsc --noEmit and oxlint clean. Scanner verified with live mainnet runs.

🤖 Generated with Claude Code

Ramarti and others added 5 commits July 22, 2026 13:24
FundForwarderV2 accepts two authorized source vaults (the original TLV
and the 2026-08-13 TLV, the latter optional via bytes32(0)) and forwards
the full msg.value of every source transfer into one target vault (the
user's 2028 TLV). The V1 tradingDesk/forwardThreshold split logic is
removed; non-source transfers stay in the EOA and are never swept.

FundForwarderFactoryV2 keys the CREATE3 salt on the beneficiary hash so
each user gets exactly one forwarder shared by both source vaults, and
verifies the target vault's beneficiary() matches the beneficiary hash.
computeForwarderAddress(sourceVault) keeps the V1 signature and resolves
the beneficiary hash from the vault. The deployed V1 sources remain
untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
DeployFundForwarderFactoryV2 deploys the V2 factory with a fresh salt.
DeployFundForwardersBatch consumes the 2028 vault batch output JSON,
derives the source hashes from the vault addresses (zero aug2026Vault =
no secondary), verifies each target vault's beneficiary, deploys the
forwarders via CREATE3, and writes the enriched batch with forwarder
addresses. Idempotent: already-deployed forwarders are config-verified
and skipped. Covered by DeployFundForwardersBatch.t.sol.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds docs/TLV-2028-Extension.md: vault-generation economic parameters
(2028 TLV: cliff 2028-02-13, 25% at cliff, linear monthly over 36
months, fully vested 2031-02-13), staking rewards claimable from
deployment, allocation policy, the V1->V2 forwarder economics change
(threshold/tradingDesk split removed, 100% forward to the 2028 TLV),
contract changes, the JSON deployment pipeline with runbook, and the
addresses table. Exempts the file from the docs/ gitignore.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Discovers all deployed TLVs on Story with no trusted input list, via
three modalities: VaultCreated logs from the known TLV factories,
ForwarderDeployed logs from the V1 forwarder factory plus a prediction
cross-check, and Blockscout verified-contract search. Candidates are
validated with multicall-batched TLV getters at a block pinned after
log discovery (public RPCs prune historical state), bucketed by cliff
date, grouped per beneficiary hash, and classified into the design-doc
user states with data flags and a suggestedAllocation2028. Outputs a
rich status report and the DeployVaults2028 input file; an optional
"address,name" CSV attaches labels. Verified against Story mainnet:
316 users, 283 original and 49 aug-2026 vaults discovered.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Five verified FundForwarderFactory deployments exist on Story mainnet
(three with ForwarderDeployed events, 533 total). The scanner config now
takes v1ForwarderFactories as an array: events are collected across all
of them, the newest record per source vault wins, and the prediction
cross-check runs per factory to catch pre-event deployments. The
effective aug-2026 vault is the one the newest forwarder targets (users
have one target vault per forwarder round). Adds selfTargetForwarder
flag: 215 newest records on mainnet target their own source vault, an
on-chain re-lock pattern that needs ops review. getLogs chunks now retry
with backoff and a 30s HTTP timeout (public RPC flakiness). Example
config ships the five factory addresses; docs record the mainnet
findings and the zero-secondary deviation from the Code Change
Reference.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Ramarti
Ramarti force-pushed the feat/fund-forwarder-v2 branch from cc24603 to c79fe1d Compare July 22, 2026 17:31
Ramarti and others added 2 commits July 22, 2026 18:25
The mainnet scan showed 23 users with 64M IP of remaining entitlement in
additional original vaults that the single-vault suggestion ignored. The
suggestion now adds the remaining of every original vault not covered by
the aug-2026 vault (covered = the vault the routing forwarder drains,
else an exact remaining==allocation sizing match, else originals[0] as a
conservative default against double counting); without an aug-2026 vault
it sums all originals. Adds a moreThanTwoSources flag for users whose
potential sources exceed FundForwarderV2's two source slots and need
out-of-band handling.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Forwarders deploy once per beneficiary and cannot be redeployed, so an
accidental zero secondary would be a permanent silent misconfiguration:
the user's 2026-08-13 TLV withdrawals would land liquid in their EOA.
FundForwarderV2 and FundForwarderFactoryV2 now revert with
InvalidSecondarySourceVaultHash on a zero secondary, matching the Code
Change Reference. Users without a 2026-08-13 TLV deploy with the
explicit per-beneficiary sentinel noSecondarySentinel(beneficiaryHash)
= keccak256("TLV2028_NO_SECONDARY" || beneficiaryHash), which is inert
at runtime; the batch script fills it in automatically for entries with
a zero aug2026Vault.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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