diff --git a/contracts/adapters/BaseParaSwapAdapter.sol b/contracts/adapters/BaseParaSwapAdapter.sol new file mode 100644 index 00000000..9b84dd93 --- /dev/null +++ b/contracts/adapters/BaseParaSwapAdapter.sol @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity 0.6.12; +pragma experimental ABIEncoderV2; + +import {SafeMath} from '../dependencies/openzeppelin/contracts/SafeMath.sol'; +import {IERC20} from '../dependencies/openzeppelin/contracts/IERC20.sol'; +import {IERC20Detailed} from '../dependencies/openzeppelin/contracts/IERC20Detailed.sol'; +import {SafeERC20} from '../dependencies/openzeppelin/contracts/SafeERC20.sol'; +import {Ownable} from '../dependencies/openzeppelin/contracts/Ownable.sol'; +import {ILendingPoolAddressesProvider} from '../interfaces/ILendingPoolAddressesProvider.sol'; +import {DataTypes} from '../protocol/libraries/types/DataTypes.sol'; +import {IPriceOracleGetter} from '../interfaces/IPriceOracleGetter.sol'; +import {IERC20WithPermit} from '../interfaces/IERC20WithPermit.sol'; +import {FlashLoanReceiverBase} from '../flashloan/base/FlashLoanReceiverBase.sol'; + +/** + * @title BaseParaSwapAdapter + * @notice Utility functions for adapters using ParaSwap + * @author Jason Raymond Bell + */ +abstract contract BaseParaSwapAdapter is FlashLoanReceiverBase, Ownable { + using SafeMath for uint256; + using SafeERC20 for IERC20; + using SafeERC20 for IERC20Detailed; + using SafeERC20 for IERC20WithPermit; + + struct PermitSignature { + uint256 amount; + uint256 deadline; + uint8 v; + bytes32 r; + bytes32 s; + } + + // Max slippage percent allowed + uint256 public constant MAX_SLIPPAGE_PERCENT = 3000; // 30% + + IPriceOracleGetter public immutable ORACLE; + + event Swapped(address indexed fromAsset, address indexed toAsset, uint256 fromAmount, uint256 receivedAmount); + + constructor( + ILendingPoolAddressesProvider addressesProvider + ) public FlashLoanReceiverBase(addressesProvider) { + ORACLE = IPriceOracleGetter(addressesProvider.getPriceOracle()); + } + + /** + * @dev Get the price of the asset from the oracle denominated in eth + * @param asset address + * @return eth price for the asset + */ + function _getPrice(address asset) internal view returns (uint256) { + return ORACLE.getAssetPrice(asset); + } + + /** + * @dev Get the decimals of an asset + * @return number of decimals of the asset + */ + function _getDecimals(IERC20Detailed asset) internal view returns (uint8) { + uint8 decimals = asset.decimals(); + // Ensure 10**decimals won't overflow a uint256 + require(decimals <= 77, 'TOO_MANY_DECIMALS_ON_TOKEN'); + return decimals; + } + + /** + * @dev Get the aToken associated to the asset + * @return address of the aToken + */ + function _getReserveData(address asset) internal view returns (DataTypes.ReserveData memory) { + return LENDING_POOL.getReserveData(asset); + } + + /** + * @dev Pull the ATokens from the user + * @param reserve address of the asset + * @param reserveAToken address of the aToken of the reserve + * @param user address + * @param amount of tokens to be transferred to the contract + * @param permitSignature struct containing the permit signature + */ + function _pullATokenAndWithdraw( + address reserve, + IERC20WithPermit reserveAToken, + address user, + uint256 amount, + PermitSignature memory permitSignature + ) internal { + // If deadline is set to zero, assume there is no signature for permit + if (permitSignature.deadline != 0) { + reserveAToken.permit( + user, + address(this), + permitSignature.amount, + permitSignature.deadline, + permitSignature.v, + permitSignature.r, + permitSignature.s + ); + } + + // transfer from user to adapter + reserveAToken.safeTransferFrom(user, address(this), amount); + + // withdraw reserve + require( + LENDING_POOL.withdraw(reserve, amount, address(this)) == amount, + 'UNEXPECTED_AMOUNT_WITHDRAWN' + ); + } + + /** + * @dev Emergency rescue for token stucked on this contract, as failsafe mechanism + * - Funds should never remain in this contract more time than during transactions + * - Only callable by the owner + */ + function rescueTokens(IERC20 token) external onlyOwner { + token.safeTransfer(owner(), token.balanceOf(address(this))); + } +} diff --git a/contracts/adapters/BaseParaSwapSellAdapter.sol b/contracts/adapters/BaseParaSwapSellAdapter.sol new file mode 100644 index 00000000..93d3cc5b --- /dev/null +++ b/contracts/adapters/BaseParaSwapSellAdapter.sol @@ -0,0 +1,109 @@ +// 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 BaseParaSwapSellAdapter + * @notice Implements the logic for selling tokens on ParaSwap + * @author Jason Raymond Bell + */ +abstract contract BaseParaSwapSellAdapter 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))); + AUGUSTUS_REGISTRY = augustusRegistry; + } + + /** + * @dev Swaps a token for another using ParaSwap + * @param fromAmountOffset Offset of fromAmount in Augustus calldata if it should be overwritten, otherwise 0 + * @param swapCalldata Calldata for ParaSwap's AugustusSwapper contract + * @param augustus Address of ParaSwap's AugustusSwapper contract + * @param assetToSwapFrom Address of the asset to be swapped from + * @param assetToSwapTo Address of the asset to be swapped to + * @param amountToSwap Amount to be swapped + * @param minAmountToReceive Minimum amount to be received from the swap + * @return amountReceived The amount received from the swap + */ + function _sellOnParaSwap( + uint256 fromAmountOffset, + bytes memory swapCalldata, + IParaSwapAugustus augustus, + IERC20Detailed assetToSwapFrom, + IERC20Detailed assetToSwapTo, + uint256 amountToSwap, + uint256 minAmountToReceive + ) internal returns (uint256 amountReceived) { + 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 expectedMinAmountOut = + amountToSwap + .mul(fromAssetPrice.mul(10**toAssetDecimals)) + .div(toAssetPrice.mul(10**fromAssetDecimals)) + .percentMul(PercentageMath.PERCENTAGE_FACTOR - MAX_SLIPPAGE_PERCENT); + + require(expectedMinAmountOut <= minAmountToReceive, 'MIN_AMOUNT_EXCEEDS_MAX_SLIPPAGE'); + } + + uint256 balanceBeforeAssetFrom = assetToSwapFrom.balanceOf(address(this)); + require(balanceBeforeAssetFrom >= amountToSwap, 'INSUFFICIENT_BALANCE_BEFORE_SWAP'); + uint256 balanceBeforeAssetTo = assetToSwapTo.balanceOf(address(this)); + + address tokenTransferProxy = augustus.getTokenTransferProxy(); + assetToSwapFrom.safeApprove(tokenTransferProxy, 0); + assetToSwapFrom.safeApprove(tokenTransferProxy, amountToSwap); + + if (fromAmountOffset != 0) { + // Ensure 256 bit (32 bytes) fromAmount value is within bounds of the + // calldata, not overlapping with the first 4 bytes (function selector). + require(fromAmountOffset >= 4 && + fromAmountOffset <= swapCalldata.length.sub(32), + 'FROM_AMOUNT_OFFSET_OUT_OF_RANGE'); + // Overwrite the fromAmount with the correct amount for the swap. + // In memory, swapCalldata 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(swapCalldata, add(fromAmountOffset, 32)), amountToSwap) + } + } + (bool success,) = address(augustus).call(swapCalldata); + if (!success) { + // Copy revert reason from call + assembly { + returndatacopy(0, 0, returndatasize()) + revert(0, returndatasize()) + } + } + require(assetToSwapFrom.balanceOf(address(this)) == balanceBeforeAssetFrom - amountToSwap, 'WRONG_BALANCE_AFTER_SWAP'); + amountReceived = assetToSwapTo.balanceOf(address(this)).sub(balanceBeforeAssetTo); + require(amountReceived >= minAmountToReceive, 'INSUFFICIENT_AMOUNT_RECEIVED'); + + emit Swapped( + address(assetToSwapFrom), + address(assetToSwapTo), + amountToSwap, + amountReceived + ); + } +} diff --git a/contracts/adapters/ParaSwapLiquiditySwapAdapter.sol b/contracts/adapters/ParaSwapLiquiditySwapAdapter.sol new file mode 100644 index 00000000..7cc1105d --- /dev/null +++ b/contracts/adapters/ParaSwapLiquiditySwapAdapter.sol @@ -0,0 +1,210 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity 0.6.12; +pragma experimental ABIEncoderV2; + +import {BaseParaSwapSellAdapter} from './BaseParaSwapSellAdapter.sol'; +import {ILendingPoolAddressesProvider} from '../interfaces/ILendingPoolAddressesProvider.sol'; +import {IParaSwapAugustusRegistry} from '../interfaces/IParaSwapAugustusRegistry.sol'; +import {IERC20Detailed} from '../dependencies/openzeppelin/contracts/IERC20Detailed.sol'; +import {IERC20WithPermit} from '../interfaces/IERC20WithPermit.sol'; +import {IParaSwapAugustus} from '../interfaces/IParaSwapAugustus.sol'; +import {ReentrancyGuard} from '../dependencies/openzeppelin/contracts/ReentrancyGuard.sol'; + +/** + * @title ParaSwapLiquiditySwapAdapter + * @notice Adapter to swap liquidity using ParaSwap. + * @author Jason Raymond Bell + */ +contract ParaSwapLiquiditySwapAdapter is BaseParaSwapSellAdapter, ReentrancyGuard { + constructor( + ILendingPoolAddressesProvider addressesProvider, + IParaSwapAugustusRegistry augustusRegistry + ) public BaseParaSwapSellAdapter(addressesProvider, augustusRegistry) { + // This is only required to initialize BaseParaSwapSellAdapter + } + + /** + * @dev Swaps the received reserve amount from the flash loan into the asset specified in the params. + * The received funds from the swap are then deposited into the protocol on behalf of the user. + * The user should give this contract allowance to pull the ATokens in order to withdraw the underlying asset and repay the flash loan. + * @param assets Address of the underlying asset to be swapped from + * @param amounts Amount of the flash loan i.e. maximum amount to swap + * @param premiums Fee of the flash loan + * @param initiator Account that initiated the flash loan + * @param params Additional variadic field to include extra params. Expected parameters: + * address assetToSwapTo Address of the underlying asset to be swapped to and deposited + * uint256 minAmountToReceive Min amount to be received from the swap + * uint256 swapAllBalanceOffset Set to offset of fromAmount in Augustus calldata if wanting to swap all balance, otherwise 0 + * bytes swapCalldata Calldata for ParaSwap's AugustusSwapper contract + * address augustus Address of ParaSwap's AugustusSwapper contract + * 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 flashLoanAmount = amounts[0]; + uint256 premium = premiums[0]; + address initiatorLocal = initiator; + IERC20Detailed assetToSwapFrom = IERC20Detailed(assets[0]); + ( + IERC20Detailed assetToSwapTo, + uint256 minAmountToReceive, + uint256 swapAllBalanceOffset, + bytes memory swapCalldata, + IParaSwapAugustus augustus, + PermitSignature memory permitParams + ) = abi.decode(params, ( + IERC20Detailed, + uint256, + uint256, + bytes, + IParaSwapAugustus, + PermitSignature + )); + + _swapLiquidity( + swapAllBalanceOffset, + swapCalldata, + augustus, + permitParams, + flashLoanAmount, + premium, + initiatorLocal, + assetToSwapFrom, + assetToSwapTo, + minAmountToReceive + ); + + return true; + } + + /** + * @dev Swaps an amount of an asset to another and deposits the new asset amount on behalf of the user without using a flash loan. + * 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 and perform the swap. + * @param assetToSwapFrom Address of the underlying asset to be swapped from + * @param assetToSwapTo Address of the underlying asset to be swapped to and deposited + * @param amountToSwap Amount to be swapped, or maximum amount when swapping all balance + * @param minAmountToReceive Minimum amount to be received from the swap + * @param swapAllBalanceOffset Set to offset of fromAmount in Augustus calldata if wanting to swap all balance, otherwise 0 + * @param swapCalldata Calldata for ParaSwap's AugustusSwapper contract + * @param augustus Address of ParaSwap's AugustusSwapper contract + * @param permitParams Struct containing the permit signatures, set to all zeroes if not used + */ + function swapAndDeposit( + IERC20Detailed assetToSwapFrom, + IERC20Detailed assetToSwapTo, + uint256 amountToSwap, + uint256 minAmountToReceive, + uint256 swapAllBalanceOffset, + bytes calldata swapCalldata, + IParaSwapAugustus augustus, + PermitSignature calldata permitParams + ) external nonReentrant { + IERC20WithPermit aToken = + IERC20WithPermit(_getReserveData(address(assetToSwapFrom)).aTokenAddress); + + if (swapAllBalanceOffset != 0) { + uint256 balance = aToken.balanceOf(msg.sender); + require(balance <= amountToSwap, 'INSUFFICIENT_AMOUNT_TO_SWAP'); + amountToSwap = balance; + } + + _pullATokenAndWithdraw( + address(assetToSwapFrom), + aToken, + msg.sender, + amountToSwap, + permitParams + ); + + uint256 amountReceived = _sellOnParaSwap( + swapAllBalanceOffset, + swapCalldata, + augustus, + assetToSwapFrom, + assetToSwapTo, + amountToSwap, + minAmountToReceive + ); + + assetToSwapTo.safeApprove(address(LENDING_POOL), 0); + assetToSwapTo.safeApprove(address(LENDING_POOL), amountReceived); + LENDING_POOL.deposit(address(assetToSwapTo), amountReceived, msg.sender, 0); + } + + /** + * @dev Swaps an amount of an asset to another and deposits the funds on behalf of the initiator. + * @param swapAllBalanceOffset Set to offset of fromAmount in Augustus calldata if wanting to swap all balance, otherwise 0 + * @param swapCalldata Calldata for ParaSwap's AugustusSwapper contract + * @param augustus Address of ParaSwap's AugustusSwapper contract + * @param permitParams Struct containing the permit signatures, set to all zeroes if not used + * @param flashLoanAmount Amount of the flash loan i.e. maximum amount to swap + * @param premium Fee of the flash loan + * @param initiator Account that initiated the flash loan + * @param assetToSwapFrom Address of the underyling asset to be swapped from + * @param assetToSwapTo Address of the underlying asset to be swapped to and deposited + * @param minAmountToReceive Min amount to be received from the swap + */ + function _swapLiquidity ( + uint256 swapAllBalanceOffset, + bytes memory swapCalldata, + IParaSwapAugustus augustus, + PermitSignature memory permitParams, + uint256 flashLoanAmount, + uint256 premium, + address initiator, + IERC20Detailed assetToSwapFrom, + IERC20Detailed assetToSwapTo, + uint256 minAmountToReceive + ) internal { + IERC20WithPermit aToken = + IERC20WithPermit(_getReserveData(address(assetToSwapFrom)).aTokenAddress); + uint256 amountToSwap = flashLoanAmount; + + uint256 balance = aToken.balanceOf(initiator); + if (swapAllBalanceOffset != 0) { + uint256 balanceToSwap = balance.sub(premium); + require(balanceToSwap <= amountToSwap, 'INSUFFICIENT_AMOUNT_TO_SWAP'); + amountToSwap = balanceToSwap; + } else { + require(balance >= amountToSwap.add(premium), 'INSUFFICIENT_ATOKEN_BALANCE'); + } + + uint256 amountReceived = _sellOnParaSwap( + swapAllBalanceOffset, + swapCalldata, + augustus, + assetToSwapFrom, + assetToSwapTo, + amountToSwap, + minAmountToReceive + ); + + assetToSwapTo.safeApprove(address(LENDING_POOL), 0); + assetToSwapTo.safeApprove(address(LENDING_POOL), amountReceived); + LENDING_POOL.deposit(address(assetToSwapTo), amountReceived, initiator, 0); + + _pullATokenAndWithdraw( + address(assetToSwapFrom), + aToken, + initiator, + amountToSwap.add(premium), + permitParams + ); + + // Repay flash loan + assetToSwapFrom.safeApprove(address(LENDING_POOL), 0); + assetToSwapFrom.safeApprove(address(LENDING_POOL), flashLoanAmount.add(premium)); + } +} diff --git a/contracts/dependencies/openzeppelin/contracts/ReentrancyGuard.sol b/contracts/dependencies/openzeppelin/contracts/ReentrancyGuard.sol new file mode 100644 index 00000000..24c90c32 --- /dev/null +++ b/contracts/dependencies/openzeppelin/contracts/ReentrancyGuard.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.6.0 <0.8.0; + +/** + * @dev Contract module that helps prevent reentrant calls to a function. + * + * Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier + * available, which can be applied to functions to make sure there are no nested + * (reentrant) calls to them. + * + * Note that because there is a single `nonReentrant` guard, functions marked as + * `nonReentrant` may not call one another. This can be worked around by making + * those functions `private`, and then adding `external` `nonReentrant` entry + * points to them. + * + * TIP: If you would like to learn more about reentrancy and alternative ways + * to protect against it, check out our blog post + * https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul]. + */ +abstract contract ReentrancyGuard { + // Booleans are more expensive than uint256 or any type that takes up a full + // word because each write operation emits an extra SLOAD to first read the + // slot's contents, replace the bits taken up by the boolean, and then write + // back. This is the compiler's defense against contract upgrades and + // pointer aliasing, and it cannot be disabled. + + // The values being non-zero value makes deployment a bit more expensive, + // but in exchange the refund on every call to nonReentrant will be lower in + // amount. Since refunds are capped to a percentage of the total + // transaction's gas, it is best to keep them low in cases like this one, to + // increase the likelihood of the full refund coming into effect. + uint256 private constant _NOT_ENTERED = 1; + uint256 private constant _ENTERED = 2; + + uint256 private _status; + + constructor () internal { + _status = _NOT_ENTERED; + } + + /** + * @dev Prevents a contract from calling itself, directly or indirectly. + * Calling a `nonReentrant` function from another `nonReentrant` + * function is not supported. It is possible to prevent this from happening + * by making the `nonReentrant` function external, and make it call a + * `private` function that does the actual work. + */ + modifier nonReentrant() { + // On the first call to nonReentrant, _notEntered will be true + require(_status != _ENTERED, "ReentrancyGuard: reentrant call"); + + // Any calls to nonReentrant after this point will fail + _status = _ENTERED; + + _; + + // By storing the original value once again, a refund is triggered (see + // https://eips.ethereum.org/EIPS/eip-2200) + _status = _NOT_ENTERED; + } +} diff --git a/contracts/interfaces/IParaSwapAugustus.sol b/contracts/interfaces/IParaSwapAugustus.sol new file mode 100644 index 00000000..bd0714d0 --- /dev/null +++ b/contracts/interfaces/IParaSwapAugustus.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity 0.6.12; +pragma experimental ABIEncoderV2; + +interface IParaSwapAugustus { + function getTokenTransferProxy() external view returns (address); +} diff --git a/contracts/interfaces/IParaSwapAugustusRegistry.sol b/contracts/interfaces/IParaSwapAugustusRegistry.sol new file mode 100644 index 00000000..f10e13dd --- /dev/null +++ b/contracts/interfaces/IParaSwapAugustusRegistry.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity 0.6.12; +pragma experimental ABIEncoderV2; + +interface IParaSwapAugustusRegistry { + function isValidAugustus(address augustus) external view returns (bool); +} diff --git a/contracts/mocks/swap/MockParaSwapAugustus.sol b/contracts/mocks/swap/MockParaSwapAugustus.sol new file mode 100644 index 00000000..1cf32171 --- /dev/null +++ b/contracts/mocks/swap/MockParaSwapAugustus.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity 0.6.12; +pragma experimental ABIEncoderV2; + +import {IParaSwapAugustus} from '../../interfaces/IParaSwapAugustus.sol'; +import {MockParaSwapTokenTransferProxy} from './MockParaSwapTokenTransferProxy.sol'; +import {IERC20} from '../../dependencies/openzeppelin/contracts/IERC20.sol'; +import {MintableERC20} from '../tokens/MintableERC20.sol'; + +contract MockParaSwapAugustus is IParaSwapAugustus { + MockParaSwapTokenTransferProxy immutable TOKEN_TRANSFER_PROXY; + bool _expectingSwap; + address _expectedFromToken; + address _expectedToToken; + uint256 _expectedFromAmountMin; + uint256 _expectedFromAmountMax; + uint256 _receivedAmount; + + constructor() public { + TOKEN_TRANSFER_PROXY = new MockParaSwapTokenTransferProxy(); + } + + function getTokenTransferProxy() external view override returns (address) { + return address(TOKEN_TRANSFER_PROXY); + } + + function expectSwap( + address fromToken, + address toToken, + uint256 fromAmountMin, + uint256 fromAmountMax, + uint256 receivedAmount + ) external { + _expectingSwap = true; + _expectedFromToken = fromToken; + _expectedToToken = toToken; + _expectedFromAmountMin = fromAmountMin; + _expectedFromAmountMax = fromAmountMax; + _receivedAmount = receivedAmount; + } + + function swap( + 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(fromAmount >= _expectedFromAmountMin && fromAmount <= _expectedFromAmountMax, 'From amount out of range'); + require(_receivedAmount >= toAmount, 'Received amount of tokens are less than expected'); + TOKEN_TRANSFER_PROXY.transferFrom(fromToken, msg.sender, address(this), fromAmount); + MintableERC20(toToken).mint(_receivedAmount); + IERC20(toToken).transfer(msg.sender, _receivedAmount); + _expectingSwap = false; + return _receivedAmount; + } +} diff --git a/contracts/mocks/swap/MockParaSwapAugustusRegistry.sol b/contracts/mocks/swap/MockParaSwapAugustusRegistry.sol new file mode 100644 index 00000000..c8998ec1 --- /dev/null +++ b/contracts/mocks/swap/MockParaSwapAugustusRegistry.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity 0.6.12; +pragma experimental ABIEncoderV2; + +import {IParaSwapAugustusRegistry} from '../../interfaces/IParaSwapAugustusRegistry.sol'; + +contract MockParaSwapAugustusRegistry is IParaSwapAugustusRegistry { + address immutable AUGUSTUS; + + constructor(address augustus) public { + AUGUSTUS = augustus; + } + + function isValidAugustus(address augustus) external view override returns (bool) { + return augustus == AUGUSTUS; + } +} diff --git a/contracts/mocks/swap/MockParaSwapTokenTransferProxy.sol b/contracts/mocks/swap/MockParaSwapTokenTransferProxy.sol new file mode 100644 index 00000000..a405cec3 --- /dev/null +++ b/contracts/mocks/swap/MockParaSwapTokenTransferProxy.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity 0.6.12; +pragma experimental ABIEncoderV2; + +import {Ownable} from '../../dependencies/openzeppelin/contracts/Ownable.sol'; +import {IERC20} from '../../dependencies/openzeppelin/contracts/IERC20.sol'; + +contract MockParaSwapTokenTransferProxy is Ownable { + function transferFrom( + address token, + address from, + address to, + uint256 amount + ) external onlyOwner { + IERC20(token).transferFrom(from, to, amount); + } +} diff --git a/helpers/contracts-deployments.ts b/helpers/contracts-deployments.ts index 700f1a43..13c64a78 100644 --- a/helpers/contracts-deployments.ts +++ b/helpers/contracts-deployments.ts @@ -34,9 +34,12 @@ import { MockAggregatorFactory, MockATokenFactory, MockFlashLoanReceiverFactory, + MockParaSwapAugustusFactory, + MockParaSwapAugustusRegistryFactory, MockStableDebtTokenFactory, MockVariableDebtTokenFactory, MockUniswapV2Router02Factory, + ParaSwapLiquiditySwapAdapterFactory, PriceOracleFactory, ReserveLogicFactory, SelfdestructTransferFactory, @@ -708,3 +711,32 @@ export const deployRateStrategy = async ( ).address; } }; +export const deployMockParaSwapAugustus = async (verify?: boolean) => + withSaveAndVerify( + await new MockParaSwapAugustusFactory(await getFirstSigner()).deploy(), + eContractid.MockParaSwapAugustus, + [], + verify + ); + +export const deployMockParaSwapAugustusRegistry = async ( + args: [tEthereumAddress], + verify?: boolean +) => + withSaveAndVerify( + await new MockParaSwapAugustusRegistryFactory(await getFirstSigner()).deploy(...args), + eContractid.MockParaSwapAugustusRegistry, + args, + verify + ); + +export const deployParaSwapLiquiditySwapAdapter = async ( + args: [tEthereumAddress, tEthereumAddress], + verify?: boolean +) => + withSaveAndVerify( + await new ParaSwapLiquiditySwapAdapterFactory(await getFirstSigner()).deploy(...args), + eContractid.ParaSwapLiquiditySwapAdapter, + args, + verify + ); diff --git a/helpers/contracts-getters.ts b/helpers/contracts-getters.ts index 21641d97..cac9fe70 100644 --- a/helpers/contracts-getters.ts +++ b/helpers/contracts-getters.ts @@ -18,6 +18,9 @@ import { MockStableDebtTokenFactory, MockVariableDebtTokenFactory, MockUniswapV2Router02Factory, + MockParaSwapAugustusFactory, + MockParaSwapAugustusRegistryFactory, + ParaSwapLiquiditySwapAdapterFactory, PriceOracleFactory, ReserveLogicFactory, SelfdestructTransferFactory, @@ -417,3 +420,27 @@ export const getFlashLiquidationAdapter = async (address?: tEthereumAddress) => ).address, await getFirstSigner() ); + +export const getMockParaSwapAugustus = async (address?: tEthereumAddress) => + await MockParaSwapAugustusFactory.connect( + address || + (await getDb().get(`${eContractid.MockParaSwapAugustus}.${DRE.network.name}`).value()) + .address, + await getFirstSigner() + ); + +export const getMockParaSwapAugustusRegistry = async (address?: tEthereumAddress) => + await MockParaSwapAugustusRegistryFactory.connect( + address || + (await getDb().get(`${eContractid.MockParaSwapAugustusRegistry}.${DRE.network.name}`).value()) + .address, + await getFirstSigner() + ); + +export const getParaSwapLiquiditySwapAdapter = async (address?: tEthereumAddress) => + await ParaSwapLiquiditySwapAdapterFactory.connect( + address || + (await getDb().get(`${eContractid.ParaSwapLiquiditySwapAdapter}.${DRE.network.name}`).value()) + .address, + await getFirstSigner() + ); diff --git a/helpers/contracts-helpers.ts b/helpers/contracts-helpers.ts index cd79c9a5..dc786128 100644 --- a/helpers/contracts-helpers.ts +++ b/helpers/contracts-helpers.ts @@ -341,6 +341,38 @@ export const buildFlashLiquidationAdapterParams = ( ); }; +export const buildParaSwapLiquiditySwapParams = ( + assetToSwapTo: tEthereumAddress, + minAmountToReceive: BigNumberish, + swapAllBalanceOffset: BigNumberish, + swapCalldata: string | Buffer, + augustus: tEthereumAddress, + permitAmount: BigNumberish, + deadline: BigNumberish, + v: BigNumberish, + r: string | Buffer, + s: string | Buffer +) => { + return ethers.utils.defaultAbiCoder.encode( + [ + 'address', + 'uint256', + 'uint256', + 'bytes', + 'address', + 'tuple(uint256,uint256,uint8,bytes32,bytes32)', + ], + [ + assetToSwapTo, + minAmountToReceive, + swapAllBalanceOffset, + swapCalldata, + augustus, + [permitAmount, deadline, v, r, s], + ] + ); +}; + export const verifyContract = async ( id: string, instance: Contract, diff --git a/helpers/types.ts b/helpers/types.ts index f0a39994..babd113e 100644 --- a/helpers/types.ts +++ b/helpers/types.ts @@ -95,6 +95,9 @@ export enum eContractid { UniswapLiquiditySwapAdapter = 'UniswapLiquiditySwapAdapter', UniswapRepayAdapter = 'UniswapRepayAdapter', FlashLiquidationAdapter = 'FlashLiquidationAdapter', + MockParaSwapAugustus = 'MockParaSwapAugustus', + MockParaSwapAugustusRegistry = 'MockParaSwapAugustusRegistry', + ParaSwapLiquiditySwapAdapter = 'ParaSwapLiquiditySwapAdapter', } /* diff --git a/package.json b/package.json index f5e98227..dfe0b2d4 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,7 @@ "dev:UniswapLiquiditySwapAdapter": "hardhat --network kovan deploy-UniswapLiquiditySwapAdapter --provider 0x88757f2f99175387aB4C6a4b3067c77A695b0349 --router 0xfcd87315f0e4067070ade8682fcdbc3006631441 --weth 0xd0a1e359811322d97991e03f863a0c30c2cf029c", "main:deployUniswapRepayAdapter": "hardhat --network main deploy-UniswapRepayAdapter --provider 0xB53C1a33016B2DC2fF3653530bfF1848a515c8c5 --router 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D --weth 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", "main:UniswapLiquiditySwapAdapter": "hardhat --network main deploy-UniswapLiquiditySwapAdapter --provider 0xB53C1a33016B2DC2fF3653530bfF1848a515c8c5 --router 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D --weth 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "main:ParaSwapLiquiditySwapAdapter": "hardhat --network main deploy-ParaSwapLiquiditySwapAdapter --provider 0xB53C1a33016B2DC2fF3653530bfF1848a515c8c5 --augustusRegistry 0xa68bEA62Dc4034A689AA0F58A76681433caCa663", "kovan:verify": "npm run hardhat:kovan verify:general -- --all --pool Aave", "ropsten:verify": "npm run hardhat:ropsten verify:general -- --all --pool Aave", "mainnet:verify": "npm run hardhat:main verify:general -- --all --pool Aave", diff --git a/tasks/deployments/deploy-ParaSwapLiquiditySwapAdapter.ts b/tasks/deployments/deploy-ParaSwapLiquiditySwapAdapter.ts new file mode 100644 index 00000000..bc8da64e --- /dev/null +++ b/tasks/deployments/deploy-ParaSwapLiquiditySwapAdapter.ts @@ -0,0 +1,36 @@ +import { task } from 'hardhat/config'; + +import { ParaSwapLiquiditySwapAdapterFactory } from '../../types'; +import { verifyContract } from '../../helpers/contracts-helpers'; +import { getFirstSigner } from '../../helpers/contracts-getters'; +import { eContractid } from '../../helpers/types'; + +const CONTRACT_NAME = 'ParaSwapLiquiditySwapAdapter'; + +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 ParaSwapLiquiditySwapAdapterFactory( + await getFirstSigner() + ).deploy(provider, augustusRegistry); + await adapter.deployTransaction.wait(); + console.log(`${CONTRACT_NAME}.address`, adapter.address); + + if (verify) { + await verifyContract(eContractid.ParaSwapLiquiditySwapAdapter, 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 cc9bee13..7b358667 100644 --- a/test-suites/test-aave/__setup.spec.ts +++ b/test-suites/test-aave/__setup.spec.ts @@ -26,6 +26,9 @@ import { deployUniswapLiquiditySwapAdapter, deployUniswapRepayAdapter, deployFlashLiquidationAdapter, + deployMockParaSwapAugustus, + deployMockParaSwapAugustusRegistry, + deployParaSwapLiquiditySwapAdapter, authorizeWETHGateway, deployATokenImplementations, deployAaveOracle, @@ -293,6 +296,12 @@ const buildTestEnv = async (deployer: Signer, secondaryWallet: Signer) => { await deployUniswapRepayAdapter(adapterParams); await deployFlashLiquidationAdapter(adapterParams); + const augustus = await deployMockParaSwapAugustus(); + + const augustusRegistry = await deployMockParaSwapAugustusRegistry([augustus.address]); + + await deployParaSwapLiquiditySwapAdapter([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 e5421031..c4eb7893 100644 --- a/test-suites/test-aave/helpers/make-suite.ts +++ b/test-suites/test-aave/helpers/make-suite.ts @@ -14,6 +14,7 @@ import { getUniswapLiquiditySwapAdapter, getUniswapRepayAdapter, getFlashLiquidationAdapter, + getParaSwapLiquiditySwapAdapter, } from '../../../helpers/contracts-getters'; import { eEthereumNetwork, eNetwork, tEthereumAddress } from '../../../helpers/types'; import { LendingPool } from '../../../types/LendingPool'; @@ -32,6 +33,7 @@ import { LendingPoolAddressesProviderRegistry } from '../../../types/LendingPool import { getEthersSigners } from '../../../helpers/contracts-helpers'; import { UniswapLiquiditySwapAdapter } from '../../../types/UniswapLiquiditySwapAdapter'; import { UniswapRepayAdapter } from '../../../types/UniswapRepayAdapter'; +import { ParaSwapLiquiditySwapAdapter } from '../../../types/ParaSwapLiquiditySwapAdapter'; import { getParamPerNetwork } from '../../../helpers/contracts-helpers'; import { WETH9Mocked } from '../../../types/WETH9Mocked'; import { WETHGateway } from '../../../types/WETHGateway'; @@ -68,6 +70,7 @@ export interface TestEnv { registry: LendingPoolAddressesProviderRegistry; wethGateway: WETHGateway; flashLiquidationAdapter: FlashLiquidationAdapter; + paraswapLiquiditySwapAdapter: ParaSwapLiquiditySwapAdapter; } let buidlerevmSnapshotId: string = '0x1'; @@ -92,6 +95,7 @@ const testEnv: TestEnv = { uniswapLiquiditySwapAdapter: {} as UniswapLiquiditySwapAdapter, uniswapRepayAdapter: {} as UniswapRepayAdapter, flashLiquidationAdapter: {} as FlashLiquidationAdapter, + paraswapLiquiditySwapAdapter: {} as ParaSwapLiquiditySwapAdapter, registry: {} as LendingPoolAddressesProviderRegistry, wethGateway: {} as WETHGateway, } as TestEnv; @@ -158,6 +162,8 @@ export async function initializeMakeSuite() { testEnv.uniswapLiquiditySwapAdapter = await getUniswapLiquiditySwapAdapter(); testEnv.uniswapRepayAdapter = await getUniswapRepayAdapter(); testEnv.flashLiquidationAdapter = await getFlashLiquidationAdapter(); + + testEnv.paraswapLiquiditySwapAdapter = await getParaSwapLiquiditySwapAdapter(); } const setSnapshot = async () => { diff --git a/test-suites/test-aave/paraswapAdapters.liquiditySwap.spec.ts b/test-suites/test-aave/paraswapAdapters.liquiditySwap.spec.ts new file mode 100644 index 00000000..1a4a424b --- /dev/null +++ b/test-suites/test-aave/paraswapAdapters.liquiditySwap.spec.ts @@ -0,0 +1,2393 @@ +import { makeSuite, TestEnv } from './helpers/make-suite'; +import { + convertToCurrencyDecimals, + getContract, + buildPermitParams, + getSignatureFromTypedData, + buildParaSwapLiquiditySwapParams, +} from '../../helpers/contracts-helpers'; +import { + getMockParaSwapAugustus, + getMockParaSwapAugustusRegistry, +} from '../../helpers/contracts-getters'; +import { deployParaSwapLiquiditySwapAdapter } from '../../helpers/contracts-deployments'; +import { MockParaSwapAugustus } from '../../types/MockParaSwapAugustus'; +import { MockParaSwapAugustusRegistry } from '../../types/MockParaSwapAugustusRegistry'; +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 { AToken } from '../../types/AToken'; +import { BUIDLEREVM_CHAINID } from '../../helpers/buidler-constants'; +import { MAX_UINT_AMOUNT } from '../../helpers/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('ParaSwapLiquiditySwapAdapter', () => { + describe('constructor', () => { + it('should deploy with correct parameters', async () => { + const { addressesProvider } = testEnv; + await deployParaSwapLiquiditySwapAdapter([ + addressesProvider.address, + mockAugustusRegistry.address, + ]); + }); + + it('should revert if not valid addresses provider', async () => { + await expect( + deployParaSwapLiquiditySwapAdapter([ + 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( + deployParaSwapLiquiditySwapAdapter([ + addressesProvider.address, + mockAugustus.address, // any invalid contract can be used here + ]) + ).to.be.reverted; + }); + }); + + describe('executeOperation', () => { + beforeEach(async () => { + const { users, weth, dai, 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); + + await weth.mint(parseEther('10000')); + await weth.approve(pool.address, parseEther('10000')); + await pool.deposit(weth.address, parseEther('10000'), deployer.address, 0); + + // Make a deposit for user + await weth.mint(parseEther('100')); + await weth.approve(pool.address, parseEther('100')); + await pool.deposit(weth.address, parseEther('100'), userAddress, 0); + }); + + it('should correctly swap tokens and deposit the out tokens in the pool', async () => { + const { + users, + weth, + oracle, + dai, + aDai, + aWETH, + pool, + paraswapLiquiditySwapAdapter, + } = 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) + ); + + await mockAugustus.expectSwap(weth.address, dai.address, amountWETHtoSwap, amountWETHtoSwap, expectedDaiAmount); + + const flashloanPremium = amountWETHtoSwap.mul(9).div(10000); + const flashloanTotal = amountWETHtoSwap.add(flashloanPremium); + + // User will swap liquidity aEth to aDai + const userAEthBalanceBefore = await aWETH.balanceOf(userAddress); + await aWETH.connect(user).approve(paraswapLiquiditySwapAdapter.address, flashloanTotal); + + const mockAugustusCalldata = mockAugustus.interface.encodeFunctionData( + 'swap', + [weth.address, dai.address, amountWETHtoSwap, expectedDaiAmount] + ); + + const params = buildParaSwapLiquiditySwapParams( + dai.address, + expectedDaiAmount, + 0, + mockAugustusCalldata, + mockAugustus.address, + 0, + 0, + 0, + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000' + ); + + await expect( + pool + .connect(user) + .flashLoan( + paraswapLiquiditySwapAdapter.address, + [weth.address], + [amountWETHtoSwap], + [0], + userAddress, + params, + 0 + ) + ) + .to.emit(paraswapLiquiditySwapAdapter, 'Swapped') + .withArgs(weth.address, dai.address, amountWETHtoSwap, expectedDaiAmount); + + const adapterWethBalance = await weth.balanceOf(paraswapLiquiditySwapAdapter.address); + const adapterDaiBalance = await dai.balanceOf(paraswapLiquiditySwapAdapter.address); + const userADaiBalance = await aDai.balanceOf(userAddress); + const userAEthBalance = await aWETH.balanceOf(userAddress); + + // N.B. will get some portion of flashloan premium back from the pool + expect(adapterWethBalance).to.be.eq(Zero); + expect(adapterDaiBalance).to.be.eq(Zero); + expect(userADaiBalance).to.be.eq(expectedDaiAmount); + expect(userAEthBalance).to.be.gte(userAEthBalanceBefore.sub(flashloanTotal)); + expect(userAEthBalance).to.be.lte(userAEthBalanceBefore.sub(amountWETHtoSwap)); + }); + + it('should correctly swap tokens using permit', async () => { + const { + users, + weth, + oracle, + dai, + aDai, + aWETH, + pool, + paraswapLiquiditySwapAdapter, + } = 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) + ); + + await mockAugustus.expectSwap(weth.address, dai.address, amountWETHtoSwap, amountWETHtoSwap, expectedDaiAmount); + + const flashloanPremium = amountWETHtoSwap.mul(9).div(10000); + const flashloanTotal = amountWETHtoSwap.add(flashloanPremium); + + // User will swap liquidity aEth to aDai + 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, + paraswapLiquiditySwapAdapter.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 mockAugustusCalldata = mockAugustus.interface.encodeFunctionData( + 'swap', + [weth.address, dai.address, amountWETHtoSwap, expectedDaiAmount] + ); + + const params = buildParaSwapLiquiditySwapParams( + dai.address, + expectedDaiAmount, + 0, + mockAugustusCalldata, + mockAugustus.address, + flashloanTotal, + deadline, + v, + r, + s + ); + + await expect( + pool + .connect(user) + .flashLoan( + paraswapLiquiditySwapAdapter.address, + [weth.address], + [amountWETHtoSwap], + [0], + userAddress, + params, + 0 + ) + ) + .to.emit(paraswapLiquiditySwapAdapter, 'Swapped') + .withArgs(weth.address, dai.address, amountWETHtoSwap, expectedDaiAmount); + + const adapterWethBalance = await weth.balanceOf(paraswapLiquiditySwapAdapter.address); + const adapterDaiBalance = await dai.balanceOf(paraswapLiquiditySwapAdapter.address); + const userADaiBalance = await aDai.balanceOf(userAddress); + const userAEthBalance = await aWETH.balanceOf(userAddress); + + // N.B. will get some portion of flashloan premium back from the pool + expect(adapterWethBalance).to.be.eq(Zero); + expect(adapterDaiBalance).to.be.eq(Zero); + expect(userADaiBalance).to.be.eq(expectedDaiAmount); + expect(userAEthBalance).to.be.gte(userAEthBalanceBefore.sub(flashloanTotal)); + expect(userAEthBalance).to.be.lte(userAEthBalanceBefore.sub(amountWETHtoSwap)); + }); + + it('should revert if caller not lending pool', async () => { + const { + users, + weth, + oracle, + dai, + aWETH, + paraswapLiquiditySwapAdapter, + } = 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) + ); + + await mockAugustus.expectSwap(weth.address, dai.address, amountWETHtoSwap, amountWETHtoSwap, expectedDaiAmount); + + const flashloanPremium = amountWETHtoSwap.mul(9).div(10000); + const flashloanTotal = amountWETHtoSwap.add(flashloanPremium); + + // User will swap liquidity aEth to aDai + await aWETH.connect(user).approve(paraswapLiquiditySwapAdapter.address, flashloanTotal); + + const mockAugustusCalldata = mockAugustus.interface.encodeFunctionData( + 'swap', + [weth.address, dai.address, amountWETHtoSwap, expectedDaiAmount] + ); + + const params = buildParaSwapLiquiditySwapParams( + dai.address, + expectedDaiAmount, + 0, + mockAugustusCalldata, + mockAugustus.address, + 0, + 0, + 0, + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000' + ); + + await expect( + paraswapLiquiditySwapAdapter + .connect(user) + .executeOperation( + [weth.address], + [amountWETHtoSwap], + [0], + userAddress, + params + ) + ).to.be.revertedWith('CALLER_MUST_BE_LENDING_POOL'); + }); + + it('should work correctly with tokens of different decimals', async () => { + const { + users, + usdc, + oracle, + dai, + aDai, + paraswapLiquiditySwapAdapter, + pool, + deployer, + } = testEnv; + const user = users[0].signer; + const userAddress = users[0].address; + + const amountUSDCtoSwap = await convertToCurrencyDecimals(usdc.address, '10'); + const liquidity = await convertToCurrencyDecimals(usdc.address, '20000'); + + const flashloanPremium = amountUSDCtoSwap.mul(9).div(10000); + const flashloanTotal = amountUSDCtoSwap.add(flashloanPremium); + + // Provider liquidity + await usdc.mint(liquidity); + await usdc.approve(pool.address, liquidity); + await pool.deposit(usdc.address, liquidity, deployer.address, 0); + + // Make a deposit for user + await usdc.connect(user).mint(flashloanTotal); + await usdc.connect(user).approve(pool.address, flashloanTotal); + await pool.connect(user).deposit(usdc.address, flashloanTotal, userAddress, 0); + + const usdcPrice = await oracle.getAssetPrice(usdc.address); + const daiPrice = await oracle.getAssetPrice(dai.address); + + const collateralDecimals = (await usdc.decimals()).toString(); + const principalDecimals = (await dai.decimals()).toString(); + + const expectedDaiAmount = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountUSDCtoSwap.toString()) + .times( + new BigNumber(usdcPrice.toString()).times(new BigNumber(10).pow(principalDecimals)) + ) + .div( + new BigNumber(daiPrice.toString()).times(new BigNumber(10).pow(collateralDecimals)) + ) + .div(new BigNumber(10).pow(principalDecimals)) + .toFixed(0) + ); + + await mockAugustus.expectSwap(usdc.address, dai.address, amountUSDCtoSwap, amountUSDCtoSwap, expectedDaiAmount); + + const aUsdcData = await pool.getReserveData(usdc.address); + const aUsdc = await getContract(eContractid.AToken, aUsdcData.aTokenAddress); + + // User will swap liquidity aUsdc to aDai + const userAUsdcBalanceBefore = await aUsdc.balanceOf(userAddress); + await aUsdc.connect(user).approve(paraswapLiquiditySwapAdapter.address, flashloanTotal); + + const mockAugustusCalldata = mockAugustus.interface.encodeFunctionData( + 'swap', + [usdc.address, dai.address, amountUSDCtoSwap, expectedDaiAmount] + ); + + const params = buildParaSwapLiquiditySwapParams( + dai.address, + expectedDaiAmount, + 0, + mockAugustusCalldata, + mockAugustus.address, + 0, + 0, + 0, + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000' + ); + + await expect( + pool + .connect(user) + .flashLoan( + paraswapLiquiditySwapAdapter.address, + [usdc.address], + [amountUSDCtoSwap], + [0], + userAddress, + params, + 0 + ) + ) + .to.emit(paraswapLiquiditySwapAdapter, 'Swapped') + .withArgs(usdc.address, dai.address, amountUSDCtoSwap, expectedDaiAmount); + + const adapterUsdcBalance = await usdc.balanceOf(paraswapLiquiditySwapAdapter.address); + const adapterDaiBalance = await dai.balanceOf(paraswapLiquiditySwapAdapter.address); + const userADaiBalance = await aDai.balanceOf(userAddress); + const userAUsdcBalance = await aUsdc.balanceOf(userAddress); + + // N.B. will get some portion of flashloan premium back from the pool + expect(adapterUsdcBalance).to.be.eq(Zero); + expect(adapterDaiBalance).to.be.eq(Zero); + expect(userADaiBalance).to.be.eq(expectedDaiAmount); + expect(userAUsdcBalance).to.be.gte(userAUsdcBalanceBefore.sub(flashloanTotal)); + expect(userAUsdcBalance).to.be.lte(userAUsdcBalanceBefore.sub(amountUSDCtoSwap)); + }); + + it('should revert when min amount to receive exceeds the max slippage amount', async () => { + const { + users, + weth, + oracle, + dai, + aWETH, + pool, + paraswapLiquiditySwapAdapter, + } = 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) + ); + + await mockAugustus.expectSwap(weth.address, dai.address, amountWETHtoSwap, amountWETHtoSwap, expectedDaiAmount); + + const smallExpectedDaiAmount = expectedDaiAmount.div(2); + + const flashloanPremium = amountWETHtoSwap.mul(9).div(10000); + const flashloanTotal = amountWETHtoSwap.add(flashloanPremium); + + // User will swap liquidity aEth to aDai + await aWETH.connect(user).approve(paraswapLiquiditySwapAdapter.address, flashloanTotal); + + const mockAugustusCalldata = mockAugustus.interface.encodeFunctionData( + 'swap', + [weth.address, dai.address, amountWETHtoSwap, expectedDaiAmount] + ); + + const params = buildParaSwapLiquiditySwapParams( + dai.address, + smallExpectedDaiAmount, + 0, + mockAugustusCalldata, + mockAugustus.address, + 0, + 0, + 0, + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000' + ); + + await expect( + pool + .connect(user) + .flashLoan( + paraswapLiquiditySwapAdapter.address, + [weth.address], + [amountWETHtoSwap], + [0], + userAddress, + params, + 0 + ) + ).to.be.revertedWith('MIN_AMOUNT_EXCEEDS_MAX_SLIPPAGE'); + }); + + it('should revert when min amount to receive exceeds the max slippage amount (with tokens of different decimals)', async () => { + const { + users, + usdc, + oracle, + dai, + paraswapLiquiditySwapAdapter, + pool, + deployer, + } = testEnv; + const user = users[0].signer; + const userAddress = users[0].address; + + const amountUSDCtoSwap = await convertToCurrencyDecimals(usdc.address, '10'); + const liquidity = await convertToCurrencyDecimals(usdc.address, '20000'); + + const flashloanPremium = amountUSDCtoSwap.mul(9).div(10000); + const flashloanTotal = amountUSDCtoSwap.add(flashloanPremium); + + // Provider liquidity + await usdc.mint(liquidity); + await usdc.approve(pool.address, liquidity); + await pool.deposit(usdc.address, liquidity, deployer.address, 0); + + // Make a deposit for user + await usdc.connect(user).mint(flashloanTotal); + await usdc.connect(user).approve(pool.address, flashloanTotal); + await pool.connect(user).deposit(usdc.address, flashloanTotal, userAddress, 0); + + const usdcPrice = await oracle.getAssetPrice(usdc.address); + const daiPrice = await oracle.getAssetPrice(dai.address); + + const collateralDecimals = (await usdc.decimals()).toString(); + const principalDecimals = (await dai.decimals()).toString(); + + const expectedDaiAmount = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountUSDCtoSwap.toString()) + .times( + new BigNumber(usdcPrice.toString()).times(new BigNumber(10).pow(principalDecimals)) + ) + .div( + new BigNumber(daiPrice.toString()).times(new BigNumber(10).pow(collateralDecimals)) + ) + .div(new BigNumber(10).pow(principalDecimals)) + .toFixed(0) + ); + + await mockAugustus.expectSwap(usdc.address, dai.address, amountUSDCtoSwap, amountUSDCtoSwap, expectedDaiAmount); + + const smallExpectedDaiAmount = expectedDaiAmount.div(2); + + const aUsdcData = await pool.getReserveData(usdc.address); + const aUsdc = await getContract(eContractid.AToken, aUsdcData.aTokenAddress); + + // User will swap liquidity aUsdc to aDai + await aUsdc.connect(user).approve(paraswapLiquiditySwapAdapter.address, flashloanTotal); + + const mockAugustusCalldata = mockAugustus.interface.encodeFunctionData( + 'swap', + [usdc.address, dai.address, amountUSDCtoSwap, expectedDaiAmount] + ); + + const params = buildParaSwapLiquiditySwapParams( + dai.address, + smallExpectedDaiAmount, + 0, + mockAugustusCalldata, + mockAugustus.address, + 0, + 0, + 0, + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000' + ); + + await expect( + pool + .connect(user) + .flashLoan( + paraswapLiquiditySwapAdapter.address, + [usdc.address], + [amountUSDCtoSwap], + [0], + userAddress, + params, + 0 + ) + ).to.be.revertedWith('MIN_AMOUNT_EXCEEDS_MAX_SLIPPAGE'); + }); + + it('should correctly swap tokens all the balance', async () => { + const { + users, + weth, + oracle, + dai, + aDai, + aWETH, + pool, + paraswapLiquiditySwapAdapter, + } = 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) + ); + + await mockAugustus.expectSwap(weth.address, dai.address, amountWETHtoSwap, amountWETHtoSwap, expectedDaiAmount); + + const bigAmountToSwap = parseEther('11'); + const flashloanPremium = bigAmountToSwap.mul(9).div(10000); + const flashloanTotal = bigAmountToSwap.add(flashloanPremium); + + // Remove other balance + await aWETH.connect(user).transfer(users[1].address, parseEther('90').sub(flashloanPremium)); + + // User will swap liquidity aEth to aDai + const userAEthBalanceBefore = await aWETH.balanceOf(userAddress); + expect(userAEthBalanceBefore).to.be.eq(amountWETHtoSwap.add(flashloanPremium)); + + await aWETH.connect(user).approve(paraswapLiquiditySwapAdapter.address, flashloanTotal); + + const mockAugustusCalldata = mockAugustus.interface.encodeFunctionData( + 'swap', + [weth.address, dai.address, bigAmountToSwap, expectedDaiAmount] + ); + + const params = buildParaSwapLiquiditySwapParams( + dai.address, + expectedDaiAmount, + 4 + 2*32, + mockAugustusCalldata, + mockAugustus.address, + 0, + 0, + 0, + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000' + ); + + await expect( + pool + .connect(user) + .flashLoan( + paraswapLiquiditySwapAdapter.address, + [weth.address], + [bigAmountToSwap], + [0], + userAddress, + params, + 0 + ) + ) + .to.emit(paraswapLiquiditySwapAdapter, 'Swapped') + .withArgs(weth.address, dai.address, amountWETHtoSwap, expectedDaiAmount); + + const adapterWethBalance = await weth.balanceOf(paraswapLiquiditySwapAdapter.address); + const adapterDaiBalance = await dai.balanceOf(paraswapLiquiditySwapAdapter.address); + const userADaiBalance = await aDai.balanceOf(userAddress); + const userAEthBalance = await aWETH.balanceOf(userAddress); + + expect(adapterWethBalance).to.be.eq(Zero); + expect(adapterDaiBalance).to.be.eq(Zero); + expect(userADaiBalance).to.be.eq(expectedDaiAmount); + expect(userAEthBalance).to.be.eq(Zero); + }); + + it('should correctly swap tokens all the balance using permit', async () => { + const { + users, + weth, + oracle, + dai, + aDai, + aWETH, + pool, + paraswapLiquiditySwapAdapter, + } = 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) + ); + + await mockAugustus.expectSwap(weth.address, dai.address, amountWETHtoSwap, amountWETHtoSwap, expectedDaiAmount); + + const bigAmountToSwap = parseEther('11'); + const flashloanPremium = bigAmountToSwap.mul(9).div(10000); + const flashloanTotal = bigAmountToSwap.add(flashloanPremium); + + // Remove other balance + await aWETH.connect(user).transfer(users[1].address, parseEther('90').sub(flashloanPremium)); + + // User will swap liquidity aEth to aDai + const userAEthBalanceBefore = await aWETH.balanceOf(userAddress); + expect(userAEthBalanceBefore).to.be.eq(amountWETHtoSwap.add(flashloanPremium)); + + 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, + paraswapLiquiditySwapAdapter.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 mockAugustusCalldata = mockAugustus.interface.encodeFunctionData( + 'swap', + [weth.address, dai.address, bigAmountToSwap, expectedDaiAmount] + ); + + const params = buildParaSwapLiquiditySwapParams( + dai.address, + expectedDaiAmount, + 4 + 2*32, + mockAugustusCalldata, + mockAugustus.address, + flashloanTotal, + deadline, + v, + r, + s + ); + + await expect( + pool + .connect(user) + .flashLoan( + paraswapLiquiditySwapAdapter.address, + [weth.address], + [bigAmountToSwap], + [0], + userAddress, + params, + 0 + ) + ) + .to.emit(paraswapLiquiditySwapAdapter, 'Swapped') + .withArgs(weth.address, dai.address, amountWETHtoSwap, expectedDaiAmount); + + const adapterWethBalance = await weth.balanceOf(paraswapLiquiditySwapAdapter.address); + const adapterDaiBalance = await dai.balanceOf(paraswapLiquiditySwapAdapter.address); + const userADaiBalance = await aDai.balanceOf(userAddress); + const userAEthBalance = await aWETH.balanceOf(userAddress); + + expect(adapterWethBalance).to.be.eq(Zero); + expect(adapterDaiBalance).to.be.eq(Zero); + expect(userADaiBalance).to.be.eq(expectedDaiAmount); + expect(userAEthBalance).to.be.eq(Zero); + }); + + it('should revert trying to swap all the balance with insufficient amount', async () => { + const { + users, + weth, + oracle, + dai, + aWETH, + pool, + paraswapLiquiditySwapAdapter, + } = 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) + ); + + await mockAugustus.expectSwap(weth.address, dai.address, amountWETHtoSwap, amountWETHtoSwap, expectedDaiAmount); + + const smallAmountToSwap = parseEther('9'); + const flashloanPremium = smallAmountToSwap.mul(9).div(10000); + const flashloanTotal = smallAmountToSwap.add(flashloanPremium); + + // Remove other balance + await aWETH.connect(user).transfer(users[1].address, parseEther('90').sub(flashloanPremium)); + + // User will swap liquidity aEth to aDai + const userAEthBalanceBefore = await aWETH.balanceOf(userAddress); + expect(userAEthBalanceBefore).to.be.eq(amountWETHtoSwap.add(flashloanPremium)); + + await aWETH.connect(user).approve(paraswapLiquiditySwapAdapter.address, flashloanTotal); + + const mockAugustusCalldata = mockAugustus.interface.encodeFunctionData( + 'swap', + [weth.address, dai.address, smallAmountToSwap, expectedDaiAmount] + ); + + const params = buildParaSwapLiquiditySwapParams( + dai.address, + expectedDaiAmount, + 4 + 2*32, + mockAugustusCalldata, + mockAugustus.address, + 0, + 0, + 0, + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000' + ); + + await expect( + pool + .connect(user) + .flashLoan( + paraswapLiquiditySwapAdapter.address, + [weth.address], + [smallAmountToSwap], + [0], + userAddress, + params, + 0 + ) + ).to.be.revertedWith('INSUFFICIENT_AMOUNT_TO_SWAP'); + }); + + it('should revert trying to swap more than balance', async () => { + const { + users, + weth, + oracle, + dai, + aWETH, + pool, + paraswapLiquiditySwapAdapter, + } = testEnv; + const user = users[0].signer; + const userAddress = users[0].address; + + const amountWETHtoSwap = await convertToCurrencyDecimals(weth.address, '101'); + + const daiPrice = await oracle.getAssetPrice(dai.address); + const expectedDaiAmount = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) + ); + + await mockAugustus.expectSwap(weth.address, dai.address, amountWETHtoSwap, amountWETHtoSwap, expectedDaiAmount); + + const flashloanPremium = amountWETHtoSwap.mul(9).div(10000); + const flashloanTotal = amountWETHtoSwap.add(flashloanPremium); + + // User will swap liquidity aEth to aDai + await aWETH.connect(user).approve(paraswapLiquiditySwapAdapter.address, flashloanTotal); + + const mockAugustusCalldata = mockAugustus.interface.encodeFunctionData( + 'swap', + [weth.address, dai.address, amountWETHtoSwap, expectedDaiAmount] + ); + + const params = buildParaSwapLiquiditySwapParams( + dai.address, + expectedDaiAmount, + 0, + mockAugustusCalldata, + mockAugustus.address, + 0, + 0, + 0, + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000' + ); + + await expect( + pool + .connect(user) + .flashLoan( + paraswapLiquiditySwapAdapter.address, + [weth.address], + [amountWETHtoSwap], + [0], + userAddress, + params, + 0 + ) + ).to.be.revertedWith('INSUFFICIENT_ATOKEN_BALANCE'); + }); + + it('should not touch any token balance already in the adapter', async () => { + const { + users, + weth, + oracle, + dai, + aDai, + aWETH, + pool, + paraswapLiquiditySwapAdapter, + } = testEnv; + const user = users[0].signer; + const userAddress = users[0].address; + + // Put token balances in the adapter + const adapterWethBalanceBefore = parseEther('123'); + await weth.mint(adapterWethBalanceBefore); + await weth.transfer(paraswapLiquiditySwapAdapter.address, adapterWethBalanceBefore); + const adapterDaiBalanceBefore = parseEther('234'); + await dai.mint(adapterDaiBalanceBefore); + await dai.transfer(paraswapLiquiditySwapAdapter.address, adapterDaiBalanceBefore); + + 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) + ); + + await mockAugustus.expectSwap(weth.address, dai.address, amountWETHtoSwap, amountWETHtoSwap, expectedDaiAmount); + + const flashloanPremium = amountWETHtoSwap.mul(9).div(10000); + const flashloanTotal = amountWETHtoSwap.add(flashloanPremium); + + // User will swap liquidity aEth to aDai + const userAEthBalanceBefore = await aWETH.balanceOf(userAddress); + await aWETH.connect(user).approve(paraswapLiquiditySwapAdapter.address, flashloanTotal); + + const mockAugustusCalldata = mockAugustus.interface.encodeFunctionData( + 'swap', + [weth.address, dai.address, amountWETHtoSwap, expectedDaiAmount] + ); + + const params = buildParaSwapLiquiditySwapParams( + dai.address, + expectedDaiAmount, + 0, + mockAugustusCalldata, + mockAugustus.address, + 0, + 0, + 0, + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000' + ); + + await expect( + pool + .connect(user) + .flashLoan( + paraswapLiquiditySwapAdapter.address, + [weth.address], + [amountWETHtoSwap], + [0], + userAddress, + params, + 0 + ) + ) + .to.emit(paraswapLiquiditySwapAdapter, 'Swapped') + .withArgs(weth.address, dai.address, amountWETHtoSwap, expectedDaiAmount); + + const adapterWethBalance = await weth.balanceOf(paraswapLiquiditySwapAdapter.address); + const adapterDaiBalance = await dai.balanceOf(paraswapLiquiditySwapAdapter.address); + const userADaiBalance = await aDai.balanceOf(userAddress); + const userAEthBalance = await aWETH.balanceOf(userAddress); + + // N.B. will get some portion of flashloan premium back from the pool + expect(adapterWethBalance).to.be.eq(adapterWethBalanceBefore); + expect(adapterDaiBalance).to.be.eq(adapterDaiBalanceBefore); + expect(userADaiBalance).to.be.eq(expectedDaiAmount); + expect(userAEthBalance).to.be.gte(userAEthBalanceBefore.sub(flashloanTotal)); + expect(userAEthBalance).to.be.lte(userAEthBalanceBefore.sub(amountWETHtoSwap)); + }); + }); + + describe('executeOperation with borrowing', () => { + beforeEach(async () => { + const { users, weth, dai, pool, deployer } = testEnv; + const userAddress = users[0].address; + const borrower = users[1].signer; + const borrowerAddress = users[1].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); + + await weth.mint(parseEther('10000')); + await weth.approve(pool.address, parseEther('10000')); + await pool.deposit(weth.address, parseEther('10000'), deployer.address, 0); + + // Make a deposit for user + await weth.mint(parseEther('100')); + await weth.approve(pool.address, parseEther('100')); + await pool.deposit(weth.address, parseEther('100'), userAddress, 0); + + // Add borrowing + const collateralAmount = parseEther('10000000'); + await dai.mint(collateralAmount); + await dai.approve(pool.address, collateralAmount); + await pool.deposit(dai.address, collateralAmount, borrowerAddress, 0); + await pool.connect(borrower).borrow(weth.address, parseEther('5000'), 2, 0, borrowerAddress); + }); + + it('should correctly swap tokens and deposit the out tokens in the pool', async () => { + const { + users, + weth, + oracle, + dai, + aDai, + aWETH, + pool, + paraswapLiquiditySwapAdapter, + } = 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) + ); + + await mockAugustus.expectSwap(weth.address, dai.address, amountWETHtoSwap, amountWETHtoSwap, expectedDaiAmount); + + const flashloanPremium = amountWETHtoSwap.mul(9).div(10000); + const flashloanTotal = amountWETHtoSwap.add(flashloanPremium); + + // User will swap liquidity aEth to aDai + const userAEthBalanceBefore = await aWETH.balanceOf(userAddress); + await aWETH.connect(user).approve(paraswapLiquiditySwapAdapter.address, flashloanTotal); + + const mockAugustusCalldata = mockAugustus.interface.encodeFunctionData( + 'swap', + [weth.address, dai.address, amountWETHtoSwap, expectedDaiAmount] + ); + + const params = buildParaSwapLiquiditySwapParams( + dai.address, + expectedDaiAmount, + 0, + mockAugustusCalldata, + mockAugustus.address, + 0, + 0, + 0, + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000' + ); + + await expect( + pool + .connect(user) + .flashLoan( + paraswapLiquiditySwapAdapter.address, + [weth.address], + [amountWETHtoSwap], + [0], + userAddress, + params, + 0 + ) + ) + .to.emit(paraswapLiquiditySwapAdapter, 'Swapped') + .withArgs(weth.address, dai.address, amountWETHtoSwap, expectedDaiAmount); + + const adapterWethBalance = await weth.balanceOf(paraswapLiquiditySwapAdapter.address); + const adapterDaiBalance = await dai.balanceOf(paraswapLiquiditySwapAdapter.address); + const userADaiBalance = await aDai.balanceOf(userAddress); + const userAEthBalance = await aWETH.balanceOf(userAddress); + + // N.B. will get some portion of flashloan premium back from the pool + expect(adapterWethBalance).to.be.eq(Zero); + expect(adapterDaiBalance).to.be.eq(Zero); + expect(userADaiBalance).to.be.eq(expectedDaiAmount); + expect(userAEthBalance).to.be.gt(userAEthBalanceBefore.sub(flashloanTotal)); + expect(userAEthBalance).to.be.lt(userAEthBalanceBefore.mul(10001).div(10000).sub(amountWETHtoSwap)); + }); + + it('should correctly swap tokens using permit', async () => { + const { + users, + weth, + oracle, + dai, + aDai, + aWETH, + pool, + paraswapLiquiditySwapAdapter, + } = 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) + ); + + await mockAugustus.expectSwap(weth.address, dai.address, amountWETHtoSwap, amountWETHtoSwap, expectedDaiAmount); + + const flashloanPremium = amountWETHtoSwap.mul(9).div(10000); + const flashloanTotal = amountWETHtoSwap.add(flashloanPremium); + + // User will swap liquidity aEth to aDai + 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, + paraswapLiquiditySwapAdapter.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 mockAugustusCalldata = mockAugustus.interface.encodeFunctionData( + 'swap', + [weth.address, dai.address, amountWETHtoSwap, expectedDaiAmount] + ); + + const params = buildParaSwapLiquiditySwapParams( + dai.address, + expectedDaiAmount, + 0, + mockAugustusCalldata, + mockAugustus.address, + flashloanTotal, + deadline, + v, + r, + s + ); + + await expect( + pool + .connect(user) + .flashLoan( + paraswapLiquiditySwapAdapter.address, + [weth.address], + [amountWETHtoSwap], + [0], + userAddress, + params, + 0 + ) + ) + .to.emit(paraswapLiquiditySwapAdapter, 'Swapped') + .withArgs(weth.address, dai.address, amountWETHtoSwap, expectedDaiAmount); + + const adapterWethBalance = await weth.balanceOf(paraswapLiquiditySwapAdapter.address); + const adapterDaiBalance = await dai.balanceOf(paraswapLiquiditySwapAdapter.address); + const userADaiBalance = await aDai.balanceOf(userAddress); + const userAEthBalance = await aWETH.balanceOf(userAddress); + + // N.B. will get some portion of flashloan premium back from the pool + expect(adapterWethBalance).to.be.eq(Zero); + expect(adapterDaiBalance).to.be.eq(Zero); + expect(userADaiBalance).to.be.eq(expectedDaiAmount); + expect(userAEthBalance).to.be.gt(userAEthBalanceBefore.sub(flashloanTotal)); + expect(userAEthBalance).to.be.lt(userAEthBalanceBefore.mul(10001).div(10000).sub(amountWETHtoSwap)); + }); + + it('should correctly swap tokens all the balance', async () => { + const { + users, + weth, + oracle, + dai, + aDai, + aWETH, + pool, + paraswapLiquiditySwapAdapter, + } = 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) + ); + + await mockAugustus.expectSwap(weth.address, dai.address, amountWETHtoSwap.add(1), amountWETHtoSwap.mul(10001).div(10000), expectedDaiAmount); + + const bigAmountToSwap = parseEther('11'); + const flashloanPremium = bigAmountToSwap.mul(9).div(10000); + const flashloanTotal = bigAmountToSwap.add(flashloanPremium); + + // Remove other balance + await aWETH.connect(user).transfer(users[1].address, parseEther('90').sub(flashloanPremium)); + + // User will swap liquidity aEth to aDai + await aWETH.connect(user).approve(paraswapLiquiditySwapAdapter.address, flashloanTotal); + + const mockAugustusCalldata = mockAugustus.interface.encodeFunctionData( + 'swap', + [weth.address, dai.address, bigAmountToSwap, expectedDaiAmount] + ); + + const params = buildParaSwapLiquiditySwapParams( + dai.address, + expectedDaiAmount, + 4 + 2*32, + mockAugustusCalldata, + mockAugustus.address, + 0, + 0, + 0, + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000' + ); + + await expect( + pool + .connect(user) + .flashLoan( + paraswapLiquiditySwapAdapter.address, + [weth.address], + [bigAmountToSwap], + [0], + userAddress, + params, + 0 + ) + ).to.emit(paraswapLiquiditySwapAdapter, 'Swapped'); + + const adapterWethBalance = await weth.balanceOf(paraswapLiquiditySwapAdapter.address); + const adapterDaiBalance = await dai.balanceOf(paraswapLiquiditySwapAdapter.address); + const userADaiBalance = await aDai.balanceOf(userAddress); + const userAEthBalance = await aWETH.balanceOf(userAddress); + + expect(adapterWethBalance).to.be.eq(Zero); + expect(adapterDaiBalance).to.be.eq(Zero); + expect(userADaiBalance).to.be.eq(expectedDaiAmount); + expect(userAEthBalance).to.be.eq(Zero); + }); + + it('should correctly swap tokens all the balance using permit', async () => { + const { + users, + weth, + oracle, + dai, + aDai, + aWETH, + pool, + paraswapLiquiditySwapAdapter, + } = 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) + ); + + await mockAugustus.expectSwap(weth.address, dai.address, amountWETHtoSwap.add(1), amountWETHtoSwap.mul(10001).div(10000), expectedDaiAmount); + + const bigAmountToSwap = parseEther('11'); + const flashloanPremium = bigAmountToSwap.mul(9).div(10000); + const flashloanTotal = bigAmountToSwap.add(flashloanPremium); + + // Remove other balance + await aWETH.connect(user).transfer(users[1].address, parseEther('90').sub(flashloanPremium)); + + // User will swap liquidity aEth to aDai + 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, + paraswapLiquiditySwapAdapter.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 mockAugustusCalldata = mockAugustus.interface.encodeFunctionData( + 'swap', + [weth.address, dai.address, bigAmountToSwap, expectedDaiAmount] + ); + + const params = buildParaSwapLiquiditySwapParams( + dai.address, + expectedDaiAmount, + 4 + 2*32, + mockAugustusCalldata, + mockAugustus.address, + flashloanTotal, + deadline, + v, + r, + s + ); + + await expect( + pool + .connect(user) + .flashLoan( + paraswapLiquiditySwapAdapter.address, + [weth.address], + [bigAmountToSwap], + [0], + userAddress, + params, + 0 + ) + ).to.emit(paraswapLiquiditySwapAdapter, 'Swapped'); + + const adapterWethBalance = await weth.balanceOf(paraswapLiquiditySwapAdapter.address); + const adapterDaiBalance = await dai.balanceOf(paraswapLiquiditySwapAdapter.address); + const userADaiBalance = await aDai.balanceOf(userAddress); + const userAEthBalance = await aWETH.balanceOf(userAddress); + + expect(adapterWethBalance).to.be.eq(Zero); + expect(adapterDaiBalance).to.be.eq(Zero); + expect(userADaiBalance).to.be.eq(expectedDaiAmount); + expect(userAEthBalance).to.be.eq(Zero); + }); + }); + + describe('swapAndDeposit', () => { + beforeEach(async () => { + const { users, weth, dai, 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); + + await weth.mint(parseEther('10000')); + await weth.approve(pool.address, parseEther('10000')); + await pool.deposit(weth.address, parseEther('10000'), deployer.address, 0); + + // Make a deposit for user + await weth.mint(parseEther('100')); + await weth.approve(pool.address, parseEther('100')); + await pool.deposit(weth.address, parseEther('100'), userAddress, 0); + }); + + it('should correctly swap tokens and deposit the out tokens in the pool', async () => { + const { users, weth, oracle, dai, aDai, aWETH, paraswapLiquiditySwapAdapter } = 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) + ); + + await mockAugustus.expectSwap(weth.address, dai.address, amountWETHtoSwap, amountWETHtoSwap, expectedDaiAmount); + + // User will swap liquidity aEth to aDai + const userAEthBalanceBefore = await aWETH.balanceOf(userAddress); + await aWETH.connect(user).approve(paraswapLiquiditySwapAdapter.address, amountWETHtoSwap); + + const mockAugustusCalldata = mockAugustus.interface.encodeFunctionData( + 'swap', + [weth.address, dai.address, amountWETHtoSwap, expectedDaiAmount] + ); + + await expect( + paraswapLiquiditySwapAdapter.connect(user).swapAndDeposit( + weth.address, + dai.address, + amountWETHtoSwap, + expectedDaiAmount, + 0, + mockAugustusCalldata, + mockAugustus.address, + { + amount: 0, + deadline: 0, + v: 0, + r: '0x0000000000000000000000000000000000000000000000000000000000000000', + s: '0x0000000000000000000000000000000000000000000000000000000000000000', + } + ) + ) + .to.emit(paraswapLiquiditySwapAdapter, 'Swapped') + .withArgs(weth.address, dai.address, amountWETHtoSwap, expectedDaiAmount); + + const adapterWethBalance = await weth.balanceOf(paraswapLiquiditySwapAdapter.address); + const adapterDaiBalance = await dai.balanceOf(paraswapLiquiditySwapAdapter.address); + const userADaiBalance = await aDai.balanceOf(userAddress); + const userAEthBalance = await aWETH.balanceOf(userAddress); + + expect(adapterWethBalance).to.be.eq(Zero); + expect(adapterDaiBalance).to.be.eq(Zero); + expect(userADaiBalance).to.be.eq(expectedDaiAmount); + expect(userAEthBalance).to.be.eq(userAEthBalanceBefore.sub(amountWETHtoSwap)); + }); + + it('should correctly swap tokens using permit', async () => { + const { users, weth, oracle, dai, aDai, aWETH, paraswapLiquiditySwapAdapter } = 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) + ); + + await mockAugustus.expectSwap(weth.address, dai.address, amountWETHtoSwap, amountWETHtoSwap, expectedDaiAmount); + + // User will swap liquidity aEth to aDai + 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, + paraswapLiquiditySwapAdapter.address, + nonce, + deadline, + amountWETHtoSwap.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 mockAugustusCalldata = mockAugustus.interface.encodeFunctionData( + 'swap', + [weth.address, dai.address, amountWETHtoSwap, expectedDaiAmount] + ); + + await expect( + paraswapLiquiditySwapAdapter.connect(user).swapAndDeposit( + weth.address, + dai.address, + amountWETHtoSwap, + expectedDaiAmount, + 0, + mockAugustusCalldata, + mockAugustus.address, + { + amount: amountWETHtoSwap, + deadline, + v, + r, + s, + } + ) + ) + .to.emit(paraswapLiquiditySwapAdapter, 'Swapped') + .withArgs(weth.address, dai.address, amountWETHtoSwap, expectedDaiAmount); + + const adapterWethBalance = await weth.balanceOf(paraswapLiquiditySwapAdapter.address); + const adapterDaiBalance = await dai.balanceOf(paraswapLiquiditySwapAdapter.address); + const userADaiBalance = await aDai.balanceOf(userAddress); + const userAEthBalance = await aWETH.balanceOf(userAddress); + + expect(adapterWethBalance).to.be.eq(Zero); + expect(adapterDaiBalance).to.be.eq(Zero); + expect(userADaiBalance).to.be.eq(expectedDaiAmount); + expect(userAEthBalance).to.be.eq(userAEthBalanceBefore.sub(amountWETHtoSwap)); + }); + + it('should revert when trying to swap more than balance', async () => { + const { users, weth, oracle, dai, aWETH, paraswapLiquiditySwapAdapter } = testEnv; + const user = users[0].signer; + const userAddress = users[0].address; + + const amountWETHtoSwap = (await convertToCurrencyDecimals(weth.address, '100')).add(1); + + const daiPrice = await oracle.getAssetPrice(dai.address); + const expectedDaiAmount = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) + ); + + await mockAugustus.expectSwap(weth.address, dai.address, amountWETHtoSwap, amountWETHtoSwap, expectedDaiAmount); + + // User will swap liquidity aEth to aDai + await aWETH.connect(user).approve(paraswapLiquiditySwapAdapter.address, amountWETHtoSwap); + + const mockAugustusCalldata = mockAugustus.interface.encodeFunctionData( + 'swap', + [weth.address, dai.address, amountWETHtoSwap, expectedDaiAmount] + ); + + await expect( + paraswapLiquiditySwapAdapter.connect(user).swapAndDeposit( + weth.address, + dai.address, + amountWETHtoSwap, + expectedDaiAmount, + 0, + mockAugustusCalldata, + mockAugustus.address, + { + amount: 0, + deadline: 0, + v: 0, + r: '0x0000000000000000000000000000000000000000000000000000000000000000', + s: '0x0000000000000000000000000000000000000000000000000000000000000000', + } + ) + ).to.be.revertedWith('SafeERC20: low-level call failed'); + }); + + it('should revert when trying to swap more than allowance', async () => { + const { users, weth, oracle, dai, aWETH, paraswapLiquiditySwapAdapter } = 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) + ); + + await mockAugustus.expectSwap(weth.address, dai.address, amountWETHtoSwap, amountWETHtoSwap, expectedDaiAmount); + + // User will swap liquidity aEth to aDai + await aWETH.connect(user).approve(paraswapLiquiditySwapAdapter.address, amountWETHtoSwap.sub(1)); + + const mockAugustusCalldata = mockAugustus.interface.encodeFunctionData( + 'swap', + [weth.address, dai.address, amountWETHtoSwap, expectedDaiAmount] + ); + + await expect( + paraswapLiquiditySwapAdapter.connect(user).swapAndDeposit( + weth.address, + dai.address, + amountWETHtoSwap, + expectedDaiAmount, + 0, + mockAugustusCalldata, + mockAugustus.address, + { + amount: 0, + deadline: 0, + v: 0, + r: '0x0000000000000000000000000000000000000000000000000000000000000000', + s: '0x0000000000000000000000000000000000000000000000000000000000000000', + } + ) + ).to.be.revertedWith('SafeERC20: low-level call failed'); + }); + + it('should revert when min amount to receive exceeds the max slippage amount', async () => { + const { users, weth, oracle, dai, aWETH, paraswapLiquiditySwapAdapter } = 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) + ); + + await mockAugustus.expectSwap(weth.address, dai.address, amountWETHtoSwap, amountWETHtoSwap, expectedDaiAmount); + + const smallExpectedDaiAmount = expectedDaiAmount.div(2); + + // User will swap liquidity aEth to aDai + await aWETH.connect(user).approve(paraswapLiquiditySwapAdapter.address, amountWETHtoSwap); + + const mockAugustusCalldata = mockAugustus.interface.encodeFunctionData( + 'swap', + [weth.address, dai.address, amountWETHtoSwap, expectedDaiAmount] + ); + + await expect( + paraswapLiquiditySwapAdapter.connect(user).swapAndDeposit( + weth.address, + dai.address, + amountWETHtoSwap, + smallExpectedDaiAmount, + 0, + mockAugustusCalldata, + mockAugustus.address, + { + amount: 0, + deadline: 0, + v: 0, + r: '0x0000000000000000000000000000000000000000000000000000000000000000', + s: '0x0000000000000000000000000000000000000000000000000000000000000000', + } + ) + ).to.be.revertedWith('MIN_AMOUNT_EXCEEDS_MAX_SLIPPAGE'); + }); + + it('should revert if wrong address used for Augustus', async () => { + const { users, weth, oracle, dai, aWETH, paraswapLiquiditySwapAdapter } = 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) + ); + + await mockAugustus.expectSwap(weth.address, dai.address, amountWETHtoSwap, amountWETHtoSwap, expectedDaiAmount); + + // User will swap liquidity aEth to aDai + await aWETH.connect(user).approve(paraswapLiquiditySwapAdapter.address, amountWETHtoSwap); + + const mockAugustusCalldata = mockAugustus.interface.encodeFunctionData( + 'swap', + [weth.address, dai.address, amountWETHtoSwap, expectedDaiAmount] + ); + + await expect( + paraswapLiquiditySwapAdapter.connect(user).swapAndDeposit( + weth.address, + dai.address, + amountWETHtoSwap, + expectedDaiAmount, + 0, + mockAugustusCalldata, + oracle.address, // using arbitrary contract instead of mock Augustus + { + amount: 0, + deadline: 0, + v: 0, + r: '0x0000000000000000000000000000000000000000000000000000000000000000', + s: '0x0000000000000000000000000000000000000000000000000000000000000000', + } + ) + ).to.be.revertedWith('INVALID_AUGUSTUS'); + }); + + it('should bubble up errors from Augustus', async () => { + const { users, weth, oracle, dai, aWETH, paraswapLiquiditySwapAdapter } = 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) + ); + + await mockAugustus.expectSwap(weth.address, dai.address, amountWETHtoSwap, amountWETHtoSwap, expectedDaiAmount); + + // User will swap liquidity aEth to aDai + await aWETH.connect(user).approve(paraswapLiquiditySwapAdapter.address, amountWETHtoSwap); + + // Add 1 to expected amount so it will fail + const mockAugustusCalldata = mockAugustus.interface.encodeFunctionData( + 'swap', + [weth.address, dai.address, amountWETHtoSwap, expectedDaiAmount.add(1)] + ); + + await expect( + paraswapLiquiditySwapAdapter.connect(user).swapAndDeposit( + weth.address, + dai.address, + amountWETHtoSwap, + expectedDaiAmount, + 0, + mockAugustusCalldata, + mockAugustus.address, + { + amount: 0, + deadline: 0, + v: 0, + r: '0x0000000000000000000000000000000000000000000000000000000000000000', + s: '0x0000000000000000000000000000000000000000000000000000000000000000', + } + ) + ).to.be.revertedWith('Received amount of tokens are less than expected'); + }); + + it('should revert if Augustus swaps for less than minimum to receive', async () => { + const { users, weth, oracle, dai, aWETH, paraswapLiquiditySwapAdapter } = 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) + ); + const actualDaiAmount = expectedDaiAmount.sub(1); + + await mockAugustus.expectSwap(weth.address, dai.address, amountWETHtoSwap, amountWETHtoSwap, actualDaiAmount); + + // User will swap liquidity aEth to aDai + await aWETH.connect(user).approve(paraswapLiquiditySwapAdapter.address, amountWETHtoSwap); + + const mockAugustusCalldata = mockAugustus.interface.encodeFunctionData( + 'swap', + [weth.address, dai.address, amountWETHtoSwap, actualDaiAmount] + ); + + await expect( + paraswapLiquiditySwapAdapter.connect(user).swapAndDeposit( + weth.address, + dai.address, + amountWETHtoSwap, + expectedDaiAmount, + 0, + mockAugustusCalldata, + mockAugustus.address, + { + amount: 0, + deadline: 0, + v: 0, + r: '0x0000000000000000000000000000000000000000000000000000000000000000', + s: '0x0000000000000000000000000000000000000000000000000000000000000000', + } + ) + ).to.be.revertedWith('INSUFFICIENT_AMOUNT_RECEIVED'); + }); + + it("should revert if Augustus doesn't swap correct amount", async () => { + const { users, weth, oracle, dai, aWETH, paraswapLiquiditySwapAdapter } = 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) + ); + + const augustusSwapAmount = amountWETHtoSwap.sub(1); + + await mockAugustus.expectSwap(weth.address, dai.address, augustusSwapAmount, augustusSwapAmount, expectedDaiAmount); + + // User will swap liquidity aEth to aDai + await aWETH.connect(user).approve(paraswapLiquiditySwapAdapter.address, amountWETHtoSwap); + + const mockAugustusCalldata = mockAugustus.interface.encodeFunctionData( + 'swap', + [weth.address, dai.address, augustusSwapAmount, expectedDaiAmount] + ); + + await expect( + paraswapLiquiditySwapAdapter.connect(user).swapAndDeposit( + weth.address, + dai.address, + amountWETHtoSwap, + expectedDaiAmount, + 0, + mockAugustusCalldata, + mockAugustus.address, + { + amount: 0, + deadline: 0, + v: 0, + r: '0x0000000000000000000000000000000000000000000000000000000000000000', + s: '0x0000000000000000000000000000000000000000000000000000000000000000', + } + ) + ).to.be.revertedWith('WRONG_BALANCE_AFTER_SWAP'); + }); + + it('should correctly swap all the balance when using a bigger amount', async () => { + const { users, weth, oracle, dai, aDai, aWETH, paraswapLiquiditySwapAdapter } = 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) + ); + + await mockAugustus.expectSwap(weth.address, dai.address, amountWETHtoSwap, amountWETHtoSwap, expectedDaiAmount); + + // Remove other balance + await aWETH.connect(user).transfer(users[1].address, parseEther('90')); + + // User will swap liquidity aEth to aDai + const userAEthBalanceBefore = await aWETH.balanceOf(userAddress); + expect(userAEthBalanceBefore).to.be.eq(amountWETHtoSwap); + + const bigAmountToSwap = parseEther('11'); + await aWETH.connect(user).approve(paraswapLiquiditySwapAdapter.address, bigAmountToSwap); + + const mockAugustusCalldata = mockAugustus.interface.encodeFunctionData( + 'swap', + [weth.address, dai.address, bigAmountToSwap, expectedDaiAmount] + ); + + await expect( + paraswapLiquiditySwapAdapter.connect(user).swapAndDeposit( + weth.address, + dai.address, + bigAmountToSwap, + expectedDaiAmount, + 4 + 2*32, + mockAugustusCalldata, + mockAugustus.address, + { + amount: 0, + deadline: 0, + v: 0, + r: '0x0000000000000000000000000000000000000000000000000000000000000000', + s: '0x0000000000000000000000000000000000000000000000000000000000000000', + } + ) + ) + .to.emit(paraswapLiquiditySwapAdapter, 'Swapped') + .withArgs(weth.address, dai.address, amountWETHtoSwap, expectedDaiAmount); + + const adapterWethBalance = await weth.balanceOf(paraswapLiquiditySwapAdapter.address); + const adapterDaiBalance = await dai.balanceOf(paraswapLiquiditySwapAdapter.address); + const userADaiBalance = await aDai.balanceOf(userAddress); + const userAEthBalance = await aWETH.balanceOf(userAddress); + + expect(adapterWethBalance).to.be.eq(Zero); + expect(adapterDaiBalance).to.be.eq(Zero); + expect(userADaiBalance).to.be.eq(expectedDaiAmount); + expect(userAEthBalance).to.be.eq(Zero); + }); + + it('should correctly swap all the balance when using permit', async () => { + const { users, weth, oracle, dai, aDai, aWETH, paraswapLiquiditySwapAdapter } = 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) + ); + + await mockAugustus.expectSwap(weth.address, dai.address, amountWETHtoSwap, amountWETHtoSwap, expectedDaiAmount); + + // Remove other balance + await aWETH.connect(user).transfer(users[1].address, parseEther('90')); + + // User will swap liquidity aEth to aDai + const userAEthBalanceBefore = await aWETH.balanceOf(userAddress); + expect(userAEthBalanceBefore).to.be.eq(amountWETHtoSwap); + + const bigAmountToSwap = parseEther('11'); + + 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, + paraswapLiquiditySwapAdapter.address, + nonce, + deadline, + bigAmountToSwap.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 mockAugustusCalldata = mockAugustus.interface.encodeFunctionData( + 'swap', + [weth.address, dai.address, bigAmountToSwap, expectedDaiAmount] + ); + + await expect( + paraswapLiquiditySwapAdapter.connect(user).swapAndDeposit( + weth.address, + dai.address, + bigAmountToSwap, + expectedDaiAmount, + 4 + 2*32, + mockAugustusCalldata, + mockAugustus.address, + { + amount: bigAmountToSwap, + deadline, + v, + r, + s, + } + ) + ) + .to.emit(paraswapLiquiditySwapAdapter, 'Swapped') + .withArgs(weth.address, dai.address, amountWETHtoSwap, expectedDaiAmount); + + const adapterWethBalance = await weth.balanceOf(paraswapLiquiditySwapAdapter.address); + const adapterDaiBalance = await dai.balanceOf(paraswapLiquiditySwapAdapter.address); + const userADaiBalance = await aDai.balanceOf(userAddress); + const userAEthBalance = await aWETH.balanceOf(userAddress); + + expect(adapterWethBalance).to.be.eq(Zero); + expect(adapterDaiBalance).to.be.eq(Zero); + expect(userADaiBalance).to.be.eq(expectedDaiAmount); + expect(userAEthBalance).to.be.eq(Zero); + }); + + it('should revert trying to swap all the balance when using a smaller amount', async () => { + const { users, weth, oracle, dai, aWETH, paraswapLiquiditySwapAdapter } = 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) + ); + + await mockAugustus.expectSwap(weth.address, dai.address, amountWETHtoSwap, amountWETHtoSwap, expectedDaiAmount); + + // Remove other balance + await aWETH.connect(user).transfer(users[1].address, parseEther('90')); + + // User will swap liquidity aEth to aDai + const userAEthBalanceBefore = await aWETH.balanceOf(userAddress); + expect(userAEthBalanceBefore).to.be.eq(amountWETHtoSwap); + + const smallAmountToSwap = parseEther('10').sub(1); + await aWETH.connect(user).approve(paraswapLiquiditySwapAdapter.address, smallAmountToSwap); + + const mockAugustusCalldata = mockAugustus.interface.encodeFunctionData( + 'swap', + [weth.address, dai.address, smallAmountToSwap, expectedDaiAmount] + ); + + await expect( + paraswapLiquiditySwapAdapter.connect(user).swapAndDeposit( + weth.address, + dai.address, + smallAmountToSwap, + expectedDaiAmount, + 4 + 2*32, + mockAugustusCalldata, + mockAugustus.address, + { + amount: 0, + deadline: 0, + v: 0, + r: '0x0000000000000000000000000000000000000000000000000000000000000000', + s: '0x0000000000000000000000000000000000000000000000000000000000000000', + } + ) + ).to.be.revertedWith('INSUFFICIENT_AMOUNT_TO_SWAP'); + }); + + it('should not touch any token balance already in the adapter', async () => { + const { users, weth, oracle, dai, aDai, aWETH, paraswapLiquiditySwapAdapter } = testEnv; + const user = users[0].signer; + const userAddress = users[0].address; + + // Put token balances in the adapter + const adapterWethBalanceBefore = parseEther('123'); + await weth.mint(adapterWethBalanceBefore); + await weth.transfer(paraswapLiquiditySwapAdapter.address, adapterWethBalanceBefore); + const adapterDaiBalanceBefore = parseEther('234'); + await dai.mint(adapterDaiBalanceBefore); + await dai.transfer(paraswapLiquiditySwapAdapter.address, adapterDaiBalanceBefore); + + 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) + ); + + await mockAugustus.expectSwap(weth.address, dai.address, amountWETHtoSwap, amountWETHtoSwap, expectedDaiAmount); + + // User will swap liquidity aEth to aDai + const userAEthBalanceBefore = await aWETH.balanceOf(userAddress); + await aWETH.connect(user).approve(paraswapLiquiditySwapAdapter.address, amountWETHtoSwap); + + const mockAugustusCalldata = mockAugustus.interface.encodeFunctionData( + 'swap', + [weth.address, dai.address, amountWETHtoSwap, expectedDaiAmount] + ); + + await expect( + paraswapLiquiditySwapAdapter.connect(user).swapAndDeposit( + weth.address, + dai.address, + amountWETHtoSwap, + expectedDaiAmount, + 0, + mockAugustusCalldata, + mockAugustus.address, + { + amount: 0, + deadline: 0, + v: 0, + r: '0x0000000000000000000000000000000000000000000000000000000000000000', + s: '0x0000000000000000000000000000000000000000000000000000000000000000', + } + ) + ) + .to.emit(paraswapLiquiditySwapAdapter, 'Swapped') + .withArgs(weth.address, dai.address, amountWETHtoSwap, expectedDaiAmount); + + const adapterWethBalance = await weth.balanceOf(paraswapLiquiditySwapAdapter.address); + const adapterDaiBalance = await dai.balanceOf(paraswapLiquiditySwapAdapter.address); + const userADaiBalance = await aDai.balanceOf(userAddress); + const userAEthBalance = await aWETH.balanceOf(userAddress); + + expect(adapterWethBalance).to.be.eq(adapterWethBalanceBefore); + expect(adapterDaiBalance).to.be.eq(adapterDaiBalanceBefore); + expect(userADaiBalance).to.be.eq(expectedDaiAmount); + expect(userAEthBalance).to.be.eq(userAEthBalanceBefore.sub(amountWETHtoSwap)); + }); + }); + + describe('swapAndDeposit with borrowing', () => { + beforeEach(async () => { + const { users, weth, dai, pool, deployer } = testEnv; + const userAddress = users[0].address; + const borrower = users[1].signer; + const borrowerAddress = users[1].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); + + await weth.mint(parseEther('10000')); + await weth.approve(pool.address, parseEther('10000')); + await pool.deposit(weth.address, parseEther('10000'), deployer.address, 0); + + // Make a deposit for user + await weth.mint(parseEther('100')); + await weth.approve(pool.address, parseEther('100')); + await pool.deposit(weth.address, parseEther('100'), userAddress, 0); + + // Add borrowing + const collateralAmount = parseEther('10000000'); + await dai.mint(collateralAmount); + await dai.approve(pool.address, collateralAmount); + await pool.deposit(dai.address, collateralAmount, borrowerAddress, 0); + await pool.connect(borrower).borrow(weth.address, parseEther('5000'), 2, 0, borrowerAddress); + }); + + it('should correctly swap tokens and deposit the out tokens in the pool', async () => { + const { users, weth, oracle, dai, aDai, aWETH, paraswapLiquiditySwapAdapter } = 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) + ); + + await mockAugustus.expectSwap(weth.address, dai.address, amountWETHtoSwap, amountWETHtoSwap, expectedDaiAmount); + + // User will swap liquidity aEth to aDai + const userAEthBalanceBefore = await aWETH.balanceOf(userAddress); + await aWETH.connect(user).approve(paraswapLiquiditySwapAdapter.address, amountWETHtoSwap); + + const mockAugustusCalldata = mockAugustus.interface.encodeFunctionData( + 'swap', + [weth.address, dai.address, amountWETHtoSwap, expectedDaiAmount] + ); + + await expect( + paraswapLiquiditySwapAdapter.connect(user).swapAndDeposit( + weth.address, + dai.address, + amountWETHtoSwap, + expectedDaiAmount, + 0, + mockAugustusCalldata, + mockAugustus.address, + { + amount: 0, + deadline: 0, + v: 0, + r: '0x0000000000000000000000000000000000000000000000000000000000000000', + s: '0x0000000000000000000000000000000000000000000000000000000000000000', + } + ) + ) + .to.emit(paraswapLiquiditySwapAdapter, 'Swapped') + .withArgs(weth.address, dai.address, amountWETHtoSwap, expectedDaiAmount); + + const adapterWethBalance = await weth.balanceOf(paraswapLiquiditySwapAdapter.address); + const adapterDaiBalance = await dai.balanceOf(paraswapLiquiditySwapAdapter.address); + const userADaiBalance = await aDai.balanceOf(userAddress); + const userAEthBalance = await aWETH.balanceOf(userAddress); + + expect(adapterWethBalance).to.be.eq(Zero); + expect(adapterDaiBalance).to.be.eq(Zero); + expect(userADaiBalance).to.be.eq(expectedDaiAmount); + expect(userAEthBalance).to.be.gt(userAEthBalanceBefore.sub(amountWETHtoSwap)); + expect(userAEthBalance).to.be.lt(userAEthBalanceBefore.mul(10001).div(10000).sub(amountWETHtoSwap)); + }); + + it('should correctly swap tokens using permit', async () => { + const { users, weth, oracle, dai, aDai, aWETH, paraswapLiquiditySwapAdapter } = 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) + ); + + await mockAugustus.expectSwap(weth.address, dai.address, amountWETHtoSwap, amountWETHtoSwap, expectedDaiAmount); + + // User will swap liquidity aEth to aDai + 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, + paraswapLiquiditySwapAdapter.address, + nonce, + deadline, + amountWETHtoSwap.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 mockAugustusCalldata = mockAugustus.interface.encodeFunctionData( + 'swap', + [weth.address, dai.address, amountWETHtoSwap, expectedDaiAmount] + ); + + await expect( + paraswapLiquiditySwapAdapter.connect(user).swapAndDeposit( + weth.address, + dai.address, + amountWETHtoSwap, + expectedDaiAmount, + 0, + mockAugustusCalldata, + mockAugustus.address, + { + amount: amountWETHtoSwap, + deadline, + v, + r, + s, + } + ) + ) + .to.emit(paraswapLiquiditySwapAdapter, 'Swapped') + .withArgs(weth.address, dai.address, amountWETHtoSwap, expectedDaiAmount); + + const adapterWethBalance = await weth.balanceOf(paraswapLiquiditySwapAdapter.address); + const adapterDaiBalance = await dai.balanceOf(paraswapLiquiditySwapAdapter.address); + const userADaiBalance = await aDai.balanceOf(userAddress); + const userAEthBalance = await aWETH.balanceOf(userAddress); + + expect(adapterWethBalance).to.be.eq(Zero); + expect(adapterDaiBalance).to.be.eq(Zero); + expect(userADaiBalance).to.be.eq(expectedDaiAmount); + expect(userAEthBalance).to.be.gt(userAEthBalanceBefore.sub(amountWETHtoSwap)); + expect(userAEthBalance).to.be.lt(userAEthBalanceBefore.mul(10001).div(10000).sub(amountWETHtoSwap)); + }); + + it('should correctly swap all the balance when using a bigger amount', async () => { + const { users, weth, oracle, dai, aDai, aWETH, paraswapLiquiditySwapAdapter } = 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) + ); + + await mockAugustus.expectSwap(weth.address, dai.address, amountWETHtoSwap.add(1), amountWETHtoSwap.mul(10001).div(10000), expectedDaiAmount); + + // Remove other balance + await aWETH.connect(user).transfer(users[1].address, parseEther('90')); + + // User will swap liquidity aEth to aDai + const bigAmountToSwap = parseEther('11'); + await aWETH.connect(user).approve(paraswapLiquiditySwapAdapter.address, bigAmountToSwap); + + const mockAugustusCalldata = mockAugustus.interface.encodeFunctionData( + 'swap', + [weth.address, dai.address, bigAmountToSwap, expectedDaiAmount] + ); + + await expect( + paraswapLiquiditySwapAdapter.connect(user).swapAndDeposit( + weth.address, + dai.address, + bigAmountToSwap, + expectedDaiAmount, + 4 + 2*32, + mockAugustusCalldata, + mockAugustus.address, + { + amount: 0, + deadline: 0, + v: 0, + r: '0x0000000000000000000000000000000000000000000000000000000000000000', + s: '0x0000000000000000000000000000000000000000000000000000000000000000', + } + ) + ).to.emit(paraswapLiquiditySwapAdapter, 'Swapped'); + + const adapterWethBalance = await weth.balanceOf(paraswapLiquiditySwapAdapter.address); + const adapterDaiBalance = await dai.balanceOf(paraswapLiquiditySwapAdapter.address); + const userADaiBalance = await aDai.balanceOf(userAddress); + const userAEthBalance = await aWETH.balanceOf(userAddress); + + expect(adapterWethBalance).to.be.eq(Zero); + expect(adapterDaiBalance).to.be.eq(Zero); + expect(userADaiBalance).to.be.eq(expectedDaiAmount); + expect(userAEthBalance).to.be.eq(Zero); + }); + + it('should correctly swap all the balance when using permit', async () => { + const { users, weth, oracle, dai, aDai, aWETH, paraswapLiquiditySwapAdapter } = 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) + ); + + await mockAugustus.expectSwap(weth.address, dai.address, amountWETHtoSwap.add(1), amountWETHtoSwap.mul(10001).div(10000), expectedDaiAmount); + + // Remove other balance + await aWETH.connect(user).transfer(users[1].address, parseEther('90')); + + // User will swap liquidity aEth to aDai + const bigAmountToSwap = parseEther('11'); + + 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, + paraswapLiquiditySwapAdapter.address, + nonce, + deadline, + bigAmountToSwap.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 mockAugustusCalldata = mockAugustus.interface.encodeFunctionData( + 'swap', + [weth.address, dai.address, bigAmountToSwap, expectedDaiAmount] + ); + + await expect( + paraswapLiquiditySwapAdapter.connect(user).swapAndDeposit( + weth.address, + dai.address, + bigAmountToSwap, + expectedDaiAmount, + 4 + 2*32, + mockAugustusCalldata, + mockAugustus.address, + { + amount: bigAmountToSwap, + deadline, + v, + r, + s, + } + ) + ).to.emit(paraswapLiquiditySwapAdapter, 'Swapped'); + + const adapterWethBalance = await weth.balanceOf(paraswapLiquiditySwapAdapter.address); + const adapterDaiBalance = await dai.balanceOf(paraswapLiquiditySwapAdapter.address); + const userADaiBalance = await aDai.balanceOf(userAddress); + const userAEthBalance = await aWETH.balanceOf(userAddress); + + expect(adapterWethBalance).to.be.eq(Zero); + expect(adapterDaiBalance).to.be.eq(Zero); + expect(userADaiBalance).to.be.eq(expectedDaiAmount); + expect(userAEthBalance).to.be.eq(Zero); + }); + }); + }); +}); diff --git a/test-suites/test-aave/uniswapAdapters.flashLiquidation.spec.ts b/test-suites/test-aave/uniswapAdapters.flashLiquidation.spec.ts index cc161b4b..37779ab7 100644 --- a/test-suites/test-aave/uniswapAdapters.flashLiquidation.spec.ts +++ b/test-suites/test-aave/uniswapAdapters.flashLiquidation.spec.ts @@ -198,7 +198,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { it('should revert if not valid addresses provider', async () => { const { weth } = testEnv; - expect( + await expect( deployFlashLiquidationAdapter([ mockUniswapRouter.address, mockUniswapRouter.address, diff --git a/test-suites/test-aave/uniswapAdapters.liquiditySwap.spec.ts b/test-suites/test-aave/uniswapAdapters.liquiditySwap.spec.ts index 42224c52..01179c31 100644 --- a/test-suites/test-aave/uniswapAdapters.liquiditySwap.spec.ts +++ b/test-suites/test-aave/uniswapAdapters.liquiditySwap.spec.ts @@ -50,7 +50,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { it('should revert if not valid addresses provider', async () => { const { weth } = testEnv; - expect( + await expect( deployUniswapLiquiditySwapAdapter([ mockUniswapRouter.address, mockUniswapRouter.address, @@ -196,6 +196,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { .div( new BigNumber(daiPrice.toString()).times(new BigNumber(10).pow(collateralDecimals)) ) + .div(new BigNumber(10).pow(principalDecimals)) .toFixed(0) ); @@ -318,6 +319,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { .div( new BigNumber(daiPrice.toString()).times(new BigNumber(10).pow(collateralDecimals)) ) + .div(new BigNumber(10).pow(principalDecimals)) .toFixed(0) ); @@ -871,6 +873,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { .div( new BigNumber(daiPrice.toString()).times(new BigNumber(10).pow(collateralDecimals)) ) + .div(new BigNumber(10).pow(principalDecimals)) .toFixed(0) ); @@ -1493,6 +1496,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { .div( new BigNumber(daiPrice.toString()).times(new BigNumber(10).pow(collateralDecimals)) ) + .div(new BigNumber(10).pow(principalDecimals)) .toFixed(0) ); @@ -1601,6 +1605,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { .div( new BigNumber(daiPrice.toString()).times(new BigNumber(10).pow(collateralDecimals)) ) + .div(new BigNumber(10).pow(principalDecimals)) .toFixed(0) ); diff --git a/test-suites/test-aave/uniswapAdapters.repay.spec.ts b/test-suites/test-aave/uniswapAdapters.repay.spec.ts index c2046cbd..54e1a137 100644 --- a/test-suites/test-aave/uniswapAdapters.repay.spec.ts +++ b/test-suites/test-aave/uniswapAdapters.repay.spec.ts @@ -87,7 +87,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { it('should revert if not valid addresses provider', async () => { const { weth } = testEnv; - expect( + await expect( deployUniswapRepayAdapter([ mockUniswapRouter.address, mockUniswapRouter.address,