> 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.md).

# Leverage

Leveraging in Curvance lets an account increase exposure inside one isolated market by borrowing against posted collateral, swapping the borrowed asset, and depositing the output as more collateral.

Curvance's Position Managers wrap that borrow, swap, and deposit flow into one atomic transaction. The transaction still runs the same market checks for borrowability, caps, collateral value, price availability, and liquidity, but the user does not need to manually borrow, swap, deposit, check health, and repeat.

This overview sits above the Leveraging and Deleveraging subpages. It covers the shared structs, swap rules, environment setup, and health reads that both flows use.

For a deeper dive, see [Position Managers](/app/developer-docs/protocol-managers.md).

***

### Core Structures for Leveraging

#### `LeverageAction` (`IPositionManager.sol:25`)

```solidity
struct LeverageAction {
    IBorrowableCToken borrowableCToken; // cToken to borrow from.
    uint256 borrowAssets;               // Amount of underlying to borrow.
    ICToken cToken;                     // cToken that receives the swapped output as collateral.
    uint256 expectedShares;             // Minimum shares expected from the collateral deposit.
    SwapperLib.Swap swapAction;         // Swap from debt asset into collateral-side input.
    bytes auxData;                      // Optional PositionManager-specific data.
}
```

#### `DeleverageAction` (`IPositionManager.sol:47`)

```solidity
struct DeleverageAction {
    ICToken cToken;                     // cToken being unwound.
    uint256 collateralAssets;           // Amount of cToken underlying to withdraw.
    IBorrowableCToken borrowableCToken; // cToken whose debt will be repaid.
    uint256 repayAssets;                // Minimum debt asset amount required after swaps.
    SwapperLib.Swap[] swapActions;      // Swap path from collateral output into debt asset.
    bytes auxData;                      // Optional PositionManager-specific data.
}
```

{% hint style="info" %}
`repayAssets` is a minimum amount that must be held by the PositionManager after the deleverage swap path. The PositionManager then repays as much of the owner's debt as possible, up to the current debt balance, and sends any leftover debt or collateral asset back to the owner.
{% endhint %}

#### `SwapperLib.Swap` (`SwapperLib.sol:36`)

```solidity
struct Swap {
    address inputToken;   // Token being swapped from.
    uint256 inputAmount;  // Amount of inputToken to swap.
    address outputToken;  // Token being swapped to.
    address target;       // Swap router or aggregator address.
    uint256 slippage;     // WAD-scaled value-loss tolerance for _swapSafe.
    bytes call;           // Encoded call sent to target.
}
```

{% hint style="danger" %}
`LeverageAction` has one `swapAction`. `DeleverageAction` has `swapActions[]`, but multi-hop behavior depends on the specific PositionManager. For example, `SimplePositionManager` requires exactly one deleverage swap when the collateral and debt assets differ, while Pendle and LP-style managers can use multiple swaps after their protocol-specific exit step.
{% endhint %}

***

### Swap Rules

#### Leveraging

When leveraging, a swap converts the borrowed (debt) asset into the collateral asset. Populate the `call` field of `swapAction` with your aggregator's calldata (Kyber, 1inch, Odos, Paraswap, etc.).&#x20;

The swap output must be routed back to the PositionManager. `SwapperLib` calls the configured calldata checker with `expectedRecipient = address(this)`, so calldata with another recipient reverts.

`swapAction.target` must have an external calldata checker configured in the Central Registry. If `centralRegistry.externalCalldataChecker(swapAction.target)` is zero, `SwapperLib` reverts with `SwapperLib__UnknownCalldata()`.

#### Deleveraging

During deleveraging, swaps convert the collateral asset into the debt asset. The `call` bytes in each swap entry are calldata for `swapAction.target`; the `target` field is the router or aggregator address. The encoded swap recipient still needs to be the PositionManager.

Treat Position Managers as stateless routers. Do not leave user-specific balances on them between calls. For vault and multi-hop deleverage paths, encode each `swapAction.inputAmount` to match the amount that the previous step actually produces.

#### Auxiliary Data

Some assets require extra router-specific instructions to enter or exit. For example, entering a Pendle PT position requires calldata for Pendle's internal SY-mint flow in addition to the underlying  aggregator swap. It is empty for simple ERC20 leverage.

For Pendle PT leverage, `PendlePTPositionManager` decodes `auxData` as:

```solidity
(address lpToken, uint256 minPtAmount, PendleLib.PendleAction pendleAction)
```

The supplied draft encoded the Pendle PT tuple as `(PendleAction, address, uint256)`, which would not match the on-chain decoder. Keep the tuple order in sync with the specific PositionManager contract you call

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

const pendleLpMarketAddress = "0x...";
const minPtAmount = ethers.parseUnits("9.8", 18);
const INPUT_TOKEN_DECIMALS = 18;
const SY_INPUT_TOKEN_ADDRESS = "0x...";
const SY_MINT_TOKEN_ADDRESS = "0x...";
const PENDLE_SWAP_ADDRESS = "0x...";

const SWAP_DATA_TUPLE =
  "tuple(uint8 swapType,address extRouter,bytes extCalldata,bool needScale)";
const APPROX_TUPLE =
  "tuple(uint256 guessMin,uint256 guessMax,uint256 guessOffchain,uint256 maxIteration,uint256 eps)";
const TOKEN_INPUT_TUPLE =
  `tuple(address tokenIn,uint256 netTokenIn,address tokenMintSy,address pendleSwap,${SWAP_DATA_TUPLE} swapData)`;
const TOKEN_OUTPUT_TUPLE =
  `tuple(address tokenOut,uint256 minTokenOut,address tokenRedeemSy,address pendleSwap,${SWAP_DATA_TUPLE} swapData)`;

// Use empty limit-order arrays unless your Pendle route intentionally uses them.
const LIMIT_ORDER_DATA_TUPLE =
  "tuple(address limitRouter,uint256 epsSkipMarket,tuple(tuple(uint256 salt,uint256 expiry,uint256 nonce,uint8 orderType,address token,address YT,address maker,address receiver,uint256 makingAmount,uint256 lnImpliedRate,uint256 failSafeRate,bytes permit) order,bytes signature,uint256 makingAmount)[] normalFills,tuple(tuple(uint256 salt,uint256 expiry,uint256 nonce,uint8 orderType,address token,address YT,address maker,address receiver,uint256 makingAmount,uint256 lnImpliedRate,uint256 failSafeRate,bytes permit) order,bytes signature,uint256 makingAmount)[] flashFills,bytes optData)";

const PENDLE_ACTION_TUPLE =
  `tuple(${APPROX_TUPLE} approx,${TOKEN_INPUT_TUPLE} input,${TOKEN_OUTPUT_TUPLE} output,${LIMIT_ORDER_DATA_TUPLE} limit)`;

const pendleAction = {
  approx: {
    guessMin: ethers.parseUnits("9.9", 18),
    guessMax: ethers.parseUnits("10.0", 18),
    guessOffchain: 0n,
    maxIteration: 30n,
    eps: ethers.parseUnits("0.001", 18)
  },
  input: {
    tokenIn: SY_INPUT_TOKEN_ADDRESS,
    netTokenIn: ethers.parseUnits("10", INPUT_TOKEN_DECIMALS),
    tokenMintSy: SY_MINT_TOKEN_ADDRESS,
    pendleSwap: PENDLE_SWAP_ADDRESS,
    swapData: {
      swapType: 0, // NONE. Use another enum value only when Pendle should call its aggregator.
      extRouter: ethers.ZeroAddress,
      extCalldata: "0x",
      needScale: false
    }
  },
  output: {
    tokenOut: ethers.ZeroAddress,
    minTokenOut: 0n,
    tokenRedeemSy: ethers.ZeroAddress,
    pendleSwap: ethers.ZeroAddress,
    swapData: {
      swapType: 0,
      extRouter: ethers.ZeroAddress,
      extCalldata: "0x",
      needScale: false
    }
  },
  limit: {
    limitRouter: ethers.ZeroAddress,
    epsSkipMarket: 0n,
    normalFills: [],
    flashFills: [],
    optData: "0x"
  }
};

const abiCoder = ethers.AbiCoder.defaultAbiCoder();

// Pendle PT PositionManager leverage auxData:
const auxData = abiCoder.encode(
  ["address", "uint256", PENDLE_ACTION_TUPLE],
  [pendleLpMarketAddress, minPtAmount, pendleAction]
);
```

If the PositionManager first runs a Curvance `swapAction` before entering Pendle, `swapAction.outputToken` must match `pendleAction.input.tokenIn` for Pendle PT, or a valid SY input token for Pendle LP.

***

### Setting Up Your Environment

These examples use ethers v6, placeholder addresses, and plain `ethers.Contract` instances. Source real addresses from the deployment registry for the chain you're on.

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

const provider = new ethers.JsonRpcProvider("YOUR_RPC_URL");
const signer = new ethers.Wallet("YOUR_PRIVATE_KEY", provider);
const userAddress = await signer.getAddress();

const MARKET_MANAGER = "0x...";
const CENTRAL_REGISTRY = "0x...";
const POSITION_MANAGER = "0x...";
const C_TOKEN_COLLATERAL = "0x...";
const C_TOKEN_DEBT = "0x...";
const PROTOCOL_READER = "0x...";
const APPROVED_SWAP_TARGET = "0x...";

const ERC20_ABI = [
  "function allowance(address owner,address spender) view returns (uint256)",
  "function approve(address spender,uint256 amount) returns (bool)",
  "function decimals() view returns (uint8)"
];

const marketManager = new ethers.Contract(MARKET_MANAGER, MARKET_MANAGER_ABI, provider);
const centralRegistry = new ethers.Contract(CENTRAL_REGISTRY, CENTRAL_REGISTRY_ABI, provider);
const positionManager = new ethers.Contract(POSITION_MANAGER, POSITION_MANAGER_ABI, signer);
const collateralCToken = new ethers.Contract(C_TOKEN_COLLATERAL, CTOKEN_ABI, provider);
const debtCToken = new ethers.Contract(C_TOKEN_DEBT, CTOKEN_ABI, provider);
const protocolReader = new ethers.Contract(PROTOCOL_READER, PROTOCOL_READER_ABI, provider);
```

Real PositionManager function signatures (from `BasePositionManager.sol`):

```solidity
function depositAndLeverage(uint256 assets, LeverageAction action, uint256 slippage) external;
function leverage(LeverageAction action, uint256 slippage) external;
function leverageFor(LeverageAction action, address account, uint256 slippage) external;
function deleverage(DeleverageAction action, uint256 slippage) external;
```

`slippage` is WAD-scaled for the PositionManager's net position-value sanity check. A common UI value is `ethers.parseUnits("0.01", 18)` for 1%. The slippage check is enforced by a `checkSlippage` modifier that snapshots position value before and after and reverts if the loss exceeds the configured tolerance.

{% hint style="info" %}
`leverageFor` and `deleverageFor` (delegation variants) require the account to have pre-approved the caller via `setDelegateApproval` on the cTokens involved. See the [Integration Cookbook: Collateralizing on Behalf of Users ](/app/developer-docs/quick-start-guides/integration-cookbook.md#collateralizing-on-behalf-of-users)for the delegation model.
{% endhint %}

The PositionManager `checkSlippage` modifier snapshots `marketManager.statusOf(account)` before and after the action and reverts if the account's net value loss exceeds the supplied tolerance. The contract comments describe this as a sanity check, not a replacement for route quotes, min-out checks, `expectedShares`, or swap-level slippage checks.

PositionManager callbacks also apply `centralRegistry.protocolLeverageFee()` before the internal swap or vault operation. The top-level action amount still needs to match the amount borrowed or withdrawn, but the swap path should be quoted against the net amount after this fee.

***

### Simple PositionManager Flow

Use `SimplePositionManager` for generic ERC20-to-ERC20 leverage where the cToken's underlying asset is the final collateral asset. In this path, the debt asset and collateral asset must differ, and a leverage-in action always needs one swap.

Simple leverage-in rules:

* `action.borrowAssets` is the gross amount borrowed from `borrowableCToken`.
* `swapAction.inputAmount` is the net amount after the PositionManager fee.
* `swapAction.inputToken` is `action.borrowableCToken.asset()`.
* `swapAction.outputToken` is `action.cToken.asset()`.
* `auxData` is `0x`.

```js
const WAD = 10n ** 18n;
const ONE_PERCENT = ethers.parseUnits("0.01", 18);

async function assertLeveragePreconditions() {
  const [isPM, debtCap, [, , borrowPaused]] = await Promise.all([
    marketManager.isPositionManager(POSITION_MANAGER),
    marketManager.debtCaps(C_TOKEN_DEBT),
    marketManager.actionsPaused(C_TOKEN_DEBT)
  ]);

  if (!isPM) throw new Error("PositionManager is not enabled for this market");
  if (debtCap === 0n) throw new Error("Debt cToken is not borrowable in this market");
  if (borrowPaused) throw new Error("Borrowing is paused for the debt cToken");
}

async function buildSimpleLeverageAction({
  borrowAmount,
  quotedCollateralOut,
  swapCalldata
}) {
  await assertLeveragePreconditions();
  const netBorrowAmount = await netAfterPositionManagerFee(borrowAmount);

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

  const expectedShares = await collateralCToken.previewDeposit(quotedCollateralOut);

  return {
    borrowableCToken: C_TOKEN_DEBT,
    borrowAssets: borrowAmount,
    cToken: C_TOKEN_COLLATERAL,
    expectedShares,
    swapAction: {
      inputToken: debtAsset,
      inputAmount: netBorrowAmount,
      outputToken: collateralAsset,
      target: APPROVED_SWAP_TARGET,
      slippage: ONE_PERCENT,
      call: swapCalldata
    },
    auxData: "0x"
  };
}

async function leverageExistingPosition(action) {
  const tx = await positionManager.leverage(action, ONE_PERCENT);
  return tx.wait();
}
```

&#x20;`expectedShares` is enforced after the swap and deposit: if the deposit returns fewer shares than `action.expectedShares`, the PositionManager reverts with `BasePositionManager__InvalidSlippage()`.

Simple deleverage uses the same shape in reverse, but `DeleverageAction.swapActions` must contain exactly one swap. The top-level `collateralAssets` is the gross cToken underlying amount withdrawn through `withdrawByPositionManager`; the swap's `inputAmount` should be the net amount after the PositionManager fee.

***

### Vault PositionManager Flows

Vault PositionManagers handle cTokens whose underlying asset is a vault share or receipt token. The leverage-in shape is:

1. Borrow the debt asset.
2. Optionally swap the debt asset into the vault's deposit asset.
3. Deposit into the vault to receive vault shares or receipt tokens.
4. Deposit those vault shares or receipt tokens into the Curvance cToken as collateral.

#### Single-Sided Vaults

&#x20;`SingleSidedVaultPositionManager` handles non-native vault deposits. If the borrowed asset already matches the vault's underlying asset, it skips the Curvance swap and deposits directly into the vault. Otherwise it requires one swap from the debt asset into the vault underlying.

This manager inherits the simple deleverage path. That means deleverage swaps the vault share or receipt token through an aggregator, rather than redeeming the vault first. Use this shape for vault collateral that cannot be instantly redeemed on the deleverage path.

```js
const VAULT_ABI = [
  "function asset() view returns (address)"
];

function sameAddress(a, b) {
  return a.toLowerCase() === b.toLowerCase();
}

function emptySwapAction() {
  return {
    inputToken: ethers.ZeroAddress,
    inputAmount: 0n,
    outputToken: ethers.ZeroAddress,
    target: ethers.ZeroAddress,
    slippage: 0n,
    call: "0x"
  };
}

async function buildSingleSidedVaultLeverageAction({
  borrowAmount,
  vaultAddress,
  expectedVaultShares,
  swapCalldata
}) {
  await assertLeveragePreconditions();

  const [debtAsset, vaultUnderlying] = await Promise.all([
    debtCToken.asset(),
    new ethers.Contract(vaultAddress, VAULT_ABI, provider).asset()
  ]);

  const netBorrowAmount = await netAfterPositionManagerFee(borrowAmount);
  const needsSwap = !sameAddress(debtAsset, vaultUnderlying);

  const expectedShares = await collateralCToken.previewDeposit(expectedVaultShares);

  return {
    borrowableCToken: C_TOKEN_DEBT,
    borrowAssets: borrowAmount,
    cToken: C_TOKEN_COLLATERAL,
    expectedShares,
    swapAction: needsSwap
      ? {
          inputToken: debtAsset,
          inputAmount: netBorrowAmount,
          outputToken: vaultUnderlying,
          target: APPROVED_SWAP_TARGET,
          slippage: ONE_PERCENT,
          call: swapCalldata
        }
      : emptySwapAction(),
    auxData: "0x"
  };
}
```

`expectedVaultShares` should come from your route quote or simulation for the vault deposit. The cToken-level `expectedShares` then uses `previewDeposit(expectedVaultShares)` because the final Curvance deposit receives vault shares or receipt tokens.

***

#### Native Vaults

`NativeVaultPositionManager` is for vaults that take the chain's native gas token. If the debt asset is the wrapped native token, the manager unwraps it and deposits native directly. Otherwise it requires one swap from the debt asset into the native token. In that swap, `swapAction.outputToken` may be `address(0)` or `0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE`.

```js
const NATIVE_TOKEN = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";

async function buildNativeVaultLeverageAction({
  borrowAmount,
  wrappedNative,
  expectedVaultShares,
  swapCalldata
}) {
  await assertLeveragePreconditions();

  const debtAsset = await debtCToken.asset();
  const netBorrowAmount = await netAfterPositionManagerFee(borrowAmount);
  const needsSwap = !sameAddress(debtAsset, wrappedNative);
  const expectedShares = await collateralCToken.previewDeposit(expectedVaultShares);

  return {
    borrowableCToken: C_TOKEN_DEBT,
    borrowAssets: borrowAmount,
    cToken: C_TOKEN_COLLATERAL,
    expectedShares,
    swapAction: needsSwap
      ? {
          inputToken: debtAsset,
          inputAmount: netBorrowAmount,
          outputToken: NATIVE_TOKEN,
          target: APPROVED_SWAP_TARGET,
          slippage: ONE_PERCENT,
          call: swapCalldata
        }
      : emptySwapAction(),
    auxData: "0x"
  };
}
```

***

### Monitoring Position Health

A healthy leverage ratio is essential to avoid liquidation. Use `ProtocolReader.getPositionHealth()` to check an existing position or simulate a hypothetical action.

**Interpretation:**

* `positionHealth` is WAD-scaled. `positionHealth >= 1e18` means the position is at or above the soft liquidation threshold. `positionHealth < 1e18` means it is liquidatable.
* `errorCodeHit` is `true` if the reader encountered a pricing or oracle error; treat a `true` value as "result is unreliable; stop and investigate."

**Simulating vs inspecting:**

* Pass **zero addresses and zero amounts** for the `cToken` / `borrowableCToken` / `collateralAssets` / `debtAssets` parameters to simply inspect the current position.
* Pass a non-zero `cToken` with `collateralAssets > 0` and `isDeposit = true` to simulate "what would my health be if I deposited `collateralAssets` of `cToken`?"
* Pass a non-zero `borrowableCToken` with `debtAssets > 0` and `isRepayment = false` to simulate "what would my health be if I borrowed `debtAssets`?"
* In `getPositionHealth`, `bufferTime` is used only by the debt-side hypothetical branch. For projected aggregate debt and leverage sizing, use `hypotheticalLeverageOf`, `getLeverageSnapshot`, or `debtBalanceAtTimestamp` where appropriate.

```js
const HEALTH_WAD = 10n ** 18n;

function formatHealth(positionHealth) {
  if (positionHealth === ethers.MaxUint256) return "no debt";
  return `${ethers.formatUnits(positionHealth, 18)}x`;
}

async function checkPositionHealth() {
  const [positionHealth, errorCodeHit] =
    await protocolReader.getPositionHealth(
      MARKET_MANAGER,
      userAddress,
      ethers.ZeroAddress,
      ethers.ZeroAddress,
      false,
      0n,
      false,
      0n,
      0n
    );

  if (errorCodeHit) {
    throw new Error("Oracle or reader error hit; health value is unreliable");
  }

  const healthy = positionHealth >= HEALTH_WAD;
  console.log(`Position health: ${formatHealth(positionHealth)}`);

  return { positionHealth, healthy };
}
```

For leverage sizing before a new deposit, `hypotheticalLeverageOf(account, cToken, borrowableCToken, assets, bufferTime)` returns current leverage, theoretical maximum leverage, adjusted maximum leverage, and the maximum borrowable debt amount after cap, collateral-cap, and liquidity limits. Its own comments warn that it can overestimate executable leverage when AMM fees and slippage apply.

***

### Common Error Scenarios

| Error                                                   | Meaning                                                                                                                                  |
| ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `BasePositionManager__Unauthorized()`                   | The cToken or borrowable cToken is not listed for the PositionManager's MarketManager, or a callback came from an unexpected caller.     |
| `BasePositionManager__InvalidParam()`                   | The action fields do not match the callback, swap path, target, input token, output token, amount, or PositionManager-specific aux data. |
| `BasePositionManager__InvalidAmount()`                  | A required amount was zero, such as `repayAssets == 0` for deleverage.                                                                   |
| `BasePositionManager__InvalidSlippage()`                | The net value sanity check failed, or a leverage deposit returned fewer shares than `expectedShares`.                                    |
| `BasePositionManager__InsufficientAssetsForRepayment()` | Deleverage produced less debt asset than `repayAssets`.                                                                                  |
| `SwapperLib__UnknownCalldata()`                         | `swapAction.target` has no external calldata checker configured in the Central Registry.                                                 |
| `SwapperLib__Slippage(uint256)`                         | Swap-level value loss exceeded `swapAction.slippage`, or `swapAction.slippage >= 1e18`.                                                  |
| `SwapperLib__TokenPrice(address)`                       | The swap-level oracle price read failed.                                                                                                 |
| `PluginDelegable__Unauthorized()`                       | A delegate called `leverageFor` or `deleverageFor` without PositionManager delegation from the account.                                  |
| `MarketManager__CapReached()`                           | The borrow would exceed the configured `debtCaps(cToken)`.                                                                               |

For ethers v6 integrations, decode reverts with the `iface.parseError(errData)` pattern from Borrow: Error Handling. Include the PositionManager ABI, MarketManager ABI, cToken ABI, ProtocolReader ABI, and small ABI fragments for `SwapperLib` and `PluginDelegable` errors if your generated ABIs do not include them.


---

# 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:

```
GET https://docs.curvance.com/app/developer-docs/quick-start-guides/leverage.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
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.
