--- name: lending-instructions description: Load when working with KapanRouter instructions, UTXO tracking, flash loan flows, or debugging transaction failures --- # Kapan Lending Instructions System The KapanRouter uses a **UTXO-based instruction system** where each operation can consume inputs from previous outputs and produce new outputs. Understanding this is critical for building correct flows. ## Core Concepts ### UTXO (Unspent Transaction Output) Each instruction can create outputs that subsequent instructions reference by index. The router maintains an `outputs[]` array during execution. ### Two Instruction Types 1. **Router Instructions** (`protocolName: "router"`) - Handled by KapanRouter directly 2. **Protocol Instructions** (`protocolName: "aave"`, `"compound"`, etc.) - Delegated to gateways ## Router Instructions Defined in `utils/v2/instructionHelpers.ts:4-13`: | Type | Creates UTXO? | Description | |------|--------------|-------------| | `FlashLoan` | **Yes** | Borrows from flash loan provider, creates output for repayment amount | | `PullToken` | **No** | Pulls tokens from user to router (requires user approval) | | `PushToken` | **No** | Sends tokens from UTXO to user | | `ToOutput` | **Yes** | Creates a UTXO from hardcoded amount/token (no actual transfer) | | `Approve` | **Yes** (empty) | Approves gateway to spend UTXO. Creates empty output for index sync! | | `Split` | **Yes** (2) | Splits UTXO into fee portion + remainder (for Aave flash loan fees) | | `Add` | **Yes** | Combines two UTXOs of same token | | `Subtract` | **Yes** | Subtracts one UTXO from another | ### Critical: Approve Creates Empty Output The `Approve` instruction creates an empty output to maintain index synchronization. This is why output indices jump after approvals: ```typescript // After Approve(2), the next output is index 3, not 2! addRouter(encodeApprove(2, "aave"), true); // createsUtxo=true ``` ## Lending Operations Defined in `utils/v2/instructionHelpers.ts:26-36`: | Op | Creates UTXO? | Description | |----|--------------|-------------| | `Deposit` | No | Supply tokens to lending protocol | | `DepositCollateral` | No | Supply as collateral (Morpho, Compound) | | `WithdrawCollateral` | **Yes** | Withdraw collateral, creates output | | `Borrow` | **Yes** | Borrow tokens, creates output | | `Repay` | **Yes** | Repay debt, creates refund output (usually 0) | | `GetBorrowBalance` | **Yes** | Query debt balance, creates output with amount | | `GetSupplyBalance` | **Yes** | Query supply balance, creates output with amount | | `Swap` | **Yes** (2) | Swap via DEX, creates (tokenOut, refund) outputs | | `SwapExactOut` | **Yes** (2) | Exact output swap, creates (tokenOut, refund) outputs | ## Input Index Conventions - `inputIndex: 999` - Ignore input, use `amount` parameter directly - `inputIndex: 0-998` - Read amount/token from `outputs[inputIndex]` ## Flash Loan Providers ```typescript enum FlashLoanProvider { BalancerV2 = 0, // 0% fee BalancerV3 = 1, // 0% fee Aave = 2, // ~5-9 bps fee ZeroLend = 3, // ~5 bps fee UniswapV3 = 4, // Requires pool address Morpho = 5, // 0% fee } ``` **Important**: Aave/ZeroLend flash loans create an output with the **repayment amount** (principal + fee), not the borrowed amount. The router receives the principal but must track repayment separately. ## Common Flows ### Basic Deposit ```typescript [ PullToken(amount, token, user), // No output Approve(0, protocol), // Output[0] = empty (for sync) DepositCollateral(token, user, 0, inputIndex=0), // No output ] ``` ### Withdraw Max ```typescript [ GetSupplyBalance(token, user), // Output[0] = balance WithdrawCollateral(token, user, inputIndex=0), // Output[1] = withdrawn PushToken(1, user), // No output ] ``` ### Flash Loan Pattern ```typescript [ ToOutput(amount, token), // Output[0] = virtual UTXO FlashLoan(provider, inputIndex=0), // Output[1] = repayment amount // ... use borrowed funds (router has Output[0] worth of tokens) // ... must leave Output[1] worth in router for repayment ] ``` ### Aave Flash Loan with Fee (Max Withdrawal) When using Aave flash loans for max operations, use `Split` to account for fees: ```typescript [ GetSupplyBalance(token), // Output[0] = 100 Split(0, 9), // Output[1] = 0.09 (fee buffer), Output[2] = 99.91 FlashLoan(Aave, 2), // Output[3] = 100 (repayment), router has 99.91 // ... operations using Output[2] (what we actually have) // ... withdraw Output[3] worth to repay ] ``` ## File Locations - **Instruction Helpers**: `packages/nextjs/utils/v2/instructionHelpers.ts` - **Flow Builders**: `packages/nextjs/hooks/useKapanRouterV2.tsx:219-936` - **Move Builder**: `packages/nextjs/hooks/useKapanRouterV2.tsx:1519-1631` - **Starknet Instructions**: `packages/nextjs/hooks/useStarknetMovePosition.ts` ## Debugging Tips 1. **Track UTXO indices manually** - Draw out the output array as you build instructions 2. **Approve creates empty output** - Always increment your expected index after Approve 3. **Flash loan outputs** - Remember: Aave Output = repayment (more than borrowed) 4. **Use simulation** - `simulateInstructions()` catches most errors before execution 5. **Check authorization** - `getAuthorizations()` returns needed approvals ## Protocol Context Encoding Different protocols need different context bytes: ```typescript // Aave: empty context context = "0x" // Compound: market address context = encodeAbiParameters([{ type: "address" }], [marketAddress]) // Morpho: full MarketParams tuple context = encodeMorphoContext({ loanToken, collateralToken, oracle, irm, lltv }) ```