สรุปสั้น: ทีม 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 ทำนาย
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.
| contract | address | role |
|---|---|---|
| MoroRouterV1 | 0x93D6CD0F9CB9AB6E187663aB5693Cd1cba03973B | entry point, swapSplit with 1e6 volumes |
| MoroSwapV1 | 0xfE57512Ae3D67C602C3437e0350Dc271e66e9de7 | tradeSplit, same denominator |
| MoroPreTradeV1 | 0xb6FBC7206D7d2476F27ffe22c2ea10FFE3333807 | pre-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.
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.
| stage | grid | points per path | DP states |
|---|---|---|---|
| coarse (knapsack) | 1/100 | 100 | 100 × candidates |
| 1 | 1/1,000 | 41 | ≤ 40 × chosen paths |
| 2 | 1/10,000 | 41 | ≤ 40 × chosen paths |
| 3 | 1/100,000 | 41 | ≤ 40 × chosen paths |
| 4 | 1/1,000,000 | 41 | ≤ 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.
| pair | amount | 1 % grid | 1e6 grid | gain | paths | refine time |
|---|---|---|---|---|---|---|
| BNB → USDT | 100 | 71,461.873 | 71,461.881 | +0.00 bps | 6 | ~180 ms |
| BNB → USDT | 1,000 | 711,618.19 | 711,632.98 | +0.20 bps | 6 | ~160 ms |
| ETH → CAKE | 100 | 112,264.17 | 112,267.04 | +0.25 bps | 7 | ~100 ms |
| ETH → CAKE (thin) | 1,000 | 612,792.46 | 615,053.21 | +36.9 bps | 13 | ~160 ms |
| BUSD → USDT | 100,000 | 100,049.940 | 100,049.941 | +0.00 bps | 3 | ~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.
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.
| case | result | gas used | model predicted |
|---|---|---|---|
| BNB → USDT 100, 8-path split on the 1e6 grid | executed through the new MoroRouterV1 | 1,698,107 | 2,310,315 (+36 %) |
| BNB → USDT 1 (single) | executed | 186,332 | 198,130 |
| BNB → USDT 100 (single) | executed | 266,361 | 260,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 5 | fork 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.
routerVolumeDenominator: 1_000_000 for hyperevm, and the frontend v2 "0.1 % grid: soon" badge becomes a real gain.src/services/bestRateQuerySplit.ts (computeBestRateSplitExact), src/bestRate.ts (staged refinement), src/services/volumeDenominator.ts, CHANGELOG 1.4.0.