diff --git a/packages/gator-permissions-controller/src/GatorPermissionsController.ts b/packages/gator-permissions-controller/src/GatorPermissionsController.ts index 213ce5442b..652513bdf9 100644 --- a/packages/gator-permissions-controller/src/GatorPermissionsController.ts +++ b/packages/gator-permissions-controller/src/GatorPermissionsController.ts @@ -30,10 +30,10 @@ import type { Hex } from '@metamask/utils'; import { DELEGATION_FRAMEWORK_VERSION } from './constants'; import type { DecodedPermission } from './decodePermission'; import { - createPermissionRulesForContracts, - findRulesWithMatchingCaveatAddresses, + createPermissionDecodersForContracts, + findDecodersWithMatchingCaveatAddresses, reconstructDecodedPermission, - selectUniqueRuleAndDecodedPermission, + selectUniqueDecoderAndDecodedPermission, } from './decodePermission'; import { GatorPermissionsFetchError, @@ -592,22 +592,23 @@ export class GatorPermissionsController extends BaseController< try { const enforcers = caveats.map((caveat) => caveat.enforcer); - const permissionRules = createPermissionRulesForContracts(contracts); + const permissionDecoders = + createPermissionDecodersForContracts(contracts); - // Every rule where enforcer addresses match; multiple types may share the same - // caveat pattern and are disambiguated by validateAndDecodePermission. - const matchingRules = findRulesWithMatchingCaveatAddresses({ + // Every decoder where enforcer addresses match; multiple types may share the + // same caveat pattern and are disambiguated by validateAndDecodePermission. + const matchingDecoders = findDecodersWithMatchingCaveatAddresses({ enforcers, - permissionRules, + permissionDecoders, }); const { - rule: { permissionType }, + decoder: { permissionType }, expiry, data, rules, - } = selectUniqueRuleAndDecodedPermission({ - candidateRules: matchingRules, + } = selectUniqueDecoderAndDecodedPermission({ + candidateDecoders: matchingDecoders, caveats, }); diff --git a/packages/gator-permissions-controller/src/constants.ts b/packages/gator-permissions-controller/src/constants.ts index 7a4ad01c57..86efec8966 100644 --- a/packages/gator-permissions-controller/src/constants.ts +++ b/packages/gator-permissions-controller/src/constants.ts @@ -18,3 +18,11 @@ export const EXECUTION_PERMISSION_REDEEMER_RULE_TYPE = 'redeemer' as const; * payee restrictions. */ export const EXECUTION_PERMISSION_PAYEE_RULE_TYPE = 'payee' as const; + +/** + * `Rule.type` / `wallet_getSupportedExecutionPermissions` `ruleTypes` entry for + * permission expiry derived from a TimestampEnforcer caveat. The decoded + * permission additionally hoists the expiry value onto its top-level `expiry` + * field for convenience. + */ +export const EXECUTION_PERMISSION_EXPIRY_RULE_TYPE = 'expiry' 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 2c45defb42..37e30703d6 100644 --- a/packages/gator-permissions-controller/src/decodePermission/decodePermission.test.ts +++ b/packages/gator-permissions-controller/src/decodePermission/decodePermission.test.ts @@ -7,16 +7,16 @@ import { import { numberToHex } from '@metamask/utils'; import { - findRuleWithMatchingCaveatAddresses, - findRulesWithMatchingCaveatAddresses, + findDecoderWithMatchingCaveatAddresses, + findDecodersWithMatchingCaveatAddresses, reconstructDecodedPermission, - selectUniqueRuleAndDecodedPermission, + selectUniqueDecoderAndDecodedPermission, } from './decodePermission'; -import { createPermissionRulesForContracts } from './rules'; +import { createPermissionDecodersForContracts } from './decoders'; import type { DecodedPermission, DeployedContractsByName, - PermissionRule, + PermissionDecoder, } from './types'; // These tests use the live deployments table for version 1.3.0 to @@ -39,7 +39,7 @@ describe('decodePermission', () => { RedeemerEnforcer, } = contracts; - describe('getPermissionRuleMatchingCaveatTypes()', () => { + describe('getPermissionDecoderMatchingCaveatTypes()', () => { const zeroAddress = '0x0000000000000000000000000000000000000000' as Hex; it('throws if multiple permission types match', () => { @@ -55,16 +55,16 @@ describe('decodePermission', () => { } as unknown as DeployedContractsByName; expect(() => { - findRuleWithMatchingCaveatAddresses({ + findDecoderWithMatchingCaveatAddresses({ enforcers, - permissionRules: createPermissionRulesForContracts( + permissionDecoders: createPermissionDecodersForContracts( contractsWithDuplicates, ), }); }).toThrow('Multiple permission types match'); }); - it('returns all matching rules from findRulesWithMatchingCaveatAddresses', () => { + it('returns all matching rules from findDecodersWithMatchingCaveatAddresses', () => { const enforcers = [ExactCalldataEnforcer, NonceEnforcer, zeroAddress]; const contractsWithDuplicates = { ...contracts, @@ -72,9 +72,9 @@ describe('decodePermission', () => { NativeTokenPeriodTransferEnforcer: zeroAddress, } as unknown as DeployedContractsByName; - const rules = findRulesWithMatchingCaveatAddresses({ + const rules = findDecodersWithMatchingCaveatAddresses({ enforcers, - permissionRules: createPermissionRulesForContracts( + permissionDecoders: createPermissionDecodersForContracts( contractsWithDuplicates, ), }); @@ -100,9 +100,9 @@ describe('decodePermission', () => { ExactCalldataEnforcer, NonceEnforcer, ]; - const result = findRuleWithMatchingCaveatAddresses({ + const result = findDecoderWithMatchingCaveatAddresses({ enforcers, - permissionRules: createPermissionRulesForContracts(contracts), + permissionDecoders: createPermissionDecodersForContracts(contracts), }); expect(result.permissionType).toBe(expectedPermissionType); }); @@ -114,9 +114,9 @@ describe('decodePermission', () => { NonceEnforcer, TimestampEnforcer, ]; - const result = findRuleWithMatchingCaveatAddresses({ + const result = findDecoderWithMatchingCaveatAddresses({ enforcers, - permissionRules: createPermissionRulesForContracts(contracts), + permissionDecoders: createPermissionDecodersForContracts(contracts), }); expect(result.permissionType).toBe(expectedPermissionType); }); @@ -128,9 +128,9 @@ describe('decodePermission', () => { NonceEnforcer, RedeemerEnforcer, ]; - const result = findRuleWithMatchingCaveatAddresses({ + const result = findDecoderWithMatchingCaveatAddresses({ enforcers, - permissionRules: createPermissionRulesForContracts(contracts), + permissionDecoders: createPermissionDecodersForContracts(contracts), }); expect(result.permissionType).toBe(expectedPermissionType); }); @@ -143,9 +143,9 @@ describe('decodePermission', () => { TimestampEnforcer, RedeemerEnforcer, ]; - const result = findRuleWithMatchingCaveatAddresses({ + const result = findDecoderWithMatchingCaveatAddresses({ enforcers, - permissionRules: createPermissionRulesForContracts(contracts), + permissionDecoders: createPermissionDecodersForContracts(contracts), }); expect(result.permissionType).toBe(expectedPermissionType); }); @@ -159,9 +159,9 @@ describe('decodePermission', () => { ValueLteEnforcer, ]; expect(() => - findRuleWithMatchingCaveatAddresses({ + findDecoderWithMatchingCaveatAddresses({ enforcers, - permissionRules: createPermissionRulesForContracts(contracts), + permissionDecoders: createPermissionDecodersForContracts(contracts), }), ).toThrow('Unable to identify permission type'); }); @@ -169,9 +169,9 @@ describe('decodePermission', () => { it('rejects when required caveats are missing', () => { const enforcers = [ExactCalldataEnforcer]; expect(() => - findRuleWithMatchingCaveatAddresses({ + findDecoderWithMatchingCaveatAddresses({ enforcers, - permissionRules: createPermissionRulesForContracts(contracts), + permissionDecoders: createPermissionDecodersForContracts(contracts), }), ).toThrow('Unable to identify permission type'); }); @@ -182,9 +182,9 @@ describe('decodePermission', () => { ExactCalldataEnforcer.toLowerCase() as unknown as Hex, NonceEnforcer.toLowerCase() as unknown as Hex, ]; - const result = findRuleWithMatchingCaveatAddresses({ + const result = findDecoderWithMatchingCaveatAddresses({ enforcers, - permissionRules: createPermissionRulesForContracts(contracts), + permissionDecoders: createPermissionDecodersForContracts(contracts), }); expect(result.permissionType).toBe('native-token-stream'); }); @@ -201,9 +201,9 @@ describe('decodePermission', () => { } as unknown as DeployedContractsByName; expect(() => - findRuleWithMatchingCaveatAddresses({ + findDecoderWithMatchingCaveatAddresses({ enforcers, - permissionRules: createPermissionRulesForContracts( + permissionDecoders: createPermissionDecodersForContracts( contractsWithoutTimestampEnforcer, ), }), @@ -219,9 +219,9 @@ describe('decodePermission', () => { ExactCalldataEnforcer, NonceEnforcer, ]; - const rules = findRulesWithMatchingCaveatAddresses({ + const rules = findDecodersWithMatchingCaveatAddresses({ enforcers, - permissionRules: createPermissionRulesForContracts(contracts), + permissionDecoders: createPermissionDecodersForContracts(contracts), }); expect( rules.map((matchingRule) => matchingRule.permissionType).sort(), @@ -237,9 +237,9 @@ describe('decodePermission', () => { NonceEnforcer, TimestampEnforcer, ]; - const rules = findRulesWithMatchingCaveatAddresses({ + const rules = findDecodersWithMatchingCaveatAddresses({ enforcers, - permissionRules: createPermissionRulesForContracts(contracts), + permissionDecoders: createPermissionDecodersForContracts(contracts), }); expect( rules.map((matchingRule) => matchingRule.permissionType).sort(), @@ -257,9 +257,9 @@ describe('decodePermission', () => { ValueLteEnforcer, ]; expect(() => - findRuleWithMatchingCaveatAddresses({ + findDecoderWithMatchingCaveatAddresses({ enforcers, - permissionRules: createPermissionRulesForContracts(contracts), + permissionDecoders: createPermissionDecodersForContracts(contracts), }), ).toThrow('Unable to identify permission type'); }); @@ -267,9 +267,9 @@ describe('decodePermission', () => { it('rejects when required caveats are missing', () => { const enforcers = [ExactCalldataEnforcer]; expect(() => - findRuleWithMatchingCaveatAddresses({ + findDecoderWithMatchingCaveatAddresses({ enforcers, - permissionRules: createPermissionRulesForContracts(contracts), + permissionDecoders: createPermissionDecodersForContracts(contracts), }), ).toThrow('Unable to identify permission type'); }); @@ -280,9 +280,9 @@ describe('decodePermission', () => { ExactCalldataEnforcer.toLowerCase() as unknown as Hex, NonceEnforcer.toLowerCase() as unknown as Hex, ]; - const rules = findRulesWithMatchingCaveatAddresses({ + const rules = findDecodersWithMatchingCaveatAddresses({ enforcers, - permissionRules: createPermissionRulesForContracts(contracts), + permissionDecoders: createPermissionDecodersForContracts(contracts), }); expect( rules.map((matchingRule) => matchingRule.permissionType).sort(), @@ -303,9 +303,9 @@ describe('decodePermission', () => { } as unknown as DeployedContractsByName; expect(() => - findRuleWithMatchingCaveatAddresses({ + findDecoderWithMatchingCaveatAddresses({ enforcers, - permissionRules: createPermissionRulesForContracts( + permissionDecoders: createPermissionDecodersForContracts( contractsWithoutTimestampEnforcer, ), }), @@ -321,9 +321,9 @@ describe('decodePermission', () => { ValueLteEnforcer, NonceEnforcer, ]; - const result = findRuleWithMatchingCaveatAddresses({ + const result = findDecoderWithMatchingCaveatAddresses({ enforcers, - permissionRules: createPermissionRulesForContracts(contracts), + permissionDecoders: createPermissionDecodersForContracts(contracts), }); expect(result.permissionType).toBe(expectedPermissionType); }); @@ -335,9 +335,9 @@ describe('decodePermission', () => { NonceEnforcer, TimestampEnforcer, ]; - const result = findRuleWithMatchingCaveatAddresses({ + const result = findDecoderWithMatchingCaveatAddresses({ enforcers, - permissionRules: createPermissionRulesForContracts(contracts), + permissionDecoders: createPermissionDecodersForContracts(contracts), }); expect(result.permissionType).toBe(expectedPermissionType); }); @@ -351,9 +351,9 @@ describe('decodePermission', () => { ExactCalldataEnforcer, ]; expect(() => - findRuleWithMatchingCaveatAddresses({ + findDecoderWithMatchingCaveatAddresses({ enforcers, - permissionRules: createPermissionRulesForContracts(contracts), + permissionDecoders: createPermissionDecodersForContracts(contracts), }), ).toThrow('Unable to identify permission type'); }); @@ -361,9 +361,9 @@ describe('decodePermission', () => { it('rejects when required caveats are missing', () => { const enforcers = [ERC20StreamingEnforcer]; expect(() => - findRuleWithMatchingCaveatAddresses({ + findDecoderWithMatchingCaveatAddresses({ enforcers, - permissionRules: createPermissionRulesForContracts(contracts), + permissionDecoders: createPermissionDecodersForContracts(contracts), }), ).toThrow('Unable to identify permission type'); }); @@ -374,9 +374,9 @@ describe('decodePermission', () => { ValueLteEnforcer.toLowerCase() as unknown as Hex, NonceEnforcer.toLowerCase() as unknown as Hex, ]; - const result = findRuleWithMatchingCaveatAddresses({ + const result = findDecoderWithMatchingCaveatAddresses({ enforcers, - permissionRules: createPermissionRulesForContracts(contracts), + permissionDecoders: createPermissionDecodersForContracts(contracts), }); expect(result.permissionType).toBe(expectedPermissionType); }); @@ -393,9 +393,9 @@ describe('decodePermission', () => { } as unknown as DeployedContractsByName; expect(() => - findRuleWithMatchingCaveatAddresses({ + findDecoderWithMatchingCaveatAddresses({ enforcers, - permissionRules: createPermissionRulesForContracts( + permissionDecoders: createPermissionDecodersForContracts( contractsWithoutTimestampEnforcer, ), }), @@ -411,9 +411,9 @@ describe('decodePermission', () => { ValueLteEnforcer, NonceEnforcer, ]; - const rules = findRulesWithMatchingCaveatAddresses({ + const rules = findDecodersWithMatchingCaveatAddresses({ enforcers, - permissionRules: createPermissionRulesForContracts(contracts), + permissionDecoders: createPermissionDecodersForContracts(contracts), }); expect( rules.map((matchingRule) => matchingRule.permissionType).sort(), @@ -429,9 +429,9 @@ describe('decodePermission', () => { NonceEnforcer, TimestampEnforcer, ]; - const rules = findRulesWithMatchingCaveatAddresses({ + const rules = findDecodersWithMatchingCaveatAddresses({ enforcers, - permissionRules: createPermissionRulesForContracts(contracts), + permissionDecoders: createPermissionDecodersForContracts(contracts), }); expect( rules.map((matchingRule) => matchingRule.permissionType).sort(), @@ -449,9 +449,9 @@ describe('decodePermission', () => { ExactCalldataEnforcer, ]; expect(() => - findRuleWithMatchingCaveatAddresses({ + findDecoderWithMatchingCaveatAddresses({ enforcers, - permissionRules: createPermissionRulesForContracts(contracts), + permissionDecoders: createPermissionDecodersForContracts(contracts), }), ).toThrow('Unable to identify permission type'); }); @@ -459,9 +459,9 @@ describe('decodePermission', () => { it('rejects when required caveats are missing', () => { const enforcers = [ERC20PeriodTransferEnforcer]; expect(() => - findRuleWithMatchingCaveatAddresses({ + findDecoderWithMatchingCaveatAddresses({ enforcers, - permissionRules: createPermissionRulesForContracts(contracts), + permissionDecoders: createPermissionDecodersForContracts(contracts), }), ).toThrow('Unable to identify permission type'); }); @@ -472,9 +472,9 @@ describe('decodePermission', () => { ValueLteEnforcer.toLowerCase() as unknown as Hex, NonceEnforcer.toLowerCase() as unknown as Hex, ]; - const rules = findRulesWithMatchingCaveatAddresses({ + const rules = findDecodersWithMatchingCaveatAddresses({ enforcers, - permissionRules: createPermissionRulesForContracts(contracts), + permissionDecoders: createPermissionDecodersForContracts(contracts), }); expect( rules.map((matchingRule) => matchingRule.permissionType).sort(), @@ -495,9 +495,9 @@ describe('decodePermission', () => { } as unknown as DeployedContractsByName; expect(() => - findRuleWithMatchingCaveatAddresses({ + findDecoderWithMatchingCaveatAddresses({ enforcers, - permissionRules: createPermissionRulesForContracts( + permissionDecoders: createPermissionDecodersForContracts( contractsWithoutTimestampEnforcer, ), }), @@ -515,9 +515,9 @@ describe('decodePermission', () => { ValueLteEnforcer, NonceEnforcer, ]; - const result = findRuleWithMatchingCaveatAddresses({ + const result = findDecoderWithMatchingCaveatAddresses({ enforcers, - permissionRules: createPermissionRulesForContracts(contracts), + permissionDecoders: createPermissionDecodersForContracts(contracts), }); expect(result.permissionType).toBe(expectedPermissionType); }); @@ -530,9 +530,9 @@ describe('decodePermission', () => { NonceEnforcer, TimestampEnforcer, ]; - const result = findRuleWithMatchingCaveatAddresses({ + const result = findDecoderWithMatchingCaveatAddresses({ enforcers, - permissionRules: createPermissionRulesForContracts(contracts), + permissionDecoders: createPermissionDecodersForContracts(contracts), }); expect(result.permissionType).toBe(expectedPermissionType); }); @@ -544,9 +544,9 @@ describe('decodePermission', () => { NonceEnforcer, ]; expect(() => - findRuleWithMatchingCaveatAddresses({ + findDecoderWithMatchingCaveatAddresses({ enforcers, - permissionRules: createPermissionRulesForContracts(contracts), + permissionDecoders: createPermissionDecodersForContracts(contracts), }), ).toThrow('Unable to identify permission type'); }); @@ -560,9 +560,9 @@ describe('decodePermission', () => { NonceEnforcer, ]; expect(() => - findRuleWithMatchingCaveatAddresses({ + findDecoderWithMatchingCaveatAddresses({ enforcers, - permissionRules: createPermissionRulesForContracts(contracts), + permissionDecoders: createPermissionDecodersForContracts(contracts), }), ).toThrow('Unable to identify permission type'); }); @@ -574,9 +574,9 @@ describe('decodePermission', () => { NonceEnforcer, ]; expect(() => - findRuleWithMatchingCaveatAddresses({ + findDecoderWithMatchingCaveatAddresses({ enforcers, - permissionRules: createPermissionRulesForContracts(contracts), + permissionDecoders: createPermissionDecodersForContracts(contracts), }), ).toThrow('Unable to identify permission type'); }); @@ -591,9 +591,9 @@ describe('decodePermission', () => { ExactCalldataEnforcer, ]; expect(() => - findRuleWithMatchingCaveatAddresses({ + findDecoderWithMatchingCaveatAddresses({ enforcers, - permissionRules: createPermissionRulesForContracts(contracts), + permissionDecoders: createPermissionDecodersForContracts(contracts), }), ).toThrow('Unable to identify permission type'); }); @@ -605,9 +605,9 @@ describe('decodePermission', () => { ValueLteEnforcer.toLowerCase() as unknown as Hex, NonceEnforcer.toLowerCase() as unknown as Hex, ]; - const result = findRuleWithMatchingCaveatAddresses({ + const result = findDecoderWithMatchingCaveatAddresses({ enforcers, - permissionRules: createPermissionRulesForContracts(contracts), + permissionDecoders: createPermissionDecodersForContracts(contracts), }); expect(result.permissionType).toBe(expectedPermissionType); }); @@ -625,9 +625,9 @@ describe('decodePermission', () => { } as unknown as DeployedContractsByName; expect(() => - findRuleWithMatchingCaveatAddresses({ + findDecoderWithMatchingCaveatAddresses({ enforcers, - permissionRules: createPermissionRulesForContracts( + permissionDecoders: createPermissionDecodersForContracts( contractsWithoutAllowedCalldataEnforcer, ), }), @@ -761,12 +761,12 @@ describe('decodePermission', () => { }); }); - describe('selectUniqueRuleAndDecodedPermission', () => { + describe('selectUniqueDecoderAndDecodedPermission', () => { const emptyCaveats: Parameters< - PermissionRule['validateAndDecodePermission'] + PermissionDecoder['validateAndDecodePermission'] >[0] = []; - const dummyRuleFields = { + const dummyDecoderFields = { requiredEnforcers: new Map(), optionalEnforcers: new Set(), caveatAddressesMatch: () => true, @@ -780,9 +780,9 @@ describe('decodePermission', () => { startTime: 1, } as DecodedPermission['permission']['data']; - const rules: PermissionRule[] = [ + const decoders: PermissionDecoder[] = [ { - ...dummyRuleFields, + ...dummyDecoderFields, permissionType: 'native-token-stream', validateAndDecodePermission: () => ({ isValid: true, @@ -791,7 +791,7 @@ describe('decodePermission', () => { }), }, { - ...dummyRuleFields, + ...dummyDecoderFields, permissionType: 'native-token-periodic', validateAndDecodePermission: () => ({ isValid: false, @@ -800,20 +800,20 @@ describe('decodePermission', () => { }, ]; - const result = selectUniqueRuleAndDecodedPermission({ - candidateRules: rules, + const result = selectUniqueDecoderAndDecodedPermission({ + candidateDecoders: decoders, caveats: emptyCaveats, }); - expect(result.rule.permissionType).toBe('native-token-stream'); + expect(result.decoder.permissionType).toBe('native-token-stream'); expect(result.expiry).toBe(9); expect(result.data).toStrictEqual(data); }); it('throws when no candidate rules are provided', () => { expect(() => - selectUniqueRuleAndDecodedPermission({ - candidateRules: [], + selectUniqueDecoderAndDecodedPermission({ + candidateDecoders: [], caveats: emptyCaveats, }), ).toThrow('Unable to identify permission type'); @@ -821,9 +821,9 @@ describe('decodePermission', () => { it('rethrows the validation error when only one candidate exists and it fails', () => { const originalError = new Error('stream validation failed'); - const rules: PermissionRule[] = [ + const decoders: PermissionDecoder[] = [ { - ...dummyRuleFields, + ...dummyDecoderFields, permissionType: 'native-token-stream', validateAndDecodePermission: () => ({ isValid: false, @@ -833,8 +833,8 @@ describe('decodePermission', () => { ]; expect(() => - selectUniqueRuleAndDecodedPermission({ - candidateRules: rules, + selectUniqueDecoderAndDecodedPermission({ + candidateDecoders: decoders, caveats: emptyCaveats, }), ).toThrow(originalError); @@ -848,9 +848,9 @@ describe('decodePermission', () => { startTime: 1, } as DecodedPermission['permission']['data']; - const rules: PermissionRule[] = [ + const decoders: PermissionDecoder[] = [ { - ...dummyRuleFields, + ...dummyDecoderFields, permissionType: 'native-token-stream', validateAndDecodePermission: () => ({ isValid: true, @@ -859,7 +859,7 @@ describe('decodePermission', () => { }), }, { - ...dummyRuleFields, + ...dummyDecoderFields, permissionType: 'native-token-periodic', validateAndDecodePermission: () => ({ isValid: true, @@ -870,8 +870,8 @@ describe('decodePermission', () => { ]; expect(() => - selectUniqueRuleAndDecodedPermission({ - candidateRules: rules, + selectUniqueDecoderAndDecodedPermission({ + candidateDecoders: decoders, caveats: emptyCaveats, }), ).toThrow( @@ -880,9 +880,9 @@ describe('decodePermission', () => { }); it('throws with attempt details when no candidate validates', () => { - const rules: PermissionRule[] = [ + const decoders: PermissionDecoder[] = [ { - ...dummyRuleFields, + ...dummyDecoderFields, permissionType: 'native-token-stream', validateAndDecodePermission: () => ({ isValid: false, @@ -890,7 +890,7 @@ describe('decodePermission', () => { }), }, { - ...dummyRuleFields, + ...dummyDecoderFields, permissionType: 'native-token-periodic', validateAndDecodePermission: () => ({ isValid: false, @@ -900,8 +900,8 @@ describe('decodePermission', () => { ]; expect(() => - selectUniqueRuleAndDecodedPermission({ - candidateRules: rules, + selectUniqueDecoderAndDecodedPermission({ + candidateDecoders: decoders, caveats: emptyCaveats, }), ).toThrow( @@ -911,12 +911,12 @@ describe('decodePermission', () => { }); describe('adversarial: attempts to violate decoder expectations', () => { - describe('getPermissionRuleMatchingCaveatTypes()', () => { + describe('getPermissionDecoderMatchingCaveatTypes()', () => { it('rejects empty enforcer list', () => { expect(() => - findRuleWithMatchingCaveatAddresses({ + findDecoderWithMatchingCaveatAddresses({ enforcers: [], - permissionRules: createPermissionRulesForContracts(contracts), + permissionDecoders: createPermissionDecodersForContracts(contracts), }), ).toThrow('Unable to identify permission type'); }); @@ -924,9 +924,9 @@ describe('decodePermission', () => { it('rejects enforcer list with only unknown/forbidden addresses', () => { const unknown = '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef' as Hex; expect(() => - findRuleWithMatchingCaveatAddresses({ + findDecoderWithMatchingCaveatAddresses({ enforcers: [unknown], - permissionRules: createPermissionRulesForContracts(contracts), + permissionDecoders: createPermissionDecodersForContracts(contracts), }), ).toThrow('Unable to identify permission type'); }); @@ -939,9 +939,9 @@ describe('decodePermission', () => { NonceEnforcer, ]; expect(() => - findRuleWithMatchingCaveatAddresses({ + findDecoderWithMatchingCaveatAddresses({ enforcers, - permissionRules: createPermissionRulesForContracts(contracts), + permissionDecoders: createPermissionDecodersForContracts(contracts), }), ).toThrow('Unable to identify permission type'); }); @@ -956,9 +956,9 @@ describe('decodePermission', () => { unknownEnforcer, ]; expect(() => - findRuleWithMatchingCaveatAddresses({ + findDecoderWithMatchingCaveatAddresses({ enforcers, - permissionRules: createPermissionRulesForContracts(contracts), + permissionDecoders: createPermissionDecodersForContracts(contracts), }), ).toThrow('Unable to identify permission type'); }); @@ -970,9 +970,9 @@ describe('decodePermission', () => { NonceEnforcer, ]; expect(() => - findRuleWithMatchingCaveatAddresses({ + findDecoderWithMatchingCaveatAddresses({ enforcers, - permissionRules: createPermissionRulesForContracts(contracts), + permissionDecoders: createPermissionDecodersForContracts(contracts), }), ).toThrow('Unable to identify permission type'); }); @@ -986,9 +986,9 @@ describe('decodePermission', () => { NonceEnforcer, ]; expect(() => - findRuleWithMatchingCaveatAddresses({ + findDecoderWithMatchingCaveatAddresses({ enforcers, - permissionRules: createPermissionRulesForContracts(contracts), + permissionDecoders: createPermissionDecodersForContracts(contracts), }), ).toThrow('Unable to identify permission type'); }); diff --git a/packages/gator-permissions-controller/src/decodePermission/decodePermission.ts b/packages/gator-permissions-controller/src/decodePermission/decodePermission.ts index 0b00170b13..6946360c8d 100644 --- a/packages/gator-permissions-controller/src/decodePermission/decodePermission.ts +++ b/packages/gator-permissions-controller/src/decodePermission/decodePermission.ts @@ -5,67 +5,69 @@ import { numberToHex } from '@metamask/utils'; import type { DecodedPermission, PermissionType, - PermissionRule, + PermissionDecoder, ValidateAndDecodeResult, } from './types'; /** - * Returns every permission rule whose caveat-address pattern matches the given - * enforcer list for the chain. Used when more than one permission type can - * share the same enforcer set; the caller must disambiguate by validating - * caveat terms (see {@link selectUniqueRuleAndDecodedPermission}). + * Returns every permission decoder whose caveat-address pattern matches the + * given enforcer list for the chain. Used when more than one permission type + * can share the same enforcer set; the caller must disambiguate by validating + * caveat terms (see {@link selectUniqueDecoderAndDecodedPermission}). * * @param args - The arguments to this function. * @param args.enforcers - List of enforcer contract addresses (hex strings). - * @param args.permissionRules - The permission rules for the chain. - * @returns All rules that match, possibly empty. + * @param args.permissionDecoders - The permission decoders for the chain. + * @returns All decoders that match, possibly empty. */ -export const findRulesWithMatchingCaveatAddresses = ({ +export const findDecodersWithMatchingCaveatAddresses = ({ enforcers, - permissionRules, + permissionDecoders, }: { enforcers: Hex[]; - permissionRules: PermissionRule[]; -}): PermissionRule[] => { - return permissionRules.filter((rule) => rule.caveatAddressesMatch(enforcers)); + permissionDecoders: PermissionDecoder[]; +}): PermissionDecoder[] => { + return permissionDecoders.filter((decoder) => + decoder.caveatAddressesMatch(enforcers), + ); }; /** - * Returns the unique permission rule that matches a given set of enforcer + * Returns the unique permission decoder that matches a given set of enforcer * contract addresses (caveat types) for a specific chain. * - * A rule matches when: + * A decoder matches when: * - All of its required enforcers are present in the provided list; and - * - No provided enforcer falls outside the union of the rule's required and + * - No provided enforcer falls outside the union of the decoder's required and * optional enforcers (currently only `TimestampEnforcer` is allowed extra). * - * If exactly one rule matches, it is returned. + * If exactly one decoder matches, it is returned. * * @param args - The arguments to this function. * @param args.enforcers - List of enforcer contract addresses (hex strings). - * @param args.permissionRules - The permission rules for the chain. - * @returns The matching permission rule. - * @throws If no rule matches, or if more than one rule matches. + * @param args.permissionDecoders - The permission decoders for the chain. + * @returns The matching permission decoder. + * @throws If no decoder matches, or if more than one decoder matches. */ -export const findRuleWithMatchingCaveatAddresses = ({ +export const findDecoderWithMatchingCaveatAddresses = ({ enforcers, - permissionRules, + permissionDecoders, }: { enforcers: Hex[]; - permissionRules: PermissionRule[]; -}): PermissionRule => { - const matchingRules = findRulesWithMatchingCaveatAddresses({ + permissionDecoders: PermissionDecoder[]; +}): PermissionDecoder => { + const matchingDecoders = findDecodersWithMatchingCaveatAddresses({ enforcers, - permissionRules, + permissionDecoders, }); - if (matchingRules.length === 0) { + if (matchingDecoders.length === 0) { throw new Error('Unable to identify permission type'); } - if (matchingRules.length > 1) { + if (matchingDecoders.length > 1) { throw new Error('Multiple permission types match'); } - return matchingRules[0]; + return matchingDecoders[0]; }; type SuccessfulValidateAndDecodeResult = Extract< @@ -73,50 +75,50 @@ type SuccessfulValidateAndDecodeResult = Extract< { isValid: true } >; -type RuleAndDecodedPermission = { - rule: PermissionRule; +type DecoderAndDecodedPermission = { + decoder: PermissionDecoder; rules: SuccessfulValidateAndDecodeResult['rules']; data: SuccessfulValidateAndDecodeResult['data']; expiry: SuccessfulValidateAndDecodeResult['expiry']; }; /** - * Runs {@link PermissionRule.validateAndDecodePermission} on each candidate - * rule. Use when several rules share the same caveat addresses. + * Runs {@link PermissionDecoder.validateAndDecodePermission} on each candidate + * decoder. Use when several decoders share the same caveat addresses. * * @param args - The arguments to this function. - * @param args.candidateRules - Rules whose addresses already match the caveats. + * @param args.candidateDecoders - Decoders whose addresses already match the caveats. * @param args.caveats - Caveats from the delegation. - * @returns The unique rule and decoded expiry/data when exactly one rule validates. - * @throws If `candidateRules` is empty, if no rule validates, or if more than one rule validates. + * @returns The unique decoder and decoded expiry/data when exactly one decoder validates. + * @throws If `candidateDecoders` is empty, if no decoder validates, or if more than one decoder validates. */ -export const selectUniqueRuleAndDecodedPermission = ({ - candidateRules, +export const selectUniqueDecoderAndDecodedPermission = ({ + candidateDecoders, caveats, }: { - candidateRules: PermissionRule[]; + candidateDecoders: PermissionDecoder[]; caveats: Caveat[]; -}): RuleAndDecodedPermission => { - if (candidateRules.length === 0) { +}): DecoderAndDecodedPermission => { + if (candidateDecoders.length === 0) { throw new Error('Unable to identify permission type'); } - const successfulDecodingResult: RuleAndDecodedPermission[] = []; + const successfulDecodingResult: DecoderAndDecodedPermission[] = []; const failedAttempts: { permissionType: PermissionType; error: Error }[] = []; - for (const rule of candidateRules) { - const decodeResult = rule.validateAndDecodePermission(caveats); + for (const decoder of candidateDecoders) { + const decodeResult = decoder.validateAndDecodePermission(caveats); if (decodeResult.isValid) { successfulDecodingResult.push({ - rule, + decoder, rules: decodeResult.rules, data: decodeResult.data, expiry: decodeResult.expiry, }); } else { failedAttempts.push({ - permissionType: rule.permissionType, + permissionType: decoder.permissionType, error: decodeResult.error, }); } @@ -128,7 +130,7 @@ export const selectUniqueRuleAndDecodedPermission = ({ if (successfulDecodingResult.length > 1) { const types = successfulDecodingResult - .map((result) => result.rule.permissionType) + .map((result) => result.decoder.permissionType) .join(', '); throw new Error( `Multiple permission types validate the same delegation caveats: ${types}`, diff --git a/packages/gator-permissions-controller/src/decodePermission/decoders/erc20PayeeRule.test.ts b/packages/gator-permissions-controller/src/decodePermission/decoders/erc20PayeeRule.test.ts new file mode 100644 index 0000000000..9d0acec912 --- /dev/null +++ b/packages/gator-permissions-controller/src/decodePermission/decoders/erc20PayeeRule.test.ts @@ -0,0 +1,150 @@ +import { createAllowedCalldataTerms } from '@metamask/delegation-core'; +import { + CHAIN_ID, + DELEGATOR_CONTRACTS, +} from '@metamask/delegation-deployments'; +import { getChecksumAddress } from '@metamask/utils'; +import type { Hex } from '@metamask/utils'; + +import type { ChecksumCaveat } from '../types'; +import { getChecksumEnforcersByChainId } from '../utils'; +import { erc20PayeeRule } from './erc20PayeeRule'; + +describe('erc20PayeeRule', () => { + const contracts = DELEGATOR_CONTRACTS['1.3.0'][CHAIN_ID.sepolia]; + const contractAddresses = getChecksumEnforcersByChainId(contracts); + const { allowedCalldataEnforcer, nonceEnforcer } = contractAddresses; + const requiredEnforcers = new Map([[nonceEnforcer, 1]]); + + const PAYEE_ADDRESS: Hex = '0x3333333333333333333333333333333333333333'; + const CHECKSUM_PAYEE_INPUT: Hex = + '0x8617e340b3d01fa5f11f306f4090fd50e238070d'; + const paddedPayee: Hex = `0x${PAYEE_ADDRESS.slice(2).padStart(64, '0')}`; + + const validPayeeCaveat: ChecksumCaveat = { + enforcer: allowedCalldataEnforcer, + terms: createAllowedCalldataTerms({ + startIndex: 4, + value: paddedPayee, + }), + args: '0x' as Hex, + }; + + it('returns null when no AllowedCalldataEnforcer caveat is present', () => { + const caveats: ChecksumCaveat[] = [ + { enforcer: nonceEnforcer, terms: '0x' as Hex, args: '0x' as Hex }, + ]; + + expect( + erc20PayeeRule({ contractAddresses, caveats, requiredEnforcers }), + ).toBeNull(); + }); + + it('returns a payee rule with the decoded checksummed address', () => { + expect( + erc20PayeeRule({ + contractAddresses, + caveats: [validPayeeCaveat], + requiredEnforcers, + }), + ).toStrictEqual({ + type: 'payee', + data: { addresses: [getChecksumAddress(PAYEE_ADDRESS)] }, + }); + }); + + it('returns a checksummed payee address when encoded address is lowercase', () => { + const paddedLowercasePayee: Hex = `0x${CHECKSUM_PAYEE_INPUT.slice(2).padStart(64, '0')}`; + const caveat: ChecksumCaveat = { + enforcer: allowedCalldataEnforcer, + terms: createAllowedCalldataTerms({ + startIndex: 4, + value: paddedLowercasePayee, + }), + args: '0x' as Hex, + }; + + expect( + erc20PayeeRule({ + contractAddresses, + caveats: [caveat], + requiredEnforcers, + }), + ).toStrictEqual({ + type: 'payee', + data: { addresses: [getChecksumAddress(CHECKSUM_PAYEE_INPUT)] }, + }); + }); + + it('throws when allowedCalldataEnforcer is configured as required', () => { + const requiredWithPayee = new Map([ + [nonceEnforcer, 1], + [allowedCalldataEnforcer, 1], + ]); + + expect(() => + erc20PayeeRule({ + contractAddresses, + caveats: [validPayeeCaveat], + requiredEnforcers: requiredWithPayee, + }), + ).toThrow( + 'Invalid payee caveats: payee enforcer may not be a required caveat', + ); + }); + + it('throws when more than one AllowedCalldataEnforcer caveat is present', () => { + expect(() => + erc20PayeeRule({ + contractAddresses, + caveats: [validPayeeCaveat, validPayeeCaveat], + requiredEnforcers, + }), + ).toThrow( + 'Invalid payee caveats: multiple AllowedCalldataEnforcer caveats', + ); + }); + + it('throws when startIndex is not 4', () => { + const caveat: ChecksumCaveat = { + enforcer: allowedCalldataEnforcer, + terms: createAllowedCalldataTerms({ + startIndex: 0, + value: paddedPayee, + }), + args: '0x' as Hex, + }; + + expect(() => + erc20PayeeRule({ + contractAddresses, + caveats: [caveat], + requiredEnforcers, + }), + ).toThrow( + 'Invalid payee caveat: AllowedCalldataEnforcer startIndex must be 4', + ); + }); + + it('throws when the encoded value is not 32 bytes long', () => { + const shortValue: Hex = `0x${PAYEE_ADDRESS.slice(2)}`; + const caveat: ChecksumCaveat = { + enforcer: allowedCalldataEnforcer, + terms: createAllowedCalldataTerms({ + startIndex: 4, + value: shortValue, + }), + args: '0x' as Hex, + }; + + expect(() => + erc20PayeeRule({ + contractAddresses, + caveats: [caveat], + requiredEnforcers, + }), + ).toThrow( + 'Invalid payee caveat: AllowedCalldataEnforcer value must be 32 bytes long', + ); + }); +}); diff --git a/packages/gator-permissions-controller/src/decodePermission/decoders/erc20PayeeRule.ts b/packages/gator-permissions-controller/src/decodePermission/decoders/erc20PayeeRule.ts new file mode 100644 index 0000000000..07c9f2dc19 --- /dev/null +++ b/packages/gator-permissions-controller/src/decodePermission/decoders/erc20PayeeRule.ts @@ -0,0 +1,79 @@ +import { decodeAllowedCalldataTerms } from '@metamask/delegation-core'; +import { getChecksumAddress } from '@metamask/utils'; +import type { Hex } from '@metamask/utils'; + +import { EXECUTION_PERMISSION_PAYEE_RULE_TYPE } from '../../constants'; +import type { RuleDecoder } from '../types'; +import { getByteLength } from '../utils'; + +const ERC20_TRANSFER_PAYEE_START_INDEX = 4; +const ERC20_PAYEE_VALUE_BYTE_LENGTH = 32; + +/** + * Rule decoder for ERC-20 style payees, where a single payee address is + * encoded inside an AllowedCalldataEnforcer caveat constraining the recipient + * argument of an ERC-20 `transfer` call. + * + * Use this decoder for ERC-20 token permissions. For native-token permissions, + * use {@link nativePayeeRule} instead. + * + * @param args - The arguments to this function. + * @param args.contractAddresses - Checksummed enforcer addresses for the chain. + * @param args.caveats - Checksummed caveats from the delegation. + * @param args.requiredEnforcers - Required enforcer counts for the permission. + * @returns A `{ rule }` result containing the payee address when an + * AllowedCalldataEnforcer caveat exists, otherwise `null`. + * @throws If the AllowedCalldataEnforcer is also a required enforcer (the + * payee enforcer must not be configured as required), if multiple matching + * caveats are present, or if the encoded calldata constraint does not match + * the expected ERC-20 transfer payee shape. + */ +export const erc20PayeeRule: RuleDecoder = ({ + contractAddresses, + caveats, + requiredEnforcers, +}) => { + const { allowedCalldataEnforcer } = contractAddresses; + + if (requiredEnforcers.has(allowedCalldataEnforcer)) { + throw new Error( + 'Invalid payee caveats: payee enforcer may not be a required caveat', + ); + } + + const matchingCaveats = caveats.filter( + (caveat) => caveat.enforcer === allowedCalldataEnforcer, + ); + + if (matchingCaveats.length === 0) { + return null; + } + + if (matchingCaveats.length > 1) { + throw new Error( + 'Invalid payee caveats: multiple AllowedCalldataEnforcer caveats', + ); + } + + const [caveat] = matchingCaveats; + const decoded = decodeAllowedCalldataTerms(caveat.terms); + + if (decoded.startIndex !== ERC20_TRANSFER_PAYEE_START_INDEX) { + throw new Error( + `Invalid payee caveat: AllowedCalldataEnforcer startIndex must be ${ERC20_TRANSFER_PAYEE_START_INDEX}`, + ); + } + + if (getByteLength(decoded.value) !== ERC20_PAYEE_VALUE_BYTE_LENGTH) { + throw new Error( + `Invalid payee caveat: AllowedCalldataEnforcer value must be ${ERC20_PAYEE_VALUE_BYTE_LENGTH} bytes long`, + ); + } + + const address: Hex = `0x${decoded.value.slice(-40)}`; + + return { + type: EXECUTION_PERMISSION_PAYEE_RULE_TYPE, + data: { addresses: [getChecksumAddress(address)] }, + }; +}; diff --git a/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenAllowance.test.ts b/packages/gator-permissions-controller/src/decodePermission/decoders/erc20TokenAllowance.test.ts similarity index 88% rename from packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenAllowance.test.ts rename to packages/gator-permissions-controller/src/decodePermission/decoders/erc20TokenAllowance.test.ts index 45502376a2..c09e8caa4f 100644 --- a/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenAllowance.test.ts +++ b/packages/gator-permissions-controller/src/decodePermission/decoders/erc20TokenAllowance.test.ts @@ -5,20 +5,20 @@ import { DELEGATOR_CONTRACTS, } from '@metamask/delegation-deployments'; -import { createPermissionRulesForContracts } from '.'; +import { createPermissionDecodersForContracts } from '.'; import { ZERO_32_BYTES } from '../utils'; -describe('erc20-token-allowance rule', () => { +describe('erc20-token-allowance decoder', () => { const chainId = CHAIN_ID.sepolia; const contracts = DELEGATOR_CONTRACTS['1.3.0'][chainId]; const { TimestampEnforcer, ERC20PeriodTransferEnforcer, ValueLteEnforcer } = contracts; - const permissionRules = createPermissionRulesForContracts(contracts); - const rule = permissionRules.find( + const permissionDecoders = createPermissionDecodersForContracts(contracts); + const decoder = permissionDecoders.find( (candidate) => candidate.permissionType === 'erc20-token-allowance', ); - if (!rule) { - throw new Error('Rule not found'); + if (!decoder) { + throw new Error('Decoder not found'); } const expiryCaveat = { @@ -59,7 +59,7 @@ describe('erc20-token-allowance rule', () => { args: '0x' as const, }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(false); if (result.isValid) { @@ -82,7 +82,7 @@ describe('erc20-token-allowance rule', () => { }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(false); if (result.isValid) { @@ -105,7 +105,7 @@ describe('erc20-token-allowance rule', () => { args: '0x' as const, }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(false); if (result.isValid) { @@ -132,7 +132,7 @@ describe('erc20-token-allowance rule', () => { args: '0x' as const, }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(false); if (result.isValid) { @@ -155,7 +155,7 @@ describe('erc20-token-allowance rule', () => { }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(true); if (!result.isValid) { @@ -185,7 +185,7 @@ describe('erc20-token-allowance rule', () => { }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(true); }); @@ -203,7 +203,7 @@ describe('erc20-token-allowance rule', () => { args: '0x' as const, }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(false); if (result.isValid) { @@ -228,7 +228,7 @@ describe('erc20-token-allowance rule', () => { args: '0x' as const, }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(false); if (result.isValid) { @@ -255,7 +255,7 @@ describe('erc20-token-allowance rule', () => { }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(false); if (result.isValid) { diff --git a/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenAllowance.ts b/packages/gator-permissions-controller/src/decodePermission/decoders/erc20TokenAllowance.ts similarity index 66% rename from packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenAllowance.ts rename to packages/gator-permissions-controller/src/decodePermission/decoders/erc20TokenAllowance.ts index 01c5e02ea9..26e342cb67 100644 --- a/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenAllowance.ts +++ b/packages/gator-permissions-controller/src/decodePermission/decoders/erc20TokenAllowance.ts @@ -4,7 +4,6 @@ import type { ChecksumCaveat, ChecksumEnforcersByChainId, DecodedPermission, - PermissionRule, } from '../types'; import { getByteLength, @@ -13,74 +12,63 @@ import { UINT256_MAX, ZERO_32_BYTES, } from '../utils'; -import { makePermissionRule } from './makePermissionRule'; +import { erc20PayeeRule } from './erc20PayeeRule'; +import { expiryRule } from './expiryRule'; +import type { MakePermissionDecoderConfig } from './makePermissionDecoder'; +import { redeemerRule } from './redeemerRule'; /** - * Creates the erc20-token-allowance permission rule. + * Builds the configuration for the erc20-token-allowance permission decoder. * * This permission shares the same enforcer set as `erc20-token-periodic` but * is distinguished by a `periodDuration` of `UINT256_MAX`, which effectively * disables the periodic reset and turns the caveat into a one-off allowance. * - * @param enforcers - Checksummed enforcer addresses for the chain. - * @returns The erc20-token-allowance permission rule. + * @param contractAddresses - Checksummed enforcer addresses for the chain. + * @returns The erc20-token-allowance permission decoder configuration. */ -export function makeErc20TokenAllowanceRule( - enforcers: ChecksumEnforcersByChainId, -): PermissionRule { +export function makeErc20TokenAllowanceDecoderConfig( + contractAddresses: ChecksumEnforcersByChainId, +): MakePermissionDecoderConfig { const { timestampEnforcer, erc20PeriodicEnforcer, valueLteEnforcer, nonceEnforcer, allowedCalldataEnforcer, - allowedTargetsEnforcer, redeemerEnforcer, - } = enforcers; - return makePermissionRule({ + } = contractAddresses; + + return { permissionType: 'erc20-token-allowance', + contractAddresses, optionalEnforcers: [ - timestampEnforcer, - redeemerEnforcer, - allowedCalldataEnforcer, + timestampEnforcer, // expiry rule + redeemerEnforcer, // redeemer rule + allowedCalldataEnforcer, // payee rule ], - redeemerEnforcer, - payeeEnforcers: { - allowedCalldataEnforcer, - allowedTargetsEnforcer, - singlePayeeEnforcer: allowedCalldataEnforcer, - }, - timestampEnforcer, requiredEnforcers: { [erc20PeriodicEnforcer]: 1, [valueLteEnforcer]: 1, [nonceEnforcer]: 1, }, - validateAndDecodeData: (caveats) => - validateAndDecodeData(caveats, { - erc20PeriodicEnforcer, - valueLteEnforcer, - }), - }); + rules: [expiryRule, redeemerRule, erc20PayeeRule], + validateAndDecodeData, + }; } /** * Decodes erc20-token-allowance permission data from caveats; throws on invalid. * * @param caveats - Caveats from the permission context (checksummed). - * @param enforcers - Addresses of the enforcers. - * @param enforcers.erc20PeriodicEnforcer - Address of the ERC20PeriodicEnforcer. - * @param enforcers.valueLteEnforcer - Address of the ValueLteEnforcer. + * @param contractAddresses - Checksummed enforcer addresses for the chain. * @returns Decoded allowance terms. */ function validateAndDecodeData( caveats: ChecksumCaveat[], - enforcers: Pick< - ChecksumEnforcersByChainId, - 'erc20PeriodicEnforcer' | 'valueLteEnforcer' - >, + contractAddresses: ChecksumEnforcersByChainId, ): DecodedPermission['permission']['data'] { - const { erc20PeriodicEnforcer, valueLteEnforcer } = enforcers; + const { erc20PeriodicEnforcer, valueLteEnforcer } = contractAddresses; const valueLteTerms = getTermsByEnforcer({ caveats, diff --git a/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenPeriodic.test.ts b/packages/gator-permissions-controller/src/decodePermission/decoders/erc20TokenPeriodic.test.ts similarity index 91% rename from packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenPeriodic.test.ts rename to packages/gator-permissions-controller/src/decodePermission/decoders/erc20TokenPeriodic.test.ts index 9888108c90..e77c5cd454 100644 --- a/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenPeriodic.test.ts +++ b/packages/gator-permissions-controller/src/decodePermission/decoders/erc20TokenPeriodic.test.ts @@ -8,20 +8,20 @@ import { DELEGATOR_CONTRACTS, } from '@metamask/delegation-deployments'; -import { createPermissionRulesForContracts } from '.'; +import { createPermissionDecodersForContracts } from '.'; import { MAX_PERIOD_DURATION, ZERO_32_BYTES } from '../utils'; -describe('erc20-token-periodic rule', () => { +describe('erc20-token-periodic decoder', () => { const chainId = CHAIN_ID.sepolia; const contracts = DELEGATOR_CONTRACTS['1.3.0'][chainId]; const { TimestampEnforcer, ERC20PeriodTransferEnforcer, ValueLteEnforcer } = contracts; - const permissionRules = createPermissionRulesForContracts(contracts); - const rule = permissionRules.find( + const permissionDecoders = createPermissionDecodersForContracts(contracts); + const decoder = permissionDecoders.find( (candidate) => candidate.permissionType === 'erc20-token-periodic', ); - if (!rule) { - throw new Error('Rule not found'); + if (!decoder) { + throw new Error('Decoder not found'); } const expiryCaveat = { @@ -64,7 +64,7 @@ describe('erc20-token-periodic rule', () => { args: '0x' as const, }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(false); // this is here as a type guard @@ -86,7 +86,7 @@ describe('erc20-token-periodic rule', () => { args: '0x' as const, }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(false); // this is here as a type guard @@ -124,7 +124,7 @@ describe('erc20-token-periodic rule', () => { args: '0x' as const, }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(false); // this is here as a type guard @@ -155,7 +155,7 @@ describe('erc20-token-periodic rule', () => { args: '0x' as const, }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(true); // this is here as a type guard @@ -189,7 +189,7 @@ describe('erc20-token-periodic rule', () => { args: '0x' as const, }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(true); // this is here as a type guard @@ -221,7 +221,7 @@ describe('erc20-token-periodic rule', () => { }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(false); // this is here as a type guard @@ -255,7 +255,7 @@ describe('erc20-token-periodic rule', () => { args: '0x' as const, }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(false); // this is here as a type guard @@ -284,7 +284,7 @@ describe('erc20-token-periodic rule', () => { args: '0x' as const, }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(false); // this is here as a type guard @@ -315,7 +315,7 @@ describe('erc20-token-periodic rule', () => { }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(false); // this is here as a type guard @@ -350,7 +350,7 @@ describe('erc20-token-periodic rule', () => { }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(false); // this is here as a type guard @@ -382,7 +382,7 @@ describe('erc20-token-periodic rule', () => { args: '0x' as const, }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(true); // this is here as a type guard diff --git a/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenPeriodic.ts b/packages/gator-permissions-controller/src/decodePermission/decoders/erc20TokenPeriodic.ts similarity index 66% rename from packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenPeriodic.ts rename to packages/gator-permissions-controller/src/decodePermission/decoders/erc20TokenPeriodic.ts index 5b84141063..52614cae43 100644 --- a/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenPeriodic.ts +++ b/packages/gator-permissions-controller/src/decodePermission/decoders/erc20TokenPeriodic.ts @@ -4,7 +4,6 @@ import type { ChecksumCaveat, ChecksumEnforcersByChainId, DecodedPermission, - PermissionRule, } from '../types'; import { getByteLength, @@ -13,70 +12,59 @@ import { splitHex, ZERO_32_BYTES, } from '../utils'; -import { makePermissionRule } from './makePermissionRule'; +import { erc20PayeeRule } from './erc20PayeeRule'; +import { expiryRule } from './expiryRule'; +import type { MakePermissionDecoderConfig } from './makePermissionDecoder'; +import { redeemerRule } from './redeemerRule'; /** - * Creates the erc20-token-periodic permission rule. + * Builds the configuration for the erc20-token-periodic permission decoder. * - * @param enforcers - Checksummed enforcer addresses for the chain. - * @returns The erc20-token-periodic permission rule. + * @param contractAddresses - Checksummed enforcer addresses for the chain. + * @returns The erc20-token-periodic permission decoder configuration. */ -export function makeErc20TokenPeriodicRule( - enforcers: ChecksumEnforcersByChainId, -): PermissionRule { +export function makeErc20TokenPeriodicDecoderConfig( + contractAddresses: ChecksumEnforcersByChainId, +): MakePermissionDecoderConfig { const { timestampEnforcer, erc20PeriodicEnforcer, valueLteEnforcer, nonceEnforcer, allowedCalldataEnforcer, - allowedTargetsEnforcer, redeemerEnforcer, - } = enforcers; - return makePermissionRule({ + } = contractAddresses; + + return { permissionType: 'erc20-token-periodic', + contractAddresses, optionalEnforcers: [ - timestampEnforcer, - redeemerEnforcer, - allowedCalldataEnforcer, + timestampEnforcer, // expiry rule + redeemerEnforcer, // redeemer rule + allowedCalldataEnforcer, // payee rule ], - redeemerEnforcer, - payeeEnforcers: { - allowedCalldataEnforcer, - allowedTargetsEnforcer, - singlePayeeEnforcer: allowedCalldataEnforcer, - }, - timestampEnforcer, requiredEnforcers: { [erc20PeriodicEnforcer]: 1, [valueLteEnforcer]: 1, [nonceEnforcer]: 1, }, - validateAndDecodeData: (caveats) => - validateAndDecodeData(caveats, { - erc20PeriodicEnforcer, - valueLteEnforcer, - }), - }); + rules: [expiryRule, redeemerRule, erc20PayeeRule], + validateAndDecodeData, + }; } /** * Decodes erc20-token-periodic permission data from caveats; throws on invalid. * * @param caveats - Caveats from the permission context (checksummed). - * @param enforcers - Addresses of the enforcers. - * @param enforcers.erc20PeriodicEnforcer - Address of the ERC20PeriodicEnforcer. - * @param enforcers.valueLteEnforcer - Address of the ValueLteEnforcer. + * @param contractAddresses - Checksummed enforcer addresses for the chain. * @returns Decoded periodic terms. */ function validateAndDecodeData( caveats: ChecksumCaveat[], - enforcers: Pick< - ChecksumEnforcersByChainId, - 'erc20PeriodicEnforcer' | 'valueLteEnforcer' - >, + contractAddresses: ChecksumEnforcersByChainId, ): DecodedPermission['permission']['data'] { - const { erc20PeriodicEnforcer, valueLteEnforcer } = enforcers; + const { erc20PeriodicEnforcer, valueLteEnforcer } = contractAddresses; const valueLteTerms = getTermsByEnforcer({ caveats, diff --git a/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenRevocation.test.ts b/packages/gator-permissions-controller/src/decodePermission/decoders/erc20TokenRevocation.test.ts similarity index 89% rename from packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenRevocation.test.ts rename to packages/gator-permissions-controller/src/decodePermission/decoders/erc20TokenRevocation.test.ts index 32d2a37cd2..d23bb16a76 100644 --- a/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenRevocation.test.ts +++ b/packages/gator-permissions-controller/src/decodePermission/decoders/erc20TokenRevocation.test.ts @@ -5,9 +5,9 @@ import { } from '@metamask/delegation-deployments'; import { getChecksumAddress } from '@metamask/utils'; -import { createPermissionRulesForContracts } from '.'; +import { createPermissionDecodersForContracts } from '.'; -describe('erc20-token-revocation rule', () => { +describe('erc20-token-revocation decoder', () => { const chainId = CHAIN_ID.sepolia; const contracts = DELEGATOR_CONTRACTS['1.3.0'][chainId]; const { @@ -16,12 +16,12 @@ describe('erc20-token-revocation rule', () => { ValueLteEnforcer, RedeemerEnforcer, } = contracts; - const permissionRules = createPermissionRulesForContracts(contracts); - const rule = permissionRules.find( + const permissionDecoders = createPermissionDecodersForContracts(contracts); + const decoder = permissionDecoders.find( (candidate) => candidate.permissionType === 'erc20-token-revocation', ); - if (!rule) { - throw new Error('Rule not found'); + if (!decoder) { + throw new Error('Decoder not found'); } const expiryCaveat = { @@ -57,7 +57,7 @@ describe('erc20-token-revocation rule', () => { }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(false); // this is here as a type guard @@ -94,7 +94,7 @@ describe('erc20-token-revocation rule', () => { }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(false); // this is here as a type guard @@ -133,7 +133,7 @@ describe('erc20-token-revocation rule', () => { }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(false); // this is here as a type guard @@ -176,7 +176,7 @@ describe('erc20-token-revocation rule', () => { args: '0x' as const, }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(false); // this is here as a type guard @@ -212,7 +212,7 @@ describe('erc20-token-revocation rule', () => { args: '0x' as const, }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(true); // this is here as a type guard @@ -222,7 +222,12 @@ describe('erc20-token-revocation rule', () => { expect(result.expiry).toBe(1720000); expect(result.data).toStrictEqual({}); - expect(result.rules).toBeUndefined(); + expect(result.rules).toStrictEqual([ + { + type: 'expiry', + data: { timestamp: 1720000 }, + }, + ]); }); it('includes redeemer rule but not payee when RedeemerEnforcer caveat is present', () => { @@ -256,12 +261,16 @@ describe('erc20-token-revocation rule', () => { args: '0x' as const, }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(true); if (!result.isValid) { throw new Error('Expected valid result'); } expect(result.rules).toStrictEqual([ + { + type: 'expiry', + data: { timestamp: 1720000 }, + }, { type: 'redeemer', data: { diff --git a/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenRevocation.ts b/packages/gator-permissions-controller/src/decodePermission/decoders/erc20TokenRevocation.ts similarity index 58% rename from packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenRevocation.ts rename to packages/gator-permissions-controller/src/decodePermission/decoders/erc20TokenRevocation.ts index a127829d8e..1021fa6f0e 100644 --- a/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenRevocation.ts +++ b/packages/gator-permissions-controller/src/decodePermission/decoders/erc20TokenRevocation.ts @@ -2,7 +2,6 @@ import type { ChecksumCaveat, ChecksumEnforcersByChainId, DecodedPermission, - PermissionRule, } from '../types'; import { ERC20_APPROVE_SELECTOR_TERMS, @@ -10,65 +9,61 @@ import { getTermsByEnforcer, ZERO_32_BYTES, } from '../utils'; -import { makePermissionRule } from './makePermissionRule'; +import { expiryRule } from './expiryRule'; +import type { MakePermissionDecoderConfig } from './makePermissionDecoder'; +import { redeemerRule } from './redeemerRule'; /** - * Creates the erc20-token-revocation permission rule. + * Builds the configuration for the erc20-token-revocation permission decoder. * - * @param enforcers - Checksummed enforcer addresses for the chain. - * @returns The erc20-token-revocation permission rule. + * Revocation permissions intentionally do not support a payee rule: the + * AllowedCalldataEnforcer is required (with count=2) to encode both the + * `approve` selector and the zero-amount constraint, so it cannot also be + * used to extract a payee address. + * + * @param contractAddresses - Checksummed enforcer addresses for the chain. + * @returns The erc20-token-revocation permission decoder configuration. */ -export function makeErc20TokenRevocationRule( - enforcers: ChecksumEnforcersByChainId, -): PermissionRule { +export function makeErc20TokenRevocationDecoderConfig( + contractAddresses: ChecksumEnforcersByChainId, +): MakePermissionDecoderConfig { const { timestampEnforcer, allowedCalldataEnforcer, - allowedTargetsEnforcer, valueLteEnforcer, nonceEnforcer, redeemerEnforcer, - } = enforcers; - return makePermissionRule({ + } = contractAddresses; + + return { permissionType: 'erc20-token-revocation', - optionalEnforcers: [timestampEnforcer, redeemerEnforcer], - redeemerEnforcer, - payeeEnforcers: { - allowedCalldataEnforcer, - allowedTargetsEnforcer, - singlePayeeEnforcer: allowedCalldataEnforcer, - }, - timestampEnforcer, + contractAddresses, + optionalEnforcers: [ + timestampEnforcer, // expiry rule + redeemerEnforcer, // redeemer rule + ], requiredEnforcers: { [allowedCalldataEnforcer]: 2, [valueLteEnforcer]: 1, [nonceEnforcer]: 1, }, - validateAndDecodeData: (caveats) => - validateAndDecodeData(caveats, { - allowedCalldataEnforcer, - valueLteEnforcer, - }), - }); + rules: [expiryRule, redeemerRule], + validateAndDecodeData, + }; } /** * Decodes erc20-token-revocation permission data from caveats; throws on invalid. * * @param caveats - Caveats from the permission context (checksummed). - * @param enforcers - Addresses of the enforcers. - * @param enforcers.allowedCalldataEnforcer - Address of the AllowedCalldataEnforcer. - * @param enforcers.valueLteEnforcer - Address of the ValueLteEnforcer. + * @param contractAddresses - Checksummed enforcer addresses for the chain. * @returns Empty object (revocation has no decoded data payload). */ function validateAndDecodeData( caveats: ChecksumCaveat[], - enforcers: Pick< - ChecksumEnforcersByChainId, - 'allowedCalldataEnforcer' | 'valueLteEnforcer' - >, + contractAddresses: ChecksumEnforcersByChainId, ): DecodedPermission['permission']['data'] { - const { allowedCalldataEnforcer, valueLteEnforcer } = enforcers; + const { allowedCalldataEnforcer, valueLteEnforcer } = contractAddresses; const allowedCalldataCaveats = caveats.filter( (caveat) => caveat.enforcer === allowedCalldataEnforcer, diff --git a/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenStream.test.ts b/packages/gator-permissions-controller/src/decodePermission/decoders/erc20TokenStream.test.ts similarity index 91% rename from packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenStream.test.ts rename to packages/gator-permissions-controller/src/decodePermission/decoders/erc20TokenStream.test.ts index 099512d8ce..19304a8c44 100644 --- a/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenStream.test.ts +++ b/packages/gator-permissions-controller/src/decodePermission/decoders/erc20TokenStream.test.ts @@ -8,20 +8,20 @@ import { DELEGATOR_CONTRACTS, } from '@metamask/delegation-deployments'; -import { createPermissionRulesForContracts } from '.'; +import { createPermissionDecodersForContracts } from '.'; import { ZERO_32_BYTES } from '../utils'; -describe('erc20-token-stream rule', () => { +describe('erc20-token-stream decoder', () => { const chainId = CHAIN_ID.sepolia; const contracts = DELEGATOR_CONTRACTS['1.3.0'][chainId]; const { TimestampEnforcer, ERC20StreamingEnforcer, ValueLteEnforcer } = contracts; - const permissionRules = createPermissionRulesForContracts(contracts); - const rule = permissionRules.find( + const permissionDecoders = createPermissionDecodersForContracts(contracts); + const decoder = permissionDecoders.find( (candidate) => candidate.permissionType === 'erc20-token-stream', ); - if (!rule) { - throw new Error('Rule not found'); + if (!decoder) { + throw new Error('Decoder not found'); } const expiryCaveat = { @@ -57,7 +57,7 @@ describe('erc20-token-stream rule', () => { { enforcer: ERC20StreamingEnforcer, terms, args: '0x' as const }, { enforcer: ERC20StreamingEnforcer, terms, args: '0x' as const }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(false); // this is here as a type guard @@ -79,7 +79,7 @@ describe('erc20-token-stream rule', () => { args: '0x' as const, }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(false); // this is here as a type guard @@ -118,7 +118,7 @@ describe('erc20-token-stream rule', () => { args: '0x' as const, }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(false); // this is here as a type guard @@ -150,7 +150,7 @@ describe('erc20-token-stream rule', () => { args: '0x' as const, }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(true); // this is here as a type guard @@ -184,7 +184,7 @@ describe('erc20-token-stream rule', () => { args: '0x' as const, }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(true); // this is here as a type guard @@ -209,7 +209,7 @@ describe('erc20-token-stream rule', () => { valueLteCaveat, { enforcer: ERC20StreamingEnforcer, terms, args: '0x' as const }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(false); // this is here as a type guard @@ -234,7 +234,7 @@ describe('erc20-token-stream rule', () => { valueLteCaveat, { enforcer: ERC20StreamingEnforcer, terms, args: '0x' as const }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(false); // this is here as a type guard @@ -269,7 +269,7 @@ describe('erc20-token-stream rule', () => { args: '0x' as const, }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(false); // this is here as a type guard @@ -295,7 +295,7 @@ describe('erc20-token-stream rule', () => { valueLteCaveat, { enforcer: ERC20StreamingEnforcer, terms, args: '0x' as const }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(false); // this is here as a type guard @@ -321,7 +321,7 @@ describe('erc20-token-stream rule', () => { valueLteCaveat, { enforcer: ERC20StreamingEnforcer, terms, args: '0x' as const }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(false); // this is here as a type guard diff --git a/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenStream.ts b/packages/gator-permissions-controller/src/decodePermission/decoders/erc20TokenStream.ts similarity index 65% rename from packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenStream.ts rename to packages/gator-permissions-controller/src/decodePermission/decoders/erc20TokenStream.ts index c87c782173..7547839139 100644 --- a/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenStream.ts +++ b/packages/gator-permissions-controller/src/decodePermission/decoders/erc20TokenStream.ts @@ -4,7 +4,6 @@ import type { ChecksumCaveat, ChecksumEnforcersByChainId, DecodedPermission, - PermissionRule, } from '../types'; import { getByteLength, @@ -12,70 +11,59 @@ import { splitHex, ZERO_32_BYTES, } from '../utils'; -import { makePermissionRule } from './makePermissionRule'; +import { erc20PayeeRule } from './erc20PayeeRule'; +import { expiryRule } from './expiryRule'; +import type { MakePermissionDecoderConfig } from './makePermissionDecoder'; +import { redeemerRule } from './redeemerRule'; /** - * Creates the erc20-token-stream permission rule. + * Builds the configuration for the erc20-token-stream permission decoder. * - * @param enforcers - Checksummed enforcer addresses for the chain. - * @returns The erc20-token-stream permission rule. + * @param contractAddresses - Checksummed enforcer addresses for the chain. + * @returns The erc20-token-stream permission decoder configuration. */ -export function makeErc20TokenStreamRule( - enforcers: ChecksumEnforcersByChainId, -): PermissionRule { +export function makeErc20TokenStreamDecoderConfig( + contractAddresses: ChecksumEnforcersByChainId, +): MakePermissionDecoderConfig { const { timestampEnforcer, erc20StreamingEnforcer, valueLteEnforcer, nonceEnforcer, allowedCalldataEnforcer, - allowedTargetsEnforcer, redeemerEnforcer, - } = enforcers; - return makePermissionRule({ + } = contractAddresses; + + return { permissionType: 'erc20-token-stream', + contractAddresses, optionalEnforcers: [ - timestampEnforcer, - redeemerEnforcer, - allowedCalldataEnforcer, + timestampEnforcer, // expiry rule + redeemerEnforcer, // redeemer rule + allowedCalldataEnforcer, // payee rule ], - redeemerEnforcer, - payeeEnforcers: { - allowedCalldataEnforcer, - allowedTargetsEnforcer, - singlePayeeEnforcer: allowedCalldataEnforcer, - }, - timestampEnforcer, requiredEnforcers: { [erc20StreamingEnforcer]: 1, [valueLteEnforcer]: 1, [nonceEnforcer]: 1, }, - validateAndDecodeData: (caveats) => - validateAndDecodeData(caveats, { - erc20StreamingEnforcer, - valueLteEnforcer, - }), - }); + rules: [expiryRule, redeemerRule, erc20PayeeRule], + validateAndDecodeData, + }; } /** * Decodes erc20-token-stream permission data from caveats; throws on invalid. * * @param caveats - Caveats from the permission context (checksummed). - * @param enforcers - Addresses of the enforcers. - * @param enforcers.erc20StreamingEnforcer - Address of the ERC20StreamingEnforcer. - * @param enforcers.valueLteEnforcer - Address of the ValueLteEnforcer. + * @param contractAddresses - Checksummed enforcer addresses for the chain. * @returns Decoded stream terms. */ function validateAndDecodeData( caveats: ChecksumCaveat[], - enforcers: Pick< - ChecksumEnforcersByChainId, - 'erc20StreamingEnforcer' | 'valueLteEnforcer' - >, + contractAddresses: ChecksumEnforcersByChainId, ): DecodedPermission['permission']['data'] { - const { erc20StreamingEnforcer, valueLteEnforcer } = enforcers; + const { erc20StreamingEnforcer, valueLteEnforcer } = contractAddresses; const valueLteTerms = getTermsByEnforcer({ caveats, enforcer: valueLteEnforcer, diff --git a/packages/gator-permissions-controller/src/decodePermission/decoders/expiryRule.test.ts b/packages/gator-permissions-controller/src/decodePermission/decoders/expiryRule.test.ts new file mode 100644 index 0000000000..6966a6694a --- /dev/null +++ b/packages/gator-permissions-controller/src/decodePermission/decoders/expiryRule.test.ts @@ -0,0 +1,70 @@ +import { createTimestampTerms } from '@metamask/delegation-core'; +import { + CHAIN_ID, + DELEGATOR_CONTRACTS, +} from '@metamask/delegation-deployments'; +import type { Hex } from '@metamask/utils'; + +import type { ChecksumCaveat } from '../types'; +import { getChecksumEnforcersByChainId } from '../utils'; +import { expiryRule } from './expiryRule'; + +describe('expiryRule', () => { + const contracts = DELEGATOR_CONTRACTS['1.3.0'][CHAIN_ID.sepolia]; + const contractAddresses = getChecksumEnforcersByChainId(contracts); + const { timestampEnforcer, nonceEnforcer } = contractAddresses; + const requiredEnforcers = new Map([[nonceEnforcer, 1]]); + + it('returns null when no TimestampEnforcer caveat is present', () => { + const caveats: ChecksumCaveat[] = [ + { enforcer: nonceEnforcer, terms: '0x' as Hex, args: '0x' as Hex }, + ]; + + expect( + expiryRule({ contractAddresses, caveats, requiredEnforcers }), + ).toBeNull(); + }); + + it('returns an expiry rule with the decoded timestamp when TimestampEnforcer is present', () => { + const beforeThreshold = 1_750_000_000; + const caveats: ChecksumCaveat[] = [ + { + enforcer: timestampEnforcer, + terms: createTimestampTerms({ + afterThreshold: 0, + beforeThreshold, + }), + args: '0x' as Hex, + }, + ]; + + expect( + expiryRule({ contractAddresses, caveats, requiredEnforcers }), + ).toStrictEqual({ + type: 'expiry', + data: { timestamp: beforeThreshold }, + }); + }); + + it('ignores caveats from unrelated enforcers', () => { + const beforeThreshold = 1_700_000_000; + const caveats: ChecksumCaveat[] = [ + { enforcer: nonceEnforcer, terms: '0x' as Hex, args: '0x' as Hex }, + { + enforcer: timestampEnforcer, + terms: createTimestampTerms({ + afterThreshold: 0, + beforeThreshold, + }), + args: '0x' as Hex, + }, + ]; + + expect( + expiryRule({ contractAddresses, caveats, requiredEnforcers }), + ).toStrictEqual({ + type: 'expiry', + data: { timestamp: beforeThreshold }, + }); + }); +}); diff --git a/packages/gator-permissions-controller/src/decodePermission/decoders/expiryRule.ts b/packages/gator-permissions-controller/src/decodePermission/decoders/expiryRule.ts new file mode 100644 index 0000000000..14b7127966 --- /dev/null +++ b/packages/gator-permissions-controller/src/decodePermission/decoders/expiryRule.ts @@ -0,0 +1,35 @@ +import { EXECUTION_PERMISSION_EXPIRY_RULE_TYPE } from '../../constants'; +import type { RuleDecoder } from '../types'; +import { extractExpiryFromCaveatTerms, getTermsByEnforcer } from '../utils'; + +/** + * Rule decoder that extracts the expiry timestamp from a TimestampEnforcer + * caveat, when present. Returns a standard {@link Rule} so that + * `makePermissionDecoder` can append it to the decoded permission's `rules` + * array. The decoder body additionally hoists the timestamp onto the + * top-level `expiry` field of the response. + * + * @param args - The arguments to this function. + * @param args.contractAddresses - Checksummed enforcer addresses for the chain. + * @param args.caveats - Checksummed caveats from the delegation. + * @returns A `{ type: 'expiry', data: { timestamp } }` rule when a + * TimestampEnforcer caveat exists, otherwise `null`. + */ +export const expiryRule: RuleDecoder = ({ contractAddresses, caveats }) => { + const { timestampEnforcer } = contractAddresses; + + const expiryTerms = getTermsByEnforcer({ + caveats, + enforcer: timestampEnforcer, + throwIfNotFound: false, + }); + + if (!expiryTerms) { + return null; + } + + return { + type: EXECUTION_PERMISSION_EXPIRY_RULE_TYPE, + data: { timestamp: extractExpiryFromCaveatTerms(expiryTerms) }, + }; +}; diff --git a/packages/gator-permissions-controller/src/decodePermission/decoders/index.ts b/packages/gator-permissions-controller/src/decodePermission/decoders/index.ts new file mode 100644 index 0000000000..572248ae2d --- /dev/null +++ b/packages/gator-permissions-controller/src/decodePermission/decoders/index.ts @@ -0,0 +1,36 @@ +import type { DeployedContractsByName, PermissionDecoder } from '../types'; +import { getChecksumEnforcersByChainId } from '../utils'; +import { makeErc20TokenAllowanceDecoderConfig } from './erc20TokenAllowance'; +import { makeErc20TokenPeriodicDecoderConfig } from './erc20TokenPeriodic'; +import { makeErc20TokenRevocationDecoderConfig } from './erc20TokenRevocation'; +import { makeErc20TokenStreamDecoderConfig } from './erc20TokenStream'; +import { makePermissionDecoder } from './makePermissionDecoder'; +import { makeNativeTokenAllowanceDecoderConfig } from './nativeTokenAllowance'; +import { makeNativeTokenPeriodicDecoderConfig } from './nativeTokenPeriodic'; +import { makeNativeTokenStreamDecoderConfig } from './nativeTokenStream'; + +/** + * Builds the canonical set of permission decoders for a chain. + * + * Each decoder specifies the `permissionType`, required/optional enforcers, + * and provides `caveatAddressesMatch` and `validateAndDecodePermission` so the + * entire decode flow can be driven by the decoders. + * + * @param contracts - The deployed contracts for the chain. + * @returns A list of permission decoders used to identify and decode permission types. + * @throws Propagates any errors from resolving enforcer addresses. + */ +export const createPermissionDecodersForContracts = ( + contracts: DeployedContractsByName, +): PermissionDecoder[] => { + const contractAddresses = getChecksumEnforcersByChainId(contracts); + return [ + makeNativeTokenStreamDecoderConfig(contractAddresses), + makeNativeTokenPeriodicDecoderConfig(contractAddresses), + makeNativeTokenAllowanceDecoderConfig(contractAddresses), + makeErc20TokenStreamDecoderConfig(contractAddresses), + makeErc20TokenPeriodicDecoderConfig(contractAddresses), + makeErc20TokenAllowanceDecoderConfig(contractAddresses), + makeErc20TokenRevocationDecoderConfig(contractAddresses), + ].map(makePermissionDecoder); +}; diff --git a/packages/gator-permissions-controller/src/decodePermission/rules/makePermissionRule.test.ts b/packages/gator-permissions-controller/src/decodePermission/decoders/makePermissionDecoder.test.ts similarity index 74% rename from packages/gator-permissions-controller/src/decodePermission/rules/makePermissionRule.test.ts rename to packages/gator-permissions-controller/src/decodePermission/decoders/makePermissionDecoder.test.ts index fef9a97124..817b2cf7ef 100644 --- a/packages/gator-permissions-controller/src/decodePermission/rules/makePermissionRule.test.ts +++ b/packages/gator-permissions-controller/src/decodePermission/decoders/makePermissionDecoder.test.ts @@ -10,38 +10,33 @@ import { import { getChecksumAddress } from '@metamask/utils'; import type { Hex } from '@metamask/utils'; -import { makePermissionRule } from './makePermissionRule'; - -describe('makePermissionRule', () => { +import { getChecksumEnforcersByChainId } from '../utils'; +import { erc20PayeeRule } from './erc20PayeeRule'; +import { expiryRule } from './expiryRule'; +import { makePermissionDecoder } from './makePermissionDecoder'; +import { nativePayeeRule } from './nativePayeeRule'; +import { redeemerRule } from './redeemerRule'; + +describe('makePermissionDecoder', () => { const contracts = DELEGATOR_CONTRACTS['1.3.0'][CHAIN_ID.sepolia]; - const timestampEnforcer = contracts.TimestampEnforcer; - const requiredEnforcer = contracts.NonceEnforcer; - const redeemerEnforcer = contracts.RedeemerEnforcer; - const allowedCalldataEnforcer = contracts.AllowedCalldataEnforcer; - const allowedTargetsEnforcer = contracts.AllowedTargetsEnforcer; - - const payeeEnforcersNative = { - allowedCalldataEnforcer, - allowedTargetsEnforcer, - singlePayeeEnforcer: allowedTargetsEnforcer, - }; - - const payeeEnforcersErc20 = { + const contractAddresses = getChecksumEnforcersByChainId(contracts); + const { + timestampEnforcer, + nonceEnforcer: requiredEnforcer, + redeemerEnforcer, allowedCalldataEnforcer, allowedTargetsEnforcer, - singlePayeeEnforcer: allowedCalldataEnforcer, - }; + } = contractAddresses; - it('calls optional validate callback when provided and decoding succeeds', () => { + it('calls validate callback when decoding succeeds and extracts expiry', () => { const validateAndDecodeData = jest.fn().mockReturnValue({}); - const rule = makePermissionRule({ + const decoder = makePermissionDecoder({ permissionType: 'native-token-stream', - timestampEnforcer, - redeemerEnforcer, - payeeEnforcers: payeeEnforcersNative, - optionalEnforcers: [], + contractAddresses, + optionalEnforcers: [timestampEnforcer], requiredEnforcers: { [requiredEnforcer]: 1 }, + rules: [expiryRule], validateAndDecodeData, }); @@ -61,7 +56,7 @@ describe('makePermissionRule', () => { }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(true); if (!result.isValid) { @@ -75,13 +70,12 @@ describe('makePermissionRule', () => { it('rejects when any caveat terms are not valid hex (invalid characters)', () => { const validateAndDecodeData = jest.fn().mockReturnValue({}); - const rule = makePermissionRule({ + const decoder = makePermissionDecoder({ permissionType: 'native-token-stream', - timestampEnforcer, - redeemerEnforcer, - payeeEnforcers: payeeEnforcersNative, - optionalEnforcers: [], + contractAddresses, + optionalEnforcers: [timestampEnforcer], requiredEnforcers: { [requiredEnforcer]: 1 }, + rules: [expiryRule], validateAndDecodeData, }); @@ -98,7 +92,7 @@ describe('makePermissionRule', () => { }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(false); if (result.isValid) { @@ -111,13 +105,12 @@ describe('makePermissionRule', () => { it('rejects when any caveat terms contain non-hex characters after 0x prefix', () => { const validateAndDecodeData = jest.fn().mockReturnValue({}); - const rule = makePermissionRule({ + const decoder = makePermissionDecoder({ permissionType: 'native-token-stream', - timestampEnforcer, - redeemerEnforcer, - payeeEnforcers: payeeEnforcersNative, - optionalEnforcers: [], + contractAddresses, + optionalEnforcers: [timestampEnforcer], requiredEnforcers: { [requiredEnforcer]: 1 }, + rules: [expiryRule], validateAndDecodeData, }); @@ -135,7 +128,7 @@ describe('makePermissionRule', () => { }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(false); if (result.isValid) { @@ -148,13 +141,12 @@ describe('makePermissionRule', () => { it('rejects when required enforcer terms are not valid hex', () => { const validateAndDecodeData = jest.fn().mockReturnValue({}); - const rule = makePermissionRule({ + const decoder = makePermissionDecoder({ permissionType: 'native-token-stream', - timestampEnforcer, - redeemerEnforcer, - payeeEnforcers: payeeEnforcersNative, - optionalEnforcers: [], + contractAddresses, + optionalEnforcers: [timestampEnforcer], requiredEnforcers: { [requiredEnforcer]: 1 }, + rules: [expiryRule], validateAndDecodeData, }); @@ -174,7 +166,7 @@ describe('makePermissionRule', () => { }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(false); if (result.isValid) { @@ -187,13 +179,12 @@ describe('makePermissionRule', () => { it('accepts caveat terms with mixed-case hex', () => { const validateAndDecodeData = jest.fn().mockReturnValue({}); - const rule = makePermissionRule({ + const decoder = makePermissionDecoder({ permissionType: 'native-token-stream', - timestampEnforcer, - redeemerEnforcer, - payeeEnforcers: payeeEnforcersNative, - optionalEnforcers: [], + contractAddresses, + optionalEnforcers: [timestampEnforcer], requiredEnforcers: { [requiredEnforcer]: 1 }, + rules: [expiryRule], validateAndDecodeData, }); @@ -214,7 +205,7 @@ describe('makePermissionRule', () => { }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(true); if (!result.isValid) { @@ -227,13 +218,12 @@ describe('makePermissionRule', () => { it('accepts caveat terms with empty hex', () => { const validateAndDecodeData = jest.fn().mockReturnValue({}); - const rule = makePermissionRule({ + const decoder = makePermissionDecoder({ permissionType: 'native-token-stream', - timestampEnforcer, - redeemerEnforcer, - payeeEnforcers: payeeEnforcersNative, + contractAddresses, optionalEnforcers: [], requiredEnforcers: { [requiredEnforcer]: 1 }, + rules: [], validateAndDecodeData, }); @@ -245,7 +235,7 @@ describe('makePermissionRule', () => { }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(true); if (!result.isValid) { @@ -259,13 +249,12 @@ describe('makePermissionRule', () => { // Raw packed 20-byte address (40 hex chars), not ABI-padded 32-byte words. const packedAddr = '1111111111111111111111111111111111111111' as const; - const rule = makePermissionRule({ + const decoder = makePermissionDecoder({ permissionType: 'native-token-stream', - timestampEnforcer, - redeemerEnforcer, - payeeEnforcers: payeeEnforcersNative, - optionalEnforcers: [], + contractAddresses, + optionalEnforcers: [redeemerEnforcer], requiredEnforcers: { [requiredEnforcer]: 1 }, + rules: [redeemerRule], validateAndDecodeData, }); @@ -282,7 +271,7 @@ describe('makePermissionRule', () => { }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(true); if (!result.isValid) { @@ -306,13 +295,12 @@ describe('makePermissionRule', () => { const validateAndDecodeData = jest.fn().mockReturnValue({}); const payeeAddress = '0x2222222222222222222222222222222222222222' as Hex; - const rule = makePermissionRule({ + const decoder = makePermissionDecoder({ permissionType: 'native-token-stream', - timestampEnforcer, - redeemerEnforcer, - payeeEnforcers: payeeEnforcersNative, + contractAddresses, optionalEnforcers: [allowedTargetsEnforcer], requiredEnforcers: { [requiredEnforcer]: 1 }, + rules: [nativePayeeRule], validateAndDecodeData, }); @@ -329,7 +317,7 @@ describe('makePermissionRule', () => { }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(true); if (!result.isValid) { @@ -350,13 +338,12 @@ describe('makePermissionRule', () => { const payeeAddress = '0x3333333333333333333333333333333333333333' as Hex; const paddedAddress = `0x${payeeAddress.slice(2).padStart(64, '0')}`; - const rule = makePermissionRule({ + const decoder = makePermissionDecoder({ permissionType: 'erc20-token-stream', - timestampEnforcer, - redeemerEnforcer, - payeeEnforcers: payeeEnforcersErc20, + contractAddresses, optionalEnforcers: [allowedCalldataEnforcer], requiredEnforcers: { [requiredEnforcer]: 1 }, + rules: [erc20PayeeRule], validateAndDecodeData, }); @@ -376,7 +363,7 @@ describe('makePermissionRule', () => { }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(true); if (!result.isValid) { @@ -392,17 +379,16 @@ describe('makePermissionRule', () => { ]); }); - it('does not include payee rule when only AllowedTargetsEnforcer caveat is present (erc20)', () => { + it('does not include payee rule when no matching payee caveat is present (erc20 decoder, AllowedTargets caveat)', () => { const validateAndDecodeData = jest.fn().mockReturnValue({}); const payeeAddress = '0x2222222222222222222222222222222222222222' as Hex; - const rule = makePermissionRule({ + const decoder = makePermissionDecoder({ permissionType: 'erc20-token-stream', - timestampEnforcer, - redeemerEnforcer, - payeeEnforcers: payeeEnforcersErc20, + contractAddresses, optionalEnforcers: [allowedTargetsEnforcer], requiredEnforcers: { [requiredEnforcer]: 1 }, + rules: [erc20PayeeRule], validateAndDecodeData, }); @@ -419,7 +405,7 @@ describe('makePermissionRule', () => { }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(true); if (!result.isValid) { @@ -435,17 +421,16 @@ describe('makePermissionRule', () => { const padded1 = `0x${payeeAddress1.slice(2).padStart(64, '0')}`; const padded2 = `0x${payeeAddress2.slice(2).padStart(64, '0')}`; - const rule = makePermissionRule({ + const decoder = makePermissionDecoder({ permissionType: 'erc20-token-stream', - timestampEnforcer, - redeemerEnforcer, - payeeEnforcers: payeeEnforcersErc20, + contractAddresses, optionalEnforcers: [allowedCalldataEnforcer], requiredEnforcers: { [requiredEnforcer]: 1 }, + rules: [erc20PayeeRule], validateAndDecodeData, }); - const result = rule.validateAndDecodePermission([ + const result = decoder.validateAndDecodePermission([ { enforcer: requiredEnforcer, terms: '0x' as Hex, @@ -474,7 +459,7 @@ describe('makePermissionRule', () => { throw new Error('Expected invalid result'); } expect(result.error.message).toBe( - 'Invalid payee caveats: multiple singlePayeeEnforcer caveats', + 'Invalid payee caveats: multiple AllowedCalldataEnforcer caveats', ); }); @@ -483,13 +468,12 @@ describe('makePermissionRule', () => { const payeeAddress1 = '0x4444444444444444444444444444444444444444' as Hex; const payeeAddress2 = '0x5555555555555555555555555555555555555555' as Hex; - const rule = makePermissionRule({ + const decoder = makePermissionDecoder({ permissionType: 'native-token-stream', - timestampEnforcer, - redeemerEnforcer, - payeeEnforcers: payeeEnforcersNative, + contractAddresses, optionalEnforcers: [allowedTargetsEnforcer], requiredEnforcers: { [requiredEnforcer]: 1 }, + rules: [nativePayeeRule], validateAndDecodeData, }); @@ -508,7 +492,7 @@ describe('makePermissionRule', () => { }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(true); if (!result.isValid) { @@ -527,18 +511,17 @@ describe('makePermissionRule', () => { ]); }); - it('does not include payee rule when only AllowedCalldataEnforcer caveat is present (native)', () => { + it('does not include payee rule when no matching payee caveat is present (native decoder, AllowedCalldata caveat)', () => { const validateAndDecodeData = jest.fn().mockReturnValue({}); const payeeAddress = '0x3333333333333333333333333333333333333333' as Hex; const paddedAddress = `0x${payeeAddress.slice(2).padStart(64, '0')}`; - const rule = makePermissionRule({ + const decoder = makePermissionDecoder({ permissionType: 'native-token-stream', - timestampEnforcer, - redeemerEnforcer, - payeeEnforcers: payeeEnforcersNative, + contractAddresses, optionalEnforcers: [allowedCalldataEnforcer], requiredEnforcers: { [requiredEnforcer]: 1 }, + rules: [nativePayeeRule], validateAndDecodeData, }); @@ -558,7 +541,7 @@ describe('makePermissionRule', () => { }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(true); if (!result.isValid) { @@ -572,13 +555,12 @@ describe('makePermissionRule', () => { const redeemerAddr = '1111111111111111111111111111111111111111' as const; const payeeAddress = '0x2222222222222222222222222222222222222222' as Hex; - const rule = makePermissionRule({ + const decoder = makePermissionDecoder({ permissionType: 'native-token-stream', - timestampEnforcer, - redeemerEnforcer, - payeeEnforcers: payeeEnforcersNative, + contractAddresses, optionalEnforcers: [redeemerEnforcer, allowedTargetsEnforcer], requiredEnforcers: { [requiredEnforcer]: 1 }, + rules: [redeemerRule, nativePayeeRule], validateAndDecodeData, }); @@ -600,7 +582,7 @@ describe('makePermissionRule', () => { }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(true); if (!result.isValid) { @@ -626,13 +608,12 @@ describe('makePermissionRule', () => { it('does not include payee rule when no payee caveat is present', () => { const validateAndDecodeData = jest.fn().mockReturnValue({}); - const rule = makePermissionRule({ + const decoder = makePermissionDecoder({ permissionType: 'native-token-stream', - timestampEnforcer, - redeemerEnforcer, - payeeEnforcers: payeeEnforcersNative, - optionalEnforcers: [], + contractAddresses, + optionalEnforcers: [allowedTargetsEnforcer], requiredEnforcers: { [requiredEnforcer]: 1 }, + rules: [nativePayeeRule], validateAndDecodeData, }); @@ -644,7 +625,7 @@ describe('makePermissionRule', () => { }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(true); if (!result.isValid) { @@ -653,81 +634,42 @@ describe('makePermissionRule', () => { expect(result.rules).toBeUndefined(); }); - it('returns true from caveatAddressesMatch when enforcers match rule', () => { + it('returns true from caveatAddressesMatch when enforcers match', () => { const validateAndDecodeData = jest.fn().mockReturnValue({}); - const rule = makePermissionRule({ + const decoder = makePermissionDecoder({ permissionType: 'native-token-stream', - timestampEnforcer, - redeemerEnforcer, - payeeEnforcers: payeeEnforcersNative, + contractAddresses, optionalEnforcers: [timestampEnforcer], requiredEnforcers: { [requiredEnforcer]: 1 }, + rules: [expiryRule], validateAndDecodeData, }); expect( - rule.caveatAddressesMatch([requiredEnforcer, timestampEnforcer]), + decoder.caveatAddressesMatch([requiredEnforcer, timestampEnforcer]), ).toBe(true); - expect(rule.caveatAddressesMatch([requiredEnforcer])).toBe(true); - expect(rule.caveatAddressesMatch([])).toBe(false); - }); - - it('rejects when singlePayeeEnforcer is unrecognised', () => { - const validateAndDecodeData = jest.fn().mockReturnValue({}); - const unknownEnforcer = '0x8888888888888888888888888888888888888888' as Hex; - - const rule = makePermissionRule({ - permissionType: 'native-token-stream', - timestampEnforcer, - redeemerEnforcer, - payeeEnforcers: { - allowedCalldataEnforcer, - allowedTargetsEnforcer, - singlePayeeEnforcer: unknownEnforcer, - }, - optionalEnforcers: [unknownEnforcer], - requiredEnforcers: { [requiredEnforcer]: 1 }, - validateAndDecodeData, - }); - - const caveats = [ - { - enforcer: requiredEnforcer, - terms: '0x' as Hex, - args: '0x' as Hex, - }, - { - enforcer: unknownEnforcer, - terms: - '0x0000000000000000000000000000000000000000000000000000000000000001' as Hex, - args: '0x' as Hex, - }, - ]; - - const result = rule.validateAndDecodePermission(caveats); - - expect(result.isValid).toBe(false); + expect(decoder.caveatAddressesMatch([requiredEnforcer])).toBe(true); + expect(decoder.caveatAddressesMatch([])).toBe(false); }); - it('rejects when singlePayeeEnforcer is configured as a required enforcer', () => { + it('rejects when payee enforcer is configured as a required enforcer', () => { const validateAndDecodeData = jest.fn().mockReturnValue({}); const payeeAddress = '0x2222222222222222222222222222222222222222' as Hex; - const rule = makePermissionRule({ + const decoder = makePermissionDecoder({ permissionType: 'native-token-stream', - timestampEnforcer, - redeemerEnforcer, - payeeEnforcers: payeeEnforcersNative, + contractAddresses, optionalEnforcers: [], requiredEnforcers: { [requiredEnforcer]: 1, [allowedTargetsEnforcer]: 1, }, + rules: [nativePayeeRule], validateAndDecodeData, }); - const result = rule.validateAndDecodePermission([ + const result = decoder.validateAndDecodePermission([ { enforcer: requiredEnforcer, terms: '0x' as Hex, @@ -745,7 +687,7 @@ describe('makePermissionRule', () => { throw new Error('Expected invalid result'); } expect(result.error.message).toBe( - 'Invalid payee caveats: singlePayeeEnforcer may not be a required caveat', + 'Invalid payee caveats: payee enforcer may not be a required caveat', ); }); @@ -755,17 +697,16 @@ describe('makePermissionRule', () => { const paddedAddress = `0x${payeeAddress.slice(2).padStart(64, '0')}` as const; - const rule = makePermissionRule({ + const decoder = makePermissionDecoder({ permissionType: 'erc20-token-stream', - timestampEnforcer, - redeemerEnforcer, - payeeEnforcers: payeeEnforcersErc20, + contractAddresses, optionalEnforcers: [allowedCalldataEnforcer], requiredEnforcers: { [requiredEnforcer]: 1 }, + rules: [erc20PayeeRule], validateAndDecodeData, }); - const result = rule.validateAndDecodePermission([ + const result = decoder.validateAndDecodePermission([ { enforcer: requiredEnforcer, terms: '0x' as Hex, @@ -787,17 +728,16 @@ describe('makePermissionRule', () => { it('rejects an ERC20 payee caveat when the calldata value is not one address', () => { const validateAndDecodeData = jest.fn().mockReturnValue({}); - const rule = makePermissionRule({ + const decoder = makePermissionDecoder({ permissionType: 'erc20-token-stream', - timestampEnforcer, - redeemerEnforcer, - payeeEnforcers: payeeEnforcersErc20, + contractAddresses, optionalEnforcers: [allowedCalldataEnforcer], requiredEnforcers: { [requiredEnforcer]: 1 }, + rules: [erc20PayeeRule], validateAndDecodeData, }); - const result = rule.validateAndDecodePermission([ + const result = decoder.validateAndDecodePermission([ { enforcer: requiredEnforcer, terms: '0x' as Hex, @@ -819,17 +759,16 @@ describe('makePermissionRule', () => { it('rejects a native payee caveat with no targets', () => { const validateAndDecodeData = jest.fn().mockReturnValue({}); - const rule = makePermissionRule({ + const decoder = makePermissionDecoder({ permissionType: 'native-token-stream', - timestampEnforcer, - redeemerEnforcer, - payeeEnforcers: payeeEnforcersNative, + contractAddresses, optionalEnforcers: [allowedTargetsEnforcer], requiredEnforcers: { [requiredEnforcer]: 1 }, + rules: [nativePayeeRule], validateAndDecodeData, }); - const result = rule.validateAndDecodePermission([ + const result = decoder.validateAndDecodePermission([ { enforcer: requiredEnforcer, terms: '0x' as Hex, @@ -845,22 +784,21 @@ describe('makePermissionRule', () => { expect(result.isValid).toBe(false); }); - it('rejects multiple single-payee caveats', () => { + it('rejects multiple AllowedTargetsEnforcer caveats for native payee decoding', () => { const validateAndDecodeData = jest.fn().mockReturnValue({}); const payeeAddress1 = '0x2222222222222222222222222222222222222222' as Hex; const payeeAddress2 = '0x3333333333333333333333333333333333333333' as Hex; - const rule = makePermissionRule({ + const decoder = makePermissionDecoder({ permissionType: 'native-token-stream', - timestampEnforcer, - redeemerEnforcer, - payeeEnforcers: payeeEnforcersNative, + contractAddresses, optionalEnforcers: [allowedTargetsEnforcer], requiredEnforcers: { [requiredEnforcer]: 1 }, + rules: [nativePayeeRule], validateAndDecodeData, }); - const result = rule.validateAndDecodePermission([ + const result = decoder.validateAndDecodePermission([ { enforcer: requiredEnforcer, terms: '0x' as Hex, @@ -879,5 +817,11 @@ describe('makePermissionRule', () => { ]); expect(result.isValid).toBe(false); + if (result.isValid) { + throw new Error('Expected invalid result'); + } + expect(result.error.message).toBe( + 'Invalid payee caveats: multiple AllowedTargetsEnforcer caveats', + ); }); }); diff --git a/packages/gator-permissions-controller/src/decodePermission/decoders/makePermissionDecoder.ts b/packages/gator-permissions-controller/src/decodePermission/decoders/makePermissionDecoder.ts new file mode 100644 index 0000000000..bb15e243f1 --- /dev/null +++ b/packages/gator-permissions-controller/src/decodePermission/decoders/makePermissionDecoder.ts @@ -0,0 +1,134 @@ +import type { Rule } from '@metamask/7715-permission-types'; +import type { Caveat } from '@metamask/delegation-core'; +import { getChecksumAddress, isStrictHexString } from '@metamask/utils'; +import type { Hex } from '@metamask/utils'; + +import { EXECUTION_PERMISSION_EXPIRY_RULE_TYPE } from '../../constants'; +import type { + ChecksumCaveat, + ChecksumEnforcersByChainId, + DecodedPermission, + PermissionDecoder, + PermissionType, + RuleDecoder, + ValidateAndDecodeResult, +} from '../types'; +import { buildEnforcerCountsAndSet, enforcersMatchRule } from '../utils'; + +/** + * Configuration object describing how to decode a single permission type. + * + * Returned by each `makeDecoderConfig` factory and consumed by + * {@link makePermissionDecoder} to produce a {@link PermissionDecoder}. + */ +export type MakePermissionDecoderConfig = { + permissionType: PermissionType; + contractAddresses: ChecksumEnforcersByChainId; + optionalEnforcers: Hex[]; + requiredEnforcers: Record; + rules: RuleDecoder[]; + validateAndDecodeData: ( + caveats: ChecksumCaveat[], + contractAddresses: ChecksumEnforcersByChainId, + ) => DecodedPermission['permission']['data']; +}; + +/** + * Creates a single {@link PermissionDecoder} with the given type, enforcer + * sets, rule decoders, and decode/validate callback. + * + * @param config - The configuration describing the permission type's + * enforcers, rule decoders, and data decoder. See + * {@link MakePermissionDecoderConfig} for field documentation. + * @param config.permissionType - The type of permission to decode. + * @param config.contractAddresses - Checksummed enforcer addresses for the chain. + * @param config.optionalEnforcers - Optional enforcers for the permission. + * @param config.requiredEnforcers - Required enforcers for the permission. + * @param config.rules - Rule decoders for the permission. + * @param config.validateAndDecodeData - Data decoder for the permission. + * @returns A {@link PermissionDecoder} with `caveatAddressesMatch` and + * `validateAndDecodePermission`. + */ +export function makePermissionDecoder({ + permissionType, + contractAddresses, + optionalEnforcers, + requiredEnforcers, + rules, + validateAndDecodeData, +}: MakePermissionDecoderConfig): PermissionDecoder { + const optionalEnforcersSet = new Set(optionalEnforcers); + const requiredEnforcersMap = new Map( + Object.entries(requiredEnforcers), + ) as Map; + + const caveatAddressesMatch = (caveatAddresses: Hex[]): boolean => { + const { counts, enforcersSet } = buildEnforcerCountsAndSet(caveatAddresses); + + return enforcersMatchRule( + counts, + enforcersSet, + requiredEnforcersMap, + optionalEnforcersSet, + ); + }; + + const validateAndDecodePermission = ( + caveats: Caveat[], + ): ValidateAndDecodeResult => { + const checksumCaveats: ChecksumCaveat[] = caveats.map((caveat) => ({ + ...caveat, + enforcer: getChecksumAddress(caveat.enforcer), + })); + try { + const invalidTerms = checksumCaveats.filter( + // isStrictHexString rejects '0x' which is a valid terms value + ({ terms }) => terms !== '0x' && !isStrictHexString(terms), + ); + + if (invalidTerms.length > 0) { + throw new Error('Invalid terms: must be a hex string'); + } + + let expiry: number | null = null; + const decodedRules: Rule[] = []; + + for (const decode of rules) { + const rule = decode({ + contractAddresses, + caveats: checksumCaveats, + requiredEnforcers: requiredEnforcersMap, + }); + + if (rule === null) { + continue; + } + + decodedRules.push(rule); + + if (rule.type === EXECUTION_PERMISSION_EXPIRY_RULE_TYPE) { + expiry = rule.data.timestamp as number; + } + } + + const data = validateAndDecodeData(checksumCaveats, contractAddresses); + + return { + isValid: true, + expiry, + data, + rules: decodedRules.length > 0 ? decodedRules : undefined, + }; + } catch (caughtError) { + return { isValid: false, error: caughtError as Error }; + } + }; + + return { + permissionType, + caveatAddressesMatch, + validateAndDecodePermission, + optionalEnforcers: optionalEnforcersSet, + requiredEnforcers: requiredEnforcersMap, + }; +} diff --git a/packages/gator-permissions-controller/src/decodePermission/decoders/nativePayeeRule.test.ts b/packages/gator-permissions-controller/src/decodePermission/decoders/nativePayeeRule.test.ts new file mode 100644 index 0000000000..d7e29cef56 --- /dev/null +++ b/packages/gator-permissions-controller/src/decodePermission/decoders/nativePayeeRule.test.ts @@ -0,0 +1,129 @@ +import { createAllowedTargetsTerms } from '@metamask/delegation-core'; +import { + CHAIN_ID, + DELEGATOR_CONTRACTS, +} from '@metamask/delegation-deployments'; +import { getChecksumAddress } from '@metamask/utils'; +import type { Hex } from '@metamask/utils'; + +import type { ChecksumCaveat } from '../types'; +import { getChecksumEnforcersByChainId } from '../utils'; +import { nativePayeeRule } from './nativePayeeRule'; + +describe('nativePayeeRule', () => { + const contracts = DELEGATOR_CONTRACTS['1.3.0'][CHAIN_ID.sepolia]; + const contractAddresses = getChecksumEnforcersByChainId(contracts); + const { allowedTargetsEnforcer, nonceEnforcer } = contractAddresses; + const requiredEnforcers = new Map([[nonceEnforcer, 1]]); + + const PAYEE_A: Hex = '0x4444444444444444444444444444444444444444'; + const PAYEE_B: Hex = '0x5555555555555555555555555555555555555555'; + const CHECKSUM_PAYEE_INPUT: Hex = + '0xde709f2102306220921060314715629080e2fb77'; + + it('returns null when no AllowedTargetsEnforcer caveat is present', () => { + const caveats: ChecksumCaveat[] = [ + { enforcer: nonceEnforcer, terms: '0x' as Hex, args: '0x' as Hex }, + ]; + + expect( + nativePayeeRule({ contractAddresses, caveats, requiredEnforcers }), + ).toBeNull(); + }); + + it('returns a payee rule with a single decoded checksummed address', () => { + const caveats: ChecksumCaveat[] = [ + { + enforcer: allowedTargetsEnforcer, + terms: createAllowedTargetsTerms({ targets: [PAYEE_A] }), + args: '0x' as Hex, + }, + ]; + + expect( + nativePayeeRule({ contractAddresses, caveats, requiredEnforcers }), + ).toStrictEqual({ + type: 'payee', + data: { addresses: [getChecksumAddress(PAYEE_A)] }, + }); + }); + + it('returns a payee rule with multiple decoded checksummed addresses', () => { + const caveats: ChecksumCaveat[] = [ + { + enforcer: allowedTargetsEnforcer, + terms: createAllowedTargetsTerms({ targets: [PAYEE_A, PAYEE_B] }), + args: '0x' as Hex, + }, + ]; + + expect( + nativePayeeRule({ contractAddresses, caveats, requiredEnforcers }), + ).toStrictEqual({ + type: 'payee', + data: { + addresses: [getChecksumAddress(PAYEE_A), getChecksumAddress(PAYEE_B)], + }, + }); + }); + + it('returns checksummed payee addresses', () => { + const caveats: ChecksumCaveat[] = [ + { + enforcer: allowedTargetsEnforcer, + terms: createAllowedTargetsTerms({ targets: [CHECKSUM_PAYEE_INPUT] }), + args: '0x' as Hex, + }, + ]; + + expect( + nativePayeeRule({ contractAddresses, caveats, requiredEnforcers }), + ).toStrictEqual({ + type: 'payee', + data: { addresses: [getChecksumAddress(CHECKSUM_PAYEE_INPUT)] }, + }); + }); + + it('throws when allowedTargetsEnforcer is configured as required', () => { + const requiredWithPayee = new Map([ + [nonceEnforcer, 1], + [allowedTargetsEnforcer, 1], + ]); + const caveats: ChecksumCaveat[] = [ + { + enforcer: allowedTargetsEnforcer, + terms: createAllowedTargetsTerms({ targets: [PAYEE_A] }), + args: '0x' as Hex, + }, + ]; + + expect(() => + nativePayeeRule({ + contractAddresses, + caveats, + requiredEnforcers: requiredWithPayee, + }), + ).toThrow( + 'Invalid payee caveats: payee enforcer may not be a required caveat', + ); + }); + + it('throws when more than one AllowedTargetsEnforcer caveat is present', () => { + const caveats: ChecksumCaveat[] = [ + { + enforcer: allowedTargetsEnforcer, + terms: createAllowedTargetsTerms({ targets: [PAYEE_A] }), + args: '0x' as Hex, + }, + { + enforcer: allowedTargetsEnforcer, + terms: createAllowedTargetsTerms({ targets: [PAYEE_B] }), + args: '0x' as Hex, + }, + ]; + + expect(() => + nativePayeeRule({ contractAddresses, caveats, requiredEnforcers }), + ).toThrow('Invalid payee caveats: multiple AllowedTargetsEnforcer caveats'); + }); +}); diff --git a/packages/gator-permissions-controller/src/decodePermission/decoders/nativePayeeRule.ts b/packages/gator-permissions-controller/src/decodePermission/decoders/nativePayeeRule.ts new file mode 100644 index 0000000000..90549249fe --- /dev/null +++ b/packages/gator-permissions-controller/src/decodePermission/decoders/nativePayeeRule.ts @@ -0,0 +1,58 @@ +import { decodeAllowedTargetsTerms } from '@metamask/delegation-core'; +import { getChecksumAddress } from '@metamask/utils'; + +import { EXECUTION_PERMISSION_PAYEE_RULE_TYPE } from '../../constants'; +import type { RuleDecoder } from '../types'; + +/** + * Rule decoder for native-token style payees, where the payee address(es) are + * encoded as the targets in an AllowedTargetsEnforcer caveat. + * + * Use this decoder for native-token permissions. For ERC-20 token permissions, + * use {@link erc20PayeeRule} instead. + * + * @param args - The arguments to this function. + * @param args.contractAddresses - Checksummed enforcer addresses for the chain. + * @param args.caveats - Checksummed caveats from the delegation. + * @param args.requiredEnforcers - Required enforcer counts for the permission. + * @returns A `{ rule }` result containing the payee addresses when an + * AllowedTargetsEnforcer caveat exists, otherwise `null`. + * @throws If the AllowedTargetsEnforcer is also a required enforcer (the + * payee enforcer must not be configured as required), or if multiple matching + * caveats are present. + */ +export const nativePayeeRule: RuleDecoder = ({ + contractAddresses, + caveats, + requiredEnforcers, +}) => { + const { allowedTargetsEnforcer } = contractAddresses; + + if (requiredEnforcers.has(allowedTargetsEnforcer)) { + throw new Error( + 'Invalid payee caveats: payee enforcer may not be a required caveat', + ); + } + + const matchingCaveats = caveats.filter( + (caveat) => caveat.enforcer === allowedTargetsEnforcer, + ); + + if (matchingCaveats.length === 0) { + return null; + } + + if (matchingCaveats.length > 1) { + throw new Error( + 'Invalid payee caveats: multiple AllowedTargetsEnforcer caveats', + ); + } + + const [caveat] = matchingCaveats; + const decoded = decodeAllowedTargetsTerms(caveat.terms); + + return { + type: EXECUTION_PERMISSION_PAYEE_RULE_TYPE, + data: { addresses: decoded.targets.map(getChecksumAddress) }, + }; +}; diff --git a/packages/gator-permissions-controller/src/decodePermission/rules/nativeTokenAllowance.test.ts b/packages/gator-permissions-controller/src/decodePermission/decoders/nativeTokenAllowance.test.ts similarity index 88% rename from packages/gator-permissions-controller/src/decodePermission/rules/nativeTokenAllowance.test.ts rename to packages/gator-permissions-controller/src/decodePermission/decoders/nativeTokenAllowance.test.ts index e93646a8e7..383428beed 100644 --- a/packages/gator-permissions-controller/src/decodePermission/rules/nativeTokenAllowance.test.ts +++ b/packages/gator-permissions-controller/src/decodePermission/decoders/nativeTokenAllowance.test.ts @@ -5,9 +5,9 @@ import { DELEGATOR_CONTRACTS, } from '@metamask/delegation-deployments'; -import { createPermissionRulesForContracts } from '.'; +import { createPermissionDecodersForContracts } from '.'; -describe('native-token-allowance rule', () => { +describe('native-token-allowance decoder', () => { const chainId = CHAIN_ID.sepolia; const contracts = DELEGATOR_CONTRACTS['1.3.0'][chainId]; const { @@ -15,12 +15,12 @@ describe('native-token-allowance rule', () => { NativeTokenPeriodTransferEnforcer, ExactCalldataEnforcer, } = contracts; - const permissionRules = createPermissionRulesForContracts(contracts); - const rule = permissionRules.find( + const permissionDecoders = createPermissionDecodersForContracts(contracts); + const decoder = permissionDecoders.find( (candidate) => candidate.permissionType === 'native-token-allowance', ); - if (!rule) { - throw new Error('Rule not found'); + if (!decoder) { + throw new Error('Decoder not found'); } const expiryCaveat = { @@ -59,7 +59,7 @@ describe('native-token-allowance rule', () => { args: '0x' as const, }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(false); // this is here as a type guard @@ -83,7 +83,7 @@ describe('native-token-allowance rule', () => { }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(false); if (result.isValid) { @@ -106,7 +106,7 @@ describe('native-token-allowance rule', () => { args: '0x' as const, }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(false); if (result.isValid) { @@ -132,7 +132,7 @@ describe('native-token-allowance rule', () => { args: '0x' as const, }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(false); if (result.isValid) { @@ -155,7 +155,7 @@ describe('native-token-allowance rule', () => { }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(true); if (!result.isValid) { @@ -184,7 +184,7 @@ describe('native-token-allowance rule', () => { }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(true); }); @@ -202,7 +202,7 @@ describe('native-token-allowance rule', () => { args: '0x' as const, }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(false); if (result.isValid) { @@ -227,7 +227,7 @@ describe('native-token-allowance rule', () => { args: '0x' as const, }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(false); if (result.isValid) { @@ -254,7 +254,7 @@ describe('native-token-allowance rule', () => { }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(false); if (result.isValid) { diff --git a/packages/gator-permissions-controller/src/decodePermission/rules/nativeTokenAllowance.ts b/packages/gator-permissions-controller/src/decodePermission/decoders/nativeTokenAllowance.ts similarity index 66% rename from packages/gator-permissions-controller/src/decodePermission/rules/nativeTokenAllowance.ts rename to packages/gator-permissions-controller/src/decodePermission/decoders/nativeTokenAllowance.ts index 9f5910cc1b..5683910071 100644 --- a/packages/gator-permissions-controller/src/decodePermission/rules/nativeTokenAllowance.ts +++ b/packages/gator-permissions-controller/src/decodePermission/decoders/nativeTokenAllowance.ts @@ -4,7 +4,6 @@ import type { ChecksumCaveat, ChecksumEnforcersByChainId, DecodedPermission, - PermissionRule, } from '../types'; import { getByteLength, @@ -13,74 +12,64 @@ import { UINT256_MAX, ZERO_32_BYTES, } from '../utils'; -import { makePermissionRule } from './makePermissionRule'; +import { expiryRule } from './expiryRule'; +import type { MakePermissionDecoderConfig } from './makePermissionDecoder'; +import { nativePayeeRule } from './nativePayeeRule'; +import { redeemerRule } from './redeemerRule'; /** - * Creates the native-token-allowance permission rule. + * Builds the configuration for the native-token-allowance permission decoder. * * This permission shares the same enforcer set as `native-token-periodic` but * is distinguished by a `periodDuration` of `UINT256_MAX`, which effectively * disables the periodic reset and turns the caveat into a one-off allowance. * - * @param enforcers - Checksummed enforcer addresses for the chain. - * @returns The native-token-allowance permission rule. + * @param contractAddresses - Checksummed enforcer addresses for the chain. + * @returns The native-token-allowance permission decoder configuration. */ -export function makeNativeTokenAllowanceRule( - enforcers: ChecksumEnforcersByChainId, -): PermissionRule { +export function makeNativeTokenAllowanceDecoderConfig( + contractAddresses: ChecksumEnforcersByChainId, +): MakePermissionDecoderConfig { const { timestampEnforcer, nativeTokenPeriodicEnforcer, exactCalldataEnforcer, nonceEnforcer, - allowedCalldataEnforcer, allowedTargetsEnforcer, redeemerEnforcer, - } = enforcers; - return makePermissionRule({ + } = contractAddresses; + + return { permissionType: 'native-token-allowance', + contractAddresses, optionalEnforcers: [ - timestampEnforcer, - redeemerEnforcer, - allowedTargetsEnforcer, + timestampEnforcer, // expiry rule + redeemerEnforcer, // redeemer rule + allowedTargetsEnforcer, // payee rule ], - redeemerEnforcer, - payeeEnforcers: { - allowedCalldataEnforcer, - allowedTargetsEnforcer, - singlePayeeEnforcer: allowedTargetsEnforcer, - }, - timestampEnforcer, requiredEnforcers: { [nativeTokenPeriodicEnforcer]: 1, [exactCalldataEnforcer]: 1, [nonceEnforcer]: 1, }, - validateAndDecodeData: (caveats) => - validateAndDecodeData(caveats, { - nativeTokenPeriodicEnforcer, - exactCalldataEnforcer, - }), - }); + rules: [expiryRule, redeemerRule, nativePayeeRule], + validateAndDecodeData, + }; } /** * Decodes native-token-allowance permission data from caveats; throws on invalid. * * @param caveats - Caveats from the permission context (checksummed). - * @param enforcers - Addresses of the enforcers. - * @param enforcers.nativeTokenPeriodicEnforcer - Address of the NativeTokenPeriodicEnforcer. - * @param enforcers.exactCalldataEnforcer - Address of the ExactCalldataEnforcer. + * @param contractAddresses - Checksummed enforcer addresses for the chain. * @returns Decoded allowance terms. */ function validateAndDecodeData( caveats: ChecksumCaveat[], - enforcers: Pick< - ChecksumEnforcersByChainId, - 'nativeTokenPeriodicEnforcer' | 'exactCalldataEnforcer' - >, + contractAddresses: ChecksumEnforcersByChainId, ): DecodedPermission['permission']['data'] { - const { nativeTokenPeriodicEnforcer, exactCalldataEnforcer } = enforcers; + const { nativeTokenPeriodicEnforcer, exactCalldataEnforcer } = + contractAddresses; const exactCalldataTerms = getTermsByEnforcer({ caveats, diff --git a/packages/gator-permissions-controller/src/decodePermission/rules/nativeTokenPeriodic.test.ts b/packages/gator-permissions-controller/src/decodePermission/decoders/nativeTokenPeriodic.test.ts similarity index 90% rename from packages/gator-permissions-controller/src/decodePermission/rules/nativeTokenPeriodic.test.ts rename to packages/gator-permissions-controller/src/decodePermission/decoders/nativeTokenPeriodic.test.ts index 7079cbd161..677f0fdcc9 100644 --- a/packages/gator-permissions-controller/src/decodePermission/rules/nativeTokenPeriodic.test.ts +++ b/packages/gator-permissions-controller/src/decodePermission/decoders/nativeTokenPeriodic.test.ts @@ -8,10 +8,10 @@ import { DELEGATOR_CONTRACTS, } from '@metamask/delegation-deployments'; -import { createPermissionRulesForContracts } from '.'; +import { createPermissionDecodersForContracts } from '.'; import { MAX_PERIOD_DURATION } from '../utils'; -describe('native-token-periodic rule', () => { +describe('native-token-periodic decoder', () => { const chainId = CHAIN_ID.sepolia; const contracts = DELEGATOR_CONTRACTS['1.3.0'][chainId]; const { @@ -19,12 +19,12 @@ describe('native-token-periodic rule', () => { NativeTokenPeriodTransferEnforcer, ExactCalldataEnforcer, } = contracts; - const permissionRules = createPermissionRulesForContracts(contracts); - const rule = permissionRules.find( + const permissionDecoders = createPermissionDecodersForContracts(contracts); + const decoder = permissionDecoders.find( (candidate) => candidate.permissionType === 'native-token-periodic', ); - if (!rule) { - throw new Error('Rule not found'); + if (!decoder) { + throw new Error('Decoder not found'); } const expiryCaveat = { @@ -65,7 +65,7 @@ describe('native-token-periodic rule', () => { args: '0x' as const, }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(false); // this is here as a type guard @@ -89,7 +89,7 @@ describe('native-token-periodic rule', () => { }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(false); // this is here as a type guard @@ -121,7 +121,7 @@ describe('native-token-periodic rule', () => { args: '0x' as const, }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(false); // this is here as a type guard @@ -155,7 +155,7 @@ describe('native-token-periodic rule', () => { args: '0x' as const, }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(false); // this is here as a type guard @@ -186,7 +186,7 @@ describe('native-token-periodic rule', () => { }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(true); // this is here as a type guard @@ -215,7 +215,7 @@ describe('native-token-periodic rule', () => { args: '0x' as const, }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(false); // this is here as a type guard @@ -243,7 +243,7 @@ describe('native-token-periodic rule', () => { args: '0x' as const, }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(false); // this is here as a type guard @@ -273,7 +273,7 @@ describe('native-token-periodic rule', () => { }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(false); // this is here as a type guard @@ -306,7 +306,7 @@ describe('native-token-periodic rule', () => { }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(false); // this is here as a type guard @@ -337,7 +337,7 @@ describe('native-token-periodic rule', () => { }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(true); // this is here as a type guard diff --git a/packages/gator-permissions-controller/src/decodePermission/rules/nativeTokenPeriodic.ts b/packages/gator-permissions-controller/src/decodePermission/decoders/nativeTokenPeriodic.ts similarity index 67% rename from packages/gator-permissions-controller/src/decodePermission/rules/nativeTokenPeriodic.ts rename to packages/gator-permissions-controller/src/decodePermission/decoders/nativeTokenPeriodic.ts index cb1a0c3d29..23d2d098ad 100644 --- a/packages/gator-permissions-controller/src/decodePermission/rules/nativeTokenPeriodic.ts +++ b/packages/gator-permissions-controller/src/decodePermission/decoders/nativeTokenPeriodic.ts @@ -4,7 +4,6 @@ import type { ChecksumCaveat, ChecksumEnforcersByChainId, DecodedPermission, - PermissionRule, } from '../types'; import { getByteLength, @@ -12,70 +11,60 @@ import { MAX_PERIOD_DURATION, splitHex, } from '../utils'; -import { makePermissionRule } from './makePermissionRule'; +import { expiryRule } from './expiryRule'; +import type { MakePermissionDecoderConfig } from './makePermissionDecoder'; +import { nativePayeeRule } from './nativePayeeRule'; +import { redeemerRule } from './redeemerRule'; /** - * Creates the native-token-periodic permission rule. + * Builds the configuration for the native-token-periodic permission decoder. * - * @param enforcers - Checksummed enforcer addresses for the chain. - * @returns The native-token-periodic permission rule. + * @param contractAddresses - Checksummed enforcer addresses for the chain. + * @returns The native-token-periodic permission decoder configuration. */ -export function makeNativeTokenPeriodicRule( - enforcers: ChecksumEnforcersByChainId, -): PermissionRule { +export function makeNativeTokenPeriodicDecoderConfig( + contractAddresses: ChecksumEnforcersByChainId, +): MakePermissionDecoderConfig { const { timestampEnforcer, nativeTokenPeriodicEnforcer, exactCalldataEnforcer, nonceEnforcer, - allowedCalldataEnforcer, allowedTargetsEnforcer, redeemerEnforcer, - } = enforcers; - return makePermissionRule({ + } = contractAddresses; + + return { permissionType: 'native-token-periodic', + contractAddresses, optionalEnforcers: [ - timestampEnforcer, - redeemerEnforcer, - allowedTargetsEnforcer, + timestampEnforcer, // expiry rule + redeemerEnforcer, // redeemer rule + allowedTargetsEnforcer, // payee rule ], - redeemerEnforcer, - payeeEnforcers: { - allowedCalldataEnforcer, - allowedTargetsEnforcer, - singlePayeeEnforcer: allowedTargetsEnforcer, - }, - timestampEnforcer, requiredEnforcers: { [nativeTokenPeriodicEnforcer]: 1, [exactCalldataEnforcer]: 1, [nonceEnforcer]: 1, }, - validateAndDecodeData: (caveats) => - validateAndDecodeData(caveats, { - nativeTokenPeriodicEnforcer, - exactCalldataEnforcer, - }), - }); + rules: [expiryRule, redeemerRule, nativePayeeRule], + validateAndDecodeData, + }; } /** * Decodes native-token-periodic permission data from caveats; throws on invalid. * * @param caveats - Caveats from the permission context (checksummed). - * @param enforcers - Addresses of the enforcers. - * @param enforcers.nativeTokenPeriodicEnforcer - Address of the NativeTokenPeriodicEnforcer. - * @param enforcers.exactCalldataEnforcer - Address of the ExactCalldataEnforcer. + * @param contractAddresses - Checksummed enforcer addresses for the chain. * @returns Decoded periodic terms. */ function validateAndDecodeData( caveats: ChecksumCaveat[], - enforcers: Pick< - ChecksumEnforcersByChainId, - 'nativeTokenPeriodicEnforcer' | 'exactCalldataEnforcer' - >, + contractAddresses: ChecksumEnforcersByChainId, ): DecodedPermission['permission']['data'] { - const { nativeTokenPeriodicEnforcer, exactCalldataEnforcer } = enforcers; + const { nativeTokenPeriodicEnforcer, exactCalldataEnforcer } = + contractAddresses; const exactCalldataTerms = getTermsByEnforcer({ caveats, diff --git a/packages/gator-permissions-controller/src/decodePermission/rules/nativeTokenStream.test.ts b/packages/gator-permissions-controller/src/decodePermission/decoders/nativeTokenStream.test.ts similarity index 91% rename from packages/gator-permissions-controller/src/decodePermission/rules/nativeTokenStream.test.ts rename to packages/gator-permissions-controller/src/decodePermission/decoders/nativeTokenStream.test.ts index 5d0a84abc2..28edc4be5b 100644 --- a/packages/gator-permissions-controller/src/decodePermission/rules/nativeTokenStream.test.ts +++ b/packages/gator-permissions-controller/src/decodePermission/decoders/nativeTokenStream.test.ts @@ -8,9 +8,9 @@ import { DELEGATOR_CONTRACTS, } from '@metamask/delegation-deployments'; -import { createPermissionRulesForContracts } from '.'; +import { createPermissionDecodersForContracts } from '.'; -describe('native-token-stream rule', () => { +describe('native-token-stream decoder', () => { const chainId = CHAIN_ID.sepolia; const contracts = DELEGATOR_CONTRACTS['1.3.0'][chainId]; const { @@ -18,12 +18,12 @@ describe('native-token-stream rule', () => { NativeTokenStreamingEnforcer, ExactCalldataEnforcer, } = contracts; - const permissionRules = createPermissionRulesForContracts(contracts); - const rule = permissionRules.find( + const permissionDecoders = createPermissionDecodersForContracts(contracts); + const decoder = permissionDecoders.find( (candidate) => candidate.permissionType === 'native-token-stream', ); - if (!rule) { - throw new Error('Rule not found'); + if (!decoder) { + throw new Error('Decoder not found'); } const expiryCaveat = { @@ -68,7 +68,7 @@ describe('native-token-stream rule', () => { }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(false); // this is here as a type guard @@ -100,7 +100,7 @@ describe('native-token-stream rule', () => { }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(false); // this is here as a type guard @@ -123,7 +123,7 @@ describe('native-token-stream rule', () => { }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(false); // this is here as a type guard @@ -156,7 +156,7 @@ describe('native-token-stream rule', () => { args: '0x' as const, }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(false); // this is here as a type guard @@ -191,7 +191,7 @@ describe('native-token-stream rule', () => { args: '0x' as const, }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(false); // this is here as a type guard @@ -222,7 +222,7 @@ describe('native-token-stream rule', () => { args: '0x' as const, }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(true); // this is here as a type guard @@ -261,7 +261,7 @@ describe('native-token-stream rule', () => { }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(false); // this is here as a type guard @@ -300,7 +300,7 @@ describe('native-token-stream rule', () => { }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(false); // this is here as a type guard @@ -339,7 +339,7 @@ describe('native-token-stream rule', () => { }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(false); // this is here as a type guard @@ -364,7 +364,7 @@ describe('native-token-stream rule', () => { exactCalldataCaveat, { enforcer: NativeTokenStreamingEnforcer, terms, args: '0x' as const }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(false); // this is here as a type guard @@ -388,7 +388,7 @@ describe('native-token-stream rule', () => { exactCalldataCaveat, { enforcer: NativeTokenStreamingEnforcer, terms, args: '0x' as const }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(false); // this is here as a type guard @@ -412,7 +412,7 @@ describe('native-token-stream rule', () => { { enforcer: NativeTokenStreamingEnforcer, terms, args: '0x' as const }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(false); // this is here as a type guard @@ -437,7 +437,7 @@ describe('native-token-stream rule', () => { exactCalldataCaveat, { enforcer: NativeTokenStreamingEnforcer, terms, args: '0x' as const }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(false); // this is here as a type guard @@ -462,7 +462,7 @@ describe('native-token-stream rule', () => { { enforcer: NativeTokenStreamingEnforcer, terms, args: '0x' as const }, ]; - const result = rule.validateAndDecodePermission(caveats); + const result = decoder.validateAndDecodePermission(caveats); expect(result.isValid).toBe(false); // this is here as a type guard diff --git a/packages/gator-permissions-controller/src/decodePermission/rules/nativeTokenStream.ts b/packages/gator-permissions-controller/src/decodePermission/decoders/nativeTokenStream.ts similarity index 66% rename from packages/gator-permissions-controller/src/decodePermission/rules/nativeTokenStream.ts rename to packages/gator-permissions-controller/src/decodePermission/decoders/nativeTokenStream.ts index 57454b3929..b2dbffa8d4 100644 --- a/packages/gator-permissions-controller/src/decodePermission/rules/nativeTokenStream.ts +++ b/packages/gator-permissions-controller/src/decodePermission/decoders/nativeTokenStream.ts @@ -4,73 +4,62 @@ import type { ChecksumCaveat, ChecksumEnforcersByChainId, DecodedPermission, - PermissionRule, } from '../types'; import { getByteLength, getTermsByEnforcer, splitHex } from '../utils'; -import { makePermissionRule } from './makePermissionRule'; +import { expiryRule } from './expiryRule'; +import type { MakePermissionDecoderConfig } from './makePermissionDecoder'; +import { nativePayeeRule } from './nativePayeeRule'; +import { redeemerRule } from './redeemerRule'; /** - * Creates the native-token-stream permission rule. + * Builds the configuration for the native-token-stream permission decoder. * - * @param enforcers - Checksummed enforcer addresses for the chain. - * @returns The native-token-stream permission rule. + * @param contractAddresses - Checksummed enforcer addresses for the chain. + * @returns The native-token-stream permission decoder configuration. */ -export function makeNativeTokenStreamRule( - enforcers: ChecksumEnforcersByChainId, -): PermissionRule { +export function makeNativeTokenStreamDecoderConfig( + contractAddresses: ChecksumEnforcersByChainId, +): MakePermissionDecoderConfig { const { timestampEnforcer, nativeTokenStreamingEnforcer, exactCalldataEnforcer, nonceEnforcer, - allowedCalldataEnforcer, allowedTargetsEnforcer, redeemerEnforcer, - } = enforcers; - return makePermissionRule({ + } = contractAddresses; + + return { permissionType: 'native-token-stream', + contractAddresses, optionalEnforcers: [ - timestampEnforcer, - redeemerEnforcer, - allowedTargetsEnforcer, + timestampEnforcer, // expiry rule + redeemerEnforcer, // redeemer rule + allowedTargetsEnforcer, // payee rule ], - redeemerEnforcer, - payeeEnforcers: { - allowedCalldataEnforcer, - allowedTargetsEnforcer, - singlePayeeEnforcer: allowedTargetsEnforcer, - }, - timestampEnforcer, requiredEnforcers: { [nativeTokenStreamingEnforcer]: 1, [exactCalldataEnforcer]: 1, [nonceEnforcer]: 1, }, - validateAndDecodeData: (caveats) => - validateAndDecodeData(caveats, { - nativeTokenStreamingEnforcer, - exactCalldataEnforcer, - }), - }); + rules: [expiryRule, redeemerRule, nativePayeeRule], + validateAndDecodeData, + }; } /** * Decodes native-token-stream permission data from caveats; throws on invalid. * * @param caveats - Caveats from the permission context (checksummed). - * @param enforcers - Addresses of the enforcers. - * @param enforcers.nativeTokenStreamingEnforcer - Address of the NativeTokenStreamingEnforcer. - * @param enforcers.exactCalldataEnforcer - Address of the ExactCalldataEnforcer. + * @param contractAddresses - Checksummed enforcer addresses for the chain. * @returns Decoded stream terms. */ function validateAndDecodeData( caveats: ChecksumCaveat[], - enforcers: Pick< - ChecksumEnforcersByChainId, - 'nativeTokenStreamingEnforcer' | 'exactCalldataEnforcer' - >, + contractAddresses: ChecksumEnforcersByChainId, ): DecodedPermission['permission']['data'] { - const { nativeTokenStreamingEnforcer, exactCalldataEnforcer } = enforcers; + const { nativeTokenStreamingEnforcer, exactCalldataEnforcer } = + contractAddresses; const exactCalldataTerms = getTermsByEnforcer({ caveats, diff --git a/packages/gator-permissions-controller/src/decodePermission/decoders/redeemerRule.test.ts b/packages/gator-permissions-controller/src/decodePermission/decoders/redeemerRule.test.ts new file mode 100644 index 0000000000..c397fccf60 --- /dev/null +++ b/packages/gator-permissions-controller/src/decodePermission/decoders/redeemerRule.test.ts @@ -0,0 +1,102 @@ +import { createRedeemerTerms } from '@metamask/delegation-core'; +import { + CHAIN_ID, + DELEGATOR_CONTRACTS, +} from '@metamask/delegation-deployments'; +import { getChecksumAddress } from '@metamask/utils'; +import type { Hex } from '@metamask/utils'; + +import type { ChecksumCaveat } from '../types'; +import { getChecksumEnforcersByChainId } from '../utils'; +import { redeemerRule } from './redeemerRule'; + +describe('redeemerRule', () => { + const contracts = DELEGATOR_CONTRACTS['1.3.0'][CHAIN_ID.sepolia]; + const contractAddresses = getChecksumEnforcersByChainId(contracts); + const { redeemerEnforcer, nonceEnforcer } = contractAddresses; + const requiredEnforcers = new Map([[nonceEnforcer, 1]]); + + const ADDRESS_A: Hex = '0x1111111111111111111111111111111111111111'; + const ADDRESS_B: Hex = '0x2222222222222222222222222222222222222222'; + const CHECKSUM_REDEEMER_INPUT: Hex = + '0x52908400098527886e0f7030069857d2e4169ee7'; + + it('returns null when no RedeemerEnforcer caveat is present', () => { + const caveats: ChecksumCaveat[] = [ + { enforcer: nonceEnforcer, terms: '0x' as Hex, args: '0x' as Hex }, + ]; + + expect( + redeemerRule({ contractAddresses, caveats, requiredEnforcers }), + ).toBeNull(); + }); + + it('returns a redeemer rule with a single decoded address', () => { + const caveats: ChecksumCaveat[] = [ + { + enforcer: redeemerEnforcer, + terms: createRedeemerTerms({ redeemers: [ADDRESS_A] }), + args: '0x' as Hex, + }, + ]; + + expect( + redeemerRule({ contractAddresses, caveats, requiredEnforcers }), + ).toStrictEqual({ + type: 'redeemer', + data: { addresses: [ADDRESS_A] }, + }); + }); + + it('returns a redeemer rule with multiple decoded addresses', () => { + const caveats: ChecksumCaveat[] = [ + { + enforcer: redeemerEnforcer, + terms: createRedeemerTerms({ redeemers: [ADDRESS_A, ADDRESS_B] }), + args: '0x' as Hex, + }, + ]; + + expect( + redeemerRule({ contractAddresses, caveats, requiredEnforcers }), + ).toStrictEqual({ + type: 'redeemer', + data: { addresses: [ADDRESS_A, ADDRESS_B] }, + }); + }); + + it('returns checksummed redeemer addresses', () => { + const caveats: ChecksumCaveat[] = [ + { + enforcer: redeemerEnforcer, + terms: createRedeemerTerms({ redeemers: [CHECKSUM_REDEEMER_INPUT] }), + args: '0x' as Hex, + }, + ]; + + expect( + redeemerRule({ contractAddresses, caveats, requiredEnforcers }), + ).toStrictEqual({ + type: 'redeemer', + data: { addresses: [getChecksumAddress(CHECKSUM_REDEEMER_INPUT)] }, + }); + }); + + it('ignores caveats from unrelated enforcers', () => { + const caveats: ChecksumCaveat[] = [ + { enforcer: nonceEnforcer, terms: '0x' as Hex, args: '0x' as Hex }, + { + enforcer: redeemerEnforcer, + terms: createRedeemerTerms({ redeemers: [ADDRESS_A] }), + args: '0x' as Hex, + }, + ]; + + expect( + redeemerRule({ contractAddresses, caveats, requiredEnforcers }), + ).toStrictEqual({ + type: 'redeemer', + data: { addresses: [getChecksumAddress(ADDRESS_A)] }, + }); + }); +}); diff --git a/packages/gator-permissions-controller/src/decodePermission/decoders/redeemerRule.ts b/packages/gator-permissions-controller/src/decodePermission/decoders/redeemerRule.ts new file mode 100644 index 0000000000..0e2b00c490 --- /dev/null +++ b/packages/gator-permissions-controller/src/decodePermission/decoders/redeemerRule.ts @@ -0,0 +1,38 @@ +import { decodeRedeemerTerms } from '@metamask/delegation-core'; +import { getChecksumAddress } from '@metamask/utils'; + +import { EXECUTION_PERMISSION_REDEEMER_RULE_TYPE } from '../../constants'; +import type { RuleDecoder } from '../types'; +import { getTermsByEnforcer } from '../utils'; + +/** + * Rule decoder that extracts a redeemer allowlist from a RedeemerEnforcer + * caveat, when present. + * + * @param args - The arguments to this function. + * @param args.contractAddresses - Checksummed enforcer addresses for the chain. + * @param args.caveats - Checksummed caveats from the delegation. + * @returns A `{ rule }` result containing the redeemer addresses when a + * RedeemerEnforcer caveat exists, otherwise `null`. + */ +export const redeemerRule: RuleDecoder = ({ contractAddresses, caveats }) => { + const { redeemerEnforcer } = contractAddresses; + + const redeemerTerms = getTermsByEnforcer({ + caveats, + enforcer: redeemerEnforcer, + throwIfNotFound: false, + }); + + if (!redeemerTerms) { + return null; + } + + return { + type: EXECUTION_PERMISSION_REDEEMER_RULE_TYPE, + data: { + addresses: + decodeRedeemerTerms(redeemerTerms).redeemers.map(getChecksumAddress), + }, + }; +}; diff --git a/packages/gator-permissions-controller/src/decodePermission/index.ts b/packages/gator-permissions-controller/src/decodePermission/index.ts index 7555a45a63..7fa25e33f4 100644 --- a/packages/gator-permissions-controller/src/decodePermission/index.ts +++ b/packages/gator-permissions-controller/src/decodePermission/index.ts @@ -1,13 +1,13 @@ export { - findRuleWithMatchingCaveatAddresses, - findRulesWithMatchingCaveatAddresses, + findDecoderWithMatchingCaveatAddresses, + findDecodersWithMatchingCaveatAddresses, reconstructDecodedPermission, - selectUniqueRuleAndDecodedPermission, + selectUniqueDecoderAndDecodedPermission, } from './decodePermission'; -export { createPermissionRulesForContracts } from './rules'; +export { createPermissionDecodersForContracts } from './decoders'; export type { DecodedPermission, - PermissionRule, + PermissionDecoder, ValidateAndDecodeResult, } from './types'; diff --git a/packages/gator-permissions-controller/src/decodePermission/rules/index.ts b/packages/gator-permissions-controller/src/decodePermission/rules/index.ts deleted file mode 100644 index f6499ec166..0000000000 --- a/packages/gator-permissions-controller/src/decodePermission/rules/index.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { DeployedContractsByName, PermissionRule } from '../types'; -import { getChecksumEnforcersByChainId } from '../utils'; -import { makeErc20TokenAllowanceRule } from './erc20TokenAllowance'; -import { makeErc20TokenPeriodicRule } from './erc20TokenPeriodic'; -import { makeErc20TokenRevocationRule } from './erc20TokenRevocation'; -import { makeErc20TokenStreamRule } from './erc20TokenStream'; -import { makeNativeTokenAllowanceRule } from './nativeTokenAllowance'; -import { makeNativeTokenPeriodicRule } from './nativeTokenPeriodic'; -import { makeNativeTokenStreamRule } from './nativeTokenStream'; - -/** - * Builds the canonical set of permission matching rules for a chain. - * - * Each rule specifies the `permissionType`, required/optional enforcers, - * and provides `caveatAddressesMatch` and `validateAndDecodePermission` so the - * entire decode flow can be driven by the rules. - * - * @param contracts - The deployed contracts for the chain. - * @returns A list of permission rules used to identify and decode permission types. - * @throws Propagates any errors from resolving enforcer addresses. - */ -export const createPermissionRulesForContracts = ( - contracts: DeployedContractsByName, -): PermissionRule[] => { - const enforcers = getChecksumEnforcersByChainId(contracts); - return [ - makeNativeTokenStreamRule(enforcers), - makeNativeTokenPeriodicRule(enforcers), - makeNativeTokenAllowanceRule(enforcers), - makeErc20TokenStreamRule(enforcers), - makeErc20TokenPeriodicRule(enforcers), - makeErc20TokenAllowanceRule(enforcers), - makeErc20TokenRevocationRule(enforcers), - ]; -}; diff --git a/packages/gator-permissions-controller/src/decodePermission/rules/makePermissionRule.ts b/packages/gator-permissions-controller/src/decodePermission/rules/makePermissionRule.ts deleted file mode 100644 index b3966908fa..0000000000 --- a/packages/gator-permissions-controller/src/decodePermission/rules/makePermissionRule.ts +++ /dev/null @@ -1,254 +0,0 @@ -import type { Rule } from '@metamask/7715-permission-types'; -import type { Caveat } from '@metamask/delegation-core'; -import { - decodeAllowedCalldataTerms, - decodeAllowedTargetsTerms, - decodeRedeemerTerms, -} from '@metamask/delegation-core'; -import { getChecksumAddress, isStrictHexString } from '@metamask/utils'; -import type { Hex } from '@metamask/utils'; - -import { - EXECUTION_PERMISSION_PAYEE_RULE_TYPE, - EXECUTION_PERMISSION_REDEEMER_RULE_TYPE, -} from '../../constants'; -import type { - ChecksumCaveat, - DecodedPermission, - PermissionRule, - PermissionType, - ValidateAndDecodeResult, -} from '../types'; -import { - buildEnforcerCountsAndSet, - enforcersMatchRule, - extractExpiryFromCaveatTerms, - getByteLength, - getTermsByEnforcer, -} from '../utils'; - -const ERC20_TRANSFER_PAYEE_START_INDEX = 4; -const ERC20_PAYEE_VALUE_BYTE_LENGTH = 32; - -type PayeeEnforcerAddresses = { - allowedCalldataEnforcer: Hex; - allowedTargetsEnforcer: Hex; - singlePayeeEnforcer: Hex; -}; - -/** - * Creates a single permission rule with the given type, enforcer sets, and - * decode/validate callbacks. - * - * @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 used to extract redeemer rules. - * @param args.payeeEnforcers - Addresses of enforcers used to extract payee rules. - * @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. - * @param args.validateAndDecodeData - Callback to decode caveats into permission data; may throw. - * @returns A permission rule with caveatAddressesMatch and validateAndDecodePermission. - */ -export function makePermissionRule({ - optionalEnforcers, - redeemerEnforcer, - payeeEnforcers, - timestampEnforcer, - permissionType, - requiredEnforcers, - validateAndDecodeData, -}: { - optionalEnforcers: Hex[]; - redeemerEnforcer: Hex; - payeeEnforcers: PayeeEnforcerAddresses; - timestampEnforcer: Hex; - permissionType: PermissionType; - requiredEnforcers: Record; - validateAndDecodeData: ( - caveats: ChecksumCaveat[], - ) => DecodedPermission['permission']['data']; -}): PermissionRule { - const optionalEnforcersSet = new Set(optionalEnforcers); - const requiredEnforcersMap = new Map( - Object.entries(requiredEnforcers), - ) as Map; - - return { - permissionType, - requiredEnforcers: requiredEnforcersMap, - optionalEnforcers: optionalEnforcersSet, - caveatAddressesMatch(caveatAddresses: Hex[]): boolean { - const { counts, enforcersSet } = - buildEnforcerCountsAndSet(caveatAddresses); - - return enforcersMatchRule( - counts, - enforcersSet, - requiredEnforcersMap, - optionalEnforcersSet, - ); - }, - validateAndDecodePermission( - caveats: Caveat[], - ): ValidateAndDecodeResult { - const checksumCaveats: ChecksumCaveat[] = caveats.map((caveat) => ({ - ...caveat, - enforcer: getChecksumAddress(caveat.enforcer), - })); - try { - const invalidTerms = checksumCaveats.filter( - // isStrictHexString rejects '0x' which is a valid terms value - ({ terms }) => terms !== '0x' && !isStrictHexString(terms), - ); - - if (invalidTerms.length > 0) { - throw new Error('Invalid terms: must be a hex string'); - } - - let expiry: number | null = null; - - const expiryTerms = getTermsByEnforcer({ - caveats: checksumCaveats, - enforcer: timestampEnforcer, - throwIfNotFound: false, - }); - - if (expiryTerms) { - expiry = extractExpiryFromCaveatTerms(expiryTerms); - } - - const data = validateAndDecodeData(checksumCaveats); - - const redeemerTerms = getTermsByEnforcer({ - caveats: checksumCaveats, - enforcer: redeemerEnforcer, - throwIfNotFound: false, - }); - - const rules: Rule[] = []; - if (redeemerTerms) { - rules.push({ - type: EXECUTION_PERMISSION_REDEEMER_RULE_TYPE, - data: { - addresses: decodeRedeemerTerms(redeemerTerms).redeemers, - }, - }); - } - - // todo: this is a temporary fix to exclude payee rules from erc20-token-revocation - // a nicer solution may be to pass an array of permissionRule decoders to the makePermissionRule - // function. - if (permissionType !== 'erc20-token-revocation') { - const payeeAddresses = tryExtractPayeeAddresses( - checksumCaveats, - payeeEnforcers, - requiredEnforcersMap, - ); - if (payeeAddresses) { - rules.push({ - type: EXECUTION_PERMISSION_PAYEE_RULE_TYPE, - data: { addresses: payeeAddresses }, - }); - } - } - - return { - isValid: true, - expiry, - data, - rules: rules.length > 0 ? rules : undefined, - }; - } catch (caughtError) { - return { isValid: false, error: caughtError as Error }; - } - }, - }; -} - -/** - * Attempts to extract payee addresses from a payee enforcer caveat. - * - * @param caveat - The payee caveat to decode. - * @param payeeEnforcerAddresses - Known payee enforcer addresses for comparison. - * @param payeeEnforcerAddresses.allowedCalldataEnforcer - AllowedCalldataEnforcer address. - * @param payeeEnforcerAddresses.allowedTargetsEnforcer - AllowedTargetsEnforcer address. - * @returns The checksummed payee addresses, or null if the enforcer is unrecognised. - */ -function extractPayeeAddressesFromCaveat( - caveat: Caveat, - payeeEnforcerAddresses: { - allowedCalldataEnforcer: Hex; - allowedTargetsEnforcer: Hex; - }, -): Hex[] { - const checksumEnforcer = getChecksumAddress(caveat.enforcer); - - if (checksumEnforcer === payeeEnforcerAddresses.allowedCalldataEnforcer) { - const decoded = decodeAllowedCalldataTerms(caveat.terms); - if (decoded.startIndex !== ERC20_TRANSFER_PAYEE_START_INDEX) { - throw new Error( - `Invalid payee caveat: AllowedCalldataEnforcer startIndex must be ${ERC20_TRANSFER_PAYEE_START_INDEX}`, - ); - } - - if (getByteLength(decoded.value) !== ERC20_PAYEE_VALUE_BYTE_LENGTH) { - throw new Error( - `Invalid payee caveat: AllowedCalldataEnforcer value must be ${ERC20_PAYEE_VALUE_BYTE_LENGTH} bytes long`, - ); - } - - const address: Hex = `0x${decoded.value.slice(-40)}`; - return [getChecksumAddress(address)]; - } - - if (checksumEnforcer === payeeEnforcerAddresses.allowedTargetsEnforcer) { - const decoded = decodeAllowedTargetsTerms(caveat.terms); - return decoded.targets.map(getChecksumAddress); - } - - throw new Error('Invalid payee caveat: unrecognised enforcer'); -} - -/** - * Attempts to extract payee addresses from caveats, handling both single-payee - * (direct enforcer) and multi-payee (RedeemerEnforcer). - * - * @param caveats - Checksummed caveats from the delegation. - * @param enforcers - Payee enforcer addresses. - * @param enforcers.allowedCalldataEnforcer - AllowedCalldataEnforcer address. - * @param enforcers.allowedTargetsEnforcer - AllowedTargetsEnforcer address. - * @param enforcers.singlePayeeEnforcer - The specific enforcer for single-payee in this permission type. - * @param requiredEnforcers - Required enforcer counts for the permission rule. - * @returns Array of checksummed payee addresses, or null if no payee caveat is found. - */ -function tryExtractPayeeAddresses( - caveats: ChecksumCaveat[], - enforcers: PayeeEnforcerAddresses, - requiredEnforcers: Map, -): Hex[] | null { - if (requiredEnforcers.has(enforcers.singlePayeeEnforcer)) { - throw new Error( - 'Invalid payee caveats: singlePayeeEnforcer may not be a required caveat', - ); - } - - const singlePayeeCaveats = caveats.filter( - (caveat) => caveat.enforcer === enforcers.singlePayeeEnforcer, - ); - - // this should not be possible, unless the singlePayeeCaveat is also included for a different rule, for the permission itself - if (singlePayeeCaveats.length > 1) { - throw new Error( - 'Invalid payee caveats: multiple singlePayeeEnforcer caveats', - ); - } - - const singlePayeeCaveat = singlePayeeCaveats[0] ?? null; - - if (singlePayeeCaveat) { - return extractPayeeAddressesFromCaveat(singlePayeeCaveat, enforcers); - } - - return null; -} diff --git a/packages/gator-permissions-controller/src/decodePermission/types.ts b/packages/gator-permissions-controller/src/decodePermission/types.ts index e27d4a8033..f068745a66 100644 --- a/packages/gator-permissions-controller/src/decodePermission/types.ts +++ b/packages/gator-permissions-controller/src/decodePermission/types.ts @@ -120,17 +120,18 @@ export type ValidateAndDecodeResult = | { isValid: false; error: Error }; /** - * A rule that defines the required and optional enforcers for a permission type, - * and provides methods to test whether caveat addresses match the rule and to - * validate and decode permission terms from caveats. + * A decoder that defines the required and optional enforcers for a permission + * type, and provides methods to test whether caveat addresses match the + * permission and to validate and decode permission terms from caveats. */ -export type PermissionRule = { +export type PermissionDecoder = { permissionType: PermissionType; requiredEnforcers: Map; optionalEnforcers: Set; /** * Returns true if the given caveat addresses (enforcer addresses) match this - * rule (required enforcers present with correct multiplicity, no forbidden enforcers). + * decoder (required enforcers present with correct multiplicity, no + * forbidden enforcers). */ caveatAddressesMatch: (caveatAddresses: Hex[]) => boolean; /** @@ -141,3 +142,17 @@ export type PermissionRule = { caveats: Caveat[], ) => ValidateAndDecodeResult; }; + +/** + * A function that inspects checksummed caveats and optionally produces a + * {@link Rule} (e.g. redeemer, payee, expiry). Each rule decoder is + * responsible for a single rule type and is composed by + * `makePermissionDecoder` to populate the permission's `rules` array. The + * `expiry` rule, in addition to being appended to `rules`, has its value + * hoisted onto the top-level `expiry` field of the decoded permission. + */ +export type RuleDecoder = (args: { + contractAddresses: ChecksumEnforcersByChainId; + caveats: ChecksumCaveat[]; + requiredEnforcers: Map; +}) => Rule | null; diff --git a/packages/gator-permissions-controller/src/decodePermission/utils.test.ts b/packages/gator-permissions-controller/src/decodePermission/utils.test.ts index 2cc7902c1c..fd06940525 100644 --- a/packages/gator-permissions-controller/src/decodePermission/utils.test.ts +++ b/packages/gator-permissions-controller/src/decodePermission/utils.test.ts @@ -2,7 +2,7 @@ import type { Caveat } from '@metamask/delegation-core'; import { getChecksumAddress } from '@metamask/utils'; import type { Hex } from '@metamask/utils'; -import { createPermissionRulesForContracts } from './rules'; +import { createPermissionDecodersForContracts } from './decoders'; import type { DeployedContractsByName } from './types'; import { getChecksumEnforcersByChainId, @@ -69,8 +69,8 @@ describe('getChecksumEnforcersByChainId', () => { }); }); -describe('createPermissionRulesForChainId', () => { - it('builds canonical rules with correct required and allowed enforcers', () => { +describe('createPermissionDecodersForContracts', () => { + it('builds canonical decoders with correct required and allowed enforcers', () => { const contracts = buildContracts(); const { erc20StreamingEnforcer, @@ -94,11 +94,11 @@ describe('createPermissionRulesForChainId', () => { // native-token-allowance // erc20-token-revocation const permissionTypeCount = 7; - const rules = createPermissionRulesForContracts(contracts); - expect(rules).toHaveLength(permissionTypeCount); + const decoders = createPermissionDecodersForContracts(contracts); + expect(decoders).toHaveLength(permissionTypeCount); const byType = Object.fromEntries( - rules.map((rule) => [rule.permissionType, rule]), + decoders.map((decoder) => [decoder.permissionType, decoder]), ); // native-token-stream @@ -293,9 +293,9 @@ describe('createPermissionRulesForChainId', () => { ); }); - it('each rule has caveatAddressesMatch and validateAndDecodePermission', () => { + it('each decoder has caveatAddressesMatch and validateAndDecodePermission', () => { const contracts = buildContracts(); - const rules = createPermissionRulesForContracts(contracts); + const decoders = createPermissionDecodersForContracts(contracts); const { nativeTokenStreamingEnforcer, exactCalldataEnforcer, @@ -303,17 +303,17 @@ describe('createPermissionRulesForChainId', () => { timestampEnforcer, } = getChecksumEnforcersByChainId(contracts); - for (const rule of rules) { - expect(typeof rule.caveatAddressesMatch).toBe('function'); - expect(typeof rule.validateAndDecodePermission).toBe('function'); + for (const decoder of decoders) { + expect(typeof decoder.caveatAddressesMatch).toBe('function'); + expect(typeof decoder.validateAndDecodePermission).toBe('function'); } - const nativeStreamRule = rules.find( + const nativeStreamDecoder = decoders.find( (candidate) => candidate.permissionType === 'native-token-stream', ); - expect(nativeStreamRule).toBeDefined(); - if (!nativeStreamRule) { - throw new Error('Rule not found'); + expect(nativeStreamDecoder).toBeDefined(); + if (!nativeStreamDecoder) { + throw new Error('Decoder not found'); } const matchingCaveatAddresses: Hex[] = [ @@ -322,9 +322,9 @@ describe('createPermissionRulesForChainId', () => { nonceEnforcer, timestampEnforcer, ]; - expect(nativeStreamRule.caveatAddressesMatch(matchingCaveatAddresses)).toBe( - true, - ); + expect( + nativeStreamDecoder.caveatAddressesMatch(matchingCaveatAddresses), + ).toBe(true); }); }); diff --git a/packages/gator-permissions-controller/src/index.ts b/packages/gator-permissions-controller/src/index.ts index 0661ccf966..b424cad000 100644 --- a/packages/gator-permissions-controller/src/index.ts +++ b/packages/gator-permissions-controller/src/index.ts @@ -1,6 +1,7 @@ export { default as GatorPermissionsController } from './GatorPermissionsController'; export { DELEGATION_FRAMEWORK_VERSION, + EXECUTION_PERMISSION_EXPIRY_RULE_TYPE, EXECUTION_PERMISSION_PAYEE_RULE_TYPE, EXECUTION_PERMISSION_REDEEMER_RULE_TYPE, } from './constants';