From a81c4f5e61a213687ecd22febb77cbe4c44adda0 Mon Sep 17 00:00:00 2001 From: Vitor Marthendal Nunes Date: Wed, 11 Dec 2024 13:57:22 -0300 Subject: [PATCH] Feat: Timestamp enforcer (#61) * refactor: remove positional caveats * feat: add timestamp enforcer support * feat: add timestamp enforcer to erc20transferamount enforcer * fix: sign erc20 transfer * fix: api-delegation post delegation validation --- .../src/routes/credit-lines/index.ts | 8 +- ...ansfer-amount-enforcer-collection-total.ts | 14 +-- .../src/api/credit-lines/index.ts | 6 +- .../erc20-transfer-amount-enforcer.ts | 30 ----- .../components/card-payment-basic.tsx | 10 +- .../format-erc20-transfer-enforcer-calls.ts | 9 +- .../app/(site)/finance/credit/view-table.tsx | 1 - .../use-erc20-transfer-amount-enforcer.ts | 73 +++++------ .../src/actions/use-sign-erc20-transfer.ts | 113 ++++++++++-------- .../enforcer-erc20-transfer-amount.ts | 17 +++ .../src/enforcers/enforcer-timestamp.ts | 59 +++++++++ .../src/exports/index.ts | 1 + 12 files changed, 201 insertions(+), 140 deletions(-) create mode 100644 packages/universal-delegations-sdk/src/enforcers/enforcer-timestamp.ts diff --git a/apps/api-universal/src/routes/credit-lines/index.ts b/apps/api-universal/src/routes/credit-lines/index.ts index bab4b596..170eb7ab 100644 --- a/apps/api-universal/src/routes/credit-lines/index.ts +++ b/apps/api-universal/src/routes/credit-lines/index.ts @@ -9,6 +9,7 @@ import { getIssuedDelegations } from './utils/get-issued-delegations.js'; import { getRedeemedCreditLines } from './utils/get-redeemed-credit-Lines.js'; import { getCreditLineSchema } from './utils/validation.js'; import type { DelegationWithMetadata } from 'universal-types'; +import { getErc20TransferAmountEnforcerFromDelegation } from 'universal-delegations-sdk'; type DelegationWithOnchainData = DelegationWithMetadata & { isRevoked: boolean; @@ -88,11 +89,8 @@ const creditLineRouter = new Hono().post( }, ); - const terms = delegation.caveats[0]?.terms; - - if (!terms) { - throw new Error('No terms found for delegation'); - } + const { terms } = + getErc20TransferAmountEnforcerFromDelegation(delegation); const [token, limit] = decodeEnforcerERC20TransferAmount(terms); const tokenList = getDefaultTokenList({ chainId }); diff --git a/apps/api-universal/src/utils/calculate-erc20-transfer-amount-enforcer-collection-total.ts b/apps/api-universal/src/utils/calculate-erc20-transfer-amount-enforcer-collection-total.ts index 25a941f0..0c1f835b 100644 --- a/apps/api-universal/src/utils/calculate-erc20-transfer-amount-enforcer-collection-total.ts +++ b/apps/api-universal/src/utils/calculate-erc20-transfer-amount-enforcer-collection-total.ts @@ -1,4 +1,7 @@ -import { decodeEnforcerERC20TransferAmount } from 'universal-delegations-sdk'; +import { + decodeEnforcerERC20TransferAmount, + getErc20TransferAmountEnforcerFromDelegation, +} from 'universal-delegations-sdk'; import type { Delegation } from 'universal-types'; import type { Address } from 'viem'; @@ -7,12 +10,9 @@ export function calculateERC20TransferAmountEnforcerCollectionTotal( token: Address, ): bigint { return delegations.reduce((acc, delegation) => { - if (!delegation?.caveats?.[0]?.terms) { - return acc; - } - const [_token, _amount] = decodeEnforcerERC20TransferAmount( - delegation.caveats[0].terms, - ); + const { terms } = getErc20TransferAmountEnforcerFromDelegation(delegation); + + const [_token, _amount] = decodeEnforcerERC20TransferAmount(terms); if (String(_token).toLowerCase() !== token.toLowerCase() || !_amount) { return acc; } diff --git a/apps/delegations-indexer/src/api/credit-lines/index.ts b/apps/delegations-indexer/src/api/credit-lines/index.ts index f6b3a17b..f49ccc8e 100644 --- a/apps/delegations-indexer/src/api/credit-lines/index.ts +++ b/apps/delegations-indexer/src/api/credit-lines/index.ts @@ -1,11 +1,9 @@ import { ponder } from '@/generated'; import { zValidator } from '@hono/zod-validator'; import { and, eq } from '@ponder/core'; +import { decodeEnforcerERC20TransferAmount } from 'universal-delegations-sdk'; import { delegations } from '../../../ponder.schema.js'; -import { - decodeEnforcerERC20TransferAmount, - decodeErc20TransferAmountEvent, -} from '../../utils/delegation/enforcers/erc20-transfer-amount-enforcer.js'; +import { decodeErc20TransferAmountEvent } from '../../utils/delegation/enforcers/erc20-transfer-amount-enforcer.js'; import type { RedeemedCreditLinesResponse } from './types.js'; diff --git a/apps/delegations-indexer/src/utils/delegation/enforcers/erc20-transfer-amount-enforcer.ts b/apps/delegations-indexer/src/utils/delegation/enforcers/erc20-transfer-amount-enforcer.ts index 60133eb5..2bb88e14 100644 --- a/apps/delegations-indexer/src/utils/delegation/enforcers/erc20-transfer-amount-enforcer.ts +++ b/apps/delegations-indexer/src/utils/delegation/enforcers/erc20-transfer-amount-enforcer.ts @@ -4,11 +4,7 @@ import { type Hex, decodeAbiParameters, encodeAbiParameters, - encodePacked, getAbiItem, - hexToBigInt, - parseUnits, - sliceHex, } from 'viem'; type Erc20TransferAmountEventInputs = { @@ -61,29 +57,3 @@ export function decodeErc20TransferAmountEvent( spent, }; } - -export function encodeEnforcerERC20TransferAmount(data: { - token: Address; - amount: string; - decimals: number; -}) { - return encodePacked( - ['address', 'uint256'], - [data.token, parseUnits(data.amount, data.decimals)], - ); -} - -export function decodeEnforcerERC20TransferAmount(data: Hex) { - // Addresses are 20 bytes, uint256 is 32 bytes - const addressSize = 20; - const uint256Size = 32; - - // Decode `token` (first 20 bytes) - const token = sliceHex(data, 0, addressSize) as Address; - - // Decode `amount` (next 32 bytes) - const amountHex = sliceHex(data, addressSize, addressSize + uint256Size); - const amount = hexToBigInt(amountHex); - - return [token, BigInt(amount)] as const; -} diff --git a/apps/popup/app/(site)/sign/eth-sign-typed-data-v-4/components/card-payment-basic.tsx b/apps/popup/app/(site)/sign/eth-sign-typed-data-v-4/components/card-payment-basic.tsx index f587ab90..49d48fd7 100644 --- a/apps/popup/app/(site)/sign/eth-sign-typed-data-v-4/components/card-payment-basic.tsx +++ b/apps/popup/app/(site)/sign/eth-sign-typed-data-v-4/components/card-payment-basic.tsx @@ -3,7 +3,10 @@ import type { Delegation } from 'universal-types'; import { cn } from '@/lib/utils'; import { useMemo } from 'react'; import { findToken, getDefaultTokenList } from 'universal-data'; -import { decodeEnforcerERC20TransferAmount } from 'universal-delegations-sdk'; +import { + decodeEnforcerERC20TransferAmount, + getErc20TransferAmountEnforcerFromDelegation, +} from 'universal-delegations-sdk'; import { DebitCard } from 'universal-wallet-ui'; import { type Address, formatUnits } from 'viem'; @@ -18,9 +21,8 @@ export const CardPaymentBasic = ({ chainId, }: CardPaymentBasic) => { const data = useMemo(() => { - const formattedTerms = decodeEnforcerERC20TransferAmount( - typedData.caveats[0].terms, - ); + const { terms } = getErc20TransferAmountEnforcerFromDelegation(typedData); + const formattedTerms = decodeEnforcerERC20TransferAmount(terms); const address = formattedTerms[0] as Address; const tokenList = getDefaultTokenList({ chainId }); diff --git a/apps/popup/src/lib/delegation-framework/enforcers/erc20-transfer-amount/format-erc20-transfer-enforcer-calls.ts b/apps/popup/src/lib/delegation-framework/enforcers/erc20-transfer-amount/format-erc20-transfer-enforcer-calls.ts index 42d8720f..eec4f90c 100644 --- a/apps/popup/src/lib/delegation-framework/enforcers/erc20-transfer-amount/format-erc20-transfer-enforcer-calls.ts +++ b/apps/popup/src/lib/delegation-framework/enforcers/erc20-transfer-amount/format-erc20-transfer-enforcer-calls.ts @@ -7,6 +7,7 @@ import { decodeEnforcerERC20TransferAmount, encodeDelegation, encodeSingleExecution, + getErc20TransferAmountEnforcerFromDelegation, } from 'universal-delegations-sdk'; import type { DelegationWithMetadata, @@ -22,13 +23,9 @@ export type Erc20TransferEnforcerRedemption = { }; function encodeErc20TransferEnforcerCalldata(delegation: DelegationWithAmount) { - const caveat = delegation.caveats[0]; + const { terms } = getErc20TransferAmountEnforcerFromDelegation(delegation); - if (!caveat) { - throw new Error('No caveat found'); - } - - const [token] = decodeEnforcerERC20TransferAmount(caveat.terms); + const [token] = decodeEnforcerERC20TransferAmount(terms); const permissionContexts = [encodeDelegation(delegation)]; const execution: DelegationExecution = { value: 0n, diff --git a/apps/wallet/app/(site)/finance/credit/view-table.tsx b/apps/wallet/app/(site)/finance/credit/view-table.tsx index ec158730..72cff944 100644 --- a/apps/wallet/app/(site)/finance/credit/view-table.tsx +++ b/apps/wallet/app/(site)/finance/credit/view-table.tsx @@ -80,7 +80,6 @@ const CardAuthorization = ({ className, delegation }: CardAuthorization) => { }); const { data: enforcerData } = useErc20TransferAmountEnforcer({ - address: delegation.caveats[0].enforcer, delegation: delegation, }); diff --git a/packages/universal-delegations-sdk/src/actions/enforcers/use-erc20-transfer-amount-enforcer.ts b/packages/universal-delegations-sdk/src/actions/enforcers/use-erc20-transfer-amount-enforcer.ts index 966c2b1f..455d037b 100644 --- a/packages/universal-delegations-sdk/src/actions/enforcers/use-erc20-transfer-amount-enforcer.ts +++ b/packages/universal-delegations-sdk/src/actions/enforcers/use-erc20-transfer-amount-enforcer.ts @@ -5,25 +5,42 @@ import { findToken, getDefaultTokenList, erc20TransferAmountEnforcerAbi, + universalDeployments, } from 'universal-data'; -import { type Address, erc20Abi, formatUnits } from 'viem'; +import { erc20Abi, formatUnits } from 'viem'; import { usePublicClient, useReadContract } from 'wagmi'; import { getDelegationHash } from '../../delegation/get-delegation-hash.js'; import { decodeEnforcerERC20TransferAmount } from '../../enforcers/enforcer-erc20-transfer-amount.js'; -import type { DelegationWithMetadata } from 'universal-types'; +import type { DelegationCaveat, DelegationWithMetadata } from 'universal-types'; + +function getErc20TransferAmountEnforcerFromDelegation( + delegation: DelegationWithMetadata, +): DelegationCaveat { + const erc20TransferAmountEnforcer = delegation.caveats.find( + ({ enforcer }) => + enforcer.toLowerCase() === + universalDeployments.ERC20TransferAmountEnforcer.toLowerCase(), + ); + if (!erc20TransferAmountEnforcer) { + throw new Error('No ERC20TransferAmountEnforcer found'); + } + + return erc20TransferAmountEnforcer; +} export function useErc20TransferAmountEnforcer({ - address, delegation, }: { - address: Address; delegation: DelegationWithMetadata; }) { + const { enforcer, terms } = + getErc20TransferAmountEnforcerFromDelegation(delegation); + const client = usePublicClient(); const hash = getDelegationHash(delegation); const spentMap = useReadContract({ abi: erc20TransferAmountEnforcerAbi, - address: address, + address: enforcer, functionName: 'spentMap', args: [delegation.verifyingContract, hash], query: { @@ -32,41 +49,31 @@ export function useErc20TransferAmountEnforcer({ }); const data = useMemo(() => { - if ( - client && - delegation && - delegation?.caveats?.[0]?.terms && - typeof spentMap.data === 'bigint' - ) { - const decodedTerms = decodeEnforcerERC20TransferAmount( - delegation.caveats[0].terms, - ); + if (client && typeof spentMap.data === 'bigint') { + const [tokenAddress, amount] = decodeEnforcerERC20TransferAmount(terms); const tokenList = getDefaultTokenList({ chainId: delegation.chainId, }); - const address = decodedTerms[0] as Address; + const token = findToken({ - address, + address: tokenAddress, tokenList, }); if (token) { return { to: delegation.delegate, - token: decodedTerms[0] as Address, + token: tokenAddress, name: token.name, symbol: token.symbol, decimals: token.decimals, - amount: decodedTerms[1], - amountFormatted: formatUnits( - (decodedTerms[1] as bigint) || BigInt(0), - token.decimals, - ), + amount, + amountFormatted: formatUnits(amount || BigInt(0), token.decimals), spent: spentMap.data, spentFormatted: formatUnits( (spentMap.data as bigint) || BigInt(0), token.decimals, ), - spendLimitReached: spentMap.data >= BigInt(decodedTerms[1] as bigint), + spendLimitReached: spentMap.data >= amount, }; } if (!token) { @@ -75,17 +82,17 @@ export function useErc20TransferAmountEnforcer({ contracts: [ { abi: erc20Abi, - address: decodedTerms[0] as Address, + address: tokenAddress, functionName: 'name', }, { abi: erc20Abi, - address: decodedTerms[0] as Address, + address: tokenAddress, functionName: 'symbol', }, { abi: erc20Abi, - address: decodedTerms[0] as Address, + address: tokenAddress, functionName: 'decimals', }, ], @@ -93,22 +100,18 @@ export function useErc20TransferAmountEnforcer({ .then((result) => { return { to: delegation.delegate, - token: decodedTerms[0] as Address, + token: tokenAddress, name: result[0].result, symbol: result[1].result, decimals: result[2].result, - amount: decodedTerms[1], - amountFormatted: formatUnits( - decodedTerms[1] as bigint, - result[2].result as number, - ), + amount, + amountFormatted: formatUnits(amount, result[2].result as number), spent: spentMap.data, spentFormatted: formatUnits( (spentMap.data as bigint) || BigInt(0), result[2].result as number, ), - spendLimitReached: - (spentMap.data as bigint) >= BigInt(decodedTerms[1] as bigint), + spendLimitReached: (spentMap.data as bigint) >= amount, }; }) .catch(() => { @@ -117,7 +120,7 @@ export function useErc20TransferAmountEnforcer({ } } return null; - }, [client, delegation, spentMap.data]); + }, [client, delegation, spentMap.data, terms]); const formatted = useMemo(() => { if (!spentMap) return null; diff --git a/packages/universal-delegations-sdk/src/actions/use-sign-erc20-transfer.ts b/packages/universal-delegations-sdk/src/actions/use-sign-erc20-transfer.ts index fd36101e..62431d74 100644 --- a/packages/universal-delegations-sdk/src/actions/use-sign-erc20-transfer.ts +++ b/packages/universal-delegations-sdk/src/actions/use-sign-erc20-transfer.ts @@ -7,7 +7,12 @@ import { useInsertDelegation } from '../api/actions/insert-delegation.js'; import { eip712DelegationTypes } from '../delegation/eip712-delegation-type.js'; import { getDelegationHash } from '../delegation/get-delegation-hash.js'; import { encodeEnforcerERC20TransferAmount } from '../enforcers/enforcer-erc20-transfer-amount.js'; -import type { Delegation, DelegationWithMetadata } from 'universal-types'; +import type { + Delegation, + DelegationCaveat, + DelegationWithMetadata, +} from 'universal-types'; +import { encodeEnforcerTimestamp } from '../enforcers/enforcer-timestamp.js'; type SignDelegationParams = { chainId: number; @@ -17,6 +22,7 @@ type SignDelegationParams = { erc20: Address; decimals: number; amount: string; + timestampBefore?: bigint; }; export function useSignErc20TransferDelegation() { @@ -34,24 +40,38 @@ export function useSignErc20TransferDelegation() { erc20, decimals = 18, amount = '0', + timestampBefore, }: SignDelegationParams) { + const caveats: DelegationCaveat[] = [ + { + enforcer: universalDeployments.ERC20TransferAmountEnforcer, + terms: encodeEnforcerERC20TransferAmount({ + token: erc20, + amount: amount, + decimals: decimals, + }), + args: '0x', + }, + ]; + + if (timestampBefore) { + // If timestampBefore is provided, add a timestamp enforcer + caveats.push({ + enforcer: universalDeployments.TimestampEnforcer, + terms: encodeEnforcerTimestamp({ + timestampBefore, + }), + args: '0x', + }); + } + const coreDelegation: Delegation = { authority: ROOT_AUTHORITY, delegate: delegate, delegator: delegator, salt, signature: '0x', - caveats: [ - { - enforcer: universalDeployments.ERC20TransferAmountEnforcer, - terms: encodeEnforcerERC20TransferAmount({ - token: erc20, - amount: amount, - decimals: decimals, - }), - args: '0x', - }, - ], + caveats, }; setDelegation({ hash: getDelegationHash(coreDelegation), @@ -80,16 +100,7 @@ export function useSignErc20TransferDelegation() { delegator: delegator, authority: ROOT_AUTHORITY, salt: salt, - caveats: [ - { - enforcer: universalDeployments.ERC20TransferAmountEnforcer, - terms: encodeEnforcerERC20TransferAmount({ - token: erc20, - amount: amount, - decimals: decimals, - }), - }, - ], + caveats, }, }); } @@ -121,7 +132,29 @@ export function useSignErc20TransferDelegation() { erc20, decimals = 18, amount = '0', + timestampBefore, }: SignDelegationParams) { + const caveats: Omit[] = [ + { + enforcer: universalDeployments.ERC20TransferAmountEnforcer, + terms: encodeEnforcerERC20TransferAmount({ + token: erc20, + amount: amount, + decimals: decimals, + }), + }, + ]; + + if (timestampBefore) { + // If timestampBefore is provided, add a timestamp enforcer + caveats.push({ + enforcer: universalDeployments.TimestampEnforcer, + terms: encodeEnforcerTimestamp({ + timestampBefore, + }), + }); + } + const signature = await signTypedDataAsync({ types: eip712DelegationTypes, primaryType: 'Delegation', @@ -132,40 +165,24 @@ export function useSignErc20TransferDelegation() { verifyingContract: universalDeployments.DelegationManager, }, message: { - delegate: delegate, - delegator: delegator, + delegate, + delegator, authority: ROOT_AUTHORITY, - salt: salt, - caveats: [ - { - enforcer: universalDeployments.ERC20TransferAmountEnforcer, - terms: encodeEnforcerERC20TransferAmount({ - token: erc20, - amount: amount, - decimals: decimals, - }), - }, - ], + salt, + caveats, }, }); const _coreDelegation: Delegation = { authority: ROOT_AUTHORITY, - delegate: delegate, - delegator: delegator, + delegate, + delegator, salt, signature, - caveats: [ - { - enforcer: universalDeployments.ERC20TransferAmountEnforcer, - terms: encodeEnforcerERC20TransferAmount({ - token: erc20, - amount: amount, - decimals: decimals, - }), - args: '0x', - }, - ], + caveats: caveats.map((caveat) => ({ + ...caveat, + args: '0x', + })), }; const _delegation = { diff --git a/packages/universal-delegations-sdk/src/enforcers/enforcer-erc20-transfer-amount.ts b/packages/universal-delegations-sdk/src/enforcers/enforcer-erc20-transfer-amount.ts index fe71cf15..9b8de0dd 100644 --- a/packages/universal-delegations-sdk/src/enforcers/enforcer-erc20-transfer-amount.ts +++ b/packages/universal-delegations-sdk/src/enforcers/enforcer-erc20-transfer-amount.ts @@ -1,3 +1,5 @@ +import { universalDeployments } from 'universal-data'; +import type { Delegation, DelegationCaveat } from 'universal-types'; import { type Address, type Hex, @@ -32,3 +34,18 @@ export function decodeEnforcerERC20TransferAmount(data: Hex) { return [token, BigInt(amount)] as const; } + +export function getErc20TransferAmountEnforcerFromDelegation( + delegation: Delegation, +): DelegationCaveat { + const erc20TransferAmountEnforcer = delegation.caveats.find( + ({ enforcer }) => + enforcer.toLowerCase() === + universalDeployments.ERC20TransferAmountEnforcer.toLowerCase(), + ); + if (!erc20TransferAmountEnforcer) { + throw new Error('No ERC20TransferAmountEnforcer found'); + } + + return erc20TransferAmountEnforcer; +} diff --git a/packages/universal-delegations-sdk/src/enforcers/enforcer-timestamp.ts b/packages/universal-delegations-sdk/src/enforcers/enforcer-timestamp.ts new file mode 100644 index 00000000..876ca2c4 --- /dev/null +++ b/packages/universal-delegations-sdk/src/enforcers/enforcer-timestamp.ts @@ -0,0 +1,59 @@ +import { + encodePacked, + sliceHex, + type Hex, + hexToBigInt, + maxUint128, +} from 'viem'; + +export type EncodeEnforcerTimestampParams = + | { + timestampAfter: bigint; + timestampBefore?: never; + } + | { + timestampAfter?: never; + timestampBefore: bigint; + } + | { + timestampAfter: bigint; + timestampBefore: bigint; + }; +export type EncodeEnforcerTimestampReturnType = Hex; + +export function encodeEnforcerTimestamp({ + timestampAfter, + timestampBefore, +}: EncodeEnforcerTimestampParams): EncodeEnforcerTimestampReturnType { + // If no timestampAfter is provided, default to 0, accepting all timestamps + timestampAfter = timestampAfter ?? BigInt(0); + + // If no timestampBefore is provided, default to maxUint128, accepting all timestamps + timestampBefore = timestampBefore ?? maxUint128; + + return encodePacked( + ['uint128', 'uint128'], + [timestampBefore, timestampAfter], + ); +} + +export type DecodeEnforcerTimestampParams = Hex; +export type DecodeEnforcerTimestampReturnType = { + timestampAfter: bigint; + timestampBefore: bigint; +}; + +export function decodeEnforcerTimestamp( + data: DecodeEnforcerTimestampParams, +): DecodeEnforcerTimestampReturnType { + const uint128Size = 16; + + const timestampBefore = hexToBigInt(sliceHex(data, 0, uint128Size)); + const timestampAfter = hexToBigInt( + sliceHex(data, uint128Size, uint128Size * 2), + ); + return { + timestampBefore, + timestampAfter, + }; +} diff --git a/packages/universal-delegations-sdk/src/exports/index.ts b/packages/universal-delegations-sdk/src/exports/index.ts index 5c226d9d..55e8d70b 100644 --- a/packages/universal-delegations-sdk/src/exports/index.ts +++ b/packages/universal-delegations-sdk/src/exports/index.ts @@ -19,6 +19,7 @@ export { export { decodeEnforcerERC20TransferAmount, encodeEnforcerERC20TransferAmount, + getErc20TransferAmountEnforcerFromDelegation, } from '../enforcers/enforcer-erc20-transfer-amount.js'; export { useDelegationExecute } from '../actions/core/use-delegation-execute.js'; export { useDelegationStatus } from '../actions/core/use-delegation-status.js';