Tutorials

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:

  1. Authorized MidnightBundles on the Midnight contract (so it can act on the positio on their behalf).
  2. Approved MidnightBundles to 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);

On this page