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
16 changes: 8 additions & 8 deletions packages/delegation-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@ Creates terms for a Timestamp caveat that enforces time-based constraints on del
**Parameters:**

- `terms: TimestampTerms`
- `timestampAfterThreshold: number` - Timestamp (seconds) after which delegation can be used
- `timestampBeforeThreshold: number` - Timestamp (seconds) before which delegation can be used
- `afterThreshold: number` - Timestamp (seconds) after which delegation can be used
- `beforeThreshold: number` - Timestamp (seconds) before which delegation can be used
- `options?: EncodingOptions` - Optional encoding options

**Returns:** `Hex | Uint8Array` - 32-byte encoded terms (16 bytes per timestamp)
Expand All @@ -80,14 +80,14 @@ import { createTimestampTerms } from '@metamask/delegation-core';

// Valid between Jan 1, 2022 and Jan 1, 2023
const terms = createTimestampTerms({
timestampAfterThreshold: 1640995200, // 2022-01-01 00:00:00 UTC
timestampBeforeThreshold: 1672531200, // 2023-01-01 00:00:00 UTC
afterThreshold: 1640995200, // 2022-01-01 00:00:00 UTC
beforeThreshold: 1672531200, // 2023-01-01 00:00:00 UTC
});

// Only valid after a certain time (no end time)
const openEndedTerms = createTimestampTerms({
timestampAfterThreshold: 1640995200,
timestampBeforeThreshold: 0,
afterThreshold: 1640995200,
beforeThreshold: 0,
});
```

Expand Down Expand Up @@ -354,8 +354,8 @@ export type ValueLteTerms = {
};

export type TimestampTerms = {
timestampAfterThreshold: number;
timestampBeforeThreshold: number;
afterThreshold: number;
beforeThreshold: number;
};

export type ExactCalldataTerms = {
Expand Down
68 changes: 62 additions & 6 deletions packages/delegation-core/src/caveats/allowedCalldata.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,24 @@
/**
* ## AllowedCalldataEnforcer
*
* Constrains the calldata bytes starting at a given byte offset to match an expected fragment.
*
* Terms are encoded as a 32-byte big-endian start index followed by the expected calldata bytes (not ABI-wrapped).
*/

import { bytesToHex, remove0x, type BytesLike } from '@metamask/utils';

import { toHexString } from '../internalUtils';
import {
assertHexBytesMinLength,
extractNumber,
extractRemainingHex,
toHexString,
} from '../internalUtils';
import {
bytesLikeToHex,
defaultOptions,
prepareResult,
type DecodedBytesLike,
type EncodingOptions,
type ResultValue,
} from '../returns';
Expand All @@ -12,9 +27,9 @@ import type { Hex } from '../types';
/**
* Terms for configuring an AllowedCalldata caveat.
*/
export type AllowedCalldataTerms = {
export type AllowedCalldataTerms<TBytesLike extends BytesLike = BytesLike> = {
startIndex: number;
value: BytesLike;
value: TBytesLike;
};

/**
Expand All @@ -23,7 +38,7 @@ export type AllowedCalldataTerms = {
*
* @param terms - The terms for the AllowedCalldata caveat.
* @param encodingOptions - The encoding options for the result.
* @returns The terms as the calldata itself.
* @returns Encoded terms.
* @throws Error if the `calldata` is invalid.
*/
export function createAllowedCalldataTerms(
Expand All @@ -40,7 +55,7 @@ export function createAllowedCalldataTerms(
*
* @param terms - The terms for the AllowedCalldata caveat.
* @param encodingOptions - The encoding options for the result.
* @returns The terms as the calldata itself.
* @returns Encoded terms.
* @throws Error if the `calldata` is invalid.
*/
export function createAllowedCalldataTerms(
Expand Down Expand Up @@ -70,6 +85,47 @@ export function createAllowedCalldataTerms(

const indexHex = toHexString({ value: startIndex, size: 32 });

// The terms are the index encoded as 32 bytes followed by the expected value.
return prepareResult(`0x${indexHex}${unprefixedValue}`, encodingOptions);
}

/**
* Decodes terms for an AllowedCalldata caveat from encoded hex data.
*
* @param terms - The encoded terms as a hex string or Uint8Array.
* @param encodingOptions - Whether the decoded value fragment is returned as hex or bytes.
* @returns The decoded AllowedCalldataTerms object.
*/
export function decodeAllowedCalldataTerms(
terms: BytesLike,
encodingOptions?: EncodingOptions<'hex'>,
): AllowedCalldataTerms<DecodedBytesLike<'hex'>>;
export function decodeAllowedCalldataTerms(
terms: BytesLike,
encodingOptions: EncodingOptions<'bytes'>,
): AllowedCalldataTerms<DecodedBytesLike<'bytes'>>;
/**
* @param terms - The encoded terms as a hex string or Uint8Array.
* @param encodingOptions - Whether the decoded value fragment is returned as hex or bytes.
* @returns The decoded AllowedCalldataTerms object.
*/
export function decodeAllowedCalldataTerms(
terms: BytesLike,
encodingOptions: EncodingOptions<ResultValue> = defaultOptions,
):
| AllowedCalldataTerms<DecodedBytesLike<'hex'>>
| AllowedCalldataTerms<DecodedBytesLike<'bytes'>> {
const hexTerms = bytesLikeToHex(terms);
assertHexBytesMinLength(
hexTerms,
32,
'Invalid AllowedCalldata terms: must be at least 32 bytes',
);

const startIndex = extractNumber(hexTerms, 0, 32);
const valueHex = extractRemainingHex(hexTerms, 32);
const value = prepareResult(valueHex, encodingOptions);

return { startIndex, value } as
| AllowedCalldataTerms<DecodedBytesLike<'hex'>>
| AllowedCalldataTerms<DecodedBytesLike<'bytes'>>;
}
72 changes: 67 additions & 5 deletions packages/delegation-core/src/caveats/allowedMethods.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,24 @@
/**
* ## AllowedMethodsEnforcer
*
* Specifies 4 byte method selectors that the delegate is allowed to call.
*
* Terms are encoded as a concatenation of 4-byte function selectors with no padding between selectors.
*/

import { bytesToHex, isHexString, type BytesLike } from '@metamask/utils';

import { concatHex } from '../internalUtils';
import {
assertHexByteLengthAtLeastOneMultipleOf,
concatHex,
extractHex,
getByteLength,
} from '../internalUtils';
import {
bytesLikeToHex,
defaultOptions,
prepareResult,
type DecodedBytesLike,
type EncodingOptions,
type ResultValue,
} from '../returns';
Expand All @@ -12,9 +27,9 @@ import type { Hex } from '../types';
/**
* Terms for configuring an AllowedMethods caveat.
*/
export type AllowedMethodsTerms = {
export type AllowedMethodsTerms<TBytesLike extends BytesLike = BytesLike> = {
/** An array of 4-byte method selectors that the delegate is allowed to call. */
selectors: BytesLike[];
selectors: TBytesLike[];
};

const FUNCTION_SELECTOR_STRING_LENGTH = 10; // 0x + 8 hex chars
Expand All @@ -26,7 +41,7 @@ const INVALID_SELECTOR_ERROR =
*
* @param terms - The terms for the AllowedMethods caveat.
* @param encodingOptions - The encoding options for the result.
* @returns The terms as concatenated method selectors.
* @returns Encoded terms.
* @throws Error if the selectors array is empty or contains invalid selectors.
*/
export function createAllowedMethodsTerms(
Expand All @@ -42,7 +57,7 @@ export function createAllowedMethodsTerms(
*
* @param terms - The terms for the AllowedMethods caveat.
* @param encodingOptions - The encoding options for the result.
* @returns The terms as concatenated method selectors.
* @returns Encoded terms.
* @throws Error if the selectors array is empty or contains invalid selectors.
*/
export function createAllowedMethodsTerms(
Expand Down Expand Up @@ -76,3 +91,50 @@ export function createAllowedMethodsTerms(
const hexValue = concatHex(normalizedSelectors);
return prepareResult(hexValue, encodingOptions);
}

/**
* Decodes terms for an AllowedMethods caveat from encoded hex data.
*
* @param terms - The encoded terms as a hex string or Uint8Array.
* @param encodingOptions - Whether decoded selector values are returned as hex or bytes.
* @returns The decoded AllowedMethodsTerms object.
*/
export function decodeAllowedMethodsTerms(
terms: BytesLike,
encodingOptions?: EncodingOptions<'hex'>,
): AllowedMethodsTerms<DecodedBytesLike<'hex'>>;
export function decodeAllowedMethodsTerms(
terms: BytesLike,
encodingOptions: EncodingOptions<'bytes'>,
): AllowedMethodsTerms<DecodedBytesLike<'bytes'>>;
/**
* @param terms - The encoded terms as a hex string or Uint8Array.
* @param encodingOptions - Whether decoded selector values are returned as hex or bytes.
* @returns The decoded AllowedMethodsTerms object.
*/
export function decodeAllowedMethodsTerms(
terms: BytesLike,
encodingOptions: EncodingOptions<ResultValue> = defaultOptions,
):
| AllowedMethodsTerms<DecodedBytesLike<'hex'>>
| AllowedMethodsTerms<DecodedBytesLike<'bytes'>> {
const hexTerms = bytesLikeToHex(terms);

const selectorSize = 4;
assertHexByteLengthAtLeastOneMultipleOf(
hexTerms,
selectorSize,
'Invalid selectors: must be a multiple of 4',
);
const selectorCount = getByteLength(hexTerms) / selectorSize;

const selectors: (Hex | Uint8Array)[] = [];
for (let i = 0; i < selectorCount; i++) {
const selector = extractHex(hexTerms, i * selectorSize, selectorSize);
selectors.push(prepareResult(selector, encodingOptions));
}

return { selectors } as
| AllowedMethodsTerms<DecodedBytesLike<'hex'>>
| AllowedMethodsTerms<DecodedBytesLike<'bytes'>>;
}
73 changes: 68 additions & 5 deletions packages/delegation-core/src/caveats/allowedTargets.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,25 @@
/**
* ## AllowedTargetsEnforcer
*
* Restricts which contract addresses the delegate may call.
*
* Terms are encoded as the concatenation of 20-byte addresses in order with no padding between addresses.
*/

import type { BytesLike } from '@metamask/utils';

import { concatHex, normalizeAddress } from '../internalUtils';
import {
assertHexByteLengthAtLeastOneMultipleOf,
concatHex,
extractAddress,
getByteLength,
normalizeAddress,
} from '../internalUtils';
import {
bytesLikeToHex,
defaultOptions,
prepareResult,
type DecodedBytesLike,
type EncodingOptions,
type ResultValue,
} from '../returns';
Expand All @@ -12,17 +28,17 @@ import type { Hex } from '../types';
/**
* Terms for configuring an AllowedTargets caveat.
*/
export type AllowedTargetsTerms = {
export type AllowedTargetsTerms<TBytesLike extends BytesLike = BytesLike> = {
/** An array of target addresses that the delegate is allowed to call. */
targets: BytesLike[];
targets: TBytesLike[];
};

/**
* Creates terms for an AllowedTargets caveat that restricts calls to a set of target addresses.
*
* @param terms - The terms for the AllowedTargets caveat.
* @param encodingOptions - The encoding options for the result.
* @returns The terms as concatenated target addresses.
* @returns Encoded terms.
* @throws Error if the targets array is empty or contains invalid addresses.
*/
export function createAllowedTargetsTerms(
Expand All @@ -38,7 +54,7 @@ export function createAllowedTargetsTerms(
*
* @param terms - The terms for the AllowedTargets caveat.
* @param encodingOptions - The encoding options for the result.
* @returns The terms as concatenated target addresses.
* @returns Encoded terms.
* @throws Error if the targets array is empty or contains invalid addresses.
*/
export function createAllowedTargetsTerms(
Expand All @@ -60,3 +76,50 @@ export function createAllowedTargetsTerms(
const hexValue = concatHex(normalizedTargets);
return prepareResult(hexValue, encodingOptions);
}

/**
* Decodes terms for an AllowedTargets caveat from encoded hex data.
*
* @param terms - The encoded terms as a hex string or Uint8Array.
* @param encodingOptions - Whether decoded addresses are returned as hex or bytes.
* @returns The decoded AllowedTargetsTerms object.
*/
export function decodeAllowedTargetsTerms(
terms: BytesLike,
encodingOptions?: EncodingOptions<'hex'>,
): AllowedTargetsTerms<DecodedBytesLike<'hex'>>;
export function decodeAllowedTargetsTerms(
terms: BytesLike,
encodingOptions: EncodingOptions<'bytes'>,
): AllowedTargetsTerms<DecodedBytesLike<'bytes'>>;
/**
* @param terms - The encoded terms as a hex string or Uint8Array.
* @param encodingOptions - Whether decoded addresses are returned as hex or bytes.
* @returns The decoded AllowedTargetsTerms object.
*/
export function decodeAllowedTargetsTerms(
terms: BytesLike,
encodingOptions: EncodingOptions<ResultValue> = defaultOptions,
):
| AllowedTargetsTerms<DecodedBytesLike<'hex'>>
| AllowedTargetsTerms<DecodedBytesLike<'bytes'>> {
const hexTerms = bytesLikeToHex(terms);

const addressSize = 20;
Copy link
Member

@MoMannn MoMannn Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is more of a question. But should we also check the terms length here to make sure they are correct?

On chain we are checking terms length: https://github.com/MetaMask/delegation-framework/blob/f1d5913120fc772a4b2cc7ba4179450ffa2988c1/src/enforcers/AllowedTargetsEnforcer.sol#L58

So it should not be possible to have different terms length here. But the function could be used on a wrong data or something like that and this would be able to detect this. 🤔

If we should check terms then this should be on other enforcers as well

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call - nice little sanity check that might save someone's sanity if they're having to debug!

I've added length checks to all terms decoders.

assertHexByteLengthAtLeastOneMultipleOf(
hexTerms,
addressSize,
'Invalid targets: must be a multiple of 20',
);
const addressCount = getByteLength(hexTerms) / addressSize;

const targets: (Hex | Uint8Array)[] = [];
for (let i = 0; i < addressCount; i++) {
const target = extractAddress(hexTerms, i * addressSize);
targets.push(prepareResult(target, encodingOptions));
}

return { targets } as
| AllowedTargetsTerms<DecodedBytesLike<'hex'>>
| AllowedTargetsTerms<DecodedBytesLike<'bytes'>>;
}
Loading
Loading