Supported Protocols
Borrow Recovery integrates with two major lending protocols:
- Aave V3: Multi-chain lending pools
- Morpho Blue: Next-generation lending primitive
All protocol interactions happen through deterministic calldata encoding, submitted via ERC-4337 UserOperations.
Multi-Chain Support
From lib/chains.ts, the app supports:
export type SupportedChainId = 1 | 8453 | 42161 | 56;
export const SUPPORTED_CHAINS: readonly ChainConfig[] = [
{
id: 1,
name: "Ethereum",
nativeSymbol: "ETH",
rpcUrl: "https://rpc.ankr.com/eth",
explorerBaseUrl: "https://etherscan.io",
aaveV3PoolAddress: "0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2",
morphoBlueAddress: "0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb",
// ... other addresses
},
// Base, Arbitrum, BNB configs...
];
All chains use public RPC endpoints - no API keys required for position reading.
Chain Configuration
Each chain includes:
- RPC URL: For reading positions and submitting calls
- Explorer: For viewing transactions
- Aave V3 addresses: Pool, Gateway, Data Providers
- Morpho Blue address: Core lending contract
export type ChainConfig = {
id: SupportedChainId;
name: string;
nativeSymbol: string;
rpcUrl: string;
explorerBaseUrl: string;
// Aave V3 contracts
aaveV3PoolAddress: `0x${string}` | null;
aaveV3WethGatewayAddress: `0x${string}` | null;
aaveV3ProtocolDataProviderAddress: `0x${string}` | null;
aaveV3PoolAddressesProvider: `0x${string}` | null;
aaveV3UiPoolDataProviderAddress: `0x${string}` | null;
aaveV3UiIncentiveDataProviderAddress: `0x${string}` | null;
// Morpho Blue
morphoBlueAddress: `0x${string}` | null;
};
Aave V3 Integration
Reading Positions
From lib/protocols/aave.ts, the app reads user positions:
const aavePoolAbi = parseAbi([
"function getUserAccountData(address user) view returns (uint256 totalCollateralBase, uint256 totalDebtBase, uint256 availableBorrowsBase, uint256 currentLiquidationThreshold, uint256 ltv, uint256 healthFactor)",
]);
export type AaveUserAccountData = {
totalCollateralBase: bigint; // Total collateral in USD (8 decimals)
totalDebtBase: bigint; // Total debt in USD (8 decimals)
availableBorrowsBase: bigint; // Available to borrow
currentLiquidationThreshold: bigint; // Liquidation threshold
ltv: bigint; // Loan-to-value ratio
healthFactor: bigint; // Position health (1e18 = 100%)
};
The app encodes and decodes this data:
export function encodeAaveGetUserAccountData(user: Address): Hex {
return encodeFunctionData({
abi: aavePoolAbi,
functionName: "getUserAccountData",
args: [user],
});
}
export function decodeAaveGetUserAccountData(result: Hex): AaveUserAccountData {
const [
totalCollateralBase,
totalDebtBase,
availableBorrowsBase,
currentLiquidationThreshold,
ltv,
healthFactor,
] = decodeFunctionResult({
abi: aavePoolAbi,
functionName: "getUserAccountData",
data: result,
});
return {
totalCollateralBase,
totalDebtBase,
availableBorrowsBase,
currentLiquidationThreshold,
ltv,
healthFactor,
};
}
From lib/protocols/aave.ts:80-103.
Reserve Token Addresses
To get aToken/debtToken addresses:
const aavePoolAbi = parseAbi([
"function getReserveTokensAddresses(address asset) view returns (address aTokenAddress, address stableDebtTokenAddress, address variableDebtTokenAddress)",
]);
export function decodeAaveGetReserveTokensAddresses(result: Hex): {
aTokenAddress: Address;
stableDebtTokenAddress: Address;
variableDebtTokenAddress: Address;
} {
const [aTokenAddress, stableDebtTokenAddress, variableDebtTokenAddress] =
decodeFunctionResult({
abi: aavePoolAbi,
functionName: "getReserveTokensAddresses",
data: result,
});
return { aTokenAddress, stableDebtTokenAddress, variableDebtTokenAddress };
}
From lib/protocols/aave.ts:36-47.
Rescue Operations
Repay Debt
export function encodeAaveRepay(params: {
asset: Address; // Token to repay (e.g., USDC)
amount: bigint; // Amount to repay (or MAX_UINT256 for all)
interestRateMode: bigint; // 2 = variable rate
onBehalfOf: Address; // Kernel wallet address
}): Hex {
return encodeFunctionData({
abi: aavePoolAbi,
functionName: "repay",
args: [params.asset, params.amount, params.interestRateMode, params.onBehalfOf],
});
}
From lib/protocols/aave.ts:52-63.
To repay all debt, use MAX_UINT256 (2^256 - 1). Aave will calculate the exact amount needed.
Withdraw Collateral
export function encodeAaveWithdraw(params: {
asset: Address; // Collateral token (e.g., WBTC)
amount: bigint; // Amount to withdraw (or MAX_UINT256 for all)
to: Address; // Recipient (usually connected wallet)
}): Hex {
return encodeFunctionData({
abi: aavePoolAbi,
functionName: "withdraw",
args: [params.asset, params.amount, params.to],
});
}
From lib/protocols/aave.ts:66-76.
Execution Flow
For an Aave repay operation:
- User clicks “Repay debt”
- App constructs calldata:
const approveCall = encodeERC20Approve({
token: loanToken,
spender: aavePoolAddress,
amount: repayAmount,
});
const repayCall = encodeAaveRepay({
asset: loanToken,
amount: repayAmount,
interestRateMode: 2n,
onBehalfOf: kernelAddress,
});
- Batch into Kernel execute:
const kernelCallData = await encodeKernelExecuteCalls([
{ target: loanToken, callData: approveCall },
{ target: aavePoolAddress, callData: repayCall },
]);
- Submit as UserOperation (see Account Abstraction)
Morpho Blue Integration
Market Configuration
Morpho Blue uses isolated lending markets. From the README:
// Currently supported market
const MORPHO_MARKET_ID =
"0x9103c3b4e834476c9a62ea009ba2c884ee42e94e6e314a26f04d312434191836";
This market is available on all supported chains at the same address:
morphoBlueAddress: "0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb"
Reading Positions
From lib/protocols/morpho.ts:
const morphoBlueAbi = parseAbi([
"function position(bytes32 marketId, address user) view returns (uint256 supplyShares, uint256 borrowShares, uint256 collateral)",
]);
export type MorphoBluePosition = {
supplyShares: bigint; // Supply shares in market
borrowShares: bigint; // Borrow shares (debt)
collateral: bigint; // Collateral deposited
};
Encoding Position Query
export function encodeMorphoBluePosition(
marketId: Hex,
user: Address
): Hex {
return encodeFunctionData({
abi: morphoBlueAbi,
functionName: "position",
args: [marketId, user],
});
}
From lib/protocols/morpho.ts:14-20.
Decoding Position Data
export function decodeMorphoBluePosition(result: Hex): MorphoBluePosition {
const [supplyShares, borrowShares, collateral] = decodeFunctionResult({
abi: morphoBlueAbi,
functionName: "position",
data: result,
});
return { supplyShares, borrowShares, collateral };
}
From lib/protocols/morpho.ts:22-30.
Morpho uses shares instead of amounts. The app converts shares to tokens using market state (total supply, total borrow).
Rescue Operations
Morpho Blue rescue operations work similarly to Aave:
- Repay: Transfer loan token → call
repay() on Morpho
- Withdraw: Call
withdrawCollateral() → receive collateral
The calldata encoding follows the same pattern:
- Encode Morpho function call
- Batch with approvals if needed
- Wrap in Kernel execute
- Submit as UserOperation
ERC-20 Token Operations
From lib/protocols/erc20.ts, the app supports standard token operations:
const erc20Abi = parseAbi([
"function balanceOf(address account) view returns (uint256)",
"function approve(address spender, uint256 amount) returns (bool)",
"function transfer(address to, uint256 amount) returns (bool)",
]);
export function encodeERC20Approve(params: {
spender: Address;
amount: bigint;
}): Hex {
return encodeFunctionData({
abi: erc20Abi,
functionName: "approve",
args: [params.spender, params.amount],
});
}
export function encodeERC20Transfer(params: {
to: Address;
amount: bigint;
}): Hex {
return encodeFunctionData({
abi: erc20Abi,
functionName: "transfer",
args: [params.to, params.amount],
});
}
Token Transfers
The “Transfer all to connected wallet” feature:
- Read balance from Kernel wallet
- Encode transfer:
const transferCall = encodeERC20Transfer({
to: connectedWalletAddress,
amount: balance,
});
- Wrap in Kernel execute:
const kernelCallData = await encodeKernelExecuteCalls([
{ target: tokenAddress, callData: transferCall },
]);
- Submit UserOperation
Kernel Execution Wrapper
All protocol calls are wrapped in Kernel’s execute() function.
From lib/protocols/kernel.ts:
import { encodeCallDataEpV07 } from "@zerodev/sdk";
type KernelCall = {
target: Address; // Contract to call
value?: bigint | undefined; // ETH value (optional)
callData?: Hex | undefined; // Encoded function call
};
export async function encodeKernelExecuteCalls(
calls: readonly KernelCall[]
): Promise<Hex> {
if (calls.length === 0) throw new Error("No calls to encode.");
return encodeCallDataEpV07(
calls.map((call) => ({
to: call.target,
value: call.value ?? 0n,
data: call.callData ?? "0x",
}))
);
}
From lib/protocols/kernel.ts:15-24.
This allows batching:
- Multiple protocol operations
- Token approvals + actions
- Cross-protocol operations
Example: Full Aave Repay Flow
Here’s the complete flow for repaying Aave debt:
const userInput = {
loanToken: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC
repayAmount: 1000_000000n, // 1000 USDC (6 decimals)
kernelAddress: "0x...",
aavePoolAddress: "0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2",
};
2. Encode Calls
import { encodeERC20Approve, encodeAaveRepay, encodeKernelExecuteCalls } from "@/lib/protocols";
const approveCall = encodeERC20Approve({
spender: userInput.aavePoolAddress,
amount: userInput.repayAmount,
});
const repayCall = encodeAaveRepay({
asset: userInput.loanToken,
amount: userInput.repayAmount,
interestRateMode: 2n, // Variable rate
onBehalfOf: userInput.kernelAddress,
});
const kernelCallData = await encodeKernelExecuteCalls([
{ target: userInput.loanToken, callData: approveCall },
{ target: userInput.aavePoolAddress, callData: repayCall },
]);
3. Submit UserOperation
import { submitKernelUserOperationV07 } from "@/lib/accountAbstraction";
const userOpHash = await submitKernelUserOperationV07({
bundlerUrl: "https://rpc.zerodev.app/api/v3/{projectId}/chain/1",
chainRpcUrl: "https://rpc.ankr.com/eth",
owner: connectedWalletAddress,
kernelAddress: userInput.kernelAddress,
chainId: 1,
kernelCallData,
request: walletRequest, // EIP-1193 request function
});
console.log("UserOp submitted:", userOpHash);
4. Result
The bundler executes:
- Approve USDC to Aave Pool
- Repay 1000 USDC debt
Both operations succeed or revert atomically.
Data Providers
Beyond core protocol contracts, the app uses data provider contracts:
Aave UI Providers
aaveV3UiPoolDataProviderAddress: "0x...", // Pool reserves data
aaveV3UiIncentiveDataProviderAddress: "0x...", // Rewards data
These aggregate data for efficient UI rendering.
Protocol Data Provider
aaveV3ProtocolDataProviderAddress: "0x...",
Provides detailed reserve configuration and user reserve data.
Error Handling
Protocol interactions may fail due to:
- Insufficient balance: Not enough tokens to repay
- Insufficient collateral: Withdraw would trigger liquidation
- Interest accrual: Debt increased between estimation and execution
The app handles these with:
- Gas buffering (50% extra)
- Gas price bumping (10-20% extra)
- Clear error messages to user
From lib/accountAbstraction/submitUserOpV07.ts.
Multi-Chain Considerations
Same protocol (Aave V3 or Morpho Blue) may have different addresses on different chains.
The app uses chain-specific configurations:
import { getChainConfig } from "@/lib/chains";
const chainConfig = getChainConfig(chainId);
if (!chainConfig) throw new Error("Unsupported chain");
const aavePoolAddress = chainConfig.aaveV3PoolAddress;
const morphoAddress = chainConfig.morphoBlueAddress;
From lib/chains.ts:93-96.
Benefits of This Architecture
- No backend dependencies: All encoding happens client-side
- Transparent: Users see exact calldata before signing
- Atomic: Multiple operations succeed or fail together
- Extensible: Easy to add new protocols or chains
- Standard: Uses official protocol ABIs and addresses
Next Steps
Kernel Accounts
Learn about Kernel smart account structure
Account Abstraction
Understand how UserOps execute protocol calls
Quick Start
Start recovering your positions