From a4dad49c3fc12502333f5b9be0c659f8c3a26f91 Mon Sep 17 00:00:00 2001 From: Stephen Hankinson Date: Sat, 14 Aug 2021 18:38:58 -0300 Subject: [PATCH 01/27] Initial uniswap v2 amm adapter --- .../integration/amm/UniswapAmmAdapter.sol | 281 ++++++++++++++++++ 1 file changed, 281 insertions(+) create mode 100644 contracts/protocol/integration/amm/UniswapAmmAdapter.sol diff --git a/contracts/protocol/integration/amm/UniswapAmmAdapter.sol b/contracts/protocol/integration/amm/UniswapAmmAdapter.sol new file mode 100644 index 000000000..0266f930f --- /dev/null +++ b/contracts/protocol/integration/amm/UniswapAmmAdapter.sol @@ -0,0 +1,281 @@ +/* + Copyright 2020 Set Labs Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + SPDX-License-Identifier: Apache License, Version 2.0 +*/ + +pragma solidity 0.6.10; + +import "../../../interfaces/external/IUniswapV2Router.sol"; +import "../../../interfaces/external/IUniswapV2Pair.sol"; +import "../../../interfaces/external/IUniswapV2Factory.sol"; +import "../../../interfaces/IAmmAdapter.sol"; +import "@openzeppelin/contracts/math/SafeMath.sol"; + +struct Position { + address token0; + uint256 amount0; + address token1; + uint256 amount1; +} + +/** + * @title UniswapAmmAdapter + * @author Stephen Hankinson + * + * Adapter for Uniswap V2 Router02 that encodes adding and removing liquidty + */ +contract UniswapAmmAdapter is IAmmAdapter { + using SafeMath for uint256; + + /* ============ State Variables ============ */ + + // Address of Uniswap V2 Router02 contract + address public immutable router; + IUniswapV2Factory public immutable factory; + + // Uniswap router function string for adding liquidity + string internal constant ADD_LIQUIDITY = + "addLiquidity(address,address,uint256,uint256,uint256,uint256,address,uint256)"; + // Uniswap router function string for removing liquidity + string internal constant REMOVE_LIQUIDITY = + "removeLiquidity(address,address,uint256,uint256,uint256,address,uint256)"; + + /* ============ Constructor ============ */ + + /** + * Set state variables + * + * @param _router Address of Uniswap V2 Router02 contract + */ + constructor(address _router) public { + router = _router; + factory = IUniswapV2Factory(IUniswapV2Router(_router).factory()); + } + + /* ============ Internal Functions =================== */ + + /** + * Return tokens in sorted order + * + * @param _token0 Address of the first token + * @param _amount0 Amount of the first token + * @param _token1 Address of the first token + * @param _amount1 Amount of the second token + */ + function getPosition( + address _token0, + uint256 _amount0, + address _token1, + uint256 _amount1 + ) + internal + pure + returns ( + Position memory position + ) + { + position = _token0 < _token1 ? + Position(_token0, _amount0, _token1, _amount1) : Position(_token1, _amount1, _token0, _amount0); + } + + /* ============ External Getter Functions ============ */ + + /** + * Return calldata for Uniswap V2 Router02 + * + * @param _pool Address of liquidity token + * @param _components Address array required to add liquidity + * @param _maxTokensIn AmountsIn desired to add liquidity + * @param _minLiquidity Min liquidity amount to add + */ + function getProvideLiquidityCalldata( + address _pool, + address[] calldata _components, + uint256[] calldata _maxTokensIn, + uint256 _minLiquidity + ) + external + view + override + returns ( + address _target, + uint256 _value, + bytes memory _calldata + ) + { + + IUniswapV2Pair pair = IUniswapV2Pair(_pool); + require(factory == IUniswapV2Factory(pair.factory()), "_pool factory doesn't match the router factory"); + require(_components.length == 2, "_components length is invalid"); + require(_maxTokensIn.length == 2, "_maxTokensIn length is invalid"); + + Position memory position = + getPosition(_components[0], _maxTokensIn[0], _components[1], _maxTokensIn[1]); + + require(factory.getPair(position.token0, position.token1) == _pool, "_pool doesn't match the components"); + require(position.amount0 > 0, "supplied token0 must be greater than 0"); + require(position.amount1 > 0, "supplied token1 must be greater than 0"); + + uint256 lpTotalSupply = pair.totalSupply(); + require(lpTotalSupply >= 0, "_pool totalSupply must be > 0"); + + (uint256 reserve0, uint256 reserve1, ) = pair.getReserves(); + uint256 amount0Min = reserve0.mul(_minLiquidity).div(lpTotalSupply); + uint256 amount1Min = reserve1.mul(_minLiquidity).div(lpTotalSupply); + + require(amount0Min <= position.amount0, "_minLiquidity too high for amount0"); + require(amount1Min <= position.amount1, "_minLiquidity too high for amount1"); + + _target = router; + _value = 0; + _calldata = abi.encodeWithSignature( + ADD_LIQUIDITY, + position.token0, + position.token1, + position.amount0, + position.amount1, + amount0Min, + amount1Min, + msg.sender, + block.timestamp // solhint-disable-line not-rely-on-time + ); + } + + function getProvideLiquiditySingleAssetCalldata( + address, + address, + uint256, + uint256 + ) + external + view + override + returns ( + address, + uint256, + bytes memory + ) + { + revert("Single asset liquidity addition not supported"); + } + + /** + * Return calldata for Uniswap V2 Router02 + * + * @param _pool Address of liquidity token + * @param _components Address array required to add liquidity + * @param _minTokensOut AmountsOut minimum to remove liquidity + * @param _liquidity Liquidity amount to remove + */ + function getRemoveLiquidityCalldata( + address _pool, + address[] calldata _components, + uint256[] calldata _minTokensOut, + uint256 _liquidity + ) + external + view + override + returns ( + address _target, + uint256 _value, + bytes memory _calldata + ) + { + IUniswapV2Pair pair = IUniswapV2Pair(_pool); + require(factory == IUniswapV2Factory(pair.factory()), "_pool factory doesn't match the router factory"); + require(_components.length == 2, "_components length is invalid"); + require(_minTokensOut.length == 2, "_minTokensOut length is invalid"); + + Position memory position = + getPosition(_components[0], _minTokensOut[0], _components[1], _minTokensOut[1]); + + require(factory.getPair(position.token0, position.token1) == _pool, "_pool doesn't match the components"); + require(position.amount0 > 0, "requested token0 must be greater than 0"); + require(position.amount1 > 0, "requested token1 must be greater than 0"); + + uint256 balance = pair.balanceOf(msg.sender); + require(_liquidity <= balance, "_liquidity must be <= to current balance"); + + uint256 totalSupply = pair.totalSupply(); + (uint256 reserve0, uint256 reserve1, ) = pair.getReserves(); + uint256 ownedToken0 = reserve0.mul(balance).div(totalSupply); + uint256 ownedToken1 = reserve1.mul(balance).div(totalSupply); + + require(position.amount0 <= ownedToken0, "amount0 must be <= ownedToken0"); + require(position.amount1 <= ownedToken1, "amount1 must be <= ownedToken1"); + + _target = router; + _value = 0; + _calldata = abi.encodeWithSignature( + REMOVE_LIQUIDITY, + position.token0, + position.token1, + _liquidity, + position.amount0, + position.amount1, + msg.sender, + block.timestamp // solhint-disable-line not-rely-on-time + ); + } + + function getRemoveLiquiditySingleAssetCalldata( + address, + address, + uint256, + uint256 + ) + external + view + override + returns ( + address, + uint256, + bytes memory + ) + { + revert("Single asset liquidity removal not supported"); + } + + function getSpenderAddress(address _pool) + external + view + override + returns (address) + { + IUniswapV2Pair pair = IUniswapV2Pair(_pool); + require(factory == IUniswapV2Factory(pair.factory()), "_pool factory doesn't match the router factory"); + + return router; + } + + function isValidPool(address _pool) external view override returns (bool) { + address token0; + address token1; + IUniswapV2Pair pair = IUniswapV2Pair(_pool); + try pair.token0() returns (address _token0) { + token0 = _token0; + } catch { + return false; + } + try pair.token1() returns (address _token1) { + token1 = _token1; + } catch { + return false; + } + return factory.getPair(token0, token1) == _pool; + } +} \ No newline at end of file From 671fa4a686dfe02b8bdbe6842f699801196d6363 Mon Sep 17 00:00:00 2001 From: Stephen Hankinson Date: Sun, 15 Aug 2021 00:33:39 -0300 Subject: [PATCH 02/27] First pass at tests for Uniswap V2 AMM Adapter --- ...AmmAdapter.sol => UniswapV2AmmAdapter.sol} | 6 +- .../integration/amm/UniswapAmmAdapter.spec.ts | 280 ++++++++++++++++++ utils/contracts/index.ts | 1 + utils/deploys/deployAdapters.ts | 6 + 4 files changed, 290 insertions(+), 3 deletions(-) rename contracts/protocol/integration/amm/{UniswapAmmAdapter.sol => UniswapV2AmmAdapter.sol} (98%) create mode 100644 test/protocol/integration/amm/UniswapAmmAdapter.spec.ts diff --git a/contracts/protocol/integration/amm/UniswapAmmAdapter.sol b/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol similarity index 98% rename from contracts/protocol/integration/amm/UniswapAmmAdapter.sol rename to contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol index 0266f930f..29b9135d6 100644 --- a/contracts/protocol/integration/amm/UniswapAmmAdapter.sol +++ b/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol @@ -32,12 +32,12 @@ struct Position { } /** - * @title UniswapAmmAdapter + * @title UniswapV2AmmAdapter * @author Stephen Hankinson * - * Adapter for Uniswap V2 Router02 that encodes adding and removing liquidty + * Adapter for Uniswap V2 Router that encodes adding and removing liquidty */ -contract UniswapAmmAdapter is IAmmAdapter { +contract UniswapV2AmmAdapter is IAmmAdapter { using SafeMath for uint256; /* ============ State Variables ============ */ diff --git a/test/protocol/integration/amm/UniswapAmmAdapter.spec.ts b/test/protocol/integration/amm/UniswapAmmAdapter.spec.ts new file mode 100644 index 000000000..b18a9c0ef --- /dev/null +++ b/test/protocol/integration/amm/UniswapAmmAdapter.spec.ts @@ -0,0 +1,280 @@ +import "module-alias/register"; + +import { BigNumber } from "@ethersproject/bignumber"; + +import { Address } from "@utils/types"; +import { Account } from "@utils/test/types"; +import { + MAX_UINT_256, + ADDRESS_ZERO, + ZERO, +} from "@utils/constants"; +import { SetToken, GeneralIndexModule, AmmModule, UniswapV2AmmAdapter } from "@utils/contracts"; +import DeployHelper from "@utils/deploys"; +import { + ether, +} from "@utils/index"; +import { + addSnapshotBeforeRestoreAfterEach, + cacheBeforeEach, + getAccounts, + getSystemFixture, + getUniswapFixture, + getWaffleExpect, + getLastBlockTimestamp +} from "@utils/test/index"; + +import { SystemFixture, UniswapFixture } from "@utils/fixtures"; + +const expect = getWaffleExpect(); + +describe("UniswapV2AmmAdapter", () => { + let owner: Account; + let deployer: DeployHelper; + let setup: SystemFixture; + let uniswapSetup: UniswapFixture; + let ammModule: AmmModule; + let positionModule: Account; + + let uniswapV2AmmAdapter: UniswapV2AmmAdapter; + let uniswapV2AmmAdapterName: string; + + let set: SetToken; + let indexModule: GeneralIndexModule; + + let setComponents: Address[]; + let setUnits: BigNumber[]; + + before(async () => { + [ + owner, + , + positionModule, + ] = await getAccounts(); + + deployer = new DeployHelper(owner.wallet); + setup = getSystemFixture(owner.address); + await setup.initialize(); + + uniswapSetup = getUniswapFixture(owner.address); + await uniswapSetup.initialize( + owner, + setup.weth.address, + setup.wbtc.address, + setup.dai.address + ); + + indexModule = await deployer.modules.deployGeneralIndexModule( + setup.controller.address, + setup.weth.address + ); + await setup.controller.addModule(indexModule.address); + await setup.controller.addModule(positionModule.address); + + uniswapV2AmmAdapter = await deployer.adapters.deployUniswapV2AmmAdapter(uniswapSetup.router.address); + uniswapV2AmmAdapterName = "UNISWAPV2AMM"; + + ammModule = await deployer.modules.deployAmmModule(setup.controller.address); + await setup.controller.addModule(ammModule.address); + + await setup.integrationRegistry.addIntegration( + ammModule.address, + uniswapV2AmmAdapterName, + uniswapV2AmmAdapter.address + ); + }); + + cacheBeforeEach(async () => { + setComponents = [setup.weth.address, setup.dai.address]; + setUnits = [ ether(1), ether(3000) ]; + + set = await setup.createSetToken( + setComponents, + setUnits, + [setup.issuanceModule.address, ammModule.address, positionModule.address], + ); + + await setup.issuanceModule.initialize(set.address, ADDRESS_ZERO); + await set.connect(positionModule.wallet).initializeModule(); + + await setup.weth.connect(owner.wallet).approve(uniswapSetup.router.address, MAX_UINT_256); + await setup.dai.connect(owner.wallet).approve(uniswapSetup.router.address, MAX_UINT_256); + + await uniswapSetup.router.connect(owner.wallet).addLiquidity( + setup.weth.address, + setup.dai.address, + ether(200), + ether(600000), + ether(0), + ether(0), + owner.address, + MAX_UINT_256 + ); + }); + + addSnapshotBeforeRestoreAfterEach(); + + describe("constructor", async () => { + let subjectUniswapRouter: Address; + + beforeEach(async () => { + subjectUniswapRouter = uniswapSetup.router.address; + }); + + async function subject(): Promise { + return await deployer.adapters.deployUniswapV2AmmAdapter(subjectUniswapRouter); + } + + it("should have the correct router address", async () => { + const deployedUniswapV2AmmAdapter = await subject(); + + const actualRouterAddress = await deployedUniswapV2AmmAdapter.router(); + expect(actualRouterAddress).to.eq(uniswapSetup.router.address); + }); + }); + + describe("getSpenderAddress", async () => { + async function subject(): Promise { + return await uniswapV2AmmAdapter.getSpenderAddress(uniswapSetup.wethDaiPool.address); + } + + it("should return the correct spender address", async () => { + const spender = await subject(); + + expect(spender).to.eq(uniswapSetup.router.address); + }); + }); + + describe("isValidPool", async () => { + async function subject(): Promise { + return await uniswapV2AmmAdapter.isValidPool(uniswapSetup.wethDaiPool.address); + } + + it("should be a valid pool", async () => { + const status = await subject(); + + expect(status).to.be.true; + }); + }); + + describe("getProvideLiquiditySingleAssetCalldata", async () => { + async function subject(): Promise { + return await uniswapV2AmmAdapter.getProvideLiquiditySingleAssetCalldata( + uniswapSetup.wethDaiPool.address, + setup.weth.address, + ether(1), + ether(1)); + } + + it("should not support adding a single asset", async () => { + await expect(subject()).to.be.revertedWith("Single asset liquidity addition not supported"); + }); + }); + + describe("getRemoveLiquiditySingleAssetCalldata", async () => { + async function subject(): Promise { + return await uniswapV2AmmAdapter.getRemoveLiquiditySingleAssetCalldata( + uniswapSetup.wethDaiPool.address, + setup.weth.address, + ether(1), + ether(1)); + } + + it("should not support removing a single asset", async () => { + await expect(subject()).to.be.revertedWith("Single asset liquidity removal not supported"); + }); + }); + + describe("getProvideLiquidityCalldata", async () => { + let component0: Address; + let component1: Address; + let component0Quantity: BigNumber; + let component1Quantity: BigNumber; + let minimumComponent0Quantity: BigNumber; + let minimumComponent1Quantity: BigNumber; + let liquidity: BigNumber; + + beforeEach(async () => { + component0 = setup.dai.address; // DAI Address + component1 = setup.weth.address; // WETH Address + component1Quantity = ether(1); // Provide 1 WETH + + const [reserve0, reserve1] = await uniswapSetup.wethDaiPool.getReserves(); + component0Quantity = reserve0.mul(component1Quantity).div(reserve1); + const totalSupply = await uniswapSetup.wethDaiPool.totalSupply(); + const daiLiquidity = component0Quantity.mul(totalSupply).div(reserve0); + const wethLiquidity = component1Quantity.mul(totalSupply).div(reserve1); + liquidity = wethLiquidity < daiLiquidity ? wethLiquidity : daiLiquidity; + minimumComponent0Quantity = liquidity.mul(reserve0).div(totalSupply); + minimumComponent1Quantity = liquidity.mul(reserve1).div(totalSupply); + }); + + async function subject(): Promise { + return await uniswapV2AmmAdapter.getProvideLiquidityCalldata( + uniswapSetup.wethDaiPool.address, + [component0, component1], + [component0Quantity, component1Quantity], + liquidity); + } + + it("should return the correct provide liquidity calldata", async () => { + const calldata = await subject(); + const callTimestamp = await getLastBlockTimestamp(); + + const expectedCallData = uniswapSetup.router.interface.encodeFunctionData("addLiquidity", [ + setup.dai.address, + setup.weth.address, + component0Quantity, + component1Quantity, + minimumComponent0Quantity, + minimumComponent1Quantity, + owner.address, + callTimestamp, + ]); + expect(JSON.stringify(calldata)).to.eq(JSON.stringify([uniswapSetup.router.address, ZERO, expectedCallData])); + }); + }); + + describe("getRemoveLiquidityCalldata", async () => { + let component0: Address; + let component1: Address; + let component0Quantity: BigNumber; + let component1Quantity: BigNumber; + let liquidity: BigNumber; + + beforeEach(async () => { + component0 = setup.dai.address; // DAI Address + component1 = setup.weth.address; // WETH Address + liquidity = await uniswapSetup.wethDaiPool.balanceOf(owner.address); + const [reserve0, reserve1] = await uniswapSetup.wethDaiPool.getReserves(); + const totalSupply = await uniswapSetup.wethDaiPool.totalSupply(); + component0Quantity = reserve0.mul(liquidity).div(totalSupply); + component1Quantity = reserve1.mul(liquidity).div(totalSupply); + }); + + async function subject(): Promise { + return await uniswapV2AmmAdapter.getRemoveLiquidityCalldata( + uniswapSetup.wethDaiPool.address, + [component0, component1], + [component0Quantity, component1Quantity], + liquidity); + } + + it("should return the correct remove liquidity calldata", async () => { + const calldata = await subject(); + const callTimestamp = await getLastBlockTimestamp(); + + const expectedCallData = uniswapSetup.router.interface.encodeFunctionData("removeLiquidity", [ + setup.dai.address, + setup.weth.address, + liquidity, + component0Quantity, + component1Quantity, + owner.address, + callTimestamp, + ]); + expect(JSON.stringify(calldata)).to.eq(JSON.stringify([uniswapSetup.router.address, ZERO, expectedCallData])); + }); + }); + +}); diff --git a/utils/contracts/index.ts b/utils/contracts/index.ts index 5516d773d..d7cf18f37 100644 --- a/utils/contracts/index.ts +++ b/utils/contracts/index.ts @@ -91,6 +91,7 @@ export { TradeAdapterMock } from "../../typechain/TradeAdapterMock"; export { Uint256ArrayUtilsMock } from "../../typechain/Uint256ArrayUtilsMock"; export { Uni } from "../../typechain/Uni"; export { UniswapPairPriceAdapter } from "../../typechain/UniswapPairPriceAdapter"; +export { UniswapV2AmmAdapter } from "../../typechain/UniswapV2AmmAdapter"; export { UniswapV2ExchangeAdapter } from "../../typechain/UniswapV2ExchangeAdapter"; export { UniswapV2ExchangeAdapterV2 } from "../../typechain/UniswapV2ExchangeAdapterV2"; export { UniswapV2IndexExchangeAdapter } from "../../typechain/UniswapV2IndexExchangeAdapter"; diff --git a/utils/deploys/deployAdapters.ts b/utils/deploys/deployAdapters.ts index d5f557c63..eff0ad7cd 100644 --- a/utils/deploys/deployAdapters.ts +++ b/utils/deploys/deployAdapters.ts @@ -19,6 +19,7 @@ import { YearnWrapAdapter, YearnWrapV2Adapter, UniswapPairPriceAdapter, + UniswapV2AmmAdapter, UniswapV2ExchangeAdapter, UniswapV2ExchangeAdapterV2, UniswapV2IndexExchangeAdapter, @@ -53,6 +54,7 @@ import { YearnWrapAdapter__factory } from "../../typechain/factories/YearnWrapAd import { YearnWrapV2Adapter__factory } from "../../typechain/factories/YearnWrapV2Adapter__factory"; import { UniswapPairPriceAdapter__factory } from "../../typechain/factories/UniswapPairPriceAdapter__factory"; import { UniswapV2ExchangeAdapter__factory } from "../../typechain/factories/UniswapV2ExchangeAdapter__factory"; +import { UniswapV2AmmAdapter__factory } from "../../typechain/factories/UniswapV2AmmAdapter__factory"; import { UniswapV2TransferFeeExchangeAdapter__factory } from "../../typechain/factories/UniswapV2TransferFeeExchangeAdapter__factory"; import { UniswapV2ExchangeAdapterV2__factory } from "../../typechain/factories/UniswapV2ExchangeAdapterV2__factory"; import { UniswapV2IndexExchangeAdapter__factory } from "../../typechain/factories/UniswapV2IndexExchangeAdapter__factory"; @@ -86,6 +88,10 @@ export default class DeployAdapters { ); } + public async deployUniswapV2AmmAdapter(uniswapV2Router: Address): Promise { + return await new UniswapV2AmmAdapter__factory(this._deployerSigner).deploy(uniswapV2Router); + } + public async deployUniswapV2ExchangeAdapter(uniswapV2Router: Address): Promise { return await new UniswapV2ExchangeAdapter__factory(this._deployerSigner).deploy(uniswapV2Router); } From 97823a9d9cca89ff5840f5238180b20e3307d117 Mon Sep 17 00:00:00 2001 From: Stephen Hankinson Date: Sun, 15 Aug 2021 22:29:08 -0300 Subject: [PATCH 03/27] Make sure the correct token addresses are used in the correct order --- .../integration/amm/UniswapAmmAdapter.spec.ts | 33 +++++++++++-------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/test/protocol/integration/amm/UniswapAmmAdapter.spec.ts b/test/protocol/integration/amm/UniswapAmmAdapter.spec.ts index b18a9c0ef..a8262126b 100644 --- a/test/protocol/integration/amm/UniswapAmmAdapter.spec.ts +++ b/test/protocol/integration/amm/UniswapAmmAdapter.spec.ts @@ -195,16 +195,21 @@ describe("UniswapV2AmmAdapter", () => { let liquidity: BigNumber; beforeEach(async () => { - component0 = setup.dai.address; // DAI Address - component1 = setup.weth.address; // WETH Address - component1Quantity = ether(1); // Provide 1 WETH - + component0 = await uniswapSetup.wethDaiPool.token0(); + component1 = await uniswapSetup.wethDaiPool.token1(); const [reserve0, reserve1] = await uniswapSetup.wethDaiPool.getReserves(); - component0Quantity = reserve0.mul(component1Quantity).div(reserve1); + if ( setup.dai.address == component0 ) { + component1Quantity = ether(1); // 1 WETH + component0Quantity = reserve0.mul(component1Quantity).div(reserve1); + } + else { + component0Quantity = ether(1); // 1 WETH + component1Quantity = reserve1.mul(component0Quantity).div(reserve0); + } const totalSupply = await uniswapSetup.wethDaiPool.totalSupply(); - const daiLiquidity = component0Quantity.mul(totalSupply).div(reserve0); - const wethLiquidity = component1Quantity.mul(totalSupply).div(reserve1); - liquidity = wethLiquidity < daiLiquidity ? wethLiquidity : daiLiquidity; + const component0Liquidity = component0Quantity.mul(totalSupply).div(reserve0); + const component1Liquidity = component1Quantity.mul(totalSupply).div(reserve1); + liquidity = component0Liquidity < component1Liquidity ? component0Liquidity : component1Liquidity; minimumComponent0Quantity = liquidity.mul(reserve0).div(totalSupply); minimumComponent1Quantity = liquidity.mul(reserve1).div(totalSupply); }); @@ -222,8 +227,8 @@ describe("UniswapV2AmmAdapter", () => { const callTimestamp = await getLastBlockTimestamp(); const expectedCallData = uniswapSetup.router.interface.encodeFunctionData("addLiquidity", [ - setup.dai.address, - setup.weth.address, + component0, + component1, component0Quantity, component1Quantity, minimumComponent0Quantity, @@ -243,8 +248,8 @@ describe("UniswapV2AmmAdapter", () => { let liquidity: BigNumber; beforeEach(async () => { - component0 = setup.dai.address; // DAI Address - component1 = setup.weth.address; // WETH Address + component0 = await uniswapSetup.wethDaiPool.token0(); + component1 = await uniswapSetup.wethDaiPool.token1(); liquidity = await uniswapSetup.wethDaiPool.balanceOf(owner.address); const [reserve0, reserve1] = await uniswapSetup.wethDaiPool.getReserves(); const totalSupply = await uniswapSetup.wethDaiPool.totalSupply(); @@ -265,8 +270,8 @@ describe("UniswapV2AmmAdapter", () => { const callTimestamp = await getLastBlockTimestamp(); const expectedCallData = uniswapSetup.router.interface.encodeFunctionData("removeLiquidity", [ - setup.dai.address, - setup.weth.address, + component0, + component1, liquidity, component0Quantity, component1Quantity, From 25e817bbe3c30ffd556b228a7b17995209b3f053 Mon Sep 17 00:00:00 2001 From: Stephen Hankinson Date: Mon, 16 Aug 2021 13:17:18 -0300 Subject: [PATCH 04/27] Test for invalid pool --- .../integration/amm/UniswapAmmAdapter.spec.ts | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/test/protocol/integration/amm/UniswapAmmAdapter.spec.ts b/test/protocol/integration/amm/UniswapAmmAdapter.spec.ts index a8262126b..5970bf9a9 100644 --- a/test/protocol/integration/amm/UniswapAmmAdapter.spec.ts +++ b/test/protocol/integration/amm/UniswapAmmAdapter.spec.ts @@ -146,15 +146,32 @@ describe("UniswapV2AmmAdapter", () => { }); describe("isValidPool", async () => { + let poolAddress: Address; + + beforeEach(async () => { + poolAddress = uniswapSetup.wethDaiPool.address; + }); + async function subject(): Promise { - return await uniswapV2AmmAdapter.isValidPool(uniswapSetup.wethDaiPool.address); + return await uniswapV2AmmAdapter.isValidPool(poolAddress); } it("should be a valid pool", async () => { const status = await subject(); - expect(status).to.be.true; }); + + describe("when the pool address is invalid", async () => { + beforeEach(async () => { + poolAddress = uniswapSetup.router.address; + }); + + it("should be an invalid pool", async () => { + const status = await subject(); + expect(status).to.be.false; + }); + }); + }); describe("getProvideLiquiditySingleAssetCalldata", async () => { From ca8a49c2f8202f81f305ed5b0e151152b4c49a26 Mon Sep 17 00:00:00 2001 From: Stephen Hankinson Date: Mon, 16 Aug 2021 14:02:30 -0300 Subject: [PATCH 05/27] Rename file to take into account the Uniswap version --- .../{UniswapAmmAdapter.spec.ts => UniswapV2AmmAdapter.spec.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/protocol/integration/amm/{UniswapAmmAdapter.spec.ts => UniswapV2AmmAdapter.spec.ts} (100%) diff --git a/test/protocol/integration/amm/UniswapAmmAdapter.spec.ts b/test/protocol/integration/amm/UniswapV2AmmAdapter.spec.ts similarity index 100% rename from test/protocol/integration/amm/UniswapAmmAdapter.spec.ts rename to test/protocol/integration/amm/UniswapV2AmmAdapter.spec.ts From cd7ded8ddb14c5e252c2f5ac0c5c77b3057e79ba Mon Sep 17 00:00:00 2001 From: Stephen Hankinson Date: Tue, 17 Aug 2021 21:58:33 -0300 Subject: [PATCH 06/27] Re-write UniswapV2AmmAdapter so that it works correctly --- .../integration/amm/UniswapV2AmmAdapter.sol | 243 +++++++---- contracts/protocol/modules/AmmModule.sol | 6 + .../amm/UniswapV2AmmAdapter.spec.ts | 397 ++++++++++++------ 3 files changed, 434 insertions(+), 212 deletions(-) diff --git a/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol b/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol index 29b9135d6..f6219e36e 100644 --- a/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol +++ b/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol @@ -23,11 +23,12 @@ import "../../../interfaces/external/IUniswapV2Pair.sol"; import "../../../interfaces/external/IUniswapV2Factory.sol"; import "../../../interfaces/IAmmAdapter.sol"; import "@openzeppelin/contracts/math/SafeMath.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; struct Position { - address token0; + IERC20 token0; uint256 amount0; - address token1; + IERC20 token1; uint256 amount1; } @@ -42,26 +43,26 @@ contract UniswapV2AmmAdapter is IAmmAdapter { /* ============ State Variables ============ */ - // Address of Uniswap V2 Router02 contract - address public immutable router; + // Address of Uniswap V2 Router contract + IUniswapV2Router public immutable router; IUniswapV2Factory public immutable factory; // Uniswap router function string for adding liquidity string internal constant ADD_LIQUIDITY = - "addLiquidity(address,address,uint256,uint256,uint256,uint256,address,uint256)"; + "addLiquidity(address,address[],uint256[],uint256)"; // Uniswap router function string for removing liquidity string internal constant REMOVE_LIQUIDITY = - "removeLiquidity(address,address,uint256,uint256,uint256,address,uint256)"; + "removeLiquidity(address,address[],uint256[],uint256)"; /* ============ Constructor ============ */ /** * Set state variables * - * @param _router Address of Uniswap V2 Router02 contract + * @param _router Address of Uniswap V2 Router contract */ constructor(address _router) public { - router = _router; + router = IUniswapV2Router(_router); factory = IUniswapV2Factory(IUniswapV2Router(_router).factory()); } @@ -76,25 +77,27 @@ contract UniswapV2AmmAdapter is IAmmAdapter { * @param _amount1 Amount of the second token */ function getPosition( + IUniswapV2Pair _pair, address _token0, uint256 _amount0, address _token1, uint256 _amount1 ) internal - pure + view returns ( Position memory position ) { - position = _token0 < _token1 ? - Position(_token0, _amount0, _token1, _amount1) : Position(_token1, _amount1, _token0, _amount0); + position = _pair.token0() == _token0 ? + Position(IERC20(_token0), _amount0, IERC20(_token1), _amount1) : + Position(IERC20(_token1), _amount1, IERC20(_token0), _amount0); } /* ============ External Getter Functions ============ */ /** - * Return calldata for Uniswap V2 Router02 + * Return calldata for the add liquidity call * * @param _pool Address of liquidity token * @param _components Address array required to add liquidity @@ -116,41 +119,14 @@ contract UniswapV2AmmAdapter is IAmmAdapter { bytes memory _calldata ) { - - IUniswapV2Pair pair = IUniswapV2Pair(_pool); - require(factory == IUniswapV2Factory(pair.factory()), "_pool factory doesn't match the router factory"); - require(_components.length == 2, "_components length is invalid"); - require(_maxTokensIn.length == 2, "_maxTokensIn length is invalid"); - - Position memory position = - getPosition(_components[0], _maxTokensIn[0], _components[1], _maxTokensIn[1]); - - require(factory.getPair(position.token0, position.token1) == _pool, "_pool doesn't match the components"); - require(position.amount0 > 0, "supplied token0 must be greater than 0"); - require(position.amount1 > 0, "supplied token1 must be greater than 0"); - - uint256 lpTotalSupply = pair.totalSupply(); - require(lpTotalSupply >= 0, "_pool totalSupply must be > 0"); - - (uint256 reserve0, uint256 reserve1, ) = pair.getReserves(); - uint256 amount0Min = reserve0.mul(_minLiquidity).div(lpTotalSupply); - uint256 amount1Min = reserve1.mul(_minLiquidity).div(lpTotalSupply); - - require(amount0Min <= position.amount0, "_minLiquidity too high for amount0"); - require(amount1Min <= position.amount1, "_minLiquidity too high for amount1"); - - _target = router; + _target = address(this); _value = 0; _calldata = abi.encodeWithSignature( ADD_LIQUIDITY, - position.token0, - position.token1, - position.amount0, - position.amount1, - amount0Min, - amount1Min, - msg.sender, - block.timestamp // solhint-disable-line not-rely-on-time + _pool, + _components, + _maxTokensIn, + _minLiquidity ); } @@ -173,7 +149,7 @@ contract UniswapV2AmmAdapter is IAmmAdapter { } /** - * Return calldata for Uniswap V2 Router02 + * Return calldata for the remove liquidity call * * @param _pool Address of liquidity token * @param _components Address array required to add liquidity @@ -195,40 +171,14 @@ contract UniswapV2AmmAdapter is IAmmAdapter { bytes memory _calldata ) { - IUniswapV2Pair pair = IUniswapV2Pair(_pool); - require(factory == IUniswapV2Factory(pair.factory()), "_pool factory doesn't match the router factory"); - require(_components.length == 2, "_components length is invalid"); - require(_minTokensOut.length == 2, "_minTokensOut length is invalid"); - - Position memory position = - getPosition(_components[0], _minTokensOut[0], _components[1], _minTokensOut[1]); - - require(factory.getPair(position.token0, position.token1) == _pool, "_pool doesn't match the components"); - require(position.amount0 > 0, "requested token0 must be greater than 0"); - require(position.amount1 > 0, "requested token1 must be greater than 0"); - - uint256 balance = pair.balanceOf(msg.sender); - require(_liquidity <= balance, "_liquidity must be <= to current balance"); - - uint256 totalSupply = pair.totalSupply(); - (uint256 reserve0, uint256 reserve1, ) = pair.getReserves(); - uint256 ownedToken0 = reserve0.mul(balance).div(totalSupply); - uint256 ownedToken1 = reserve1.mul(balance).div(totalSupply); - - require(position.amount0 <= ownedToken0, "amount0 must be <= ownedToken0"); - require(position.amount1 <= ownedToken1, "amount1 must be <= ownedToken1"); - - _target = router; + _target = address(this); _value = 0; _calldata = abi.encodeWithSignature( REMOVE_LIQUIDITY, - position.token0, - position.token1, - _liquidity, - position.amount0, - position.amount1, - msg.sender, - block.timestamp // solhint-disable-line not-rely-on-time + _pool, + _components, + _minTokensOut, + _liquidity ); } @@ -259,7 +209,7 @@ contract UniswapV2AmmAdapter is IAmmAdapter { IUniswapV2Pair pair = IUniswapV2Pair(_pool); require(factory == IUniswapV2Factory(pair.factory()), "_pool factory doesn't match the router factory"); - return router; + return address(this); } function isValidPool(address _pool) external view override returns (bool) { @@ -278,4 +228,143 @@ contract UniswapV2AmmAdapter is IAmmAdapter { } return factory.getPair(token0, token1) == _pool; } + + /* ============ External Setter Functions ============ */ + + /** + * Adds liquidity via the Uniswap V2 Router + * + * @param _pool Address of liquidity token + * @param _components Address array required to add liquidity + * @param _maxTokensIn AmountsIn desired to add liquidity + * @param _minLiquidity Min liquidity amount to add + */ + function addLiquidity( + address _pool, + address[] calldata _components, + uint256[] calldata _maxTokensIn, + uint256 _minLiquidity + ) + external + returns ( + uint amountA, + uint amountB, + uint liquidity + ) + { + + IUniswapV2Pair pair = IUniswapV2Pair(_pool); + require(factory == IUniswapV2Factory(pair.factory()), "_pool factory doesn't match the router factory"); + require(_components.length == 2, "_components length is invalid"); + require(_maxTokensIn.length == 2, "_maxTokensIn length is invalid"); + require(factory.getPair(_components[0], _components[1]) == _pool, + "_pool doesn't match the components"); + require(_maxTokensIn[0] > 0, "supplied token0 must be greater than 0"); + require(_maxTokensIn[1] > 0, "supplied token1 must be greater than 0"); + + Position memory position = + getPosition(pair, _components[0], _maxTokensIn[0], _components[1], _maxTokensIn[1]); + + uint256 lpTotalSupply = pair.totalSupply(); + require(lpTotalSupply >= 0, "_pool totalSupply must be > 0"); + + (uint256 reserve0, uint256 reserve1, ) = pair.getReserves(); + uint256 amount0Min = reserve0.mul(_minLiquidity).div(lpTotalSupply); + uint256 amount1Min = reserve1.mul(_minLiquidity).div(lpTotalSupply); + + require(amount0Min <= position.amount0 && amount1Min <= position.amount1, + "_minLiquidity is too high for amount minimums"); + + // Bring the tokens to this contract so we can use the Uniswap Router + position.token0.transferFrom(msg.sender, address(this), position.amount0); + position.token1.transferFrom(msg.sender, address(this), position.amount1); + + // Approve the router to spend the tokens + position.token0.approve(address(router), position.amount0); + position.token1.approve(address(router), position.amount1); + + // Add the liquidity + (amountA, amountB, liquidity) = router.addLiquidity( + address(position.token0), + address(position.token1), + position.amount0, + position.amount1, + amount0Min, + amount1Min, + msg.sender, + block.timestamp // solhint-disable-line not-rely-on-time + ); + + // If there is token0 left, send it back + if( amountA < position.amount0 ) { + position.token0.transfer(msg.sender, position.amount0.sub(amountA) ); + } + + // If there is token1 left, send it back + if( amountB < position.amount1 ) { + position.token1.transfer(msg.sender, position.amount1.sub(amountB) ); + } + + } + + /** + * Remove liquidity via the Uniswap V2 Router + * + * @param _pool Address of liquidity token + * @param _components Address array required to add liquidity + * @param _minTokensOut AmountsOut minimum to remove liquidity + * @param _liquidity Liquidity amount to remove + */ + function removeLiquidity( + address _pool, + address[] calldata _components, + uint256[] calldata _minTokensOut, + uint256 _liquidity + ) + external + returns ( + uint amountA, + uint amountB + ) + { + IUniswapV2Pair pair = IUniswapV2Pair(_pool); + require(factory == IUniswapV2Factory(pair.factory()), "_pool factory doesn't match the router factory"); + require(_components.length == 2, "_components length is invalid"); + require(_minTokensOut.length == 2, "_minTokensOut length is invalid"); + require(factory.getPair(_components[0], _components[1]) == _pool, + "_pool doesn't match the components"); + require(_minTokensOut[0] > 0, "requested token0 must be greater than 0"); + require(_minTokensOut[1] > 0, "requested token1 must be greater than 0"); + + Position memory position = + getPosition(pair, _components[0], _minTokensOut[0], _components[1], _minTokensOut[1]); + + uint256 balance = pair.balanceOf(msg.sender); + require(_liquidity <= balance, "_liquidity must be <= to current balance"); + + uint256 totalSupply = pair.totalSupply(); + (uint256 reserve0, uint256 reserve1, ) = pair.getReserves(); + uint256 ownedToken0 = reserve0.mul(balance).div(totalSupply); + uint256 ownedToken1 = reserve1.mul(balance).div(totalSupply); + + require(position.amount0 <= ownedToken0 && position.amount1 <= ownedToken1, + "amounts must be <= ownedTokens"); + + // Bring the lp token to this contract so we can use the Uniswap Router + pair.transferFrom(msg.sender, address(this), _liquidity); + + // Approve the router to spend the lp tokens + pair.approve(address(router), _liquidity); + + // Remove the liquidity + (amountA, amountB) = router.removeLiquidity( + address(position.token0), + address(position.token1), + _liquidity, + position.amount0, + position.amount1, + msg.sender, + block.timestamp // solhint-disable-line not-rely-on-time + ); + } } \ No newline at end of file diff --git a/contracts/protocol/modules/AmmModule.sol b/contracts/protocol/modules/AmmModule.sol index 2d611bbd8..ba0c0301a 100644 --- a/contracts/protocol/modules/AmmModule.sol +++ b/contracts/protocol/modules/AmmModule.sol @@ -230,6 +230,12 @@ contract AmmModule is ModuleBase, ReentrancyGuard { _validateRemoveLiquidity(actionInfo); + _setToken.invokeApprove( + _ammPool, + actionInfo.ammAdapter.getSpenderAddress(_ammPool), + _poolTokenPositionUnits + ); + _executeRemoveLiquidity(actionInfo); _validateMinimumUnderlyingReceived(actionInfo); diff --git a/test/protocol/integration/amm/UniswapV2AmmAdapter.spec.ts b/test/protocol/integration/amm/UniswapV2AmmAdapter.spec.ts index 5970bf9a9..c8d45f9e2 100644 --- a/test/protocol/integration/amm/UniswapV2AmmAdapter.spec.ts +++ b/test/protocol/integration/amm/UniswapV2AmmAdapter.spec.ts @@ -9,19 +9,17 @@ import { ADDRESS_ZERO, ZERO, } from "@utils/constants"; -import { SetToken, GeneralIndexModule, AmmModule, UniswapV2AmmAdapter } from "@utils/contracts"; +import { SetToken, AmmModule, UniswapV2AmmAdapter } from "@utils/contracts"; import DeployHelper from "@utils/deploys"; import { - ether, + ether } from "@utils/index"; import { addSnapshotBeforeRestoreAfterEach, - cacheBeforeEach, getAccounts, getSystemFixture, getUniswapFixture, - getWaffleExpect, - getLastBlockTimestamp + getWaffleExpect } from "@utils/test/index"; import { SystemFixture, UniswapFixture } from "@utils/fixtures"; @@ -34,22 +32,13 @@ describe("UniswapV2AmmAdapter", () => { let setup: SystemFixture; let uniswapSetup: UniswapFixture; let ammModule: AmmModule; - let positionModule: Account; let uniswapV2AmmAdapter: UniswapV2AmmAdapter; let uniswapV2AmmAdapterName: string; - let set: SetToken; - let indexModule: GeneralIndexModule; - - let setComponents: Address[]; - let setUnits: BigNumber[]; - before(async () => { [ owner, - , - positionModule, ] = await getAccounts(); deployer = new DeployHelper(owner.wallet); @@ -63,43 +52,10 @@ describe("UniswapV2AmmAdapter", () => { setup.wbtc.address, setup.dai.address ); - - indexModule = await deployer.modules.deployGeneralIndexModule( - setup.controller.address, - setup.weth.address - ); - await setup.controller.addModule(indexModule.address); - await setup.controller.addModule(positionModule.address); - - uniswapV2AmmAdapter = await deployer.adapters.deployUniswapV2AmmAdapter(uniswapSetup.router.address); - uniswapV2AmmAdapterName = "UNISWAPV2AMM"; - - ammModule = await deployer.modules.deployAmmModule(setup.controller.address); - await setup.controller.addModule(ammModule.address); - - await setup.integrationRegistry.addIntegration( - ammModule.address, - uniswapV2AmmAdapterName, - uniswapV2AmmAdapter.address - ); - }); - - cacheBeforeEach(async () => { - setComponents = [setup.weth.address, setup.dai.address]; - setUnits = [ ether(1), ether(3000) ]; - - set = await setup.createSetToken( - setComponents, - setUnits, - [setup.issuanceModule.address, ammModule.address, positionModule.address], - ); - - await setup.issuanceModule.initialize(set.address, ADDRESS_ZERO); - await set.connect(positionModule.wallet).initializeModule(); - - await setup.weth.connect(owner.wallet).approve(uniswapSetup.router.address, MAX_UINT_256); - await setup.dai.connect(owner.wallet).approve(uniswapSetup.router.address, MAX_UINT_256); - + await setup.weth.connect(owner.wallet) + .approve(uniswapSetup.router.address, MAX_UINT_256); + await setup.dai.connect(owner.wallet) + .approve(uniswapSetup.router.address, MAX_UINT_256); await uniswapSetup.router.connect(owner.wallet).addLiquidity( setup.weth.address, setup.dai.address, @@ -110,25 +66,29 @@ describe("UniswapV2AmmAdapter", () => { owner.address, MAX_UINT_256 ); + + ammModule = await deployer.modules.deployAmmModule(setup.controller.address); + await setup.controller.addModule(ammModule.address); + + uniswapV2AmmAdapter = await deployer.adapters.deployUniswapV2AmmAdapter(uniswapSetup.router.address); + uniswapV2AmmAdapterName = "UNISWAPV2AMM"; + + await setup.integrationRegistry.addIntegration( + ammModule.address, + uniswapV2AmmAdapterName, + uniswapV2AmmAdapter.address + ); }); addSnapshotBeforeRestoreAfterEach(); describe("constructor", async () => { - let subjectUniswapRouter: Address; - - beforeEach(async () => { - subjectUniswapRouter = uniswapSetup.router.address; - }); - async function subject(): Promise { - return await deployer.adapters.deployUniswapV2AmmAdapter(subjectUniswapRouter); + return await uniswapV2AmmAdapter.router(); } it("should have the correct router address", async () => { - const deployedUniswapV2AmmAdapter = await subject(); - - const actualRouterAddress = await deployedUniswapV2AmmAdapter.router(); + const actualRouterAddress = await subject(); expect(actualRouterAddress).to.eq(uniswapSetup.router.address); }); }); @@ -141,14 +101,14 @@ describe("UniswapV2AmmAdapter", () => { it("should return the correct spender address", async () => { const spender = await subject(); - expect(spender).to.eq(uniswapSetup.router.address); + expect(spender).to.eq(uniswapV2AmmAdapter.address); }); }); describe("isValidPool", async () => { let poolAddress: Address; - beforeEach(async () => { + before(async () => { poolAddress = uniswapSetup.wethDaiPool.address; }); @@ -162,7 +122,7 @@ describe("UniswapV2AmmAdapter", () => { }); describe("when the pool address is invalid", async () => { - beforeEach(async () => { + before(async () => { poolAddress = uniswapSetup.router.address; }); @@ -203,100 +163,267 @@ describe("UniswapV2AmmAdapter", () => { }); describe("getProvideLiquidityCalldata", async () => { - let component0: Address; - let component1: Address; - let component0Quantity: BigNumber; - let component1Quantity: BigNumber; - let minimumComponent0Quantity: BigNumber; - let minimumComponent1Quantity: BigNumber; - let liquidity: BigNumber; - - beforeEach(async () => { - component0 = await uniswapSetup.wethDaiPool.token0(); - component1 = await uniswapSetup.wethDaiPool.token1(); - const [reserve0, reserve1] = await uniswapSetup.wethDaiPool.getReserves(); - if ( setup.dai.address == component0 ) { - component1Quantity = ether(1); // 1 WETH - component0Quantity = reserve0.mul(component1Quantity).div(reserve1); - } - else { - component0Quantity = ether(1); // 1 WETH - component1Quantity = reserve1.mul(component0Quantity).div(reserve0); - } - const totalSupply = await uniswapSetup.wethDaiPool.totalSupply(); - const component0Liquidity = component0Quantity.mul(totalSupply).div(reserve0); - const component1Liquidity = component1Quantity.mul(totalSupply).div(reserve1); - liquidity = component0Liquidity < component1Liquidity ? component0Liquidity : component1Liquidity; - minimumComponent0Quantity = liquidity.mul(reserve0).div(totalSupply); - minimumComponent1Quantity = liquidity.mul(reserve1).div(totalSupply); + let subjectAmmPool: Address; + let subjectComponents: Address[]; + let subjectMaxTokensIn: BigNumber[]; + let subjectMinLiquidity: BigNumber; + + before(async () => { + subjectAmmPool = uniswapSetup.wethDaiPool.address; + subjectComponents = [setup.weth.address, setup.dai.address]; + subjectMaxTokensIn = [ether(1), ether(3000)]; + subjectMinLiquidity = ether(1); }); async function subject(): Promise { return await uniswapV2AmmAdapter.getProvideLiquidityCalldata( - uniswapSetup.wethDaiPool.address, - [component0, component1], - [component0Quantity, component1Quantity], - liquidity); + subjectAmmPool, + subjectComponents, + subjectMaxTokensIn, + subjectMinLiquidity); } it("should return the correct provide liquidity calldata", async () => { const calldata = await subject(); - const callTimestamp = await getLastBlockTimestamp(); - - const expectedCallData = uniswapSetup.router.interface.encodeFunctionData("addLiquidity", [ - component0, - component1, - component0Quantity, - component1Quantity, - minimumComponent0Quantity, - minimumComponent1Quantity, - owner.address, - callTimestamp, + + const expectedCallData = uniswapV2AmmAdapter.interface.encodeFunctionData("addLiquidity", [ + subjectAmmPool, + subjectComponents, + subjectMaxTokensIn, + subjectMinLiquidity, ]); - expect(JSON.stringify(calldata)).to.eq(JSON.stringify([uniswapSetup.router.address, ZERO, expectedCallData])); + expect(JSON.stringify(calldata)).to.eq(JSON.stringify([uniswapV2AmmAdapter.address, ZERO, expectedCallData])); }); }); describe("getRemoveLiquidityCalldata", async () => { - let component0: Address; - let component1: Address; - let component0Quantity: BigNumber; - let component1Quantity: BigNumber; - let liquidity: BigNumber; - - beforeEach(async () => { - component0 = await uniswapSetup.wethDaiPool.token0(); - component1 = await uniswapSetup.wethDaiPool.token1(); - liquidity = await uniswapSetup.wethDaiPool.balanceOf(owner.address); - const [reserve0, reserve1] = await uniswapSetup.wethDaiPool.getReserves(); - const totalSupply = await uniswapSetup.wethDaiPool.totalSupply(); - component0Quantity = reserve0.mul(liquidity).div(totalSupply); - component1Quantity = reserve1.mul(liquidity).div(totalSupply); + let subjectAmmPool: Address; + let subjectComponents: Address[]; + let subjectMaxTokensIn: BigNumber[]; + let subjectMinLiquidity: BigNumber; + + before(async () => { + subjectAmmPool = uniswapSetup.wethDaiPool.address; + subjectComponents = [setup.weth.address, setup.dai.address]; + subjectMaxTokensIn = [ether(1), ether(3000)]; + subjectMinLiquidity = ether(1); }); async function subject(): Promise { return await uniswapV2AmmAdapter.getRemoveLiquidityCalldata( - uniswapSetup.wethDaiPool.address, - [component0, component1], - [component0Quantity, component1Quantity], - liquidity); + subjectAmmPool, + subjectComponents, + subjectMaxTokensIn, + subjectMinLiquidity); } it("should return the correct remove liquidity calldata", async () => { const calldata = await subject(); - const callTimestamp = await getLastBlockTimestamp(); - - const expectedCallData = uniswapSetup.router.interface.encodeFunctionData("removeLiquidity", [ - component0, - component1, - liquidity, - component0Quantity, - component1Quantity, - owner.address, - callTimestamp, + + const expectedCallData = uniswapV2AmmAdapter.interface.encodeFunctionData("removeLiquidity", [ + subjectAmmPool, + subjectComponents, + subjectMaxTokensIn, + subjectMinLiquidity, ]); - expect(JSON.stringify(calldata)).to.eq(JSON.stringify([uniswapSetup.router.address, ZERO, expectedCallData])); + expect(JSON.stringify(calldata)).to.eq(JSON.stringify([uniswapV2AmmAdapter.address, ZERO, expectedCallData])); }); }); + context("Add and Remove Liquidity Tests", async () => { + let subjectCaller: Account; + let subjectSetToken: Address; + let subjectIntegrationName: string; + let subjectAmmPool: Address; + + let setToken: SetToken; + + context("when there is a deployed SetToken with enabled AmmModule", async () => { + before(async () => { + // Deploy a standard SetToken with the AMM Module + setToken = await setup.createSetToken( + [setup.weth.address, setup.dai.address], + [ether(1), ether(3000)], + [setup.issuanceModule.address, ammModule.address] + ); + + await setup.issuanceModule.initialize(setToken.address, ADDRESS_ZERO); + await ammModule.initialize(setToken.address); + + // Mint some instances of the SetToken + await setup.approveAndIssueSetToken(setToken, ether(1)); + }); + + describe("#addLiquidity", async () => { + let subjectComponentsToInput: Address[]; + let subjectMaxComponentQuantities: BigNumber[]; + let subjectMinPoolTokensToMint: BigNumber; + + beforeEach(async () => { + subjectSetToken = setToken.address; + subjectIntegrationName = uniswapV2AmmAdapterName; + subjectAmmPool = uniswapSetup.wethDaiPool.address; + subjectComponentsToInput = [setup.weth.address, setup.dai.address]; + subjectMaxComponentQuantities = [ether(1), ether(3000)]; + subjectCaller = owner; + const totalSupply = await uniswapSetup.wethDaiPool.totalSupply(); + const [reserve0, reserve1, ] = await uniswapSetup.wethDaiPool.getReserves(); + const token0 = await uniswapSetup.wethDaiPool.token0(); + if ( token0 == setup.weth.address ) { + const liquidity0 = ether(1).mul(totalSupply).div(reserve0); + const liquidity1 = ether(3000).mul(totalSupply).div(reserve1); + subjectMinPoolTokensToMint = liquidity0 < liquidity1 ? liquidity0 : liquidity1; + } + else { + const liquidity0 = ether(3000).mul(totalSupply).div(reserve0); + const liquidity1 = ether(1).mul(totalSupply).div(reserve1); + subjectMinPoolTokensToMint = liquidity0 < liquidity1 ? liquidity0 : liquidity1; + } + }); + + async function subject(): Promise { + return await ammModule.connect(subjectCaller.wallet).addLiquidity( + subjectSetToken, + subjectIntegrationName, + subjectAmmPool, + subjectMinPoolTokensToMint, + subjectComponentsToInput, + subjectMaxComponentQuantities, + ); + } + + it("should mint the liquidity token to the caller", async () => { + await subject(); + const liquidityTokenBalance = await uniswapSetup.wethDaiPool.balanceOf(subjectSetToken); + expect(liquidityTokenBalance).to.eq(subjectMinPoolTokensToMint); + }); + + it("should update the positions properly", async () => { + await subject(); + const positions = await setToken.getPositions(); + expect(positions.length).to.eq(1); + expect(positions[0].component).to.eq(subjectAmmPool); + expect(positions[0].unit).to.eq(subjectMinPoolTokensToMint); + }); + + describe("when insufficient liquidity tokens are received", async () => { + beforeEach(async () => { + subjectMinPoolTokensToMint = subjectMinPoolTokensToMint.mul(2); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("_minLiquidity is too high for amount minimums"); + }); + }); + + shouldRevertIfPoolIsNotSupported(subject); + }); + + }); + + context("when there is a deployed SetToken with enabled AmmModule", async () => { + before(async () => { + // Deploy a standard SetToken with the AMM Module + setToken = await setup.createSetToken( + [uniswapSetup.wethDaiPool.address], + [ether(1)], + [setup.issuanceModule.address, ammModule.address] + ); + + await setup.issuanceModule.initialize(setToken.address, ADDRESS_ZERO); + await ammModule.initialize(setToken.address); + + // Mint some instances of the SetToken + await setup.approveAndIssueSetToken(setToken, ether(1)); + }); + + describe("#removeLiquidity", async () => { + let subjectComponentsToOutput: Address[]; + let subjectMinComponentQuantities: BigNumber[]; + let subjectPoolTokens: BigNumber; + + beforeEach(async () => { + subjectSetToken = setToken.address; + subjectIntegrationName = uniswapV2AmmAdapterName; + subjectAmmPool = uniswapSetup.wethDaiPool.address; + subjectComponentsToOutput = [setup.weth.address, setup.dai.address]; + const totalSupply = await uniswapSetup.wethDaiPool.totalSupply(); + const [reserve0, reserve1, ] = await uniswapSetup.wethDaiPool.getReserves(); + subjectPoolTokens = ether(1); + const token0 = await uniswapSetup.wethDaiPool.token0(); + if ( token0 == setup.weth.address ) { + const weth = subjectPoolTokens.mul(reserve0).div(totalSupply); + const dai = subjectPoolTokens.mul(reserve1).div(totalSupply); + subjectMinComponentQuantities = [weth, dai]; + } + else { + const dai = subjectPoolTokens.mul(reserve0).div(totalSupply); + const weth = subjectPoolTokens.mul(reserve1).div(totalSupply); + subjectMinComponentQuantities = [weth, dai]; + } + subjectCaller = owner; + }); + + async function subject(): Promise { + return await ammModule.connect(subjectCaller.wallet).removeLiquidity( + subjectSetToken, + subjectIntegrationName, + subjectAmmPool, + subjectPoolTokens, + subjectComponentsToOutput, + subjectMinComponentQuantities, + ); + } + + it("should reduce the liquidity token of the caller", async () => { + const previousLiquidityTokenBalance = await uniswapSetup.wethDaiPool.balanceOf(subjectSetToken); + + await subject(); + const liquidityTokenBalance = await uniswapSetup.wethDaiPool.balanceOf(subjectSetToken); + const expectedLiquidityBalance = previousLiquidityTokenBalance.sub(subjectPoolTokens); + expect(liquidityTokenBalance).to.eq(expectedLiquidityBalance); + }); + + it("should update the positions properly", async () => { + await subject(); + const positions = await setToken.getPositions(); + + expect(positions.length).to.eq(2); + + expect(positions[0].component).to.eq(setup.weth.address); + expect(positions[0].unit).to.eq(subjectMinComponentQuantities[0]); + expect(positions[1].component).to.eq(setup.dai.address); + expect(positions[1].unit).to.eq(subjectMinComponentQuantities[1]); + }); + + describe("when more underlying tokens are requested than owned", async () => { + beforeEach(async () => { + subjectMinComponentQuantities = [subjectMinComponentQuantities[0].mul(2), + subjectMinComponentQuantities[1]]; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("amounts must be <= ownedTokens"); + }); + }); + + shouldRevertIfPoolIsNotSupported(subject); + }); + + }); + + function shouldRevertIfPoolIsNotSupported(subject: any) { + describe("when the pool is not supported on the adapter", async () => { + beforeEach(async () => { + subjectAmmPool = setup.wbtc.address; + }); + + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Pool token must be enabled on the Adapter"); + }); + }); + } + }); + }); From 0c7f502a41727fb63b11101e60a50cb20ff20080 Mon Sep 17 00:00:00 2001 From: Stephen Hankinson Date: Fri, 20 Aug 2021 20:43:10 -0300 Subject: [PATCH 07/27] Fix the < check --- test/protocol/integration/amm/UniswapV2AmmAdapter.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/protocol/integration/amm/UniswapV2AmmAdapter.spec.ts b/test/protocol/integration/amm/UniswapV2AmmAdapter.spec.ts index c8d45f9e2..922ff6c1d 100644 --- a/test/protocol/integration/amm/UniswapV2AmmAdapter.spec.ts +++ b/test/protocol/integration/amm/UniswapV2AmmAdapter.spec.ts @@ -272,12 +272,12 @@ describe("UniswapV2AmmAdapter", () => { if ( token0 == setup.weth.address ) { const liquidity0 = ether(1).mul(totalSupply).div(reserve0); const liquidity1 = ether(3000).mul(totalSupply).div(reserve1); - subjectMinPoolTokensToMint = liquidity0 < liquidity1 ? liquidity0 : liquidity1; + subjectMinPoolTokensToMint = liquidity0.lt(liquidity1) ? liquidity0 : liquidity1; } else { const liquidity0 = ether(3000).mul(totalSupply).div(reserve0); const liquidity1 = ether(1).mul(totalSupply).div(reserve1); - subjectMinPoolTokensToMint = liquidity0 < liquidity1 ? liquidity0 : liquidity1; + subjectMinPoolTokensToMint = liquidity0.lt(liquidity1) ? liquidity0 : liquidity1; } }); From fb1e6a27487ee7dd52ce82091adda1e71cfd9aaa Mon Sep 17 00:00:00 2001 From: Stephen Hankinson Date: Sat, 21 Aug 2021 00:26:25 -0300 Subject: [PATCH 08/27] Test for token refund when required --- .../amm/UniswapV2AmmAdapter.spec.ts | 88 ++++++++++++++++++- 1 file changed, 87 insertions(+), 1 deletion(-) diff --git a/test/protocol/integration/amm/UniswapV2AmmAdapter.spec.ts b/test/protocol/integration/amm/UniswapV2AmmAdapter.spec.ts index 922ff6c1d..15674128f 100644 --- a/test/protocol/integration/amm/UniswapV2AmmAdapter.spec.ts +++ b/test/protocol/integration/amm/UniswapV2AmmAdapter.spec.ts @@ -239,7 +239,7 @@ describe("UniswapV2AmmAdapter", () => { let setToken: SetToken; context("when there is a deployed SetToken with enabled AmmModule", async () => { - before(async () => { + beforeEach(async () => { // Deploy a standard SetToken with the AMM Module setToken = await setup.createSetToken( [setup.weth.address, setup.dai.address], @@ -316,6 +316,92 @@ describe("UniswapV2AmmAdapter", () => { }); }); + describe("when extra dai tokens are supplied", async () => { + let wethRemaining: BigNumber; + let daiRemaining: BigNumber; + beforeEach(async () => { + wethRemaining = ether(0.5); + subjectMaxComponentQuantities = [ether(0.5), ether(1600)]; + const totalSupply = await uniswapSetup.wethDaiPool.totalSupply(); + const [reserve0, reserve1, ] = await uniswapSetup.wethDaiPool.getReserves(); + const token0 = await uniswapSetup.wethDaiPool.token0(); + if ( token0 == setup.weth.address ) { + const liquidity0 = ether(0.5).mul(totalSupply).div(reserve0); + const liquidity1 = ether(1600).mul(totalSupply).div(reserve1); + subjectMinPoolTokensToMint = liquidity0.lt(liquidity1) ? liquidity0 : liquidity1; + daiRemaining = ether(3000).sub(ether(0.5).mul(reserve1).div(reserve0)); + } + else { + const liquidity0 = ether(1600).mul(totalSupply).div(reserve0); + const liquidity1 = ether(0.5).mul(totalSupply).div(reserve1); + subjectMinPoolTokensToMint = liquidity0.lt(liquidity1) ? liquidity0 : liquidity1; + daiRemaining = ether(3000).sub(ether(0.5).mul(reserve0).div(reserve1)); + } + + }); + + it("should mint the correct amount of liquidity tokens to the caller", async () => { + await subject(); + const liquidityTokenBalance = await uniswapSetup.wethDaiPool.balanceOf(subjectSetToken); + expect(liquidityTokenBalance).to.eq(subjectMinPoolTokensToMint); + }); + + it("should have the expected weth, dai, and lp tokens", async () => { + await subject(); + const positions = await setToken.getPositions(); + expect(positions.length).to.eq(3); + expect(positions[0].component).to.eq(setup.weth.address); + expect(positions[0].unit).to.eq(wethRemaining); + expect(positions[1].component).to.eq(setup.dai.address); + expect(positions[1].unit).to.eq(daiRemaining); + expect(positions[2].component).to.eq(subjectAmmPool); + expect(positions[2].unit).to.eq(subjectMinPoolTokensToMint); + }); + }); + + describe("when extra weth tokens are supplied", async () => { + let wethRemaining: BigNumber; + let daiRemaining: BigNumber; + beforeEach(async () => { + daiRemaining = ether(1500); + subjectMaxComponentQuantities = [ether(0.6), ether(1500)]; + const totalSupply = await uniswapSetup.wethDaiPool.totalSupply(); + const [reserve0, reserve1, ] = await uniswapSetup.wethDaiPool.getReserves(); + const token0 = await uniswapSetup.wethDaiPool.token0(); + if ( token0 == setup.weth.address ) { + const liquidity0 = ether(0.6).mul(totalSupply).div(reserve0); + const liquidity1 = ether(1500).mul(totalSupply).div(reserve1); + subjectMinPoolTokensToMint = liquidity0.lt(liquidity1) ? liquidity0 : liquidity1; + wethRemaining = ether(1).sub(ether(1500).mul(reserve0).div(reserve1)); + } + else { + const liquidity0 = ether(1500).mul(totalSupply).div(reserve0); + const liquidity1 = ether(0.6).mul(totalSupply).div(reserve1); + subjectMinPoolTokensToMint = liquidity0.lt(liquidity1) ? liquidity0 : liquidity1; + wethRemaining = ether(1).sub(ether(1500).mul(reserve1).div(reserve0)); + } + + }); + + it("should mint the correct amount of liquidity tokens to the caller", async () => { + await subject(); + const liquidityTokenBalance = await uniswapSetup.wethDaiPool.balanceOf(subjectSetToken); + expect(liquidityTokenBalance).to.eq(subjectMinPoolTokensToMint); + }); + + it("should have the expected weth, dai, and lp tokens", async () => { + await subject(); + const positions = await setToken.getPositions(); + expect(positions.length).to.eq(3); + expect(positions[0].component).to.eq(setup.weth.address); + expect(positions[0].unit).to.eq(wethRemaining); + expect(positions[1].component).to.eq(setup.dai.address); + expect(positions[1].unit).to.eq(daiRemaining); + expect(positions[2].component).to.eq(subjectAmmPool); + expect(positions[2].unit).to.eq(subjectMinPoolTokensToMint); + }); + }); + shouldRevertIfPoolIsNotSupported(subject); }); From 9a4d30a76e4a5a16b2624c1ed32d9fc58dcb1fab Mon Sep 17 00:00:00 2001 From: Stephen Hankinson Date: Sat, 21 Aug 2021 10:46:23 -0300 Subject: [PATCH 09/27] Re-write isValidPool for higher code coverage --- .../integration/amm/UniswapV2AmmAdapter.sol | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol b/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol index f6219e36e..52ad36524 100644 --- a/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol +++ b/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol @@ -215,15 +215,15 @@ contract UniswapV2AmmAdapter is IAmmAdapter { function isValidPool(address _pool) external view override returns (bool) { address token0; address token1; - IUniswapV2Pair pair = IUniswapV2Pair(_pool); - try pair.token0() returns (address _token0) { - token0 = _token0; - } catch { - return false; + // solhint-disable-next-line avoid-low-level-calls + (bool token0Success, bytes memory token0ReturnData) = _pool.staticcall(abi.encodeWithSignature("token0()")); + // solhint-disable-next-line avoid-low-level-calls + (bool token1Success, bytes memory token1ReturnData) = _pool.staticcall(abi.encodeWithSignature("token1()")); + if( token0Success && token1Success ) { + (token0) = abi.decode(token0ReturnData, (address)); + (token1) = abi.decode(token1ReturnData, (address)); } - try pair.token1() returns (address _token1) { - token1 = _token1; - } catch { + else { return false; } return factory.getPair(token0, token1) == _pool; From a073df320fe953df23e93b8137bdb47521901232 Mon Sep 17 00:00:00 2001 From: Stephen Hankinson Date: Sat, 21 Aug 2021 11:40:38 -0300 Subject: [PATCH 10/27] Avoid using low level functions --- .../integration/amm/UniswapV2AmmAdapter.sol | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol b/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol index 52ad36524..fe00a18ff 100644 --- a/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol +++ b/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol @@ -215,18 +215,27 @@ contract UniswapV2AmmAdapter is IAmmAdapter { function isValidPool(address _pool) external view override returns (bool) { address token0; address token1; - // solhint-disable-next-line avoid-low-level-calls - (bool token0Success, bytes memory token0ReturnData) = _pool.staticcall(abi.encodeWithSignature("token0()")); - // solhint-disable-next-line avoid-low-level-calls - (bool token1Success, bytes memory token1ReturnData) = _pool.staticcall(abi.encodeWithSignature("token1()")); - if( token0Success && token1Success ) { - (token0) = abi.decode(token0ReturnData, (address)); - (token1) = abi.decode(token1ReturnData, (address)); + bool success = true; + IUniswapV2Pair pair = IUniswapV2Pair(_pool); + + try pair.token0() returns (address _token0) { + token0 = _token0; + } catch { + success = false; + } + + try pair.token1() returns (address _token1) { + token1 = _token1; + } catch { + success = false; + } + + if( success ) { + return factory.getPair(token0, token1) == _pool; } else { return false; } - return factory.getPair(token0, token1) == _pool; } /* ============ External Setter Functions ============ */ From be15e857bca2575294790665ce4d23642d41b2c5 Mon Sep 17 00:00:00 2001 From: Stephen Hankinson Date: Mon, 23 Aug 2021 00:02:19 -0300 Subject: [PATCH 11/27] Add more tests to cover all branches --- .../integration/amm/UniswapV2AmmAdapter.sol | 2 +- .../amm/UniswapV2AmmAdapter.spec.ts | 287 +++++++++++++++++- 2 files changed, 279 insertions(+), 10 deletions(-) diff --git a/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol b/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol index fe00a18ff..b574236b0 100644 --- a/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol +++ b/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol @@ -275,7 +275,7 @@ contract UniswapV2AmmAdapter is IAmmAdapter { getPosition(pair, _components[0], _maxTokensIn[0], _components[1], _maxTokensIn[1]); uint256 lpTotalSupply = pair.totalSupply(); - require(lpTotalSupply >= 0, "_pool totalSupply must be > 0"); + require(lpTotalSupply > 0, "_pool totalSupply must be > 0"); (uint256 reserve0, uint256 reserve1, ) = pair.getReserves(); uint256 amount0Min = reserve0.mul(_minLiquidity).div(lpTotalSupply); diff --git a/test/protocol/integration/amm/UniswapV2AmmAdapter.spec.ts b/test/protocol/integration/amm/UniswapV2AmmAdapter.spec.ts index 15674128f..821d92992 100644 --- a/test/protocol/integration/amm/UniswapV2AmmAdapter.spec.ts +++ b/test/protocol/integration/amm/UniswapV2AmmAdapter.spec.ts @@ -19,6 +19,7 @@ import { getAccounts, getSystemFixture, getUniswapFixture, + getUniswapV3Fixture, getWaffleExpect } from "@utils/test/index"; @@ -94,15 +95,32 @@ describe("UniswapV2AmmAdapter", () => { }); describe("getSpenderAddress", async () => { + let spenderAddress: Address; + + before(async () => { + spenderAddress = uniswapSetup.wethDaiPool.address; + }); + async function subject(): Promise { - return await uniswapV2AmmAdapter.getSpenderAddress(uniswapSetup.wethDaiPool.address); + return await uniswapV2AmmAdapter.getSpenderAddress(spenderAddress); } it("should return the correct spender address", async () => { const spender = await subject(); - expect(spender).to.eq(uniswapV2AmmAdapter.address); }); + + describe("when the pool address is invalid", async () => { + before(async () => { + const uniswapV3Setup = getUniswapV3Fixture(owner.address); + await uniswapV3Setup.initialize(owner, setup.weth, 3000.0, setup.wbtc, 40000.0, setup.dai); + spenderAddress = uniswapV3Setup.swapRouter.address; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("_pool factory doesn't match the router factory"); + }); + }); }); describe("isValidPool", async () => { @@ -196,25 +214,154 @@ describe("UniswapV2AmmAdapter", () => { }); }); - describe("getRemoveLiquidityCalldata", async () => { + describe("addLiquidity", async () => { let subjectAmmPool: Address; let subjectComponents: Address[]; let subjectMaxTokensIn: BigNumber[]; let subjectMinLiquidity: BigNumber; - before(async () => { + beforeEach(async () => { subjectAmmPool = uniswapSetup.wethDaiPool.address; subjectComponents = [setup.weth.address, setup.dai.address]; subjectMaxTokensIn = [ether(1), ether(3000)]; - subjectMinLiquidity = ether(1); + const totalSupply = await uniswapSetup.wethDaiPool.totalSupply(); + const [reserve0, reserve1, ] = await uniswapSetup.wethDaiPool.getReserves(); + const token0 = await uniswapSetup.wethDaiPool.token0(); + if ( token0 == setup.weth.address ) { + const liquidity0 = subjectMaxTokensIn[0].mul(totalSupply).div(reserve0); + const liquidity1 = subjectMaxTokensIn[1].mul(totalSupply).div(reserve1); + subjectMinLiquidity = liquidity0.lt(liquidity1) ? liquidity0 : liquidity1; + } + else { + const liquidity0 = subjectMaxTokensIn[1].mul(totalSupply).div(reserve0); + const liquidity1 = subjectMaxTokensIn[0].mul(totalSupply).div(reserve1); + subjectMinLiquidity = liquidity0.lt(liquidity1) ? liquidity0 : liquidity1; + } + await setup.weth.connect(owner.wallet) + .approve(uniswapV2AmmAdapter.address, MAX_UINT_256); + await setup.dai.connect(owner.wallet) + .approve(uniswapV2AmmAdapter.address, MAX_UINT_256); + }); + + async function subject(): Promise { + return await uniswapV2AmmAdapter.addLiquidity( + subjectAmmPool, + subjectComponents, + subjectMaxTokensIn, + subjectMinLiquidity); + } + + it("should add the correct liquidity", async () => { + const totalSupply = await uniswapSetup.wethDaiPool.totalSupply(); + const [reserve0, reserve1, ] = await uniswapSetup.wethDaiPool.getReserves(); + await subject(); + const updatedTotalSupply = await uniswapSetup.wethDaiPool.totalSupply(); + const [updatedReserve0, updatedReserve1, ] = await uniswapSetup.wethDaiPool.getReserves(); + expect(updatedTotalSupply).to.eq(totalSupply.add(subjectMinLiquidity)); + const token0 = await uniswapSetup.wethDaiPool.token0(); + if ( token0 == setup.weth.address ) { + expect(updatedReserve0).to.eq(reserve0.add(subjectMaxTokensIn[0])); + expect(updatedReserve1).to.eq(reserve1.add(subjectMaxTokensIn[1])); + } + else { + expect(updatedReserve0).to.eq(reserve0.add(subjectMaxTokensIn[1])); + expect(updatedReserve1).to.eq(reserve1.add(subjectMaxTokensIn[0])); + } + }); + + describe("when the pool address is invalid", async () => { + beforeEach(async () => { + const uniswapV3Setup = getUniswapV3Fixture(owner.address); + await uniswapV3Setup.initialize(owner, setup.weth, 3000.0, setup.wbtc, 40000.0, setup.dai); + subjectAmmPool = uniswapV3Setup.swapRouter.address; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("_pool factory doesn't match the router factory"); + }); + }); + + describe("when the _components length is invalid", async () => { + beforeEach(async () => { + subjectComponents = [setup.weth.address]; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("_components length is invalid"); + }); + }); + + describe("when the _maxTokensIn length is invalid", async () => { + beforeEach(async () => { + subjectMaxTokensIn = [ether(1)]; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("_maxTokensIn length is invalid"); + }); + }); + + describe("when the _pool doesn't match the _components", async () => { + beforeEach(async () => { + subjectComponents = [setup.weth.address, setup.wbtc.address]; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("_pool doesn't match the components"); + }); + }); + + describe("when the _maxTokensIn[0] is 0", async () => { + beforeEach(async () => { + subjectMaxTokensIn = [ether(0), ether(3000)]; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("supplied token0 must be greater than 0"); + }); + }); + + describe("when the _maxTokensIn[1] is 0", async () => { + beforeEach(async () => { + subjectMaxTokensIn = [ether(1), ether(0)]; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("supplied token1 must be greater than 0"); + }); + }); + + describe("when the _pool totalSupply is 0", async () => { + beforeEach(async () => { + subjectAmmPool = uniswapSetup.wethWbtcPool.address; + subjectComponents = [setup.weth.address, setup.wbtc.address]; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("_pool totalSupply must be > 0"); + }); + }); + }); + + describe("getRemoveLiquidityCalldata", async () => { + let subjectAmmPool: Address; + let subjectComponents: Address[]; + let subjectMinTokensOut: BigNumber[]; + let subjectLiquidity: BigNumber; + + before(async () => { + subjectAmmPool = uniswapSetup.wethDaiPool.address; + subjectComponents = [setup.weth.address, setup.dai.address]; + subjectMinTokensOut = [ether(1), ether(3000)]; + subjectLiquidity = ether(1); }); async function subject(): Promise { return await uniswapV2AmmAdapter.getRemoveLiquidityCalldata( subjectAmmPool, subjectComponents, - subjectMaxTokensIn, - subjectMinLiquidity); + subjectMinTokensOut, + subjectLiquidity); } it("should return the correct remove liquidity calldata", async () => { @@ -223,13 +370,135 @@ describe("UniswapV2AmmAdapter", () => { const expectedCallData = uniswapV2AmmAdapter.interface.encodeFunctionData("removeLiquidity", [ subjectAmmPool, subjectComponents, - subjectMaxTokensIn, - subjectMinLiquidity, + subjectMinTokensOut, + subjectLiquidity, ]); expect(JSON.stringify(calldata)).to.eq(JSON.stringify([uniswapV2AmmAdapter.address, ZERO, expectedCallData])); }); }); + describe("removeLiquidity", async () => { + let subjectAmmPool: Address; + let subjectComponents: Address[]; + let subjectMinTokensOut: BigNumber[]; + let subjectLiquidity: BigNumber; + + beforeEach(async () => { + subjectAmmPool = uniswapSetup.wethDaiPool.address; + subjectComponents = [setup.weth.address, setup.dai.address]; + subjectLiquidity = await uniswapSetup.wethDaiPool.balanceOf(owner.address); + const token0 = await uniswapSetup.wethDaiPool.token0(); + const totalSupply = await uniswapSetup.wethDaiPool.totalSupply(); + const [reserve0, reserve1, ] = await uniswapSetup.wethDaiPool.getReserves(); + if ( token0 == setup.weth.address ) { + subjectMinTokensOut = [reserve0.mul(subjectLiquidity).div(totalSupply), + reserve1.mul(subjectLiquidity).div(totalSupply)]; + } + else { + subjectMinTokensOut = [reserve1.mul(subjectLiquidity).div(totalSupply), + reserve0.mul(subjectLiquidity).div(totalSupply)]; + } + await uniswapSetup.wethDaiPool.connect(owner.wallet) + .approve(uniswapV2AmmAdapter.address, MAX_UINT_256); + }); + + async function subject(): Promise { + return await uniswapV2AmmAdapter.removeLiquidity( + subjectAmmPool, + subjectComponents, + subjectMinTokensOut, + subjectLiquidity); + } + + it("should remove the correct liquidity", async () => { + const token0 = await uniswapSetup.wethDaiPool.token0(); + const [reserve0, reserve1, ] = await uniswapSetup.wethDaiPool.getReserves(); + await subject(); + expect(await uniswapSetup.wethDaiPool.balanceOf(owner.address)).to.be.eq(ZERO); + const [updatedReserve0, updatedReserve1, ] = await uniswapSetup.wethDaiPool.getReserves(); + if ( token0 == setup.weth.address ) { + expect(updatedReserve0).to.be.eq(reserve0.sub(subjectMinTokensOut[0])); + expect(updatedReserve1).to.be.eq(reserve1.sub(subjectMinTokensOut[1])); + } + else { + expect(updatedReserve0).to.be.eq(reserve0.sub(subjectMinTokensOut[1])); + expect(updatedReserve1).to.be.eq(reserve1.sub(subjectMinTokensOut[0])); + } + }); + + describe("when the pool address is invalid", async () => { + beforeEach(async () => { + const uniswapV3Setup = getUniswapV3Fixture(owner.address); + await uniswapV3Setup.initialize(owner, setup.weth, 3000.0, setup.wbtc, 40000.0, setup.dai); + subjectAmmPool = uniswapV3Setup.swapRouter.address; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("_pool factory doesn't match the router factory"); + }); + }); + + describe("when the _components length is invalid", async () => { + beforeEach(async () => { + subjectComponents = [setup.weth.address]; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("_components length is invalid"); + }); + }); + + describe("when the _minTokensOut length is invalid", async () => { + beforeEach(async () => { + subjectMinTokensOut = [ether(1)]; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("_minTokensOut length is invalid"); + }); + }); + + describe("when the _pool doesn't match the _components", async () => { + beforeEach(async () => { + subjectComponents = [setup.weth.address, setup.wbtc.address]; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("_pool doesn't match the components"); + }); + }); + + describe("when the _minTokensOut[0] is 0", async () => { + beforeEach(async () => { + subjectMinTokensOut = [ether(0), ether(3000)]; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("requested token0 must be greater than 0"); + }); + }); + + describe("when the _minTokensOut[1] is 0", async () => { + beforeEach(async () => { + subjectMinTokensOut = [ether(1), ether(0)]; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("requested token1 must be greater than 0"); + }); + }); + + describe("when the _liquidity is more than available", async () => { + beforeEach(async () => { + subjectLiquidity = (await uniswapSetup.wethDaiPool.balanceOf(owner.address)).add(ether(1)); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("_liquidity must be <= to current balance"); + }); + }); + }); + context("Add and Remove Liquidity Tests", async () => { let subjectCaller: Account; let subjectSetToken: Address; From 790ed679ac753b696e46e42cc1812df952d06015 Mon Sep 17 00:00:00 2001 From: Stephen Hankinson Date: Mon, 23 Aug 2021 13:59:01 -0300 Subject: [PATCH 12/27] Allow liquidity addition with single asset --- .../integration/amm/UniswapV2AmmAdapter.sol | 166 ++++++++++- .../amm/UniswapV2AmmAdapter.spec.ts | 261 +++++++++++++++++- 2 files changed, 408 insertions(+), 19 deletions(-) diff --git a/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol b/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol index b574236b0..95e9ed497 100644 --- a/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol +++ b/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol @@ -47,10 +47,13 @@ contract UniswapV2AmmAdapter is IAmmAdapter { IUniswapV2Router public immutable router; IUniswapV2Factory public immutable factory; - // Uniswap router function string for adding liquidity + // Internal function string for adding liquidity string internal constant ADD_LIQUIDITY = "addLiquidity(address,address[],uint256[],uint256)"; - // Uniswap router function string for removing liquidity + // Internal function string for adding liquidity with a single asset + string internal constant ADD_LIQUIDITY_SINGLE_ASSET = + "addLiquiditySingleAsset(address,address,uint256,uint256)"; + // Internal function string for removing liquidity string internal constant REMOVE_LIQUIDITY = "removeLiquidity(address,address[],uint256[],uint256)"; @@ -94,6 +97,54 @@ contract UniswapV2AmmAdapter is IAmmAdapter { Position(IERC20(_token1), _amount1, IERC20(_token0), _amount0); } + /** + * Performs a swap via the Uniswap V2 Router + * + * @param pair Uniswap V2 Pair + * @param token Address of the token to swap + * @param token0 Address of pair token0 + * @param token1 Address of pair token1 + * @param amount Amount of the token to swap + */ + function performSwap( + IUniswapV2Pair pair, + address token, + address token0, + address token1, + uint256 amount + ) + internal + returns ( + Position memory position + ) + { + + // Use half of the provided amount in the swap + uint256 amountToSwap = amount.div(2); + + // Approve the router to spend the tokens + IERC20(token).approve(address(router), amountToSwap); + + // Determine the amount of the other token to get + (uint256 reserve0, uint256 reserve1, ) = pair.getReserves(); + uint256 amountOut = router.getAmountOut(amountToSwap, token0 == token ? reserve0 : reserve1, + token0 == token ? reserve1 : reserve0); + + address[] memory path = new address[](2); + path[0] = token; + path[1] = token == token0 ? token1 : token0; + uint[] memory amounts = router.swapExactTokensForTokens( + amountToSwap, + amountOut, + path, + address(this), + block.timestamp // solhint-disable-line not-rely-on-time + ); + + position = getPosition(pair, path[0], amount.sub(amountToSwap), path[1], amounts[1]); + + } + /* ============ External Getter Functions ============ */ /** @@ -130,22 +181,38 @@ contract UniswapV2AmmAdapter is IAmmAdapter { ); } + /** + * Return calldata for the add liquidity call for a single asset + * + * @param _pool Address of liquidity token + * @param _component Address of the token used to add liquidity + * @param _maxTokenIn AmountsIn desired to add liquidity + * @param _minLiquidity Min liquidity amount to add + */ function getProvideLiquiditySingleAssetCalldata( - address, - address, - uint256, - uint256 + address _pool, + address _component, + uint256 _maxTokenIn, + uint256 _minLiquidity ) external view override returns ( - address, - uint256, - bytes memory + address _target, + uint256 _value, + bytes memory _calldata ) { - revert("Single asset liquidity addition not supported"); + _target = address(this); + _value = 0; + _calldata = abi.encodeWithSignature( + ADD_LIQUIDITY_SINGLE_ASSET, + _pool, + _component, + _maxTokenIn, + _minLiquidity + ); } /** @@ -270,6 +337,7 @@ contract UniswapV2AmmAdapter is IAmmAdapter { "_pool doesn't match the components"); require(_maxTokensIn[0] > 0, "supplied token0 must be greater than 0"); require(_maxTokensIn[1] > 0, "supplied token1 must be greater than 0"); + require(_minLiquidity > 0, "_minLiquidity must be greater than 0"); Position memory position = getPosition(pair, _components[0], _maxTokensIn[0], _components[1], _maxTokensIn[1]); @@ -282,7 +350,7 @@ contract UniswapV2AmmAdapter is IAmmAdapter { uint256 amount1Min = reserve1.mul(_minLiquidity).div(lpTotalSupply); require(amount0Min <= position.amount0 && amount1Min <= position.amount1, - "_minLiquidity is too high for amount minimums"); + "_minLiquidity is too high for amount maximums"); // Bring the tokens to this contract so we can use the Uniswap Router position.token0.transferFrom(msg.sender, address(this), position.amount0); @@ -316,6 +384,81 @@ contract UniswapV2AmmAdapter is IAmmAdapter { } + /** + * Adds liquidity via the Uniswap V2 Router, swapping first to get both tokens + * + * @param _pool Address of liquidity token + * @param _component Address array required to add liquidity + * @param _maxTokenIn AmountsIn desired to add liquidity + * @param _minLiquidity Min liquidity amount to add + */ + function addLiquiditySingleAsset( + address _pool, + address _component, + uint256 _maxTokenIn, + uint256 _minLiquidity + ) + external + returns ( + uint amountA, + uint amountB, + uint liquidity + ) + { + + IUniswapV2Pair pair = IUniswapV2Pair(_pool); + require(factory == IUniswapV2Factory(pair.factory()), "_pool factory doesn't match the router factory"); + + address token0 = pair.token0(); + address token1 = pair.token1(); + require(token0 == _component || token1 == _component, "_pool doesn't contain the _component"); + require(_maxTokenIn > 0, "supplied _maxTokenIn must be greater than 0"); + require(_minLiquidity > 0, "supplied _minLiquidity must be greater than 0"); + + uint256 lpTotalSupply = pair.totalSupply(); + require(lpTotalSupply > 0, "_pool totalSupply must be > 0"); + + // Bring the tokens to this contract so we can use the Uniswap Router + IERC20(_component).transferFrom(msg.sender, address(this), _maxTokenIn); + + // Execute the swap + Position memory position = performSwap(pair, _component, token0, token1, _maxTokenIn); + + (uint256 reserve0, uint256 reserve1, ) = pair.getReserves(); + uint256 amount0Min = reserve0.mul(_minLiquidity).div(lpTotalSupply); + uint256 amount1Min = reserve1.mul(_minLiquidity).div(lpTotalSupply); + + require(amount0Min <= position.amount0 && amount1Min <= position.amount1, + "_minLiquidity is too high for amount maximum"); + + // Approve the router to spend the tokens + position.token0.approve(address(router), position.amount0); + position.token1.approve(address(router), position.amount1); + + // Add the liquidity + (amountA, amountB, liquidity) = router.addLiquidity( + address(position.token0), + address(position.token1), + position.amount0, + position.amount1, + amount0Min, + amount1Min, + msg.sender, + block.timestamp // solhint-disable-line not-rely-on-time + ); + + // If there is token0 left, send it back + if( amountA < position.amount0 ) { + position.token0.transfer(msg.sender, position.amount0.sub(amountA) ); + } + + // If there is token1 left, send it back + if( amountB < position.amount1 ) { + position.token1.transfer(msg.sender, position.amount1.sub(amountB) ); + } + + } + /** * Remove liquidity via the Uniswap V2 Router * @@ -344,6 +487,7 @@ contract UniswapV2AmmAdapter is IAmmAdapter { "_pool doesn't match the components"); require(_minTokensOut[0] > 0, "requested token0 must be greater than 0"); require(_minTokensOut[1] > 0, "requested token1 must be greater than 0"); + require(_liquidity > 0, "_liquidity must be greater than 0"); Position memory position = getPosition(pair, _components[0], _minTokensOut[0], _components[1], _minTokensOut[1]); diff --git a/test/protocol/integration/amm/UniswapV2AmmAdapter.spec.ts b/test/protocol/integration/amm/UniswapV2AmmAdapter.spec.ts index 821d92992..356be29b9 100644 --- a/test/protocol/integration/amm/UniswapV2AmmAdapter.spec.ts +++ b/test/protocol/integration/amm/UniswapV2AmmAdapter.spec.ts @@ -153,16 +153,36 @@ describe("UniswapV2AmmAdapter", () => { }); describe("getProvideLiquiditySingleAssetCalldata", async () => { + let subjectAmmPool: Address; + let subjectComponent: Address; + let subjectMaxTokenIn: BigNumber; + let subjectMinLiquidity: BigNumber; + + before(async () => { + subjectAmmPool = uniswapSetup.wethDaiPool.address; + subjectComponent = setup.weth.address; + subjectMaxTokenIn = ether(1); + subjectMinLiquidity = ether(1); + }); + async function subject(): Promise { return await uniswapV2AmmAdapter.getProvideLiquiditySingleAssetCalldata( - uniswapSetup.wethDaiPool.address, - setup.weth.address, - ether(1), - ether(1)); + subjectAmmPool, + subjectComponent, + subjectMaxTokenIn, + subjectMinLiquidity); } - it("should not support adding a single asset", async () => { - await expect(subject()).to.be.revertedWith("Single asset liquidity addition not supported"); + it("should return the correct provide liquidity single asset calldata", async () => { + const calldata = await subject(); + + const expectedCallData = uniswapV2AmmAdapter.interface.encodeFunctionData("addLiquiditySingleAsset", [ + subjectAmmPool, + subjectComponent, + subjectMaxTokenIn, + subjectMinLiquidity, + ]); + expect(JSON.stringify(calldata)).to.eq(JSON.stringify([uniswapV2AmmAdapter.address, ZERO, expectedCallData])); }); }); @@ -175,7 +195,7 @@ describe("UniswapV2AmmAdapter", () => { ether(1)); } - it("should not support removing a single asset", async () => { + it("should return the correct provide liquidity calldata", async () => { await expect(subject()).to.be.revertedWith("Single asset liquidity removal not supported"); }); }); @@ -267,6 +287,12 @@ describe("UniswapV2AmmAdapter", () => { expect(updatedReserve0).to.eq(reserve0.add(subjectMaxTokensIn[1])); expect(updatedReserve1).to.eq(reserve1.add(subjectMaxTokensIn[0])); } + const wethBalance = await setup.weth.balanceOf(uniswapV2AmmAdapter.address); + const daiBalance = await setup.dai.balanceOf(uniswapV2AmmAdapter.address); + const lpBalance = await uniswapSetup.wethDaiPool.balanceOf(uniswapV2AmmAdapter.address); + expect(wethBalance).to.eq(ZERO); + expect(daiBalance).to.eq(ZERO); + expect(lpBalance).to.eq(ZERO); }); describe("when the pool address is invalid", async () => { @@ -341,6 +367,209 @@ describe("UniswapV2AmmAdapter", () => { await expect(subject()).to.be.revertedWith("_pool totalSupply must be > 0"); }); }); + + describe("when the _minLiquidity is 0", async () => { + beforeEach(async () => { + subjectMinLiquidity = ZERO; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("_minLiquidity must be greater than 0"); + }); + }); + + describe("when the _minLiquidity is too high", async () => { + beforeEach(async () => { + subjectMinLiquidity = subjectMinLiquidity.mul(2); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("_minLiquidity is too high for amount maximums"); + }); + }); + }); + + describe("addLiquiditySingleAsset", async () => { + let subjectAmmPool: Address; + let subjectComponent: Address; + let subjectMaxTokenIn: BigNumber; + let subjectMinLiquidity: BigNumber; + let tokensAdded: BigNumber; + + beforeEach(async () => { + subjectAmmPool = uniswapSetup.wethDaiPool.address; + subjectComponent = setup.weth.address; + subjectMaxTokenIn = ether(1); + const amountToSwap = subjectMaxTokenIn.div(2); + const [reserve0, reserve1, ] = await uniswapSetup.wethDaiPool.getReserves(); + const totalSupply = await uniswapSetup.wethDaiPool.totalSupply(); + const token0 = await uniswapSetup.wethDaiPool.token0(); + if ( token0 == setup.weth.address ) { + const amountOut = await uniswapSetup.router.getAmountOut(amountToSwap, reserve0, reserve1); + const quote = await uniswapSetup.router.quote(amountOut, reserve1.sub(amountOut), reserve0.add(amountToSwap)); + tokensAdded = amountToSwap.add(quote); + const liquidity0 = quote.mul(totalSupply).div(reserve0.add(amountToSwap)); + const liquidity1 = amountOut.mul(totalSupply).div(reserve1.sub(amountOut)); + subjectMinLiquidity = liquidity0.lt(liquidity1) ? liquidity0 : liquidity1; + } + else { + const amountOut = await uniswapSetup.router.getAmountOut(amountToSwap, reserve1, reserve0); + const quote = await uniswapSetup.router.quote(amountOut, reserve0.sub(amountOut), reserve1.add(amountToSwap)); + tokensAdded = amountToSwap.add(quote); + const liquidity0 = amountOut.mul(totalSupply).div(reserve0.sub(amountOut)); + const liquidity1 = quote.mul(totalSupply).div(reserve1.add(amountToSwap)); + subjectMinLiquidity = liquidity0.lt(liquidity1) ? liquidity0 : liquidity1; + } + await setup.weth.connect(owner.wallet) + .approve(uniswapV2AmmAdapter.address, MAX_UINT_256); + }); + + async function subject(): Promise { + return await uniswapV2AmmAdapter.addLiquiditySingleAsset( + subjectAmmPool, + subjectComponent, + subjectMaxTokenIn, + subjectMinLiquidity); + } + + it("should add the correct liquidity with weth", async () => { + const totalSupply = await uniswapSetup.wethDaiPool.totalSupply(); + const [reserve0, reserve1, ] = await uniswapSetup.wethDaiPool.getReserves(); + await subject(); + const updatedTotalSupply = await uniswapSetup.wethDaiPool.totalSupply(); + const [updatedReserve0, updatedReserve1, ] = await uniswapSetup.wethDaiPool.getReserves(); + expect(updatedTotalSupply).to.eq(totalSupply.add(subjectMinLiquidity)); + const token0 = await uniswapSetup.wethDaiPool.token0(); + if ( token0 == setup.weth.address ) { + expect(updatedReserve0).to.eq(reserve0.add(tokensAdded)); + expect(updatedReserve1).to.eq(reserve1); + } + else { + expect(updatedReserve0).to.eq(reserve0); + expect(updatedReserve1).to.eq(reserve1.add(tokensAdded)); + } + const wethBalance = await setup.weth.balanceOf(uniswapV2AmmAdapter.address); + const daiBalance = await setup.dai.balanceOf(uniswapV2AmmAdapter.address); + const lpBalance = await uniswapSetup.wethDaiPool.balanceOf(uniswapV2AmmAdapter.address); + expect(wethBalance).to.eq(ZERO); + expect(daiBalance).to.eq(ZERO); + expect(lpBalance).to.eq(ZERO); + }); + + describe("when providing dai", async () => { + beforeEach(async () => { + subjectComponent = setup.dai.address; + const amountToSwap = subjectMaxTokenIn.div(2); + const [reserve0, reserve1, ] = await uniswapSetup.wethDaiPool.getReserves(); + const totalSupply = await uniswapSetup.wethDaiPool.totalSupply(); + const token0 = await uniswapSetup.wethDaiPool.token0(); + if ( token0 == setup.dai.address ) { + const amountOut = await uniswapSetup.router.getAmountOut(amountToSwap, reserve0, reserve1); + const quote = await uniswapSetup.router.quote(amountOut, reserve1.sub(amountOut), reserve0.add(amountToSwap)); + tokensAdded = amountToSwap.add(quote); + const liquidity0 = quote.mul(totalSupply).div(reserve0.add(amountToSwap)); + const liquidity1 = amountOut.mul(totalSupply).div(reserve1.sub(amountOut)); + subjectMinLiquidity = liquidity0.lt(liquidity1) ? liquidity0 : liquidity1; + } + else { + const amountOut = await uniswapSetup.router.getAmountOut(amountToSwap, reserve1, reserve0); + const quote = await uniswapSetup.router.quote(amountOut, reserve0.sub(amountOut), reserve1.add(amountToSwap)); + tokensAdded = amountToSwap.add(quote); + const liquidity0 = amountOut.mul(totalSupply).div(reserve0.sub(amountOut)); + const liquidity1 = quote.mul(totalSupply).div(reserve1.add(amountToSwap)); + subjectMinLiquidity = liquidity0.lt(liquidity1) ? liquidity0 : liquidity1; + } + await setup.dai.connect(owner.wallet) + .approve(uniswapV2AmmAdapter.address, MAX_UINT_256); + }); + + it("should add the correct liquidity", async () => { + const totalSupply = await uniswapSetup.wethDaiPool.totalSupply(); + const [reserve0, reserve1, ] = await uniswapSetup.wethDaiPool.getReserves(); + await subject(); + const updatedTotalSupply = await uniswapSetup.wethDaiPool.totalSupply(); + const [updatedReserve0, updatedReserve1, ] = await uniswapSetup.wethDaiPool.getReserves(); + expect(updatedTotalSupply).to.eq(totalSupply.add(subjectMinLiquidity)); + const token0 = await uniswapSetup.wethDaiPool.token0(); + if ( token0 == setup.dai.address ) { + expect(updatedReserve0).to.eq(reserve0.add(tokensAdded)); + expect(updatedReserve1).to.eq(reserve1); + } + else { + expect(updatedReserve0).to.eq(reserve0); + expect(updatedReserve1).to.eq(reserve1.add(tokensAdded)); + } + const wethBalance = await setup.weth.balanceOf(uniswapV2AmmAdapter.address); + const daiBalance = await setup.dai.balanceOf(uniswapV2AmmAdapter.address); + const lpBalance = await uniswapSetup.wethDaiPool.balanceOf(uniswapV2AmmAdapter.address); + expect(wethBalance).to.eq(ZERO); + expect(daiBalance).to.eq(ZERO); + expect(lpBalance).to.eq(ZERO); + }); + }); + + describe("when the pool address is invalid", async () => { + beforeEach(async () => { + const uniswapV3Setup = getUniswapV3Fixture(owner.address); + await uniswapV3Setup.initialize(owner, setup.weth, 3000.0, setup.wbtc, 40000.0, setup.dai); + subjectAmmPool = uniswapV3Setup.swapRouter.address; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("_pool factory doesn't match the router factory"); + }); + }); + + describe("when the _pool doesn't match the _component", async () => { + beforeEach(async () => { + subjectComponent = setup.wbtc.address; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("_pool doesn't contain the _component"); + }); + }); + + describe("when the _maxTokenIn is 0", async () => { + beforeEach(async () => { + subjectMaxTokenIn = ether(0); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("supplied _maxTokenIn must be greater than 0"); + }); + }); + + describe("when the _pool totalSupply is 0", async () => { + beforeEach(async () => { + subjectAmmPool = uniswapSetup.wethWbtcPool.address; + subjectComponent = setup.weth.address; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("_pool totalSupply must be > 0"); + }); + }); + + describe("when the _minLiquidity is 0", async () => { + beforeEach(async () => { + subjectMinLiquidity = ZERO; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("_minLiquidity must be greater than 0"); + }); + }); + + describe("when the _minLiquidity is too high", async () => { + beforeEach(async () => { + subjectMinLiquidity = subjectMinLiquidity.mul(2); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("_minLiquidity is too high for amount maximum"); + }); + }); }); describe("getRemoveLiquidityCalldata", async () => { @@ -424,6 +653,12 @@ describe("UniswapV2AmmAdapter", () => { expect(updatedReserve0).to.be.eq(reserve0.sub(subjectMinTokensOut[1])); expect(updatedReserve1).to.be.eq(reserve1.sub(subjectMinTokensOut[0])); } + const wethBalance = await setup.weth.balanceOf(uniswapV2AmmAdapter.address); + const daiBalance = await setup.dai.balanceOf(uniswapV2AmmAdapter.address); + const lpBalance = await uniswapSetup.wethDaiPool.balanceOf(uniswapV2AmmAdapter.address); + expect(wethBalance).to.eq(ZERO); + expect(daiBalance).to.eq(ZERO); + expect(lpBalance).to.eq(ZERO); }); describe("when the pool address is invalid", async () => { @@ -488,6 +723,16 @@ describe("UniswapV2AmmAdapter", () => { }); }); + describe("when the _liquidity is 0", async () => { + beforeEach(async () => { + subjectLiquidity = ZERO; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("_liquidity must be greater than 0"); + }); + }); + describe("when the _liquidity is more than available", async () => { beforeEach(async () => { subjectLiquidity = (await uniswapSetup.wethDaiPool.balanceOf(owner.address)).add(ether(1)); @@ -581,7 +826,7 @@ describe("UniswapV2AmmAdapter", () => { }); it("should revert", async () => { - await expect(subject()).to.be.revertedWith("_minLiquidity is too high for amount minimums"); + await expect(subject()).to.be.revertedWith("_minLiquidity is too high for amount maximums"); }); }); From 8444be36cfad8b5d31bdb320f2d9e795d5c2c517 Mon Sep 17 00:00:00 2001 From: Stephen Hankinson Date: Mon, 23 Aug 2021 16:22:12 -0300 Subject: [PATCH 13/27] Add support for removing liquidity to a single asset --- .../integration/amm/UniswapV2AmmAdapter.sol | 126 ++++++++++- .../amm/UniswapV2AmmAdapter.spec.ts | 207 +++++++++++++++++- 2 files changed, 319 insertions(+), 14 deletions(-) diff --git a/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol b/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol index 95e9ed497..b9d2321af 100644 --- a/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol +++ b/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol @@ -56,6 +56,9 @@ contract UniswapV2AmmAdapter is IAmmAdapter { // Internal function string for removing liquidity string internal constant REMOVE_LIQUIDITY = "removeLiquidity(address,address[],uint256[],uint256)"; + // Internal function string for removing liquidity to a single asset + string internal constant REMOVE_LIQUIDITY_SINGLE_ASSET = + "removeLiquiditySingleAsset(address,address,uint256,uint256)"; /* ============ Constructor ============ */ @@ -219,7 +222,7 @@ contract UniswapV2AmmAdapter is IAmmAdapter { * Return calldata for the remove liquidity call * * @param _pool Address of liquidity token - * @param _components Address array required to add liquidity + * @param _components Address array required to remove liquidity * @param _minTokensOut AmountsOut minimum to remove liquidity * @param _liquidity Liquidity amount to remove */ @@ -249,22 +252,38 @@ contract UniswapV2AmmAdapter is IAmmAdapter { ); } + /** + * Return calldata for the remove liquidity single asset call + * + * @param _pool Address of liquidity token + * @param _component Address of token required to remove liquidity + * @param _minTokenOut AmountsOut minimum to remove liquidity + * @param _liquidity Liquidity amount to remove + */ function getRemoveLiquiditySingleAssetCalldata( - address, - address, - uint256, - uint256 + address _pool, + address _component, + uint256 _minTokenOut, + uint256 _liquidity ) external view override returns ( - address, - uint256, - bytes memory + address _target, + uint256 _value, + bytes memory _calldata ) { - revert("Single asset liquidity removal not supported"); + _target = address(this); + _value = 0; + _calldata = abi.encodeWithSignature( + REMOVE_LIQUIDITY_SINGLE_ASSET, + _pool, + _component, + _minTokenOut, + _liquidity + ); } function getSpenderAddress(address _pool) @@ -463,7 +482,7 @@ contract UniswapV2AmmAdapter is IAmmAdapter { * Remove liquidity via the Uniswap V2 Router * * @param _pool Address of liquidity token - * @param _components Address array required to add liquidity + * @param _components Address array required to remove liquidity * @param _minTokensOut AmountsOut minimum to remove liquidity * @param _liquidity Liquidity amount to remove */ @@ -520,4 +539,91 @@ contract UniswapV2AmmAdapter is IAmmAdapter { block.timestamp // solhint-disable-line not-rely-on-time ); } + + /** + * Remove liquidity via the Uniswap V2 Router and swap to a single asset + * + * @param _pool Address of liquidity token + * @param _component Address required to remove liquidity + * @param _minTokenOut AmountOut minimum to remove liquidity + * @param _liquidity Liquidity amount to remove + */ + function removeLiquiditySingleAsset( + address _pool, + address _component, + uint256 _minTokenOut, + uint256 _liquidity + ) + external + returns ( + uint[] memory amounts + ) + { + IUniswapV2Pair pair = IUniswapV2Pair(_pool); + require(factory == IUniswapV2Factory(pair.factory()), "_pool factory doesn't match the router factory"); + + address token0 = pair.token0(); + address token1 = pair.token1(); + require(token0 == _component || token1 == _component, "_pool doesn't contain the _component"); + require(_minTokenOut > 0, "requested token must be greater than 0"); + require(_liquidity > 0, "_liquidity must be greater than 0"); + + require(_liquidity <= pair.balanceOf(msg.sender), + "_liquidity must be <= to current balance"); + + // Determine if enough of the token will be received + bool isToken0 = token0 == _component ? true : false; + uint256 totalSupply = pair.totalSupply(); + (uint256 reserve0, uint256 reserve1, ) = pair.getReserves(); + uint[] memory receivedTokens = new uint[](2); + receivedTokens[0] = reserve0.mul(_liquidity).div(totalSupply); + receivedTokens[1] = reserve1.mul(_liquidity).div(totalSupply); + uint256 amountReceived = router.getAmountOut( + isToken0 ? receivedTokens[1] : receivedTokens[0], + isToken0 ? reserve1.sub(receivedTokens[1]) : reserve0.sub(receivedTokens[0]), + isToken0 ? reserve0.sub(receivedTokens[0]) : reserve1.sub(receivedTokens[1]) + ); + + require( (isToken0 ? receivedTokens[0].add(amountReceived) : + receivedTokens[1].add(amountReceived)) >= _minTokenOut, + "_minTokenOut is too high for amount received"); + + // Bring the lp token to this contract so we can use the Uniswap Router + pair.transferFrom(msg.sender, address(this), _liquidity); + + // Approve the router to spend the lp tokens + pair.approve(address(router), _liquidity); + + // Remove the liquidity + (receivedTokens[0], receivedTokens[1]) = router.removeLiquidity( + token0, + token1, + _liquidity, + receivedTokens[0], + receivedTokens[1], + address(this), + block.timestamp // solhint-disable-line not-rely-on-time + ); + + // Approve the router to spend the swap tokens + IERC20(isToken0 ? token1 : token0).approve(address(router), + isToken0 ? receivedTokens[1] : receivedTokens[0]); + + // Swap the other token for _component + address[] memory path = new address[](2); + path[0] = isToken0 ? token1 : token0; + path[1] = _component; + amounts = router.swapExactTokensForTokens( + isToken0 ? receivedTokens[1] : receivedTokens[0], + amountReceived, + path, + address(this), + block.timestamp // solhint-disable-line not-rely-on-time + ); + + // Send the tokens back to the caller + IERC20(_component).transfer(msg.sender, + (isToken0 ? receivedTokens[0] : receivedTokens[1]).add(amounts[1])); + + } } \ No newline at end of file diff --git a/test/protocol/integration/amm/UniswapV2AmmAdapter.spec.ts b/test/protocol/integration/amm/UniswapV2AmmAdapter.spec.ts index 356be29b9..13badf08a 100644 --- a/test/protocol/integration/amm/UniswapV2AmmAdapter.spec.ts +++ b/test/protocol/integration/amm/UniswapV2AmmAdapter.spec.ts @@ -187,16 +187,36 @@ describe("UniswapV2AmmAdapter", () => { }); describe("getRemoveLiquiditySingleAssetCalldata", async () => { + let subjectAmmPool: Address; + let subjectComponent: Address; + let subjectMinTokenOut: BigNumber; + let subjectLiquidity: BigNumber; + + before(async () => { + subjectAmmPool = uniswapSetup.wethDaiPool.address; + subjectComponent = setup.weth.address; + subjectMinTokenOut = ether(1); + subjectLiquidity = ether(1); + }); + async function subject(): Promise { return await uniswapV2AmmAdapter.getRemoveLiquiditySingleAssetCalldata( uniswapSetup.wethDaiPool.address, setup.weth.address, - ether(1), - ether(1)); + subjectMinTokenOut, + subjectLiquidity); } - it("should return the correct provide liquidity calldata", async () => { - await expect(subject()).to.be.revertedWith("Single asset liquidity removal not supported"); + it("should return the correct remove liquidity single asset calldata", async () => { + const calldata = await subject(); + + const expectedCallData = uniswapV2AmmAdapter.interface.encodeFunctionData("removeLiquiditySingleAsset", [ + subjectAmmPool, + subjectComponent, + subjectMinTokenOut, + subjectLiquidity, + ]); + expect(JSON.stringify(calldata)).to.eq(JSON.stringify([uniswapV2AmmAdapter.address, ZERO, expectedCallData])); }); }); @@ -742,6 +762,185 @@ describe("UniswapV2AmmAdapter", () => { await expect(subject()).to.be.revertedWith("_liquidity must be <= to current balance"); }); }); + + describe("when the _minTokensOut is too high", async () => { + beforeEach(async () => { + subjectMinTokensOut = [subjectMinTokensOut[0].mul(2), subjectMinTokensOut[1]]; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("amounts must be <= ownedTokens"); + }); + }); + }); + + describe("removeLiquiditySingleAsset", async () => { + let subjectAmmPool: Address; + let subjectComponent: Address; + let subjectMinTokenOut: BigNumber; + let subjectLiquidity: BigNumber; + + beforeEach(async () => { + subjectAmmPool = uniswapSetup.wethDaiPool.address; + subjectComponent = setup.weth.address; + subjectLiquidity = await uniswapSetup.wethDaiPool.balanceOf(owner.address); + const token0 = await uniswapSetup.wethDaiPool.token0(); + const totalSupply = await uniswapSetup.wethDaiPool.totalSupply(); + const [reserve0, reserve1, ] = await uniswapSetup.wethDaiPool.getReserves(); + const token0Amount = subjectLiquidity.mul(reserve0).div(totalSupply); + const token1Amount = subjectLiquidity.mul(reserve1).div(totalSupply); + if ( token0 == setup.weth.address ) { + const receivedAmount = await uniswapSetup.router.getAmountOut(token1Amount, + reserve1.sub(token1Amount), reserve0.sub(token0Amount)); + subjectMinTokenOut = token0Amount.add(receivedAmount); + } + else { + const receivedAmount = await uniswapSetup.router.getAmountOut(token0Amount, + reserve0.sub(token0Amount), reserve1.sub(token1Amount)); + subjectMinTokenOut = token1Amount.add(receivedAmount); + } + await uniswapSetup.wethDaiPool.connect(owner.wallet) + .approve(uniswapV2AmmAdapter.address, MAX_UINT_256); + }); + + async function subject(): Promise { + return await uniswapV2AmmAdapter.removeLiquiditySingleAsset( + subjectAmmPool, + subjectComponent, + subjectMinTokenOut, + subjectLiquidity); + } + + it("should remove the correct liquidity with weth", async () => { + const token0 = await uniswapSetup.wethDaiPool.token0(); + const [reserve0, reserve1, ] = await uniswapSetup.wethDaiPool.getReserves(); + await subject(); + expect(await uniswapSetup.wethDaiPool.balanceOf(owner.address)).to.be.eq(ZERO); + const [updatedReserve0, updatedReserve1, ] = await uniswapSetup.wethDaiPool.getReserves(); + if ( token0 == setup.weth.address ) { + expect(updatedReserve0).to.be.eq(reserve0.sub(subjectMinTokenOut)); + expect(updatedReserve1).to.be.eq(reserve1); + } + else { + expect(updatedReserve0).to.be.eq(reserve0); + expect(updatedReserve1).to.be.eq(reserve1.sub(subjectMinTokenOut)); + } + const wethBalance = await setup.weth.balanceOf(uniswapV2AmmAdapter.address); + const daiBalance = await setup.dai.balanceOf(uniswapV2AmmAdapter.address); + const lpBalance = await uniswapSetup.wethDaiPool.balanceOf(uniswapV2AmmAdapter.address); + const ownerLiquidity = await uniswapSetup.wethDaiPool.balanceOf(owner.address); + expect(wethBalance).to.eq(ZERO); + expect(daiBalance).to.eq(ZERO); + expect(lpBalance).to.eq(ZERO); + expect(ownerLiquidity).to.eq(ZERO); + }); + + describe("when removing dai", async () => { + beforeEach(async () => { + subjectComponent = setup.dai.address; + const token0 = await uniswapSetup.wethDaiPool.token0(); + const totalSupply = await uniswapSetup.wethDaiPool.totalSupply(); + const [reserve0, reserve1, ] = await uniswapSetup.wethDaiPool.getReserves(); + const token0Amount = subjectLiquidity.mul(reserve0).div(totalSupply); + const token1Amount = subjectLiquidity.mul(reserve1).div(totalSupply); + if ( token0 == setup.dai.address ) { + const receivedAmount = await uniswapSetup.router.getAmountOut(token1Amount, + reserve1.sub(token1Amount), reserve0.sub(token0Amount)); + subjectMinTokenOut = token0Amount.add(receivedAmount); + } + else { + const receivedAmount = await uniswapSetup.router.getAmountOut(token0Amount, + reserve0.sub(token0Amount), reserve1.sub(token1Amount)); + subjectMinTokenOut = token1Amount.add(receivedAmount); + } + }); + + it("should remove the correct liquidity", async () => { + const token0 = await uniswapSetup.wethDaiPool.token0(); + const [reserve0, reserve1, ] = await uniswapSetup.wethDaiPool.getReserves(); + await subject(); + expect(await uniswapSetup.wethDaiPool.balanceOf(owner.address)).to.be.eq(ZERO); + const [updatedReserve0, updatedReserve1, ] = await uniswapSetup.wethDaiPool.getReserves(); + if ( token0 == setup.dai.address ) { + expect(updatedReserve0).to.be.eq(reserve0.sub(subjectMinTokenOut)); + expect(updatedReserve1).to.be.eq(reserve1); + } + else { + expect(updatedReserve0).to.be.eq(reserve0); + expect(updatedReserve1).to.be.eq(reserve1.sub(subjectMinTokenOut)); + } + const wethBalance = await setup.weth.balanceOf(uniswapV2AmmAdapter.address); + const daiBalance = await setup.dai.balanceOf(uniswapV2AmmAdapter.address); + const lpBalance = await uniswapSetup.wethDaiPool.balanceOf(uniswapV2AmmAdapter.address); + const ownerLiquidity = await uniswapSetup.wethDaiPool.balanceOf(owner.address); + expect(wethBalance).to.eq(ZERO); + expect(daiBalance).to.eq(ZERO); + expect(lpBalance).to.eq(ZERO); + expect(ownerLiquidity).to.eq(ZERO); + }); + }); + + describe("when the pool address is invalid", async () => { + beforeEach(async () => { + const uniswapV3Setup = getUniswapV3Fixture(owner.address); + await uniswapV3Setup.initialize(owner, setup.weth, 3000.0, setup.wbtc, 40000.0, setup.dai); + subjectAmmPool = uniswapV3Setup.swapRouter.address; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("_pool factory doesn't match the router factory"); + }); + }); + + describe("when the _pool doesn't contain the _component", async () => { + beforeEach(async () => { + subjectComponent = setup.wbtc.address; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("_pool doesn't contain the _component"); + }); + }); + + describe("when the _minTokenOut is 0", async () => { + beforeEach(async () => { + subjectMinTokenOut = ether(0); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("requested token must be greater than 0"); + }); + }); + + describe("when the _liquidity is 0", async () => { + beforeEach(async () => { + subjectLiquidity = ZERO; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("_liquidity must be greater than 0"); + }); + }); + + describe("when the _liquidity is more than available", async () => { + beforeEach(async () => { + subjectLiquidity = (await uniswapSetup.wethDaiPool.balanceOf(owner.address)).add(ether(1)); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("_liquidity must be <= to current balance"); + }); + }); + + describe("when the _minTokenOut is too high", async () => { + beforeEach(async () => { + subjectMinTokenOut = subjectMinTokenOut.mul(2); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("_minTokenOut is too high for amount received"); + }); + }); }); context("Add and Remove Liquidity Tests", async () => { From 5c497c4f0c41727a9241a3653647dfed3fa4a433 Mon Sep 17 00:00:00 2001 From: Stephen Hankinson Date: Mon, 23 Aug 2021 17:55:46 -0300 Subject: [PATCH 14/27] Add some single asset tests for a set token --- contracts/protocol/modules/AmmModule.sol | 12 +- .../amm/UniswapV2AmmAdapter.spec.ts | 158 ++++++++++++++++++ 2 files changed, 167 insertions(+), 3 deletions(-) diff --git a/contracts/protocol/modules/AmmModule.sol b/contracts/protocol/modules/AmmModule.sol index ba0c0301a..40e88d5ee 100644 --- a/contracts/protocol/modules/AmmModule.sol +++ b/contracts/protocol/modules/AmmModule.sol @@ -231,9 +231,9 @@ contract AmmModule is ModuleBase, ReentrancyGuard { _validateRemoveLiquidity(actionInfo); _setToken.invokeApprove( - _ammPool, - actionInfo.ammAdapter.getSpenderAddress(_ammPool), - _poolTokenPositionUnits + _ammPool, + actionInfo.ammAdapter.getSpenderAddress(_ammPool), + _poolTokenPositionUnits ); _executeRemoveLiquidity(actionInfo); @@ -288,6 +288,12 @@ contract AmmModule is ModuleBase, ReentrancyGuard { _validateRemoveLiquidity(actionInfo); + _setToken.invokeApprove( + _ammPool, + actionInfo.ammAdapter.getSpenderAddress(_ammPool), + _poolTokenPositionUnits + ); + _executeRemoveLiquiditySingleAsset(actionInfo); _validateMinimumUnderlyingReceived(actionInfo); diff --git a/test/protocol/integration/amm/UniswapV2AmmAdapter.spec.ts b/test/protocol/integration/amm/UniswapV2AmmAdapter.spec.ts index 13badf08a..60b0fb5c0 100644 --- a/test/protocol/integration/amm/UniswapV2AmmAdapter.spec.ts +++ b/test/protocol/integration/amm/UniswapV2AmmAdapter.spec.ts @@ -1120,6 +1120,88 @@ describe("UniswapV2AmmAdapter", () => { }); + context("when there is a deployed SetToken with enabled AmmModule", async () => { + beforeEach(async () => { + // Deploy a standard SetToken with the AMM Module + setToken = await setup.createSetToken( + [setup.weth.address], + [ether(1)], + [setup.issuanceModule.address, ammModule.address] + ); + + await setup.issuanceModule.initialize(setToken.address, ADDRESS_ZERO); + await ammModule.initialize(setToken.address); + + // Mint some instances of the SetToken + await setup.approveAndIssueSetToken(setToken, ether(1)); + }); + + describe("#addLiquiditySingleAsset", async () => { + let subjectComponentToInput: Address; + let subjectMaxComponentQuantity: BigNumber; + let subjectMinPoolTokensToMint: BigNumber; + let tokensAdded: BigNumber; + + beforeEach(async () => { + subjectSetToken = setToken.address; + subjectIntegrationName = uniswapV2AmmAdapterName; + subjectAmmPool = uniswapSetup.wethDaiPool.address; + subjectComponentToInput = setup.weth.address; + subjectMaxComponentQuantity = ether(1); + subjectCaller = owner; + const amountToSwap = subjectMaxComponentQuantity.div(2); + const [reserve0, reserve1, ] = await uniswapSetup.wethDaiPool.getReserves(); + const totalSupply = await uniswapSetup.wethDaiPool.totalSupply(); + const token0 = await uniswapSetup.wethDaiPool.token0(); + if ( token0 == setup.weth.address ) { + const amountOut = await uniswapSetup.router.getAmountOut(amountToSwap, reserve0, reserve1); + const quote = await uniswapSetup.router.quote(amountOut, reserve1.sub(amountOut), reserve0.add(amountToSwap)); + tokensAdded = amountToSwap.add(quote); + const liquidity0 = quote.mul(totalSupply).div(reserve0.add(amountToSwap)); + const liquidity1 = amountOut.mul(totalSupply).div(reserve1.sub(amountOut)); + subjectMinPoolTokensToMint = liquidity0.lt(liquidity1) ? liquidity0 : liquidity1; + } + else { + const amountOut = await uniswapSetup.router.getAmountOut(amountToSwap, reserve1, reserve0); + const quote = await uniswapSetup.router.quote(amountOut, reserve0.sub(amountOut), reserve1.add(amountToSwap)); + tokensAdded = amountToSwap.add(quote); + const liquidity0 = amountOut.mul(totalSupply).div(reserve0.sub(amountOut)); + const liquidity1 = quote.mul(totalSupply).div(reserve1.add(amountToSwap)); + subjectMinPoolTokensToMint = liquidity0.lt(liquidity1) ? liquidity0 : liquidity1; + } + }); + + async function subject(): Promise { + return await ammModule.connect(subjectCaller.wallet).addLiquiditySingleAsset( + subjectSetToken, + subjectIntegrationName, + subjectAmmPool, + subjectMinPoolTokensToMint, + subjectComponentToInput, + subjectMaxComponentQuantity, + ); + } + + it("should mint the liquidity token to the caller", async () => { + await subject(); + const liquidityTokenBalance = await uniswapSetup.wethDaiPool.balanceOf(subjectSetToken); + expect(liquidityTokenBalance).to.eq(subjectMinPoolTokensToMint); + }); + + it("should update the positions properly", async () => { + await subject(); + const positions = await setToken.getPositions(); + expect(positions.length).to.eq(2); + expect(positions[0].component).to.eq(setup.weth.address); + expect(positions[0].unit).to.eq(subjectMaxComponentQuantity.sub(tokensAdded)); + expect(positions[1].component).to.eq(subjectAmmPool); + expect(positions[1].unit).to.eq(subjectMinPoolTokensToMint); + }); + + }); + + }); + context("when there is a deployed SetToken with enabled AmmModule", async () => { before(async () => { // Deploy a standard SetToken with the AMM Module @@ -1211,6 +1293,82 @@ describe("UniswapV2AmmAdapter", () => { }); + context("when there is a deployed SetToken with enabled AmmModule", async () => { + before(async () => { + // Deploy a standard SetToken with the AMM Module + setToken = await setup.createSetToken( + [uniswapSetup.wethDaiPool.address], + [ether(1)], + [setup.issuanceModule.address, ammModule.address] + ); + + await setup.issuanceModule.initialize(setToken.address, ADDRESS_ZERO); + await ammModule.initialize(setToken.address); + + // Mint some instances of the SetToken + await setup.approveAndIssueSetToken(setToken, ether(1)); + }); + + describe("#removeLiquiditySingleAsset", async () => { + let subjectComponentToOutput: Address; + let subjectMinComponentQuantity: BigNumber; + let subjectPoolTokens: BigNumber; + + beforeEach(async () => { + subjectSetToken = setToken.address; + subjectIntegrationName = uniswapV2AmmAdapterName; + subjectAmmPool = uniswapSetup.wethDaiPool.address; + subjectComponentToOutput = setup.weth.address; + subjectPoolTokens = ether(1); + const token0 = await uniswapSetup.wethDaiPool.token0(); + const totalSupply = await uniswapSetup.wethDaiPool.totalSupply(); + const [reserve0, reserve1, ] = await uniswapSetup.wethDaiPool.getReserves(); + const token0Amount = subjectPoolTokens.mul(reserve0).div(totalSupply); + const token1Amount = subjectPoolTokens.mul(reserve1).div(totalSupply); + if ( token0 == setup.weth.address ) { + const receivedAmount = await uniswapSetup.router.getAmountOut(token1Amount, + reserve1.sub(token1Amount), reserve0.sub(token0Amount)); + subjectMinComponentQuantity = token0Amount.add(receivedAmount); + } + else { + const receivedAmount = await uniswapSetup.router.getAmountOut(token0Amount, + reserve0.sub(token0Amount), reserve1.sub(token1Amount)); + subjectMinComponentQuantity = token1Amount.add(receivedAmount); + } + subjectCaller = owner; + }); + + async function subject(): Promise { + return await ammModule.connect(subjectCaller.wallet).removeLiquiditySingleAsset( + subjectSetToken, + subjectIntegrationName, + subjectAmmPool, + subjectPoolTokens, + subjectComponentToOutput, + subjectMinComponentQuantity, + ); + } + + it("should reduce the liquidity token of the caller", async () => { + const previousLiquidityTokenBalance = await uniswapSetup.wethDaiPool.balanceOf(subjectSetToken); + await subject(); + const liquidityTokenBalance = await uniswapSetup.wethDaiPool.balanceOf(subjectSetToken); + const expectedLiquidityBalance = previousLiquidityTokenBalance.sub(subjectPoolTokens); + expect(liquidityTokenBalance).to.eq(expectedLiquidityBalance); + }); + + it("should update the positions properly", async () => { + await subject(); + const positions = await setToken.getPositions(); + expect(positions.length).to.eq(1); + expect(positions[0].component).to.eq(setup.weth.address); + expect(positions[0].unit).to.eq(subjectMinComponentQuantity); + }); + + }); + + }); + function shouldRevertIfPoolIsNotSupported(subject: any) { describe("when the pool is not supported on the adapter", async () => { beforeEach(async () => { From 7b3bb0fe42ea08975ffb7f154c68be5e340af44d Mon Sep 17 00:00:00 2001 From: Stephen Hankinson Date: Tue, 24 Aug 2021 17:33:34 -0300 Subject: [PATCH 15/27] Optimize the swap amount when adding liquidity with a single asset --- .../integration/amm/UniswapV2AmmAdapter.sol | 66 +++++++++++++-- package.json | 1 + .../amm/UniswapV2AmmAdapter.spec.ts | 84 ++++++++++++------- utils/deploys/deployAdapters.ts | 9 +- 4 files changed, 124 insertions(+), 36 deletions(-) diff --git a/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol b/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol index b9d2321af..a2430ffb7 100644 --- a/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol +++ b/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol @@ -23,6 +23,7 @@ import "../../../interfaces/external/IUniswapV2Pair.sol"; import "../../../interfaces/external/IUniswapV2Factory.sol"; import "../../../interfaces/IAmmAdapter.sol"; import "@openzeppelin/contracts/math/SafeMath.sol"; +import "@uniswap/lib/contracts/libraries/Babylonian.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; struct Position { @@ -47,6 +48,10 @@ contract UniswapV2AmmAdapter is IAmmAdapter { IUniswapV2Router public immutable router; IUniswapV2Factory public immutable factory; + // Fee settings for the AMM + uint256 internal immutable feeNumerator; + uint256 internal immutable feeDenominator; + // Internal function string for adding liquidity string internal constant ADD_LIQUIDITY = "addLiquidity(address,address[],uint256[],uint256)"; @@ -65,11 +70,15 @@ contract UniswapV2AmmAdapter is IAmmAdapter { /** * Set state variables * - * @param _router Address of Uniswap V2 Router contract + * @param _router Address of Uniswap V2 Router contract + * @param _feeNumerator Numerator of the fee component (usually 997) + * @param _feeDenominator Denominator of the fee component (usually 1000) */ - constructor(address _router) public { + constructor(address _router, uint256 _feeNumerator, uint256 _feeDenominator) public { router = IUniswapV2Router(_router); factory = IUniswapV2Factory(IUniswapV2Router(_router).factory()); + feeNumerator = _feeNumerator; + feeDenominator = _feeDenominator; } /* ============ Internal Functions =================== */ @@ -122,17 +131,20 @@ contract UniswapV2AmmAdapter is IAmmAdapter { ) { + // Get the reserves of the pair + (uint256 reserve0, uint256 reserve1, ) = pair.getReserves(); + // Use half of the provided amount in the swap - uint256 amountToSwap = amount.div(2); + uint256 amountToSwap = this.calculateSwapAmount(amount, token == token0 ? reserve0 : reserve1); // Approve the router to spend the tokens IERC20(token).approve(address(router), amountToSwap); - // Determine the amount of the other token to get - (uint256 reserve0, uint256 reserve1, ) = pair.getReserves(); + // Determine how much we should expect of token1 uint256 amountOut = router.getAmountOut(amountToSwap, token0 == token ? reserve0 : reserve1, token0 == token ? reserve1 : reserve0); + // Perform the swap address[] memory path = new address[](2); path[0] = token; path[1] = token == token0 ? token1 : token0; @@ -144,12 +156,54 @@ contract UniswapV2AmmAdapter is IAmmAdapter { block.timestamp // solhint-disable-line not-rely-on-time ); - position = getPosition(pair, path[0], amount.sub(amountToSwap), path[1], amounts[1]); + // How much token do we have left? + uint256 remaining = amount.sub(amountToSwap); + + position = getPosition(pair, path[0], remaining, path[1], amounts[1]); } /* ============ External Getter Functions ============ */ + /** + * Returns the amount of tokenA to swap + * + * @param amountA The amount of tokenA being supplied + * @param reserveA The reserve of tokenA in the pool + */ + function calculateSwapAmount( + uint256 amountA, + uint256 reserveA + ) + external + view + returns ( + uint256 swapAmount + ) + { + // Solves the following system of equations to find the ideal swapAmount + // eq1: amountA = swapAmount + amountALP + // eq2: amountBLP = swapAmount * feeNumerator * reserveB / (reserveA * feeDenominator + swapAmount * feeNumerator) + // eq3: amountALP = amountBLP * (reserveA + swapAmount) / (reserveB - amountBLP) + // Substitution: swapAmount^2 * feeNumerator + swapAmount * reserveA * (feeNumerator + feeDenominator) - amountA * reserveA * feeDenominator = 0 + // Solution: swapAmount = (-b +/- sqrt(b^2-4ac))/(2a) + // a = feeNumerator + // b = reserveA * (feeNumerator + feeDenominator) + // c = -amountA * reserveA * feeDenominator + // Note: a is always positive. b is always positive. The solved + // equation has a negative multiplier on c but that is ignored here because the + // negative in front of the 4ac in the quadratic solution would cancel it out, + // making it an addition. Since b is always positive, we never want to take + // the negative square root solution since that would always cause a negative + // swapAmount, which doesn't make sense. Therefore, we only use the positive + // square root value as the solution. + uint256 b = reserveA.mul(feeNumerator.add(feeDenominator)); + uint256 c = amountA.mul(feeDenominator).mul(reserveA); + + swapAmount = Babylonian.sqrt(b.mul(b).add(feeNumerator.mul(c).mul(4))) + .sub(b).div(feeNumerator.mul(2)); + } + /** * Return calldata for the add liquidity call * diff --git a/package.json b/package.json index 0abc0367a..5de28e053 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "@types/lodash": "^4.14.86", "@types/mocha": "^7.0.2", "@types/node": "^14.0.5", + "@uniswap/lib": "^4.0.1-alpha", "chai": "^4.2.0", "coveralls": "^3.0.1", "dotenv": "^8.2.0", diff --git a/test/protocol/integration/amm/UniswapV2AmmAdapter.spec.ts b/test/protocol/integration/amm/UniswapV2AmmAdapter.spec.ts index 60b0fb5c0..cec8e9890 100644 --- a/test/protocol/integration/amm/UniswapV2AmmAdapter.spec.ts +++ b/test/protocol/integration/amm/UniswapV2AmmAdapter.spec.ts @@ -71,7 +71,8 @@ describe("UniswapV2AmmAdapter", () => { ammModule = await deployer.modules.deployAmmModule(setup.controller.address); await setup.controller.addModule(ammModule.address); - uniswapV2AmmAdapter = await deployer.adapters.deployUniswapV2AmmAdapter(uniswapSetup.router.address); + uniswapV2AmmAdapter = await deployer.adapters.deployUniswapV2AmmAdapter(uniswapSetup.router.address, + BigNumber.from(997), BigNumber.from(1000)); uniswapV2AmmAdapterName = "UNISWAPV2AMM"; await setup.integrationRegistry.addIntegration( @@ -420,24 +421,33 @@ describe("UniswapV2AmmAdapter", () => { subjectAmmPool = uniswapSetup.wethDaiPool.address; subjectComponent = setup.weth.address; subjectMaxTokenIn = ether(1); - const amountToSwap = subjectMaxTokenIn.div(2); const [reserve0, reserve1, ] = await uniswapSetup.wethDaiPool.getReserves(); const totalSupply = await uniswapSetup.wethDaiPool.totalSupply(); const token0 = await uniswapSetup.wethDaiPool.token0(); if ( token0 == setup.weth.address ) { + const amountToSwap = await uniswapV2AmmAdapter.calculateSwapAmount(subjectMaxTokenIn, reserve0); const amountOut = await uniswapSetup.router.getAmountOut(amountToSwap, reserve0, reserve1); - const quote = await uniswapSetup.router.quote(amountOut, reserve1.sub(amountOut), reserve0.add(amountToSwap)); - tokensAdded = amountToSwap.add(quote); - const liquidity0 = quote.mul(totalSupply).div(reserve0.add(amountToSwap)); - const liquidity1 = amountOut.mul(totalSupply).div(reserve1.sub(amountOut)); + const remaining = subjectMaxTokenIn.sub(amountToSwap); + const quote0 = await uniswapSetup.router.quote(remaining, reserve0.add(amountToSwap), reserve1.sub(amountOut) ); + const quote1 = await uniswapSetup.router.quote(amountOut, reserve1.sub(amountOut), reserve0.add(amountToSwap) ); + const amount0 = quote0 <= amountOut ? remaining : quote1; + const amount1 = quote0 <= amountOut ? quote0 : amountOut; + tokensAdded = amountToSwap.add(amount0); + const liquidity0 = amount0.mul(totalSupply).div(reserve0.add(amountToSwap)); + const liquidity1 = amount1.mul(totalSupply).div(reserve1.sub(amountOut)); subjectMinLiquidity = liquidity0.lt(liquidity1) ? liquidity0 : liquidity1; } else { + const amountToSwap = await uniswapV2AmmAdapter.calculateSwapAmount(subjectMaxTokenIn, reserve1); const amountOut = await uniswapSetup.router.getAmountOut(amountToSwap, reserve1, reserve0); - const quote = await uniswapSetup.router.quote(amountOut, reserve0.sub(amountOut), reserve1.add(amountToSwap)); - tokensAdded = amountToSwap.add(quote); - const liquidity0 = amountOut.mul(totalSupply).div(reserve0.sub(amountOut)); - const liquidity1 = quote.mul(totalSupply).div(reserve1.add(amountToSwap)); + const remaining = subjectMaxTokenIn.sub(amountToSwap); + const quote0 = await uniswapSetup.router.quote(amountOut, reserve0.sub(amountOut), reserve1.add(amountToSwap)); + const quote1 = await uniswapSetup.router.quote(remaining, reserve1.add(amountToSwap), reserve0.sub(amountOut) ); + const amount0 = quote0 <= remaining ? amountOut : quote1; + const amount1 = quote0 <= remaining ? quote0 : remaining; + tokensAdded = amountToSwap.add(amount1); + const liquidity0 = amount0.mul(totalSupply).div(reserve0.sub(amountOut)); + const liquidity1 = amount1.mul(totalSupply).div(reserve1.add(amountToSwap)); subjectMinLiquidity = liquidity0.lt(liquidity1) ? liquidity0 : liquidity1; } await setup.weth.connect(owner.wallet) @@ -479,24 +489,33 @@ describe("UniswapV2AmmAdapter", () => { describe("when providing dai", async () => { beforeEach(async () => { subjectComponent = setup.dai.address; - const amountToSwap = subjectMaxTokenIn.div(2); const [reserve0, reserve1, ] = await uniswapSetup.wethDaiPool.getReserves(); const totalSupply = await uniswapSetup.wethDaiPool.totalSupply(); const token0 = await uniswapSetup.wethDaiPool.token0(); if ( token0 == setup.dai.address ) { + const amountToSwap = await uniswapV2AmmAdapter.calculateSwapAmount(subjectMaxTokenIn, reserve0); const amountOut = await uniswapSetup.router.getAmountOut(amountToSwap, reserve0, reserve1); - const quote = await uniswapSetup.router.quote(amountOut, reserve1.sub(amountOut), reserve0.add(amountToSwap)); - tokensAdded = amountToSwap.add(quote); - const liquidity0 = quote.mul(totalSupply).div(reserve0.add(amountToSwap)); - const liquidity1 = amountOut.mul(totalSupply).div(reserve1.sub(amountOut)); + const remaining = subjectMaxTokenIn.sub(amountToSwap); + const quote0 = await uniswapSetup.router.quote(remaining, reserve0.add(amountToSwap), reserve1.sub(amountOut) ); + const quote1 = await uniswapSetup.router.quote(amountOut, reserve1.sub(amountOut), reserve0.add(amountToSwap) ); + const amount0 = quote0 <= amountOut ? remaining : quote1; + const amount1 = quote0 <= amountOut ? quote0 : amountOut; + tokensAdded = amountToSwap.add(amount0); + const liquidity0 = amount0.mul(totalSupply).div(reserve0.add(amountToSwap)); + const liquidity1 = amount1.mul(totalSupply).div(reserve1.sub(amountOut)); subjectMinLiquidity = liquidity0.lt(liquidity1) ? liquidity0 : liquidity1; } else { + const amountToSwap = await uniswapV2AmmAdapter.calculateSwapAmount(subjectMaxTokenIn, reserve1); const amountOut = await uniswapSetup.router.getAmountOut(amountToSwap, reserve1, reserve0); - const quote = await uniswapSetup.router.quote(amountOut, reserve0.sub(amountOut), reserve1.add(amountToSwap)); - tokensAdded = amountToSwap.add(quote); - const liquidity0 = amountOut.mul(totalSupply).div(reserve0.sub(amountOut)); - const liquidity1 = quote.mul(totalSupply).div(reserve1.add(amountToSwap)); + const remaining = subjectMaxTokenIn.sub(amountToSwap); + const quote0 = await uniswapSetup.router.quote(amountOut, reserve0.sub(amountOut), reserve1.add(amountToSwap)); + const quote1 = await uniswapSetup.router.quote(remaining, reserve1.add(amountToSwap), reserve0.sub(amountOut) ); + const amount0 = quote0 <= remaining ? amountOut : quote1; + const amount1 = quote0 <= remaining ? quote0 : remaining; + tokensAdded = amountToSwap.add(amount1); + const liquidity0 = amount0.mul(totalSupply).div(reserve0.sub(amountOut)); + const liquidity1 = amount1.mul(totalSupply).div(reserve1.add(amountToSwap)); subjectMinLiquidity = liquidity0.lt(liquidity1) ? liquidity0 : liquidity1; } await setup.dai.connect(owner.wallet) @@ -1149,24 +1168,33 @@ describe("UniswapV2AmmAdapter", () => { subjectComponentToInput = setup.weth.address; subjectMaxComponentQuantity = ether(1); subjectCaller = owner; - const amountToSwap = subjectMaxComponentQuantity.div(2); const [reserve0, reserve1, ] = await uniswapSetup.wethDaiPool.getReserves(); const totalSupply = await uniswapSetup.wethDaiPool.totalSupply(); const token0 = await uniswapSetup.wethDaiPool.token0(); if ( token0 == setup.weth.address ) { + const amountToSwap = await uniswapV2AmmAdapter.calculateSwapAmount(subjectMaxComponentQuantity, reserve0); const amountOut = await uniswapSetup.router.getAmountOut(amountToSwap, reserve0, reserve1); - const quote = await uniswapSetup.router.quote(amountOut, reserve1.sub(amountOut), reserve0.add(amountToSwap)); - tokensAdded = amountToSwap.add(quote); - const liquidity0 = quote.mul(totalSupply).div(reserve0.add(amountToSwap)); - const liquidity1 = amountOut.mul(totalSupply).div(reserve1.sub(amountOut)); + const remaining = subjectMaxComponentQuantity.sub(amountToSwap); + const quote0 = await uniswapSetup.router.quote(remaining, reserve0.add(amountToSwap), reserve1.sub(amountOut) ); + const quote1 = await uniswapSetup.router.quote(amountOut, reserve1.sub(amountOut), reserve0.add(amountToSwap) ); + const amount0 = quote0 <= amountOut ? remaining : quote1; + const amount1 = quote0 <= amountOut ? quote0 : amountOut; + tokensAdded = amountToSwap.add(amount0); + const liquidity0 = amount0.mul(totalSupply).div(reserve0.add(amountToSwap)); + const liquidity1 = amount1.mul(totalSupply).div(reserve1.sub(amountOut)); subjectMinPoolTokensToMint = liquidity0.lt(liquidity1) ? liquidity0 : liquidity1; } else { + const amountToSwap = await uniswapV2AmmAdapter.calculateSwapAmount(subjectMaxComponentQuantity, reserve1); const amountOut = await uniswapSetup.router.getAmountOut(amountToSwap, reserve1, reserve0); - const quote = await uniswapSetup.router.quote(amountOut, reserve0.sub(amountOut), reserve1.add(amountToSwap)); - tokensAdded = amountToSwap.add(quote); - const liquidity0 = amountOut.mul(totalSupply).div(reserve0.sub(amountOut)); - const liquidity1 = quote.mul(totalSupply).div(reserve1.add(amountToSwap)); + const remaining = subjectMaxComponentQuantity.sub(amountToSwap); + const quote0 = await uniswapSetup.router.quote(amountOut, reserve0.sub(amountOut), reserve1.add(amountToSwap)); + const quote1 = await uniswapSetup.router.quote(remaining, reserve1.add(amountToSwap), reserve0.sub(amountOut) ); + const amount0 = quote0 <= remaining ? amountOut : quote1; + const amount1 = quote0 <= remaining ? quote0 : remaining; + tokensAdded = amountToSwap.add(amount1); + const liquidity0 = amount0.mul(totalSupply).div(reserve0.sub(amountOut)); + const liquidity1 = amount1.mul(totalSupply).div(reserve1.add(amountToSwap)); subjectMinPoolTokensToMint = liquidity0.lt(liquidity1) ? liquidity0 : liquidity1; } }); diff --git a/utils/deploys/deployAdapters.ts b/utils/deploys/deployAdapters.ts index eff0ad7cd..c73e3f685 100644 --- a/utils/deploys/deployAdapters.ts +++ b/utils/deploys/deployAdapters.ts @@ -34,6 +34,7 @@ import { } from "../contracts"; import { convertLibraryNameToLinkId } from "../common"; import { Address, Bytes } from "./../types"; +import { BigNumber } from "@ethersproject/bignumber"; import { AaveGovernanceAdapter__factory } from "../../typechain/factories/AaveGovernanceAdapter__factory"; import { AaveGovernanceV2Adapter__factory } from "../../typechain/factories/AaveGovernanceV2Adapter__factory"; @@ -88,8 +89,12 @@ export default class DeployAdapters { ); } - public async deployUniswapV2AmmAdapter(uniswapV2Router: Address): Promise { - return await new UniswapV2AmmAdapter__factory(this._deployerSigner).deploy(uniswapV2Router); + public async deployUniswapV2AmmAdapter( + uniswapV2Router: Address, + feeNumerator: BigNumber, + feeDenominator: BigNumber + ): Promise { + return await new UniswapV2AmmAdapter__factory(this._deployerSigner).deploy(uniswapV2Router, feeNumerator, feeDenominator); } public async deployUniswapV2ExchangeAdapter(uniswapV2Router: Address): Promise { From 845503c0e76ccd1721821896cca196251dea34fd Mon Sep 17 00:00:00 2001 From: Stephen Hankinson Date: Tue, 24 Aug 2021 23:08:02 -0300 Subject: [PATCH 16/27] Implement some simplifications --- .../integration/amm/UniswapV2AmmAdapter.sol | 126 +++++++------- .../amm/UniswapV2AmmAdapter.spec.ts | 161 ++++++------------ 2 files changed, 117 insertions(+), 170 deletions(-) diff --git a/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol b/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol index a2430ffb7..37edb3ca3 100644 --- a/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol +++ b/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol @@ -22,6 +22,7 @@ import "../../../interfaces/external/IUniswapV2Router.sol"; import "../../../interfaces/external/IUniswapV2Pair.sol"; import "../../../interfaces/external/IUniswapV2Factory.sol"; import "../../../interfaces/IAmmAdapter.sol"; +import "../../../../external/contracts/uniswap/v2/lib/UniswapV2Library.sol"; import "@openzeppelin/contracts/math/SafeMath.sol"; import "@uniswap/lib/contracts/libraries/Babylonian.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; @@ -112,43 +113,38 @@ contract UniswapV2AmmAdapter is IAmmAdapter { /** * Performs a swap via the Uniswap V2 Router * - * @param pair Uniswap V2 Pair - * @param token Address of the token to swap - * @param token0 Address of pair token0 - * @param token1 Address of pair token1 + * @param tokenA Address of the token to swap + * @param tokenB Address of pair token0 * @param amount Amount of the token to swap */ function performSwap( - IUniswapV2Pair pair, - address token, - address token0, - address token1, + address tokenA, + address tokenB, uint256 amount ) internal returns ( - Position memory position + uint[] memory amounts ) { // Get the reserves of the pair - (uint256 reserve0, uint256 reserve1, ) = pair.getReserves(); + (uint256 reserveA, uint256 reserveB) = UniswapV2Library.getReserves(address(factory), tokenA, tokenB); // Use half of the provided amount in the swap - uint256 amountToSwap = this.calculateSwapAmount(amount, token == token0 ? reserve0 : reserve1); + uint256 amountToSwap = this.calculateSwapAmount(amount, reserveA); // Approve the router to spend the tokens - IERC20(token).approve(address(router), amountToSwap); + IERC20(tokenA).approve(address(router), amountToSwap); // Determine how much we should expect of token1 - uint256 amountOut = router.getAmountOut(amountToSwap, token0 == token ? reserve0 : reserve1, - token0 == token ? reserve1 : reserve0); + uint256 amountOut = router.getAmountOut(amountToSwap, reserveA, reserveB); // Perform the swap address[] memory path = new address[](2); - path[0] = token; - path[1] = token == token0 ? token1 : token0; - uint[] memory amounts = router.swapExactTokensForTokens( + path[0] = tokenA; + path[1] = tokenB; + amounts = router.swapExactTokensForTokens( amountToSwap, amountOut, path, @@ -157,9 +153,7 @@ contract UniswapV2AmmAdapter is IAmmAdapter { ); // How much token do we have left? - uint256 remaining = amount.sub(amountToSwap); - - position = getPosition(pair, path[0], remaining, path[1], amounts[1]); + amounts[0] = amount.sub(amountToSwap); } @@ -482,52 +476,58 @@ contract UniswapV2AmmAdapter is IAmmAdapter { IUniswapV2Pair pair = IUniswapV2Pair(_pool); require(factory == IUniswapV2Factory(pair.factory()), "_pool factory doesn't match the router factory"); - address token0 = pair.token0(); - address token1 = pair.token1(); - require(token0 == _component || token1 == _component, "_pool doesn't contain the _component"); + address tokenA = pair.token0(); + address tokenB = pair.token1(); + require(tokenA == _component || tokenB == _component, "_pool doesn't contain the _component"); require(_maxTokenIn > 0, "supplied _maxTokenIn must be greater than 0"); require(_minLiquidity > 0, "supplied _minLiquidity must be greater than 0"); + // Swap them if needed + if( tokenB == _component ) { + tokenB = tokenA; + tokenA = _component; + } + uint256 lpTotalSupply = pair.totalSupply(); require(lpTotalSupply > 0, "_pool totalSupply must be > 0"); // Bring the tokens to this contract so we can use the Uniswap Router - IERC20(_component).transferFrom(msg.sender, address(this), _maxTokenIn); + IERC20(tokenA).transferFrom(msg.sender, address(this), _maxTokenIn); // Execute the swap - Position memory position = performSwap(pair, _component, token0, token1, _maxTokenIn); + uint[] memory amounts = performSwap(tokenA, tokenB, _maxTokenIn); - (uint256 reserve0, uint256 reserve1, ) = pair.getReserves(); - uint256 amount0Min = reserve0.mul(_minLiquidity).div(lpTotalSupply); - uint256 amount1Min = reserve1.mul(_minLiquidity).div(lpTotalSupply); + (uint256 reserveA, uint256 reserveB) = UniswapV2Library.getReserves(address(factory), tokenA, tokenB); + uint256 amountAMin = reserveA.mul(_minLiquidity).div(lpTotalSupply); + uint256 amountBMin = reserveB.mul(_minLiquidity).div(lpTotalSupply); - require(amount0Min <= position.amount0 && amount1Min <= position.amount1, + require(amountAMin <= amounts[0] && amountBMin <= amounts[1], "_minLiquidity is too high for amount maximum"); // Approve the router to spend the tokens - position.token0.approve(address(router), position.amount0); - position.token1.approve(address(router), position.amount1); + IERC20(tokenA).approve(address(router), amounts[0]); + IERC20(tokenB).approve(address(router), amounts[1]); // Add the liquidity (amountA, amountB, liquidity) = router.addLiquidity( - address(position.token0), - address(position.token1), - position.amount0, - position.amount1, - amount0Min, - amount1Min, + address(tokenA), + address(tokenB), + amounts[0], + amounts[1], + amountAMin, + amountBMin, msg.sender, block.timestamp // solhint-disable-line not-rely-on-time ); // If there is token0 left, send it back - if( amountA < position.amount0 ) { - position.token0.transfer(msg.sender, position.amount0.sub(amountA) ); + if( amountA < amounts[0] ) { + IERC20(tokenA).transfer(msg.sender, amounts[0].sub(amountA) ); } // If there is token1 left, send it back - if( amountB < position.amount1 ) { - position.token1.transfer(msg.sender, position.amount1.sub(amountB) ); + if( amountB < amounts[1] ) { + IERC20(tokenB).transfer(msg.sender, amounts[1].sub(amountB) ); } } @@ -616,30 +616,34 @@ contract UniswapV2AmmAdapter is IAmmAdapter { IUniswapV2Pair pair = IUniswapV2Pair(_pool); require(factory == IUniswapV2Factory(pair.factory()), "_pool factory doesn't match the router factory"); - address token0 = pair.token0(); - address token1 = pair.token1(); - require(token0 == _component || token1 == _component, "_pool doesn't contain the _component"); + address tokenA = pair.token0(); + address tokenB = pair.token1(); + require(tokenA == _component || tokenB == _component, "_pool doesn't contain the _component"); require(_minTokenOut > 0, "requested token must be greater than 0"); require(_liquidity > 0, "_liquidity must be greater than 0"); require(_liquidity <= pair.balanceOf(msg.sender), "_liquidity must be <= to current balance"); + // Swap them if needed + if( tokenB == _component ) { + tokenB = tokenA; + tokenA = _component; + } + // Determine if enough of the token will be received - bool isToken0 = token0 == _component ? true : false; uint256 totalSupply = pair.totalSupply(); - (uint256 reserve0, uint256 reserve1, ) = pair.getReserves(); + (uint256 reserveA, uint256 reserveB) = UniswapV2Library.getReserves(address(factory), tokenA, tokenB); uint[] memory receivedTokens = new uint[](2); - receivedTokens[0] = reserve0.mul(_liquidity).div(totalSupply); - receivedTokens[1] = reserve1.mul(_liquidity).div(totalSupply); + receivedTokens[0] = reserveA.mul(_liquidity).div(totalSupply); + receivedTokens[1] = reserveB.mul(_liquidity).div(totalSupply); uint256 amountReceived = router.getAmountOut( - isToken0 ? receivedTokens[1] : receivedTokens[0], - isToken0 ? reserve1.sub(receivedTokens[1]) : reserve0.sub(receivedTokens[0]), - isToken0 ? reserve0.sub(receivedTokens[0]) : reserve1.sub(receivedTokens[1]) + receivedTokens[1], + reserveB.sub(receivedTokens[1]), + reserveA.sub(receivedTokens[0]) ); - require( (isToken0 ? receivedTokens[0].add(amountReceived) : - receivedTokens[1].add(amountReceived)) >= _minTokenOut, + require( receivedTokens[0].add(amountReceived) >= _minTokenOut, "_minTokenOut is too high for amount received"); // Bring the lp token to this contract so we can use the Uniswap Router @@ -650,8 +654,8 @@ contract UniswapV2AmmAdapter is IAmmAdapter { // Remove the liquidity (receivedTokens[0], receivedTokens[1]) = router.removeLiquidity( - token0, - token1, + tokenA, + tokenB, _liquidity, receivedTokens[0], receivedTokens[1], @@ -660,15 +664,14 @@ contract UniswapV2AmmAdapter is IAmmAdapter { ); // Approve the router to spend the swap tokens - IERC20(isToken0 ? token1 : token0).approve(address(router), - isToken0 ? receivedTokens[1] : receivedTokens[0]); + IERC20(tokenB).approve(address(router), receivedTokens[1]); // Swap the other token for _component address[] memory path = new address[](2); - path[0] = isToken0 ? token1 : token0; - path[1] = _component; + path[0] = tokenB; + path[1] = tokenA; amounts = router.swapExactTokensForTokens( - isToken0 ? receivedTokens[1] : receivedTokens[0], + receivedTokens[1], amountReceived, path, address(this), @@ -676,8 +679,7 @@ contract UniswapV2AmmAdapter is IAmmAdapter { ); // Send the tokens back to the caller - IERC20(_component).transfer(msg.sender, - (isToken0 ? receivedTokens[0] : receivedTokens[1]).add(amounts[1])); + IERC20(tokenA).transfer(msg.sender, receivedTokens[0].add(amounts[1])); } } \ No newline at end of file diff --git a/test/protocol/integration/amm/UniswapV2AmmAdapter.spec.ts b/test/protocol/integration/amm/UniswapV2AmmAdapter.spec.ts index cec8e9890..ca37c0c41 100644 --- a/test/protocol/integration/amm/UniswapV2AmmAdapter.spec.ts +++ b/test/protocol/integration/amm/UniswapV2AmmAdapter.spec.ts @@ -9,7 +9,7 @@ import { ADDRESS_ZERO, ZERO, } from "@utils/constants"; -import { SetToken, AmmModule, UniswapV2AmmAdapter } from "@utils/contracts"; +import { SetToken, AmmModule, UniswapV2AmmAdapter, UniswapV2Pair } from "@utils/contracts"; import DeployHelper from "@utils/deploys"; import { ether @@ -27,6 +27,12 @@ import { SystemFixture, UniswapFixture } from "@utils/fixtures"; const expect = getWaffleExpect(); +async function getReserves(pair: UniswapV2Pair, token: string): Promise<[BigNumber, BigNumber]> { + const token0 = await pair.token0(); + const [reserve0, reserve1, ] = await pair.getReserves(); + return token0 == token ? [reserve0, reserve1] : [reserve1, reserve0]; +} + describe("UniswapV2AmmAdapter", () => { let owner: Account; let deployer: DeployHelper; @@ -421,35 +427,19 @@ describe("UniswapV2AmmAdapter", () => { subjectAmmPool = uniswapSetup.wethDaiPool.address; subjectComponent = setup.weth.address; subjectMaxTokenIn = ether(1); - const [reserve0, reserve1, ] = await uniswapSetup.wethDaiPool.getReserves(); + const [reserveA, reserveB] = await getReserves(uniswapSetup.wethDaiPool, subjectComponent); const totalSupply = await uniswapSetup.wethDaiPool.totalSupply(); - const token0 = await uniswapSetup.wethDaiPool.token0(); - if ( token0 == setup.weth.address ) { - const amountToSwap = await uniswapV2AmmAdapter.calculateSwapAmount(subjectMaxTokenIn, reserve0); - const amountOut = await uniswapSetup.router.getAmountOut(amountToSwap, reserve0, reserve1); - const remaining = subjectMaxTokenIn.sub(amountToSwap); - const quote0 = await uniswapSetup.router.quote(remaining, reserve0.add(amountToSwap), reserve1.sub(amountOut) ); - const quote1 = await uniswapSetup.router.quote(amountOut, reserve1.sub(amountOut), reserve0.add(amountToSwap) ); - const amount0 = quote0 <= amountOut ? remaining : quote1; - const amount1 = quote0 <= amountOut ? quote0 : amountOut; - tokensAdded = amountToSwap.add(amount0); - const liquidity0 = amount0.mul(totalSupply).div(reserve0.add(amountToSwap)); - const liquidity1 = amount1.mul(totalSupply).div(reserve1.sub(amountOut)); - subjectMinLiquidity = liquidity0.lt(liquidity1) ? liquidity0 : liquidity1; - } - else { - const amountToSwap = await uniswapV2AmmAdapter.calculateSwapAmount(subjectMaxTokenIn, reserve1); - const amountOut = await uniswapSetup.router.getAmountOut(amountToSwap, reserve1, reserve0); - const remaining = subjectMaxTokenIn.sub(amountToSwap); - const quote0 = await uniswapSetup.router.quote(amountOut, reserve0.sub(amountOut), reserve1.add(amountToSwap)); - const quote1 = await uniswapSetup.router.quote(remaining, reserve1.add(amountToSwap), reserve0.sub(amountOut) ); - const amount0 = quote0 <= remaining ? amountOut : quote1; - const amount1 = quote0 <= remaining ? quote0 : remaining; - tokensAdded = amountToSwap.add(amount1); - const liquidity0 = amount0.mul(totalSupply).div(reserve0.sub(amountOut)); - const liquidity1 = amount1.mul(totalSupply).div(reserve1.add(amountToSwap)); - subjectMinLiquidity = liquidity0.lt(liquidity1) ? liquidity0 : liquidity1; - } + const amountToSwap = await uniswapV2AmmAdapter.calculateSwapAmount(subjectMaxTokenIn, reserveA); + const amountOut = await uniswapSetup.router.getAmountOut(amountToSwap, reserveA, reserveB); + const remaining = subjectMaxTokenIn.sub(amountToSwap); + const quote0 = await uniswapSetup.router.quote(remaining, reserveA.add(amountToSwap), reserveB.sub(amountOut) ); + const quote1 = await uniswapSetup.router.quote(amountOut, reserveB.sub(amountOut), reserveA.add(amountToSwap) ); + const amount0 = quote0 <= amountOut ? remaining : quote1; + const amount1 = quote0 <= amountOut ? quote0 : amountOut; + tokensAdded = amountToSwap.add(amount0); + const liquidity0 = amount0.mul(totalSupply).div(reserveA.add(amountToSwap)); + const liquidity1 = amount1.mul(totalSupply).div(reserveB.sub(amountOut)); + subjectMinLiquidity = liquidity0.lt(liquidity1) ? liquidity0 : liquidity1; await setup.weth.connect(owner.wallet) .approve(uniswapV2AmmAdapter.address, MAX_UINT_256); }); @@ -464,20 +454,13 @@ describe("UniswapV2AmmAdapter", () => { it("should add the correct liquidity with weth", async () => { const totalSupply = await uniswapSetup.wethDaiPool.totalSupply(); - const [reserve0, reserve1, ] = await uniswapSetup.wethDaiPool.getReserves(); + const [reserveA, reserveB, ] = await getReserves(uniswapSetup.wethDaiPool, subjectComponent); await subject(); const updatedTotalSupply = await uniswapSetup.wethDaiPool.totalSupply(); - const [updatedReserve0, updatedReserve1, ] = await uniswapSetup.wethDaiPool.getReserves(); + const [updatedReserveA, updatedReserveB, ] = await getReserves(uniswapSetup.wethDaiPool, subjectComponent); expect(updatedTotalSupply).to.eq(totalSupply.add(subjectMinLiquidity)); - const token0 = await uniswapSetup.wethDaiPool.token0(); - if ( token0 == setup.weth.address ) { - expect(updatedReserve0).to.eq(reserve0.add(tokensAdded)); - expect(updatedReserve1).to.eq(reserve1); - } - else { - expect(updatedReserve0).to.eq(reserve0); - expect(updatedReserve1).to.eq(reserve1.add(tokensAdded)); - } + expect(updatedReserveA).to.eq(reserveA.add(tokensAdded)); + expect(updatedReserveB).to.eq(reserveB); const wethBalance = await setup.weth.balanceOf(uniswapV2AmmAdapter.address); const daiBalance = await setup.dai.balanceOf(uniswapV2AmmAdapter.address); const lpBalance = await uniswapSetup.wethDaiPool.balanceOf(uniswapV2AmmAdapter.address); @@ -489,55 +472,32 @@ describe("UniswapV2AmmAdapter", () => { describe("when providing dai", async () => { beforeEach(async () => { subjectComponent = setup.dai.address; - const [reserve0, reserve1, ] = await uniswapSetup.wethDaiPool.getReserves(); + const [reserveA, reserveB] = await getReserves(uniswapSetup.wethDaiPool, subjectComponent); const totalSupply = await uniswapSetup.wethDaiPool.totalSupply(); - const token0 = await uniswapSetup.wethDaiPool.token0(); - if ( token0 == setup.dai.address ) { - const amountToSwap = await uniswapV2AmmAdapter.calculateSwapAmount(subjectMaxTokenIn, reserve0); - const amountOut = await uniswapSetup.router.getAmountOut(amountToSwap, reserve0, reserve1); - const remaining = subjectMaxTokenIn.sub(amountToSwap); - const quote0 = await uniswapSetup.router.quote(remaining, reserve0.add(amountToSwap), reserve1.sub(amountOut) ); - const quote1 = await uniswapSetup.router.quote(amountOut, reserve1.sub(amountOut), reserve0.add(amountToSwap) ); - const amount0 = quote0 <= amountOut ? remaining : quote1; - const amount1 = quote0 <= amountOut ? quote0 : amountOut; - tokensAdded = amountToSwap.add(amount0); - const liquidity0 = amount0.mul(totalSupply).div(reserve0.add(amountToSwap)); - const liquidity1 = amount1.mul(totalSupply).div(reserve1.sub(amountOut)); - subjectMinLiquidity = liquidity0.lt(liquidity1) ? liquidity0 : liquidity1; - } - else { - const amountToSwap = await uniswapV2AmmAdapter.calculateSwapAmount(subjectMaxTokenIn, reserve1); - const amountOut = await uniswapSetup.router.getAmountOut(amountToSwap, reserve1, reserve0); - const remaining = subjectMaxTokenIn.sub(amountToSwap); - const quote0 = await uniswapSetup.router.quote(amountOut, reserve0.sub(amountOut), reserve1.add(amountToSwap)); - const quote1 = await uniswapSetup.router.quote(remaining, reserve1.add(amountToSwap), reserve0.sub(amountOut) ); - const amount0 = quote0 <= remaining ? amountOut : quote1; - const amount1 = quote0 <= remaining ? quote0 : remaining; - tokensAdded = amountToSwap.add(amount1); - const liquidity0 = amount0.mul(totalSupply).div(reserve0.sub(amountOut)); - const liquidity1 = amount1.mul(totalSupply).div(reserve1.add(amountToSwap)); - subjectMinLiquidity = liquidity0.lt(liquidity1) ? liquidity0 : liquidity1; - } + const amountToSwap = await uniswapV2AmmAdapter.calculateSwapAmount(subjectMaxTokenIn, reserveA); + const amountOut = await uniswapSetup.router.getAmountOut(amountToSwap, reserveA, reserveB); + const remaining = subjectMaxTokenIn.sub(amountToSwap); + const quote0 = await uniswapSetup.router.quote(remaining, reserveA.add(amountToSwap), reserveB.sub(amountOut) ); + const quote1 = await uniswapSetup.router.quote(amountOut, reserveB.sub(amountOut), reserveA.add(amountToSwap) ); + const amount0 = quote0 <= amountOut ? remaining : quote1; + const amount1 = quote0 <= amountOut ? quote0 : amountOut; + tokensAdded = amountToSwap.add(amount0); + const liquidity0 = amount0.mul(totalSupply).div(reserveA.add(amountToSwap)); + const liquidity1 = amount1.mul(totalSupply).div(reserveB.sub(amountOut)); + subjectMinLiquidity = liquidity0.lt(liquidity1) ? liquidity0 : liquidity1; await setup.dai.connect(owner.wallet) .approve(uniswapV2AmmAdapter.address, MAX_UINT_256); }); it("should add the correct liquidity", async () => { const totalSupply = await uniswapSetup.wethDaiPool.totalSupply(); - const [reserve0, reserve1, ] = await uniswapSetup.wethDaiPool.getReserves(); + const [reserveA, reserveB, ] = await getReserves(uniswapSetup.wethDaiPool, subjectComponent); await subject(); const updatedTotalSupply = await uniswapSetup.wethDaiPool.totalSupply(); - const [updatedReserve0, updatedReserve1, ] = await uniswapSetup.wethDaiPool.getReserves(); + const [updatedReserveA, updatedReserveB, ] = await getReserves(uniswapSetup.wethDaiPool, subjectComponent); expect(updatedTotalSupply).to.eq(totalSupply.add(subjectMinLiquidity)); - const token0 = await uniswapSetup.wethDaiPool.token0(); - if ( token0 == setup.dai.address ) { - expect(updatedReserve0).to.eq(reserve0.add(tokensAdded)); - expect(updatedReserve1).to.eq(reserve1); - } - else { - expect(updatedReserve0).to.eq(reserve0); - expect(updatedReserve1).to.eq(reserve1.add(tokensAdded)); - } + expect(updatedReserveA).to.eq(reserveA.add(tokensAdded)); + expect(updatedReserveB).to.eq(reserveB); const wethBalance = await setup.weth.balanceOf(uniswapV2AmmAdapter.address); const daiBalance = await setup.dai.balanceOf(uniswapV2AmmAdapter.address); const lpBalance = await uniswapSetup.wethDaiPool.balanceOf(uniswapV2AmmAdapter.address); @@ -545,6 +505,7 @@ describe("UniswapV2AmmAdapter", () => { expect(daiBalance).to.eq(ZERO); expect(lpBalance).to.eq(ZERO); }); + }); describe("when the pool address is invalid", async () => { @@ -1168,35 +1129,19 @@ describe("UniswapV2AmmAdapter", () => { subjectComponentToInput = setup.weth.address; subjectMaxComponentQuantity = ether(1); subjectCaller = owner; - const [reserve0, reserve1, ] = await uniswapSetup.wethDaiPool.getReserves(); + const [reserveA, reserveB] = await getReserves(uniswapSetup.wethDaiPool, subjectComponentToInput); const totalSupply = await uniswapSetup.wethDaiPool.totalSupply(); - const token0 = await uniswapSetup.wethDaiPool.token0(); - if ( token0 == setup.weth.address ) { - const amountToSwap = await uniswapV2AmmAdapter.calculateSwapAmount(subjectMaxComponentQuantity, reserve0); - const amountOut = await uniswapSetup.router.getAmountOut(amountToSwap, reserve0, reserve1); - const remaining = subjectMaxComponentQuantity.sub(amountToSwap); - const quote0 = await uniswapSetup.router.quote(remaining, reserve0.add(amountToSwap), reserve1.sub(amountOut) ); - const quote1 = await uniswapSetup.router.quote(amountOut, reserve1.sub(amountOut), reserve0.add(amountToSwap) ); - const amount0 = quote0 <= amountOut ? remaining : quote1; - const amount1 = quote0 <= amountOut ? quote0 : amountOut; - tokensAdded = amountToSwap.add(amount0); - const liquidity0 = amount0.mul(totalSupply).div(reserve0.add(amountToSwap)); - const liquidity1 = amount1.mul(totalSupply).div(reserve1.sub(amountOut)); - subjectMinPoolTokensToMint = liquidity0.lt(liquidity1) ? liquidity0 : liquidity1; - } - else { - const amountToSwap = await uniswapV2AmmAdapter.calculateSwapAmount(subjectMaxComponentQuantity, reserve1); - const amountOut = await uniswapSetup.router.getAmountOut(amountToSwap, reserve1, reserve0); - const remaining = subjectMaxComponentQuantity.sub(amountToSwap); - const quote0 = await uniswapSetup.router.quote(amountOut, reserve0.sub(amountOut), reserve1.add(amountToSwap)); - const quote1 = await uniswapSetup.router.quote(remaining, reserve1.add(amountToSwap), reserve0.sub(amountOut) ); - const amount0 = quote0 <= remaining ? amountOut : quote1; - const amount1 = quote0 <= remaining ? quote0 : remaining; - tokensAdded = amountToSwap.add(amount1); - const liquidity0 = amount0.mul(totalSupply).div(reserve0.sub(amountOut)); - const liquidity1 = amount1.mul(totalSupply).div(reserve1.add(amountToSwap)); - subjectMinPoolTokensToMint = liquidity0.lt(liquidity1) ? liquidity0 : liquidity1; - } + const amountToSwap = await uniswapV2AmmAdapter.calculateSwapAmount(subjectMaxComponentQuantity, reserveA); + const amountOut = await uniswapSetup.router.getAmountOut(amountToSwap, reserveA, reserveB); + const remaining = subjectMaxComponentQuantity.sub(amountToSwap); + const quote0 = await uniswapSetup.router.quote(remaining, reserveA.add(amountToSwap), reserveB.sub(amountOut) ); + const quote1 = await uniswapSetup.router.quote(amountOut, reserveB.sub(amountOut), reserveA.add(amountToSwap) ); + const amount0 = quote0 <= amountOut ? remaining : quote1; + const amount1 = quote0 <= amountOut ? quote0 : amountOut; + tokensAdded = amountToSwap.add(amount0); + const liquidity0 = amount0.mul(totalSupply).div(reserveA.add(amountToSwap)); + const liquidity1 = amount1.mul(totalSupply).div(reserveB.sub(amountOut)); + subjectMinPoolTokensToMint = liquidity0.lt(liquidity1) ? liquidity0 : liquidity1; }); async function subject(): Promise { @@ -1220,7 +1165,7 @@ describe("UniswapV2AmmAdapter", () => { await subject(); const positions = await setToken.getPositions(); expect(positions.length).to.eq(2); - expect(positions[0].component).to.eq(setup.weth.address); + expect(positions[0].component).to.eq(subjectComponentToInput); expect(positions[0].unit).to.eq(subjectMaxComponentQuantity.sub(tokensAdded)); expect(positions[1].component).to.eq(subjectAmmPool); expect(positions[1].unit).to.eq(subjectMinPoolTokensToMint); From ee95dddfdc91493d3ecbef4d2be813c0103f2307 Mon Sep 17 00:00:00 2001 From: Stephen Hankinson Date: Wed, 25 Aug 2021 11:36:19 -0300 Subject: [PATCH 17/27] Re-use internal functions for better code coverage --- .../integration/amm/UniswapV2AmmAdapter.sol | 227 ++++++-------- .../amm/UniswapV2AmmAdapter.spec.ts | 291 ++++++++---------- 2 files changed, 223 insertions(+), 295 deletions(-) diff --git a/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol b/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol index 37edb3ca3..ad9915d7f 100644 --- a/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol +++ b/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol @@ -22,16 +22,15 @@ import "../../../interfaces/external/IUniswapV2Router.sol"; import "../../../interfaces/external/IUniswapV2Pair.sol"; import "../../../interfaces/external/IUniswapV2Factory.sol"; import "../../../interfaces/IAmmAdapter.sol"; -import "../../../../external/contracts/uniswap/v2/lib/UniswapV2Library.sol"; import "@openzeppelin/contracts/math/SafeMath.sol"; import "@uniswap/lib/contracts/libraries/Babylonian.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; struct Position { - IERC20 token0; - uint256 amount0; - IERC20 token1; - uint256 amount1; + IERC20 tokenA; + uint256 amountA; + IERC20 tokenB; + uint256 amountB; } /** @@ -55,13 +54,13 @@ contract UniswapV2AmmAdapter is IAmmAdapter { // Internal function string for adding liquidity string internal constant ADD_LIQUIDITY = - "addLiquidity(address,address[],uint256[],uint256)"; + "addLiquidity(address,address[],uint256[],uint256,bool)"; // Internal function string for adding liquidity with a single asset string internal constant ADD_LIQUIDITY_SINGLE_ASSET = "addLiquiditySingleAsset(address,address,uint256,uint256)"; // Internal function string for removing liquidity string internal constant REMOVE_LIQUIDITY = - "removeLiquidity(address,address[],uint256[],uint256)"; + "removeLiquidity(address,address[],uint256[],uint256,bool)"; // Internal function string for removing liquidity to a single asset string internal constant REMOVE_LIQUIDITY_SINGLE_ASSET = "removeLiquiditySingleAsset(address,address,uint256,uint256)"; @@ -85,39 +84,37 @@ contract UniswapV2AmmAdapter is IAmmAdapter { /* ============ Internal Functions =================== */ /** - * Return tokens in sorted order + * Returns the pair reserves in an expected order * - * @param _token0 Address of the first token - * @param _amount0 Amount of the first token - * @param _token1 Address of the first token - * @param _amount1 Amount of the second token + * @param pair The pair to get the reserves from + * @param tokenA Address of the token to swap */ - function getPosition( - IUniswapV2Pair _pair, - address _token0, - uint256 _amount0, - address _token1, - uint256 _amount1 + function getReserves( + IUniswapV2Pair pair, + address tokenA ) internal view returns ( - Position memory position + uint reserveA, + uint reserveB ) { - position = _pair.token0() == _token0 ? - Position(IERC20(_token0), _amount0, IERC20(_token1), _amount1) : - Position(IERC20(_token1), _amount1, IERC20(_token0), _amount0); + address token0 = pair.token0(); + (uint reserve0, uint reserve1,) = pair.getReserves(); + (reserveA, reserveB) = tokenA == token0 ? (reserve0, reserve1) : (reserve1, reserve0); } /** * Performs a swap via the Uniswap V2 Router * - * @param tokenA Address of the token to swap - * @param tokenB Address of pair token0 - * @param amount Amount of the token to swap + * @param pair The pair to perform the swap on + * @param tokenA Address of the token to swap + * @param tokenB Address of pair token0 + * @param amount Amount of the token to swap */ function performSwap( + IUniswapV2Pair pair, address tokenA, address tokenB, uint256 amount @@ -129,7 +126,7 @@ contract UniswapV2AmmAdapter is IAmmAdapter { { // Get the reserves of the pair - (uint256 reserveA, uint256 reserveB) = UniswapV2Library.getReserves(address(factory), tokenA, tokenB); + (uint256 reserveA, uint256 reserveB) = getReserves(pair, tokenA); // Use half of the provided amount in the swap uint256 amountToSwap = this.calculateSwapAmount(amount, reserveA); @@ -228,7 +225,8 @@ contract UniswapV2AmmAdapter is IAmmAdapter { _pool, _components, _maxTokensIn, - _minLiquidity + _minLiquidity, + true ); } @@ -296,7 +294,8 @@ contract UniswapV2AmmAdapter is IAmmAdapter { _pool, _components, _minTokensOut, - _liquidity + _liquidity, + true ); } @@ -334,6 +333,11 @@ contract UniswapV2AmmAdapter is IAmmAdapter { ); } + /** + * Returns the address of the spender + * + * @param _pool Address of liquidity token + */ function getSpenderAddress(address _pool) external view @@ -346,6 +350,11 @@ contract UniswapV2AmmAdapter is IAmmAdapter { return address(this); } + /** + * Verifies that this is a valid Uniswap V2 _pool + * + * @param _pool Address of liquidity token + */ function isValidPool(address _pool) external view override returns (bool) { address token0; address token1; @@ -381,14 +390,16 @@ contract UniswapV2AmmAdapter is IAmmAdapter { * @param _components Address array required to add liquidity * @param _maxTokensIn AmountsIn desired to add liquidity * @param _minLiquidity Min liquidity amount to add + * @param _shouldTransfer Should the tokens be transferred from the sender */ function addLiquidity( address _pool, - address[] calldata _components, - uint256[] calldata _maxTokensIn, - uint256 _minLiquidity + address[] memory _components, + uint256[] memory _maxTokensIn, + uint256 _minLiquidity, + bool _shouldTransfer ) - external + public returns ( uint amountA, uint amountB, @@ -406,47 +417,49 @@ contract UniswapV2AmmAdapter is IAmmAdapter { require(_maxTokensIn[1] > 0, "supplied token1 must be greater than 0"); require(_minLiquidity > 0, "_minLiquidity must be greater than 0"); - Position memory position = - getPosition(pair, _components[0], _maxTokensIn[0], _components[1], _maxTokensIn[1]); + Position memory position = Position(IERC20(_components[0]), _maxTokensIn[0], + IERC20(_components[1]), _maxTokensIn[1]); uint256 lpTotalSupply = pair.totalSupply(); require(lpTotalSupply > 0, "_pool totalSupply must be > 0"); - (uint256 reserve0, uint256 reserve1, ) = pair.getReserves(); - uint256 amount0Min = reserve0.mul(_minLiquidity).div(lpTotalSupply); - uint256 amount1Min = reserve1.mul(_minLiquidity).div(lpTotalSupply); + (uint256 reserveA, uint256 reserveB) = getReserves(pair, _components[0]); + uint256 amountAMin = reserveA.mul(_minLiquidity).div(lpTotalSupply); + uint256 amountBMin = reserveB.mul(_minLiquidity).div(lpTotalSupply); - require(amount0Min <= position.amount0 && amount1Min <= position.amount1, + require(amountAMin <= position.amountA && amountBMin <= position.amountB, "_minLiquidity is too high for amount maximums"); - // Bring the tokens to this contract so we can use the Uniswap Router - position.token0.transferFrom(msg.sender, address(this), position.amount0); - position.token1.transferFrom(msg.sender, address(this), position.amount1); + // Bring the tokens to this contract, if needed, so we can use the Uniswap Router + if( _shouldTransfer ) { + position.tokenA.transferFrom(msg.sender, address(this), position.amountA); + position.tokenB.transferFrom(msg.sender, address(this), position.amountB); + } // Approve the router to spend the tokens - position.token0.approve(address(router), position.amount0); - position.token1.approve(address(router), position.amount1); + position.tokenA.approve(address(router), position.amountA); + position.tokenB.approve(address(router), position.amountB); // Add the liquidity (amountA, amountB, liquidity) = router.addLiquidity( - address(position.token0), - address(position.token1), - position.amount0, - position.amount1, - amount0Min, - amount1Min, + address(position.tokenA), + address(position.tokenB), + position.amountA, + position.amountB, + amountAMin, + amountBMin, msg.sender, block.timestamp // solhint-disable-line not-rely-on-time ); // If there is token0 left, send it back - if( amountA < position.amount0 ) { - position.token0.transfer(msg.sender, position.amount0.sub(amountA) ); + if( amountA < position.amountA ) { + position.tokenA.transfer(msg.sender, position.amountA.sub(amountA) ); } // If there is token1 left, send it back - if( amountB < position.amount1 ) { - position.token1.transfer(msg.sender, position.amount1.sub(amountB) ); + if( amountB < position.amountB ) { + position.tokenB.transfer(msg.sender, position.amountB.sub(amountB) ); } } @@ -495,40 +508,14 @@ contract UniswapV2AmmAdapter is IAmmAdapter { IERC20(tokenA).transferFrom(msg.sender, address(this), _maxTokenIn); // Execute the swap - uint[] memory amounts = performSwap(tokenA, tokenB, _maxTokenIn); + uint[] memory amounts = performSwap(pair, tokenA, tokenB, _maxTokenIn); - (uint256 reserveA, uint256 reserveB) = UniswapV2Library.getReserves(address(factory), tokenA, tokenB); - uint256 amountAMin = reserveA.mul(_minLiquidity).div(lpTotalSupply); - uint256 amountBMin = reserveB.mul(_minLiquidity).div(lpTotalSupply); - - require(amountAMin <= amounts[0] && amountBMin <= amounts[1], - "_minLiquidity is too high for amount maximum"); - - // Approve the router to spend the tokens - IERC20(tokenA).approve(address(router), amounts[0]); - IERC20(tokenB).approve(address(router), amounts[1]); + address[] memory components = new address[](2); + components[0] = tokenA; + components[1] = tokenB; // Add the liquidity - (amountA, amountB, liquidity) = router.addLiquidity( - address(tokenA), - address(tokenB), - amounts[0], - amounts[1], - amountAMin, - amountBMin, - msg.sender, - block.timestamp // solhint-disable-line not-rely-on-time - ); - - // If there is token0 left, send it back - if( amountA < amounts[0] ) { - IERC20(tokenA).transfer(msg.sender, amounts[0].sub(amountA) ); - } - - // If there is token1 left, send it back - if( amountB < amounts[1] ) { - IERC20(tokenB).transfer(msg.sender, amounts[1].sub(amountB) ); - } + (amountA, amountB, liquidity) = addLiquidity(_pool, components, amounts, _minLiquidity, false); } @@ -539,14 +526,16 @@ contract UniswapV2AmmAdapter is IAmmAdapter { * @param _components Address array required to remove liquidity * @param _minTokensOut AmountsOut minimum to remove liquidity * @param _liquidity Liquidity amount to remove + * @param _shouldReturn Should the tokens be returned to the sender? */ function removeLiquidity( address _pool, - address[] calldata _components, - uint256[] calldata _minTokensOut, - uint256 _liquidity + address[] memory _components, + uint256[] memory _minTokensOut, + uint256 _liquidity, + bool _shouldReturn ) - external + public returns ( uint amountA, uint amountB @@ -562,18 +551,20 @@ contract UniswapV2AmmAdapter is IAmmAdapter { require(_minTokensOut[1] > 0, "requested token1 must be greater than 0"); require(_liquidity > 0, "_liquidity must be greater than 0"); - Position memory position = - getPosition(pair, _components[0], _minTokensOut[0], _components[1], _minTokensOut[1]); + Position memory position = Position(IERC20(_components[0]), _minTokensOut[0], + IERC20(_components[1]), _minTokensOut[1]); uint256 balance = pair.balanceOf(msg.sender); require(_liquidity <= balance, "_liquidity must be <= to current balance"); - uint256 totalSupply = pair.totalSupply(); - (uint256 reserve0, uint256 reserve1, ) = pair.getReserves(); - uint256 ownedToken0 = reserve0.mul(balance).div(totalSupply); - uint256 ownedToken1 = reserve1.mul(balance).div(totalSupply); + // Calculate how many tokens are owned by the liquidity + uint[] memory tokenInfo = new uint[](3); + tokenInfo[2] = pair.totalSupply(); + (tokenInfo[0], tokenInfo[1]) = getReserves(pair, _components[0]); + tokenInfo[0] = tokenInfo[0].mul(balance).div(tokenInfo[2]); + tokenInfo[1] = tokenInfo[1].mul(balance).div(tokenInfo[2]); - require(position.amount0 <= ownedToken0 && position.amount1 <= ownedToken1, + require(position.amountA <= tokenInfo[0] && position.amountB <= tokenInfo[1], "amounts must be <= ownedTokens"); // Bring the lp token to this contract so we can use the Uniswap Router @@ -584,12 +575,12 @@ contract UniswapV2AmmAdapter is IAmmAdapter { // Remove the liquidity (amountA, amountB) = router.removeLiquidity( - address(position.token0), - address(position.token1), + address(position.tokenA), + address(position.tokenB), _liquidity, - position.amount0, - position.amount1, - msg.sender, + position.amountA, + position.amountB, + _shouldReturn ? msg.sender : address(this), block.timestamp // solhint-disable-line not-rely-on-time ); } @@ -622,9 +613,6 @@ contract UniswapV2AmmAdapter is IAmmAdapter { require(_minTokenOut > 0, "requested token must be greater than 0"); require(_liquidity > 0, "_liquidity must be greater than 0"); - require(_liquidity <= pair.balanceOf(msg.sender), - "_liquidity must be <= to current balance"); - // Swap them if needed if( tokenB == _component ) { tokenB = tokenA; @@ -633,10 +621,17 @@ contract UniswapV2AmmAdapter is IAmmAdapter { // Determine if enough of the token will be received uint256 totalSupply = pair.totalSupply(); - (uint256 reserveA, uint256 reserveB) = UniswapV2Library.getReserves(address(factory), tokenA, tokenB); + (uint256 reserveA, uint256 reserveB) = getReserves(pair, _component); uint[] memory receivedTokens = new uint[](2); receivedTokens[0] = reserveA.mul(_liquidity).div(totalSupply); receivedTokens[1] = reserveB.mul(_liquidity).div(totalSupply); + + address[] memory components = new address[](2); + components[0] = tokenA; + components[1] = tokenB; + + (receivedTokens[0], receivedTokens[1]) = removeLiquidity(_pool, components, receivedTokens, _liquidity, false); + uint256 amountReceived = router.getAmountOut( receivedTokens[1], reserveB.sub(receivedTokens[1]), @@ -646,34 +641,16 @@ contract UniswapV2AmmAdapter is IAmmAdapter { require( receivedTokens[0].add(amountReceived) >= _minTokenOut, "_minTokenOut is too high for amount received"); - // Bring the lp token to this contract so we can use the Uniswap Router - pair.transferFrom(msg.sender, address(this), _liquidity); - - // Approve the router to spend the lp tokens - pair.approve(address(router), _liquidity); - - // Remove the liquidity - (receivedTokens[0], receivedTokens[1]) = router.removeLiquidity( - tokenA, - tokenB, - _liquidity, - receivedTokens[0], - receivedTokens[1], - address(this), - block.timestamp // solhint-disable-line not-rely-on-time - ); - // Approve the router to spend the swap tokens IERC20(tokenB).approve(address(router), receivedTokens[1]); // Swap the other token for _component - address[] memory path = new address[](2); - path[0] = tokenB; - path[1] = tokenA; + components[0] = tokenB; + components[1] = tokenA; amounts = router.swapExactTokensForTokens( receivedTokens[1], amountReceived, - path, + components, address(this), block.timestamp // solhint-disable-line not-rely-on-time ); diff --git a/test/protocol/integration/amm/UniswapV2AmmAdapter.spec.ts b/test/protocol/integration/amm/UniswapV2AmmAdapter.spec.ts index ca37c0c41..f35e56417 100644 --- a/test/protocol/integration/amm/UniswapV2AmmAdapter.spec.ts +++ b/test/protocol/integration/amm/UniswapV2AmmAdapter.spec.ts @@ -256,6 +256,7 @@ describe("UniswapV2AmmAdapter", () => { subjectComponents, subjectMaxTokensIn, subjectMinLiquidity, + true, ]); expect(JSON.stringify(calldata)).to.eq(JSON.stringify([uniswapV2AmmAdapter.address, ZERO, expectedCallData])); }); @@ -272,18 +273,10 @@ describe("UniswapV2AmmAdapter", () => { subjectComponents = [setup.weth.address, setup.dai.address]; subjectMaxTokensIn = [ether(1), ether(3000)]; const totalSupply = await uniswapSetup.wethDaiPool.totalSupply(); - const [reserve0, reserve1, ] = await uniswapSetup.wethDaiPool.getReserves(); - const token0 = await uniswapSetup.wethDaiPool.token0(); - if ( token0 == setup.weth.address ) { - const liquidity0 = subjectMaxTokensIn[0].mul(totalSupply).div(reserve0); - const liquidity1 = subjectMaxTokensIn[1].mul(totalSupply).div(reserve1); - subjectMinLiquidity = liquidity0.lt(liquidity1) ? liquidity0 : liquidity1; - } - else { - const liquidity0 = subjectMaxTokensIn[1].mul(totalSupply).div(reserve0); - const liquidity1 = subjectMaxTokensIn[0].mul(totalSupply).div(reserve1); - subjectMinLiquidity = liquidity0.lt(liquidity1) ? liquidity0 : liquidity1; - } + const [reserveA, reserveB] = await getReserves(uniswapSetup.wethDaiPool, subjectComponents[0]); + const liquidity0 = subjectMaxTokensIn[0].mul(totalSupply).div(reserveA); + const liquidity1 = subjectMaxTokensIn[1].mul(totalSupply).div(reserveB); + subjectMinLiquidity = liquidity0.lt(liquidity1) ? liquidity0 : liquidity1; await setup.weth.connect(owner.wallet) .approve(uniswapV2AmmAdapter.address, MAX_UINT_256); await setup.dai.connect(owner.wallet) @@ -295,31 +288,34 @@ describe("UniswapV2AmmAdapter", () => { subjectAmmPool, subjectComponents, subjectMaxTokensIn, - subjectMinLiquidity); + subjectMinLiquidity, + true); } it("should add the correct liquidity", async () => { const totalSupply = await uniswapSetup.wethDaiPool.totalSupply(); - const [reserve0, reserve1, ] = await uniswapSetup.wethDaiPool.getReserves(); + const [reserveA, reserveB] = await getReserves(uniswapSetup.wethDaiPool, subjectComponents[0]); + const ownerWethBalance = await setup.weth.balanceOf(owner.address); + const ownerDaiBalance = await setup.dai.balanceOf(owner.address); + const ownerLpBalance = await uniswapSetup.wethDaiPool.balanceOf(owner.address); await subject(); const updatedTotalSupply = await uniswapSetup.wethDaiPool.totalSupply(); - const [updatedReserve0, updatedReserve1, ] = await uniswapSetup.wethDaiPool.getReserves(); + const [updatedReserveA, updatedReserveB] = await getReserves(uniswapSetup.wethDaiPool, subjectComponents[0]); expect(updatedTotalSupply).to.eq(totalSupply.add(subjectMinLiquidity)); - const token0 = await uniswapSetup.wethDaiPool.token0(); - if ( token0 == setup.weth.address ) { - expect(updatedReserve0).to.eq(reserve0.add(subjectMaxTokensIn[0])); - expect(updatedReserve1).to.eq(reserve1.add(subjectMaxTokensIn[1])); - } - else { - expect(updatedReserve0).to.eq(reserve0.add(subjectMaxTokensIn[1])); - expect(updatedReserve1).to.eq(reserve1.add(subjectMaxTokensIn[0])); - } + expect(updatedReserveA).to.eq(reserveA.add(subjectMaxTokensIn[0])); + expect(updatedReserveB).to.eq(reserveB.add(subjectMaxTokensIn[1])); const wethBalance = await setup.weth.balanceOf(uniswapV2AmmAdapter.address); const daiBalance = await setup.dai.balanceOf(uniswapV2AmmAdapter.address); const lpBalance = await uniswapSetup.wethDaiPool.balanceOf(uniswapV2AmmAdapter.address); expect(wethBalance).to.eq(ZERO); expect(daiBalance).to.eq(ZERO); expect(lpBalance).to.eq(ZERO); + const updateOwnerWethBalance = await setup.weth.balanceOf(owner.address); + const updateOwnerDaiBalance = await setup.dai.balanceOf(owner.address); + const updateOwnerLpBalance = await uniswapSetup.wethDaiPool.balanceOf(owner.address); + expect(updateOwnerWethBalance).to.eq(ownerWethBalance.sub(subjectMaxTokensIn[0])); + expect(updateOwnerDaiBalance).to.eq(ownerDaiBalance.sub(subjectMaxTokensIn[1])); + expect(updateOwnerLpBalance).to.eq(ownerLpBalance.add(subjectMinLiquidity)); }); describe("when the pool address is invalid", async () => { @@ -454,10 +450,13 @@ describe("UniswapV2AmmAdapter", () => { it("should add the correct liquidity with weth", async () => { const totalSupply = await uniswapSetup.wethDaiPool.totalSupply(); - const [reserveA, reserveB, ] = await getReserves(uniswapSetup.wethDaiPool, subjectComponent); + const [reserveA, reserveB] = await getReserves(uniswapSetup.wethDaiPool, subjectComponent); + const ownerWethBalance = await setup.weth.balanceOf(owner.address); + const ownerDaiBalance = await setup.dai.balanceOf(owner.address); + const ownerLpBalance = await uniswapSetup.wethDaiPool.balanceOf(owner.address); await subject(); const updatedTotalSupply = await uniswapSetup.wethDaiPool.totalSupply(); - const [updatedReserveA, updatedReserveB, ] = await getReserves(uniswapSetup.wethDaiPool, subjectComponent); + const [updatedReserveA, updatedReserveB] = await getReserves(uniswapSetup.wethDaiPool, subjectComponent); expect(updatedTotalSupply).to.eq(totalSupply.add(subjectMinLiquidity)); expect(updatedReserveA).to.eq(reserveA.add(tokensAdded)); expect(updatedReserveB).to.eq(reserveB); @@ -467,6 +466,12 @@ describe("UniswapV2AmmAdapter", () => { expect(wethBalance).to.eq(ZERO); expect(daiBalance).to.eq(ZERO); expect(lpBalance).to.eq(ZERO); + const updateOwnerWethBalance = await setup.weth.balanceOf(owner.address); + const updateOwnerDaiBalance = await setup.dai.balanceOf(owner.address); + const updateOwnerLpBalance = await uniswapSetup.wethDaiPool.balanceOf(owner.address); + expect(updateOwnerWethBalance).to.eq(ownerWethBalance.sub(tokensAdded)); + expect(updateOwnerDaiBalance).to.eq(ownerDaiBalance); + expect(updateOwnerLpBalance).to.eq(ownerLpBalance.add(subjectMinLiquidity)); }); describe("when providing dai", async () => { @@ -492,6 +497,9 @@ describe("UniswapV2AmmAdapter", () => { it("should add the correct liquidity", async () => { const totalSupply = await uniswapSetup.wethDaiPool.totalSupply(); const [reserveA, reserveB, ] = await getReserves(uniswapSetup.wethDaiPool, subjectComponent); + const ownerWethBalance = await setup.weth.balanceOf(owner.address); + const ownerDaiBalance = await setup.dai.balanceOf(owner.address); + const ownerLpBalance = await uniswapSetup.wethDaiPool.balanceOf(owner.address); await subject(); const updatedTotalSupply = await uniswapSetup.wethDaiPool.totalSupply(); const [updatedReserveA, updatedReserveB, ] = await getReserves(uniswapSetup.wethDaiPool, subjectComponent); @@ -504,6 +512,12 @@ describe("UniswapV2AmmAdapter", () => { expect(wethBalance).to.eq(ZERO); expect(daiBalance).to.eq(ZERO); expect(lpBalance).to.eq(ZERO); + const updateOwnerWethBalance = await setup.weth.balanceOf(owner.address); + const updateOwnerDaiBalance = await setup.dai.balanceOf(owner.address); + const updateOwnerLpBalance = await uniswapSetup.wethDaiPool.balanceOf(owner.address); + expect(updateOwnerWethBalance).to.eq(ownerWethBalance); + expect(updateOwnerDaiBalance).to.eq(ownerDaiBalance.sub(tokensAdded)); + expect(updateOwnerLpBalance).to.eq(ownerLpBalance.add(subjectMinLiquidity)); }); }); @@ -601,6 +615,7 @@ describe("UniswapV2AmmAdapter", () => { subjectComponents, subjectMinTokensOut, subjectLiquidity, + true, ]); expect(JSON.stringify(calldata)).to.eq(JSON.stringify([uniswapV2AmmAdapter.address, ZERO, expectedCallData])); }); @@ -616,17 +631,10 @@ describe("UniswapV2AmmAdapter", () => { subjectAmmPool = uniswapSetup.wethDaiPool.address; subjectComponents = [setup.weth.address, setup.dai.address]; subjectLiquidity = await uniswapSetup.wethDaiPool.balanceOf(owner.address); - const token0 = await uniswapSetup.wethDaiPool.token0(); const totalSupply = await uniswapSetup.wethDaiPool.totalSupply(); - const [reserve0, reserve1, ] = await uniswapSetup.wethDaiPool.getReserves(); - if ( token0 == setup.weth.address ) { - subjectMinTokensOut = [reserve0.mul(subjectLiquidity).div(totalSupply), - reserve1.mul(subjectLiquidity).div(totalSupply)]; - } - else { - subjectMinTokensOut = [reserve1.mul(subjectLiquidity).div(totalSupply), - reserve0.mul(subjectLiquidity).div(totalSupply)]; - } + const [reserveA, reserveB] = await getReserves(uniswapSetup.wethDaiPool, subjectComponents[0]); + subjectMinTokensOut = [reserveA.mul(subjectLiquidity).div(totalSupply), + reserveB.mul(subjectLiquidity).div(totalSupply)]; await uniswapSetup.wethDaiPool.connect(owner.wallet) .approve(uniswapV2AmmAdapter.address, MAX_UINT_256); }); @@ -636,29 +644,32 @@ describe("UniswapV2AmmAdapter", () => { subjectAmmPool, subjectComponents, subjectMinTokensOut, - subjectLiquidity); + subjectLiquidity, + true); } it("should remove the correct liquidity", async () => { - const token0 = await uniswapSetup.wethDaiPool.token0(); - const [reserve0, reserve1, ] = await uniswapSetup.wethDaiPool.getReserves(); + const [reserveA, reserveB] = await getReserves(uniswapSetup.wethDaiPool, subjectComponents[0]); + const ownerWethBalance = await setup.weth.balanceOf(owner.address); + const ownerDaiBalance = await setup.dai.balanceOf(owner.address); + const ownerLpBalance = await uniswapSetup.wethDaiPool.balanceOf(owner.address); await subject(); expect(await uniswapSetup.wethDaiPool.balanceOf(owner.address)).to.be.eq(ZERO); - const [updatedReserve0, updatedReserve1, ] = await uniswapSetup.wethDaiPool.getReserves(); - if ( token0 == setup.weth.address ) { - expect(updatedReserve0).to.be.eq(reserve0.sub(subjectMinTokensOut[0])); - expect(updatedReserve1).to.be.eq(reserve1.sub(subjectMinTokensOut[1])); - } - else { - expect(updatedReserve0).to.be.eq(reserve0.sub(subjectMinTokensOut[1])); - expect(updatedReserve1).to.be.eq(reserve1.sub(subjectMinTokensOut[0])); - } + const [updatedReserveA, updatedReserveB] = await getReserves(uniswapSetup.wethDaiPool, subjectComponents[0]); + expect(updatedReserveA).to.be.eq(reserveA.sub(subjectMinTokensOut[0])); + expect(updatedReserveB).to.be.eq(reserveB.sub(subjectMinTokensOut[1])); const wethBalance = await setup.weth.balanceOf(uniswapV2AmmAdapter.address); const daiBalance = await setup.dai.balanceOf(uniswapV2AmmAdapter.address); const lpBalance = await uniswapSetup.wethDaiPool.balanceOf(uniswapV2AmmAdapter.address); expect(wethBalance).to.eq(ZERO); expect(daiBalance).to.eq(ZERO); expect(lpBalance).to.eq(ZERO); + const updateOwnerWethBalance = await setup.weth.balanceOf(owner.address); + const updateOwnerDaiBalance = await setup.dai.balanceOf(owner.address); + const updateOwnerLpBalance = await uniswapSetup.wethDaiPool.balanceOf(owner.address); + expect(updateOwnerWethBalance).to.eq(ownerWethBalance.add(subjectMinTokensOut[0])); + expect(updateOwnerDaiBalance).to.eq(ownerDaiBalance.add(subjectMinTokensOut[1])); + expect(updateOwnerLpBalance).to.eq(ownerLpBalance.sub(subjectLiquidity)); }); describe("when the pool address is invalid", async () => { @@ -764,21 +775,13 @@ describe("UniswapV2AmmAdapter", () => { subjectAmmPool = uniswapSetup.wethDaiPool.address; subjectComponent = setup.weth.address; subjectLiquidity = await uniswapSetup.wethDaiPool.balanceOf(owner.address); - const token0 = await uniswapSetup.wethDaiPool.token0(); const totalSupply = await uniswapSetup.wethDaiPool.totalSupply(); - const [reserve0, reserve1, ] = await uniswapSetup.wethDaiPool.getReserves(); - const token0Amount = subjectLiquidity.mul(reserve0).div(totalSupply); - const token1Amount = subjectLiquidity.mul(reserve1).div(totalSupply); - if ( token0 == setup.weth.address ) { - const receivedAmount = await uniswapSetup.router.getAmountOut(token1Amount, - reserve1.sub(token1Amount), reserve0.sub(token0Amount)); - subjectMinTokenOut = token0Amount.add(receivedAmount); - } - else { - const receivedAmount = await uniswapSetup.router.getAmountOut(token0Amount, - reserve0.sub(token0Amount), reserve1.sub(token1Amount)); - subjectMinTokenOut = token1Amount.add(receivedAmount); - } + const [reserveA, reserveB] = await getReserves(uniswapSetup.wethDaiPool, subjectComponent); + const tokenAAmount = subjectLiquidity.mul(reserveA).div(totalSupply); + const tokenBAmount = subjectLiquidity.mul(reserveB).div(totalSupply); + const receivedAmount = await uniswapSetup.router.getAmountOut(tokenBAmount, + reserveB.sub(tokenBAmount), reserveA.sub(tokenAAmount)); + subjectMinTokenOut = tokenAAmount.add(receivedAmount); await uniswapSetup.wethDaiPool.connect(owner.wallet) .approve(uniswapV2AmmAdapter.address, MAX_UINT_256); }); @@ -792,71 +795,63 @@ describe("UniswapV2AmmAdapter", () => { } it("should remove the correct liquidity with weth", async () => { - const token0 = await uniswapSetup.wethDaiPool.token0(); - const [reserve0, reserve1, ] = await uniswapSetup.wethDaiPool.getReserves(); + const [reserveA, reserveB] = await getReserves(uniswapSetup.wethDaiPool, subjectComponent); + const ownerWethBalance = await setup.weth.balanceOf(owner.address); + const ownerDaiBalance = await setup.dai.balanceOf(owner.address); + const ownerLpBalance = await uniswapSetup.wethDaiPool.balanceOf(owner.address); await subject(); + const [updatedReserveA, updatedReserveB] = await getReserves(uniswapSetup.wethDaiPool, subjectComponent); expect(await uniswapSetup.wethDaiPool.balanceOf(owner.address)).to.be.eq(ZERO); - const [updatedReserve0, updatedReserve1, ] = await uniswapSetup.wethDaiPool.getReserves(); - if ( token0 == setup.weth.address ) { - expect(updatedReserve0).to.be.eq(reserve0.sub(subjectMinTokenOut)); - expect(updatedReserve1).to.be.eq(reserve1); - } - else { - expect(updatedReserve0).to.be.eq(reserve0); - expect(updatedReserve1).to.be.eq(reserve1.sub(subjectMinTokenOut)); - } + expect(updatedReserveA).to.be.eq(reserveA.sub(subjectMinTokenOut)); + expect(updatedReserveB).to.be.eq(reserveB); const wethBalance = await setup.weth.balanceOf(uniswapV2AmmAdapter.address); const daiBalance = await setup.dai.balanceOf(uniswapV2AmmAdapter.address); const lpBalance = await uniswapSetup.wethDaiPool.balanceOf(uniswapV2AmmAdapter.address); - const ownerLiquidity = await uniswapSetup.wethDaiPool.balanceOf(owner.address); expect(wethBalance).to.eq(ZERO); expect(daiBalance).to.eq(ZERO); expect(lpBalance).to.eq(ZERO); - expect(ownerLiquidity).to.eq(ZERO); + const updateOwnerWethBalance = await setup.weth.balanceOf(owner.address); + const updateOwnerDaiBalance = await setup.dai.balanceOf(owner.address); + const updateOwnerLpBalance = await uniswapSetup.wethDaiPool.balanceOf(owner.address); + expect(updateOwnerWethBalance).to.eq(ownerWethBalance.add(subjectMinTokenOut)); + expect(updateOwnerDaiBalance).to.eq(ownerDaiBalance); + expect(updateOwnerLpBalance).to.eq(ownerLpBalance.sub(subjectLiquidity)); }); describe("when removing dai", async () => { beforeEach(async () => { subjectComponent = setup.dai.address; - const token0 = await uniswapSetup.wethDaiPool.token0(); const totalSupply = await uniswapSetup.wethDaiPool.totalSupply(); - const [reserve0, reserve1, ] = await uniswapSetup.wethDaiPool.getReserves(); - const token0Amount = subjectLiquidity.mul(reserve0).div(totalSupply); - const token1Amount = subjectLiquidity.mul(reserve1).div(totalSupply); - if ( token0 == setup.dai.address ) { - const receivedAmount = await uniswapSetup.router.getAmountOut(token1Amount, - reserve1.sub(token1Amount), reserve0.sub(token0Amount)); - subjectMinTokenOut = token0Amount.add(receivedAmount); - } - else { - const receivedAmount = await uniswapSetup.router.getAmountOut(token0Amount, - reserve0.sub(token0Amount), reserve1.sub(token1Amount)); - subjectMinTokenOut = token1Amount.add(receivedAmount); - } + const [reserveA, reserveB] = await getReserves(uniswapSetup.wethDaiPool, subjectComponent); + const tokenAAmount = subjectLiquidity.mul(reserveA).div(totalSupply); + const tokenBAmount = subjectLiquidity.mul(reserveB).div(totalSupply); + const receivedAmount = await uniswapSetup.router.getAmountOut(tokenBAmount, + reserveB.sub(tokenBAmount), reserveA.sub(tokenAAmount)); + subjectMinTokenOut = tokenAAmount.add(receivedAmount); }); it("should remove the correct liquidity", async () => { - const token0 = await uniswapSetup.wethDaiPool.token0(); - const [reserve0, reserve1, ] = await uniswapSetup.wethDaiPool.getReserves(); + const [reserveA, reserveB] = await getReserves(uniswapSetup.wethDaiPool, subjectComponent); + const ownerWethBalance = await setup.weth.balanceOf(owner.address); + const ownerDaiBalance = await setup.dai.balanceOf(owner.address); + const ownerLpBalance = await uniswapSetup.wethDaiPool.balanceOf(owner.address); await subject(); + const [updatedReserveA, updatedReserveB] = await getReserves(uniswapSetup.wethDaiPool, subjectComponent); expect(await uniswapSetup.wethDaiPool.balanceOf(owner.address)).to.be.eq(ZERO); - const [updatedReserve0, updatedReserve1, ] = await uniswapSetup.wethDaiPool.getReserves(); - if ( token0 == setup.dai.address ) { - expect(updatedReserve0).to.be.eq(reserve0.sub(subjectMinTokenOut)); - expect(updatedReserve1).to.be.eq(reserve1); - } - else { - expect(updatedReserve0).to.be.eq(reserve0); - expect(updatedReserve1).to.be.eq(reserve1.sub(subjectMinTokenOut)); - } + expect(updatedReserveA).to.be.eq(reserveA.sub(subjectMinTokenOut)); + expect(updatedReserveB).to.be.eq(reserveB); const wethBalance = await setup.weth.balanceOf(uniswapV2AmmAdapter.address); const daiBalance = await setup.dai.balanceOf(uniswapV2AmmAdapter.address); const lpBalance = await uniswapSetup.wethDaiPool.balanceOf(uniswapV2AmmAdapter.address); - const ownerLiquidity = await uniswapSetup.wethDaiPool.balanceOf(owner.address); expect(wethBalance).to.eq(ZERO); expect(daiBalance).to.eq(ZERO); expect(lpBalance).to.eq(ZERO); - expect(ownerLiquidity).to.eq(ZERO); + const updateOwnerWethBalance = await setup.weth.balanceOf(owner.address); + const updateOwnerDaiBalance = await setup.dai.balanceOf(owner.address); + const updateOwnerLpBalance = await uniswapSetup.wethDaiPool.balanceOf(owner.address); + expect(updateOwnerWethBalance).to.eq(ownerWethBalance); + expect(updateOwnerDaiBalance).to.eq(ownerDaiBalance.add(subjectMinTokenOut)); + expect(updateOwnerLpBalance).to.eq(ownerLpBalance.sub(subjectLiquidity)); }); }); @@ -960,18 +955,10 @@ describe("UniswapV2AmmAdapter", () => { subjectMaxComponentQuantities = [ether(1), ether(3000)]; subjectCaller = owner; const totalSupply = await uniswapSetup.wethDaiPool.totalSupply(); - const [reserve0, reserve1, ] = await uniswapSetup.wethDaiPool.getReserves(); - const token0 = await uniswapSetup.wethDaiPool.token0(); - if ( token0 == setup.weth.address ) { - const liquidity0 = ether(1).mul(totalSupply).div(reserve0); - const liquidity1 = ether(3000).mul(totalSupply).div(reserve1); - subjectMinPoolTokensToMint = liquidity0.lt(liquidity1) ? liquidity0 : liquidity1; - } - else { - const liquidity0 = ether(3000).mul(totalSupply).div(reserve0); - const liquidity1 = ether(1).mul(totalSupply).div(reserve1); - subjectMinPoolTokensToMint = liquidity0.lt(liquidity1) ? liquidity0 : liquidity1; - } + const [reserveA, reserveB] = await getReserves(uniswapSetup.wethDaiPool, subjectComponentsToInput[0]); + const liquidityA = subjectMaxComponentQuantities[0].mul(totalSupply).div(reserveA); + const liquidityB = subjectMaxComponentQuantities[1].mul(totalSupply).div(reserveB); + subjectMinPoolTokensToMint = liquidityA.lt(liquidityB) ? liquidityA : liquidityB; }); async function subject(): Promise { @@ -1016,21 +1003,11 @@ describe("UniswapV2AmmAdapter", () => { wethRemaining = ether(0.5); subjectMaxComponentQuantities = [ether(0.5), ether(1600)]; const totalSupply = await uniswapSetup.wethDaiPool.totalSupply(); - const [reserve0, reserve1, ] = await uniswapSetup.wethDaiPool.getReserves(); - const token0 = await uniswapSetup.wethDaiPool.token0(); - if ( token0 == setup.weth.address ) { - const liquidity0 = ether(0.5).mul(totalSupply).div(reserve0); - const liquidity1 = ether(1600).mul(totalSupply).div(reserve1); - subjectMinPoolTokensToMint = liquidity0.lt(liquidity1) ? liquidity0 : liquidity1; - daiRemaining = ether(3000).sub(ether(0.5).mul(reserve1).div(reserve0)); - } - else { - const liquidity0 = ether(1600).mul(totalSupply).div(reserve0); - const liquidity1 = ether(0.5).mul(totalSupply).div(reserve1); - subjectMinPoolTokensToMint = liquidity0.lt(liquidity1) ? liquidity0 : liquidity1; - daiRemaining = ether(3000).sub(ether(0.5).mul(reserve0).div(reserve1)); - } - + const [reserveA, reserveB] = await getReserves(uniswapSetup.wethDaiPool, subjectComponentsToInput[0]); + const liquidityA = ether(0.5).mul(totalSupply).div(reserveA); + const liquidityB = ether(1600).mul(totalSupply).div(reserveB); + subjectMinPoolTokensToMint = liquidityA.lt(liquidityB) ? liquidityA : liquidityB; + daiRemaining = ether(3000).sub(ether(0.5).mul(reserveB).div(reserveA)); }); it("should mint the correct amount of liquidity tokens to the caller", async () => { @@ -1059,21 +1036,11 @@ describe("UniswapV2AmmAdapter", () => { daiRemaining = ether(1500); subjectMaxComponentQuantities = [ether(0.6), ether(1500)]; const totalSupply = await uniswapSetup.wethDaiPool.totalSupply(); - const [reserve0, reserve1, ] = await uniswapSetup.wethDaiPool.getReserves(); - const token0 = await uniswapSetup.wethDaiPool.token0(); - if ( token0 == setup.weth.address ) { - const liquidity0 = ether(0.6).mul(totalSupply).div(reserve0); - const liquidity1 = ether(1500).mul(totalSupply).div(reserve1); - subjectMinPoolTokensToMint = liquidity0.lt(liquidity1) ? liquidity0 : liquidity1; - wethRemaining = ether(1).sub(ether(1500).mul(reserve0).div(reserve1)); - } - else { - const liquidity0 = ether(1500).mul(totalSupply).div(reserve0); - const liquidity1 = ether(0.6).mul(totalSupply).div(reserve1); - subjectMinPoolTokensToMint = liquidity0.lt(liquidity1) ? liquidity0 : liquidity1; - wethRemaining = ether(1).sub(ether(1500).mul(reserve1).div(reserve0)); - } - + const [reserveA, reserveB] = await getReserves(uniswapSetup.wethDaiPool, subjectComponentsToInput[0]); + const liquidityA = ether(0.6).mul(totalSupply).div(reserveA); + const liquidityB = ether(1500).mul(totalSupply).div(reserveB); + subjectMinPoolTokensToMint = liquidityA.lt(liquidityB) ? liquidityA : liquidityB; + wethRemaining = ether(1).sub(ether(1500).mul(reserveA).div(reserveB)); }); it("should mint the correct amount of liquidity tokens to the caller", async () => { @@ -1202,19 +1169,11 @@ describe("UniswapV2AmmAdapter", () => { subjectAmmPool = uniswapSetup.wethDaiPool.address; subjectComponentsToOutput = [setup.weth.address, setup.dai.address]; const totalSupply = await uniswapSetup.wethDaiPool.totalSupply(); - const [reserve0, reserve1, ] = await uniswapSetup.wethDaiPool.getReserves(); + const [reserveA, reserveB] = await getReserves(uniswapSetup.wethDaiPool, subjectComponentsToOutput[0]); subjectPoolTokens = ether(1); - const token0 = await uniswapSetup.wethDaiPool.token0(); - if ( token0 == setup.weth.address ) { - const weth = subjectPoolTokens.mul(reserve0).div(totalSupply); - const dai = subjectPoolTokens.mul(reserve1).div(totalSupply); - subjectMinComponentQuantities = [weth, dai]; - } - else { - const dai = subjectPoolTokens.mul(reserve0).div(totalSupply); - const weth = subjectPoolTokens.mul(reserve1).div(totalSupply); - subjectMinComponentQuantities = [weth, dai]; - } + const weth = subjectPoolTokens.mul(reserveA).div(totalSupply); + const dai = subjectPoolTokens.mul(reserveB).div(totalSupply); + subjectMinComponentQuantities = [weth, dai]; subjectCaller = owner; }); @@ -1293,21 +1252,13 @@ describe("UniswapV2AmmAdapter", () => { subjectAmmPool = uniswapSetup.wethDaiPool.address; subjectComponentToOutput = setup.weth.address; subjectPoolTokens = ether(1); - const token0 = await uniswapSetup.wethDaiPool.token0(); const totalSupply = await uniswapSetup.wethDaiPool.totalSupply(); - const [reserve0, reserve1, ] = await uniswapSetup.wethDaiPool.getReserves(); - const token0Amount = subjectPoolTokens.mul(reserve0).div(totalSupply); - const token1Amount = subjectPoolTokens.mul(reserve1).div(totalSupply); - if ( token0 == setup.weth.address ) { - const receivedAmount = await uniswapSetup.router.getAmountOut(token1Amount, - reserve1.sub(token1Amount), reserve0.sub(token0Amount)); - subjectMinComponentQuantity = token0Amount.add(receivedAmount); - } - else { - const receivedAmount = await uniswapSetup.router.getAmountOut(token0Amount, - reserve0.sub(token0Amount), reserve1.sub(token1Amount)); - subjectMinComponentQuantity = token1Amount.add(receivedAmount); - } + const [reserveA, reserveB] = await getReserves(uniswapSetup.wethDaiPool, subjectComponentToOutput); + const tokenAAmount = subjectPoolTokens.mul(reserveA).div(totalSupply); + const tokenBAmount = subjectPoolTokens.mul(reserveB).div(totalSupply); + const receivedAmount = await uniswapSetup.router.getAmountOut(tokenBAmount, + reserveB.sub(tokenBAmount), reserveA.sub(tokenAAmount)); + subjectMinComponentQuantity = tokenAAmount.add(receivedAmount); subjectCaller = owner; }); From 40dff2f64d832b942144c414cf7e1a9eba0398cf Mon Sep 17 00:00:00 2001 From: Stephen Hankinson Date: Wed, 25 Aug 2021 22:59:46 -0300 Subject: [PATCH 18/27] Approve the correct liquidity size --- contracts/protocol/modules/AmmModule.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/protocol/modules/AmmModule.sol b/contracts/protocol/modules/AmmModule.sol index 40e88d5ee..8b528b81d 100644 --- a/contracts/protocol/modules/AmmModule.sol +++ b/contracts/protocol/modules/AmmModule.sol @@ -233,7 +233,7 @@ contract AmmModule is ModuleBase, ReentrancyGuard { _setToken.invokeApprove( _ammPool, actionInfo.ammAdapter.getSpenderAddress(_ammPool), - _poolTokenPositionUnits + actionInfo.liquidityQuantity ); _executeRemoveLiquidity(actionInfo); @@ -291,7 +291,7 @@ contract AmmModule is ModuleBase, ReentrancyGuard { _setToken.invokeApprove( _ammPool, actionInfo.ammAdapter.getSpenderAddress(_ammPool), - _poolTokenPositionUnits + actionInfo.liquidityQuantity ); _executeRemoveLiquiditySingleAsset(actionInfo); From 2dc13adac895b8e145ce0fe24447889ee01b51e1 Mon Sep 17 00:00:00 2001 From: Stephen Hankinson Date: Thu, 26 Aug 2021 23:52:51 -0300 Subject: [PATCH 19/27] Implements updates requested in PR review --- contracts/interfaces/IAmmAdapter.sol | 4 + .../integration/amm/UniswapV2AmmAdapter.sol | 600 +++---------- contracts/protocol/modules/AmmModule.sol | 4 + .../amm/UniswapV2AmmAdapter.spec.ts | 822 +++--------------- utils/deploys/deployAdapters.ts | 9 +- 5 files changed, 256 insertions(+), 1183 deletions(-) diff --git a/contracts/interfaces/IAmmAdapter.sol b/contracts/interfaces/IAmmAdapter.sol index 13921fff8..ba1fa6200 100644 --- a/contracts/interfaces/IAmmAdapter.sol +++ b/contracts/interfaces/IAmmAdapter.sol @@ -25,6 +25,7 @@ pragma solidity 0.6.10; interface IAmmAdapter { function getProvideLiquidityCalldata( + address _setToken, address _pool, address[] calldata _components, uint256[] calldata _maxTokensIn, @@ -35,6 +36,7 @@ interface IAmmAdapter { returns (address _target, uint256 _value, bytes memory _calldata); function getProvideLiquiditySingleAssetCalldata( + address _setToken, address _pool, address _component, uint256 _maxTokenIn, @@ -42,6 +44,7 @@ interface IAmmAdapter { ) external view returns (address _target, uint256 _value, bytes memory _calldata); function getRemoveLiquidityCalldata( + address _setToken, address _pool, address[] calldata _components, uint256[] calldata _minTokensOut, @@ -49,6 +52,7 @@ interface IAmmAdapter { ) external view returns (address _target, uint256 _value, bytes memory _calldata); function getRemoveLiquiditySingleAssetCalldata( + address _setToken, address _pool, address _component, uint256 _minTokenOut, diff --git a/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol b/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol index ad9915d7f..2b2f39784 100644 --- a/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol +++ b/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol @@ -23,14 +23,19 @@ import "../../../interfaces/external/IUniswapV2Pair.sol"; import "../../../interfaces/external/IUniswapV2Factory.sol"; import "../../../interfaces/IAmmAdapter.sol"; import "@openzeppelin/contracts/math/SafeMath.sol"; -import "@uniswap/lib/contracts/libraries/Babylonian.sol"; -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; struct Position { - IERC20 tokenA; + address setToken; + address tokenA; uint256 amountA; - IERC20 tokenB; + address tokenB; uint256 amountB; + uint256 balance; + uint256 totalSupply; + uint256 reserveA; + uint256 reserveB; + uint256 calculatedAmountA; + uint256 calculatedAmountB; } /** @@ -45,25 +50,15 @@ contract UniswapV2AmmAdapter is IAmmAdapter { /* ============ State Variables ============ */ // Address of Uniswap V2 Router contract - IUniswapV2Router public immutable router; + address public immutable router; IUniswapV2Factory public immutable factory; - // Fee settings for the AMM - uint256 internal immutable feeNumerator; - uint256 internal immutable feeDenominator; - // Internal function string for adding liquidity string internal constant ADD_LIQUIDITY = - "addLiquidity(address,address[],uint256[],uint256,bool)"; - // Internal function string for adding liquidity with a single asset - string internal constant ADD_LIQUIDITY_SINGLE_ASSET = - "addLiquiditySingleAsset(address,address,uint256,uint256)"; + "addLiquidity(address,address,uint256,uint256,uint256,uint256,address,uint256)"; // Internal function string for removing liquidity string internal constant REMOVE_LIQUIDITY = - "removeLiquidity(address,address[],uint256[],uint256,bool)"; - // Internal function string for removing liquidity to a single asset - string internal constant REMOVE_LIQUIDITY_SINGLE_ASSET = - "removeLiquiditySingleAsset(address,address,uint256,uint256)"; + "removeLiquidity(address,address,uint256,uint256,uint256,address,uint256)"; /* ============ Constructor ============ */ @@ -71,139 +66,25 @@ contract UniswapV2AmmAdapter is IAmmAdapter { * Set state variables * * @param _router Address of Uniswap V2 Router contract - * @param _feeNumerator Numerator of the fee component (usually 997) - * @param _feeDenominator Denominator of the fee component (usually 1000) */ - constructor(address _router, uint256 _feeNumerator, uint256 _feeDenominator) public { - router = IUniswapV2Router(_router); + constructor(address _router) public { + router = _router; factory = IUniswapV2Factory(IUniswapV2Router(_router).factory()); - feeNumerator = _feeNumerator; - feeDenominator = _feeDenominator; - } - - /* ============ Internal Functions =================== */ - - /** - * Returns the pair reserves in an expected order - * - * @param pair The pair to get the reserves from - * @param tokenA Address of the token to swap - */ - function getReserves( - IUniswapV2Pair pair, - address tokenA - ) - internal - view - returns ( - uint reserveA, - uint reserveB - ) - { - address token0 = pair.token0(); - (uint reserve0, uint reserve1,) = pair.getReserves(); - (reserveA, reserveB) = tokenA == token0 ? (reserve0, reserve1) : (reserve1, reserve0); - } - - /** - * Performs a swap via the Uniswap V2 Router - * - * @param pair The pair to perform the swap on - * @param tokenA Address of the token to swap - * @param tokenB Address of pair token0 - * @param amount Amount of the token to swap - */ - function performSwap( - IUniswapV2Pair pair, - address tokenA, - address tokenB, - uint256 amount - ) - internal - returns ( - uint[] memory amounts - ) - { - - // Get the reserves of the pair - (uint256 reserveA, uint256 reserveB) = getReserves(pair, tokenA); - - // Use half of the provided amount in the swap - uint256 amountToSwap = this.calculateSwapAmount(amount, reserveA); - - // Approve the router to spend the tokens - IERC20(tokenA).approve(address(router), amountToSwap); - - // Determine how much we should expect of token1 - uint256 amountOut = router.getAmountOut(amountToSwap, reserveA, reserveB); - - // Perform the swap - address[] memory path = new address[](2); - path[0] = tokenA; - path[1] = tokenB; - amounts = router.swapExactTokensForTokens( - amountToSwap, - amountOut, - path, - address(this), - block.timestamp // solhint-disable-line not-rely-on-time - ); - - // How much token do we have left? - amounts[0] = amount.sub(amountToSwap); - } /* ============ External Getter Functions ============ */ - /** - * Returns the amount of tokenA to swap - * - * @param amountA The amount of tokenA being supplied - * @param reserveA The reserve of tokenA in the pool - */ - function calculateSwapAmount( - uint256 amountA, - uint256 reserveA - ) - external - view - returns ( - uint256 swapAmount - ) - { - // Solves the following system of equations to find the ideal swapAmount - // eq1: amountA = swapAmount + amountALP - // eq2: amountBLP = swapAmount * feeNumerator * reserveB / (reserveA * feeDenominator + swapAmount * feeNumerator) - // eq3: amountALP = amountBLP * (reserveA + swapAmount) / (reserveB - amountBLP) - // Substitution: swapAmount^2 * feeNumerator + swapAmount * reserveA * (feeNumerator + feeDenominator) - amountA * reserveA * feeDenominator = 0 - // Solution: swapAmount = (-b +/- sqrt(b^2-4ac))/(2a) - // a = feeNumerator - // b = reserveA * (feeNumerator + feeDenominator) - // c = -amountA * reserveA * feeDenominator - // Note: a is always positive. b is always positive. The solved - // equation has a negative multiplier on c but that is ignored here because the - // negative in front of the 4ac in the quadratic solution would cancel it out, - // making it an addition. Since b is always positive, we never want to take - // the negative square root solution since that would always cause a negative - // swapAmount, which doesn't make sense. Therefore, we only use the positive - // square root value as the solution. - uint256 b = reserveA.mul(feeNumerator.add(feeDenominator)); - uint256 c = amountA.mul(feeDenominator).mul(reserveA); - - swapAmount = Babylonian.sqrt(b.mul(b).add(feeNumerator.mul(c).mul(4))) - .sub(b).div(feeNumerator.mul(2)); - } - /** * Return calldata for the add liquidity call * + * @param _setToken Address of the SetToken * @param _pool Address of liquidity token * @param _components Address array required to add liquidity * @param _maxTokensIn AmountsIn desired to add liquidity * @param _minLiquidity Min liquidity amount to add */ function getProvideLiquidityCalldata( + address _setToken, address _pool, address[] calldata _components, uint256[] calldata _maxTokensIn, @@ -212,67 +93,74 @@ contract UniswapV2AmmAdapter is IAmmAdapter { external view override - returns ( - address _target, - uint256 _value, - bytes memory _calldata - ) + returns (address target, uint256 value, bytes memory data) { - _target = address(this); - _value = 0; - _calldata = abi.encodeWithSignature( + IUniswapV2Pair pair = IUniswapV2Pair(_pool); + require(factory == IUniswapV2Factory(pair.factory()), "_pool factory doesn't match the router factory"); + require(_components.length == 2, "_components length is invalid"); + require(_maxTokensIn.length == 2, "_maxTokensIn length is invalid"); + require(factory.getPair(_components[0], _components[1]) == _pool, + "_pool doesn't match the components"); + require(_maxTokensIn[0] > 0, "supplied token0 must be greater than 0"); + require(_maxTokensIn[1] > 0, "supplied token1 must be greater than 0"); + require(_minLiquidity > 0, "_minLiquidity must be greater than 0"); + + Position memory position = Position(_setToken, _components[0], _maxTokensIn[0], _components[1], _maxTokensIn[1], + 0, pair.totalSupply(), 0, 0, 0, 0); + require(position.totalSupply > 0, "_pool totalSupply must be > 0"); + + // Determine how much of each token the _minLiquidity would return + (position.reserveA, position.reserveB) = _getReserves(pair, position.tokenA); + position.calculatedAmountA = position.reserveA.mul(_minLiquidity).div(position.totalSupply); + position.calculatedAmountB = position.reserveB.mul(_minLiquidity).div(position.totalSupply); + + require(position.calculatedAmountA <= position.amountA && position.calculatedAmountB <= position.amountB, + "_minLiquidity is too high for input token limit"); + + target = router; + value = 0; + data = abi.encodeWithSignature( ADD_LIQUIDITY, - _pool, - _components, - _maxTokensIn, - _minLiquidity, - true + position.tokenA, + position.tokenB, + position.amountA, + position.amountB, + position.calculatedAmountA, + position.calculatedAmountB, + position.setToken, + block.timestamp // solhint-disable-line not-rely-on-time ); } /** * Return calldata for the add liquidity call for a single asset - * - * @param _pool Address of liquidity token - * @param _component Address of the token used to add liquidity - * @param _maxTokenIn AmountsIn desired to add liquidity - * @param _minLiquidity Min liquidity amount to add */ function getProvideLiquiditySingleAssetCalldata( - address _pool, - address _component, - uint256 _maxTokenIn, - uint256 _minLiquidity + address, + address, + address, + uint256, + uint256 ) external view override - returns ( - address _target, - uint256 _value, - bytes memory _calldata - ) + returns (address, uint256, bytes memory) { - _target = address(this); - _value = 0; - _calldata = abi.encodeWithSignature( - ADD_LIQUIDITY_SINGLE_ASSET, - _pool, - _component, - _maxTokenIn, - _minLiquidity - ); + revert("Uniswap V2 single asset addition is not supported"); } /** * Return calldata for the remove liquidity call * + * @param _setToken Address of the SetToken * @param _pool Address of liquidity token * @param _components Address array required to remove liquidity * @param _minTokensOut AmountsOut minimum to remove liquidity * @param _liquidity Liquidity amount to remove */ function getRemoveLiquidityCalldata( + address _setToken, address _pool, address[] calldata _components, uint256[] calldata _minTokensOut, @@ -281,56 +169,61 @@ contract UniswapV2AmmAdapter is IAmmAdapter { external view override - returns ( - address _target, - uint256 _value, - bytes memory _calldata - ) + returns (address target, uint256 value, bytes memory data) { - _target = address(this); - _value = 0; - _calldata = abi.encodeWithSignature( + IUniswapV2Pair pair = IUniswapV2Pair(_pool); + require(factory == IUniswapV2Factory(pair.factory()), "_pool factory doesn't match the router factory"); + require(_components.length == 2, "_components length is invalid"); + require(_minTokensOut.length == 2, "_minTokensOut length is invalid"); + require(factory.getPair(_components[0], _components[1]) == _pool, + "_pool doesn't match the components"); + require(_minTokensOut[0] > 0, "requested token0 must be greater than 0"); + require(_minTokensOut[1] > 0, "requested token1 must be greater than 0"); + require(_liquidity > 0, "_liquidity must be greater than 0"); + + Position memory position = Position(_setToken, _components[0], _minTokensOut[0], _components[1], _minTokensOut[1], + pair.balanceOf(_setToken), pair.totalSupply(), 0, 0, 0, 0); + + require(_liquidity <= position.balance, "_liquidity must be <= to current balance"); + + // Calculate how many tokens are owned by the liquidity + (position.reserveA, position.reserveB) = _getReserves(pair, position.tokenA); + position.calculatedAmountA = position.reserveA.mul(position.balance).div(position.totalSupply); + position.calculatedAmountB = position.reserveB.mul(position.balance).div(position.totalSupply); + + require(position.amountA <= position.calculatedAmountA && position.amountB <= position.calculatedAmountB, + "amounts must be <= ownedTokens"); + + target = router; + value = 0; + data = abi.encodeWithSignature( REMOVE_LIQUIDITY, - _pool, - _components, - _minTokensOut, + position.tokenA, + position.tokenB, _liquidity, - true + position.amountA, + position.amountB, + position.setToken, + block.timestamp // solhint-disable-line not-rely-on-time ); } /** * Return calldata for the remove liquidity single asset call - * - * @param _pool Address of liquidity token - * @param _component Address of token required to remove liquidity - * @param _minTokenOut AmountsOut minimum to remove liquidity - * @param _liquidity Liquidity amount to remove */ function getRemoveLiquiditySingleAssetCalldata( - address _pool, - address _component, - uint256 _minTokenOut, - uint256 _liquidity + address, + address, + address, + uint256, + uint256 ) external view override - returns ( - address _target, - uint256 _value, - bytes memory _calldata - ) + returns (address, uint256, bytes memory) { - _target = address(this); - _value = 0; - _calldata = abi.encodeWithSignature( - REMOVE_LIQUIDITY_SINGLE_ASSET, - _pool, - _component, - _minTokenOut, - _liquidity - ); + revert("Uniswap V2 single asset removal is not supported"); } /** @@ -342,12 +235,12 @@ contract UniswapV2AmmAdapter is IAmmAdapter { external view override - returns (address) + returns (address spender) { IUniswapV2Pair pair = IUniswapV2Pair(_pool); require(factory == IUniswapV2Factory(pair.factory()), "_pool factory doesn't match the router factory"); - return address(this); + spender = router; } /** @@ -355,7 +248,11 @@ contract UniswapV2AmmAdapter is IAmmAdapter { * * @param _pool Address of liquidity token */ - function isValidPool(address _pool) external view override returns (bool) { + function isValidPool(address _pool) + external + view + override + returns (bool isValid) { address token0; address token1; bool success = true; @@ -374,289 +271,32 @@ contract UniswapV2AmmAdapter is IAmmAdapter { } if( success ) { - return factory.getPair(token0, token1) == _pool; + isValid = factory.getPair(token0, token1) == _pool; } else { return false; } } - /* ============ External Setter Functions ============ */ - - /** - * Adds liquidity via the Uniswap V2 Router - * - * @param _pool Address of liquidity token - * @param _components Address array required to add liquidity - * @param _maxTokensIn AmountsIn desired to add liquidity - * @param _minLiquidity Min liquidity amount to add - * @param _shouldTransfer Should the tokens be transferred from the sender - */ - function addLiquidity( - address _pool, - address[] memory _components, - uint256[] memory _maxTokensIn, - uint256 _minLiquidity, - bool _shouldTransfer - ) - public - returns ( - uint amountA, - uint amountB, - uint liquidity - ) - { - - IUniswapV2Pair pair = IUniswapV2Pair(_pool); - require(factory == IUniswapV2Factory(pair.factory()), "_pool factory doesn't match the router factory"); - require(_components.length == 2, "_components length is invalid"); - require(_maxTokensIn.length == 2, "_maxTokensIn length is invalid"); - require(factory.getPair(_components[0], _components[1]) == _pool, - "_pool doesn't match the components"); - require(_maxTokensIn[0] > 0, "supplied token0 must be greater than 0"); - require(_maxTokensIn[1] > 0, "supplied token1 must be greater than 0"); - require(_minLiquidity > 0, "_minLiquidity must be greater than 0"); - - Position memory position = Position(IERC20(_components[0]), _maxTokensIn[0], - IERC20(_components[1]), _maxTokensIn[1]); - - uint256 lpTotalSupply = pair.totalSupply(); - require(lpTotalSupply > 0, "_pool totalSupply must be > 0"); - - (uint256 reserveA, uint256 reserveB) = getReserves(pair, _components[0]); - uint256 amountAMin = reserveA.mul(_minLiquidity).div(lpTotalSupply); - uint256 amountBMin = reserveB.mul(_minLiquidity).div(lpTotalSupply); - - require(amountAMin <= position.amountA && amountBMin <= position.amountB, - "_minLiquidity is too high for amount maximums"); - - // Bring the tokens to this contract, if needed, so we can use the Uniswap Router - if( _shouldTransfer ) { - position.tokenA.transferFrom(msg.sender, address(this), position.amountA); - position.tokenB.transferFrom(msg.sender, address(this), position.amountB); - } - - // Approve the router to spend the tokens - position.tokenA.approve(address(router), position.amountA); - position.tokenB.approve(address(router), position.amountB); - - // Add the liquidity - (amountA, amountB, liquidity) = router.addLiquidity( - address(position.tokenA), - address(position.tokenB), - position.amountA, - position.amountB, - amountAMin, - amountBMin, - msg.sender, - block.timestamp // solhint-disable-line not-rely-on-time - ); - - // If there is token0 left, send it back - if( amountA < position.amountA ) { - position.tokenA.transfer(msg.sender, position.amountA.sub(amountA) ); - } - - // If there is token1 left, send it back - if( amountB < position.amountB ) { - position.tokenB.transfer(msg.sender, position.amountB.sub(amountB) ); - } - - } - - /** - * Adds liquidity via the Uniswap V2 Router, swapping first to get both tokens - * - * @param _pool Address of liquidity token - * @param _component Address array required to add liquidity - * @param _maxTokenIn AmountsIn desired to add liquidity - * @param _minLiquidity Min liquidity amount to add - */ - function addLiquiditySingleAsset( - address _pool, - address _component, - uint256 _maxTokenIn, - uint256 _minLiquidity - ) - external - returns ( - uint amountA, - uint amountB, - uint liquidity - ) - { - - IUniswapV2Pair pair = IUniswapV2Pair(_pool); - require(factory == IUniswapV2Factory(pair.factory()), "_pool factory doesn't match the router factory"); - - address tokenA = pair.token0(); - address tokenB = pair.token1(); - require(tokenA == _component || tokenB == _component, "_pool doesn't contain the _component"); - require(_maxTokenIn > 0, "supplied _maxTokenIn must be greater than 0"); - require(_minLiquidity > 0, "supplied _minLiquidity must be greater than 0"); - - // Swap them if needed - if( tokenB == _component ) { - tokenB = tokenA; - tokenA = _component; - } - - uint256 lpTotalSupply = pair.totalSupply(); - require(lpTotalSupply > 0, "_pool totalSupply must be > 0"); - - // Bring the tokens to this contract so we can use the Uniswap Router - IERC20(tokenA).transferFrom(msg.sender, address(this), _maxTokenIn); - - // Execute the swap - uint[] memory amounts = performSwap(pair, tokenA, tokenB, _maxTokenIn); - - address[] memory components = new address[](2); - components[0] = tokenA; - components[1] = tokenB; - - // Add the liquidity - (amountA, amountB, liquidity) = addLiquidity(_pool, components, amounts, _minLiquidity, false); - - } + /* ============ Internal Functions =================== */ /** - * Remove liquidity via the Uniswap V2 Router + * Returns the pair reserves in an expected order * - * @param _pool Address of liquidity token - * @param _components Address array required to remove liquidity - * @param _minTokensOut AmountsOut minimum to remove liquidity - * @param _liquidity Liquidity amount to remove - * @param _shouldReturn Should the tokens be returned to the sender? + * @param pair The pair to get the reserves from + * @param tokenA Address of the token to swap */ - function removeLiquidity( - address _pool, - address[] memory _components, - uint256[] memory _minTokensOut, - uint256 _liquidity, - bool _shouldReturn + function _getReserves( + IUniswapV2Pair pair, + address tokenA ) - public - returns ( - uint amountA, - uint amountB - ) + internal + view + returns (uint reserveA, uint reserveB) { - IUniswapV2Pair pair = IUniswapV2Pair(_pool); - require(factory == IUniswapV2Factory(pair.factory()), "_pool factory doesn't match the router factory"); - require(_components.length == 2, "_components length is invalid"); - require(_minTokensOut.length == 2, "_minTokensOut length is invalid"); - require(factory.getPair(_components[0], _components[1]) == _pool, - "_pool doesn't match the components"); - require(_minTokensOut[0] > 0, "requested token0 must be greater than 0"); - require(_minTokensOut[1] > 0, "requested token1 must be greater than 0"); - require(_liquidity > 0, "_liquidity must be greater than 0"); - - Position memory position = Position(IERC20(_components[0]), _minTokensOut[0], - IERC20(_components[1]), _minTokensOut[1]); - - uint256 balance = pair.balanceOf(msg.sender); - require(_liquidity <= balance, "_liquidity must be <= to current balance"); - - // Calculate how many tokens are owned by the liquidity - uint[] memory tokenInfo = new uint[](3); - tokenInfo[2] = pair.totalSupply(); - (tokenInfo[0], tokenInfo[1]) = getReserves(pair, _components[0]); - tokenInfo[0] = tokenInfo[0].mul(balance).div(tokenInfo[2]); - tokenInfo[1] = tokenInfo[1].mul(balance).div(tokenInfo[2]); - - require(position.amountA <= tokenInfo[0] && position.amountB <= tokenInfo[1], - "amounts must be <= ownedTokens"); - - // Bring the lp token to this contract so we can use the Uniswap Router - pair.transferFrom(msg.sender, address(this), _liquidity); - - // Approve the router to spend the lp tokens - pair.approve(address(router), _liquidity); - - // Remove the liquidity - (amountA, amountB) = router.removeLiquidity( - address(position.tokenA), - address(position.tokenB), - _liquidity, - position.amountA, - position.amountB, - _shouldReturn ? msg.sender : address(this), - block.timestamp // solhint-disable-line not-rely-on-time - ); + address token0 = pair.token0(); + (uint reserve0, uint reserve1,) = pair.getReserves(); + (reserveA, reserveB) = tokenA == token0 ? (reserve0, reserve1) : (reserve1, reserve0); } - /** - * Remove liquidity via the Uniswap V2 Router and swap to a single asset - * - * @param _pool Address of liquidity token - * @param _component Address required to remove liquidity - * @param _minTokenOut AmountOut minimum to remove liquidity - * @param _liquidity Liquidity amount to remove - */ - function removeLiquiditySingleAsset( - address _pool, - address _component, - uint256 _minTokenOut, - uint256 _liquidity - ) - external - returns ( - uint[] memory amounts - ) - { - IUniswapV2Pair pair = IUniswapV2Pair(_pool); - require(factory == IUniswapV2Factory(pair.factory()), "_pool factory doesn't match the router factory"); - - address tokenA = pair.token0(); - address tokenB = pair.token1(); - require(tokenA == _component || tokenB == _component, "_pool doesn't contain the _component"); - require(_minTokenOut > 0, "requested token must be greater than 0"); - require(_liquidity > 0, "_liquidity must be greater than 0"); - - // Swap them if needed - if( tokenB == _component ) { - tokenB = tokenA; - tokenA = _component; - } - - // Determine if enough of the token will be received - uint256 totalSupply = pair.totalSupply(); - (uint256 reserveA, uint256 reserveB) = getReserves(pair, _component); - uint[] memory receivedTokens = new uint[](2); - receivedTokens[0] = reserveA.mul(_liquidity).div(totalSupply); - receivedTokens[1] = reserveB.mul(_liquidity).div(totalSupply); - - address[] memory components = new address[](2); - components[0] = tokenA; - components[1] = tokenB; - - (receivedTokens[0], receivedTokens[1]) = removeLiquidity(_pool, components, receivedTokens, _liquidity, false); - - uint256 amountReceived = router.getAmountOut( - receivedTokens[1], - reserveB.sub(receivedTokens[1]), - reserveA.sub(receivedTokens[0]) - ); - - require( receivedTokens[0].add(amountReceived) >= _minTokenOut, - "_minTokenOut is too high for amount received"); - - // Approve the router to spend the swap tokens - IERC20(tokenB).approve(address(router), receivedTokens[1]); - - // Swap the other token for _component - components[0] = tokenB; - components[1] = tokenA; - amounts = router.swapExactTokensForTokens( - receivedTokens[1], - amountReceived, - components, - address(this), - block.timestamp // solhint-disable-line not-rely-on-time - ); - - // Send the tokens back to the caller - IERC20(tokenA).transfer(msg.sender, receivedTokens[0].add(amounts[1])); - - } } \ No newline at end of file diff --git a/contracts/protocol/modules/AmmModule.sol b/contracts/protocol/modules/AmmModule.sol index 8b528b81d..8f9ae5e7a 100644 --- a/contracts/protocol/modules/AmmModule.sol +++ b/contracts/protocol/modules/AmmModule.sol @@ -447,6 +447,7 @@ contract AmmModule is ModuleBase, ReentrancyGuard { ( address targetAmm, uint256 callValue, bytes memory methodData ) = _actionInfo.ammAdapter.getProvideLiquidityCalldata( + address(_actionInfo.setToken), _actionInfo.liquidityToken, _actionInfo.components, _actionInfo.totalNotionalComponents, @@ -460,6 +461,7 @@ contract AmmModule is ModuleBase, ReentrancyGuard { ( address targetAmm, uint256 callValue, bytes memory methodData ) = _actionInfo.ammAdapter.getProvideLiquiditySingleAssetCalldata( + address(_actionInfo.setToken), _actionInfo.liquidityToken, _actionInfo.components[0], _actionInfo.totalNotionalComponents[0], @@ -473,6 +475,7 @@ contract AmmModule is ModuleBase, ReentrancyGuard { ( address targetAmm, uint256 callValue, bytes memory methodData ) = _actionInfo.ammAdapter.getRemoveLiquidityCalldata( + address(_actionInfo.setToken), _actionInfo.liquidityToken, _actionInfo.components, _actionInfo.totalNotionalComponents, @@ -486,6 +489,7 @@ contract AmmModule is ModuleBase, ReentrancyGuard { ( address targetAmm, uint256 callValue, bytes memory methodData ) = _actionInfo.ammAdapter.getRemoveLiquiditySingleAssetCalldata( + address(_actionInfo.setToken), _actionInfo.liquidityToken, _actionInfo.components[0], _actionInfo.totalNotionalComponents[0], diff --git a/test/protocol/integration/amm/UniswapV2AmmAdapter.spec.ts b/test/protocol/integration/amm/UniswapV2AmmAdapter.spec.ts index f35e56417..eac60fc39 100644 --- a/test/protocol/integration/amm/UniswapV2AmmAdapter.spec.ts +++ b/test/protocol/integration/amm/UniswapV2AmmAdapter.spec.ts @@ -20,7 +20,8 @@ import { getSystemFixture, getUniswapFixture, getUniswapV3Fixture, - getWaffleExpect + getWaffleExpect, + getLastBlockTimestamp } from "@utils/test/index"; import { SystemFixture, UniswapFixture } from "@utils/fixtures"; @@ -77,8 +78,7 @@ describe("UniswapV2AmmAdapter", () => { ammModule = await deployer.modules.deployAmmModule(setup.controller.address); await setup.controller.addModule(ammModule.address); - uniswapV2AmmAdapter = await deployer.adapters.deployUniswapV2AmmAdapter(uniswapSetup.router.address, - BigNumber.from(997), BigNumber.from(1000)); + uniswapV2AmmAdapter = await deployer.adapters.deployUniswapV2AmmAdapter(uniswapSetup.router.address); uniswapV2AmmAdapterName = "UNISWAPV2AMM"; await setup.integrationRegistry.addIntegration( @@ -102,26 +102,26 @@ describe("UniswapV2AmmAdapter", () => { }); describe("getSpenderAddress", async () => { - let spenderAddress: Address; + let poolAddress: Address; before(async () => { - spenderAddress = uniswapSetup.wethDaiPool.address; + poolAddress = uniswapSetup.wethDaiPool.address; }); async function subject(): Promise { - return await uniswapV2AmmAdapter.getSpenderAddress(spenderAddress); + return await uniswapV2AmmAdapter.getSpenderAddress(poolAddress); } it("should return the correct spender address", async () => { const spender = await subject(); - expect(spender).to.eq(uniswapV2AmmAdapter.address); + expect(spender).to.eq(uniswapSetup.router.address); }); describe("when the pool address is invalid", async () => { before(async () => { const uniswapV3Setup = getUniswapV3Fixture(owner.address); await uniswapV3Setup.initialize(owner, setup.weth, 3000.0, setup.wbtc, 40000.0, setup.dai); - spenderAddress = uniswapV3Setup.swapRouter.address; + poolAddress = uniswapV3Setup.swapRouter.address; }); it("should revert", async () => { @@ -174,22 +174,15 @@ describe("UniswapV2AmmAdapter", () => { async function subject(): Promise { return await uniswapV2AmmAdapter.getProvideLiquiditySingleAssetCalldata( + uniswapSetup.router.address, subjectAmmPool, subjectComponent, subjectMaxTokenIn, subjectMinLiquidity); } - it("should return the correct provide liquidity single asset calldata", async () => { - const calldata = await subject(); - - const expectedCallData = uniswapV2AmmAdapter.interface.encodeFunctionData("addLiquiditySingleAsset", [ - subjectAmmPool, - subjectComponent, - subjectMaxTokenIn, - subjectMinLiquidity, - ]); - expect(JSON.stringify(calldata)).to.eq(JSON.stringify([uniswapV2AmmAdapter.address, ZERO, expectedCallData])); + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Uniswap V2 single asset addition is not supported"); }); }); @@ -208,22 +201,15 @@ describe("UniswapV2AmmAdapter", () => { async function subject(): Promise { return await uniswapV2AmmAdapter.getRemoveLiquiditySingleAssetCalldata( - uniswapSetup.wethDaiPool.address, - setup.weth.address, + owner.address, + subjectAmmPool, + subjectComponent[0], subjectMinTokenOut, subjectLiquidity); } - it("should return the correct remove liquidity single asset calldata", async () => { - const calldata = await subject(); - - const expectedCallData = uniswapV2AmmAdapter.interface.encodeFunctionData("removeLiquiditySingleAsset", [ - subjectAmmPool, - subjectComponent, - subjectMinTokenOut, - subjectLiquidity, - ]); - expect(JSON.stringify(calldata)).to.eq(JSON.stringify([uniswapV2AmmAdapter.address, ZERO, expectedCallData])); + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Uniswap V2 single asset removal is not supported"); }); }); @@ -233,89 +219,47 @@ describe("UniswapV2AmmAdapter", () => { let subjectMaxTokensIn: BigNumber[]; let subjectMinLiquidity: BigNumber; - before(async () => { - subjectAmmPool = uniswapSetup.wethDaiPool.address; - subjectComponents = [setup.weth.address, setup.dai.address]; - subjectMaxTokensIn = [ether(1), ether(3000)]; - subjectMinLiquidity = ether(1); - }); - - async function subject(): Promise { - return await uniswapV2AmmAdapter.getProvideLiquidityCalldata( - subjectAmmPool, - subjectComponents, - subjectMaxTokensIn, - subjectMinLiquidity); - } - - it("should return the correct provide liquidity calldata", async () => { - const calldata = await subject(); - - const expectedCallData = uniswapV2AmmAdapter.interface.encodeFunctionData("addLiquidity", [ - subjectAmmPool, - subjectComponents, - subjectMaxTokensIn, - subjectMinLiquidity, - true, - ]); - expect(JSON.stringify(calldata)).to.eq(JSON.stringify([uniswapV2AmmAdapter.address, ZERO, expectedCallData])); - }); - }); - - describe("addLiquidity", async () => { - let subjectAmmPool: Address; - let subjectComponents: Address[]; - let subjectMaxTokensIn: BigNumber[]; - let subjectMinLiquidity: BigNumber; - beforeEach(async () => { subjectAmmPool = uniswapSetup.wethDaiPool.address; subjectComponents = [setup.weth.address, setup.dai.address]; subjectMaxTokensIn = [ether(1), ether(3000)]; const totalSupply = await uniswapSetup.wethDaiPool.totalSupply(); - const [reserveA, reserveB] = await getReserves(uniswapSetup.wethDaiPool, subjectComponents[0]); - const liquidity0 = subjectMaxTokensIn[0].mul(totalSupply).div(reserveA); - const liquidity1 = subjectMaxTokensIn[1].mul(totalSupply).div(reserveB); - subjectMinLiquidity = liquidity0.lt(liquidity1) ? liquidity0 : liquidity1; - await setup.weth.connect(owner.wallet) - .approve(uniswapV2AmmAdapter.address, MAX_UINT_256); - await setup.dai.connect(owner.wallet) - .approve(uniswapV2AmmAdapter.address, MAX_UINT_256); + const [reserveA, reserveB] = await getReserves(uniswapSetup.wethDaiPool, setup.weth.address); + const liquidityA = subjectMaxTokensIn[0].mul(totalSupply).div(reserveA); + const liquidityB = subjectMaxTokensIn[1].mul(totalSupply).div(reserveB); + subjectMinLiquidity = liquidityA.lt(liquidityB) ? liquidityA : liquidityB; }); async function subject(): Promise { - return await uniswapV2AmmAdapter.addLiquidity( + return await uniswapV2AmmAdapter.getProvideLiquidityCalldata( + owner.address, subjectAmmPool, subjectComponents, subjectMaxTokensIn, - subjectMinLiquidity, - true); + subjectMinLiquidity); } - it("should add the correct liquidity", async () => { - const totalSupply = await uniswapSetup.wethDaiPool.totalSupply(); - const [reserveA, reserveB] = await getReserves(uniswapSetup.wethDaiPool, subjectComponents[0]); - const ownerWethBalance = await setup.weth.balanceOf(owner.address); - const ownerDaiBalance = await setup.dai.balanceOf(owner.address); - const ownerLpBalance = await uniswapSetup.wethDaiPool.balanceOf(owner.address); - await subject(); - const updatedTotalSupply = await uniswapSetup.wethDaiPool.totalSupply(); - const [updatedReserveA, updatedReserveB] = await getReserves(uniswapSetup.wethDaiPool, subjectComponents[0]); - expect(updatedTotalSupply).to.eq(totalSupply.add(subjectMinLiquidity)); - expect(updatedReserveA).to.eq(reserveA.add(subjectMaxTokensIn[0])); - expect(updatedReserveB).to.eq(reserveB.add(subjectMaxTokensIn[1])); - const wethBalance = await setup.weth.balanceOf(uniswapV2AmmAdapter.address); - const daiBalance = await setup.dai.balanceOf(uniswapV2AmmAdapter.address); - const lpBalance = await uniswapSetup.wethDaiPool.balanceOf(uniswapV2AmmAdapter.address); - expect(wethBalance).to.eq(ZERO); - expect(daiBalance).to.eq(ZERO); - expect(lpBalance).to.eq(ZERO); - const updateOwnerWethBalance = await setup.weth.balanceOf(owner.address); - const updateOwnerDaiBalance = await setup.dai.balanceOf(owner.address); - const updateOwnerLpBalance = await uniswapSetup.wethDaiPool.balanceOf(owner.address); - expect(updateOwnerWethBalance).to.eq(ownerWethBalance.sub(subjectMaxTokensIn[0])); - expect(updateOwnerDaiBalance).to.eq(ownerDaiBalance.sub(subjectMaxTokensIn[1])); - expect(updateOwnerLpBalance).to.eq(ownerLpBalance.add(subjectMinLiquidity)); + it("should return the correct provide liquidity calldata", async () => { + const calldata = await subject(); + const blockTimestamp = await getLastBlockTimestamp(); + + // Determine how much of each token the _minLiquidity would return + const totalSupply = await uniswapSetup.wethDaiPool.totalSupply(); + const [reserveA, reserveB] = await getReserves(uniswapSetup.wethDaiPool, setup.weth.address); + const amountAMin = reserveA.mul(subjectMinLiquidity).div(totalSupply); + const amountBMin = reserveB.mul(subjectMinLiquidity).div(totalSupply); + + const expectedCallData = uniswapSetup.router.interface.encodeFunctionData("addLiquidity", [ + setup.weth.address, + setup.dai.address, + subjectMaxTokensIn[0], + subjectMaxTokensIn[1], + amountAMin, + amountBMin, + owner.address, + blockTimestamp, + ]); + expect(JSON.stringify(calldata)).to.eq(JSON.stringify([uniswapSetup.router.address, ZERO, expectedCallData])); }); describe("when the pool address is invalid", async () => { @@ -407,181 +351,7 @@ describe("UniswapV2AmmAdapter", () => { }); it("should revert", async () => { - await expect(subject()).to.be.revertedWith("_minLiquidity is too high for amount maximums"); - }); - }); - }); - - describe("addLiquiditySingleAsset", async () => { - let subjectAmmPool: Address; - let subjectComponent: Address; - let subjectMaxTokenIn: BigNumber; - let subjectMinLiquidity: BigNumber; - let tokensAdded: BigNumber; - - beforeEach(async () => { - subjectAmmPool = uniswapSetup.wethDaiPool.address; - subjectComponent = setup.weth.address; - subjectMaxTokenIn = ether(1); - const [reserveA, reserveB] = await getReserves(uniswapSetup.wethDaiPool, subjectComponent); - const totalSupply = await uniswapSetup.wethDaiPool.totalSupply(); - const amountToSwap = await uniswapV2AmmAdapter.calculateSwapAmount(subjectMaxTokenIn, reserveA); - const amountOut = await uniswapSetup.router.getAmountOut(amountToSwap, reserveA, reserveB); - const remaining = subjectMaxTokenIn.sub(amountToSwap); - const quote0 = await uniswapSetup.router.quote(remaining, reserveA.add(amountToSwap), reserveB.sub(amountOut) ); - const quote1 = await uniswapSetup.router.quote(amountOut, reserveB.sub(amountOut), reserveA.add(amountToSwap) ); - const amount0 = quote0 <= amountOut ? remaining : quote1; - const amount1 = quote0 <= amountOut ? quote0 : amountOut; - tokensAdded = amountToSwap.add(amount0); - const liquidity0 = amount0.mul(totalSupply).div(reserveA.add(amountToSwap)); - const liquidity1 = amount1.mul(totalSupply).div(reserveB.sub(amountOut)); - subjectMinLiquidity = liquidity0.lt(liquidity1) ? liquidity0 : liquidity1; - await setup.weth.connect(owner.wallet) - .approve(uniswapV2AmmAdapter.address, MAX_UINT_256); - }); - - async function subject(): Promise { - return await uniswapV2AmmAdapter.addLiquiditySingleAsset( - subjectAmmPool, - subjectComponent, - subjectMaxTokenIn, - subjectMinLiquidity); - } - - it("should add the correct liquidity with weth", async () => { - const totalSupply = await uniswapSetup.wethDaiPool.totalSupply(); - const [reserveA, reserveB] = await getReserves(uniswapSetup.wethDaiPool, subjectComponent); - const ownerWethBalance = await setup.weth.balanceOf(owner.address); - const ownerDaiBalance = await setup.dai.balanceOf(owner.address); - const ownerLpBalance = await uniswapSetup.wethDaiPool.balanceOf(owner.address); - await subject(); - const updatedTotalSupply = await uniswapSetup.wethDaiPool.totalSupply(); - const [updatedReserveA, updatedReserveB] = await getReserves(uniswapSetup.wethDaiPool, subjectComponent); - expect(updatedTotalSupply).to.eq(totalSupply.add(subjectMinLiquidity)); - expect(updatedReserveA).to.eq(reserveA.add(tokensAdded)); - expect(updatedReserveB).to.eq(reserveB); - const wethBalance = await setup.weth.balanceOf(uniswapV2AmmAdapter.address); - const daiBalance = await setup.dai.balanceOf(uniswapV2AmmAdapter.address); - const lpBalance = await uniswapSetup.wethDaiPool.balanceOf(uniswapV2AmmAdapter.address); - expect(wethBalance).to.eq(ZERO); - expect(daiBalance).to.eq(ZERO); - expect(lpBalance).to.eq(ZERO); - const updateOwnerWethBalance = await setup.weth.balanceOf(owner.address); - const updateOwnerDaiBalance = await setup.dai.balanceOf(owner.address); - const updateOwnerLpBalance = await uniswapSetup.wethDaiPool.balanceOf(owner.address); - expect(updateOwnerWethBalance).to.eq(ownerWethBalance.sub(tokensAdded)); - expect(updateOwnerDaiBalance).to.eq(ownerDaiBalance); - expect(updateOwnerLpBalance).to.eq(ownerLpBalance.add(subjectMinLiquidity)); - }); - - describe("when providing dai", async () => { - beforeEach(async () => { - subjectComponent = setup.dai.address; - const [reserveA, reserveB] = await getReserves(uniswapSetup.wethDaiPool, subjectComponent); - const totalSupply = await uniswapSetup.wethDaiPool.totalSupply(); - const amountToSwap = await uniswapV2AmmAdapter.calculateSwapAmount(subjectMaxTokenIn, reserveA); - const amountOut = await uniswapSetup.router.getAmountOut(amountToSwap, reserveA, reserveB); - const remaining = subjectMaxTokenIn.sub(amountToSwap); - const quote0 = await uniswapSetup.router.quote(remaining, reserveA.add(amountToSwap), reserveB.sub(amountOut) ); - const quote1 = await uniswapSetup.router.quote(amountOut, reserveB.sub(amountOut), reserveA.add(amountToSwap) ); - const amount0 = quote0 <= amountOut ? remaining : quote1; - const amount1 = quote0 <= amountOut ? quote0 : amountOut; - tokensAdded = amountToSwap.add(amount0); - const liquidity0 = amount0.mul(totalSupply).div(reserveA.add(amountToSwap)); - const liquidity1 = amount1.mul(totalSupply).div(reserveB.sub(amountOut)); - subjectMinLiquidity = liquidity0.lt(liquidity1) ? liquidity0 : liquidity1; - await setup.dai.connect(owner.wallet) - .approve(uniswapV2AmmAdapter.address, MAX_UINT_256); - }); - - it("should add the correct liquidity", async () => { - const totalSupply = await uniswapSetup.wethDaiPool.totalSupply(); - const [reserveA, reserveB, ] = await getReserves(uniswapSetup.wethDaiPool, subjectComponent); - const ownerWethBalance = await setup.weth.balanceOf(owner.address); - const ownerDaiBalance = await setup.dai.balanceOf(owner.address); - const ownerLpBalance = await uniswapSetup.wethDaiPool.balanceOf(owner.address); - await subject(); - const updatedTotalSupply = await uniswapSetup.wethDaiPool.totalSupply(); - const [updatedReserveA, updatedReserveB, ] = await getReserves(uniswapSetup.wethDaiPool, subjectComponent); - expect(updatedTotalSupply).to.eq(totalSupply.add(subjectMinLiquidity)); - expect(updatedReserveA).to.eq(reserveA.add(tokensAdded)); - expect(updatedReserveB).to.eq(reserveB); - const wethBalance = await setup.weth.balanceOf(uniswapV2AmmAdapter.address); - const daiBalance = await setup.dai.balanceOf(uniswapV2AmmAdapter.address); - const lpBalance = await uniswapSetup.wethDaiPool.balanceOf(uniswapV2AmmAdapter.address); - expect(wethBalance).to.eq(ZERO); - expect(daiBalance).to.eq(ZERO); - expect(lpBalance).to.eq(ZERO); - const updateOwnerWethBalance = await setup.weth.balanceOf(owner.address); - const updateOwnerDaiBalance = await setup.dai.balanceOf(owner.address); - const updateOwnerLpBalance = await uniswapSetup.wethDaiPool.balanceOf(owner.address); - expect(updateOwnerWethBalance).to.eq(ownerWethBalance); - expect(updateOwnerDaiBalance).to.eq(ownerDaiBalance.sub(tokensAdded)); - expect(updateOwnerLpBalance).to.eq(ownerLpBalance.add(subjectMinLiquidity)); - }); - - }); - - describe("when the pool address is invalid", async () => { - beforeEach(async () => { - const uniswapV3Setup = getUniswapV3Fixture(owner.address); - await uniswapV3Setup.initialize(owner, setup.weth, 3000.0, setup.wbtc, 40000.0, setup.dai); - subjectAmmPool = uniswapV3Setup.swapRouter.address; - }); - - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("_pool factory doesn't match the router factory"); - }); - }); - - describe("when the _pool doesn't match the _component", async () => { - beforeEach(async () => { - subjectComponent = setup.wbtc.address; - }); - - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("_pool doesn't contain the _component"); - }); - }); - - describe("when the _maxTokenIn is 0", async () => { - beforeEach(async () => { - subjectMaxTokenIn = ether(0); - }); - - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("supplied _maxTokenIn must be greater than 0"); - }); - }); - - describe("when the _pool totalSupply is 0", async () => { - beforeEach(async () => { - subjectAmmPool = uniswapSetup.wethWbtcPool.address; - subjectComponent = setup.weth.address; - }); - - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("_pool totalSupply must be > 0"); - }); - }); - - describe("when the _minLiquidity is 0", async () => { - beforeEach(async () => { - subjectMinLiquidity = ZERO; - }); - - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("_minLiquidity must be greater than 0"); - }); - }); - - describe("when the _minLiquidity is too high", async () => { - beforeEach(async () => { - subjectMinLiquidity = subjectMinLiquidity.mul(2); - }); - - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("_minLiquidity is too high for amount maximum"); + await expect(subject()).to.be.revertedWith("_minLiquidity is too high for input token limit"); }); }); }); @@ -592,7 +362,7 @@ describe("UniswapV2AmmAdapter", () => { let subjectMinTokensOut: BigNumber[]; let subjectLiquidity: BigNumber; - before(async () => { + beforeEach(async () => { subjectAmmPool = uniswapSetup.wethDaiPool.address; subjectComponents = [setup.weth.address, setup.dai.address]; subjectMinTokensOut = [ether(1), ether(3000)]; @@ -601,6 +371,7 @@ describe("UniswapV2AmmAdapter", () => { async function subject(): Promise { return await uniswapV2AmmAdapter.getRemoveLiquidityCalldata( + owner.address, subjectAmmPool, subjectComponents, subjectMinTokensOut, @@ -609,313 +380,115 @@ describe("UniswapV2AmmAdapter", () => { it("should return the correct remove liquidity calldata", async () => { const calldata = await subject(); + const blockTimestamp = await getLastBlockTimestamp(); - const expectedCallData = uniswapV2AmmAdapter.interface.encodeFunctionData("removeLiquidity", [ - subjectAmmPool, - subjectComponents, - subjectMinTokensOut, + const expectedCallData = uniswapSetup.router.interface.encodeFunctionData("removeLiquidity", [ + setup.weth.address, + setup.dai.address, subjectLiquidity, - true, + subjectMinTokensOut[0], + subjectMinTokensOut[1], + owner.address, + blockTimestamp, ]); - expect(JSON.stringify(calldata)).to.eq(JSON.stringify([uniswapV2AmmAdapter.address, ZERO, expectedCallData])); + expect(JSON.stringify(calldata)).to.eq(JSON.stringify([uniswapSetup.router.address, ZERO, expectedCallData])); }); - }); - - describe("removeLiquidity", async () => { - let subjectAmmPool: Address; - let subjectComponents: Address[]; - let subjectMinTokensOut: BigNumber[]; - let subjectLiquidity: BigNumber; - - beforeEach(async () => { - subjectAmmPool = uniswapSetup.wethDaiPool.address; - subjectComponents = [setup.weth.address, setup.dai.address]; - subjectLiquidity = await uniswapSetup.wethDaiPool.balanceOf(owner.address); - const totalSupply = await uniswapSetup.wethDaiPool.totalSupply(); - const [reserveA, reserveB] = await getReserves(uniswapSetup.wethDaiPool, subjectComponents[0]); - subjectMinTokensOut = [reserveA.mul(subjectLiquidity).div(totalSupply), - reserveB.mul(subjectLiquidity).div(totalSupply)]; - await uniswapSetup.wethDaiPool.connect(owner.wallet) - .approve(uniswapV2AmmAdapter.address, MAX_UINT_256); - }); - async function subject(): Promise { - return await uniswapV2AmmAdapter.removeLiquidity( - subjectAmmPool, - subjectComponents, - subjectMinTokensOut, - subjectLiquidity, - true); - } - - it("should remove the correct liquidity", async () => { - const [reserveA, reserveB] = await getReserves(uniswapSetup.wethDaiPool, subjectComponents[0]); - const ownerWethBalance = await setup.weth.balanceOf(owner.address); - const ownerDaiBalance = await setup.dai.balanceOf(owner.address); - const ownerLpBalance = await uniswapSetup.wethDaiPool.balanceOf(owner.address); - await subject(); - expect(await uniswapSetup.wethDaiPool.balanceOf(owner.address)).to.be.eq(ZERO); - const [updatedReserveA, updatedReserveB] = await getReserves(uniswapSetup.wethDaiPool, subjectComponents[0]); - expect(updatedReserveA).to.be.eq(reserveA.sub(subjectMinTokensOut[0])); - expect(updatedReserveB).to.be.eq(reserveB.sub(subjectMinTokensOut[1])); - const wethBalance = await setup.weth.balanceOf(uniswapV2AmmAdapter.address); - const daiBalance = await setup.dai.balanceOf(uniswapV2AmmAdapter.address); - const lpBalance = await uniswapSetup.wethDaiPool.balanceOf(uniswapV2AmmAdapter.address); - expect(wethBalance).to.eq(ZERO); - expect(daiBalance).to.eq(ZERO); - expect(lpBalance).to.eq(ZERO); - const updateOwnerWethBalance = await setup.weth.balanceOf(owner.address); - const updateOwnerDaiBalance = await setup.dai.balanceOf(owner.address); - const updateOwnerLpBalance = await uniswapSetup.wethDaiPool.balanceOf(owner.address); - expect(updateOwnerWethBalance).to.eq(ownerWethBalance.add(subjectMinTokensOut[0])); - expect(updateOwnerDaiBalance).to.eq(ownerDaiBalance.add(subjectMinTokensOut[1])); - expect(updateOwnerLpBalance).to.eq(ownerLpBalance.sub(subjectLiquidity)); - }); - - describe("when the pool address is invalid", async () => { - beforeEach(async () => { - const uniswapV3Setup = getUniswapV3Fixture(owner.address); - await uniswapV3Setup.initialize(owner, setup.weth, 3000.0, setup.wbtc, 40000.0, setup.dai); - subjectAmmPool = uniswapV3Setup.swapRouter.address; - }); - - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("_pool factory doesn't match the router factory"); - }); - }); - - describe("when the _components length is invalid", async () => { - beforeEach(async () => { - subjectComponents = [setup.weth.address]; - }); - - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("_components length is invalid"); - }); - }); - - describe("when the _minTokensOut length is invalid", async () => { - beforeEach(async () => { - subjectMinTokensOut = [ether(1)]; - }); - - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("_minTokensOut length is invalid"); - }); - }); - - describe("when the _pool doesn't match the _components", async () => { - beforeEach(async () => { - subjectComponents = [setup.weth.address, setup.wbtc.address]; - }); - - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("_pool doesn't match the components"); - }); - }); - - describe("when the _minTokensOut[0] is 0", async () => { - beforeEach(async () => { - subjectMinTokensOut = [ether(0), ether(3000)]; - }); - - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("requested token0 must be greater than 0"); - }); - }); - - describe("when the _minTokensOut[1] is 0", async () => { - beforeEach(async () => { - subjectMinTokensOut = [ether(1), ether(0)]; - }); - - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("requested token1 must be greater than 0"); - }); - }); - - describe("when the _liquidity is 0", async () => { - beforeEach(async () => { - subjectLiquidity = ZERO; - }); - - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("_liquidity must be greater than 0"); - }); - }); - - describe("when the _liquidity is more than available", async () => { - beforeEach(async () => { - subjectLiquidity = (await uniswapSetup.wethDaiPool.balanceOf(owner.address)).add(ether(1)); - }); + describe("when the pool address is invalid", async () => { + beforeEach(async () => { + const uniswapV3Setup = getUniswapV3Fixture(owner.address); + await uniswapV3Setup.initialize(owner, setup.weth, 3000.0, setup.wbtc, 40000.0, setup.dai); + subjectAmmPool = uniswapV3Setup.swapRouter.address; + }); - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("_liquidity must be <= to current balance"); + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("_pool factory doesn't match the router factory"); + }); }); - }); - describe("when the _minTokensOut is too high", async () => { - beforeEach(async () => { - subjectMinTokensOut = [subjectMinTokensOut[0].mul(2), subjectMinTokensOut[1]]; - }); + describe("when the _components length is invalid", async () => { + beforeEach(async () => { + subjectComponents = [setup.weth.address]; + }); - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("amounts must be <= ownedTokens"); + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("_components length is invalid"); + }); }); - }); - }); - - describe("removeLiquiditySingleAsset", async () => { - let subjectAmmPool: Address; - let subjectComponent: Address; - let subjectMinTokenOut: BigNumber; - let subjectLiquidity: BigNumber; - - beforeEach(async () => { - subjectAmmPool = uniswapSetup.wethDaiPool.address; - subjectComponent = setup.weth.address; - subjectLiquidity = await uniswapSetup.wethDaiPool.balanceOf(owner.address); - const totalSupply = await uniswapSetup.wethDaiPool.totalSupply(); - const [reserveA, reserveB] = await getReserves(uniswapSetup.wethDaiPool, subjectComponent); - const tokenAAmount = subjectLiquidity.mul(reserveA).div(totalSupply); - const tokenBAmount = subjectLiquidity.mul(reserveB).div(totalSupply); - const receivedAmount = await uniswapSetup.router.getAmountOut(tokenBAmount, - reserveB.sub(tokenBAmount), reserveA.sub(tokenAAmount)); - subjectMinTokenOut = tokenAAmount.add(receivedAmount); - await uniswapSetup.wethDaiPool.connect(owner.wallet) - .approve(uniswapV2AmmAdapter.address, MAX_UINT_256); - }); - - async function subject(): Promise { - return await uniswapV2AmmAdapter.removeLiquiditySingleAsset( - subjectAmmPool, - subjectComponent, - subjectMinTokenOut, - subjectLiquidity); - } - - it("should remove the correct liquidity with weth", async () => { - const [reserveA, reserveB] = await getReserves(uniswapSetup.wethDaiPool, subjectComponent); - const ownerWethBalance = await setup.weth.balanceOf(owner.address); - const ownerDaiBalance = await setup.dai.balanceOf(owner.address); - const ownerLpBalance = await uniswapSetup.wethDaiPool.balanceOf(owner.address); - await subject(); - const [updatedReserveA, updatedReserveB] = await getReserves(uniswapSetup.wethDaiPool, subjectComponent); - expect(await uniswapSetup.wethDaiPool.balanceOf(owner.address)).to.be.eq(ZERO); - expect(updatedReserveA).to.be.eq(reserveA.sub(subjectMinTokenOut)); - expect(updatedReserveB).to.be.eq(reserveB); - const wethBalance = await setup.weth.balanceOf(uniswapV2AmmAdapter.address); - const daiBalance = await setup.dai.balanceOf(uniswapV2AmmAdapter.address); - const lpBalance = await uniswapSetup.wethDaiPool.balanceOf(uniswapV2AmmAdapter.address); - expect(wethBalance).to.eq(ZERO); - expect(daiBalance).to.eq(ZERO); - expect(lpBalance).to.eq(ZERO); - const updateOwnerWethBalance = await setup.weth.balanceOf(owner.address); - const updateOwnerDaiBalance = await setup.dai.balanceOf(owner.address); - const updateOwnerLpBalance = await uniswapSetup.wethDaiPool.balanceOf(owner.address); - expect(updateOwnerWethBalance).to.eq(ownerWethBalance.add(subjectMinTokenOut)); - expect(updateOwnerDaiBalance).to.eq(ownerDaiBalance); - expect(updateOwnerLpBalance).to.eq(ownerLpBalance.sub(subjectLiquidity)); - }); - describe("when removing dai", async () => { - beforeEach(async () => { - subjectComponent = setup.dai.address; - const totalSupply = await uniswapSetup.wethDaiPool.totalSupply(); - const [reserveA, reserveB] = await getReserves(uniswapSetup.wethDaiPool, subjectComponent); - const tokenAAmount = subjectLiquidity.mul(reserveA).div(totalSupply); - const tokenBAmount = subjectLiquidity.mul(reserveB).div(totalSupply); - const receivedAmount = await uniswapSetup.router.getAmountOut(tokenBAmount, - reserveB.sub(tokenBAmount), reserveA.sub(tokenAAmount)); - subjectMinTokenOut = tokenAAmount.add(receivedAmount); - }); + describe("when the _minTokensOut length is invalid", async () => { + beforeEach(async () => { + subjectMinTokensOut = [ether(1)]; + }); - it("should remove the correct liquidity", async () => { - const [reserveA, reserveB] = await getReserves(uniswapSetup.wethDaiPool, subjectComponent); - const ownerWethBalance = await setup.weth.balanceOf(owner.address); - const ownerDaiBalance = await setup.dai.balanceOf(owner.address); - const ownerLpBalance = await uniswapSetup.wethDaiPool.balanceOf(owner.address); - await subject(); - const [updatedReserveA, updatedReserveB] = await getReserves(uniswapSetup.wethDaiPool, subjectComponent); - expect(await uniswapSetup.wethDaiPool.balanceOf(owner.address)).to.be.eq(ZERO); - expect(updatedReserveA).to.be.eq(reserveA.sub(subjectMinTokenOut)); - expect(updatedReserveB).to.be.eq(reserveB); - const wethBalance = await setup.weth.balanceOf(uniswapV2AmmAdapter.address); - const daiBalance = await setup.dai.balanceOf(uniswapV2AmmAdapter.address); - const lpBalance = await uniswapSetup.wethDaiPool.balanceOf(uniswapV2AmmAdapter.address); - expect(wethBalance).to.eq(ZERO); - expect(daiBalance).to.eq(ZERO); - expect(lpBalance).to.eq(ZERO); - const updateOwnerWethBalance = await setup.weth.balanceOf(owner.address); - const updateOwnerDaiBalance = await setup.dai.balanceOf(owner.address); - const updateOwnerLpBalance = await uniswapSetup.wethDaiPool.balanceOf(owner.address); - expect(updateOwnerWethBalance).to.eq(ownerWethBalance); - expect(updateOwnerDaiBalance).to.eq(ownerDaiBalance.add(subjectMinTokenOut)); - expect(updateOwnerLpBalance).to.eq(ownerLpBalance.sub(subjectLiquidity)); + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("_minTokensOut length is invalid"); + }); }); - }); - describe("when the pool address is invalid", async () => { - beforeEach(async () => { - const uniswapV3Setup = getUniswapV3Fixture(owner.address); - await uniswapV3Setup.initialize(owner, setup.weth, 3000.0, setup.wbtc, 40000.0, setup.dai); - subjectAmmPool = uniswapV3Setup.swapRouter.address; - }); + describe("when the _pool doesn't match the _components", async () => { + beforeEach(async () => { + subjectComponents = [setup.weth.address, setup.wbtc.address]; + }); - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("_pool factory doesn't match the router factory"); + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("_pool doesn't match the components"); + }); }); - }); - describe("when the _pool doesn't contain the _component", async () => { - beforeEach(async () => { - subjectComponent = setup.wbtc.address; - }); + describe("when the _minTokensOut[0] is 0", async () => { + beforeEach(async () => { + subjectMinTokensOut = [ether(0), ether(3000)]; + }); - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("_pool doesn't contain the _component"); + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("requested token0 must be greater than 0"); + }); }); - }); - describe("when the _minTokenOut is 0", async () => { - beforeEach(async () => { - subjectMinTokenOut = ether(0); - }); + describe("when the _minTokensOut[1] is 0", async () => { + beforeEach(async () => { + subjectMinTokensOut = [ether(1), ether(0)]; + }); - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("requested token must be greater than 0"); + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("requested token1 must be greater than 0"); + }); }); - }); - describe("when the _liquidity is 0", async () => { - beforeEach(async () => { - subjectLiquidity = ZERO; - }); + describe("when the _liquidity is 0", async () => { + beforeEach(async () => { + subjectLiquidity = ZERO; + }); - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("_liquidity must be greater than 0"); + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("_liquidity must be greater than 0"); + }); }); - }); - describe("when the _liquidity is more than available", async () => { - beforeEach(async () => { - subjectLiquidity = (await uniswapSetup.wethDaiPool.balanceOf(owner.address)).add(ether(1)); - }); + describe("when the _liquidity is more than available", async () => { + beforeEach(async () => { + subjectLiquidity = (await uniswapSetup.wethDaiPool.balanceOf(owner.address)).add(ether(1)); + }); - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("_liquidity must be <= to current balance"); + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("_liquidity must be <= to current balance"); + }); }); - }); - describe("when the _minTokenOut is too high", async () => { - beforeEach(async () => { - subjectMinTokenOut = subjectMinTokenOut.mul(2); - }); + describe("when the _minTokensOut is too high", async () => { + beforeEach(async () => { + const balance = await uniswapSetup.wethDaiPool.balanceOf(owner.address); + const totalSupply = await uniswapSetup.wethDaiPool.totalSupply(); + const [reserveA, ] = await getReserves(uniswapSetup.wethDaiPool, setup.weth.address); + const tooMuchEth = balance.mul(reserveA).div(totalSupply).add(ether(1)); + subjectMinTokensOut = [tooMuchEth, subjectMinTokensOut[1]]; + }); - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("_minTokenOut is too high for amount received"); + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("amounts must be <= ownedTokens"); + }); }); - }); }); context("Add and Remove Liquidity Tests", async () => { @@ -992,7 +565,7 @@ describe("UniswapV2AmmAdapter", () => { }); it("should revert", async () => { - await expect(subject()).to.be.revertedWith("_minLiquidity is too high for amount maximums"); + await expect(subject()).to.be.revertedWith("_minLiquidity is too high for input token limit"); }); }); @@ -1067,81 +640,6 @@ describe("UniswapV2AmmAdapter", () => { }); - context("when there is a deployed SetToken with enabled AmmModule", async () => { - beforeEach(async () => { - // Deploy a standard SetToken with the AMM Module - setToken = await setup.createSetToken( - [setup.weth.address], - [ether(1)], - [setup.issuanceModule.address, ammModule.address] - ); - - await setup.issuanceModule.initialize(setToken.address, ADDRESS_ZERO); - await ammModule.initialize(setToken.address); - - // Mint some instances of the SetToken - await setup.approveAndIssueSetToken(setToken, ether(1)); - }); - - describe("#addLiquiditySingleAsset", async () => { - let subjectComponentToInput: Address; - let subjectMaxComponentQuantity: BigNumber; - let subjectMinPoolTokensToMint: BigNumber; - let tokensAdded: BigNumber; - - beforeEach(async () => { - subjectSetToken = setToken.address; - subjectIntegrationName = uniswapV2AmmAdapterName; - subjectAmmPool = uniswapSetup.wethDaiPool.address; - subjectComponentToInput = setup.weth.address; - subjectMaxComponentQuantity = ether(1); - subjectCaller = owner; - const [reserveA, reserveB] = await getReserves(uniswapSetup.wethDaiPool, subjectComponentToInput); - const totalSupply = await uniswapSetup.wethDaiPool.totalSupply(); - const amountToSwap = await uniswapV2AmmAdapter.calculateSwapAmount(subjectMaxComponentQuantity, reserveA); - const amountOut = await uniswapSetup.router.getAmountOut(amountToSwap, reserveA, reserveB); - const remaining = subjectMaxComponentQuantity.sub(amountToSwap); - const quote0 = await uniswapSetup.router.quote(remaining, reserveA.add(amountToSwap), reserveB.sub(amountOut) ); - const quote1 = await uniswapSetup.router.quote(amountOut, reserveB.sub(amountOut), reserveA.add(amountToSwap) ); - const amount0 = quote0 <= amountOut ? remaining : quote1; - const amount1 = quote0 <= amountOut ? quote0 : amountOut; - tokensAdded = amountToSwap.add(amount0); - const liquidity0 = amount0.mul(totalSupply).div(reserveA.add(amountToSwap)); - const liquidity1 = amount1.mul(totalSupply).div(reserveB.sub(amountOut)); - subjectMinPoolTokensToMint = liquidity0.lt(liquidity1) ? liquidity0 : liquidity1; - }); - - async function subject(): Promise { - return await ammModule.connect(subjectCaller.wallet).addLiquiditySingleAsset( - subjectSetToken, - subjectIntegrationName, - subjectAmmPool, - subjectMinPoolTokensToMint, - subjectComponentToInput, - subjectMaxComponentQuantity, - ); - } - - it("should mint the liquidity token to the caller", async () => { - await subject(); - const liquidityTokenBalance = await uniswapSetup.wethDaiPool.balanceOf(subjectSetToken); - expect(liquidityTokenBalance).to.eq(subjectMinPoolTokensToMint); - }); - - it("should update the positions properly", async () => { - await subject(); - const positions = await setToken.getPositions(); - expect(positions.length).to.eq(2); - expect(positions[0].component).to.eq(subjectComponentToInput); - expect(positions[0].unit).to.eq(subjectMaxComponentQuantity.sub(tokensAdded)); - expect(positions[1].component).to.eq(subjectAmmPool); - expect(positions[1].unit).to.eq(subjectMinPoolTokensToMint); - }); - - }); - - }); - context("when there is a deployed SetToken with enabled AmmModule", async () => { before(async () => { // Deploy a standard SetToken with the AMM Module @@ -1225,74 +723,6 @@ describe("UniswapV2AmmAdapter", () => { }); - context("when there is a deployed SetToken with enabled AmmModule", async () => { - before(async () => { - // Deploy a standard SetToken with the AMM Module - setToken = await setup.createSetToken( - [uniswapSetup.wethDaiPool.address], - [ether(1)], - [setup.issuanceModule.address, ammModule.address] - ); - - await setup.issuanceModule.initialize(setToken.address, ADDRESS_ZERO); - await ammModule.initialize(setToken.address); - - // Mint some instances of the SetToken - await setup.approveAndIssueSetToken(setToken, ether(1)); - }); - - describe("#removeLiquiditySingleAsset", async () => { - let subjectComponentToOutput: Address; - let subjectMinComponentQuantity: BigNumber; - let subjectPoolTokens: BigNumber; - - beforeEach(async () => { - subjectSetToken = setToken.address; - subjectIntegrationName = uniswapV2AmmAdapterName; - subjectAmmPool = uniswapSetup.wethDaiPool.address; - subjectComponentToOutput = setup.weth.address; - subjectPoolTokens = ether(1); - const totalSupply = await uniswapSetup.wethDaiPool.totalSupply(); - const [reserveA, reserveB] = await getReserves(uniswapSetup.wethDaiPool, subjectComponentToOutput); - const tokenAAmount = subjectPoolTokens.mul(reserveA).div(totalSupply); - const tokenBAmount = subjectPoolTokens.mul(reserveB).div(totalSupply); - const receivedAmount = await uniswapSetup.router.getAmountOut(tokenBAmount, - reserveB.sub(tokenBAmount), reserveA.sub(tokenAAmount)); - subjectMinComponentQuantity = tokenAAmount.add(receivedAmount); - subjectCaller = owner; - }); - - async function subject(): Promise { - return await ammModule.connect(subjectCaller.wallet).removeLiquiditySingleAsset( - subjectSetToken, - subjectIntegrationName, - subjectAmmPool, - subjectPoolTokens, - subjectComponentToOutput, - subjectMinComponentQuantity, - ); - } - - it("should reduce the liquidity token of the caller", async () => { - const previousLiquidityTokenBalance = await uniswapSetup.wethDaiPool.balanceOf(subjectSetToken); - await subject(); - const liquidityTokenBalance = await uniswapSetup.wethDaiPool.balanceOf(subjectSetToken); - const expectedLiquidityBalance = previousLiquidityTokenBalance.sub(subjectPoolTokens); - expect(liquidityTokenBalance).to.eq(expectedLiquidityBalance); - }); - - it("should update the positions properly", async () => { - await subject(); - const positions = await setToken.getPositions(); - expect(positions.length).to.eq(1); - expect(positions[0].component).to.eq(setup.weth.address); - expect(positions[0].unit).to.eq(subjectMinComponentQuantity); - }); - - }); - - }); - function shouldRevertIfPoolIsNotSupported(subject: any) { describe("when the pool is not supported on the adapter", async () => { beforeEach(async () => { diff --git a/utils/deploys/deployAdapters.ts b/utils/deploys/deployAdapters.ts index 81abb1e48..5629223c2 100644 --- a/utils/deploys/deployAdapters.ts +++ b/utils/deploys/deployAdapters.ts @@ -35,7 +35,6 @@ import { } from "../contracts"; import { convertLibraryNameToLinkId } from "../common"; import { Address, Bytes } from "./../types"; -import { BigNumber } from "@ethersproject/bignumber"; import { AaveGovernanceAdapter__factory } from "../../typechain/factories/AaveGovernanceAdapter__factory"; import { AaveGovernanceV2Adapter__factory } from "../../typechain/factories/AaveGovernanceV2Adapter__factory"; @@ -91,12 +90,8 @@ export default class DeployAdapters { ); } - public async deployUniswapV2AmmAdapter( - uniswapV2Router: Address, - feeNumerator: BigNumber, - feeDenominator: BigNumber - ): Promise { - return await new UniswapV2AmmAdapter__factory(this._deployerSigner).deploy(uniswapV2Router, feeNumerator, feeDenominator); + public async deployUniswapV2AmmAdapter(uniswapV2Router: Address): Promise { + return await new UniswapV2AmmAdapter__factory(this._deployerSigner).deploy(uniswapV2Router); } public async deployUniswapV2ExchangeAdapter(uniswapV2Router: Address): Promise { From 00e2e4be4677629ee598c8b87f5edb1763b24d95 Mon Sep 17 00:00:00 2001 From: Stephen Hankinson Date: Fri, 27 Aug 2021 00:11:44 -0300 Subject: [PATCH 20/27] Minor fix to a test --- test/protocol/integration/amm/UniswapV2AmmAdapter.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/protocol/integration/amm/UniswapV2AmmAdapter.spec.ts b/test/protocol/integration/amm/UniswapV2AmmAdapter.spec.ts index eac60fc39..ce25213e4 100644 --- a/test/protocol/integration/amm/UniswapV2AmmAdapter.spec.ts +++ b/test/protocol/integration/amm/UniswapV2AmmAdapter.spec.ts @@ -203,7 +203,7 @@ describe("UniswapV2AmmAdapter", () => { return await uniswapV2AmmAdapter.getRemoveLiquiditySingleAssetCalldata( owner.address, subjectAmmPool, - subjectComponent[0], + subjectComponent, subjectMinTokenOut, subjectLiquidity); } From d21a2815d3954a208a511b1c7e525f60271346d0 Mon Sep 17 00:00:00 2001 From: Stephen Hankinson Date: Fri, 27 Aug 2021 10:32:28 -0300 Subject: [PATCH 21/27] Fix the AmmMock for the argument change --- .../mocks/integrations/AmmAdapterMock.sol | 4 ++++ .../integration/amm/UniswapV2AmmAdapter.sol | 24 +++++++++---------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/contracts/mocks/integrations/AmmAdapterMock.sol b/contracts/mocks/integrations/AmmAdapterMock.sol index 33c74ed3b..691deb2bd 100644 --- a/contracts/mocks/integrations/AmmAdapterMock.sol +++ b/contracts/mocks/integrations/AmmAdapterMock.sol @@ -85,6 +85,7 @@ contract AmmAdapterMock is ERC20 { /* ============ Adapter Functions ============ */ function getProvideLiquidityCalldata( + address /* _setToken */, address _pool, address[] calldata /* _components */, uint256[] calldata _maxTokensIn, @@ -103,6 +104,7 @@ contract AmmAdapterMock is ERC20 { } function getProvideLiquiditySingleAssetCalldata( + address /* _setToken */, address _pool, address _component, uint256 _maxTokenIn, @@ -121,6 +123,7 @@ contract AmmAdapterMock is ERC20 { } function getRemoveLiquidityCalldata( + address /* _setToken */, address _pool, address[] calldata /* _components */, uint256[] calldata _minTokensOut, @@ -134,6 +137,7 @@ contract AmmAdapterMock is ERC20 { } function getRemoveLiquiditySingleAssetCalldata( + address /* _setToken */, address _pool, address _component, uint256 _minTokenOut, diff --git a/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol b/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol index 2b2f39784..fe9b08e42 100644 --- a/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol +++ b/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol @@ -136,16 +136,16 @@ contract UniswapV2AmmAdapter is IAmmAdapter { * Return calldata for the add liquidity call for a single asset */ function getProvideLiquiditySingleAssetCalldata( - address, - address, - address, - uint256, - uint256 + address /* _setToken */, + address /*_pool*/, + address /*_component*/, + uint256 /*_maxTokenIn*/, + uint256 /*_minLiquidity*/ ) external view override - returns (address, uint256, bytes memory) + returns (address /*target*/, uint256 /*value*/, bytes memory /*data*/) { revert("Uniswap V2 single asset addition is not supported"); } @@ -212,16 +212,16 @@ contract UniswapV2AmmAdapter is IAmmAdapter { * Return calldata for the remove liquidity single asset call */ function getRemoveLiquiditySingleAssetCalldata( - address, - address, - address, - uint256, - uint256 + address /* _setToken */, + address /*_pool*/, + address /*_component*/, + uint256 /*_minTokenOut*/, + uint256 /*_liquidity*/ ) external view override - returns (address, uint256, bytes memory) + returns (address /*target*/, uint256 /*value*/, bytes memory /*data*/) { revert("Uniswap V2 single asset removal is not supported"); } From 016660d6640f2d44a5054d0fa31a5dbbb5eb079f Mon Sep 17 00:00:00 2001 From: Stephen Hankinson Date: Sat, 28 Aug 2021 19:46:36 -0300 Subject: [PATCH 22/27] Updates for PR comments --- contracts/interfaces/IAmmAdapter.sol | 2 +- .../mocks/integrations/AmmAdapterMock.sol | 22 +- .../integration/amm/UniswapV2AmmAdapter.sol | 187 +++++---- contracts/protocol/modules/AmmModule.sol | 30 +- .../amm/UniswapV2AmmAdapter.spec.ts | 370 ++++++++++-------- 5 files changed, 327 insertions(+), 284 deletions(-) diff --git a/contracts/interfaces/IAmmAdapter.sol b/contracts/interfaces/IAmmAdapter.sol index ba1fa6200..8c571c864 100644 --- a/contracts/interfaces/IAmmAdapter.sol +++ b/contracts/interfaces/IAmmAdapter.sol @@ -60,5 +60,5 @@ interface IAmmAdapter { ) external view returns (address _target, uint256 _value, bytes memory _calldata); function getSpenderAddress(address _pool) external view returns(address); - function isValidPool(address _pool) external view returns(bool); + function isValidPool(address _pool, address[] memory _components) external view returns(bool); } \ No newline at end of file diff --git a/contracts/mocks/integrations/AmmAdapterMock.sol b/contracts/mocks/integrations/AmmAdapterMock.sol index 691deb2bd..dffb0a5e9 100644 --- a/contracts/mocks/integrations/AmmAdapterMock.sol +++ b/contracts/mocks/integrations/AmmAdapterMock.sol @@ -87,7 +87,7 @@ contract AmmAdapterMock is ERC20 { function getProvideLiquidityCalldata( address /* _setToken */, address _pool, - address[] calldata /* _components */, + address[] calldata _components, uint256[] calldata _maxTokensIn, uint256 _minLiquidity ) @@ -95,7 +95,7 @@ contract AmmAdapterMock is ERC20 { view returns (address _target, uint256 _value, bytes memory _calldata) { - isValidPool(_pool); + isValidPool(_pool, _components); // Check that components match the pool tokens @@ -110,8 +110,12 @@ contract AmmAdapterMock is ERC20 { uint256 _maxTokenIn, uint256 _minLiquidity ) external view returns (address _target, uint256 _value, bytes memory _calldata) { + + address[] memory components = new address[](1); + components[0] = _component; + // This address must be the pool - isValidPool(_pool); + isValidPool(_pool, components); bytes memory callData = abi.encodeWithSignature( "joinswapPoolAmountOut(address,uint256,uint256)", @@ -125,12 +129,12 @@ contract AmmAdapterMock is ERC20 { function getRemoveLiquidityCalldata( address /* _setToken */, address _pool, - address[] calldata /* _components */, + address[] calldata _components, uint256[] calldata _minTokensOut, uint256 _liquidity ) external view returns (address _target, uint256 _value, bytes memory _calldata) { // Validate the pool and components are legit? - isValidPool(_pool); + isValidPool(_pool, _components); bytes memory callData = abi.encodeWithSignature("exitPool(uint256,uint256[])", _liquidity, _minTokensOut); return (address(this), 0, callData); @@ -143,8 +147,12 @@ contract AmmAdapterMock is ERC20 { uint256 _minTokenOut, uint256 _liquidity ) external view returns (address _target, uint256 _value, bytes memory _calldata) { + + address[] memory components = new address[](1); + components[0] = _component; + // Pool must be this address - isValidPool(_pool); + isValidPool(_pool, components); bytes memory callData = abi.encodeWithSignature( "exitswapPoolAmountIn(address,uint256,uint256)", @@ -155,7 +163,7 @@ contract AmmAdapterMock is ERC20 { return (address(this), 0, callData); } - function isValidPool(address _pool) public view returns(bool) { + function isValidPool(address _pool,address[] memory /*_components*/) public view returns(bool) { return _pool == address(this) || _pool == approvedToken; } diff --git a/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol b/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol index fe9b08e42..9897f3642 100644 --- a/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol +++ b/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol @@ -1,5 +1,5 @@ /* - Copyright 2020 Set Labs Inc. + Copyright 2021 Set Labs Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -22,22 +22,9 @@ import "../../../interfaces/external/IUniswapV2Router.sol"; import "../../../interfaces/external/IUniswapV2Pair.sol"; import "../../../interfaces/external/IUniswapV2Factory.sol"; import "../../../interfaces/IAmmAdapter.sol"; +import "@openzeppelin/contracts/math/Math.sol"; import "@openzeppelin/contracts/math/SafeMath.sol"; -struct Position { - address setToken; - address tokenA; - uint256 amountA; - address tokenB; - uint256 amountB; - uint256 balance; - uint256 totalSupply; - uint256 reserveA; - uint256 reserveB; - uint256 calculatedAmountA; - uint256 calculatedAmountB; -} - /** * @title UniswapV2AmmAdapter * @author Stephen Hankinson @@ -95,39 +82,53 @@ contract UniswapV2AmmAdapter is IAmmAdapter { override returns (address target, uint256 value, bytes memory data) { + address setToken = _setToken; IUniswapV2Pair pair = IUniswapV2Pair(_pool); - require(factory == IUniswapV2Factory(pair.factory()), "_pool factory doesn't match the router factory"); - require(_components.length == 2, "_components length is invalid"); - require(_maxTokensIn.length == 2, "_maxTokensIn length is invalid"); - require(factory.getPair(_components[0], _components[1]) == _pool, - "_pool doesn't match the components"); - require(_maxTokensIn[0] > 0, "supplied token0 must be greater than 0"); - require(_maxTokensIn[1] > 0, "supplied token1 must be greater than 0"); - require(_minLiquidity > 0, "_minLiquidity must be greater than 0"); - - Position memory position = Position(_setToken, _components[0], _maxTokensIn[0], _components[1], _maxTokensIn[1], - 0, pair.totalSupply(), 0, 0, 0, 0); - require(position.totalSupply > 0, "_pool totalSupply must be > 0"); - - // Determine how much of each token the _minLiquidity would return - (position.reserveA, position.reserveB) = _getReserves(pair, position.tokenA); - position.calculatedAmountA = position.reserveA.mul(_minLiquidity).div(position.totalSupply); - position.calculatedAmountB = position.reserveB.mul(_minLiquidity).div(position.totalSupply); - - require(position.calculatedAmountA <= position.amountA && position.calculatedAmountB <= position.amountB, - "_minLiquidity is too high for input token limit"); + + require(_maxTokensIn[0] > 0 && _maxTokensIn[1] > 0, "Component quantity must be nonzero"); + + // We expect the totalSupply to be greater than 0 because the isValidPool would + // have passed by this point, meaning a pool for these tokens exist, which also + // means there is at least MINIMUM_LIQUIDITY liquidity tokens in the pool + // https://github.com/Uniswap/uniswap-v2-core/blob/master/contracts/UniswapV2Pair.sol#L121 + require(pair.totalSupply() > 0, "_pool totalSupply must be nonzero"); + + // As mentioned above, the totalSupply of the pool should be greater than 0, and if + // this is the case, we know the liquidity returned from the pool is equal to the minimum + // of the given supplied token multiplied by the totalSupply of liquidity tokens divided by + // the pool reserves of that token. + // https://github.com/Uniswap/uniswap-v2-core/blob/master/contracts/UniswapV2Pair.sol#L123 + uint[] memory reserves = new uint[](2); + (reserves[0], reserves[1]) = _getReserves(pair, _components[0]); + uint256 liquidityExpectedFromSuppliedTokens = Math.min( + _maxTokensIn[0].mul(pair.totalSupply()).div(reserves[0]), + _maxTokensIn[1].mul(pair.totalSupply()).div(reserves[1]) + ); + + require( + _minLiquidity <= liquidityExpectedFromSuppliedTokens, + "_minLiquidity is too high for input token limit" + ); + + // Now that we know the minimum expected liquidity to receive for the amount of tokens + // that are being supplied, we can reverse the above equations in the min function to + // determine how much actual tokens are supplied to the pool, therefore setting our + // amountAMin and amountBMin of the addLiquidity call to the expected amounts. + uint[] memory minTokensIn = new uint[](2); + minTokensIn[0] = liquidityExpectedFromSuppliedTokens.mul(reserves[0]).div(pair.totalSupply()); + minTokensIn[1] = liquidityExpectedFromSuppliedTokens.mul(reserves[1]).div(pair.totalSupply()); target = router; value = 0; data = abi.encodeWithSignature( ADD_LIQUIDITY, - position.tokenA, - position.tokenB, - position.amountA, - position.amountB, - position.calculatedAmountA, - position.calculatedAmountB, - position.setToken, + _components[0], + _components[1], + _maxTokensIn[0], + _maxTokensIn[1], + minTokensIn[0], + minTokensIn[1], + setToken, block.timestamp // solhint-disable-line not-rely-on-time ); } @@ -171,39 +172,42 @@ contract UniswapV2AmmAdapter is IAmmAdapter { override returns (address target, uint256 value, bytes memory data) { + address setToken = _setToken; IUniswapV2Pair pair = IUniswapV2Pair(_pool); - require(factory == IUniswapV2Factory(pair.factory()), "_pool factory doesn't match the router factory"); - require(_components.length == 2, "_components length is invalid"); - require(_minTokensOut.length == 2, "_minTokensOut length is invalid"); - require(factory.getPair(_components[0], _components[1]) == _pool, - "_pool doesn't match the components"); - require(_minTokensOut[0] > 0, "requested token0 must be greater than 0"); - require(_minTokensOut[1] > 0, "requested token1 must be greater than 0"); - require(_liquidity > 0, "_liquidity must be greater than 0"); - Position memory position = Position(_setToken, _components[0], _minTokensOut[0], _components[1], _minTokensOut[1], - pair.balanceOf(_setToken), pair.totalSupply(), 0, 0, 0, 0); - - require(_liquidity <= position.balance, "_liquidity must be <= to current balance"); - - // Calculate how many tokens are owned by the liquidity - (position.reserveA, position.reserveB) = _getReserves(pair, position.tokenA); - position.calculatedAmountA = position.reserveA.mul(position.balance).div(position.totalSupply); - position.calculatedAmountB = position.reserveB.mul(position.balance).div(position.totalSupply); - - require(position.amountA <= position.calculatedAmountA && position.amountB <= position.calculatedAmountB, - "amounts must be <= ownedTokens"); + // Make sure that only up the amount of liquidity tokens owned by the Set Token are redeemed + uint256 setTokenLiquidityBalance = pair.balanceOf(setToken); + require(_liquidity <= setTokenLiquidityBalance, "_liquidity must be <= to current balance"); + + // For a given Uniswap V2 Liquidity Pool, an owner of a liquidity token is able to claim + // a portion of the reserves of that pool based on the percentage of liquidity tokens that + // they own in relation to the total supply of the liquidity tokens. So if a user owns 25% + // of the pool tokens, they would in effect own 25% of both reserveA and reserveB contained + // within the pool. Therefore, given the value of _liquidity we can calculate how much of the + // reserves the caller is requesting and can then validate that the _minTokensOut values are + // less than or equal to that amount. If not, they are requesting too much of the _components + // relative to the amount of liquidty that they are redeeming. + uint[] memory reserves = new uint[](2); + uint[] memory reservesOwnedByLiquidity = new uint[](2); + (reserves[0], reserves[1]) = _getReserves(pair, _components[0]); + reservesOwnedByLiquidity[0] = reserves[0].mul(_liquidity).div(pair.totalSupply()); + reservesOwnedByLiquidity[1] = reserves[1].mul(_liquidity).div(pair.totalSupply()); + + require( + _minTokensOut[0] <= reservesOwnedByLiquidity[0] && _minTokensOut[1] <= reservesOwnedByLiquidity[1], + "amounts must be <= ownedTokens" + ); target = router; value = 0; data = abi.encodeWithSignature( REMOVE_LIQUIDITY, - position.tokenA, - position.tokenB, + _components[0], + _components[1], _liquidity, - position.amountA, - position.amountB, - position.setToken, + _minTokensOut[0], + _minTokensOut[1], + setToken, block.timestamp // solhint-disable-line not-rely-on-time ); } @@ -228,54 +232,47 @@ contract UniswapV2AmmAdapter is IAmmAdapter { /** * Returns the address of the spender - * - * @param _pool Address of liquidity token */ - function getSpenderAddress(address _pool) + function getSpenderAddress(address /*_pool*/) external view override returns (address spender) { - IUniswapV2Pair pair = IUniswapV2Pair(_pool); - require(factory == IUniswapV2Factory(pair.factory()), "_pool factory doesn't match the router factory"); - spender = router; } /** - * Verifies that this is a valid Uniswap V2 _pool + * Verifies that this is a valid Uniswap V2 pool * - * @param _pool Address of liquidity token + * @param _pool Address of liquidity token + * @param _components Address array of supplied/requested tokens */ - function isValidPool(address _pool) + function isValidPool(address _pool, address[] memory _components) external view override - returns (bool isValid) { - address token0; - address token1; - bool success = true; - IUniswapV2Pair pair = IUniswapV2Pair(_pool); - - try pair.token0() returns (address _token0) { - token0 = _token0; - } catch { - success = false; - } - - try pair.token1() returns (address _token1) { - token1 = _token1; + returns (bool) { + // Attempt to get the factory of the provided pool + IUniswapV2Factory poolFactory; + try IUniswapV2Pair(_pool).factory() returns (address _factory) { + poolFactory = IUniswapV2Factory(_factory); } catch { - success = false; + return false; } - if( success ) { - isValid = factory.getPair(token0, token1) == _pool; - } - else { + // Make sure the pool factory is the expected value, that we have the + // two required components, and that the pair address returned + // by the factory matches the supplied _pool value + if( + factory != poolFactory || + _components.length != 2 || + factory.getPair(_components[0], _components[1]) != _pool + ) { return false; } + + return true; } /* ============ Internal Functions =================== */ diff --git a/contracts/protocol/modules/AmmModule.sol b/contracts/protocol/modules/AmmModule.sol index 8f9ae5e7a..f2603c3a9 100644 --- a/contracts/protocol/modules/AmmModule.sol +++ b/contracts/protocol/modules/AmmModule.sol @@ -128,8 +128,6 @@ contract AmmModule is ModuleBase, ReentrancyGuard { _validateAddLiquidity(actionInfo); - _executeApprovals(actionInfo); - _executeAddLiquidity(actionInfo); _validateMinimumLiquidityReceived(actionInfo); @@ -177,8 +175,6 @@ contract AmmModule is ModuleBase, ReentrancyGuard { _validateAddLiquidity(actionInfo); - _executeApprovals(actionInfo); - _executeAddLiquiditySingleAsset(actionInfo); _validateMinimumLiquidityReceived(actionInfo); @@ -230,12 +226,6 @@ contract AmmModule is ModuleBase, ReentrancyGuard { _validateRemoveLiquidity(actionInfo); - _setToken.invokeApprove( - _ammPool, - actionInfo.ammAdapter.getSpenderAddress(_ammPool), - actionInfo.liquidityQuantity - ); - _executeRemoveLiquidity(actionInfo); _validateMinimumUnderlyingReceived(actionInfo); @@ -425,12 +415,12 @@ contract AmmModule is ModuleBase, ReentrancyGuard { require(_actionInfo.liquidityQuantity > 0, "Token quantity must be nonzero"); require( - _actionInfo.ammAdapter.isValidPool(_actionInfo.liquidityToken), + _actionInfo.ammAdapter.isValidPool(_actionInfo.liquidityToken, _actionInfo.components), "Pool token must be enabled on the Adapter" ); } - function _executeApprovals(ActionInfo memory _actionInfo) internal { + function _executeComponentApprovals(ActionInfo memory _actionInfo) internal { address spender = _actionInfo.ammAdapter.getSpenderAddress(_actionInfo.liquidityToken); // Loop through and approve total notional tokens to spender @@ -454,6 +444,8 @@ contract AmmModule is ModuleBase, ReentrancyGuard { _actionInfo.liquidityQuantity ); + _executeComponentApprovals(_actionInfo); + _actionInfo.setToken.invoke(targetAmm, callValue, methodData); } @@ -468,6 +460,8 @@ contract AmmModule is ModuleBase, ReentrancyGuard { _actionInfo.liquidityQuantity ); + _executeComponentApprovals(_actionInfo); + _actionInfo.setToken.invoke(targetAmm, callValue, methodData); } @@ -482,6 +476,12 @@ contract AmmModule is ModuleBase, ReentrancyGuard { _actionInfo.liquidityQuantity ); + _actionInfo.setToken.invokeApprove( + _actionInfo.liquidityToken, + _actionInfo.ammAdapter.getSpenderAddress(_actionInfo.liquidityToken), + _actionInfo.liquidityQuantity + ); + _actionInfo.setToken.invoke(targetAmm, callValue, methodData); } @@ -496,6 +496,12 @@ contract AmmModule is ModuleBase, ReentrancyGuard { _actionInfo.liquidityQuantity ); + _actionInfo.setToken.invokeApprove( + _actionInfo.liquidityToken, + _actionInfo.ammAdapter.getSpenderAddress(_actionInfo.liquidityToken), + _actionInfo.liquidityQuantity + ); + _actionInfo.setToken.invoke(targetAmm, callValue, methodData); } diff --git a/test/protocol/integration/amm/UniswapV2AmmAdapter.spec.ts b/test/protocol/integration/amm/UniswapV2AmmAdapter.spec.ts index ce25213e4..13204a31a 100644 --- a/test/protocol/integration/amm/UniswapV2AmmAdapter.spec.ts +++ b/test/protocol/integration/amm/UniswapV2AmmAdapter.spec.ts @@ -19,7 +19,6 @@ import { getAccounts, getSystemFixture, getUniswapFixture, - getUniswapV3Fixture, getWaffleExpect, getLastBlockTimestamp } from "@utils/test/index"; @@ -117,28 +116,19 @@ describe("UniswapV2AmmAdapter", () => { expect(spender).to.eq(uniswapSetup.router.address); }); - describe("when the pool address is invalid", async () => { - before(async () => { - const uniswapV3Setup = getUniswapV3Fixture(owner.address); - await uniswapV3Setup.initialize(owner, setup.weth, 3000.0, setup.wbtc, 40000.0, setup.dai); - poolAddress = uniswapV3Setup.swapRouter.address; - }); - - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("_pool factory doesn't match the router factory"); - }); - }); }); describe("isValidPool", async () => { - let poolAddress: Address; + let subjectAmmPool: Address; + let subjectComponents: Address[]; - before(async () => { - poolAddress = uniswapSetup.wethDaiPool.address; + beforeEach(async () => { + subjectAmmPool = uniswapSetup.wethDaiPool.address; + subjectComponents = [setup.weth.address, setup.dai.address]; }); async function subject(): Promise { - return await uniswapV2AmmAdapter.isValidPool(poolAddress); + return await uniswapV2AmmAdapter.isValidPool(subjectAmmPool, subjectComponents); } it("should be a valid pool", async () => { @@ -147,14 +137,49 @@ describe("UniswapV2AmmAdapter", () => { }); describe("when the pool address is invalid", async () => { - before(async () => { - poolAddress = uniswapSetup.router.address; - }); + beforeEach(async () => { + subjectAmmPool = setup.weth.address; + }); - it("should be an invalid pool", async () => { - const status = await subject(); - expect(status).to.be.false; - }); + it("should be an invalid pool", async () => { + const status = await subject(); + expect(status).to.be.false; + }); + }); + + describe("when the components don't match", async () => { + beforeEach(async () => { + subjectComponents = [setup.weth.address, setup.wbtc.address]; + }); + + it("should be an invalid pool", async () => { + const status = await subject(); + expect(status).to.be.false; + }); + }); + + describe("when the number of components is incorrect", async () => { + beforeEach(async () => { + subjectComponents = [setup.weth.address]; + }); + + it("should be an invalid pool", async () => { + const status = await subject(); + expect(status).to.be.false; + }); + }); + + describe("when the router doesn't match", async () => { + beforeEach(async () => { + const otherUniswapSetup = getUniswapFixture(owner.address); + await otherUniswapSetup.initialize(owner, setup.weth.address, setup.wbtc.address, setup.dai.address); + subjectAmmPool = otherUniswapSetup.wethDaiPool.address; + }); + + it("should be an invalid pool", async () => { + const status = await subject(); + expect(status).to.be.false; + }); }); }); @@ -262,68 +287,6 @@ describe("UniswapV2AmmAdapter", () => { expect(JSON.stringify(calldata)).to.eq(JSON.stringify([uniswapSetup.router.address, ZERO, expectedCallData])); }); - describe("when the pool address is invalid", async () => { - beforeEach(async () => { - const uniswapV3Setup = getUniswapV3Fixture(owner.address); - await uniswapV3Setup.initialize(owner, setup.weth, 3000.0, setup.wbtc, 40000.0, setup.dai); - subjectAmmPool = uniswapV3Setup.swapRouter.address; - }); - - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("_pool factory doesn't match the router factory"); - }); - }); - - describe("when the _components length is invalid", async () => { - beforeEach(async () => { - subjectComponents = [setup.weth.address]; - }); - - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("_components length is invalid"); - }); - }); - - describe("when the _maxTokensIn length is invalid", async () => { - beforeEach(async () => { - subjectMaxTokensIn = [ether(1)]; - }); - - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("_maxTokensIn length is invalid"); - }); - }); - - describe("when the _pool doesn't match the _components", async () => { - beforeEach(async () => { - subjectComponents = [setup.weth.address, setup.wbtc.address]; - }); - - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("_pool doesn't match the components"); - }); - }); - - describe("when the _maxTokensIn[0] is 0", async () => { - beforeEach(async () => { - subjectMaxTokensIn = [ether(0), ether(3000)]; - }); - - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("supplied token0 must be greater than 0"); - }); - }); - - describe("when the _maxTokensIn[1] is 0", async () => { - beforeEach(async () => { - subjectMaxTokensIn = [ether(1), ether(0)]; - }); - - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("supplied token1 must be greater than 0"); - }); - }); - describe("when the _pool totalSupply is 0", async () => { beforeEach(async () => { subjectAmmPool = uniswapSetup.wethWbtcPool.address; @@ -331,17 +294,7 @@ describe("UniswapV2AmmAdapter", () => { }); it("should revert", async () => { - await expect(subject()).to.be.revertedWith("_pool totalSupply must be > 0"); - }); - }); - - describe("when the _minLiquidity is 0", async () => { - beforeEach(async () => { - subjectMinLiquidity = ZERO; - }); - - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("_minLiquidity must be greater than 0"); + await expect(subject()).to.be.revertedWith("_pool totalSupply must be nonzero"); }); }); @@ -365,8 +318,15 @@ describe("UniswapV2AmmAdapter", () => { beforeEach(async () => { subjectAmmPool = uniswapSetup.wethDaiPool.address; subjectComponents = [setup.weth.address, setup.dai.address]; - subjectMinTokensOut = [ether(1), ether(3000)]; subjectLiquidity = ether(1); + + // Determine how much of each token the subjectLiquidity should return + const totalSupply = await uniswapSetup.wethDaiPool.totalSupply(); + const [reserveA, reserveB] = await getReserves(uniswapSetup.wethDaiPool, setup.weth.address); + const amountAMin = reserveA.mul(subjectLiquidity).div(totalSupply); + const amountBMin = reserveB.mul(subjectLiquidity).div(totalSupply); + + subjectMinTokensOut = [amountAMin, amountBMin]; }); async function subject(): Promise { @@ -394,78 +354,6 @@ describe("UniswapV2AmmAdapter", () => { expect(JSON.stringify(calldata)).to.eq(JSON.stringify([uniswapSetup.router.address, ZERO, expectedCallData])); }); - describe("when the pool address is invalid", async () => { - beforeEach(async () => { - const uniswapV3Setup = getUniswapV3Fixture(owner.address); - await uniswapV3Setup.initialize(owner, setup.weth, 3000.0, setup.wbtc, 40000.0, setup.dai); - subjectAmmPool = uniswapV3Setup.swapRouter.address; - }); - - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("_pool factory doesn't match the router factory"); - }); - }); - - describe("when the _components length is invalid", async () => { - beforeEach(async () => { - subjectComponents = [setup.weth.address]; - }); - - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("_components length is invalid"); - }); - }); - - describe("when the _minTokensOut length is invalid", async () => { - beforeEach(async () => { - subjectMinTokensOut = [ether(1)]; - }); - - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("_minTokensOut length is invalid"); - }); - }); - - describe("when the _pool doesn't match the _components", async () => { - beforeEach(async () => { - subjectComponents = [setup.weth.address, setup.wbtc.address]; - }); - - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("_pool doesn't match the components"); - }); - }); - - describe("when the _minTokensOut[0] is 0", async () => { - beforeEach(async () => { - subjectMinTokensOut = [ether(0), ether(3000)]; - }); - - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("requested token0 must be greater than 0"); - }); - }); - - describe("when the _minTokensOut[1] is 0", async () => { - beforeEach(async () => { - subjectMinTokensOut = [ether(1), ether(0)]; - }); - - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("requested token1 must be greater than 0"); - }); - }); - - describe("when the _liquidity is 0", async () => { - beforeEach(async () => { - subjectLiquidity = ZERO; - }); - - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("_liquidity must be greater than 0"); - }); - }); - describe("when the _liquidity is more than available", async () => { beforeEach(async () => { subjectLiquidity = (await uniswapSetup.wethDaiPool.balanceOf(owner.address)).add(ether(1)); @@ -635,6 +523,78 @@ describe("UniswapV2AmmAdapter", () => { }); }); + describe("when the pool address is invalid", async () => { + beforeEach(async () => { + const otherUniswapSetup = getUniswapFixture(owner.address); + await otherUniswapSetup.initialize(owner, setup.weth.address, setup.wbtc.address, setup.dai.address); + subjectAmmPool = otherUniswapSetup.wethDaiPool.address; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Pool token must be enabled on the Adapter"); + }); + }); + + describe("when the _components length is invalid", async () => { + beforeEach(async () => { + subjectComponentsToInput = [setup.weth.address]; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Components and units must be equal length"); + }); + }); + + describe("when the _maxTokensIn length is invalid", async () => { + beforeEach(async () => { + subjectMaxComponentQuantities = [ether(1)]; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Components and units must be equal length"); + }); + }); + + describe("when the _pool doesn't match the _components", async () => { + beforeEach(async () => { + subjectComponentsToInput = [setup.weth.address, setup.wbtc.address]; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Pool token must be enabled on the Adapter"); + }); + }); + + describe("when the _maxTokensIn[0] is 0", async () => { + beforeEach(async () => { + subjectMaxComponentQuantities = [ether(0), ether(3000)]; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Component quantity must be nonzero"); + }); + }); + + describe("when the _maxTokensIn[1] is 0", async () => { + beforeEach(async () => { + subjectMaxComponentQuantities = [ether(1), ether(0)]; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Component quantity must be nonzero"); + }); + }); + + describe("when the _minLiquidity is 0", async () => { + beforeEach(async () => { + subjectMinPoolTokensToMint = ZERO; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Token quantity must be nonzero"); + }); + }); + shouldRevertIfPoolIsNotSupported(subject); }); @@ -718,6 +678,78 @@ describe("UniswapV2AmmAdapter", () => { }); }); + describe("when the pool address is invalid", async () => { + beforeEach(async () => { + const otherUniswapSetup = getUniswapFixture(owner.address); + await otherUniswapSetup.initialize(owner, setup.weth.address, setup.wbtc.address, setup.dai.address); + subjectAmmPool = otherUniswapSetup.wethDaiPool.address; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Pool token must be enabled on the Adapter"); + }); + }); + + describe("when the _components length is invalid", async () => { + beforeEach(async () => { + subjectComponentsToOutput = [setup.weth.address]; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Components and units must be equal length"); + }); + }); + + describe("when the _minTokensOut length is invalid", async () => { + beforeEach(async () => { + subjectMinComponentQuantities = [ether(1)]; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Components and units must be equal length"); + }); + }); + + describe("when the _pool doesn't match the _components", async () => { + beforeEach(async () => { + subjectComponentsToOutput = [setup.weth.address, setup.wbtc.address]; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Pool token must be enabled on the Adapter"); + }); + }); + + describe("when the _minTokensOut[0] is 0", async () => { + beforeEach(async () => { + subjectMinComponentQuantities = [ether(0), ether(3000)]; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Component quantity must be nonzero"); + }); + }); + + describe("when the _minTokensOut[1] is 0", async () => { + beforeEach(async () => { + subjectMinComponentQuantities = [ether(1), ether(0)]; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Component quantity must be nonzero"); + }); + }); + + describe("when the _liquidity is 0", async () => { + beforeEach(async () => { + subjectPoolTokens = ZERO; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Token quantity must be nonzero"); + }); + }); + shouldRevertIfPoolIsNotSupported(subject); }); From 07eec6964b64830f8708c758d270a2f15a15bfa3 Mon Sep 17 00:00:00 2001 From: Stephen Hankinson Date: Sat, 28 Aug 2021 19:58:36 -0300 Subject: [PATCH 23/27] remove double approval --- contracts/protocol/modules/AmmModule.sol | 6 ------ 1 file changed, 6 deletions(-) diff --git a/contracts/protocol/modules/AmmModule.sol b/contracts/protocol/modules/AmmModule.sol index f2603c3a9..d8a943d65 100644 --- a/contracts/protocol/modules/AmmModule.sol +++ b/contracts/protocol/modules/AmmModule.sol @@ -278,12 +278,6 @@ contract AmmModule is ModuleBase, ReentrancyGuard { _validateRemoveLiquidity(actionInfo); - _setToken.invokeApprove( - _ammPool, - actionInfo.ammAdapter.getSpenderAddress(_ammPool), - actionInfo.liquidityQuantity - ); - _executeRemoveLiquiditySingleAsset(actionInfo); _validateMinimumUnderlyingReceived(actionInfo); From db2f5cf236b27b5b7dcea8ed6f2f3d8a19a3de9f Mon Sep 17 00:00:00 2001 From: Stephen Hankinson Date: Mon, 30 Aug 2021 14:26:43 -0300 Subject: [PATCH 24/27] Minor gas saving changes --- .../integration/amm/UniswapV2AmmAdapter.sol | 27 +++++++++---------- .../amm/UniswapV2AmmAdapter.spec.ts | 2 +- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol b/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol index 9897f3642..6896da93e 100644 --- a/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol +++ b/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol @@ -91,18 +91,16 @@ contract UniswapV2AmmAdapter is IAmmAdapter { // have passed by this point, meaning a pool for these tokens exist, which also // means there is at least MINIMUM_LIQUIDITY liquidity tokens in the pool // https://github.com/Uniswap/uniswap-v2-core/blob/master/contracts/UniswapV2Pair.sol#L121 - require(pair.totalSupply() > 0, "_pool totalSupply must be nonzero"); - - // As mentioned above, the totalSupply of the pool should be greater than 0, and if - // this is the case, we know the liquidity returned from the pool is equal to the minimum + // If this is the case, we know the liquidity returned from the pool is equal to the minimum // of the given supplied token multiplied by the totalSupply of liquidity tokens divided by // the pool reserves of that token. // https://github.com/Uniswap/uniswap-v2-core/blob/master/contracts/UniswapV2Pair.sol#L123 - uint[] memory reserves = new uint[](2); - (reserves[0], reserves[1]) = _getReserves(pair, _components[0]); + uint[] memory reservesAndTotalSupply = new uint[](3); + reservesAndTotalSupply[2] = pair.totalSupply(); + (reservesAndTotalSupply[0], reservesAndTotalSupply[1]) = _getReserves(pair, _components[0]); uint256 liquidityExpectedFromSuppliedTokens = Math.min( - _maxTokensIn[0].mul(pair.totalSupply()).div(reserves[0]), - _maxTokensIn[1].mul(pair.totalSupply()).div(reserves[1]) + _maxTokensIn[0].mul(reservesAndTotalSupply[2]).div(reservesAndTotalSupply[0]), + _maxTokensIn[1].mul(reservesAndTotalSupply[2]).div(reservesAndTotalSupply[1]) ); require( @@ -115,8 +113,8 @@ contract UniswapV2AmmAdapter is IAmmAdapter { // determine how much actual tokens are supplied to the pool, therefore setting our // amountAMin and amountBMin of the addLiquidity call to the expected amounts. uint[] memory minTokensIn = new uint[](2); - minTokensIn[0] = liquidityExpectedFromSuppliedTokens.mul(reserves[0]).div(pair.totalSupply()); - minTokensIn[1] = liquidityExpectedFromSuppliedTokens.mul(reserves[1]).div(pair.totalSupply()); + minTokensIn[0] = liquidityExpectedFromSuppliedTokens.mul(reservesAndTotalSupply[0]).div(reservesAndTotalSupply[2]); + minTokensIn[1] = liquidityExpectedFromSuppliedTokens.mul(reservesAndTotalSupply[1]).div(reservesAndTotalSupply[2]); target = router; value = 0; @@ -187,11 +185,12 @@ contract UniswapV2AmmAdapter is IAmmAdapter { // reserves the caller is requesting and can then validate that the _minTokensOut values are // less than or equal to that amount. If not, they are requesting too much of the _components // relative to the amount of liquidty that they are redeeming. - uint[] memory reserves = new uint[](2); + uint[] memory reservesAndTotalSupply = new uint[](3); uint[] memory reservesOwnedByLiquidity = new uint[](2); - (reserves[0], reserves[1]) = _getReserves(pair, _components[0]); - reservesOwnedByLiquidity[0] = reserves[0].mul(_liquidity).div(pair.totalSupply()); - reservesOwnedByLiquidity[1] = reserves[1].mul(_liquidity).div(pair.totalSupply()); + reservesAndTotalSupply[2] = pair.totalSupply(); + (reservesAndTotalSupply[0], reservesAndTotalSupply[1]) = _getReserves(pair, _components[0]); + reservesOwnedByLiquidity[0] = reservesAndTotalSupply[0].mul(_liquidity).div(reservesAndTotalSupply[2]); + reservesOwnedByLiquidity[1] = reservesAndTotalSupply[1].mul(_liquidity).div(reservesAndTotalSupply[2]); require( _minTokensOut[0] <= reservesOwnedByLiquidity[0] && _minTokensOut[1] <= reservesOwnedByLiquidity[1], diff --git a/test/protocol/integration/amm/UniswapV2AmmAdapter.spec.ts b/test/protocol/integration/amm/UniswapV2AmmAdapter.spec.ts index 13204a31a..96f91c7a5 100644 --- a/test/protocol/integration/amm/UniswapV2AmmAdapter.spec.ts +++ b/test/protocol/integration/amm/UniswapV2AmmAdapter.spec.ts @@ -294,7 +294,7 @@ describe("UniswapV2AmmAdapter", () => { }); it("should revert", async () => { - await expect(subject()).to.be.revertedWith("_pool totalSupply must be nonzero"); + await expect(subject()).to.be.revertedWith("SafeMath: division by zero"); }); }); From 49a576e94a917a5b54295458fee86c1bb4d8622b Mon Sep 17 00:00:00 2001 From: Stephen Hankinson Date: Mon, 30 Aug 2021 16:16:07 -0300 Subject: [PATCH 25/27] Updates to make the code more readable --- .../integration/amm/UniswapV2AmmAdapter.sol | 71 +++++++++++-------- 1 file changed, 43 insertions(+), 28 deletions(-) diff --git a/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol b/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol index 6896da93e..58bfda808 100644 --- a/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol +++ b/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol @@ -83,9 +83,12 @@ contract UniswapV2AmmAdapter is IAmmAdapter { returns (address target, uint256 value, bytes memory data) { address setToken = _setToken; + address[] memory components = _components; + uint256[] memory maxTokensIn = _maxTokensIn; + uint256 minLiquidity = _minLiquidity; IUniswapV2Pair pair = IUniswapV2Pair(_pool); - require(_maxTokensIn[0] > 0 && _maxTokensIn[1] > 0, "Component quantity must be nonzero"); + require(maxTokensIn[0] > 0 && maxTokensIn[1] > 0, "Component quantity must be nonzero"); // We expect the totalSupply to be greater than 0 because the isValidPool would // have passed by this point, meaning a pool for these tokens exist, which also @@ -95,16 +98,21 @@ contract UniswapV2AmmAdapter is IAmmAdapter { // of the given supplied token multiplied by the totalSupply of liquidity tokens divided by // the pool reserves of that token. // https://github.com/Uniswap/uniswap-v2-core/blob/master/contracts/UniswapV2Pair.sol#L123 - uint[] memory reservesAndTotalSupply = new uint[](3); - reservesAndTotalSupply[2] = pair.totalSupply(); - (reservesAndTotalSupply[0], reservesAndTotalSupply[1]) = _getReserves(pair, _components[0]); + + uint256 amountAMin; + uint256 amountBMin; + { // scope for reserveA, reserveB, totalSupply and liquidityExpectedFromSuppliedTokens, avoids stack too deep errors + + uint256 totalSupply = pair.totalSupply(); + (uint256 reserveA, uint256 reserveB) = _getReserves(pair, components[0]); + uint256 liquidityExpectedFromSuppliedTokens = Math.min( - _maxTokensIn[0].mul(reservesAndTotalSupply[2]).div(reservesAndTotalSupply[0]), - _maxTokensIn[1].mul(reservesAndTotalSupply[2]).div(reservesAndTotalSupply[1]) + maxTokensIn[0].mul(totalSupply).div(reserveA), + maxTokensIn[1].mul(totalSupply).div(reserveB) ); require( - _minLiquidity <= liquidityExpectedFromSuppliedTokens, + minLiquidity <= liquidityExpectedFromSuppliedTokens, "_minLiquidity is too high for input token limit" ); @@ -112,20 +120,22 @@ contract UniswapV2AmmAdapter is IAmmAdapter { // that are being supplied, we can reverse the above equations in the min function to // determine how much actual tokens are supplied to the pool, therefore setting our // amountAMin and amountBMin of the addLiquidity call to the expected amounts. - uint[] memory minTokensIn = new uint[](2); - minTokensIn[0] = liquidityExpectedFromSuppliedTokens.mul(reservesAndTotalSupply[0]).div(reservesAndTotalSupply[2]); - minTokensIn[1] = liquidityExpectedFromSuppliedTokens.mul(reservesAndTotalSupply[1]).div(reservesAndTotalSupply[2]); + + amountAMin = liquidityExpectedFromSuppliedTokens.mul(reserveA).div(totalSupply); + amountBMin = liquidityExpectedFromSuppliedTokens.mul(reserveB).div(totalSupply); + + } target = router; value = 0; data = abi.encodeWithSignature( ADD_LIQUIDITY, - _components[0], - _components[1], - _maxTokensIn[0], - _maxTokensIn[1], - minTokensIn[0], - minTokensIn[1], + components[0], + components[1], + maxTokensIn[0], + maxTokensIn[1], + amountAMin, + amountBMin, setToken, block.timestamp // solhint-disable-line not-rely-on-time ); @@ -171,12 +181,17 @@ contract UniswapV2AmmAdapter is IAmmAdapter { returns (address target, uint256 value, bytes memory data) { address setToken = _setToken; + address[] memory components = _components; + uint256[] memory minTokensOut = _minTokensOut; + uint256 liquidity = _liquidity; IUniswapV2Pair pair = IUniswapV2Pair(_pool); // Make sure that only up the amount of liquidity tokens owned by the Set Token are redeemed uint256 setTokenLiquidityBalance = pair.balanceOf(setToken); require(_liquidity <= setTokenLiquidityBalance, "_liquidity must be <= to current balance"); + { // scope for reserveA, reserveB, totalSupply, reservesOwnedByLiquidityA, and reservesOwnedByLiquidityB, avoids stack too deep errors + // For a given Uniswap V2 Liquidity Pool, an owner of a liquidity token is able to claim // a portion of the reserves of that pool based on the percentage of liquidity tokens that // they own in relation to the total supply of the liquidity tokens. So if a user owns 25% @@ -185,27 +200,27 @@ contract UniswapV2AmmAdapter is IAmmAdapter { // reserves the caller is requesting and can then validate that the _minTokensOut values are // less than or equal to that amount. If not, they are requesting too much of the _components // relative to the amount of liquidty that they are redeeming. - uint[] memory reservesAndTotalSupply = new uint[](3); - uint[] memory reservesOwnedByLiquidity = new uint[](2); - reservesAndTotalSupply[2] = pair.totalSupply(); - (reservesAndTotalSupply[0], reservesAndTotalSupply[1]) = _getReserves(pair, _components[0]); - reservesOwnedByLiquidity[0] = reservesAndTotalSupply[0].mul(_liquidity).div(reservesAndTotalSupply[2]); - reservesOwnedByLiquidity[1] = reservesAndTotalSupply[1].mul(_liquidity).div(reservesAndTotalSupply[2]); + uint256 totalSupply = pair.totalSupply(); + (uint256 reserveA, uint256 reserveB) = _getReserves(pair, components[0]); + uint256 reservesOwnedByLiquidityA = reserveA.mul(liquidity).div(totalSupply); + uint256 reservesOwnedByLiquidityB = reserveB.mul(liquidity).div(totalSupply); require( - _minTokensOut[0] <= reservesOwnedByLiquidity[0] && _minTokensOut[1] <= reservesOwnedByLiquidity[1], + minTokensOut[0] <= reservesOwnedByLiquidityA && minTokensOut[1] <= reservesOwnedByLiquidityB, "amounts must be <= ownedTokens" ); + } + target = router; value = 0; data = abi.encodeWithSignature( REMOVE_LIQUIDITY, - _components[0], - _components[1], - _liquidity, - _minTokensOut[0], - _minTokensOut[1], + components[0], + components[1], + liquidity, + minTokensOut[0], + minTokensOut[1], setToken, block.timestamp // solhint-disable-line not-rely-on-time ); From f134a986913ee3c43bafa2fad4d2810fb30a46b0 Mon Sep 17 00:00:00 2001 From: Stephen Hankinson Date: Tue, 31 Aug 2021 15:53:51 -0300 Subject: [PATCH 26/27] Remove empty lines --- contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol | 4 ---- 1 file changed, 4 deletions(-) diff --git a/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol b/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol index 58bfda808..20841b0ba 100644 --- a/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol +++ b/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol @@ -102,7 +102,6 @@ contract UniswapV2AmmAdapter is IAmmAdapter { uint256 amountAMin; uint256 amountBMin; { // scope for reserveA, reserveB, totalSupply and liquidityExpectedFromSuppliedTokens, avoids stack too deep errors - uint256 totalSupply = pair.totalSupply(); (uint256 reserveA, uint256 reserveB) = _getReserves(pair, components[0]); @@ -123,7 +122,6 @@ contract UniswapV2AmmAdapter is IAmmAdapter { amountAMin = liquidityExpectedFromSuppliedTokens.mul(reserveA).div(totalSupply); amountBMin = liquidityExpectedFromSuppliedTokens.mul(reserveB).div(totalSupply); - } target = router; @@ -191,7 +189,6 @@ contract UniswapV2AmmAdapter is IAmmAdapter { require(_liquidity <= setTokenLiquidityBalance, "_liquidity must be <= to current balance"); { // scope for reserveA, reserveB, totalSupply, reservesOwnedByLiquidityA, and reservesOwnedByLiquidityB, avoids stack too deep errors - // For a given Uniswap V2 Liquidity Pool, an owner of a liquidity token is able to claim // a portion of the reserves of that pool based on the percentage of liquidity tokens that // they own in relation to the total supply of the liquidity tokens. So if a user owns 25% @@ -209,7 +206,6 @@ contract UniswapV2AmmAdapter is IAmmAdapter { minTokensOut[0] <= reservesOwnedByLiquidityA && minTokensOut[1] <= reservesOwnedByLiquidityB, "amounts must be <= ownedTokens" ); - } target = router; From 13ecfe2b6adceed73aa94b1ce25fcf4f2032b7be Mon Sep 17 00:00:00 2001 From: Stephen Hankinson Date: Tue, 31 Aug 2021 16:20:17 -0300 Subject: [PATCH 27/27] Remove spaces --- contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol b/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol index 20841b0ba..c0cf8ab3c 100644 --- a/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol +++ b/contracts/protocol/integration/amm/UniswapV2AmmAdapter.sol @@ -143,7 +143,7 @@ contract UniswapV2AmmAdapter is IAmmAdapter { * Return calldata for the add liquidity call for a single asset */ function getProvideLiquiditySingleAssetCalldata( - address /* _setToken */, + address /*_setToken*/, address /*_pool*/, address /*_component*/, uint256 /*_maxTokenIn*/,