Exit borrow at maturity
Wallet setup
Define the wallet client and the borrower’s address. This is the account that will sign transactions and receive units at maturity.
import {
createWalletClient,
http,
parseUnits,
zeroAddress,
type Address,
} from "viem";
import { base } from "viem/chains";
import { privateKeyToAccount } from "viem/accounts";
// The existing borrower (your wallet address)
const borrower: Address = "0xYourWalletAddress";
// Create a wallet client connected to Base
// This example uses a simple EOA with a private key for brevity
// You can substitute in any wallet solution that fits your setup
const walletClient = createWalletClient({
account: privateKeyToAccount("0xYourPrivateKey"),
chain: base,
transport: http(),
});Read outstanding debt & collateral
In order to exit your position at maturity, repay your debt in full. Collateral can only be released in full once the debt is fully repaid. To fetch a user's outstanding debt and locked collateral, use the following query:
GET https://api.morpho.org/v0/midnight/markets
/{marketId}/users/{userAddress}/position
// Response
{
"data": {
"chain_id": 8453,
"market_id": "0xd92de5e7fbb...7614", // market the borrower borrowed from
"user_address": "0xYourWalletAddress",
"last_indexed_block": "47457420",
"loan_token": "0x8335...2913", // USDC
"maturity": "1798761600",
"type": "borrow", // "lend" | "borrow" | "collateral_only"
"credit": "0", // lender side (0 for a borrower)
"debt": "10527000000", // outstanding debt to clear
"pending_fee": "0", // relevant for type: "credit" (over the defined maturity date, here's what you'll owe as a lender)
"last_loss_factor": "0",
"loss_factor": "0",
"collaterals": [ // each entry is { token, amount }
{
"token": "0x4200000000000000000000000000000000000006", // WETH
"amount": "5000000000000000000" // 5 WETH (18 dec)
}
]
}
}Positions with zero credit and debt but non-empty collaterals are returned as type: "collateral_only".
Repay & withdraw collateral
A full collateral withdrawal only succeeds once the position's debt is zero. If accrual leaves even 1 unit unpaid, the withdrawal of the corresponding collateral reverts (it would push the position under its LLTV). Over-approving slightly plus a small repay buffer is what makes the at-maturity exit reliably clean.
Authorizations & Approvals
Prerequisites
The borrower must have:
- Authorized
MidnightBundleson the Midnight contract (so it can act on the positio on their behalf). - Approved
MidnightBundlesto pull the loan token
Pick between Path 1, Path 2, or Path 3 depending on your UX and signing needs.
import {
erc20Abi, createPublicClient, encodeAbiParameters, parseAbiParameters, parseSignature, parseUnits, http,
} from "viem";
import { base } from "viem/chains";
const MIDNIGHT_BUNDLES = "0x6688dEc8878f43905e11B3C6Bc025E098133144f"; // MidnightBundles contract
const USDC = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"; // USDC on Base
// Full obligation = debt + accrued pending_fee
const debtAssets = BigInt(position.debt) + BigInt(position.pending_fee);
// Approve a bit more than the debtAssets. Any accrual between reading and executing could leave dust that blocks full collateral release.
const approveAmount = debtAssets + parseUnits("1", 6);
// Path 1: approve()
// 1. Let MidnightBundles pull USDC
// 2. Set limit based on approveAmount computed above
await walletClient.writeContract({
account: borrower, address: USDC, abi: erc20Abi,
functionName: "approve", args: [MIDNIGHT_BUNDLES, approveAmount],
});
// Variable to set loanTokenPermit later in bundler argument
const tokenPermitNone = { kind: 0, data: "0x" } as const;
// --------------------------------------------------------------
// Path 2: EIP-2612
// Authorize exactly the USDC you'll spend buying units
const publicClient = createPublicClient({ chain: base, transport: http() });
// Ceiling you authorize via the permit (covers fee accruing before the tx lands)
// Same as "approvedAmount" defined above for the approval flow
const permitValue = BigInt(position.debt) + BigInt(position.pending_fee)
+ parseUnits("1", 6);
const deadline = BigInt(Math.floor(Date.now() / 1000) + 3600);
// Read the owner's current EIP-2612 nonce from the token.
const nonce = await publicClient.readContract({
address: USDC,
abi: [{ name: "nonces", type: "function", stateMutability: "view",
inputs: [{ name: "owner", type: "address" }],
outputs: [{ type: "uint256" }] }],
functionName: "nonces",
args: [borrower],
});
// Offchain signature (no transaction, no gas)
const sig = await walletClient.signTypedData({
account: borrower,
domain: { name: "USD Coin", version: "2", chainId: 8453, verifyingContract: USDC },
types: { Permit: [
{ name: "owner", type: "address" },
{ name: "spender", type: "address" },
{ name: "value", type: "uint256" },
{ name: "nonce", type: "uint256" },
{ name: "deadline", type: "uint256" },
] },
primaryType: "Permit",
message: { owner: borrower, spender: MIDNIGHT_BUNDLES,
value: permitValue, nonce, deadline },
});
const { r, s, v } = parseSignature(sig);
// Variable to set the loanTokenPermit variable to (bundler argument) in the Execute step that follows
const loanTokenPermitEIP2612 = {
kind: 1, // 1 = ERC2612
data: encodeAbiParameters(
parseAbiParameters("uint256 deadline, uint8 v, bytes32 r, bytes32 s"),
[deadline, Number(v), r, s],
),
} as const;
// --------------------------------------------------------------
// Path 3: Permit2
// One-time setup (once per token, ever): approve Permit2 on USDC
const PERMIT2 = "0x000000000022D473030F116dDEE9F6B43aC78BA3"; // canonical, same on every chain
const repayCeiling = BigInt(position.debt) + BigInt(position.pending_fee)
+ parseUnits("1", 6);
// One-time setup (once per token, ever): approve Permit2 on USDC.
await walletClient.writeContract({
account: borrower, address: USDC, abi: erc20Abi,
functionName: "approve", args: [PERMIT2, maxUint256],
});
// Per-repay: sign a Permit2 SignatureTransfer (offchain, no gas).
const nonce = BigInt(Date.now()); // unordered nonce (Permit2 uses a bitmap)
const deadline = BigInt(Math.floor(Date.now() / 1000) + 3600);
const sig = await walletClient.signTypedData({
account: borrower,
domain: { name: "Permit2", chainId: 8453, verifyingContract: PERMIT2 },
types: {
PermitTransferFrom: [
{ name: "permitted", type: "TokenPermissions" },
{ name: "spender", type: "address" },
{ name: "nonce", type: "uint256" },
{ name: "deadline", type: "uint256" },
],
TokenPermissions: [
{ name: "token", type: "address" },
{ name: "amount", type: "uint256" },
],
},
primaryType: "PermitTransferFrom",
message: {
permitted: { token: USDC, amount: repayCeiling },
spender: MIDNIGHT_BUNDLES,
nonce,
deadline,
},
});
// Variable to set the loanTokenPermit variable to (bundler argument) in the Execute step that follows
const loanTokenPermit2 = {
kind: 2, // 2 = Permit2
data: encodeAbiParameters(
parseAbiParameters("uint256 nonce, uint256 deadline, bytes signature"),
[nonce, deadline, sig],
),
} as const;
// --------------------------------------------------------------
// Authorize MidnightBundles to act on your position (one-time boolean, persists across future actions)
await walletClient.writeContract({
account: borrower,
address: MIDNIGHT,
abi: midnightAbi,
functionName: "setIsAuthorized",
args: [MIDNIGHT_BUNDLES, true, borrower],
});
Execute
import { midnightBundlesAbi } from "@morpho-org/midnight-sdk";
// Release the full balance of each collateral:
// The position lists collateral by { token, amount } and the contract wants an INDEX into market.collateralParams[], so resolve token → index here.
const collateralWithdrawals = position.collaterals.map((c: any) => ({
collateralIndex: BigInt(
market.collateralParams.findIndex(
(cp: any) => cp.token.toLowerCase() === c.token.toLowerCase()
)
),
assets: BigInt(c.amount),
}));
const repayAssets = BigInt(position.debt) + BigInt(position.pending_fee);
// Deadline: the transaction reverts if not included before this timestamp
const deadline = BigInt(Math.floor(Date.now() / 1000) + 3600); // 1 hour from now
const hash = await walletClient.writeContract({
account: borrower,
address: MIDNIGHT_BUNDLES,
abi: midnightBundlesAbi,
functionName: "midnightBundlesV1RepayAndWithdrawCollateral",
args: [
market, // Market struct (Step 1)
repayAssets, // loan tokens to repay (full debt)
borrower, // onBehalf — whose position is repaid
tokenPermitNone, // if Permit EIP-2612 or Permit2 is used use loanTokenPermitEIP2612 or loanTokenPermit2, respectively
collateralWithdrawals, // CollateralWithdrawal[] (release all collateral)
borrower, // collateralReceiver (where the WETH is sent)
0n, // referralFeePct (0 = none)
zeroAddress, // referralFeeRecipient (none)
deadline // deadline (reverts if tx is included after this timestamp)
],
});
console.log("Repay + withdraw tx:", hash);