Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,39 @@ describe('erc20-token-periodic rule', () => {
expect(result.data.startTime).toBe(1715664);
});

it('decodes mixed-case token address', () => {
const mixedCaseAddress = contracts.ERC20PeriodTransferEnforcer;
const caveats = [
expiryCaveat,
valueLteCaveat,
{
enforcer: ERC20PeriodTransferEnforcer,
terms: createERC20TokenPeriodTransferTerms(
{
tokenAddress: mixedCaseAddress,
periodAmount: 200n,
periodDuration: 86400,
startDate: 1715664,
},
{ out: 'hex' },
),
args: '0x' as const,
},
];
const result = rule.validateAndDecodePermission(caveats);
expect(result.isValid).toBe(true);

// this is here as a type guard
if (!result.isValid) {
throw new Error('Expected valid result');
}

expect(result.expiry).toBe(1720000);
expect(result.data.tokenAddress.toLowerCase()).toBe(
mixedCaseAddress.toLowerCase(),
);
});

it('rejects when periodDuration is 0', () => {
const tokenAddress = '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' as Hex;
const periodAmountHex = 100n.toString(16).padStart(64, '0');
Expand Down Expand Up @@ -294,35 +327,4 @@ describe('erc20-token-periodic rule', () => {
'Invalid erc20-token-periodic terms: periodAmount must be a positive number',
);
});

it('rejects when tokenAddress is not valid hex (invalid characters)', () => {
const invalidTokenAddress = 'gg';
const periodAmountHex = 100n.toString(16).padStart(64, '0');
const periodDurationHex = (86400).toString(16).padStart(64, '0');
const startDateHex = (1715664).toString(16).padStart(64, '0');
const terms =
`0x${invalidTokenAddress}${'0'.repeat(38)}${periodAmountHex}${periodDurationHex}${startDateHex}` as Hex;

const caveats = [
expiryCaveat,
valueLteCaveat,
{
enforcer: ERC20PeriodTransferEnforcer,
terms,
args: '0x' as const,
},
];

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

// this is here as a type guard
if (result.isValid) {
throw new Error('Expected invalid result');
}

expect(result.error.message).toContain(
'Invalid erc20-token-periodic terms: tokenAddress must be a valid hex string',
);
});
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { hexToBigInt, hexToNumber, isHexAddress } from '@metamask/utils';
import { hexToBigInt, hexToNumber } from '@metamask/utils';

import { makePermissionRule } from './makePermissionRule';
import type {
Expand Down Expand Up @@ -89,12 +89,6 @@ function validateAndDecodeData(
const periodAmountBigInt = hexToBigInt(periodAmount);
const startTime = hexToNumber(startTimeRaw);

if (!isHexAddress(tokenAddress)) {
throw new Error(
'Invalid erc20-token-periodic terms: tokenAddress must be a valid hex string',
);
}

if (periodAmountBigInt === 0n) {
throw new Error(
'Invalid erc20-token-periodic terms: periodAmount must be a positive number',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,40 @@ describe('erc20-token-stream rule', () => {
expect(result.error.message).toContain('must be');
});

it('decodes mixed-case token address', () => {
const mixedCaseAddress = contracts.ERC20StreamingEnforcer;
const caveats = [
expiryCaveat,
valueLteCaveat,
{
enforcer: ERC20StreamingEnforcer,
terms: createERC20StreamingTerms(
{
tokenAddress: mixedCaseAddress,
initialAmount: 1n,
maxAmount: 2n,
amountPerSecond: 1n,
startTime: 1715664,
},
{ out: 'hex' },
),
args: '0x' as const,
},
];
const result = rule.validateAndDecodePermission(caveats);
expect(result.isValid).toBe(true);

// this is here as a type guard
if (!result.isValid) {
throw new Error('Expected valid result');
}

expect(result.expiry).toBe(1720000);
expect(result.data?.tokenAddress.toLowerCase()).toBe(
mixedCaseAddress.toLowerCase(),
);
});

it('decodes zero token address', () => {
const zeroAddress = '0x0000000000000000000000000000000000000000' as Hex;
const caveats = [
Expand Down Expand Up @@ -299,32 +333,4 @@ describe('erc20-token-stream rule', () => {
'Invalid erc20-token-stream terms: startTime must be a positive number',
);
});

it('rejects when tokenAddress is not valid hex (invalid characters)', () => {
const invalidTokenAddress = 'gg';
const initialAmountHex = 1n.toString(16).padStart(64, '0');
const maxAmountHex = 2n.toString(16).padStart(64, '0');
const amountPerSecondHex = 1n.toString(16).padStart(64, '0');
const startTimeHex = (1715664).toString(16).padStart(64, '0');
const terms =
`0x${invalidTokenAddress}${'0'.repeat(38)}${initialAmountHex}${maxAmountHex}${amountPerSecondHex}${startTimeHex}` as Hex;

const caveats = [
expiryCaveat,
valueLteCaveat,
{ enforcer: ERC20StreamingEnforcer, terms, args: '0x' as const },
];

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

// this is here as a type guard
if (result.isValid) {
throw new Error('Expected invalid result');
}

expect(result.error.message).toContain(
'Invalid erc20-token-stream terms: tokenAddress must be a valid hex string',
);
});
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { hexToBigInt, hexToNumber, isHexAddress } from '@metamask/utils';
import { hexToBigInt, hexToNumber } from '@metamask/utils';

import { makePermissionRule } from './makePermissionRule';
import type {
Expand Down Expand Up @@ -96,12 +96,6 @@ function validateAndDecodeData(
const maxAmountBigInt = hexToBigInt(maxAmount);
const amountPerSecondBigInt = hexToBigInt(amountPerSecond);

if (!isHexAddress(tokenAddress)) {
throw new Error(
'Invalid erc20-token-stream terms: tokenAddress must be a valid hex string',
);
}

if (maxAmountBigInt <= initialAmountBigInt) {
throw new Error(
'Invalid erc20-token-stream terms: maxAmount must be greater than initialAmount',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,176 @@ describe('makePermissionRule', () => {
expect(result.data).toStrictEqual({});
expect(validateAndDecodeData).toHaveBeenCalled();
});

it('rejects when any caveat terms are not valid hex (invalid characters)', () => {
const validateAndDecodeData = jest.fn().mockReturnValue({});

const rule = makePermissionRule({
permissionType: 'native-token-stream',
timestampEnforcer,
optionalEnforcers: [],
requiredEnforcers: { [requiredEnforcer]: 1 },
validateAndDecodeData,
});

const caveats = [
{
enforcer: timestampEnforcer,
terms: '0xgg' as Hex,
args: '0x' as Hex,
},
{
enforcer: requiredEnforcer,
terms: '0x' as Hex,
args: '0x' as Hex,
},
];

const result = rule.validateAndDecodePermission(caveats);

expect(result.isValid).toBe(false);
if (result.isValid) {
throw new Error('Expected invalid result');
}
expect(result.error.message).toBe('Invalid terms: must be a hex string');
expect(validateAndDecodeData).not.toHaveBeenCalled();
});

it('rejects when any caveat terms contain non-hex characters after 0x prefix', () => {
const validateAndDecodeData = jest.fn().mockReturnValue({});

const rule = makePermissionRule({
permissionType: 'native-token-stream',
timestampEnforcer,
optionalEnforcers: [],
requiredEnforcers: { [requiredEnforcer]: 1 },
validateAndDecodeData,
});

const caveats = [
{
enforcer: timestampEnforcer,
terms:
'0x000000000000000000000000000000000000000000000000000000000000000z' as Hex,
args: '0x' as Hex,
},
{
enforcer: requiredEnforcer,
terms: '0x' as Hex,
args: '0x' as Hex,
},
];

const result = rule.validateAndDecodePermission(caveats);

expect(result.isValid).toBe(false);
if (result.isValid) {
throw new Error('Expected invalid result');
}
expect(result.error.message).toBe('Invalid terms: must be a hex string');
expect(validateAndDecodeData).not.toHaveBeenCalled();
});

it('rejects when required enforcer terms are not valid hex', () => {
const validateAndDecodeData = jest.fn().mockReturnValue({});

const rule = makePermissionRule({
permissionType: 'native-token-stream',
timestampEnforcer,
optionalEnforcers: [],
requiredEnforcers: { [requiredEnforcer]: 1 },
validateAndDecodeData,
});

const caveats = [
{
enforcer: timestampEnforcer,
terms: createTimestampTerms({
timestampAfterThreshold: 0,
timestampBeforeThreshold: 1720000,
}),
args: '0x' as Hex,
},
{
enforcer: requiredEnforcer,
terms: '0xNOTHEX' as Hex,
args: '0x' as Hex,
},
];

const result = rule.validateAndDecodePermission(caveats);

expect(result.isValid).toBe(false);
if (result.isValid) {
throw new Error('Expected invalid result');
}
expect(result.error.message).toBe('Invalid terms: must be a hex string');
expect(validateAndDecodeData).not.toHaveBeenCalled();
});

it('accepts caveat terms with mixed-case hex', () => {
const validateAndDecodeData = jest.fn().mockReturnValue({});

const rule = makePermissionRule({
permissionType: 'native-token-stream',
timestampEnforcer,
optionalEnforcers: [],
requiredEnforcers: { [requiredEnforcer]: 1 },
validateAndDecodeData,
});

const caveats = [
{
enforcer: timestampEnforcer,
terms: createTimestampTerms({
timestampAfterThreshold: 0,
timestampBeforeThreshold: 1720000,
}),
args: '0x' as Hex,
},
{
enforcer: requiredEnforcer,
terms:
'0x000000000000000000000000000000000000000000000000000000000000abAB' 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.expiry).toBe(1720000);
expect(validateAndDecodeData).toHaveBeenCalled();
});

it('accepts caveat terms with empty hex', () => {
const validateAndDecodeData = jest.fn().mockReturnValue({});

const rule = makePermissionRule({
permissionType: 'native-token-stream',
timestampEnforcer,
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(validateAndDecodeData).toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Caveat } from '@metamask/delegation-core';
import { getChecksumAddress } from '@metamask/utils';
import { getChecksumAddress, isStrictHexString } from '@metamask/utils';
import type { Hex } from '@metamask/utils';

import type {
Expand Down Expand Up @@ -71,6 +71,15 @@ export function makePermissionRule({
enforcer: getChecksumAddress(caveat.enforcer),
}));
try {
const invalidTerms = checksumCaveats.filter(
// isStrictHexString rejects '0x' which is a valid terms value
({ terms }) => terms !== '0x' && !isStrictHexString(terms),
);

if (invalidTerms.length > 0) {
throw new Error('Invalid terms: must be a hex string');
}

let expiry: number | null = null;

const expiryTerms = getTermsByEnforcer({
Expand Down
Loading