> 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/leverage/deleveraging.md).

# Deleveraging

### Understanding Deleveraging

Deleveraging reduces leverage inside one isolated market by withdrawing cToken underlying through a PositionManager, converting the collateral-side asset into the debt asset, repaying the account's debt, and returning eligible leftovers to the owner.

This page covers two common flows:

* **Partial deleverage:** reduce debt and collateral while keeping the position active.
* **Full unwind:** repay the target debt position and remove as much of the selected collateral position as possible.

This page assumes you already read Leverage, which covers `DeleverageAction`, `SwapperLib.Swap`, swap calldata rules, `auxData`, PositionManager fees, and position-health reads.

***

### 1. Preparing for Deleveraging

Before building calldata, identify the exact market and manager path:

1. Understand your current position. Check posted collateral shares with `cToken.collateralPosted(account)`, convert shares to underlying with `previewRedeem` when sizing a full unwind, read fresh debt with `debtBalanceUpdated.staticCall(account)`, and inspect position health with `ProtocolReader.getPositionHealth(...)`.<br>

   For full-unwind sizing, `ProtocolReader.getLeverageSnapshot(account, cToken, borrowableCToken, bufferTime)` can return projected `debtTokenBalance` using a `bufferTime`. Treat `oracleError` as a stop condition.
2. Confirm both cTokens are listed in that market with `await marketManager.isListed(cToken)`.
3. Read fresh debt with `await debtCToken.debtBalanceUpdated.staticCall(userAddress)`, or use `ProtocolReader.getLeverageSnapshot(...)` with a `bufferTime` for projected full-unwind sizing.
4. Remember that `collateralAssets` is an underlying-asset amount for `action.cToken`, not cToken shares. The cToken converts that amount into shares during `withdrawByPositionManager`.
5. Set three slippage controls: the aggregator route's own min-out, each `swapAction.slippage` for `SwapperLib._swapSafe`, and the top-level PositionManager `slippage` checked by `checkSlippage`.
6. Compute `collateralAssets` in `cToken` underlying units, not shares.

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

const MARKET_MANAGER = "0x...";
const POSITION_MANAGER = "0x...";
const COLLATERAL_CTOKEN = "0x...";
const DEBT_CTOKEN = "0x...";
const APPROVED_SWAP_TARGET = "0x...";
const PROTOCOL_READER = "0x...";

const COLLATERAL_DECIMALS = 18;
const DEBT_DECIMALS = 6;
const BPS = 10_000n;
const ONE_PERCENT_WAD = ethers.parseUnits("0.01", 18);

const MARKET_MANAGER_ABI = [
  "function isPositionManager(address positionManager) view returns (bool)",
  "function isListed(address cToken) view returns (bool)",
  "function redeemPaused() view returns (uint8)"
];

const POSITION_MANAGER_ABI = [
  "function deleverage((address cToken,uint256 collateralAssets,address borrowableCToken,uint256 repayAssets,(address inputToken,uint256 inputAmount,address outputToken,address target,uint256 slippage,bytes call)[] swapActions,bytes auxData) action,uint256 slippage)",
  "function deleverageFor((address cToken,uint256 collateralAssets,address borrowableCToken,uint256 repayAssets,(address inputToken,uint256 inputAmount,address outputToken,address target,uint256 slippage,bytes call)[] swapActions,bytes auxData) action,address account,uint256 slippage)",
  "function centralRegistry() view returns (address)",
  "function setDelegateApproval(address delegate,bool isApproved)"
];

const CTOKEN_ABI = [
  "function asset() view returns (address)",
  "function balanceOf(address account) view returns (uint256)",
  "function collateralPosted(address account) view returns (uint256)",
  "function previewRedeem(uint256 shares) view returns (uint256)",
  "function previewWithdraw(uint256 assets) view returns (uint256)"
];

const DEBT_CTOKEN_ABI = [
  ...CTOKEN_ABI,
  "function debtBalance(address account) view returns (uint256)",
  "function debtBalanceUpdated(address account) returns (uint256)"
];

const CENTRAL_REGISTRY_ABI = [
  "function protocolLeverageFee() view returns (uint256)"
];

const PROTOCOL_READER_ABI = [
  "function getPositionHealth(address mm,address account,address cToken,address borrowableCToken,bool isDeposit,uint256 collateralAssets,bool isRepayment,uint256 debtAssets,uint256 bufferTime) view returns (uint256,bool)",
  "function getLeverageSnapshot(address account,address cToken,address borrowableCToken,uint256 bufferTime) view returns (uint256 collateralUsd,uint256 debtUsd,uint256 collateralAssetPrice,uint256 sharePrice,uint256 debtAssetPrice,uint256 debtTokenBalance,bool oracleError)"
];

const marketManager = new ethers.Contract(
  MARKET_MANAGER,
  MARKET_MANAGER_ABI,
  provider
);
const positionManager = new ethers.Contract(
  POSITION_MANAGER,
  POSITION_MANAGER_ABI,
  signer
);
const collateralCToken = new ethers.Contract(
  COLLATERAL_CTOKEN,
  CTOKEN_ABI,
  provider
);
const debtCToken = new ethers.Contract(
  DEBT_CTOKEN,
  DEBT_CTOKEN_ABI,
  provider
);
const protocolReader = new ethers.Contract(
  PROTOCOL_READER,
  PROTOCOL_READER_ABI,
  provider
);

const userAddress = await signer.getAddress();
```

Run basic prechecks before quoting the route:

```js
const [
  isPositionManager,
  collateralListed,
  debtListed,
  redeemPaused,
  currentDebt
] = await Promise.all([
  marketManager.isPositionManager(POSITION_MANAGER),
  marketManager.isListed(COLLATERAL_CTOKEN),
  marketManager.isListed(DEBT_CTOKEN),
  marketManager.redeemPaused(),
  debtCToken.debtBalanceUpdated.staticCall(userAddress)
]);

if (!isPositionManager) throw new Error("PositionManager is not enabled");
if (!collateralListed) throw new Error("Collateral cToken is not listed");
if (!debtListed) throw new Error("Debt cToken is not listed");
if (redeemPaused === 2n) throw new Error("Redemption is paused for this market");
if (currentDebt === 0n) throw new Error("No debt to repay on this cToken");
```

### 2. Understand `repayAssets`

`DeleverageAction.repayAssets` is a minimum debt-asset balance floor after the deleverage swap path. It is not always the exact final repayment amount.

**The PositionManager flow is:**

1. Withdraw `action.collateralAssets` of `action.cToken` underlying from the owner through `withdrawByPositionManager`.
2. Apply `centralRegistry.protocolLeverageFee()` to the redeemed collateral asset before the manager-specific swap or exit logic.
3. Execute the manager-specific conversion from collateral-side asset into debt asset.
4. Require the PositionManager's debt-asset balance to be at least `action.repayAssets`.
5. Read fresh debt with `borrowableCToken.debtBalanceUpdated(owner)`.
6. Repay `min(assetsHeld, currentDebt)`.

`repayAssets = 0` reverts with `BasePositionManager__InvalidAmount()`. If the swap path produces less debt asset than `repayAssets`, the action reverts with `BasePositionManager__InsufficientAssetsForRepayment()`.

{% hint style="info" %}
If you over-quote a full unwind, the PositionManager caps the repayment at the current debt and returns remaining debt asset to the owner. The floor can still cause an avoidable revert if it is higher than the amount the route actually returns, so set it from a fresh quote plus a deliberate buffer.
{% endhint %}

***

### 3. Constructing the `DeleverageAction` struct

In `SimplePositionManager`, deleverage requires exactly one swap when the collateral asset and debt asset differ. The swap must be:

* `inputToken = action.cToken.asset()`.
* `outputToken = action.borrowableCToken.asset()`.
* `inputAmount = action.collateralAssets` after the PositionManager fee.
* `target` set to an approved swap target with a Central Registry calldata checker.
* `call` encoded so the swap output recipient is the PositionManager.

```js
function mulDivUp(a, b, denominator) {
  return (a * b + denominator - 1n) / denominator;
}

async function netAfterPositionManagerFee(positionManagerContract, grossAmount) {
  const centralRegistryAddress = await positionManagerContract.centralRegistry();
  const centralRegistry = new ethers.Contract(
    centralRegistryAddress,
    CENTRAL_REGISTRY_ABI,
    provider
  );

  const feeBps = await centralRegistry.protocolLeverageFee();
  const fee = mulDivUp(grossAmount, feeBps, BPS);
  if (fee >= grossAmount) throw new Error("PositionManager fee exceeds amount");
  return grossAmount - fee;
}
```

```js
const collateralAssets = ethers.parseUnits("300", COLLATERAL_DECIMALS);
const netCollateralToSwap = await netAfterPositionManagerFee(
  positionManager,
  collateralAssets
);
const minDebtOut = ethers.parseUnits("200", DEBT_DECIMALS);

const [collateralAsset, debtAsset] = await Promise.all([
  collateralCToken.asset(),
  debtCToken.asset()
]);

const netCollateralToSwap = await netAfterPositionManagerFee(
  positionManager,
  collateralAssets
);

// Aggregator calldata must send output to POSITION_MANAGER.
const swapCalldata = "0x...";

const swapActions = [
  {
    inputToken: collateralAsset,
    inputAmount: netCollateralToSwap,
    outputToken: debtAsset,
    target: APPROVED_SWAP_TARGET,
    slippage: ethers.parseUnits("0.01", 18),
    call: swapCalldata
  }
];

const deleverageAction = {
  cToken: COLLATERAL_CTOKEN,
  collateralAssets,
  borrowableCToken: DEBT_CTOKEN,
  repayAssets: minDebtOut,
  swapActions,
  auxData: "0x"
};
```

The supplied draft used `collateralAssets = 300` and `swapActions[0].inputAmount = 250` without tying the difference to the PositionManager fee. For the simple path, that mismatch reverts unless the post-fee amount is exactly `250`.

### 4. Simulate Position Health

Use `ProtocolReader.getPositionHealth(...)` to inspect the current account or simulate the collateral and debt changes from a deleverage.

```js
const [positionHealth, oracleError] =
  await protocolReader.getPositionHealth(
    MARKET_MANAGER,
    userAddress,
    COLLATERAL_CTOKEN,
    DEBT_CTOKEN,
    false,
    collateralAssets,
    true,
    minDebtOut,
    0n
  );

if (oracleError) throw new Error("Oracle error while simulating position");
if (positionHealth < ethers.parseUnits("1", 18)) {
  throw new Error("Projected position is below soft liquidation health");
}
```

{% hint style="info" %}
For full-unwind sizing, `getLeverageSnapshot(account, cToken, borrowableCToken, bufferTime)` returns a projected `debtTokenBalance`. Use a small `bufferTime` when you want to account for expected interest accrual before execution.
{% endhint %}

```js
const BUFFER_TIME_SECONDS = 120n;

const leverageSnapshot = await protocolReader.getLeverageSnapshot(
  userAddress,
  COLLATERAL_CTOKEN,
  DEBT_CTOKEN,
  BUFFER_TIME_SECONDS
);

const projectedDebt = leverageSnapshot.debtTokenBalance;
if (leverageSnapshot.oracleError) {
  throw new Error("Oracle error while sizing full deleverage");
}
```

***

### Executing the Deleverage Operation

```js
// Whole-action slippage tolerance, WAD-scaled. 1e16 = 1%.
const slippage = ethers.parseUnits('0.01', 18);

const tx = await positionManager.deleverage(
  deleverageAction,
  slippage,
  { gasLimit: 2_000_000n }  // optional overrides, not a positional arg
);

const receipt = await tx.wait();
console.log(`Position deleveraged. Tx hash: ${receipt.hash}`);
```

***

### Full vs Partial Deleverage

Both flows use the same `deleverage(action, slippage)` entrypoint; the difference is in `collateralAssets` and `repayAssets`:

* **Partial:** set `collateralAssets` to a fraction of your posted collateral and `repayAssets` to your minimum expected swap output. The debt position remains active afterward.
* **Full:** set `collateralAssets` to your full posted collateral and `repayAssets` to at least your current debt. After the swaps, the PositionManager caps the actual repay at `min(assetsHeld, currentDebt)`, so any surplus debt-asset is refunded. If the swap under-delivers, the action reverts entirely, leaving your position intact.

{% hint style="info" %}
**Tip for full unwinds:** because `repayAssets = min(assetsHeld, currentDebt)` on-chain, you can pass a slightly-more-than-debt `repayAssets` as the floor without worrying about "overpaying": the protocol won't accept a payment larger than the debt and will refund any surplus. Use `await cDebt.debtBalanceUpdated.staticCall(userAddress)` to get the fresh current debt for this calc, then add a small buffer (1 to 2%) to account for interest accruing during your tx's mempool dwell time.
{% endhint %}

***

### Important Considerations

#### Swap data configuration

* `swapActions` is an array, but multi-hop support depends on the specific PositionManager.
* Each entry must set its aggregator calldata `recipient` to the PositionManager so the output lands where the next step (or the repayment) can consume it.
* Per-swap `slippage` is separate from the whole-action `slippage` passed to `deleverage(...)`. The former bounds individual hops; the latter enforces overall position-value preservation via the `checkSlippage` modifier.

#### Amount calculations

* `collateralAssets` is expressed in the **underlying decimals**, not cToken share decimals. Internally the cToken converts to shares on your behalf.
* For a full-unwind flow, read fresh debt with `debtBalanceUpdated.staticCall` (see the Integration Cookbook: Stale vs Fresh Reads), then buffer for interest that will accrue during the transaction's mempool time.
* Aggregator routes change continuously; fetch fresh swap calldata close to broadcast time.

#### Leftovers are refunded to the user

The base deleverage callback sweeps remaining debt asset, remaining collateral asset, and balances of each swapActions\[i].outputToken back to the owner. Simple over-delivery in the debt asset is therefore refunded after repayment is capped at current debt.

Do not treat the PositionManager as user-specific custody. PositionManagers are router-style contracts, and arbitrary stranded balances outside the swept tokens are not a user custody guarantee.

#### Delegated deleverage

For platforms deleveraging on behalf of users, call `deleverageFor(DeleverageAction action, address account, uint256 slippage)`.

The account must approve the delegate on the PositionManager contract by calling `setDelegateApproval(delegate, true)` on that PositionManager. This is PositionManager delegation, not cToken delegation. cToken delegation powers cToken-specific `*For` functions such as `depositAsCollateralFor`, `postCollateralFor`, `redeemFor`, and `removeCollateralFor`.

#### Hold-period interaction

Posting collateral or borrowing starts or resets the 20-minute `MIN_HOLD_PERIOD` for the account. Deleveraging during that window can revert.

In the deleverage path, `repayFor` calls `MarketManager.canRepayWithReview(...)`, which checks the owner's hold period. The final redemption review also checks transfer and redeem eligibility through the `MarketManager`. Wait for the cooldown to clear before retrying a deleverage that follows a recent collateral post or borrow.

#### Protocol-specific features

* Some markets require populated `auxData` (Pendle PT, for instance). See the auxiliary-swap example in the Leveraging intro for the Pendle shape.
* For straightforward markets (USDC / WETH / WBTC), `auxData = '0x'`.

***
