diff --git a/packages/gator-permissions-controller/CHANGELOG.md b/packages/gator-permissions-controller/CHANGELOG.md index 8296742ad1..629bc3f7e7 100644 --- a/packages/gator-permissions-controller/CHANGELOG.md +++ b/packages/gator-permissions-controller/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Add `payee` rule to execution permission decoding for all known permission types ([#8668](https://github.com/MetaMask/core/pull/8668)) - Support `RedeemerEnforcer` caveat when decoding execution permissions ([#8537](https://github.com/MetaMask/core/pull/8537)) - Permission decoding now recognizes the `RedeemerEnforcer` as an optional caveat on all execution permission types and extracts a `redeemer` rule containing the allowlisted addresses. - `DecodedPermission` type now includes an optional `rules` property for rules recovered from caveats. diff --git a/packages/gator-permissions-controller/package.json b/packages/gator-permissions-controller/package.json index eff5bae911..117b5086a9 100644 --- a/packages/gator-permissions-controller/package.json +++ b/packages/gator-permissions-controller/package.json @@ -56,8 +56,8 @@ "@metamask/7715-permission-types": "^0.6.0", "@metamask/abi-utils": "^2.0.3", "@metamask/base-controller": "^9.1.0", - "@metamask/delegation-core": "^1.1.0", - "@metamask/delegation-deployments": "^0.12.0", + "@metamask/delegation-core": "^2.0.0", + "@metamask/delegation-deployments": "^1.3.0", "@metamask/messenger": "^1.2.0", "@metamask/network-controller": "^30.1.0", "@metamask/snaps-controllers": "^19.0.0", diff --git a/packages/gator-permissions-controller/src/constants.ts b/packages/gator-permissions-controller/src/constants.ts index feda806752..7a4ad01c57 100644 --- a/packages/gator-permissions-controller/src/constants.ts +++ b/packages/gator-permissions-controller/src/constants.ts @@ -10,3 +10,11 @@ export const DELEGATION_FRAMEWORK_VERSION = '1.3.0'; * supported execution permission type. */ export const EXECUTION_PERMISSION_REDEEMER_RULE_TYPE = 'redeemer' as const; + +/** + * `Rule.type` / `wallet_getSupportedExecutionPermissions` `ruleTypes` entry for + * payee allowlists (AllowedCalldataEnforcer / AllowedTargetsEnforcer). Hosts + * should advertise this for every supported execution permission type that supports + * payee restrictions. + */ +export const EXECUTION_PERMISSION_PAYEE_RULE_TYPE = 'payee' as const; diff --git a/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenAllowance.ts b/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenAllowance.ts index 47d41de17f..01c5e02ea9 100644 --- a/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenAllowance.ts +++ b/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenAllowance.ts @@ -33,12 +33,23 @@ export function makeErc20TokenAllowanceRule( erc20PeriodicEnforcer, valueLteEnforcer, nonceEnforcer, + allowedCalldataEnforcer, + allowedTargetsEnforcer, redeemerEnforcer, } = enforcers; return makePermissionRule({ permissionType: 'erc20-token-allowance', - optionalEnforcers: [timestampEnforcer, redeemerEnforcer], + optionalEnforcers: [ + timestampEnforcer, + redeemerEnforcer, + allowedCalldataEnforcer, + ], redeemerEnforcer, + payeeEnforcers: { + allowedCalldataEnforcer, + allowedTargetsEnforcer, + singlePayeeEnforcer: allowedCalldataEnforcer, + }, timestampEnforcer, requiredEnforcers: { [erc20PeriodicEnforcer]: 1, diff --git a/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenPeriodic.ts b/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenPeriodic.ts index 7ccd7430e1..5b84141063 100644 --- a/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenPeriodic.ts +++ b/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenPeriodic.ts @@ -29,12 +29,23 @@ export function makeErc20TokenPeriodicRule( erc20PeriodicEnforcer, valueLteEnforcer, nonceEnforcer, + allowedCalldataEnforcer, + allowedTargetsEnforcer, redeemerEnforcer, } = enforcers; return makePermissionRule({ permissionType: 'erc20-token-periodic', - optionalEnforcers: [timestampEnforcer, redeemerEnforcer], + optionalEnforcers: [ + timestampEnforcer, + redeemerEnforcer, + allowedCalldataEnforcer, + ], redeemerEnforcer, + payeeEnforcers: { + allowedCalldataEnforcer, + allowedTargetsEnforcer, + singlePayeeEnforcer: allowedCalldataEnforcer, + }, timestampEnforcer, requiredEnforcers: { [erc20PeriodicEnforcer]: 1, diff --git a/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenRevocation.test.ts b/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenRevocation.test.ts index b27c8b5af4..32d2a37cd2 100644 --- a/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenRevocation.test.ts +++ b/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenRevocation.test.ts @@ -3,14 +3,19 @@ import { CHAIN_ID, DELEGATOR_CONTRACTS, } from '@metamask/delegation-deployments'; +import { getChecksumAddress } from '@metamask/utils'; import { createPermissionRulesForContracts } from '.'; describe('erc20-token-revocation rule', () => { const chainId = CHAIN_ID.sepolia; const contracts = DELEGATOR_CONTRACTS['1.3.0'][chainId]; - const { TimestampEnforcer, AllowedCalldataEnforcer, ValueLteEnforcer } = - contracts; + const { + TimestampEnforcer, + AllowedCalldataEnforcer, + ValueLteEnforcer, + RedeemerEnforcer, + } = contracts; const permissionRules = createPermissionRulesForContracts(contracts); const rule = permissionRules.find( (candidate) => candidate.permissionType === 'erc20-token-revocation', @@ -217,5 +222,56 @@ describe('erc20-token-revocation rule', () => { expect(result.expiry).toBe(1720000); expect(result.data).toStrictEqual({}); + expect(result.rules).toBeUndefined(); + }); + + it('includes redeemer rule but not payee when RedeemerEnforcer caveat is present', () => { + const packedAddr = '1111111111111111111111111111111111111111' as const; + const approveSelectorTerms = + '0x0000000000000000000000000000000000000000000000000000000000000000095ea7b3' as const; + const zeroAmountTerms = + '0x00000000000000000000000000000000000000000000000000000000000000240000000000000000000000000000000000000000000000000000000000000000' as const; + const zeroValueLteTerms = + '0x0000000000000000000000000000000000000000000000000000000000000000' as const; + const caveats = [ + expiryCaveat, + { + enforcer: AllowedCalldataEnforcer, + terms: approveSelectorTerms, + args: '0x' as const, + }, + { + enforcer: AllowedCalldataEnforcer, + terms: zeroAmountTerms, + args: '0x' as const, + }, + { + enforcer: ValueLteEnforcer, + terms: zeroValueLteTerms, + args: '0x' as const, + }, + { + enforcer: RedeemerEnforcer, + terms: `0x${packedAddr}` as const, + args: '0x' as const, + }, + ]; + const result = rule.validateAndDecodePermission(caveats); + expect(result.isValid).toBe(true); + if (!result.isValid) { + throw new Error('Expected valid result'); + } + expect(result.rules).toStrictEqual([ + { + type: 'redeemer', + data: { + addresses: [ + getChecksumAddress( + '0x1111111111111111111111111111111111111111' as const, + ), + ], + }, + }, + ]); }); }); diff --git a/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenRevocation.ts b/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenRevocation.ts index 8dde558038..a127829d8e 100644 --- a/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenRevocation.ts +++ b/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenRevocation.ts @@ -24,6 +24,7 @@ export function makeErc20TokenRevocationRule( const { timestampEnforcer, allowedCalldataEnforcer, + allowedTargetsEnforcer, valueLteEnforcer, nonceEnforcer, redeemerEnforcer, @@ -32,6 +33,11 @@ export function makeErc20TokenRevocationRule( permissionType: 'erc20-token-revocation', optionalEnforcers: [timestampEnforcer, redeemerEnforcer], redeemerEnforcer, + payeeEnforcers: { + allowedCalldataEnforcer, + allowedTargetsEnforcer, + singlePayeeEnforcer: allowedCalldataEnforcer, + }, timestampEnforcer, requiredEnforcers: { [allowedCalldataEnforcer]: 2, diff --git a/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenStream.ts b/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenStream.ts index ee08c3852f..c87c782173 100644 --- a/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenStream.ts +++ b/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenStream.ts @@ -28,12 +28,23 @@ export function makeErc20TokenStreamRule( erc20StreamingEnforcer, valueLteEnforcer, nonceEnforcer, + allowedCalldataEnforcer, + allowedTargetsEnforcer, redeemerEnforcer, } = enforcers; return makePermissionRule({ permissionType: 'erc20-token-stream', - optionalEnforcers: [timestampEnforcer, redeemerEnforcer], + optionalEnforcers: [ + timestampEnforcer, + redeemerEnforcer, + allowedCalldataEnforcer, + ], redeemerEnforcer, + payeeEnforcers: { + allowedCalldataEnforcer, + allowedTargetsEnforcer, + singlePayeeEnforcer: allowedCalldataEnforcer, + }, timestampEnforcer, requiredEnforcers: { [erc20StreamingEnforcer]: 1, diff --git a/packages/gator-permissions-controller/src/decodePermission/rules/makePermissionRule.test.ts b/packages/gator-permissions-controller/src/decodePermission/rules/makePermissionRule.test.ts index 37a7906840..fef9a97124 100644 --- a/packages/gator-permissions-controller/src/decodePermission/rules/makePermissionRule.test.ts +++ b/packages/gator-permissions-controller/src/decodePermission/rules/makePermissionRule.test.ts @@ -1,4 +1,8 @@ -import { createTimestampTerms } from '@metamask/delegation-core'; +import { + createAllowedCalldataTerms, + createAllowedTargetsTerms, + createTimestampTerms, +} from '@metamask/delegation-core'; import { CHAIN_ID, DELEGATOR_CONTRACTS, @@ -13,6 +17,20 @@ describe('makePermissionRule', () => { 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 = { + allowedCalldataEnforcer, + allowedTargetsEnforcer, + singlePayeeEnforcer: allowedCalldataEnforcer, + }; it('calls optional validate callback when provided and decoding succeeds', () => { const validateAndDecodeData = jest.fn().mockReturnValue({}); @@ -21,6 +39,7 @@ describe('makePermissionRule', () => { permissionType: 'native-token-stream', timestampEnforcer, redeemerEnforcer, + payeeEnforcers: payeeEnforcersNative, optionalEnforcers: [], requiredEnforcers: { [requiredEnforcer]: 1 }, validateAndDecodeData, @@ -60,6 +79,7 @@ describe('makePermissionRule', () => { permissionType: 'native-token-stream', timestampEnforcer, redeemerEnforcer, + payeeEnforcers: payeeEnforcersNative, optionalEnforcers: [], requiredEnforcers: { [requiredEnforcer]: 1 }, validateAndDecodeData, @@ -95,6 +115,7 @@ describe('makePermissionRule', () => { permissionType: 'native-token-stream', timestampEnforcer, redeemerEnforcer, + payeeEnforcers: payeeEnforcersNative, optionalEnforcers: [], requiredEnforcers: { [requiredEnforcer]: 1 }, validateAndDecodeData, @@ -131,6 +152,7 @@ describe('makePermissionRule', () => { permissionType: 'native-token-stream', timestampEnforcer, redeemerEnforcer, + payeeEnforcers: payeeEnforcersNative, optionalEnforcers: [], requiredEnforcers: { [requiredEnforcer]: 1 }, validateAndDecodeData, @@ -169,6 +191,7 @@ describe('makePermissionRule', () => { permissionType: 'native-token-stream', timestampEnforcer, redeemerEnforcer, + payeeEnforcers: payeeEnforcersNative, optionalEnforcers: [], requiredEnforcers: { [requiredEnforcer]: 1 }, validateAndDecodeData, @@ -208,6 +231,7 @@ describe('makePermissionRule', () => { permissionType: 'native-token-stream', timestampEnforcer, redeemerEnforcer, + payeeEnforcers: payeeEnforcersNative, optionalEnforcers: [], requiredEnforcers: { [requiredEnforcer]: 1 }, validateAndDecodeData, @@ -239,6 +263,7 @@ describe('makePermissionRule', () => { permissionType: 'native-token-stream', timestampEnforcer, redeemerEnforcer, + payeeEnforcers: payeeEnforcersNative, optionalEnforcers: [], requiredEnforcers: { [requiredEnforcer]: 1 }, validateAndDecodeData, @@ -276,4 +301,583 @@ describe('makePermissionRule', () => { }, ]); }); + + it('includes payee rule when AllowedTargetsEnforcer caveat is present (native)', () => { + const validateAndDecodeData = jest.fn().mockReturnValue({}); + const payeeAddress = '0x2222222222222222222222222222222222222222' as Hex; + + const rule = makePermissionRule({ + permissionType: 'native-token-stream', + timestampEnforcer, + redeemerEnforcer, + payeeEnforcers: payeeEnforcersNative, + optionalEnforcers: [allowedTargetsEnforcer], + requiredEnforcers: { [requiredEnforcer]: 1 }, + validateAndDecodeData, + }); + + const caveats = [ + { + enforcer: requiredEnforcer, + terms: '0x' as Hex, + args: '0x' as Hex, + }, + { + enforcer: allowedTargetsEnforcer, + terms: createAllowedTargetsTerms({ targets: [payeeAddress] }), + args: '0x' as Hex, + }, + ]; + + const result = rule.validateAndDecodePermission(caveats); + + expect(result.isValid).toBe(true); + if (!result.isValid) { + throw new Error('Expected valid result'); + } + expect(result.rules).toStrictEqual([ + { + type: 'payee', + data: { + addresses: [getChecksumAddress(payeeAddress)], + }, + }, + ]); + }); + + it('includes payee rule when AllowedCalldataEnforcer caveat is present (erc20)', () => { + const validateAndDecodeData = jest.fn().mockReturnValue({}); + const payeeAddress = '0x3333333333333333333333333333333333333333' as Hex; + const paddedAddress = `0x${payeeAddress.slice(2).padStart(64, '0')}`; + + const rule = makePermissionRule({ + permissionType: 'erc20-token-stream', + timestampEnforcer, + redeemerEnforcer, + payeeEnforcers: payeeEnforcersErc20, + optionalEnforcers: [allowedCalldataEnforcer], + requiredEnforcers: { [requiredEnforcer]: 1 }, + validateAndDecodeData, + }); + + const caveats = [ + { + enforcer: requiredEnforcer, + terms: '0x' as Hex, + args: '0x' as Hex, + }, + { + enforcer: allowedCalldataEnforcer, + terms: createAllowedCalldataTerms({ + startIndex: 4, + value: paddedAddress, + }), + args: '0x' as Hex, + }, + ]; + + const result = rule.validateAndDecodePermission(caveats); + + expect(result.isValid).toBe(true); + if (!result.isValid) { + throw new Error('Expected valid result'); + } + expect(result.rules).toStrictEqual([ + { + type: 'payee', + data: { + addresses: [getChecksumAddress(payeeAddress)], + }, + }, + ]); + }); + + it('does not include payee rule when only AllowedTargetsEnforcer caveat is present (erc20)', () => { + const validateAndDecodeData = jest.fn().mockReturnValue({}); + const payeeAddress = '0x2222222222222222222222222222222222222222' as Hex; + + const rule = makePermissionRule({ + permissionType: 'erc20-token-stream', + timestampEnforcer, + redeemerEnforcer, + payeeEnforcers: payeeEnforcersErc20, + optionalEnforcers: [allowedTargetsEnforcer], + requiredEnforcers: { [requiredEnforcer]: 1 }, + validateAndDecodeData, + }); + + const caveats = [ + { + enforcer: requiredEnforcer, + terms: '0x' as Hex, + args: '0x' as Hex, + }, + { + enforcer: allowedTargetsEnforcer, + terms: createAllowedTargetsTerms({ targets: [payeeAddress] }), + args: '0x' as Hex, + }, + ]; + + const result = rule.validateAndDecodePermission(caveats); + + expect(result.isValid).toBe(true); + if (!result.isValid) { + throw new Error('Expected valid result'); + } + expect(result.rules).toBeUndefined(); + }); + + it('rejects multiple AllowedCalldataEnforcer caveats for erc20 payee decoding', () => { + const validateAndDecodeData = jest.fn().mockReturnValue({}); + const payeeAddress1 = '0x2222222222222222222222222222222222222222' as Hex; + const payeeAddress2 = '0x3333333333333333333333333333333333333333' as Hex; + const padded1 = `0x${payeeAddress1.slice(2).padStart(64, '0')}`; + const padded2 = `0x${payeeAddress2.slice(2).padStart(64, '0')}`; + + const rule = makePermissionRule({ + permissionType: 'erc20-token-stream', + timestampEnforcer, + redeemerEnforcer, + payeeEnforcers: payeeEnforcersErc20, + optionalEnforcers: [allowedCalldataEnforcer], + requiredEnforcers: { [requiredEnforcer]: 1 }, + validateAndDecodeData, + }); + + const result = rule.validateAndDecodePermission([ + { + enforcer: requiredEnforcer, + terms: '0x' as Hex, + args: '0x' as Hex, + }, + { + enforcer: allowedCalldataEnforcer, + terms: createAllowedCalldataTerms({ + startIndex: 4, + value: padded1, + }), + args: '0x' as Hex, + }, + { + enforcer: allowedCalldataEnforcer, + terms: createAllowedCalldataTerms({ + startIndex: 4, + value: padded2, + }), + args: '0x' as Hex, + }, + ]); + + expect(result.isValid).toBe(false); + if (result.isValid) { + throw new Error('Expected invalid result'); + } + expect(result.error.message).toBe( + 'Invalid payee caveats: multiple singlePayeeEnforcer caveats', + ); + }); + + it('includes payee rule with multiple addresses via AllowedTargetsEnforcer (native)', () => { + const validateAndDecodeData = jest.fn().mockReturnValue({}); + const payeeAddress1 = '0x4444444444444444444444444444444444444444' as Hex; + const payeeAddress2 = '0x5555555555555555555555555555555555555555' as Hex; + + const rule = makePermissionRule({ + permissionType: 'native-token-stream', + timestampEnforcer, + redeemerEnforcer, + payeeEnforcers: payeeEnforcersNative, + optionalEnforcers: [allowedTargetsEnforcer], + requiredEnforcers: { [requiredEnforcer]: 1 }, + validateAndDecodeData, + }); + + const caveats = [ + { + enforcer: requiredEnforcer, + terms: '0x' as Hex, + args: '0x' as Hex, + }, + { + enforcer: allowedTargetsEnforcer, + terms: createAllowedTargetsTerms({ + targets: [payeeAddress1, payeeAddress2], + }), + args: '0x' as Hex, + }, + ]; + + const result = rule.validateAndDecodePermission(caveats); + + expect(result.isValid).toBe(true); + if (!result.isValid) { + throw new Error('Expected valid result'); + } + expect(result.rules).toStrictEqual([ + { + type: 'payee', + data: { + addresses: [ + getChecksumAddress(payeeAddress1), + getChecksumAddress(payeeAddress2), + ], + }, + }, + ]); + }); + + it('does not include payee rule when only AllowedCalldataEnforcer caveat is present (native)', () => { + const validateAndDecodeData = jest.fn().mockReturnValue({}); + const payeeAddress = '0x3333333333333333333333333333333333333333' as Hex; + const paddedAddress = `0x${payeeAddress.slice(2).padStart(64, '0')}`; + + const rule = makePermissionRule({ + permissionType: 'native-token-stream', + timestampEnforcer, + redeemerEnforcer, + payeeEnforcers: payeeEnforcersNative, + optionalEnforcers: [allowedCalldataEnforcer], + requiredEnforcers: { [requiredEnforcer]: 1 }, + validateAndDecodeData, + }); + + const caveats = [ + { + enforcer: requiredEnforcer, + terms: '0x' as Hex, + args: '0x' as Hex, + }, + { + enforcer: allowedCalldataEnforcer, + terms: createAllowedCalldataTerms({ + startIndex: 4, + value: paddedAddress, + }), + args: '0x' as Hex, + }, + ]; + + const result = rule.validateAndDecodePermission(caveats); + + expect(result.isValid).toBe(true); + if (!result.isValid) { + throw new Error('Expected valid result'); + } + expect(result.rules).toBeUndefined(); + }); + + it('includes both redeemer and payee rules when both caveats present', () => { + const validateAndDecodeData = jest.fn().mockReturnValue({}); + const redeemerAddr = '1111111111111111111111111111111111111111' as const; + const payeeAddress = '0x2222222222222222222222222222222222222222' as Hex; + + const rule = makePermissionRule({ + permissionType: 'native-token-stream', + timestampEnforcer, + redeemerEnforcer, + payeeEnforcers: payeeEnforcersNative, + optionalEnforcers: [redeemerEnforcer, allowedTargetsEnforcer], + requiredEnforcers: { [requiredEnforcer]: 1 }, + validateAndDecodeData, + }); + + const caveats = [ + { + enforcer: requiredEnforcer, + terms: '0x' as Hex, + args: '0x' as Hex, + }, + { + enforcer: redeemerEnforcer, + terms: `0x${redeemerAddr}` as Hex, + args: '0x' as Hex, + }, + { + enforcer: allowedTargetsEnforcer, + terms: createAllowedTargetsTerms({ targets: [payeeAddress] }), + args: '0x' as Hex, + }, + ]; + + const result = rule.validateAndDecodePermission(caveats); + + expect(result.isValid).toBe(true); + if (!result.isValid) { + throw new Error('Expected valid result'); + } + expect(result.rules).toHaveLength(2); + expect(result.rules).toStrictEqual([ + { + type: 'redeemer', + data: { + addresses: [getChecksumAddress(`0x${redeemerAddr}` as Hex)], + }, + }, + { + type: 'payee', + data: { + addresses: [getChecksumAddress(payeeAddress)], + }, + }, + ]); + }); + + it('does not include payee rule when no payee caveat is present', () => { + const validateAndDecodeData = jest.fn().mockReturnValue({}); + + const rule = makePermissionRule({ + permissionType: 'native-token-stream', + timestampEnforcer, + redeemerEnforcer, + payeeEnforcers: payeeEnforcersNative, + optionalEnforcers: [], + requiredEnforcers: { [requiredEnforcer]: 1 }, + validateAndDecodeData, + }); + + const caveats = [ + { + enforcer: requiredEnforcer, + terms: '0x' as Hex, + args: '0x' as Hex, + }, + ]; + + const result = rule.validateAndDecodePermission(caveats); + + expect(result.isValid).toBe(true); + if (!result.isValid) { + throw new Error('Expected valid result'); + } + expect(result.rules).toBeUndefined(); + }); + + it('returns true from caveatAddressesMatch when enforcers match rule', () => { + const validateAndDecodeData = jest.fn().mockReturnValue({}); + + const rule = makePermissionRule({ + permissionType: 'native-token-stream', + timestampEnforcer, + redeemerEnforcer, + payeeEnforcers: payeeEnforcersNative, + optionalEnforcers: [timestampEnforcer], + requiredEnforcers: { [requiredEnforcer]: 1 }, + validateAndDecodeData, + }); + + expect( + rule.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); + }); + + it('rejects when singlePayeeEnforcer is configured as a required enforcer', () => { + const validateAndDecodeData = jest.fn().mockReturnValue({}); + const payeeAddress = '0x2222222222222222222222222222222222222222' as Hex; + + const rule = makePermissionRule({ + permissionType: 'native-token-stream', + timestampEnforcer, + redeemerEnforcer, + payeeEnforcers: payeeEnforcersNative, + optionalEnforcers: [], + requiredEnforcers: { + [requiredEnforcer]: 1, + [allowedTargetsEnforcer]: 1, + }, + validateAndDecodeData, + }); + + const result = rule.validateAndDecodePermission([ + { + enforcer: requiredEnforcer, + terms: '0x' as Hex, + args: '0x' as Hex, + }, + { + enforcer: allowedTargetsEnforcer, + terms: createAllowedTargetsTerms({ targets: [payeeAddress] }), + args: '0x' as Hex, + }, + ]); + + expect(result.isValid).toBe(false); + if (result.isValid) { + throw new Error('Expected invalid result'); + } + expect(result.error.message).toBe( + 'Invalid payee caveats: singlePayeeEnforcer may not be a required caveat', + ); + }); + + it('rejects an ERC20 payee caveat with the wrong calldata start index', () => { + const validateAndDecodeData = jest.fn().mockReturnValue({}); + const payeeAddress = '0x3333333333333333333333333333333333333333' as const; + const paddedAddress = + `0x${payeeAddress.slice(2).padStart(64, '0')}` as const; + + const rule = makePermissionRule({ + permissionType: 'erc20-token-stream', + timestampEnforcer, + redeemerEnforcer, + payeeEnforcers: payeeEnforcersErc20, + optionalEnforcers: [allowedCalldataEnforcer], + requiredEnforcers: { [requiredEnforcer]: 1 }, + validateAndDecodeData, + }); + + const result = rule.validateAndDecodePermission([ + { + enforcer: requiredEnforcer, + terms: '0x' as Hex, + args: '0x' as Hex, + }, + { + enforcer: allowedCalldataEnforcer, + terms: createAllowedCalldataTerms({ + startIndex: 36, + value: paddedAddress, + }), + args: '0x' as Hex, + }, + ]); + + expect(result.isValid).toBe(false); + }); + + it('rejects an ERC20 payee caveat when the calldata value is not one address', () => { + const validateAndDecodeData = jest.fn().mockReturnValue({}); + + const rule = makePermissionRule({ + permissionType: 'erc20-token-stream', + timestampEnforcer, + redeemerEnforcer, + payeeEnforcers: payeeEnforcersErc20, + optionalEnforcers: [allowedCalldataEnforcer], + requiredEnforcers: { [requiredEnforcer]: 1 }, + validateAndDecodeData, + }); + + const result = rule.validateAndDecodePermission([ + { + enforcer: requiredEnforcer, + terms: '0x' as Hex, + args: '0x' as Hex, + }, + { + enforcer: allowedCalldataEnforcer, + terms: createAllowedCalldataTerms({ + startIndex: 4, + value: '0x1234', + }), + args: '0x' as Hex, + }, + ]); + + expect(result.isValid).toBe(false); + }); + + it('rejects a native payee caveat with no targets', () => { + const validateAndDecodeData = jest.fn().mockReturnValue({}); + + const rule = makePermissionRule({ + permissionType: 'native-token-stream', + timestampEnforcer, + redeemerEnforcer, + payeeEnforcers: payeeEnforcersNative, + optionalEnforcers: [allowedTargetsEnforcer], + requiredEnforcers: { [requiredEnforcer]: 1 }, + validateAndDecodeData, + }); + + const result = rule.validateAndDecodePermission([ + { + enforcer: requiredEnforcer, + terms: '0x' as Hex, + args: '0x' as Hex, + }, + { + enforcer: allowedTargetsEnforcer, + terms: '0x' as Hex, + args: '0x' as Hex, + }, + ]); + + expect(result.isValid).toBe(false); + }); + + it('rejects multiple single-payee caveats', () => { + const validateAndDecodeData = jest.fn().mockReturnValue({}); + const payeeAddress1 = '0x2222222222222222222222222222222222222222' as Hex; + const payeeAddress2 = '0x3333333333333333333333333333333333333333' as Hex; + + const rule = makePermissionRule({ + permissionType: 'native-token-stream', + timestampEnforcer, + redeemerEnforcer, + payeeEnforcers: payeeEnforcersNative, + optionalEnforcers: [allowedTargetsEnforcer], + requiredEnforcers: { [requiredEnforcer]: 1 }, + validateAndDecodeData, + }); + + const result = rule.validateAndDecodePermission([ + { + enforcer: requiredEnforcer, + terms: '0x' as Hex, + args: '0x' as Hex, + }, + { + enforcer: allowedTargetsEnforcer, + terms: createAllowedTargetsTerms({ targets: [payeeAddress1] }), + args: '0x' as Hex, + }, + { + enforcer: allowedTargetsEnforcer, + terms: createAllowedTargetsTerms({ targets: [payeeAddress2] }), + args: '0x' as Hex, + }, + ]); + + expect(result.isValid).toBe(false); + }); }); diff --git a/packages/gator-permissions-controller/src/decodePermission/rules/makePermissionRule.ts b/packages/gator-permissions-controller/src/decodePermission/rules/makePermissionRule.ts index a2cefb1f21..b3966908fa 100644 --- a/packages/gator-permissions-controller/src/decodePermission/rules/makePermissionRule.ts +++ b/packages/gator-permissions-controller/src/decodePermission/rules/makePermissionRule.ts @@ -1,10 +1,17 @@ import type { Rule } from '@metamask/7715-permission-types'; import type { Caveat } from '@metamask/delegation-core'; -import { decodeRedeemerTerms } 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_REDEEMER_RULE_TYPE } from '../../constants'; +import { + EXECUTION_PERMISSION_PAYEE_RULE_TYPE, + EXECUTION_PERMISSION_REDEEMER_RULE_TYPE, +} from '../../constants'; import type { ChecksumCaveat, DecodedPermission, @@ -16,9 +23,19 @@ 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. @@ -26,6 +43,7 @@ import { * @param args - The arguments to this function. * @param args.optionalEnforcers - Enforcer addresses that may appear in addition to required. * @param args.redeemerEnforcer - Address of the RedeemerEnforcer 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. @@ -35,6 +53,7 @@ import { export function makePermissionRule({ optionalEnforcers, redeemerEnforcer, + payeeEnforcers, timestampEnforcer, permissionType, requiredEnforcers, @@ -42,6 +61,7 @@ export function makePermissionRule({ }: { optionalEnforcers: Hex[]; redeemerEnforcer: Hex; + payeeEnforcers: PayeeEnforcerAddresses; timestampEnforcer: Hex; permissionType: PermissionType; requiredEnforcers: Record; @@ -106,22 +126,129 @@ export function makePermissionRule({ throwIfNotFound: false, }); - let rules: Rule[] | undefined; + const rules: Rule[] = []; if (redeemerTerms) { - rules = [ - { - type: EXECUTION_PERMISSION_REDEEMER_RULE_TYPE, - data: { - addresses: decodeRedeemerTerms(redeemerTerms).redeemers, - }, + rules.push({ + type: EXECUTION_PERMISSION_REDEEMER_RULE_TYPE, + data: { + addresses: decodeRedeemerTerms(redeemerTerms).redeemers, }, - ]; + }); } - return { isValid: true, expiry, data, rules }; + // 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/rules/nativeTokenAllowance.ts b/packages/gator-permissions-controller/src/decodePermission/rules/nativeTokenAllowance.ts index 4ead98866c..9f5910cc1b 100644 --- a/packages/gator-permissions-controller/src/decodePermission/rules/nativeTokenAllowance.ts +++ b/packages/gator-permissions-controller/src/decodePermission/rules/nativeTokenAllowance.ts @@ -33,12 +33,23 @@ export function makeNativeTokenAllowanceRule( nativeTokenPeriodicEnforcer, exactCalldataEnforcer, nonceEnforcer, + allowedCalldataEnforcer, + allowedTargetsEnforcer, redeemerEnforcer, } = enforcers; return makePermissionRule({ permissionType: 'native-token-allowance', - optionalEnforcers: [timestampEnforcer, redeemerEnforcer], + optionalEnforcers: [ + timestampEnforcer, + redeemerEnforcer, + allowedTargetsEnforcer, + ], redeemerEnforcer, + payeeEnforcers: { + allowedCalldataEnforcer, + allowedTargetsEnforcer, + singlePayeeEnforcer: allowedTargetsEnforcer, + }, timestampEnforcer, requiredEnforcers: { [nativeTokenPeriodicEnforcer]: 1, diff --git a/packages/gator-permissions-controller/src/decodePermission/rules/nativeTokenPeriodic.ts b/packages/gator-permissions-controller/src/decodePermission/rules/nativeTokenPeriodic.ts index a86a020219..cb1a0c3d29 100644 --- a/packages/gator-permissions-controller/src/decodePermission/rules/nativeTokenPeriodic.ts +++ b/packages/gator-permissions-controller/src/decodePermission/rules/nativeTokenPeriodic.ts @@ -28,12 +28,23 @@ export function makeNativeTokenPeriodicRule( nativeTokenPeriodicEnforcer, exactCalldataEnforcer, nonceEnforcer, + allowedCalldataEnforcer, + allowedTargetsEnforcer, redeemerEnforcer, } = enforcers; return makePermissionRule({ permissionType: 'native-token-periodic', - optionalEnforcers: [timestampEnforcer, redeemerEnforcer], + optionalEnforcers: [ + timestampEnforcer, + redeemerEnforcer, + allowedTargetsEnforcer, + ], redeemerEnforcer, + payeeEnforcers: { + allowedCalldataEnforcer, + allowedTargetsEnforcer, + singlePayeeEnforcer: allowedTargetsEnforcer, + }, timestampEnforcer, requiredEnforcers: { [nativeTokenPeriodicEnforcer]: 1, diff --git a/packages/gator-permissions-controller/src/decodePermission/rules/nativeTokenStream.ts b/packages/gator-permissions-controller/src/decodePermission/rules/nativeTokenStream.ts index 4c19a036dc..57454b3929 100644 --- a/packages/gator-permissions-controller/src/decodePermission/rules/nativeTokenStream.ts +++ b/packages/gator-permissions-controller/src/decodePermission/rules/nativeTokenStream.ts @@ -23,12 +23,23 @@ export function makeNativeTokenStreamRule( nativeTokenStreamingEnforcer, exactCalldataEnforcer, nonceEnforcer, + allowedCalldataEnforcer, + allowedTargetsEnforcer, redeemerEnforcer, } = enforcers; return makePermissionRule({ permissionType: 'native-token-stream', - optionalEnforcers: [timestampEnforcer, redeemerEnforcer], + optionalEnforcers: [ + timestampEnforcer, + redeemerEnforcer, + allowedTargetsEnforcer, + ], redeemerEnforcer, + payeeEnforcers: { + allowedCalldataEnforcer, + allowedTargetsEnforcer, + singlePayeeEnforcer: allowedTargetsEnforcer, + }, timestampEnforcer, requiredEnforcers: { [nativeTokenStreamingEnforcer]: 1, diff --git a/packages/gator-permissions-controller/src/decodePermission/types.ts b/packages/gator-permissions-controller/src/decodePermission/types.ts index 08ae71abd6..e27d4a8033 100644 --- a/packages/gator-permissions-controller/src/decodePermission/types.ts +++ b/packages/gator-permissions-controller/src/decodePermission/types.ts @@ -99,6 +99,7 @@ export type ChecksumEnforcersByChainId = { timestampEnforcer: Hex; nonceEnforcer: Hex; allowedCalldataEnforcer: Hex; + allowedTargetsEnforcer: Hex; redeemerEnforcer: Hex; }; diff --git a/packages/gator-permissions-controller/src/decodePermission/utils.test.ts b/packages/gator-permissions-controller/src/decodePermission/utils.test.ts index 7cf7f43d54..2cc7902c1c 100644 --- a/packages/gator-permissions-controller/src/decodePermission/utils.test.ts +++ b/packages/gator-permissions-controller/src/decodePermission/utils.test.ts @@ -22,6 +22,7 @@ const buildContracts = (): DeployedContractsByName => ({ ValueLteEnforcer: '0x7777777777777777777777777777777777777777', NonceEnforcer: '0x8888888888888888888888888888888888888888', AllowedCalldataEnforcer: '0x9999999999999999999999999999999999999999', + AllowedTargetsEnforcer: '0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', RedeemerEnforcer: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', }); @@ -52,6 +53,9 @@ describe('getChecksumEnforcersByChainId', () => { allowedCalldataEnforcer: getChecksumAddress( contracts.AllowedCalldataEnforcer, ), + allowedTargetsEnforcer: getChecksumAddress( + contracts.AllowedTargetsEnforcer, + ), redeemerEnforcer: getChecksumAddress(contracts.RedeemerEnforcer), }); }); @@ -78,6 +82,7 @@ describe('createPermissionRulesForChainId', () => { timestampEnforcer, nonceEnforcer, allowedCalldataEnforcer, + allowedTargetsEnforcer, redeemerEnforcer, } = getChecksumEnforcersByChainId(contracts); @@ -101,13 +106,18 @@ describe('createPermissionRulesForChainId', () => { expect(byType['native-token-stream'].permissionType).toBe( 'native-token-stream', ); - expect(byType['native-token-stream'].optionalEnforcers.size).toBe(2); + expect(byType['native-token-stream'].optionalEnforcers.size).toBe(3); expect( byType['native-token-stream'].optionalEnforcers.has(timestampEnforcer), ).toBe(true); expect( byType['native-token-stream'].optionalEnforcers.has(redeemerEnforcer), ).toBe(true); + expect( + byType['native-token-stream'].optionalEnforcers.has( + allowedTargetsEnforcer, + ), + ).toBe(true); expect(byType['native-token-stream'].requiredEnforcers.size).toBe(3); expect( Array.from(byType['native-token-stream'].requiredEnforcers.entries()), @@ -124,13 +134,18 @@ describe('createPermissionRulesForChainId', () => { expect(byType['native-token-periodic'].permissionType).toBe( 'native-token-periodic', ); - expect(byType['native-token-periodic'].optionalEnforcers.size).toBe(2); + expect(byType['native-token-periodic'].optionalEnforcers.size).toBe(3); expect( byType['native-token-periodic'].optionalEnforcers.has(timestampEnforcer), ).toBe(true); expect( byType['native-token-periodic'].optionalEnforcers.has(redeemerEnforcer), ).toBe(true); + expect( + byType['native-token-periodic'].optionalEnforcers.has( + allowedTargetsEnforcer, + ), + ).toBe(true); expect(byType['native-token-periodic'].requiredEnforcers.size).toBe(3); expect( Array.from(byType['native-token-periodic'].requiredEnforcers.entries()), @@ -147,13 +162,18 @@ describe('createPermissionRulesForChainId', () => { expect(byType['erc20-token-stream'].permissionType).toBe( 'erc20-token-stream', ); - expect(byType['erc20-token-stream'].optionalEnforcers.size).toBe(2); + expect(byType['erc20-token-stream'].optionalEnforcers.size).toBe(3); expect( byType['erc20-token-stream'].optionalEnforcers.has(timestampEnforcer), ).toBe(true); expect( byType['erc20-token-stream'].optionalEnforcers.has(redeemerEnforcer), ).toBe(true); + expect( + byType['erc20-token-stream'].optionalEnforcers.has( + allowedCalldataEnforcer, + ), + ).toBe(true); expect(byType['erc20-token-stream'].requiredEnforcers.size).toBe(3); expect( Array.from(byType['erc20-token-stream'].requiredEnforcers.entries()), @@ -170,13 +190,18 @@ describe('createPermissionRulesForChainId', () => { expect(byType['erc20-token-periodic'].permissionType).toBe( 'erc20-token-periodic', ); - expect(byType['erc20-token-periodic'].optionalEnforcers.size).toBe(2); + expect(byType['erc20-token-periodic'].optionalEnforcers.size).toBe(3); expect( byType['erc20-token-periodic'].optionalEnforcers.has(timestampEnforcer), ).toBe(true); expect( byType['erc20-token-periodic'].optionalEnforcers.has(redeemerEnforcer), ).toBe(true); + expect( + byType['erc20-token-periodic'].optionalEnforcers.has( + allowedCalldataEnforcer, + ), + ).toBe(true); expect(byType['erc20-token-periodic'].requiredEnforcers.size).toBe(3); expect( Array.from(byType['erc20-token-periodic'].requiredEnforcers.entries()), @@ -193,13 +218,18 @@ describe('createPermissionRulesForChainId', () => { expect(byType['native-token-allowance'].permissionType).toBe( 'native-token-allowance', ); - expect(byType['native-token-allowance'].optionalEnforcers.size).toBe(2); + expect(byType['native-token-allowance'].optionalEnforcers.size).toBe(3); expect( byType['native-token-allowance'].optionalEnforcers.has(timestampEnforcer), ).toBe(true); expect( byType['native-token-allowance'].optionalEnforcers.has(redeemerEnforcer), ).toBe(true); + expect( + byType['native-token-allowance'].optionalEnforcers.has( + allowedTargetsEnforcer, + ), + ).toBe(true); expect(byType['native-token-allowance'].requiredEnforcers.size).toBe(3); expect( Array.from(byType['native-token-allowance'].requiredEnforcers.entries()), @@ -216,13 +246,18 @@ describe('createPermissionRulesForChainId', () => { expect(byType['erc20-token-allowance'].permissionType).toBe( 'erc20-token-allowance', ); - expect(byType['erc20-token-allowance'].optionalEnforcers.size).toBe(2); + expect(byType['erc20-token-allowance'].optionalEnforcers.size).toBe(3); expect( byType['erc20-token-allowance'].optionalEnforcers.has(timestampEnforcer), ).toBe(true); expect( byType['erc20-token-allowance'].optionalEnforcers.has(redeemerEnforcer), ).toBe(true); + expect( + byType['erc20-token-allowance'].optionalEnforcers.has( + allowedCalldataEnforcer, + ), + ).toBe(true); expect(byType['erc20-token-allowance'].requiredEnforcers.size).toBe(3); expect( Array.from(byType['erc20-token-allowance'].requiredEnforcers.entries()), diff --git a/packages/gator-permissions-controller/src/decodePermission/utils.ts b/packages/gator-permissions-controller/src/decodePermission/utils.ts index 603dcd6772..4177159c64 100644 --- a/packages/gator-permissions-controller/src/decodePermission/utils.ts +++ b/packages/gator-permissions-controller/src/decodePermission/utils.ts @@ -20,6 +20,7 @@ const ENFORCER_CONTRACT_NAMES = { ValueLteEnforcer: 'ValueLteEnforcer', NonceEnforcer: 'NonceEnforcer', AllowedCalldataEnforcer: 'AllowedCalldataEnforcer', + AllowedTargetsEnforcer: 'AllowedTargetsEnforcer', RedeemerEnforcer: 'RedeemerEnforcer', }; @@ -109,6 +110,10 @@ export const getChecksumEnforcersByChainId = ( ENFORCER_CONTRACT_NAMES.AllowedCalldataEnforcer, ); + const allowedTargetsEnforcer = getChecksumContractAddress( + ENFORCER_CONTRACT_NAMES.AllowedTargetsEnforcer, + ); + const redeemerEnforcer = getChecksumContractAddress( ENFORCER_CONTRACT_NAMES.RedeemerEnforcer, ); @@ -123,6 +128,7 @@ export const getChecksumEnforcersByChainId = ( timestampEnforcer, nonceEnforcer, allowedCalldataEnforcer, + allowedTargetsEnforcer, redeemerEnforcer, }; }; diff --git a/packages/gator-permissions-controller/src/index.ts b/packages/gator-permissions-controller/src/index.ts index 125248cb72..0661ccf966 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_PAYEE_RULE_TYPE, EXECUTION_PERMISSION_REDEEMER_RULE_TYPE, } from './constants'; export type { @@ -37,6 +38,7 @@ export type { SupportedPermissionType, } from './types'; +export type { PayeeRule } from './payeeRule'; export type { RedeemerRule } from './redeemerRule'; export type { NativeTokenStreamPermission, diff --git a/packages/gator-permissions-controller/src/payeeRule.ts b/packages/gator-permissions-controller/src/payeeRule.ts new file mode 100644 index 0000000000..4731c16c35 --- /dev/null +++ b/packages/gator-permissions-controller/src/payeeRule.ts @@ -0,0 +1,12 @@ +import type { Hex } from '@metamask/utils'; + +/** + * Execution permission rule restricting which addresses may receive payments + * (on-chain AllowedCalldataEnforcer / AllowedTargetsEnforcer caveat). + */ +export type PayeeRule = { + type: 'payee'; + data: { + addresses: Hex[]; + }; +}; diff --git a/yarn.lock b/yarn.lock index df7b371804..35fbdb22f7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3479,21 +3479,21 @@ __metadata: languageName: unknown linkType: soft -"@metamask/delegation-core@npm:^1.1.0": - version: 1.1.0 - resolution: "@metamask/delegation-core@npm:1.1.0" +"@metamask/delegation-core@npm:^2.0.0": + version: 2.0.0 + resolution: "@metamask/delegation-core@npm:2.0.0" dependencies: "@metamask/abi-utils": "npm:^3.0.0" "@metamask/utils": "npm:^11.4.0" "@noble/hashes": "npm:^1.8.0" - checksum: 10/672f9e2e2b4e8c312f2cd2ff166bbc508fbdb6e141fe92e678abc9993b9ccbdd17db711477a9b97b6ce3919fa6d51d759c16f6c6fda3f89cb95e303b8aa76f7d + checksum: 10/b473160e4cb4a6d463c6015de6e90d057034d2e8f2905068e1f44f93c8247618c5d84a155e86dfaa125dacb040951643517b9a76961bf8d215c194dc4d1cc0ad languageName: node linkType: hard -"@metamask/delegation-deployments@npm:^0.12.0": - version: 0.12.0 - resolution: "@metamask/delegation-deployments@npm:0.12.0" - checksum: 10/fd3b373efc1857cc867b44b4ca33db0cf8487c1109d6f2ed7e3ce10e6a65d4165b7fcc034cab92d919d6f0833e3749a055ff862adc8d7a348cdd3a0f593f6aa6 +"@metamask/delegation-deployments@npm:^1.3.0": + version: 1.3.0 + resolution: "@metamask/delegation-deployments@npm:1.3.0" + checksum: 10/58f4aafb5f0e3cbc543811cbc0100efab4ed67b9c9794b83192962153e4edbe12fd6ab6fa7be689503309862a65eb7fde771f632893d38ab54f8171aa682b34f languageName: node linkType: hard @@ -4109,8 +4109,8 @@ __metadata: "@metamask/abi-utils": "npm:^2.0.3" "@metamask/auto-changelog": "npm:^6.1.0" "@metamask/base-controller": "npm:^9.1.0" - "@metamask/delegation-core": "npm:^1.1.0" - "@metamask/delegation-deployments": "npm:^0.12.0" + "@metamask/delegation-core": "npm:^2.0.0" + "@metamask/delegation-deployments": "npm:^1.3.0" "@metamask/messenger": "npm:^1.2.0" "@metamask/network-controller": "npm:^30.1.0" "@metamask/snaps-controllers": "npm:^19.0.0"