Moro SDK · ResearchReport #7

SDK 1.4.0: the new BNB router executes 0.0001 % splits — what that buys, and the search that had to change

@moromoro/moro-sdk 1.3.6 → 1.4.0 BNB Chain · new MoroRouterV1 / MoroSwapV1 / MoroPreTradeV1 measured 2026-09-13 branch feat/optimize
TL;DR

สรุปสั้น: ทีม research deploy MoroRouter ชุดใหม่บน BNB ที่รับ _volumes แบบ 1e6 = 100 % (ละเอียด 0.0001 %) แทนจำนวนเต็มเปอร์เซ็นต์ SDK 1.4.0 จึง quote บน grid 1e6 เป็นค่าเริ่มต้นบน BNB และ swap ได้จริง — แต่การ "ขยาย grid" ตรง ๆ ทำให้ knapsack เดิมทำงาน 15 นาทีไม่จบ เราจึงเปลี่ยน refinement เป็นแบบเป็นขั้น (×10 ต่อขั้น, ปรับเฉพาะ path ที่ถูกเลือก) ใช้เวลาเพิ่มแค่ ~100–400 ms ผลด้านราคา: คู่สภาพคล่องดีแทบไม่เปลี่ยน (0–0.25 bps) แต่เทรดบางบน pool บาง ๆ ได้ +36.9 bps ตรงตามที่ report #4 ทำนาย

1e6
volume denominator
was 100 (whole percents)
+36.9 bps
1,000 ETH → CAKE
1e6 grid vs 1 % grid
≤ 0.25 bps
liquid pairs
BNB→USDT, BUSD→USDT
15 min → 0.3 s
refinement
single jump → staged

1 · What changed on chain

The research team deployed a new contract set on BNB Chain. The router and swap expose VOLUME_DENOMINATOR() = 1,000,000 and require the split volumes to sum to exactly that value.

contractaddressrole
MoroRouterV10x93D6CD0F9CB9AB6E187663aB5693Cd1cba03973Bentry point, swapSplit with 1e6 volumes
MoroSwapV10xfE57512Ae3D67C602C3437e0350Dc271e66e9de7tradeSplit, same denominator
MoroPreTradeV10xb6FBC7206D7d2476F27ffe22c2ea10FFE3333807pre-trade hook linked from the router

SDK side (commits by the research team, then 1.4.0): BNB defaults to splitResolution: 1_000_000 and routerVolumeDenominator: 1_000_000; before every swapSplit the SDK reads VOLUME_DENOMINATOR() on both the router and the swap contract (cached per address) and refuses a quote whose grid does not match. Old /100 contracts revert on that selector, which is accepted only on the 1 % grid. Other chains keep the 1 % grid until their routers are upgraded.

2 · The search had to change

The split optimiser is a multiple-choice knapsack whose cost is points × resolution. Report #4 introduced a coarse-to-fine step: solve on the 1 % grid, then re-solve on the fine grid inside a ±2 % window around each chosen share. At 0.1 % that is 41 points per path; at 0.0001 % it is 40,001 points per path and a capacity of 1,000,000 — the first 1e6 quote we ran was still computing after 15 minutes.

1.4.0 refines in stages. The 1 % solution decides which paths take part. Each stage multiplies the grid by 10 (100 → 1e3 → 1e4 → 1e5 → 1e6) and re-optimises only the chosen paths' shares within ±2 coarse steps, expressed in the new units (±20), with an exact dynamic programme over the window offsets (computeBestRateSplitExact). Its cost is paths × window² and does not depend on the resolution at all.

stagegridpoints per pathDP states
coarse (knapsack)1/100100100 × candidates
11/1,00041≤ 40 × chosen paths
21/10,00041≤ 40 × chosen paths
31/100,00041≤ 40 × chosen paths
41/1,000,00041≤ 40 × chosen paths

Each stage keeps the previous solution if no better exact combination exists in its window, so the result can only improve on the 1 % answer. Shares of paths not chosen at 1 % stay at zero — the coarse stage is the path selector.

3 · What the fine grid buys (BNB Chain, public RPCs, same pool state)

pairamount1 % grid1e6 gridgainpathsrefine time
BNB → USDT10071,461.87371,461.881+0.00 bps6~180 ms
BNB → USDT1,000711,618.19711,632.98+0.20 bps6~160 ms
ETH → CAKE100112,264.17112,267.04+0.25 bps7~100 ms
ETH → CAKE (thin)1,000612,792.46615,053.21+36.9 bps13~160 ms
BUSD → USDT100,000100,049.940100,049.941+0.00 bps3~385 ms

Same pattern as report #4: on liquid pairs the 1 % optimum is already within rounding of the continuous optimum; on a thin pool set the finer shares avoid pushing a pool past its cheap liquidity. The volumes of every 1e6 quote sum to exactly 1,000,000, which is what the new contracts require.

4 · Execution on a fork

The BNB cases of benchmark/gas-calibration.ts --chain bsc were replayed on an Anvil fork of BNB Chain (quote on the live RPC, execute on the fork with an impersonated whale) through the new contracts.

caseresultgas usedmodel predicted
BNB → USDT 100, 8-path split on the 1e6 gridexecuted through the new MoroRouterV11,698,1072,310,315 (+36 %)
BNB → USDT 1 (single)executed186,332198,130
BNB → USDT 100 (single)executed266,361260,923
USDT → BNB 1,000 / 100,000 · ETH → CAKE 100 (ERC-20 source)reverted on the fork without a reason string––
BNB → USDT 1,000 · BNB → CAKE 10 · BNB → BTCB 5fork endpoint rate limit (bsc-dataseed)––

Two take-aways. The volumes the SDK sends (summing to exactly 1,000,000) are accepted and the split executes, which is the point of this release. And the new router is about a third cheaper than the gas constants assume, so src/constants/gas.ts for BNB should be recalibrated once a fork endpoint without rate limits is available (dRPC's public tier refused Anvil's genesis fetch). The ERC-20-source reverts need a verified run through the new MoroSwapV1 / MoroPreTradeV1 before that path is declared good; they may be a fork artefact (approval to the new swap address) rather than a contract issue.

5 · What to do with it

Code: korrio/moro-sdk-optimized — src/services/bestRateQuerySplit.ts (computeBestRateSplitExact), src/bestRate.ts (staged refinement), src/services/volumeDenominator.ts, CHANGELOG 1.4.0.