From 7e2f727aedd6b7820953c901a491fe18abd13620 Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Tue, 1 Jul 2025 12:32:29 +1200 Subject: [PATCH 1/2] Add erc20TokenPeriodTransfer terms builder --- packages/delegation-core/README.md | 27 ++++++ .../src/caveats/erc20Streaming.ts | 8 +- .../src/caveats/erc20TokenPeriodTransfer.ts | 96 +++++++++++++++++++ packages/delegation-core/src/caveats/index.ts | 1 + packages/delegation-core/src/index.ts | 1 + .../erc20PeriodTransferBuilder.ts | 28 ++---- 6 files changed, 135 insertions(+), 26 deletions(-) create mode 100644 packages/delegation-core/src/caveats/erc20TokenPeriodTransfer.ts diff --git a/packages/delegation-core/README.md b/packages/delegation-core/README.md index 6f8ea0ad..43d7e3d9 100644 --- a/packages/delegation-core/README.md +++ b/packages/delegation-core/README.md @@ -184,6 +184,33 @@ const terms = createERC20StreamingTerms({ }); ``` +#### `createERC20TokenPeriodTransferTerms(terms, options?)` + +Creates terms for an ERC20TokenPeriodTransfer caveat that validates that ERC20 token transfers do not exceed a specified amount within a given time period. + +**Parameters:** +- `terms: ERC20TokenPeriodTransferTerms` + - `tokenAddress: BytesLike` - The address of the ERC20 token. + - `periodAmount: bigint` - The maximum amount that can be transferred within each period. + - `periodDuration: number` - The duration of each period in seconds. + - `startDate: number` - Unix timestamp when the first period begins. +- `options?: EncodingOptions` - Optional encoding options + +**Returns:** `Hex | Uint8Array` - 128-byte encoded terms (32 bytes per parameter) + +**Example:** +```typescript +import { createERC20TokenPeriodTransferTerms } from '@metamask/delegation-core'; + +// Allow 100 tokens per day starting from a specific date +const terms = createERC20TokenPeriodTransferTerms({ + tokenAddress: '0xA0b86a33E6441E74C65c6BF2A6d73B895B9b34A2', + periodAmount: 100n, // 100 tokens + periodDuration: 86400, // 24 hours in seconds + startDate: 1640995200 // 2022-01-01 00:00:00 UTC +}); +``` + ### Delegation Utilities #### `encodeDelegations(delegations)` diff --git a/packages/delegation-core/src/caveats/erc20Streaming.ts b/packages/delegation-core/src/caveats/erc20Streaming.ts index cbb96d94..1fedae06 100644 --- a/packages/delegation-core/src/caveats/erc20Streaming.ts +++ b/packages/delegation-core/src/caveats/erc20Streaming.ts @@ -71,18 +71,18 @@ export function createERC20StreamingTerms( throw new Error('Invalid tokenAddress: must be a valid address'); } - let tokenAddressHex: string; + let prefixedTokenAddressHex: string; if (typeof tokenAddress === 'string') { if (!isHexString(tokenAddress) || tokenAddress.length !== 42) { throw new Error('Invalid tokenAddress: must be a valid address'); } - tokenAddressHex = tokenAddress.slice(2); + prefixedTokenAddressHex = tokenAddress; } else { if (tokenAddress.length !== 20) { throw new Error('Invalid tokenAddress: must be a valid address'); } - tokenAddressHex = bytesToHex(tokenAddress).slice(2); + prefixedTokenAddressHex = bytesToHex(tokenAddress); } if (initialAmount < 0n) { @@ -116,7 +116,7 @@ export function createERC20StreamingTerms( const amountPerSecondHex = toHexString({ value: amountPerSecond, size: 32 }); const startTimeHex = toHexString({ value: startTime, size: 32 }); - const hexValue = `0x${tokenAddressHex}${initialAmountHex}${maxAmountHex}${amountPerSecondHex}${startTimeHex}`; + const hexValue = `${prefixedTokenAddressHex}${initialAmountHex}${maxAmountHex}${amountPerSecondHex}${startTimeHex}`; return prepareResult(hexValue, encodingOptions); } diff --git a/packages/delegation-core/src/caveats/erc20TokenPeriodTransfer.ts b/packages/delegation-core/src/caveats/erc20TokenPeriodTransfer.ts new file mode 100644 index 00000000..1505de10 --- /dev/null +++ b/packages/delegation-core/src/caveats/erc20TokenPeriodTransfer.ts @@ -0,0 +1,96 @@ +import { type BytesLike, isHexString, bytesToHex } from '@metamask/utils'; + +import { + defaultOptions, + prepareResult, + type EncodingOptions, + type ResultValue, +} from '../returns'; +import type { Hex } from '../types'; +import { toHexString } from '../utils'; + +/** + * Terms for configuring a periodic transfer allowance of ERC20 tokens. + */ +export type ERC20TokenPeriodTransferTerms = { + /** The address of the ERC20 token. */ + tokenAddress: BytesLike; + /** The maximum amount that can be transferred within each period. */ + periodAmount: bigint; + /** The duration of each period in seconds. */ + periodDuration: number; + /** Unix timestamp when the first period begins. */ + startDate: number; +}; + +/** + * Creates terms for an ERC20TokenPeriodTransfer caveat that validates that ERC20 token transfers + * do not exceed a specified amount within a given time period. The transferable amount resets at the + * beginning of each period, and any unused tokens are forfeited once the period ends. + * + * @param terms - The terms for the ERC20TokenPeriodTransfer caveat. + * @param encodingOptions - The encoding options for the result. + * @returns The terms as a 128-byte hex string (32 bytes for each parameter). + * @throws Error if any of the numeric parameters are invalid. + */ +export function createERC20TokenPeriodTransferTerms( + terms: ERC20TokenPeriodTransferTerms, + encodingOptions?: EncodingOptions<'hex'>, +): Hex; +export function createERC20TokenPeriodTransferTerms( + terms: ERC20TokenPeriodTransferTerms, + encodingOptions: EncodingOptions<'bytes'>, +): Uint8Array; +/** + * Creates terms for an ERC20TokenPeriodTransfer caveat that validates that ERC20 token transfers + * do not exceed a specified amount within a given time period. + * + * @param terms - The terms for the ERC20TokenPeriodTransfer caveat. + * @param encodingOptions - The encoding options for the result. + * @returns The terms as a 128-byte hex string (32 bytes for each parameter). + * @throws Error if any of the numeric parameters are invalid. + */ +export function createERC20TokenPeriodTransferTerms( + terms: ERC20TokenPeriodTransferTerms, + encodingOptions: EncodingOptions = defaultOptions, +): Hex | Uint8Array { + const { tokenAddress, periodAmount, periodDuration, startDate } = terms; + + if (!tokenAddress) { + throw new Error('Invalid tokenAddress: must be a valid address'); + } + + let prefixedTokenAddressHex: string; + + if (typeof tokenAddress === 'string') { + if (!isHexString(tokenAddress) || tokenAddress.length !== 42) { + throw new Error('Invalid tokenAddress: must be a valid address'); + } + prefixedTokenAddressHex = tokenAddress; + } else { + if (tokenAddress.length !== 20) { + throw new Error('Invalid tokenAddress: must be a valid address'); + } + prefixedTokenAddressHex = bytesToHex(tokenAddress); + } + + if (periodAmount <= 0n) { + throw new Error('Invalid periodAmount: must be a positive number'); + } + + if (periodDuration <= 0) { + throw new Error('Invalid periodDuration: must be a positive number'); + } + + if (startDate <= 0) { + throw new Error('Invalid startDate: must be a positive number'); + } + + const periodAmountHex = toHexString({ value: periodAmount, size: 32 }); + const periodDurationHex = toHexString({ value: periodDuration, size: 32 }); + const startDateHex = toHexString({ value: startDate, size: 32 }); + + const hexValue = `${prefixedTokenAddressHex}${periodAmountHex}${periodDurationHex}${startDateHex}`; + + return prepareResult(hexValue, encodingOptions); +} diff --git a/packages/delegation-core/src/caveats/index.ts b/packages/delegation-core/src/caveats/index.ts index 7b27f71e..b6dd3487 100644 --- a/packages/delegation-core/src/caveats/index.ts +++ b/packages/delegation-core/src/caveats/index.ts @@ -4,3 +4,4 @@ export { createNativeTokenPeriodTransferTerms } from './nativeTokenPeriodTransfe export { createExactCalldataTerms } from './exactCalldata'; export { createNativeTokenStreamingTerms } from './nativeTokenStreaming'; export { createERC20StreamingTerms } from './erc20Streaming'; +export { createERC20TokenPeriodTransferTerms } from './erc20TokenPeriodTransfer'; diff --git a/packages/delegation-core/src/index.ts b/packages/delegation-core/src/index.ts index 594788e1..7aec9042 100644 --- a/packages/delegation-core/src/index.ts +++ b/packages/delegation-core/src/index.ts @@ -11,6 +11,7 @@ export { createExactCalldataTerms, createNativeTokenStreamingTerms, createERC20StreamingTerms, + createERC20TokenPeriodTransferTerms, } from './caveats'; export { diff --git a/packages/delegation-toolkit/src/caveatBuilder/erc20PeriodTransferBuilder.ts b/packages/delegation-toolkit/src/caveatBuilder/erc20PeriodTransferBuilder.ts index 92aad34e..528793f3 100644 --- a/packages/delegation-toolkit/src/caveatBuilder/erc20PeriodTransferBuilder.ts +++ b/packages/delegation-toolkit/src/caveatBuilder/erc20PeriodTransferBuilder.ts @@ -1,4 +1,4 @@ -import { concat, isAddress, toHex } from 'viem'; +import { createERC20TokenPeriodTransferTerms } from '@metamask/delegation-core'; import type { Address } from 'viem'; import type { Caveat, DeleGatorEnvironment } from '../types'; @@ -26,28 +26,12 @@ export const erc20PeriodTransferBuilder = ( periodDuration: number, startDate: number, ): Caveat => { - if (!isAddress(tokenAddress)) { - throw new Error('Invalid tokenAddress: must be a valid address'); - } - - if (periodAmount <= 0n) { - throw new Error('Invalid periodAmount: must be a positive number'); - } - - if (periodDuration <= 0) { - throw new Error('Invalid periodDuration: must be a positive number'); - } - - if (startDate <= 0) { - throw new Error('Invalid startDate: must be a positive number'); - } - - const terms = concat([ + const terms = createERC20TokenPeriodTransferTerms({ tokenAddress, - toHex(periodAmount, { size: 32 }), - toHex(periodDuration, { size: 32 }), - toHex(startDate, { size: 32 }), - ]); + periodAmount, + periodDuration, + startDate, + }); const { caveatEnforcers: { ERC20PeriodTransferEnforcer }, From 1c1bff7f2ec1ed6c7e6c350f879d370df27358a9 Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Wed, 2 Jul 2025 08:40:27 +1200 Subject: [PATCH 2/2] Fix incorrect terms length description in readme --- packages/delegation-core/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/delegation-core/README.md b/packages/delegation-core/README.md index 43d7e3d9..1347d534 100644 --- a/packages/delegation-core/README.md +++ b/packages/delegation-core/README.md @@ -196,7 +196,7 @@ Creates terms for an ERC20TokenPeriodTransfer caveat that validates that ERC20 t - `startDate: number` - Unix timestamp when the first period begins. - `options?: EncodingOptions` - Optional encoding options -**Returns:** `Hex | Uint8Array` - 128-byte encoded terms (32 bytes per parameter) +**Returns:** `Hex | Uint8Array` - 116-byte encoded terms (20 bytes for address + 32 bytes per parameter) **Example:** ```typescript