diff --git a/contracts/adapters/BaseUniswapAdapter.sol b/contracts/adapters/BaseUniswapAdapter.sol index 1b11a8b7..2e52742b 100644 --- a/contracts/adapters/BaseUniswapAdapter.sol +++ b/contracts/adapters/BaseUniswapAdapter.sol @@ -42,6 +42,10 @@ contract BaseUniswapAdapter { // Max slippage percent allowed uint256 public constant MAX_SLIPPAGE_PERCENT = 3000; // 30% + // FLash Loan fee set in lending pool + uint256 public constant FLASHLOAN_PREMIUM_TOTAL = 9; + // USD oracle asset address + address public constant USD_ADDRESS = 0x10F7Fc1F91Ba351f9C629c5947AD69bD03C05b96; ILendingPool public immutable POOL; IPriceOracleGetter public immutable ORACLE; @@ -56,24 +60,39 @@ contract BaseUniswapAdapter { } /** - * @dev Given an input asset amount, returns the maximum output amount of the other asset + * @dev Given an input asset amount, returns the maximum output amount of the other asset and the prices * @param amountIn Amount of reserveIn * @param reserveIn Address of the asset to be swap from * @param reserveOut Address of the asset to be swap to - * @return uint256 amountOut + * @return uint256 Amount out fo the reserveOut + * @return uint256 The price of out amount denominated in the reserveIn currency (18 decimals) + * @return uint256 In amount of reserveIn value denominated in USD (8 decimals) + * @return uint256 Out amount of reserveOut value denominated in USD (8 decimals) */ - function getAmountOut(uint256 amountIn, address reserveIn, address reserveOut) - public - view - returns (uint256) + function getAmountsOut(uint256 amountIn, address reserveIn, address reserveOut) + external + view + returns (uint256, uint256, uint256, uint256) { - address[] memory path = new address[](2); - path[0] = reserveIn; - path[1] = reserveOut; + // Subtract flash loan fee + uint256 finalAmountIn = amountIn.sub(amountIn.mul(FLASHLOAN_PREMIUM_TOTAL).div(10000)); - uint256[] memory amounts = UNISWAP_ROUTER.getAmountsOut(amountIn, path); + uint256 amountOut = _getAmountsOut(reserveIn, reserveOut, finalAmountIn); - return amounts[1]; + uint256 reserveInDecimals = _getDecimals(reserveIn); + uint256 reserveOutDecimals = _getDecimals(reserveOut); + + uint256 outPerInPrice = finalAmountIn + .mul(10**18) + .mul(10**reserveOutDecimals) + .div(amountOut.mul(10**reserveInDecimals)); + + return ( + amountOut, + outPerInPrice, + _calcUsdValue(reserveIn, amountIn, reserveInDecimals), + _calcUsdValue(reserveOut, amountOut, reserveOutDecimals) + ); } /** @@ -84,9 +103,9 @@ contract BaseUniswapAdapter { * @return uint256 amountIn */ function getAmountIn(uint256 amountOut, address reserveIn, address reserveOut) - public - view - returns (uint256) + external + view + returns (uint256) { address[] memory path = new address[](2); path[0] = reserveIn; @@ -294,4 +313,39 @@ contract BaseUniswapAdapter { return !(uint256(signature.deadline) == uint256(signature.v) && uint256(signature.deadline) == 0); } + + /** + * @dev Calculates the value denominated in USD + * @param reserve Address of the reserve + * @param amount Amount of the reserve + * @param decimals Decimals of the reserve + * @return whether or not permit should be called + */ + function _calcUsdValue(address reserve, uint256 amount, uint256 decimals) internal view returns (uint256) { + uint256 ethUsdPrice = _getPrice(USD_ADDRESS); + uint256 reservePrice = _getPrice(reserve); + + return amount + .mul(reservePrice) + .div(10**decimals) + .mul(ethUsdPrice) + .div(10**18); + } + + /** + * @dev Given an input asset amount, returns the maximum output amount of the other asset + * @param reserveIn Address of the asset to be swap from + * @param reserveOut Address of the asset to be swap to + * @param amountIn Amount of reserveIn + * @return the output amount + */ + function _getAmountsOut(address reserveIn, address reserveOut, uint256 amountIn) internal view returns (uint256) { + address[] memory path = new address[](2); + path[0] = reserveIn; + path[1] = reserveOut; + + uint256[] memory amounts = UNISWAP_ROUTER.getAmountsOut(amountIn, path); + + return amounts[1]; + } } diff --git a/test/uniswapAdapters.spec.ts b/test/uniswapAdapters.spec.ts index 35803467..1ebc21f8 100644 --- a/test/uniswapAdapters.spec.ts +++ b/test/uniswapAdapters.spec.ts @@ -19,7 +19,7 @@ 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'; +import {MAX_UINT_AMOUNT, USD_ADDRESS} from '../helpers/constants'; const {parseEther} = ethers.utils; const {expect} = require('chai'); @@ -41,17 +41,106 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { }); describe('BaseUniswapAdapter', () => { - describe('getAmountOut', () => { - it('should return the estimated amountOut for the asset swap', async () => { - const {weth, dai, uniswapLiquiditySwapAdapter} = testEnv; + describe('getAmountsOut', () => { + it('should return the estimated amountOut and prices for the asset swap', async () => { + const {weth, dai, uniswapLiquiditySwapAdapter, oracle} = testEnv; + const amountIn = parseEther('1'); - const amountOut = parseEther('2'); + const flashloanPremium = amountIn.mul(9).div(10000); + const amountToSwap = amountIn.sub(flashloanPremium); - await mockUniswapRouter.setAmountOut(amountIn, weth.address, dai.address, amountOut); + const wethPrice = await oracle.getAssetPrice(weth.address); + const daiPrice = await oracle.getAssetPrice(dai.address); + const usdPrice = await oracle.getAssetPrice(USD_ADDRESS); - expect( - await uniswapLiquiditySwapAdapter.getAmountOut(amountIn, weth.address, dai.address) - ).to.be.eq(amountOut); + const expectedDaiAmount = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountToSwap.toString()).div(daiPrice.toString()).toFixed(0) + ); + + const outPerInPrice = amountToSwap + .mul(parseEther('1')) + .mul(parseEther('1')) + .div(expectedDaiAmount.mul(parseEther('1'))); + const ethUsdValue = amountIn + .mul(wethPrice) + .div(parseEther('1')) + .mul(usdPrice) + .div(parseEther('1')); + const daiUsdValue = expectedDaiAmount + .mul(daiPrice) + .div(parseEther('1')) + .mul(usdPrice) + .div(parseEther('1')); + + await mockUniswapRouter.setAmountOut( + amountToSwap, + weth.address, + dai.address, + expectedDaiAmount + ); + + const result = await uniswapLiquiditySwapAdapter.getAmountsOut( + amountIn, + weth.address, + dai.address + ); + + expect(result['0']).to.be.eq(expectedDaiAmount); + expect(result['1']).to.be.eq(outPerInPrice); + expect(result['2']).to.be.eq(ethUsdValue); + expect(result['3']).to.be.eq(daiUsdValue); + }); + it('should work correctly with different decimals', async () => { + const {lend, usdc, uniswapLiquiditySwapAdapter, oracle} = testEnv; + + const amountIn = parseEther('10'); + const flashloanPremium = amountIn.mul(9).div(10000); + const amountToSwap = amountIn.sub(flashloanPremium); + + const lendPrice = await oracle.getAssetPrice(lend.address); + const usdcPrice = await oracle.getAssetPrice(usdc.address); + const usdPrice = await oracle.getAssetPrice(USD_ADDRESS); + + const expectedUSDCAmount = await convertToCurrencyDecimals( + usdc.address, + new BigNumber(amountToSwap.toString()).div(usdcPrice.toString()).toFixed(0) + ); + + const outPerInPrice = amountToSwap + .mul(parseEther('1')) + .mul('1000000') + .div(expectedUSDCAmount.mul(parseEther('1'))); + + const lendUsdValue = amountIn + .mul(lendPrice) + .div(parseEther('1')) + .mul(usdPrice) + .div(parseEther('1')); + + const usdcUsdValue = expectedUSDCAmount + .mul(usdcPrice) + .div('1000000') + .mul(usdPrice) + .div(parseEther('1')); + + await mockUniswapRouter.setAmountOut( + amountToSwap, + lend.address, + usdc.address, + expectedUSDCAmount + ); + + const result = await uniswapLiquiditySwapAdapter.getAmountsOut( + amountIn, + lend.address, + usdc.address + ); + + expect(result['0']).to.be.eq(expectedUSDCAmount); + expect(result['1']).to.be.eq(outPerInPrice); + expect(result['2']).to.be.eq(lendUsdValue); + expect(result['3']).to.be.eq(usdcUsdValue); }); });