For the complete documentation index, see llms.txt. This page is also available as Markdown.

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.


Core Structures for Leveraging

LeverageAction (IPositionManager.sol:25)

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)

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.
}

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.

SwapperLib.Swap (SwapperLib.sol:36)


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

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:

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

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.

Real PositionManager function signatures (from BasePositionManager.sol):

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.

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 for the delegation model.

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.

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

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.

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.


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.

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.

Last updated

Was this helpful?