From c9ad548dfb32e52e1b9dc65d8375d4640cbb1d40 Mon Sep 17 00:00:00 2001 From: MJ Kiwi Date: Tue, 21 Apr 2026 21:18:31 +1200 Subject: [PATCH] feat: Implement redeemer rule for execution permissions and update related tests --- ...et-supported-execution-permissions.test.ts | 4 +- ...llet-request-execution-permissions.test.ts | 28 ++++++++ .../wallet-request-execution-permissions.ts | 11 +++- .../src/GatorPermissionsController.ts | 3 +- .../src/constants.ts | 7 ++ .../decodePermission/decodePermission.test.ts | 65 +++++++++++++++++++ .../src/decodePermission/decodePermission.ts | 6 +- .../src/decodePermission/redeemer.test.ts | 39 +++++++++++ .../src/decodePermission/redeemer.ts | 41 ++++++++++++ .../rules/erc20TokenPeriodic.ts | 2 + .../rules/erc20TokenRevocation.ts | 2 + .../rules/erc20TokenStream.ts | 2 + .../rules/makePermissionRule.test.ts | 55 ++++++++++++++++ .../rules/makePermissionRule.ts | 31 ++++++++- .../rules/nativeTokenPeriodic.ts | 2 + .../rules/nativeTokenStream.ts | 2 + .../src/decodePermission/types.ts | 5 ++ .../src/decodePermission/utils.test.ts | 28 ++++++-- .../src/decodePermission/utils.ts | 6 ++ .../gator-permissions-controller/src/index.ts | 6 +- .../src/redeemerRule.ts | 12 ++++ 21 files changed, 344 insertions(+), 13 deletions(-) create mode 100644 packages/gator-permissions-controller/src/decodePermission/redeemer.test.ts create mode 100644 packages/gator-permissions-controller/src/decodePermission/redeemer.ts create mode 100644 packages/gator-permissions-controller/src/redeemerRule.ts diff --git a/packages/eth-json-rpc-middleware/src/methods/wallet-get-supported-execution-permissions.test.ts b/packages/eth-json-rpc-middleware/src/methods/wallet-get-supported-execution-permissions.test.ts index cafd7ff8a48..2fcec4d658f 100644 --- a/packages/eth-json-rpc-middleware/src/methods/wallet-get-supported-execution-permissions.test.ts +++ b/packages/eth-json-rpc-middleware/src/methods/wallet-get-supported-execution-permissions.test.ts @@ -10,7 +10,7 @@ import { createWalletGetSupportedExecutionPermissionsHandler } from './wallet-ge const RESULT_MOCK: GetSupportedExecutionPermissionsResult = { 'native-token-allowance': { chainIds: ['0x123', '0x345'] as Hex[], - ruleTypes: ['expiry'], + ruleTypes: ['expiry', 'redeemer'], }, 'erc20-token-allowance': { chainIds: ['0x123'] as Hex[], @@ -18,7 +18,7 @@ const RESULT_MOCK: GetSupportedExecutionPermissionsResult = { }, 'erc721-token-allowance': { chainIds: ['0x123'] as Hex[], - ruleTypes: ['expiry'], + ruleTypes: ['expiry', 'redeemer'], }, }; diff --git a/packages/eth-json-rpc-middleware/src/methods/wallet-request-execution-permissions.test.ts b/packages/eth-json-rpc-middleware/src/methods/wallet-request-execution-permissions.test.ts index 13e95d44acd..6f2e83ed2c6 100644 --- a/packages/eth-json-rpc-middleware/src/methods/wallet-request-execution-permissions.test.ts +++ b/packages/eth-json-rpc-middleware/src/methods/wallet-request-execution-permissions.test.ts @@ -143,6 +143,34 @@ describe('wallet_requestExecutionPermissions', () => { ); }); + it('accepts redeemer rule with valid checksum addresses', async () => { + params[0].rules = [ + { + type: 'redeemer', + data: { addresses: [FROM_ADDRESS_MOCK, TO_ADDRESS_MOCK] }, + }, + ]; + + await callMethod(); + + expect(processRequestExecutionPermissionsMock).toHaveBeenCalledWith( + params, + request, + context, + ); + }); + + it('rejects redeemer rule with invalid address entry', async () => { + params[0].rules = [ + { + type: 'redeemer', + data: { addresses: ['0x123'] }, + }, + ]; + + await expect(callMethod()).rejects.toThrow('Invalid params'); + }); + it('throws if no hook', async () => { await expect( createWalletRequestExecutionPermissionsHandler({})({ diff --git a/packages/eth-json-rpc-middleware/src/methods/wallet-request-execution-permissions.ts b/packages/eth-json-rpc-middleware/src/methods/wallet-request-execution-permissions.ts index dc2ec4027a8..941193d4d09 100644 --- a/packages/eth-json-rpc-middleware/src/methods/wallet-request-execution-permissions.ts +++ b/packages/eth-json-rpc-middleware/src/methods/wallet-request-execution-permissions.ts @@ -24,11 +24,20 @@ const PermissionStruct = object({ data: record(string(), unknown()), }); -const RuleStruct = object({ +const RedeemerRuleStruct = object({ + type: literal('redeemer'), + data: object({ + addresses: array(HexChecksumAddressStruct), + }), +}); + +const GenericRuleStruct = object({ type: string(), data: record(string(), unknown()), }); +const RuleStruct = union([RedeemerRuleStruct, GenericRuleStruct]); + const PermissionRequestStruct = object({ chainId: StrictHexStruct, from: optional(HexChecksumAddressStruct), diff --git a/packages/gator-permissions-controller/src/GatorPermissionsController.ts b/packages/gator-permissions-controller/src/GatorPermissionsController.ts index 05f59263093..c322f0b4717 100644 --- a/packages/gator-permissions-controller/src/GatorPermissionsController.ts +++ b/packages/gator-permissions-controller/src/GatorPermissionsController.ts @@ -608,7 +608,7 @@ export class GatorPermissionsController extends BaseController< }); } - const { expiry, data } = decodeResult; + const { expiry, data, rules } = decodeResult; const permission = reconstructDecodedPermission({ chainId, @@ -620,6 +620,7 @@ export class GatorPermissionsController extends BaseController< data, justification, specifiedOrigin, + rules, }); return permission; diff --git a/packages/gator-permissions-controller/src/constants.ts b/packages/gator-permissions-controller/src/constants.ts index 9dca8c093a4..feda8067523 100644 --- a/packages/gator-permissions-controller/src/constants.ts +++ b/packages/gator-permissions-controller/src/constants.ts @@ -3,3 +3,10 @@ * contract addresses from `@metamask/delegation-deployments`. */ export const DELEGATION_FRAMEWORK_VERSION = '1.3.0'; + +/** + * `Rule.type` / `wallet_getSupportedExecutionPermissions` `ruleTypes` entry for + * redeemer allowlists (RedeemerEnforcer). Hosts should advertise this for every + * supported execution permission type. + */ +export const EXECUTION_PERMISSION_REDEEMER_RULE_TYPE = 'redeemer' as const; diff --git a/packages/gator-permissions-controller/src/decodePermission/decodePermission.test.ts b/packages/gator-permissions-controller/src/decodePermission/decodePermission.test.ts index 9fa8abf45c1..85c7db8eeca 100644 --- a/packages/gator-permissions-controller/src/decodePermission/decodePermission.test.ts +++ b/packages/gator-permissions-controller/src/decodePermission/decodePermission.test.ts @@ -30,6 +30,7 @@ describe('decodePermission', () => { NativeTokenStreamingEnforcer, NativeTokenPeriodTransferEnforcer, NonceEnforcer, + RedeemerEnforcer, } = contracts; describe('getPermissionRuleMatchingCaveatTypes()', () => { @@ -87,6 +88,35 @@ describe('decodePermission', () => { expect(result.permissionType).toBe(expectedPermissionType); }); + it('allows RedeemerEnforcer as extra', () => { + const enforcers = [ + NativeTokenStreamingEnforcer, + ExactCalldataEnforcer, + NonceEnforcer, + RedeemerEnforcer, + ]; + const result = findRuleWithMatchingCaveatAddresses({ + enforcers, + permissionRules: createPermissionRulesForContracts(contracts), + }); + expect(result.permissionType).toBe(expectedPermissionType); + }); + + it('allows TimestampEnforcer and RedeemerEnforcer as extras', () => { + const enforcers = [ + NativeTokenStreamingEnforcer, + ExactCalldataEnforcer, + NonceEnforcer, + TimestampEnforcer, + RedeemerEnforcer, + ]; + const result = findRuleWithMatchingCaveatAddresses({ + enforcers, + permissionRules: createPermissionRulesForContracts(contracts), + }); + expect(result.permissionType).toBe(expectedPermissionType); + }); + it('rejects forbidden extra caveat', () => { const enforcers = [ NativeTokenStreamingEnforcer, @@ -589,6 +619,41 @@ describe('decodePermission', () => { expect(result.origin).toBe(specifiedOrigin); }); + it('includes rules when provided', () => { + const permissionType = 'native-token-stream' as const; + const data: DecodedPermission['permission']['data'] = { + initialAmount: '0x01', + maxAmount: '0x02', + amountPerSecond: '0x03', + startTime: 1715664, + } as const; + const rules = [ + { + type: 'redeemer' as const, + data: { + addresses: [ + '0x1111111111111111111111111111111111111111' as Hex, + ], + }, + }, + ]; + + const result = reconstructDecodedPermission({ + chainId, + permissionType, + delegator, + delegate, + authority: ROOT_AUTHORITY, + expiry: null, + data, + justification, + specifiedOrigin, + rules, + }); + + expect(result.rules).toStrictEqual(rules); + }); + it('constructs DecodedPermission with null expiry', () => { const permissionType = 'erc20-token-periodic' as const; const data: DecodedPermission['permission']['data'] = { diff --git a/packages/gator-permissions-controller/src/decodePermission/decodePermission.ts b/packages/gator-permissions-controller/src/decodePermission/decodePermission.ts index e8997009c66..e5405ba2c25 100644 --- a/packages/gator-permissions-controller/src/decodePermission/decodePermission.ts +++ b/packages/gator-permissions-controller/src/decodePermission/decodePermission.ts @@ -63,7 +63,8 @@ export const findRuleWithMatchingCaveatAddresses = ({ * @param args.data - Permission-specific decoded data payload. * @param args.justification - Human-readable justification for the permission. * @param args.specifiedOrigin - The origin reported in the request metadata. - * + * @param args.rules - Rules recovered from caveats (e.g. redeemer allowlist). + * * @returns The reconstructed {@link DecodedPermission}. */ export const reconstructDecodedPermission = ({ @@ -76,6 +77,7 @@ export const reconstructDecodedPermission = ({ data, justification, specifiedOrigin, + rules, }: { chainId: number; permissionType: PermissionType; @@ -86,6 +88,7 @@ export const reconstructDecodedPermission = ({ data: DecodedPermission['permission']['data']; justification: string; specifiedOrigin: string; + rules?: DecodedPermission['rules']; }): DecodedPermission => { if (authority !== ROOT_AUTHORITY) { throw new Error('Invalid authority'); @@ -102,6 +105,7 @@ export const reconstructDecodedPermission = ({ }, expiry, origin: specifiedOrigin, + ...(rules !== undefined ? { rules } : {}), }; return permission; diff --git a/packages/gator-permissions-controller/src/decodePermission/redeemer.test.ts b/packages/gator-permissions-controller/src/decodePermission/redeemer.test.ts new file mode 100644 index 00000000000..a9fc1dbfbf4 --- /dev/null +++ b/packages/gator-permissions-controller/src/decodePermission/redeemer.test.ts @@ -0,0 +1,39 @@ +import type { Hex } from '@metamask/utils'; +import { getChecksumAddress } from '@metamask/utils'; + +import { decodeRedeemerEnforcerTerms } from './redeemer'; + +describe('decodeRedeemerEnforcerTerms', () => { + it('decodes a single packed address', () => { + const raw = + '1111111111111111111111111111111111111111' as const; + const terms = `0x${raw}` as Hex; + expect(decodeRedeemerEnforcerTerms(terms)).toStrictEqual([ + getChecksumAddress(`0x${raw}` as Hex), + ]); + }); + + it('decodes two concatenated addresses', () => { + const a = '1111111111111111111111111111111111111111'; + const b = '2222222222222222222222222222222222222222'; + const terms = `0x${a}${b}` as Hex; + expect(decodeRedeemerEnforcerTerms(terms)).toStrictEqual([ + getChecksumAddress(`0x${a}` as Hex), + getChecksumAddress(`0x${b}` as Hex), + ]); + }); + + it('rejects empty payload', () => { + expect(() => decodeRedeemerEnforcerTerms('0x' as Hex)).toThrow( + 'Invalid redeemer enforcer terms: empty payload', + ); + }); + + it('rejects length not divisible by 20 bytes', () => { + expect(() => + decodeRedeemerEnforcerTerms('0x11' as Hex), + ).toThrow( + 'Invalid redeemer enforcer terms: length must be a multiple of 20 bytes', + ); + }); +}); diff --git a/packages/gator-permissions-controller/src/decodePermission/redeemer.ts b/packages/gator-permissions-controller/src/decodePermission/redeemer.ts new file mode 100644 index 00000000000..183b6e7f449 --- /dev/null +++ b/packages/gator-permissions-controller/src/decodePermission/redeemer.ts @@ -0,0 +1,41 @@ +import { getChecksumAddress } from '@metamask/utils'; +import type { Hex } from '@metamask/utils'; + +import { getByteLength } from './utils'; + +/** + * Decodes {@link https://github.com/MetaMask/delegation-framework/blob/main/src/enforcers/RedeemerEnforcer.sol RedeemerEnforcer} + * caveat terms: concatenated 20-byte Ethereum addresses (no ABI header). + * + * @param terms - Hex-encoded packed addresses from the caveat. + * @returns Checksummed redeemer addresses. + */ +export function decodeRedeemerEnforcerTerms(terms: Hex): Hex[] { + if (terms === '0x') { + throw new Error('Invalid redeemer enforcer terms: empty payload'); + } + + const byteLength = getByteLength(terms); + + if (byteLength === 0) { + throw new Error( + 'Invalid redeemer enforcer terms: length must be positive', + ); + } + + if (byteLength % 20 !== 0) { + throw new Error( + 'Invalid redeemer enforcer terms: length must be a multiple of 20 bytes', + ); + } + + const addresses: Hex[] = []; + const body = terms.slice(2); + + for (let i = 0; i < body.length; i += 40) { + const slice = body.slice(i, i + 40); + addresses.push(getChecksumAddress(`0x${slice}` as Hex)); + } + + return addresses; +} diff --git a/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenPeriodic.ts b/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenPeriodic.ts index 81be534fdc8..ccacd2b17f4 100644 --- a/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenPeriodic.ts +++ b/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenPeriodic.ts @@ -28,10 +28,12 @@ export function makeErc20TokenPeriodicRule( erc20PeriodicEnforcer, valueLteEnforcer, nonceEnforcer, + redeemerEnforcer, } = enforcers; return makePermissionRule({ permissionType: 'erc20-token-periodic', optionalEnforcers: [timestampEnforcer], + redeemerEnforcer, timestampEnforcer, requiredEnforcers: { [erc20PeriodicEnforcer]: 1, diff --git a/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenRevocation.ts b/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenRevocation.ts index 728d85ea641..cd74822b2b3 100644 --- a/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenRevocation.ts +++ b/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenRevocation.ts @@ -26,10 +26,12 @@ export function makeErc20TokenRevocationRule( allowedCalldataEnforcer, valueLteEnforcer, nonceEnforcer, + redeemerEnforcer, } = enforcers; return makePermissionRule({ permissionType: 'erc20-token-revocation', optionalEnforcers: [timestampEnforcer], + redeemerEnforcer, timestampEnforcer, requiredEnforcers: { [allowedCalldataEnforcer]: 2, diff --git a/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenStream.ts b/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenStream.ts index 4aa1c5b022f..1640921cc19 100644 --- a/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenStream.ts +++ b/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenStream.ts @@ -28,10 +28,12 @@ export function makeErc20TokenStreamRule( erc20StreamingEnforcer, valueLteEnforcer, nonceEnforcer, + redeemerEnforcer, } = enforcers; return makePermissionRule({ permissionType: 'erc20-token-stream', optionalEnforcers: [timestampEnforcer], + redeemerEnforcer, timestampEnforcer, requiredEnforcers: { [erc20StreamingEnforcer]: 1, diff --git a/packages/gator-permissions-controller/src/decodePermission/rules/makePermissionRule.test.ts b/packages/gator-permissions-controller/src/decodePermission/rules/makePermissionRule.test.ts index be7128a161e..37da7889dee 100644 --- a/packages/gator-permissions-controller/src/decodePermission/rules/makePermissionRule.test.ts +++ b/packages/gator-permissions-controller/src/decodePermission/rules/makePermissionRule.test.ts @@ -3,6 +3,7 @@ import { CHAIN_ID, DELEGATOR_CONTRACTS, } from '@metamask/delegation-deployments'; +import { getChecksumAddress } from '@metamask/utils'; import type { Hex } from '@metamask/utils'; import { makePermissionRule } from './makePermissionRule'; @@ -11,6 +12,7 @@ describe('makePermissionRule', () => { const contracts = DELEGATOR_CONTRACTS['1.3.0'][CHAIN_ID.sepolia]; const timestampEnforcer = contracts.TimestampEnforcer; const requiredEnforcer = contracts.NonceEnforcer; + const redeemerEnforcer = contracts.RedeemerEnforcer; it('calls optional validate callback when provided and decoding succeeds', () => { const validateAndDecodeData = jest.fn().mockReturnValue({}); @@ -18,6 +20,7 @@ describe('makePermissionRule', () => { const rule = makePermissionRule({ permissionType: 'native-token-stream', timestampEnforcer, + redeemerEnforcer, optionalEnforcers: [], requiredEnforcers: { [requiredEnforcer]: 1 }, validateAndDecodeData, @@ -56,6 +59,7 @@ describe('makePermissionRule', () => { const rule = makePermissionRule({ permissionType: 'native-token-stream', timestampEnforcer, + redeemerEnforcer, optionalEnforcers: [], requiredEnforcers: { [requiredEnforcer]: 1 }, validateAndDecodeData, @@ -90,6 +94,7 @@ describe('makePermissionRule', () => { const rule = makePermissionRule({ permissionType: 'native-token-stream', timestampEnforcer, + redeemerEnforcer, optionalEnforcers: [], requiredEnforcers: { [requiredEnforcer]: 1 }, validateAndDecodeData, @@ -125,6 +130,7 @@ describe('makePermissionRule', () => { const rule = makePermissionRule({ permissionType: 'native-token-stream', timestampEnforcer, + redeemerEnforcer, optionalEnforcers: [], requiredEnforcers: { [requiredEnforcer]: 1 }, validateAndDecodeData, @@ -162,6 +168,7 @@ describe('makePermissionRule', () => { const rule = makePermissionRule({ permissionType: 'native-token-stream', timestampEnforcer, + redeemerEnforcer, optionalEnforcers: [], requiredEnforcers: { [requiredEnforcer]: 1 }, validateAndDecodeData, @@ -200,6 +207,7 @@ describe('makePermissionRule', () => { const rule = makePermissionRule({ permissionType: 'native-token-stream', timestampEnforcer, + redeemerEnforcer, optionalEnforcers: [], requiredEnforcers: { [requiredEnforcer]: 1 }, validateAndDecodeData, @@ -221,4 +229,51 @@ describe('makePermissionRule', () => { } expect(validateAndDecodeData).toHaveBeenCalled(); }); + + it('includes redeemer rule when RedeemerEnforcer caveat is present', () => { + const validateAndDecodeData = jest.fn().mockReturnValue({}); + const packedAddr = + '0000000000000000000000001111111111111111111111111111111111111111' as const; + + const rule = makePermissionRule({ + permissionType: 'native-token-stream', + timestampEnforcer, + redeemerEnforcer, + optionalEnforcers: [], + requiredEnforcers: { [requiredEnforcer]: 1 }, + validateAndDecodeData, + }); + + const caveats = [ + { + enforcer: requiredEnforcer, + terms: '0x' as Hex, + args: '0x' as Hex, + }, + { + enforcer: redeemerEnforcer, + terms: `0x${packedAddr}` as Hex, + args: '0x' as Hex, + }, + ]; + + const result = rule.validateAndDecodePermission(caveats); + + expect(result.isValid).toBe(true); + if (!result.isValid) { + throw new Error('Expected valid result'); + } + expect(result.rules).toEqual([ + { + type: 'redeemer', + data: { + addresses: [ + getChecksumAddress( + '0x1111111111111111111111111111111111111111' as Hex, + ), + ], + }, + }, + ]); + }); }); diff --git a/packages/gator-permissions-controller/src/decodePermission/rules/makePermissionRule.ts b/packages/gator-permissions-controller/src/decodePermission/rules/makePermissionRule.ts index d7cdf8b8694..aadd23e62ef 100644 --- a/packages/gator-permissions-controller/src/decodePermission/rules/makePermissionRule.ts +++ b/packages/gator-permissions-controller/src/decodePermission/rules/makePermissionRule.ts @@ -1,7 +1,10 @@ import type { Caveat } from '@metamask/delegation-core'; +import type { Rule } from '@metamask/7715-permission-types'; import { getChecksumAddress, isStrictHexString } from '@metamask/utils'; import type { Hex } from '@metamask/utils'; +import { EXECUTION_PERMISSION_REDEEMER_RULE_TYPE } from '../../constants'; +import { decodeRedeemerEnforcerTerms } from '../redeemer'; import type { ChecksumCaveat, DecodedPermission, @@ -22,6 +25,7 @@ import { * * @param args - The arguments to this function. * @param args.optionalEnforcers - Enforcer addresses that may appear in addition to required. + * @param args.redeemerEnforcer - Address of the RedeemerEnforcer (optional caveat). * @param args.timestampEnforcer - Address of the TimestampEnforcer used to extract expiry. * @param args.permissionType - The permission type identifier. * @param args.requiredEnforcers - Map of required enforcer address to required count. @@ -30,12 +34,14 @@ import { */ export function makePermissionRule({ optionalEnforcers, + redeemerEnforcer, timestampEnforcer, permissionType, requiredEnforcers, validateAndDecodeData, }: { optionalEnforcers: Hex[]; + redeemerEnforcer: Hex; timestampEnforcer: Hex; permissionType: PermissionType; requiredEnforcers: Record; @@ -43,7 +49,10 @@ export function makePermissionRule({ caveats: ChecksumCaveat[], ) => DecodedPermission['permission']['data']; }): PermissionRule { - const optionalEnforcersSet = new Set(optionalEnforcers); + const optionalEnforcersSet = new Set([ + ...optionalEnforcers, + redeemerEnforcer, + ]); const requiredEnforcersMap = new Map( Object.entries(requiredEnforcers), ) as Map; @@ -94,7 +103,25 @@ export function makePermissionRule({ const data = validateAndDecodeData(checksumCaveats); - return { isValid: true, expiry, data }; + const redeemerTerms = getTermsByEnforcer({ + caveats: checksumCaveats, + enforcer: redeemerEnforcer, + throwIfNotFound: false, + }); + + let rules: Rule[] | undefined; + if (redeemerTerms) { + rules = [ + { + type: EXECUTION_PERMISSION_REDEEMER_RULE_TYPE, + data: { + addresses: decodeRedeemerEnforcerTerms(redeemerTerms), + }, + }, + ]; + } + + return { isValid: true, expiry, data, rules }; } catch (caughtError) { return { isValid: false, error: caughtError as Error }; } diff --git a/packages/gator-permissions-controller/src/decodePermission/rules/nativeTokenPeriodic.ts b/packages/gator-permissions-controller/src/decodePermission/rules/nativeTokenPeriodic.ts index 8309dc8c5ae..3697ff040d8 100644 --- a/packages/gator-permissions-controller/src/decodePermission/rules/nativeTokenPeriodic.ts +++ b/packages/gator-permissions-controller/src/decodePermission/rules/nativeTokenPeriodic.ts @@ -23,10 +23,12 @@ export function makeNativeTokenPeriodicRule( nativeTokenPeriodicEnforcer, exactCalldataEnforcer, nonceEnforcer, + redeemerEnforcer, } = enforcers; return makePermissionRule({ permissionType: 'native-token-periodic', optionalEnforcers: [timestampEnforcer], + redeemerEnforcer, timestampEnforcer, requiredEnforcers: { [nativeTokenPeriodicEnforcer]: 1, diff --git a/packages/gator-permissions-controller/src/decodePermission/rules/nativeTokenStream.ts b/packages/gator-permissions-controller/src/decodePermission/rules/nativeTokenStream.ts index dfdd7f37a9e..36763db604f 100644 --- a/packages/gator-permissions-controller/src/decodePermission/rules/nativeTokenStream.ts +++ b/packages/gator-permissions-controller/src/decodePermission/rules/nativeTokenStream.ts @@ -23,10 +23,12 @@ export function makeNativeTokenStreamRule( nativeTokenStreamingEnforcer, exactCalldataEnforcer, nonceEnforcer, + redeemerEnforcer, } = enforcers; return makePermissionRule({ permissionType: 'native-token-stream', optionalEnforcers: [timestampEnforcer], + redeemerEnforcer, timestampEnforcer, requiredEnforcers: { [nativeTokenStreamingEnforcer]: 1, diff --git a/packages/gator-permissions-controller/src/decodePermission/types.ts b/packages/gator-permissions-controller/src/decodePermission/types.ts index d6a6e8ac0dd..c263ff84ddb 100644 --- a/packages/gator-permissions-controller/src/decodePermission/types.ts +++ b/packages/gator-permissions-controller/src/decodePermission/types.ts @@ -1,6 +1,7 @@ import type { PermissionRequest, PermissionTypes, + Rule, } from '@metamask/7715-permission-types'; import type { Caveat } from '@metamask/delegation-core'; import type { DELEGATOR_CONTRACTS } from '@metamask/delegation-deployments'; @@ -31,6 +32,8 @@ export type DecodedPermission = Pick< }; expiry: number | null; origin: string; + /** Rules recovered from caveats (e.g. redeemer allowlist). */ + rules?: Rule[]; }; /** @@ -51,6 +54,7 @@ export type ChecksumEnforcersByChainId = { timestampEnforcer: Hex; nonceEnforcer: Hex; allowedCalldataEnforcer: Hex; + redeemerEnforcer: Hex; }; /** Caveat with checksummed enforcer address; used by rule decode functions. */ @@ -65,6 +69,7 @@ export type ValidateAndDecodeResult = isValid: true; expiry: number | null; data: DecodedPermission['permission']['data']; + rules?: Rule[]; } | { isValid: false; error: Error }; diff --git a/packages/gator-permissions-controller/src/decodePermission/utils.test.ts b/packages/gator-permissions-controller/src/decodePermission/utils.test.ts index 78992f5494c..b81c286f56c 100644 --- a/packages/gator-permissions-controller/src/decodePermission/utils.test.ts +++ b/packages/gator-permissions-controller/src/decodePermission/utils.test.ts @@ -22,6 +22,7 @@ const buildContracts = (): DeployedContractsByName => ({ ValueLteEnforcer: '0x7777777777777777777777777777777777777777', NonceEnforcer: '0x8888888888888888888888888888888888888888', AllowedCalldataEnforcer: '0x9999999999999999999999999999999999999999', + RedeemerEnforcer: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', }); describe('getChecksumEnforcersByChainId', () => { @@ -51,6 +52,7 @@ describe('getChecksumEnforcersByChainId', () => { allowedCalldataEnforcer: getChecksumAddress( contracts.AllowedCalldataEnforcer, ), + redeemerEnforcer: getChecksumAddress(contracts.RedeemerEnforcer), }); }); @@ -76,6 +78,7 @@ describe('createPermissionRulesForChainId', () => { timestampEnforcer, nonceEnforcer, allowedCalldataEnforcer, + redeemerEnforcer, } = getChecksumEnforcersByChainId(contracts); // erc20-token-stream @@ -96,10 +99,13 @@ describe('createPermissionRulesForChainId', () => { expect(byType['native-token-stream'].permissionType).toBe( 'native-token-stream', ); - expect(byType['native-token-stream'].optionalEnforcers.size).toBe(1); + expect(byType['native-token-stream'].optionalEnforcers.size).toBe(2); expect( byType['native-token-stream'].optionalEnforcers.has(timestampEnforcer), ).toBe(true); + expect( + byType['native-token-stream'].optionalEnforcers.has(redeemerEnforcer), + ).toBe(true); expect(byType['native-token-stream'].requiredEnforcers.size).toBe(3); expect( Array.from(byType['native-token-stream'].requiredEnforcers.entries()), @@ -116,10 +122,13 @@ describe('createPermissionRulesForChainId', () => { expect(byType['native-token-periodic'].permissionType).toBe( 'native-token-periodic', ); - expect(byType['native-token-periodic'].optionalEnforcers.size).toBe(1); + expect(byType['native-token-periodic'].optionalEnforcers.size).toBe(2); expect( byType['native-token-periodic'].optionalEnforcers.has(timestampEnforcer), ).toBe(true); + expect( + byType['native-token-periodic'].optionalEnforcers.has(redeemerEnforcer), + ).toBe(true); expect(byType['native-token-periodic'].requiredEnforcers.size).toBe(3); expect( Array.from(byType['native-token-periodic'].requiredEnforcers.entries()), @@ -136,10 +145,13 @@ describe('createPermissionRulesForChainId', () => { expect(byType['erc20-token-stream'].permissionType).toBe( 'erc20-token-stream', ); - expect(byType['erc20-token-stream'].optionalEnforcers.size).toBe(1); + expect(byType['erc20-token-stream'].optionalEnforcers.size).toBe(2); expect( byType['erc20-token-stream'].optionalEnforcers.has(timestampEnforcer), ).toBe(true); + expect( + byType['erc20-token-stream'].optionalEnforcers.has(redeemerEnforcer), + ).toBe(true); expect(byType['erc20-token-stream'].requiredEnforcers.size).toBe(3); expect( Array.from(byType['erc20-token-stream'].requiredEnforcers.entries()), @@ -156,10 +168,13 @@ describe('createPermissionRulesForChainId', () => { expect(byType['erc20-token-periodic'].permissionType).toBe( 'erc20-token-periodic', ); - expect(byType['erc20-token-periodic'].optionalEnforcers.size).toBe(1); + expect(byType['erc20-token-periodic'].optionalEnforcers.size).toBe(2); expect( byType['erc20-token-periodic'].optionalEnforcers.has(timestampEnforcer), ).toBe(true); + expect( + byType['erc20-token-periodic'].optionalEnforcers.has(redeemerEnforcer), + ).toBe(true); expect(byType['erc20-token-periodic'].requiredEnforcers.size).toBe(3); expect( Array.from(byType['erc20-token-periodic'].requiredEnforcers.entries()), @@ -176,10 +191,13 @@ describe('createPermissionRulesForChainId', () => { expect(byType['erc20-token-revocation'].permissionType).toBe( 'erc20-token-revocation', ); - expect(byType['erc20-token-revocation'].optionalEnforcers.size).toBe(1); + expect(byType['erc20-token-revocation'].optionalEnforcers.size).toBe(2); expect( byType['erc20-token-revocation'].optionalEnforcers.has(timestampEnforcer), ).toBe(true); + expect( + byType['erc20-token-revocation'].optionalEnforcers.has(redeemerEnforcer), + ).toBe(true); expect(byType['erc20-token-revocation'].requiredEnforcers.size).toBe(3); expect( Array.from(byType['erc20-token-revocation'].requiredEnforcers.entries()), diff --git a/packages/gator-permissions-controller/src/decodePermission/utils.ts b/packages/gator-permissions-controller/src/decodePermission/utils.ts index 4a40671cbd5..e239972ca22 100644 --- a/packages/gator-permissions-controller/src/decodePermission/utils.ts +++ b/packages/gator-permissions-controller/src/decodePermission/utils.ts @@ -20,6 +20,7 @@ const ENFORCER_CONTRACT_NAMES = { ValueLteEnforcer: 'ValueLteEnforcer', NonceEnforcer: 'NonceEnforcer', AllowedCalldataEnforcer: 'AllowedCalldataEnforcer', + RedeemerEnforcer: 'RedeemerEnforcer', }; /** @@ -99,6 +100,10 @@ export const getChecksumEnforcersByChainId = ( ENFORCER_CONTRACT_NAMES.AllowedCalldataEnforcer, ); + const redeemerEnforcer = getChecksumContractAddress( + ENFORCER_CONTRACT_NAMES.RedeemerEnforcer, + ); + return { erc20StreamingEnforcer, erc20PeriodicEnforcer, @@ -109,6 +114,7 @@ export const getChecksumEnforcersByChainId = ( timestampEnforcer, nonceEnforcer, allowedCalldataEnforcer, + redeemerEnforcer, }; }; diff --git a/packages/gator-permissions-controller/src/index.ts b/packages/gator-permissions-controller/src/index.ts index dd34e049f9b..125248cb729 100644 --- a/packages/gator-permissions-controller/src/index.ts +++ b/packages/gator-permissions-controller/src/index.ts @@ -1,4 +1,8 @@ export { default as GatorPermissionsController } from './GatorPermissionsController'; +export { + DELEGATION_FRAMEWORK_VERSION, + EXECUTION_PERMISSION_REDEEMER_RULE_TYPE, +} from './constants'; export type { GatorPermissionsControllerFetchAndUpdateGatorPermissionsAction, GatorPermissionsControllerAddPendingRevocationAction, @@ -18,7 +22,6 @@ export type { GatorPermissionsControllerStateChangeEvent, } from './GatorPermissionsController'; export type { DecodedPermission } from './decodePermission'; -export { DELEGATION_FRAMEWORK_VERSION } from './constants'; export type { GatorPermissionsControllerErrorCode, GatorPermissionsSnapRpcMethod, @@ -34,6 +37,7 @@ export type { SupportedPermissionType, } from './types'; +export type { RedeemerRule } from './redeemerRule'; export type { NativeTokenStreamPermission, NativeTokenPeriodicPermission, diff --git a/packages/gator-permissions-controller/src/redeemerRule.ts b/packages/gator-permissions-controller/src/redeemerRule.ts new file mode 100644 index 00000000000..43ef16b6f89 --- /dev/null +++ b/packages/gator-permissions-controller/src/redeemerRule.ts @@ -0,0 +1,12 @@ +import type { Hex } from '@metamask/utils'; + +/** + * Execution permission rule restricting which addresses may redeem the delegation + * (on-chain RedeemerEnforcer caveat). + */ +export type RedeemerRule = { + type: 'redeemer'; + data: { + addresses: Hex[]; + }; +};