diff --git a/src/common/types.ml b/src/common/types.ml index 97614b9c2a8f7816223f9000414b177e0284834f..c4963d21f914c250194fe98a74883399fbab89a4 100644 --- a/src/common/types.ml +++ b/src/common/types.ml @@ -1,6 +1,6 @@ -module Ethereum_indexer = struct - open Eth +open Eth +module Ethereum_indexer = struct type contract_type = | ERC20 [@key "Erc20"] | UniswapV2 [@key "UniswapV2"] @@ -366,6 +366,87 @@ module Bitcoin_indexer = struct [@@deriving encoding { remove_prefix = "cn_" }] end +module Ethereum_analysis = struct + type swap = { + s_amountIn : bz; + s_amountOut : bz; + s_in_token : address; + s_out_token : address; + s_sender : address; + s_recipient : address; + s_contract : address; + } + [@@deriving encoding { remove_prefix = "s_" }] + + type token_swaped = { + ts_in_token : address; + ts_out_token : address; + ts_contract : address; + } + [@@deriving encoding { remove_prefix = "ts_" }] + + type swap_transaction = { + st_transaction_hash : b; + st_index : bint; + st_sender : address; + st_swap_list : swap list; + st_exchange_token_contract : token_swaped list; + } + [@@deriving encoding { remove_prefix = "st_" }] + + type uni_v2_swap = { + uv2s_contract : address; + uv2s_sender : address; + uv2s_recipient : address; + uv2s_amount0In : bz; + uv2s_amount1In : bz; + uv2s_amount0Out : bz; + uv2s_amount1Out : bz; + uv2s_token0 : address; + uv2s_token1 : address; + } + [@@deriving encoding { remove_prefix = "uv2s_" }] + + type uni_v3_swap = { + uv3s_contract : address; + uv3s_sender : Eth.address; + uv3s_recipient : Eth.address; + uv3s_amount0 : bz; + uv3s_amount1 : bz; + uv3s_sqrt_price : bz; + uv3s_liquidity : bz; + uv3s_tick : bz; + uv3s_token0 : address; + uv3s_token1 : address; + } + [@@deriving encoding { remove_prefix = "uv3s_" }] + + (* A sandwich is defined for a unique token_swap. We can have multiple + sandwich with the same victim, front run and back run, but with different + token_swap.*) + type sandwich = { + sd_front_run : swap_transaction; + sd_back_run : swap_transaction; + sd_victims : swap_transaction list; + sd_token_sandwich : token_swaped; + } + [@@deriving encoding { remove_prefix = "sd_" }] + + type token_ratio_swap = { + trs_swap_transaction : swap_transaction; + trs_ratio : (bz * bz) list; + } + [@@deriving encoding { remove_prefix = "trs_" }] + + type sandwich_analysed = { + sa_sandwich : sandwich; + sa_token_ratio_swap_fr : token_ratio_swap; + sa_token_ratio_swap_br : token_ratio_swap; + sa_token_ratio_swap_victim : token_ratio_swap list; + } + [@@deriving encoding { remove_prefix = "sa_" }] +end + type verbose = | Nothing | Low diff --git a/src/ethereum-analysis/dune b/src/ethereum-analysis/dune new file mode 100644 index 0000000000000000000000000000000000000000..88a4e309ee04e019fe0757754d174afe60c08484 --- /dev/null +++ b/src/ethereum-analysis/dune @@ -0,0 +1,4 @@ +(library + (name eth_analysis_sandwich) + (modules eth_analysis_sandwich) + (libraries eth_indexer lwt common)) diff --git a/src/ethereum-analysis/eth_analysis_sandwich.ml b/src/ethereum-analysis/eth_analysis_sandwich.ml new file mode 100644 index 0000000000000000000000000000000000000000..94358e11eb18804929a0db03938a2dcc9c436ebe --- /dev/null +++ b/src/ethereum-analysis/eth_analysis_sandwich.ml @@ -0,0 +1,483 @@ +open Lwt +open Eth +open Common.Types.Ethereum_analysis +open Common.Types.Ethereum_indexer +open Common.Tools + +let eq_inverse_token_swaped ts1 ts2 = + ts1.ts_out_token = ts2.ts_in_token + && ts1.ts_in_token = ts2.ts_out_token + && ts1.ts_contract = ts2.ts_contract + +let eq_token_swaped ts1 ts2 = + ts1.ts_in_token = ts2.ts_in_token + && ts1.ts_out_token = ts2.ts_out_token + && ts1.ts_contract = ts2.ts_contract + +let list_uniq_token_swaped list = + List.fold_left + (fun acc arg -> + match List.find_opt (eq_token_swaped arg) acc with + | None -> arg :: acc + | Some _ -> acc) + [] list + +let get_token_address (log : debug_trace_result_log) = + match log.dtrl_decode_signature with + | None -> failwith "couldn't retreive token address" + | Some arg -> ( + match arg.evdi_decode with + | Event_unknown_abi _ -> failwith "couldn't retreive token address" + | Event_abi event_abi -> ( + let contract_info = event_abi.evda_contract_info in + match contract_info.dci_pool_contracts with + | None -> failwith "couldn't retreive token address" + | Some (contract1, contract2) -> + (contract_info.dci_address, contract1.dci_address, contract2.dci_address) + )) + +let v2_event_id = + b "d78ad95fa46c994b6551d0da85fc275fe613ce37657fb8d5e3d130840159d822" + +let v3_event_id = + b "c42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67" + +let get_address_from_evmvalue (evm_value : Eth.evm_value) = + match evm_value with + | `address a -> a + | `bytes bytes -> ( + match Eth.Evm.decode `address bytes with + | `address a -> a + | e -> + failwith + @@ Format.sprintf "fail get_address_from_evmvalue %s" + @@ EzEncoding.construct Eth.evm_value_enc e) + | e -> + failwith + @@ Format.sprintf "fail get_address_from_evmvalue %s" + @@ EzEncoding.construct Eth.evm_value_enc e + +let get_int_from_evmvalue (evm_value : Eth.evm_value) = + match evm_value with + | `int int -> int + | e -> + failwith + @@ Format.sprintf "fail get_address_from_evmvalue %s" + @@ EzEncoding.construct Eth.evm_value_enc e + +let general_swapV2 (swap : uni_v2_swap) = + let s_amountIn, s_in_token = + match Z.compare swap.uv2s_amount0In swap.uv2s_amount1In with + | -1 -> (swap.uv2s_amount1In, swap.uv2s_token1) + | 1 -> (swap.uv2s_amount0In, swap.uv2s_token0) + | _ -> assert false in + let s_amountOut, s_out_token = + match Z.compare swap.uv2s_amount0Out swap.uv2s_amount1Out with + | -1 -> (swap.uv2s_amount1Out, swap.uv2s_token1) + | 1 -> (swap.uv2s_amount0Out, swap.uv2s_token0) + | _ -> assert false in + let s_sender, s_recipient, s_contract = + (swap.uv2s_sender, swap.uv2s_recipient, swap.uv2s_contract) in + { + s_amountIn; + s_amountOut; + s_in_token; + s_out_token; + s_sender; + s_recipient; + s_contract; + } + +let decode_abi_v2 (log : debug_trace_result_log) (abi : event_decode_abi) = + try + let contract = abi.evda_contract_info in + match contract.dci_type with + | None -> None + | Some contract_type -> ( + match contract_type with + | ERC20 | UniswapV3 -> None + | UniswapV2 -> ( + match log.dtrl_decode_signature with + | None -> None + | Some decode_signature -> ( + match + String.equal + (decode_signature.evdi_hash :> string) + (v2_event_id :> string) + with + | false -> None + | true -> + let inputs = + List.sort + (fun arg1 arg2 -> compare arg1.pda_index arg2.pda_index) + abi.evda_inputs.evdai_inputs in + let uv2s_contract, uv2s_token0, uv2s_token1 = + get_token_address log in + let uv2s_sender = + let tmp = List.nth inputs 0 in + get_address_from_evmvalue tmp.pda_data in + let uv2s_amount0In = + let tmp = List.nth inputs 1 in + get_int_from_evmvalue tmp.pda_data in + let uv2s_amount1In = + let tmp = List.nth inputs 2 in + get_int_from_evmvalue tmp.pda_data in + let uv2s_amount0Out = + let tmp = List.nth inputs 3 in + get_int_from_evmvalue tmp.pda_data in + let uv2s_amount1Out = + let tmp = List.nth inputs 4 in + get_int_from_evmvalue tmp.pda_data in + let uv2s_recipient = + let tmp = List.nth inputs 5 in + get_address_from_evmvalue tmp.pda_data in + Some + { + uv2s_contract; + uv2s_sender; + uv2s_recipient; + uv2s_amount0In; + uv2s_amount1In; + uv2s_amount0Out; + uv2s_amount1Out; + uv2s_token0; + uv2s_token1; + }))) + with _ -> None + +let decode_abi_v3 (log : debug_trace_result_log) (abi : event_decode_abi) = + try + let contract = abi.evda_contract_info in + match contract.dci_type with + | None -> None + | Some contract_type -> ( + match contract_type with + | ERC20 | UniswapV2 -> None + | UniswapV3 -> ( + match log.dtrl_decode_signature with + | None -> None + | Some decode_signature -> ( + match + String.equal + (decode_signature.evdi_hash :> string) + (v3_event_id :> string) + with + | false -> None + | true -> + let inputs = + List.sort + (fun arg1 arg2 -> compare arg1.pda_index arg2.pda_index) + abi.evda_inputs.evdai_inputs in + + let uv3s_contract, uv3s_token0, uv3s_token1 = + get_token_address log in + let uv3s_sender = + let tmp = List.nth inputs 0 in + get_address_from_evmvalue tmp.pda_data in + let uv3s_recipient = + let tmp = List.nth inputs 1 in + get_address_from_evmvalue tmp.pda_data in + let uv3s_amount0 = + let tmp = List.nth inputs 2 in + get_int_from_evmvalue tmp.pda_data in + let uv3s_amount1 = + let tmp = List.nth inputs 3 in + get_int_from_evmvalue tmp.pda_data in + let uv3s_sqrt_price = + let tmp = List.nth inputs 4 in + get_int_from_evmvalue tmp.pda_data in + let uv3s_liquidity = + let tmp = List.nth inputs 5 in + get_int_from_evmvalue tmp.pda_data in + let uv3s_tick = + let tmp = List.nth inputs 6 in + get_int_from_evmvalue tmp.pda_data in + + Some + { + uv3s_contract; + uv3s_sender; + uv3s_recipient; + uv3s_amount0; + uv3s_amount1; + uv3s_sqrt_price; + uv3s_liquidity; + uv3s_tick; + uv3s_token0; + uv3s_token1; + }))) + with _ -> None + +let get_v2_swap (log : debug_trace_result_log) = + match log.dtrl_decode_signature with + | None -> Lwt.return @@ None + | Some s -> ( + let event_decode = s.evdi_decode in + match String.equal (s.evdi_hash :> string) (v2_event_id :> string) with + | false -> Lwt.return @@ None + | true -> ( + match event_decode with + | Event_abi abi -> Lwt.return @@ decode_abi_v2 log abi + | Event_unknown_abi _ -> Lwt.return_none)) + +let get_v3_swap (log : debug_trace_result_log) = + match log.dtrl_decode_signature with + | None -> Lwt.return @@ None + | Some s -> ( + let event_decode = s.evdi_decode in + match String.equal (s.evdi_hash :> string) (v3_event_id :> string) with + | false -> Lwt.return @@ None + | true -> ( + match event_decode with + | Event_abi abi -> Lwt.return @@ decode_abi_v3 log abi + | Event_unknown_abi _ -> Lwt.return_none)) + +let get_v2_swaps tx_logs = Lwt_list.filter_map_s get_v2_swap tx_logs + +let get_v3_swaps tx_logs = Lwt_list.filter_map_s get_v3_swap tx_logs + +let general_swapV3 (swap : uni_v3_swap) = + try + let s_amountIn, s_amountOut, s_in_token, s_out_token = + match Z.gt swap.uv3s_amount0 Z.zero with + | true -> + let b1 = not @@ Z.equal swap.uv3s_amount1 Z.zero in + let b2 = Z.gt Z.zero swap.uv3s_amount1 in + if b1 && b2 then + ( swap.uv3s_amount0, + Z.neg swap.uv3s_amount1, + swap.uv3s_token0, + swap.uv3s_token1 ) + else + failwith "assert failed general_swapV3" + | false -> + let b1 = not @@ Z.equal swap.uv3s_amount0 Z.zero in + let b2 = Z.gt swap.uv3s_amount1 Z.zero in + if b1 && b2 then + ( swap.uv3s_amount1, + Z.neg swap.uv3s_amount0, + swap.uv3s_token1, + swap.uv3s_token0 ) + else + failwith "assert failed general_swapV3" in + let s_sender, s_recipient, s_contract = + (swap.uv3s_sender, swap.uv3s_recipient, swap.uv3s_contract) in + Some + { + s_amountIn; + s_amountOut; + s_in_token; + s_out_token; + s_sender; + s_recipient; + s_contract; + } + with _ -> None + +let find_swaps (transaction_trace : debug_trace_result) = + let rec get_pair_swaps_v2_v3 (log : debug_trace_result) (swapv2, swapv3) : + ('a * 'b) t = + let> swapv2', swapv3' = + match log.dtr_logs with + | None -> Lwt.return (swapv2, swapv3) + | Some dtr_logs -> + let> v2 = get_v2_swaps dtr_logs in + let> v3 = get_v3_swaps dtr_logs in + Lwt.return (concat_list swapv2 v2, concat_list swapv3 v3) in + match log.dtr_calls with + | None -> Lwt.return (swapv2', swapv3') + | Some dtr_calls -> + let> sv2, sv3 = + Lwt_list.fold_left_s + (fun (acc_s2, acc_s3) arg -> + let> s2, s3 = get_pair_swaps_v2_v3 arg (acc_s2, acc_s3) in + Lwt.return (s2, s3)) + ([], []) dtr_calls in + Lwt.return (concat_list sv2 swapv2', concat_list sv3 swapv3') in + get_pair_swaps_v2_v3 transaction_trace ([], []) + +let gen_swap_transaction (st_sender : address) (st_transaction_hash : b) + (st_index : bint) (st_swap_list : swap list) = + let st_exchange_token_contract = + list_uniq_token_swaped + @@ List.fold_left + (fun acc (arg : swap) -> + let ts_in_token, ts_out_token, ts_contract = + (arg.s_in_token, arg.s_out_token, arg.s_contract) in + { ts_in_token; ts_out_token; ts_contract } :: acc) + [] st_swap_list in + { + st_transaction_hash; + st_index; + st_sender; + st_swap_list; + st_exchange_token_contract; + } + +let detect_fr_br (swap_transaction_list : swap_transaction list) = + let rec inter (swap_transaction_list : swap_transaction list) acc_fr_br = + match swap_transaction_list with + | [] | _ :: [] -> acc_fr_br + | swap :: tl_swaps -> ( + let sender = swap.st_sender in + let exchange_token_contract = swap.st_exchange_token_contract in + let possible_br = + List.find_all (fun arg -> arg.st_sender = sender) tl_swaps in + let br : (swap_transaction * token_swaped list) list = + List.fold_left + (fun acc arg -> + let br_exchange_token_contract = arg.st_exchange_token_contract in + let br_token_swap = + (* I only keep the token_swaped from the Front Run *) + List.fold_left + (fun acc_token_swaped arg_token_swaped -> + match + List.find_opt + (eq_inverse_token_swaped arg_token_swaped) + br_exchange_token_contract + with + | None -> acc_token_swaped + | Some _ -> arg_token_swaped :: acc_token_swaped) + [] exchange_token_contract in + match br_token_swap with + | [] -> acc + | br_token_swap -> (arg, br_token_swap) :: acc) + [] possible_br in + match br with + | [] -> inter tl_swaps acc_fr_br + | br -> inter tl_swaps @@ ((swap, br) :: acc_fr_br)) in + let swap_transaction_list = + List.sort + (fun swap1 swap2 -> + if swap1.st_index > swap2.st_index then + 1 + else if swap1.st_index = swap2.st_index then + 0 + else + -1) + swap_transaction_list in + inter swap_transaction_list [] + +let find_victim + (fr_br : swap_transaction * (swap_transaction * token_swaped list)) + (swap_list : swap_transaction list) = + let fr, (br, token_swaped_list) = fr_br in + let deb_index, fin_index = (fr.st_index, br.st_index) in + let in_index_swap_list = + List.filter + (fun arg -> arg.st_index > deb_index && arg.st_index < fin_index) + swap_list in + match in_index_swap_list with + | [] -> [] + | in_index_swap_list -> + let victim_by_token = + List.fold_left + (fun acc arg -> + (* We're looking for swap_transactions where for a token_exchange of a + front run, it exist in his token_exchange list. Knowing that each + token_swap of swap_transaction should be unique in the list) *) + let victims = + List.find_all + (fun in_index_swap -> + List.exists (eq_token_swaped arg) + in_index_swap.st_exchange_token_contract) + in_index_swap_list in + match victims with + | [] -> acc + | sd_victims -> + let sandwich = + { + sd_front_run = fr; + sd_back_run = br; + sd_victims; + sd_token_sandwich = arg; + } in + sandwich :: acc) + [] token_swaped_list in + victim_by_token + +(* Each Fr can be used for multiple Br with different token_swaped *) +let general_find_victim + (fr_br_list : + (swap_transaction * (swap_transaction * token_swaped list) list) list) + (swap_list : swap_transaction list) = + List.fold_left + (fun acc arg -> + let fr, br_token_swap_list = arg in + let sandwichs = + List.fold_left + (fun acc_sandwich br_token_swap -> + match find_victim (fr, br_token_swap) swap_list with + | [] -> acc_sandwich + | sandwichs -> concat_list sandwichs acc_sandwich) + [] br_token_swap_list in + match sandwichs with + | [] -> acc + | sandwichs -> concat_list sandwichs acc) + [] fr_br_list + +let analyse_sandwich (sa_sandwich : sandwich) = + let get_ratio_fr_victims (token_sandwich : token_swaped) + (trs_swap_transaction : swap_transaction) = + let swaps = + List.filter + (fun (swap : swap) -> + swap.s_in_token = token_sandwich.ts_in_token + && swap.s_out_token = token_sandwich.ts_out_token + && swap.s_contract = token_sandwich.ts_contract) + trs_swap_transaction.st_swap_list in + let trs_ratio = + List.map (fun swap -> (swap.s_amountIn, swap.s_amountOut)) swaps in + { trs_swap_transaction; trs_ratio } in + let get_ratio_br (token_sandwich : token_swaped) + (trs_swap_transaction : swap_transaction) = + let swaps = + List.filter + (fun (swap : swap) -> + swap.s_in_token = token_sandwich.ts_out_token + && swap.s_out_token = token_sandwich.ts_in_token + && swap.s_contract = token_sandwich.ts_contract) + trs_swap_transaction.st_swap_list in + let trs_ratio = + List.map (fun swap -> (swap.s_amountIn, swap.s_amountOut)) swaps in + { trs_ratio; trs_swap_transaction } in + let token = sa_sandwich.sd_token_sandwich in + let sa_token_ratio_swap_fr = + get_ratio_fr_victims token sa_sandwich.sd_front_run in + let sa_token_ratio_swap_br = get_ratio_br token sa_sandwich.sd_back_run in + let sa_token_ratio_swap_victim = + List.map (get_ratio_fr_victims token) sa_sandwich.sd_victims in + { + sa_sandwich; + sa_token_ratio_swap_fr; + sa_token_ratio_swap_br; + sa_token_ratio_swap_victim; + } + +let analyse_sandwich_block (sandwich_list : sandwich list) = + List.map analyse_sandwich sandwich_list + +let detect_sandwich debug_trace_list = + let> swap_transaction_list = + Lwt_list.fold_left_s + (fun acc arg -> + let transaction_hash = arg.dt_tx_hash in + let sender = arg.dt_result.dtr_from in + let index = arg.dt_tx_index in + let> swapV2, swapV3 = find_swaps arg.dt_result in + let swap_list = + let v2 = List.map general_swapV2 swapV2 in + let v3 = List.filter_map general_swapV3 swapV3 in + concat_list v2 v3 in + match swap_list with + | [] -> Lwt.return acc + | swap_list -> + Lwt.return + (gen_swap_transaction sender transaction_hash index swap_list :: acc)) + [] debug_trace_list in + let detect_fr_br = detect_fr_br swap_transaction_list in + Lwt.return + @@ + match detect_fr_br with + | [] -> [] + | detect_fr_br -> general_find_victim detect_fr_br swap_transaction_list diff --git a/src/ethereum-analysis/eth_analysis_sandwich.mli b/src/ethereum-analysis/eth_analysis_sandwich.mli new file mode 100644 index 0000000000000000000000000000000000000000..0b66cb0a18ad012309a76f3b2c42642f5131a396 --- /dev/null +++ b/src/ethereum-analysis/eth_analysis_sandwich.mli @@ -0,0 +1,7 @@ +val analyse_sandwich_block : + Common.Types.Ethereum_analysis.sandwich list -> + Common.Types.Ethereum_analysis.sandwich_analysed list + +val detect_sandwich : + Common.Types.Ethereum_indexer.debug_trace list -> + Common.Types.Ethereum_analysis.sandwich list Lwt.t