Hi team
This is one report with two linked legs, plus a pointer to the WeavingStates report already filed. The three must ship in one fix, because fixing the pool gate alone turns the second leg into a permissionless Critical.
Leg one, pool gate. In weaver lib dot rs, validate_unsigned at line 589 matches only extend_warp and weave_weft before the catch-all arm returns the invalid call error. The pull_thread dispatchable at line 430 opens with an ensure none origin check. An honest pull_thread claim is therefore rejected by every transaction pool, and a signed claim fails the origin check in dispatch. The only production inclusion path at HEAD is a block author placing the unsigned extrinsic into its own block, because block execution does not re-run pool validation. Honest users are locked out while a single block author keeps a private claim path.
let validity = GhostWeaver::validate_unsigned(TransactionSource::External, &call);
assert_eq!(
validity,
Err(TransactionValidityError::Invalid(InvalidTransaction::Call).into())
Leg two, empty-leaf mint. In weaver types dot rs, get_preimage_slice at line 365 replaces the slot at the given index with the computed arguments hash when the index is in range, and otherwise returns the empty hash. The arguments hash at line 413 binds amount, receiver, slot index and network, but on the out-of-range branch that binding is computed nowhere, so receiver and amount are never bound to a leaf. get_unique_key at line 383 mixes session, slot index, proof index and network and places no bound on the slot index. I installed the root of a fully empty 256-leaf tree, claimed with an empty slots vector, slot index 999 and an arbitrary receiver, and minted 1,000,000 units with no deposit anywhere. A second claim with slot index 1000 minted 2,000,000 more, because the unique key changes with the unbounded slot index.
let proof1 = ThreadProof::EvmThreadProof(EvmThreadProof::new(
999, 5, receiver.clone(), 1_000_000u64, proof.clone(), Vec::new()));
assert!(GhostWeaver::pull_thread(RuntimeOrigin::none(), network_id, session, proof1).is_ok());
assert_eq!(Balances::free_balance(&receiver), 1_000_000);
let proof2 = ThreadProof::EvmThreadProof(EvmThreadProof::new(
1000, 5, receiver.clone(), 2_000_000u64, proof, Vec::new()));
assert!(GhostWeaver::pull_thread(RuntimeOrigin::none(), network_id, session, proof2).is_ok());
assert_eq!(Balances::free_balance(&receiver), 3_000_000);
Severity, stated honestly. As shipped this is High, gated by leg one to block-author inclusion. It becomes Critical the moment claim submission is repaired without the companion preimage fix, because the branch fix for the pool gate calls the same proof verification that still contains the empty-leaf branch. No victim cooperation is needed; the attacker controls slot index, proof index, receiver, amount and the slots vector, and every partially filled gatekeeper tree supplies empty leaves publicly.
Suggested fix, all in one change: bind the supplied slot against the computed arguments hash first, reset WeavingStates at session finalization second, open the pool gate last. The WeavingStates leg is filed separately and must ship in the same change.
Shahmeer