Gas-lean byte-code DEX aggregator for Monad that executes byte-encoded programs to compose swaps across multiple exchanges in one atomic transaction.
The router reads a byte stream sequentially using an InputStream. Here's the exact flow of how bytes are parsed:
The program consists of a sequence of operations. Each operation starts with:
uint8 opcode
The router reads opcodes in a loop until the byte stream is empty.
Based on the opcode read, the router expects specific subsequent bytes:
Uses router's existing token balance
Byte sequence:
uint8(0x01) // Opcode
address(token) // Token to process from router's balance
<distribution_block> // Distribution and swap parameters
What happens:
- Reads the token address (20 bytes)
- Gets router's balance of that token
- Proceeds to distribution block
Pulls tokens from user (first encounter only)
Byte sequence:
uint8(0x02) // Opcode
address(token) // Token to pull from user
<distribution_block> // Distribution and swap parameters
What happens:
- Reads the token address (20 bytes)
- Validates it matches the expected input token
- On first encounter, pulls
amountInfrom user - Proceeds to distribution block
Uses msg.value (native MON)
Byte sequence:
uint8(0x03) // Opcode
<distribution_block> // Distribution and swap parameters (no token address)
What happens:
- No additional reads - uses
address(this).balanceas amount - Proceeds to distribution block
Single downstream pool optimization (saves one token transfer)
Byte sequence:
uint8(0x04) // Opcode
address(token) // Token to process
<swap_block> // Single swap parameters (no distribution)
What happens:
- Reads the token address (20 bytes)
- Directly calls swap with amount = 0
- When amount = 0, the router calculates the effective input by reading the pool's balance delta
- This saves one token transfer: tokens can be sent directly to the pool, bypassing the router temporarily
- Proceeds directly to swap block (skips distribution)
Apply ERC-2612 permit
Byte sequence:
uint8(0x05) // Opcode
address(token) // Token contract address (20 bytes)
uint256(value) // Permit amount (32 bytes)
uint256(deadline) // Permit deadline (32 bytes)
uint8(v) // Signature v component (1 byte)
uint256(r) // Signature r component (32 bytes)
uint256(s) // Signature s component (32 bytes)
What happens:
- Reads all permit parameters sequentially
- Calls
permit()on the token contract - Continues to next opcode (no swap happens)
For opcodes 0x01, 0x02, and 0x03, after the opcode-specific reads, the router reads:
uint8(numDistributions) // Number of ways to split the amount
Then for each distribution (repeated numDistributions times):
uint16(share) // Share of total amount (out of 65535)
<swap_block> // Swap parameters for this distribution
Share calculation:
amount = (totalAmount * share) / 65535- All shares should sum to 65535 (100%)
- Each distribution gets its calculated amount and proceeds to swap
For each swap (whether from distribution or direct), the router reads:
uint8(poolType) // Pool type identifier
<pool_specific_params> // Parameters specific to pool type
Byte sequence:
uint8(0) // Pool type
address(pool) // UniV2 pair address (20 bytes)
uint8(direction) // 0: token0→token1, 1: token1→token0
uint24(fee) // Fee in basis points (3 bytes)
Notes:
- Output tokens are always sent to
address(this)(the router) - Supports single pool optimization: when used with opcode 0x04, calculates input from pool balance delta
Byte sequence:
uint8(1) // Pool type
address(pool) // UniV3 pool address (20 bytes)
uint8(direction) // 0: token1→token0, 1: token0→token1 (converted to bool)
Notes:
- Output tokens are always sent to
address(this)(the router) - Uses callback mechanism (
uniswapV3SwapCallback,pancakeV3SwapCallback,zfV3SwapCallback)
Byte sequence:
uint8(2) // Pool type
uint8(flags) // Bit 0: 1=wrap, 0=unwrap | Bit 1: 1=custom WMON address
[address(wmonAddress)] // Only if flags & 2 == 2 (20 bytes)
Flag bits:
flags & 1 == 1: Wrap (MON → WMON)flags & 1 == 0: Unwrap (WMON → MON)flags & 2 == 2: Read custom WMON addressflags & 2 == 0: Use default WMON address
Notes:
- Output is held by the router (does not need explicit transfer)
Byte sequence:
uint8(3) // Pool type
address(market) // Kuru market address (20 bytes)
uint8(isBuy) // 0: sell, 1: buy
uint32(pricePrecision) // Price precision multiplier (4 bytes)
uint256(sizePrecision) // Size precision multiplier (32 bytes)
uint8(tokenDecimals) // Input token decimals (1 byte)
Parameter calculation:
uint256 divisor = 10 ** tokenDecimals;
uint96 param = isBuy
? (amountIn * pricePrecision) / divisor // For buy orders
: (amountIn * sizePrecision) / divisor; // For sell ordersByte sequence:
uint8(4) // Pool type
address(market) // Crystal market address (20 bytes)
uint8(isBuy) // 0: sell, 1: buy
uint256(orderType) // Crystal-specific order type (32 bytes)
Byte sequence:
uint8(5) // Pool type
address(pool) // Curve pool address (20 bytes)
uint8(poolType) // 0: StableSwap, 1: Crypto
uint8(fromIndex) // Input token index in pool (cast to int128)
uint8(toIndex) // Output token index in pool (cast to int128)
Notes:
- Supports both StableSwap and Crypto pool variants
- Handles native token swaps when tokenIn is NATIVE
- Legacy pools that don't return amountOut are not supported
Byte sequence:
uint8(6) // Pool type
address(pool) // PalindromeFi pool address (20 bytes)
uint8(tokenInIsBase) // 0: tokenIn is quote, 1: tokenIn is base
Notes:
- Uses
quoteAndSwapfunction with min_dy = 0 - Output is sent to
address(this)(the router)
Byte sequence:
uint8(7) // Pool type
address(pool) // LBPair pool address (20 bytes)
uint8(swapForY) // 0: swap for tokenX, 1: swap for tokenY
Notes:
- Tokens are transferred to the pool before calling
swap() - Output is sent to
address(this)(the router)
Determine how tokens enter the router:
0x02: Pull from user (most common)0x03: Use native MON from msg.value0x01: Use router's existing balance (for multi-hop)0x05: Apply permit first (if needed)
For opcodes 0x01, 0x02, 0x03, specify how to split the amount:
uint8(1) // Single distribution (100% to one swap)
uint16(65535) // 100% shareOr for multiple distributions:
uint8(2) // Two distributions
uint16(32767) // 50% to first swap (32767/65535)
uint16(32768) // 50% to second swap (32768/65535)For each distribution, add the swap block based on the pool type needed.
Chain operations by using router balance from previous swaps:
[Initial opcode + distribution + swap] → Router has tokenA
[0x01 + tokenA + distribution + swap] → Router has tokenB
[0x01 + tokenB + distribution + swap] → Router has tokenC
bytes memory program = abi.encodePacked(
uint8(0x03), // PROCESS_NATIVE
uint8(1), // 1 distribution
uint16(65535), // 100% allocation
uint8(2), // PT_WRAP
uint8(1) // flags: wrap=true, no custom address
);Byte breakdown:
0x03: Use native MON0x01: One distribution0xFFFF: 100% share (65535)0x02: Wrap pool type0x01: Wrap flag (wrapped MON stays in router)
bytes memory program = abi.encodePacked(
uint8(0x02), // PROCESS_USER_ERC20
address(WMON), // Token to pull from user
uint8(1), // 1 distribution
uint16(65535), // 100% allocation
uint8(0), // PT_UNIV2
address(pair), // Pool address
uint8(0), // Direction: token0→token1
uint24(3000) // 0.3% fee
);Byte breakdown:
0x02: Pull from user20 bytes: WMON address0x01: One distribution0xFFFF: 100% share0x00: UniV2 pool type20 bytes: Pair address0x00: Direction3 bytes: Fee (3000) - output stays in router
When you have only one pool and want to save gas by skipping the distribution step and one token transfer:
// Assuming tokens are already sent directly to the pool
bytes memory program = abi.encodePacked(
uint8(0x04), // PROCESS_ONE_POOL (optimization)
address(WMON), // Token being swapped
uint8(0), // PT_UNIV2
address(pair), // Pool address
uint8(0), // Direction: token0→token1
uint24(3000) // 0.3% fee
);Byte breakdown:
0x04: Single pool optimization20 bytes: WMON address0x00: UniV2 pool type20 bytes: Pair address0x00: Direction3 bytes: Fee (3000)
How it works:
- Tokens are sent directly to the pool (bypassing router temporarily)
- Router calculates the input amount by reading
pool.balanceOf(token) - pool.reserves - Saves ~21,000 gas by eliminating one token transfer
// MON → WMON → CHOG → WMON → MON
bytes memory program = abi.encodePacked(
// Step 1: Wrap MON to WMON
uint8(0x03), // PROCESS_NATIVE
uint8(1), uint16(65535), // 100% allocation
uint8(2), uint8(1), // Wrap to WMON
// Step 2: Swap WMON to CHOG
uint8(0x01), // PROCESS_ROUTER_ERC20
address(WMON), // Process WMON balance
uint8(1), uint16(65535), // 100% allocation
uint8(0), // PT_UNIV2
address(pair), uint8(0), uint24(3000),
// Step 3: Swap CHOG back to WMON
uint8(0x01), // PROCESS_ROUTER_ERC20
address(CHOG), // Process CHOG balance
uint8(1), uint16(65535), // 100% allocation
uint8(0), // PT_UNIV2
address(pair), uint8(1), uint24(3000),
// Step 4: Unwrap WMON to MON
uint8(0x01), // PROCESS_ROUTER_ERC20
address(WMON), // Process WMON balance
uint8(1), uint16(65535), // 100% allocation
uint8(2), uint8(0) // Unwrap to MON
);This creates a complete roundtrip route that the router executes atomically. All intermediate tokens stay in the router until the final output is sent to msg.sender.
| Error | Selector |
|---|---|
| KuruFlowEntrypoint_BuyAndSellTokensAreSame() | f8aa715b |
| KuruFlowEntrypoint_InsufficientAmountAfterFees() | 5264a63f |
| KuruFlowEntrypoint_InsufficientNativeValue() | 266ae8e1 |
| KuruFlowEntrypoint_InvalidFeeCollector() | 0bc52ea8 |
| KuruFlowEntrypoint_InvalidFeeStructure() | 0040cf18 |
| KuruFlowEntrypoint_InvalidReferrer() | 4d1e7c56 |
| KuruFlowEntrypoint_InvalidRouter() | fa1b73c8 |
| OwnableInvalidOwner(address) | 1e4fbdf7 |
| OwnableUnauthorizedAccount(address) | 118cdaa7 |
| ReentrancyGuardReentrantCall() | 3ee5aeb5 |
| Error | Selector |
|---|---|
| ApproveFailed() | 3e3f8f73 |
| ApproveResetFailed() | 25f0fa4c |
| InsufficientNativeBalance() | dbc3a71f |
| InvalidOpCode() | bf337638 |
| InvalidPoolType() | 2946cbf1 |
| InvalidTokenForUnwrap() | 2366c6ba |
| InvalidTokenForWrap() | 2b2def77 |
| NativeSendFailed() | a0c968e7 |
| ReentrancyGuardReentrantCall() | 3ee5aeb5 |
| SafeERC20FailedOperation(address) | 5274afe7 |
| SlippageExceeded() | 8199f5f3 |
| TokenMismatch() | 936bb5ad |
| Uint96Overflow() | e233e012 |
| UniV3CallbackInvalidSource() | f0cbbb4b |
| UniV3CallbackMissed() | 6d09f943 |
| UniV3CallbackNegativeAmount() | 24292634 |
| ZeroRouterBalance() | 1e2ce7e0 |