From 91145039f5664a5801c980f7dc87cc6b6a0c50ef Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Thu, 14 May 2026 14:15:44 +1200 Subject: [PATCH 1/6] Add decoders for token-approval-revocation permission type --- .../decodePermission/decodePermission.test.ts | 65 ++++++++ .../src/decodePermission/decoders/index.ts | 2 + .../decoders/tokenApprovalRevocation.test.ts | 157 ++++++++++++++++++ .../decoders/tokenApprovalRevocation.ts | 100 +++++++++++ .../src/decodePermission/types.ts | 29 +++- .../src/decodePermission/utils.test.ts | 31 +++- .../src/decodePermission/utils.ts | 5 + 7 files changed, 384 insertions(+), 5 deletions(-) create mode 100644 packages/gator-permissions-controller/src/decodePermission/decoders/tokenApprovalRevocation.test.ts create mode 100644 packages/gator-permissions-controller/src/decodePermission/decoders/tokenApprovalRevocation.ts diff --git a/packages/gator-permissions-controller/src/decodePermission/decodePermission.test.ts b/packages/gator-permissions-controller/src/decodePermission/decodePermission.test.ts index 7b297cae60..f4690bff7c 100644 --- a/packages/gator-permissions-controller/src/decodePermission/decodePermission.test.ts +++ b/packages/gator-permissions-controller/src/decodePermission/decodePermission.test.ts @@ -17,6 +17,7 @@ import type { DeployedContractsByName, PermissionDecoder, } from './types'; +import { getChecksumEnforcersByChainId } from './utils'; // These tests use the live deployments table for version 1.3.0 to // construct deterministic caveat address sets for a known chain. @@ -37,6 +38,8 @@ describe('decodePermission', () => { NonceEnforcer, RedeemerEnforcer, } = contracts; + const { approvalRevocationEnforcer } = + getChecksumEnforcersByChainId(contracts); describe('findDecodersWithMatchingCaveatAddresses()', () => { const zeroAddress = '0x0000000000000000000000000000000000000000' as Hex; @@ -621,6 +624,68 @@ describe('decodePermission', () => { ).toThrow('Contract not found: AllowedCalldataEnforcer'); }); }); + + describe('token-approval-revocation', () => { + const expectedPermissionType = 'token-approval-revocation'; + + it('matches with ApprovalRevocationEnforcer and NonceEnforcer', () => { + const enforcers = [approvalRevocationEnforcer, NonceEnforcer]; + const result = findDecoderWithMatchingCaveatAddresses({ + enforcers, + permissionDecoders: createPermissionDecodersForContracts(contracts), + }); + expect(result.permissionType).toBe(expectedPermissionType); + }); + + it('allows TimestampEnforcer as extra', () => { + const enforcers = [ + approvalRevocationEnforcer, + NonceEnforcer, + TimestampEnforcer, + ]; + const result = findDecoderWithMatchingCaveatAddresses({ + enforcers, + permissionDecoders: createPermissionDecodersForContracts(contracts), + }); + expect(result.permissionType).toBe(expectedPermissionType); + }); + + it('rejects when NonceEnforcer is missing', () => { + const enforcers = [approvalRevocationEnforcer]; + expect(() => + findDecoderWithMatchingCaveatAddresses({ + enforcers, + permissionDecoders: createPermissionDecodersForContracts(contracts), + }), + ).toThrow('Unable to identify permission type'); + }); + + it('rejects forbidden extra caveat', () => { + const enforcers = [ + approvalRevocationEnforcer, + NonceEnforcer, + ValueLteEnforcer, + ]; + expect(() => + findDecoderWithMatchingCaveatAddresses({ + enforcers, + permissionDecoders: createPermissionDecodersForContracts(contracts), + }), + ).toThrow('Unable to identify permission type'); + }); + + it('accepts lowercased addresses', () => { + const enforcers: Hex[] = [ + approvalRevocationEnforcer.toLowerCase() as unknown as Hex, + NonceEnforcer.toLowerCase() as unknown as Hex, + ]; + const result = findDecoderWithMatchingCaveatAddresses({ + enforcers, + permissionDecoders: createPermissionDecodersForContracts(contracts), + }); + expect(result.permissionType).toBe(expectedPermissionType); + }); + }); }); describe('reconstructDecodedPermission', () => { diff --git a/packages/gator-permissions-controller/src/decodePermission/decoders/index.ts b/packages/gator-permissions-controller/src/decodePermission/decoders/index.ts index 572248ae2d..58d818b8a8 100644 --- a/packages/gator-permissions-controller/src/decodePermission/decoders/index.ts +++ b/packages/gator-permissions-controller/src/decodePermission/decoders/index.ts @@ -8,6 +8,7 @@ import { makePermissionDecoder } from './makePermissionDecoder'; import { makeNativeTokenAllowanceDecoderConfig } from './nativeTokenAllowance'; import { makeNativeTokenPeriodicDecoderConfig } from './nativeTokenPeriodic'; import { makeNativeTokenStreamDecoderConfig } from './nativeTokenStream'; +import { makeTokenApprovalRevocationDecoderConfig } from './tokenApprovalRevocation'; /** * Builds the canonical set of permission decoders for a chain. @@ -32,5 +33,6 @@ export const createPermissionDecodersForContracts = ( makeErc20TokenPeriodicDecoderConfig(contractAddresses), makeErc20TokenAllowanceDecoderConfig(contractAddresses), makeErc20TokenRevocationDecoderConfig(contractAddresses), + makeTokenApprovalRevocationDecoderConfig(contractAddresses), ].map(makePermissionDecoder); }; diff --git a/packages/gator-permissions-controller/src/decodePermission/decoders/tokenApprovalRevocation.test.ts b/packages/gator-permissions-controller/src/decodePermission/decoders/tokenApprovalRevocation.test.ts new file mode 100644 index 0000000000..6f1ad5246d --- /dev/null +++ b/packages/gator-permissions-controller/src/decodePermission/decoders/tokenApprovalRevocation.test.ts @@ -0,0 +1,157 @@ +import { createTimestampTerms } from '@metamask/delegation-core'; +import { + CHAIN_ID, + DELEGATOR_CONTRACTS, +} from '@metamask/delegation-deployments'; + +import { createPermissionDecodersForContracts } from '.'; +import { getChecksumEnforcersByChainId } from '../utils'; + +describe('token-approval-revocation decoder', () => { + const chainId = CHAIN_ID.sepolia; + const contracts = DELEGATOR_CONTRACTS['1.3.0'][chainId]; + const { timestampEnforcer, approvalRevocationEnforcer, nonceEnforcer } = + getChecksumEnforcersByChainId(contracts); + const permissionDecoders = createPermissionDecodersForContracts(contracts); + const decoder = permissionDecoders.find( + (candidate) => candidate.permissionType === 'token-approval-revocation', + ); + + if (!decoder) { + throw new Error('Decoder not found'); + } + + const expiryCaveat = { + enforcer: timestampEnforcer, + terms: createTimestampTerms({ + afterThreshold: 0, + beforeThreshold: 1720000, + }), + args: '0x' as const, + }; + + it('rejects empty terms', () => { + const caveats = [ + expiryCaveat, + { + enforcer: approvalRevocationEnforcer, + terms: '0x' as const, + args: '0x' as const, + }, + { + enforcer: nonceEnforcer, + terms: '0x' as const, + args: '0x' as const, + }, + ]; + + const result = decoder.validateAndDecodePermission(caveats); + expect(result.isValid).toBe(false); + + if (result.isValid) { + throw new Error('Expected invalid result'); + } + + expect(result.error.message).toContain( + 'Invalid ApprovalRevocation terms: must be greater than 0', + ); + }); + + it('rejects terms whose mask exceeds the supported max', () => { + const caveats = [ + expiryCaveat, + { + enforcer: approvalRevocationEnforcer, + terms: '0x40' as const, + args: '0x' as const, + }, + { + enforcer: nonceEnforcer, + terms: '0x' as const, + args: '0x' as const, + }, + ]; + + const result = decoder.validateAndDecodePermission(caveats); + expect(result.isValid).toBe(false); + + if (result.isValid) { + throw new Error('Expected invalid result'); + } + + expect(result.error.message).toContain( + 'Invalid ApprovalRevocation terms: must be less than or equal to 63', + ); + }); + + it('successfully decodes valid token-approval-revocation caveats', () => { + const caveats = [ + expiryCaveat, + { + enforcer: approvalRevocationEnforcer, + terms: '0x01' as const, + args: '0x' as const, + }, + { + enforcer: nonceEnforcer, + terms: '0x' as const, + args: '0x' as const, + }, + ]; + + const result = decoder.validateAndDecodePermission(caveats); + expect(result.isValid).toBe(true); + + if (!result.isValid) { + throw new Error('Expected valid result'); + } + + expect(result.expiry).toBe(1720000); + expect(result.data).toStrictEqual({ + erc20Approve: true, + erc721Approve: false, + erc721SetApprovalForAll: false, + permit2Approve: false, + permit2Lockdown: false, + permit2InvalidateNonces: false, + }); + expect(result.rules).toStrictEqual([ + { + type: 'expiry', + data: { timestamp: 1720000 }, + }, + ]); + }); + + it('decodes all supported flags from the terms bitmask', () => { + const caveats = [ + expiryCaveat, + { + enforcer: approvalRevocationEnforcer, + terms: '0x3f' as const, + args: '0x' as const, + }, + { + enforcer: nonceEnforcer, + terms: '0x' as const, + args: '0x' as const, + }, + ]; + + const result = decoder.validateAndDecodePermission(caveats); + expect(result.isValid).toBe(true); + + if (!result.isValid) { + throw new Error('Expected valid result'); + } + + expect(result.data).toStrictEqual({ + erc20Approve: true, + erc721Approve: true, + erc721SetApprovalForAll: true, + permit2Approve: true, + permit2Lockdown: true, + permit2InvalidateNonces: true, + }); + }); +}); diff --git a/packages/gator-permissions-controller/src/decodePermission/decoders/tokenApprovalRevocation.ts b/packages/gator-permissions-controller/src/decodePermission/decoders/tokenApprovalRevocation.ts new file mode 100644 index 0000000000..153942e641 --- /dev/null +++ b/packages/gator-permissions-controller/src/decodePermission/decoders/tokenApprovalRevocation.ts @@ -0,0 +1,100 @@ +import type { + ChecksumCaveat, + ChecksumEnforcersByChainId, + DecodedPermission, +} from '../types'; +import { hexToNumber } from '@metamask/utils'; +import { getTermsByEnforcer } from '../utils'; +import { expiryRule } from './expiryRule'; +import type { MakePermissionDecoderConfig } from './makePermissionDecoder'; + +enum ApprovalRevocationFlag { + Erc20Approve = 0x01, + Erc721Approve = 0x02, + Erc721SetApprovalForAll = 0x04, + Permit2Approve = 0x08, + Permit2Lockdown = 0x10, + Permit2InvalidateNonces = 0x20, +} + +// eslint-disable-next-line no-bitwise +const MAX_APPROVAL_REVOCATION_MASK = ApprovalRevocationFlag.Permit2InvalidateNonces | ApprovalRevocationFlag.Permit2Lockdown | ApprovalRevocationFlag.Permit2Approve | ApprovalRevocationFlag.Erc721SetApprovalForAll | ApprovalRevocationFlag.Erc721Approve | ApprovalRevocationFlag.Erc20Approve; + +/** + * Builds the configuration for the token-approval-revocation permission decoder. + * + * @param contractAddresses - Checksummed enforcer addresses for the chain. + * @returns The token-approval-revocation permission decoder configuration. + */ +export function makeTokenApprovalRevocationDecoderConfig( + contractAddresses: ChecksumEnforcersByChainId, +): MakePermissionDecoderConfig { + const { timestampEnforcer, approvalRevocationEnforcer, nonceEnforcer } = + contractAddresses; + + return { + permissionType: 'token-approval-revocation', + contractAddresses, + optionalEnforcers: [ + timestampEnforcer, // expiry rule + ], + requiredEnforcers: { + [approvalRevocationEnforcer]: 1, + [nonceEnforcer]: 1, + }, + rules: [expiryRule], + validateAndDecodeData, + }; +} + +/** + * Decodes token-approval-revocation permission data from caveats; throws on invalid. + * + * @param caveats - Caveats from the permission context (checksummed). + * @param contractAddresses - Checksummed enforcer addresses for the chain. + * @returns Decoded approval-revocation capability flags. + */ +function validateAndDecodeData( + caveats: ChecksumCaveat[], + contractAddresses: ChecksumEnforcersByChainId, +): DecodedPermission['permission']['data'] { + const { approvalRevocationEnforcer } = contractAddresses; + + const terms = getTermsByEnforcer({ + caveats, + enforcer: approvalRevocationEnforcer, + }); + + const mask = hexToNumber(terms); + + if (mask > MAX_APPROVAL_REVOCATION_MASK) { + throw new Error(`Invalid ApprovalRevocation terms: must be less than or equal to ${MAX_APPROVAL_REVOCATION_MASK}`); + } + + if (mask === 0) { + throw new Error('Invalid ApprovalRevocation terms: must be greater than 0'); + } + + return { + erc20Approve: isFlagEnabled(mask, ApprovalRevocationFlag.Erc20Approve), + erc721Approve: isFlagEnabled(mask, ApprovalRevocationFlag.Erc721Approve), + erc721SetApprovalForAll: isFlagEnabled( + mask, + ApprovalRevocationFlag.Erc721SetApprovalForAll, + ), + permit2Approve: isFlagEnabled(mask, ApprovalRevocationFlag.Permit2Approve), + permit2Lockdown: isFlagEnabled( + mask, + ApprovalRevocationFlag.Permit2Lockdown, + ), + permit2InvalidateNonces: isFlagEnabled( + mask, + ApprovalRevocationFlag.Permit2InvalidateNonces, + ), + }; +} + +function isFlagEnabled(mask: number, flag: number): boolean { + // eslint-disable-next-line no-bitwise + return (mask & flag) === flag; +} diff --git a/packages/gator-permissions-controller/src/decodePermission/types.ts b/packages/gator-permissions-controller/src/decodePermission/types.ts index 42eae41901..06b06656d1 100644 --- a/packages/gator-permissions-controller/src/decodePermission/types.ts +++ b/packages/gator-permissions-controller/src/decodePermission/types.ts @@ -46,6 +46,23 @@ type NativeTokenAllowancePermission = BasePermission & { }; }; +/** + * Permission type for token approval revocation. + * + * Not yet defined in `@metamask/7715-permission-types`, so declared locally. + */ +type TokenApprovalRevocationPermission = BasePermission & { + type: 'token-approval-revocation'; + data: MetaMaskBasePermissionData & { + erc20Approve: boolean; + erc721Approve: boolean; + erc721SetApprovalForAll: boolean; + permit2Approve: boolean; + permit2Lockdown: boolean; + permit2InvalidateNonces: boolean; + }; +}; + /** * Extended permission union, including types not yet published in * `@metamask/7715-permission-types` but supported by this package's decoder. @@ -53,7 +70,8 @@ type NativeTokenAllowancePermission = BasePermission & { type ExtendedPermissionTypes = | PermissionTypes | Erc20TokenAllowancePermission - | NativeTokenAllowancePermission; + | NativeTokenAllowancePermission + | TokenApprovalRevocationPermission; // This is a somewhat convoluted type - it includes all of the fields that are decoded from the permission context. /** @@ -65,13 +83,15 @@ type ExtendedPermissionTypes = * `TimestampEnforcer` terms, as well as the `origin` property. */ export type DecodedPermission = Pick< - PermissionRequest, + PermissionRequest, 'chainId' | 'from' | 'to' > & { permission: Omit< - PermissionRequest['permission'], - 'isAdjustmentAllowed' + PermissionRequest['permission'], + 'isAdjustmentAllowed' | 'type' | 'data' > & { + type: ExtendedPermissionTypes['type']; + data: ExtendedPermissionTypes['data']; // PermissionRequest type does not work well without the specific permission type, so we amend it here justification?: string; }; @@ -97,6 +117,7 @@ export type ChecksumEnforcersByChainId = { erc20PeriodicEnforcer: Hex; nativeTokenStreamingEnforcer: Hex; nativeTokenPeriodicEnforcer: Hex; + approvalRevocationEnforcer: Hex; exactCalldataEnforcer: Hex; valueLteEnforcer: Hex; timestampEnforcer: Hex; diff --git a/packages/gator-permissions-controller/src/decodePermission/utils.test.ts b/packages/gator-permissions-controller/src/decodePermission/utils.test.ts index fd06940525..d28bc61de4 100644 --- a/packages/gator-permissions-controller/src/decodePermission/utils.test.ts +++ b/packages/gator-permissions-controller/src/decodePermission/utils.test.ts @@ -14,6 +14,7 @@ import { const buildContracts = (): DeployedContractsByName => ({ ERC20PeriodTransferEnforcer: '0x1111111111111111111111111111111111111111', ERC20StreamingEnforcer: '0x2222222222222222222222222222222222222222', + ApprovalRevocationEnforcer: '0x1212121212121212121212121212121212121212', ExactCalldataEnforcer: '0x3333333333333333333333333333333333333333', NativeTokenPeriodTransferEnforcer: '0x4444444444444444444444444444444444444444', @@ -44,6 +45,9 @@ describe('getChecksumEnforcersByChainId', () => { nativeTokenPeriodicEnforcer: getChecksumAddress( contracts.NativeTokenPeriodTransferEnforcer, ), + approvalRevocationEnforcer: getChecksumAddress( + contracts.ApprovalRevocationEnforcer, + ), exactCalldataEnforcer: getChecksumAddress( contracts.ExactCalldataEnforcer, ), @@ -77,6 +81,7 @@ describe('createPermissionDecodersForContracts', () => { erc20PeriodicEnforcer, nativeTokenStreamingEnforcer, nativeTokenPeriodicEnforcer, + approvalRevocationEnforcer, exactCalldataEnforcer, valueLteEnforcer, timestampEnforcer, @@ -93,7 +98,8 @@ describe('createPermissionDecodersForContracts', () => { // native-token-periodic // native-token-allowance // erc20-token-revocation - const permissionTypeCount = 7; + // token-approval-revocation + const permissionTypeCount = 8; const decoders = createPermissionDecodersForContracts(contracts); expect(decoders).toHaveLength(permissionTypeCount); @@ -291,6 +297,29 @@ describe('createPermissionDecodersForContracts', () => { [nonceEnforcer, 1], ]), ); + + // token-approval-revocation + expect(byType['token-approval-revocation']).toBeDefined(); + expect(byType['token-approval-revocation'].permissionType).toBe( + 'token-approval-revocation', + ); + expect(byType['token-approval-revocation'].optionalEnforcers.size).toBe(1); + expect( + byType['token-approval-revocation'].optionalEnforcers.has( + timestampEnforcer, + ), + ).toBe(true); + expect(byType['token-approval-revocation'].requiredEnforcers.size).toBe(2); + expect( + Array.from( + byType['token-approval-revocation'].requiredEnforcers.entries(), + ), + ).toStrictEqual( + expect.arrayContaining([ + [approvalRevocationEnforcer, 1], + [nonceEnforcer, 1], + ]), + ); }); it('each decoder has caveatAddressesMatch and validateAndDecodePermission', () => { diff --git a/packages/gator-permissions-controller/src/decodePermission/utils.ts b/packages/gator-permissions-controller/src/decodePermission/utils.ts index 4177159c64..a1dac59472 100644 --- a/packages/gator-permissions-controller/src/decodePermission/utils.ts +++ b/packages/gator-permissions-controller/src/decodePermission/utils.ts @@ -13,6 +13,7 @@ import type { const ENFORCER_CONTRACT_NAMES = { ERC20PeriodTransferEnforcer: 'ERC20PeriodTransferEnforcer', ERC20StreamingEnforcer: 'ERC20StreamingEnforcer', + ApprovalRevocationEnforcer: 'ApprovalRevocationEnforcer', ExactCalldataEnforcer: 'ExactCalldataEnforcer', NativeTokenPeriodTransferEnforcer: 'NativeTokenPeriodTransferEnforcer', NativeTokenStreamingEnforcer: 'NativeTokenStreamingEnforcer', @@ -91,6 +92,9 @@ export const getChecksumEnforcersByChainId = ( const nativeTokenPeriodicEnforcer = getChecksumContractAddress( ENFORCER_CONTRACT_NAMES.NativeTokenPeriodTransferEnforcer, ); + const approvalRevocationEnforcer = getChecksumContractAddress( + ENFORCER_CONTRACT_NAMES.ApprovalRevocationEnforcer, + ); // general enforcers const exactCalldataEnforcer = getChecksumContractAddress( @@ -123,6 +127,7 @@ export const getChecksumEnforcersByChainId = ( erc20PeriodicEnforcer, nativeTokenStreamingEnforcer, nativeTokenPeriodicEnforcer, + approvalRevocationEnforcer, exactCalldataEnforcer, valueLteEnforcer, timestampEnforcer, From 4e2ccd1121fe98b94145ecc9e3c5084fed33e252 Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Fri, 15 May 2026 17:11:37 +1200 Subject: [PATCH 2/6] Fix linting --- .../decoders/tokenApprovalRevocation.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/gator-permissions-controller/src/decodePermission/decoders/tokenApprovalRevocation.ts b/packages/gator-permissions-controller/src/decodePermission/decoders/tokenApprovalRevocation.ts index 153942e641..5d354f6636 100644 --- a/packages/gator-permissions-controller/src/decodePermission/decoders/tokenApprovalRevocation.ts +++ b/packages/gator-permissions-controller/src/decodePermission/decoders/tokenApprovalRevocation.ts @@ -1,9 +1,11 @@ +/* eslint-disable no-bitwise */ +import { hexToNumber } from '@metamask/utils'; + import type { ChecksumCaveat, ChecksumEnforcersByChainId, DecodedPermission, } from '../types'; -import { hexToNumber } from '@metamask/utils'; import { getTermsByEnforcer } from '../utils'; import { expiryRule } from './expiryRule'; import type { MakePermissionDecoderConfig } from './makePermissionDecoder'; @@ -17,8 +19,13 @@ enum ApprovalRevocationFlag { Permit2InvalidateNonces = 0x20, } -// eslint-disable-next-line no-bitwise -const MAX_APPROVAL_REVOCATION_MASK = ApprovalRevocationFlag.Permit2InvalidateNonces | ApprovalRevocationFlag.Permit2Lockdown | ApprovalRevocationFlag.Permit2Approve | ApprovalRevocationFlag.Erc721SetApprovalForAll | ApprovalRevocationFlag.Erc721Approve | ApprovalRevocationFlag.Erc20Approve; +const MAX_APPROVAL_REVOCATION_MASK = + ApprovalRevocationFlag.Permit2InvalidateNonces | + ApprovalRevocationFlag.Permit2Lockdown | + ApprovalRevocationFlag.Permit2Approve | + ApprovalRevocationFlag.Erc721SetApprovalForAll | + ApprovalRevocationFlag.Erc721Approve | + ApprovalRevocationFlag.Erc20Approve; /** * Builds the configuration for the token-approval-revocation permission decoder. @@ -68,7 +75,9 @@ function validateAndDecodeData( const mask = hexToNumber(terms); if (mask > MAX_APPROVAL_REVOCATION_MASK) { - throw new Error(`Invalid ApprovalRevocation terms: must be less than or equal to ${MAX_APPROVAL_REVOCATION_MASK}`); + throw new Error( + `Invalid ApprovalRevocation terms: must be less than or equal to ${MAX_APPROVAL_REVOCATION_MASK}`, + ); } if (mask === 0) { @@ -95,6 +104,5 @@ function validateAndDecodeData( } function isFlagEnabled(mask: number, flag: number): boolean { - // eslint-disable-next-line no-bitwise return (mask & flag) === flag; } From fe34a8fe7a80f02702beae640bd7c86949d3591d Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Fri, 15 May 2026 17:15:30 +1200 Subject: [PATCH 3/6] Add changelog entry --- packages/gator-permissions-controller/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/gator-permissions-controller/CHANGELOG.md b/packages/gator-permissions-controller/CHANGELOG.md index 6149c30db9..58354206d2 100644 --- a/packages/gator-permissions-controller/CHANGELOG.md +++ b/packages/gator-permissions-controller/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add `token-approval-revocation` execution permission type decoding ([#8823](https://github.com/MetaMask/core/pull/8823)) + ### Changed - Bump `@metamask/transaction-controller` from `^65.3.0` to `^65.4.0` ([#8796](https://github.com/MetaMask/core/pull/8796)) From b8dc0191613b51fe0fe2a8a0bf62c0112eb95f6d Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Tue, 19 May 2026 10:24:16 +1200 Subject: [PATCH 4/6] Bump delegation dependencies: - @metamask/7715-permission-types from ^0.6.0 to ^0.7.0 - @metamask/delegation-core from ^2.0.0 to ^2.2.0 - @metamask/delegation-deployments from ^1.3.0 to ^1.4.0 --- .../gator-permissions-controller/package.json | 6 ++-- .../CHANGELOG.md | 5 +++ .../package.json | 4 +-- yarn.lock | 34 +++++++++---------- 4 files changed, 27 insertions(+), 22 deletions(-) diff --git a/packages/gator-permissions-controller/package.json b/packages/gator-permissions-controller/package.json index 4958f2fe27..bffd59e457 100644 --- a/packages/gator-permissions-controller/package.json +++ b/packages/gator-permissions-controller/package.json @@ -53,11 +53,11 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/7715-permission-types": "^0.6.0", + "@metamask/7715-permission-types": "^0.7.0", "@metamask/abi-utils": "^2.0.3", "@metamask/base-controller": "^9.1.0", - "@metamask/delegation-core": "^2.0.0", - "@metamask/delegation-deployments": "^1.3.0", + "@metamask/delegation-core": "^2.2.0", + "@metamask/delegation-deployments": "^1.4.0", "@metamask/messenger": "^1.2.0", "@metamask/network-controller": "^32.0.0", "@metamask/snaps-controllers": "^19.0.0", diff --git a/packages/money-account-upgrade-controller/CHANGELOG.md b/packages/money-account-upgrade-controller/CHANGELOG.md index d6303187bb..0054494297 100644 --- a/packages/money-account-upgrade-controller/CHANGELOG.md +++ b/packages/money-account-upgrade-controller/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- Bump `@metamask/delegation-core` from `^2.0.0` to `^2.2.0` ([#8823](https://github.com/MetaMask/core/pull/8823)) +- Bump `@metamask/delegation-deployments` from `^1.3.0` to `^1.4.0` ([#8823](https://github.com/MetaMask/core/pull/8823)) + ## [2.0.2] ### Changed diff --git a/packages/money-account-upgrade-controller/package.json b/packages/money-account-upgrade-controller/package.json index 302452669b..3de72092d4 100644 --- a/packages/money-account-upgrade-controller/package.json +++ b/packages/money-account-upgrade-controller/package.json @@ -57,8 +57,8 @@ "@metamask/base-controller": "^9.1.0", "@metamask/chomp-api-service": "^3.1.0", "@metamask/delegation-controller": "^3.0.0", - "@metamask/delegation-core": "^2.0.0", - "@metamask/delegation-deployments": "^1.3.0", + "@metamask/delegation-core": "^2.2.0", + "@metamask/delegation-deployments": "^1.4.0", "@metamask/keyring-controller": "^25.5.0", "@metamask/messenger": "^1.2.0", "@metamask/network-controller": "^32.0.0", diff --git a/yarn.lock b/yarn.lock index 9eff13004d..768ec435d6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2492,10 +2492,10 @@ __metadata: languageName: node linkType: hard -"@metamask/7715-permission-types@npm:^0.6.0": - version: 0.6.0 - resolution: "@metamask/7715-permission-types@npm:0.6.0" - checksum: 10/bac0741ed0d880d9f418a58ef5d1f165cff0171636cb4431bc42a05b471b92afd6593810ac805a427a76dc02abcba651cc3c1c05b67d5a4b6ad3923b057de039 +"@metamask/7715-permission-types@npm:^0.7.0": + version: 0.7.0 + resolution: "@metamask/7715-permission-types@npm:0.7.0" + checksum: 10/b6295e45fd500679b141ae88afd90d0fcea1e2450bc207cf0064ae63eeff2654abd954b58e79b66ded62cbd959bbf6bc8d643d22b8eabf332dde4aa980457d7e languageName: node linkType: hard @@ -3502,21 +3502,21 @@ __metadata: languageName: unknown linkType: soft -"@metamask/delegation-core@npm:^2.0.0": - version: 2.0.0 - resolution: "@metamask/delegation-core@npm:2.0.0" +"@metamask/delegation-core@npm:^2.2.0": + version: 2.2.0 + resolution: "@metamask/delegation-core@npm:2.2.0" dependencies: "@metamask/abi-utils": "npm:^3.0.0" "@metamask/utils": "npm:^11.4.0" "@noble/hashes": "npm:^1.8.0" - checksum: 10/b473160e4cb4a6d463c6015de6e90d057034d2e8f2905068e1f44f93c8247618c5d84a155e86dfaa125dacb040951643517b9a76961bf8d215c194dc4d1cc0ad + checksum: 10/cd9d06f3d5cb778591838e7887f9203791dd37aa366e73bbbc7b1497cce1faed0cb9211e091750531b1210712b591fdbf188851dc19853bef2edf481d3a016a2 languageName: node linkType: hard -"@metamask/delegation-deployments@npm:^1.3.0": - version: 1.3.0 - resolution: "@metamask/delegation-deployments@npm:1.3.0" - checksum: 10/58f4aafb5f0e3cbc543811cbc0100efab4ed67b9c9794b83192962153e4edbe12fd6ab6fa7be689503309862a65eb7fde771f632893d38ab54f8171aa682b34f +"@metamask/delegation-deployments@npm:^1.4.0": + version: 1.4.0 + resolution: "@metamask/delegation-deployments@npm:1.4.0" + checksum: 10/e5e7b83e27daec5b1b61482647d43d4b685954818ff02687e2dbe8169a4dfe199cc8d2ed444242b60425ac2e7d2cf2a5ca29f3e69936abe6a8391f578ec693bb languageName: node linkType: hard @@ -4128,12 +4128,12 @@ __metadata: dependencies: "@lavamoat/allow-scripts": "npm:^3.0.4" "@lavamoat/preinstall-always-fail": "npm:^2.1.0" - "@metamask/7715-permission-types": "npm:^0.6.0" + "@metamask/7715-permission-types": "npm:^0.7.0" "@metamask/abi-utils": "npm:^2.0.3" "@metamask/auto-changelog": "npm:^6.1.0" "@metamask/base-controller": "npm:^9.1.0" - "@metamask/delegation-core": "npm:^2.0.0" - "@metamask/delegation-deployments": "npm:^1.3.0" + "@metamask/delegation-core": "npm:^2.2.0" + "@metamask/delegation-deployments": "npm:^1.4.0" "@metamask/messenger": "npm:^1.2.0" "@metamask/network-controller": "npm:^32.0.0" "@metamask/snaps-controllers": "npm:^19.0.0" @@ -4546,8 +4546,8 @@ __metadata: "@metamask/base-controller": "npm:^9.1.0" "@metamask/chomp-api-service": "npm:^3.1.0" "@metamask/delegation-controller": "npm:^3.0.0" - "@metamask/delegation-core": "npm:^2.0.0" - "@metamask/delegation-deployments": "npm:^1.3.0" + "@metamask/delegation-core": "npm:^2.2.0" + "@metamask/delegation-deployments": "npm:^1.4.0" "@metamask/keyring-controller": "npm:^25.5.0" "@metamask/messenger": "npm:^1.2.0" "@metamask/network-controller": "npm:^32.0.0" From 4c87d7fe761dda6a3d3720b04c887102c171295a Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Tue, 19 May 2026 17:18:40 +1200 Subject: [PATCH 5/6] Fix some tests, add validation for empty terms, and fix some linting --- .../decodePermission/decodePermission.test.ts | 48 ++++++++----------- .../decoders/tokenApprovalRevocation.test.ts | 27 +++++++++++ .../decoders/tokenApprovalRevocation.ts | 4 ++ 3 files changed, 52 insertions(+), 27 deletions(-) diff --git a/packages/gator-permissions-controller/src/decodePermission/decodePermission.test.ts b/packages/gator-permissions-controller/src/decodePermission/decodePermission.test.ts index f4690bff7c..4076225489 100644 --- a/packages/gator-permissions-controller/src/decodePermission/decodePermission.test.ts +++ b/packages/gator-permissions-controller/src/decodePermission/decodePermission.test.ts @@ -627,14 +627,18 @@ describe('decodePermission', () => { describe('token-approval-revocation', () => { const expectedPermissionType = 'token-approval-revocation'; - - it('matches with ApprovalRevocationEnforcer and NonceEnforcer', () => { - const enforcers = [approvalRevocationEnforcer, NonceEnforcer]; - const result = findDecoderWithMatchingCaveatAddresses({ + const findMatchingDecoders = (enforcers: Hex[]): PermissionDecoder[] => + findDecodersWithMatchingCaveatAddresses({ enforcers, permissionDecoders: createPermissionDecodersForContracts(contracts), }); - expect(result.permissionType).toBe(expectedPermissionType); + + it('matches with ApprovalRevocationEnforcer and NonceEnforcer', () => { + const enforcers = [approvalRevocationEnforcer, NonceEnforcer]; + const rules = findMatchingDecoders(enforcers); + expect( + rules.map((matchingRule) => matchingRule.permissionType), + ).toStrictEqual([expectedPermissionType]); }); it('allows TimestampEnforcer as extra', () => { @@ -643,21 +647,16 @@ describe('decodePermission', () => { NonceEnforcer, TimestampEnforcer, ]; - const result = findDecoderWithMatchingCaveatAddresses({ - enforcers, - permissionDecoders: createPermissionDecodersForContracts(contracts), - }); - expect(result.permissionType).toBe(expectedPermissionType); + const rules = findMatchingDecoders(enforcers); + expect( + rules.map((matchingRule) => matchingRule.permissionType), + ).toStrictEqual([expectedPermissionType]); }); it('rejects when NonceEnforcer is missing', () => { const enforcers = [approvalRevocationEnforcer]; - expect(() => - findDecoderWithMatchingCaveatAddresses({ - enforcers, - permissionDecoders: createPermissionDecodersForContracts(contracts), - }), - ).toThrow('Unable to identify permission type'); + const rules = findMatchingDecoders(enforcers); + expect(rules).toStrictEqual([]); }); it('rejects forbidden extra caveat', () => { @@ -666,12 +665,8 @@ describe('decodePermission', () => { NonceEnforcer, ValueLteEnforcer, ]; - expect(() => - findDecoderWithMatchingCaveatAddresses({ - enforcers, - permissionDecoders: createPermissionDecodersForContracts(contracts), - }), - ).toThrow('Unable to identify permission type'); + const rules = findMatchingDecoders(enforcers); + expect(rules).toStrictEqual([]); }); it('accepts lowercased addresses', () => { @@ -679,11 +674,10 @@ describe('decodePermission', () => { approvalRevocationEnforcer.toLowerCase() as unknown as Hex, NonceEnforcer.toLowerCase() as unknown as Hex, ]; - const result = findDecoderWithMatchingCaveatAddresses({ - enforcers, - permissionDecoders: createPermissionDecodersForContracts(contracts), - }); - expect(result.permissionType).toBe(expectedPermissionType); + const rules = findMatchingDecoders(enforcers); + expect( + rules.map((matchingRule) => matchingRule.permissionType), + ).toStrictEqual([expectedPermissionType]); }); }); }); diff --git a/packages/gator-permissions-controller/src/decodePermission/decoders/tokenApprovalRevocation.test.ts b/packages/gator-permissions-controller/src/decodePermission/decoders/tokenApprovalRevocation.test.ts index 6f1ad5246d..0d8b48a99a 100644 --- a/packages/gator-permissions-controller/src/decodePermission/decoders/tokenApprovalRevocation.test.ts +++ b/packages/gator-permissions-controller/src/decodePermission/decoders/tokenApprovalRevocation.test.ts @@ -57,6 +57,33 @@ describe('token-approval-revocation decoder', () => { ); }); + it('rejects 0x00 terms', () => { + const caveats = [ + expiryCaveat, + { + enforcer: approvalRevocationEnforcer, + terms: '0x00' as const, + args: '0x' as const, + }, + { + enforcer: nonceEnforcer, + terms: '0x' as const, + args: '0x' as const, + }, + ]; + + const result = decoder.validateAndDecodePermission(caveats); + expect(result.isValid).toBe(false); + + if (result.isValid) { + throw new Error('Expected invalid result'); + } + + expect(result.error.message).toContain( + 'Invalid ApprovalRevocation terms: must be greater than 0', + ); + }); + it('rejects terms whose mask exceeds the supported max', () => { const caveats = [ expiryCaveat, diff --git a/packages/gator-permissions-controller/src/decodePermission/decoders/tokenApprovalRevocation.ts b/packages/gator-permissions-controller/src/decodePermission/decoders/tokenApprovalRevocation.ts index 5d354f6636..0b4758148d 100644 --- a/packages/gator-permissions-controller/src/decodePermission/decoders/tokenApprovalRevocation.ts +++ b/packages/gator-permissions-controller/src/decodePermission/decoders/tokenApprovalRevocation.ts @@ -72,6 +72,10 @@ function validateAndDecodeData( enforcer: approvalRevocationEnforcer, }); + if (terms === '0x') { + throw new Error('Invalid ApprovalRevocation terms: must be greater than 0'); + } + const mask = hexToNumber(terms); if (mask > MAX_APPROVAL_REVOCATION_MASK) { From cecc5c0cfa1583c9fedbb52b9589b0ed409b0b19 Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Wed, 20 May 2026 13:15:04 +1200 Subject: [PATCH 6/6] Bump @metamask/7715-permission-types from ^0.7.0 to ^0.7.1 @metamask/delegation-core from ^2.2.0 to ^2.2.1 --- .../gator-permissions-controller/package.json | 4 ++-- .../CHANGELOG.md | 2 +- .../package.json | 2 +- yarn.lock | 22 +++++++++---------- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/gator-permissions-controller/package.json b/packages/gator-permissions-controller/package.json index bffd59e457..6321c83309 100644 --- a/packages/gator-permissions-controller/package.json +++ b/packages/gator-permissions-controller/package.json @@ -53,10 +53,10 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/7715-permission-types": "^0.7.0", + "@metamask/7715-permission-types": "^0.7.1", "@metamask/abi-utils": "^2.0.3", "@metamask/base-controller": "^9.1.0", - "@metamask/delegation-core": "^2.2.0", + "@metamask/delegation-core": "^2.2.1", "@metamask/delegation-deployments": "^1.4.0", "@metamask/messenger": "^1.2.0", "@metamask/network-controller": "^32.0.0", diff --git a/packages/money-account-upgrade-controller/CHANGELOG.md b/packages/money-account-upgrade-controller/CHANGELOG.md index 0054494297..bac6d1b1af 100644 --- a/packages/money-account-upgrade-controller/CHANGELOG.md +++ b/packages/money-account-upgrade-controller/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- Bump `@metamask/delegation-core` from `^2.0.0` to `^2.2.0` ([#8823](https://github.com/MetaMask/core/pull/8823)) +- Bump `@metamask/delegation-core` from `^2.0.0` to `^2.2.1` ([#8823](https://github.com/MetaMask/core/pull/8823)) - Bump `@metamask/delegation-deployments` from `^1.3.0` to `^1.4.0` ([#8823](https://github.com/MetaMask/core/pull/8823)) ## [2.0.2] diff --git a/packages/money-account-upgrade-controller/package.json b/packages/money-account-upgrade-controller/package.json index 3de72092d4..d116ff0813 100644 --- a/packages/money-account-upgrade-controller/package.json +++ b/packages/money-account-upgrade-controller/package.json @@ -57,7 +57,7 @@ "@metamask/base-controller": "^9.1.0", "@metamask/chomp-api-service": "^3.1.0", "@metamask/delegation-controller": "^3.0.0", - "@metamask/delegation-core": "^2.2.0", + "@metamask/delegation-core": "^2.2.1", "@metamask/delegation-deployments": "^1.4.0", "@metamask/keyring-controller": "^25.5.0", "@metamask/messenger": "^1.2.0", diff --git a/yarn.lock b/yarn.lock index 768ec435d6..5342741c7d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2492,10 +2492,10 @@ __metadata: languageName: node linkType: hard -"@metamask/7715-permission-types@npm:^0.7.0": - version: 0.7.0 - resolution: "@metamask/7715-permission-types@npm:0.7.0" - checksum: 10/b6295e45fd500679b141ae88afd90d0fcea1e2450bc207cf0064ae63eeff2654abd954b58e79b66ded62cbd959bbf6bc8d643d22b8eabf332dde4aa980457d7e +"@metamask/7715-permission-types@npm:^0.7.1": + version: 0.7.1 + resolution: "@metamask/7715-permission-types@npm:0.7.1" + checksum: 10/2cbe7b8d09ae9b11171d2f9c36bf94f1acea17f9541b399bf99caa14e500c0e91d79e69a4846db2c80b18aa0df0215b36be1ea661cc99f4d6b5bc8d9eea5a4dd languageName: node linkType: hard @@ -3502,14 +3502,14 @@ __metadata: languageName: unknown linkType: soft -"@metamask/delegation-core@npm:^2.2.0": - version: 2.2.0 - resolution: "@metamask/delegation-core@npm:2.2.0" +"@metamask/delegation-core@npm:^2.2.1": + version: 2.2.1 + resolution: "@metamask/delegation-core@npm:2.2.1" dependencies: "@metamask/abi-utils": "npm:^3.0.0" "@metamask/utils": "npm:^11.4.0" "@noble/hashes": "npm:^1.8.0" - checksum: 10/cd9d06f3d5cb778591838e7887f9203791dd37aa366e73bbbc7b1497cce1faed0cb9211e091750531b1210712b591fdbf188851dc19853bef2edf481d3a016a2 + checksum: 10/8cc532e4a5fdc83eddb36fada6283d51629272fcce8b7c7c2d3b4addde5aff7c741365a3bcd701364460fbbcbbe94fccc0460ee3ef9af284a82f84ca708f0e06 languageName: node linkType: hard @@ -4128,11 +4128,11 @@ __metadata: dependencies: "@lavamoat/allow-scripts": "npm:^3.0.4" "@lavamoat/preinstall-always-fail": "npm:^2.1.0" - "@metamask/7715-permission-types": "npm:^0.7.0" + "@metamask/7715-permission-types": "npm:^0.7.1" "@metamask/abi-utils": "npm:^2.0.3" "@metamask/auto-changelog": "npm:^6.1.0" "@metamask/base-controller": "npm:^9.1.0" - "@metamask/delegation-core": "npm:^2.2.0" + "@metamask/delegation-core": "npm:^2.2.1" "@metamask/delegation-deployments": "npm:^1.4.0" "@metamask/messenger": "npm:^1.2.0" "@metamask/network-controller": "npm:^32.0.0" @@ -4546,7 +4546,7 @@ __metadata: "@metamask/base-controller": "npm:^9.1.0" "@metamask/chomp-api-service": "npm:^3.1.0" "@metamask/delegation-controller": "npm:^3.0.0" - "@metamask/delegation-core": "npm:^2.2.0" + "@metamask/delegation-core": "npm:^2.2.1" "@metamask/delegation-deployments": "npm:^1.4.0" "@metamask/keyring-controller": "npm:^25.5.0" "@metamask/messenger": "npm:^1.2.0"