This repository contains the ParaSwapDebtSwapAdapter, which aims to allow users to arbitrage borrow APY and exit illiquid debt positions. Therefore, this contract is able to swap one debt position to another debt position - either partially or completely.
You could for example swap your 1000 BUSD
debt to max(1010 USDC)
debt.
In order to perform this task, swapDebt
:
- Creates a flashLoan with variable debt mode with the target debt(
1010 USDC
) on behalf of the user- On aave v2 you need to approve the debtSwapAdapter for credit delegation
- On aave v3 you can also pass a credit delegation permit
- It then swaps the flashed assets to the underlying of the current debt(
1000 BUSD
) via exact out swap (meaning it will receive1000 BUSD
, but might only need1000.1 USDC
for the swap) - Repays the current debt (
1000 BUSD
) - Uses potential (
9.9 USDC
) to repay parts of the newly created target debt
The user has now payed off his 1000 BUSD
debt position, and created a new 1000.1 USDC
debt position.
In situations where a user's real loan-to-value (LTV) is higher than their maximum LTV but lower than their liquidation threshold (LT), extra collateral is needed to "wrap" around the flashloan-and-swap outlined above. The flow would then look like this:
- Create a standard, repayable flashloan with the specified extra collateral asset and amount
- Supply the flashed collateral on behalf of the user
- Create the variable debt flashloan with the target debt(
1010 USDC
) on behalf of the user - Swap the flashloaned target debt asset to the underlying of the current debt(
1000 BUSD
), needing only1000.1 USDC
- Repay the current debt (
1000 BUSD
) - Repay the flashloaned collateral asset and premium if needed (requires
aToken
approval) - Use the remaining new debt asset (
9.9 USDC
) to repay parts of the newly created target debt
Notice how steps 3, 4, 5, and 7 are the same four steps from the collateral-less flow.
The guidelines for selecting a proper extra collateral asset are as follows:
For Aave V3:
- Ensure that the potential asset's LTV is nonzero.
- Ensure that the potential asset's LT is nonzero.
- Ensure that the potential asset's Supply Cap has sufficient capacity.
- If the user is in isolation mode, ensure the asset is the same as the isolated collateral asset.
For Aave V2:
- Ensure that the potential asset's LTV is nonzero.
- Ensure that the potential asset's LT is nonzero.
- Ensure that the extra collateral asset is the same as the new debt asset.
- Ensure that the collateral flashloan premium is added to the
newDebtAmount
.
When possible, for both V2 and V3 deployments, use the from/to debt asset in order to reduce cold storage access costs and save gas.
The recommended formula to determine the minimum amount of extra collateral is derived below:
USER_TOTAL_BORROW / (USER_OLD_COLLATERAL * OLD_COLLATERAL_LTV + EXTRA_COLLATERAL * EXTRA_COLLATERAL_ltv) = 1
USER_OLD_COLLATERAL * OLD_COLLATERAL_LTV + EXTRA_COLLATERAL * EXTRA_COLLATERAL_LTV = USER_TOTAL_BORROW
Therefore:
EXTRA_COLLATERAL = USER_TOTAL_BORROW * EXTRA_COLLATERAL_LTV / (USER_OLD_COLLATERAL * OLD_COLLATERAL_LTV)
We recommend a margin to account for interest accrual and health factor fluctuation until execution.
The function swapDebt(DebtSwapParams memory debtSwapParams, CreditDelegationInput memory creditDelegationPermit, PermitInput memory collateralATokenPermit)
expects three parameters.
The first one describes the swap:
struct DebtSwapParams {
address debtAsset; // the asset you want to swap away from
uint256 debtRepayAmount; // the amount of debt you want to eliminate
uint256 debtRateMode; // the type of debt (1 for stable, 2 for variable)
address newDebtAsset; // the asset you want to swap to
uint256 maxNewDebtAmount; // the max amount of debt your're willing to receive in excahnge for repaying debtRepayAmount
address extraCollateralAsset; // The asset to flash and add as collateral if needed
uint256 extraCollateralAmount; // The amount of `extraCollateralAsset` to flash and supply momentarily
bytes paraswapData; // encoded exactOut swap
}
The second one describes the (optional) creditDelegation permit:
struct CreditDelegationInput {
ICreditDelegationToken debtToken;
uint256 value;
uint256 deadline;
uint8 v;
bytes32 r;
bytes32 s;
}
The third one describes the (optional) collateral aToken permit:
struct PermitInput {
IERC20WithPermit aToken;
uint256 value;
uint256 deadline;
uint8 v;
bytes32 r;
bytes32 s;
}
For usage examples please check the tests.
-
This contract is a extra layer on top of BaseParaswapBuyAdapter which is used in production for ParaSwapRepayAdapter. It uses the exact same mechanism for exact out swap.
-
In contrast to ParaSwapRepayAdapter the ParaSwapDebtSwapAdapter will always repay on the pool on behalf of the user. So instead of having approvals per transaction the adapter will approve
type(uint256).max
once to reduce gas consumption. -
The Aave
POOL
is considered a trustable entity for allowance purposes. -
The contract only interact with
msg.sender
and therefore ensures isolation between users. -
The contract is not upgradable.
-
The contract is ownable and will be owned by governance, so the governance will be the only entity able to call
tokenRescue
. -
The approach with credit delegation and borrow-mode flashLoans is very similar to what is done on V2-V3 Migration helper
-
The contract inherits the security and limitations of Aave v2/v3. The contract itself does not validate for frozen/inactive reserves and also does not consider isolation/eMode or borrowCaps. It is the responsibility of the interface integrating this contract to correctly handle all user position compositions and pool configurations.
-
The contract implements an upper bound of 30% price impact, which would revert any swap. The slippage has to be properly configured in incorporated into the
DebtSwapParams.maxNewDebt
parameter.
This repo has forge and npm dependencies, so you will need to install foundry then run:
forge install
and also run:
yarn
To run the tests just run:
forge test
This code is based on the existing aave paraswap adapters for v3.
The BaseParaSwapAdapter.sol was slightly adjusted to receive the POOL via constructor instead of fetching it.
This makes the code agnostic for v2 and v3, as the only methods used are unchanged between the two versions.