UNPKG

mev-inspect

Version:

A JS port of 'mev-inspect-py' optimised for ease of use.

498 lines 16.6 kB
// eslint-disable-next-line import/no-extraneous-dependencies import { JsonRpcProvider as LegacyJsonRpcProvider } from '@ethersproject/providers'; import { Coder } from 'abi-coder'; import { Contract, Provider as EthcallProvider } from 'ethcall'; import { ZeroAddress, } from 'ethers'; import erc1155Abi from '../abi/erc1155.js'; import erc20Abi from '../abi/erc20.js'; import erc721Abi from '../abi/erc721.js'; import wethAbi from '../abi/weth.js'; const WETH = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'; const TRANSFER_EVENT_TOPIC = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'; const THRESHOLD = 1000; const assetTypes = {}; function getAssets(receipts) { const assets = new Set(); for (const receipt of receipts) { for (const log of receipt.logs) { if (log.topics[0] === TRANSFER_EVENT_TOPIC) { assets.add(log.address.toLowerCase()); } } } return [...assets]; } async function fetchAssetTypes(provider, assets) { const newAssets = assets.filter((asset) => !assetTypes[asset]); const decimalCalls = newAssets.map((asset) => { const contract = new Contract(asset, erc20Abi); return contract.decimals(); }); const INTERFACE_ID_ERC721 = '0x80ac58cd'; const supportsInterfaceCalls = newAssets.map((asset) => { const contract = new Contract(asset, erc721Abi); return contract.supportsInterface(INTERFACE_ID_ERC721); }); const decimalResults = await getNullableCallResults(decimalCalls, 50, provider); const supportsInterfaceResults = await getNullableCallResults(supportsInterfaceCalls, 5, provider); // Store new assets for (const index in newAssets) { const asset = newAssets[index]; const decimalResult = decimalResults[index]; const supportsInterfaceResult = supportsInterfaceResults[index]; const type = decimalResult ? 'erc20' : supportsInterfaceResult ? 'erc721' : 'unknown'; assetTypes[asset] = type; } } function classify(traces, receipts) { const mev = []; for (const txHash in traces) { const trace = traces[txHash]; const receipt = receipts[txHash]; const ethTransfers = getEthTransfers(trace); const erc20Transfers = getErc20Transfers(receipt); const erc721Transfers = getErc721Transfers(receipt); const erc1155Transfers = getErc1155Transfers(receipt); if (isContractDeployment(receipt)) { continue; } if (isTransfer(trace)) { continue; } const txMev = getPureArbitrages(receipt, ethTransfers, erc20Transfers, erc721Transfers, erc1155Transfers); for (const mevItem of txMev) { mev.push(mevItem); } } return mev; } function getEthTransfers(trace) { const transfers = []; for (const call of trace.calls) { const { from, to, value } = call; if (value > 0n) { transfers.push({ from, to, amount: value, }); } } return transfers; } function getErc20Transfers(receipt) { const transfers = []; const wethCoder = new Coder(wethAbi); const coder = new Coder(erc20Abi); for (const log of receipt.logs) { const asset = log.address.toLowerCase(); const type = assetTypes[asset]; if (type !== 'erc20') { continue; } if (asset === WETH) { const event = wethCoder.decodeEvent(log.topics, log.data); if (event.name === 'Transfer') { transfers.push({ asset, from: event.values.src.toLowerCase(), to: event.values.dst.toLowerCase(), amount: event.values.wad, }); } if (event.name === 'Deposit') { transfers.push({ asset, from: '0x0000000000000000000000000000000000000000', to: event.values.dst.toLowerCase(), amount: event.values.wad, }); } if (event.name === 'Withdrawal') { transfers.push({ asset, from: event.values.src.toLowerCase(), to: '0x0000000000000000000000000000000000000000', amount: event.values.wad, }); } } else { try { const event = coder.decodeEvent(log.topics, log.data); if (event.name !== 'Transfer') { continue; } transfers.push({ asset, from: event.values.from.toLowerCase(), to: event.values.to.toLowerCase(), amount: event.values.value, }); } catch (e) { continue; } } } return transfers; } function getErc721Transfers(receipt) { const transfers = []; const coder = new Coder(erc721Abi); for (const log of receipt.logs) { const collection = log.address.toLowerCase(); const type = assetTypes[collection]; if (type !== 'erc721') { continue; } try { const event = coder.decodeEvent(log.topics, log.data); if (event.name !== 'Transfer') { continue; } transfers.push({ collection, id: event.values.tokenId, from: event.values.from.toLowerCase(), to: event.values.to.toLowerCase(), }); } catch (e) { continue; } } return transfers; } function getErc1155Transfers(receipt) { const transfers = []; const coder = new Coder(erc1155Abi); for (const log of receipt.logs) { try { const collection = log.address.toLowerCase(); const event = coder.decodeEvent(log.topics, log.data); if (event.name === 'TransferSingle') { transfers.push({ collection, ids: [event.values.id], amounts: [event.values.value], from: event.values.from.toLowerCase(), to: event.values.to.toLowerCase(), }); } if (event.name === 'TransferBatch') { transfers.push({ collection, ids: event.values.ids, amounts: event.values.values, from: event.values.from.toLowerCase(), to: event.values.to.toLowerCase(), }); } } catch (e) { continue; } } return transfers; } function isContractDeployment(receipt) { return !receipt.to; } function isTransfer(trace) { if (trace.calls.length === 0) { return false; } const topLevelCall = trace.calls[0]; const { value, input } = topLevelCall; // ETH transfer if (value > 0n && input === '0x') { return true; } // ERC20 transfer const erc20Coder = new Coder(erc20Abi); try { const decodedCall = erc20Coder.decodeFunction(input); if (decodedCall.name === 'transfer') { return true; } // eslint-disable-next-line no-empty } catch (e) { } // ERC721 transfer const erc721Coder = new Coder(erc721Abi); try { const decodedCall = erc721Coder.decodeFunction(input); if (decodedCall.name === 'safeTransferFrom') { return true; } // eslint-disable-next-line no-empty } catch (e) { } return false; } function getPureArbitrages(receipt, ethTransfers, erc20Transfers, erc721Transfers, erc1155Transfers) { const arbitrages = []; // account => amount const ethDelta = {}; // account => asset => amount const erc20Delta = {}; // account => collection => id => amount const erc721Delta = {}; // account => collection => id => amount const erc1155Delta = {}; // related accounts const accounts = new Set(); for (const transfer of ethTransfers) { const { from, to, amount } = transfer; if (!ethDelta[from]) { ethDelta[from] = 0n; } if (!ethDelta[to]) { ethDelta[to] = 0n; } ethDelta[from] -= amount; ethDelta[to] += amount; } for (const transfer of erc20Transfers) { const { asset, from, to, amount } = transfer; if (!erc20Delta[from]) { erc20Delta[from] = {}; } if (!erc20Delta[from][asset]) { erc20Delta[from][asset] = 0n; } if (!erc20Delta[to]) { erc20Delta[to] = {}; } if (!erc20Delta[to][asset]) { erc20Delta[to][asset] = 0n; } erc20Delta[from][asset] -= amount; erc20Delta[to][asset] += amount; accounts.add(from); accounts.add(to); } for (const transfer of erc721Transfers) { const { collection, id, from, to } = transfer; if (!erc721Delta[from]) { erc721Delta[from] = {}; } if (!erc721Delta[from][collection]) { erc721Delta[from][collection] = {}; } if (!erc721Delta[from][collection][id.toString()]) { erc721Delta[from][collection][id.toString()] = 0n; } if (!erc721Delta[to]) { erc721Delta[to] = {}; } if (!erc721Delta[to][collection]) { erc721Delta[to][collection] = {}; } if (!erc721Delta[to][collection][id.toString()]) { erc721Delta[to][collection][id.toString()] = 0n; } erc721Delta[from][collection][id.toString()] -= 1n; erc721Delta[to][collection][id.toString()] += 1n; accounts.add(from); accounts.add(to); } for (const transfer of erc1155Transfers) { const { collection, ids, from, to, amounts } = transfer; for (const index in ids) { const id = ids[index]; const amount = amounts[index]; if (!erc1155Delta[from]) { erc1155Delta[from] = {}; } if (!erc1155Delta[from][collection]) { erc1155Delta[from][collection] = {}; } if (!erc1155Delta[from][collection][id.toString()]) { erc1155Delta[from][collection][id.toString()] = 0n; } if (!erc1155Delta[to]) { erc1155Delta[to] = {}; } if (!erc1155Delta[to][collection]) { erc1155Delta[to][collection] = {}; } if (!erc1155Delta[to][collection][id.toString()]) { erc1155Delta[to][collection][id.toString()] = 0n; } erc1155Delta[from][collection][id.toString()] -= amount; erc1155Delta[to][collection][id.toString()] += amount; accounts.add(from); accounts.add(to); } } const senderBalances = getAccountAssetBalances(receipt.from.toLowerCase(), ethDelta, erc20Delta, erc721Delta, erc1155Delta); for (const account of accounts) { let score = 1; if (hasOutgoingTransfers(account, ethTransfers, erc20Transfers, erc721Transfers, erc1155Transfers)) { score *= 8; } if (account !== ZeroAddress) { score *= 10; } if (account === receipt.from.toLowerCase()) { score *= 10; } if (account === receipt.to?.toLowerCase()) { score *= 5; } if (senderBalances) { score *= 4; } if (score < THRESHOLD) { continue; } const balances = getAccountAssetBalances(account, ethDelta, erc20Delta, erc721Delta, erc1155Delta); if (!!balances && balances.length > 0) { arbitrages.push({ transactions: [receipt.hash], receipts: [receipt], searcher: receipt.from.toLowerCase(), beneficiary: account, assets: balances, }); } } return arbitrages; } async function getNullableCallResults(allCalls, limit, provider, block) { const legacyProvider = new LegacyJsonRpcProvider(provider._getConnection().url); const ethcallProvider = new EthcallProvider(); await ethcallProvider.init(legacyProvider); const allResults = []; for (let i = 0; i < allCalls.length / limit; i++) { const startIndex = i * limit; const endIndex = Math.min((i + 1) * limit, allCalls.length); const calls = allCalls.slice(startIndex, endIndex); let results = null; while (!results) { try { results = await ethcallProvider.tryAll(calls, block); } catch (e) { const errorCode = e.code; if (errorCode === 'TIMEOUT') { console.log(`Failed to fetch state, reason: ${errorCode}, retrying`); } else { throw e; } } } for (const result of results) { allResults.push(result); } } return allResults; } function getAccountAssetBalances(account, ethDelta, erc20Delta, erc721Delta, erc1155Delta) { let hasNegativeDelta = false; const actorEthDelta = ethDelta[account]; if (actorEthDelta < 0n) { hasNegativeDelta = true; } const actorErc20Delta = erc20Delta[account]; for (const asset in actorErc20Delta) { const amount = actorErc20Delta[asset]; if (amount < 0n) { hasNegativeDelta = true; } } const actorErc721Delta = erc721Delta[account]; for (const collection in actorErc721Delta) { const actorCollectionDelta = actorErc721Delta[collection]; for (const id in actorCollectionDelta) { const amount = actorCollectionDelta[id]; if (amount < 0n) { hasNegativeDelta = true; } } } const actorErc1155Delta = erc1155Delta[account]; for (const collection in actorErc1155Delta) { const actorCollectionDelta = actorErc1155Delta[collection]; for (const id in actorCollectionDelta) { const amount = actorCollectionDelta[id]; if (amount < 0n) { hasNegativeDelta = true; } } } if (hasNegativeDelta) { return null; } const assets = []; if (actorEthDelta > 0n) { assets.push({ amount: actorEthDelta, }); } for (const asset in actorErc20Delta) { const amount = actorErc20Delta[asset]; if (amount > 0n) { assets.push({ address: asset, amount, }); } } for (const collection in actorErc721Delta) { for (const id in actorErc721Delta[collection]) { const amount = actorErc721Delta[collection][id]; if (amount > 0n) { assets.push({ collection, id: BigInt(id), }); } } } for (const collection in actorErc1155Delta) { for (const id in actorErc1155Delta[collection]) { const amount = actorErc1155Delta[collection][id]; if (amount > 0n) { assets.push({ collection, id: BigInt(id), amount, }); } } } return assets; } function hasOutgoingTransfers(account, ethTransfers, erc20Transfers, erc721Transfers, erc1155Transfers) { for (const transfer of ethTransfers) { if (transfer.from === account) { return true; } } for (const transfer of erc20Transfers) { if (transfer.from === account) { return true; } } for (const transfer of erc721Transfers) { if (transfer.from === account) { return true; } } for (const transfer of erc1155Transfers) { if (transfer.from === account) { return true; } } return false; } export { getAssets, fetchAssetTypes, classify }; //# sourceMappingURL=mev.js.map