> For the complete documentation index, see [llms.txt](https://docs.curvance.com/app/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.curvance.com/app/developer-docs/quick-start-guides/borrowing-and-repayment/repaying-debt.md).

# Repaying Debt

When you're ready to reduce or eliminate debt, repay through the same `BorrowableCToken` that tracks the debt. This page uses cUSDC in a `WETH | USDC` or `WBTC | USDC` isolated market as the example; the same pattern applies to any borrowable cToken. Source real addresses from the deployment registry for the chain you're on.

Repayment requires the payer to approve the cToken to spend the underlying asset, then call `repay(assets)` for their own debt or `repayFor(assets, owner)` to pay another account's debt.

***

### Before calling `repay()`

A repay reverts if any of these hold:

* **The debt owner is inside the 20-minute hold period:** posting collateral or borrowing starts `MIN_HOLD_PERIOD = 20 minutes`. During that window, `canRepayWithReview` calls `_checkHoldPeriod(owner)` and reverts with `MarketManager__MinimumHoldPeriod()`. This applies to the debt owner, not necessarily the payer, so it also gates `repayFor`.
* **No outstanding debt:** `repay(0)` and `repayFor(0, owner)` resolve `assets` to the owner's full debt. If that debt is zero, `_checkZeroAmount` reverts with `BaseCToken__ZeroAmount()`. If a nonzero repayment amount is submitted for an owner with zero debt, the cToken reverts with `BorrowableCToken__InvalidParameter()`.
* **The amount exceeds the owner's debt:** after interest accrues, `_repay` checks `assets > debtOf` and reverts with `BorrowableCToken__InvalidParameter()` for overpayment.
* **A partial repay would leave dust below `MIN_LOAN_SIZE`:** full repayment skips the loan-size price check because `newNetDebt == 0`. Partial repayment must leave an active debt amount large enough to pass the MarketManager's USD-denominated `MIN_LOAN_SIZE` check.
* **A required price read fails for partial repayment:** partial repayment uses the debt asset price to validate the remaining loan size. Full repayment skips that price path after the hold-period and active-position checks pass.
* **Approval or balance is too small:** the cToken pulls underlying with `SafeTransferLib.safeTransferFrom`. If the payer's allowance or balance is insufficient, the pull reverts with `TransferFromFailed()`.

***

### Setup

These examples use ethers v6, placeholder addresses, and plain `ethers.Contract` instances.

```js
import { ethers } from "ethers";

const provider = new ethers.JsonRpcProvider("YOUR_RPC_URL");
const wallet = new ethers.Wallet("YOUR_PRIVATE_KEY", provider);

const cUSDCAddress = "0x...";
const USDCAddress = "0x...";
const protocolReaderAddress = "0x...";

const cUSDC = new ethers.Contract(cUSDCAddress, BORROWABLE_CTOKEN_ABI, wallet);
const USDC = new ethers.Contract(USDCAddress, ERC20_ABI, wallet);

const protocolReader = new ethers.Contract(
  protocolReaderAddress,
  PROTOCOL_READER_ABI,
  provider
);

const marketManager = new ethers.Contract(
  await cUSDC.marketManager(),
  MARKET_MANAGER_ABI,
  provider
);
```

### Helper Functions

Use projected debt for full-repay approvals. The repay transaction accrues interest before computing the pull amount, so approving only the last-accrued debt can be too low by the time `repay(0)` executes.

```js
const MIN_HOLD_PERIOD_SECONDS = 20n * 60n;

async function assertRepayCooldownHasPassed(ownerAddress) {
  const latestBlock = await provider.getBlock("latest");
  if (latestBlock === null) throw new Error("Could not load latest block");

  // accountAssets(owner) returns the cooldown timestamp from the MarketManager.
  const cooldownStarted = await marketManager.accountAssets(ownerAddress);
  const cooldownEnds = cooldownStarted + MIN_HOLD_PERIOD_SECONDS;

  if (cooldownEnds > BigInt(latestBlock.timestamp)) {
    throw new Error("Debt owner is inside the 20-minute hold period");
  }
}

async function projectedFullDebt(ownerAddress, bufferSeconds = 60) {
  const latestBlock = await provider.getBlock("latest");
  if (latestBlock === null) throw new Error("Could not load latest block");

  return protocolReader.debtBalanceAtTimestamp(
    ownerAddress,
    cUSDCAddress,
    latestBlock.timestamp + bufferSeconds
  );
}

async function approveUSDCIfNeeded(payerAddress, approvalAmount) {
  if (approvalAmount === 0n) throw new Error("No outstanding debt to repay");

  const currentAllowance = await USDC.allowance(payerAddress, cUSDCAddress);
  if (currentAllowance < approvalAmount) {
    const approveTx = await USDC.approve(cUSDCAddress, approvalAmount);
    await approveTx.wait();
  }
}
```

The buffer is a UX guard for ordinary accrual drift between approval and repayment. It is still not a protocol guarantee. For the simplest full-repay flow, approve `ethers.MaxUint256` once if that matches your risk model.

***

### Repaying Your Own Debt

```js
async function repayUSDCDebt(amount) {
  const userAddress = await wallet.getAddress();
  const USDCDecimals = await USDC.decimals();

  const isFullRepay = amount === "full";
  const repayArg = isFullRepay
    ? 0n
    : ethers.parseUnits(amount.toString(), USDCDecimals);

  if (!isFullRepay && repayArg === 0n) {
    throw new Error("Repay amount must be greater than zero");
  }

  await assertRepayCooldownHasPassed(userAddress);

  const approvalAmount = isFullRepay
    ? await projectedFullDebt(userAddress)
    : repayArg;

  await approveUSDCIfNeeded(userAddress, approvalAmount);

  // Pass 0 to clear the entire outstanding balance after on-chain accrual.
  const repayTx = await cUSDC.repay(repayArg);
  const receipt = await repayTx.wait();

  console.log(`Repaid ${isFullRepay ? "full debt" : amount + " USDC"}`);
  return receipt;
}
```

If a user intends to fully close the position, prefer `repayUSDCDebt("full")` over passing a quoted debt amount. Passing the last-read debt as a nonzero amount can leave a small residual after interest accrues, and that residual can fail the `MIN_LOAN_SIZE` check.

#### The `repay(0)` shortcut

Curvance treats `repay(0)` as "repay the entire outstanding debt." Internally, `_repay` accrues interest, reads the owner's fresh debt with `debtBalance(owner)`, replaces `assets == 0` with that debt, and pulls exactly that amount from the payer

This is the recommended pattern when the user wants to fully exit a debt position:

* The cToken accrues interest before computing the repayment amount.
* The payer still needs underlying approval covering the freshly accrued debt. Use a projected debt approval, a small buffer, or a max approval.
* The full-repay path passes `newNetDebt == 0` into `canRepayWithReview`, so it skips the residual loan-size price check.

***

### Repaying Debt on Behalf of Others (`repayFor`)

Anyone can repay anyone else's debt without delegation or permission from the borrower. `repayFor(assets, owner)` pulls underlying from `msg.sender` and applies it to `owner`'s debt.

Use cases:

* Helping a friend avoid liquidation.
* Treasury management across multiple wallets in a DAO or organization.
* Liquidation-avoidance automation or third-party insurance services.
* Complex DeFi strategies that unwind positions on behalf of users.

```js
async function repayForAccount(ownerAddress, amount) {
  const payerAddress = await wallet.getAddress();
  const USDCDecimals = await USDC.decimals();

  const isFullRepay = amount === "full";
  const repayArg = isFullRepay
    ? 0n
    : ethers.parseUnits(amount.toString(), USDCDecimals);

  if (!isFullRepay && repayArg === 0n) {
    throw new Error("Repay amount must be greater than zero");
  }

  await assertRepayCooldownHasPassed(ownerAddress);

  const approvalAmount = isFullRepay
    ? await projectedFullDebt(ownerAddress)
    : repayArg;

  await approveUSDCIfNeeded(payerAddress, approvalAmount);

  // Argument order is (assets, owner). Passing 0 fully repays owner debt.
  const repayTx = await cUSDC.repayFor(repayArg, ownerAddress);
  const receipt = await repayTx.wait();

  console.log(
    `Repaid ${isFullRepay ? "full debt" : amount + " USDC"} for ${ownerAddress}`
  );
  return receipt;
}
```

{% hint style="info" %}
**Hold-period note:** `repayFor` checks the owner's cooldown, not the payer's. If the target account posted collateral or borrowed in the last 20 minutes, `repayFor` reverts with `MarketManager__MinimumHoldPeriod()` even when the payer is a separate account. This is a protocol check against flash-loan and multi-block manipulation, not a permission check.
{% endhint %}

***

### Common error scenarios

| Error                                      | Meaning                                                                                                                                                                   |
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `MarketManager__MinimumHoldPeriod()`       | The debt owner is inside the 20-minute cooldown after posting collateral or borrowing.                                                                                    |
| `BaseCToken__ZeroAmount()`                 | `repay(0)` or `repayFor(0, owner)` resolved to zero because the owner has no outstanding debt.                                                                            |
| `BorrowableCToken__InvalidParameter()`     | Nonzero repayment amount exceeds the owner's freshly accrued debt, or the repayment input is otherwise invalid for the cToken.                                            |
| `LiquidityManager__InsufficientLoanSize()` | Partial repayment would leave remaining debt below `MIN_LOAN_SIZE`. Use full repayment or choose a smaller partial repayment.                                             |
| `MarketManager__PriceError()`              | A price read required for the partial-repay residual loan-size check failed. Full repayment skips this price check after the hold-period and active-position checks pass. |
| `TransferFromFailed()`                     | The payer's underlying transfer failed, usually because allowance or balance is insufficient.                                                                             |

For the recommended ethers v6 revert-decoding pattern using `iface.parseError`, see Borrow: Error Handling.

***


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.curvance.com/app/developer-docs/quick-start-guides/borrowing-and-repayment/repaying-debt.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
