Skip to content

feat: add token approval revocation functionality#328

Draft
mj-kiwi wants to merge 5 commits into
mainfrom
faet/add-token-appovel-revocation
Draft

feat: add token approval revocation functionality#328
mj-kiwi wants to merge 5 commits into
mainfrom
faet/add-token-appovel-revocation

Conversation

@mj-kiwi
Copy link
Copy Markdown
Contributor

@mj-kiwi mj-kiwi commented May 14, 2026

Description

Replaces the disabled erc20-token-revocation permission with token-approval-revocation.

This adds support for common token approval revocation mechanisms:

  • ERC-20 approve(spender, 0)
  • ERC-721 approve(address(0), tokenId)
  • ERC-721/ERC-1155 setApprovalForAll(operator, false)
  • Permit2 approve(token, spender, 0, 0)
  • Permit2 lockdown
  • Permit2 invalidateNonces

The PR wires the new permission through offer creation, validation, caveat terms, permission intro rendering, existing permission formatting, site request UI, and site redemption calldata generation.

Need to update the dependence when MetaMask/smart-accounts-kit#226 is ready.

Changelog entry: Not added in this branch.

Related issues

Fixes:

Manual testing steps

  1. Run yarn start.
  2. Open the site at http://localhost:8000/.
  3. Connect/install the local snaps if prompted.
  4. Select the token-approval-revocation permission type.
  5. Confirm the Revocation methods section renders as a vertical checkbox list.
  6. Request the permission with one or more revocation methods selected.
  7. Confirm the permission intro and existing permissions UI render the selected revocation methods.
  8. Redeem the permission from the site and verify the generated target/calldata updates for the selected revocation method.

Screenshots/Recordings

Before

N/A

After

image image

Pre-merge author checklist

Pre-merge reviewer checklist

  • I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed).
  • I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots.

Note

Medium Risk
Introduces a new revocation permission and caveat encoding backed by a new enforcer address (currently configured as zero), which could block revocation flows or change on-chain call constraints if misconfigured.

Overview
Replaces the disabled erc20-token-revocation permission with a new token-approval-revocation permission that supports multiple approval-revocation mechanisms (ERC-20, ERC-721, and Permit2) and surfaces the selected mechanisms in both confirmation and existing-permissions UIs.

Adds a new ApprovalRevocationEnforcer contract slot to chain metadata and switches revocation caveat construction from hard-coded calldata/value constraints to a single enforcer term bitmask (rejecting requests if the enforcer address is unset).

Updates supported-permission wiring end-to-end (offers, handler factory, intro content, shared supported rule types) and updates the site demo to request mechanism flags and generate the correct target/calldata for the chosen revocation method.

Reviewed by Cursor Bugbot for commit b12594e. Bugbot is set up for automated code reviews on this repo. Configure here.

- Add tests for token approval revocation context, content, and validation.
- Create a new TokenApprovalRevocationForm component to handle user input for revocation methods.
- Update the RedemptionForm to support various token approval revocation methods.
- Refactor existing ERC20TokenRevocationForm references to the new TokenApprovalRevocationForm.
- Modify the RPC handler and supported rule types to accommodate the new token approval revocation type.
- Remove deprecated ERC20TokenRevocationForm component.
@mj-kiwi mj-kiwi requested a review from a team as a code owner May 14, 2026 23:10
@mj-kiwi mj-kiwi marked this pull request as draft May 14, 2026 23:45
@socket-security
Copy link
Copy Markdown

socket-security Bot commented May 15, 2026

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Updated@​metamask/​7715-permission-types@​0.6.0 ⏵ 0.7.0100 +110072 +193 +4100
Updated@​metamask/​delegation-core@​2.0.0 ⏵ 2.2.0100 +110010095 +2100
Updated@​metamask/​smart-accounts-kit@​1.4.0 ⏵ 0.0.0-use.local100 +19100100100 +5100

View full report

Copy link
Copy Markdown
Contributor

@jeffsmale90 jeffsmale90 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally I think this looks great - let's get some guidance on the UX of displaying the different primitives.

"revokeTokenApprovalsLabel": {
"message": "Revoke token approvals"
},
"approvalRevocationMechanismsLabel": {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit; we've used the term Primitive to refer to the different ways that a permit can be revoked - at least in code I think it's a good idea to standardise that.

({ key }) => permissionData[key] === true,
).map(({ labelKey }) => t(labelKey));
const revokeValue =
revocationMechanisms.length > 0
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are we saying that if none of the booleans values in the data bag are true, then all primitives are allowed?

I had considered this an invalid request.

But maybe it would be a good idea, if all of the revocation primitives are true that we should say "All revocation primitives" or something instead of listing them all out. I suspect that would be the more common experience.

const revokeValue = t('allTokens');
const revocationMechanisms = TOKEN_APPROVAL_REVOCATION_PRIMITIVES.filter(
({ key }) => permissionData[key] === true,
).map(({ labelKey }) => t(labelKey));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what does t() return if there's no match? should we default to the labelKey, or throw?

Comment on lines +23 to +27
if (contracts.approvalRevocationEnforcer === ZERO_ADDRESS) {
throw new InvalidInputError(
'ApprovalRevocationEnforcer address is not configured.',
);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this a temporary check until we add the revocation enforcer? I don't think we have this check on the other enforcers.

Comment on lines +42 to +44
{enabledMechanisms.map(({ key, labelKey }) => (
<Text key={key}>{t(labelKey)}</Text>
))}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've raised with UX, but can we at least make this a bullet list? (I don't know if we can 😢)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe not for this PR, but we should probably just import @metamask/delegation-deployments instead of copying the contract addresses 🤔

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.

2 participants