diff --git a/contracts/adapters/BaseUniswapAdapter.sol b/contracts/adapters/BaseUniswapAdapter.sol index 26aa60b5..48fe5db9 100644 --- a/contracts/adapters/BaseUniswapAdapter.sol +++ b/contracts/adapters/BaseUniswapAdapter.sol @@ -12,6 +12,7 @@ import {ILendingPool} from '../interfaces/ILendingPool.sol'; import {ReserveLogic} from '../libraries/logic/ReserveLogic.sol'; import {IUniswapV2Router02} from '../interfaces/IUniswapV2Router02.sol'; import {IPriceOracleGetter} from '../interfaces/IPriceOracleGetter.sol'; +import {IERC20WithPermit} from '../interfaces/IERC20WithPermit.sol'; /** * @title BaseUniswapAdapter @@ -25,6 +26,20 @@ contract BaseUniswapAdapter { enum LeftoverAction {DEPOSIT, TRANSFER} + struct PermitParams { + uint256[] deadline; + uint8[] v; + bytes32[] r; + bytes32[] s; + } + + struct PermitSignature { + uint256 deadline; + uint8 v; + bytes32 r; + bytes32 s; + } + // Max slippage percent allow by param uint256 public constant MAX_SLIPPAGE_PERCENT = 3000; // 30% // Min slippage percent allow by param @@ -221,14 +236,28 @@ contract BaseUniswapAdapter { * @param reserve address of the asset * @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 user, - uint256 amount + uint256 amount, + PermitSignature memory permitSignature ) internal { address reserveAToken = getAToken(reserve); + if (_usePermit(permitSignature)) { + IERC20WithPermit(reserveAToken).permit( + user, + address(this), + amount, + permitSignature.deadline, + permitSignature.v, + permitSignature.r, + permitSignature.s + ); + } + // transfer from user to adapter IERC20(reserveAToken).safeTransferFrom(user, address(this), amount); @@ -241,15 +270,28 @@ contract BaseUniswapAdapter { * @param reserve address of the asset * @param user address * @param flashLoanDebt need to be repaid + * @param permitSignature struct containing the permit signature */ function pullATokenAndRepayFlashLoan( address reserve, address user, - uint256 flashLoanDebt + uint256 flashLoanDebt, + PermitSignature memory permitSignature ) internal { - pullAToken(reserve, user, flashLoanDebt); + pullAToken(reserve, user, flashLoanDebt, permitSignature); // Repay flashloan IERC20(reserve).approve(address(POOL), flashLoanDebt); } + + /** + * @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); + } } diff --git a/contracts/adapters/UniswapLiquiditySwapAdapter.sol b/contracts/adapters/UniswapLiquiditySwapAdapter.sol index 2a40e0bc..221baada 100644 --- a/contracts/adapters/UniswapLiquiditySwapAdapter.sol +++ b/contracts/adapters/UniswapLiquiditySwapAdapter.sol @@ -15,6 +15,12 @@ import {IERC20} from '../dependencies/openzeppelin/contracts/IERC20.sol'; **/ contract UniswapLiquiditySwapAdapter is BaseUniswapAdapter, IFlashLoanReceiver { + struct SwapParams { + address[] assetToSwapToList; + uint256 slippage; + PermitParams permitParams; + } + constructor( ILendingPoolAddressesProvider addressesProvider, IUniswapV2Router02 uniswapRouter @@ -35,6 +41,10 @@ contract UniswapLiquiditySwapAdapter is BaseUniswapAdapter, IFlashLoanReceiver { * @param params Additional variadic field to include extra params. Expected parameters: * address[] assetToSwapToList List of the addresses of the reserve to be swapped to and deposited * uint256 slippage The max slippage percentage allowed for the swap + * uint256[] deadline List of deadlines for the permit signature + * uint8[] v List of v param for the permit signature + * bytes32[] r List of r param for the permit signature + * bytes32[] s List of s param for the permit signature */ function executeOperation( address[] calldata assets, @@ -45,19 +55,46 @@ contract UniswapLiquiditySwapAdapter is BaseUniswapAdapter, IFlashLoanReceiver { ) external override returns (bool) { require(msg.sender == address(POOL), "CALLER_MUST_BE_LENDING_POOL"); - (address[] memory assetToSwapToList, uint256 slippage) = abi.decode(params, (address[], uint256)); - require(slippage < MAX_SLIPPAGE_PERCENT && slippage >= MIN_SLIPPAGE_PERCENT, 'SLIPPAGE_OUT_OF_RANGE'); - require(assetToSwapToList.length == assets.length, 'INCONSISTENT_PARAMS'); + SwapParams memory decodedParams = _decodeParams(params); + + require( + decodedParams.slippage < MAX_SLIPPAGE_PERCENT && decodedParams.slippage >= MIN_SLIPPAGE_PERCENT, + 'SLIPPAGE_OUT_OF_RANGE' + ); + + require( + decodedParams.assetToSwapToList.length == assets.length + && assets.length == decodedParams.permitParams.deadline.length + && assets.length == decodedParams.permitParams.v.length + && assets.length == decodedParams.permitParams.r.length + && assets.length == decodedParams.permitParams.s.length, + 'INCONSISTENT_PARAMS' + ); for (uint256 i = 0; i < assets.length; i++) { - uint256 receivedAmount = swapExactTokensForTokens(assets[i], assetToSwapToList[i], amounts[i], slippage); + uint256 receivedAmount = swapExactTokensForTokens( + assets[i], + decodedParams.assetToSwapToList[i], + amounts[i], + decodedParams.slippage + ); // Deposit new reserve - IERC20(assetToSwapToList[i]).approve(address(POOL), receivedAmount); - POOL.deposit(assetToSwapToList[i], receivedAmount, initiator, 0); + IERC20(decodedParams.assetToSwapToList[i]).approve(address(POOL), receivedAmount); + POOL.deposit(decodedParams.assetToSwapToList[i], receivedAmount, initiator, 0); uint256 flashLoanDebt = amounts[i].add(premiums[i]); - pullATokenAndRepayFlashLoan(assets[i], initiator, flashLoanDebt); + pullATokenAndRepayFlashLoan( + assets[i], + initiator, + flashLoanDebt, + PermitSignature( + decodedParams.permitParams.deadline[i], + decodedParams.permitParams.v[i], + decodedParams.permitParams.r[i], + decodedParams.permitParams.s[i] + ) + ); } return true; @@ -72,20 +109,32 @@ contract UniswapLiquiditySwapAdapter is BaseUniswapAdapter, IFlashLoanReceiver { * @param assetToSwapToList List of addresses of the underlying asset to be swap to and deposited * @param amountToSwapList List of amounts to be swapped * @param slippage The max slippage percentage allowed for the swap + * uint256[] deadline List of deadlines for the permit signature + * uint8[] v List of v param for the permit signature + * bytes32[] r List of r param for the permit signature + * bytes32[] s List of s param for the permit signature */ function swapAndDeposit( address[] calldata assetToSwapFromList, address[] calldata assetToSwapToList, uint256[] calldata amountToSwapList, - uint256 slippage + uint256 slippage, + PermitSignature[] calldata permitParams ) external { require( - assetToSwapFromList.length == assetToSwapToList.length && assetToSwapFromList.length == amountToSwapList.length, + assetToSwapFromList.length == assetToSwapToList.length + && assetToSwapFromList.length == amountToSwapList.length + && assetToSwapFromList.length == permitParams.length, 'INCONSISTENT_PARAMS' ); for (uint256 i = 0; i < assetToSwapFromList.length; i++) { - pullAToken(assetToSwapFromList[i], msg.sender, amountToSwapList[i]); + pullAToken( + assetToSwapFromList[i], + msg.sender, + amountToSwapList[i], + permitParams[i] + ); uint256 receivedAmount = swapExactTokensForTokens( assetToSwapFromList[i], @@ -99,4 +148,29 @@ contract UniswapLiquiditySwapAdapter is BaseUniswapAdapter, IFlashLoanReceiver { POOL.deposit(assetToSwapToList[i], receivedAmount, msg.sender, 0); } } + + /** + * @dev Decodes debt information encoded in flashloan params + * @param params Additional variadic field to include extra params. Expected parameters: + * address[] assetToSwapToList List of the addresses of the reserve to be swapped to and deposited + * uint256 slippage The max slippage percentage allowed for the swap + * uint256[] deadline List of deadlines for the permit signature + * uint256[] deadline List of deadlines for the permit signature + * uint8[] v List of v param for the permit signature + * bytes32[] r List of r param for the permit signature + * bytes32[] s List of s param for the permit signature + * @return SwapParams struct containing decoded params + */ + function _decodeParams(bytes memory params) internal returns (SwapParams memory) { + ( + address[] memory assetToSwapToList, + uint256 slippage, + uint256[] memory deadline, + uint8[] memory v, + bytes32[] memory r, + bytes32[] memory s + ) = abi.decode(params, (address[], uint256, uint256[], uint8[], bytes32[], bytes32[])); + + return SwapParams(assetToSwapToList, slippage, PermitParams(deadline, v, r, s)); + } } diff --git a/contracts/adapters/UniswapRepayAdapter.sol b/contracts/adapters/UniswapRepayAdapter.sol index 755bfb13..d105cb5c 100644 --- a/contracts/adapters/UniswapRepayAdapter.sol +++ b/contracts/adapters/UniswapRepayAdapter.sol @@ -20,6 +20,7 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { LeftoverAction leftOverAction; uint256[] repayAmounts; uint256[] rateModes; + PermitParams permitParams; } constructor( @@ -46,6 +47,10 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { * (1) Direct transfer to user * uint256[] repayAmounts List of amounts of debt to be repaid * uint256[] rateModes List of the rate modes of the debt to be repaid + * uint256[] deadline List of deadlines for the permit signature + * uint8[] v List of v param for the permit signature + * bytes32[] r List of r param for the permit signature + * bytes32[] s List of s param for the permit signature */ function executeOperation( address[] calldata assets, @@ -61,7 +66,11 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { require( assets.length == decodedParams.assetToSwapToList.length && assets.length == decodedParams.repayAmounts.length - && assets.length == decodedParams.rateModes.length, + && assets.length == decodedParams.rateModes.length + && assets.length == decodedParams.permitParams.deadline.length + && assets.length == decodedParams.permitParams.v.length + && assets.length == decodedParams.permitParams.r.length + && assets.length == decodedParams.permitParams.s.length, 'INCONSISTENT_PARAMS'); for (uint256 i = 0; i < assets.length; i++) { @@ -73,7 +82,13 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { decodedParams.rateModes[i], initiator, decodedParams.leftOverAction, - premiums[i] + premiums[i], + PermitSignature( + decodedParams.permitParams.deadline[i], + decodedParams.permitParams.v[i], + decodedParams.permitParams.r[i], + decodedParams.permitParams.s[i] + ) ); } @@ -91,6 +106,7 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { * @param initiator Address of the user * @param leftOverAction enum indicating what to do with the left over balance from the swap * @param premium Fee of the flash loan + * @param permitSignature struct containing the permit signature */ function _swapAndRepay( address assetFrom, @@ -100,7 +116,8 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { uint256 rateMode, address initiator, LeftoverAction leftOverAction, - uint256 premium + uint256 premium, + PermitSignature memory permitSignature ) internal { swapTokensForExactTokens(assetFrom, assetTo, amount, repayAmount); @@ -109,7 +126,7 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { POOL.repay(assetTo, repayAmount, rateMode, initiator); uint256 flashLoanDebt = amount.add(premium); - pullATokenAndRepayFlashLoan(assetFrom, initiator, flashLoanDebt); + pullATokenAndRepayFlashLoan(assetFrom, initiator, flashLoanDebt, permitSignature); // Take care of reserve leftover from the swap sendLeftovers(assetFrom, flashLoanDebt, leftOverAction, initiator); @@ -124,6 +141,10 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { * (1) Direct transfer to user * uint256[] repayAmounts List of amounts of debt to be repaid * uint256[] rateModes List of the rate modes of the debt to be repaid + * uint256[] deadline List of deadlines for the permit signature + * uint8[] v List of v param for the permit signature + * bytes32[] r List of r param for the permit signature + * bytes32[] s List of s param for the permit signature * @return RepayParams struct containing decoded params */ function _decodeParams(bytes memory params) internal returns (RepayParams memory) { @@ -131,9 +152,24 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { address[] memory assetToSwapToList, LeftoverAction leftOverAction, uint256[] memory repayAmounts, - uint256[] memory rateModes - ) = abi.decode(params, (address[], LeftoverAction, uint256[], uint256[])); + uint256[] memory rateModes, + uint256[] memory deadline, + uint8[] memory v, + bytes32[] memory r, + bytes32[] memory s + ) = abi.decode(params, (address[], LeftoverAction, uint256[], uint256[], uint256[], uint8[], bytes32[], bytes32[])); - return RepayParams(assetToSwapToList, leftOverAction, repayAmounts, rateModes); + return RepayParams( + assetToSwapToList, + leftOverAction, + repayAmounts, + rateModes, + PermitParams( + deadline, + v, + r, + s + ) + ); } } diff --git a/contracts/interfaces/IERC20WithPermit.sol b/contracts/interfaces/IERC20WithPermit.sol new file mode 100644 index 00000000..448b383b --- /dev/null +++ b/contracts/interfaces/IERC20WithPermit.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity ^0.6.8; + +import {IERC20} from '../dependencies/openzeppelin/contracts/IERC20.sol'; + +interface IERC20WithPermit is IERC20 { + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external; +} diff --git a/test/uniswapAdapters.spec.ts b/test/uniswapAdapters.spec.ts index 555f0ef0..3d9e43aa 100644 --- a/test/uniswapAdapters.spec.ts +++ b/test/uniswapAdapters.spec.ts @@ -1,5 +1,10 @@ import {makeSuite, TestEnv} from './helpers/make-suite'; -import {convertToCurrencyDecimals, getContract} from '../helpers/contracts-helpers'; +import { + convertToCurrencyDecimals, + getContract, + buildPermitParams, + getSignatureFromTypedData, +} from '../helpers/contracts-helpers'; import {getMockUniswapRouter} from '../helpers/contracts-getters'; import { deployUniswapLiquiditySwapAdapter, @@ -8,11 +13,13 @@ import { import {MockUniswapV2Router02} from '../types/MockUniswapV2Router02'; import {Zero} from '@ethersproject/constants'; import BigNumber from 'bignumber.js'; -import {evmRevert, evmSnapshot} from '../helpers/misc-utils'; +import {BRE, evmRevert, evmSnapshot} from '../helpers/misc-utils'; import {ethers} from 'ethers'; import {eContractid} from '../helpers/types'; import {AToken} from '../types/AToken'; import {StableDebtToken} from '../types/StableDebtToken'; +import {BUIDLEREVM_CHAINID} from '../helpers/buidler-constants'; +import {MAX_UINT_AMOUNT} from '../helpers/constants'; const {parseEther} = ethers.utils; const {expect} = require('chai'); @@ -121,8 +128,15 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { // 0,5% slippage const params = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256'], - [[dai.address], 50] + ['address[]', 'uint256', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], + [ + [dai.address], + 50, + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ] ); await expect( @@ -158,6 +172,102 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { expect(userAEthBalance).to.be.gte(userAEthBalanceBefore.sub(liquidityToSwap)); }); + it('should correctly swap tokens with permit', async () => { + const {users, weth, oracle, dai, aDai, aWETH, pool, uniswapLiquiditySwapAdapter} = 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 mockUniswapRouter.setAmountToReturn(expectedDaiAmount); + + // User will swap liquidity 10 aEth to aDai + const liquidityToSwap = parseEther('10'); + const userAEthBalanceBefore = await aWETH.balanceOf(userAddress); + + // IMPORTANT: Round down to work equal to solidity to get the correct value for permit call + BigNumber.config({ + ROUNDING_MODE: 1, //round down + }); + + // Subtract the FL fee from the amount to be swapped 0,09% + const flashloanAmountBN = new BigNumber(liquidityToSwap.toString()).div(1.0009); + const flashloanAmount = flashloanAmountBN.toFixed(0); + const flashloanFee = flashloanAmountBN.multipliedBy(9).div(10000); + const amountToPermit = flashloanAmountBN.plus(flashloanFee); + + const chainId = BRE.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, + uniswapLiquiditySwapAdapter.address, + nonce, + deadline, + amountToPermit.toFixed(0).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); + + // 0,5% slippage + const params = ethers.utils.defaultAbiCoder.encode( + ['address[]', 'uint256', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], + [[dai.address], 50, [deadline], [v], [r], [s]] + ); + + await expect( + pool + .connect(user) + .flashLoan( + uniswapLiquiditySwapAdapter.address, + [weth.address], + [flashloanAmount.toString()], + [0], + userAddress, + params, + 0 + ) + ) + .to.emit(uniswapLiquiditySwapAdapter, 'Swapped') + .withArgs(weth.address, dai.address, flashloanAmount.toString(), expectedDaiAmount); + + const adapterWethBalance = await weth.balanceOf(uniswapLiquiditySwapAdapter.address); + const adapterDaiBalance = await dai.balanceOf(uniswapLiquiditySwapAdapter.address); + const adapterDaiAllowance = await dai.allowance( + uniswapLiquiditySwapAdapter.address, + userAddress + ); + 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(adapterDaiAllowance).to.be.eq(Zero); + expect(userADaiBalance).to.be.eq(expectedDaiAmount); + expect(userAEthBalance).to.be.lt(userAEthBalanceBefore); + expect(userAEthBalance).to.be.gte(userAEthBalanceBefore.sub(liquidityToSwap)); + + // Restore round up + BigNumber.config({ + ROUNDING_MODE: 0, //round up + }); + }); + it('should revert if inconsistent params', async () => { const {users, weth, oracle, dai, aWETH, pool, uniswapLiquiditySwapAdapter} = testEnv; const user = users[0].signer; @@ -182,8 +292,15 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { // 0,5% slippage const params = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256'], - [[dai.address, weth.address], 50] + ['address[]', 'uint256', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], + [ + [dai.address, weth.address], + 50, + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ] ); await expect( @@ -199,6 +316,116 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { 0 ) ).to.be.revertedWith('INCONSISTENT_PARAMS'); + + const params2 = ethers.utils.defaultAbiCoder.encode( + ['address[]', 'uint256', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], + [ + [dai.address, weth.address], + 50, + [0, 0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ] + ); + + await expect( + pool + .connect(user) + .flashLoan( + uniswapLiquiditySwapAdapter.address, + [weth.address], + [flashloanAmount.toString()], + [0], + userAddress, + params2, + 0 + ) + ).to.be.revertedWith('INCONSISTENT_PARAMS'); + + const params3 = ethers.utils.defaultAbiCoder.encode( + ['address[]', 'uint256', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], + [ + [dai.address, weth.address], + 50, + [0], + [0, 0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ] + ); + + await expect( + pool + .connect(user) + .flashLoan( + uniswapLiquiditySwapAdapter.address, + [weth.address], + [flashloanAmount.toString()], + [0], + userAddress, + params3, + 0 + ) + ).to.be.revertedWith('INCONSISTENT_PARAMS'); + + const params4 = ethers.utils.defaultAbiCoder.encode( + ['address[]', 'uint256', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], + [ + [dai.address, weth.address], + 50, + [0], + [0], + [ + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000', + ], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ] + ); + + await expect( + pool + .connect(user) + .flashLoan( + uniswapLiquiditySwapAdapter.address, + [weth.address], + [flashloanAmount.toString()], + [0], + userAddress, + params4, + 0 + ) + ).to.be.revertedWith('INCONSISTENT_PARAMS'); + + const params5 = ethers.utils.defaultAbiCoder.encode( + ['address[]', 'uint256', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], + [ + [dai.address, weth.address], + 50, + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + [ + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000', + ], + ] + ); + + await expect( + pool + .connect(user) + .flashLoan( + uniswapLiquiditySwapAdapter.address, + [weth.address], + [flashloanAmount.toString()], + [0], + userAddress, + params5, + 0 + ) + ).to.be.revertedWith('INCONSISTENT_PARAMS'); }); it('should revert if caller not lending pool', async () => { @@ -225,8 +452,15 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { // 0,5% slippage const params = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256'], - [[dai.address, weth.address], 50] + ['address[]', 'uint256', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], + [ + [dai.address], + 50, + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ] ); await expect( @@ -299,8 +533,15 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { // 0,5% slippage const params = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256'], - [[dai.address], 50] + ['address[]', 'uint256', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], + [ + [dai.address], + 50, + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ] ); await expect( @@ -359,14 +600,28 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { // 30% slippage const params1 = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256'], - [[dai.address], 3000] + ['address[]', 'uint256', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], + [ + [dai.address], + 3000, + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ] ); // 0,05% slippage const params2 = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256'], - [[dai.address], 5] + ['address[]', 'uint256', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], + [ + [dai.address], + 5, + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ] ); await expect( @@ -437,7 +692,14 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { await expect( uniswapLiquiditySwapAdapter .connect(user) - .swapAndDeposit([weth.address], [dai.address], [amountWETHtoSwap], 50) + .swapAndDeposit([weth.address], [dai.address], [amountWETHtoSwap], 50, [ + { + deadline: 0, + v: 0, + r: '0x0000000000000000000000000000000000000000000000000000000000000000', + s: '0x0000000000000000000000000000000000000000000000000000000000000000', + }, + ]) ) .to.emit(uniswapLiquiditySwapAdapter, 'Swapped') .withArgs(weth.address, dai.address, amountWETHtoSwap.toString(), expectedDaiAmount); @@ -458,6 +720,80 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { expect(userAEthBalance).to.be.lt(userAEthBalanceBefore); expect(userAEthBalance).to.be.gte(userAEthBalanceBefore.sub(liquidityToSwap)); }); + + it('should correctly swap tokens using permit', async () => { + const {users, weth, oracle, dai, aDai, aWETH, uniswapLiquiditySwapAdapter} = 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 mockUniswapRouter.setAmountToReturn(expectedDaiAmount); + + // User will swap liquidity 10 aEth to aDai + const liquidityToSwap = parseEther('10'); + const userAEthBalanceBefore = await aWETH.balanceOf(userAddress); + + const chainId = BRE.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, + uniswapLiquiditySwapAdapter.address, + nonce, + deadline, + liquidityToSwap.toString() + ); + + const ownerPrivateKey = require('../test-wallets.js').accounts[1].secretKey; + if (!ownerPrivateKey) { + throw new Error('INVALID_OWNER_PK'); + } + + const {v, r, s} = getSignatureFromTypedData(ownerPrivateKey, msgParams); + + await expect( + uniswapLiquiditySwapAdapter + .connect(user) + .swapAndDeposit([weth.address], [dai.address], [amountWETHtoSwap], 50, [ + { + deadline, + v, + r, + s, + }, + ]) + ) + .to.emit(uniswapLiquiditySwapAdapter, 'Swapped') + .withArgs(weth.address, dai.address, amountWETHtoSwap.toString(), expectedDaiAmount); + + const adapterWethBalance = await weth.balanceOf(uniswapLiquiditySwapAdapter.address); + const adapterDaiBalance = await dai.balanceOf(uniswapLiquiditySwapAdapter.address); + const adapterDaiAllowance = await dai.allowance( + uniswapLiquiditySwapAdapter.address, + userAddress + ); + 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(adapterDaiAllowance).to.be.eq(Zero); + expect(userADaiBalance).to.be.eq(expectedDaiAmount); + expect(userAEthBalance).to.be.lt(userAEthBalanceBefore); + expect(userAEthBalance).to.be.gte(userAEthBalanceBefore.sub(liquidityToSwap)); + }); + it('should revert if inconsistent params', async () => { const {users, weth, dai, uniswapLiquiditySwapAdapter} = testEnv; const user = users[0].signer; @@ -467,19 +803,52 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { await expect( uniswapLiquiditySwapAdapter .connect(user) - .swapAndDeposit([weth.address, dai.address], [dai.address], [amountWETHtoSwap], 50) + .swapAndDeposit([weth.address, dai.address], [dai.address], [amountWETHtoSwap], 50, [ + { + deadline: 0, + v: 0, + r: '0x0000000000000000000000000000000000000000000000000000000000000000', + s: '0x0000000000000000000000000000000000000000000000000000000000000000', + }, + ]) ).to.be.revertedWith('INCONSISTENT_PARAMS'); await expect( uniswapLiquiditySwapAdapter .connect(user) - .swapAndDeposit([weth.address], [dai.address, weth.address], [amountWETHtoSwap], 50) + .swapAndDeposit([weth.address], [dai.address, weth.address], [amountWETHtoSwap], 50, [ + { + deadline: 0, + v: 0, + r: '0x0000000000000000000000000000000000000000000000000000000000000000', + s: '0x0000000000000000000000000000000000000000000000000000000000000000', + }, + ]) ).to.be.revertedWith('INCONSISTENT_PARAMS'); await expect( uniswapLiquiditySwapAdapter .connect(user) - .swapAndDeposit([weth.address], [dai.address], [amountWETHtoSwap, amountWETHtoSwap], 50) + .swapAndDeposit( + [weth.address], + [dai.address], + [amountWETHtoSwap, amountWETHtoSwap], + 50, + [ + { + deadline: 0, + v: 0, + r: '0x0000000000000000000000000000000000000000000000000000000000000000', + s: '0x0000000000000000000000000000000000000000000000000000000000000000', + }, + ] + ) + ).to.be.revertedWith('INCONSISTENT_PARAMS'); + + await expect( + uniswapLiquiditySwapAdapter + .connect(user) + .swapAndDeposit([weth.address], [dai.address], [amountWETHtoSwap], 50, []) ).to.be.revertedWith('INCONSISTENT_PARAMS'); }); }); @@ -561,8 +930,26 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { await mockUniswapRouter.connect(user).setAmountToReturn(expectedDaiAmount); const params = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256', 'uint256[]', 'uint256[]'], - [[dai.address], 0, [expectedDaiAmount], [1]] + [ + 'address[]', + 'uint256', + 'uint256[]', + 'uint256[]', + 'uint256[]', + 'uint8[]', + 'bytes32[]', + 'bytes32[]', + ], + [ + [dai.address], + 0, + [expectedDaiAmount], + [1], + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ] ); await expect( @@ -594,6 +981,129 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { expect(userAEthBalance).to.be.gte(userAEthBalanceBefore.sub(liquidityToSwap)); }); + it('should correctly swap tokens and repay debt with permit', async () => { + const { + users, + pool, + weth, + aWETH, + oracle, + dai, + uniswapRepayAdapter, + helpersContract, + } = testEnv; + const user = users[0].signer; + const userAddress = users[0].address; + + const amountWETHtoSwap = await convertToCurrencyDecimals(weth.address, '10'); + + const daiPrice = await oracle.getAssetPrice(dai.address); + const expectedDaiAmount = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) + ); + + // Open user Debt + await pool.connect(user).borrow(dai.address, expectedDaiAmount, 1, 0, userAddress); + + const daiStableDebtTokenAddress = ( + await helpersContract.getReserveTokensAddresses(dai.address) + ).stableDebtTokenAddress; + + const daiStableDebtContract = await getContract( + eContractid.StableDebtToken, + daiStableDebtTokenAddress + ); + + const userDaiStableDebtAmountBefore = await daiStableDebtContract.balanceOf(userAddress); + + const liquidityToSwap = amountWETHtoSwap; + const userAEthBalanceBefore = await aWETH.balanceOf(userAddress); + + // IMPORTANT: Round down to work equal to solidity to get the correct value for permit call + BigNumber.config({ + ROUNDING_MODE: 1, //round down + }); + + // Subtract the FL fee from the amount to be swapped 0,09% + const flashloanAmountBN = new BigNumber(liquidityToSwap.toString()).div(1.0009); + const flashloanAmount = flashloanAmountBN.toFixed(0); + const flashloanFee = flashloanAmountBN.multipliedBy(9).div(10000); + const amountToPermit = flashloanAmountBN.plus(flashloanFee); + + const chainId = BRE.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, + uniswapRepayAdapter.address, + nonce, + deadline, + amountToPermit.toFixed(0).toString() + ); + + const ownerPrivateKey = require('../test-wallets.js').accounts[1].secretKey; + if (!ownerPrivateKey) { + throw new Error('INVALID_OWNER_PK'); + } + + const {v, r, s} = getSignatureFromTypedData(ownerPrivateKey, msgParams); + + await mockUniswapRouter.connect(user).setAmountToSwap(flashloanAmount); + await mockUniswapRouter.connect(user).setAmountToReturn(expectedDaiAmount); + + const params = ethers.utils.defaultAbiCoder.encode( + [ + 'address[]', + 'uint256', + 'uint256[]', + 'uint256[]', + 'uint256[]', + 'uint8[]', + 'bytes32[]', + 'bytes32[]', + ], + [[dai.address], 0, [expectedDaiAmount], [1], [deadline], [v], [r], [s]] + ); + + await expect( + pool + .connect(user) + .flashLoan( + uniswapRepayAdapter.address, + [weth.address], + [flashloanAmount.toString()], + [0], + userAddress, + params, + 0 + ) + ) + .to.emit(uniswapRepayAdapter, 'Swapped') + .withArgs(weth.address, dai.address, flashloanAmount.toString(), expectedDaiAmount); + + const adapterWethBalance = await weth.balanceOf(uniswapRepayAdapter.address); + const adapterDaiBalance = await dai.balanceOf(uniswapRepayAdapter.address); + const userDaiStableDebtAmount = await daiStableDebtContract.balanceOf(userAddress); + const userAEthBalance = await aWETH.balanceOf(userAddress); + + expect(adapterWethBalance).to.be.eq(Zero); + expect(adapterDaiBalance).to.be.eq(Zero); + expect(userDaiStableDebtAmountBefore).to.be.gte(expectedDaiAmount); + expect(userDaiStableDebtAmount).to.be.lt(expectedDaiAmount); + expect(userAEthBalance).to.be.lt(userAEthBalanceBefore); + expect(userAEthBalance).to.be.gte(userAEthBalanceBefore.sub(liquidityToSwap)); + + // Restore round up + BigNumber.config({ + ROUNDING_MODE: 0, //round up + }); + }); + it('should revert if inconsistent params', async () => { const {users, pool, weth, aWETH, oracle, dai, uniswapRepayAdapter} = testEnv; const user = users[0].signer; @@ -620,8 +1130,26 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { await mockUniswapRouter.connect(user).setAmountToReturn(expectedDaiAmount); const params1 = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256', 'uint256[]', 'uint256[]'], - [[dai.address, weth.address], 0, [expectedDaiAmount], [1]] + [ + 'address[]', + 'uint256', + 'uint256[]', + 'uint256[]', + 'uint256[]', + 'uint8[]', + 'bytes32[]', + 'bytes32[]', + ], + [ + [dai.address, weth.address], + 0, + [expectedDaiAmount], + [1], + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ] ); await expect( @@ -639,8 +1167,26 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { ).to.be.revertedWith('INCONSISTENT_PARAMS'); const params2 = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256', 'uint256[]', 'uint256[]'], - [[dai.address], 0, [expectedDaiAmount, expectedDaiAmount], [1]] + [ + 'address[]', + 'uint256', + 'uint256[]', + 'uint256[]', + 'uint256[]', + 'uint8[]', + 'bytes32[]', + 'bytes32[]', + ], + [ + [dai.address], + 0, + [expectedDaiAmount, expectedDaiAmount], + [1], + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ] ); await expect( @@ -658,8 +1204,26 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { ).to.be.revertedWith('INCONSISTENT_PARAMS'); const params3 = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256', 'uint256[]', 'uint256[]'], - [[dai.address], 0, [expectedDaiAmount], [1, 1]] + [ + 'address[]', + 'uint256', + 'uint256[]', + 'uint256[]', + 'uint256[]', + 'uint8[]', + 'bytes32[]', + 'bytes32[]', + ], + [ + [dai.address], + 0, + [expectedDaiAmount], + [1, 1], + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ] ); await expect( @@ -675,6 +1239,160 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { 0 ) ).to.be.revertedWith('INCONSISTENT_PARAMS'); + + const params4 = ethers.utils.defaultAbiCoder.encode( + [ + 'address[]', + 'uint256', + 'uint256[]', + 'uint256[]', + 'uint256[]', + 'uint8[]', + 'bytes32[]', + 'bytes32[]', + ], + [ + [dai.address], + 0, + [expectedDaiAmount], + [1], + [0, 0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ] + ); + + await expect( + pool + .connect(user) + .flashLoan( + uniswapRepayAdapter.address, + [weth.address], + [flashloanAmount.toString()], + [0], + userAddress, + params4, + 0 + ) + ).to.be.revertedWith('INCONSISTENT_PARAMS'); + + const params5 = ethers.utils.defaultAbiCoder.encode( + [ + 'address[]', + 'uint256', + 'uint256[]', + 'uint256[]', + 'uint256[]', + 'uint8[]', + 'bytes32[]', + 'bytes32[]', + ], + [ + [dai.address], + 0, + [expectedDaiAmount], + [1], + [0], + [0, 0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ] + ); + + await expect( + pool + .connect(user) + .flashLoan( + uniswapRepayAdapter.address, + [weth.address], + [flashloanAmount.toString()], + [0], + userAddress, + params5, + 0 + ) + ).to.be.revertedWith('INCONSISTENT_PARAMS'); + + const params6 = ethers.utils.defaultAbiCoder.encode( + [ + 'address[]', + 'uint256', + 'uint256[]', + 'uint256[]', + 'uint256[]', + 'uint8[]', + 'bytes32[]', + 'bytes32[]', + ], + [ + [dai.address], + 0, + [expectedDaiAmount], + [1], + [0], + [0], + [ + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000', + ], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ] + ); + + await expect( + pool + .connect(user) + .flashLoan( + uniswapRepayAdapter.address, + [weth.address], + [flashloanAmount.toString()], + [0], + userAddress, + params6, + 0 + ) + ).to.be.revertedWith('INCONSISTENT_PARAMS'); + + const params7 = ethers.utils.defaultAbiCoder.encode( + [ + 'address[]', + 'uint256', + 'uint256[]', + 'uint256[]', + 'uint256[]', + 'uint8[]', + 'bytes32[]', + 'bytes32[]', + ], + [ + [dai.address], + 0, + [expectedDaiAmount], + [1], + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + [ + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000', + ], + ] + ); + + await expect( + pool + .connect(user) + .flashLoan( + uniswapRepayAdapter.address, + [weth.address], + [flashloanAmount.toString()], + [0], + userAddress, + params7, + 0 + ) + ).to.be.revertedWith('INCONSISTENT_PARAMS'); }); it('should revert if caller not lending pool', async () => { @@ -703,8 +1421,26 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { await mockUniswapRouter.connect(user).setAmountToReturn(expectedDaiAmount); const params = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256', 'uint256[]', 'uint256[]'], - [[dai.address], 0, [expectedDaiAmount], [1]] + [ + 'address[]', + 'uint256', + 'uint256[]', + 'uint256[]', + 'uint256[]', + 'uint8[]', + 'bytes32[]', + 'bytes32[]', + ], + [ + [dai.address], + 0, + [expectedDaiAmount], + [1], + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ] ); await expect( @@ -749,8 +1485,26 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { await mockUniswapRouter.connect(user).setAmountToReturn(expectedDaiAmount); const params = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256', 'uint256[]', 'uint256[]'], - [[dai.address], 0, [expectedDaiAmount], [1]] + [ + 'address[]', + 'uint256', + 'uint256[]', + 'uint256[]', + 'uint256[]', + 'uint8[]', + 'bytes32[]', + 'bytes32[]', + ], + [ + [dai.address], + 0, + [expectedDaiAmount], + [1], + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ] ); await expect( @@ -794,8 +1548,26 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { await mockUniswapRouter.connect(user).setAmountToReturn(expectedDaiAmount); const params = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256', 'uint256[]', 'uint256[]'], - [[dai.address], 0, [expectedDaiAmount], [1]] + [ + 'address[]', + 'uint256', + 'uint256[]', + 'uint256[]', + 'uint256[]', + 'uint8[]', + 'bytes32[]', + 'bytes32[]', + ], + [ + [dai.address], + 0, + [expectedDaiAmount], + [1], + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ] ); await expect( @@ -839,8 +1611,26 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { await mockUniswapRouter.connect(user).setAmountToReturn(expectedDaiAmount); const params = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256', 'uint256[]', 'uint256[]'], - [[dai.address], 0, [expectedDaiAmount], [1]] + [ + 'address[]', + 'uint256', + 'uint256[]', + 'uint256[]', + 'uint256[]', + 'uint8[]', + 'bytes32[]', + 'bytes32[]', + ], + [ + [dai.address], + 0, + [expectedDaiAmount], + [1], + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ] ); await expect( @@ -911,8 +1701,26 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { await mockUniswapRouter.connect(user).setAmountToReturn(expectedDaiAmount); const params = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256', 'uint256[]', 'uint256[]'], - [[dai.address], 0, [expectedDaiAmount], [1]] + [ + 'address[]', + 'uint256', + 'uint256[]', + 'uint256[]', + 'uint256[]', + 'uint8[]', + 'bytes32[]', + 'bytes32[]', + ], + [ + [dai.address], + 0, + [expectedDaiAmount], + [1], + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ] ); await expect( @@ -1002,8 +1810,26 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { const wethBalanceBefore = await weth.balanceOf(userAddress); const params = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256', 'uint256[]', 'uint256[]'], - [[dai.address], 1, [expectedDaiAmount], [1]] + [ + 'address[]', + 'uint256', + 'uint256[]', + 'uint256[]', + 'uint256[]', + 'uint8[]', + 'bytes32[]', + 'bytes32[]', + ], + [ + [dai.address], + 1, + [expectedDaiAmount], + [1], + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ] ); await expect(