diff --git a/contracts/adapters/BaseParaSwapAdapter.sol b/contracts/adapters/BaseParaSwapAdapter.sol new file mode 100644 index 00000000..72ce55fd --- /dev/null +++ b/contracts/adapters/BaseParaSwapAdapter.sol @@ -0,0 +1,124 @@ +// 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; + + 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(address asset) internal view returns (uint256) { + return IERC20Detailed(asset).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 _pullAToken( + address reserve, + address reserveAToken, + address user, + uint256 amount, + PermitSignature memory permitSignature + ) internal { + if (_usePermit(permitSignature)) { + IERC20WithPermit(reserveAToken).permit( + user, + address(this), + permitSignature.amount, + permitSignature.deadline, + permitSignature.v, + permitSignature.r, + permitSignature.s + ); + } + + // transfer from user to adapter + IERC20(reserveAToken).safeTransferFrom(user, address(this), amount); + + // withdraw reserve + LENDING_POOL.withdraw(reserve, amount, address(this)); + } + + /** + * @dev Tells if the permit method should be called by inspecting if there is a valid signature. + * If signature params are set to 0, then permit won't be called. + * @param signature struct containing the permit signature + * @return whether or not permit should be called + */ + function _usePermit(PermitSignature memory signature) internal pure returns (bool) { + return + !(uint256(signature.deadline) == uint256(signature.v) && uint256(signature.deadline) == 0); + } + + /** + * @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.transfer(owner(), token.balanceOf(address(this))); + } +} diff --git a/contracts/adapters/BaseParaSwapSellAdapter.sol b/contracts/adapters/BaseParaSwapSellAdapter.sol new file mode 100644 index 00000000..909ca2d3 --- /dev/null +++ b/contracts/adapters/BaseParaSwapSellAdapter.sol @@ -0,0 +1,92 @@ +// 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 {ILendingPoolAddressesProvider} from '../interfaces/ILendingPoolAddressesProvider.sol'; +import {IERC20} from '../dependencies/openzeppelin/contracts/IERC20.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; + + constructor( + ILendingPoolAddressesProvider addressesProvider + ) public BaseParaSwapAdapter(addressesProvider) { + } + + /** + * @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, + address augustus, + address assetToSwapFrom, + address assetToSwapTo, + uint256 amountToSwap, + uint256 minAmountToReceive + ) internal returns (uint256 amountReceived) { + { + uint256 fromAssetDecimals = _getDecimals(assetToSwapFrom); + uint256 toAssetDecimals = _getDecimals(assetToSwapTo); + + uint256 fromAssetPrice = _getPrice(assetToSwapFrom); + uint256 toAssetPrice = _getPrice(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 = IERC20(assetToSwapFrom).balanceOf(address(this)); + require(balanceBeforeAssetFrom >= amountToSwap, 'INSUFFICIENT_BALANCE_BEFORE_SWAP'); + uint256 balanceBeforeAssetTo = IERC20(assetToSwapTo).balanceOf(address(this)); + + address tokenTransferProxy = IParaSwapAugustus(augustus).getTokenTransferProxy(); + IERC20(assetToSwapFrom).safeApprove(tokenTransferProxy, 0); + IERC20(assetToSwapFrom).safeApprove(tokenTransferProxy, amountToSwap); + + if (fromAmountOffset != 0) { + require(fromAmountOffset >= 4 && + fromAmountOffset <= swapCalldata.length.sub(32), + 'FROM_AMOUNT_OFFSET_OUT_OF_RANGE'); + assembly { + mstore(add(swapCalldata, add(fromAmountOffset, 32)), amountToSwap) + } + } + (bool success,) = augustus.call(swapCalldata); + if (!success) { + // Copy revert reason from call + assembly { + let ptr := mload(0x40) + let size := returndatasize() + returndatacopy(ptr, 0, size) + revert(ptr, size) + } + } + require(IERC20(assetToSwapFrom).balanceOf(address(this)) == balanceBeforeAssetFrom - amountToSwap, 'WRONG_BALANCE_AFTER_SWAP'); + amountReceived = IERC20(assetToSwapTo).balanceOf(address(this)).sub(balanceBeforeAssetTo); + require(amountReceived >= minAmountToReceive, 'INSUFFICIENT_AMOUNT_RECEIVED'); + + emit Swapped(assetToSwapFrom, assetToSwapTo, amountToSwap, amountReceived); + } +} diff --git a/contracts/adapters/ParaSwapLiquiditySwapAdapter.sol b/contracts/adapters/ParaSwapLiquiditySwapAdapter.sol new file mode 100644 index 00000000..4e6ef962 --- /dev/null +++ b/contracts/adapters/ParaSwapLiquiditySwapAdapter.sol @@ -0,0 +1,191 @@ +// 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 {IERC20} from '../dependencies/openzeppelin/contracts/IERC20.sol'; + +/** + * @title ParaSwapLiquiditySwapAdapter + * @notice Adapter to swap liquidity using ParaSwap. + * @author Jason Raymond Bell + */ +contract ParaSwapLiquiditySwapAdapter is BaseParaSwapSellAdapter { + constructor( + ILendingPoolAddressesProvider addressesProvider + ) public BaseParaSwapSellAdapter(addressesProvider) {} + + /** + * @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 returns (bool) { + require(msg.sender == address(LENDING_POOL), 'CALLER_MUST_BE_LENDING_POOL'); + require(assets.length == 1, 'FLASHLOAN_MULTIPLE_ASSETS_NOT_SUPPORTED'); + + uint256 flashLoanAmount = amounts[0]; + uint256 premium = premiums[0]; + address initiatorLocal = initiator; + address assetToSwapFrom = assets[0]; + ( + address assetToSwapTo, + uint256 minAmountToReceive, + uint256 swapAllBalanceOffset, + bytes memory swapCalldata, + address augustus, + PermitSignature memory permitParams + ) = abi.decode(params, (address, uint256, uint256, bytes, address, 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( + address assetToSwapFrom, + address assetToSwapTo, + uint256 amountToSwap, + uint256 minAmountToReceive, + uint256 swapAllBalanceOffset, + bytes calldata swapCalldata, + address augustus, + PermitSignature calldata permitParams + ) external { + address aToken = _getReserveData(assetToSwapFrom).aTokenAddress; + + if (swapAllBalanceOffset != 0) { + uint256 balance = IERC20(aToken).balanceOf(msg.sender); + require(balance <= amountToSwap, 'INSUFFICIENT_AMOUNT_TO_SWAP'); + amountToSwap = balance; + } + + _pullAToken( + assetToSwapFrom, + aToken, + msg.sender, + amountToSwap, + permitParams + ); + + uint256 amountReceived = _sellOnParaSwap( + swapAllBalanceOffset, + swapCalldata, + augustus, + assetToSwapFrom, + assetToSwapTo, + amountToSwap, + minAmountToReceive + ); + + IERC20(assetToSwapTo).safeApprove(address(LENDING_POOL), 0); + IERC20(assetToSwapTo).safeApprove(address(LENDING_POOL), amountReceived); + LENDING_POOL.deposit(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, + address augustus, + PermitSignature memory permitParams, + uint256 flashLoanAmount, + uint256 premium, + address initiator, + address assetToSwapFrom, + address assetToSwapTo, + uint256 minAmountToReceive + ) internal { + address aToken = _getReserveData(assetToSwapFrom).aTokenAddress; + uint256 amountToSwap = flashLoanAmount; + + uint256 balance = IERC20(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 + ); + + IERC20(assetToSwapTo).safeApprove(address(LENDING_POOL), 0); + IERC20(assetToSwapTo).safeApprove(address(LENDING_POOL), amountReceived); + LENDING_POOL.deposit(assetToSwapTo, amountReceived, initiator, 0); + + _pullAToken( + assetToSwapFrom, + aToken, + initiator, + amountToSwap.add(premium), + permitParams + ); + + // Repay flash loan + IERC20(assetToSwapFrom).safeApprove(address(LENDING_POOL), 0); + IERC20(assetToSwapFrom).safeApprove(address(LENDING_POOL), flashLoanAmount.add(premium)); + } +} 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/mocks/swap/MockParaSwapAugustus.sol b/contracts/mocks/swap/MockParaSwapAugustus.sol new file mode 100644 index 00000000..8a6874c6 --- /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 _tokenTransferProxy; + bool _expectingSwap; + address _expectedFromToken; + address _expectedToToken; + uint256 _expectedFromAmountMin; + uint256 _expectedFromAmountMax; + uint256 _receivedAmount; + + constructor() public { + _tokenTransferProxy = new MockParaSwapTokenTransferProxy(); + } + + function getTokenTransferProxy() external view override returns (address) { + return address(_tokenTransferProxy); + } + + 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'); + _tokenTransferProxy.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/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 fb284b35..eba11e27 100644 --- a/helpers/contracts-deployments.ts +++ b/helpers/contracts-deployments.ts @@ -36,9 +36,11 @@ import { MockAggregatorFactory, MockATokenFactory, MockFlashLoanReceiverFactory, + MockParaSwapAugustusFactory, MockStableDebtTokenFactory, MockVariableDebtTokenFactory, MockUniswapV2Router02Factory, + ParaSwapLiquiditySwapAdapterFactory, PriceOracleFactory, ReserveLogicFactory, SelfdestructTransferFactory, @@ -537,3 +539,22 @@ export const deployFlashLiquidationAdapter = async ( args, verify ); + +export const deployMockParaSwapAugustus = async (verify?: boolean) => + withSaveAndVerify( + await new MockParaSwapAugustusFactory(await getFirstSigner()).deploy(), + eContractid.MockParaSwapAugustus, + [], + verify + ); + +export const deployParaSwapLiquiditySwapAdapter = async ( + args: [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 6504cceb..cd1aa792 100644 --- a/helpers/contracts-getters.ts +++ b/helpers/contracts-getters.ts @@ -18,6 +18,8 @@ import { MockStableDebtTokenFactory, MockVariableDebtTokenFactory, MockUniswapV2Router02Factory, + MockParaSwapAugustusFactory, + ParaSwapLiquiditySwapAdapterFactory, PriceOracleFactory, ReserveLogicFactory, SelfdestructTransferFactory, @@ -363,3 +365,19 @@ 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 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 6fce99b6..50fd77aa 100644 --- a/helpers/contracts-helpers.ts +++ b/helpers/contracts-helpers.ts @@ -301,3 +301,35 @@ export const buildFlashLiquidationAdapterParams = ( [collateralAsset, debtAsset, user, debtToCover, useEthPath] ); }; + +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], + ] + ); +}; diff --git a/helpers/types.ts b/helpers/types.ts index bc1e31e6..07dff857 100644 --- a/helpers/types.ts +++ b/helpers/types.ts @@ -71,6 +71,8 @@ export enum eContractid { UniswapLiquiditySwapAdapter = 'UniswapLiquiditySwapAdapter', UniswapRepayAdapter = 'UniswapRepayAdapter', FlashLiquidationAdapter = 'FlashLiquidationAdapter', + MockParaSwapAugustus = 'MockParaSwapAugustus', + ParaSwapLiquiditySwapAdapter = 'ParaSwapLiquiditySwapAdapter', } /* diff --git a/package.json b/package.json index 1f8d3043..b57a10dd 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "test-subgraph:scenarios": "hardhat --network hardhatevm_docker test test/__setup.spec.ts test/subgraph-scenarios.spec.ts", "test-weth": "hardhat test test/__setup.spec.ts test/weth-gateway.spec.ts", "test-uniswap": "hardhat test test/__setup.spec.ts test/uniswapAdapters*.spec.ts", + "test-paraswap": "hardhat test test/__setup.spec.ts test/paraswapAdapters*.spec.ts", "test:main:check-list": "MAINNET_FORK=true TS_NODE_TRANSPILE_ONLY=1 hardhat test test/__setup.spec.ts test/mainnet/check-list.spec.ts", "dev:coverage": "buidler compile --force && buidler coverage --network coverage", "aave:evm:dev:migration": "npm run compile && hardhat aave:dev", diff --git a/test/__setup.spec.ts b/test/__setup.spec.ts index 37ff9cfc..20a4f02c 100644 --- a/test/__setup.spec.ts +++ b/test/__setup.spec.ts @@ -26,6 +26,8 @@ import { deployUniswapLiquiditySwapAdapter, deployUniswapRepayAdapter, deployFlashLiquidationAdapter, + deployMockParaSwapAugustus, + deployParaSwapLiquiditySwapAdapter, } from '../helpers/contracts-deployments'; import { Signer } from 'ethers'; import { TokenContractId, eContractid, tEthereumAddress, AavePools } from '../helpers/types'; @@ -247,6 +249,10 @@ const buildTestEnv = async (deployer: Signer, secondaryWallet: Signer) => { await deployUniswapRepayAdapter(adapterParams); await deployFlashLiquidationAdapter(adapterParams); + await deployMockParaSwapAugustus(); + + await deployParaSwapLiquiditySwapAdapter([addressesProvider.address]); + await deployWalletBalancerProvider(); await deployWETHGateway([mockTokens.WETH.address, lendingPoolAddress]); diff --git a/test/helpers/make-suite.ts b/test/helpers/make-suite.ts index 4a75e54d..f6641a79 100644 --- a/test/helpers/make-suite.ts +++ b/test/helpers/make-suite.ts @@ -14,6 +14,7 @@ import { getUniswapLiquiditySwapAdapter, getUniswapRepayAdapter, getFlashLiquidationAdapter, + getParaSwapLiquiditySwapAdapter, } from '../../helpers/contracts-getters'; import { eEthereumNetwork, tEthereumAddress } from '../../helpers/types'; import { LendingPool } from '../../types/LendingPool'; @@ -32,6 +33,7 @@ import { LendingPoolAddressesProviderRegistry } from '../../types/LendingPoolAdd 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/paraswapAdapters.liquiditySwap.spec.ts b/test/paraswapAdapters.liquiditySwap.spec.ts new file mode 100644 index 00000000..9561f7df --- /dev/null +++ b/test/paraswapAdapters.liquiditySwap.spec.ts @@ -0,0 +1,2332 @@ +import { makeSuite, TestEnv } from './helpers/make-suite'; +import { + convertToCurrencyDecimals, + getContract, + buildPermitParams, + getSignatureFromTypedData, + buildParaSwapLiquiditySwapParams, +} from '../helpers/contracts-helpers'; +import { getMockParaSwapAugustus } from '../helpers/contracts-getters'; +import { deployParaSwapLiquiditySwapAdapter } from '../helpers/contracts-deployments'; +import { MockParaSwapAugustus } from '../types/MockParaSwapAugustus'; +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 evmSnapshotId: string; + + before(async () => { + mockAugustus = await getMockParaSwapAugustus(); + }); + + 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, + ]); + }); + + it('should revert if not valid addresses provider', async () => { + await expect( + deployParaSwapLiquiditySwapAdapter([ + mockAugustus.address, + ]) + ).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 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/uniswapAdapters.flashLiquidation.spec.ts b/test/uniswapAdapters.flashLiquidation.spec.ts index 063c6930..800a1b2c 100644 --- a/test/uniswapAdapters.flashLiquidation.spec.ts +++ b/test/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/uniswapAdapters.liquiditySwap.spec.ts b/test/uniswapAdapters.liquiditySwap.spec.ts index 1e30b2b3..bc8467e6 100644 --- a/test/uniswapAdapters.liquiditySwap.spec.ts +++ b/test/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/uniswapAdapters.repay.spec.ts b/test/uniswapAdapters.repay.spec.ts index c271917e..cd5450d3 100644 --- a/test/uniswapAdapters.repay.spec.ts +++ b/test/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,