Skip to content

fix(spend-permission): return the wallet-substituted account in requestSpendPermission - #365

Open
Nexory wants to merge 1 commit into
base:masterfrom
Nexory:fix/request-spend-permission-substituted-account
Open

fix(spend-permission): return the wallet-substituted account in requestSpendPermission#365
Nexory wants to merge 1 commit into
base:masterfrom
Nexory:fix/request-spend-permission-substituted-account

Conversation

@Nexory

@Nexory Nexory commented Jul 18, 2026

Copy link
Copy Markdown

Summary

requestSpendPermission sends mutableData: { fields: ['message.account'] } when capabilities are provided, so the wallet may substitute account with a smart-wallet address. permissionHash is already derived from the wallet-returned (post-substitution) message, but the returned permission object used the original pre-substitution typedData.message. As a result result.permission.account and result.permissionHash described different permissions, and any consumer reading result.permission.account (display, storage, prepareSpendCallData, fetchPermissions) saw the wrong account.

This hoists the returned message into a permissionMessage variable, set from signResult.signedData.message in the wallet_sign branch and from typedData.message in the eth_signTypedData_v4 branch, so permission and permissionHash stay consistent. The eth_signTypedData_v4 path is unchanged in behavior.

Fixes #324.

How did you test your changes?

Added a unit test in requestSpendPermission.test.ts where the wallet returns signedData.message.account set to a different address than the request; it asserts result.permission matches the post-substitution message. The test fails on the current code (returns the original account) and passes with this change.

  • requestSpendPermission.test.ts: 17 passed (the new test fails without the fix)
  • full spend-permission suite: 9 files, 137 tests passing
  • tsc --noEmit: no new type errors from this change

…stSpendPermission

When capabilities are provided, requestSpendPermission sends
mutableData: { fields: ['message.account'] }, allowing the wallet to
substitute account with a smart-wallet address. permissionHash is already
derived from the wallet-returned (post-substitution) message, but the
returned permission object used the original pre-substitution
typedData.message, so result.permission.account and result.permissionHash
described different permissions.

Hoist the returned message into permissionMessage, set from
signResult.signedData.message in the wallet_sign branch and from
typedData.message in the eth_signTypedData_v4 branch, so permission and
permissionHash stay consistent. Adds a test where the wallet substitutes
account with a different address; it fails before this change and passes
after.
@cb-heimdall

Copy link
Copy Markdown
Collaborator

🟡 Heimdall Review Status

Requirement Status More Info
Reviews 🟡 0/1
Denominator calculation
Show calculation
1 if user is bot 0
1 if user is external 0
2 if repo is sensitive 0
From .codeflow.yml 1
Additional review requirements
Show calculation
Max 0
0
From CODEOWNERS 0
Global minimum 0
Max 1
1
1 if commit is unverified 0
Sum 1

@cb-heimdall

Copy link
Copy Markdown
Collaborator

Review Error for Themoor1 @ 2026-08-04 04:15:15 UTC
User failed mfa authentication, either user does not exist or public email is not set on your github profile. \ see go/mfa-help

@Nexory

Nexory commented Aug 4, 2026

Copy link
Copy Markdown
Author

One observation while here, for a possible follow-up. In the wallet_sign branch this PR adopts the wallet-returned message (permissionMessage = signResult.signedData.message), which is the right fix so the returned permission matches the hash. But mutableData only sanctions message.account, and nothing checks that the wallet honored that, so if a wallet returned a message that also changed something else, the spender for instance, it would be adopted silently. That is worth a thought because spender becomes the from of the spend call (prepareSpendCallData: from: permission.permission.spender).

The wallet is trusted, so this is defense in depth rather than a hole, but the check is cheap: compare the returned message against the requested one field by field, normalizing representations (getAddress for addresses, BigInt for allowance/salt, Number for periods), and throw if anything other than account changed. Happy to add it here or as a follow-up if you think it is worth carrying.

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.

spend-permission: requestSpendPermission returns inconsistent SpendPermission when wallet substitutes account via mutableData

2 participants