diff --git a/contracts/adapters/BaseParaSwapAdapter.sol b/contracts/adapters/BaseParaSwapAdapter.sol index 9b84dd93f..7fe34e587 100644 --- a/contracts/adapters/BaseParaSwapAdapter.sol +++ b/contracts/adapters/BaseParaSwapAdapter.sol @@ -37,11 +37,23 @@ abstract contract BaseParaSwapAdapter is FlashLoanReceiverBase, Ownable { IPriceOracleGetter public immutable ORACLE; - event Swapped(address indexed fromAsset, address indexed toAsset, uint256 fromAmount, uint256 receivedAmount); + event Swapped( + address indexed fromAsset, + address indexed toAsset, + uint256 fromAmount, + uint256 receivedAmount + ); + event Bought( + address indexed fromAsset, + address indexed toAsset, + uint256 amountSold, + uint256 receivedAmount + ); - constructor( - ILendingPoolAddressesProvider addressesProvider - ) public FlashLoanReceiverBase(addressesProvider) { + constructor(ILendingPoolAddressesProvider addressesProvider) + public + FlashLoanReceiverBase(addressesProvider) + { ORACLE = IPriceOracleGetter(addressesProvider.getPriceOracle()); } @@ -73,6 +85,17 @@ abstract contract BaseParaSwapAdapter is FlashLoanReceiverBase, Ownable { return LENDING_POOL.getReserveData(asset); } + function _pullATokenAndWithdraw( + address reserve, + address user, + uint256 amount, + PermitSignature memory permitSignature + ) internal { + IERC20WithPermit reserveAToken = + IERC20WithPermit(_getReserveData(address(reserve)).aTokenAddress); + _pullATokenAndWithdraw(reserve, reserveAToken, user, amount, permitSignature); + } + /** * @dev Pull the ATokens from the user * @param reserve address of the asset diff --git a/contracts/adapters/BaseParaSwapBuyAdapter.sol b/contracts/adapters/BaseParaSwapBuyAdapter.sol new file mode 100644 index 000000000..60780987b --- /dev/null +++ b/contracts/adapters/BaseParaSwapBuyAdapter.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity 0.6.12; +pragma experimental ABIEncoderV2; + +import {BaseParaSwapAdapter} from './BaseParaSwapAdapter.sol'; +import {PercentageMath} from '../protocol/libraries/math/PercentageMath.sol'; +import {IParaSwapAugustus} from '../interfaces/IParaSwapAugustus.sol'; +import {IParaSwapAugustusRegistry} from '../interfaces/IParaSwapAugustusRegistry.sol'; +import {ILendingPoolAddressesProvider} from '../interfaces/ILendingPoolAddressesProvider.sol'; +import {IERC20Detailed} from '../dependencies/openzeppelin/contracts/IERC20Detailed.sol'; + +/** + * @title BaseParaSwapBuyAdapter + * @notice Implements the logic for buying tokens on ParaSwap + */ +abstract contract BaseParaSwapBuyAdapter is BaseParaSwapAdapter { + using PercentageMath for uint256; + + IParaSwapAugustusRegistry public immutable AUGUSTUS_REGISTRY; + + constructor( + ILendingPoolAddressesProvider addressesProvider, + IParaSwapAugustusRegistry augustusRegistry + ) public BaseParaSwapAdapter(addressesProvider) { + // Do something on Augustus registry to check the right contract was passed + require(!augustusRegistry.isValidAugustus(address(0)), "Not a valid Augustus address"); + AUGUSTUS_REGISTRY = augustusRegistry; + } + + /** + * @dev Swaps a token for another using ParaSwap + * @param toAmountOffset Offset of toAmount in Augustus calldata if it should be overwritten, otherwise 0 + * @param paraswapData Data for Paraswap Adapter + * @param assetToSwapFrom Address of the asset to be swapped from + * @param assetToSwapTo Address of the asset to be swapped to + * @param maxAmountToSwap Max amount to be swapped + * @param amountToReceive Amount to be received from the swap + * @return amountSold The amount sold during the swap + */ + function _buyOnParaSwap( + uint256 toAmountOffset, + bytes memory paraswapData, + IERC20Detailed assetToSwapFrom, + IERC20Detailed assetToSwapTo, + uint256 maxAmountToSwap, + uint256 amountToReceive + ) internal returns (uint256 amountSold) { + (bytes memory buyCalldata, IParaSwapAugustus augustus) = + abi.decode(paraswapData, (bytes, IParaSwapAugustus)); + + require(AUGUSTUS_REGISTRY.isValidAugustus(address(augustus)), 'INVALID_AUGUSTUS'); + + { + uint256 fromAssetDecimals = _getDecimals(assetToSwapFrom); + uint256 toAssetDecimals = _getDecimals(assetToSwapTo); + + uint256 fromAssetPrice = _getPrice(address(assetToSwapFrom)); + uint256 toAssetPrice = _getPrice(address(assetToSwapTo)); + + uint256 expectedMaxAmountToSwap = + amountToReceive + .mul(toAssetPrice.mul(10**fromAssetDecimals)) + .div(fromAssetPrice.mul(10**toAssetDecimals)) + .percentMul(PercentageMath.PERCENTAGE_FACTOR.add(MAX_SLIPPAGE_PERCENT)); + + require(maxAmountToSwap <= expectedMaxAmountToSwap, 'maxAmountToSwap exceed max slippage'); + } + + uint256 balanceBeforeAssetFrom = assetToSwapFrom.balanceOf(address(this)); + require(balanceBeforeAssetFrom >= maxAmountToSwap, 'INSUFFICIENT_BALANCE_BEFORE_SWAP'); + uint256 balanceBeforeAssetTo = assetToSwapTo.balanceOf(address(this)); + + address tokenTransferProxy = augustus.getTokenTransferProxy(); + assetToSwapFrom.safeApprove(tokenTransferProxy, 0); + assetToSwapFrom.safeApprove(tokenTransferProxy, maxAmountToSwap); + + if (toAmountOffset != 0) { + // Ensure 256 bit (32 bytes) toAmountOffset value is within bounds of the + // calldata, not overlapping with the first 4 bytes (function selector). + require( + toAmountOffset >= 4 && toAmountOffset <= buyCalldata.length.sub(32), + 'TO_AMOUNT_OFFSET_OUT_OF_RANGE' + ); + // Overwrite the toAmount with the correct amount for the buy. + // In memory, buyCalldata consists of a 256 bit length field, followed by + // the actual bytes data, that is why 32 is added to the byte offset. + assembly { + mstore(add(buyCalldata, add(toAmountOffset, 32)), amountToReceive) + } + } + (bool success, ) = address(augustus).call(buyCalldata); + if (!success) { + // Copy revert reason from call + assembly { + returndatacopy(0, 0, returndatasize()) + revert(0, returndatasize()) + } + } + + uint256 balanceAfterAssetFrom = assetToSwapFrom.balanceOf(address(this)); + amountSold = balanceBeforeAssetFrom - balanceAfterAssetFrom; + require(amountSold <= maxAmountToSwap, 'WRONG_BALANCE_AFTER_SWAP'); + uint256 amountReceived = assetToSwapTo.balanceOf(address(this)).sub(balanceBeforeAssetTo); + require(amountReceived >= amountToReceive, 'INSUFFICIENT_AMOUNT_RECEIVED'); + + emit Bought(address(assetToSwapFrom), address(assetToSwapTo), amountSold, amountReceived); + } +} diff --git a/contracts/adapters/ParaSwapRepayAdapter.sol b/contracts/adapters/ParaSwapRepayAdapter.sol new file mode 100644 index 000000000..99fc67068 --- /dev/null +++ b/contracts/adapters/ParaSwapRepayAdapter.sol @@ -0,0 +1,233 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity 0.6.12; +pragma experimental ABIEncoderV2; + +import {BaseParaSwapBuyAdapter} from './BaseParaSwapBuyAdapter.sol'; +import {ILendingPoolAddressesProvider} from '../interfaces/ILendingPoolAddressesProvider.sol'; +import {IERC20} from '../dependencies/openzeppelin/contracts/IERC20.sol'; +import {DataTypes} from '../protocol/libraries/types/DataTypes.sol'; +import {IParaSwapAugustus} from '../interfaces/IParaSwapAugustus.sol'; +import {IParaSwapAugustusRegistry} from '../interfaces/IParaSwapAugustusRegistry.sol'; +import {ReentrancyGuard} from '../dependencies/openzeppelin/contracts/ReentrancyGuard.sol'; +import {IERC20Detailed} from '../dependencies/openzeppelin/contracts/IERC20Detailed.sol'; +import {IERC20WithPermit} from '../interfaces/IERC20WithPermit.sol'; + +/** + * @title UniswapRepayAdapter + * @notice Uniswap V2 Adapter to perform a repay of a debt with collateral. + * @author Aave + **/ +contract ParaSwapRepayAdapter is BaseParaSwapBuyAdapter, ReentrancyGuard { + struct RepayParams { + address collateralAsset; + uint256 collateralAmount; + uint256 rateMode; + PermitSignature permitSignature; + bool useEthPath; + } + + constructor( + ILendingPoolAddressesProvider addressesProvider, + IParaSwapAugustusRegistry augustusRegistry + ) public BaseParaSwapBuyAdapter(addressesProvider, augustusRegistry) { + // This is only required to initialize BaseParaSwapBuyAdapter + } + + /** + * @dev Uses the received funds from the flash loan to repay a debt on the protocol on behalf of the user. Then pulls + * the collateral from the user and swaps it to the debt asset to repay the flash loan. + * The user should give this contract allowance to pull the ATokens in order to withdraw the underlying asset, swap it + * and repay the flash loan. + * Supports only one asset on the flash loan. + * @param assets Address of collateral asset(Flash loan asset) + * @param amounts Amount of flash loan taken + * @param premiums Fee of the flash loan + * @param initiator Address of the user + * @param params Additional variadic field to include extra params. Expected parameters: + * IERC20Detailed debtAsset Address of the debt asset + * uint256 debtAmount Amount of debt to be repaid + * uint256 rateMode Rate modes of the debt to be repaid + * uint256 deadline Deadline for the permit signature + * uint256 debtRateMode Rate mode of the debt to be repaid + * bytes paraswapData Paraswap Data + * * bytes buyCallData Call data for augustus + * * IParaSwapAugustus augustus Address of Augustus Swapper + * PermitSignature permitParams Struct containing the permit signatures, set to all zeroes if not used + */ + function executeOperation( + address[] calldata assets, + uint256[] calldata amounts, + uint256[] calldata premiums, + address initiator, + bytes calldata params + ) external override nonReentrant returns (bool) { + require(msg.sender == address(LENDING_POOL), 'CALLER_MUST_BE_LENDING_POOL'); + + require( + assets.length == 1 && amounts.length == 1 && premiums.length == 1, + 'FLASHLOAN_MULTIPLE_ASSETS_NOT_SUPPORTED' + ); + + uint256 collateralAmount = amounts[0]; + uint256 premium = premiums[0]; + address initiatorLocal = initiator; + + IERC20Detailed collateralAsset = IERC20Detailed(assets[0]); + + _swapAndRepay(params, premium, initiatorLocal, collateralAsset, collateralAmount); + + return true; + } + + /** + * @dev Swaps the user collateral for the debt asset and then repay the debt on the protocol on behalf of the user + * without using flash loans. This method can be used when the temporary transfer of the collateral asset to this + * contract does not affect the user position. + * The user should give this contract allowance to pull the ATokens in order to withdraw the underlying asset + * @param collateralAsset Address of asset to be swapped + * @param debtAsset Address of debt asset + * @param collateralAmount max Amount of the collateral to be swapped + * @param debtRepayAmount Amount of the debt to be repaid, or maximum amount when repaying entire debt + * @param debtRateMode Rate mode of the debt to be repaid + * @param buyAllBalanceOffset Set to offset of toAmount in Augustus calldata if wanting to pay entire debt, otherwise 0 + * @param paraswapData Data for Paraswap Adapter + * @param permitSignature struct containing the permit signature + + */ + function swapAndRepay( + IERC20Detailed collateralAsset, + IERC20Detailed debtAsset, + uint256 collateralAmount, + uint256 debtRepayAmount, + uint256 debtRateMode, + uint256 buyAllBalanceOffset, + bytes calldata paraswapData, + PermitSignature calldata permitSignature + ) external nonReentrant { + debtRepayAmount = getDebtRepayAmount( + debtAsset, + debtRateMode, + buyAllBalanceOffset, + debtRepayAmount, + msg.sender + ); + + // Pull aTokens from user + _pullATokenAndWithdraw(address(collateralAsset), msg.sender, collateralAmount, permitSignature); + //buy debt asset using collateral asset + uint256 amountSold = + _buyOnParaSwap( + buyAllBalanceOffset, + paraswapData, + collateralAsset, + debtAsset, + collateralAmount, + debtRepayAmount + ); + + uint256 collateralBalanceLeft = collateralAmount - amountSold; + + //deposit collateral back in the pool, if left after the swap(buy) + if (collateralBalanceLeft > 0) { + IERC20(collateralAsset).safeApprove(address(LENDING_POOL), 0); + IERC20(collateralAsset).safeApprove(address(LENDING_POOL), collateralBalanceLeft); + LENDING_POOL.deposit(address(collateralAsset), collateralBalanceLeft, msg.sender, 0); + } + + // Repay debt. Approves 0 first to comply with tokens that implement the anti frontrunning approval fix + IERC20(debtAsset).safeApprove(address(LENDING_POOL), 0); + IERC20(debtAsset).safeApprove(address(LENDING_POOL), debtRepayAmount); + LENDING_POOL.repay(address(debtAsset), debtRepayAmount, debtRateMode, msg.sender); + } + + /** + * @dev Perform the repay of the debt, pulls the initiator collateral and swaps to repay the flash loan + * @param premium Fee of the flash loan + * @param initiator Address of the user + * @param collateralAsset Address of token to be swapped + * @param collateralAmount Amount of the reserve to be swapped(flash loan amount) + */ + + function _swapAndRepay( + bytes calldata params, + uint256 premium, + address initiator, + IERC20Detailed collateralAsset, + uint256 collateralAmount + ) private { + ( + IERC20Detailed debtAsset, + uint256 debtRepayAmount, + uint256 buyAllBalanceOffset, + uint256 rateMode, + bytes memory paraswapData, + PermitSignature memory permitSignature + ) = abi.decode(params, (IERC20Detailed, uint256, uint256, uint256, bytes, PermitSignature)); + + debtRepayAmount = getDebtRepayAmount( + debtAsset, + rateMode, + buyAllBalanceOffset, + debtRepayAmount, + initiator + ); + + uint256 amountSold = debtRepayAmount; + + if (collateralAsset != debtAsset) { + amountSold = _buyOnParaSwap( + buyAllBalanceOffset, + paraswapData, + collateralAsset, + debtAsset, + collateralAmount, + debtRepayAmount + ); + } + + // Repay debt. Approves for 0 first to comply with tokens that implement the anti frontrunning approval fix. + IERC20(debtAsset).safeApprove(address(LENDING_POOL), 0); + IERC20(debtAsset).safeApprove(address(LENDING_POOL), debtRepayAmount); + LENDING_POOL.repay(address(debtAsset), debtRepayAmount, rateMode, initiator); + + uint256 neededForFlashLoanRepay = amountSold.add(premium); + + // Pull aTokens from user + _pullATokenAndWithdraw( + address(collateralAsset), + initiator, + neededForFlashLoanRepay, + permitSignature + ); + + // Repay flashloan. Approves for 0 first to comply with tokens that implement the anti frontrunning approval fix. + IERC20(collateralAsset).safeApprove(address(LENDING_POOL), 0); + IERC20(collateralAsset).safeApprove(address(LENDING_POOL), collateralAmount.add(premium)); + } + + function getDebtRepayAmount( + IERC20Detailed debtAsset, + uint256 rateMode, + uint256 buyAllBalanceOffset, + uint256 debtRepayAmount, + address initiator + ) private view returns (uint256) { + DataTypes.ReserveData memory debtReserveData = _getReserveData(address(debtAsset)); + + address debtToken = + DataTypes.InterestRateMode(rateMode) == DataTypes.InterestRateMode.STABLE + ? debtReserveData.stableDebtTokenAddress + : debtReserveData.variableDebtTokenAddress; + + uint256 currentDebt = IERC20(debtToken).balanceOf(initiator); + + if (buyAllBalanceOffset != 0) { + require(currentDebt <= debtRepayAmount, 'INSUFFICIENT_AMOUNT_TO_REPAY'); + debtRepayAmount = currentDebt; + } else { + require(debtRepayAmount <= currentDebt, 'INVALID_DEBT_REPAY_AMOUNT'); + } + + return debtRepayAmount; + } +} diff --git a/contracts/interfaces/IChainlinkAggregator.sol b/contracts/interfaces/IChainlinkAggregator.sol index 756863712..4b8c40df8 100644 --- a/contracts/interfaces/IChainlinkAggregator.sol +++ b/contracts/interfaces/IChainlinkAggregator.sol @@ -3,7 +3,7 @@ pragma solidity 0.6.12; interface IChainlinkAggregator { function decimals() external view returns (uint8); - + function latestAnswer() external view returns (int256); function latestTimestamp() external view returns (uint256); @@ -16,4 +16,4 @@ interface IChainlinkAggregator { event AnswerUpdated(int256 indexed current, uint256 indexed roundId, uint256 timestamp); event NewRound(uint256 indexed roundId, address indexed startedBy); -} \ No newline at end of file +} diff --git a/contracts/misc/UiIncentiveDataProviderV2.sol b/contracts/misc/UiIncentiveDataProviderV2.sol index 56b3c0b89..5ba549b4b 100644 --- a/contracts/misc/UiIncentiveDataProviderV2.sol +++ b/contracts/misc/UiIncentiveDataProviderV2.sol @@ -52,7 +52,9 @@ contract UiIncentiveDataProviderV2 is IUiIncentiveDataProviderV2 { DataTypes.ReserveData memory baseData = lendingPool.getReserveData(reserves[i]); - try IStableDebtToken(baseData.aTokenAddress).getIncentivesController() returns (IAaveIncentivesController aTokenIncentiveController) { + try IStableDebtToken(baseData.aTokenAddress).getIncentivesController() returns ( + IAaveIncentivesController aTokenIncentiveController + ) { if (address(aTokenIncentiveController) != address(0)) { address aRewardToken = aTokenIncentiveController.REWARD_TOKEN(); @@ -72,7 +74,9 @@ contract UiIncentiveDataProviderV2 is IUiIncentiveDataProviderV2 { IERC20Detailed(aRewardToken).decimals(), aTokenIncentiveController.PRECISION() ); - } catch (bytes memory /*lowLevelData*/) { + } catch ( + bytes memory /*lowLevelData*/ + ) { ( uint256 aEmissionPerSecond, uint256 aIncentivesLastUpdateTimestamp, @@ -90,15 +94,18 @@ contract UiIncentiveDataProviderV2 is IUiIncentiveDataProviderV2 { IERC20Detailed(aRewardToken).decimals(), aTokenIncentiveController.PRECISION() ); - } + } } - } catch(bytes memory /*lowLevelData*/) { + } catch ( + bytes memory /*lowLevelData*/ + ) { // Will not get here - } + } - try IStableDebtToken(baseData.stableDebtTokenAddress).getIncentivesController() returns (IAaveIncentivesController sTokenIncentiveController) { + try IStableDebtToken(baseData.stableDebtTokenAddress).getIncentivesController() returns ( + IAaveIncentivesController sTokenIncentiveController + ) { if (address(sTokenIncentiveController) != address(0)) { - address sRewardToken = sTokenIncentiveController.REWARD_TOKEN(); try sTokenIncentiveController.getAssetData(baseData.stableDebtTokenAddress) returns ( uint256 sTokenIncentivesIndex, @@ -116,7 +123,9 @@ contract UiIncentiveDataProviderV2 is IUiIncentiveDataProviderV2 { IERC20Detailed(sRewardToken).decimals(), sTokenIncentiveController.PRECISION() ); - } catch (bytes memory /*lowLevelData*/) { + } catch ( + bytes memory /*lowLevelData*/ + ) { ( uint256 sEmissionPerSecond, uint256 sIncentivesLastUpdateTimestamp, @@ -134,13 +143,17 @@ contract UiIncentiveDataProviderV2 is IUiIncentiveDataProviderV2 { IERC20Detailed(sRewardToken).decimals(), sTokenIncentiveController.PRECISION() ); - } + } } - } catch(bytes memory /*lowLevelData*/) { + } catch ( + bytes memory /*lowLevelData*/ + ) { // Will not get here } - try IStableDebtToken(baseData.variableDebtTokenAddress).getIncentivesController() returns (IAaveIncentivesController vTokenIncentiveController) { + try IStableDebtToken(baseData.variableDebtTokenAddress).getIncentivesController() returns ( + IAaveIncentivesController vTokenIncentiveController + ) { if (address(vTokenIncentiveController) != address(0)) { address vRewardToken = vTokenIncentiveController.REWARD_TOKEN(); @@ -160,7 +173,9 @@ contract UiIncentiveDataProviderV2 is IUiIncentiveDataProviderV2 { IERC20Detailed(vRewardToken).decimals(), vTokenIncentiveController.PRECISION() ); - } catch (bytes memory /*lowLevelData*/) { + } catch ( + bytes memory /*lowLevelData*/ + ) { ( uint256 vEmissionPerSecond, uint256 vIncentivesLastUpdateTimestamp, @@ -180,7 +195,9 @@ contract UiIncentiveDataProviderV2 is IUiIncentiveDataProviderV2 { ); } } - } catch(bytes memory /*lowLevelData*/) { + } catch ( + bytes memory /*lowLevelData*/ + ) { // Will not get here } } @@ -215,74 +232,77 @@ contract UiIncentiveDataProviderV2 is IUiIncentiveDataProviderV2 { IUiIncentiveDataProviderV2.UserIncentiveData memory aUserIncentiveData; - try IAToken(baseData.aTokenAddress).getIncentivesController() returns (IAaveIncentivesController aTokenIncentiveController) { + try IAToken(baseData.aTokenAddress).getIncentivesController() returns ( + IAaveIncentivesController aTokenIncentiveController + ) { if (address(aTokenIncentiveController) != address(0)) { address aRewardToken = aTokenIncentiveController.REWARD_TOKEN(); aUserIncentiveData.tokenincentivesUserIndex = aTokenIncentiveController.getUserAssetData( user, baseData.aTokenAddress ); - aUserIncentiveData.userUnclaimedRewards = aTokenIncentiveController.getUserUnclaimedRewards( - user - ); + aUserIncentiveData.userUnclaimedRewards = aTokenIncentiveController + .getUserUnclaimedRewards(user); aUserIncentiveData.tokenAddress = baseData.aTokenAddress; aUserIncentiveData.rewardTokenAddress = aRewardToken; aUserIncentiveData.incentiveControllerAddress = address(aTokenIncentiveController); aUserIncentiveData.rewardTokenDecimals = IERC20Detailed(aRewardToken).decimals(); } - } catch (bytes memory /*lowLevelData*/) { - - } + } catch ( + bytes memory /*lowLevelData*/ + ) {} userReservesIncentivesData[i].aTokenIncentivesUserData = aUserIncentiveData; UserIncentiveData memory vUserIncentiveData; - try IVariableDebtToken(baseData.variableDebtTokenAddress).getIncentivesController() returns(IAaveIncentivesController vTokenIncentiveController) { + try IVariableDebtToken(baseData.variableDebtTokenAddress).getIncentivesController() returns ( + IAaveIncentivesController vTokenIncentiveController + ) { if (address(vTokenIncentiveController) != address(0)) { address vRewardToken = vTokenIncentiveController.REWARD_TOKEN(); vUserIncentiveData.tokenincentivesUserIndex = vTokenIncentiveController.getUserAssetData( user, baseData.variableDebtTokenAddress ); - vUserIncentiveData.userUnclaimedRewards = vTokenIncentiveController.getUserUnclaimedRewards( - user - ); + vUserIncentiveData.userUnclaimedRewards = vTokenIncentiveController + .getUserUnclaimedRewards(user); vUserIncentiveData.tokenAddress = baseData.variableDebtTokenAddress; vUserIncentiveData.rewardTokenAddress = vRewardToken; vUserIncentiveData.incentiveControllerAddress = address(vTokenIncentiveController); vUserIncentiveData.rewardTokenDecimals = IERC20Detailed(vRewardToken).decimals(); } - } catch (bytes memory /*lowLevelData*/) { - - } + } catch ( + bytes memory /*lowLevelData*/ + ) {} userReservesIncentivesData[i].vTokenIncentivesUserData = vUserIncentiveData; UserIncentiveData memory sUserIncentiveData; - try IStableDebtToken(baseData.stableDebtTokenAddress).getIncentivesController() returns (IAaveIncentivesController sTokenIncentiveController) { + try IStableDebtToken(baseData.stableDebtTokenAddress).getIncentivesController() returns ( + IAaveIncentivesController sTokenIncentiveController + ) { if (address(sTokenIncentiveController) != address(0)) { address sRewardToken = sTokenIncentiveController.REWARD_TOKEN(); sUserIncentiveData.tokenincentivesUserIndex = sTokenIncentiveController.getUserAssetData( user, baseData.stableDebtTokenAddress ); - sUserIncentiveData.userUnclaimedRewards = sTokenIncentiveController.getUserUnclaimedRewards( - user - ); + sUserIncentiveData.userUnclaimedRewards = sTokenIncentiveController + .getUserUnclaimedRewards(user); sUserIncentiveData.tokenAddress = baseData.stableDebtTokenAddress; sUserIncentiveData.rewardTokenAddress = sRewardToken; sUserIncentiveData.incentiveControllerAddress = address(sTokenIncentiveController); sUserIncentiveData.rewardTokenDecimals = IERC20Detailed(sRewardToken).decimals(); } - } catch (bytes memory /*lowLevelData*/) { - - } + } catch ( + bytes memory /*lowLevelData*/ + ) {} userReservesIncentivesData[i].sTokenIncentivesUserData = sUserIncentiveData; } return (userReservesIncentivesData); } -} \ No newline at end of file +} diff --git a/contracts/misc/UiIncentiveDataProviderV2V3.sol b/contracts/misc/UiIncentiveDataProviderV2V3.sol index 5c2dff1ae..eff299f1d 100644 --- a/contracts/misc/UiIncentiveDataProviderV2V3.sol +++ b/contracts/misc/UiIncentiveDataProviderV2V3.sol @@ -46,8 +46,8 @@ contract UiIncentiveDataProviderV2V3 is IUiIncentiveDataProviderV3 { { ILendingPool lendingPool = ILendingPool(provider.getLendingPool()); address[] memory reserves = lendingPool.getReservesList(); - AggregatedReserveIncentiveData[] - memory reservesIncentiveData = new AggregatedReserveIncentiveData[](reserves.length); + AggregatedReserveIncentiveData[] memory reservesIncentiveData = + new AggregatedReserveIncentiveData[](reserves.length); for (uint256 i = 0; i < reserves.length; i++) { AggregatedReserveIncentiveData memory reserveIncentiveData = reservesIncentiveData[i]; @@ -67,20 +67,19 @@ contract UiIncentiveDataProviderV2V3 is IUiIncentiveDataProviderV3 { uint256 aEmissionPerSecond, uint256 aIncentivesLastUpdateTimestamp ) { - - aRewardsInformation[0] = RewardInfo( - getSymbol(aRewardToken), - aRewardToken, - address(0), - aEmissionPerSecond, - aIncentivesLastUpdateTimestamp, - aTokenIncentivesIndex, - aTokenIncentiveController.DISTRIBUTION_END(), - 0, - IERC20Detailed(aRewardToken).decimals(), - aTokenIncentiveController.PRECISION(), - 0 - ); + aRewardsInformation[0] = RewardInfo( + getSymbol(aRewardToken), + aRewardToken, + address(0), + aEmissionPerSecond, + aIncentivesLastUpdateTimestamp, + aTokenIncentivesIndex, + aTokenIncentiveController.DISTRIBUTION_END(), + 0, + IERC20Detailed(aRewardToken).decimals(), + aTokenIncentiveController.PRECISION(), + 0 + ); reserveIncentiveData.aIncentiveData = IncentiveData( baseData.aTokenAddress, address(aTokenIncentiveController), @@ -274,9 +273,8 @@ contract UiIncentiveDataProviderV2V3 is IUiIncentiveDataProviderV3 { ILendingPool lendingPool = ILendingPool(provider.getLendingPool()); address[] memory reserves = lendingPool.getReservesList(); - UserReserveIncentiveData[] memory userReservesIncentivesData = new UserReserveIncentiveData[]( - user != address(0) ? reserves.length : 0 - ); + UserReserveIncentiveData[] memory userReservesIncentivesData = + new UserReserveIncentiveData[](user != address(0) ? reserves.length : 0); for (uint256 i = 0; i < reserves.length; i++) { DataTypes.ReserveData memory baseData = lendingPool.getReserveData(reserves[i]); @@ -395,4 +393,4 @@ contract UiIncentiveDataProviderV2V3 is IUiIncentiveDataProviderV3 { } return string(bytesArray); } -} \ No newline at end of file +} diff --git a/contracts/misc/UiPoolDataProvider.sol b/contracts/misc/UiPoolDataProvider.sol index 8c01f81c2..d3bf1c52a 100644 --- a/contracts/misc/UiPoolDataProvider.sol +++ b/contracts/misc/UiPoolDataProvider.sol @@ -142,13 +142,13 @@ contract UiPoolDataProvider is IUiPoolDataProvider { reserveData.aEmissionPerSecond, reserveData.aIncentivesLastUpdateTimestamp ) = incentivesController.getAssetData(reserveData.aTokenAddress); - + ( reserveData.sTokenIncentivesIndex, reserveData.sEmissionPerSecond, reserveData.sIncentivesLastUpdateTimestamp ) = incentivesController.getAssetData(reserveData.stableDebtTokenAddress); - + ( reserveData.vTokenIncentivesIndex, reserveData.vEmissionPerSecond, @@ -315,14 +315,14 @@ contract UiPoolDataProvider is IUiPoolDataProvider { reserveData.aTokenIncentivesIndex, reserveData.aEmissionPerSecond, reserveData.aIncentivesLastUpdateTimestamp - ) = incentivesController.getAssetData(reserveData.aTokenAddress); - + ) = incentivesController.getAssetData(reserveData.aTokenAddress); + ( reserveData.sTokenIncentivesIndex, reserveData.sEmissionPerSecond, reserveData.sIncentivesLastUpdateTimestamp ) = incentivesController.getAssetData(reserveData.stableDebtTokenAddress); - + ( reserveData.vTokenIncentivesIndex, reserveData.vEmissionPerSecond, diff --git a/contracts/misc/UiPoolDataProviderV2.sol b/contracts/misc/UiPoolDataProviderV2.sol index bb561683f..583a56992 100644 --- a/contracts/misc/UiPoolDataProviderV2.sol +++ b/contracts/misc/UiPoolDataProviderV2.sol @@ -15,7 +15,9 @@ import {ReserveConfiguration} from '../protocol/libraries/configuration/ReserveC import {UserConfiguration} from '../protocol/libraries/configuration/UserConfiguration.sol'; import {DataTypes} from '../protocol/libraries/types/DataTypes.sol'; import {IChainlinkAggregator} from '../interfaces/IChainlinkAggregator.sol'; -import {DefaultReserveInterestRateStrategy} from '../protocol/lendingpool/DefaultReserveInterestRateStrategy.sol'; +import { + DefaultReserveInterestRateStrategy +} from '../protocol/lendingpool/DefaultReserveInterestRateStrategy.sol'; import {IERC20DetailedBytes} from './interfaces/IERC20DetailedBytes.sol'; contract UiPoolDataProviderV2 is IUiPoolDataProviderV2 { @@ -80,9 +82,8 @@ contract UiPoolDataProviderV2 is IUiPoolDataProviderV2 { reserveData.underlyingAsset = reserves[i]; // reserve current state - DataTypes.ReserveData memory baseData = lendingPool.getReserveData( - reserveData.underlyingAsset - ); + DataTypes.ReserveData memory baseData = + lendingPool.getReserveData(reserveData.underlyingAsset); reserveData.liquidityIndex = baseData.liquidityIndex; reserveData.variableBorrowIndex = baseData.variableBorrowIndex; reserveData.liquidityRate = baseData.currentLiquidityRate; @@ -150,8 +151,8 @@ contract UiPoolDataProviderV2 is IUiPoolDataProviderV2 { if (ETH_CURRENCY_UNIT == baseCurrencyUnit) { baseCurrencyInfo.marketReferenceCurrencyUnit = ETH_CURRENCY_UNIT; baseCurrencyInfo - .marketReferenceCurrencyPriceInUsd = marketReferenceCurrencyPriceInUsdProxyAggregator - .latestAnswer(); + .marketReferenceCurrencyPriceInUsd = marketReferenceCurrencyPriceInUsdProxyAggregator + .latestAnswer(); } else { baseCurrencyInfo.marketReferenceCurrencyUnit = baseCurrencyUnit; baseCurrencyInfo.marketReferenceCurrencyPriceInUsd = int256(baseCurrencyUnit); @@ -178,9 +179,8 @@ contract UiPoolDataProviderV2 is IUiPoolDataProviderV2 { address[] memory reserves = lendingPool.getReservesList(); DataTypes.UserConfigurationMap memory userConfig = lendingPool.getUserConfiguration(user); - UserReserveData[] memory userReservesData = new UserReserveData[]( - user != address(0) ? reserves.length : 0 - ); + UserReserveData[] memory userReservesData = + new UserReserveData[](user != address(0) ? reserves.length : 0); for (uint256 i = 0; i < reserves.length; i++) { DataTypes.ReserveData memory baseData = lendingPool.getReserveData(reserves[i]); @@ -194,16 +194,20 @@ contract UiPoolDataProviderV2 is IUiPoolDataProviderV2 { if (userConfig.isBorrowing(i)) { userReservesData[i].scaledVariableDebt = IVariableDebtToken( - baseData.variableDebtTokenAddress - ).scaledBalanceOf(user); + baseData + .variableDebtTokenAddress + ) + .scaledBalanceOf(user); userReservesData[i].principalStableDebt = IStableDebtToken(baseData.stableDebtTokenAddress) .principalBalanceOf(user); if (userReservesData[i].principalStableDebt != 0) { userReservesData[i].stableBorrowRate = IStableDebtToken(baseData.stableDebtTokenAddress) .getUserStableRate(user); userReservesData[i].stableBorrowLastUpdateTimestamp = IStableDebtToken( - baseData.stableDebtTokenAddress - ).getUserLastUpdated(user); + baseData + .stableDebtTokenAddress + ) + .getUserLastUpdated(user); } } } @@ -222,4 +226,4 @@ contract UiPoolDataProviderV2 is IUiPoolDataProviderV2 { } return string(bytesArray); } -} \ No newline at end of file +} diff --git a/contracts/misc/UiPoolDataProviderV2V3.sol b/contracts/misc/UiPoolDataProviderV2V3.sol index cf9a59296..9b90d34d0 100644 --- a/contracts/misc/UiPoolDataProviderV2V3.sol +++ b/contracts/misc/UiPoolDataProviderV2V3.sol @@ -238,4 +238,4 @@ contract UiPoolDataProviderV2V3 is IUiPoolDataProviderV3 { } return string(bytesArray); } -} +} \ No newline at end of file diff --git a/contracts/misc/interfaces/IAaveOracle.sol b/contracts/misc/interfaces/IAaveOracle.sol index 4c6014162..0bb0c6d8d 100644 --- a/contracts/misc/interfaces/IAaveOracle.sol +++ b/contracts/misc/interfaces/IAaveOracle.sol @@ -8,6 +8,7 @@ pragma solidity 0.6.12; interface IAaveOracle { function BASE_CURRENCY() external view returns (address); // if usd returns 0x0, if eth returns weth address + function BASE_CURRENCY_UNIT() external view returns (uint256); /*********** diff --git a/contracts/misc/interfaces/IERC20DetailedBytes.sol b/contracts/misc/interfaces/IERC20DetailedBytes.sol index 8c47df162..81c837bb3 100644 --- a/contracts/misc/interfaces/IERC20DetailedBytes.sol +++ b/contracts/misc/interfaces/IERC20DetailedBytes.sol @@ -9,4 +9,4 @@ interface IERC20DetailedBytes is IERC20 { function symbol() external view returns (bytes32); function decimals() external view returns (uint8); -} \ No newline at end of file +} diff --git a/contracts/misc/interfaces/IUiIncentiveDataProviderV2.sol b/contracts/misc/interfaces/IUiIncentiveDataProviderV2.sol index f397a30c7..99a419e0b 100644 --- a/contracts/misc/interfaces/IUiIncentiveDataProviderV2.sol +++ b/contracts/misc/interfaces/IUiIncentiveDataProviderV2.sol @@ -55,4 +55,4 @@ interface IUiIncentiveDataProviderV2 { external view returns (AggregatedReserveIncentiveData[] memory, UserReserveIncentiveData[] memory); -} \ No newline at end of file +} diff --git a/contracts/misc/interfaces/IUiIncentiveDataProviderV3.sol b/contracts/misc/interfaces/IUiIncentiveDataProviderV3.sol index 857546583..45190ad8e 100644 --- a/contracts/misc/interfaces/IUiIncentiveDataProviderV3.sol +++ b/contracts/misc/interfaces/IUiIncentiveDataProviderV3.sol @@ -38,13 +38,13 @@ interface IUiIncentiveDataProviderV3 { UserIncentiveData vTokenIncentivesUserData; UserIncentiveData sTokenIncentivesUserData; } - + struct UserIncentiveData { address tokenAddress; address incentiveControllerAddress; UserRewardInfo[] userRewardsInformation; } - + struct UserRewardInfo { string rewardTokenSymbol; address rewardOracleAddress; @@ -54,7 +54,6 @@ interface IUiIncentiveDataProviderV3 { int256 rewardPriceFeed; uint8 priceFeedDecimals; uint8 rewardTokenDecimals; - } function getReservesIncentivesData(ILendingPoolAddressesProvider provider) @@ -72,4 +71,4 @@ interface IUiIncentiveDataProviderV3 { external view returns (AggregatedReserveIncentiveData[] memory, UserReserveIncentiveData[] memory); -} \ No newline at end of file +} diff --git a/contracts/misc/interfaces/IUiPoolDataProviderV2.sol b/contracts/misc/interfaces/IUiPoolDataProviderV2.sol index a286f9ccc..1741eef6f 100644 --- a/contracts/misc/interfaces/IUiPoolDataProviderV2.sol +++ b/contracts/misc/interfaces/IUiPoolDataProviderV2.sol @@ -68,15 +68,10 @@ interface IUiPoolDataProviderV2 { function getReservesData(ILendingPoolAddressesProvider provider) external view - returns ( - AggregatedReserveData[] memory, - BaseCurrencyInfo memory - ); + returns (AggregatedReserveData[] memory, BaseCurrencyInfo memory); function getUserReservesData(ILendingPoolAddressesProvider provider, address user) external view - returns ( - UserReserveData[] memory - ); -} \ No newline at end of file + returns (UserReserveData[] memory); +} diff --git a/contracts/mocks/swap/MockParaSwapAugustus.sol b/contracts/mocks/swap/MockParaSwapAugustus.sol index 1cf321717..b243c1e28 100644 --- a/contracts/mocks/swap/MockParaSwapAugustus.sol +++ b/contracts/mocks/swap/MockParaSwapAugustus.sol @@ -12,10 +12,15 @@ contract MockParaSwapAugustus is IParaSwapAugustus { bool _expectingSwap; address _expectedFromToken; address _expectedToToken; + uint256 _expectedFromAmountMin; uint256 _expectedFromAmountMax; uint256 _receivedAmount; + uint256 _fromAmount; + uint256 _expectedToAmountMax; + uint256 _expectedToAmountMin; + constructor() public { TOKEN_TRANSFER_PROXY = new MockParaSwapTokenTransferProxy(); } @@ -39,6 +44,21 @@ contract MockParaSwapAugustus is IParaSwapAugustus { _receivedAmount = receivedAmount; } + function expectBuy( + address fromToken, + address toToken, + uint256 fromAmount, + uint256 toAmountMin, + uint256 toAmountMax + ) external { + _expectingSwap = true; + _expectedFromToken = fromToken; + _expectedToToken = toToken; + _fromAmount = fromAmount; + _expectedToAmountMin = toAmountMin; + _expectedToAmountMax = toAmountMax; + } + function swap( address fromToken, address toToken, @@ -56,4 +76,22 @@ contract MockParaSwapAugustus is IParaSwapAugustus { _expectingSwap = false; return _receivedAmount; } + + function buy( + address fromToken, + address toToken, + uint256 fromAmount, + uint256 toAmount + ) external returns (uint256) { + require(_expectingSwap, 'Not expecting swap'); + require(fromToken == _expectedFromToken, 'Unexpected from token'); + require(toToken == _expectedToToken, 'Unexpected to token'); + require(toAmount >= _expectedToAmountMin && toAmount <= _expectedToAmountMax, 'To amount out of range'); + require(_fromAmount <= fromAmount, 'From amount of tokens are higher than expected'); + TOKEN_TRANSFER_PROXY.transferFrom(fromToken, msg.sender, address(this), _fromAmount); + MintableERC20(toToken).mint(toAmount); + IERC20(toToken).transfer(msg.sender, toAmount); + _expectingSwap = false; + return fromAmount; + } } diff --git a/helpers/configuration.ts b/helpers/configuration.ts index ddc568d76..e4e29c018 100644 --- a/helpers/configuration.ts +++ b/helpers/configuration.ts @@ -23,7 +23,7 @@ export enum ConfigNames { Aave = 'Aave', Matic = 'Matic', Amm = 'Amm', - Avalanche = 'Avalanche' + Avalanche = 'Avalanche', } export const loadPoolConfig = (configName: ConfigNames): PoolConfiguration => { @@ -34,8 +34,8 @@ export const loadPoolConfig = (configName: ConfigNames): PoolConfiguration => { return MaticConfig; case ConfigNames.Amm: return AmmConfig; - case ConfigNames.Avalanche: - return AvalancheConfig; + case ConfigNames.Avalanche: + return AvalancheConfig; case ConfigNames.Commons: return CommonsConfig; default: @@ -65,7 +65,7 @@ export const getReservesConfigByPool = (pool: AavePools): iMultiPoolsAssets<IRes }, [AavePools.avalanche]: { ...AvalancheConfig.ReservesConfig, - } + }, }, pool ); diff --git a/helpers/contracts-deployments.ts b/helpers/contracts-deployments.ts index 7292a217f..e28500cb6 100644 --- a/helpers/contracts-deployments.ts +++ b/helpers/contracts-deployments.ts @@ -55,6 +55,7 @@ import { UiPoolDataProviderV2V3Factory, UiIncentiveDataProviderV2V3, UiIncentiveDataProviderV2Factory, + ParaSwapRepayAdapterFactory, } from '../types'; import { withSaveAndVerify, @@ -791,3 +792,14 @@ export const deployParaSwapLiquiditySwapAdapter = async ( args, verify ); + +export const deployParaSwapRepayAdapter = async ( + args: [tEthereumAddress, tEthereumAddress], + verify?: boolean +) => + withSaveAndVerify( + await new ParaSwapRepayAdapterFactory(await getFirstSigner()).deploy(...args), + eContractid.ParaSwapRepayAdapter, + args, + verify + ); diff --git a/helpers/contracts-getters.ts b/helpers/contracts-getters.ts index 0395cdd71..5372a83f9 100644 --- a/helpers/contracts-getters.ts +++ b/helpers/contracts-getters.ts @@ -33,6 +33,7 @@ import { WETH9MockedFactory, WETHGatewayFactory, FlashLiquidationAdapterFactory, + ParaSwapRepayAdapterFactory, } from '../types'; import { IERC20DetailedFactory } from '../types/IERC20DetailedFactory'; import { getEthersSigners, MockTokenMap } from './contracts-helpers'; @@ -451,3 +452,12 @@ export const getParaSwapLiquiditySwapAdapter = async (address?: tEthereumAddress ).address, await getFirstSigner() ); + +export const getParaSwapRepayAdapter = async (address?: tEthereumAddress) => + await ParaSwapRepayAdapterFactory.connect( + address || + ( + await getDb().get(`${eContractid.ParaSwapRepayAdapter}.${DRE.network.name}`).value() + ).address, + await getFirstSigner() + ); diff --git a/helpers/contracts-helpers.ts b/helpers/contracts-helpers.ts index 98cf2236a..a64a753d6 100644 --- a/helpers/contracts-helpers.ts +++ b/helpers/contracts-helpers.ts @@ -378,6 +378,46 @@ export const buildParaSwapLiquiditySwapParams = ( ); }; +export const buildParaswapBuyParams = ( + buyCalldata: string | Buffer, + augustus: tEthereumAddress +) => { + return ethers.utils.defaultAbiCoder.encode(['bytes', 'address'], [buyCalldata, augustus]); +}; +export const buildParaSwapRepayParams = ( + collateralAsset: tEthereumAddress, + collateralAmount: BigNumberish, + buyAllBalanceOffset: BigNumberish, + debtRateMode: BigNumberish, + buyCalldata: string | Buffer, + augustus: tEthereumAddress, + permitAmount: BigNumberish, + deadline: BigNumberish, + v: BigNumberish, + r: string | Buffer, + s: string | Buffer +) => { + const paraswapData = buildParaswapBuyParams(buyCalldata, augustus); + return ethers.utils.defaultAbiCoder.encode( + [ + 'address', + 'uint256', + 'uint256', + 'uint256', + 'bytes', + 'tuple(uint256,uint256,uint8,bytes32,bytes32)', + ], + [ + collateralAsset, + collateralAmount, + buyAllBalanceOffset, + debtRateMode, + paraswapData, + [permitAmount, deadline, v, r, s], + ] + ); +}; + export const verifyContract = async ( id: string, instance: Contract, diff --git a/helpers/types.ts b/helpers/types.ts index d4619ddf8..51b76dea2 100644 --- a/helpers/types.ts +++ b/helpers/types.ts @@ -102,6 +102,7 @@ export enum eContractid { MockParaSwapAugustus = 'MockParaSwapAugustus', MockParaSwapAugustusRegistry = 'MockParaSwapAugustusRegistry', ParaSwapLiquiditySwapAdapter = 'ParaSwapLiquiditySwapAdapter', + ParaSwapRepayAdapter = 'ParaSwapRepayAdapter', UiIncentiveDataProviderV2V3 = 'UiIncentiveDataProviderV2V3', UiIncentiveDataProviderV2 = 'UiIncentiveDataProviderV2', } diff --git a/tasks/deployments/deploy-ParaSwapRepayAdapter.ts b/tasks/deployments/deploy-ParaSwapRepayAdapter.ts new file mode 100644 index 000000000..efbc338d7 --- /dev/null +++ b/tasks/deployments/deploy-ParaSwapRepayAdapter.ts @@ -0,0 +1,36 @@ +import { task } from 'hardhat/config'; + +import { ParaSwapRepayAdapterFactory } from '../../types'; +import { verifyContract } from '../../helpers/contracts-helpers'; +import { getFirstSigner } from '../../helpers/contracts-getters'; +import { eContractid } from '../../helpers/types'; + +const CONTRACT_NAME = 'ParaSwapRepayAdapter'; + +task(`deploy-${CONTRACT_NAME}`, `Deploys the ${CONTRACT_NAME} contract`) + .addParam('provider', 'Address of the LendingPoolAddressesProvider') + .addParam('augustusRegistry', 'Address of ParaSwap AugustusRegistry') + .addFlag('verify', `Verify ${CONTRACT_NAME} contract via Etherscan API.`) + .setAction(async ({ provider, augustusRegistry, verify }, localBRE) => { + await localBRE.run('set-DRE'); + + if (!localBRE.network.config.chainId) { + throw new Error('INVALID_CHAIN_ID'); + } + + console.log(`\n- ${CONTRACT_NAME} deployment`); + const adapter = await new ParaSwapRepayAdapterFactory( + await getFirstSigner() + ).deploy(provider, augustusRegistry); + await adapter.deployTransaction.wait(); + console.log(`${CONTRACT_NAME}.address`, adapter.address); + + if (verify) { + await verifyContract(eContractid.ParaSwapRepayAdapter, adapter, [ + provider, + augustusRegistry, + ]); + } + + console.log(`\tFinished ${CONTRACT_NAME} deployment`); + }); diff --git a/test-suites/test-aave/__setup.spec.ts b/test-suites/test-aave/__setup.spec.ts index 38d2a88f8..dbb7afe68 100644 --- a/test-suites/test-aave/__setup.spec.ts +++ b/test-suites/test-aave/__setup.spec.ts @@ -29,6 +29,7 @@ import { deployMockParaSwapAugustus, deployMockParaSwapAugustusRegistry, deployParaSwapLiquiditySwapAdapter, + deployParaSwapRepayAdapter, authorizeWETHGateway, deployATokenImplementations, deployAaveOracle, @@ -303,6 +304,8 @@ const buildTestEnv = async (deployer: Signer, secondaryWallet: Signer) => { await deployParaSwapLiquiditySwapAdapter([addressesProvider.address, augustusRegistry.address]); + await deployParaSwapRepayAdapter([addressesProvider.address, augustusRegistry.address]); + await deployWalletBalancerProvider(); const gateWay = await deployWETHGateway([mockTokens.WETH.address]); diff --git a/test-suites/test-aave/helpers/make-suite.ts b/test-suites/test-aave/helpers/make-suite.ts index c4eb7893d..df7b95861 100644 --- a/test-suites/test-aave/helpers/make-suite.ts +++ b/test-suites/test-aave/helpers/make-suite.ts @@ -15,6 +15,7 @@ import { getUniswapRepayAdapter, getFlashLiquidationAdapter, getParaSwapLiquiditySwapAdapter, + getParaSwapRepayAdapter, } from '../../../helpers/contracts-getters'; import { eEthereumNetwork, eNetwork, tEthereumAddress } from '../../../helpers/types'; import { LendingPool } from '../../../types/LendingPool'; @@ -39,7 +40,7 @@ import { WETH9Mocked } from '../../../types/WETH9Mocked'; import { WETHGateway } from '../../../types/WETHGateway'; import { solidity } from 'ethereum-waffle'; import { AaveConfig } from '../../../markets/aave'; -import { FlashLiquidationAdapter } from '../../../types'; +import { FlashLiquidationAdapter, ParaSwapRepayAdapter } from '../../../types'; import { HardhatRuntimeEnvironment } from 'hardhat/types'; import { usingTenderly } from '../../../helpers/tenderly-utils'; @@ -71,6 +72,8 @@ export interface TestEnv { wethGateway: WETHGateway; flashLiquidationAdapter: FlashLiquidationAdapter; paraswapLiquiditySwapAdapter: ParaSwapLiquiditySwapAdapter; + paraswapRepayAdapter: ParaSwapRepayAdapter; + } let buidlerevmSnapshotId: string = '0x1'; @@ -96,6 +99,7 @@ const testEnv: TestEnv = { uniswapRepayAdapter: {} as UniswapRepayAdapter, flashLiquidationAdapter: {} as FlashLiquidationAdapter, paraswapLiquiditySwapAdapter: {} as ParaSwapLiquiditySwapAdapter, + paraswapRepayAdapter: {} as ParaSwapRepayAdapter, registry: {} as LendingPoolAddressesProviderRegistry, wethGateway: {} as WETHGateway, } as TestEnv; @@ -164,6 +168,7 @@ export async function initializeMakeSuite() { testEnv.flashLiquidationAdapter = await getFlashLiquidationAdapter(); testEnv.paraswapLiquiditySwapAdapter = await getParaSwapLiquiditySwapAdapter(); + testEnv.paraswapRepayAdapter = await getParaSwapRepayAdapter(); } const setSnapshot = async () => { diff --git a/test-suites/test-aave/paraswapAdapters.repay.spec.ts b/test-suites/test-aave/paraswapAdapters.repay.spec.ts new file mode 100644 index 000000000..7c4d44f07 --- /dev/null +++ b/test-suites/test-aave/paraswapAdapters.repay.spec.ts @@ -0,0 +1,1576 @@ +import { makeSuite, TestEnv } from './helpers/make-suite'; +import { + convertToCurrencyDecimals, + getContract, + buildPermitParams, + getSignatureFromTypedData, + buildRepayAdapterParams, + buildParaSwapRepayParams, + buildParaswapBuyParams, +} from '../../helpers/contracts-helpers'; +import { + getMockParaSwapAugustus, + getMockParaSwapAugustusRegistry, +} from '../../helpers/contracts-getters'; +import { deployParaSwapRepayAdapter } from '../../helpers/contracts-deployments'; +import { Zero } from '@ethersproject/constants'; +import BigNumber from 'bignumber.js'; +import { DRE, evmRevert, evmSnapshot } from '../../helpers/misc-utils'; +import { ethers } from 'ethers'; +import { eContractid } from '../../helpers/types'; +import { StableDebtToken } from '../../types/StableDebtToken'; +import { BUIDLEREVM_CHAINID } from '../../helpers/buidler-constants'; +import { MAX_UINT_AMOUNT } from '../../helpers/constants'; +import { MockParaSwapAugustus, MockParaSwapAugustusRegistry, VariableDebtToken } from '../../types'; +import exp from 'constants'; +const { parseEther } = ethers.utils; + +const { expect } = require('chai'); + +makeSuite('Paraswap adapters', (testEnv: TestEnv) => { + let mockAugustus: MockParaSwapAugustus; + let mockAugustusRegistry: MockParaSwapAugustusRegistry; + let evmSnapshotId: string; + + before(async () => { + mockAugustus = await getMockParaSwapAugustus(); + mockAugustusRegistry = await getMockParaSwapAugustusRegistry(); + }); + + beforeEach(async () => { + evmSnapshotId = await evmSnapshot(); + }); + + afterEach(async () => { + await evmRevert(evmSnapshotId); + }); + + describe('ParaswapRepayAdapter', () => { + beforeEach(async () => { + const { users, weth, dai, usdc, aave, pool, deployer } = testEnv; + const userAddress = users[0].address; + + // Provide liquidity + await dai.mint(parseEther('20000')); + await dai.approve(pool.address, parseEther('20000')); + await pool.deposit(dai.address, parseEther('20000'), deployer.address, 0); + + const usdcLiquidity = await convertToCurrencyDecimals(usdc.address, '2000000'); + await usdc.mint(usdcLiquidity); + await usdc.approve(pool.address, usdcLiquidity); + await pool.deposit(usdc.address, usdcLiquidity, deployer.address, 0); + + await weth.mint(parseEther('100')); + await weth.approve(pool.address, parseEther('100')); + await pool.deposit(weth.address, parseEther('100'), deployer.address, 0); + + await aave.mint(parseEther('1000000')); + await aave.approve(pool.address, parseEther('1000000')); + await pool.deposit(aave.address, parseEther('1000000'), deployer.address, 0); + + // Make a deposit for user + await weth.mint(parseEther('1000')); + await weth.approve(pool.address, parseEther('1000')); + await pool.deposit(weth.address, parseEther('1000'), userAddress, 0); + + await aave.mint(parseEther('1000000')); + await aave.approve(pool.address, parseEther('1000000')); + await pool.deposit(aave.address, parseEther('1000000'), userAddress, 0); + + await usdc.mint(usdcLiquidity); + await usdc.approve(pool.address, usdcLiquidity); + await pool.deposit(usdc.address, usdcLiquidity, userAddress, 0); + }); + + describe('constructor', () => { + it('should deploy with correct parameters', async () => { + const { addressesProvider } = testEnv; + await deployParaSwapRepayAdapter([addressesProvider.address, mockAugustusRegistry.address]); + }); + + it('should revert if not valid addresses provider', async () => { + await expect( + deployParaSwapRepayAdapter([ + mockAugustus.address, // any invalid contract can be used here + mockAugustusRegistry.address, + ]) + ).to.be.reverted; + }); + + it('should revert if not valid augustus registry', async () => { + const { addressesProvider } = testEnv; + await expect( + deployParaSwapRepayAdapter([ + addressesProvider.address, + mockAugustus.address, // any invalid contract can be used here + ]) + ).to.be.reverted; + }); + }); + + describe('executeOperation', () => { + it('should correctly swap tokens and repay debt', async () => { + const { users, pool, weth, aWETH, oracle, dai, paraswapRepayAdapter, helpersContract } = + testEnv; + const user = users[0].signer; + const userAddress = users[0].address; + const amountWETHtoSwap = await convertToCurrencyDecimals(weth.address, '10'); + + const daiPrice = await oracle.getAssetPrice(dai.address); + const expectedDaiAmount = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) + ); + // Open user Debt + await pool.connect(user).borrow(dai.address, expectedDaiAmount, 1, 0, userAddress); + + const daiStableDebtTokenAddress = ( + await helpersContract.getReserveTokensAddresses(dai.address) + ).stableDebtTokenAddress; + + const daiStableDebtContract = await getContract<StableDebtToken>( + eContractid.StableDebtToken, + daiStableDebtTokenAddress + ); + + const userDaiStableDebtAmountBefore = await daiStableDebtContract.balanceOf(userAddress); + + await mockAugustus.expectBuy( + weth.address, + dai.address, + amountWETHtoSwap, + expectedDaiAmount, + expectedDaiAmount + ); + const mockAugustusCalldata = mockAugustus.interface.encodeFunctionData('buy', [ + weth.address, + dai.address, + amountWETHtoSwap, + expectedDaiAmount, + ]); + + const flashloanPremium = amountWETHtoSwap.mul(9).div(10000); + const flashloanTotal = amountWETHtoSwap.add(flashloanPremium); + await aWETH.connect(user).approve(paraswapRepayAdapter.address, flashloanTotal); + const userAEthBalanceBefore = await aWETH.balanceOf(userAddress); + + const params = buildParaSwapRepayParams( + dai.address, + expectedDaiAmount, + 0, + 1, + mockAugustusCalldata, + mockAugustus.address, + 0, + 0, + 0, + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000' + ); + + await expect( + pool + .connect(user) + .flashLoan( + paraswapRepayAdapter.address, + [weth.address], + [amountWETHtoSwap.toString()], + [0], + userAddress, + params, + 0 + ) + ) + .to.emit(paraswapRepayAdapter, 'Bought') + .withArgs(weth.address, dai.address, amountWETHtoSwap, expectedDaiAmount); + + const adapterWethBalance = await weth.balanceOf(paraswapRepayAdapter.address); + const adapterDaiBalance = await dai.balanceOf(paraswapRepayAdapter.address); + const userDaiStableDebtAmount = await daiStableDebtContract.balanceOf(userAddress); + const userAEthBalance = await aWETH.balanceOf(userAddress); + + expect(adapterWethBalance).to.be.eq(Zero); + expect(adapterDaiBalance).to.be.eq(Zero); + expect(userDaiStableDebtAmountBefore).to.be.gte(expectedDaiAmount); + expect(userDaiStableDebtAmount).to.be.lt(expectedDaiAmount); + expect(userAEthBalance).to.be.lt(userAEthBalanceBefore); + expect(userAEthBalance).to.be.gte(userAEthBalanceBefore.sub(flashloanTotal)); + }); + + it('should correctly repay debt with same collateral', async () => { + const { users, pool, weth, aWETH, paraswapRepayAdapter, helpersContract } = testEnv; + const user = users[0].signer; + const userAddress = users[0].address; + const amountWETHtoRepay = await convertToCurrencyDecimals(weth.address, '10'); + + // Open user Debt + await pool.connect(user).borrow(weth.address, amountWETHtoRepay, 2, 0, userAddress); + + const wethVariableDebtTokenAddress = ( + await helpersContract.getReserveTokensAddresses(weth.address) + ).variableDebtTokenAddress; + + const wethVariableDebtContract = await getContract<VariableDebtToken>( + eContractid.VariableDebtToken, + wethVariableDebtTokenAddress + ); + + const userwethVariableDebtAmountBefore = await wethVariableDebtContract.balanceOf( + userAddress + ); + + await mockAugustus.expectBuy( + weth.address, + weth.address, + amountWETHtoRepay, + amountWETHtoRepay, + amountWETHtoRepay + ); + const mockAugustusCalldata = mockAugustus.interface.encodeFunctionData('buy', [ + weth.address, + weth.address, + amountWETHtoRepay, + amountWETHtoRepay, + ]); + + const flashloanPremium = amountWETHtoRepay.mul(9).div(10000); + const flashloanTotal = amountWETHtoRepay.add(flashloanPremium); + await aWETH.connect(user).approve(paraswapRepayAdapter.address, flashloanTotal); + const userAEthBalanceBefore = await aWETH.balanceOf(userAddress); + + const params = buildParaSwapRepayParams( + weth.address, + amountWETHtoRepay, + 0, + 2, + mockAugustusCalldata, + mockAugustus.address, + 0, + 0, + 0, + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000' + ); + + await expect( + pool + .connect(user) + .flashLoan( + paraswapRepayAdapter.address, + [weth.address], + [amountWETHtoRepay.toString()], + [0], + userAddress, + params, + 0 + ) + ); + + const adapterWethBalance = await weth.balanceOf(paraswapRepayAdapter.address); + const userwethVariableDebtAmount = await wethVariableDebtContract.balanceOf(userAddress); + const userAEthBalance = await aWETH.balanceOf(userAddress); + + expect(adapterWethBalance).to.be.eq(Zero); + expect(userwethVariableDebtAmountBefore).to.be.gte(amountWETHtoRepay); + expect(userwethVariableDebtAmount).to.be.lt(amountWETHtoRepay); + expect(userAEthBalance).to.be.lt(userAEthBalanceBefore); + expect(userAEthBalance).to.be.gte(userAEthBalanceBefore.sub(flashloanTotal)); + }); + + it('should correctly swap tokens and repay debt with permit', async () => { + const { users, pool, weth, aWETH, oracle, dai, paraswapRepayAdapter, helpersContract } = + testEnv; + const user = users[0].signer; + const userAddress = users[0].address; + + const amountWETHtoSwap = await convertToCurrencyDecimals(weth.address, '10'); + + const daiPrice = await oracle.getAssetPrice(dai.address); + const expectedDaiAmount = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) + ); + + // Open user Debt + await pool.connect(user).borrow(dai.address, expectedDaiAmount, 1, 0, userAddress); + + const daiStableDebtTokenAddress = ( + await helpersContract.getReserveTokensAddresses(dai.address) + ).stableDebtTokenAddress; + + const daiStableDebtContract = await getContract<StableDebtToken>( + eContractid.StableDebtToken, + daiStableDebtTokenAddress + ); + + const userDaiStableDebtAmountBefore = await daiStableDebtContract.balanceOf(userAddress); + + await mockAugustus.expectBuy( + weth.address, + dai.address, + amountWETHtoSwap, + expectedDaiAmount, + expectedDaiAmount + ); + const mockAugustusCalldata = mockAugustus.interface.encodeFunctionData('buy', [ + weth.address, + dai.address, + amountWETHtoSwap, + expectedDaiAmount, + ]); + + const flashloanPremium = amountWETHtoSwap.mul(9).div(10000); + const flashloanTotal = amountWETHtoSwap.add(flashloanPremium); + await aWETH.connect(user).approve(paraswapRepayAdapter.address, flashloanTotal); + const userAEthBalanceBefore = await aWETH.balanceOf(userAddress); + + const chainId = DRE.network.config.chainId || BUIDLEREVM_CHAINID; + const deadline = MAX_UINT_AMOUNT; + const nonce = (await aWETH._nonces(userAddress)).toNumber(); + const msgParams = buildPermitParams( + chainId, + aWETH.address, + '1', + await aWETH.name(), + userAddress, + paraswapRepayAdapter.address, + nonce, + deadline, + flashloanTotal.toString() + ); + + const ownerPrivateKey = require('../../test-wallets.js').accounts[1].secretKey; + if (!ownerPrivateKey) { + throw new Error('INVALID_OWNER_PK'); + } + + const { v, r, s } = getSignatureFromTypedData(ownerPrivateKey, msgParams); + + const params = buildParaSwapRepayParams( + dai.address, + expectedDaiAmount, + 0, + 1, + mockAugustusCalldata, + mockAugustus.address, + flashloanTotal, + deadline, + v, + r, + s + ); + + await expect( + pool + .connect(user) + .flashLoan( + paraswapRepayAdapter.address, + [weth.address], + [amountWETHtoSwap.toString()], + [0], + userAddress, + params, + 0 + ) + ) + .to.emit(paraswapRepayAdapter, 'Bought') + .withArgs(weth.address, dai.address, amountWETHtoSwap, expectedDaiAmount); + + const adapterWethBalance = await weth.balanceOf(paraswapRepayAdapter.address); + const adapterDaiBalance = await dai.balanceOf(paraswapRepayAdapter.address); + const userDaiStableDebtAmount = await daiStableDebtContract.balanceOf(userAddress); + const userAEthBalance = await aWETH.balanceOf(userAddress); + + expect(adapterWethBalance).to.be.eq(Zero); + expect(adapterDaiBalance).to.be.eq(Zero); + expect(userDaiStableDebtAmountBefore).to.be.gte(expectedDaiAmount); + expect(userDaiStableDebtAmount).to.be.lt(expectedDaiAmount); + expect(userAEthBalance).to.be.lt(userAEthBalanceBefore); + expect(userAEthBalance).to.be.gte(userAEthBalanceBefore.sub(flashloanTotal)); + }); + + it('should revert if caller not lending pool', async () => { + const { users, pool, weth, aWETH, oracle, dai, paraswapRepayAdapter } = testEnv; + const user = users[0].signer; + const userAddress = users[0].address; + + const amountWETHtoSwap = await convertToCurrencyDecimals(weth.address, '10'); + + const daiPrice = await oracle.getAssetPrice(dai.address); + const expectedDaiAmount = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) + ); + + // Open user Debt + await pool.connect(user).borrow(dai.address, expectedDaiAmount, 1, 0, userAddress); + + await mockAugustus.expectBuy( + weth.address, + dai.address, + amountWETHtoSwap, + expectedDaiAmount, + expectedDaiAmount + ); + const mockAugustusCalldata = mockAugustus.interface.encodeFunctionData('buy', [ + weth.address, + dai.address, + amountWETHtoSwap, + expectedDaiAmount, + ]); + + const flashloanPremium = amountWETHtoSwap.mul(9).div(10000); + const flashloanTotal = amountWETHtoSwap.add(flashloanPremium); + await aWETH.connect(user).approve(paraswapRepayAdapter.address, flashloanTotal); + + const params = buildParaSwapRepayParams( + dai.address, + expectedDaiAmount, + 0, + 1, + mockAugustusCalldata, + mockAugustus.address, + 0, + 0, + 0, + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000' + ); + + await expect( + paraswapRepayAdapter + .connect(user) + .executeOperation( + [weth.address], + [amountWETHtoSwap.toString()], + [0], + userAddress, + params + ) + ).to.be.revertedWith('CALLER_MUST_BE_LENDING_POOL'); + }); + + it('should revert if there is not debt to repay with the specified rate mode', async () => { + const { users, pool, weth, oracle, dai, paraswapRepayAdapter, aWETH } = testEnv; + const user = users[0].signer; + const userAddress = users[0].address; + + const amountWETHtoSwap = await convertToCurrencyDecimals(weth.address, '10'); + + await weth.connect(user).mint(amountWETHtoSwap); + await weth.connect(user).transfer(paraswapRepayAdapter.address, amountWETHtoSwap); + + const daiPrice = await oracle.getAssetPrice(dai.address); + const expectedDaiAmount = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) + ); + + // Open user Debt + await pool.connect(user).borrow(dai.address, expectedDaiAmount, 2, 0, userAddress); + + await mockAugustus.expectBuy( + weth.address, + dai.address, + amountWETHtoSwap, + expectedDaiAmount, + expectedDaiAmount + ); + const mockAugustusCalldata = mockAugustus.interface.encodeFunctionData('buy', [ + weth.address, + dai.address, + amountWETHtoSwap, + expectedDaiAmount, + ]); + + const flashloanPremium = amountWETHtoSwap.mul(9).div(10000); + const flashloanTotal = amountWETHtoSwap.add(flashloanPremium); + await aWETH.connect(user).approve(paraswapRepayAdapter.address, flashloanTotal); + + const params = buildParaSwapRepayParams( + dai.address, + expectedDaiAmount, + 0, + 1, + mockAugustusCalldata, + mockAugustus.address, + 0, + 0, + 0, + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000' + ); + + await expect( + pool + .connect(user) + .flashLoan( + paraswapRepayAdapter.address, + [weth.address], + [amountWETHtoSwap.toString()], + [0], + userAddress, + params, + 0 + ) + ).to.be.reverted; + }); + + it('should revert if there is not debt to repay', async () => { + const { users, pool, weth, oracle, dai, paraswapRepayAdapter, aWETH } = testEnv; + const user = users[0].signer; + const userAddress = users[0].address; + + const amountWETHtoSwap = await convertToCurrencyDecimals(weth.address, '10'); + + await weth.connect(user).mint(amountWETHtoSwap); + await weth.connect(user).transfer(paraswapRepayAdapter.address, amountWETHtoSwap); + + const daiPrice = await oracle.getAssetPrice(dai.address); + const expectedDaiAmount = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) + ); + + await mockAugustus.expectBuy( + weth.address, + dai.address, + amountWETHtoSwap, + expectedDaiAmount, + expectedDaiAmount + ); + const mockAugustusCalldata = mockAugustus.interface.encodeFunctionData('buy', [ + weth.address, + dai.address, + amountWETHtoSwap, + expectedDaiAmount, + ]); + + const flashloanPremium = amountWETHtoSwap.mul(9).div(10000); + const flashloanTotal = amountWETHtoSwap.add(flashloanPremium); + await aWETH.connect(user).approve(paraswapRepayAdapter.address, flashloanTotal); + + const params = buildParaSwapRepayParams( + dai.address, + expectedDaiAmount, + 0, + 1, + mockAugustusCalldata, + mockAugustus.address, + 0, + 0, + 0, + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000' + ); + + await expect( + pool + .connect(user) + .flashLoan( + paraswapRepayAdapter.address, + [weth.address], + [amountWETHtoSwap.toString()], + [0], + userAddress, + params, + 0 + ) + ).to.be.reverted; + }); + + it('should revert when max amount allowed to swap is bigger than max slippage', async () => { + const { users, pool, weth, oracle, dai, aWETH, paraswapRepayAdapter } = testEnv; + const user = users[0].signer; + const userAddress = users[0].address; + + const amountWETHtoSwap = await convertToCurrencyDecimals(weth.address, '10'); + + const daiPrice = await oracle.getAssetPrice(dai.address); + const expectedDaiAmount = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) + ); + + // Open user Debt + await pool.connect(user).borrow(dai.address, expectedDaiAmount, 1, 0, userAddress); + + const bigMaxAmountToSwap = amountWETHtoSwap.mul(2); + await mockAugustus.expectBuy( + weth.address, + dai.address, + bigMaxAmountToSwap, + expectedDaiAmount, + expectedDaiAmount + ); + const mockAugustusCalldata = mockAugustus.interface.encodeFunctionData('buy', [ + weth.address, + dai.address, + bigMaxAmountToSwap, + expectedDaiAmount, + ]); + + const flashloanPremium = bigMaxAmountToSwap.mul(9).div(10000); + const flashloanTotal = bigMaxAmountToSwap.add(flashloanPremium); + await aWETH.connect(user).approve(paraswapRepayAdapter.address, flashloanTotal); + + const params = buildParaSwapRepayParams( + dai.address, + bigMaxAmountToSwap, + 0, + 1, + mockAugustusCalldata, + mockAugustus.address, + 0, + 0, + 0, + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000' + ); + + await expect( + pool + .connect(user) + .flashLoan( + paraswapRepayAdapter.address, + [weth.address], + [amountWETHtoSwap.toString()], + [0], + userAddress, + params, + 0 + ) + ).to.be.revertedWith('maxAmountToSwap exceed max slippage'); + }); + + it('should swap, repay debt and pull the needed ATokens leaving no leftovers', async () => { + const { users, pool, weth, aWETH, oracle, dai, paraswapRepayAdapter, helpersContract } = + testEnv; + const user = users[0].signer; + const userAddress = users[0].address; + + const amountWETHtoSwap = await convertToCurrencyDecimals(weth.address, '10'); + + const daiPrice = await oracle.getAssetPrice(dai.address); + const expectedDaiAmount = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) + ); + + // Open user Debt + await pool.connect(user).borrow(dai.address, expectedDaiAmount, 1, 0, userAddress); + + const daiStableDebtTokenAddress = ( + await helpersContract.getReserveTokensAddresses(dai.address) + ).stableDebtTokenAddress; + + const daiStableDebtContract = await getContract<StableDebtToken>( + eContractid.StableDebtToken, + daiStableDebtTokenAddress + ); + + const userDaiStableDebtAmountBefore = await daiStableDebtContract.balanceOf(userAddress); + + await mockAugustus.expectBuy( + weth.address, + dai.address, + amountWETHtoSwap, + expectedDaiAmount, + expectedDaiAmount + ); + const mockAugustusCalldata = mockAugustus.interface.encodeFunctionData('buy', [ + weth.address, + dai.address, + amountWETHtoSwap, + expectedDaiAmount, + ]); + + const flashloanPremium = amountWETHtoSwap.mul(9).div(10000); + const flashloanTotal = amountWETHtoSwap.add(flashloanPremium); + await aWETH.connect(user).approve(paraswapRepayAdapter.address, flashloanTotal); + const userAEthBalanceBefore = await aWETH.balanceOf(userAddress); + + const params = buildParaSwapRepayParams( + dai.address, + expectedDaiAmount, + 0, + 1, + mockAugustusCalldata, + mockAugustus.address, + 0, + 0, + 0, + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000' + ); + + await expect( + pool + .connect(user) + .flashLoan( + paraswapRepayAdapter.address, + [weth.address], + [amountWETHtoSwap.toString()], + [0], + userAddress, + params, + 0 + ) + ) + .to.emit(paraswapRepayAdapter, 'Bought') + .withArgs(weth.address, dai.address, amountWETHtoSwap.toString(), expectedDaiAmount); + + const adapterWethBalance = await weth.balanceOf(paraswapRepayAdapter.address); + const adapterDaiBalance = await dai.balanceOf(paraswapRepayAdapter.address); + const userDaiStableDebtAmount = await daiStableDebtContract.balanceOf(userAddress); + const userAEthBalance = await aWETH.balanceOf(userAddress); + const adapterAEthBalance = await aWETH.balanceOf(paraswapRepayAdapter.address); + + expect(adapterAEthBalance).to.be.eq(Zero); + expect(adapterWethBalance).to.be.eq(Zero); + expect(adapterDaiBalance).to.be.eq(Zero); + expect(userDaiStableDebtAmountBefore).to.be.gte(expectedDaiAmount); + expect(userDaiStableDebtAmount).to.be.lt(expectedDaiAmount); + expect(userAEthBalance).to.be.lt(userAEthBalanceBefore); + expect(userAEthBalance).to.be.gte(userAEthBalanceBefore.sub(flashloanTotal)); + }); + + it('should correctly swap tokens and repay the whole stable debt', async () => { + const { users, pool, weth, aWETH, oracle, dai, paraswapRepayAdapter, helpersContract } = + testEnv; + const user = users[0].signer; + const userAddress = users[0].address; + + const amountWETHtoSwap = await convertToCurrencyDecimals(weth.address, '10'); + + const daiPrice = await oracle.getAssetPrice(dai.address); + const expectedDaiAmount = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) + ); + + // Open user Debt + await pool.connect(user).borrow(dai.address, expectedDaiAmount, 1, 0, userAddress); + + const daiStableDebtTokenAddress = ( + await helpersContract.getReserveTokensAddresses(dai.address) + ).stableDebtTokenAddress; + + const daiStableDebtContract = await getContract<StableDebtToken>( + eContractid.StableDebtToken, + daiStableDebtTokenAddress + ); + + const userDaiStableDebtAmountBefore = await daiStableDebtContract.balanceOf(userAddress); + + // Add a % to repay on top of the debt + const flashLoanAmount = await convertToCurrencyDecimals(weth.address, '11'); + const amountToRepay = new BigNumber(expectedDaiAmount.toString()) + .multipliedBy(1.1) + .toFixed(0); + const liquidityToSwap = await convertToCurrencyDecimals(weth.address, '10.8'); + await mockAugustus.expectBuy( + weth.address, + dai.address, + liquidityToSwap, + expectedDaiAmount, + amountToRepay + ); + + const mockAugustusCalldata = mockAugustus.interface.encodeFunctionData('buy', [ + weth.address, + dai.address, + flashLoanAmount, + expectedDaiAmount, + ]); + + const flashloanPremium = flashLoanAmount.mul(9).div(10000); + const flashloanTotal = flashLoanAmount.add(flashloanPremium); + await aWETH.connect(user).approve(paraswapRepayAdapter.address, flashloanTotal); + const userAEthBalanceBefore = await aWETH.balanceOf(userAddress); + + // Add a % to repay on top of the debt + + const params = buildParaSwapRepayParams( + dai.address, + amountToRepay, + 4 + 3 * 32, + 1, + mockAugustusCalldata, + mockAugustus.address, + 0, + 0, + 0, + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000' + ); + + await pool + .connect(user) + .flashLoan( + paraswapRepayAdapter.address, + [weth.address], + [flashLoanAmount.toString()], + [0], + userAddress, + params, + 0 + ); + + const adapterWethBalance = await weth.balanceOf(paraswapRepayAdapter.address); + const adapterDaiBalance = await dai.balanceOf(paraswapRepayAdapter.address); + const userDaiStableDebtAmount = await daiStableDebtContract.balanceOf(userAddress); + const userAEthBalance = await aWETH.balanceOf(userAddress); + const adapterAEthBalance = await aWETH.balanceOf(paraswapRepayAdapter.address); + + expect(adapterAEthBalance).to.be.eq(Zero); + expect(adapterWethBalance).to.be.eq(Zero); + expect(adapterDaiBalance).to.be.eq(Zero); + expect(userDaiStableDebtAmountBefore).to.be.gte(expectedDaiAmount); + expect(userDaiStableDebtAmount).to.be.eq(Zero); + expect(userAEthBalance).to.be.lt(userAEthBalanceBefore); + expect(userAEthBalance).to.be.gte(userAEthBalanceBefore.sub(flashloanTotal)); + }); + + it('should correctly swap tokens and repay the whole variable debt', async () => { + const { users, pool, weth, aWETH, oracle, dai, paraswapRepayAdapter, helpersContract } = + testEnv; + const user = users[0].signer; + const userAddress = users[0].address; + + const amountWETHtoSwap = await convertToCurrencyDecimals(weth.address, '10'); + + const daiPrice = await oracle.getAssetPrice(dai.address); + const expectedDaiAmount = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) + ); + + // Open user Debt + await pool.connect(user).borrow(dai.address, expectedDaiAmount, 2, 0, userAddress); + + const daiStableVariableTokenAddress = ( + await helpersContract.getReserveTokensAddresses(dai.address) + ).variableDebtTokenAddress; + + const daiVariableDebtContract = await getContract<StableDebtToken>( + eContractid.VariableDebtToken, + daiStableVariableTokenAddress + ); + + const userDaiVariableDebtAmountBefore = await daiVariableDebtContract.balanceOf( + userAddress + ); + + const flashLoanAmount = await convertToCurrencyDecimals(weth.address, '11'); + const liquidityToSwap = await convertToCurrencyDecimals(weth.address, '10.8'); + const amountToRepay = new BigNumber(expectedDaiAmount.toString()) + .multipliedBy(1.1) + .toFixed(0); + + await mockAugustus.expectBuy( + weth.address, + dai.address, + liquidityToSwap, + expectedDaiAmount, + amountToRepay + ); + const mockAugustusCalldata = mockAugustus.interface.encodeFunctionData('buy', [ + weth.address, + dai.address, + flashLoanAmount, + expectedDaiAmount, + ]); + + const flashloanPremium = flashLoanAmount.mul(9).div(10000); + const flashloanTotal = flashLoanAmount.add(flashloanPremium); + await aWETH.connect(user).approve(paraswapRepayAdapter.address, flashloanTotal); + const userAEthBalanceBefore = await aWETH.balanceOf(userAddress); + + const params = buildParaSwapRepayParams( + dai.address, + amountToRepay, + 4 + 3 * 32, + 2, + mockAugustusCalldata, + mockAugustus.address, + 0, + 0, + 0, + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000' + ); + + await pool + .connect(user) + .flashLoan( + paraswapRepayAdapter.address, + [weth.address], + [flashLoanAmount.toString()], + [0], + userAddress, + params, + 0 + ); + + const adapterWethBalance = await weth.balanceOf(paraswapRepayAdapter.address); + const adapterDaiBalance = await dai.balanceOf(paraswapRepayAdapter.address); + const userDaiVariableDebtAmount = await daiVariableDebtContract.balanceOf(userAddress); + const userAEthBalance = await aWETH.balanceOf(userAddress); + const adapterAEthBalance = await aWETH.balanceOf(paraswapRepayAdapter.address); + + expect(adapterAEthBalance).to.be.eq(Zero); + expect(adapterWethBalance).to.be.eq(Zero); + expect(adapterDaiBalance).to.be.eq(Zero); + expect(userDaiVariableDebtAmountBefore).to.be.gte(expectedDaiAmount); + expect(userDaiVariableDebtAmount).to.be.eq(Zero); + expect(userAEthBalance).to.be.lt(userAEthBalanceBefore); + expect(userAEthBalance).to.be.gte(userAEthBalanceBefore.sub(flashloanTotal)); + }); + }); + + describe('swapAndRepay', () => { + it('should correctly swap tokens and repay debt', async () => { + const { users, pool, weth, aWETH, oracle, dai, paraswapRepayAdapter, helpersContract } = + testEnv; + const user = users[0].signer; + const userAddress = users[0].address; + + const amountWETHtoSwap = await convertToCurrencyDecimals(weth.address, '10'); + + const daiPrice = await oracle.getAssetPrice(dai.address); + const expectedDaiAmount = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) + ); + + // Open user Debt + await pool.connect(user).borrow(dai.address, expectedDaiAmount, 1, 0, userAddress); + + const daiStableDebtTokenAddress = ( + await helpersContract.getReserveTokensAddresses(dai.address) + ).stableDebtTokenAddress; + + const daiStableDebtContract = await getContract<StableDebtToken>( + eContractid.StableDebtToken, + daiStableDebtTokenAddress + ); + + const userDaiStableDebtAmountBefore = await daiStableDebtContract.balanceOf(userAddress); + + const liquidityToSwap = amountWETHtoSwap; + const userAEthBalanceBefore = await aWETH.balanceOf(userAddress); + + await mockAugustus.expectBuy( + weth.address, + dai.address, + liquidityToSwap, + expectedDaiAmount, + expectedDaiAmount + ); + const mockAugustusCalldata = mockAugustus.interface.encodeFunctionData('buy', [ + weth.address, + dai.address, + liquidityToSwap, + expectedDaiAmount, + ]); + + await aWETH.connect(user).approve(paraswapRepayAdapter.address, liquidityToSwap); + const params = buildParaswapBuyParams(mockAugustusCalldata, mockAugustus.address); + await paraswapRepayAdapter + .connect(user) + .swapAndRepay( + weth.address, + dai.address, + liquidityToSwap, + expectedDaiAmount, + 1, + 0, + params, + { + amount: 0, + deadline: 0, + v: 0, + r: '0x0000000000000000000000000000000000000000000000000000000000000000', + s: '0x0000000000000000000000000000000000000000000000000000000000000000', + } + ); + + const adapterWethBalance = await weth.balanceOf(paraswapRepayAdapter.address); + const adapterDaiBalance = await dai.balanceOf(paraswapRepayAdapter.address); + const userDaiStableDebtAmount = await daiStableDebtContract.balanceOf(userAddress); + const userAEthBalance = await aWETH.balanceOf(userAddress); + + expect(adapterWethBalance).to.be.eq(Zero); + expect(adapterDaiBalance).to.be.eq(Zero); + expect(userDaiStableDebtAmountBefore).to.be.gte(expectedDaiAmount); + expect(userDaiStableDebtAmount).to.be.lt(expectedDaiAmount); + expect(userAEthBalance).to.be.lt(userAEthBalanceBefore); + expect(userAEthBalance).to.be.gte(userAEthBalanceBefore.sub(liquidityToSwap)); + }); + + it('should correctly swap tokens and repay debt with permit', async () => { + const { users, pool, weth, aWETH, oracle, dai, paraswapRepayAdapter, helpersContract } = + testEnv; + const user = users[0].signer; + const userAddress = users[0].address; + + const amountWETHtoSwap = await convertToCurrencyDecimals(weth.address, '10'); + + const daiPrice = await oracle.getAssetPrice(dai.address); + const expectedDaiAmount = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) + ); + + // Open user Debt + await pool.connect(user).borrow(dai.address, expectedDaiAmount, 1, 0, userAddress); + + const daiStableDebtTokenAddress = ( + await helpersContract.getReserveTokensAddresses(dai.address) + ).stableDebtTokenAddress; + + const daiStableDebtContract = await getContract<StableDebtToken>( + eContractid.StableDebtToken, + daiStableDebtTokenAddress + ); + + const userDaiStableDebtAmountBefore = await daiStableDebtContract.balanceOf(userAddress); + + const liquidityToSwap = amountWETHtoSwap; + const userAEthBalanceBefore = await aWETH.balanceOf(userAddress); + + await mockAugustus.expectBuy( + weth.address, + dai.address, + liquidityToSwap, + expectedDaiAmount, + expectedDaiAmount + ); + const mockAugustusCalldata = mockAugustus.interface.encodeFunctionData('buy', [ + weth.address, + dai.address, + liquidityToSwap, + expectedDaiAmount, + ]); + + const params = buildParaswapBuyParams(mockAugustusCalldata, mockAugustus.address); + + const chainId = DRE.network.config.chainId || BUIDLEREVM_CHAINID; + const deadline = MAX_UINT_AMOUNT; + const nonce = (await aWETH._nonces(userAddress)).toNumber(); + const msgParams = buildPermitParams( + chainId, + aWETH.address, + '1', + await aWETH.name(), + userAddress, + paraswapRepayAdapter.address, + nonce, + deadline, + liquidityToSwap.toString() + ); + + const ownerPrivateKey = require('../../test-wallets.js').accounts[1].secretKey; + if (!ownerPrivateKey) { + throw new Error('INVALID_OWNER_PK'); + } + + const { v, r, s } = getSignatureFromTypedData(ownerPrivateKey, msgParams); + + await paraswapRepayAdapter + .connect(user) + .swapAndRepay( + weth.address, + dai.address, + liquidityToSwap, + expectedDaiAmount, + 1, + 0, + params, + { + amount: liquidityToSwap, + deadline, + v, + r, + s, + } + ); + + const adapterWethBalance = await weth.balanceOf(paraswapRepayAdapter.address); + const adapterDaiBalance = await dai.balanceOf(paraswapRepayAdapter.address); + const userDaiStableDebtAmount = await daiStableDebtContract.balanceOf(userAddress); + const userAEthBalance = await aWETH.balanceOf(userAddress); + + expect(adapterWethBalance).to.be.eq(Zero); + expect(adapterDaiBalance).to.be.eq(Zero); + expect(userDaiStableDebtAmountBefore).to.be.gte(expectedDaiAmount); + expect(userDaiStableDebtAmount).to.be.lt(expectedDaiAmount); + expect(userAEthBalance).to.be.lt(userAEthBalanceBefore); + expect(userAEthBalance).to.be.gte(userAEthBalanceBefore.sub(liquidityToSwap)); + }); + + it('should revert if there is not debt to repay', async () => { + const { users, weth, aWETH, oracle, dai, paraswapRepayAdapter } = testEnv; + const user = users[0].signer; + + const amountWETHtoSwap = await convertToCurrencyDecimals(weth.address, '10'); + + const daiPrice = await oracle.getAssetPrice(dai.address); + const expectedDaiAmount = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) + ); + + const liquidityToSwap = amountWETHtoSwap; + + await mockAugustus.expectBuy( + weth.address, + dai.address, + liquidityToSwap, + expectedDaiAmount, + expectedDaiAmount + ); + const mockAugustusCalldata = mockAugustus.interface.encodeFunctionData('buy', [ + weth.address, + dai.address, + liquidityToSwap, + expectedDaiAmount, + ]); + + const params = buildParaswapBuyParams(mockAugustusCalldata, mockAugustus.address); + await aWETH.connect(user).approve(paraswapRepayAdapter.address, liquidityToSwap); + + await expect( + paraswapRepayAdapter + .connect(user) + .swapAndRepay( + weth.address, + dai.address, + liquidityToSwap, + expectedDaiAmount, + 1, + 0, + params, + { + amount: 0, + deadline: 0, + v: 0, + r: '0x0000000000000000000000000000000000000000000000000000000000000000', + s: '0x0000000000000000000000000000000000000000000000000000000000000000', + } + ) + ).to.be.reverted; + }); + + it('should revert when max amount allowed to swap is bigger than max slippage', async () => { + const { users, pool, weth, aWETH, oracle, dai, paraswapRepayAdapter } = testEnv; + const user = users[0].signer; + const userAddress = users[0].address; + + const amountWETHtoSwap = await convertToCurrencyDecimals(weth.address, '10'); + + const daiPrice = await oracle.getAssetPrice(dai.address); + const expectedDaiAmount = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) + ); + + // Open user Debt + await pool.connect(user).borrow(dai.address, expectedDaiAmount, 1, 0, userAddress); + + const bigMaxAmountToSwap = amountWETHtoSwap.mul(2); + await aWETH.connect(user).approve(paraswapRepayAdapter.address, bigMaxAmountToSwap); + + await mockAugustus.expectBuy( + weth.address, + dai.address, + bigMaxAmountToSwap, + expectedDaiAmount, + expectedDaiAmount + ); + const mockAugustusCalldata = mockAugustus.interface.encodeFunctionData('buy', [ + weth.address, + dai.address, + bigMaxAmountToSwap, + expectedDaiAmount, + ]); + + const params = buildParaswapBuyParams(mockAugustusCalldata, mockAugustus.address); + await expect( + paraswapRepayAdapter + .connect(user) + .swapAndRepay( + weth.address, + dai.address, + bigMaxAmountToSwap, + expectedDaiAmount, + 1, + 0, + params, + { + amount: 0, + deadline: 0, + v: 0, + r: '0x0000000000000000000000000000000000000000000000000000000000000000', + s: '0x0000000000000000000000000000000000000000000000000000000000000000', + } + ) + ).to.be.revertedWith('maxAmountToSwap exceed max slippage'); + }); + + it('should swap, repay debt and pull the needed ATokens leaving no leftovers', async () => { + const { users, pool, weth, aWETH, oracle, dai, paraswapRepayAdapter, helpersContract } = + testEnv; + const user = users[0].signer; + const userAddress = users[0].address; + + const amountWETHtoSwap = await convertToCurrencyDecimals(weth.address, '10'); + + const daiPrice = await oracle.getAssetPrice(dai.address); + const expectedDaiAmount = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) + ); + + // Open user Debt + await pool.connect(user).borrow(dai.address, expectedDaiAmount, 1, 0, userAddress); + + const daiStableDebtTokenAddress = ( + await helpersContract.getReserveTokensAddresses(dai.address) + ).stableDebtTokenAddress; + + const daiStableDebtContract = await getContract<StableDebtToken>( + eContractid.StableDebtToken, + daiStableDebtTokenAddress + ); + + const userDaiStableDebtAmountBefore = await daiStableDebtContract.balanceOf(userAddress); + + const liquidityToSwap = amountWETHtoSwap; + + await mockAugustus.expectBuy( + weth.address, + dai.address, + liquidityToSwap, + expectedDaiAmount, + expectedDaiAmount + ); + const mockAugustusCalldata = mockAugustus.interface.encodeFunctionData('buy', [ + weth.address, + dai.address, + liquidityToSwap, + expectedDaiAmount, + ]); + + const params = buildParaswapBuyParams(mockAugustusCalldata, mockAugustus.address); + await aWETH.connect(user).approve(paraswapRepayAdapter.address, liquidityToSwap); + const userAEthBalanceBefore = await aWETH.balanceOf(userAddress); + const userWethBalanceBefore = await weth.balanceOf(userAddress); + + await paraswapRepayAdapter + .connect(user) + .swapAndRepay( + weth.address, + dai.address, + liquidityToSwap, + expectedDaiAmount, + 1, + 0, + params, + { + amount: 0, + deadline: 0, + v: 0, + r: '0x0000000000000000000000000000000000000000000000000000000000000000', + s: '0x0000000000000000000000000000000000000000000000000000000000000000', + } + ); + + const adapterWethBalance = await weth.balanceOf(paraswapRepayAdapter.address); + const adapterDaiBalance = await dai.balanceOf(paraswapRepayAdapter.address); + const userDaiStableDebtAmount = await daiStableDebtContract.balanceOf(userAddress); + const userAEthBalance = await aWETH.balanceOf(userAddress); + const adapterAEthBalance = await aWETH.balanceOf(paraswapRepayAdapter.address); + const userWethBalance = await weth.balanceOf(userAddress); + + expect(adapterAEthBalance).to.be.eq(Zero); + expect(adapterWethBalance).to.be.eq(Zero); + expect(adapterDaiBalance).to.be.eq(Zero); + expect(userDaiStableDebtAmountBefore).to.be.gte(expectedDaiAmount); + expect(userDaiStableDebtAmount).to.be.lt(expectedDaiAmount); + expect(userAEthBalance).to.be.lt(userAEthBalanceBefore); + expect(userAEthBalance).to.be.eq(userAEthBalanceBefore.sub(liquidityToSwap)); + expect(userWethBalance).to.be.eq(userWethBalanceBefore); + }); + + it('should swap (not whole amount), repay debt and pull the needed ATokens leaving no leftovers', async () => { + const { users, pool, weth, aWETH, oracle, dai, paraswapRepayAdapter, helpersContract } = + testEnv; + const user = users[0].signer; + const userAddress = users[0].address; + + const amountWETHtoSwap = await convertToCurrencyDecimals(weth.address, '10'); + + const daiPrice = await oracle.getAssetPrice(dai.address); + const expectedDaiAmount = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) + ); + + // Open user Debt + await pool.connect(user).borrow(dai.address, expectedDaiAmount, 1, 0, userAddress); + + const daiStableDebtTokenAddress = ( + await helpersContract.getReserveTokensAddresses(dai.address) + ).stableDebtTokenAddress; + + const daiStableDebtContract = await getContract<StableDebtToken>( + eContractid.StableDebtToken, + daiStableDebtTokenAddress + ); + + const userDaiStableDebtAmountBefore = await daiStableDebtContract.balanceOf(userAddress); + + const liquidityToSwap = amountWETHtoSwap; + const swappedAmount = await convertToCurrencyDecimals(weth.address, '9.9'); + + await mockAugustus.expectBuy( + weth.address, + dai.address, + swappedAmount, + expectedDaiAmount, + expectedDaiAmount + ); + const mockAugustusCalldata = mockAugustus.interface.encodeFunctionData('buy', [ + weth.address, + dai.address, + liquidityToSwap, + expectedDaiAmount, + ]); + + const params = buildParaswapBuyParams(mockAugustusCalldata, mockAugustus.address); + await aWETH.connect(user).approve(paraswapRepayAdapter.address, liquidityToSwap); + const userAEthBalanceBefore = await aWETH.balanceOf(userAddress); + const userWethBalanceBefore = await weth.balanceOf(userAddress); + + await paraswapRepayAdapter + .connect(user) + .swapAndRepay( + weth.address, + dai.address, + liquidityToSwap, + expectedDaiAmount, + 1, + 0, + params, + { + amount: 0, + deadline: 0, + v: 0, + r: '0x0000000000000000000000000000000000000000000000000000000000000000', + s: '0x0000000000000000000000000000000000000000000000000000000000000000', + } + ); + + const adapterWethBalance = await weth.balanceOf(paraswapRepayAdapter.address); + const adapterDaiBalance = await dai.balanceOf(paraswapRepayAdapter.address); + const userDaiStableDebtAmount = await daiStableDebtContract.balanceOf(userAddress); + const userAEthBalance = await aWETH.balanceOf(userAddress); + const adapterAEthBalance = await aWETH.balanceOf(paraswapRepayAdapter.address); + const userWethBalance = await weth.balanceOf(userAddress); + + expect(adapterAEthBalance).to.be.eq(Zero); + expect(adapterWethBalance).to.be.eq(Zero); + expect(adapterDaiBalance).to.be.eq(Zero); + expect(userDaiStableDebtAmountBefore).to.be.gte(expectedDaiAmount); + expect(userDaiStableDebtAmount).to.be.lt(expectedDaiAmount); + expect(userAEthBalance).to.be.lt(userAEthBalanceBefore); + expect(userAEthBalance).to.be.eq(userAEthBalanceBefore.sub(swappedAmount)); + expect(userWethBalance).to.be.eq(userWethBalanceBefore); + }); + + it('should correctly swap tokens and repay the whole stable debt', async () => { + const { users, pool, weth, aWETH, oracle, dai, paraswapRepayAdapter, helpersContract } = + testEnv; + const user = users[0].signer; + const userAddress = users[0].address; + + const amountWETHtoSwap = await convertToCurrencyDecimals(weth.address, '10'); + + const daiPrice = await oracle.getAssetPrice(dai.address); + const expectedDaiAmount = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) + ); + + // Open user Debt + await pool.connect(user).borrow(dai.address, expectedDaiAmount, 1, 0, userAddress); + + const daiStableDebtTokenAddress = ( + await helpersContract.getReserveTokensAddresses(dai.address) + ).stableDebtTokenAddress; + + const daiStableDebtContract = await getContract<StableDebtToken>( + eContractid.StableDebtToken, + daiStableDebtTokenAddress + ); + + const userDaiStableDebtAmountBefore = await daiStableDebtContract.balanceOf(userAddress); + + // Add a % to repay on top of the debt + const liquidityToSwap = new BigNumber(amountWETHtoSwap.toString()) + .multipliedBy(1.1) + .toFixed(0); + + // Add a % to repay on top of the debt + const amountToRepay = new BigNumber(expectedDaiAmount.toString()) + .multipliedBy(1.1) + .toFixed(0); + + await mockAugustus.expectBuy( + weth.address, + dai.address, + liquidityToSwap, + expectedDaiAmount, + amountToRepay + ); + const mockAugustusCalldata = mockAugustus.interface.encodeFunctionData('buy', [ + weth.address, + dai.address, + liquidityToSwap, + expectedDaiAmount, + ]); + + const params = buildParaswapBuyParams(mockAugustusCalldata, mockAugustus.address); + await aWETH.connect(user).approve(paraswapRepayAdapter.address, liquidityToSwap); + const userAEthBalanceBefore = await aWETH.balanceOf(userAddress); + + await paraswapRepayAdapter + .connect(user) + .swapAndRepay( + weth.address, + dai.address, + liquidityToSwap, + amountToRepay, + 1, + 4 + 3 * 32, + params, + { + amount: 0, + deadline: 0, + v: 0, + r: '0x0000000000000000000000000000000000000000000000000000000000000000', + s: '0x0000000000000000000000000000000000000000000000000000000000000000', + } + ); + + const adapterWethBalance = await weth.balanceOf(paraswapRepayAdapter.address); + const adapterDaiBalance = await dai.balanceOf(paraswapRepayAdapter.address); + const userDaiStableDebtAmount = await daiStableDebtContract.balanceOf(userAddress); + const userAEthBalance = await aWETH.balanceOf(userAddress); + const adapterAEthBalance = await aWETH.balanceOf(paraswapRepayAdapter.address); + + expect(adapterAEthBalance).to.be.eq(Zero); + expect(adapterWethBalance).to.be.eq(Zero); + expect(adapterDaiBalance).to.be.eq(Zero); + expect(userDaiStableDebtAmountBefore).to.be.gte(expectedDaiAmount); + expect(userDaiStableDebtAmount).to.be.eq(Zero); + expect(userAEthBalance).to.be.lt(userAEthBalanceBefore); + expect(userAEthBalance).to.be.gte(userAEthBalanceBefore.sub(liquidityToSwap)); + }); + + it('should correctly swap tokens and repay the whole variable debt', async () => { + const { users, pool, weth, aWETH, oracle, dai, paraswapRepayAdapter, helpersContract } = + testEnv; + const user = users[0].signer; + const userAddress = users[0].address; + + const amountWETHtoSwap = await convertToCurrencyDecimals(weth.address, '10'); + + const daiPrice = await oracle.getAssetPrice(dai.address); + const expectedDaiAmount = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) + ); + + // Open user Debt + await pool.connect(user).borrow(dai.address, expectedDaiAmount, 2, 0, userAddress); + + const daiStableVariableTokenAddress = ( + await helpersContract.getReserveTokensAddresses(dai.address) + ).variableDebtTokenAddress; + + const daiVariableDebtContract = await getContract<StableDebtToken>( + eContractid.VariableDebtToken, + daiStableVariableTokenAddress + ); + + const userDaiVariableDebtAmountBefore = await daiVariableDebtContract.balanceOf( + userAddress + ); + + // Add a % to repay on top of the debt + const liquidityToSwap = new BigNumber(amountWETHtoSwap.toString()) + .multipliedBy(1.1) + .toFixed(0); + + // Add a % to repay on top of the debt + const amountToRepay = new BigNumber(expectedDaiAmount.toString()) + .multipliedBy(1.1) + .toFixed(0); + + await mockAugustus.expectBuy( + weth.address, + dai.address, + liquidityToSwap, + expectedDaiAmount, + amountToRepay + ); + const mockAugustusCalldata = mockAugustus.interface.encodeFunctionData('buy', [ + weth.address, + dai.address, + liquidityToSwap, + expectedDaiAmount, + ]); + + const params = buildParaswapBuyParams(mockAugustusCalldata, mockAugustus.address); + await aWETH.connect(user).approve(paraswapRepayAdapter.address, liquidityToSwap); + const userAEthBalanceBefore = await aWETH.balanceOf(userAddress); + + await paraswapRepayAdapter + .connect(user) + .swapAndRepay( + weth.address, + dai.address, + liquidityToSwap, + amountToRepay, + 2, + 4 + 3 * 32, + params, + { + amount: 0, + deadline: 0, + v: 0, + r: '0x0000000000000000000000000000000000000000000000000000000000000000', + s: '0x0000000000000000000000000000000000000000000000000000000000000000', + } + ); + + const adapterWethBalance = await weth.balanceOf(paraswapRepayAdapter.address); + const adapterDaiBalance = await dai.balanceOf(paraswapRepayAdapter.address); + const userDaiVariableDebtAmount = await daiVariableDebtContract.balanceOf(userAddress); + const userAEthBalance = await aWETH.balanceOf(userAddress); + const adapterAEthBalance = await aWETH.balanceOf(paraswapRepayAdapter.address); + + expect(adapterAEthBalance).to.be.eq(Zero); + expect(adapterWethBalance).to.be.eq(Zero); + expect(adapterDaiBalance).to.be.eq(Zero); + expect(userDaiVariableDebtAmountBefore).to.be.gte(expectedDaiAmount); + expect(userDaiVariableDebtAmount).to.be.eq(Zero); + expect(userAEthBalance).to.be.lt(userAEthBalanceBefore); + expect(userAEthBalance).to.be.gte(userAEthBalanceBefore.sub(liquidityToSwap)); + }); + }); + }); +});