From 242826ded60c7a2cbe5bb0abe704735f7acf5c60 Mon Sep 17 00:00:00 2001 From: Jason Raymond Bell Date: Thu, 18 Mar 2021 15:44:52 +0000 Subject: [PATCH 01/15] ParaSwap adapter for collateral swaps Complete with unit tests (using a mock AugustusSwapper contract). Has similar functionality/tests as for existing Uniswap adapter. Fixed a couple bugs in tests for Uniswap adapters. --- contracts/adapters/BaseParaSwapAdapter.sol | 124 + .../adapters/BaseParaSwapSellAdapter.sol | 92 + .../adapters/ParaSwapLiquiditySwapAdapter.sol | 191 ++ contracts/interfaces/IParaSwapAugustus.sol | 7 + contracts/mocks/swap/MockParaSwapAugustus.sol | 59 + .../swap/MockParaSwapTokenTransferProxy.sol | 17 + helpers/contracts-deployments.ts | 21 + helpers/contracts-getters.ts | 18 + helpers/contracts-helpers.ts | 32 + helpers/types.ts | 2 + package.json | 1 + test/__setup.spec.ts | 6 + test/helpers/make-suite.ts | 6 + test/paraswapAdapters.liquiditySwap.spec.ts | 2332 +++++++++++++++++ test/uniswapAdapters.flashLiquidation.spec.ts | 2 +- test/uniswapAdapters.liquiditySwap.spec.ts | 7 +- test/uniswapAdapters.repay.spec.ts | 2 +- 17 files changed, 2916 insertions(+), 3 deletions(-) create mode 100644 contracts/adapters/BaseParaSwapAdapter.sol create mode 100644 contracts/adapters/BaseParaSwapSellAdapter.sol create mode 100644 contracts/adapters/ParaSwapLiquiditySwapAdapter.sol create mode 100644 contracts/interfaces/IParaSwapAugustus.sol create mode 100644 contracts/mocks/swap/MockParaSwapAugustus.sol create mode 100644 contracts/mocks/swap/MockParaSwapTokenTransferProxy.sol create mode 100644 test/paraswapAdapters.liquiditySwap.spec.ts 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, From 29772961ac28698f74e0cdeccb566ea5ba3e5df7 Mon Sep 17 00:00:00 2001 From: Jason Raymond Bell Date: Fri, 19 Mar 2021 00:16:39 +0000 Subject: [PATCH 02/15] Simplify ASM for copying revert reason --- contracts/adapters/BaseParaSwapSellAdapter.sol | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/contracts/adapters/BaseParaSwapSellAdapter.sol b/contracts/adapters/BaseParaSwapSellAdapter.sol index 909ca2d3..ec50f4b7 100644 --- a/contracts/adapters/BaseParaSwapSellAdapter.sol +++ b/contracts/adapters/BaseParaSwapSellAdapter.sol @@ -77,10 +77,8 @@ abstract contract BaseParaSwapSellAdapter is BaseParaSwapAdapter { if (!success) { // Copy revert reason from call assembly { - let ptr := mload(0x40) - let size := returndatasize() - returndatacopy(ptr, 0, size) - revert(ptr, size) + returndatacopy(0, 0, returndatasize()) + revert(0, returndatasize()) } } require(IERC20(assetToSwapFrom).balanceOf(address(this)) == balanceBeforeAssetFrom - amountToSwap, 'WRONG_BALANCE_AFTER_SWAP'); From 14e2ab47d95f42ec5ee486f367067e78a7588878 Mon Sep 17 00:00:00 2001 From: Jason Raymond Bell Date: Mon, 29 Mar 2021 17:19:26 +0100 Subject: [PATCH 03/15] Add task to deploy ParaSwapLiquiditySwapAdapter Note that it didn't actually work as written since gasPrice was set too low and also the verifyContract didn't work (so I had to verify it manually). Deployed to mainnet at 0xC91864A5A1E2F52aDdfb69e31b89a3b7783733c3. --- package.json | 1 + .../deploy-ParaSwapLiquiditySwapAdapter.ts | 28 +++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 tasks/deployments/deploy-ParaSwapLiquiditySwapAdapter.ts diff --git a/package.json b/package.json index 1746c5a3..1105b4b7 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "dev:UniswapLiquiditySwapAdapter": "hardhat --network kovan deploy-UniswapLiquiditySwapAdapter --provider 0x88757f2f99175387aB4C6a4b3067c77A695b0349 --router 0xfcd87315f0e4067070ade8682fcdbc3006631441 --weth 0xd0a1e359811322d97991e03f863a0c30c2cf029c", "main:deployUniswapRepayAdapter": "hardhat --network main deploy-UniswapRepayAdapter --provider 0xB53C1a33016B2DC2fF3653530bfF1848a515c8c5 --router 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D --weth 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", "main:UniswapLiquiditySwapAdapter": "hardhat --network main deploy-UniswapLiquiditySwapAdapter --provider 0xB53C1a33016B2DC2fF3653530bfF1848a515c8c5 --router 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D --weth 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "main:ParaSwapLiquiditySwapAdapter": "hardhat --network main deploy-ParaSwapLiquiditySwapAdapter --provider 0xB53C1a33016B2DC2fF3653530bfF1848a515c8c5", "kovan:verify": "npm run hardhat:kovan verify:general -- --all --pool Aave", "ropsten:verify": "npm run hardhat:ropsten verify:general -- --all --pool Aave", "mainnet:verify": "npm run hardhat:main verify:general -- --all --pool Aave", diff --git a/tasks/deployments/deploy-ParaSwapLiquiditySwapAdapter.ts b/tasks/deployments/deploy-ParaSwapLiquiditySwapAdapter.ts new file mode 100644 index 00000000..1ee0720f --- /dev/null +++ b/tasks/deployments/deploy-ParaSwapLiquiditySwapAdapter.ts @@ -0,0 +1,28 @@ +import { task } from 'hardhat/config'; + +import { ParaSwapLiquiditySwapAdapterFactory } from '../../types'; +import { verifyContract } from '../../helpers/etherscan-verification'; +import { getFirstSigner } from '../../helpers/contracts-getters'; + +const CONTRACT_NAME = 'ParaSwapLiquiditySwapAdapter'; + +task(`deploy-${CONTRACT_NAME}`, `Deploys the ${CONTRACT_NAME} contract`) + .addParam('provider', 'Address of the LendingPoolAddressesProvider') + .addFlag('verify', `Verify ${CONTRACT_NAME} contract via Etherscan API.`) + .setAction(async ({ provider, verify }, localBRE) => { + await localBRE.run('set-DRE'); + + if (!localBRE.network.config.chainId) { + throw new Error('INVALID_CHAIN_ID'); + } + + console.log(`\n- ${CONTRACT_NAME} deployment`); + const adapter = await new ParaSwapLiquiditySwapAdapterFactory( + await getFirstSigner() + ).deploy(provider); + await adapter.deployTransaction.wait(); + console.log(`${CONTRACT_NAME}.address`, adapter.address); + await verifyContract(adapter.address, [provider]); + + console.log(`\tFinished ${CONTRACT_NAME} deployment`); + }); From fe05ceccd63e4bfe1e1f59cae9fa7af64ffc873b Mon Sep 17 00:00:00 2001 From: Jason Raymond Bell Date: Thu, 20 May 2021 13:40:55 +0100 Subject: [PATCH 04/15] Use safeTransfer for rescueTokens Fixes MixBytes Warning 3. --- contracts/adapters/BaseParaSwapAdapter.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/adapters/BaseParaSwapAdapter.sol b/contracts/adapters/BaseParaSwapAdapter.sol index 72ce55fd..0b9488aa 100644 --- a/contracts/adapters/BaseParaSwapAdapter.sol +++ b/contracts/adapters/BaseParaSwapAdapter.sol @@ -119,6 +119,6 @@ abstract contract BaseParaSwapAdapter is FlashLoanReceiverBase, Ownable { * - Only callable by the owner */ function rescueTokens(IERC20 token) external onlyOwner { - token.transfer(owner(), token.balanceOf(address(this))); + token.safeTransfer(owner(), token.balanceOf(address(this))); } } From b13a01d8b897ec5ac8b2c80b18ecff40c2adaf96 Mon Sep 17 00:00:00 2001 From: Jason Raymond Bell Date: Thu, 20 May 2021 13:45:50 +0100 Subject: [PATCH 05/15] Rename _pullAToken function Fixes MixBytes Comment 4. --- contracts/adapters/BaseParaSwapAdapter.sol | 2 +- contracts/adapters/ParaSwapLiquiditySwapAdapter.sol | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/adapters/BaseParaSwapAdapter.sol b/contracts/adapters/BaseParaSwapAdapter.sol index 0b9488aa..70947d92 100644 --- a/contracts/adapters/BaseParaSwapAdapter.sol +++ b/contracts/adapters/BaseParaSwapAdapter.sol @@ -76,7 +76,7 @@ abstract contract BaseParaSwapAdapter is FlashLoanReceiverBase, Ownable { * @param amount of tokens to be transferred to the contract * @param permitSignature struct containing the permit signature */ - function _pullAToken( + function _pullATokenAndWithdraw( address reserve, address reserveAToken, address user, diff --git a/contracts/adapters/ParaSwapLiquiditySwapAdapter.sol b/contracts/adapters/ParaSwapLiquiditySwapAdapter.sol index 4e6ef962..9053ad50 100644 --- a/contracts/adapters/ParaSwapLiquiditySwapAdapter.sol +++ b/contracts/adapters/ParaSwapLiquiditySwapAdapter.sol @@ -102,7 +102,7 @@ contract ParaSwapLiquiditySwapAdapter is BaseParaSwapSellAdapter { amountToSwap = balance; } - _pullAToken( + _pullATokenAndWithdraw( assetToSwapFrom, aToken, msg.sender, @@ -176,7 +176,7 @@ contract ParaSwapLiquiditySwapAdapter is BaseParaSwapSellAdapter { IERC20(assetToSwapTo).safeApprove(address(LENDING_POOL), amountReceived); LENDING_POOL.deposit(assetToSwapTo, amountReceived, initiator, 0); - _pullAToken( + _pullATokenAndWithdraw( assetToSwapFrom, aToken, initiator, From 11d0367d3c3c8a16cffc9d7c0d48c9462e0b1a00 Mon Sep 17 00:00:00 2001 From: Jason Raymond Bell Date: Thu, 20 May 2021 14:00:34 +0100 Subject: [PATCH 06/15] Explain code to overwrite fromAmount in comment Fixes MixBytes Comment 1. --- contracts/adapters/BaseParaSwapSellAdapter.sol | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/contracts/adapters/BaseParaSwapSellAdapter.sol b/contracts/adapters/BaseParaSwapSellAdapter.sol index ec50f4b7..0e15f0e1 100644 --- a/contracts/adapters/BaseParaSwapSellAdapter.sol +++ b/contracts/adapters/BaseParaSwapSellAdapter.sol @@ -66,9 +66,14 @@ abstract contract BaseParaSwapSellAdapter is BaseParaSwapAdapter { IERC20(assetToSwapFrom).safeApprove(tokenTransferProxy, amountToSwap); if (fromAmountOffset != 0) { + // Ensure 256 bit (32 bytes) fromAmount value is within bounds of the + // calldata, not overlapping with the first 4 bytes (function selector). require(fromAmountOffset >= 4 && fromAmountOffset <= swapCalldata.length.sub(32), 'FROM_AMOUNT_OFFSET_OUT_OF_RANGE'); + // Overwrite the fromAmount with the correct amount for the swap. + // In memory, swapCalldata consists of a 256 bit length field, followed by + // the actual bytes data, that is why 32 is added to the byte offset. assembly { mstore(add(swapCalldata, add(fromAmountOffset, 32)), amountToSwap) } From d26b1beb68e086e77ffda7aa429c05ee085e24d5 Mon Sep 17 00:00:00 2001 From: Jason Raymond Bell Date: Thu, 20 May 2021 14:14:51 +0100 Subject: [PATCH 07/15] Fix _getDecimals function Added a limit on number of decimals and changed return type to uint8. Fixes MixBytes Warning 5 and ABDK CVF-30. --- contracts/adapters/BaseParaSwapAdapter.sol | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/contracts/adapters/BaseParaSwapAdapter.sol b/contracts/adapters/BaseParaSwapAdapter.sol index 70947d92..2ef5314a 100644 --- a/contracts/adapters/BaseParaSwapAdapter.sol +++ b/contracts/adapters/BaseParaSwapAdapter.sol @@ -56,8 +56,11 @@ abstract contract BaseParaSwapAdapter is FlashLoanReceiverBase, Ownable { * @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(); + function _getDecimals(address asset) internal view returns (uint8) { + uint8 decimals = IERC20Detailed(asset).decimals(); + // Ensure 10**decimals won't overflow a uint256 + require(decimals <= 77, 'TOO_MANY_DECIMALS_ON_TOKEN'); + return decimals; } /** From 5b45be6a44181db110035d8c96bda743dc338be2 Mon Sep 17 00:00:00 2001 From: Jason Raymond Bell Date: Thu, 20 May 2021 14:38:24 +0100 Subject: [PATCH 08/15] Don't ignore return value of withdraw Fixes MixBytes Warning 4. --- contracts/adapters/BaseParaSwapAdapter.sol | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/contracts/adapters/BaseParaSwapAdapter.sol b/contracts/adapters/BaseParaSwapAdapter.sol index 2ef5314a..849e1f1e 100644 --- a/contracts/adapters/BaseParaSwapAdapter.sol +++ b/contracts/adapters/BaseParaSwapAdapter.sol @@ -102,7 +102,10 @@ abstract contract BaseParaSwapAdapter is FlashLoanReceiverBase, Ownable { IERC20(reserveAToken).safeTransferFrom(user, address(this), amount); // withdraw reserve - LENDING_POOL.withdraw(reserve, amount, address(this)); + require( + LENDING_POOL.withdraw(reserve, amount, address(this)) == amount, + 'UNEXPECTED_AMOUNT_WITHDRAWN' + ); } /** From afeb5fb996d693e69b8d3da1b74950c9aeb66797 Mon Sep 17 00:00:00 2001 From: Jason Raymond Bell Date: Thu, 20 May 2021 14:47:10 +0100 Subject: [PATCH 09/15] Remove _usePermit function Just use a simple check if deadline is set or not. Fixes ABDK CVF-34 and CVF-35. --- contracts/adapters/BaseParaSwapAdapter.sol | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/contracts/adapters/BaseParaSwapAdapter.sol b/contracts/adapters/BaseParaSwapAdapter.sol index 849e1f1e..d06e9ec5 100644 --- a/contracts/adapters/BaseParaSwapAdapter.sol +++ b/contracts/adapters/BaseParaSwapAdapter.sol @@ -86,7 +86,8 @@ abstract contract BaseParaSwapAdapter is FlashLoanReceiverBase, Ownable { uint256 amount, PermitSignature memory permitSignature ) internal { - if (_usePermit(permitSignature)) { + // If deadline is set to zero, assume there is no signature for permit + if (permitSignature.deadline != 0) { IERC20WithPermit(reserveAToken).permit( user, address(this), @@ -108,17 +109,6 @@ abstract contract BaseParaSwapAdapter is FlashLoanReceiverBase, Ownable { ); } - /** - * @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 From 38bec942da636c2dc6120dfa3d9290bad1c4e73f Mon Sep 17 00:00:00 2001 From: Jason Raymond Bell Date: Thu, 20 May 2021 15:00:22 +0100 Subject: [PATCH 10/15] Check lengths of all the arrays Fixes ABDK CVF-5. --- contracts/adapters/ParaSwapLiquiditySwapAdapter.sol | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/contracts/adapters/ParaSwapLiquiditySwapAdapter.sol b/contracts/adapters/ParaSwapLiquiditySwapAdapter.sol index 9053ad50..edc7f745 100644 --- a/contracts/adapters/ParaSwapLiquiditySwapAdapter.sol +++ b/contracts/adapters/ParaSwapLiquiditySwapAdapter.sol @@ -40,7 +40,10 @@ contract ParaSwapLiquiditySwapAdapter is BaseParaSwapSellAdapter { 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'); + require( + assets.length == 1 && amounts.length == 1 && premiums.length == 1, + 'FLASHLOAN_MULTIPLE_ASSETS_NOT_SUPPORTED' + ); uint256 flashLoanAmount = amounts[0]; uint256 premium = premiums[0]; From bf7b19a1815e434347e8d8ec4a1215069943437c Mon Sep 17 00:00:00 2001 From: Jason Raymond Bell Date: Thu, 20 May 2021 15:41:59 +0100 Subject: [PATCH 11/15] Add comments to empty constructor bodies Fixes ABDK CVF-4 and CVF-15. --- contracts/adapters/BaseParaSwapSellAdapter.sol | 1 + contracts/adapters/ParaSwapLiquiditySwapAdapter.sol | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/contracts/adapters/BaseParaSwapSellAdapter.sol b/contracts/adapters/BaseParaSwapSellAdapter.sol index 0e15f0e1..c433f3f7 100644 --- a/contracts/adapters/BaseParaSwapSellAdapter.sol +++ b/contracts/adapters/BaseParaSwapSellAdapter.sol @@ -19,6 +19,7 @@ abstract contract BaseParaSwapSellAdapter is BaseParaSwapAdapter { constructor( ILendingPoolAddressesProvider addressesProvider ) public BaseParaSwapAdapter(addressesProvider) { + // This is only required to initialize BaseParaSwapAdapter } /** diff --git a/contracts/adapters/ParaSwapLiquiditySwapAdapter.sol b/contracts/adapters/ParaSwapLiquiditySwapAdapter.sol index edc7f745..a80c9df9 100644 --- a/contracts/adapters/ParaSwapLiquiditySwapAdapter.sol +++ b/contracts/adapters/ParaSwapLiquiditySwapAdapter.sol @@ -14,7 +14,9 @@ import {IERC20} from '../dependencies/openzeppelin/contracts/IERC20.sol'; contract ParaSwapLiquiditySwapAdapter is BaseParaSwapSellAdapter { constructor( ILendingPoolAddressesProvider addressesProvider - ) public BaseParaSwapSellAdapter(addressesProvider) {} + ) public BaseParaSwapSellAdapter(addressesProvider) { + // This is only required to initialize BaseParaSwapSellAdapter + } /** * @dev Swaps the received reserve amount from the flash loan into the asset specified in the params. From d8b8d50de82485144012e4dd34b849fb0cf4c686 Mon Sep 17 00:00:00 2001 From: Jason Raymond Bell Date: Thu, 20 May 2021 16:50:20 +0100 Subject: [PATCH 12/15] Use better types than address Fixes ABDK CVF-6, CVF-7, CVF-16, CVF-17, CVF-31 and CVF-32. --- contracts/adapters/BaseParaSwapAdapter.sol | 12 ++-- .../adapters/BaseParaSwapSellAdapter.sol | 35 ++++++----- .../adapters/ParaSwapLiquiditySwapAdapter.sol | 61 +++++++++++-------- 3 files changed, 63 insertions(+), 45 deletions(-) diff --git a/contracts/adapters/BaseParaSwapAdapter.sol b/contracts/adapters/BaseParaSwapAdapter.sol index d06e9ec5..9b84dd93 100644 --- a/contracts/adapters/BaseParaSwapAdapter.sol +++ b/contracts/adapters/BaseParaSwapAdapter.sol @@ -21,6 +21,8 @@ import {FlashLoanReceiverBase} from '../flashloan/base/FlashLoanReceiverBase.sol abstract contract BaseParaSwapAdapter is FlashLoanReceiverBase, Ownable { using SafeMath for uint256; using SafeERC20 for IERC20; + using SafeERC20 for IERC20Detailed; + using SafeERC20 for IERC20WithPermit; struct PermitSignature { uint256 amount; @@ -56,8 +58,8 @@ abstract contract BaseParaSwapAdapter is FlashLoanReceiverBase, Ownable { * @dev Get the decimals of an asset * @return number of decimals of the asset */ - function _getDecimals(address asset) internal view returns (uint8) { - uint8 decimals = IERC20Detailed(asset).decimals(); + function _getDecimals(IERC20Detailed asset) internal view returns (uint8) { + uint8 decimals = asset.decimals(); // Ensure 10**decimals won't overflow a uint256 require(decimals <= 77, 'TOO_MANY_DECIMALS_ON_TOKEN'); return decimals; @@ -81,14 +83,14 @@ abstract contract BaseParaSwapAdapter is FlashLoanReceiverBase, Ownable { */ function _pullATokenAndWithdraw( address reserve, - address reserveAToken, + IERC20WithPermit reserveAToken, address user, uint256 amount, PermitSignature memory permitSignature ) internal { // If deadline is set to zero, assume there is no signature for permit if (permitSignature.deadline != 0) { - IERC20WithPermit(reserveAToken).permit( + reserveAToken.permit( user, address(this), permitSignature.amount, @@ -100,7 +102,7 @@ abstract contract BaseParaSwapAdapter is FlashLoanReceiverBase, Ownable { } // transfer from user to adapter - IERC20(reserveAToken).safeTransferFrom(user, address(this), amount); + reserveAToken.safeTransferFrom(user, address(this), amount); // withdraw reserve require( diff --git a/contracts/adapters/BaseParaSwapSellAdapter.sol b/contracts/adapters/BaseParaSwapSellAdapter.sol index c433f3f7..5b750287 100644 --- a/contracts/adapters/BaseParaSwapSellAdapter.sol +++ b/contracts/adapters/BaseParaSwapSellAdapter.sol @@ -6,7 +6,7 @@ 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'; +import {IERC20Detailed} from '../dependencies/openzeppelin/contracts/IERC20Detailed.sol'; /** * @title BaseParaSwapSellAdapter @@ -36,9 +36,9 @@ abstract contract BaseParaSwapSellAdapter is BaseParaSwapAdapter { function _sellOnParaSwap( uint256 fromAmountOffset, bytes memory swapCalldata, - address augustus, - address assetToSwapFrom, - address assetToSwapTo, + IParaSwapAugustus augustus, + IERC20Detailed assetToSwapFrom, + IERC20Detailed assetToSwapTo, uint256 amountToSwap, uint256 minAmountToReceive ) internal returns (uint256 amountReceived) { @@ -46,8 +46,8 @@ abstract contract BaseParaSwapSellAdapter is BaseParaSwapAdapter { uint256 fromAssetDecimals = _getDecimals(assetToSwapFrom); uint256 toAssetDecimals = _getDecimals(assetToSwapTo); - uint256 fromAssetPrice = _getPrice(assetToSwapFrom); - uint256 toAssetPrice = _getPrice(assetToSwapTo); + uint256 fromAssetPrice = _getPrice(address(assetToSwapFrom)); + uint256 toAssetPrice = _getPrice(address(assetToSwapTo)); uint256 expectedMinAmountOut = amountToSwap @@ -58,13 +58,13 @@ abstract contract BaseParaSwapSellAdapter is BaseParaSwapAdapter { require(expectedMinAmountOut <= minAmountToReceive, 'MIN_AMOUNT_EXCEEDS_MAX_SLIPPAGE'); } - uint256 balanceBeforeAssetFrom = IERC20(assetToSwapFrom).balanceOf(address(this)); + uint256 balanceBeforeAssetFrom = assetToSwapFrom.balanceOf(address(this)); require(balanceBeforeAssetFrom >= amountToSwap, 'INSUFFICIENT_BALANCE_BEFORE_SWAP'); - uint256 balanceBeforeAssetTo = IERC20(assetToSwapTo).balanceOf(address(this)); + uint256 balanceBeforeAssetTo = assetToSwapTo.balanceOf(address(this)); - address tokenTransferProxy = IParaSwapAugustus(augustus).getTokenTransferProxy(); - IERC20(assetToSwapFrom).safeApprove(tokenTransferProxy, 0); - IERC20(assetToSwapFrom).safeApprove(tokenTransferProxy, amountToSwap); + address tokenTransferProxy = augustus.getTokenTransferProxy(); + assetToSwapFrom.safeApprove(tokenTransferProxy, 0); + assetToSwapFrom.safeApprove(tokenTransferProxy, amountToSwap); if (fromAmountOffset != 0) { // Ensure 256 bit (32 bytes) fromAmount value is within bounds of the @@ -79,7 +79,7 @@ abstract contract BaseParaSwapSellAdapter is BaseParaSwapAdapter { mstore(add(swapCalldata, add(fromAmountOffset, 32)), amountToSwap) } } - (bool success,) = augustus.call(swapCalldata); + (bool success,) = address(augustus).call(swapCalldata); if (!success) { // Copy revert reason from call assembly { @@ -87,10 +87,15 @@ abstract contract BaseParaSwapSellAdapter is BaseParaSwapAdapter { revert(0, returndatasize()) } } - require(IERC20(assetToSwapFrom).balanceOf(address(this)) == balanceBeforeAssetFrom - amountToSwap, 'WRONG_BALANCE_AFTER_SWAP'); - amountReceived = IERC20(assetToSwapTo).balanceOf(address(this)).sub(balanceBeforeAssetTo); + require(assetToSwapFrom.balanceOf(address(this)) == balanceBeforeAssetFrom - amountToSwap, 'WRONG_BALANCE_AFTER_SWAP'); + amountReceived = assetToSwapTo.balanceOf(address(this)).sub(balanceBeforeAssetTo); require(amountReceived >= minAmountToReceive, 'INSUFFICIENT_AMOUNT_RECEIVED'); - emit Swapped(assetToSwapFrom, assetToSwapTo, amountToSwap, amountReceived); + emit Swapped( + address(assetToSwapFrom), + address(assetToSwapTo), + amountToSwap, + amountReceived + ); } } diff --git a/contracts/adapters/ParaSwapLiquiditySwapAdapter.sol b/contracts/adapters/ParaSwapLiquiditySwapAdapter.sol index a80c9df9..338fa09e 100644 --- a/contracts/adapters/ParaSwapLiquiditySwapAdapter.sol +++ b/contracts/adapters/ParaSwapLiquiditySwapAdapter.sol @@ -4,7 +4,9 @@ pragma experimental ABIEncoderV2; import {BaseParaSwapSellAdapter} from './BaseParaSwapSellAdapter.sol'; import {ILendingPoolAddressesProvider} from '../interfaces/ILendingPoolAddressesProvider.sol'; -import {IERC20} from '../dependencies/openzeppelin/contracts/IERC20.sol'; +import {IERC20Detailed} from '../dependencies/openzeppelin/contracts/IERC20Detailed.sol'; +import {IERC20WithPermit} from '../interfaces/IERC20WithPermit.sol'; +import {IParaSwapAugustus} from '../interfaces/IParaSwapAugustus.sol'; /** * @title ParaSwapLiquiditySwapAdapter @@ -50,15 +52,22 @@ contract ParaSwapLiquiditySwapAdapter is BaseParaSwapSellAdapter { uint256 flashLoanAmount = amounts[0]; uint256 premium = premiums[0]; address initiatorLocal = initiator; - address assetToSwapFrom = assets[0]; + IERC20Detailed assetToSwapFrom = IERC20Detailed(assets[0]); ( - address assetToSwapTo, + IERC20Detailed assetToSwapTo, uint256 minAmountToReceive, uint256 swapAllBalanceOffset, bytes memory swapCalldata, - address augustus, + IParaSwapAugustus augustus, PermitSignature memory permitParams - ) = abi.decode(params, (address, uint256, uint256, bytes, address, PermitSignature)); + ) = abi.decode(params, ( + IERC20Detailed, + uint256, + uint256, + bytes, + IParaSwapAugustus, + PermitSignature + )); _swapLiquidity( swapAllBalanceOffset, @@ -90,25 +99,26 @@ contract ParaSwapLiquiditySwapAdapter is BaseParaSwapSellAdapter { * @param permitParams Struct containing the permit signatures, set to all zeroes if not used */ function swapAndDeposit( - address assetToSwapFrom, - address assetToSwapTo, + IERC20Detailed assetToSwapFrom, + IERC20Detailed assetToSwapTo, uint256 amountToSwap, uint256 minAmountToReceive, uint256 swapAllBalanceOffset, bytes calldata swapCalldata, - address augustus, + IParaSwapAugustus augustus, PermitSignature calldata permitParams ) external { - address aToken = _getReserveData(assetToSwapFrom).aTokenAddress; + IERC20WithPermit aToken = + IERC20WithPermit(_getReserveData(address(assetToSwapFrom)).aTokenAddress); if (swapAllBalanceOffset != 0) { - uint256 balance = IERC20(aToken).balanceOf(msg.sender); + uint256 balance = aToken.balanceOf(msg.sender); require(balance <= amountToSwap, 'INSUFFICIENT_AMOUNT_TO_SWAP'); amountToSwap = balance; } _pullATokenAndWithdraw( - assetToSwapFrom, + address(assetToSwapFrom), aToken, msg.sender, amountToSwap, @@ -125,9 +135,9 @@ contract ParaSwapLiquiditySwapAdapter is BaseParaSwapSellAdapter { minAmountToReceive ); - IERC20(assetToSwapTo).safeApprove(address(LENDING_POOL), 0); - IERC20(assetToSwapTo).safeApprove(address(LENDING_POOL), amountReceived); - LENDING_POOL.deposit(assetToSwapTo, amountReceived, msg.sender, 0); + assetToSwapTo.safeApprove(address(LENDING_POOL), 0); + assetToSwapTo.safeApprove(address(LENDING_POOL), amountReceived); + LENDING_POOL.deposit(address(assetToSwapTo), amountReceived, msg.sender, 0); } /** @@ -146,19 +156,20 @@ contract ParaSwapLiquiditySwapAdapter is BaseParaSwapSellAdapter { function _swapLiquidity ( uint256 swapAllBalanceOffset, bytes memory swapCalldata, - address augustus, + IParaSwapAugustus augustus, PermitSignature memory permitParams, uint256 flashLoanAmount, uint256 premium, address initiator, - address assetToSwapFrom, - address assetToSwapTo, + IERC20Detailed assetToSwapFrom, + IERC20Detailed assetToSwapTo, uint256 minAmountToReceive ) internal { - address aToken = _getReserveData(assetToSwapFrom).aTokenAddress; + IERC20WithPermit aToken = + IERC20WithPermit(_getReserveData(address(assetToSwapFrom)).aTokenAddress); uint256 amountToSwap = flashLoanAmount; - uint256 balance = IERC20(aToken).balanceOf(initiator); + uint256 balance = aToken.balanceOf(initiator); if (swapAllBalanceOffset != 0) { uint256 balanceToSwap = balance.sub(premium); require(balanceToSwap <= amountToSwap, 'INSUFFICIENT_AMOUNT_TO_SWAP'); @@ -177,12 +188,12 @@ contract ParaSwapLiquiditySwapAdapter is BaseParaSwapSellAdapter { minAmountToReceive ); - IERC20(assetToSwapTo).safeApprove(address(LENDING_POOL), 0); - IERC20(assetToSwapTo).safeApprove(address(LENDING_POOL), amountReceived); - LENDING_POOL.deposit(assetToSwapTo, amountReceived, initiator, 0); + assetToSwapTo.safeApprove(address(LENDING_POOL), 0); + assetToSwapTo.safeApprove(address(LENDING_POOL), amountReceived); + LENDING_POOL.deposit(address(assetToSwapTo), amountReceived, initiator, 0); _pullATokenAndWithdraw( - assetToSwapFrom, + address(assetToSwapFrom), aToken, initiator, amountToSwap.add(premium), @@ -190,7 +201,7 @@ contract ParaSwapLiquiditySwapAdapter is BaseParaSwapSellAdapter { ); // Repay flash loan - IERC20(assetToSwapFrom).safeApprove(address(LENDING_POOL), 0); - IERC20(assetToSwapFrom).safeApprove(address(LENDING_POOL), flashLoanAmount.add(premium)); + assetToSwapFrom.safeApprove(address(LENDING_POOL), 0); + assetToSwapFrom.safeApprove(address(LENDING_POOL), flashLoanAmount.add(premium)); } } From 9d1cb50d769adb2b584f18d1c82aa2bf7bf76668 Mon Sep 17 00:00:00 2001 From: Jason Raymond Bell Date: Thu, 20 May 2021 17:36:41 +0100 Subject: [PATCH 13/15] Add reentrancy guard to adapter Fixes MixBytes Warning 2. --- .../adapters/ParaSwapLiquiditySwapAdapter.sol | 7 ++- .../contracts/ReentrancyGuard.sol | 62 +++++++++++++++++++ 2 files changed, 66 insertions(+), 3 deletions(-) create mode 100644 contracts/dependencies/openzeppelin/contracts/ReentrancyGuard.sol diff --git a/contracts/adapters/ParaSwapLiquiditySwapAdapter.sol b/contracts/adapters/ParaSwapLiquiditySwapAdapter.sol index 338fa09e..c5ff6305 100644 --- a/contracts/adapters/ParaSwapLiquiditySwapAdapter.sol +++ b/contracts/adapters/ParaSwapLiquiditySwapAdapter.sol @@ -7,13 +7,14 @@ import {ILendingPoolAddressesProvider} from '../interfaces/ILendingPoolAddresses import {IERC20Detailed} from '../dependencies/openzeppelin/contracts/IERC20Detailed.sol'; import {IERC20WithPermit} from '../interfaces/IERC20WithPermit.sol'; import {IParaSwapAugustus} from '../interfaces/IParaSwapAugustus.sol'; +import {ReentrancyGuard} from '../dependencies/openzeppelin/contracts/ReentrancyGuard.sol'; /** * @title ParaSwapLiquiditySwapAdapter * @notice Adapter to swap liquidity using ParaSwap. * @author Jason Raymond Bell */ -contract ParaSwapLiquiditySwapAdapter is BaseParaSwapSellAdapter { +contract ParaSwapLiquiditySwapAdapter is BaseParaSwapSellAdapter, ReentrancyGuard { constructor( ILendingPoolAddressesProvider addressesProvider ) public BaseParaSwapSellAdapter(addressesProvider) { @@ -42,7 +43,7 @@ contract ParaSwapLiquiditySwapAdapter is BaseParaSwapSellAdapter { uint256[] calldata premiums, address initiator, bytes calldata params - ) external override returns (bool) { + ) external override nonReentrant returns (bool) { require(msg.sender == address(LENDING_POOL), 'CALLER_MUST_BE_LENDING_POOL'); require( assets.length == 1 && amounts.length == 1 && premiums.length == 1, @@ -107,7 +108,7 @@ contract ParaSwapLiquiditySwapAdapter is BaseParaSwapSellAdapter { bytes calldata swapCalldata, IParaSwapAugustus augustus, PermitSignature calldata permitParams - ) external { + ) external nonReentrant { IERC20WithPermit aToken = IERC20WithPermit(_getReserveData(address(assetToSwapFrom)).aTokenAddress); diff --git a/contracts/dependencies/openzeppelin/contracts/ReentrancyGuard.sol b/contracts/dependencies/openzeppelin/contracts/ReentrancyGuard.sol new file mode 100644 index 00000000..24c90c32 --- /dev/null +++ b/contracts/dependencies/openzeppelin/contracts/ReentrancyGuard.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.6.0 <0.8.0; + +/** + * @dev Contract module that helps prevent reentrant calls to a function. + * + * Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier + * available, which can be applied to functions to make sure there are no nested + * (reentrant) calls to them. + * + * Note that because there is a single `nonReentrant` guard, functions marked as + * `nonReentrant` may not call one another. This can be worked around by making + * those functions `private`, and then adding `external` `nonReentrant` entry + * points to them. + * + * TIP: If you would like to learn more about reentrancy and alternative ways + * to protect against it, check out our blog post + * https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul]. + */ +abstract contract ReentrancyGuard { + // Booleans are more expensive than uint256 or any type that takes up a full + // word because each write operation emits an extra SLOAD to first read the + // slot's contents, replace the bits taken up by the boolean, and then write + // back. This is the compiler's defense against contract upgrades and + // pointer aliasing, and it cannot be disabled. + + // The values being non-zero value makes deployment a bit more expensive, + // but in exchange the refund on every call to nonReentrant will be lower in + // amount. Since refunds are capped to a percentage of the total + // transaction's gas, it is best to keep them low in cases like this one, to + // increase the likelihood of the full refund coming into effect. + uint256 private constant _NOT_ENTERED = 1; + uint256 private constant _ENTERED = 2; + + uint256 private _status; + + constructor () internal { + _status = _NOT_ENTERED; + } + + /** + * @dev Prevents a contract from calling itself, directly or indirectly. + * Calling a `nonReentrant` function from another `nonReentrant` + * function is not supported. It is possible to prevent this from happening + * by making the `nonReentrant` function external, and make it call a + * `private` function that does the actual work. + */ + modifier nonReentrant() { + // On the first call to nonReentrant, _notEntered will be true + require(_status != _ENTERED, "ReentrancyGuard: reentrant call"); + + // Any calls to nonReentrant after this point will fail + _status = _ENTERED; + + _; + + // By storing the original value once again, a refund is triggered (see + // https://eips.ethereum.org/EIPS/eip-2200) + _status = _NOT_ENTERED; + } +} From 4fe36c8fa4c470e2553a4e6632a7d4cc544e5e4c Mon Sep 17 00:00:00 2001 From: Jason Raymond Bell Date: Thu, 20 May 2021 23:25:51 +0100 Subject: [PATCH 14/15] Introduce registry of valid Augustus addresses Set in constructor of BaseParaSwapSellAdapter and validates before swap. Created mock registry that only validates one address. Changed the test fixtures to accomodate registry and added two new tests. Updated deployment script. Registry address left as a placeholder in package.json since not known yet. Fixes MixBytes Warning 1. --- .../adapters/BaseParaSwapSellAdapter.sol | 12 +++- .../adapters/ParaSwapLiquiditySwapAdapter.sol | 6 +- .../interfaces/IParaSwapAugustusRegistry.sol | 7 ++ contracts/mocks/swap/MockParaSwapAugustus.sol | 8 +-- .../swap/MockParaSwapAugustusRegistry.sol | 17 +++++ helpers/contracts-deployments.ts | 14 +++- helpers/contracts-getters.ts | 9 +++ helpers/types.ts | 1 + package.json | 2 +- .../deploy-ParaSwapLiquiditySwapAdapter.ts | 7 +- test-suites/test-aave/__setup.spec.ts | 7 +- .../paraswapAdapters.liquiditySwap.spec.ts | 65 ++++++++++++++++++- 12 files changed, 138 insertions(+), 17 deletions(-) create mode 100644 contracts/interfaces/IParaSwapAugustusRegistry.sol create mode 100644 contracts/mocks/swap/MockParaSwapAugustusRegistry.sol diff --git a/contracts/adapters/BaseParaSwapSellAdapter.sol b/contracts/adapters/BaseParaSwapSellAdapter.sol index 5b750287..93d3cc5b 100644 --- a/contracts/adapters/BaseParaSwapSellAdapter.sol +++ b/contracts/adapters/BaseParaSwapSellAdapter.sol @@ -5,6 +5,7 @@ pragma experimental ABIEncoderV2; import {BaseParaSwapAdapter} from './BaseParaSwapAdapter.sol'; import {PercentageMath} from '../protocol/libraries/math/PercentageMath.sol'; import {IParaSwapAugustus} from '../interfaces/IParaSwapAugustus.sol'; +import {IParaSwapAugustusRegistry} from '../interfaces/IParaSwapAugustusRegistry.sol'; import {ILendingPoolAddressesProvider} from '../interfaces/ILendingPoolAddressesProvider.sol'; import {IERC20Detailed} from '../dependencies/openzeppelin/contracts/IERC20Detailed.sol'; @@ -16,10 +17,15 @@ import {IERC20Detailed} from '../dependencies/openzeppelin/contracts/IERC20Detai abstract contract BaseParaSwapSellAdapter is BaseParaSwapAdapter { using PercentageMath for uint256; + IParaSwapAugustusRegistry public immutable AUGUSTUS_REGISTRY; + constructor( - ILendingPoolAddressesProvider addressesProvider + ILendingPoolAddressesProvider addressesProvider, + IParaSwapAugustusRegistry augustusRegistry ) public BaseParaSwapAdapter(addressesProvider) { - // This is only required to initialize BaseParaSwapAdapter + // Do something on Augustus registry to check the right contract was passed + require(!augustusRegistry.isValidAugustus(address(0))); + AUGUSTUS_REGISTRY = augustusRegistry; } /** @@ -42,6 +48,8 @@ abstract contract BaseParaSwapSellAdapter is BaseParaSwapAdapter { uint256 amountToSwap, uint256 minAmountToReceive ) internal returns (uint256 amountReceived) { + require(AUGUSTUS_REGISTRY.isValidAugustus(address(augustus)), 'INVALID_AUGUSTUS'); + { uint256 fromAssetDecimals = _getDecimals(assetToSwapFrom); uint256 toAssetDecimals = _getDecimals(assetToSwapTo); diff --git a/contracts/adapters/ParaSwapLiquiditySwapAdapter.sol b/contracts/adapters/ParaSwapLiquiditySwapAdapter.sol index c5ff6305..7cc1105d 100644 --- a/contracts/adapters/ParaSwapLiquiditySwapAdapter.sol +++ b/contracts/adapters/ParaSwapLiquiditySwapAdapter.sol @@ -4,6 +4,7 @@ pragma experimental ABIEncoderV2; import {BaseParaSwapSellAdapter} from './BaseParaSwapSellAdapter.sol'; import {ILendingPoolAddressesProvider} from '../interfaces/ILendingPoolAddressesProvider.sol'; +import {IParaSwapAugustusRegistry} from '../interfaces/IParaSwapAugustusRegistry.sol'; import {IERC20Detailed} from '../dependencies/openzeppelin/contracts/IERC20Detailed.sol'; import {IERC20WithPermit} from '../interfaces/IERC20WithPermit.sol'; import {IParaSwapAugustus} from '../interfaces/IParaSwapAugustus.sol'; @@ -16,8 +17,9 @@ import {ReentrancyGuard} from '../dependencies/openzeppelin/contracts/Reentrancy */ contract ParaSwapLiquiditySwapAdapter is BaseParaSwapSellAdapter, ReentrancyGuard { constructor( - ILendingPoolAddressesProvider addressesProvider - ) public BaseParaSwapSellAdapter(addressesProvider) { + ILendingPoolAddressesProvider addressesProvider, + IParaSwapAugustusRegistry augustusRegistry + ) public BaseParaSwapSellAdapter(addressesProvider, augustusRegistry) { // This is only required to initialize BaseParaSwapSellAdapter } diff --git a/contracts/interfaces/IParaSwapAugustusRegistry.sol b/contracts/interfaces/IParaSwapAugustusRegistry.sol new file mode 100644 index 00000000..f10e13dd --- /dev/null +++ b/contracts/interfaces/IParaSwapAugustusRegistry.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity 0.6.12; +pragma experimental ABIEncoderV2; + +interface IParaSwapAugustusRegistry { + function isValidAugustus(address augustus) external view returns (bool); +} diff --git a/contracts/mocks/swap/MockParaSwapAugustus.sol b/contracts/mocks/swap/MockParaSwapAugustus.sol index 8a6874c6..1cf32171 100644 --- a/contracts/mocks/swap/MockParaSwapAugustus.sol +++ b/contracts/mocks/swap/MockParaSwapAugustus.sol @@ -8,7 +8,7 @@ import {IERC20} from '../../dependencies/openzeppelin/contracts/IERC20.sol'; import {MintableERC20} from '../tokens/MintableERC20.sol'; contract MockParaSwapAugustus is IParaSwapAugustus { - MockParaSwapTokenTransferProxy _tokenTransferProxy; + MockParaSwapTokenTransferProxy immutable TOKEN_TRANSFER_PROXY; bool _expectingSwap; address _expectedFromToken; address _expectedToToken; @@ -17,11 +17,11 @@ contract MockParaSwapAugustus is IParaSwapAugustus { uint256 _receivedAmount; constructor() public { - _tokenTransferProxy = new MockParaSwapTokenTransferProxy(); + TOKEN_TRANSFER_PROXY = new MockParaSwapTokenTransferProxy(); } function getTokenTransferProxy() external view override returns (address) { - return address(_tokenTransferProxy); + return address(TOKEN_TRANSFER_PROXY); } function expectSwap( @@ -50,7 +50,7 @@ contract MockParaSwapAugustus is IParaSwapAugustus { 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); + TOKEN_TRANSFER_PROXY.transferFrom(fromToken, msg.sender, address(this), fromAmount); MintableERC20(toToken).mint(_receivedAmount); IERC20(toToken).transfer(msg.sender, _receivedAmount); _expectingSwap = false; diff --git a/contracts/mocks/swap/MockParaSwapAugustusRegistry.sol b/contracts/mocks/swap/MockParaSwapAugustusRegistry.sol new file mode 100644 index 00000000..c8998ec1 --- /dev/null +++ b/contracts/mocks/swap/MockParaSwapAugustusRegistry.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity 0.6.12; +pragma experimental ABIEncoderV2; + +import {IParaSwapAugustusRegistry} from '../../interfaces/IParaSwapAugustusRegistry.sol'; + +contract MockParaSwapAugustusRegistry is IParaSwapAugustusRegistry { + address immutable AUGUSTUS; + + constructor(address augustus) public { + AUGUSTUS = augustus; + } + + function isValidAugustus(address augustus) external view override returns (bool) { + return augustus == AUGUSTUS; + } +} diff --git a/helpers/contracts-deployments.ts b/helpers/contracts-deployments.ts index 9f5e4eee..97331547 100644 --- a/helpers/contracts-deployments.ts +++ b/helpers/contracts-deployments.ts @@ -36,6 +36,7 @@ import { MockATokenFactory, MockFlashLoanReceiverFactory, MockParaSwapAugustusFactory, + MockParaSwapAugustusRegistryFactory, MockStableDebtTokenFactory, MockVariableDebtTokenFactory, MockUniswapV2Router02Factory, @@ -620,9 +621,20 @@ export const deployMockParaSwapAugustus = async (verify?: boolean) => verify ); -export const deployParaSwapLiquiditySwapAdapter = async ( +export const deployMockParaSwapAugustusRegistry = async ( args: [tEthereumAddress], verify?: boolean +) => + withSaveAndVerify( + await new MockParaSwapAugustusRegistryFactory(await getFirstSigner()).deploy(...args), + eContractid.MockParaSwapAugustusRegistry, + args, + verify + ); + +export const deployParaSwapLiquiditySwapAdapter = async ( + args: [tEthereumAddress, tEthereumAddress], + verify?: boolean ) => withSaveAndVerify( await new ParaSwapLiquiditySwapAdapterFactory(await getFirstSigner()).deploy(...args), diff --git a/helpers/contracts-getters.ts b/helpers/contracts-getters.ts index d7281291..82b3df5b 100644 --- a/helpers/contracts-getters.ts +++ b/helpers/contracts-getters.ts @@ -19,6 +19,7 @@ import { MockVariableDebtTokenFactory, MockUniswapV2Router02Factory, MockParaSwapAugustusFactory, + MockParaSwapAugustusRegistryFactory, ParaSwapLiquiditySwapAdapterFactory, PriceOracleFactory, ReserveLogicFactory, @@ -374,6 +375,14 @@ export const getMockParaSwapAugustus = async (address?: tEthereumAddress) => await getFirstSigner() ); +export const getMockParaSwapAugustusRegistry = async (address?: tEthereumAddress) => + await MockParaSwapAugustusRegistryFactory.connect( + address || + (await getDb().get(`${eContractid.MockParaSwapAugustusRegistry}.${DRE.network.name}`).value()) + .address, + await getFirstSigner() + ); + export const getParaSwapLiquiditySwapAdapter = async (address?: tEthereumAddress) => await ParaSwapLiquiditySwapAdapterFactory.connect( address || diff --git a/helpers/types.ts b/helpers/types.ts index 38bcebd0..1feac1ed 100644 --- a/helpers/types.ts +++ b/helpers/types.ts @@ -88,6 +88,7 @@ export enum eContractid { UniswapRepayAdapter = 'UniswapRepayAdapter', FlashLiquidationAdapter = 'FlashLiquidationAdapter', MockParaSwapAugustus = 'MockParaSwapAugustus', + MockParaSwapAugustusRegistry = 'MockParaSwapAugustusRegistry', ParaSwapLiquiditySwapAdapter = 'ParaSwapLiquiditySwapAdapter', } diff --git a/package.json b/package.json index 1105b4b7..46c04270 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "dev:UniswapLiquiditySwapAdapter": "hardhat --network kovan deploy-UniswapLiquiditySwapAdapter --provider 0x88757f2f99175387aB4C6a4b3067c77A695b0349 --router 0xfcd87315f0e4067070ade8682fcdbc3006631441 --weth 0xd0a1e359811322d97991e03f863a0c30c2cf029c", "main:deployUniswapRepayAdapter": "hardhat --network main deploy-UniswapRepayAdapter --provider 0xB53C1a33016B2DC2fF3653530bfF1848a515c8c5 --router 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D --weth 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", "main:UniswapLiquiditySwapAdapter": "hardhat --network main deploy-UniswapLiquiditySwapAdapter --provider 0xB53C1a33016B2DC2fF3653530bfF1848a515c8c5 --router 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D --weth 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", - "main:ParaSwapLiquiditySwapAdapter": "hardhat --network main deploy-ParaSwapLiquiditySwapAdapter --provider 0xB53C1a33016B2DC2fF3653530bfF1848a515c8c5", + "main:ParaSwapLiquiditySwapAdapter": "hardhat --network main deploy-ParaSwapLiquiditySwapAdapter --provider 0xB53C1a33016B2DC2fF3653530bfF1848a515c8c5 --augustusRegistry 0x0000000000000000000000000000000000000000", "kovan:verify": "npm run hardhat:kovan verify:general -- --all --pool Aave", "ropsten:verify": "npm run hardhat:ropsten verify:general -- --all --pool Aave", "mainnet:verify": "npm run hardhat:main verify:general -- --all --pool Aave", diff --git a/tasks/deployments/deploy-ParaSwapLiquiditySwapAdapter.ts b/tasks/deployments/deploy-ParaSwapLiquiditySwapAdapter.ts index 1ee0720f..1322645b 100644 --- a/tasks/deployments/deploy-ParaSwapLiquiditySwapAdapter.ts +++ b/tasks/deployments/deploy-ParaSwapLiquiditySwapAdapter.ts @@ -8,8 +8,9 @@ const CONTRACT_NAME = 'ParaSwapLiquiditySwapAdapter'; task(`deploy-${CONTRACT_NAME}`, `Deploys the ${CONTRACT_NAME} contract`) .addParam('provider', 'Address of the LendingPoolAddressesProvider') + .addParam('augustusRegistry', 'Address of ParaSwap AugustusRegistry') .addFlag('verify', `Verify ${CONTRACT_NAME} contract via Etherscan API.`) - .setAction(async ({ provider, verify }, localBRE) => { + .setAction(async ({ provider, augustusRegistry, verify }, localBRE) => { await localBRE.run('set-DRE'); if (!localBRE.network.config.chainId) { @@ -19,10 +20,10 @@ task(`deploy-${CONTRACT_NAME}`, `Deploys the ${CONTRACT_NAME} contract`) console.log(`\n- ${CONTRACT_NAME} deployment`); const adapter = await new ParaSwapLiquiditySwapAdapterFactory( await getFirstSigner() - ).deploy(provider); + ).deploy(provider, augustusRegistry); await adapter.deployTransaction.wait(); console.log(`${CONTRACT_NAME}.address`, adapter.address); - await verifyContract(adapter.address, [provider]); + await verifyContract(adapter.address, [provider, augustusRegistry]); console.log(`\tFinished ${CONTRACT_NAME} deployment`); }); diff --git a/test-suites/test-aave/__setup.spec.ts b/test-suites/test-aave/__setup.spec.ts index 9e4422ca..7dd73c15 100644 --- a/test-suites/test-aave/__setup.spec.ts +++ b/test-suites/test-aave/__setup.spec.ts @@ -27,6 +27,7 @@ import { deployUniswapRepayAdapter, deployFlashLiquidationAdapter, deployMockParaSwapAugustus, + deployMockParaSwapAugustusRegistry, deployParaSwapLiquiditySwapAdapter, authorizeWETHGateway, } from '../../helpers/contracts-deployments'; @@ -286,9 +287,11 @@ const buildTestEnv = async (deployer: Signer, secondaryWallet: Signer) => { await deployUniswapRepayAdapter(adapterParams); await deployFlashLiquidationAdapter(adapterParams); - await deployMockParaSwapAugustus(); + const augustus = await deployMockParaSwapAugustus(); - await deployParaSwapLiquiditySwapAdapter([addressesProvider.address]); + const augustusRegistry = await deployMockParaSwapAugustusRegistry([augustus.address]); + + await deployParaSwapLiquiditySwapAdapter([addressesProvider.address, augustusRegistry.address]); await deployWalletBalancerProvider(); diff --git a/test-suites/test-aave/paraswapAdapters.liquiditySwap.spec.ts b/test-suites/test-aave/paraswapAdapters.liquiditySwap.spec.ts index 415bb474..1a4a424b 100644 --- a/test-suites/test-aave/paraswapAdapters.liquiditySwap.spec.ts +++ b/test-suites/test-aave/paraswapAdapters.liquiditySwap.spec.ts @@ -6,9 +6,13 @@ import { getSignatureFromTypedData, buildParaSwapLiquiditySwapParams, } from '../../helpers/contracts-helpers'; -import { getMockParaSwapAugustus } from '../../helpers/contracts-getters'; +import { + getMockParaSwapAugustus, + getMockParaSwapAugustusRegistry, +} from '../../helpers/contracts-getters'; import { deployParaSwapLiquiditySwapAdapter } from '../../helpers/contracts-deployments'; import { MockParaSwapAugustus } from '../../types/MockParaSwapAugustus'; +import { MockParaSwapAugustusRegistry } from '../../types/MockParaSwapAugustusRegistry'; import { Zero } from '@ethersproject/constants'; import BigNumber from 'bignumber.js'; import { DRE, evmRevert, evmSnapshot } from '../../helpers/misc-utils'; @@ -23,10 +27,12 @@ const { expect } = require('chai'); makeSuite('ParaSwap adapters', (testEnv: TestEnv) => { let mockAugustus: MockParaSwapAugustus; + let mockAugustusRegistry: MockParaSwapAugustusRegistry; let evmSnapshotId: string; before(async () => { mockAugustus = await getMockParaSwapAugustus(); + mockAugustusRegistry = await getMockParaSwapAugustusRegistry(); }); beforeEach(async () => { @@ -43,13 +49,25 @@ makeSuite('ParaSwap adapters', (testEnv: TestEnv) => { const { addressesProvider } = testEnv; await deployParaSwapLiquiditySwapAdapter([ addressesProvider.address, + mockAugustusRegistry.address, ]); }); it('should revert if not valid addresses provider', async () => { await expect( deployParaSwapLiquiditySwapAdapter([ - mockAugustus.address, + mockAugustus.address, // any invalid contract can be used here + mockAugustusRegistry.address, + ]) + ).to.be.reverted; + }); + + it('should revert if not valid augustus registry', async () => { + const { addressesProvider } = testEnv; + await expect( + deployParaSwapLiquiditySwapAdapter([ + addressesProvider.address, + mockAugustus.address, // any invalid contract can be used here ]) ).to.be.reverted; }); @@ -1637,6 +1655,49 @@ makeSuite('ParaSwap adapters', (testEnv: TestEnv) => { ).to.be.revertedWith('MIN_AMOUNT_EXCEEDS_MAX_SLIPPAGE'); }); + it('should revert if wrong address used for Augustus', async () => { + const { users, weth, oracle, dai, aWETH, paraswapLiquiditySwapAdapter } = testEnv; + const user = users[0].signer; + const userAddress = users[0].address; + + const amountWETHtoSwap = await convertToCurrencyDecimals(weth.address, '10'); + + const daiPrice = await oracle.getAssetPrice(dai.address); + const expectedDaiAmount = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) + ); + + await mockAugustus.expectSwap(weth.address, dai.address, amountWETHtoSwap, amountWETHtoSwap, expectedDaiAmount); + + // User will swap liquidity aEth to aDai + await aWETH.connect(user).approve(paraswapLiquiditySwapAdapter.address, amountWETHtoSwap); + + const mockAugustusCalldata = mockAugustus.interface.encodeFunctionData( + 'swap', + [weth.address, dai.address, amountWETHtoSwap, expectedDaiAmount] + ); + + await expect( + paraswapLiquiditySwapAdapter.connect(user).swapAndDeposit( + weth.address, + dai.address, + amountWETHtoSwap, + expectedDaiAmount, + 0, + mockAugustusCalldata, + oracle.address, // using arbitrary contract instead of mock Augustus + { + amount: 0, + deadline: 0, + v: 0, + r: '0x0000000000000000000000000000000000000000000000000000000000000000', + s: '0x0000000000000000000000000000000000000000000000000000000000000000', + } + ) + ).to.be.revertedWith('INVALID_AUGUSTUS'); + }); + it('should bubble up errors from Augustus', async () => { const { users, weth, oracle, dai, aWETH, paraswapLiquiditySwapAdapter } = testEnv; const user = users[0].signer; From 0ec5e213efc0276cccfafc29ab36a166d6f863f7 Mon Sep 17 00:00:00 2001 From: Jason Raymond Bell Date: Tue, 15 Jun 2021 15:56:25 +0100 Subject: [PATCH 15/15] Add AugustusRegistry address --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 46c04270..d2f4ed73 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "dev:UniswapLiquiditySwapAdapter": "hardhat --network kovan deploy-UniswapLiquiditySwapAdapter --provider 0x88757f2f99175387aB4C6a4b3067c77A695b0349 --router 0xfcd87315f0e4067070ade8682fcdbc3006631441 --weth 0xd0a1e359811322d97991e03f863a0c30c2cf029c", "main:deployUniswapRepayAdapter": "hardhat --network main deploy-UniswapRepayAdapter --provider 0xB53C1a33016B2DC2fF3653530bfF1848a515c8c5 --router 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D --weth 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", "main:UniswapLiquiditySwapAdapter": "hardhat --network main deploy-UniswapLiquiditySwapAdapter --provider 0xB53C1a33016B2DC2fF3653530bfF1848a515c8c5 --router 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D --weth 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", - "main:ParaSwapLiquiditySwapAdapter": "hardhat --network main deploy-ParaSwapLiquiditySwapAdapter --provider 0xB53C1a33016B2DC2fF3653530bfF1848a515c8c5 --augustusRegistry 0x0000000000000000000000000000000000000000", + "main:ParaSwapLiquiditySwapAdapter": "hardhat --network main deploy-ParaSwapLiquiditySwapAdapter --provider 0xB53C1a33016B2DC2fF3653530bfF1848a515c8c5 --augustusRegistry 0xa68bEA62Dc4034A689AA0F58A76681433caCa663", "kovan:verify": "npm run hardhat:kovan verify:general -- --all --pool Aave", "ropsten:verify": "npm run hardhat:ropsten verify:general -- --all --pool Aave", "mainnet:verify": "npm run hardhat:main verify:general -- --all --pool Aave",