diff --git a/packages/eth-json-rpc-middleware/src/methods/wallet-get-supported-execution-permissions.test.ts b/packages/eth-json-rpc-middleware/src/methods/wallet-get-supported-execution-permissions.test.ts index cafd7ff8a4..2fcec4d658 100644 --- a/packages/eth-json-rpc-middleware/src/methods/wallet-get-supported-execution-permissions.test.ts +++ b/packages/eth-json-rpc-middleware/src/methods/wallet-get-supported-execution-permissions.test.ts @@ -10,7 +10,7 @@ import { createWalletGetSupportedExecutionPermissionsHandler } from './wallet-ge const RESULT_MOCK: GetSupportedExecutionPermissionsResult = { 'native-token-allowance': { chainIds: ['0x123', '0x345'] as Hex[], - ruleTypes: ['expiry'], + ruleTypes: ['expiry', 'redeemer'], }, 'erc20-token-allowance': { chainIds: ['0x123'] as Hex[], @@ -18,7 +18,7 @@ const RESULT_MOCK: GetSupportedExecutionPermissionsResult = { }, 'erc721-token-allowance': { chainIds: ['0x123'] as Hex[], - ruleTypes: ['expiry'], + ruleTypes: ['expiry', 'redeemer'], }, }; diff --git a/packages/gator-permissions-controller/CHANGELOG.md b/packages/gator-permissions-controller/CHANGELOG.md index 5c94ffd67b..c65b38bf28 100644 --- a/packages/gator-permissions-controller/CHANGELOG.md +++ b/packages/gator-permissions-controller/CHANGELOG.md @@ -7,8 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- 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. + - Export new `EXECUTION_PERMISSION_REDEEMER_RULE_TYPE` constant and `RedeemerRule` type. + ### Changed +- Use `decodeRedeemerTerms` from `@metamask/delegation-core` instead of a local implementation ([#8537](https://github.com/MetaMask/core/pull/8537)) +- Bump `@metamask/delegation-core` from `^0.2.0` to `^1.1.0` ([#8537](https://github.com/MetaMask/core/pull/8537)) - Bump `@metamask/transaction-controller` from `^64.2.0` to `^65.0.0` ([#8482](https://github.com/MetaMask/core/pull/8482), [#8585](https://github.com/MetaMask/core/pull/8585), [#8613](https://github.com/MetaMask/core/pull/8613)) ## [4.0.0] diff --git a/packages/gator-permissions-controller/package.json b/packages/gator-permissions-controller/package.json index 8971e159b2..1f7ecbf3f0 100644 --- a/packages/gator-permissions-controller/package.json +++ b/packages/gator-permissions-controller/package.json @@ -56,7 +56,7 @@ "@metamask/7715-permission-types": "^0.5.0", "@metamask/abi-utils": "^2.0.3", "@metamask/base-controller": "^9.1.0", - "@metamask/delegation-core": "^0.2.0", + "@metamask/delegation-core": "^1.1.0", "@metamask/delegation-deployments": "^0.12.0", "@metamask/messenger": "^1.1.1", "@metamask/network-controller": "^30.0.1", diff --git a/packages/gator-permissions-controller/src/GatorPermissionsController.test.ts b/packages/gator-permissions-controller/src/GatorPermissionsController.test.ts index c78761ce1e..22c08aab8f 100644 --- a/packages/gator-permissions-controller/src/GatorPermissionsController.test.ts +++ b/packages/gator-permissions-controller/src/GatorPermissionsController.test.ts @@ -835,9 +835,9 @@ describe('GatorPermissionsController', () => { const delegator = delegatorAddressA; const delegate = delegateAddressB; - const timestampBeforeThreshold = 1720000; + const beforeThreshold = 1720000; const expiryTerms = createTimestampTerms( - { timestampAfterThreshold: 0, timestampBeforeThreshold }, + { afterThreshold: 0, beforeThreshold }, { out: 'hex' }, ); @@ -886,7 +886,7 @@ describe('GatorPermissionsController', () => { expect(result.from).toBe(delegator); expect(result.to).toStrictEqual(delegate); expect(result.permission.type).toBe('native-token-stream'); - expect(result.expiry).toBe(timestampBeforeThreshold); + expect(result.expiry).toBe(beforeThreshold); // amounts are hex-encoded in decoded data; startTime is numeric expect(result.permission.data.startTime).toBe(startTime); // BigInt fields are encoded as hex; compare after decoding @@ -923,7 +923,7 @@ describe('GatorPermissionsController', () => { const { TimestampEnforcer, ValueLteEnforcer } = contracts; const expiryTerms = createTimestampTerms( - { timestampAfterThreshold: 0, timestampBeforeThreshold: 100 }, + { afterThreshold: 0, beforeThreshold: 100 }, { out: 'hex' }, ); @@ -964,7 +964,7 @@ describe('GatorPermissionsController', () => { } = contracts; const expiryTerms = createTimestampTerms( - { timestampAfterThreshold: 0, timestampBeforeThreshold: 1720000 }, + { afterThreshold: 0, beforeThreshold: 1720000 }, { out: 'hex' }, ); @@ -1014,9 +1014,9 @@ describe('GatorPermissionsController', () => { const delegator = delegatorAddressA; const delegate = delegateAddressB; - const timestampBeforeThreshold = 2000; + const beforeThreshold = 2000; const expiryTerms = createTimestampTerms( - { timestampAfterThreshold: 0, timestampBeforeThreshold }, + { afterThreshold: 0, beforeThreshold }, { out: 'hex' }, ); diff --git a/packages/gator-permissions-controller/src/GatorPermissionsController.ts b/packages/gator-permissions-controller/src/GatorPermissionsController.ts index 05f5926309..c322f0b471 100644 --- a/packages/gator-permissions-controller/src/GatorPermissionsController.ts +++ b/packages/gator-permissions-controller/src/GatorPermissionsController.ts @@ -608,7 +608,7 @@ export class GatorPermissionsController extends BaseController< }); } - const { expiry, data } = decodeResult; + const { expiry, data, rules } = decodeResult; const permission = reconstructDecodedPermission({ chainId, @@ -620,6 +620,7 @@ export class GatorPermissionsController extends BaseController< data, justification, specifiedOrigin, + rules, }); return permission; diff --git a/packages/gator-permissions-controller/src/constants.ts b/packages/gator-permissions-controller/src/constants.ts index 9dca8c093a..feda806752 100644 --- a/packages/gator-permissions-controller/src/constants.ts +++ b/packages/gator-permissions-controller/src/constants.ts @@ -3,3 +3,10 @@ * contract addresses from `@metamask/delegation-deployments`. */ export const DELEGATION_FRAMEWORK_VERSION = '1.3.0'; + +/** + * `Rule.type` / `wallet_getSupportedExecutionPermissions` `ruleTypes` entry for + * redeemer allowlists (RedeemerEnforcer). Hosts should advertise this for every + * supported execution permission type. + */ +export const EXECUTION_PERMISSION_REDEEMER_RULE_TYPE = 'redeemer' as const; diff --git a/packages/gator-permissions-controller/src/decodePermission/decodePermission.test.ts b/packages/gator-permissions-controller/src/decodePermission/decodePermission.test.ts index 9fa8abf45c..fc3062d6dc 100644 --- a/packages/gator-permissions-controller/src/decodePermission/decodePermission.test.ts +++ b/packages/gator-permissions-controller/src/decodePermission/decodePermission.test.ts @@ -30,6 +30,7 @@ describe('decodePermission', () => { NativeTokenStreamingEnforcer, NativeTokenPeriodTransferEnforcer, NonceEnforcer, + RedeemerEnforcer, } = contracts; describe('getPermissionRuleMatchingCaveatTypes()', () => { @@ -87,6 +88,35 @@ describe('decodePermission', () => { expect(result.permissionType).toBe(expectedPermissionType); }); + it('allows RedeemerEnforcer as extra', () => { + const enforcers = [ + NativeTokenStreamingEnforcer, + ExactCalldataEnforcer, + NonceEnforcer, + RedeemerEnforcer, + ]; + const result = findRuleWithMatchingCaveatAddresses({ + enforcers, + permissionRules: createPermissionRulesForContracts(contracts), + }); + expect(result.permissionType).toBe(expectedPermissionType); + }); + + it('allows TimestampEnforcer and RedeemerEnforcer as extras', () => { + const enforcers = [ + NativeTokenStreamingEnforcer, + ExactCalldataEnforcer, + NonceEnforcer, + TimestampEnforcer, + RedeemerEnforcer, + ]; + const result = findRuleWithMatchingCaveatAddresses({ + enforcers, + permissionRules: createPermissionRulesForContracts(contracts), + }); + expect(result.permissionType).toBe(expectedPermissionType); + }); + it('rejects forbidden extra caveat', () => { const enforcers = [ NativeTokenStreamingEnforcer, @@ -589,6 +619,39 @@ describe('decodePermission', () => { expect(result.origin).toBe(specifiedOrigin); }); + it('includes rules when provided', () => { + const permissionType = 'native-token-stream' as const; + const data: DecodedPermission['permission']['data'] = { + initialAmount: '0x01', + maxAmount: '0x02', + amountPerSecond: '0x03', + startTime: 1715664, + } as const; + const rules = [ + { + type: 'redeemer' as const, + data: { + addresses: ['0x1111111111111111111111111111111111111111' as Hex], + }, + }, + ]; + + const result = reconstructDecodedPermission({ + chainId, + permissionType, + delegator, + delegate, + authority: ROOT_AUTHORITY, + expiry: null, + data, + justification, + specifiedOrigin, + rules, + }); + + expect(result.rules).toStrictEqual(rules); + }); + it('constructs DecodedPermission with null expiry', () => { const permissionType = 'erc20-token-periodic' as const; const data: DecodedPermission['permission']['data'] = { diff --git a/packages/gator-permissions-controller/src/decodePermission/decodePermission.ts b/packages/gator-permissions-controller/src/decodePermission/decodePermission.ts index e8997009c6..81cdc1b632 100644 --- a/packages/gator-permissions-controller/src/decodePermission/decodePermission.ts +++ b/packages/gator-permissions-controller/src/decodePermission/decodePermission.ts @@ -63,6 +63,7 @@ export const findRuleWithMatchingCaveatAddresses = ({ * @param args.data - Permission-specific decoded data payload. * @param args.justification - Human-readable justification for the permission. * @param args.specifiedOrigin - The origin reported in the request metadata. + * @param args.rules - Rules recovered from caveats (e.g. redeemer allowlist). * * @returns The reconstructed {@link DecodedPermission}. */ @@ -76,6 +77,7 @@ export const reconstructDecodedPermission = ({ data, justification, specifiedOrigin, + rules, }: { chainId: number; permissionType: PermissionType; @@ -86,6 +88,7 @@ export const reconstructDecodedPermission = ({ data: DecodedPermission['permission']['data']; justification: string; specifiedOrigin: string; + rules?: DecodedPermission['rules']; }): DecodedPermission => { if (authority !== ROOT_AUTHORITY) { throw new Error('Invalid authority'); @@ -102,6 +105,7 @@ export const reconstructDecodedPermission = ({ }, expiry, origin: specifiedOrigin, + ...(rules === undefined ? {} : { rules }), }; return permission; diff --git a/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenPeriodic.test.ts b/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenPeriodic.test.ts index a71d18d21a..b3e3019db5 100644 --- a/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenPeriodic.test.ts +++ b/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenPeriodic.test.ts @@ -27,8 +27,8 @@ describe('erc20-token-periodic rule', () => { const expiryCaveat = { enforcer: TimestampEnforcer, terms: createTimestampTerms({ - timestampAfterThreshold: 0, - timestampBeforeThreshold: 1720000, + afterThreshold: 0, + beforeThreshold: 1720000, }), args: '0x' as const, }; diff --git a/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenPeriodic.ts b/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenPeriodic.ts index 81be534fdc..de1ecc8aef 100644 --- a/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenPeriodic.ts +++ b/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenPeriodic.ts @@ -28,10 +28,12 @@ export function makeErc20TokenPeriodicRule( erc20PeriodicEnforcer, valueLteEnforcer, nonceEnforcer, + redeemerEnforcer, } = enforcers; return makePermissionRule({ permissionType: 'erc20-token-periodic', - optionalEnforcers: [timestampEnforcer], + optionalEnforcers: [timestampEnforcer, redeemerEnforcer], + redeemerEnforcer, 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 3e27a482c7..b27c8b5af4 100644 --- a/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenRevocation.test.ts +++ b/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenRevocation.test.ts @@ -22,8 +22,8 @@ describe('erc20-token-revocation rule', () => { const expiryCaveat = { enforcer: TimestampEnforcer, terms: createTimestampTerms({ - timestampAfterThreshold: 0, - timestampBeforeThreshold: 1720000, + afterThreshold: 0, + beforeThreshold: 1720000, }), args: '0x' 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 728d85ea64..8dde558038 100644 --- a/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenRevocation.ts +++ b/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenRevocation.ts @@ -26,10 +26,12 @@ export function makeErc20TokenRevocationRule( allowedCalldataEnforcer, valueLteEnforcer, nonceEnforcer, + redeemerEnforcer, } = enforcers; return makePermissionRule({ permissionType: 'erc20-token-revocation', - optionalEnforcers: [timestampEnforcer], + optionalEnforcers: [timestampEnforcer, redeemerEnforcer], + redeemerEnforcer, timestampEnforcer, requiredEnforcers: { [allowedCalldataEnforcer]: 2, diff --git a/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenStream.test.ts b/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenStream.test.ts index a6f2237826..099512d8ce 100644 --- a/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenStream.test.ts +++ b/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenStream.test.ts @@ -27,8 +27,8 @@ describe('erc20-token-stream rule', () => { const expiryCaveat = { enforcer: TimestampEnforcer, terms: createTimestampTerms({ - timestampAfterThreshold: 0, - timestampBeforeThreshold: 1720000, + afterThreshold: 0, + beforeThreshold: 1720000, }), args: '0x' as const, }; diff --git a/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenStream.ts b/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenStream.ts index 4aa1c5b022..ee08c3852f 100644 --- a/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenStream.ts +++ b/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenStream.ts @@ -28,10 +28,12 @@ export function makeErc20TokenStreamRule( erc20StreamingEnforcer, valueLteEnforcer, nonceEnforcer, + redeemerEnforcer, } = enforcers; return makePermissionRule({ permissionType: 'erc20-token-stream', - optionalEnforcers: [timestampEnforcer], + optionalEnforcers: [timestampEnforcer, redeemerEnforcer], + redeemerEnforcer, timestampEnforcer, requiredEnforcers: { [erc20StreamingEnforcer]: 1, diff --git a/packages/gator-permissions-controller/src/decodePermission/rules/makePermissionRule.test.ts b/packages/gator-permissions-controller/src/decodePermission/rules/makePermissionRule.test.ts index be7128a161..37a7906840 100644 --- a/packages/gator-permissions-controller/src/decodePermission/rules/makePermissionRule.test.ts +++ b/packages/gator-permissions-controller/src/decodePermission/rules/makePermissionRule.test.ts @@ -3,6 +3,7 @@ import { CHAIN_ID, DELEGATOR_CONTRACTS, } from '@metamask/delegation-deployments'; +import { getChecksumAddress } from '@metamask/utils'; import type { Hex } from '@metamask/utils'; import { makePermissionRule } from './makePermissionRule'; @@ -11,6 +12,7 @@ describe('makePermissionRule', () => { const contracts = DELEGATOR_CONTRACTS['1.3.0'][CHAIN_ID.sepolia]; const timestampEnforcer = contracts.TimestampEnforcer; const requiredEnforcer = contracts.NonceEnforcer; + const redeemerEnforcer = contracts.RedeemerEnforcer; it('calls optional validate callback when provided and decoding succeeds', () => { const validateAndDecodeData = jest.fn().mockReturnValue({}); @@ -18,6 +20,7 @@ describe('makePermissionRule', () => { const rule = makePermissionRule({ permissionType: 'native-token-stream', timestampEnforcer, + redeemerEnforcer, optionalEnforcers: [], requiredEnforcers: { [requiredEnforcer]: 1 }, validateAndDecodeData, @@ -27,8 +30,8 @@ describe('makePermissionRule', () => { { enforcer: timestampEnforcer, terms: createTimestampTerms({ - timestampAfterThreshold: 0, - timestampBeforeThreshold: 1720000, + afterThreshold: 0, + beforeThreshold: 1720000, }), args: '0x' as Hex, }, @@ -56,6 +59,7 @@ describe('makePermissionRule', () => { const rule = makePermissionRule({ permissionType: 'native-token-stream', timestampEnforcer, + redeemerEnforcer, optionalEnforcers: [], requiredEnforcers: { [requiredEnforcer]: 1 }, validateAndDecodeData, @@ -90,6 +94,7 @@ describe('makePermissionRule', () => { const rule = makePermissionRule({ permissionType: 'native-token-stream', timestampEnforcer, + redeemerEnforcer, optionalEnforcers: [], requiredEnforcers: { [requiredEnforcer]: 1 }, validateAndDecodeData, @@ -125,6 +130,7 @@ describe('makePermissionRule', () => { const rule = makePermissionRule({ permissionType: 'native-token-stream', timestampEnforcer, + redeemerEnforcer, optionalEnforcers: [], requiredEnforcers: { [requiredEnforcer]: 1 }, validateAndDecodeData, @@ -134,8 +140,8 @@ describe('makePermissionRule', () => { { enforcer: timestampEnforcer, terms: createTimestampTerms({ - timestampAfterThreshold: 0, - timestampBeforeThreshold: 1720000, + afterThreshold: 0, + beforeThreshold: 1720000, }), args: '0x' as Hex, }, @@ -162,6 +168,7 @@ describe('makePermissionRule', () => { const rule = makePermissionRule({ permissionType: 'native-token-stream', timestampEnforcer, + redeemerEnforcer, optionalEnforcers: [], requiredEnforcers: { [requiredEnforcer]: 1 }, validateAndDecodeData, @@ -171,8 +178,8 @@ describe('makePermissionRule', () => { { enforcer: timestampEnforcer, terms: createTimestampTerms({ - timestampAfterThreshold: 0, - timestampBeforeThreshold: 1720000, + afterThreshold: 0, + beforeThreshold: 1720000, }), args: '0x' as Hex, }, @@ -200,6 +207,7 @@ describe('makePermissionRule', () => { const rule = makePermissionRule({ permissionType: 'native-token-stream', timestampEnforcer, + redeemerEnforcer, optionalEnforcers: [], requiredEnforcers: { [requiredEnforcer]: 1 }, validateAndDecodeData, @@ -221,4 +229,51 @@ describe('makePermissionRule', () => { } expect(validateAndDecodeData).toHaveBeenCalled(); }); + + it('includes redeemer rule when RedeemerEnforcer caveat is present', () => { + const validateAndDecodeData = jest.fn().mockReturnValue({}); + // Raw packed 20-byte address (40 hex chars), not ABI-padded 32-byte words. + const packedAddr = '1111111111111111111111111111111111111111' as const; + + const rule = makePermissionRule({ + permissionType: 'native-token-stream', + timestampEnforcer, + redeemerEnforcer, + optionalEnforcers: [], + requiredEnforcers: { [requiredEnforcer]: 1 }, + validateAndDecodeData, + }); + + const caveats = [ + { + enforcer: requiredEnforcer, + terms: '0x' as Hex, + args: '0x' as Hex, + }, + { + enforcer: redeemerEnforcer, + terms: `0x${packedAddr}` as Hex, + args: '0x' as Hex, + }, + ]; + + const result = rule.validateAndDecodePermission(caveats); + + expect(result.isValid).toBe(true); + if (!result.isValid) { + throw new Error('Expected valid result'); + } + expect(result.rules).toStrictEqual([ + { + type: 'redeemer', + data: { + addresses: [ + getChecksumAddress( + '0x1111111111111111111111111111111111111111' as Hex, + ), + ], + }, + }, + ]); + }); }); diff --git a/packages/gator-permissions-controller/src/decodePermission/rules/makePermissionRule.ts b/packages/gator-permissions-controller/src/decodePermission/rules/makePermissionRule.ts index d7cdf8b869..a2cefb1f21 100644 --- a/packages/gator-permissions-controller/src/decodePermission/rules/makePermissionRule.ts +++ b/packages/gator-permissions-controller/src/decodePermission/rules/makePermissionRule.ts @@ -1,7 +1,10 @@ +import type { Rule } from '@metamask/7715-permission-types'; import type { Caveat } from '@metamask/delegation-core'; +import { 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 type { ChecksumCaveat, DecodedPermission, @@ -22,6 +25,7 @@ import { * * @param args - The arguments to this function. * @param args.optionalEnforcers - Enforcer addresses that may appear in addition to required. + * @param args.redeemerEnforcer - Address of the RedeemerEnforcer used to extract redeemer 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. @@ -30,12 +34,14 @@ import { */ export function makePermissionRule({ optionalEnforcers, + redeemerEnforcer, timestampEnforcer, permissionType, requiredEnforcers, validateAndDecodeData, }: { optionalEnforcers: Hex[]; + redeemerEnforcer: Hex; timestampEnforcer: Hex; permissionType: PermissionType; requiredEnforcers: Record; @@ -94,7 +100,25 @@ export function makePermissionRule({ const data = validateAndDecodeData(checksumCaveats); - return { isValid: true, expiry, data }; + const redeemerTerms = getTermsByEnforcer({ + caveats: checksumCaveats, + enforcer: redeemerEnforcer, + throwIfNotFound: false, + }); + + let rules: Rule[] | undefined; + if (redeemerTerms) { + rules = [ + { + type: EXECUTION_PERMISSION_REDEEMER_RULE_TYPE, + data: { + addresses: decodeRedeemerTerms(redeemerTerms).redeemers, + }, + }, + ]; + } + + return { isValid: true, expiry, data, rules }; } catch (caughtError) { return { isValid: false, error: caughtError as Error }; } diff --git a/packages/gator-permissions-controller/src/decodePermission/rules/nativeTokenPeriodic.test.ts b/packages/gator-permissions-controller/src/decodePermission/rules/nativeTokenPeriodic.test.ts index 0fa484ba2f..8ea1ed3528 100644 --- a/packages/gator-permissions-controller/src/decodePermission/rules/nativeTokenPeriodic.test.ts +++ b/packages/gator-permissions-controller/src/decodePermission/rules/nativeTokenPeriodic.test.ts @@ -29,8 +29,8 @@ describe('native-token-periodic rule', () => { const expiryCaveat = { enforcer: TimestampEnforcer, terms: createTimestampTerms({ - timestampAfterThreshold: 0, - timestampBeforeThreshold: 1720000, + afterThreshold: 0, + beforeThreshold: 1720000, }), args: '0x' as const, }; diff --git a/packages/gator-permissions-controller/src/decodePermission/rules/nativeTokenPeriodic.ts b/packages/gator-permissions-controller/src/decodePermission/rules/nativeTokenPeriodic.ts index 8309dc8c5a..1e16e33f72 100644 --- a/packages/gator-permissions-controller/src/decodePermission/rules/nativeTokenPeriodic.ts +++ b/packages/gator-permissions-controller/src/decodePermission/rules/nativeTokenPeriodic.ts @@ -23,10 +23,12 @@ export function makeNativeTokenPeriodicRule( nativeTokenPeriodicEnforcer, exactCalldataEnforcer, nonceEnforcer, + redeemerEnforcer, } = enforcers; return makePermissionRule({ permissionType: 'native-token-periodic', - optionalEnforcers: [timestampEnforcer], + optionalEnforcers: [timestampEnforcer, redeemerEnforcer], + redeemerEnforcer, timestampEnforcer, requiredEnforcers: { [nativeTokenPeriodicEnforcer]: 1, diff --git a/packages/gator-permissions-controller/src/decodePermission/rules/nativeTokenStream.test.ts b/packages/gator-permissions-controller/src/decodePermission/rules/nativeTokenStream.test.ts index 368b6ec45e..5d0a84abc2 100644 --- a/packages/gator-permissions-controller/src/decodePermission/rules/nativeTokenStream.test.ts +++ b/packages/gator-permissions-controller/src/decodePermission/rules/nativeTokenStream.test.ts @@ -29,8 +29,8 @@ describe('native-token-stream rule', () => { const expiryCaveat = { enforcer: TimestampEnforcer, terms: createTimestampTerms({ - timestampAfterThreshold: 0, - timestampBeforeThreshold: 1720000, + afterThreshold: 0, + beforeThreshold: 1720000, }), args: '0x' as const, }; @@ -48,8 +48,8 @@ describe('native-token-stream rule', () => { { enforcer: TimestampEnforcer, terms: createTimestampTerms({ - timestampAfterThreshold: 0, - timestampBeforeThreshold: 9999, + afterThreshold: 0, + beforeThreshold: 9999, }), args: '0x' as const, }, @@ -280,8 +280,8 @@ describe('native-token-stream rule', () => { { enforcer: TimestampEnforcer, terms: createTimestampTerms({ - timestampAfterThreshold: 0, - timestampBeforeThreshold: 0, + afterThreshold: 0, + beforeThreshold: 0, }), args: '0x' as const, }, @@ -319,8 +319,8 @@ describe('native-token-stream rule', () => { { enforcer: TimestampEnforcer, terms: createTimestampTerms({ - timestampAfterThreshold: 1, - timestampBeforeThreshold: 1720000, + afterThreshold: 1, + beforeThreshold: 1720000, }), args: '0x' as const, }, diff --git a/packages/gator-permissions-controller/src/decodePermission/rules/nativeTokenStream.ts b/packages/gator-permissions-controller/src/decodePermission/rules/nativeTokenStream.ts index dfdd7f37a9..4c19a036dc 100644 --- a/packages/gator-permissions-controller/src/decodePermission/rules/nativeTokenStream.ts +++ b/packages/gator-permissions-controller/src/decodePermission/rules/nativeTokenStream.ts @@ -23,10 +23,12 @@ export function makeNativeTokenStreamRule( nativeTokenStreamingEnforcer, exactCalldataEnforcer, nonceEnforcer, + redeemerEnforcer, } = enforcers; return makePermissionRule({ permissionType: 'native-token-stream', - optionalEnforcers: [timestampEnforcer], + optionalEnforcers: [timestampEnforcer, redeemerEnforcer], + redeemerEnforcer, timestampEnforcer, requiredEnforcers: { [nativeTokenStreamingEnforcer]: 1, diff --git a/packages/gator-permissions-controller/src/decodePermission/types.ts b/packages/gator-permissions-controller/src/decodePermission/types.ts index d6a6e8ac0d..c263ff84dd 100644 --- a/packages/gator-permissions-controller/src/decodePermission/types.ts +++ b/packages/gator-permissions-controller/src/decodePermission/types.ts @@ -1,6 +1,7 @@ import type { PermissionRequest, PermissionTypes, + Rule, } from '@metamask/7715-permission-types'; import type { Caveat } from '@metamask/delegation-core'; import type { DELEGATOR_CONTRACTS } from '@metamask/delegation-deployments'; @@ -31,6 +32,8 @@ export type DecodedPermission = Pick< }; expiry: number | null; origin: string; + /** Rules recovered from caveats (e.g. redeemer allowlist). */ + rules?: Rule[]; }; /** @@ -51,6 +54,7 @@ export type ChecksumEnforcersByChainId = { timestampEnforcer: Hex; nonceEnforcer: Hex; allowedCalldataEnforcer: Hex; + redeemerEnforcer: Hex; }; /** Caveat with checksummed enforcer address; used by rule decode functions. */ @@ -65,6 +69,7 @@ export type ValidateAndDecodeResult = isValid: true; expiry: number | null; data: DecodedPermission['permission']['data']; + rules?: Rule[]; } | { isValid: false; error: Error }; diff --git a/packages/gator-permissions-controller/src/decodePermission/utils.test.ts b/packages/gator-permissions-controller/src/decodePermission/utils.test.ts index 78992f5494..b81c286f56 100644 --- a/packages/gator-permissions-controller/src/decodePermission/utils.test.ts +++ b/packages/gator-permissions-controller/src/decodePermission/utils.test.ts @@ -22,6 +22,7 @@ const buildContracts = (): DeployedContractsByName => ({ ValueLteEnforcer: '0x7777777777777777777777777777777777777777', NonceEnforcer: '0x8888888888888888888888888888888888888888', AllowedCalldataEnforcer: '0x9999999999999999999999999999999999999999', + RedeemerEnforcer: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', }); describe('getChecksumEnforcersByChainId', () => { @@ -51,6 +52,7 @@ describe('getChecksumEnforcersByChainId', () => { allowedCalldataEnforcer: getChecksumAddress( contracts.AllowedCalldataEnforcer, ), + redeemerEnforcer: getChecksumAddress(contracts.RedeemerEnforcer), }); }); @@ -76,6 +78,7 @@ describe('createPermissionRulesForChainId', () => { timestampEnforcer, nonceEnforcer, allowedCalldataEnforcer, + redeemerEnforcer, } = getChecksumEnforcersByChainId(contracts); // erc20-token-stream @@ -96,10 +99,13 @@ describe('createPermissionRulesForChainId', () => { expect(byType['native-token-stream'].permissionType).toBe( 'native-token-stream', ); - expect(byType['native-token-stream'].optionalEnforcers.size).toBe(1); + expect(byType['native-token-stream'].optionalEnforcers.size).toBe(2); expect( byType['native-token-stream'].optionalEnforcers.has(timestampEnforcer), ).toBe(true); + expect( + byType['native-token-stream'].optionalEnforcers.has(redeemerEnforcer), + ).toBe(true); expect(byType['native-token-stream'].requiredEnforcers.size).toBe(3); expect( Array.from(byType['native-token-stream'].requiredEnforcers.entries()), @@ -116,10 +122,13 @@ describe('createPermissionRulesForChainId', () => { expect(byType['native-token-periodic'].permissionType).toBe( 'native-token-periodic', ); - expect(byType['native-token-periodic'].optionalEnforcers.size).toBe(1); + expect(byType['native-token-periodic'].optionalEnforcers.size).toBe(2); expect( byType['native-token-periodic'].optionalEnforcers.has(timestampEnforcer), ).toBe(true); + expect( + byType['native-token-periodic'].optionalEnforcers.has(redeemerEnforcer), + ).toBe(true); expect(byType['native-token-periodic'].requiredEnforcers.size).toBe(3); expect( Array.from(byType['native-token-periodic'].requiredEnforcers.entries()), @@ -136,10 +145,13 @@ describe('createPermissionRulesForChainId', () => { expect(byType['erc20-token-stream'].permissionType).toBe( 'erc20-token-stream', ); - expect(byType['erc20-token-stream'].optionalEnforcers.size).toBe(1); + expect(byType['erc20-token-stream'].optionalEnforcers.size).toBe(2); expect( byType['erc20-token-stream'].optionalEnforcers.has(timestampEnforcer), ).toBe(true); + expect( + byType['erc20-token-stream'].optionalEnforcers.has(redeemerEnforcer), + ).toBe(true); expect(byType['erc20-token-stream'].requiredEnforcers.size).toBe(3); expect( Array.from(byType['erc20-token-stream'].requiredEnforcers.entries()), @@ -156,10 +168,13 @@ describe('createPermissionRulesForChainId', () => { expect(byType['erc20-token-periodic'].permissionType).toBe( 'erc20-token-periodic', ); - expect(byType['erc20-token-periodic'].optionalEnforcers.size).toBe(1); + expect(byType['erc20-token-periodic'].optionalEnforcers.size).toBe(2); expect( byType['erc20-token-periodic'].optionalEnforcers.has(timestampEnforcer), ).toBe(true); + expect( + byType['erc20-token-periodic'].optionalEnforcers.has(redeemerEnforcer), + ).toBe(true); expect(byType['erc20-token-periodic'].requiredEnforcers.size).toBe(3); expect( Array.from(byType['erc20-token-periodic'].requiredEnforcers.entries()), @@ -176,10 +191,13 @@ describe('createPermissionRulesForChainId', () => { expect(byType['erc20-token-revocation'].permissionType).toBe( 'erc20-token-revocation', ); - expect(byType['erc20-token-revocation'].optionalEnforcers.size).toBe(1); + expect(byType['erc20-token-revocation'].optionalEnforcers.size).toBe(2); expect( byType['erc20-token-revocation'].optionalEnforcers.has(timestampEnforcer), ).toBe(true); + expect( + byType['erc20-token-revocation'].optionalEnforcers.has(redeemerEnforcer), + ).toBe(true); expect(byType['erc20-token-revocation'].requiredEnforcers.size).toBe(3); expect( Array.from(byType['erc20-token-revocation'].requiredEnforcers.entries()), diff --git a/packages/gator-permissions-controller/src/decodePermission/utils.ts b/packages/gator-permissions-controller/src/decodePermission/utils.ts index 4a40671cbd..e239972ca2 100644 --- a/packages/gator-permissions-controller/src/decodePermission/utils.ts +++ b/packages/gator-permissions-controller/src/decodePermission/utils.ts @@ -20,6 +20,7 @@ const ENFORCER_CONTRACT_NAMES = { ValueLteEnforcer: 'ValueLteEnforcer', NonceEnforcer: 'NonceEnforcer', AllowedCalldataEnforcer: 'AllowedCalldataEnforcer', + RedeemerEnforcer: 'RedeemerEnforcer', }; /** @@ -99,6 +100,10 @@ export const getChecksumEnforcersByChainId = ( ENFORCER_CONTRACT_NAMES.AllowedCalldataEnforcer, ); + const redeemerEnforcer = getChecksumContractAddress( + ENFORCER_CONTRACT_NAMES.RedeemerEnforcer, + ); + return { erc20StreamingEnforcer, erc20PeriodicEnforcer, @@ -109,6 +114,7 @@ export const getChecksumEnforcersByChainId = ( timestampEnforcer, nonceEnforcer, allowedCalldataEnforcer, + redeemerEnforcer, }; }; diff --git a/packages/gator-permissions-controller/src/index.ts b/packages/gator-permissions-controller/src/index.ts index dd34e049f9..125248cb72 100644 --- a/packages/gator-permissions-controller/src/index.ts +++ b/packages/gator-permissions-controller/src/index.ts @@ -1,4 +1,8 @@ export { default as GatorPermissionsController } from './GatorPermissionsController'; +export { + DELEGATION_FRAMEWORK_VERSION, + EXECUTION_PERMISSION_REDEEMER_RULE_TYPE, +} from './constants'; export type { GatorPermissionsControllerFetchAndUpdateGatorPermissionsAction, GatorPermissionsControllerAddPendingRevocationAction, @@ -18,7 +22,6 @@ export type { GatorPermissionsControllerStateChangeEvent, } from './GatorPermissionsController'; export type { DecodedPermission } from './decodePermission'; -export { DELEGATION_FRAMEWORK_VERSION } from './constants'; export type { GatorPermissionsControllerErrorCode, GatorPermissionsSnapRpcMethod, @@ -34,6 +37,7 @@ export type { SupportedPermissionType, } from './types'; +export type { RedeemerRule } from './redeemerRule'; export type { NativeTokenStreamPermission, NativeTokenPeriodicPermission, diff --git a/packages/gator-permissions-controller/src/permissionOnChainStatus.test.ts b/packages/gator-permissions-controller/src/permissionOnChainStatus.test.ts index ad1932a30f..2b7eb65839 100644 --- a/packages/gator-permissions-controller/src/permissionOnChainStatus.test.ts +++ b/packages/gator-permissions-controller/src/permissionOnChainStatus.test.ts @@ -43,8 +43,8 @@ describe('permissionOnChainStatus', () => { it('returns expiry from TimestampEnforcer caveat terms', () => { const expirySeconds = 1893456000; const terms = createTimestampTerms({ - timestampAfterThreshold: 0, - timestampBeforeThreshold: expirySeconds, + afterThreshold: 0, + beforeThreshold: expirySeconds, }); const delegation: Delegation = { delegate: '0x4f71DA06987BfeDE90aF0b33E1e3e4ffDCEE7a63', @@ -329,8 +329,8 @@ describe('permissionOnChainStatus', () => { it('sets Expired when latest block time is at or past timestamp caveat expiry', async () => { const expirySeconds = 1_000_000_000; const terms = createTimestampTerms({ - timestampAfterThreshold: 0, - timestampBeforeThreshold: expirySeconds, + afterThreshold: 0, + beforeThreshold: expirySeconds, }); const delegation: Delegation = { delegate: '0x4f71DA06987BfeDE90aF0b33E1e3e4ffDCEE7a63', @@ -523,8 +523,8 @@ describe('permissionOnChainStatus', () => { const expirySeconds = 2_500_000_000; const blockSeconds = expirySeconds - 10_000; const terms = createTimestampTerms({ - timestampAfterThreshold: 0, - timestampBeforeThreshold: expirySeconds, + afterThreshold: 0, + beforeThreshold: expirySeconds, }); const delegation: Delegation = { delegate: '0x4f71DA06987BfeDE90aF0b33E1e3e4ffDCEE7a63', diff --git a/packages/gator-permissions-controller/src/redeemerRule.ts b/packages/gator-permissions-controller/src/redeemerRule.ts new file mode 100644 index 0000000000..43ef16b6f8 --- /dev/null +++ b/packages/gator-permissions-controller/src/redeemerRule.ts @@ -0,0 +1,12 @@ +import type { Hex } from '@metamask/utils'; + +/** + * Execution permission rule restricting which addresses may redeem the delegation + * (on-chain RedeemerEnforcer caveat). + */ +export type RedeemerRule = { + type: 'redeemer'; + data: { + addresses: Hex[]; + }; +}; diff --git a/yarn.lock b/yarn.lock index ed3ee16bda..1c6fd13b6b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3478,14 +3478,14 @@ __metadata: languageName: unknown linkType: soft -"@metamask/delegation-core@npm:^0.2.0": - version: 0.2.0 - resolution: "@metamask/delegation-core@npm:0.2.0" +"@metamask/delegation-core@npm:^1.1.0": + version: 1.1.0 + resolution: "@metamask/delegation-core@npm:1.1.0" dependencies: "@metamask/abi-utils": "npm:^3.0.0" "@metamask/utils": "npm:^11.4.0" "@noble/hashes": "npm:^1.8.0" - checksum: 10/ed9430ae854971f9db1082beb26da4de14fa3956a642ca894252abee02c43f61533b274188e3fc7577e9de4ab701f77e6ed2cce30f9fac88806db44c59910bd5 + checksum: 10/672f9e2e2b4e8c312f2cd2ff166bbc508fbdb6e141fe92e678abc9993b9ccbdd17db711477a9b97b6ce3919fa6d51d759c16f6c6fda3f89cb95e303b8aa76f7d languageName: node linkType: hard @@ -4108,7 +4108,7 @@ __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:^0.2.0" + "@metamask/delegation-core": "npm:^1.1.0" "@metamask/delegation-deployments": "npm:^0.12.0" "@metamask/messenger": "npm:^1.1.1" "@metamask/network-controller": "npm:^30.0.1"