From 465a91647ca5a3d763108937e5803ba4b849a815 Mon Sep 17 00:00:00 2001 From: cgewecke Date: Thu, 10 Jun 2021 15:26:31 -0700 Subject: [PATCH] Add forked provider infrastructure and simple TradeModule / ExchangeAdapter tests (#64) --- .circleci/config.yml | 41 ++-- .env.default | 1 + contracts/mocks/ForceFunderMock.sol | 33 +++ hardhat.config.ts | 31 ++- package.json | 2 + .../sushiswapExchangeTradeModule.spec.ts | 204 ++++++++++++++++++ .../uniswapV2ExchangeTradeModule.spec.ts | 203 +++++++++++++++++ utils/contracts/index.ts | 1 + utils/deploys/dependencies.ts | 11 + utils/deploys/deployExternal.ts | 6 +- utils/deploys/deployMocks.ts | 11 + utils/fixtures/uniswapFixture.ts | 11 +- utils/test/accountUtils.ts | 57 ++++- utils/test/index.ts | 4 + utils/test/types.ts | 5 + 15 files changed, 598 insertions(+), 23 deletions(-) create mode 100644 contracts/mocks/ForceFunderMock.sol create mode 100644 test/integration/sushiswapExchangeTradeModule.spec.ts create mode 100644 test/integration/uniswapV2ExchangeTradeModule.spec.ts diff --git a/.circleci/config.yml b/.circleci/config.yml index 580f5e7d0..b160837ee 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -10,8 +10,6 @@ jobs: working_directory: ~/set-protocol-v2 steps: - checkout - - setup_remote_docker: - docker_layer_caching: false - restore_cache: key: module-cache-{{ checksum "yarn.lock" }} - run: @@ -37,11 +35,6 @@ jobs: working_directory: ~/set-protocol-v2 parallelism: 3 steps: - - setup_remote_docker: - docker_layer_caching: false - - run: - name: Fetch solc version - command: docker pull ethereum/solc:0.6.10 - restore_cache: key: compiled-env-{{ .Environment.CIRCLE_SHA1 }} - run: @@ -54,9 +47,23 @@ jobs: - run: name: Hardhat Test command: | - TEST_FILES="$(circleci tests glob "./test/**/*.spec.ts" | circleci tests split --split-by=timings)" + TEST_FILES="$(circleci tests glob "./test/**/*.spec.ts" | circleci tests split)" yarn test ${TEST_FILES} + test_forked_network: + docker: + - image: circleci/node:10.16.0 + working_directory: ~/set-protocol-v2 + steps: + - restore_cache: + key: compiled-env-{{ .Environment.CIRCLE_SHA1 }} + - run: + name: Set Up Environment Variables + command: cp .env.default .env + - run: + name: Hardhat Test + command: yarn test:fork + coverage: docker: - image: circleci/node:10.11.0 @@ -67,11 +74,6 @@ jobs: # to istanbul-combine in the `report_coverage` job parallelism: 5 steps: - - setup_remote_docker: - docker_layer_caching: false - - run: - name: Fetch solc version - command: docker pull ethereum/solc:0.6.10 - restore_cache: key: compiled-env-{{ .Environment.CIRCLE_SHA1 }} - run: @@ -84,7 +86,7 @@ jobs: name: Coverage command: | TEST_FILES="{$(circleci tests glob "./test/**/*.spec.ts" | \ - circleci tests split --split-by=timings | xargs | sed -e 's/ /,/g')}" + circleci tests split | xargs | sed -e 's/ /,/g')}" yarn coverage -- --testfiles "$TEST_FILES" - run: name: Save coverage @@ -112,9 +114,13 @@ jobs: - run: name: Combine coverage reports command: | - mkdir -p reports cp -R /tmp/coverage/* . - npx istanbul-combine-updated -r lcov cov_0.json cov_1.json cov_2.json cov_3.json cov_4.json + npx istanbul-combine-updated -r lcov \ + cov_0.json \ + cov_1.json \ + cov_2.json \ + cov_3.json \ + cov_4.json - run: name: Upload coverage command: | @@ -128,6 +134,9 @@ workflows: - test: requires: - checkout_and_compile + - test_forked_network: + requires: + - checkout_and_compile - coverage: requires: - checkout_and_compile diff --git a/.env.default b/.env.default index 4734022b1..6a2cbc5b9 100644 --- a/.env.default +++ b/.env.default @@ -1,4 +1,5 @@ # These are randomly generated hex so that CircleCI will work +ALCHEMY_TOKEN=fake_alchemy_token INFURA_TOKEN=799e620c4b39064f7a8cfd8452976ed1 KOVAN_DEPLOY_PRIVATE_KEY=0f3456f7f1ed59aaa29f35f4674a87e754e1055249a2888bdaec3dea49d2e456 STAGING_MAINNET_DEPLOY_PRIVATE_KEY=0f3456f7f1ed59aaa29f35f4674a87e754e1055249a2888bdaec3dea49d2e456 diff --git a/contracts/mocks/ForceFunderMock.sol b/contracts/mocks/ForceFunderMock.sol new file mode 100644 index 000000000..213bac272 --- /dev/null +++ b/contracts/mocks/ForceFunderMock.sol @@ -0,0 +1,33 @@ +/* + 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; + + +contract ForceFunderMock { + /** + * Convenience method for depositing eth into non-payable contracts + * which the forked provider tests would like to impersonate + * as a message sender. + * + * @param destination destination of eth payment + */ + function fund(address destination) public payable { + selfdestruct(payable(address(destination))); + } +} diff --git a/hardhat.config.ts b/hardhat.config.ts index d9891bebc..8493ec4f3 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -1,5 +1,6 @@ require("dotenv").config(); +import chalk from "chalk"; import { HardhatUserConfig } from "hardhat/config"; import { privateKeys } from "./utils/wallets"; @@ -9,6 +10,19 @@ import "solidity-coverage"; import "hardhat-deploy"; import "./tasks"; +const forkingConfig = { + url: `https://eth-mainnet.alchemyapi.io/v2/${process.env.ALCHEMY_TOKEN}`, + blockNumber: 12198000, +}; + +const mochaConfig = { + grep: "@forked-mainnet", + invert: (process.env.FORK) ? false : true, + timeout: (process.env.FORK) ? 50000 : 20000, +} as Mocha.MochaOptions; + +checkForkedProviderEnvironment(); + const config: HardhatUserConfig = { solidity: { version: "0.6.10", @@ -21,6 +35,7 @@ const config: HardhatUserConfig = { }, networks: { hardhat: { + forking: (process.env.FORK) ? forkingConfig : undefined, accounts: getHardhatPrivateKeys(), }, localhost: { @@ -54,9 +69,7 @@ const config: HardhatUserConfig = { outDir: "typechain", target: "ethers-v5", }, - mocha: { - timeout: 100000, - }, + mocha: mochaConfig, }; function getHardhatPrivateKeys() { @@ -69,4 +82,16 @@ function getHardhatPrivateKeys() { }); } +function checkForkedProviderEnvironment() { + if (process.env.FORK && + (!process.env.ALCHEMY_TOKEN || process.env.ALCHEMY_TOKEN === "fake_alchemy_token") + ) { + console.log(chalk.red( + "You are running forked provider tests with invalid Alchemy credentials.\n" + + "Update your ALCHEMY_TOKEN settings in the `.env` file." + )); + process.exit(1); + } +} + export default config; diff --git a/package.json b/package.json index 207de7110..eaa45ab4e 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,8 @@ "prepublishOnly": "yarn clean && yarn build:npm", "rename-extensions": "for f in typechain/*.d.ts; do mv -- \"$f\" \"${f%.d.ts}.ts\"; done", "test": "npx hardhat test --network localhost", + "test:fork": "FORK=true npx hardhat test", + "test:fork:fast": "NO_COMPILE=true TS_NODE_TRANSPILE_ONLY=1 FORK=true npx hardhat test --no-compile", "test:clean": "yarn clean && yarn build && yarn test", "test:fast": "NO_COMPILE=true TS_NODE_TRANSPILE_ONLY=1 npx hardhat test --network localhost --no-compile", "test:fast:compile": "TS_NODE_TRANSPILE_ONLY=1 npx hardhat test --network localhost", diff --git a/test/integration/sushiswapExchangeTradeModule.spec.ts b/test/integration/sushiswapExchangeTradeModule.spec.ts new file mode 100644 index 000000000..13955881d --- /dev/null +++ b/test/integration/sushiswapExchangeTradeModule.spec.ts @@ -0,0 +1,204 @@ +import "module-alias/register"; + +import { BigNumber } from "@ethersproject/bignumber"; +import { ethers } from "hardhat"; + +import { Address, Bytes } from "@utils/types"; +import { Account } from "@utils/test/types"; +import { IERC20, UniswapV2Router02 } from "@typechain/index"; +import { + SetToken, + TradeModule, + ManagerIssuanceHookMock, + UniswapV2ExchangeAdapterV2, +} from "@utils/contracts"; +import { ZERO } from "@utils/constants"; +import DeployHelper from "@utils/deploys"; +import { + ether, + bitcoin, +} from "@utils/index"; +import { + cacheBeforeEach, + getAccounts, + getSystemFixture, + getWaffleExpect, + getUniswapFixture, + getForkedTokens, + initializeForkedTokens, + ForkedTokens +} from "@utils/test/index"; + +import { SystemFixture, UniswapFixture } from "@utils/fixtures"; + +const expect = getWaffleExpect(); + +describe("SushiSwap TradeModule Integration [ @forked-mainnet ]", () => { + let owner: Account; + let manager: Account; + + let deployer: DeployHelper; + + let uniswapExchangeAdapterV2: UniswapV2ExchangeAdapterV2; + let uniswapAdapterV2Name: string; + + let setup: SystemFixture; + let sushiswapSetup: UniswapFixture; + let sushiswapRouter: UniswapV2Router02; + let tradeModule: TradeModule; + let tokens: ForkedTokens; + let wbtcRate: BigNumber; + + before(async () => { + [ + owner, + manager, + ] = await getAccounts(); + + + deployer = new DeployHelper(owner.wallet); + setup = getSystemFixture(owner.address); + await setup.initialize(); + + wbtcRate = ether(29); + + sushiswapSetup = getUniswapFixture(owner.address); + sushiswapRouter = sushiswapSetup.getForkedSushiswapRouter(); + + uniswapExchangeAdapterV2 = await deployer.adapters.deployUniswapV2ExchangeAdapterV2(sushiswapRouter.address); + uniswapAdapterV2Name = "UNISWAPV2"; + + tradeModule = await deployer.modules.deployTradeModule(setup.controller.address); + await setup.controller.addModule(tradeModule.address); + + await setup.integrationRegistry.addIntegration( + tradeModule.address, + uniswapAdapterV2Name, + uniswapExchangeAdapterV2.address + ); + + await initializeForkedTokens(deployer); + }); + + describe("#trade", function() { + let sourceToken: IERC20; + let wbtcUnits: BigNumber; + let destinationToken: IERC20; + let setToken: SetToken; + let issueQuantity: BigNumber; + + context("when trading a Default component on Sushiswap (version 2 adapter)", async () => { + let mockPreIssuanceHook: ManagerIssuanceHookMock; + let sourceTokenQuantity: BigNumber; + let destinationTokenQuantity: BigNumber; + + let subjectDestinationToken: Address; + let subjectSourceToken: Address; + let subjectSourceQuantity: BigNumber; + let subjectAdapterName: string; + let subjectSetToken: Address; + let subjectMinDestinationQuantity: BigNumber; + let subjectData: Bytes; + let subjectCaller: Account; + + cacheBeforeEach(async () => { + tokens = getForkedTokens(); + + sourceToken = tokens.wbtc; + destinationToken = tokens.weth; + wbtcUnits = BigNumber.from(100000000); // 1 WBTC in base units 1 * 10 ** 8 + + // Create Set token + setToken = await setup.createSetToken( + [sourceToken.address], + [wbtcUnits], + [setup.issuanceModule.address, tradeModule.address], + manager.address + ); + + await sourceToken.approve(sushiswapRouter.address, bitcoin(100)); + await destinationToken.approve(sushiswapRouter.address, ether(3400)); + + tradeModule = tradeModule.connect(manager.wallet); + await tradeModule.initialize(setToken.address); + + sourceTokenQuantity = wbtcUnits; + const sourceTokenDecimals = 8; + destinationTokenQuantity = wbtcRate.mul(sourceTokenQuantity).div(10 ** sourceTokenDecimals); + + // Transfer from wbtc whale to manager + await sourceToken.transfer(manager.address, wbtcUnits.mul(1)); + + // 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; + subjectMinDestinationQuantity = destinationTokenQuantity.sub(ether(1)); // Receive a min of 28 WETH for 1 WBTC + subjectAdapterName = uniswapAdapterV2Name; + + const tradePath = [subjectSourceToken, subjectDestinationToken]; + + const shouldSwapExactTokenForToken = true; + subjectData = await uniswapExchangeAdapterV2.getUniswapExchangeData( + tradePath, + shouldSwapExactTokenForToken + ); + + 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 sushiswapRouter.getAmountsOut( + subjectSourceQuantity, + [subjectSourceToken, subjectDestinationToken] + ); + + await subject(); + + const expectedDestinationTokenBalance = oldDestinationTokenBalance.add(expectedReceiveQuantity); + const newDestinationTokenBalance = await destinationToken.balanceOf(setToken.address); + expect(expectedReceiveQuantity).to.be.gt(ZERO); + 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); + }); + }); + }); +}); diff --git a/test/integration/uniswapV2ExchangeTradeModule.spec.ts b/test/integration/uniswapV2ExchangeTradeModule.spec.ts new file mode 100644 index 000000000..065249e69 --- /dev/null +++ b/test/integration/uniswapV2ExchangeTradeModule.spec.ts @@ -0,0 +1,203 @@ +import "module-alias/register"; + +import { BigNumber } from "@ethersproject/bignumber"; +import { ethers } from "hardhat"; + +import { Address, Bytes } from "@utils/types"; +import { Account } from "@utils/test/types"; +import { IERC20, UniswapV2Router02 } from "@typechain/index"; +import { + SetToken, + TradeModule, + ManagerIssuanceHookMock, + UniswapV2ExchangeAdapterV2, +} from "@utils/contracts"; +import { ZERO } from "@utils/constants"; +import DeployHelper from "@utils/deploys"; +import { + ether, + bitcoin, +} from "@utils/index"; +import { + cacheBeforeEach, + getAccounts, + getSystemFixture, + getWaffleExpect, + getUniswapFixture, + getForkedTokens, + initializeForkedTokens, + ForkedTokens +} from "@utils/test/index"; + +import { SystemFixture, UniswapFixture } from "@utils/fixtures"; + +const expect = getWaffleExpect(); + +describe("UniswapExchangeV2 TradeModule Integration [ @forked-mainnet ]", () => { + let owner: Account; + let manager: Account; + + let deployer: DeployHelper; + + let uniswapExchangeAdapterV2: UniswapV2ExchangeAdapterV2; + let uniswapAdapterV2Name: string; + + let setup: SystemFixture; + let uniswapSetup: UniswapFixture; + let uniswapRouter: UniswapV2Router02; + let tradeModule: TradeModule; + let tokens: ForkedTokens; + let wbtcRate: BigNumber; + + before(async () => { + [ + owner, + manager, + ] = await getAccounts(); + + + deployer = new DeployHelper(owner.wallet); + setup = getSystemFixture(owner.address); + await setup.initialize(); + + wbtcRate = ether(29); + + uniswapSetup = getUniswapFixture(owner.address); + uniswapRouter = uniswapSetup.getForkedUniswapRouter(); + + uniswapExchangeAdapterV2 = await deployer.adapters.deployUniswapV2ExchangeAdapterV2(uniswapRouter.address); + uniswapAdapterV2Name = "UNISWAPV2"; + + tradeModule = await deployer.modules.deployTradeModule(setup.controller.address); + await setup.controller.addModule(tradeModule.address); + + await setup.integrationRegistry.addIntegration( + tradeModule.address, + uniswapAdapterV2Name, + uniswapExchangeAdapterV2.address + ); + + await initializeForkedTokens(deployer); + }); + + describe("#trade", function() { + let sourceToken: IERC20; + let wbtcUnits: BigNumber; + let destinationToken: IERC20; + let setToken: SetToken; + let issueQuantity: BigNumber; + + context("when trading a Default component on Uniswap version 2 adapter", async () => { + let mockPreIssuanceHook: ManagerIssuanceHookMock; + let sourceTokenQuantity: BigNumber; + let destinationTokenQuantity: BigNumber; + + let subjectDestinationToken: Address; + let subjectSourceToken: Address; + let subjectSourceQuantity: BigNumber; + let subjectAdapterName: string; + let subjectSetToken: Address; + let subjectMinDestinationQuantity: BigNumber; + let subjectData: Bytes; + let subjectCaller: Account; + + cacheBeforeEach(async () => { + tokens = getForkedTokens(); + + sourceToken = tokens.wbtc; + destinationToken = tokens.weth; + wbtcUnits = BigNumber.from(100000000); // 1 WBTC in base units 1 * 10 ** 8 + + // Create Set token + setToken = await setup.createSetToken( + [sourceToken.address], + [wbtcUnits], + [setup.issuanceModule.address, tradeModule.address], + manager.address + ); + + await sourceToken.approve(uniswapRouter.address, bitcoin(100)); + await destinationToken.approve(uniswapRouter.address, ether(3400)); + + tradeModule = tradeModule.connect(manager.wallet); + await tradeModule.initialize(setToken.address); + + sourceTokenQuantity = wbtcUnits; + const sourceTokenDecimals = 8; + destinationTokenQuantity = wbtcRate.mul(sourceTokenQuantity).div(10 ** sourceTokenDecimals); + + // Transfer from wbtc whale to manager + await sourceToken.transfer(manager.address, wbtcUnits.mul(1)); + + // 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; + subjectMinDestinationQuantity = destinationTokenQuantity.sub(ether(1)); // Receive a min of 28 WETH for 1 WBTC + subjectAdapterName = uniswapAdapterV2Name; + + const tradePath = [subjectSourceToken, subjectDestinationToken]; + const shouldSwapExactTokenForToken = true; + + subjectData = await uniswapExchangeAdapterV2.getUniswapExchangeData( + tradePath, + shouldSwapExactTokenForToken + ); + 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 uniswapRouter.getAmountsOut( + subjectSourceQuantity, + [subjectSourceToken, subjectDestinationToken] + ); + + await subject(); + + const expectedDestinationTokenBalance = oldDestinationTokenBalance.add(expectedReceiveQuantity); + const newDestinationTokenBalance = await destinationToken.balanceOf(setToken.address); + expect(expectedReceiveQuantity).to.be.gt(ZERO); + 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); + }); + }); + }); +}); diff --git a/utils/contracts/index.ts b/utils/contracts/index.ts index f0da9665f..591186f97 100644 --- a/utils/contracts/index.ts +++ b/utils/contracts/index.ts @@ -32,6 +32,7 @@ export { DebtIssuanceModule } from "../../typechain/DebtIssuanceModule"; export { DebtModuleMock } from "../../typechain/DebtModuleMock"; export { DelegateRegistry } from "../../typechain/DelegateRegistry"; export { ExplicitERC20Mock } from "../../typechain/ExplicitERC20Mock"; +export { ForceFunderMock } from "../../typechain/ForceFunderMock"; export { GaugeControllerMock } from "../../typechain/GaugeControllerMock"; export { GeneralIndexModule } from "../../typechain/GeneralIndexModule"; export { GodModeMock } from "../../typechain/GodModeMock"; diff --git a/utils/deploys/dependencies.ts b/utils/deploys/dependencies.ts index ae14177e7..5763239f8 100644 --- a/utils/deploys/dependencies.ts +++ b/utils/deploys/dependencies.ts @@ -158,6 +158,9 @@ export default { ONE_INCH_EXCHANGE_ADDRESS: { 1: "0x11111254369792b2ca5d084ab5eea397ca8fa48b", }, + ZERO_EX_EXCHANGE: { + 1: "0xDef1C0ded9bec7F1a1670819833240f027b25EfF", + }, AAVE_MIGRATION_PROXY: { 1: "0x317625234562B1526Ea2FaC4030Ea499C5291de4", 42: "0x7c24e875D3ea8bc19cEEC6d8BcF26aA69bfFDC4C", @@ -167,6 +170,14 @@ export default { 42: "kovan", 50: "test-rpc", }, + + // WHALES (for forked mainnet testing) + + USDC_WHALE: "0x47ac0Fb4F2D84898e4D9E7b4DaB3C24507a6D503", + DAI_WHALE: "0x47ac0Fb4F2D84898e4D9E7b4DaB3C24507a6D503", + WETH_WHALE: "0x94B0A3d511b6EcDb17eBF877278Ab030acb0A878", + WBTC_WHALE: "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599", + } as any; export const DEPENDENCY = { diff --git a/utils/deploys/deployExternal.ts b/utils/deploys/deployExternal.ts index cd52ba729..de1ece93b 100644 --- a/utils/deploys/deployExternal.ts +++ b/utils/deploys/deployExternal.ts @@ -102,7 +102,7 @@ import { UniswapTimelock, UniswapV2Factory, UniswapV2Pair, - UniswapV2Router02 + UniswapV2Router02, } from "../contracts/uniswap"; import { StakingRewards__factory } from "../../typechain/factories/StakingRewards__factory"; @@ -519,6 +519,10 @@ export default class DeployExternalContracts { return await new UniswapV2Router02__factory(this._deployerSigner).deploy(_factory, _weth); } + public getForkedUniswapV2Router02(_mainnetRouter: Address): UniswapV2Router02 { + return UniswapV2Router02__factory.connect(_mainnetRouter, this._deployerSigner); + } + public async deployUniswapV2Pair(_factory: Address, _weth: Address): Promise { return await new UniswapV2Pair__factory(this._deployerSigner).deploy(); } diff --git a/utils/deploys/deployMocks.ts b/utils/deploys/deployMocks.ts index 5aad45b66..a30d5001f 100644 --- a/utils/deploys/deployMocks.ts +++ b/utils/deploys/deployMocks.ts @@ -15,6 +15,7 @@ import { DebtIssuanceMock, DebtModuleMock, ExplicitERC20Mock, + ForceFunderMock, GaugeControllerMock, GodModeMock, GovernanceAdapterMock, @@ -45,6 +46,7 @@ import { } from "../contracts"; import { convertLibraryNameToLinkId, ether } from "../common"; +import dependencies from "./dependencies"; import { AaveLendingPoolCoreMock__factory } from "../../typechain/factories/AaveLendingPoolCoreMock__factory"; import { AaveLendingPoolMock__factory } from "../../typechain/factories/AaveLendingPoolMock__factory"; @@ -58,6 +60,7 @@ import { CustomSetValuerMock__factory } from "../../typechain/factories/CustomSe import { DebtIssuanceMock__factory } from "../../typechain/factories/DebtIssuanceMock__factory"; import { DebtModuleMock__factory } from "../../typechain/factories/DebtModuleMock__factory"; import { ExplicitERC20Mock__factory } from "../../typechain/factories/ExplicitERC20Mock__factory"; +import { ForceFunderMock__factory } from "../../typechain/factories/ForceFunderMock__factory"; import { GaugeControllerMock__factory } from "../../typechain/factories/GaugeControllerMock__factory"; import { GodModeMock__factory } from "../../typechain/factories/GodModeMock__factory"; import { GovernanceAdapterMock__factory } from "../../typechain/factories/GovernanceAdapterMock__factory"; @@ -328,6 +331,10 @@ export default class DeployMocks { return await new YearnStrategyMock__factory(this._deployerSigner).deploy(vault); } + public async deployForceFunderMock(): Promise { + return await new ForceFunderMock__factory(this._deployerSigner).deploy(); + } + /************************************* * Instance getters ************************************/ @@ -335,4 +342,8 @@ export default class DeployMocks { public async getTokenMock(token: Address): Promise { return await new StandardTokenMock__factory(this._deployerSigner).attach(token); } + + public async getForkedZeroExExchange(): Promise { + return await ZeroExMock__factory.connect(dependencies.ZERO_EX_EXCHANGE[1], this._deployerSigner); + } } diff --git a/utils/fixtures/uniswapFixture.ts b/utils/fixtures/uniswapFixture.ts index c70c19e7d..19e221128 100644 --- a/utils/fixtures/uniswapFixture.ts +++ b/utils/fixtures/uniswapFixture.ts @@ -15,6 +15,7 @@ import { UniswapV2Router02 } from "../contracts/uniswap"; import { UniswapV2Pair__factory } from "../../typechain/factories/UniswapV2Pair__factory"; +import dependencies from "../deploys/dependencies"; import { ether } from "../index"; import { ONE_DAY_IN_SECONDS } from "../constants"; @@ -96,4 +97,12 @@ export class UniswapFixture { public getTokenOrder(_tokenOne: Address, _tokenTwo: Address): [Address, Address] { return _tokenOne.toLowerCase() < _tokenTwo.toLowerCase() ? [_tokenOne, _tokenTwo] : [_tokenTwo, _tokenOne]; } -} \ No newline at end of file + + public getForkedUniswapRouter(): UniswapV2Router02 { + return this._deployer.external.getForkedUniswapV2Router02(dependencies.UNISWAP_ROUTER[1]); + } + + public getForkedSushiswapRouter(): UniswapV2Router02 { + return this._deployer.external.getForkedUniswapV2Router02(dependencies.SUSHISWAP_ROUTER[1]); + } +} diff --git a/utils/test/accountUtils.ts b/utils/test/accountUtils.ts index 9989f9043..79dcc53e2 100644 --- a/utils/test/accountUtils.ts +++ b/utils/test/accountUtils.ts @@ -1,8 +1,12 @@ -import { ethers } from "hardhat"; +import { ethers, network } from "hardhat"; import { BigNumber } from "@ethersproject/bignumber"; import { Address } from "../types"; -import { Account } from "./types"; +import { Account, ForkedTokens } from "./types"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; +import dependencies from "../deploys/dependencies"; +import { IERC20__factory } from "../../typechain"; +import { ether } from "../common"; +import type DeployHelper from "../deploys"; const provider = ethers.provider; @@ -34,3 +38,52 @@ export const getEthBalance = async (account: Address): Promise => { export const getWallets = async (): Promise => { return (await ethers.getSigners() as SignerWithAddress[]); }; + +const getForkedDependencyAddresses = (): any => { + return { + whales: [ + dependencies.DAI_WHALE, + dependencies.WETH_WHALE, + dependencies.WBTC_WHALE, + dependencies.USDC_WHALE, + ], + + tokens: [ + dependencies.DAI[1], + dependencies.WETH[1], + dependencies.WBTC[1], + dependencies.USDC[1], + ], + }; +}; + +// Mainnet token instances connected their impersonated +// top holders to enable approval / transfer etc. +export const getForkedTokens = (): ForkedTokens => { + const enum ids { DAI, WETH, WBTC, USDC } + const { whales, tokens } = getForkedDependencyAddresses(); + + const forkedTokens = { + dai: IERC20__factory.connect(tokens[ids.DAI], provider.getSigner(whales[ids.DAI])), + weth: IERC20__factory.connect(tokens[ids.WETH], provider.getSigner(whales[ids.WETH])), + wbtc: IERC20__factory.connect(tokens[ids.WBTC], provider.getSigner(whales[ids.WBTC])), + usdc: IERC20__factory.connect(tokens[ids.USDC], provider.getSigner(whales[ids.USDC])), + }; + + return forkedTokens; +}; + +export const initializeForkedTokens = async (deployer: DeployHelper): Promise => { + const { whales } = getForkedDependencyAddresses(); + + for (const whale of whales) { + await network.provider.request({ + method: "hardhat_impersonateAccount", + params: [whale]}, + ); + + const funder = await deployer.mocks.deployForceFunderMock(); + await funder.fund(whale, {value: ether(100)}); // Gas money + } +}; + diff --git a/utils/test/index.ts b/utils/test/index.ts index 185a80212..dd1335bee 100644 --- a/utils/test/index.ts +++ b/utils/test/index.ts @@ -19,10 +19,14 @@ export const getUniswapFixture = (ownerAddress: Address) => new UniswapFixture(p export const getYearnFixture = (ownerAddress: Address) => new YearnFixture(provider, ownerAddress); export const getUniswapV3Fixture = (ownerAddress: Address) => new UniswapV3Fixture(provider, ownerAddress); +export { ForkedTokens } from "./types"; + export { getAccounts, getEthBalance, getRandomAccount, + getForkedTokens, + initializeForkedTokens, } from "./accountUtils"; export { addSnapshotBeforeRestoreAfterEach, diff --git a/utils/test/types.ts b/utils/test/types.ts index 76b52cdba..5acbcf9ae 100644 --- a/utils/test/types.ts +++ b/utils/test/types.ts @@ -1,7 +1,12 @@ import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; import { Address } from "@utils/types"; +import { IERC20 } from "../../typechain"; export type Account = { address: Address; wallet: SignerWithAddress; }; + +export type ForkedTokens = { + [key: string]: IERC20; +};