[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"article-deep-evm-14-chaigeogeorae-saikeul-chatgi-pul-geuraepeu-dfs":3},{"article":4,"author":54},{"id":5,"category_id":6,"title":7,"slug":8,"excerpt":9,"content_md":10,"content_html":11,"locale":12,"author_id":13,"published":14,"published_at":15,"meta_title":7,"meta_description":16,"focus_keyword":17,"og_image":18,"canonical_url":18,"robots_meta":19,"created_at":15,"updated_at":15,"tags":20,"category_name":34,"related_articles":35},"d5000000-0000-0000-0000-000000000114","a0000000-0000-0000-0000-000000000056","Deep EVM #14: 차익거래 사이클 찾기 — 풀 그래프에서의 DFS","deep-evm-14-chaigeogeorae-saikeul-chatgi-pul-geuraepeu-dfs","Rust에서 토큰-풀 그래프에 대한 깊이 우선 검색을 사용하여 프로덕션 차익거래 사이클 찾기를 구축합니다. 수천 개의 풀, 수백만 개의 사이클을 처리하고 keccak256으로 중복을 제거합니다.","## 토큰-풀 그래프\n\n차익거래 사이클은 동일한 토큰으로 시작하고 끝나는 스왑 시퀀스로, 입력보다 더 많은 출력을 생산합니다. 이러한 사이클을 체계적으로 찾기 위해 DeFi 생태계를 방향 그래프로 모델링합니다:\n\n- **노드** = 토큰 (WETH, USDC, DAI, WBTC, ...)\n- **엣지** = 두 토큰 간 스왑을 가능하게 하는 유동성 풀\n\nUniswap V2 페어(WETH\u002FUSDC)는 두 개의 방향 엣지를 생성합니다: WETH→USDC와 USDC→WETH. Uniswap V3 풀도 동일하지만 다른 가격 책정(집중 유동성)을 사용합니다. 3개의 토큰이 있는 Balancer 가중 풀은 6개의 방향 엣지를 생성합니다(모든 쌍의 양방향).\n\n```rust\nuse std::collections::HashMap;\n\n#[derive(Clone, Debug)]\npub struct Pool {\n    pub address: Address,\n    pub token0: Address,\n    pub token1: Address,\n    pub protocol: Protocol,\n    pub fee: u32,        \u002F\u002F basis points\n    pub reserve0: U256,\n    pub reserve1: U256,\n}\n\n#[derive(Clone, Debug)]\npub enum Protocol {\n    UniswapV2,\n    UniswapV3,\n    SushiSwap,\n    Curve,\n    Balancer,\n}\n\npub struct TokenGraph {\n    \u002F\u002F token_address → vec of (other_token, pool)\n    pub adjacency: HashMap\u003CAddress, Vec\u003C(Address, Pool)>>,\n}\n\nimpl TokenGraph {\n    pub fn new() -> Self {\n        Self {\n            adjacency: HashMap::new(),\n        }\n    }\n\n    pub fn add_pool(&mut self, pool: Pool) {\n        self.adjacency\n            .entry(pool.token0)\n            .or_default()\n            .push((pool.token1, pool.clone()));\n        self.adjacency\n            .entry(pool.token1)\n            .or_default()\n            .push((pool.token0, pool));\n    }\n}\n```\n\n이더리움 메인넷에는 약 80,000개의 활성 유동성 풀이 있습니다. 그래프에는 약 15,000개의 고유 토큰과 약 160,000개의 방향 엣지가 있습니다.\n\n## DFS 사이클 감지\n\n\"기본 토큰\"(일반적으로 WETH, ETH 단위로 이익을 원하므로)에서 시작하는 깊이 우선 검색을 사용하여 차익거래 사이클을 찾습니다.\n\n```rust\n#[derive(Clone, Debug)]\npub struct Cycle {\n    pub pools: Vec\u003CPool>,\n    pub tokens: Vec\u003CAddress>,  \u002F\u002F 토큰 경로: [WETH, USDC, DAI, WETH]\n    pub id: [u8; 32],         \u002F\u002F 중복 제거를 위한 keccak256 해시\n}\n\npub fn find_cycles(\n    graph: &TokenGraph,\n    start_token: Address,\n    max_depth: usize,   \u002F\u002F 일반적으로 2-4 홉\n) -> Vec\u003CCycle> {\n    let mut results = Vec::new();\n    let mut path_tokens = vec![start_token];\n    let mut path_pools: Vec\u003CPool> = Vec::new();\n    let mut visited = HashSet::new();\n    visited.insert(start_token);\n\n    dfs(\n        graph,\n        start_token,\n        start_token,\n        max_depth,\n        &mut path_tokens,\n        &mut path_pools,\n        &mut visited,\n        &mut results,\n    );\n\n    results\n}\n\nfn dfs(\n    graph: &TokenGraph,\n    current: Address,\n    start: Address,\n    max_depth: usize,\n    path_tokens: &mut Vec\u003CAddress>,\n    path_pools: &mut Vec\u003CPool>,\n    visited: &mut HashSet\u003CAddress>,\n    results: &mut Vec\u003CCycle>,\n) {\n    if path_pools.len() >= max_depth {\n        return;\n    }\n\n    let neighbors = match graph.adjacency.get(&current) {\n        Some(n) => n,\n        None => return,\n    };\n\n    for (next_token, pool) in neighbors {\n        \u002F\u002F 시작점으로 2홉 이상 돌아왔으면 사이클을 찾은 것\n        if *next_token == start && path_pools.len() >= 2 {\n            let mut cycle_tokens = path_tokens.clone();\n            cycle_tokens.push(start);\n            let mut cycle_pools = path_pools.clone();\n            cycle_pools.push(pool.clone());\n\n            let id = compute_cycle_id(&cycle_pools);\n            results.push(Cycle {\n                pools: cycle_pools,\n                tokens: cycle_tokens,\n                id,\n            });\n            continue;\n        }\n\n        \u002F\u002F 토큰을 재방문하지 않음 (시작점 제외)\n        if visited.contains(next_token) {\n            continue;\n        }\n\n        visited.insert(*next_token);\n        path_tokens.push(*next_token);\n        path_pools.push(pool.clone());\n\n        dfs(\n            graph,\n            *next_token,\n            start,\n            max_depth,\n            path_tokens,\n            path_pools,\n            visited,\n            results,\n        );\n\n        path_tokens.pop();\n        path_pools.pop();\n        visited.remove(next_token);\n    }\n}\n```\n\n## `max_depth` 매개변수\n\n깊이 매개변수는 사이클 길이(홉 수)를 제어합니다:\n\n- **depth=2:** 직접 차익거래. WETH→USDC→WETH (두 풀). 단순하고 경쟁적.\n- **depth=3:** 삼각 차익거래. WETH→USDC→DAI→WETH. 최적 지점 — 경쟁 서처가 적을 만큼 복잡하지만 빠르게 시뮬레이션할 만큼 단순.\n- **depth=4:** 사각 차익거래. 더 높은 잠재 이익이지만 기하급수적으로 더 많은 사이클을 평가해야 하고 가스 비용이 높음.\n- **depth=5+:** 가스 비용 후 거의 수익성이 없음. 검색 공간이 조합적으로 폭발.\n\n```\n풀: 80,000\n깊이 2 사이클: ~50,000\n깊이 3 사이클: ~15,000,000\n깊이 4 사이클: ~2,000,000,000+\n```\n\n실무에서 깊이 3이 프로덕션 최적 지점입니다. 약 1,500만 개의 삼각 사이클을 찾고 시뮬레이션할 가치가 있는 약 100,000개로 필터링합니다.\n\n## Keccak256으로 중복 제거\n\nDFS는 중복 사이클을 생성합니다: WETH→USDC→DAI→WETH와 WETH→DAI→USDC→WETH는 반대 방향으로 순회한 같은 사이클입니다. 정규 해시를 사용하여 중복을 제거합니다:\n\n```rust\nuse tiny_keccak::{Hasher, Keccak};\n\nfn compute_cycle_id(pools: &[Pool]) -> [u8; 32] {\n    \u002F\u002F 정규 표현을 위해 풀 주소를 정렬\n    let mut addresses: Vec\u003CAddress> = pools.iter().map(|p| p.address).collect();\n    addresses.sort();\n\n    let mut hasher = Keccak::v256();\n    for addr in &addresses {\n        hasher.update(addr.as_bytes());\n    }\n    let mut output = [0u8; 32];\n    hasher.finalize(&mut output);\n    output\n}\n```\n\n해싱 전에 풀 주소를 정렬하면 같은 사이클의 양 방향이 동일한 ID를 생성합니다. `HashSet\u003C[u8; 32]>`에 삽입하고 중복을 건너뜁니다.\n\n왜 더 간단한 해시 대신 keccak256인가? 1,500만 사이클 집합에서 128비트 해시의 충돌 확률은 무시할 수 없습니다. 256비트에서는 충돌이 천문학적으로 불가능합니다.\n\n## 필터링: 수백만을 수천으로 줄이기\n\n모든 사이클이 시뮬레이션할 가치가 있는 것은 아닙니다. 필터를 적용합니다:\n\n### 유동성 필터\n\n준비금이 임계값 이하인 풀이 포함된 사이클을 폐기합니다. 유동성이 100달러인 풀은 의미 있는 차익거래를 생산할 수 없습니다.\n\n```rust\nfn has_sufficient_liquidity(cycle: &Cycle, min_reserve_usd: f64) -> bool {\n    cycle.pools.iter().all(|pool| {\n        let reserve_usd = estimate_reserve_usd(pool);\n        reserve_usd >= min_reserve_usd\n    })\n}\n```\n\n### 부실 필터\n\nN 블록 이상 유휴(스왑 없음) 상태인 모든 풀이 포함된 사이클을 폐기합니다. 부실 풀에는 차익거래가 거의 없습니다.\n\n### 토큰 블랙리스트\n\n스캠 토큰, 전송 수수료 토큰, 전송 제한이 있는 토큰을 제외합니다. 시뮬레이션에서는 수익성이 있어 보이지만 온체인에서 실패합니다.\n\n```rust\nconst BLACKLISTED_TOKENS: &[&str] = &[\n    \"0x...\",  \u002F\u002F 알려진 전송 수수료 토큰\n    \"0x...\",  \u002F\u002F 알려진 허니팟\n];\n\nfn contains_blacklisted_token(cycle: &Cycle) -> bool {\n    cycle.tokens.iter().any(|t| {\n        BLACKLISTED_TOKENS.contains(&format!(\"{:?}\", t).as_str())\n    })\n}\n```\n\n### 정적 수익성 추정\n\n완전한 시뮬레이션 전에, 상수곱 공식을 사용하여 대략적인 수익성 추정치를 계산합니다:\n\n```rust\nfn estimate_output_v2(\n    amount_in: U256,\n    reserve_in: U256,\n    reserve_out: U256,\n    fee_bps: u32,\n) -> U256 {\n    let fee_factor = 10000 - fee_bps;  \u002F\u002F 예: 0.3% 수수료에 9970\n    let numerator = amount_in * reserve_out * U256::from(fee_factor);\n    let denominator = reserve_in * U256::from(10000) + amount_in * U256::from(fee_factor);\n    numerator \u002F denominator\n}\n\nfn estimate_cycle_profit(\n    cycle: &Cycle,\n    input_amount: U256,\n) -> Option\u003CU256> {\n    let mut current_amount = input_amount;\n\n    for (i, pool) in cycle.pools.iter().enumerate() {\n        let token_in = cycle.tokens[i];\n        let (reserve_in, reserve_out) = if token_in == pool.token0 {\n            (pool.reserve0, pool.reserve1)\n        } else {\n            (pool.reserve1, pool.reserve0)\n        };\n\n        current_amount = estimate_output_v2(\n            current_amount,\n            reserve_in,\n            reserve_out,\n            pool.fee,\n        );\n    }\n\n    if current_amount > input_amount {\n        Some(current_amount - input_amount)\n    } else {\n        None\n    }\n}\n```\n\n이 추정치는 불완전하지만(Uniswap V3 틱 수학, Curve 스테이블스왑 곡선 등을 무시) 비용이 많이 드는 시뮬레이션 전에 95%의 비수익 사이클을 제거합니다.\n\n## 확장: 병렬화와 점진적 업데이트\n\n80,000개 풀에서 전체 그래프를 구축하고 DFS를 실행하는 데 단일 코어에서 5-10초가 걸립니다. 프로덕션 봇의 경우 시작 비용으로 괜찮습니다. 그러나 풀 상태는 매 블록(12초)마다 변경되므로 점진적 업데이트가 필요합니다:\n\n```rust\npub struct CycleIndex {\n    pub cycles_by_pool: HashMap\u003CAddress, Vec\u003CCycleId>>,\n    pub all_cycles: HashMap\u003CCycleId, Cycle>,\n}\n\nimpl CycleIndex {\n    \u002F\u002F\u002F 풀의 준비금이 변경되면, 이 풀을 포함하는\n    \u002F\u002F\u002F 사이클만 무효화하고 재평가\n    pub fn on_pool_update(&self, pool_address: Address) -> Vec\u003C&Cycle> {\n        self.cycles_by_pool\n            .get(&pool_address)\n            .map(|ids| {\n                ids.iter()\n                    .filter_map(|id| self.all_cycles.get(id))\n                    .collect()\n            })\n            .unwrap_or_default()\n    }\n}\n```\n\nUniswap V2 풀에서 `Sync` 이벤트가 발생하면 해당 풀의 준비금을 업데이트하고 해당 풀을 포함하는 사이클만 재평가합니다. 이렇게 하면 블록당 작업이 수백만 사이클에서 수백 개로 줄어듭니다.\n\nDFS 자체에 대해서는 Rayon을 사용하여 기본 토큰 간에 병렬화합니다:\n\n```rust\nuse rayon::prelude::*;\n\nlet base_tokens = vec![weth, usdc, usdt, dai, wbtc];\n\nlet all_cycles: Vec\u003CCycle> = base_tokens\n    .par_iter()\n    .flat_map(|base| find_cycles(&graph, *base, 3))\n    .collect();\n```\n\n## 요약\n\n사이클 찾기는 MEV 파이프라인의 첫 번째 단계입니다. 유동성 풀의 평면 목록을 구조화된 차익거래 기회 집합으로 변환합니다. 핵심 결정 — 그래프 표현, DFS 깊이, 중복 제거 전략, 필터링 휴리스틱 — 이 기회 집합의 품질과 범위를 결정합니다. 다음 기사에서는 이러한 사이클을 시뮬레이션합니다: 차입 금액에 대한 이진 검색, 상태 포크, 12초 블록 마감 시간과의 경쟁.","\u003Ch2 id=\"\">토큰-풀 그래프\u003C\u002Fh2>\n\u003Cp>차익거래 사이클은 동일한 토큰으로 시작하고 끝나는 스왑 시퀀스로, 입력보다 더 많은 출력을 생산합니다. 이러한 사이클을 체계적으로 찾기 위해 DeFi 생태계를 방향 그래프로 모델링합니다:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Cstrong>노드\u003C\u002Fstrong> = 토큰 (WETH, USDC, DAI, WBTC, …)\u003C\u002Fli>\n\u003Cli>\u003Cstrong>엣지\u003C\u002Fstrong> = 두 토큰 간 스왑을 가능하게 하는 유동성 풀\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>Uniswap V2 페어(WETH\u002FUSDC)는 두 개의 방향 엣지를 생성합니다: WETH→USDC와 USDC→WETH. Uniswap V3 풀도 동일하지만 다른 가격 책정(집중 유동성)을 사용합니다. 3개의 토큰이 있는 Balancer 가중 풀은 6개의 방향 엣지를 생성합니다(모든 쌍의 양방향).\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">use std::collections::HashMap;\n\n#[derive(Clone, Debug)]\npub struct Pool {\n    pub address: Address,\n    pub token0: Address,\n    pub token1: Address,\n    pub protocol: Protocol,\n    pub fee: u32,        \u002F\u002F basis points\n    pub reserve0: U256,\n    pub reserve1: U256,\n}\n\n#[derive(Clone, Debug)]\npub enum Protocol {\n    UniswapV2,\n    UniswapV3,\n    SushiSwap,\n    Curve,\n    Balancer,\n}\n\npub struct TokenGraph {\n    \u002F\u002F token_address → vec of (other_token, pool)\n    pub adjacency: HashMap&lt;Address, Vec&lt;(Address, Pool)&gt;&gt;,\n}\n\nimpl TokenGraph {\n    pub fn new() -&gt; Self {\n        Self {\n            adjacency: HashMap::new(),\n        }\n    }\n\n    pub fn add_pool(&amp;mut self, pool: Pool) {\n        self.adjacency\n            .entry(pool.token0)\n            .or_default()\n            .push((pool.token1, pool.clone()));\n        self.adjacency\n            .entry(pool.token1)\n            .or_default()\n            .push((pool.token0, pool));\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>이더리움 메인넷에는 약 80,000개의 활성 유동성 풀이 있습니다. 그래프에는 약 15,000개의 고유 토큰과 약 160,000개의 방향 엣지가 있습니다.\u003C\u002Fp>\n\u003Ch2 id=\"dfs\">DFS 사이클 감지\u003C\u002Fh2>\n\u003Cp>“기본 토큰”(일반적으로 WETH, ETH 단위로 이익을 원하므로)에서 시작하는 깊이 우선 검색을 사용하여 차익거래 사이클을 찾습니다.\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">#[derive(Clone, Debug)]\npub struct Cycle {\n    pub pools: Vec&lt;Pool&gt;,\n    pub tokens: Vec&lt;Address&gt;,  \u002F\u002F 토큰 경로: [WETH, USDC, DAI, WETH]\n    pub id: [u8; 32],         \u002F\u002F 중복 제거를 위한 keccak256 해시\n}\n\npub fn find_cycles(\n    graph: &amp;TokenGraph,\n    start_token: Address,\n    max_depth: usize,   \u002F\u002F 일반적으로 2-4 홉\n) -&gt; Vec&lt;Cycle&gt; {\n    let mut results = Vec::new();\n    let mut path_tokens = vec![start_token];\n    let mut path_pools: Vec&lt;Pool&gt; = Vec::new();\n    let mut visited = HashSet::new();\n    visited.insert(start_token);\n\n    dfs(\n        graph,\n        start_token,\n        start_token,\n        max_depth,\n        &amp;mut path_tokens,\n        &amp;mut path_pools,\n        &amp;mut visited,\n        &amp;mut results,\n    );\n\n    results\n}\n\nfn dfs(\n    graph: &amp;TokenGraph,\n    current: Address,\n    start: Address,\n    max_depth: usize,\n    path_tokens: &amp;mut Vec&lt;Address&gt;,\n    path_pools: &amp;mut Vec&lt;Pool&gt;,\n    visited: &amp;mut HashSet&lt;Address&gt;,\n    results: &amp;mut Vec&lt;Cycle&gt;,\n) {\n    if path_pools.len() &gt;= max_depth {\n        return;\n    }\n\n    let neighbors = match graph.adjacency.get(&amp;current) {\n        Some(n) =&gt; n,\n        None =&gt; return,\n    };\n\n    for (next_token, pool) in neighbors {\n        \u002F\u002F 시작점으로 2홉 이상 돌아왔으면 사이클을 찾은 것\n        if *next_token == start &amp;&amp; path_pools.len() &gt;= 2 {\n            let mut cycle_tokens = path_tokens.clone();\n            cycle_tokens.push(start);\n            let mut cycle_pools = path_pools.clone();\n            cycle_pools.push(pool.clone());\n\n            let id = compute_cycle_id(&amp;cycle_pools);\n            results.push(Cycle {\n                pools: cycle_pools,\n                tokens: cycle_tokens,\n                id,\n            });\n            continue;\n        }\n\n        \u002F\u002F 토큰을 재방문하지 않음 (시작점 제외)\n        if visited.contains(next_token) {\n            continue;\n        }\n\n        visited.insert(*next_token);\n        path_tokens.push(*next_token);\n        path_pools.push(pool.clone());\n\n        dfs(\n            graph,\n            *next_token,\n            start,\n            max_depth,\n            path_tokens,\n            path_pools,\n            visited,\n            results,\n        );\n\n        path_tokens.pop();\n        path_pools.pop();\n        visited.remove(next_token);\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"max-depth\">\u003Ccode>max_depth\u003C\u002Fcode> 매개변수\u003C\u002Fh2>\n\u003Cp>깊이 매개변수는 사이클 길이(홉 수)를 제어합니다:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Cstrong>depth=2:\u003C\u002Fstrong> 직접 차익거래. WETH→USDC→WETH (두 풀). 단순하고 경쟁적.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>depth=3:\u003C\u002Fstrong> 삼각 차익거래. WETH→USDC→DAI→WETH. 최적 지점 — 경쟁 서처가 적을 만큼 복잡하지만 빠르게 시뮬레이션할 만큼 단순.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>depth=4:\u003C\u002Fstrong> 사각 차익거래. 더 높은 잠재 이익이지만 기하급수적으로 더 많은 사이클을 평가해야 하고 가스 비용이 높음.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>depth=5+:\u003C\u002Fstrong> 가스 비용 후 거의 수익성이 없음. 검색 공간이 조합적으로 폭발.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cpre>\u003Ccode>풀: 80,000\n깊이 2 사이클: ~50,000\n깊이 3 사이클: ~15,000,000\n깊이 4 사이클: ~2,000,000,000+\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>실무에서 깊이 3이 프로덕션 최적 지점입니다. 약 1,500만 개의 삼각 사이클을 찾고 시뮬레이션할 가치가 있는 약 100,000개로 필터링합니다.\u003C\u002Fp>\n\u003Ch2 id=\"keccak256\">Keccak256으로 중복 제거\u003C\u002Fh2>\n\u003Cp>DFS는 중복 사이클을 생성합니다: WETH→USDC→DAI→WETH와 WETH→DAI→USDC→WETH는 반대 방향으로 순회한 같은 사이클입니다. 정규 해시를 사용하여 중복을 제거합니다:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">use tiny_keccak::{Hasher, Keccak};\n\nfn compute_cycle_id(pools: &amp;[Pool]) -&gt; [u8; 32] {\n    \u002F\u002F 정규 표현을 위해 풀 주소를 정렬\n    let mut addresses: Vec&lt;Address&gt; = pools.iter().map(|p| p.address).collect();\n    addresses.sort();\n\n    let mut hasher = Keccak::v256();\n    for addr in &amp;addresses {\n        hasher.update(addr.as_bytes());\n    }\n    let mut output = [0u8; 32];\n    hasher.finalize(&amp;mut output);\n    output\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>해싱 전에 풀 주소를 정렬하면 같은 사이클의 양 방향이 동일한 ID를 생성합니다. \u003Ccode>HashSet&lt;[u8; 32]&gt;\u003C\u002Fcode>에 삽입하고 중복을 건너뜁니다.\u003C\u002Fp>\n\u003Cp>왜 더 간단한 해시 대신 keccak256인가? 1,500만 사이클 집합에서 128비트 해시의 충돌 확률은 무시할 수 없습니다. 256비트에서는 충돌이 천문학적으로 불가능합니다.\u003C\u002Fp>\n\u003Ch2 id=\"\">필터링: 수백만을 수천으로 줄이기\u003C\u002Fh2>\n\u003Cp>모든 사이클이 시뮬레이션할 가치가 있는 것은 아닙니다. 필터를 적용합니다:\u003C\u002Fp>\n\u003Ch3>유동성 필터\u003C\u002Fh3>\n\u003Cp>준비금이 임계값 이하인 풀이 포함된 사이클을 폐기합니다. 유동성이 100달러인 풀은 의미 있는 차익거래를 생산할 수 없습니다.\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">fn has_sufficient_liquidity(cycle: &amp;Cycle, min_reserve_usd: f64) -&gt; bool {\n    cycle.pools.iter().all(|pool| {\n        let reserve_usd = estimate_reserve_usd(pool);\n        reserve_usd &gt;= min_reserve_usd\n    })\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>부실 필터\u003C\u002Fh3>\n\u003Cp>N 블록 이상 유휴(스왑 없음) 상태인 모든 풀이 포함된 사이클을 폐기합니다. 부실 풀에는 차익거래가 거의 없습니다.\u003C\u002Fp>\n\u003Ch3>토큰 블랙리스트\u003C\u002Fh3>\n\u003Cp>스캠 토큰, 전송 수수료 토큰, 전송 제한이 있는 토큰을 제외합니다. 시뮬레이션에서는 수익성이 있어 보이지만 온체인에서 실패합니다.\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">const BLACKLISTED_TOKENS: &amp;[&amp;str] = &amp;[\n    \"0x...\",  \u002F\u002F 알려진 전송 수수료 토큰\n    \"0x...\",  \u002F\u002F 알려진 허니팟\n];\n\nfn contains_blacklisted_token(cycle: &amp;Cycle) -&gt; bool {\n    cycle.tokens.iter().any(|t| {\n        BLACKLISTED_TOKENS.contains(&amp;format!(\"{:?}\", t).as_str())\n    })\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>정적 수익성 추정\u003C\u002Fh3>\n\u003Cp>완전한 시뮬레이션 전에, 상수곱 공식을 사용하여 대략적인 수익성 추정치를 계산합니다:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">fn estimate_output_v2(\n    amount_in: U256,\n    reserve_in: U256,\n    reserve_out: U256,\n    fee_bps: u32,\n) -&gt; U256 {\n    let fee_factor = 10000 - fee_bps;  \u002F\u002F 예: 0.3% 수수료에 9970\n    let numerator = amount_in * reserve_out * U256::from(fee_factor);\n    let denominator = reserve_in * U256::from(10000) + amount_in * U256::from(fee_factor);\n    numerator \u002F denominator\n}\n\nfn estimate_cycle_profit(\n    cycle: &amp;Cycle,\n    input_amount: U256,\n) -&gt; Option&lt;U256&gt; {\n    let mut current_amount = input_amount;\n\n    for (i, pool) in cycle.pools.iter().enumerate() {\n        let token_in = cycle.tokens[i];\n        let (reserve_in, reserve_out) = if token_in == pool.token0 {\n            (pool.reserve0, pool.reserve1)\n        } else {\n            (pool.reserve1, pool.reserve0)\n        };\n\n        current_amount = estimate_output_v2(\n            current_amount,\n            reserve_in,\n            reserve_out,\n            pool.fee,\n        );\n    }\n\n    if current_amount &gt; input_amount {\n        Some(current_amount - input_amount)\n    } else {\n        None\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>이 추정치는 불완전하지만(Uniswap V3 틱 수학, Curve 스테이블스왑 곡선 등을 무시) 비용이 많이 드는 시뮬레이션 전에 95%의 비수익 사이클을 제거합니다.\u003C\u002Fp>\n\u003Ch2 id=\"\">확장: 병렬화와 점진적 업데이트\u003C\u002Fh2>\n\u003Cp>80,000개 풀에서 전체 그래프를 구축하고 DFS를 실행하는 데 단일 코어에서 5-10초가 걸립니다. 프로덕션 봇의 경우 시작 비용으로 괜찮습니다. 그러나 풀 상태는 매 블록(12초)마다 변경되므로 점진적 업데이트가 필요합니다:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">pub struct CycleIndex {\n    pub cycles_by_pool: HashMap&lt;Address, Vec&lt;CycleId&gt;&gt;,\n    pub all_cycles: HashMap&lt;CycleId, Cycle&gt;,\n}\n\nimpl CycleIndex {\n    \u002F\u002F\u002F 풀의 준비금이 변경되면, 이 풀을 포함하는\n    \u002F\u002F\u002F 사이클만 무효화하고 재평가\n    pub fn on_pool_update(&amp;self, pool_address: Address) -&gt; Vec&lt;&amp;Cycle&gt; {\n        self.cycles_by_pool\n            .get(&amp;pool_address)\n            .map(|ids| {\n                ids.iter()\n                    .filter_map(|id| self.all_cycles.get(id))\n                    .collect()\n            })\n            .unwrap_or_default()\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Uniswap V2 풀에서 \u003Ccode>Sync\u003C\u002Fcode> 이벤트가 발생하면 해당 풀의 준비금을 업데이트하고 해당 풀을 포함하는 사이클만 재평가합니다. 이렇게 하면 블록당 작업이 수백만 사이클에서 수백 개로 줄어듭니다.\u003C\u002Fp>\n\u003Cp>DFS 자체에 대해서는 Rayon을 사용하여 기본 토큰 간에 병렬화합니다:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">use rayon::prelude::*;\n\nlet base_tokens = vec![weth, usdc, usdt, dai, wbtc];\n\nlet all_cycles: Vec&lt;Cycle&gt; = base_tokens\n    .par_iter()\n    .flat_map(|base| find_cycles(&amp;graph, *base, 3))\n    .collect();\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"\">요약\u003C\u002Fh2>\n\u003Cp>사이클 찾기는 MEV 파이프라인의 첫 번째 단계입니다. 유동성 풀의 평면 목록을 구조화된 차익거래 기회 집합으로 변환합니다. 핵심 결정 — 그래프 표현, DFS 깊이, 중복 제거 전략, 필터링 휴리스틱 — 이 기회 집합의 품질과 범위를 결정합니다. 다음 기사에서는 이러한 사이클을 시뮬레이션합니다: 차입 금액에 대한 이진 검색, 상태 포크, 12초 블록 마감 시간과의 경쟁.\u003C\u002Fp>\n","ko","b0000000-0000-0000-0000-000000000001",true,"2026-03-28T10:44:28.047690Z","Rust로 프로덕션 차익거래 사이클 찾기 구축: 토큰-풀 그래프, DFS 사이클 감지, keccak256 중복 제거, 수백만 사이클로 확장.","차익거래 사이클 찾기 dfs",null,"index, follow",[21,26,30],{"id":22,"name":23,"slug":24,"created_at":25},"c0000000-0000-0000-0000-000000000016","EVM","evm","2026-03-28T10:44:21.513630Z",{"id":27,"name":28,"slug":29,"created_at":25},"c0000000-0000-0000-0000-000000000019","MEV","mev",{"id":31,"name":32,"slug":33,"created_at":25},"c0000000-0000-0000-0000-000000000001","Rust","rust","엔지니어링",[36,42,48],{"id":37,"title":38,"slug":39,"excerpt":40,"locale":12,"category_name":34,"published_at":41},"d0000000-0000-0000-0000-000000000674","2026년, Bali가 동남아시아의 임팩트 테크 허브가 되고 있는 이유","bali-2026-dongnamasia-impaekteu-tekeu-heobeu-iyu","Bali는 동남아시아 스타트업 생태계에서 16위를 차지하고 있습니다. Web3 빌더, AI 지속가능성 스타트업, 에코 여행 테크 기업이 집중되면서, 이 섬은 지역 임팩트 테크의 수도로 자리매김하고 있습니다.","2026-03-28T10:44:49.294484Z",{"id":43,"title":44,"slug":45,"excerpt":46,"locale":12,"category_name":34,"published_at":47},"d0000000-0000-0000-0000-000000000673","ASEAN 데이터 보호 패치워크: 개발자를 위한 컴플라이언스 체크리스트","asean-deiteo-boho-paechiwokeu-gaebaljaleul-wihan-keompeullaieonseuchekeuriseuteu","7개 ASEAN 국가가 포괄적인 데이터 보호법을 시행하고 있으며, 각각 다른 동의 모델, 현지화 요건, 벌칙 구조를 가지고 있습니다. 다중 국가 애플리케이션을 구축하는 개발자를 위한 실용적인 컴플라이언스 체크리스트입니다.","2026-03-28T10:44:49.286400Z",{"id":49,"title":50,"slug":51,"excerpt":52,"locale":12,"category_name":34,"published_at":53},"d0000000-0000-0000-0000-000000000672","Indonesia 290억 달러 디지털 전환: 소프트웨어 기업을 위한 기회","indonesia-290eok-dallleo-dijiteol-jeonhwan-sopeuteuweo-gieopui-gihoe","Indonesia IT 서비스 시장은 2026년 290.3억 달러에 달할 것으로 예상되며, 이는 2025년 243.7억 달러에서 증가한 수치입니다. 클라우드 인프라, AI, 전자상거래, 데이터센터가 동남아시아에서 가장 빠른 성장을 주도하고 있습니다.","2026-03-28T10:44:49.265609Z",{"id":13,"name":55,"slug":56,"bio":57,"photo_url":18,"linkedin":18,"role":58,"created_at":59,"updated_at":59},"Open Soft Team","open-soft-team","The engineering team at Open Soft, building premium software solutions from Bali, Indonesia.","Engineering Team","2026-03-28T08:31:22.226811Z"]