Skip to content

Commit

Permalink
Feat: Timestamp enforcer (#61)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
marthendalnunes authored Dec 11, 2024
1 parent aeee77b commit a81c4f5
Show file tree
Hide file tree
Showing 12 changed files with 201 additions and 140 deletions.
8 changes: 3 additions & 5 deletions apps/api-universal/src/routes/credit-lines/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 });
Expand Down
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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;
}
Expand Down
6 changes: 2 additions & 4 deletions apps/delegations-indexer/src/api/credit-lines/index.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,7 @@ import {
type Hex,
decodeAbiParameters,
encodeAbiParameters,
encodePacked,
getAbiItem,
hexToBigInt,
parseUnits,
sliceHex,
} from 'viem';

type Erc20TransferAmountEventInputs = {
Expand Down Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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 });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
decodeEnforcerERC20TransferAmount,
encodeDelegation,
encodeSingleExecution,
getErc20TransferAmountEnforcerFromDelegation,
} from 'universal-delegations-sdk';
import type {
DelegationWithMetadata,
Expand All @@ -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,
Expand Down
1 change: 0 additions & 1 deletion apps/wallet/app/(site)/finance/credit/view-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@ const CardAuthorization = ({ className, delegation }: CardAuthorization) => {
});

const { data: enforcerData } = useErc20TransferAmountEnforcer({
address: delegation.caveats[0].enforcer,
delegation: delegation,
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -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) {
Expand All @@ -75,40 +82,36 @@ 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',
},
],
})
.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(() => {
Expand All @@ -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;
Expand Down
Loading

0 comments on commit a81c4f5

Please sign in to comment.