diff --git a/contracts/interfaces/external/ISwapRouter.sol b/contracts/interfaces/external/ISwapRouter.sol new file mode 100644 index 000000000..c28fee316 --- /dev/null +++ b/contracts/interfaces/external/ISwapRouter.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity 0.6.10; +pragma experimental ABIEncoderV2; + + +/// @title Router token swapping functionality +/// @notice Functions for swapping tokens via Uniswap V3 +interface ISwapRouter { + struct ExactInputSingleParams { + address tokenIn; + address tokenOut; + uint24 fee; + address recipient; + uint256 deadline; + uint256 amountIn; + uint256 amountOutMinimum; + uint160 sqrtPriceLimitX96; + } + + /// @notice Swaps `amountIn` of one token for as much as possible of another token + /// @param params The parameters necessary for the swap, encoded as `ExactInputSingleParams` in calldata + /// @return amountOut The amount of the received token + function exactInputSingle(ExactInputSingleParams calldata params) external payable returns (uint256 amountOut); + + struct ExactInputParams { + bytes path; + address recipient; + uint256 deadline; + uint256 amountIn; + uint256 amountOutMinimum; + } + + /// @notice Swaps `amountIn` of one token for as much as possible of another along the specified path + /// @param params The parameters necessary for the multi-hop swap, encoded as `ExactInputParams` in calldata + /// @return amountOut The amount of the received token + function exactInput(ExactInputParams calldata params) external payable returns (uint256 amountOut); + + struct ExactOutputSingleParams { + address tokenIn; + address tokenOut; + uint24 fee; + address recipient; + uint256 deadline; + uint256 amountOut; + uint256 amountInMaximum; + uint160 sqrtPriceLimitX96; + } + + /// @notice Swaps as little as possible of one token for `amountOut` of another token + /// @param params The parameters necessary for the swap, encoded as `ExactOutputSingleParams` in calldata + /// @return amountIn The amount of the input token + function exactOutputSingle(ExactOutputSingleParams calldata params) external payable returns (uint256 amountIn); + + struct ExactOutputParams { + bytes path; + address recipient; + uint256 deadline; + uint256 amountOut; + uint256 amountInMaximum; + } + + /// @notice Swaps as little as possible of one token for `amountOut` of another along the specified path (reversed) + /// @param params The parameters necessary for the multi-hop swap, encoded as `ExactOutputParams` in calldata + /// @return amountIn The amount of the input token + function exactOutput(ExactOutputParams calldata params) external payable returns (uint256 amountIn); +} \ No newline at end of file diff --git a/contracts/protocol/integration/exchange/UniswapV3ExchangeAdapter.sol b/contracts/protocol/integration/exchange/UniswapV3ExchangeAdapter.sol new file mode 100644 index 000000000..3e4485cfa --- /dev/null +++ b/contracts/protocol/integration/exchange/UniswapV3ExchangeAdapter.sol @@ -0,0 +1,129 @@ +/* + 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. + 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; +pragma experimental "ABIEncoderV2"; + +import { BytesLib } from "../../../../external/contracts/uniswap/v3/lib/BytesLib.sol"; +import { ISwapRouter } from "../../../interfaces/external/ISwapRouter.sol"; + + +/** + * @title UniswapV3TradeAdapter + * @author Set Protocol + * + * Exchange adapter for Uniswap V3 SwapRouter that encodes trade data + */ +contract UniswapV3ExchangeAdapter { + + using BytesLib for bytes; + + /* ============= Constants ================= */ + + // signature of exactInput SwapRouter function + string internal constant EXACT_INPUT = "exactInput((bytes,address,uint256,uint256,uint256))"; + + /* ============ State Variables ============ */ + + // Address of Uniswap V3 SwapRouter contract + address public immutable swapRouter; + + /* ============ Constructor ============ */ + + /** + * Set state variables + * + * @param _swapRouter Address of Uniswap V3 SwapRouter + */ + constructor(address _swapRouter) public { + swapRouter = _swapRouter; + } + + /* ============ External Getter Functions ============ */ + + /** + * Return calldata for Uniswap V3 SwapRouter + * + * @param _sourceToken Address of source token to be sold + * @param _destinationToken Address of destination token to buy + * @param _destinationAddress Address that assets should be transferred to + * @param _sourceQuantity Amount of source token to sell + * @param _minDestinationQuantity Min amount of destination token to buy + * @param _data Uniswap V3 path. Equals the output of the generateDataParam function + * + * @return address Target contract address + * @return uint256 Call value + * @return bytes Trade calldata + */ + function getTradeCalldata( + address _sourceToken, + address _destinationToken, + address _destinationAddress, + uint256 _sourceQuantity, + uint256 _minDestinationQuantity, + bytes calldata _data + ) + external + view + returns (address, uint256, bytes memory) + { + + address sourceFromPath = _data.toAddress(0); + require(_sourceToken == sourceFromPath, "UniswapV3ExchangeAdapter: source token path mismatch"); + + address destinationFromPath = _data.toAddress(_data.length - 20); + require(_destinationToken == destinationFromPath, "UniswapV3ExchangeAdapter: destination token path mismatch"); + + ISwapRouter.ExactInputParams memory params = ISwapRouter.ExactInputParams( + _data, + _destinationAddress, + block.timestamp, + _sourceQuantity, + _minDestinationQuantity + ); + + bytes memory callData = abi.encodeWithSignature(EXACT_INPUT, params); + return (swapRouter, 0, callData); + } + + /** + * Returns the address to approve source tokens to for trading. This is the Uniswap SwapRouter address + * + * @return address Address of the contract to approve tokens to + */ + function getSpender() external view returns (address) { + return swapRouter; + } + + /** + * Returns the appropriate _data argument for getTradeCalldata. Equal to the encodePacked path with the + * fee of each hop between it, e.g [token1, fee1, token2, fee2, token3]. Note: _fees.length == _path.length - 1 + * + * @param _path array of addresses to use as the path for the trade + * @param _fees array of uint24 representing the pool fee to use for each hop + */ + function generateDataParam(address[] calldata _path, uint24[] calldata _fees) external pure returns (bytes memory) { + bytes memory data = ""; + for (uint256 i = 0; i < _path.length - 1; i++) { + data = abi.encodePacked(data, _path[i], _fees[i]); + } + + // last encode has no fee associated with it since _fees.length == _path.length - 1 + return abi.encodePacked(data, _path[_path.length - 1]); + } +} \ No newline at end of file diff --git a/test/fixtures/uniswapV3.spec.ts b/test/fixtures/uniswapV3.spec.ts index 865e1b014..ca74396b8 100644 --- a/test/fixtures/uniswapV3.spec.ts +++ b/test/fixtures/uniswapV3.spec.ts @@ -95,7 +95,7 @@ describe("UniswapV3Fixture", () => { const slot0 = await pool.slot0(); - const expectedSqrtPrice = uniswapV3Fixture._getSqrtPriceX96(1e-12); + const expectedSqrtPrice = uniswapV3Fixture._getSqrtPriceX96(1e12); expect(slot0.sqrtPriceX96).to.eq(expectedSqrtPrice); }); diff --git a/test/protocol/integration/exchange/uniswapV3ExchangeAdapter.spec.ts b/test/protocol/integration/exchange/uniswapV3ExchangeAdapter.spec.ts new file mode 100644 index 000000000..e31945083 --- /dev/null +++ b/test/protocol/integration/exchange/uniswapV3ExchangeAdapter.spec.ts @@ -0,0 +1,189 @@ +import "module-alias/register"; + +import { BigNumber, BigNumberish } from "@ethersproject/bignumber"; +import { solidityPack } from "ethers/lib/utils"; + +import { Address, Bytes } from "@utils/types"; +import { Account } from "@utils/test/types"; +import { ZERO } from "@utils/constants"; +import { UniswapV3ExchangeAdapter } from "@utils/contracts"; +import DeployHelper from "@utils/deploys"; +import { ether } from "@utils/index"; +import { + addSnapshotBeforeRestoreAfterEach, + getAccounts, + getSystemFixture, + getWaffleExpect, + getLastBlockTimestamp, + getUniswapV3Fixture, + getRandomAddress +} from "@utils/test/index"; + +import { SystemFixture, UniswapV3Fixture } from "@utils/fixtures"; + +const expect = getWaffleExpect(); + +describe("UniswapV3ExchangeAdapter", () => { + let owner: Account; + let mockSetToken: Account; + let deployer: DeployHelper; + let setup: SystemFixture; + let uniswapV3Fixture: UniswapV3Fixture; + + let uniswapV3ExchangeAdapter: UniswapV3ExchangeAdapter; + + before(async () => { + [ + owner, + mockSetToken, + ] = await getAccounts(); + + deployer = new DeployHelper(owner.wallet); + setup = getSystemFixture(owner.address); + await setup.initialize(); + + uniswapV3Fixture = getUniswapV3Fixture(owner.address); + await uniswapV3Fixture.initialize( + owner, + setup.weth, + 2500, + setup.wbtc, + 35000, + setup.dai + ); + + uniswapV3ExchangeAdapter = await deployer.adapters.deployUniswapV3ExchangeAdapter(uniswapV3Fixture.swapRouter.address); + }); + + addSnapshotBeforeRestoreAfterEach(); + + describe("#constructor", async () => { + let subjectSwapRouter: Address; + + beforeEach(async () => { + subjectSwapRouter = uniswapV3Fixture.swapRouter.address; + }); + + async function subject(): Promise { + return await deployer.adapters.deployUniswapV3ExchangeAdapter(subjectSwapRouter); + } + + it("should have the correct SwapRouter address", async () => { + const deployedUniswapV3ExchangeAdapter = await subject(); + + const actualRouterAddress = await deployedUniswapV3ExchangeAdapter.swapRouter(); + expect(actualRouterAddress).to.eq(uniswapV3Fixture.swapRouter.address); + }); + }); + + describe("#getSpender", async () => { + async function subject(): Promise { + return await uniswapV3ExchangeAdapter.getSpender(); + } + + it("should return the correct spender address", async () => { + const spender = await subject(); + + expect(spender).to.eq(uniswapV3Fixture.swapRouter.address); + }); + }); + + describe("#getTradeCalldata", async () => { + + let subjectMockSetToken: Address; + let subjectSourceToken: Address; + let subjectDestinationToken: Address; + let subjectSourceQuantity: BigNumber; + let subjectMinDestinationQuantity: BigNumber; + let subjectPath: Bytes; + + beforeEach(async () => { + subjectSourceToken = setup.wbtc.address; + subjectSourceQuantity = BigNumber.from(100000000); + subjectDestinationToken = setup.weth.address; + subjectMinDestinationQuantity = ether(25); + + subjectMockSetToken = mockSetToken.address; + + subjectPath = solidityPack(["address", "uint24", "address"], [subjectSourceToken, BigNumber.from(3000), subjectDestinationToken]); + }); + + async function subject(): Promise { + return await uniswapV3ExchangeAdapter.getTradeCalldata( + subjectSourceToken, + subjectDestinationToken, + subjectMockSetToken, + subjectSourceQuantity, + subjectMinDestinationQuantity, + subjectPath, + ); + } + + it("should return the correct trade calldata", async () => { + const calldata = await subject(); + const callTimestamp = await getLastBlockTimestamp(); + + const expectedCallData = uniswapV3Fixture.swapRouter.interface.encodeFunctionData("exactInput", [{ + path: subjectPath, + recipient: mockSetToken.address, + deadline: callTimestamp, + amountIn: subjectSourceQuantity, + amountOutMinimum: subjectMinDestinationQuantity, + }]); + + expect(JSON.stringify(calldata)).to.eq(JSON.stringify([uniswapV3Fixture.swapRouter.address, ZERO, expectedCallData])); + }); + + context("when source token does not match path", async () => { + beforeEach(async () => { + subjectSourceToken = await getRandomAddress(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("UniswapV3ExchangeAdapter: source token path mismatch"); + }); + }); + + context("when destination token does not match path", async () => { + beforeEach(async () => { + subjectDestinationToken = await getRandomAddress(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("UniswapV3ExchangeAdapter: destination token path mismatch"); + }); + }); + }); + + describe("#generateDataParam", async () => { + + let subjectToken1: Address; + let subjectFee1: BigNumberish; + let subjectToken2: Address; + let subjectFee2: BigNumberish; + let subjectToken3: Address; + + beforeEach(async () => { + subjectToken1 = setup.wbtc.address; + subjectFee1 = 3000; + subjectToken2 = setup.weth.address; + subjectFee2 = 500; + subjectToken3 = setup.weth.address; + }); + + async function subject(): Promise { + return await uniswapV3ExchangeAdapter.generateDataParam([subjectToken1, subjectToken2, subjectToken3], [subjectFee1, subjectFee2]); + } + + it("should create the correct path data", async () => { + const data = await subject(); + + const expectedData = solidityPack( + ["address", "uint24", "address", "uint24", "address"], + [subjectToken1, subjectFee1, subjectToken2, subjectFee2, subjectToken3] + ); + + expect(data).to.eq(expectedData); + }); + }); +}); diff --git a/test/protocol/modules/tradeModule.spec.ts b/test/protocol/modules/tradeModule.spec.ts index be0c113c7..5ef4834fe 100644 --- a/test/protocol/modules/tradeModule.spec.ts +++ b/test/protocol/modules/tradeModule.spec.ts @@ -22,6 +22,7 @@ import { WETH9, ZeroExApiAdapter, ZeroExMock, + UniswapV3ExchangeAdapter, } from "@utils/contracts"; import { ADDRESS_ZERO, EMPTY_BYTES, MAX_UINT_256, ZERO } from "@utils/constants"; import DeployHelper from "@utils/deploys"; @@ -35,10 +36,11 @@ import { getRandomAccount, getSystemFixture, getUniswapFixture, + getUniswapV3Fixture, getWaffleExpect, } from "@utils/test/index"; -import { SystemFixture, UniswapFixture } from "@utils/fixtures"; +import { SystemFixture, UniswapFixture, UniswapV3Fixture } from "@utils/fixtures"; const web3 = new Web3(); const expect = getWaffleExpect(); @@ -64,6 +66,8 @@ describe("TradeModule", () => { let uniswapTransferFeeAdapterName: string; let uniswapExchangeAdapterV2: UniswapV2ExchangeAdapterV2; let uniswapAdapterV2Name: string; + let uniswapV3ExchangeAdapter: UniswapV3ExchangeAdapter; + let uniswapV3AdapterName: string; let zeroExMock: ZeroExMock; let zeroExApiAdapter: ZeroExApiAdapter; @@ -72,6 +76,7 @@ describe("TradeModule", () => { let wbtcRate: BigNumber; let setup: SystemFixture; let uniswapSetup: UniswapFixture; + let uniswapV3Setup: UniswapV3Fixture; let tradeModule: TradeModule; cacheBeforeEach(async () => { @@ -122,9 +127,20 @@ describe("TradeModule", () => { setup.dai.address ); + uniswapV3Setup = getUniswapV3Fixture(owner.address); + await uniswapV3Setup.initialize( + owner, + setup.weth, + 2500, + setup.wbtc, + 35000, + setup.dai + ); + uniswapExchangeAdapter = await deployer.adapters.deployUniswapV2ExchangeAdapter(uniswapSetup.router.address); uniswapTransferFeeExchangeAdapter = await deployer.adapters.deployUniswapV2TransferFeeExchangeAdapter(uniswapSetup.router.address); uniswapExchangeAdapterV2 = await deployer.adapters.deployUniswapV2ExchangeAdapterV2(uniswapSetup.router.address); + uniswapV3ExchangeAdapter = await deployer.adapters.deployUniswapV3ExchangeAdapter(uniswapV3Setup.swapRouter.address); zeroExMock = await deployer.mocks.deployZeroExMock( setup.wbtc.address, @@ -141,13 +157,30 @@ describe("TradeModule", () => { uniswapTransferFeeAdapterName = "UNISWAP_TRANSFER_FEE"; uniswapAdapterV2Name = "UNISWAPV2"; zeroExApiAdapterName = "ZERO_EX"; + uniswapV3AdapterName = "UNISWAPV3"; tradeModule = await deployer.modules.deployTradeModule(setup.controller.address); await setup.controller.addModule(tradeModule.address); await setup.integrationRegistry.batchAddIntegration( - [tradeModule.address, tradeModule.address, tradeModule.address, tradeModule.address, tradeModule.address, tradeModule.address], - [kyberAdapterName, oneInchAdapterName, uniswapAdapterName, uniswapTransferFeeAdapterName, uniswapAdapterV2Name, zeroExApiAdapterName], + [ + tradeModule.address, + tradeModule.address, + tradeModule.address, + tradeModule.address, + tradeModule.address, + tradeModule.address, + tradeModule.address, + ], + [ + kyberAdapterName, + oneInchAdapterName, + uniswapAdapterName, + uniswapTransferFeeAdapterName, + uniswapAdapterV2Name, + zeroExApiAdapterName, + uniswapV3AdapterName, + ], [ kyberExchangeAdapter.address, oneInchExchangeAdapter.address, @@ -155,6 +188,7 @@ describe("TradeModule", () => { uniswapTransferFeeExchangeAdapter.address, uniswapExchangeAdapterV2.address, zeroExApiAdapter.address, + uniswapV3ExchangeAdapter.address, ] ); }); @@ -1640,6 +1674,155 @@ describe("TradeModule", () => { }); }); }); + + context("when trading a Default component on Uniswap V3", async () => { + cacheBeforeEach(async () => { + await setup.weth.connect(owner.wallet).approve(uniswapV3Setup.nftPositionManager.address, ether(350)); + await setup.wbtc.connect(owner.wallet).approve(uniswapV3Setup.nftPositionManager.address, bitcoin(25)); + + await uniswapV3Setup.addLiquidityWide( + setup.weth, + setup.wbtc, + 3000, + ether(350), + bitcoin(25), + owner.address + ); + + tradeModule = tradeModule.connect(manager.wallet); + await tradeModule.initialize(setToken.address); + + sourceTokenQuantity = wbtcUnits; + + // Transfer sourceToken from owner to manager for issuance + sourceToken = sourceToken.connect(owner.wallet); + await sourceToken.transfer(manager.address, wbtcUnits.mul(100)); + + // Approve tokens to Controller and call issue + sourceToken = sourceToken.connect(manager.wallet); + await sourceToken.approve(setup.issuanceModule.address, ethers.constants.MaxUint256); + + // Deploy mock issuance hook and initialize issuance module + setup.issuanceModule = setup.issuanceModule.connect(manager.wallet); + mockPreIssuanceHook = await deployer.mocks.deployManagerIssuanceHookMock(); + await setup.issuanceModule.initialize(setToken.address, mockPreIssuanceHook.address); + + issueQuantity = ether(1); + await setup.issuanceModule.issue(setToken.address, issueQuantity, owner.address); + }); + + beforeEach(async () => { + subjectSourceToken = sourceToken.address; + subjectDestinationToken = destinationToken.address; + subjectSourceQuantity = sourceTokenQuantity; + subjectSetToken = setToken.address; + subjectAdapterName = uniswapV3AdapterName; + subjectData = await uniswapV3ExchangeAdapter.generateDataParam([setup.wbtc.address, setup.weth.address], [3000]); + subjectMinDestinationQuantity = BigNumber.from(0); + subjectCaller = manager; + }); + + async function subject(): Promise { + tradeModule = tradeModule.connect(subjectCaller.wallet); + return tradeModule.trade( + subjectSetToken, + subjectAdapterName, + subjectSourceToken, + subjectSourceQuantity, + subjectDestinationToken, + subjectMinDestinationQuantity, + subjectData + ); + } + + it("should transfer the correct components to the SetToken", async () => { + const oldDestinationTokenBalance = await destinationToken.balanceOf(setToken.address); + const expectedReceiveQuantity = await uniswapV3Setup.quoter.callStatic.quoteExactInputSingle( + subjectSourceToken, + subjectDestinationToken, + 3000, + subjectSourceQuantity, + 0 + ); + + await subject(); + + const expectedDestinationTokenBalance = oldDestinationTokenBalance.add(expectedReceiveQuantity); + const newDestinationTokenBalance = await destinationToken.balanceOf(setToken.address); + expect(newDestinationTokenBalance).to.eq(expectedDestinationTokenBalance); + }); + + it("should transfer the correct components from the SetToken", async () => { + const oldSourceTokenBalance = await sourceToken.balanceOf(setToken.address); + + await subject(); + + const totalSourceQuantity = issueQuantity.mul(sourceTokenQuantity).div(ether(1)); + const expectedSourceTokenBalance = oldSourceTokenBalance.sub(totalSourceQuantity); + const newSourceTokenBalance = await sourceToken.balanceOf(setToken.address); + expect(newSourceTokenBalance).to.eq(expectedSourceTokenBalance); + }); + + it("should update the positions on the SetToken correctly", async () => { + const initialPositions = await setToken.getPositions(); + const expectedReceiveQuantity = await uniswapV3Setup.quoter.callStatic.quoteExactInputSingle( + subjectSourceToken, + subjectDestinationToken, + 3000, + subjectSourceQuantity, + 0 + ); + + await subject(); + + // All WBTC is sold for WETH + const currentPositions = await setToken.getPositions(); + const newFirstPosition = (await setToken.getPositions())[0]; + + expect(initialPositions.length).to.eq(1); + expect(currentPositions.length).to.eq(1); + expect(newFirstPosition.component).to.eq(destinationToken.address); + expect(newFirstPosition.unit).to.eq(expectedReceiveQuantity); + expect(newFirstPosition.module).to.eq(ADDRESS_ZERO); + }); + + describe("when path is through multiple trading pairs", async () => { + beforeEach(async () => { + await setup.weth.connect(owner.wallet).approve(uniswapV3Setup.nftPositionManager.address, ether(1000)); + await setup.dai.connect(owner.wallet).approve(uniswapV3Setup.nftPositionManager.address, ether(1000000)); + + await uniswapV3Setup.addLiquidityWide( + setup.weth, + setup.dai, + 3000, + ether(1000), + ether(1000000), + owner.address + ); + + subjectDestinationToken = setup.dai.address; + + const tradePath = [subjectSourceToken, setup.weth.address, subjectDestinationToken]; + const fees = [3000, 3000]; + + subjectData = await uniswapV3ExchangeAdapter.generateDataParam(tradePath, fees); + }); + + it("should transfer the correct components to the SetToken", async () => { + const oldDestinationTokenBalance = await setup.dai.balanceOf(setToken.address); + const expectedReceiveQuantity = await uniswapV3Setup.quoter.callStatic.quoteExactInput( + subjectData, + subjectSourceQuantity + ); + + await subject(); + + const expectedDestinationTokenBalance = oldDestinationTokenBalance.add(expectedReceiveQuantity); + const newDestinationTokenBalance = await setup.dai.balanceOf(setToken.address); + expect(newDestinationTokenBalance).to.eq(expectedDestinationTokenBalance); + }); + }); + }); }); describe("#removeModule", async () => { diff --git a/utils/contracts/index.ts b/utils/contracts/index.ts index 3c9a07838..f0da9665f 100644 --- a/utils/contracts/index.ts +++ b/utils/contracts/index.ts @@ -91,6 +91,7 @@ export { UniswapV2Factory } from "../../typechain/UniswapV2Factory"; export { UniswapV2Pair } from "../../typechain/UniswapV2Pair"; export { UniswapV2Router02 } from "../../typechain/UniswapV2Router02"; export { UniswapYieldHook } from "../../typechain/UniswapYieldHook"; +export { UniswapV3ExchangeAdapter } from "../../typechain/UniswapV3ExchangeAdapter"; export { WETH9 } from "../../typechain/WETH9"; export { WrapAdapterMock } from "../../typechain/WrapAdapterMock"; export { WrapModule } from "../../typechain/WrapModule"; diff --git a/utils/deploys/deployAdapters.ts b/utils/deploys/deployAdapters.ts index c95474026..66d3b0512 100644 --- a/utils/deploys/deployAdapters.ts +++ b/utils/deploys/deployAdapters.ts @@ -19,6 +19,7 @@ import { UniswapV2ExchangeAdapterV2, UniswapV2IndexExchangeAdapter, UniswapV2TransferFeeExchangeAdapter, + UniswapV3ExchangeAdapter, ZeroExApiAdapter, SnapshotGovernanceAdapter, SynthetixExchangeAdapter, @@ -47,6 +48,7 @@ import { UniswapV2ExchangeAdapter__factory } from "../../typechain/factories/Uni import { UniswapV2TransferFeeExchangeAdapter__factory } from "../../typechain/factories/UniswapV2TransferFeeExchangeAdapter__factory"; import { UniswapV2ExchangeAdapterV2__factory } from "../../typechain/factories/UniswapV2ExchangeAdapterV2__factory"; import { UniswapV2IndexExchangeAdapter__factory } from "../../typechain/factories/UniswapV2IndexExchangeAdapter__factory"; +import { UniswapV3ExchangeAdapter__factory } from "../../typechain/factories/UniswapV3ExchangeAdapter__factory"; import { SnapshotGovernanceAdapter__factory } from "../../typechain/factories/SnapshotGovernanceAdapter__factory"; import { SynthetixExchangeAdapter__factory } from "../../typechain/factories/SynthetixExchangeAdapter__factory"; import { CompoundBravoGovernanceAdapter__factory } from "../../typechain/factories/CompoundBravoGovernanceAdapter__factory"; @@ -188,4 +190,8 @@ export default class DeployAdapters { synthetixExchangerAddress ); } + + public async deployUniswapV3ExchangeAdapter(swapRouter: Address): Promise { + return await new UniswapV3ExchangeAdapter__factory(this._deployerSigner).deploy(swapRouter); + } } diff --git a/utils/fixtures/uniswapV3Fixture.ts b/utils/fixtures/uniswapV3Fixture.ts index 58c50261e..9ee2d8ff4 100644 --- a/utils/fixtures/uniswapV3Fixture.ts +++ b/utils/fixtures/uniswapV3Fixture.ts @@ -83,7 +83,7 @@ export class UniswapV3Fixture { ): Promise { - let ratio = _ratio * (10 ** (await _token0.decimals() - await _token1.decimals())); + let ratio = _ratio * (10 ** (await _token1.decimals() - await _token0.decimals())); if (_token0.address.toLowerCase() > _token1.address.toLowerCase()) { ratio = 1 / ratio;