Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/gator-permissions-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add `token-approval-revocation` execution permission type decoding ([#8823](https://github.com/MetaMask/core/pull/8823))

### Changed

- Bump `@metamask/transaction-controller` from `^65.3.0` to `^65.4.0` ([#8796](https://github.com/MetaMask/core/pull/8796))
Expand Down
6 changes: 3 additions & 3 deletions packages/gator-permissions-controller/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,11 @@
"test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch"
},
"dependencies": {
"@metamask/7715-permission-types": "^0.6.0",
"@metamask/7715-permission-types": "^0.7.0",
"@metamask/abi-utils": "^2.0.3",
"@metamask/base-controller": "^9.1.0",
"@metamask/delegation-core": "^2.0.0",
"@metamask/delegation-deployments": "^1.3.0",
"@metamask/delegation-core": "^2.2.0",
"@metamask/delegation-deployments": "^1.4.0",
"@metamask/messenger": "^1.2.0",
"@metamask/network-controller": "^32.0.0",
"@metamask/snaps-controllers": "^19.0.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type {
DeployedContractsByName,
PermissionDecoder,
} from './types';
import { getChecksumEnforcersByChainId } from './utils';

// These tests use the live deployments table for version 1.3.0 to
// construct deterministic caveat address sets for a known chain.
Expand All @@ -37,6 +38,8 @@ describe('decodePermission', () => {
NonceEnforcer,
RedeemerEnforcer,
} = contracts;
const { approvalRevocationEnforcer } =
getChecksumEnforcersByChainId(contracts);

describe('findDecodersWithMatchingCaveatAddresses()', () => {
const zeroAddress = '0x0000000000000000000000000000000000000000' as Hex;
Expand Down Expand Up @@ -621,6 +624,62 @@ describe('decodePermission', () => {
).toThrow('Contract not found: AllowedCalldataEnforcer');
});
});

describe('token-approval-revocation', () => {
const expectedPermissionType = 'token-approval-revocation';
const findMatchingDecoders = (enforcers: Hex[]): PermissionDecoder[] =>
findDecodersWithMatchingCaveatAddresses({
enforcers,
permissionDecoders: createPermissionDecodersForContracts(contracts),
});

it('matches with ApprovalRevocationEnforcer and NonceEnforcer', () => {
const enforcers = [approvalRevocationEnforcer, NonceEnforcer];
const rules = findMatchingDecoders(enforcers);
expect(
rules.map((matchingRule) => matchingRule.permissionType),
).toStrictEqual([expectedPermissionType]);
});

it('allows TimestampEnforcer as extra', () => {
const enforcers = [
approvalRevocationEnforcer,
NonceEnforcer,
TimestampEnforcer,
];
const rules = findMatchingDecoders(enforcers);
expect(
rules.map((matchingRule) => matchingRule.permissionType),
).toStrictEqual([expectedPermissionType]);
});

it('rejects when NonceEnforcer is missing', () => {
const enforcers = [approvalRevocationEnforcer];
const rules = findMatchingDecoders(enforcers);
expect(rules).toStrictEqual([]);
});

it('rejects forbidden extra caveat', () => {
const enforcers = [
approvalRevocationEnforcer,
NonceEnforcer,
ValueLteEnforcer,
];
const rules = findMatchingDecoders(enforcers);
expect(rules).toStrictEqual([]);
});

it('accepts lowercased addresses', () => {
const enforcers: Hex[] = [
approvalRevocationEnforcer.toLowerCase() as unknown as Hex,
NonceEnforcer.toLowerCase() as unknown as Hex,
];
const rules = findMatchingDecoders(enforcers);
expect(
rules.map((matchingRule) => matchingRule.permissionType),
).toStrictEqual([expectedPermissionType]);
});
});
});

describe('reconstructDecodedPermission', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { makePermissionDecoder } from './makePermissionDecoder';
import { makeNativeTokenAllowanceDecoderConfig } from './nativeTokenAllowance';
import { makeNativeTokenPeriodicDecoderConfig } from './nativeTokenPeriodic';
import { makeNativeTokenStreamDecoderConfig } from './nativeTokenStream';
import { makeTokenApprovalRevocationDecoderConfig } from './tokenApprovalRevocation';

/**
* Builds the canonical set of permission decoders for a chain.
Expand All @@ -32,5 +33,6 @@ export const createPermissionDecodersForContracts = (
makeErc20TokenPeriodicDecoderConfig(contractAddresses),
makeErc20TokenAllowanceDecoderConfig(contractAddresses),
makeErc20TokenRevocationDecoderConfig(contractAddresses),
makeTokenApprovalRevocationDecoderConfig(contractAddresses),
].map(makePermissionDecoder);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import { createTimestampTerms } from '@metamask/delegation-core';
import {
CHAIN_ID,
DELEGATOR_CONTRACTS,
} from '@metamask/delegation-deployments';

import { createPermissionDecodersForContracts } from '.';
import { getChecksumEnforcersByChainId } from '../utils';

describe('token-approval-revocation decoder', () => {
const chainId = CHAIN_ID.sepolia;
const contracts = DELEGATOR_CONTRACTS['1.3.0'][chainId];
const { timestampEnforcer, approvalRevocationEnforcer, nonceEnforcer } =
getChecksumEnforcersByChainId(contracts);
const permissionDecoders = createPermissionDecodersForContracts(contracts);
const decoder = permissionDecoders.find(
(candidate) => candidate.permissionType === 'token-approval-revocation',
);

if (!decoder) {
throw new Error('Decoder not found');
}

const expiryCaveat = {
enforcer: timestampEnforcer,
terms: createTimestampTerms({
afterThreshold: 0,
beforeThreshold: 1720000,
}),
args: '0x' as const,
};

it('rejects empty terms', () => {
const caveats = [
expiryCaveat,
{
enforcer: approvalRevocationEnforcer,
terms: '0x' as const,
args: '0x' as const,
},
{
enforcer: nonceEnforcer,
terms: '0x' as const,
args: '0x' as const,
},
];

const result = decoder.validateAndDecodePermission(caveats);
expect(result.isValid).toBe(false);

if (result.isValid) {
throw new Error('Expected invalid result');
}

expect(result.error.message).toContain(
'Invalid ApprovalRevocation terms: must be greater than 0',
);
});

it('rejects 0x00 terms', () => {
const caveats = [
expiryCaveat,
{
enforcer: approvalRevocationEnforcer,
terms: '0x00' as const,
args: '0x' as const,
},
{
enforcer: nonceEnforcer,
terms: '0x' as const,
args: '0x' as const,
},
];

const result = decoder.validateAndDecodePermission(caveats);
expect(result.isValid).toBe(false);

if (result.isValid) {
throw new Error('Expected invalid result');
}

expect(result.error.message).toContain(
'Invalid ApprovalRevocation terms: must be greater than 0',
);
});

it('rejects terms whose mask exceeds the supported max', () => {
const caveats = [
expiryCaveat,
{
enforcer: approvalRevocationEnforcer,
terms: '0x40' as const,
args: '0x' as const,
},
{
enforcer: nonceEnforcer,
terms: '0x' as const,
args: '0x' as const,
},
];

const result = decoder.validateAndDecodePermission(caveats);
expect(result.isValid).toBe(false);

if (result.isValid) {
throw new Error('Expected invalid result');
}

expect(result.error.message).toContain(
'Invalid ApprovalRevocation terms: must be less than or equal to 63',
);
});

it('successfully decodes valid token-approval-revocation caveats', () => {
const caveats = [
expiryCaveat,
{
enforcer: approvalRevocationEnforcer,
terms: '0x01' as const,
args: '0x' as const,
},
{
enforcer: nonceEnforcer,
terms: '0x' as const,
args: '0x' as const,
},
];

const result = decoder.validateAndDecodePermission(caveats);
expect(result.isValid).toBe(true);

if (!result.isValid) {
throw new Error('Expected valid result');
}

expect(result.expiry).toBe(1720000);
expect(result.data).toStrictEqual({
erc20Approve: true,
erc721Approve: false,
erc721SetApprovalForAll: false,
permit2Approve: false,
permit2Lockdown: false,
permit2InvalidateNonces: false,
});
expect(result.rules).toStrictEqual([
{
type: 'expiry',
data: { timestamp: 1720000 },
},
]);
});

it('decodes all supported flags from the terms bitmask', () => {
const caveats = [
expiryCaveat,
{
enforcer: approvalRevocationEnforcer,
terms: '0x3f' as const,
args: '0x' as const,
},
{
enforcer: nonceEnforcer,
terms: '0x' as const,
args: '0x' as const,
},
];

const result = decoder.validateAndDecodePermission(caveats);
expect(result.isValid).toBe(true);

if (!result.isValid) {
throw new Error('Expected valid result');
}

expect(result.data).toStrictEqual({
erc20Approve: true,
erc721Approve: true,
erc721SetApprovalForAll: true,
permit2Approve: true,
permit2Lockdown: true,
permit2InvalidateNonces: true,
});
});
});
Loading
Loading