Summary
requestSpendPermission declares that the wallet may mutate message.account via mutableData, but then constructs the returned SpendPermission object using the original (pre-substitution) typed data for the permission field while using the wallet-returned (post-substitution) signed data only for permissionHash. This produces an internally inconsistent object.
Affected file
packages/account-sdk/src/interface/public-utilities/spend-permission/methods/requestSpendPermission.ts — HEAD 24ab30c (account 2.5.6)
Code path
When capabilities is provided (lines 82-128), the function:
- Sends
mutableData: { fields: ['message.account'] } to the wallet (lines 90-92), explicitly allowing the wallet to substitute account with a smart-wallet address.
- Computes
permissionHash from the wallet's response: getHash({ permission: signResult.signedData.message, chainId }) (lines 125-128) — correct, uses post-substitution data.
- Builds the returned object with
permission: typedData.message (line 145) — incorrect, uses pre-substitution data.
When the wallet exercises the substitution (e.g., replaces the caller EOA 0xCaller with a smart-wallet 0xSW):
result.permissionHash → hash of { account: 0xSW, ... } // post-substitution
result.permission.account → 0xCaller // pre-substitution (WRONG)
The two fields now describe different permissions. Any downstream consumer that reads result.permission.account (e.g., to display to the user, to pass to prepareSpendCallData, or to call fetchPermissions) sees the wrong account address.
Fix
Replace typedData.message with signResult.signedData.message in the object construction:
- permission: typedData.message,
+ permission: signResult.signedData.message,
This should be applied inside the if (capabilities) branch only (the else branch has no mutableData substitution and is correct as-is).
Missing test coverage
The existing wallet_sign test ('should use wallet_sign when capabilities are provided', line 277) mocks signedData as the same object as the original typedData, so no substitution actually occurs and the divergence is invisible. A test with signedData.message.account set to a different address than the request account would expose this.
Non-security classification
This is a correctness/data-consistency bug, not a fund-drain vulnerability — the signature itself is produced by the wallet and remains valid. However, callers relying on result.permission for display, storage, or subsequent API calls will observe a wrong account field, which can cause silent failures downstream.
Summary
requestSpendPermissiondeclares that the wallet may mutatemessage.accountviamutableData, but then constructs the returnedSpendPermissionobject using the original (pre-substitution) typed data for thepermissionfield while using the wallet-returned (post-substitution) signed data only forpermissionHash. This produces an internally inconsistent object.Affected file
packages/account-sdk/src/interface/public-utilities/spend-permission/methods/requestSpendPermission.ts— HEAD24ab30c(account 2.5.6)Code path
When
capabilitiesis provided (lines 82-128), the function:mutableData: { fields: ['message.account'] }to the wallet (lines 90-92), explicitly allowing the wallet to substituteaccountwith a smart-wallet address.permissionHashfrom the wallet's response:getHash({ permission: signResult.signedData.message, chainId })(lines 125-128) — correct, uses post-substitution data.permission: typedData.message(line 145) — incorrect, uses pre-substitution data.When the wallet exercises the substitution (e.g., replaces the caller EOA
0xCallerwith a smart-wallet0xSW):The two fields now describe different permissions. Any downstream consumer that reads
result.permission.account(e.g., to display to the user, to pass toprepareSpendCallData, or to callfetchPermissions) sees the wrong account address.Fix
Replace
typedData.messagewithsignResult.signedData.messagein the object construction:This should be applied inside the
if (capabilities)branch only (theelsebranch has no mutableData substitution and is correct as-is).Missing test coverage
The existing
wallet_signtest ('should use wallet_sign when capabilities are provided', line 277) mockssignedDataas the same object as the originaltypedData, so no substitution actually occurs and the divergence is invisible. A test withsignedData.message.accountset to a different address than the requestaccountwould expose this.Non-security classification
This is a correctness/data-consistency bug, not a fund-drain vulnerability — the signature itself is produced by the wallet and remains valid. However, callers relying on
result.permissionfor display, storage, or subsequent API calls will observe a wrong account field, which can cause silent failures downstream.