From 145493cb614e7d76d2f87c92d97192564579a373 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/3] 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 37e30703d6..72f8f08ba7 100644 --- a/packages/gator-permissions-controller/src/decodePermission/decodePermission.test.ts +++ b/packages/gator-permissions-controller/src/decodePermission/decodePermission.test.ts @@ -18,6 +18,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. @@ -38,6 +39,8 @@ describe('decodePermission', () => { NonceEnforcer, RedeemerEnforcer, } = contracts; + const { approvalRevocationEnforcer } = + getChecksumEnforcersByChainId(contracts); describe('getPermissionDecoderMatchingCaveatTypes()', () => { const zeroAddress = '0x0000000000000000000000000000000000000000' as Hex; @@ -634,6 +637,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 9aa50864a7d01bd542458250463d3f938ebac262 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/3] 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..8743075c6c 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 3ba3f5313539212b25d863d89b0f926728ead553 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/3] 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 6e15fcb9d0..b423d3c63f 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.1.0` to `^65.2.0` ([#8722](https://github.com/MetaMask/core/pull/8722))