สรุปสั้น: สแกนหา arbitrage ระหว่าง pool ด้วย SDK รวม 489 รอบบนสองเชน: Hybra V3↔V4 ข้ามคืน (158 รอบ), ทุกคู่ pool บน HyperEVM (299 รอบ ประมาณ 7,244 cycle ต่อรอบ) และทำซ้ำ repo pancake-bakery-arbitrage บน BSC (32 รอบ 7,623 cycle ต่อรอบ) โดยใช้ตัวหาขนาดเทรดที่ดีที่สุดของ SDK ไม่มี cycle ไหนได้กำไรหลังหัก gas มี cycle ที่ได้กำไรก่อนหัก gas ทุกรอบ (1,471 บน HyperEVM, 200 บน BSC) แต่มากสุดแค่ $0.0015 และ $0.0074 ขณะที่ gas ต่อธุรกรรมอยู่ที่ $0.0066 และ $0.0134 คู่ router ของ repo ต้นฉบับขาดทุนทุกขนาด (ที่ 1,000 BNB ขาดทุน 83–95%) เพราะ pool นอกจาก Pancake แทบไม่มีสภาพคล่องแล้ว ฝั่ง execution: สัญญา MoroFlashArb ทำงานบน fork ทั้งสองเชน รวมถึง flash จาก CL pool บน HyperEVM แล้วขายผ่าน Moro router แบบ split ที่ SDK สร้าง calldata ให้
In short: across 489 rounds the SDK found cycles that make money before gas in almost every round, and none that survive gas. The largest was $0.0074 on BSC and $0.0015 on HyperEVM, against $0.0134 and $0.0066 of gas per transaction. The 2021 repo's own router pairs lose at every size today. The execution path it sketches, a flash swap repaid in the callback, works on both chains' forks, and with the SDK the second leg can be a full Moro router split.
| scan | chain | scope | window (UTC) | rounds | cycles / round | profit before gas | after gas |
|---|---|---|---|---|---|---|---|
| Hybra V3 ↔ V4 | HyperEVM | 64 pairs on both versions, both directions, 10 sizes $10–$10k | 13 Sep 17:20 → 23:59 | 158 | 2,560 | 0 | 0 |
| all pool pairs, size grid | HyperEVM | 273 Hybra-listed pairs × every SDK route, 5 leg modes, 10 sizes | 14 Sep 00:22 → 02:56 | 299 | ≈ 7,500 | grid best +4.43 bps once | 0 |
| all pool pairs, optimal size | HyperEVM | same, best 12 pool pairs per start token refined to their optimal size | 14 Sep 01:48 → 02:56 | 37 | 7,244 | 1,471 | 0 |
| pancake-bakery-arbitrage, generalised | BSC | 8 routing tokens × 52 SDK routes; repo-style V2 forks and all pools; optimal size | 14 Sep 01:50 → 02:56 | 32 | 7,623 | 200 | 0 |
A cycle is start token → intermediate on one pool, then back on a different pool. Each round fetches pool state once (createRouteScanner + fetchAllReserves), then prices every cycle in memory with the SDK's pool models. The optimizer (optimizeCycleSize) searches the size that maximises profit: a two-leg cycle's profit is concave in size for constant-product and concentrated-liquidity pools and gas does not depend on size, so a ternary search finds the true optimum. BSC rounds took a median of 3.6 s.
| HyperEVM | BSC | |
|---|---|---|
| cycles profitable before gas per round (median) | 40 | 6 |
| best profit before gas | $0.0015 | $0.0074 |
| that cycle | UBTC/USD₮0 at $0.95, prjx-v3-500 → hybra-v4-10 | BUSD→USDT→BUSD at $745, PNCK:V3F100 → DODO:V1USDT |
| median profit before gas of those cycles | $0.00016 | — |
| gas per round trip (median, at live gas price) | $0.0066 | $0.0134 |
| best spread at a fixed $1,000 (all pools) | grid at $10: median −0.15 bps | best +0.06 bps, median −0.80 bps |
| best spread at $1,000, V2 forks only (the repo's view) | — | median −50.5 bps |
Prices between pools on both chains sit within a fraction of a basis point of each other once fees are paid. The optimizer finds a profitable size almost every round, but it is small because a larger trade moves the pool faster than the spread pays. The overnight Hybra run agrees: its closest pairs were USDC/USDM (−$0.002 at $50) and USDC/USD₮0 (−$0.003 at $10). On BSC the recurring closest cycle was BUSD→USDT→BUSD DODO:V1USDT → PNCK:V3F100, best in 20 of 32 rounds.
| mode | cycle | bps at $1,000 | pools | families |
|---|---|---|---|---|
| all pools | BUSD → USDT → BUSD | +0.06 | PNCK:V3F100 → DODO:V1USDT | cl/other |
| all pools | USDT → BUSD → USDT | +0.06 | DODO:V1USDT → PNCK:V3F100 | other/cl |
| all pools | USDC → BUSD → USDC | −1.54 | DODO:V1BUSD → PNCK:V3F100 | other/cl |
| all pools | DAI → USDT → DAI | −2.19 | UNI:V3F100 → PNCK:V3F100 | cl/cl |
| all pools | WBNB → USDT → WBNB | −2.59 | PNCK:V3F500 → PNCK:V3F100 | cl/cl |
| all pools | WETH → WBNB → WETH | −4.14 | PNCK:V3F100 → PNCK:V3F500 | cl/cl |
| all pools | BTCB → WBNB → BTCB | −5.40 | PNCK:V3F500 → PNCK:V3F100 | cl/cl |
| all pools | CAKE → WBNB → CAKE | −19.21 | PNCK:V3F500 → PANCAKEV2 | cl/v2 |
BSC, last round: the best cycle per start token and mode at $1,000.
yuyasugano/pancake-bakery-arbitrage (2021–22) is a Node loop and a flash-swap contract. On every new BSC block it calls the contract's check() for six hard-coded BNB/BUSD router pairs (Pancake V2 against Panther, Ape and Bakery), asks each router's getAmountsOut for a fixed 1,000 BNB round trip, and when the profit clears $0.60 after gas it sends startArbitrage: borrow BUSD from one V2 pair, sell it on the other router, repay the pair in BNB inside pancakeCall.
| repo (2021) | measured 2026-09-14 | |
|---|---|---|
| BSC block time | ≈ 3 s ("<3 seconds" budget) | 0.45 s (400 blocks in 180 s) |
| gas price | 5 gwei | 0.05 gwei |
| WBNB/BUSD liquidity: Pancake V2 · Ape · Bakery · Panther | — | $9.04M · $181k · $135k · $12.5k |
| WBNB/USDT liquidity: Pancake V2 · Bakery · Ape · Panther | — | $77.7M · $20.4k · $4.0k · $254 |
| leg 1 → leg 2 | flash on leg 1 | profit at 1,000 BNB | in USD | bps | bps at 1 BNB | profitable size |
|---|---|---|---|---|---|---|
| PANCAKEV2 → APE | yes | −889.9 BNB | −$636,199 | −8899 | −121.4 | none |
| PANCAKEV2 → PANTHER | yes | −891.7 BNB | −$637,446 | −8917 | −123.4 | none |
| PANCAKEV2 → BAKERY | yes | −915.3 BNB | −$654,346 | −9153 | −158.9 | none |
| PANCAKEV2 → BISWAP | yes | −828.7 BNB | −$592,404 | −8287 | −90.8 | none |
| APE → PANCAKEV2 | yes | −890.2 BNB | −$636,394 | −8902 | −127.6 | none |
| APE → PANTHER | yes | −941.2 BNB | −$672,862 | −9412 | −196.8 | none |
| APE → BAKERY | yes | −949.0 BNB | −$678,406 | −9490 | −231.9 | none |
| APE → BISWAP | yes | −926.6 BNB | −$662,386 | −9266 | −164.7 | none |
| PANTHER → PANCAKEV2 | yes | −891.9 BNB | −$637,634 | −8919 | −128.4 | none |
| PANTHER → APE | yes | −941.2 BNB | −$672,860 | −9412 | −195.7 | none |
| PANTHER → BAKERY | yes | −949.3 BNB | −$678,676 | −9493 | −232.7 | none |
| PANTHER → BISWAP | yes | −927.3 BNB | −$662,943 | −9273 | −165.5 | none |
| BAKERY → PANCAKEV2 | no | −915.5 BNB | −$654,491 | −9155 | −162.6 | none |
| BAKERY → APE | no | −949.0 BNB | −$678,396 | −9490 | −229.4 | none |
| BAKERY → PANTHER | no | −949.3 BNB | −$678,668 | −9493 | −231.3 | none |
| BAKERY → BISWAP | no | −938.8 BNB | −$671,146 | −9388 | −199.4 | none |
| BISWAP → PANCAKEV2 | yes | −829.0 BNB | −$592,669 | −8290 | −94.5 | none |
| BISWAP → APE | yes | −926.5 BNB | −$662,354 | −9265 | −162.3 | none |
| BISWAP → PANTHER | yes | −927.3 BNB | −$662,914 | −9273 | −164.3 | none |
| BISWAP → BAKERY | yes | −938.8 BNB | −$671,127 | −9388 | −199.5 | none |
WBNB → BUSD → WBNB on every ordered pair of Pancake V2, Ape, Panther, Bakery and Biswap. At the repo's 1,000 BNB ($715k) the second pool is far too small, so the trip loses most of its value; at 1 BNB it still loses 91–233 bps, and the optimizer finds no profitable size for any pair. A 2,001-point log grid on the closest pair (PANCAKEV2 → BISWAP) peaks at -4.3e-09 BNB for a 0.000001 BNB trade: the only "profitable" size is no trade.
| pool | flash entry | callback it calls |
|---|---|---|
| Pancake V2 · Ape (BSC) | swap(…, bytes) | pancakeCall |
| Biswap (BSC) | swap(…, bytes) | BiswapCall |
| Panther, SDK factory (BSC) | swap(…, bytes) | pantherCall |
| Panther, the repo's factory (BSC) | swap(…, bytes) | none of the known names |
| Bakery (BSC) | swap without a data argument | no flash swap: "bakery > pancake" can never execute |
| Hybra V3 and V4 (HyperEVM) | flash() and CL swap | uniswapV3SwapCallback, uniswapV3FlashCallback |
| Hybra V2 (HyperEVM) | swap(…, bytes) | hook (Velodrome style) |
| HyperSwap V3 (HyperEVM, seen in a fork trace) | CL swap | hyperswapV3SwapCallback |
block.number + process.env.BLOCKNUMBER concatenates a number and the string "3", so block 7920991 becomes 79209913.pancakeCall and the other eight never check that the caller is a real pair or that the swap was started by the contract, and _data chooses the router the contract approves. Anyone can make it approve a router of their choice for tokens it holds; impact is limited because profit is forwarded to the owner.estimateGas, with a 1,500 ms budget that now spans three BSC blocks; prices from CoinGecko every five minutes; an 18-decimal assumption in the profit maths; transactions broadcast to public nodes at +22 % gas, where they can be copied.| repo | with the Moro SDK (1.4.3-dev.4, branch feat/arb) |
|---|---|
check() eth_call per pair per block | one state fetch per round, then 7,623 (BSC) or 7,244 (HyperEVM) cycles priced in memory: createRouteScanner, searchCycles |
| 4 V2 routers, one pair | 52 BSC routes and 31 HyperEVM routes: V2 forks, Pancake and Uniswap V3, Algebra, Curve, DODO, Hybra V3/V4, per-leg route filters |
| fixed 1,000 BNB | optimizeCycleSize: the profit-maximising size within 1 ppm, about 150 in-memory evaluations |
| leg 2 on a single router | populateSwap: Moro router calldata for a contract trader, so leg 2 can be a best-rate split across every DEX |
| CoinGecko prices | USD prices from the same pool state as the cycle |
Flashswap.sol | MoroFlashArb sketch: active-pool check in every callback, immutable executor, exact approvals reset to zero, owner-only start, real block deadline, on-chain V2 repay maths, V2 + CL legs |
| test | chain · block | result | gas |
|---|---|---|---|
| flash-borrow 1,000 BUSD from Pancake V2 WBNB/BUSD, sell on ApeSwap, repay in WBNB | BSC · latest (two runs) | executed; pair K preserved; the round trip cost 0.0214–0.0225 WBNB (≈ $15–16), covered by pre-funding | 217,127 (startV2) |
| same trip without pre-funding | BSC | reverts atomically: nothing borrowed is kept | — |
| callback from anyone but the active pool · non-owner start · expired block | BSC | all three revert with BadCallback, NotOwner, Expired | — |
flash 1 WHYPE on Hybra V3 WHYPE/USD₮0 0.25 % (79.10381 USD₮0 out), sell through the Moro router: swapSplit over 4 routes, calldata from populateSwap | HyperEVM · 45,849,875 | executed and repaid in the callback; the round trip cost 0.00699 WHYPE (≈ 70 bps, mostly the 0.25 % pool fee) | 710,843 (startCL) |
Two things learned on the way. getDepositAddress returns Moro's deposit contract for a route, not the pool (3,234 bytes, owner-controlled, calls pool.swap itself), so a flash leg needs the real pool address. And HyperEVM's small blocks carry 3,000,000 gas (measured at the head); a flash cycle with a ten-route split used 711k, so it fits, but a wider split may not.
searchCycles, optimizeCycleSize, populateSwap, SDK 1.4.3-dev.4) · moro-bot: src/scan.ts (Hybra V3↔V4), src/cycles.ts (HyperEVM), src/pba.ts (BSC), flash/ (MoroFlashArb and fork tests). Prices are the SDK's off-chain pool models; executions are on forks, not mainnet. MoroFlashArb is a research sketch: not audited, not deployed.