Component: ghost-lite @ a420393, src/hooks/useCalldata.tsx:193-232, consumed by src/containers/Nominations.tsx:357 (handleOnSetPayee).
Description: usePayeeCalldata(expectedPayee, destinationReceiver) builds the Staking.set_payee calldata via SWR, but the SWR key does NOT include destinationReceiver:
// useCalldata.tsx:197-198, destinationReceiver missing from the key
const { data: calldata } = useSWR(
client && chainId && expectedPayee && metadata
? ["withdraw_unbonded", client, chainId, metadata, expectedPayee] // <-- receiver absent
: null,
([_, client, _chainId, metadata, expectedPayee]) => {
...
case "Account":
destination = RewardDestination().Account(destinationReceiver) // line 210, from closure
Trigger flow: (1) user selects payee “Account”, types valid receiver A → calldata_A computed; (2) user EDITS the input to valid receiver B → SWR key unchanged → NO refetch; (3) user clicks “Change Destination” → handleTransaction({ calldata: payeeCalldata /* still calldata_A */ }) → the wallet signs set_payee(Account(A)) while the screen shows B. The button is only disabled by !payeeCalldata / !destinationReceiverIsValid (Nominations.tsx:347-353), both still satisfied by value B. The only remaining defense is the wallet extension’s decode popup showing the real address (A). Systematic comparison: every other calldata hook includes its distinguishing parameters in the SWR key (useTransferCalldata: destination, amount; line 62; useNominateCalldata: addresses; line 160; bond/unbond/extra: amount; useWithdrawCalldata: numberOfSpans; line 176). Only usePayeeCalldata is defective. (Bonus: the "withdraw_unbonded" key label at line 198 is a copy-paste, functionally harmless.)
Impact: Staking rewards redirected to an address the user did not intend (reward stream only, not principal). Needs a specific interaction sequence (edit the field after initial computation, then click directly).
Reproduction:
Open /nominations with a bonded account (local/testnet) → change Destination to “Account” → fill valid receiver A → wait for payeeCalldata to populate → change the input to valid receiver B → immediately click “Change Destination” → compare the address in the wallet signing popup vs the input content.
Severity: Medium. Reason: violates the core “what you see is what you sign” property; impact is limited because (a) only the reward stream, not principal, (b) needs a specific interaction sequence, (c) the wallet decode popup still shows the correct value as a last defense. Not High because there is no direct fund-exfiltration path.
Recommendation: Add destinationReceiver to the SWR key: ["set_payee", client, chainId, metadata, expectedPayee, destinationReceiver] (and fix the wrong key label while at it).
Verification: VERIFIED.