Skip to content

Repay & Withdraw (T3 Vault)

T3 vaults use single-token collateral (e.g. ETH) and dual-token debt (e.g. USDC/USDT). Use the NFT API (fetchNft) to get position and vault data. Supports both operate() (token-amount based) and operatePerfect() (share-based).

Helper: calculateDebtSharesMinMax (payback)

For payback: based on your initial borrow, pass one token amount if you want to repay entirely in that token, or pass both amounts in any desired split. Use depositAndBorrow: false.

Helper: calculatePerfectDebtAmounts (payback)

Pass borrow0AmountWei (or borrow1AmountWei) only; the other amount is auto-computed from pool proportion. Use paybackAndWithdraw: true.

Repay Debt

  • Approve the vault to spend both borrow tokens (e.g. USDC, USDT). Use a 1% buffer for accrued interest:
    typescript
    const approveAmt0 = (repay0Wei * 101n) / 100n;
    const approveAmt1 = (repay1Wei * 101n) / 100n;

Exact amounts: operate()

For exact token amounts, use calculateDebtSharesMinMax with depositAndBorrow: false to get debtSharesMinMax, then pass negative amounts:

typescript
const nft = await fetchNft(chainId, nftId);
const debtResult = calculateDebtSharesMinMax({
  vault: nft.vault,
  borrow0AmountInWei: repay0Wei.toString(),
  borrow1AmountInWei: repay1Wei.toString(),
  borrowSlippage: SLIPPAGE,
  depositAndBorrow: false,
});

// operate() with negative values for repay-and-withdraw
args: [
  nftId,
  -withdrawColWei,
  -repay0Wei,
  -repay1Wei,
  BigInt(debtResult.sharesWithSlippage),
  accountAddress
]

Exact amounts: operatePerfect()

Use calculatePerfectDebtAmounts for perfectDebtShares, and negative token min/max for slippage:

typescript
const debtResult = calculatePerfectDebtAmounts({
  vault: nft.vault,
  borrowSlippage: SLIPPAGE,
  paybackAndWithdraw: true,
  proportion: "fixed",
  borrow0AmountWei: repay0Wei.toString(),
  borrow1AmountWei: "",  // auto-computed from pool proportion
});

const perfectDebtShares = -BigInt(debtResult.shares);
const debtToken0MinMax = -BigInt(debtResult.token0AmtWithSlippage);
const debtToken1MinMax = -BigInt(debtResult.token1AmtWithSlippage);

args: [nftId, -withdrawColWei, perfectDebtShares, debtToken0MinMax, debtToken1MinMax, accountAddress]

Max (close position): operatePerfect()

For full close with operatePerfect(), use minInt for collateral and debt shares:

  • newCol = minInt
  • perfectDebtShares = minInt
  • debtToken0MinMax = -paybackResult.token0AmtWithSlippage
  • debtToken1MinMax = -paybackResult.token1AmtWithSlippage

Examples

Exact amounts with operate():

ts
/**
 * Example: Repay borrowed USDC + USDT and withdraw ETH collateral (exact amounts) using operate().
 *
 * Flow:
 *  (1) Fetch NFT, compute debt shares for repay via calculateDebtSharesMinMax.
 *  (2) Approve vault to spend USDC and USDT for repay.
 *  (3) Call operate() with negative newCol, newDebtToken0, newDebtToken1, debtSharesMinMax.
 * Set PRIVATE_KEY and RPC_URL in .env.
 *
 * Run: pnpm run borrow:t3-withdraw-repay -- --network arbitrum
 */

import { encodeFunctionData, erc20Abi, formatUnits, parseUnits } from "viem";
import { vaultT3Abi } from "../shared/abis/vaultT3Abi.js";
import {
  account,
  chain,
  publicClient,
  walletClient,
} from "../shared/config.js";
import { fetchNft } from "../shared/utils/utils.js";
import { calculateDebtSharesMinMax } from "../shared/utils/dex/shares.js";

const SLIPPAGE = 0.1;

// Set your NFT position ID here
const NFT_ID = "";

// Exact amounts
const withdrawColWei = parseUnits("0.000100086894324", 18); // ETH
const repay0Wei = parseUnits("0.2", 6); // USDC
const repay1Wei = parseUnits("0.01", 6); // USDT

async function main() {
  if (!NFT_ID.trim()) {
    console.error("Set NFT_ID in this file");
    process.exit(1);
  }
  const nftId = BigInt(NFT_ID.trim());

  const nft = await fetchNft(chain.id, NFT_ID.trim());
  const vault = nft.vault;
  const vaultAddr = vault.address as `0x${string}`;
  if (!vault.borrowToken.token1)
    throw new Error("T3 vault must have borrowToken.token1");

  // calculateDebtSharesMinMax (payback): pass one token if repaying all from it, or both in any split
  const debtResult = calculateDebtSharesMinMax({
    vault,
    supply0AmountInWei: "",
    supply1AmountInWei: "",
    borrow0AmountInWei: repay0Wei.toString(),
    borrow1AmountInWei: "",
    supplySlippage: 0,
    borrowSlippage: SLIPPAGE,
    depositAndBorrow: false,
  });
  if (!debtResult) throw new Error("calculateDebtSharesMinMax failed");

  const newCol = -withdrawColWei;
  const newDebtToken0 = -repay0Wei;
  const newDebtToken1 = -repay1Wei;
  const debtSharesMinMax = BigInt(debtResult.sharesWithSlippage);

  console.log("T3 operate: repay & withdraw (exact amounts, Arbitrum)");
  console.log("  Vault:", vaultAddr);
  console.log("  NFT ID:", nftId.toString());
  console.log(
    "  Repay:",
    formatUnits(repay0Wei, vault.borrowToken.token0.decimals),
    vault.borrowToken.token0.symbol,
    "+",
    formatUnits(repay1Wei, vault.borrowToken.token1.decimals),
    vault.borrowToken.token1.symbol,
  );
  console.log(
    "  Withdraw col:",
    formatUnits(withdrawColWei, vault.supplyToken.token0.decimals),
    vault.supplyToken.token0.symbol,
  );

  const approveAmt0 = (repay0Wei * 101n) / 100n;
  const approveAmt1 = (repay1Wei * 101n) / 100n;

  const approve = async (token: `0x${string}`, amount: bigint) => {
    const data = encodeFunctionData({
      abi: erc20Abi,
      functionName: "approve",
      args: [vaultAddr, amount],
    });
    const hash = await walletClient.sendTransaction({
      to: token,
      data,
      account,
    });
    await publicClient.waitForTransactionReceipt({ hash });
    console.log("  Approve tx:", hash);
  };

  await approve(vault.borrowToken.token0.address as `0x${string}`, approveAmt0);
  await approve(
    vault.borrowToken.token1!.address as `0x${string}`,
    approveAmt1,
  );

  const operateData = encodeFunctionData({
    abi: vaultT3Abi,
    functionName: "operate",
    args: [
      nftId,
      newCol,
      newDebtToken0,
      newDebtToken1,
      debtSharesMinMax,
      account.address,
    ],
  });

  const hash = await walletClient.sendTransaction({
    to: vaultAddr,
    data: operateData,
    value: 0n,
    account,
  });

  console.log("  Operate tx:", hash);
  console.log("Explorer:", `${chain.blockExplorers?.default?.url}/tx/${hash}`);

  const receipt = await publicClient.waitForTransactionReceipt({ hash });
  if (receipt.status !== "success") {
    console.error("Operate tx reverted");
    process.exit(1);
  }
  console.log("Position updated. USDC + USDT repaid, ETH withdrawn.");
}

main().catch((e) => {
  console.error(e);
  process.exit(1);
});

Exact amounts with operatePerfect():

ts
/**
 * Example: Repay borrowed USDC + USDT and withdraw ETH collateral (exact amounts) using operatePerfect().
 *
 * Flow:
 *  (1) Fetch NFT, compute debt amounts for repay via calculatePerfectDebtAmounts.
 *  (2) Approve vault to spend USDC and USDT for repay.
 *  (3) Call operatePerfect() with negative newCol, perfectDebtShares, debtToken0MinMax, debtToken1MinMax.
 * Set PRIVATE_KEY and RPC_URL in .env.
 *
 * Run: pnpm run borrow:t3-withdraw-repay-perfect -- --network arbitrum
 */

import { encodeFunctionData, erc20Abi, formatUnits, parseUnits } from "viem";
import { vaultT3Abi } from "../shared/abis/vaultT3Abi.js";
import {
  account,
  chain,
  publicClient,
  walletClient,
} from "../shared/config.js";
import { fetchNft } from "../shared/utils/utils.js";
import { calculatePerfectDebtAmounts } from "../shared/utils/dex/perfectAmounts.js";

const SLIPPAGE = 0.1;

// Set your NFT position ID here
const NFT_ID = "";

// Exact amounts
const withdrawColWei = parseUnits("0.001", 18); // ETH
const repay0Wei = parseUnits("0.001034", 6); // USDC
const repay1Wei = parseUnits("0.001007", 6); // USDT

async function main() {
  if (!NFT_ID.trim()) {
    console.error("Set NFT_ID in this file");
    process.exit(1);
  }
  const nftId = BigInt(NFT_ID.trim());

  const nft = await fetchNft(chain.id, NFT_ID.trim());
  const vault = nft.vault;
  const vaultAddr = vault.address as `0x${string}`;
  if (!vault.borrowToken.token1)
    throw new Error("T3 vault must have borrowToken.token1");

  // calculatePerfectDebtAmounts: pass only borrow0; borrow1 auto-computed from pool proportion
  const debtResult = calculatePerfectDebtAmounts({
    vault,
    borrowSlippage: SLIPPAGE,
    paybackAndWithdraw: true,
    proportion: "fixed",
    borrow0AmountWei: repay0Wei.toString(),
    borrow1AmountWei: "",
  });
  if (!debtResult) throw new Error("calculatePerfectDebtAmounts failed");

  const newCol = -withdrawColWei;
  const perfectDebtShares = -BigInt(debtResult.shares);
  const debtToken0MinMax = -BigInt(debtResult.token0AmtWithSlippage);
  const debtToken1MinMax = -BigInt(debtResult.token1AmtWithSlippage);

  console.log("T3 operatePerfect: repay & withdraw (exact amounts, Arbitrum)");
  console.log("  Vault:", vaultAddr);
  console.log("  NFT ID:", nftId.toString());
  console.log(
    "  Repay:",
    formatUnits(repay0Wei, vault.borrowToken.token0.decimals),
    vault.borrowToken.token0.symbol,
    "+",
    formatUnits(repay1Wei, vault.borrowToken.token1.decimals),
    vault.borrowToken.token1.symbol,
  );
  console.log(
    "  Withdraw col:",
    formatUnits(withdrawColWei, vault.supplyToken.token0.decimals),
    vault.supplyToken.token0.symbol,
  );

  const approveAmt0 = (repay0Wei * 101n) / 100n;
  const approveAmt1 = (repay1Wei * 101n) / 100n;

  const approve = async (token: `0x${string}`, amount: bigint) => {
    const data = encodeFunctionData({
      abi: erc20Abi,
      functionName: "approve",
      args: [vaultAddr, amount],
    });
    const hash = await walletClient.sendTransaction({
      to: token,
      data,
      account,
    });
    await publicClient.waitForTransactionReceipt({ hash });
    console.log("  Approve tx:", hash);
  };

  await approve(vault.borrowToken.token0.address as `0x${string}`, approveAmt0);
  await approve(
    vault.borrowToken.token1!.address as `0x${string}`,
    approveAmt1,
  );

  const operateData = encodeFunctionData({
    abi: vaultT3Abi,
    functionName: "operatePerfect",
    args: [
      nftId,
      newCol,
      perfectDebtShares,
      debtToken0MinMax,
      debtToken1MinMax,
      account.address,
    ],
  });

  const hash = await walletClient.sendTransaction({
    to: vaultAddr,
    data: operateData,
    value: 0n,
    account,
  });

  console.log("  OperatePerfect tx:", hash);
  console.log("Explorer:", `${chain.blockExplorers?.default?.url}/tx/${hash}`);

  const receipt = await publicClient.waitForTransactionReceipt({ hash });
  if (receipt.status !== "success") {
    console.error("OperatePerfect tx reverted");
    process.exit(1);
  }
  console.log("Position updated. USDC + USDT repaid, ETH withdrawn.");
}

main().catch((e) => {
  console.error(e);
  process.exit(1);
});

Close position (operatePerfect):

ts
/**
 * Example: Repay all borrowed USDC + USDT and withdraw all ETH collateral (close position) using operatePerfect().
 *
 * Flow:
 *  (1) Fetch NFT, compute max payback amounts via calculatePerfectMaxPaybackAmounts.
 *  (2) Approve vault to spend USDC and USDT for max repay.
 *  (3) Call operatePerfect() with minInt for newCol and perfectDebtShares.
 * Set PRIVATE_KEY and RPC_URL in .env.
 *
 * Run: pnpm run borrow:t3-withdraw-repay-max-perfect -- --network arbitrum
 */

import { encodeFunctionData, erc20Abi, formatUnits } from "viem";
import { vaultT3Abi } from "../shared/abis/vaultT3Abi.js";
import {
  account,
  chain,
  publicClient,
  walletClient,
} from "../shared/config.js";
import { fetchNft } from "../shared/utils/utils.js";
import { calculatePerfectMaxPaybackAmounts } from "../shared/utils/dex/perfectAmounts.js";
import { minInt } from "../shared/utils/constants.js";

const SLIPPAGE = 0.1;

// Set your NFT position ID here
const NFT_ID = "";

async function main() {
  if (!NFT_ID.trim()) {
    console.error("Set NFT_ID in this file");
    process.exit(1);
  }
  const nftId = BigInt(NFT_ID.trim());

  const nft = await fetchNft(chain.id, NFT_ID.trim());
  const vault = nft.vault;
  const vaultAddr = vault.address as `0x${string}`;
  if (!vault.borrowToken.token1)
    throw new Error("T3 vault must have borrowToken.token1");

  // calculatePerfectMaxPaybackAmounts: computes max repay amounts for full close
  const paybackResult = calculatePerfectMaxPaybackAmounts({
    nft,
    borrowSlippage: SLIPPAGE,
  });
  if (!paybackResult)
    throw new Error("calculatePerfectMaxPaybackAmounts failed");

  const repay0Wei = BigInt(paybackResult.token0AmtWithSlippage);
  const repay1Wei = BigInt(paybackResult.token1AmtWithSlippage);
  const newCol = BigInt(minInt);
  const perfectDebtShares = BigInt(minInt);
  const debtToken0MinMax = -repay0Wei;
  const debtToken1MinMax = -repay1Wei;

  const approveAmt0 = (repay0Wei * 101n) / 100n;
  const approveAmt1 = (repay1Wei * 101n) / 100n;

  console.log("T3 operatePerfect: repay max & withdraw max (Arbitrum)");
  console.log("  Vault:", vaultAddr);
  console.log("  NFT ID:", nftId.toString());
  console.log(
    "  Repay: ~",
    formatUnits(repay0Wei, vault.borrowToken.token0.decimals),
    vault.borrowToken.token0.symbol,
    "+",
    formatUnits(repay1Wei, vault.borrowToken.token1.decimals),
    vault.borrowToken.token1.symbol,
  );

  const approve = async (token: `0x${string}`, amount: bigint) => {
    const data = encodeFunctionData({
      abi: erc20Abi,
      functionName: "approve",
      args: [vaultAddr, amount],
    });
    const hash = await walletClient.sendTransaction({
      to: token,
      data,
      account,
    });
    await publicClient.waitForTransactionReceipt({ hash });
    console.log("  Approve tx:", hash);
  };

  await approve(vault.borrowToken.token0.address as `0x${string}`, approveAmt0);
  await approve(
    vault.borrowToken.token1!.address as `0x${string}`,
    approveAmt1,
  );

  const operateData = encodeFunctionData({
    abi: vaultT3Abi,
    functionName: "operatePerfect",
    args: [
      nftId,
      newCol,
      perfectDebtShares,
      debtToken0MinMax,
      debtToken1MinMax,
      account.address,
    ],
  });

  const hash = await walletClient.sendTransaction({
    to: vaultAddr,
    data: operateData,
    value: 0n,
    account,
  });

  console.log("  OperatePerfect tx:", hash);
  console.log("Explorer:", `${chain.blockExplorers?.default?.url}/tx/${hash}`);

  const receipt = await publicClient.waitForTransactionReceipt({ hash });
  if (receipt.status !== "success") {
    console.error("OperatePerfect tx reverted");
    process.exit(1);
  }
  console.log("Position closed. USDC + USDT repaid, ETH withdrawn (max).");
}

main().catch((e) => {
  console.error(e);
  process.exit(1);
});