Skip to content

Commit

Permalink
Feat: Allow Debt Swaps When LT > Current_LTV > LTV (#17)
Browse files Browse the repository at this point in the history
* feat: (WIP) Over LTV DebtSwap draft

* fix: (WIP) Fix debt swapper error, modify tests to provide sufficient margin, debug logs still present

* misc: Cleanup

* misc: Clarify comment

* test: Add extra collateral debt swap tests to Gho tests

* test: Added minor testing

* test: Add misc GHO swap adapter tests

* test: Added duplicated test with allowance renewal

* feat: Update readme

* feat: Allow collateral flashloans of the same new debt asset

* fix: Fixed V2 debt swap and cleanup tests

* feat: Add permit for collateral use

* misc: Slight cleanup

* fix: Use supply instead of deposit

* refactor: Modified to make _flash call clearer

* feat: Default to max debt if an amount greater than it is passed

* feat: Add docs and clarify parameter name
  • Loading branch information
Zer0dot authored Aug 9, 2023
1 parent c6d8de5 commit e5ca740
Show file tree
Hide file tree
Showing 30 changed files with 1,228 additions and 134 deletions.
35 changes: 32 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,35 @@
# BGD labs <> Aave Debt Swap Adapter

This repository contains the [ParaSwapDebtSwapAdapter](./src/contracts/ParaSwapDebtSwapAdapter.sol), 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 complete.
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`:

1. 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 permit
- On aave v3 you can also pass a credit delegation permit
2. It then swaps the flashed assets to the underlying of the **current debt**(`1000 BUSD`) via exact out swap (meaning it will receive `1000 BUSD`, but might only need `1000.1 USDC` for the swap)
3. Repays the **current debt** (`1000 BUSD`)
4. 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.

The `function swapDebt( DebtSwapParams memory debtSwapParams, CreditDelegationInput memory creditDelegationPermit )` expects two parameters.
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:

1. Create a standard, repayable flashloan with the specified extra collateral asset and amount
2. Supply the flashed collateral on behalf of the user
3. Create the variable debt flashloan with the **target debt**(`1010 USDC`) on behalf of the user
4. Swap the flashloaned target debt asset to the underlying of the **current debt**(`1000 BUSD`), needing only `1000.1 USDC`
5. Repay the **current debt** (`1000 BUSD`)
6. Repay the flashloaned collateral asset (requires `aToken` approval)
7. 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.

In order to select adequate extra collateral asset and amount parameters, consider the extra collateral's LTV and supply cap. Where possible, it is recommended to use the from/to debt asset in order to reduce gas costs.

The `function swapDebt(DebtSwapParams memory debtSwapParams, CreditDelegationInput memory creditDelegationPermit, PermitInput memory collateralATokenPermit)` expects three parameters.

The first one describes the swap:

Expand All @@ -26,6 +40,8 @@ struct DebtSwapParams {
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
}
Expand All @@ -45,6 +61,19 @@ struct CreditDelegationInput {
```

The third one describes the (optional) collateral aToken permit:

```solidity
struct PermitInput {
IERC20WithPermit aToken;
uint256 value;
uint256 deadline;
uint8 v;
bytes32 r;
bytes32 s;
}
```

For usage examples please check the [tests](./tests/).

## Security
Expand Down
12 changes: 11 additions & 1 deletion src/contracts/BaseParaSwapAdapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,18 @@ abstract contract BaseParaSwapAdapter is IFlashLoanReceiverBase, Ownable {
* @dev Get the vToken, sToken associated to the asset
* @return address of the vToken
* @return address of the sToken
* @return address of the aToken
*/
function _getReserveData(address asset) internal view virtual returns (address, address);
function _getReserveData(address asset) internal view virtual returns (address, address, address);

/**
* @dev Supply "amount" of "asset" to Aave
* @param asset Address of the asset to be supplied
* @param amount Amount of the asset to be supplied
* @param to Address receiving the aTokens
* @param referralCode Referral code to pass to Aave
*/
function _supply(address asset, uint256 amount, address to, uint16 referralCode) internal virtual;

/**
* @dev Emergency rescue for token stucked on this contract, as failsafe mechanism
Expand Down
122 changes: 93 additions & 29 deletions src/contracts/ParaSwapDebtSwapAdapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,12 @@ abstract contract ParaSwapDebtSwapAdapter is
* 4. repay old debt
* @param debtSwapParams the parameters describing the swap
* @param creditDelegationPermit optional permit for credit delegation
* @param collateralATokenPermit optional permit for collateral aToken
*/
function swapDebt(
DebtSwapParams memory debtSwapParams,
CreditDelegationInput memory creditDelegationPermit
CreditDelegationInput memory creditDelegationPermit,
PermitInput memory collateralATokenPermit
) external {
uint256 excessBefore = IERC20Detailed(debtSwapParams.newDebtAsset).balanceOf(address(this));
// delegate credit
Expand All @@ -76,49 +78,82 @@ abstract contract ParaSwapDebtSwapAdapter is
creditDelegationPermit.s
);
}
// flash & repay
if (debtSwapParams.debtRepayAmount == type(uint256).max) {
(address vToken, address sToken) = _getReserveData(debtSwapParams.debtAsset);
debtSwapParams.debtRepayAmount = debtSwapParams.debtRateMode == 2
? IERC20WithPermit(vToken).balanceOf(msg.sender)
: IERC20WithPermit(sToken).balanceOf(msg.sender);
// Default to the entire debt if an amount greater than it is passed.
(address vToken, address sToken, ) = _getReserveData(debtSwapParams.debtAsset);
uint256 maxDebtRepayAmount = debtSwapParams.debtRateMode == 2
? IERC20WithPermit(vToken).balanceOf(msg.sender)
: IERC20WithPermit(sToken).balanceOf(msg.sender);

if (debtSwapParams.debtRepayAmount > maxDebtRepayAmount) {
debtSwapParams.debtRepayAmount = maxDebtRepayAmount;
}
FlashParams memory flashParams = FlashParams(
debtSwapParams.debtAsset,
debtSwapParams.debtRepayAmount,
debtSwapParams.debtRateMode,
address(0),
0,
debtSwapParams.paraswapData,
debtSwapParams.offset,
msg.sender
);
_flash(flashParams, debtSwapParams);

// If we need extra collateral, execute the flashloan with the collateral asset instead of the debt asset.
if (debtSwapParams.extraCollateralAsset != address(0)) {
// Permit collateral aToken if needed.
if (collateralATokenPermit.deadline != 0) {
collateralATokenPermit.aToken.permit(
msg.sender,
address(this),
collateralATokenPermit.value,
collateralATokenPermit.deadline,
collateralATokenPermit.v,
collateralATokenPermit.r,
collateralATokenPermit.s
);
}
flashParams.nestedFlashloanDebtAsset = debtSwapParams.newDebtAsset;
flashParams.nestedFlashloanDebtAmount = debtSwapParams.maxNewDebtAmount;
// Execute the flashloan with the extra collateral asset.
_flash(
flashParams,
debtSwapParams.extraCollateralAsset,
debtSwapParams.extraCollateralAmount
);
} else {
// Execute the flashloan with the debt asset.
_flash(flashParams, debtSwapParams.newDebtAsset, debtSwapParams.maxNewDebtAmount);
}

// use excess to repay parts of flash debt
uint256 excessAfter = IERC20Detailed(debtSwapParams.newDebtAsset).balanceOf(address(this));
uint256 excess = excessAfter - excessBefore;
if (excess > 0) {
uint256 allowance = IERC20(debtSwapParams.newDebtAsset).allowance(
address(this),
address(POOL)
);
if (allowance < excess) {
renewAllowance(debtSwapParams.newDebtAsset);
}
_conditionalRenewAllowance(debtSwapParams.newDebtAsset, excess);
POOL.repay(debtSwapParams.newDebtAsset, excess, 2, msg.sender);
}
}

function _flash(
FlashParams memory flashParams,
DebtSwapParams memory debtSwapParams
) internal virtual {
function _flash(FlashParams memory flashParams, address asset, uint256 amount) internal virtual {
bytes memory params = abi.encode(flashParams);

address[] memory assets = new address[](1);
assets[0] = debtSwapParams.newDebtAsset;
assets[0] = asset;
uint256[] memory amounts = new uint256[](1);
amounts[0] = debtSwapParams.maxNewDebtAmount;
amounts[0] = amount;
uint256[] memory interestRateModes = new uint256[](1);
interestRateModes[0] = 2;
POOL.flashLoan(address(this), assets, amounts, interestRateModes, msg.sender, params, REFERRER);
// This is only true if there is no need for extra collateral.
interestRateModes[0] = flashParams.nestedFlashloanDebtAsset == address(0) ? 2 : 0;

POOL.flashLoan(
address(this),
assets,
amounts,
interestRateModes,
flashParams.user,
params,
REFERRER
);
}

/**
Expand All @@ -141,9 +176,34 @@ abstract contract ParaSwapDebtSwapAdapter is
require(msg.sender == address(POOL), 'CALLER_MUST_BE_POOL');
require(initiator == address(this), 'INITIATOR_MUST_BE_THIS');

FlashParams memory swapParams = abi.decode(params, (FlashParams));
_swapAndRepay(swapParams, IERC20Detailed(assets[0]), amounts[0]);
FlashParams memory flashParams = abi.decode(params, (FlashParams));

// This is only non-zero if we flashed extra collateral.
if (flashParams.nestedFlashloanDebtAsset != address(0)) {
// Wrap the swap with a supply and withdraw.
address collateralAsset = assets[0];
uint256 collateralAmount = amounts[0];

// Supply
_supply(collateralAsset, collateralAmount, flashParams.user, REFERRER);

// Execute the nested flashloan
address newAsset = flashParams.nestedFlashloanDebtAsset;
flashParams.nestedFlashloanDebtAsset = address(0);
_flash(flashParams, newAsset, flashParams.nestedFlashloanDebtAmount);

// Fetch and transfer back in the aToken to allow the pool to pull it.
(, , address aToken) = _getReserveData(collateralAsset);
IERC20WithPermit(aToken).safeTransferFrom(flashParams.user, address(this), collateralAmount); // Could be rounding error but it's insignificant
POOL.withdraw(collateralAsset, collateralAmount, address(this));
_conditionalRenewAllowance(collateralAsset, collateralAmount);

// Return out of this scope.
return true;
}

// There is no need for additional collateral, execute the swap.
_swapAndRepay(flashParams, IERC20Detailed(assets[0]), amounts[0]);
return true;
}

Expand All @@ -167,10 +227,7 @@ abstract contract ParaSwapDebtSwapAdapter is
swapParams.debtRepayAmount
);

uint256 allowance = IERC20(swapParams.debtAsset).allowance(address(this), address(POOL));
if (allowance < swapParams.debtRepayAmount) {
renewAllowance(address(swapParams.debtAsset));
}
_conditionalRenewAllowance(swapParams.debtAsset, swapParams.debtRepayAmount);

POOL.repay(
address(swapParams.debtAsset),
Expand All @@ -180,4 +237,11 @@ abstract contract ParaSwapDebtSwapAdapter is
);
return amountSold;
}

function _conditionalRenewAllowance(address asset, uint256 minAmount) internal {
uint256 allowance = IERC20(asset).allowance(address(this), address(POOL));
if (allowance < minAmount) {
renewAllowance(asset);
}
}
}
17 changes: 15 additions & 2 deletions src/contracts/ParaSwapDebtSwapAdapterV2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,21 @@ contract ParaSwapDebtSwapAdapterV2 is ParaSwapDebtSwapAdapter {
address owner
) ParaSwapDebtSwapAdapter(addressesProvider, pool, augustusRegistry, owner) {}

function _getReserveData(address asset) internal view override returns (address, address) {
function _getReserveData(address asset) internal view override returns (address, address, address) {
DataTypes.ReserveData memory reserveData = ILendingPool(address(POOL)).getReserveData(asset);
return (reserveData.variableDebtTokenAddress, reserveData.stableDebtTokenAddress);
return (
reserveData.variableDebtTokenAddress,
reserveData.stableDebtTokenAddress,
reserveData.aTokenAddress
);
}

function _supply(
address asset,
uint256 amount,
address to,
uint16 referralCode
) internal override {
ILendingPool(address(POOL)).deposit(asset, amount, to, referralCode);
}
}
19 changes: 17 additions & 2 deletions src/contracts/ParaSwapDebtSwapAdapterV3.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,23 @@ contract ParaSwapDebtSwapAdapterV3 is ParaSwapDebtSwapAdapter {
address owner
) ParaSwapDebtSwapAdapter(addressesProvider, pool, augustusRegistry, owner) {}

function _getReserveData(address asset) internal view override returns (address, address) {
function _getReserveData(
address asset
) internal view override returns (address, address, address) {
DataTypes.ReserveData memory reserveData = POOL.getReserveData(asset);
return (reserveData.variableDebtTokenAddress, reserveData.stableDebtTokenAddress);
return (
reserveData.variableDebtTokenAddress,
reserveData.stableDebtTokenAddress,
reserveData.aTokenAddress
);
}

function _supply(
address asset,
uint256 amount,
address to,
uint16 referralCode
) internal override {
POOL.supply(asset, amount, to, referralCode);
}
}
12 changes: 5 additions & 7 deletions src/contracts/ParaSwapDebtSwapAdapterV3GHO.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {IParaSwapAugustusRegistry} from '../interfaces/IParaSwapAugustusRegistry
import {IERC3156FlashBorrower} from '../interfaces/IERC3156FlashBorrower.sol';
import {IERC3156FlashLender} from '../interfaces/IERC3156FlashLender.sol';

// send collateral if needed via params
/**
* @title ParaSwapDebtSwapAdapter
* @notice ParaSwap Adapter to perform a swap of debt to another debt.
Expand Down Expand Up @@ -50,19 +51,16 @@ contract ParaSwapDebtSwapAdapterV3GHO is ParaSwapDebtSwapAdapterV3, IERC3156Flas
return keccak256('ERC3156FlashBorrower.onFlashLoan');
}

function _flash(
FlashParams memory flashParams,
DebtSwapParams memory debtSwapParams
) internal override {
if (debtSwapParams.newDebtAsset == GHO) {
function _flash(FlashParams memory flashParams, address asset, uint256 amount) internal override {
if (asset == GHO) {
GHO_FLASH_MINTER.flashLoan(
IERC3156FlashBorrower(address(this)),
GHO,
debtSwapParams.maxNewDebtAmount,
amount,
abi.encode(flashParams)
);
} else {
super._flash(flashParams, debtSwapParams);
super._flash(flashParams, asset, amount);
}
}
}
Loading

0 comments on commit e5ca740

Please sign in to comment.