Skip to content

Commit

Permalink
fix: account for native token transfer & meta transactions
Browse files Browse the repository at this point in the history
  • Loading branch information
hzhu committed Jul 10, 2024
1 parent 052037c commit 96dea73
Show file tree
Hide file tree
Showing 5 changed files with 399 additions and 25 deletions.
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ jobs:
- run: npm test --coverage
env:
ETH_MAINNET_RPC: ${{ secrets.ETH_MAINNET_RPC }}
ALCHEMY_API_KEY: ${{ secrets.ALCHEMY_API_KEY }}
- uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
Expand Down
216 changes: 198 additions & 18 deletions src/parseSwapV2.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,53 @@
import { erc20Abi, getAddress, formatUnits } from "viem";
import {
fromHex,
erc20Abi,
parseUnits,
getAddress,
formatUnits,
formatEther,
multicall3Abi,
decodeFunctionData,
} from "viem";

import type {
Hex,
Hash,
Chain,
Address,
Transport,
PublicClient,
TransactionReceipt,
} from "viem";

const settlerAbi = [
{
inputs: [
{
components: [
{ internalType: "address", name: "recipient", type: "address" },
{
internalType: "contract IERC20",
name: "buyToken",
type: "address",
},
{ internalType: "uint256", name: "minAmountOut", type: "uint256" },
],
internalType: "struct SettlerBase.AllowedSlippage",
name: "slippage",
type: "tuple",
},
{ internalType: "bytes[]", name: "actions", type: "bytes[]" },
{ internalType: "bytes32", name: "", type: "bytes32" },
{ internalType: "address", name: "msgSender", type: "address" },
{ internalType: "bytes", name: "sig", type: "bytes" },
],
name: "executeMetaTxn",
outputs: [{ internalType: "bool", name: "", type: "bool" }],
stateMutability: "nonpayable",
type: "function",
},
];

const NATIVE_TOKEN_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";

type SupportedChainId = 1 | 10 | 56 | 137 | 8453 | 42161 | 43114;
Expand All @@ -22,6 +63,8 @@ export const NATIVE_SYMBOL_BY_CHAIN_ID: { [key in SupportedChainId]: string } =
43114: "AVAX", // Avalanche
};

const MULTICALL3_ADDRESS = "0xcA11bde05977b3631167028862bE2a173976CA11";

export function isChainIdSupported(
chainId: number
): chainId is SupportedChainId {
Expand All @@ -41,10 +84,76 @@ export interface EnrichedLog {
decimals: number;
}

interface Trace {
to: Address;
from: Address;
gas: Hex;
gasUsed: Hex;
input: "Hash";
output: Hash;
calls: Trace[];
value: Hex;
type: "CALL" | "STATICCALL" | "DELEGATECALL" | "CREATE" | "CREATE2";
}

type TraceTransactionSchema = {
Parameters: [
hash: Hash,
options:
| {
disableStorage?: boolean;
disableStack?: boolean;
enableMemory?: boolean;
enableReturnData?: boolean;
tracer?: string;
}
| {
timeout?: string;
tracerConfig?: {
onlyTopCall?: boolean;
withLog?: boolean;
};
}
| undefined
];
ReturnType: Trace;
};

function extractNativeTransfer(trace: Trace, recipient: Address) {
let totalTransferred = 0n;

function traverseCalls(calls: Trace[]) {
calls.forEach((call) => {
if (
call.to.toLowerCase() === recipient &&
fromHex(call.value, "bigint") > 0n
) {
totalTransferred = totalTransferred + fromHex(call.value, "bigint");
}
if (call.calls && call.calls.length > 0) {
traverseCalls(call.calls);
}
});
}

traverseCalls(trace.calls);

return formatEther(totalTransferred);
}

export async function transferLogs({
publicClient,
transactionReceipt,
}: EnrichLogsArgs): Promise<any> {
}: EnrichLogsArgs): Promise<
{
to: `0x${string}`;
from: `0x${string}`;
symbol: string;
amount: string;
address: `0x${string}`;
decimals: number;
}[]
> {
const EVENT_SIGNATURES = {
Transfer:
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
Expand Down Expand Up @@ -91,45 +200,116 @@ function convertHexToAddress(hexString: string): string {

export async function parseSwapV2({
publicClient,
transactionHash,
transactionHash: hash,
}: {
publicClient: PublicClient<Transport, Chain>;
transactionHash: Address;
}) {
const chainId = await publicClient.getChainId();

if (!isChainIdSupported(chainId)) {
throw new Error(`chainId ${chainId} is unsupported…`);
}

const transactionReceipt = await publicClient.getTransactionReceipt({
hash: transactionHash,
});
const { value } = await publicClient.getTransaction({
hash: transactionHash,
});
const client = publicClient.extend((client) => ({
async traceCall(args: { hash: Hash }) {
return client.request<TraceTransactionSchema>({
method: "debug_traceTransaction",
params: [args.hash, { tracer: "callTracer" }],
});
},
}));

const trace = await client.traceCall({ hash });

const transaction = await publicClient.getTransaction({ hash });

const { from: taker, value, to } = transaction;

const nativeTransferAmount = extractNativeTransfer(trace, taker);

const transactionReceipt = await publicClient.getTransactionReceipt({ hash });

const isNativeSell = value > 0n;

const logs = await transferLogs({
publicClient,
transactionReceipt,
});

const input = logs[0];
const output = logs[logs.length - 1];
let input = logs[0];

let output =
nativeTransferAmount === "0"
? logs.find((log) => {
return log.to.toLowerCase() === taker.toLowerCase();
})
: {
symbol: NATIVE_SYMBOL_BY_CHAIN_ID[chainId],
amount: nativeTransferAmount,
address: NATIVE_TOKEN_ADDRESS,
};

if (to?.toLowerCase() === MULTICALL3_ADDRESS.toLowerCase()) {
const { args: multicallArgs } = decodeFunctionData({
abi: multicall3Abi,
data: transaction.input,
});

const { args: settlerArgs } = decodeFunctionData<any[]>({
abi: settlerAbi,
data: multicallArgs[0][1].callData,
});

const takerForGaslessApprovalSwap =
settlerArgs[0].recipient.toLowerCase() as Address;

const nativeTransferAmount = extractNativeTransfer(
trace,
takerForGaslessApprovalSwap
);

if (nativeTransferAmount === "0") {
output = output = logs[logs.length - 1];
} else {
output = {
symbol: NATIVE_SYMBOL_BY_CHAIN_ID[chainId],
amount: nativeTransferAmount,
address: NATIVE_TOKEN_ADDRESS,
};
}
}

if (isNativeSell) {
const sellAmount = formatUnits(value, 18);
const nativeSellAmount = formatEther(value);
const tokenOut = logs
.filter((log) => log.to.toLowerCase() === taker)
.reduce(
(acc, curr) => ({
symbol: curr.symbol,
amount: formatUnits(
BigInt(acc.amount) + parseUnits(curr.amount, curr.decimals),
curr.decimals
),
address: curr.address,
}),
{ symbol: "", amount: "", address: "" }
);

return {
tokenIn: {
symbol: NATIVE_SYMBOL_BY_CHAIN_ID[chainId],
amount: sellAmount,
address: NATIVE_TOKEN_ADDRESS,
amount: nativeSellAmount,
},
tokenOut: {
symbol: output.symbol,
amount: output.amount,
address: output.address,
},
tokenOut,
};
}

if (!output) {
return null;
}

return {
tokenIn: {
symbol: input.symbol,
Expand Down
Loading

0 comments on commit 96dea73

Please sign in to comment.