├── .env.example ├── .github └── workflows │ ├── lint.yml │ └── tests.yml ├── .gitignore ├── .husky ├── .gitignore ├── commit-msg └── pre-commit ├── .mocharc.js ├── .prettierignore ├── .prettierrc ├── .solcover.js ├── .solhint.json ├── .solhintignore ├── .vscode └── settings.json ├── BUG_BOUNTY.md ├── LICENSE ├── README.md ├── commitlint.config.js ├── contracts ├── DCAFeeManager │ └── DCAFeeManager.sol ├── DCAHubCompanion │ ├── DCAHubCompanion.sol │ ├── DCAHubCompanionHubProxyHandler.sol │ └── DCAHubCompanionLibrariesHandler.sol ├── DCAHubSwapper │ ├── CallerOnlyDCAHubSwapper.sol │ ├── ThirdPartyDCAHubSwapper.sol │ └── utils │ │ └── DeadlineValidation.sol ├── DCAKeep3rJob │ └── DCAKeep3rJob.sol ├── interfaces │ ├── ICallerOnlyDCAHubSwapper.sol │ ├── IDCAFeeManager.sol │ ├── IDCAHubCompanion.sol │ ├── IDCAKeep3rJob.sol │ ├── ILegacyDCAHub.sol │ ├── ISharedTypes.sol │ └── external │ │ └── IPermit2.sol ├── libraries │ ├── InputBuilding.sol │ ├── ModifyPositionWithRate.sol │ ├── Permit2Transfers.sol │ └── SecondsUntilNextSwap.sol ├── mocks │ ├── DCAFeeManager │ │ └── DCAFeeManager.sol │ ├── DCAHubCompanion │ │ └── DCAHubCompanionHubProxyHandler.sol │ ├── DCAHubSwapper │ │ └── CallerOnlyDCAHubSwapper.sol │ ├── ISwapper.sol │ ├── LegacyDCASwapper.sol │ ├── libraries │ │ ├── InputBuilding.sol │ │ ├── ModifyPositionWithRate.sol │ │ └── SecondsUntilNextSwap.sol │ └── utils │ │ └── BaseCompanion.sol └── utils │ ├── BaseCompanion.sol │ ├── Governable.sol │ ├── PayableMulticall.sol │ ├── SwapAdapter.sol │ ├── interfaces │ └── IGovernable.sol │ └── types │ ├── SwapContext.sol │ └── TransferOutBalance.sol ├── deploy ├── 001_companion.ts ├── 002_caller_only_swapper.ts ├── 003_third_party_swapper.ts ├── 004_fee_manager.ts └── 005_keep3r_job.ts ├── hardhat.config.ts ├── package.json ├── tasks └── npm-publish-clean-typechain.ts ├── test ├── integration │ ├── DCAHubCompanion │ │ ├── multi-call.spec.ts │ │ └── position-migrator.spec.ts │ ├── DCAHubSwapper │ │ ├── multi-pair-swap-with-dex.spec.ts │ │ ├── swap-for-caller.spec.ts │ │ └── swap-with-dex-native.spec.ts │ ├── DCAKeep3rJob │ │ └── keep3r-job.spec.ts │ ├── abis │ │ └── Keep3r.json │ ├── fork-block-numbers.ts │ ├── protocols │ │ └── liquidities.spec.ts │ └── utils.ts ├── unit │ ├── DCAFeeManager │ │ └── dca-fee-manager.spec.ts │ ├── DCAHubCompanion │ │ └── dca-hub-companion-hub-proxy-handler.spec.ts │ ├── DCAHubSwapper │ │ ├── caller-only-dca-hub-swapper.spec.ts │ │ └── third-party-dca-hub-swapper.spec.ts │ ├── DCAKeep3rJob │ │ └── dca-keep3r-job.spec.ts │ ├── libraries │ │ ├── input-building.spec.ts │ │ ├── modify-position-with-rate.spec.ts │ │ └── seconds-until-next-swap.spec.ts │ └── utils │ │ └── base-companion.spec.ts └── utils │ ├── bdd.ts │ ├── behaviours.ts │ ├── bn.ts │ ├── chainlink.ts │ ├── coingecko.ts │ ├── constants.ts │ ├── contracts.ts │ ├── dexes │ ├── oneinch.ts │ └── paraswap.ts │ ├── event-utils.ts │ ├── evm.ts │ ├── index.ts │ ├── interval-utils.ts │ ├── swap-utils.ts │ └── wallet.ts ├── tsconfig.json ├── tsconfig.publish.json ├── utils └── network.ts ├── workspace.code-workspace └── yarn.lock /.env.example: -------------------------------------------------------------------------------- 1 | # network specific node uri: `"ETH_NODE_URI_" + networkName.toUpperCase()` 2 | ETH_NODE_URI_MAINNET=https://eth-mainnet.alchemyapi.io/v2/ 3 | # generic node uri (if no specific found): 4 | ETH_NODE_URI=https://eth-{{networkName}}.infura.io/v3/ 5 | 6 | # network specific mnemonic: `MNEMONIC_${networkName.toUpperCase()}` 7 | MNEMONIC_MAINNET= 8 | # generic mnemonic (if no specific found): 9 | MNEMONIC= 10 | 11 | # Mocha (10 minutes) 12 | MOCHA_TIMEOUT=600000 13 | 14 | # Coinmarketcap 15 | COINMARKETCAP_API_KEY= 16 | COINMARKETCAP_DEFAULT_CURRENCY=USD 17 | 18 | # Etherscan 19 | ETHERSCAN_API_KEY= 20 | 21 | # Necessary for tests 22 | TS_NODE_SKIP_IGNORE=true -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: [push] 4 | 5 | jobs: 6 | files: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - name: Check out github repository 11 | uses: actions/checkout@v2 12 | with: 13 | fetch-depth: 1 14 | 15 | - name: Cache node modules 16 | uses: actions/cache@v3 17 | env: 18 | cache-name: cache-node-modules 19 | with: 20 | path: "**/node_modules" 21 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/yarn.lock') }} 22 | 23 | - name: Install node 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: "18.x" 27 | 28 | - name: Install dependencies 29 | run: yarn --frozen-lockfile 30 | 31 | - name: Run linter 32 | run: yarn lint:check 33 | 34 | commits: 35 | runs-on: ubuntu-latest 36 | 37 | steps: 38 | - name: Check out github repository 39 | uses: actions/checkout@v2 40 | with: 41 | fetch-depth: 0 42 | 43 | - name: Run commitlint 44 | uses: wagoid/commitlint-github-action@v2 45 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | unit: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Check out github repository 14 | uses: actions/checkout@v2 15 | with: 16 | fetch-depth: 1 17 | 18 | - name: Cache node modules 19 | uses: actions/cache@v3 20 | env: 21 | cache-name: cache-node-modules 22 | with: 23 | path: "**/node_modules" 24 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/yarn.lock') }} 25 | 26 | - name: Install node 27 | uses: actions/setup-node@v1 28 | with: 29 | node-version: "18.x" 30 | 31 | - name: Install dependencies 32 | run: yarn --frozen-lockfile 33 | 34 | - name: Run unit tests 35 | run: yarn test:unit 36 | timeout-minutes: 15 37 | 38 | integration: 39 | needs: ["unit"] 40 | runs-on: ubuntu-latest 41 | steps: 42 | - name: Check out github repository 43 | uses: actions/checkout@v2 44 | with: 45 | fetch-depth: 1 46 | 47 | - name: Cache node modules 48 | uses: actions/cache@v3 49 | env: 50 | cache-name: cache-node-modules 51 | with: 52 | path: "**/node_modules" 53 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/yarn.lock') }} 54 | 55 | - name: Cache hardhat network fork 56 | uses: actions/cache@v3 57 | env: 58 | cache-name: cache-hardhat-network-fork 59 | with: 60 | path: cache/hardhat-network-fork 61 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('test/integration/fork-block-numbers.ts') }} 62 | 63 | - name: Install node 64 | uses: actions/setup-node@v1 65 | with: 66 | node-version: "18.x" 67 | 68 | - name: Install dependencies 69 | run: yarn --frozen-lockfile 70 | 71 | - name: Run integration tests 72 | run: yarn test:integration 73 | env: 74 | TS_NODE_SKIP_IGNORE: true 75 | ETH_NODE_URI_ETHEREUM: https://eth-mainnet.g.alchemy.com/v2/${{ secrets.ALCHEMY_API_KEY }} 76 | ETH_NODE_URI_OPTIMISM: https://opt-mainnet.g.alchemy.com/v2/${{ secrets.ALCHEMY_API_KEY }} 77 | ETH_NODE_URI_OPTIMISM_KOVAN: https://opt-kovan.g.alchemy.com/v2/${{ secrets.ALCHEMY_API_KEY }} 78 | ETH_NODE_URI_POLYGON: https://polygon-mainnet.g.alchemy.com/v2/${{ secrets.ALCHEMY_API_KEY }} 79 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # General 2 | dist 3 | node_modules 4 | .DS_STORE 5 | 6 | # Coverage 7 | coverage.json 8 | coverage 9 | gasReporterOutput.json 10 | gasReporterOutput-*.json 11 | 12 | # Hardhat files 13 | cache 14 | artifacts 15 | typechained 16 | deployments/hardhat 17 | deployments/localhost 18 | 19 | # Config files 20 | .env 21 | .config.json 22 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit $1 -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run lint:fix && git add -A -------------------------------------------------------------------------------- /.mocharc.js: -------------------------------------------------------------------------------- 1 | const dotenv = require('dotenv'); 2 | dotenv.config(); 3 | 4 | module.exports = { 5 | require: ['hardhat/register'], 6 | extension: ['.ts'], 7 | ignore: ['./test/utils/**'], 8 | recursive: true, 9 | }; 10 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # General 2 | dist 3 | workspace.code-workspace 4 | CHANGELOG.md 5 | .DS_STORE 6 | .prettierignore 7 | .solhintignore 8 | .husky 9 | .gitignore 10 | .gitattributes 11 | .versionrc 12 | .env 13 | .env.example 14 | codechecks.yml 15 | gasReporterOutput-*.json 16 | **/LICENSE 17 | 18 | # Hardhat 19 | artifacts 20 | cache 21 | deployments 22 | 23 | # JS 24 | node_modules 25 | yarn.lock 26 | 27 | # Coverage 28 | coverage 29 | coverage.json 30 | 31 | # Solidity 32 | contracts/mock 33 | typechained 34 | 35 | # Gas report 36 | gasReporterOutput.json 37 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "overrides": [ 3 | { 4 | "files": "**.sol", 5 | "options": { 6 | "printWidth": 145, 7 | "tabWidth": 2, 8 | "useTabs": false, 9 | "singleQuote": true, 10 | "bracketSpacing": false 11 | } 12 | }, 13 | { 14 | "files": ["**.ts", "**.js"], 15 | "options": { 16 | "printWidth": 145, 17 | "tabWidth": 2, 18 | "semi": true, 19 | "singleQuote": true, 20 | "useTabs": false, 21 | "endOfLine": "auto" 22 | } 23 | }, 24 | { 25 | "files": "**.json", 26 | "options": { 27 | "tabWidth": 2, 28 | "printWidth": 200 29 | } 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /.solcover.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | skipFiles: ['interfaces'], 3 | mocha: { 4 | forbidOnly: true, 5 | grep: '@skip-on-coverage', 6 | invert: true, 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:recommended", 3 | "plugins": ["prettier"], 4 | "rules": { 5 | "prettier/prettier": "error", 6 | "func-visibility": ["warn", { "ignoreConstructors": true }], 7 | "compiler-version": ["off"], 8 | "constructor-syntax": "warn", 9 | "quotes": ["error", "single"], 10 | "private-vars-leading-underscore": ["warn", { "strict": false }], 11 | "reason-string": ["off"], 12 | "not-rely-on-time": "off", 13 | "no-empty-blocks": "off" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.solhintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | contracts/mock 3 | typechained 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "solidity.compileUsingRemoteVersion": "v0.8.9+commit.e5eed63a", 3 | "files.exclude": {} 4 | } 5 | -------------------------------------------------------------------------------- /BUG_BOUNTY.md: -------------------------------------------------------------------------------- 1 | # Balmy Bug Bounty 2 | 3 | Balmy ImmuneFi bug bounty program is focused on the prevention of negative impacts to the Balmy ecosystem, which currently covers our smart contracts and integrations. As such, all bug disclosures must be done through ImmuneFi's platform. 4 | 5 | **Further details and bounty values can be found here**: https://immunefi.com/bounty/balmy/ 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DCA V2 - Periphery 2 | 3 | [![Lint](https://github.com/Balmy-protocol/dca-v2-periphery/actions/workflows/lint.yml/badge.svg)](https://github.com/Balmy-protocol/dca-v2-periphery/actions/workflows/lint.yml) 4 | [![Tests (unit, integration)](https://github.com/Balmy-protocol/dca-v2-periphery/actions/workflows/tests.yml/badge.svg)](https://github.com/Balmy-protocol/dca-v2-periphery/actions/workflows/tests.yml) 5 | [![npm version](https://img.shields.io/npm/v/@balmy/dca-v2-periphery/latest.svg)](https://www.npmjs.com/package/@balmy/dca-v2-periphery/v/latest) 6 | 7 | This repository contains the periphery smart contracts for the DCA V2 Protocol. 8 | 9 | ## 💰 Bug bounty 10 | 11 | This repository is subject to the DCA V2 bug bounty program, per the terms defined [here](./BUG_BOUNTY.md). 12 | 13 | ## 📖 Docs 14 | 15 | Check our docs at [docs.balmy.xyz](https://docs.balmy.xyz) 16 | 17 | ## 📦 NPM/YARN Package 18 | 19 | - NPM Installation 20 | 21 | ```bash 22 | npm install @balmy/dca-v2-periphery 23 | ``` 24 | 25 | - Yarn installation 26 | 27 | ```bash 28 | yarn add @balmy/dca-v2-periphery 29 | ``` 30 | 31 | ## 👨‍💻 Development environment 32 | 33 | - Copy environment file 34 | 35 | ```bash 36 | cp .env.example .env 37 | ``` 38 | 39 | - Fill environment file with your information 40 | 41 | ```bash 42 | nano .env 43 | ``` 44 | 45 | ## 🧪 Testing 46 | 47 | ### Unit 48 | 49 | ```bash 50 | yarn test:unit 51 | ``` 52 | 53 | Will run all tests under [test/unit](./test/unit) 54 | 55 | ### Integration 56 | 57 | You will need to set up the development environment first, please refer to the [development environment](#-development-environment) section. 58 | 59 | ```bash 60 | yarn test:integration 61 | ``` 62 | 63 | Will run all tests under [test/integration](./test/integration) 64 | 65 | ## 🚢 Deployment 66 | 67 | You will need to set up the development environment first, please refer to the [development environment](#-development-environment) section. 68 | 69 | ```bash 70 | yarn deploy --network [network] 71 | ``` 72 | 73 | The plugin `hardhat-deploy` is used to deploy contracts. 74 | 75 | ## Licensing 76 | 77 | The primary license for DCA V2 Periphery is the GNU General Public License v2.0 (`GPL-2.0-or-later`), see [`LICENSE`](./LICENSE). 78 | 79 | ### Exceptions 80 | 81 | - All files in `contracts/mocks` remain unlicensed. 82 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] }; 2 | -------------------------------------------------------------------------------- /contracts/DCAFeeManager/DCAFeeManager.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.8.22; 3 | 4 | import '@openzeppelin/contracts/access/AccessControl.sol'; 5 | import '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; 6 | import '@openzeppelin/contracts/utils/Multicall.sol'; 7 | import '@mean-finance/call-simulation/contracts/SimulationAdapter.sol'; 8 | import '../interfaces/IDCAFeeManager.sol'; 9 | import '../utils/SwapAdapter.sol'; 10 | 11 | contract DCAFeeManager is SwapAdapter, AccessControl, Multicall, IDCAFeeManager, SimulationAdapter { 12 | bytes32 public constant SUPER_ADMIN_ROLE = keccak256('SUPER_ADMIN_ROLE'); 13 | bytes32 public constant ADMIN_ROLE = keccak256('ADMIN_ROLE'); 14 | 15 | using SafeERC20 for IERC20; 16 | using Address for address payable; 17 | 18 | constructor(address _superAdmin, address[] memory _initialAdmins) SwapAdapter() { 19 | if (_superAdmin == address(0)) revert ZeroAddress(); 20 | // We are setting the super admin role as its own admin so we can transfer it 21 | _setRoleAdmin(SUPER_ADMIN_ROLE, SUPER_ADMIN_ROLE); 22 | _setRoleAdmin(ADMIN_ROLE, SUPER_ADMIN_ROLE); 23 | _grantRole(SUPER_ADMIN_ROLE, _superAdmin); 24 | for (uint256 i; i < _initialAdmins.length; i++) { 25 | _grantRole(ADMIN_ROLE, _initialAdmins[i]); 26 | } 27 | } 28 | 29 | receive() external payable {} 30 | 31 | /// @inheritdoc IDCAFeeManager 32 | function runSwapsAndTransferMany(RunSwapsAndTransferManyParams calldata _parameters) public payable onlyRole(ADMIN_ROLE) { 33 | // Approve whatever is necessary 34 | for (uint256 i = 0; i < _parameters.allowanceTargets.length; ++i) { 35 | AllowanceTarget memory _allowance = _parameters.allowanceTargets[i]; 36 | _maxApproveSpender(_allowance.token, _allowance.allowanceTarget); 37 | } 38 | 39 | // Execute swaps 40 | for (uint256 i = 0; i < _parameters.swaps.length; ++i) { 41 | SwapContext memory _context = _parameters.swapContext[i]; 42 | _executeSwap(_parameters.swappers[_context.swapperIndex], _parameters.swaps[i], _context.value); 43 | } 44 | 45 | // Transfer out whatever was left in the contract 46 | for (uint256 i = 0; i < _parameters.transferOutBalance.length; ++i) { 47 | TransferOutBalance memory _transferOutBalance = _parameters.transferOutBalance[i]; 48 | _sendBalanceOnContractToRecipient(_transferOutBalance.token, _transferOutBalance.recipient); 49 | } 50 | } 51 | 52 | /// @inheritdoc IDCAFeeManager 53 | function withdrawFromPlatformBalance( 54 | IDCAHub _hub, 55 | IDCAHub.AmountOfToken[] calldata _amountToWithdraw, 56 | address _recipient 57 | ) external onlyRole(ADMIN_ROLE) { 58 | _hub.withdrawFromPlatformBalance(_amountToWithdraw, _recipient); 59 | } 60 | 61 | /// @inheritdoc IDCAFeeManager 62 | function withdrawFromBalance(IDCAHub.AmountOfToken[] calldata _amountToWithdraw, address _recipient) external onlyRole(ADMIN_ROLE) { 63 | for (uint256 i = 0; i < _amountToWithdraw.length; ++i) { 64 | IDCAHub.AmountOfToken memory _amountOfToken = _amountToWithdraw[i]; 65 | if (_amountOfToken.amount == type(uint256).max) { 66 | _sendBalanceOnContractToRecipient(_amountOfToken.token, _recipient); 67 | } else { 68 | _sendToRecipient(_amountOfToken.token, _amountOfToken.amount, _recipient); 69 | } 70 | } 71 | } 72 | 73 | /// @inheritdoc IDCAFeeManager 74 | function revokeAllowances(RevokeAction[] calldata _revokeActions) external onlyRole(ADMIN_ROLE) { 75 | _revokeAllowances(_revokeActions); 76 | } 77 | 78 | /// @inheritdoc IDCAFeeManager 79 | function availableBalances(IDCAHub _hub, address[] calldata _tokens) external view returns (AvailableBalance[] memory _balances) { 80 | _balances = new AvailableBalance[](_tokens.length); 81 | for (uint256 i = 0; i < _tokens.length; i++) { 82 | address _token = _tokens[i]; 83 | _balances[i] = AvailableBalance({ 84 | token: _token, 85 | platformBalance: _hub.platformBalance(_token), 86 | feeManagerBalance: IERC20(_token).balanceOf(address(this)) 87 | }); 88 | } 89 | } 90 | 91 | function supportsInterface(bytes4 _interfaceId) public view virtual override(AccessControl, SimulationAdapter) returns (bool) { 92 | return SimulationAdapter.supportsInterface(_interfaceId) || AccessControl.supportsInterface(_interfaceId); 93 | } 94 | 95 | function getPositionKey(address _from, address _to) public pure returns (bytes32) { 96 | return keccak256(abi.encodePacked(_from, _to)); 97 | } 98 | 99 | /// @dev This version does not check the swapper registry at all 100 | function _maxApproveSpender(IERC20 _token, address _spender) internal { 101 | _token.forceApprove(_spender, type(uint256).max); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /contracts/DCAHubCompanion/DCAHubCompanion.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.8.22; 3 | 4 | import './DCAHubCompanionLibrariesHandler.sol'; 5 | import './DCAHubCompanionHubProxyHandler.sol'; 6 | import '../utils/BaseCompanion.sol'; 7 | 8 | contract DCAHubCompanion is DCAHubCompanionLibrariesHandler, DCAHubCompanionHubProxyHandler, BaseCompanion, IDCAHubCompanion { 9 | constructor( 10 | address _swapper, 11 | address _allowanceTarget, 12 | address _governor, 13 | IPermit2 _permit2 14 | ) BaseCompanion(_swapper, _allowanceTarget, _governor, _permit2) {} 15 | } 16 | -------------------------------------------------------------------------------- /contracts/DCAHubCompanion/DCAHubCompanionHubProxyHandler.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.8.22; 3 | 4 | import '../interfaces/IDCAHubCompanion.sol'; 5 | import '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; 6 | 7 | /// @dev All public functions are payable, so that they can be multicalled together with other payable functions when msg.value > 0 8 | abstract contract DCAHubCompanionHubProxyHandler is IDCAHubCompanionHubProxyHandler { 9 | using SafeERC20 for IERC20; 10 | 11 | /// @inheritdoc IDCAHubCompanionHubProxyHandler 12 | function permissionPermit( 13 | IDCAPermissionManager _permissionManager, 14 | IDCAPermissionManager.PermissionSet[] calldata _permissions, 15 | uint256 _tokenId, 16 | uint256 _deadline, 17 | uint8 _v, 18 | bytes32 _r, 19 | bytes32 _s 20 | ) external payable { 21 | _permissionManager.permissionPermit(_permissions, _tokenId, _deadline, _v, _r, _s); 22 | } 23 | 24 | /// @inheritdoc IDCAHubCompanionHubProxyHandler 25 | function multiPermissionPermit( 26 | IDCAPermissionManager _permissionManager, 27 | IDCAPermissionManager.PositionPermissions[] calldata _permissions, 28 | uint256 _deadline, 29 | uint8 _v, 30 | bytes32 _r, 31 | bytes32 _s 32 | ) external payable { 33 | _permissionManager.multiPermissionPermit(_permissions, _deadline, _v, _r, _s); 34 | } 35 | 36 | /// @inheritdoc IDCAHubCompanionHubProxyHandler 37 | function deposit( 38 | IDCAHub _hub, 39 | address _from, 40 | address _to, 41 | uint256 _amount, 42 | uint32 _amountOfSwaps, 43 | uint32 _swapInterval, 44 | address _owner, 45 | IDCAPermissionManager.PermissionSet[] calldata _permissions, 46 | bytes calldata _miscellaneous 47 | ) public payable virtual returns (uint256 _positionId) { 48 | _approveHub(address(_from), _hub, _amount); 49 | _positionId = _miscellaneous.length > 0 50 | ? _hub.deposit(_from, _to, _amount, _amountOfSwaps, _swapInterval, _owner, _permissions, _miscellaneous) 51 | : _hub.deposit(_from, _to, _amount, _amountOfSwaps, _swapInterval, _owner, _permissions); 52 | } 53 | 54 | /// @inheritdoc IDCAHubCompanionHubProxyHandler 55 | function depositWithBalanceOnContract( 56 | IDCAHub _hub, 57 | address _from, 58 | address _to, 59 | uint32 _amountOfSwaps, 60 | uint32 _swapInterval, 61 | address _owner, 62 | IDCAPermissionManager.PermissionSet[] calldata _permissions, 63 | bytes calldata _miscellaneous 64 | ) external payable returns (uint256 _positionId) { 65 | uint256 _amount = IERC20(_from).balanceOf(address(this)); 66 | return deposit(_hub, _from, _to, _amount, _amountOfSwaps, _swapInterval, _owner, _permissions, _miscellaneous); 67 | } 68 | 69 | /// @inheritdoc IDCAHubCompanionHubProxyHandler 70 | function withdrawSwapped( 71 | IDCAHub _hub, 72 | uint256 _positionId, 73 | address _recipient 74 | ) external payable verifyPermission(_hub, _positionId, IDCAPermissionManager.Permission.WITHDRAW) returns (uint256 _swapped) { 75 | _swapped = _hub.withdrawSwapped(_positionId, _recipient); 76 | } 77 | 78 | /// @inheritdoc IDCAHubCompanionHubProxyHandler 79 | function withdrawSwappedMany( 80 | IDCAHub _hub, 81 | IDCAHub.PositionSet[] calldata _positions, 82 | address _recipient 83 | ) external payable returns (uint256[] memory _withdrawn) { 84 | for (uint256 i = 0; i < _positions.length; ++i) { 85 | uint256[] memory _positionIds = _positions[i].positionIds; 86 | for (uint256 j = 0; j < _positionIds.length; ++j) { 87 | _checkPermissionOrFail(_hub, _positionIds[j], IDCAPermissionManager.Permission.WITHDRAW); 88 | } 89 | } 90 | _withdrawn = _hub.withdrawSwappedMany(_positions, _recipient); 91 | } 92 | 93 | /// @inheritdoc IDCAHubCompanionHubProxyHandler 94 | function increasePosition( 95 | IDCAHub _hub, 96 | uint256 _positionId, 97 | uint256 _amount, 98 | uint32 _newSwaps 99 | ) external payable verifyPermission(_hub, _positionId, IDCAPermissionManager.Permission.INCREASE) { 100 | IERC20Metadata _from = _hub.userPosition(_positionId).from; 101 | _approveHub(address(_from), _hub, _amount); 102 | _hub.increasePosition(_positionId, _amount, _newSwaps); 103 | } 104 | 105 | /// @inheritdoc IDCAHubCompanionHubProxyHandler 106 | function increasePositionWithBalanceOnContract( 107 | IDCAHub _hub, 108 | uint256 _positionId, 109 | uint32 _newSwaps 110 | ) external payable verifyPermission(_hub, _positionId, IDCAPermissionManager.Permission.INCREASE) { 111 | IERC20Metadata _from = _hub.userPosition(_positionId).from; 112 | uint256 _amount = _from.balanceOf(address(this)); 113 | _approveHub(address(_from), _hub, _amount); 114 | _hub.increasePosition(_positionId, _amount, _newSwaps); 115 | } 116 | 117 | /// @inheritdoc IDCAHubCompanionHubProxyHandler 118 | function reducePosition( 119 | IDCAHub _hub, 120 | uint256 _positionId, 121 | uint256 _amount, 122 | uint32 _newSwaps, 123 | address _recipient 124 | ) external payable verifyPermission(_hub, _positionId, IDCAPermissionManager.Permission.REDUCE) { 125 | _hub.reducePosition(_positionId, _amount, _newSwaps, _recipient); 126 | } 127 | 128 | /// @inheritdoc IDCAHubCompanionHubProxyHandler 129 | function terminate( 130 | IDCAHub _hub, 131 | uint256 _positionId, 132 | address _recipientUnswapped, 133 | address _recipientSwapped 134 | ) 135 | external 136 | payable 137 | verifyPermission(_hub, _positionId, IDCAPermissionManager.Permission.TERMINATE) 138 | returns (uint256 _unswapped, uint256 _swapped) 139 | { 140 | (_unswapped, _swapped) = _hub.terminate(_positionId, _recipientUnswapped, _recipientSwapped); 141 | } 142 | 143 | function _approveHub( 144 | address _token, 145 | IDCAHub _hub, 146 | uint256 _amount 147 | ) internal { 148 | uint256 _allowance = IERC20(_token).allowance(address(this), address(_hub)); 149 | if (_allowance < _amount) { 150 | IERC20(_token).forceApprove(address(_hub), type(uint256).max); 151 | } 152 | } 153 | 154 | function _checkPermissionOrFail( 155 | IDCAHub _hub, 156 | uint256 _positionId, 157 | IDCAPermissionManager.Permission _permission 158 | ) internal view { 159 | if (!_hub.permissionManager().hasPermission(_positionId, msg.sender, _permission)) revert UnauthorizedCaller(); 160 | } 161 | 162 | modifier verifyPermission( 163 | IDCAHub _hub, 164 | uint256 _positionId, 165 | IDCAPermissionManager.Permission _permission 166 | ) { 167 | _checkPermissionOrFail(_hub, _positionId, _permission); 168 | _; 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /contracts/DCAHubCompanion/DCAHubCompanionLibrariesHandler.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.8.22; 3 | 4 | import '../libraries/InputBuilding.sol'; 5 | import '../libraries/SecondsUntilNextSwap.sol'; 6 | import '../interfaces/IDCAHubCompanion.sol'; 7 | 8 | abstract contract DCAHubCompanionLibrariesHandler is IDCAHubCompanionLibrariesHandler { 9 | /// @inheritdoc IDCAHubCompanionLibrariesHandler 10 | function getNextSwapInfo( 11 | IDCAHub _hub, 12 | Pair[] calldata _pairs, 13 | bool _calculatePrivilegedAvailability, 14 | bytes calldata _oracleData 15 | ) external view returns (IDCAHub.SwapInfo memory) { 16 | (address[] memory _tokens, IDCAHub.PairIndexes[] memory _indexes) = InputBuilding.buildGetNextSwapInfoInput(_pairs); 17 | return _hub.getNextSwapInfo(_tokens, _indexes, _calculatePrivilegedAvailability, _oracleData); 18 | } 19 | 20 | /// @inheritdoc IDCAHubCompanionLibrariesHandler 21 | function legacyGetNextSwapInfo(ILegacyDCAHub _hub, Pair[] calldata _pairs) external view returns (ILegacyDCAHub.SwapInfo memory) { 22 | (address[] memory _tokens, IDCAHub.PairIndexes[] memory _indexes) = InputBuilding.buildGetNextSwapInfoInput(_pairs); 23 | return _hub.getNextSwapInfo(_tokens, _indexes); 24 | } 25 | 26 | /// @inheritdoc IDCAHubCompanionLibrariesHandler 27 | function secondsUntilNextSwap( 28 | IDCAHub _hub, 29 | Pair[] calldata _pairs, 30 | bool _calculatePrivilegedAvailability 31 | ) external view returns (uint256[] memory) { 32 | return SecondsUntilNextSwap.secondsUntilNextSwap(_hub, _pairs, _calculatePrivilegedAvailability); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /contracts/DCAHubSwapper/CallerOnlyDCAHubSwapper.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.8.22; 3 | 4 | import '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; 5 | import '../interfaces/ICallerOnlyDCAHubSwapper.sol'; 6 | import './utils/DeadlineValidation.sol'; 7 | 8 | contract CallerOnlyDCAHubSwapper is DeadlineValidation, ICallerOnlyDCAHubSwapper { 9 | using SafeERC20 for IERC20; 10 | using Address for address; 11 | 12 | /// @notice Thrown when the caller tries to execute a swap, but they are not the privileged swapper 13 | error NotPrivilegedSwapper(); 14 | 15 | bytes32 public constant PRIVILEGED_SWAPPER_ROLE = keccak256('PRIVILEGED_SWAPPER_ROLE'); 16 | 17 | /// @notice Represents the lack of an executor. We are not using the zero address so that it's cheaper to modify 18 | address internal constant _NO_EXECUTOR = 0x000000000000000000000000000000000000dEaD; 19 | /// @notice The caller who initiated a swap execution 20 | address internal _swapExecutor = _NO_EXECUTOR; 21 | 22 | /// @inheritdoc ICallerOnlyDCAHubSwapper 23 | function swapForCaller(SwapForCallerParams calldata _parameters) 24 | external 25 | payable 26 | checkDeadline(_parameters.deadline) 27 | returns (IDCAHub.SwapInfo memory _swapInfo) 28 | { 29 | if (!_parameters.hub.hasRole(PRIVILEGED_SWAPPER_ROLE, msg.sender)) { 30 | revert NotPrivilegedSwapper(); 31 | } 32 | 33 | // Set the swap's executor 34 | _swapExecutor = msg.sender; 35 | 36 | // Execute swap 37 | _swapInfo = _parameters.hub.swap( 38 | _parameters.tokens, 39 | _parameters.pairsToSwap, 40 | _parameters.recipient, 41 | address(this), 42 | new uint256[](_parameters.tokens.length), 43 | '', 44 | _parameters.oracleData 45 | ); 46 | 47 | // Check that limits were met 48 | for (uint256 i = 0; i < _swapInfo.tokens.length; ++i) { 49 | IDCAHub.TokenInSwap memory _tokenInSwap = _swapInfo.tokens[i]; 50 | if (_tokenInSwap.reward < _parameters.minimumOutput[i]) { 51 | revert RewardNotEnough(); 52 | } else if (_tokenInSwap.toProvide > _parameters.maximumInput[i]) { 53 | revert ToProvideIsTooMuch(); 54 | } 55 | } 56 | 57 | // Clear the swap executor 58 | _swapExecutor = _NO_EXECUTOR; 59 | } 60 | 61 | // solhint-disable-next-line func-name-mixedcase 62 | function DCAHubSwapCall( 63 | address, 64 | IDCAHub.TokenInSwap[] calldata _tokens, 65 | uint256[] calldata, 66 | bytes calldata 67 | ) external { 68 | // Load to mem to avoid reading storage multiple times 69 | address _swapExecutorMem = _swapExecutor; 70 | for (uint256 i = 0; i < _tokens.length; ++i) { 71 | IDCAHub.TokenInSwap memory _token = _tokens[i]; 72 | if (_token.toProvide > 0) { 73 | // We assume that msg.sender is the DCAHub 74 | IERC20(_token.token).safeTransferFrom(_swapExecutorMem, msg.sender, _token.toProvide); 75 | } 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /contracts/DCAHubSwapper/ThirdPartyDCAHubSwapper.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.8.22; 3 | 4 | import '@openzeppelin/contracts/access/IAccessControl.sol'; 5 | import '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; 6 | import '@mean-finance/dca-v2-core/contracts/interfaces/IDCAHubSwapCallee.sol'; 7 | 8 | contract ThirdPartyDCAHubSwapper is IDCAHubSwapCallee { 9 | /// @notice A target we want to give allowance to 10 | struct Allowance { 11 | IERC20 token; 12 | address spender; 13 | } 14 | 15 | /// @notice The data necessary for a swap to be executed 16 | struct SwapExecution { 17 | address swapper; 18 | uint256 value; 19 | bytes swapData; 20 | } 21 | 22 | /// @notice Data used for the callback 23 | struct SwapWithDexesCallbackData { 24 | // If this is a test check 25 | bool isTest; 26 | // Timestamp where the tx is no longer valid 27 | uint256 deadline; 28 | // Targets to set allowance to 29 | Allowance[] allowanceTargets; 30 | // The different swaps to execute 31 | SwapExecution[] executions; 32 | // A list of tokens to check for unspent balance (should not be reward/to provide) 33 | IERC20[] intermediateTokensToCheck; 34 | // The address that will receive the unspent tokens 35 | address leftoverRecipient; 36 | } 37 | 38 | /// @notice An amount of certain token 39 | struct AmountOfToken { 40 | address token; 41 | uint256 amount; 42 | } 43 | 44 | /// @notice Thrown when the swap is a test. It reports the amount of tokens help by the swapper 45 | error SwapResults(AmountOfToken[] amounts); 46 | 47 | /// @notice Thrown when deadline has passed 48 | error TransactionTooOld(); 49 | 50 | /// @notice Thrown when the caller tries to execute a swap, but they are not the privileged swapper 51 | error NotPrivilegedSwapper(); 52 | 53 | using SafeERC20 for IERC20; 54 | using Address for address; 55 | 56 | bytes32 public constant PRIVILEGED_SWAPPER_ROLE = keccak256('PRIVILEGED_SWAPPER_ROLE'); 57 | 58 | // solhint-disable-next-line func-name-mixedcase 59 | function DCAHubSwapCall( 60 | address, 61 | IDCAHub.TokenInSwap[] calldata _tokens, 62 | uint256[] calldata, 63 | bytes calldata _data 64 | ) external { 65 | SwapWithDexesCallbackData memory _callbackData = abi.decode(_data, (SwapWithDexesCallbackData)); 66 | if (block.timestamp > _callbackData.deadline) revert TransactionTooOld(); 67 | _approveAllowances(_callbackData.allowanceTargets); 68 | _executeSwaps(_callbackData.executions); 69 | if (_callbackData.isTest) { 70 | _revertWithResults(_tokens, _callbackData.intermediateTokensToCheck); 71 | } 72 | _handleSwapTokens(_tokens, _callbackData.leftoverRecipient); 73 | _handleIntermediateTokens(_callbackData.intermediateTokensToCheck, _callbackData.leftoverRecipient); 74 | } 75 | 76 | /** 77 | * @notice Executed a DCA swap 78 | * @dev There are some cases where the oracles differ from what the markets can offer, so a swap can't be executed. But 79 | * it could happen that even if the amounts being swap are really big, the difference between oracle and market is 80 | * only a few dollars. In that case, it would be nice if someone could just pay for the difference. 81 | * The idea here is that instead of calling the hub directly, someone could call the swapper with some native token, 82 | * so that when the swapper gets called, they can use that native token balance as part of the swap, and cover the 83 | * difference 84 | */ 85 | function executeSwap( 86 | IDCAHubWithAccessControl _hub, 87 | address[] calldata _tokens, 88 | IDCAHub.PairIndexes[] calldata _pairsToSwap, 89 | uint256[] calldata _borrow, 90 | bytes calldata _callbackData, 91 | bytes calldata _oracleData 92 | ) external payable { 93 | if (!_hub.hasRole(PRIVILEGED_SWAPPER_ROLE, msg.sender)) { 94 | revert NotPrivilegedSwapper(); 95 | } 96 | _hub.swap(_tokens, _pairsToSwap, address(this), address(this), _borrow, _callbackData, _oracleData); 97 | } 98 | 99 | function _approveAllowances(Allowance[] memory _allowanceTargets) internal { 100 | for (uint256 i = 0; i < _allowanceTargets.length; ++i) { 101 | Allowance memory _target = _allowanceTargets[i]; 102 | _target.token.forceApprove(_target.spender, type(uint256).max); 103 | } 104 | } 105 | 106 | function _executeSwaps(SwapExecution[] memory _executions) internal { 107 | for (uint256 i = 0; i < _executions.length; ++i) { 108 | SwapExecution memory _execution = _executions[i]; 109 | _execution.swapper.functionCallWithValue(_execution.swapData, _execution.value); 110 | } 111 | } 112 | 113 | function _handleSwapTokens(IDCAHub.TokenInSwap[] calldata _tokens, address _leftoverRecipient) internal { 114 | for (uint256 i = 0; i < _tokens.length; ++i) { 115 | IERC20 _token = IERC20(_tokens[i].token); 116 | uint256 _balance = _token.balanceOf(address(this)); 117 | if (_balance > 0) { 118 | uint256 _toProvide = _tokens[i].toProvide; 119 | if (_toProvide > 0) { 120 | // Send everything to hub (we assume the hub is msg.sender) 121 | _token.safeTransfer(msg.sender, _balance); 122 | } else { 123 | // Send reward to the leftover recipient 124 | _token.safeTransfer(_leftoverRecipient, _balance); 125 | } 126 | } 127 | } 128 | } 129 | 130 | function _handleIntermediateTokens(IERC20[] memory _intermediateTokens, address _leftoverRecipient) internal { 131 | for (uint256 i = 0; i < _intermediateTokens.length; ++i) { 132 | uint256 _balance = _intermediateTokens[i].balanceOf(address(this)); 133 | if (_balance > 0) { 134 | _intermediateTokens[i].safeTransfer(_leftoverRecipient, _balance); 135 | } 136 | } 137 | } 138 | 139 | function _revertWithResults(IDCAHub.TokenInSwap[] calldata _tokens, IERC20[] memory _intermediateTokens) internal view { 140 | AmountOfToken[] memory _amounts = new AmountOfToken[](_tokens.length + _intermediateTokens.length); 141 | for (uint256 i; i < _tokens.length; i++) { 142 | address _token = _tokens[i].token; 143 | _amounts[i] = AmountOfToken({token: _token, amount: IERC20(_token).balanceOf(address(this))}); 144 | } 145 | for (uint256 i; i < _intermediateTokens.length; i++) { 146 | _amounts[i + _tokens.length] = AmountOfToken({ 147 | token: address(_intermediateTokens[i]), 148 | amount: _intermediateTokens[i].balanceOf(address(this)) 149 | }); 150 | } 151 | revert SwapResults(_amounts); 152 | } 153 | } 154 | 155 | interface IDCAHubWithAccessControl is IDCAHub, IAccessControl {} 156 | -------------------------------------------------------------------------------- /contracts/DCAHubSwapper/utils/DeadlineValidation.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.8.22; 3 | 4 | abstract contract DeadlineValidation { 5 | modifier checkDeadline(uint256 deadline) { 6 | require(_blockTimestamp() <= deadline, 'Transaction too old'); 7 | _; 8 | } 9 | 10 | /// @dev Method that exists purely to be overridden for tests 11 | /// @return The current block timestamp 12 | function _blockTimestamp() internal view virtual returns (uint256) { 13 | return block.timestamp; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /contracts/DCAKeep3rJob/DCAKeep3rJob.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.8.22; 3 | 4 | import '@openzeppelin/contracts/access/AccessControl.sol'; 5 | import '@openzeppelin/contracts/utils/Address.sol'; 6 | import '@openzeppelin/contracts/utils/cryptography/ECDSA.sol'; 7 | import '@openzeppelin/contracts/utils/cryptography/EIP712.sol'; 8 | import '../interfaces/IDCAKeep3rJob.sol'; 9 | 10 | contract DCAKeep3rJob is AccessControl, EIP712, IDCAKeep3rJob { 11 | using Address for address; 12 | 13 | bytes32 public constant SUPER_ADMIN_ROLE = keccak256('SUPER_ADMIN_ROLE'); 14 | bytes32 public constant CAN_SIGN_ROLE = keccak256('CAN_SIGN_ROLE'); 15 | bytes32 public constant WORK_TYPEHASH = keccak256('Work(address swapper,bytes data,uint256 nonce)'); 16 | 17 | /// @inheritdoc IDCAKeep3rJob 18 | IKeep3r public immutable keep3r; 19 | /// @inheritdoc IDCAKeep3rJob 20 | SwapperAndNonce public swapperAndNonce; // Note: data grouped in struct to reduce SLOADs 21 | 22 | constructor( 23 | IKeep3r _keep3r, 24 | address _swapper, 25 | address _superAdmin, 26 | address[] memory _initialCanSign 27 | ) EIP712('Mean Finance - DCA Keep3r Job', '1') { 28 | if (address(_keep3r) == address(0)) revert ZeroAddress(); 29 | if (_swapper == address(0)) revert ZeroAddress(); 30 | if (_superAdmin == address(0)) revert ZeroAddress(); 31 | 32 | keep3r = _keep3r; 33 | swapperAndNonce.swapper = _swapper; 34 | 35 | // We are setting the super admin role as its own admin so we can transfer it 36 | _setRoleAdmin(SUPER_ADMIN_ROLE, SUPER_ADMIN_ROLE); 37 | _setRoleAdmin(CAN_SIGN_ROLE, SUPER_ADMIN_ROLE); 38 | _grantRole(SUPER_ADMIN_ROLE, _superAdmin); 39 | 40 | for (uint256 i = 0; i < _initialCanSign.length; ++i) { 41 | _grantRole(CAN_SIGN_ROLE, _initialCanSign[i]); 42 | } 43 | } 44 | 45 | /// @inheritdoc IDCAKeep3rJob 46 | // solhint-disable-next-line func-name-mixedcase 47 | function DOMAIN_SEPARATOR() external view returns (bytes32) { 48 | return _domainSeparatorV4(); 49 | } 50 | 51 | /// @inheritdoc IDCAKeep3rJob 52 | function setSwapper(address _swapper) external onlyRole(SUPER_ADMIN_ROLE) { 53 | if (address(_swapper) == address(0)) revert ZeroAddress(); 54 | swapperAndNonce.swapper = _swapper; 55 | emit NewSwapperSet(_swapper); 56 | } 57 | 58 | /// @inheritdoc IDCAKeep3rJob 59 | function work( 60 | bytes calldata _call, 61 | uint8 _v, 62 | bytes32 _r, 63 | bytes32 _s 64 | ) external { 65 | if (!keep3r.isKeeper(msg.sender)) revert NotAKeeper(); 66 | 67 | SwapperAndNonce memory _swapperAndNonce = swapperAndNonce; 68 | bytes32 _structHash = keccak256(abi.encode(WORK_TYPEHASH, _swapperAndNonce.swapper, keccak256(_call), _swapperAndNonce.nonce)); 69 | bytes32 _hash = _hashTypedDataV4(_structHash); 70 | address _signer = ECDSA.recover(_hash, _v, _r, _s); 71 | if (!hasRole(CAN_SIGN_ROLE, _signer)) revert SignerCannotSignWork(); 72 | 73 | swapperAndNonce.nonce = _swapperAndNonce.nonce + 1; 74 | _swapperAndNonce.swapper.functionCall(_call); 75 | 76 | keep3r.worked(msg.sender); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /contracts/interfaces/ICallerOnlyDCAHubSwapper.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.8.7; 3 | 4 | import '@openzeppelin/contracts/access/IAccessControl.sol'; 5 | import '@mean-finance/dca-v2-core/contracts/interfaces/IDCAHub.sol'; 6 | import '@mean-finance/dca-v2-core/contracts/interfaces/IDCAHubSwapCallee.sol'; 7 | 8 | interface ICallerOnlyDCAHubSwapper is IDCAHubSwapCallee { 9 | /// @notice Parameters to execute a swap for caller 10 | struct SwapForCallerParams { 11 | // The address of the DCAHub 12 | IDCAHubWithAccessControl hub; 13 | // The tokens involved in the swap 14 | address[] tokens; 15 | // The pairs to swap 16 | IDCAHub.PairIndexes[] pairsToSwap; 17 | // Bytes to send to the oracle when executing a quote 18 | bytes oracleData; 19 | // The minimum amount of tokens to receive as part of the swap 20 | uint256[] minimumOutput; 21 | // The maximum amount of tokens to provide as part of the swap 22 | uint256[] maximumInput; 23 | // Address that will receive all the tokens from the swap 24 | address recipient; 25 | // Deadline when the swap becomes invalid 26 | uint256 deadline; 27 | } 28 | 29 | /// @notice Thrown when the reward is less that the specified minimum 30 | error RewardNotEnough(); 31 | 32 | /// @notice Thrown when the amount to provide is more than the specified maximum 33 | error ToProvideIsTooMuch(); 34 | 35 | /** 36 | * @notice Executes a swap for the caller, by sending them the reward, and taking from them the needed tokens 37 | * @dev Can only be called by user with appropriate role 38 | * Will revert: 39 | * - With RewardNotEnough if the minimum output is not met 40 | * - With ToProvideIsTooMuch if the hub swap requires more than the given maximum input 41 | * @return The information about the executed swap 42 | */ 43 | function swapForCaller(SwapForCallerParams calldata parameters) external payable returns (IDCAHub.SwapInfo memory); 44 | } 45 | 46 | interface IDCAHubWithAccessControl is IDCAHub, IAccessControl {} 47 | -------------------------------------------------------------------------------- /contracts/interfaces/IDCAFeeManager.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.8.7; 3 | 4 | import {IDCAHub, IERC20} from '@mean-finance/dca-v2-core/contracts/interfaces/IDCAHub.sol'; 5 | import {SwapAdapter} from '../utils/SwapAdapter.sol'; 6 | import {SwapContext} from '../utils/types/SwapContext.sol'; 7 | import {TransferOutBalance} from '../utils/types/TransferOutBalance.sol'; 8 | 9 | /** 10 | * @title DCA Fee Manager 11 | * @notice This contract will manage all platform fees. Since fees come in different tokens, this manager 12 | * will be in charge of taking them and converting them to different tokens, for example ETH/MATIC 13 | * or stablecoins. Allowed users will to withdraw fees as generated, or DCA them into tokens 14 | * of their choosing 15 | */ 16 | interface IDCAFeeManager { 17 | /// @notice The parameters to execute the call 18 | struct RunSwapsAndTransferManyParams { 19 | // The accounts that should be approved for spending 20 | AllowanceTarget[] allowanceTargets; 21 | // The different swappers involved in the swap 22 | address[] swappers; 23 | // The different swapps to execute 24 | bytes[] swaps; 25 | // Context necessary for the swap execution 26 | SwapContext[] swapContext; 27 | // Tokens to transfer after swaps have been executed 28 | TransferOutBalance[] transferOutBalance; 29 | } 30 | 31 | /// @notice An allowance to provide for the swaps to work 32 | struct AllowanceTarget { 33 | // The token that should be approved 34 | IERC20 token; 35 | // The spender 36 | address allowanceTarget; 37 | } 38 | 39 | /// @notice Represents how much is available for withdraw, for a specific token 40 | struct AvailableBalance { 41 | address token; 42 | uint256 platformBalance; 43 | uint256 feeManagerBalance; 44 | } 45 | 46 | /// @notice Thrown when one of the parameters is a zero address 47 | error ZeroAddress(); 48 | 49 | /** 50 | * @notice Executes multiple swaps 51 | * @dev Can only be executed by admins 52 | * @param parameters The parameters for the swap 53 | */ 54 | function runSwapsAndTransferMany(RunSwapsAndTransferManyParams calldata parameters) external payable; 55 | 56 | /** 57 | * @notice Withdraws tokens from the platform balance, and sends them to the given recipient 58 | * @dev Can only be executed by admins 59 | * @param hub The address of the DCA Hub 60 | * @param amountToWithdraw The tokens to withdraw, and their amounts 61 | * @param recipient The address of the recipient 62 | */ 63 | function withdrawFromPlatformBalance( 64 | IDCAHub hub, 65 | IDCAHub.AmountOfToken[] calldata amountToWithdraw, 66 | address recipient 67 | ) external; 68 | 69 | /** 70 | * @notice Withdraws tokens from the contract's balance, and sends them to the given recipient 71 | * @dev Can only be executed by admins 72 | * @param amountToWithdraw The tokens to withdraw, and their amounts 73 | * @param recipient The address of the recipient 74 | */ 75 | function withdrawFromBalance(IDCAHub.AmountOfToken[] calldata amountToWithdraw, address recipient) external; 76 | 77 | /** 78 | * @notice Revokes ERC20 allowances for the given spenders 79 | * @dev Can only be executed by admins 80 | * @param revokeActions The spenders and tokens to revoke 81 | */ 82 | function revokeAllowances(SwapAdapter.RevokeAction[] calldata revokeActions) external; 83 | 84 | /** 85 | * @notice Returns how much is available for withdraw, for the given tokens 86 | * @dev This is meant for off-chan purposes 87 | * @param hub The address of the DCA Hub 88 | * @param tokens The tokens to check the balance for 89 | * @return How much is available for withdraw, for the given tokens 90 | */ 91 | function availableBalances(IDCAHub hub, address[] calldata tokens) external view returns (AvailableBalance[] memory); 92 | } 93 | -------------------------------------------------------------------------------- /contracts/interfaces/IDCAKeep3rJob.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.8.7; 3 | 4 | import 'keep3r-v2/solidity/interfaces/IKeep3r.sol'; 5 | 6 | interface IDCAKeep3rJob { 7 | /// @notice A struct that contains the swapper and nonce to use 8 | struct SwapperAndNonce { 9 | address swapper; 10 | uint96 nonce; 11 | } 12 | 13 | /// @notice Thrown when one of the parameters is a zero address 14 | error ZeroAddress(); 15 | 16 | /// @notice Thrown when a user tries to execute work but the signature is invalid 17 | error SignerCannotSignWork(); 18 | 19 | /// @notice Thrown when a non keep3r address tries to execute work 20 | error NotAKeeper(); 21 | 22 | /** 23 | * @notice Emitted when a new swapper is set 24 | * @param newSwapper The new swapper 25 | */ 26 | event NewSwapperSet(address newSwapper); 27 | 28 | /** 29 | * @notice The domain separator used for the work signature 30 | * @return The domain separator used for the work signature 31 | */ 32 | // solhint-disable-next-line func-name-mixedcase 33 | function DOMAIN_SEPARATOR() external view returns (bytes32); 34 | 35 | /** 36 | * @notice Returns the swapper address 37 | * @return swapper The swapper's address 38 | * @return nonce The next nonce to use 39 | */ 40 | function swapperAndNonce() external returns (address swapper, uint96 nonce); 41 | 42 | /** 43 | * @notice Returns the Keep3r address 44 | * @return The Keep3r address address 45 | */ 46 | function keep3r() external returns (IKeep3r); 47 | 48 | /** 49 | * @notice Sets a new swapper address 50 | * @dev Will revert with ZeroAddress if the zero address is passed 51 | * Can only be called by an admin 52 | * @param swapper The new swapper address 53 | */ 54 | function setSwapper(address swapper) external; 55 | 56 | /** 57 | * @notice Takes an encoded call and executes it against the swapper 58 | * @dev Will revert with: 59 | * - NotAKeeper if the caller is not a keep3r 60 | * - SignerCannotSignWork if the address who signed the message cannot sign work 61 | * @param call The call to execut against the swapper 62 | * @param v Must produce valid secp256k1 signature from the holder along with `r` and `s` 63 | * @param r Must produce valid secp256k1 signature from the holder along with `v` and `s` 64 | * @param s Must produce valid secp256k1 signature from the holder along with `r` and `v` 65 | */ 66 | function work( 67 | bytes calldata call, 68 | uint8 v, 69 | bytes32 r, 70 | bytes32 s 71 | ) external; 72 | } 73 | -------------------------------------------------------------------------------- /contracts/interfaces/ILegacyDCAHub.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.8.7; 3 | 4 | import '@mean-finance/dca-v2-core/contracts/interfaces/IDCAHub.sol'; 5 | 6 | interface ILegacyDCAHub { 7 | /// @notice Information about a swap 8 | struct SwapInfo { 9 | // The tokens involved in the swap 10 | TokenInSwap[] tokens; 11 | // The pairs involved in the swap 12 | PairInSwap[] pairs; 13 | } 14 | 15 | /// @notice Information about a token's role in a swap 16 | struct TokenInSwap { 17 | // The token's address 18 | address token; 19 | // How much will be given of this token as a reward 20 | uint256 reward; 21 | // How much of this token needs to be provided by swapper 22 | uint256 toProvide; 23 | // How much of this token will be paid to the platform 24 | uint256 platformFee; 25 | } 26 | 27 | /// @notice Information about a pair in a swap 28 | struct PairInSwap { 29 | // The address of one of the tokens 30 | address tokenA; 31 | // The address of the other token 32 | address tokenB; 33 | // How much is 1 unit of token A when converted to B 34 | uint256 ratioAToB; 35 | // How much is 1 unit of token B when converted to A 36 | uint256 ratioBToA; 37 | // The swap intervals involved in the swap, represented as a byte 38 | bytes1 intervalsInSwap; 39 | } 40 | 41 | /** 42 | * @notice Returns all information related to the next swap 43 | * @dev Will revert with: 44 | * - With InvalidTokens if tokens are not sorted, or if there are duplicates 45 | * - With InvalidPairs if pairs are not sorted (first by indexTokenA and then indexTokenB), or if indexTokenA >= indexTokenB for any pair 46 | * @param tokens The tokens involved in the next swap 47 | * @param pairs The pairs that you want to swap. Each element of the list points to the index of the token in the tokens array 48 | * @return swapInformation The information about the next swap 49 | */ 50 | function getNextSwapInfo(address[] calldata tokens, IDCAHub.PairIndexes[] calldata pairs) 51 | external 52 | view 53 | returns (SwapInfo memory swapInformation); 54 | 55 | /** 56 | * @notice Executes a flash swap 57 | * @dev Will revert with: 58 | * - With InvalidTokens if tokens are not sorted, or if there are duplicates 59 | * - With InvalidPairs if pairs are not sorted (first by indexTokenA and then indexTokenB), or if indexTokenA >= indexTokenB for any pair 60 | * - With Paused if swaps are paused by protocol 61 | * - With NoSwapsToExecute if there are no swaps to execute for the given pairs 62 | * - With LiquidityNotReturned if the required tokens were not back during the callback 63 | * @param tokens The tokens involved in the next swap 64 | * @param pairsToSwap The pairs that you want to swap. Each element of the list points to the index of the token in the tokens array 65 | * @param rewardRecipient The address to send the reward to 66 | * @param callbackHandler Address to call for callback (and send the borrowed tokens to) 67 | * @param borrow How much to borrow of each of the tokens in tokens. The amount must match the position of the token in the tokens array 68 | * @param callbackData Bytes to send to the caller during the callback 69 | * @return Information about the executed swap 70 | */ 71 | function swap( 72 | address[] calldata tokens, 73 | IDCAHub.PairIndexes[] calldata pairsToSwap, 74 | address rewardRecipient, 75 | address callbackHandler, 76 | uint256[] calldata borrow, 77 | bytes calldata callbackData 78 | ) external returns (SwapInfo memory); 79 | } 80 | -------------------------------------------------------------------------------- /contracts/interfaces/ISharedTypes.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.8.7; 3 | 4 | /// @notice A pair of tokens 5 | struct Pair { 6 | address tokenA; 7 | address tokenB; 8 | } 9 | -------------------------------------------------------------------------------- /contracts/interfaces/external/IPermit2.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.8.0; 3 | 4 | // Minimal Permit2 interface, derived from 5 | // https://github.com/Uniswap/permit2/blob/main/src/interfaces/ISignatureTransfer.sol 6 | interface IPermit2 { 7 | struct TokenPermissions { 8 | address token; 9 | uint256 amount; 10 | } 11 | 12 | struct PermitTransferFrom { 13 | TokenPermissions permitted; 14 | uint256 nonce; 15 | uint256 deadline; 16 | } 17 | 18 | struct PermitBatchTransferFrom { 19 | TokenPermissions[] permitted; 20 | uint256 nonce; 21 | uint256 deadline; 22 | } 23 | 24 | struct SignatureTransferDetails { 25 | address to; 26 | uint256 requestedAmount; 27 | } 28 | 29 | // solhint-disable-next-line func-name-mixedcase 30 | function DOMAIN_SEPARATOR() external view returns (bytes32); 31 | 32 | function permitTransferFrom( 33 | PermitTransferFrom calldata permit, 34 | SignatureTransferDetails calldata transferDetails, 35 | address owner, 36 | bytes calldata signature 37 | ) external; 38 | 39 | function permitTransferFrom( 40 | PermitBatchTransferFrom memory permit, 41 | SignatureTransferDetails[] calldata transferDetails, 42 | address owner, 43 | bytes calldata signature 44 | ) external; 45 | } 46 | -------------------------------------------------------------------------------- /contracts/libraries/InputBuilding.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.8.7; 3 | 4 | import '@mean-finance/dca-v2-core/contracts/interfaces/IDCAHub.sol'; 5 | import '../interfaces/ISharedTypes.sol'; 6 | 7 | /// @title Input Building Library 8 | /// @notice Provides functions to build input for swap related actions 9 | /// @dev Please note that these functions are very expensive. Ideally, these would be used for off-chain purposes 10 | library InputBuilding { 11 | /// @notice Takes a list of pairs and returns the input necessary to check the next swap 12 | /// @dev Even though this function allows it, the DCAHub will fail if duplicated pairs are used 13 | /// @return _tokens A sorted list of all the tokens involved in the swap 14 | /// @return _pairsToSwap A sorted list of indexes that represent the pairs involved in the swap 15 | function buildGetNextSwapInfoInput(Pair[] calldata _pairs) 16 | internal 17 | pure 18 | returns (address[] memory _tokens, IDCAHub.PairIndexes[] memory _pairsToSwap) 19 | { 20 | (_tokens, _pairsToSwap, ) = buildSwapInput(_pairs, new IDCAHub.AmountOfToken[](0)); 21 | } 22 | 23 | /// @notice Takes a list of pairs and a list of tokens to borrow and returns the input necessary to execute a swap 24 | /// @dev Even though this function allows it, the DCAHub will fail if duplicated pairs are used 25 | /// @return _tokens A sorted list of all the tokens involved in the swap 26 | /// @return _pairsToSwap A sorted list of indexes that represent the pairs involved in the swap 27 | /// @return _borrow A list of amounts to borrow, based on the sorted token list 28 | function buildSwapInput(Pair[] calldata _pairs, IDCAHub.AmountOfToken[] memory _toBorrow) 29 | internal 30 | pure 31 | returns ( 32 | address[] memory _tokens, 33 | IDCAHub.PairIndexes[] memory _pairsToSwap, 34 | uint256[] memory _borrow 35 | ) 36 | { 37 | _tokens = _calculateUniqueTokens(_pairs, _toBorrow); 38 | _pairsToSwap = _calculatePairIndexes(_pairs, _tokens); 39 | _borrow = _calculateTokensToBorrow(_toBorrow, _tokens); 40 | } 41 | 42 | /// @dev Given a list of token pairs and tokens to borrow, returns a list of all the tokens involved, sorted 43 | function _calculateUniqueTokens(Pair[] memory _pairs, IDCAHub.AmountOfToken[] memory _toBorrow) 44 | private 45 | pure 46 | returns (address[] memory _tokens) 47 | { 48 | uint256 _uniqueTokens; 49 | address[] memory _tokensPlaceholder = new address[](_pairs.length * 2 + _toBorrow.length); 50 | 51 | // Load tokens in pairs onto placeholder 52 | for (uint256 i; i < _pairs.length; i++) { 53 | bool _foundA = false; 54 | bool _foundB = false; 55 | for (uint256 j; j < _uniqueTokens && !(_foundA && _foundB); j++) { 56 | if (!_foundA && _tokensPlaceholder[j] == _pairs[i].tokenA) _foundA = true; 57 | if (!_foundB && _tokensPlaceholder[j] == _pairs[i].tokenB) _foundB = true; 58 | } 59 | 60 | if (!_foundA) _tokensPlaceholder[_uniqueTokens++] = _pairs[i].tokenA; 61 | if (!_foundB) _tokensPlaceholder[_uniqueTokens++] = _pairs[i].tokenB; 62 | } 63 | 64 | // Load tokens to borrow onto placeholder 65 | for (uint256 i; i < _toBorrow.length; i++) { 66 | bool _found = false; 67 | for (uint256 j; j < _uniqueTokens && !_found; j++) { 68 | if (_tokensPlaceholder[j] == _toBorrow[i].token) _found = true; 69 | } 70 | if (!_found) _tokensPlaceholder[_uniqueTokens++] = _toBorrow[i].token; 71 | } 72 | 73 | // Load sorted into new array 74 | _tokens = new address[](_uniqueTokens); 75 | for (uint256 i; i < _uniqueTokens; i++) { 76 | address _token = _tokensPlaceholder[i]; 77 | 78 | // Find index where the token should be 79 | uint256 _tokenIndex; 80 | while (_tokens[_tokenIndex] < _token && _tokens[_tokenIndex] != address(0)) _tokenIndex++; 81 | 82 | // Move everything one place back 83 | for (uint256 j = i; j > _tokenIndex; j--) { 84 | _tokens[j] = _tokens[j - 1]; 85 | } 86 | 87 | // Set token on the correct index 88 | _tokens[_tokenIndex] = _token; 89 | } 90 | } 91 | 92 | /// @dev Given a list of pairs, and a list of sorted tokens, it translates the first list into indexes of the second list. This list of indexes will 93 | /// be sorted. For example, if pairs are [{ tokenA, tokenB }, { tokenC, tokenB }] and tokens are: [ tokenA, tokenB, tokenC ], the following is returned 94 | /// [ { 0, 1 }, { 1, 1 }, { 1, 2 } ] 95 | function _calculatePairIndexes(Pair[] calldata _pairs, address[] memory _tokens) 96 | private 97 | pure 98 | returns (IDCAHub.PairIndexes[] memory _pairIndexes) 99 | { 100 | _pairIndexes = new IDCAHub.PairIndexes[](_pairs.length); 101 | uint256 _count; 102 | 103 | for (uint8 i; i < _tokens.length; i++) { 104 | for (uint8 j = i + 1; j < _tokens.length; j++) { 105 | for (uint256 k; k < _pairs.length; k++) { 106 | if ( 107 | (_tokens[i] == _pairs[k].tokenA && _tokens[j] == _pairs[k].tokenB) || 108 | (_tokens[i] == _pairs[k].tokenB && _tokens[j] == _pairs[k].tokenA) 109 | ) { 110 | _pairIndexes[_count++] = IDCAHubSwapHandler.PairIndexes({indexTokenA: i, indexTokenB: j}); 111 | } 112 | } 113 | } 114 | } 115 | } 116 | 117 | /// @dev Given a list of tokens to borrow and a list of sorted tokens, it translated the first list into a list of amounts, sorted by the indexed of 118 | /// the seconds list. For example, if `toBorrow` are [{ tokenA, 100 }, { tokenC, 200 }, { tokenB, 500 }] and tokens are [ tokenA, tokenB, tokenC], the 119 | /// following is returned [100, 500, 200] 120 | function _calculateTokensToBorrow(IDCAHub.AmountOfToken[] memory _toBorrow, address[] memory _tokens) 121 | private 122 | pure 123 | returns (uint256[] memory _borrow) 124 | { 125 | _borrow = new uint256[](_tokens.length); 126 | 127 | for (uint256 i; i < _toBorrow.length; i++) { 128 | uint256 j; 129 | while (_tokens[j] != _toBorrow[i].token) j++; 130 | _borrow[j] = _toBorrow[i].amount; 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /contracts/libraries/ModifyPositionWithRate.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.8.7; 3 | 4 | import '@mean-finance/dca-v2-core/contracts/interfaces/IDCAHub.sol'; 5 | 6 | /// @title Modify Position With Rate Library 7 | /// @notice Provides functions modify a position by using rate/amount of swaps 8 | library ModifyPositionWithRate { 9 | /// @notice Modifies the rate of a position. Could request more funds or return deposited funds 10 | /// depending on whether the new rate is greater than the previous one. 11 | /// @param _hub The address of the DCA Hub 12 | /// @param _positionId The position's id 13 | /// @param _newRate The new rate to set 14 | function modifyRate( 15 | IDCAHub _hub, 16 | uint256 _positionId, 17 | uint120 _newRate 18 | ) internal { 19 | IDCAHub.UserPosition memory _position = _hub.userPosition(_positionId); 20 | if (_newRate != _position.rate) { 21 | _modify(_hub, _positionId, _position, _newRate, _position.swapsLeft); 22 | } 23 | } 24 | 25 | /// @notice Modifies the amount of swaps of a position. Could request more funds or return 26 | /// deposited funds depending on whether the new amount of swaps is greater than the swaps left. 27 | /// @param _hub The address of the DCA Hub 28 | /// @param _positionId The position's id 29 | /// @param _newSwaps The new amount of swaps 30 | function modifySwaps( 31 | IDCAHub _hub, 32 | uint256 _positionId, 33 | uint32 _newSwaps 34 | ) internal { 35 | IDCAHub.UserPosition memory _position = _hub.userPosition(_positionId); 36 | if (_newSwaps != _position.swapsLeft) { 37 | _modify(_hub, _positionId, _position, _position.rate, _newSwaps); 38 | } 39 | } 40 | 41 | /// @notice Modifies both the rate and amount of swaps of a position. Could request more funds or return 42 | /// deposited funds depending on whether the new parameters require more or less than the current unswapped funds. 43 | /// @param _hub The address of the DCA Hub 44 | /// @param _positionId The position's id 45 | /// @param _newRate The new rate to set 46 | /// @param _newSwaps The new amount of swaps 47 | function modifyRateAndSwaps( 48 | IDCAHub _hub, 49 | uint256 _positionId, 50 | uint120 _newRate, 51 | uint32 _newSwaps 52 | ) internal { 53 | IDCAHub.UserPosition memory _position = _hub.userPosition(_positionId); 54 | if (_position.rate != _newRate && _newSwaps != _position.swapsLeft) { 55 | _modify(_hub, _positionId, _position, _newRate, _newSwaps); 56 | } 57 | } 58 | 59 | function _modify( 60 | IDCAHub _hub, 61 | uint256 _positionId, 62 | IDCAHub.UserPosition memory _position, 63 | uint120 _newRate, 64 | uint32 _newAmountOfSwaps 65 | ) private { 66 | uint256 _totalNecessary = uint256(_newRate) * _newAmountOfSwaps; 67 | if (_totalNecessary >= _position.remaining) { 68 | _hub.increasePosition(_positionId, _totalNecessary - _position.remaining, _newAmountOfSwaps); 69 | } else { 70 | _hub.reducePosition(_positionId, _position.remaining - _totalNecessary, _newAmountOfSwaps, msg.sender); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /contracts/libraries/Permit2Transfers.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.8.22; 3 | 4 | import {IPermit2} from '../interfaces/external/IPermit2.sol'; 5 | 6 | /** 7 | * @title Permit2 Transfers Library 8 | * @author Sam Bugs 9 | * @notice A small library to call Permit2's transfer from methods 10 | */ 11 | library Permit2Transfers { 12 | /** 13 | * @notice Executes a transfer from using Permit2 14 | * @param _permit2 The Permit2 contract 15 | * @param _token The token to transfer 16 | * @param _amount The amount to transfer 17 | * @param _nonce The owner's nonce 18 | * @param _deadline The signature's expiration deadline 19 | * @param _signature The signature that allows the transfer 20 | * @param _recipient The address that will receive the funds 21 | */ 22 | function takeFromCaller( 23 | IPermit2 _permit2, 24 | address _token, 25 | uint256 _amount, 26 | uint256 _nonce, 27 | uint256 _deadline, 28 | bytes calldata _signature, 29 | address _recipient 30 | ) internal { 31 | _permit2.permitTransferFrom( 32 | // The permit message. 33 | IPermit2.PermitTransferFrom({permitted: IPermit2.TokenPermissions({token: _token, amount: _amount}), nonce: _nonce, deadline: _deadline}), 34 | // The transfer recipient and amount. 35 | IPermit2.SignatureTransferDetails({to: _recipient, requestedAmount: _amount}), 36 | // The owner of the tokens, which must also be 37 | // the signer of the message, otherwise this call 38 | // will fail. 39 | msg.sender, 40 | // The packed signature that was the result of signing 41 | // the EIP712 hash of `permit`. 42 | _signature 43 | ); 44 | } 45 | 46 | /** 47 | * @notice Executes a batch transfer from using Permit2 48 | * @param _permit2 The Permit2 contract 49 | * @param _tokens The amount of tokens to transfer 50 | * @param _nonce The owner's nonce 51 | * @param _deadline The signature's expiration deadline 52 | * @param _signature The signature that allows the transfer 53 | * @param _recipient The address that will receive the funds 54 | */ 55 | function batchTakeFromCaller( 56 | IPermit2 _permit2, 57 | IPermit2.TokenPermissions[] calldata _tokens, 58 | uint256 _nonce, 59 | uint256 _deadline, 60 | bytes calldata _signature, 61 | address _recipient 62 | ) internal { 63 | if (_tokens.length > 0) { 64 | _permit2.permitTransferFrom( 65 | // The permit message. 66 | IPermit2.PermitBatchTransferFrom({permitted: _tokens, nonce: _nonce, deadline: _deadline}), 67 | // The transfer recipients and amounts. 68 | _buildTransferDetails(_tokens, _recipient), 69 | // The owner of the tokens, which must also be 70 | // the signer of the message, otherwise this call 71 | // will fail. 72 | msg.sender, 73 | // The packed signature that was the result of signing 74 | // the EIP712 hash of `permit`. 75 | _signature 76 | ); 77 | } 78 | } 79 | 80 | function _buildTransferDetails(IPermit2.TokenPermissions[] calldata _tokens, address _recipient) 81 | private 82 | pure 83 | returns (IPermit2.SignatureTransferDetails[] memory _details) 84 | { 85 | _details = new IPermit2.SignatureTransferDetails[](_tokens.length); 86 | for (uint256 i; i < _details.length; ++i) { 87 | _details[i] = IPermit2.SignatureTransferDetails({to: _recipient, requestedAmount: _tokens[i].amount}); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /contracts/libraries/SecondsUntilNextSwap.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.8.7; 3 | 4 | import '@mean-finance/dca-v2-core/contracts/interfaces/IDCAHub.sol'; 5 | import '@mean-finance/dca-v2-core/contracts/libraries/TokenSorting.sol'; 6 | import '@mean-finance/dca-v2-core/contracts/libraries/Intervals.sol'; 7 | import '../interfaces/ISharedTypes.sol'; 8 | 9 | /** 10 | * @title Seconds Until Next Swap Library 11 | * @notice Provides functions to calculate how long users have to wait until a pair's next swap is available 12 | */ 13 | library SecondsUntilNextSwap { 14 | /** 15 | * @notice Returns how many seconds left until the next swap is available for a specific pair 16 | * @dev _tokenA and _tokenB may be passed in either tokenA/tokenB or tokenB/tokenA order 17 | * @param _hub The address of the DCA Hub 18 | * @param _tokenA One of the pair's tokens 19 | * @param _tokenB The other of the pair's tokens 20 | * @param _calculatePrivilegedAvailability Some accounts get privileged availability and can execute swaps before others. This flag provides 21 | * the possibility to calculate the seconds until next swap for privileged and non-privileged accounts 22 | * @return The amount of seconds until next swap. Returns 0 if a swap can already be executed and max(uint256) if there is nothing to swap 23 | */ 24 | function secondsUntilNextSwap( 25 | IDCAHub _hub, 26 | address _tokenA, 27 | address _tokenB, 28 | bool _calculatePrivilegedAvailability 29 | ) internal view returns (uint256) { 30 | (address __tokenA, address __tokenB) = TokenSorting.sortTokens(_tokenA, _tokenB); 31 | bytes1 _activeIntervals = _hub.activeSwapIntervals(__tokenA, __tokenB); 32 | bytes1 _mask = 0x01; 33 | uint256 _smallerIntervalBlocking; 34 | while (_activeIntervals >= _mask && _mask > 0) { 35 | if (_activeIntervals & _mask == _mask) { 36 | (, uint224 _nextAmountToSwapAToB, uint32 _lastSwappedAt, uint224 _nextAmountToSwapBToA) = _hub.swapData(_tokenA, _tokenB, _mask); 37 | uint32 _swapInterval = Intervals.maskToInterval(_mask); 38 | uint256 _nextAvailable = ((_lastSwappedAt / _swapInterval) + 1) * _swapInterval; 39 | if (!_calculatePrivilegedAvailability) { 40 | // If the caller does not have privileges, then they will have to wait a little more to execute swaps 41 | _nextAvailable += _swapInterval / 3; 42 | } 43 | if (_nextAmountToSwapAToB > 0 || _nextAmountToSwapBToA > 0) { 44 | if (_nextAvailable <= block.timestamp) { 45 | return _smallerIntervalBlocking; 46 | } else { 47 | return _nextAvailable - block.timestamp; 48 | } 49 | } else if (_nextAvailable > block.timestamp) { 50 | _smallerIntervalBlocking = _smallerIntervalBlocking == 0 ? _nextAvailable - block.timestamp : _smallerIntervalBlocking; 51 | } 52 | } 53 | _mask <<= 1; 54 | } 55 | return type(uint256).max; 56 | } 57 | 58 | /** 59 | * @notice Returns how many seconds left until the next swap is available for a list of pairs 60 | * @dev Tokens in pairs may be passed in either tokenA/tokenB or tokenB/tokenA order 61 | * @param _hub The address of the DCA Hub 62 | * @param _pairs Pairs to check 63 | * @return _seconds The amount of seconds until next swap for each of the pairs 64 | * @param _calculatePrivilegedAvailability Some accounts get privileged availability and can execute swaps before others. This flag provides 65 | * the possibility to calculate the seconds until next swap for privileged and non-privileged accounts 66 | */ 67 | function secondsUntilNextSwap( 68 | IDCAHub _hub, 69 | Pair[] calldata _pairs, 70 | bool _calculatePrivilegedAvailability 71 | ) internal view returns (uint256[] memory _seconds) { 72 | _seconds = new uint256[](_pairs.length); 73 | for (uint256 i; i < _pairs.length; i++) { 74 | _seconds[i] = secondsUntilNextSwap(_hub, _pairs[i].tokenA, _pairs[i].tokenB, _calculatePrivilegedAvailability); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /contracts/mocks/DCAFeeManager/DCAFeeManager.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.22; 3 | 4 | import '../../DCAFeeManager/DCAFeeManager.sol'; 5 | 6 | contract DCAFeeManagerMock is DCAFeeManager { 7 | struct SendToRecipientCall { 8 | address token; 9 | uint256 amount; 10 | address recipient; 11 | } 12 | 13 | struct SendBalanceOnContractToRecipientCall { 14 | address token; 15 | address recipient; 16 | } 17 | 18 | SendToRecipientCall[] internal _sendToRecipientCalls; 19 | SendBalanceOnContractToRecipientCall[] internal _sendBalanceOnContractToRecipientCalls; 20 | RevokeAction[][] internal _revokeCalls; 21 | 22 | constructor(address _superAdmin, address[] memory _initialAdmins) DCAFeeManager(_superAdmin, _initialAdmins) {} 23 | 24 | function sendBalanceOnContractToRecipientCalls() external view returns (SendBalanceOnContractToRecipientCall[] memory) { 25 | return _sendBalanceOnContractToRecipientCalls; 26 | } 27 | 28 | function sendToRecipientCalls() external view returns (SendToRecipientCall[] memory) { 29 | return _sendToRecipientCalls; 30 | } 31 | 32 | function revokeAllowancesCalls() external view returns (RevokeAction[][] memory) { 33 | return _revokeCalls; 34 | } 35 | 36 | function _sendBalanceOnContractToRecipient(address _token, address _recipient) internal override { 37 | _sendBalanceOnContractToRecipientCalls.push(SendBalanceOnContractToRecipientCall(_token, _recipient)); 38 | } 39 | 40 | function _sendToRecipient( 41 | address _token, 42 | uint256 _amount, 43 | address _recipient 44 | ) internal override { 45 | _sendToRecipientCalls.push(SendToRecipientCall(_token, _amount, _recipient)); 46 | } 47 | 48 | function _revokeAllowances(RevokeAction[] calldata _revokeActions) internal override { 49 | _revokeCalls.push(); 50 | uint256 _currentCall = _revokeCalls.length - 1; 51 | for (uint256 i; i < _revokeActions.length; i++) { 52 | _revokeCalls[_currentCall].push(_revokeActions[i]); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /contracts/mocks/DCAHubCompanion/DCAHubCompanionHubProxyHandler.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.22; 3 | 4 | import '../../DCAHubCompanion/DCAHubCompanionHubProxyHandler.sol'; 5 | 6 | contract DCAHubCompanionHubProxyHandlerMock is DCAHubCompanionHubProxyHandler { 7 | struct DepositCall { 8 | IDCAHub hub; 9 | address from; 10 | address to; 11 | uint256 amount; 12 | uint32 amountOfSwaps; 13 | uint32 swapInterval; 14 | address owner; 15 | IDCAPermissionManager.PermissionSet[] permissions; 16 | bytes miscellaneous; 17 | } 18 | 19 | DepositCall[] private _depositCalls; 20 | 21 | function depositCalls() external view returns (DepositCall[] memory) { 22 | return _depositCalls; 23 | } 24 | 25 | function deposit( 26 | IDCAHub _hub, 27 | address _from, 28 | address _to, 29 | uint256 _amount, 30 | uint32 _amountOfSwaps, 31 | uint32 _swapInterval, 32 | address _owner, 33 | IDCAPermissionManager.PermissionSet[] calldata _permissions, 34 | bytes calldata _miscellaneous 35 | ) public payable override returns (uint256 _positionId) { 36 | _depositCalls.push(); 37 | DepositCall storage _ref = _depositCalls[_depositCalls.length - 1]; 38 | _ref.hub = _hub; 39 | _ref.from = _from; 40 | _ref.to = _to; 41 | _ref.amount = _amount; 42 | _ref.amountOfSwaps = _amountOfSwaps; 43 | _ref.swapInterval = _swapInterval; 44 | _ref.owner = _owner; 45 | _ref.miscellaneous = _miscellaneous; 46 | for (uint256 i = 0; i < _permissions.length; i++) { 47 | _ref.permissions.push(_permissions[i]); 48 | } 49 | return super.deposit(_hub, _from, _to, _amount, _amountOfSwaps, _swapInterval, _owner, _permissions, _miscellaneous); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /contracts/mocks/DCAHubSwapper/CallerOnlyDCAHubSwapper.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.22; 3 | 4 | import '../../DCAHubSwapper/CallerOnlyDCAHubSwapper.sol'; 5 | 6 | contract CallerOnlyDCAHubSwapperMock is CallerOnlyDCAHubSwapper { 7 | function isSwapExecutorEmpty() external view returns (bool) { 8 | return _swapExecutor == _NO_EXECUTOR; 9 | } 10 | 11 | function setSwapExecutor(address _newSwapExecutor) external { 12 | _swapExecutor = _newSwapExecutor; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /contracts/mocks/ISwapper.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | 3 | pragma solidity >=0.8.22; 4 | 5 | /// @notice Simply used for tests with Smock 6 | interface ISwapper { 7 | function swap( 8 | address tokenIn, 9 | uint256 amountIn, 10 | address tokenOut 11 | ) external; 12 | } 13 | -------------------------------------------------------------------------------- /contracts/mocks/LegacyDCASwapper.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.8.22; 3 | 4 | import '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; 5 | import '../interfaces/ILegacyDCAHub.sol'; 6 | 7 | contract LegacyDCASwapper { 8 | using SafeERC20 for IERC20; 9 | 10 | address internal _swapExecutor; 11 | 12 | function swapForCaller( 13 | ILegacyDCAHub _hub, 14 | address[] calldata _tokens, 15 | IDCAHub.PairIndexes[] calldata _pairsToSwap, 16 | address _recipient 17 | ) external { 18 | // Set the executor 19 | _swapExecutor = msg.sender; 20 | 21 | // Execute swap 22 | _hub.swap(_tokens, _pairsToSwap, _recipient, address(this), new uint256[](_tokens.length), ''); 23 | 24 | // Clear the swap executor 25 | _swapExecutor = address(0); 26 | } 27 | 28 | // solhint-disable-next-line func-name-mixedcase 29 | function DCAHubSwapCall( 30 | address, 31 | IDCAHub.TokenInSwap[] calldata _tokens, 32 | uint256[] calldata, 33 | bytes calldata 34 | ) external { 35 | address _swapExecutorMem = _swapExecutor; 36 | for (uint256 i = 0; i < _tokens.length; ++i) { 37 | IDCAHub.TokenInSwap memory _token = _tokens[i]; 38 | if (_token.toProvide > 0) { 39 | // We assume that msg.sender is the DCAHub 40 | IERC20(_token.token).safeTransferFrom(_swapExecutorMem, msg.sender, _token.toProvide); 41 | } 42 | } 43 | } 44 | 45 | function _handleSwapForCallerCallback(IDCAHub.TokenInSwap[] calldata _tokens) internal { 46 | // Load to mem to avoid reading storage multiple times 47 | address _swapExecutorMem = _swapExecutor; 48 | for (uint256 i = 0; i < _tokens.length; ++i) { 49 | IDCAHub.TokenInSwap memory _token = _tokens[i]; 50 | if (_token.toProvide > 0) { 51 | // We assume that msg.sender is the DCAHub 52 | IERC20(_token.token).safeTransferFrom(_swapExecutorMem, msg.sender, _token.toProvide); 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /contracts/mocks/libraries/InputBuilding.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.22; 3 | 4 | import '../../libraries/InputBuilding.sol'; 5 | 6 | contract InputBuildingMock { 7 | function buildGetNextSwapInfoInput(Pair[] calldata _pairs) 8 | external 9 | pure 10 | returns (address[] memory _tokens, IDCAHub.PairIndexes[] memory _pairsToSwap) 11 | { 12 | return InputBuilding.buildGetNextSwapInfoInput(_pairs); 13 | } 14 | 15 | function buildSwapInput(Pair[] calldata _pairs, IDCAHub.AmountOfToken[] memory _toBorrow) 16 | external 17 | pure 18 | returns ( 19 | address[] memory _tokens, 20 | IDCAHub.PairIndexes[] memory _pairsToSwap, 21 | uint256[] memory _borrow 22 | ) 23 | { 24 | return InputBuilding.buildSwapInput(_pairs, _toBorrow); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /contracts/mocks/libraries/ModifyPositionWithRate.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.22; 3 | 4 | import '../../libraries/ModifyPositionWithRate.sol'; 5 | 6 | contract ModifyPositionWithRateMock { 7 | function modifyRate( 8 | IDCAHub _hub, 9 | uint256 _positionId, 10 | uint120 _newRate 11 | ) external { 12 | ModifyPositionWithRate.modifyRate(_hub, _positionId, _newRate); 13 | } 14 | 15 | function modifySwaps( 16 | IDCAHub _hub, 17 | uint256 _positionId, 18 | uint32 _newSwaps 19 | ) external { 20 | ModifyPositionWithRate.modifySwaps(_hub, _positionId, _newSwaps); 21 | } 22 | 23 | function modifyRateAndSwaps( 24 | IDCAHub _hub, 25 | uint256 _positionId, 26 | uint120 _newRate, 27 | uint32 _newSwaps 28 | ) external { 29 | ModifyPositionWithRate.modifyRateAndSwaps(_hub, _positionId, _newRate, _newSwaps); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /contracts/mocks/libraries/SecondsUntilNextSwap.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.22; 3 | 4 | import '../../libraries/SecondsUntilNextSwap.sol'; 5 | 6 | contract SecondsUntilNextSwapMock { 7 | function secondsUntilNextSwap( 8 | IDCAHub _hub, 9 | address _tokenA, 10 | address _tokenB, 11 | bool _calculatePrivilegedAvailability 12 | ) external view returns (uint256) { 13 | return SecondsUntilNextSwap.secondsUntilNextSwap(_hub, _tokenA, _tokenB, _calculatePrivilegedAvailability); 14 | } 15 | 16 | function secondsUntilNextSwap( 17 | IDCAHub _hub, 18 | Pair[] calldata _pairs, 19 | bool _calculatePrivilegedAvailability 20 | ) external view returns (uint256[] memory) { 21 | return SecondsUntilNextSwap.secondsUntilNextSwap(_hub, _pairs, _calculatePrivilegedAvailability); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /contracts/mocks/utils/BaseCompanion.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.22; 3 | 4 | import '../../utils/BaseCompanion.sol'; 5 | 6 | contract BaseCompanionMock is BaseCompanion { 7 | constructor( 8 | address _swapper, 9 | address _allowanceTarget, 10 | address _governor, 11 | IPermit2 _permit2 12 | ) BaseCompanion(_swapper, _allowanceTarget, _governor, _permit2) {} 13 | 14 | struct TakeFromMsgSenderCall { 15 | IERC20 token; 16 | uint256 amount; 17 | } 18 | 19 | struct SendBalanceOnContractToRecipientCall { 20 | address token; 21 | address recipient; 22 | } 23 | 24 | struct SendToRecipientCall { 25 | address token; 26 | uint256 amount; 27 | address recipient; 28 | } 29 | 30 | TakeFromMsgSenderCall[] internal _takeFromMsgSenderCalls; 31 | SendBalanceOnContractToRecipientCall[] internal _sendBalanceOnContractToRecipientCalls; 32 | SendToRecipientCall[] internal _sendToRecipientCalls; 33 | 34 | function takeFromMsgSenderCalls() external view returns (TakeFromMsgSenderCall[] memory) { 35 | return _takeFromMsgSenderCalls; 36 | } 37 | 38 | function sendBalanceOnContractToRecipientCalls() external view returns (SendBalanceOnContractToRecipientCall[] memory) { 39 | return _sendBalanceOnContractToRecipientCalls; 40 | } 41 | 42 | function sendToRecipientCalls() external view returns (SendToRecipientCall[] memory) { 43 | return _sendToRecipientCalls; 44 | } 45 | 46 | function _takeFromMsgSender(IERC20 _token, uint256 _amount) internal override { 47 | _takeFromMsgSenderCalls.push(TakeFromMsgSenderCall(_token, _amount)); 48 | super._takeFromMsgSender(_token, _amount); 49 | } 50 | 51 | function _sendBalanceOnContractToRecipient(address _token, address _recipient) internal override { 52 | _sendBalanceOnContractToRecipientCalls.push(SendBalanceOnContractToRecipientCall(_token, _recipient)); 53 | super._sendBalanceOnContractToRecipient(_token, _recipient); 54 | } 55 | 56 | function _sendToRecipient( 57 | address _token, 58 | uint256 _amount, 59 | address _recipient 60 | ) internal override { 61 | _sendToRecipientCalls.push(SendToRecipientCall(_token, _amount, _recipient)); 62 | super._sendToRecipient(_token, _amount, _recipient); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /contracts/utils/BaseCompanion.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.8.22; 3 | 4 | import './SwapAdapter.sol'; 5 | import './PayableMulticall.sol'; 6 | import {SimulationAdapter} from '@mean-finance/call-simulation/contracts/SimulationAdapter.sol'; 7 | import {IPermit2} from '../interfaces/external/IPermit2.sol'; 8 | import {Permit2Transfers} from '../libraries/Permit2Transfers.sol'; 9 | import './Governable.sol'; 10 | 11 | /** 12 | * @notice This contract will work as base companion for all our contracts. It will extend the capabilities of our companion 13 | * contracts so that they can execute multicalls, swaps, revokes and more 14 | * @dev All public functions are payable, so that they can be multicalled together with other payable functions when msg.value > 0 15 | */ 16 | abstract contract BaseCompanion is SimulationAdapter, Governable, SwapAdapter, PayableMulticall { 17 | using Permit2Transfers for IPermit2; 18 | using SafeERC20 for IERC20; 19 | 20 | /** 21 | * @notice Returns the address of the Permit2 contract 22 | * @dev This value is constant and cannot change 23 | * @return The address of the Permit2 contract 24 | */ 25 | // solhint-disable-next-line var-name-mixedcase 26 | IPermit2 public immutable PERMIT2; 27 | 28 | /// @notice The address of the swapper 29 | address public swapper; 30 | 31 | /// @notice The address of the allowance target 32 | address public allowanceTarget; 33 | 34 | constructor( 35 | address _swapper, 36 | address _allowanceTarget, 37 | address _governor, 38 | IPermit2 _permit2 39 | ) SwapAdapter() Governable(_governor) { 40 | swapper = _swapper; 41 | allowanceTarget = _allowanceTarget; 42 | PERMIT2 = _permit2; 43 | } 44 | 45 | receive() external payable {} 46 | 47 | /** 48 | * @notice Sends the specified amount of the given token to the recipient 49 | * @param _token The token to transfer 50 | * @param _amount The amount to transfer 51 | * @param _recipient The recipient of the token balance 52 | */ 53 | function sendToRecipient( 54 | address _token, 55 | uint256 _amount, 56 | address _recipient 57 | ) external payable { 58 | _sendToRecipient(_token, _amount, _recipient); 59 | } 60 | 61 | /** 62 | * @notice Takes the given amount of tokens from the caller and transfers it to this contract 63 | * @param _token The token to take 64 | * @param _amount The amount to take 65 | */ 66 | function takeFromCaller( 67 | IERC20 _token, 68 | uint256 _amount, 69 | address _recipient 70 | ) external payable { 71 | _token.safeTransferFrom(msg.sender, _recipient, _amount); 72 | } 73 | 74 | /** 75 | * @notice Executes a swap against the swapper 76 | * @param _allowanceToken The token to set allowance for (can be set to zero address to ignore) 77 | * @param _value The value to send to the swapper as part of the swap 78 | * @param _swapData The swap data 79 | * @param _tokenOut The token that will be bought as part of the swap 80 | */ 81 | function runSwap( 82 | address _allowanceToken, 83 | uint256 _value, 84 | bytes calldata _swapData, 85 | address _tokenOut 86 | ) external payable returns (uint256 _amountOut) { 87 | if (_allowanceToken != address(0)) { 88 | IERC20(_allowanceToken).forceApprove(allowanceTarget, type(uint256).max); 89 | } 90 | 91 | _executeSwap(swapper, _swapData, _value); 92 | 93 | _amountOut = _tokenOut == PROTOCOL_TOKEN ? address(this).balance : IERC20(_tokenOut).balanceOf(address(this)); 94 | } 95 | 96 | /** 97 | * @notice Takes the given amount of tokens from the caller with Permit2 and transfers it to this contract 98 | * @param _token The token to take 99 | * @param _amount The amount to take 100 | * @param _nonce The signed nonce 101 | * @param _deadline The signature's deadline 102 | * @param _signature The owner's signature 103 | * @param _recipient The address that will receive the funds 104 | */ 105 | function permitTakeFromCaller( 106 | address _token, 107 | uint256 _amount, 108 | uint256 _nonce, 109 | uint256 _deadline, 110 | bytes calldata _signature, 111 | address _recipient 112 | ) external payable { 113 | PERMIT2.takeFromCaller(_token, _amount, _nonce, _deadline, _signature, _recipient); 114 | } 115 | 116 | /** 117 | * @notice Takes the a batch of tokens from the caller with Permit2 and transfers it to this contract 118 | * @param _tokens The tokens to take 119 | * @param _nonce The signed nonce 120 | * @param _deadline The signature's deadline 121 | * @param _signature The owner's signature 122 | * @param _recipient The address that will receive the funds 123 | */ 124 | function batchPermitTakeFromCaller( 125 | IPermit2.TokenPermissions[] calldata _tokens, 126 | uint256 _nonce, 127 | uint256 _deadline, 128 | bytes calldata _signature, 129 | address _recipient 130 | ) external payable { 131 | PERMIT2.batchTakeFromCaller(_tokens, _nonce, _deadline, _signature, _recipient); 132 | } 133 | 134 | /** 135 | * @notice Checks if the contract has any balance of the given token, and if it does, 136 | * it sends it to the given recipient 137 | * @param _token The token to check 138 | * @param _recipient The recipient of the token balance 139 | */ 140 | function sendBalanceOnContractToRecipient(address _token, address _recipient) external payable { 141 | _sendBalanceOnContractToRecipient(_token, _recipient); 142 | } 143 | 144 | /** 145 | * @notice Sets a new swapper and allowance target 146 | * @param _newSwapper The address of the new swapper 147 | * @param _newAllowanceTarget The address of the new allowance target 148 | */ 149 | function setSwapper(address _newSwapper, address _newAllowanceTarget) external onlyGovernor { 150 | swapper = _newSwapper; 151 | allowanceTarget = _newAllowanceTarget; 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /contracts/utils/Governable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.8.7 <0.9.0; 3 | 4 | import './interfaces/IGovernable.sol'; 5 | 6 | /** 7 | * @notice This contract is meant to be used in other contracts. By using this contract, 8 | * a specific address will be given a "governor" role, which basically will be able to 9 | * control certains aspects of the contract. There are other contracts that do the same, 10 | * but this contract forces a new governor to accept the role before it's transferred. 11 | * This is a basically a safety measure to prevent losing access to the contract. 12 | */ 13 | abstract contract Governable is IGovernable { 14 | /// @inheritdoc IGovernable 15 | address public governor; 16 | 17 | /// @inheritdoc IGovernable 18 | address public pendingGovernor; 19 | 20 | constructor(address _governor) { 21 | if (_governor == address(0)) revert GovernorIsZeroAddress(); 22 | governor = _governor; 23 | } 24 | 25 | /// @inheritdoc IGovernable 26 | function isGovernor(address _account) public view returns (bool) { 27 | return _account == governor; 28 | } 29 | 30 | /// @inheritdoc IGovernable 31 | function isPendingGovernor(address _account) public view returns (bool) { 32 | return _account == pendingGovernor; 33 | } 34 | 35 | /// @inheritdoc IGovernable 36 | function setPendingGovernor(address _pendingGovernor) external onlyGovernor { 37 | pendingGovernor = _pendingGovernor; 38 | emit PendingGovernorSet(_pendingGovernor); 39 | } 40 | 41 | /// @inheritdoc IGovernable 42 | function acceptPendingGovernor() external onlyPendingGovernor { 43 | governor = pendingGovernor; 44 | pendingGovernor = address(0); 45 | emit PendingGovernorAccepted(); 46 | } 47 | 48 | modifier onlyGovernor() { 49 | if (!isGovernor(msg.sender)) revert OnlyGovernor(); 50 | _; 51 | } 52 | 53 | modifier onlyPendingGovernor() { 54 | if (!isPendingGovernor(msg.sender)) revert OnlyPendingGovernor(); 55 | _; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /contracts/utils/PayableMulticall.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | pragma solidity >=0.8.7 <0.9.0; 4 | 5 | import '@openzeppelin/contracts/utils/Address.sol'; 6 | 7 | /** 8 | * @dev Adding this contract will enable batching calls. This is basically the same as Open Zeppelin's 9 | * Multicall contract, but we have made it payable. It supports both payable and non payable 10 | * functions. However, if `msg.value` is not zero, then non payable functions cannot be called. 11 | * Any contract that uses this Multicall version should be very careful when using msg.value. 12 | * For more context, read: https://github.com/Uniswap/v3-periphery/issues/52 13 | */ 14 | abstract contract PayableMulticall { 15 | /** 16 | * @notice Receives and executes a batch of function calls on this contract. 17 | * @param _data A list of different function calls to execute 18 | * @return _results The result of executing each of those calls 19 | */ 20 | function multicall(bytes[] calldata _data) external payable returns (bytes[] memory _results) { 21 | _results = new bytes[](_data.length); 22 | for (uint256 i = 0; i < _data.length; ) { 23 | _results[i] = Address.functionDelegateCall(address(this), _data[i]); 24 | unchecked { 25 | i++; 26 | } 27 | } 28 | return _results; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /contracts/utils/SwapAdapter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.8.7 <0.9.0; 3 | 4 | import '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; 5 | import '@openzeppelin/contracts/utils/Address.sol'; 6 | 7 | abstract contract SwapAdapter { 8 | using SafeERC20 for IERC20; 9 | using Address for address; 10 | using Address for address payable; 11 | 12 | /// @notice Describes how the allowance should be revoked for the given spender 13 | struct RevokeAction { 14 | address spender; 15 | IERC20[] tokens; 16 | } 17 | 18 | address public constant PROTOCOL_TOKEN = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; 19 | 20 | /** 21 | * @notice Takes the given amount of tokens from the caller 22 | * @param _token The token to check 23 | * @param _amount The amount to take 24 | */ 25 | function _takeFromMsgSender(IERC20 _token, uint256 _amount) internal virtual { 26 | _token.safeTransferFrom(msg.sender, address(this), _amount); 27 | } 28 | 29 | /** 30 | * @notice Executes a swap for the given swapper 31 | * @param _swapper The actual swapper 32 | * @param _swapData The swap execution data 33 | */ 34 | function _executeSwap( 35 | address _swapper, 36 | bytes calldata _swapData, 37 | uint256 _value 38 | ) internal virtual { 39 | _swapper.functionCallWithValue(_swapData, _value); 40 | } 41 | 42 | /** 43 | * @notice Transfers the given amount of tokens from the contract to the recipient 44 | * @param _token The token to check 45 | * @param _amount The amount to send 46 | * @param _recipient The recipient 47 | */ 48 | function _sendToRecipient( 49 | address _token, 50 | uint256 _amount, 51 | address _recipient 52 | ) internal virtual { 53 | if (_recipient == address(0)) _recipient = msg.sender; 54 | if (_token == PROTOCOL_TOKEN) { 55 | payable(_recipient).sendValue(_amount); 56 | } else { 57 | IERC20(_token).safeTransfer(_recipient, _amount); 58 | } 59 | } 60 | 61 | /** 62 | * @notice Checks if the contract has any balance of the given token, and if it does, 63 | * it sends it to the given recipient 64 | * @param _token The token to check 65 | * @param _recipient The recipient of the token balance 66 | */ 67 | function _sendBalanceOnContractToRecipient(address _token, address _recipient) internal virtual { 68 | uint256 _balance = _token == PROTOCOL_TOKEN ? address(this).balance : IERC20(_token).balanceOf(address(this)); 69 | if (_balance > 0) { 70 | _sendToRecipient(_token, _balance, _recipient); 71 | } 72 | } 73 | 74 | /** 75 | * @notice Revokes ERC20 allowances for the given spenders 76 | * @dev If exposed, then it should be permissioned 77 | * @param _revokeActions The spenders and tokens to revoke 78 | */ 79 | function _revokeAllowances(RevokeAction[] calldata _revokeActions) internal virtual { 80 | for (uint256 i = 0; i < _revokeActions.length; ) { 81 | RevokeAction memory _action = _revokeActions[i]; 82 | for (uint256 j = 0; j < _action.tokens.length; ) { 83 | _action.tokens[j].forceApprove(_action.spender, 0); 84 | unchecked { 85 | j++; 86 | } 87 | } 88 | unchecked { 89 | i++; 90 | } 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /contracts/utils/interfaces/IGovernable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.8.7 <0.9.0; 3 | 4 | /** 5 | * @title A contract that manages a "governor" role 6 | */ 7 | interface IGovernable { 8 | /// @notice Thrown when trying to set the zero address as governor 9 | error GovernorIsZeroAddress(); 10 | 11 | /// @notice Thrown when trying to execute an action that only the governor an execute 12 | error OnlyGovernor(); 13 | 14 | /// @notice Thrown when trying to execute an action that only the pending governor an execute 15 | error OnlyPendingGovernor(); 16 | 17 | /** 18 | * @notice Emitted when a new pending governor is set 19 | * @param newPendingGovernor The new pending governor 20 | */ 21 | event PendingGovernorSet(address newPendingGovernor); 22 | 23 | /** 24 | * @notice Emitted when the pending governor accepts the role and becomes the governor 25 | */ 26 | event PendingGovernorAccepted(); 27 | 28 | /** 29 | * @notice Returns the address of the governor 30 | * @return The address of the governor 31 | */ 32 | function governor() external view returns (address); 33 | 34 | /** 35 | * @notice Returns the address of the pending governor 36 | * @return The address of the pending governor 37 | */ 38 | function pendingGovernor() external view returns (address); 39 | 40 | /** 41 | * @notice Returns whether the given account is the current governor 42 | * @param account The account to check 43 | * @return Whether it is the current governor or not 44 | */ 45 | function isGovernor(address account) external view returns (bool); 46 | 47 | /** 48 | * @notice Returns whether the given account is the pending governor 49 | * @param account The account to check 50 | * @return Whether it is the pending governor or not 51 | */ 52 | function isPendingGovernor(address account) external view returns (bool); 53 | 54 | /** 55 | * @notice Sets a new pending governor 56 | * @dev Only the current governor can execute this action 57 | * @param pendingGovernor The new pending governor 58 | */ 59 | function setPendingGovernor(address pendingGovernor) external; 60 | 61 | /** 62 | * @notice Sets the pending governor as the governor 63 | * @dev Only the pending governor can execute this action 64 | */ 65 | function acceptPendingGovernor() external; 66 | } 67 | -------------------------------------------------------------------------------- /contracts/utils/types/SwapContext.sol: -------------------------------------------------------------------------------- 1 | /// @notice Context necessary for the swap execution 2 | struct SwapContext { 3 | // The index of the swapper that should execute each swap. This might look strange but it's way cheaper than alternatives 4 | uint8 swapperIndex; 5 | // The ETH/MATIC/BNB to send as part of the swap 6 | uint256 value; 7 | } 8 | -------------------------------------------------------------------------------- /contracts/utils/types/TransferOutBalance.sol: -------------------------------------------------------------------------------- 1 | /// @notice A token that was left on the contract and should be transferred out 2 | struct TransferOutBalance { 3 | // The token to transfer 4 | address token; 5 | // The recipient of those tokens 6 | address recipient; 7 | } 8 | -------------------------------------------------------------------------------- /deploy/001_companion.ts: -------------------------------------------------------------------------------- 1 | import { HardhatRuntimeEnvironment } from 'hardhat/types'; 2 | import { DeployFunction } from '@0xged/hardhat-deploy/types'; 3 | import { bytecode } from '../artifacts/contracts/DCAHubCompanion/DCAHubCompanion.sol/DCAHubCompanion.json'; 4 | import { deployThroughDeterministicFactory } from '@mean-finance/deterministic-factory/utils/deployment'; 5 | 6 | const deployFunction: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { 7 | const { deployer, msig } = await hre.getNamedAccounts(); 8 | 9 | const permit2 = '0x000000000022d473030f116ddee9f6b43ac78ba3'; 10 | const swapper = '0xA70C8401C058B6198e1cb085091DE13498CEc0dC'; 11 | 12 | await deployThroughDeterministicFactory({ 13 | deployer, 14 | name: 'DCAHubCompanion', 15 | salt: 'MF-DCAV2-DCAHubCompanion-V5', 16 | contract: 'contracts/DCAHubCompanion/DCAHubCompanion.sol:DCAHubCompanion', 17 | bytecode, 18 | constructorArgs: { 19 | types: ['address', 'address', 'address', 'address'], 20 | values: [swapper, swapper, msig, permit2], 21 | }, 22 | log: !process.env.TEST, 23 | overrides: !!process.env.COVERAGE 24 | ? {} 25 | : { 26 | gasLimit: 6_000_000, 27 | }, 28 | }); 29 | }; 30 | 31 | deployFunction.tags = ['DCAHubCompanion']; 32 | export default deployFunction; 33 | -------------------------------------------------------------------------------- /deploy/002_caller_only_swapper.ts: -------------------------------------------------------------------------------- 1 | import { HardhatRuntimeEnvironment } from 'hardhat/types'; 2 | import { DeployFunction } from '@0xged/hardhat-deploy/types'; 3 | import { bytecode } from '../artifacts/contracts/DCAHubSwapper/CallerOnlyDCAHubSwapper.sol/CallerOnlyDCAHubSwapper.json'; 4 | import { deployThroughDeterministicFactory } from '@mean-finance/deterministic-factory/utils/deployment'; 5 | 6 | const deployFunction: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { 7 | const { deployer } = await hre.getNamedAccounts(); 8 | 9 | await deployThroughDeterministicFactory({ 10 | deployer, 11 | name: 'CallerOnlyDCAHubSwapper', 12 | salt: 'MF-DCAV2-CallerDCAHubSwapper-V2', 13 | contract: 'contracts/DCAHubSwapper/CallerOnlyDCAHubSwapper.sol:CallerOnlyDCAHubSwapper', 14 | bytecode, 15 | constructorArgs: { 16 | types: [], 17 | values: [], 18 | }, 19 | log: !process.env.TEST, 20 | overrides: !!process.env.COVERAGE 21 | ? {} 22 | : { 23 | gasLimit: 6_000_000, 24 | }, 25 | }); 26 | }; 27 | 28 | deployFunction.dependencies = []; 29 | deployFunction.tags = ['CallerOnlyDCAHubSwapper']; 30 | export default deployFunction; 31 | -------------------------------------------------------------------------------- /deploy/003_third_party_swapper.ts: -------------------------------------------------------------------------------- 1 | import { HardhatRuntimeEnvironment } from 'hardhat/types'; 2 | import { DeployFunction } from '@0xged/hardhat-deploy/types'; 3 | import { bytecode } from '../artifacts/contracts/DCAHubSwapper/ThirdPartyDCAHubSwapper.sol/ThirdPartyDCAHubSwapper.json'; 4 | import { deployThroughDeterministicFactory } from '@mean-finance/deterministic-factory/utils/deployment'; 5 | 6 | const deployFunction: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { 7 | const { deployer } = await hre.getNamedAccounts(); 8 | 9 | await deployThroughDeterministicFactory({ 10 | deployer, 11 | name: 'ThirdPartyDCAHubSwapper', 12 | salt: 'MF-DCAV2-3PartySwapper-V3', 13 | contract: 'contracts/DCAHubSwapper/ThirdPartyDCAHubSwapper.sol:ThirdPartyDCAHubSwapper', 14 | bytecode, 15 | constructorArgs: { 16 | types: [], 17 | values: [], 18 | }, 19 | log: !process.env.TEST, 20 | overrides: !!process.env.COVERAGE 21 | ? {} 22 | : { 23 | gasLimit: 12_000_000, 24 | }, 25 | }); 26 | }; 27 | 28 | deployFunction.dependencies = []; 29 | deployFunction.tags = ['ThirdPartyDCAHubSwapper']; 30 | export default deployFunction; 31 | -------------------------------------------------------------------------------- /deploy/004_fee_manager.ts: -------------------------------------------------------------------------------- 1 | import { HardhatRuntimeEnvironment } from 'hardhat/types'; 2 | import { DeployFunction } from '@0xged/hardhat-deploy/types'; 3 | import { bytecode } from '../artifacts/contracts/DCAFeeManager/DCAFeeManager.sol/DCAFeeManager.json'; 4 | import { deployThroughDeterministicFactory } from '@mean-finance/deterministic-factory/utils/deployment'; 5 | 6 | const deployFunction: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { 7 | const { deployer, msig } = await hre.getNamedAccounts(); 8 | 9 | await deployThroughDeterministicFactory({ 10 | deployer, 11 | name: 'DCAFeeManager', 12 | salt: 'MF-DCAV2-DCAFeeManager-V3', 13 | contract: 'contracts/DCAFeeManager/DCAFeeManager.sol:DCAFeeManager', 14 | bytecode, 15 | constructorArgs: { 16 | types: ['address', 'address[]'], 17 | values: [msig, [msig]], 18 | }, 19 | log: !process.env.TEST, 20 | overrides: !!process.env.COVERAGE 21 | ? {} 22 | : { 23 | gasLimit: 4_000_000, 24 | }, 25 | }); 26 | }; 27 | 28 | deployFunction.tags = ['DCAFeeManager']; 29 | export default deployFunction; 30 | -------------------------------------------------------------------------------- /deploy/005_keep3r_job.ts: -------------------------------------------------------------------------------- 1 | import { HardhatRuntimeEnvironment } from 'hardhat/types'; 2 | import { DeployFunction } from '@0xged/hardhat-deploy/types'; 3 | import { bytecode } from '../artifacts/contracts/DCAKeep3rJob/DCAKeep3rJob.sol/DCAKeep3rJob.json'; 4 | import { deployThroughDeterministicFactory } from '@mean-finance/deterministic-factory/utils/deployment'; 5 | 6 | const deployFunction: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { 7 | const { deployer, msig } = await hre.getNamedAccounts(); 8 | 9 | const keep3r = '0xeb02addCfD8B773A5FFA6B9d1FE99c566f8c44CC'; 10 | 11 | if (hre.deployments.getNetworkName() !== 'ethereum') { 12 | console.log('Avoiding deployment of Keep3r Job'); 13 | return; 14 | } 15 | 16 | const dcaHub = await hre.deployments.get('DCAHub'); 17 | await deployThroughDeterministicFactory({ 18 | deployer, 19 | name: 'DCAKeep3rJob', 20 | salt: 'MF-DCAV2-Keep3rJob-V2', 21 | contract: 'contracts/DCAKeep3rJob/DCAKeep3rJob.sol:DCAKeep3rJob', 22 | bytecode, 23 | constructorArgs: { 24 | types: ['address', 'address', 'address', 'address[]'], 25 | values: [keep3r, dcaHub.address, msig, []], 26 | }, 27 | log: !process.env.TEST, 28 | overrides: !!process.env.COVERAGE 29 | ? {} 30 | : { 31 | gasLimit: 4_000_000, 32 | }, 33 | }); 34 | }; 35 | 36 | deployFunction.dependencies = ['DCAHub']; 37 | deployFunction.tags = ['DCAKeep3rJob']; 38 | export default deployFunction; 39 | -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | import '@nomiclabs/hardhat-waffle'; 3 | import '@nomiclabs/hardhat-ethers'; 4 | import '@nomiclabs/hardhat-etherscan'; 5 | import '@typechain/hardhat'; 6 | import '@typechain/hardhat/dist/type-extensions'; 7 | import { removeConsoleLog } from 'hardhat-preprocessor'; 8 | import 'hardhat-gas-reporter'; 9 | import 'hardhat-contract-sizer'; 10 | import '@0xged/hardhat-deploy'; 11 | import 'solidity-coverage'; 12 | import './tasks/npm-publish-clean-typechain'; 13 | import { HardhatUserConfig, MultiSolcUserConfig, NetworksUserConfig } from 'hardhat/types'; 14 | import { getNodeUrl, accounts } from './utils/network'; 15 | import 'tsconfig-paths/register'; 16 | 17 | const networks: NetworksUserConfig = process.env.TEST 18 | ? { 19 | hardhat: { 20 | allowUnlimitedContractSize: true, 21 | }, 22 | } 23 | : { 24 | hardhat: { 25 | forking: { 26 | enabled: process.env.FORK ? true : false, 27 | url: getNodeUrl('mainnet'), 28 | }, 29 | tags: ['test', 'local'], 30 | }, 31 | localhost: { 32 | url: getNodeUrl('localhost'), 33 | live: false, 34 | accounts: accounts('localhost'), 35 | tags: ['local'], 36 | }, 37 | rinkeby: { 38 | url: getNodeUrl('rinkeby'), 39 | accounts: accounts('rinkeby'), 40 | tags: ['staging'], 41 | }, 42 | ropsten: { 43 | url: getNodeUrl('ropsten'), 44 | accounts: accounts('ropsten'), 45 | tags: ['staging'], 46 | }, 47 | kovan: { 48 | url: getNodeUrl('kovan'), 49 | accounts: accounts('kovan'), 50 | tags: ['staging'], 51 | }, 52 | goerli: { 53 | url: getNodeUrl('goerli'), 54 | accounts: accounts('goerli'), 55 | tags: ['staging'], 56 | }, 57 | ethereum: { 58 | url: getNodeUrl('ethereum'), 59 | accounts: accounts('ethereum'), 60 | tags: ['production'], 61 | }, 62 | 'optimism-kovan': { 63 | url: 'https://kovan.optimism.io', 64 | accounts: accounts('optimism-kovan'), 65 | tags: ['staging'], 66 | }, 67 | optimism: { 68 | url: 'https://mainnet.optimism.io', 69 | accounts: accounts('optimism'), 70 | tags: ['production'], 71 | }, 72 | arbitrum: { 73 | url: getNodeUrl('arbitrum'), 74 | accounts: accounts('arbitrum'), 75 | tags: ['production'], 76 | }, 77 | mumbai: { 78 | url: getNodeUrl('mumbai'), 79 | accounts: accounts('mumbai'), 80 | tags: ['staging'], 81 | }, 82 | polygon: { 83 | url: 'https://polygon-rpc.com', 84 | accounts: accounts('polygon'), 85 | tags: ['production'], 86 | }, 87 | }; 88 | 89 | const config: HardhatUserConfig = { 90 | defaultNetwork: 'hardhat', 91 | mocha: { 92 | timeout: process.env.MOCHA_TIMEOUT || 300000, 93 | }, 94 | namedAccounts: { 95 | deployer: { 96 | default: 4, 97 | }, 98 | eoaAdmin: '0x1a00e1E311009E56e3b0B9Ed6F86f5Ce128a1C01', 99 | msig: { 100 | ethereum: '0xEC864BE26084ba3bbF3cAAcF8F6961A9263319C4', 101 | optimism: '0x308810881807189cAe91950888b2cB73A1CC5920', 102 | polygon: '0xCe9F6991b48970d6c9Ef99Fffb112359584488e3', 103 | arbitrum: '0x84F4836e8022765Af9FBCE3Bb2887fD826c668f1', 104 | }, 105 | }, 106 | networks, 107 | solidity: { 108 | compilers: [ 109 | { 110 | version: '0.8.22', 111 | settings: { 112 | optimizer: { 113 | enabled: true, 114 | runs: 9999, 115 | }, 116 | evmVersion: 'paris', // Prevent using the `PUSH0` opcode 117 | }, 118 | }, 119 | ], 120 | }, 121 | gasReporter: { 122 | currency: process.env.COINMARKETCAP_DEFAULT_CURRENCY || 'USD', 123 | coinmarketcap: process.env.COINMARKETCAP_API_KEY, 124 | enabled: true, 125 | outputFile: 'gasReporterOutput.json', 126 | noColors: true, 127 | }, 128 | preprocess: { 129 | eachLine: removeConsoleLog((hre) => hre.network.name !== 'hardhat'), 130 | }, 131 | etherscan: { 132 | apiKey: process.env.ETHERSCAN_API_KEY, 133 | }, 134 | external: { 135 | deployments: { 136 | mainnet: [ 137 | 'node_modules/@mean-finance/dca-v2-core/deployments/mainnet', 138 | 'node_modules/@mean-finance/chainlink-registry/deployments/mainnet', 139 | ], 140 | mumbai: ['node_modules/@mean-finance/dca-v2-core/deployments/mumbai', 'node_modules/@mean-finance/chainlink-registry/deployments/mumbai'], 141 | optimism: [ 142 | 'node_modules/@mean-finance/dca-v2-core/deployments/optimism', 143 | 'node_modules/@mean-finance/chainlink-registry/deployments/optimism', 144 | ], 145 | 'optimism-kovan': [ 146 | 'node_modules/@mean-finance/dca-v2-core/deployments/optimism-kovan', 147 | 'node_modules/@mean-finance/chainlink-registry/deployments/optimismkovan', 148 | ], 149 | arbitrum: [ 150 | 'node_modules/@mean-finance/dca-v2-core/deployments/arbitrum', 151 | 'node_modules/@mean-finance/chainlink-registry/deployments/arbitrum', 152 | ], 153 | polygon: [ 154 | 'node_modules/@mean-finance/dca-v2-core/deployments/polygon', 155 | 'node_modules/@mean-finance/chainlink-registry/deployments/polygon', 156 | ], 157 | }, 158 | }, 159 | typechain: { 160 | outDir: 'typechained', 161 | target: 'ethers-v5', 162 | }, 163 | }; 164 | 165 | if (process.env.TEST) { 166 | config.external!.contracts = [ 167 | { 168 | artifacts: 'node_modules/@mean-finance/nft-descriptors/artifacts', 169 | deploy: 'node_modules/@mean-finance/nft-descriptors/deploy', 170 | }, 171 | { 172 | artifacts: 'node_modules/@mean-finance/transformers/artifacts', 173 | deploy: 'node_modules/@mean-finance/transformers/deploy', 174 | }, 175 | 176 | { 177 | artifacts: 'node_modules/@mean-finance/chainlink-registry/artifacts', 178 | deploy: 'node_modules/@mean-finance/chainlink-registry/deploy', 179 | }, 180 | { 181 | artifacts: 'node_modules/@mean-finance/oracles/artifacts', 182 | deploy: 'node_modules/@mean-finance/oracles/deploy', 183 | }, 184 | { 185 | artifacts: 'node_modules/@mean-finance/dca-v2-core/artifacts', 186 | deploy: 'node_modules/@mean-finance/dca-v2-core/deploy', 187 | }, 188 | { 189 | artifacts: 'node_modules/@mean-finance/swappers/artifacts', 190 | deploy: 'node_modules/@mean-finance/swappers/deploy', 191 | }, 192 | ]; 193 | const solidity = config.solidity as MultiSolcUserConfig; 194 | solidity.compilers.forEach((_, i) => { 195 | solidity.compilers[i].settings! = { 196 | ...solidity.compilers[i].settings!, 197 | outputSelection: { 198 | '*': { 199 | '*': ['storageLayout'], 200 | }, 201 | }, 202 | }; 203 | }); 204 | config.solidity = solidity; 205 | } 206 | 207 | export default config; 208 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@balmy/dca-v2-periphery", 3 | "version": "3.9.0", 4 | "description": "💱 Periphery smart contracts of DCA V2 by balmy.xyz", 5 | "keywords": [ 6 | "ethereum", 7 | "smart", 8 | "contracts", 9 | "mean", 10 | "balmy", 11 | "dca" 12 | ], 13 | "homepage": "https://balmy.xyz", 14 | "bugs": { 15 | "url": "https://github.com/Balmy-protocol/dca-v2-periphery/issues" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/Balmy-protocol/dca-v2-periphery.git" 20 | }, 21 | "license": "GPL-2.0", 22 | "main": "dist", 23 | "types": "dist", 24 | "files": [ 25 | "dist", 26 | "contracts", 27 | "!contracts/mocks", 28 | "artifacts/contracts/**/*.json", 29 | "!artifacts/contracts/mocks/**/*", 30 | "!artifacts/contracts/**/**/*.dbg.json", 31 | "!/**/*Mock*", 32 | "deployments", 33 | "deploy", 34 | "!deployments/localhost", 35 | "!.env", 36 | "!**/.DS_Store" 37 | ], 38 | "scripts": { 39 | "compile": "npx hardhat compile", 40 | "coverage": "TS_NODE_SKIP_IGNORE=true TEST=true COVERAGE=true npx hardhat coverage --solcoverjs ./solcover.js", 41 | "deploy": "TS_NODE_SKIP_IGNORE=true npx hardhat deploy", 42 | "fork": "FORK=true npx hardhat node", 43 | "fork:script": "FORK=true npx hardhat run", 44 | "fork:test": "FORK=true npx hardhat test", 45 | "postinstall": "husky install", 46 | "lint:check": "solhint 'contracts/**/*.sol' 'interfaces/**/*.sol' && prettier --check './**'", 47 | "lint:fix": "sort-package-json && prettier --write './**' && solhint --fix 'contracts/**/*.sol' 'interfaces/**/*.sol'", 48 | "prepublishOnly": "hardhat clean && PUBLISHING_NPM=true hardhat compile && yarn transpile && pinst --disable", 49 | "postpublish": "pinst --enable", 50 | "release": "standard-version", 51 | "sizer": "TEST=true hardhat compile && TEST=true npx hardhat size-contracts", 52 | "test": "TEST=true npx hardhat compile && TS_NODE_SKIP_IGNORE=true TEST=true npx hardhat test", 53 | "test:all": "yarn test ./test/e2e/**/*.spec.ts test/integration/**/*.spec.ts test/unit/**/*.spec.ts", 54 | "test:all:parallel": "yarn test:parallel './test/e2e/**/*.spec.ts' 'test/unit/**/*.spec.ts'", 55 | "test:gas": "TS_NODE_SKIP_IGNORE=true USE_RANDOM_SALT=true yarn test", 56 | "test:integration": "TS_NODE_SKIP_IGNORE=true USE_RANDOM_SALT=true yarn test ./test/integration/**/*.spec.ts", 57 | "test:integration:parallel": "TS_NODE_SKIP_IGNORE=true yarn test:parallel ./test/integration/**/*.spec.ts", 58 | "test:parallel": "TEST=true hardhat compile && TEST=true mocha --parallel", 59 | "test:unit": "yarn test test/unit/**/*.spec.ts", 60 | "test:unit:parallel": "yarn test:parallel 'test/unit/**/*.spec.ts'", 61 | "transpile": "rm -rf dist && npx tsc -p tsconfig.publish.json", 62 | "verify": "npx hardhat run scripts/verify-contracts.ts" 63 | }, 64 | "dependencies": { 65 | "@0xged/hardhat-deploy": "0.11.5", 66 | "@mean-finance/call-simulation": "0.0.2", 67 | "@mean-finance/dca-v2-core": "3.4.0", 68 | "@mean-finance/deterministic-factory": "1.6.0", 69 | "@openzeppelin/contracts": "5.0.1", 70 | "keep3r-v2": "1.0.0" 71 | }, 72 | "devDependencies": { 73 | "@codechecks/client": "0.1.12", 74 | "@commitlint/cli": "16.2.4", 75 | "@commitlint/config-conventional": "16.2.4", 76 | "@defi-wonderland/smock": "2.4.0", 77 | "@mean-finance/sdk": "0.0.119", 78 | "@nomiclabs/hardhat-ethers": "npm:hardhat-deploy-ethers@0.3.0-beta.13", 79 | "@nomiclabs/hardhat-etherscan": "3.1.0", 80 | "@nomiclabs/hardhat-waffle": "2.0.3", 81 | "@openzeppelin/test-helpers": "0.5.15", 82 | "@typechain/ethers-v5": "10.1.0", 83 | "@typechain/hardhat": "6.1.2", 84 | "@types/axios": "0.14.0", 85 | "@types/chai": "4.3.1", 86 | "@types/chai-as-promised": "7.1.5", 87 | "@types/lodash": "4.14.182", 88 | "@types/mocha": "9.1.1", 89 | "@types/node": "17.0.31", 90 | "axios": "0.27.2", 91 | "axios-retry": "3.2.4", 92 | "bignumber.js": "9.0.2", 93 | "chai": "4.3.6", 94 | "chai-as-promised": "7.1.1", 95 | "cross-env": "7.0.3", 96 | "dotenv": "16.0.0", 97 | "ethereum-waffle": "3.4.4", 98 | "ethers": "5.6.5", 99 | "hardhat": "^2.23.0", 100 | "hardhat-contract-sizer": "2.0.3", 101 | "hardhat-gas-reporter": "1.0.8", 102 | "hardhat-preprocessor": "0.1.4", 103 | "husky": "7.0.4", 104 | "lint-staged": "12.4.1", 105 | "lodash": "4.17.21", 106 | "mocha": "10.0.0", 107 | "moment": "2.29.3", 108 | "pinst": "3.0.0", 109 | "prettier": "2.6.2", 110 | "prettier-plugin-solidity": "1.0.0-beta.19", 111 | "solc-0.8": "npm:solc@0.8.13", 112 | "solhint": "3.3.7", 113 | "solhint-plugin-prettier": "0.0.5", 114 | "solidity-coverage": "https://github.com/adjisb/solidity-coverage", 115 | "solidity-docgen": "0.5.16", 116 | "sort-package-json": "1.57.0", 117 | "standard-version": "9.3.2", 118 | "ts-node": "10.7.0", 119 | "tsconfig-paths": "4.0.0", 120 | "typechain": "8.1.0", 121 | "typescript": "4.7.2" 122 | }, 123 | "publishConfig": { 124 | "access": "public" 125 | }, 126 | "authors": [ 127 | { 128 | "name": "Alejo Amiras", 129 | "url": "https://github.com/alejoamiras" 130 | }, 131 | { 132 | "name": "Nicolás Chamo", 133 | "url": "https://github.com/nchamo", 134 | "email": "nchamo@balmy.xyz" 135 | } 136 | ] 137 | } 138 | -------------------------------------------------------------------------------- /tasks/npm-publish-clean-typechain.ts: -------------------------------------------------------------------------------- 1 | import { subtask } from 'hardhat/config'; 2 | import { TASK_COMPILE_SOLIDITY_COMPILE_JOBS } from 'hardhat/builtin-tasks/task-names'; 3 | import fs from 'fs/promises'; 4 | 5 | subtask(TASK_COMPILE_SOLIDITY_COMPILE_JOBS, 'Clean mocks from types if needed').setAction(async (taskArgs, { run }, runSuper) => { 6 | const compileSolOutput = await runSuper(taskArgs); 7 | if (!!process.env.PUBLISHING_NPM) { 8 | console.log('🫠 Removing all mock references from typechain'); 9 | // Cleaning typechained/index 10 | console.log(` 🧹 Excluding from main index`); 11 | let typechainIndexBuffer = await fs.readFile('./typechained/index.ts'); 12 | let finalTypechainIndex = typechainIndexBuffer 13 | .toString('utf-8') 14 | .split(/\r?\n/) 15 | .filter((line) => !line.includes('Mock') && !line.includes('mock')) 16 | .join('\n'); 17 | await fs.writeFile('./typechained/index.ts', finalTypechainIndex, 'utf-8'); 18 | // Cleaning typechained/contracts/index 19 | console.log(` 🧹 Excluding from contracts index`); 20 | typechainIndexBuffer = await fs.readFile('./typechained/contracts/index.ts'); 21 | finalTypechainIndex = typechainIndexBuffer 22 | .toString('utf-8') 23 | .split(/\r?\n/) 24 | .filter((line) => !line.includes('Mock') && !line.includes('mock')) 25 | .join('\n'); 26 | await fs.writeFile('./typechained/contracts/index.ts', finalTypechainIndex, 'utf-8'); 27 | // Cleaning typechained/factories/contracts/index 28 | console.log(` 🧹 Excluding from factories contract's index`); 29 | typechainIndexBuffer = await fs.readFile('./typechained/factories/contracts/index.ts'); 30 | finalTypechainIndex = typechainIndexBuffer 31 | .toString('utf-8') 32 | .split(/\r?\n/) 33 | .filter((line) => !line.includes('Mock') && !line.includes('mock')) 34 | .join('\n'); 35 | await fs.writeFile('./typechained/factories/contracts/index.ts', finalTypechainIndex, 'utf-8'); 36 | } 37 | return compileSolOutput; 38 | }); 39 | -------------------------------------------------------------------------------- /test/integration/DCAHubSwapper/swap-for-caller.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { ethers } from 'hardhat'; 3 | import { JsonRpcSigner, TransactionResponse } from '@ethersproject/providers'; 4 | import { constants, wallet } from '@test-utils'; 5 | import { contract, given, then, when } from '@test-utils/bdd'; 6 | import evm, { snapshot } from '@test-utils/evm'; 7 | import { CallerOnlyDCAHubSwapper, IERC20 } from '@typechained'; 8 | import { DCAHub } from '@mean-finance/dca-v2-core'; 9 | import { abi as DCA_HUB_ABI } from '@mean-finance/dca-v2-core/artifacts/contracts/DCAHub/DCAHub.sol/DCAHub.json'; 10 | import { abi as IERC20_ABI } from '@openzeppelin/contracts/build/contracts/IERC20.json'; 11 | import { BigNumber, utils } from 'ethers'; 12 | import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; 13 | import { SwapInterval } from '@test-utils/interval-utils'; 14 | import forkBlockNumber from '@integration/fork-block-numbers'; 15 | import { deploy } from '@integration/utils'; 16 | 17 | const WETH_ADDRESS = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'; 18 | const USDC_ADDRESS = '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48'; 19 | const WETH_WHALE_ADDRESS = '0xf04a5cc80b1e94c69b48f5ee68a08cd2f09a7c3e'; 20 | const USDC_WHALE_ADDRESS = '0xcffad3200574698b78f32232aa9d63eabd290703'; 21 | 22 | contract('Swap for caller', () => { 23 | let WETH: IERC20, USDC: IERC20; 24 | let governor: JsonRpcSigner; 25 | let cindy: SignerWithAddress, swapper: SignerWithAddress, recipient: SignerWithAddress; 26 | let DCAHubSwapper: CallerOnlyDCAHubSwapper; 27 | let DCAHub: DCAHub; 28 | let initialPerformedSwaps: number; 29 | let snapshotId: string; 30 | 31 | const RATE = utils.parseEther('0.1'); 32 | const AMOUNT_OF_SWAPS = 10; 33 | 34 | before(async () => { 35 | await evm.reset({ 36 | network: 'ethereum', 37 | blockNumber: forkBlockNumber['swap-for-caller'], 38 | }); 39 | [cindy, swapper, recipient] = await ethers.getSigners(); 40 | 41 | ({ msig: governor } = await deploy()); 42 | 43 | DCAHub = await ethers.getContract('DCAHub'); 44 | DCAHubSwapper = await ethers.getContract('CallerOnlyDCAHubSwapper'); 45 | 46 | // Allow tokens 47 | await DCAHub.connect(governor).setAllowedTokens([WETH_ADDRESS, USDC_ADDRESS], [true, true]); 48 | 49 | // Allow one minute interval 50 | await DCAHub.connect(governor).addSwapIntervalsToAllowedList([SwapInterval.ONE_MINUTE.seconds]); 51 | 52 | // Allow swapper 53 | await DCAHub.connect(governor).grantRole(await DCAHub.PRIVILEGED_SWAPPER_ROLE(), DCAHubSwapper.address); 54 | await DCAHub.connect(governor).grantRole(await DCAHub.PRIVILEGED_SWAPPER_ROLE(), swapper.address); 55 | 56 | WETH = await ethers.getContractAt(IERC20_ABI, WETH_ADDRESS); 57 | USDC = await ethers.getContractAt(IERC20_ABI, USDC_ADDRESS); 58 | 59 | // Send tokens from whales, to our users 60 | await distributeTokensToUsers(); 61 | 62 | const depositAmount = RATE.mul(AMOUNT_OF_SWAPS); 63 | await WETH.connect(cindy).approve(DCAHub.address, depositAmount); 64 | await USDC.connect(swapper).approve(DCAHubSwapper.address, BigNumber.from(10).pow(12)); 65 | await DCAHub.connect(cindy)['deposit(address,address,uint256,uint32,uint32,address,(address,uint8[])[])']( 66 | WETH.address, 67 | USDC.address, 68 | depositAmount, 69 | AMOUNT_OF_SWAPS, 70 | SwapInterval.ONE_MINUTE.seconds, 71 | cindy.address, 72 | [] 73 | ); 74 | initialPerformedSwaps = await performedSwaps(); 75 | snapshotId = await snapshot.take(); 76 | }); 77 | beforeEach('Deploy and configure', async () => { 78 | await snapshot.revert(snapshotId); 79 | }); 80 | 81 | describe('swap for caller', () => { 82 | when('a swap for caller is executed', () => { 83 | let rewardWETH: BigNumber, toProvideUSDC: BigNumber; 84 | let initialHubWETHBalance: BigNumber, initialHubUSDCBalance: BigNumber; 85 | let initialSwapperUSDCBalance: BigNumber; 86 | given(async () => { 87 | initialSwapperUSDCBalance = await USDC.balanceOf(swapper.address); 88 | initialHubWETHBalance = await WETH.balanceOf(DCAHub.address); 89 | initialHubUSDCBalance = await USDC.balanceOf(DCAHub.address); 90 | const swapTx = await DCAHubSwapper.connect(swapper).swapForCaller({ 91 | hub: DCAHub.address, 92 | tokens: [USDC_ADDRESS, WETH_ADDRESS], 93 | pairsToSwap: [{ indexTokenA: 0, indexTokenB: 1 }], 94 | oracleData: [], 95 | minimumOutput: [0, 0], 96 | maximumInput: [constants.MAX_UINT_256, constants.MAX_UINT_256], 97 | recipient: recipient.address, 98 | deadline: constants.MAX_UINT_256, 99 | }); 100 | ({ rewardWETH, toProvideUSDC } = await getTransfers(swapTx)); 101 | }); 102 | then('swap is executed', async () => { 103 | expect(await performedSwaps()).to.equal(initialPerformedSwaps + 1); 104 | }); 105 | then('hub balance is correct', async () => { 106 | const hubWETHBalance = await WETH.balanceOf(DCAHub.address); 107 | const hubUSDCBalance = await USDC.balanceOf(DCAHub.address); 108 | expect(hubWETHBalance).to.equal(initialHubWETHBalance.sub(RATE)); 109 | expect(hubUSDCBalance).to.equal(initialHubUSDCBalance.add(toProvideUSDC)); 110 | }); 111 | then('all reward is sent to recipient', async () => { 112 | const recipientWETHBalance = await WETH.balanceOf(recipient.address); 113 | expect(recipientWETHBalance).to.equal(rewardWETH); 114 | }); 115 | then('all "toProvide" is taken from swapper', async () => { 116 | const swapperUSDCBalance = await USDC.balanceOf(swapper.address); 117 | expect(swapperUSDCBalance).to.equal(initialSwapperUSDCBalance.sub(toProvideUSDC)); 118 | }); 119 | }); 120 | }); 121 | async function distributeTokensToUsers() { 122 | const wethWhale = await wallet.impersonate(WETH_WHALE_ADDRESS); 123 | const usdcWhale = await wallet.impersonate(USDC_WHALE_ADDRESS); 124 | await ethers.provider.send('hardhat_setBalance', [WETH_WHALE_ADDRESS, '0xffffffffffffffff']); 125 | await ethers.provider.send('hardhat_setBalance', [USDC_WHALE_ADDRESS, '0xffffffffffffffff']); 126 | await WETH.connect(wethWhale).transfer(cindy.address, BigNumber.from(10).pow(20)); 127 | await WETH.connect(wethWhale).transfer(swapper.address, BigNumber.from(10).pow(20)); 128 | await USDC.connect(usdcWhale).transfer(swapper.address, BigNumber.from(10).pow(12)); 129 | } 130 | 131 | async function performedSwaps(): Promise { 132 | const { performedSwaps } = await DCAHub.swapData(USDC_ADDRESS, WETH_ADDRESS, SwapInterval.ONE_MINUTE.mask); 133 | return performedSwaps; 134 | } 135 | 136 | async function getTransfers(tx: TransactionResponse) { 137 | const swappedEvent = await getSwappedEvent(tx); 138 | const [usdc, weth] = swappedEvent.args.swapInformation.tokens; 139 | const rewardWETH = weth.reward; 140 | const toProvideUSDC = usdc.toProvide; 141 | return { rewardWETH, toProvideUSDC }; 142 | } 143 | 144 | function getSwappedEvent(tx: TransactionResponse): Promise { 145 | return findLogs(tx, new utils.Interface(DCA_HUB_ABI), 'Swapped'); 146 | } 147 | 148 | async function findLogs( 149 | tx: TransactionResponse, 150 | contractInterface: utils.Interface, 151 | eventTopic: string, 152 | extraFilter?: (_: utils.LogDescription) => boolean 153 | ): Promise { 154 | const txReceipt = await tx.wait(); 155 | const logs = txReceipt.logs; 156 | for (let i = 0; i < logs.length; i++) { 157 | for (let x = 0; x < logs[i].topics.length; x++) { 158 | if (logs[i].topics[x] === contractInterface.getEventTopic(eventTopic)) { 159 | const parsedLog = contractInterface.parseLog(logs[i]); 160 | if (!extraFilter || extraFilter(parsedLog)) { 161 | return parsedLog; 162 | } 163 | } 164 | } 165 | } 166 | return Promise.reject(); 167 | } 168 | }); 169 | -------------------------------------------------------------------------------- /test/integration/DCAHubSwapper/swap-with-dex-native.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { ethers } from 'hardhat'; 3 | import { JsonRpcSigner, TransactionResponse } from '@ethersproject/providers'; 4 | import { constants, wallet } from '@test-utils'; 5 | import { contract, given, then, when } from '@test-utils/bdd'; 6 | import evm, { snapshot } from '@test-utils/evm'; 7 | import { IERC20, ThirdPartyDCAHubSwapper } from '@typechained'; 8 | import { StatefulChainlinkOracle } from '@mean-finance/oracles'; 9 | import { DCAHub } from '@mean-finance/dca-v2-core'; 10 | import { abi as DCA_HUB_ABI } from '@mean-finance/dca-v2-core/artifacts/contracts/DCAHub/DCAHub.sol/DCAHub.json'; 11 | import { abi as IERC20_ABI } from '@openzeppelin/contracts/build/contracts/IERC20.json'; 12 | import { BigNumber, BigNumberish, BytesLike, utils } from 'ethers'; 13 | import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; 14 | import { SwapInterval } from '@test-utils/interval-utils'; 15 | import { deploy } from '@integration/utils'; 16 | import { buildSDK } from '@mean-finance/sdk'; 17 | 18 | const ETH_ADDRESS = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'; 19 | const WETH_ADDRESS = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'; 20 | const USDC_ADDRESS = '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48'; 21 | // USDC < WETH 22 | const WETH_WHALE_ADDRESS = '0xf04a5cc80b1e94c69b48f5ee68a08cd2f09a7c3e'; 23 | 24 | contract('Swap with DEX, using native', () => { 25 | const ABI_CODER = new utils.AbiCoder(); 26 | let WETH: IERC20; 27 | let USDC: IERC20; 28 | let governor: JsonRpcSigner, timelock: JsonRpcSigner; 29 | let cindy: SignerWithAddress, recipient: SignerWithAddress, swapStarter: SignerWithAddress; 30 | let DCAHubSwapper: ThirdPartyDCAHubSwapper; 31 | let DCAHub: DCAHub; 32 | let initialPerformedSwaps: number; 33 | let snapshotId: string; 34 | 35 | const RATE = utils.parseEther('0.1'); 36 | const AMOUNT_OF_SWAPS = 10; 37 | 38 | before(async () => { 39 | await evm.reset({ 40 | network: 'ethereum', 41 | }); 42 | 43 | [cindy, swapStarter, recipient] = await ethers.getSigners(); 44 | 45 | ({ msig: governor, timelock } = await deploy('ThirdPartyDCAHubSwapper')); 46 | DCAHub = await ethers.getContract('DCAHub'); 47 | DCAHubSwapper = await ethers.getContract('ThirdPartyDCAHubSwapper'); 48 | const chainlinkOracle = await ethers.getContract('StatefulChainlinkOracle'); 49 | 50 | // Allow tokens 51 | await DCAHub.connect(governor).setAllowedTokens([WETH_ADDRESS, USDC_ADDRESS], [true, true]); 52 | // Allow one minute interval 53 | await DCAHub.connect(governor).addSwapIntervalsToAllowedList([SwapInterval.ONE_MINUTE.seconds]); 54 | //We are setting a very high fee, so that there is a surplus in both reward and toProvide tokens 55 | await DCAHub.connect(timelock).setSwapFee(20000); // 2% 56 | // Allow swap started 57 | await DCAHub.connect(governor).grantRole(await DCAHub.PRIVILEGED_SWAPPER_ROLE(), swapStarter.address); 58 | 59 | await chainlinkOracle.connect(governor).addMappings([WETH_ADDRESS], ['0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE']); 60 | 61 | WETH = await ethers.getContractAt(IERC20_ABI, WETH_ADDRESS); 62 | USDC = await ethers.getContractAt(IERC20_ABI, USDC_ADDRESS); 63 | const wethWhale = await wallet.impersonate(WETH_WHALE_ADDRESS); 64 | await ethers.provider.send('hardhat_setBalance', [WETH_WHALE_ADDRESS, '0xffffffffffffffff']); 65 | 66 | const depositAmount = RATE.mul(AMOUNT_OF_SWAPS); 67 | await WETH.connect(wethWhale).transfer(cindy.address, depositAmount); 68 | await WETH.connect(cindy).approve(DCAHub.address, depositAmount); 69 | await DCAHub.connect(cindy)['deposit(address,address,uint256,uint32,uint32,address,(address,uint8[])[])']( 70 | WETH.address, 71 | USDC.address, 72 | depositAmount, 73 | AMOUNT_OF_SWAPS, 74 | SwapInterval.ONE_MINUTE.seconds, 75 | cindy.address, 76 | [] 77 | ); 78 | initialPerformedSwaps = await performedSwaps(); 79 | 80 | snapshotId = await snapshot.take(); 81 | }); 82 | beforeEach('Deploy and configure', async () => { 83 | await snapshot.revert(snapshotId); 84 | }); 85 | 86 | describe('swap with dex', () => { 87 | when('executing a swap with 0x', () => { 88 | let initialHubWETHBalance: BigNumber, initialHubUSDCBalance: BigNumber, initialRecipientUSDCBalance: BigNumber; 89 | let reward: BigNumber, receivedFromAgg: BigNumber; 90 | given(async () => { 91 | initialHubWETHBalance = await WETH.balanceOf(DCAHub.address); 92 | initialHubUSDCBalance = await USDC.balanceOf(DCAHub.address); 93 | initialRecipientUSDCBalance = await USDC.balanceOf(recipient.address); 94 | const dexQuote = await buildSDK() 95 | .quoteService.getAllQuotes({ 96 | request: { 97 | chainId: 1, 98 | sellToken: ETH_ADDRESS, 99 | buyToken: USDC_ADDRESS, 100 | order: { type: 'sell', sellAmount: RATE.toBigInt() }, 101 | slippagePercentage: 1, 102 | takerAddress: DCAHubSwapper.address, 103 | }, 104 | config: { 105 | timeout: '3s', 106 | }, 107 | }) 108 | .then((quotes) => quotes[0]); 109 | const tokensInSwap = [USDC_ADDRESS, WETH_ADDRESS]; 110 | const indexesInSwap = [{ indexTokenA: 0, indexTokenB: 1 }]; 111 | const data = encode({ 112 | allowanceTargets: [], 113 | executions: [{ swapper: dexQuote.tx.to, data: dexQuote.tx.data, value: RATE }], 114 | }); 115 | const swapTx = await DCAHubSwapper.connect(swapStarter).executeSwap(DCAHub.address, tokensInSwap, indexesInSwap, [0, 0], data, '0x', { 116 | value: RATE, 117 | }); 118 | ({ reward, receivedFromAgg } = await getTransfers(swapTx)); 119 | }); 120 | then('swap is executed', async () => { 121 | expect(await performedSwaps()).to.equal(initialPerformedSwaps + 1); 122 | }); 123 | then('hub balance is correct', async () => { 124 | const hubWETHBalance = await WETH.balanceOf(DCAHub.address); 125 | const hubUSDCBalance = await USDC.balanceOf(DCAHub.address); 126 | expect(hubWETHBalance).to.equal(initialHubWETHBalance.sub(reward)); 127 | expect(hubUSDCBalance).to.equal(initialHubUSDCBalance.add(receivedFromAgg)); 128 | }); 129 | then('all reward is sent to leftover recipient', async () => { 130 | const recipientWETHBalance = await WETH.balanceOf(recipient.address); 131 | expect(recipientWETHBalance).to.equal(reward); 132 | }); 133 | then('leftover recipient has no "toProvide" balance', async () => { 134 | const recipientUSDCBalance = await USDC.balanceOf(recipient.address); 135 | expect(recipientUSDCBalance).to.equal(initialRecipientUSDCBalance); 136 | }); 137 | }); 138 | }); 139 | 140 | type SwapData = { 141 | allowanceTargets?: { token: string; spender: string; amount: BigNumberish }[]; 142 | executions: { data: BytesLike; swapper: string; value: BigNumberish }[]; 143 | extraTokens?: string[]; 144 | }; 145 | function encode(bytes: SwapData) { 146 | return ABI_CODER.encode( 147 | ['tuple(bool, uint256, tuple(address, address, uint256)[], tuple(address, uint256, bytes)[], address[], address)'], 148 | [ 149 | [ 150 | false, 151 | constants.MAX_UINT_256, 152 | bytes.allowanceTargets?.map(({ token, spender, amount }) => [token, spender, amount]) ?? [], 153 | bytes.executions?.map(({ swapper, data, value }) => [swapper, value, data]) ?? [], 154 | bytes.extraTokens ?? [], 155 | recipient.address, 156 | ], 157 | ] 158 | ); 159 | } 160 | 161 | async function performedSwaps(): Promise { 162 | const { performedSwaps } = await DCAHub.swapData(USDC_ADDRESS, WETH_ADDRESS, SwapInterval.ONE_MINUTE.mask); 163 | return performedSwaps; 164 | } 165 | 166 | async function getTransfers( 167 | tx: TransactionResponse 168 | ): Promise<{ reward: BigNumber; toProvide: BigNumber; sentToAgg: BigNumber; receivedFromAgg: BigNumber }> { 169 | const swappedEvent = await getSwappedEvent(tx); 170 | const [tokenA, tokenB] = swappedEvent.args.swapInformation.tokens; 171 | const reward = tokenA.reward.gt(tokenB.reward) ? tokenA.reward : tokenB.reward; 172 | const toProvide = tokenA.toProvide.gt(tokenB.toProvide) ? tokenA.toProvide : tokenB.toProvide; 173 | 174 | const receivedFromAgg = await findTransferValue(tx, { notFrom: DCAHub, to: DCAHubSwapper }); 175 | const sentToAgg = await findTransferValue(tx, { from: DCAHubSwapper, notTo: DCAHub }); 176 | return { reward, toProvide, receivedFromAgg, sentToAgg }; 177 | } 178 | 179 | function getSwappedEvent(tx: TransactionResponse): Promise { 180 | return findLogs(tx, new utils.Interface(DCA_HUB_ABI), 'Swapped'); 181 | } 182 | 183 | async function findTransferValue( 184 | tx: TransactionResponse, 185 | { 186 | from, 187 | notFrom, 188 | to, 189 | notTo, 190 | }: { from?: { address: string }; notFrom?: { address: string }; to?: { address: string }; notTo?: { address: string } } 191 | ) { 192 | const log = await findLogs( 193 | tx, 194 | USDC.interface, 195 | 'Transfer', 196 | (log) => 197 | (!from || log.args.from === from.address) && 198 | (!to || log.args.to === to.address) && 199 | (!notFrom || log.args.from !== notFrom.address) && 200 | (!notTo || log.args.to !== notTo.address) 201 | ); 202 | return BigNumber.from(log.args.value); 203 | } 204 | 205 | async function findLogs( 206 | tx: TransactionResponse, 207 | contractInterface: utils.Interface, 208 | eventTopic: string, 209 | extraFilter?: (_: utils.LogDescription) => boolean 210 | ): Promise { 211 | const txReceipt = await tx.wait(); 212 | const logs = txReceipt.logs; 213 | for (let i = 0; i < logs.length; i++) { 214 | for (let x = 0; x < logs[i].topics.length; x++) { 215 | if (logs[i].topics[x] === contractInterface.getEventTopic(eventTopic)) { 216 | const parsedLog = contractInterface.parseLog(logs[i]); 217 | if (!extraFilter || extraFilter(parsedLog)) { 218 | return parsedLog; 219 | } 220 | } 221 | } 222 | } 223 | return Promise.reject(); 224 | } 225 | }); 226 | -------------------------------------------------------------------------------- /test/integration/DCAKeep3rJob/keep3r-job.spec.ts: -------------------------------------------------------------------------------- 1 | import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; 2 | import { JsonRpcSigner, TransactionResponse } from '@ethersproject/providers'; 3 | import { BigNumber, BigNumberish, BytesLike, Contract, utils } from 'ethers'; 4 | import { ethers } from 'hardhat'; 5 | import { abi as IERC20_ABI } from '@openzeppelin/contracts/build/contracts/IERC20.json'; 6 | import { expect } from 'chai'; 7 | import { DCAKeep3rJob, IERC20, ThirdPartyDCAHubSwapper } from '@typechained'; 8 | import { SwapInterval } from '@test-utils/interval-utils'; 9 | import evm, { snapshot } from '@test-utils/evm'; 10 | import { contract, given, then, when } from '@test-utils/bdd'; 11 | import { wallet, constants } from '@test-utils'; 12 | import { deploy } from '@integration/utils'; 13 | import { DCAHub } from '@mean-finance/dca-v2-core'; 14 | import { fromRpcSig } from 'ethereumjs-util'; 15 | import KEEP3R_ABI from '../abis/Keep3r.json'; 16 | import { buildSDK } from '@mean-finance/sdk'; 17 | 18 | const WETH_ADDRESS = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'; 19 | const USDC_ADDRESS = '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48'; 20 | const KP3R_ADDRESS = '0x1ceb5cb57c4d4e2b2433641b95dd330a33185a44'; 21 | const WETH_WHALE_ADDRESS = '0xf04a5cc80b1e94c69b48f5ee68a08cd2f09a7c3e'; 22 | const KEEP3R_GOVERNANCE = '0x0d5dc686d0a2abbfdafdfb4d0533e886517d4e83'; 23 | 24 | contract('DCAKeep3rJob', () => { 25 | let WETH: IERC20, K3PR: IERC20; 26 | 27 | let DCAKeep3rJob: DCAKeep3rJob; 28 | let thirdPartySwapper: ThirdPartyDCAHubSwapper; 29 | let DCAHub: DCAHub; 30 | let keep3rV2: Contract; 31 | 32 | let cindy: SignerWithAddress, signer: SignerWithAddress, keeper: SignerWithAddress; 33 | let msig: JsonRpcSigner, timelock: JsonRpcSigner, keep3rGovernance: JsonRpcSigner; 34 | let initialPerformedSwaps: number; 35 | let chainId: BigNumber; 36 | let snapshotId: string; 37 | 38 | before(async () => { 39 | [cindy, signer, keeper] = await ethers.getSigners(); 40 | 41 | await evm.reset({ 42 | network: 'ethereum', 43 | }); 44 | 45 | ({ msig, timelock } = await deploy('ThirdPartyDCAHubSwapper', 'DCAKeep3rJob')); 46 | 47 | DCAHub = await ethers.getContract('DCAHub'); 48 | thirdPartySwapper = await ethers.getContract('ThirdPartyDCAHubSwapper'); 49 | DCAKeep3rJob = await ethers.getContract('DCAKeep3rJob'); 50 | keep3rV2 = await ethers.getContractAt(KEEP3R_ABI, await DCAKeep3rJob.keep3r()); 51 | 52 | const wethWhale = await wallet.impersonate(WETH_WHALE_ADDRESS); 53 | keep3rGovernance = await wallet.impersonate(KEEP3R_GOVERNANCE); 54 | await ethers.provider.send('hardhat_setBalance', [WETH_WHALE_ADDRESS, '0xffffffffffffffff']); 55 | await ethers.provider.send('hardhat_setBalance', [KEEP3R_GOVERNANCE, '0xffffffffffffffff']); 56 | 57 | await DCAHub.connect(timelock).setSwapFee(50000); // 5% 58 | await DCAHub.connect(msig).setAllowedTokens([WETH_ADDRESS, USDC_ADDRESS], [true, true]); 59 | await DCAHub.connect(msig).grantRole(await DCAHub.PRIVILEGED_SWAPPER_ROLE(), DCAKeep3rJob.address); 60 | await DCAKeep3rJob.connect(msig).grantRole(DCAKeep3rJob.CAN_SIGN_ROLE(), signer.address); 61 | 62 | WETH = await ethers.getContractAt(IERC20_ABI, WETH_ADDRESS); 63 | K3PR = await ethers.getContractAt(IERC20_ABI, KP3R_ADDRESS); 64 | 65 | const amountOfSwaps = 10; 66 | const depositAmount = utils.parseEther('0.1').mul(amountOfSwaps); 67 | await WETH.connect(wethWhale).transfer(cindy.address, depositAmount); 68 | await WETH.connect(cindy).approve(DCAHub.address, depositAmount); 69 | await DCAHub.connect(cindy)['deposit(address,address,uint256,uint32,uint32,address,(address,uint8[])[])']( 70 | WETH.address, 71 | USDC_ADDRESS, 72 | depositAmount, 73 | amountOfSwaps, 74 | SwapInterval.ONE_DAY.seconds, 75 | cindy.address, 76 | [] 77 | ); 78 | 79 | initialPerformedSwaps = await performedSwaps(); 80 | chainId = BigNumber.from((await ethers.provider.getNetwork()).chainId); 81 | 82 | const bondTime = await keep3rV2.bondTime(); 83 | await keep3rV2.connect(keeper).bond(K3PR.address, 0); 84 | await evm.advanceTimeAndBlock(bondTime.toNumber()); 85 | await keep3rV2.connect(keeper).activate(K3PR.address); 86 | await keep3rV2.addJob(DCAKeep3rJob.address); 87 | 88 | snapshotId = await snapshot.take(); 89 | }); 90 | 91 | beforeEach(async () => { 92 | await snapshot.revert(snapshotId); 93 | }); 94 | 95 | describe('work', () => { 96 | when("job doesn't have credits", () => { 97 | let workTx: Promise; 98 | given(async () => { 99 | const { 100 | data, 101 | signature: { r, v, s }, 102 | } = await generateCallAndSignature(); 103 | workTx = DCAKeep3rJob.connect(keeper).work(data, v, r, s); 104 | }); 105 | then('tx is reverted', async () => { 106 | await expect(workTx).to.be.reverted; 107 | }); 108 | }); 109 | 110 | when('job has credits and is worked by a keeper', () => { 111 | let initialBonds: BigNumber, initialCredits: BigNumber; 112 | given(async () => { 113 | await keep3rV2.connect(keep3rGovernance).forceLiquidityCreditsToJob(DCAKeep3rJob.address, utils.parseEther('10')); 114 | 115 | // Remember initial bonds and credits 116 | initialBonds = await keep3rV2.bonds(keeper.address, K3PR.address); 117 | initialCredits = await keep3rV2.jobLiquidityCredits(DCAKeep3rJob.address); 118 | 119 | // Execute work 120 | const { 121 | data, 122 | signature: { v, r, s }, 123 | } = await generateCallAndSignature(); 124 | await DCAKeep3rJob.connect(keeper).work(data, v, r, s); 125 | }); 126 | then('credits are transfered to keeper as bonds', async () => { 127 | const bonds = await keep3rV2.bonds(keeper.address, K3PR.address); 128 | const credits = await keep3rV2.jobLiquidityCredits(DCAKeep3rJob.address); 129 | const liquidityCreditsSpent = initialCredits.sub(credits); 130 | const bondsEarned = bonds.sub(initialBonds); 131 | expect(liquidityCreditsSpent).to.be.eq(bondsEarned); 132 | expect(liquidityCreditsSpent).to.be.gt(0); 133 | }); 134 | then('swap gets executed', async () => { 135 | expect(await performedSwaps()).to.equal(initialPerformedSwaps + 1); 136 | }); 137 | }); 138 | }); 139 | 140 | async function generateCallAndSignature() { 141 | const quote = await buildSDK() 142 | .quoteService.getAllQuotes({ 143 | request: { 144 | chainId: 1, 145 | sellToken: WETH_ADDRESS, 146 | buyToken: USDC_ADDRESS, 147 | order: { type: 'sell', sellAmount: utils.parseEther('0.1').toBigInt() }, 148 | slippagePercentage: 0.01, 149 | takerAddress: thirdPartySwapper.address, 150 | }, 151 | config: { 152 | timeout: '3s', 153 | }, 154 | }) 155 | .then((quotes) => quotes[0]); 156 | 157 | const bytes = encodeSwap({ 158 | allowanceTargets: [{ token: quote.sellToken.address, spender: quote.source.allowanceTarget }], 159 | executions: [{ swapper: quote.tx.to, data: quote.tx.data }], 160 | leftoverRecipient: keeper, 161 | extraTokens: [], 162 | }); 163 | 164 | const swapTx = await DCAHub.populateTransaction.swap( 165 | [USDC_ADDRESS, WETH_ADDRESS], 166 | [{ indexTokenA: 0, indexTokenB: 1 }], 167 | thirdPartySwapper.address, 168 | thirdPartySwapper.address, 169 | [0, 0], 170 | bytes, 171 | [] 172 | ); 173 | 174 | const signature = await getSignature({ 175 | signer, 176 | swapper: DCAHub.address, 177 | data: swapTx.data!, 178 | nonce: 0, 179 | chainId, 180 | }); 181 | 182 | return { data: swapTx.data!, signature }; 183 | } 184 | 185 | async function performedSwaps(): Promise { 186 | const { performedSwaps } = await DCAHub.swapData(USDC_ADDRESS, WETH_ADDRESS, SwapInterval.ONE_DAY.mask); 187 | return performedSwaps; 188 | } 189 | 190 | const Work = [ 191 | { name: 'swapper', type: 'address' }, 192 | { name: 'data', type: 'bytes' }, 193 | { name: 'nonce', type: 'uint256' }, 194 | ]; 195 | 196 | async function getSignature(options: OperationData) { 197 | const { domain, types, value } = buildWorkData(options); 198 | const signature = await options.signer._signTypedData(domain, types, value); 199 | return fromRpcSig(signature); 200 | } 201 | 202 | function buildWorkData(options: OperationData) { 203 | return { 204 | primaryType: 'Work', 205 | types: { Work }, 206 | domain: { name: 'Mean Finance - DCA Keep3r Job', version: '1', chainId: options.chainId, verifyingContract: DCAKeep3rJob.address }, 207 | value: { swapper: options.swapper, data: options.data, nonce: options.nonce }, 208 | }; 209 | } 210 | 211 | type OperationData = { 212 | signer: SignerWithAddress; 213 | swapper: string; 214 | data: BytesLike; 215 | nonce: BigNumberish; 216 | chainId: BigNumberish; 217 | }; 218 | 219 | type SwapData = { 220 | allowanceTargets: { token: string; spender: string }[]; 221 | executions: { data: BytesLike; swapper: string }[]; 222 | extraTokens: string[]; 223 | leftoverRecipient: { address: string }; 224 | }; 225 | function encodeSwap(bytes: SwapData) { 226 | const abiCoder = new utils.AbiCoder(); 227 | return abiCoder.encode( 228 | ['tuple(bool, uint256, tuple(address, address)[], tuple(address, uint256, bytes)[], address[], address)'], 229 | [ 230 | [ 231 | false, 232 | constants.MAX_UINT_256, 233 | bytes.allowanceTargets.map(({ token, spender }) => [token, spender]), 234 | bytes.executions.map(({ swapper, data }) => [swapper, 0, data]), 235 | bytes.extraTokens, 236 | bytes.leftoverRecipient.address, 237 | ], 238 | ] 239 | ); 240 | } 241 | }); 242 | -------------------------------------------------------------------------------- /test/integration/fork-block-numbers.ts: -------------------------------------------------------------------------------- 1 | const forkBlockNumber = { 2 | 'dca-fee-manager': 15583285, // Ethereum 3 | 'swap-for-caller': 15583285, // Ethereum 4 | 'position-migrator': 24283642, // Optimism 5 | }; 6 | 7 | export default forkBlockNumber; 8 | -------------------------------------------------------------------------------- /test/integration/utils.ts: -------------------------------------------------------------------------------- 1 | import { DeterministicFactory, DeterministicFactory__factory } from '@mean-finance/deterministic-factory'; 2 | import { address as DETERMINISTIC_FACTORY_ADDRESS } from '@mean-finance/deterministic-factory/deployments/ethereum/DeterministicFactory.json'; 3 | import { wallet } from '@test-utils'; 4 | import { getNamedAccounts, deployments, ethers } from 'hardhat'; 5 | import { JsonRpcSigner } from '@ethersproject/providers'; 6 | 7 | export async function deploy(...contracts: string[]): Promise<{ msig: JsonRpcSigner; eoaAdmin: JsonRpcSigner; timelock: JsonRpcSigner }> { 8 | const { msig } = await getNamedAccounts(); 9 | return deployWithAddress(msig, ...contracts); 10 | } 11 | 12 | export async function deployWithAddress( 13 | deployerAddress: string, 14 | ...contracts: string[] 15 | ): Promise<{ msig: JsonRpcSigner; eoaAdmin: JsonRpcSigner; timelock: JsonRpcSigner }> { 16 | const { eoaAdmin: eoaAdminAddress, deployer, msig: msigAddress } = await getNamedAccounts(); 17 | const eoaAdmin = await wallet.impersonate(eoaAdminAddress); 18 | const msig = await wallet.impersonate(msigAddress); 19 | const deployerAdmin = await wallet.impersonate(deployerAddress); 20 | await ethers.provider.send('hardhat_setBalance', [eoaAdminAddress, '0xffffffffffffffff']); 21 | await ethers.provider.send('hardhat_setBalance', [msigAddress, '0xffffffffffffffff']); 22 | await ethers.provider.send('hardhat_setBalance', [deployerAddress, '0xffffffffffffffff']); 23 | 24 | const deterministicFactory = await ethers.getContractAt( 25 | DeterministicFactory__factory.abi, 26 | DETERMINISTIC_FACTORY_ADDRESS 27 | ); 28 | 29 | await deterministicFactory.connect(deployerAdmin).grantRole(await deterministicFactory.DEPLOYER_ROLE(), deployer); 30 | await deployments.run( 31 | [ 32 | 'DCAHubPositionDescriptor', 33 | 'ChainlinkFeedRegistry', 34 | 'TransformerOracle', 35 | 'ProtocolTokenWrapperTransformer', 36 | 'TransformerRegistry', 37 | 'DCAHub', 38 | 'CallerOnlyDCAHubSwapper', 39 | ...contracts, 40 | ], 41 | { 42 | resetMemory: true, 43 | deletePreviousDeployments: false, 44 | writeDeploymentsToFiles: false, 45 | } 46 | ); 47 | 48 | const timelockContract = await ethers.getContract('Timelock'); 49 | const timelock = await wallet.impersonate(timelockContract.address); 50 | await ethers.provider.send('hardhat_setBalance', [timelockContract.address, '0xffffffffffffffff']); 51 | return { msig, eoaAdmin, timelock }; 52 | } 53 | -------------------------------------------------------------------------------- /test/unit/DCAFeeManager/dca-fee-manager.spec.ts: -------------------------------------------------------------------------------- 1 | import chai, { expect } from 'chai'; 2 | import { ethers } from 'hardhat'; 3 | import { contract, given, then, when } from '@test-utils/bdd'; 4 | import { snapshot } from '@test-utils/evm'; 5 | import { DCAFeeManagerMock, DCAFeeManagerMock__factory, IDCAHub, IDCAHubPositionHandler, IERC20 } from '@typechained'; 6 | import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; 7 | import { duration } from 'moment'; 8 | import { behaviours, wallet } from '@test-utils'; 9 | import { IDCAFeeManager } from '@typechained/contracts/DCAFeeManager/DCAFeeManager'; 10 | import { FakeContract, smock } from '@defi-wonderland/smock'; 11 | import { BigNumber, BigNumberish, constants, utils } from 'ethers'; 12 | 13 | chai.use(smock.matchers); 14 | 15 | contract('DCAFeeManager', () => { 16 | const TOKEN_A = '0x0000000000000000000000000000000000000010'; 17 | const TOKEN_B = '0x0000000000000000000000000000000000000011'; 18 | const MAX_SHARES = 10000; 19 | const SWAP_INTERVAL = duration(1, 'day').asSeconds(); 20 | let DCAHub: FakeContract; 21 | let DCAFeeManager: DCAFeeManagerMock; 22 | let DCAFeeManagerFactory: DCAFeeManagerMock__factory; 23 | let erc20Token: FakeContract; 24 | let random: SignerWithAddress, superAdmin: SignerWithAddress, admin: SignerWithAddress; 25 | let superAdminRole: string, adminRole: string; 26 | let snapshotId: string; 27 | 28 | before('Setup accounts and contracts', async () => { 29 | [random, superAdmin, admin] = await ethers.getSigners(); 30 | DCAHub = await smock.fake('IDCAHub'); 31 | erc20Token = await smock.fake('IERC20'); 32 | DCAFeeManagerFactory = await ethers.getContractFactory('contracts/mocks/DCAFeeManager/DCAFeeManager.sol:DCAFeeManagerMock'); 33 | DCAFeeManager = await DCAFeeManagerFactory.deploy(superAdmin.address, [admin.address]); 34 | superAdminRole = await DCAFeeManager.SUPER_ADMIN_ROLE(); 35 | adminRole = await DCAFeeManager.ADMIN_ROLE(); 36 | snapshotId = await snapshot.take(); 37 | }); 38 | 39 | beforeEach(async () => { 40 | await snapshot.revert(snapshotId); 41 | DCAHub.platformBalance.reset(); 42 | DCAHub.withdrawFromPlatformBalance.reset(); 43 | DCAHub.withdrawSwappedMany.reset(); 44 | DCAHub['deposit(address,address,uint256,uint32,uint32,address,(address,uint8[])[])'].reset(); 45 | DCAHub.increasePosition.reset(); 46 | DCAHub.terminate.reset(); 47 | erc20Token.allowance.reset(); 48 | erc20Token.approve.reset(); 49 | erc20Token.transfer.reset(); 50 | erc20Token.approve.returns(true); 51 | }); 52 | 53 | describe('constructor', () => { 54 | when('super admin is zero address', () => { 55 | then('tx is reverted with reason error', async () => { 56 | await behaviours.deployShouldRevertWithMessage({ 57 | contract: DCAFeeManagerFactory, 58 | args: [constants.AddressZero, []], 59 | message: 'ZeroAddress', 60 | }); 61 | }); 62 | }); 63 | when('contract is initiated', () => { 64 | then('super admin is set correctly', async () => { 65 | const hasRole = await DCAFeeManager.hasRole(superAdminRole, superAdmin.address); 66 | expect(hasRole).to.be.true; 67 | }); 68 | then('initial admins are set correctly', async () => { 69 | const hasRole = await DCAFeeManager.hasRole(adminRole, admin.address); 70 | expect(hasRole).to.be.true; 71 | }); 72 | then('super admin role is set as admin for super admin role', async () => { 73 | const admin = await DCAFeeManager.getRoleAdmin(superAdminRole); 74 | expect(admin).to.equal(superAdminRole); 75 | }); 76 | then('super admin role is set as admin for admin role', async () => { 77 | const admin = await DCAFeeManager.getRoleAdmin(adminRole); 78 | expect(admin).to.equal(superAdminRole); 79 | }); 80 | }); 81 | }); 82 | 83 | describe('runSwapsAndTransferMany', () => { 84 | // Note: we can't test that the underlying function was called 85 | behaviours.shouldBeExecutableOnlyByRole({ 86 | contract: () => DCAFeeManager, 87 | funcAndSignature: 'runSwapsAndTransferMany', 88 | params: () => [ 89 | { 90 | allowanceTargets: [], 91 | swappers: [], 92 | swaps: [], 93 | swapContext: [], 94 | transferOutBalance: [], 95 | }, 96 | ], 97 | addressWithRole: () => admin, 98 | role: () => adminRole, 99 | }); 100 | }); 101 | 102 | describe('withdrawFromPlatformBalance', () => { 103 | const RECIPIENT = wallet.generateRandomAddress(); 104 | when('withdraw is executed', () => { 105 | const AMOUNT_TO_WITHDRAW = [{ token: TOKEN_A, amount: utils.parseEther('1') }]; 106 | given(async () => { 107 | await DCAFeeManager.connect(admin).withdrawFromPlatformBalance(DCAHub.address, AMOUNT_TO_WITHDRAW, RECIPIENT); 108 | }); 109 | then('hub is called correctly', () => { 110 | expect(DCAHub.withdrawFromPlatformBalance).to.have.been.calledOnce; 111 | const [amountToWithdraw, recipient] = DCAHub.withdrawFromPlatformBalance.getCall(0).args as [AmountToWithdraw[], string]; 112 | expectAmounToWithdrawToBe(amountToWithdraw, AMOUNT_TO_WITHDRAW); 113 | expect(recipient).to.equal(RECIPIENT); 114 | }); 115 | }); 116 | behaviours.shouldBeExecutableOnlyByRole({ 117 | contract: () => DCAFeeManager, 118 | funcAndSignature: 'withdrawFromPlatformBalance', 119 | params: () => [DCAHub.address, [], RECIPIENT], 120 | addressWithRole: () => admin, 121 | role: () => adminRole, 122 | }); 123 | }); 124 | 125 | describe('withdrawFromBalance', () => { 126 | const RECIPIENT = wallet.generateRandomAddress(); 127 | when('withdraw is executed', () => { 128 | const AMOUNT_TO_WITHDRAW = utils.parseEther('1'); 129 | given(async () => { 130 | await DCAFeeManager.connect(admin).withdrawFromBalance([{ token: erc20Token.address, amount: AMOUNT_TO_WITHDRAW }], RECIPIENT); 131 | }); 132 | then('internal function is called correctly', async () => { 133 | const calls = await DCAFeeManager.sendToRecipientCalls(); 134 | expect(calls).to.have.lengthOf(1); 135 | expect(calls[0].token).to.equal(erc20Token.address); 136 | expect(calls[0].amount).to.equal(AMOUNT_TO_WITHDRAW); 137 | expect(calls[0].recipient).to.equal(RECIPIENT); 138 | expect(await DCAFeeManager.sendBalanceOnContractToRecipientCalls()).to.be.empty; 139 | }); 140 | }); 141 | when('withdraw with max(uint256) is executed', () => { 142 | given(async () => { 143 | await DCAFeeManager.connect(admin).withdrawFromBalance([{ token: erc20Token.address, amount: constants.MaxUint256 }], RECIPIENT); 144 | }); 145 | then('internal function is called correctly', async () => { 146 | const calls = await DCAFeeManager.sendBalanceOnContractToRecipientCalls(); 147 | expect(calls).to.have.lengthOf(1); 148 | expect(calls[0].token).to.equal(erc20Token.address); 149 | expect(calls[0].recipient).to.equal(RECIPIENT); 150 | expect(await DCAFeeManager.sendToRecipientCalls()).to.be.empty; 151 | }); 152 | }); 153 | behaviours.shouldBeExecutableOnlyByRole({ 154 | contract: () => DCAFeeManager, 155 | funcAndSignature: 'withdrawFromBalance', 156 | params: [[], RECIPIENT], 157 | addressWithRole: () => admin, 158 | role: () => adminRole, 159 | }); 160 | }); 161 | 162 | describe('availableBalances', () => { 163 | when('function is executed', () => { 164 | const PLATFORM_BALANCE = utils.parseEther('1'); 165 | const FEE_MANAGER_BALANCE = utils.parseEther('2'); 166 | given(async () => { 167 | DCAHub.platformBalance.returns(PLATFORM_BALANCE); 168 | erc20Token.balanceOf.returns(FEE_MANAGER_BALANCE); 169 | }); 170 | then('balances are returned correctly', async () => { 171 | const balances = await DCAFeeManager.availableBalances(DCAHub.address, [erc20Token.address]); 172 | expect(balances).to.have.lengthOf(1); 173 | expect(balances[0].token).to.equal(erc20Token.address); 174 | expect(balances[0].platformBalance).to.equal(PLATFORM_BALANCE); 175 | expect(balances[0].feeManagerBalance).to.equal(FEE_MANAGER_BALANCE); 176 | }); 177 | }); 178 | }); 179 | 180 | describe('revokeAllowances', () => { 181 | when('allowance is revoked', () => { 182 | given(async () => { 183 | await DCAFeeManager.connect(admin).revokeAllowances([{ spender: random.address, tokens: [erc20Token.address] }]); 184 | }); 185 | then('revoke was called correctly', async () => { 186 | const calls = await DCAFeeManager.revokeAllowancesCalls(); 187 | expect(calls).to.have.lengthOf(1); 188 | expect(calls[0]).to.have.lengthOf(1); 189 | expect((calls[0][0] as any).spender).to.equal(random.address); 190 | expect((calls[0][0] as any).tokens).to.eql([erc20Token.address]); 191 | }); 192 | }); 193 | behaviours.shouldBeExecutableOnlyByRole({ 194 | contract: () => DCAFeeManager, 195 | funcAndSignature: 'revokeAllowances', 196 | params: [[]], 197 | addressWithRole: () => admin, 198 | role: () => adminRole, 199 | }); 200 | }); 201 | 202 | type AmountToWithdraw = { token: string; amount: BigNumberish }; 203 | function expectAmounToWithdrawToBe(actual: AmountToWithdraw[], expected: AmountToWithdraw[]) { 204 | expect(actual).to.have.lengthOf(expected.length); 205 | for (let i = 0; i < actual.length; i++) { 206 | expect(actual[i].token).to.equal(expected[i].token); 207 | expect(actual[i].amount).to.equal(expected[i].amount); 208 | } 209 | } 210 | 211 | type PositionSet = { token: string; positionIds: BigNumberish[] }; 212 | function expectPositionSetsToBe(actual: PositionSet[], expected: PositionSet[]) { 213 | expect(actual).to.have.lengthOf(expected.length); 214 | for (let i = 0; i < actual.length; i++) { 215 | expect(actual[i].token).to.equal(expected[i].token); 216 | expect(actual[i].positionIds).to.have.lengthOf(expected[i].positionIds.length); 217 | for (let j = 0; j < actual[i].positionIds.length; j++) { 218 | expect(actual[i].positionIds[j]).to.equal(expected[i].positionIds[j]); 219 | } 220 | } 221 | } 222 | }); 223 | -------------------------------------------------------------------------------- /test/unit/DCAHubSwapper/caller-only-dca-hub-swapper.spec.ts: -------------------------------------------------------------------------------- 1 | import chai, { expect } from 'chai'; 2 | import { ethers } from 'hardhat'; 3 | import { behaviours, constants, wallet } from '@test-utils'; 4 | import { contract, given, then, when } from '@test-utils/bdd'; 5 | import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; 6 | import { snapshot } from '@test-utils/evm'; 7 | import { CallerOnlyDCAHubSwapperMock, CallerOnlyDCAHubSwapperMock__factory, IDCAHubWithAccessControl, IERC20 } from '@typechained'; 8 | import { FakeContract, smock } from '@defi-wonderland/smock'; 9 | import { BigNumberish } from '@ethersproject/bignumber'; 10 | import { BytesLike } from '@ethersproject/bytes'; 11 | import { utils } from 'ethers'; 12 | 13 | chai.use(smock.matchers); 14 | 15 | contract('CallerOnlyDCAHubSwapper', () => { 16 | const BYTES = utils.hexlify(utils.randomBytes(10)); 17 | let swapExecutioner: SignerWithAddress, recipient: SignerWithAddress, admin: SignerWithAddress, superAdmin: SignerWithAddress; 18 | let DCAHub: FakeContract; 19 | let DCAHubSwapperFactory: CallerOnlyDCAHubSwapperMock__factory; 20 | let DCAHubSwapper: CallerOnlyDCAHubSwapperMock; 21 | let tokenA: FakeContract, tokenB: FakeContract, intermediateToken: FakeContract; 22 | let snapshotId: string; 23 | 24 | const INDEXES = [{ indexTokenA: 0, indexTokenB: 1 }]; 25 | let tokens: string[]; 26 | 27 | before('Setup accounts and contracts', async () => { 28 | [, swapExecutioner, admin, recipient, superAdmin] = await ethers.getSigners(); 29 | DCAHubSwapperFactory = await ethers.getContractFactory( 30 | 'contracts/mocks/DCAHubSwapper/CallerOnlyDCAHubSwapper.sol:CallerOnlyDCAHubSwapperMock' 31 | ); 32 | DCAHub = await smock.fake('contracts/interfaces/ICallerOnlyDCAHubSwapper.sol:IDCAHubWithAccessControl'); 33 | DCAHubSwapper = await DCAHubSwapperFactory.deploy(); 34 | tokenA = await smock.fake('IERC20'); 35 | tokenB = await smock.fake('IERC20'); 36 | intermediateToken = await smock.fake('IERC20'); 37 | tokens = [tokenA.address, tokenB.address]; 38 | snapshotId = await snapshot.take(); 39 | }); 40 | 41 | beforeEach('Deploy and configure', async () => { 42 | await snapshot.revert(snapshotId); 43 | DCAHub.swap.reset(); 44 | tokenA.transfer.reset(); 45 | tokenA.transfer.returns(true); 46 | tokenB.transfer.returns(true); 47 | tokenA.transferFrom.returns(true); 48 | tokenB.transferFrom.returns(true); 49 | DCAHub.hasRole.returns(true); 50 | }); 51 | describe('swapForCaller', () => { 52 | const SOME_RANDOM_ADDRESS = wallet.generateRandomAddress(); 53 | whenDeadlineHasExpiredThenTxReverts({ 54 | func: 'swapForCaller', 55 | args: () => [ 56 | { 57 | hub: DCAHub.address, 58 | tokens, 59 | pairsToSwap: INDEXES, 60 | oracleData: BYTES, 61 | minimumOutput: [], 62 | maximumInput: [], 63 | recipient: SOME_RANDOM_ADDRESS, 64 | deadline: 0, 65 | }, 66 | ], 67 | }); 68 | when('caller doesnt have privilege', () => { 69 | given(() => { 70 | DCAHub.hasRole.returns(false); 71 | }); 72 | then('tx reverts', async () => { 73 | await behaviours.txShouldRevertWithMessage({ 74 | contract: DCAHubSwapper, 75 | func: 'swapForCaller', 76 | args: [ 77 | { 78 | hub: DCAHub.address, 79 | tokens, 80 | pairsToSwap: INDEXES, 81 | oracleData: BYTES, 82 | minimumOutput: [], 83 | maximumInput: [], 84 | recipient: SOME_RANDOM_ADDRESS, 85 | deadline: constants.MAX_UINT_256, 86 | }, 87 | ], 88 | message: 'NotPrivilegedSwapper', 89 | }); 90 | }); 91 | }); 92 | when('hub returns less than minimum output', () => { 93 | const MIN_OUTPUT = 200000; 94 | const MAX_INPUT = constants.MAX_UINT_256; 95 | given(() => { 96 | DCAHub.swap.returns({ 97 | tokens: [ 98 | { 99 | token: tokenA.address, 100 | reward: MIN_OUTPUT - 1, 101 | toProvide: MAX_INPUT, 102 | platformFee: 0, 103 | }, 104 | { 105 | token: tokenB.address, 106 | reward: MIN_OUTPUT - 1, 107 | toProvide: MAX_INPUT, 108 | platformFee: 0, 109 | }, 110 | ], 111 | pairs: [], 112 | }); 113 | }); 114 | then('reverts with message', async () => { 115 | await behaviours.txShouldRevertWithMessage({ 116 | contract: DCAHubSwapper.connect(swapExecutioner), 117 | func: 'swapForCaller', 118 | args: [ 119 | { 120 | hub: DCAHub.address, 121 | tokens, 122 | pairsToSwap: INDEXES, 123 | oracleData: BYTES, 124 | minimumOutput: [MIN_OUTPUT, MIN_OUTPUT], 125 | maximumInput: [MAX_INPUT, MAX_INPUT], 126 | recipient: SOME_RANDOM_ADDRESS, 127 | deadline: constants.MAX_UINT_256, 128 | }, 129 | ], 130 | message: 'RewardNotEnough', 131 | }); 132 | }); 133 | }); 134 | when('hub asks for more than maximum input', () => { 135 | const MIN_OUTPUT = 200000; 136 | const MAX_INPUT = 5000000; 137 | given(() => { 138 | DCAHub.swap.returns({ 139 | tokens: [ 140 | { 141 | token: tokenA.address, 142 | reward: MIN_OUTPUT, 143 | toProvide: MAX_INPUT + 1, 144 | platformFee: 0, 145 | }, 146 | { 147 | token: tokenB.address, 148 | reward: MIN_OUTPUT, 149 | toProvide: MAX_INPUT + 1, 150 | platformFee: 0, 151 | }, 152 | ], 153 | pairs: [], 154 | }); 155 | }); 156 | then('reverts with message', async () => { 157 | await behaviours.txShouldRevertWithMessage({ 158 | contract: DCAHubSwapper.connect(swapExecutioner), 159 | func: 'swapForCaller', 160 | args: [ 161 | { 162 | hub: DCAHub.address, 163 | tokens, 164 | pairsToSwap: INDEXES, 165 | oracleData: BYTES, 166 | minimumOutput: [MIN_OUTPUT, MIN_OUTPUT], 167 | maximumInput: [MAX_INPUT, MAX_INPUT], 168 | recipient: SOME_RANDOM_ADDRESS, 169 | deadline: constants.MAX_UINT_256, 170 | }, 171 | ], 172 | message: 'ToProvideIsTooMuch', 173 | }); 174 | }); 175 | }); 176 | when('swap is executed', () => { 177 | given(async () => { 178 | await DCAHubSwapper.connect(swapExecutioner).swapForCaller({ 179 | hub: DCAHub.address, 180 | tokens, 181 | pairsToSwap: INDEXES, 182 | oracleData: BYTES, 183 | minimumOutput: [], 184 | maximumInput: [], 185 | recipient: SOME_RANDOM_ADDRESS, 186 | deadline: constants.MAX_UINT_256, 187 | }); 188 | }); 189 | thenHubIsCalledWith({ 190 | rewardRecipient: SOME_RANDOM_ADDRESS, 191 | oracleData: BYTES, 192 | }); 193 | then('swap executor is cleared', async () => { 194 | expect(await DCAHubSwapper.isSwapExecutorEmpty()).to.be.true; 195 | }); 196 | }); 197 | }); 198 | describe('DCAHubSwapCall', () => { 199 | let tokensInSwap: { token: string; toProvide: BigNumberish; reward: BigNumberish; platformFee: BigNumberish }[]; 200 | let hub: SignerWithAddress; 201 | given(async () => { 202 | tokensInSwap = [ 203 | { token: tokenB.address, toProvide: utils.parseEther('0.1'), reward: 0, platformFee: 0 }, 204 | { token: tokenA.address, toProvide: utils.parseEther('20'), reward: 0, platformFee: 0 }, 205 | ]; 206 | hub = await ethers.getSigner(DCAHub.address); 207 | await wallet.setBalance({ account: hub.address, balance: utils.parseEther('1') }); 208 | }); 209 | when('swap for caller', () => { 210 | given(async () => { 211 | await DCAHubSwapper.setSwapExecutor(swapExecutioner.address); 212 | await DCAHubSwapper.connect(hub).DCAHubSwapCall(DCAHubSwapper.address, tokensInSwap, [], []); 213 | }); 214 | then('tokens are sent from the swap executor to the hub correctly', () => { 215 | for (const tokenInSwap of tokensInSwap) { 216 | const token = fromAddressToToken(tokenInSwap.token); 217 | expect(token.transferFrom).to.have.been.calledWith(swapExecutioner.address, hub.address, tokenInSwap.toProvide); 218 | } 219 | }); 220 | }); 221 | }); 222 | function fromAddressToToken(tokenAddress: string): FakeContract { 223 | switch (tokenAddress) { 224 | case tokenA.address: 225 | return tokenA; 226 | case tokenB.address: 227 | return tokenB; 228 | } 229 | throw new Error('Unknown address'); 230 | } 231 | function whenDeadlineHasExpiredThenTxReverts({ func, args }: { func: keyof CallerOnlyDCAHubSwapperMock['functions']; args: () => any[] }) { 232 | when('deadline has expired', () => { 233 | then('reverts with message', async () => { 234 | await behaviours.txShouldRevertWithMessage({ 235 | contract: DCAHubSwapper.connect(swapExecutioner), 236 | func, 237 | args: args(), 238 | message: 'Transaction too old', 239 | }); 240 | }); 241 | }); 242 | } 243 | function thenHubIsCalledWith({ 244 | oracleData: expectedOracleData, 245 | rewardRecipient: expectedRewardRecipient, 246 | }: { 247 | rewardRecipient: string | (() => { address: string }); 248 | oracleData: BytesLike; 249 | }) { 250 | then('hub was called with the correct parameters', () => { 251 | expect(DCAHub.swap).to.have.been.calledOnce; 252 | const [tokensInHub, indexes, rewardRecipient, callbackHandler, borrow, callbackData, oracleData] = DCAHub.swap.getCall(0).args; 253 | expect(tokensInHub).to.eql(tokens); 254 | expect((indexes as any)[0]).to.eql([0, 1]); 255 | expect(rewardRecipient).to.equal( 256 | typeof expectedRewardRecipient === 'string' ? expectedRewardRecipient : expectedRewardRecipient().address 257 | ); 258 | expect(callbackHandler).to.equal(DCAHubSwapper.address); 259 | expect(borrow).to.have.lengthOf(2); 260 | expect((borrow as any)[0]).to.equal(constants.ZERO); 261 | expect((borrow as any)[1]).to.equal(constants.ZERO); 262 | expect(callbackData).to.equal('0x'); 263 | expect(oracleData).to.equal(expectedOracleData); 264 | }); 265 | } 266 | }); 267 | -------------------------------------------------------------------------------- /test/unit/libraries/input-building.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { ethers } from 'hardhat'; 3 | import { contract, then, when } from '@test-utils/bdd'; 4 | import { snapshot } from '@test-utils/evm'; 5 | import { InputBuildingMock, InputBuildingMock__factory } from '@typechained'; 6 | import { BigNumber } from '@ethersproject/bignumber'; 7 | 8 | contract('InputBuilding', () => { 9 | const TOKEN_A = '0x0000000000000000000000000000000000000001'; 10 | const TOKEN_B = '0x0000000000000000000000000000000000000002'; 11 | const TOKEN_C = '0x0000000000000000000000000000000000000003'; 12 | const TOKEN_D = '0x0000000000000000000000000000000000000004'; 13 | 14 | let inputBuilding: InputBuildingMock; 15 | let snapshotId: string; 16 | 17 | before('Setup accounts and contracts', async () => { 18 | const InputBuildingFactory: InputBuildingMock__factory = await ethers.getContractFactory( 19 | 'contracts/mocks/libraries/InputBuilding.sol:InputBuildingMock' 20 | ); 21 | inputBuilding = await InputBuildingFactory.deploy(); 22 | snapshotId = await snapshot.take(); 23 | }); 24 | 25 | beforeEach('Deploy and configure', async () => { 26 | await snapshot.revert(snapshotId); 27 | }); 28 | 29 | describe('Swap Utils', () => { 30 | describe('buildGetNextSwapInfoInput', () => { 31 | when('no pairs are given', () => { 32 | then('the result is empty', async () => { 33 | const [tokens, pairIndexes] = await inputBuilding.buildGetNextSwapInfoInput([]); 34 | expect(tokens).to.be.empty; 35 | expect(pairIndexes).to.be.empty; 36 | }); 37 | }); 38 | 39 | when('one pair has the same token', () => { 40 | then('the result is returned correctly', async () => { 41 | const pair = { tokenA: TOKEN_A, tokenB: TOKEN_A }; 42 | const [tokens, pairIndexes] = await inputBuilding.buildGetNextSwapInfoInput([pair]); 43 | expect(tokens).to.eql([TOKEN_A, TOKEN_A]); 44 | expect(pairIndexes).to.eql([[0, 1]]); 45 | }); 46 | }); 47 | 48 | when('there are duplicated pairs', () => { 49 | then('the result is returned correctly', async () => { 50 | const pair = { tokenA: TOKEN_A, tokenB: TOKEN_B }; 51 | const [tokens, pairIndexes] = await inputBuilding.buildGetNextSwapInfoInput([pair, pair]); 52 | expect(tokens).to.eql([TOKEN_A, TOKEN_B]); 53 | expect(pairIndexes).to.eql([ 54 | [0, 1], 55 | [0, 1], 56 | ]); 57 | }); 58 | }); 59 | 60 | when('there are duplicated pairs', () => { 61 | then('the result is returned correctly', async () => { 62 | const pair1 = { tokenA: TOKEN_A, tokenB: TOKEN_B }; 63 | const pair2 = { tokenA: TOKEN_B, tokenB: TOKEN_A }; 64 | const [tokens, pairIndexes] = await inputBuilding.buildGetNextSwapInfoInput([pair1, pair2]); 65 | expect(tokens).to.eql([TOKEN_A, TOKEN_B]); 66 | expect(pairIndexes).to.eql([ 67 | [0, 1], 68 | [0, 1], 69 | ]); 70 | }); 71 | }); 72 | 73 | when('one pair is provided', () => { 74 | then('the result is returned correctly', async () => { 75 | const pair = { tokenA: TOKEN_B, tokenB: TOKEN_A }; 76 | const [tokens, pairIndexes] = await inputBuilding.buildGetNextSwapInfoInput([pair]); 77 | expect(tokens).to.eql([TOKEN_A, TOKEN_B]); 78 | expect(pairIndexes).to.eql([[0, 1]]); 79 | }); 80 | }); 81 | 82 | when('multiple pairs are provided', () => { 83 | then('the result is returned correctly', async () => { 84 | const [tokens, pairIndexes] = await inputBuilding.buildGetNextSwapInfoInput([ 85 | { tokenA: TOKEN_C, tokenB: TOKEN_A }, 86 | { tokenA: TOKEN_B, tokenB: TOKEN_A }, 87 | { tokenA: TOKEN_D, tokenB: TOKEN_B }, 88 | { tokenA: TOKEN_D, tokenB: TOKEN_C }, 89 | { tokenA: TOKEN_B, tokenB: TOKEN_C }, 90 | ]); 91 | expect(tokens).to.eql([TOKEN_A, TOKEN_B, TOKEN_C, TOKEN_D]); 92 | expect(pairIndexes).to.eql([ 93 | [0, 1], 94 | [0, 2], 95 | [1, 2], 96 | [1, 3], 97 | [2, 3], 98 | ]); 99 | }); 100 | }); 101 | }); 102 | 103 | describe('buildSwapInput', () => { 104 | const ZERO = BigNumber.from(0); 105 | when('borrowing tokens that are also being swapped', () => { 106 | const BORROW_TOKEN_A = BigNumber.from(30); 107 | const BORROW_TOKEN_B = BigNumber.from(40); 108 | then('the result is returned correctly', async () => { 109 | const [tokens, pairIndexes, borrow] = await inputBuilding.buildSwapInput( 110 | [ 111 | { tokenA: TOKEN_C, tokenB: TOKEN_A }, 112 | { tokenA: TOKEN_B, tokenB: TOKEN_A }, 113 | ], 114 | [ 115 | { token: TOKEN_A, amount: BORROW_TOKEN_A }, 116 | { token: TOKEN_B, amount: BORROW_TOKEN_B }, 117 | ] 118 | ); 119 | expect(tokens).to.eql([TOKEN_A, TOKEN_B, TOKEN_C]); 120 | expect(pairIndexes).to.eql([ 121 | [0, 1], 122 | [0, 2], 123 | ]); 124 | expect(borrow).to.have.lengthOf(3); 125 | expect(borrow[0]).to.equal(BORROW_TOKEN_A); 126 | expect(borrow[1]).to.equal(BORROW_TOKEN_B); 127 | expect(borrow[2]).to.equal(ZERO); 128 | }); 129 | }); 130 | 131 | when('borrowing tokens that are not being swapped', () => { 132 | const BORROW_TOKEN_D = BigNumber.from(40); 133 | then('the result is returned correctly', async () => { 134 | const [tokens, pairIndexes, borrow] = await inputBuilding.buildSwapInput( 135 | [ 136 | { tokenA: TOKEN_C, tokenB: TOKEN_A }, 137 | { tokenA: TOKEN_B, tokenB: TOKEN_A }, 138 | ], 139 | [{ token: TOKEN_D, amount: BORROW_TOKEN_D }] 140 | ); 141 | expect(tokens).to.eql([TOKEN_A, TOKEN_B, TOKEN_C, TOKEN_D]); 142 | expect(pairIndexes).to.eql([ 143 | [0, 1], 144 | [0, 2], 145 | ]); 146 | expect(borrow).to.have.lengthOf(4); 147 | expect(borrow[0]).to.equal(ZERO); 148 | expect(borrow[1]).to.equal(ZERO); 149 | expect(borrow[2]).to.equal(ZERO); 150 | expect(borrow[3]).to.equal(BORROW_TOKEN_D); 151 | }); 152 | }); 153 | }); 154 | }); 155 | }); 156 | -------------------------------------------------------------------------------- /test/unit/libraries/modify-position-with-rate.spec.ts: -------------------------------------------------------------------------------- 1 | import chai, { expect } from 'chai'; 2 | import { ethers } from 'hardhat'; 3 | import { contract, given, then, when } from '@test-utils/bdd'; 4 | import { snapshot } from '@test-utils/evm'; 5 | import { IDCAHub, ModifyPositionWithRateMock, ModifyPositionWithRateMock__factory } from '@typechained'; 6 | import { FakeContract, smock } from '@defi-wonderland/smock'; 7 | import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; 8 | import constants from '@test-utils/constants'; 9 | 10 | chai.use(smock.matchers); 11 | 12 | contract('ModifyPositionWithRate', () => { 13 | const POSITION_ID = 1; 14 | const ORIGINAL_RATE = 100000; 15 | const SWAPS_LEFT = 10; 16 | 17 | let sender: SignerWithAddress; 18 | let DCAHub: FakeContract; 19 | let modifyPositionWithRate: ModifyPositionWithRateMock; 20 | let snapshotId: string; 21 | 22 | before('Setup accounts and contracts', async () => { 23 | [sender] = await ethers.getSigners(); 24 | const modifyPositionWithRateFactory: ModifyPositionWithRateMock__factory = await ethers.getContractFactory( 25 | 'contracts/mocks/libraries/ModifyPositionWithRate.sol:ModifyPositionWithRateMock' 26 | ); 27 | DCAHub = await smock.fake('IDCAHub'); 28 | DCAHub.userPosition.returns({ 29 | from: constants.NOT_ZERO_ADDRESS, 30 | to: constants.NOT_ZERO_ADDRESS, 31 | swapInterval: 10, 32 | swapsExecuted: 10, 33 | swapped: 10, 34 | swapsLeft: SWAPS_LEFT, 35 | remaining: SWAPS_LEFT * ORIGINAL_RATE, 36 | rate: ORIGINAL_RATE, 37 | }); 38 | modifyPositionWithRate = await modifyPositionWithRateFactory.deploy(); 39 | snapshotId = await snapshot.take(); 40 | }); 41 | 42 | beforeEach('Deploy and configure', async () => { 43 | await snapshot.revert(snapshotId); 44 | DCAHub.increasePosition.reset(); 45 | DCAHub.reducePosition.reset(); 46 | }); 47 | 48 | describe('modifyRate', () => { 49 | when('modifying rate and the new rate is bigger', () => { 50 | given(() => modifyPositionWithRate.modifyRate(DCAHub.address, POSITION_ID, ORIGINAL_RATE + 10)); 51 | thenPositionIsIncreased({ amount: 10 * SWAPS_LEFT, newSwaps: SWAPS_LEFT }); 52 | }); 53 | when('modifying rate and the new rate is the same as before', () => { 54 | given(() => modifyPositionWithRate.modifyRate(DCAHub.address, POSITION_ID, ORIGINAL_RATE)); 55 | thenNothingHappens(); 56 | }); 57 | when('modifying rate and the new rate is smaller', () => { 58 | given(() => modifyPositionWithRate.modifyRate(DCAHub.address, POSITION_ID, ORIGINAL_RATE - 10)); 59 | thenPositionIsReduced({ amount: 10 * SWAPS_LEFT, newSwaps: SWAPS_LEFT }); 60 | }); 61 | }); 62 | 63 | describe('modifySwaps', () => { 64 | when('modifying swaps and new amount of swaps is bigger', () => { 65 | given(() => modifyPositionWithRate.modifySwaps(DCAHub.address, POSITION_ID, SWAPS_LEFT + 5)); 66 | thenPositionIsIncreased({ amount: 5 * ORIGINAL_RATE, newSwaps: SWAPS_LEFT + 5 }); 67 | }); 68 | when('modifying swaps and new amount of swaps is the same as before', () => { 69 | given(() => modifyPositionWithRate.modifySwaps(DCAHub.address, POSITION_ID, SWAPS_LEFT)); 70 | thenNothingHappens(); 71 | }); 72 | when('modifying swaps and new amount of swaps is smaller', () => { 73 | given(() => modifyPositionWithRate.modifySwaps(DCAHub.address, POSITION_ID, SWAPS_LEFT - 5)); 74 | thenPositionIsReduced({ amount: 5 * ORIGINAL_RATE, newSwaps: SWAPS_LEFT - 5 }); 75 | }); 76 | }); 77 | 78 | describe('modifyRateAndSwaps', () => { 79 | when('modifying rate and swaps and both parameters are the same', () => { 80 | given(() => modifyPositionWithRate.modifyRateAndSwaps(DCAHub.address, POSITION_ID, ORIGINAL_RATE, SWAPS_LEFT)); 81 | thenNothingHappens(); 82 | }); 83 | when('rate and swaps and the number of funds needed increases', () => { 84 | given(() => modifyPositionWithRate.modifyRateAndSwaps(DCAHub.address, POSITION_ID, ORIGINAL_RATE + 10, SWAPS_LEFT + 2)); 85 | thenPositionIsIncreased({ amount: (ORIGINAL_RATE + 10) * (SWAPS_LEFT + 2) - ORIGINAL_RATE * SWAPS_LEFT, newSwaps: SWAPS_LEFT + 2 }); 86 | }); 87 | when('modifying rate and swaps and the number of funds needed decreases', () => { 88 | given(() => modifyPositionWithRate.modifyRateAndSwaps(DCAHub.address, POSITION_ID, ORIGINAL_RATE / 2, SWAPS_LEFT + 1)); 89 | thenPositionIsReduced({ amount: ORIGINAL_RATE * SWAPS_LEFT - (ORIGINAL_RATE / 2) * (SWAPS_LEFT + 1), newSwaps: SWAPS_LEFT + 1 }); 90 | }); 91 | when('modifying rate and swaps and the number of funds needes is the same', () => { 92 | given(() => modifyPositionWithRate.modifyRateAndSwaps(DCAHub.address, POSITION_ID, ORIGINAL_RATE / 2, SWAPS_LEFT * 2)); 93 | thenPositionIsIncreased({ amount: 0, newSwaps: SWAPS_LEFT * 2 }); 94 | }); 95 | }); 96 | 97 | function thenPositionIsIncreased({ amount, newSwaps }: { amount: number; newSwaps: number }) { 98 | then('position is increased', () => { 99 | expect(DCAHub.increasePosition).to.have.been.calledWith(POSITION_ID, amount, newSwaps); 100 | }); 101 | } 102 | 103 | function thenPositionIsReduced({ amount, newSwaps }: { amount: number; newSwaps: number }) { 104 | then('position is reduced', () => { 105 | expect(DCAHub.reducePosition).to.have.been.calledWith(POSITION_ID, amount, newSwaps, sender.address); 106 | }); 107 | } 108 | 109 | function thenNothingHappens() { 110 | then('position is increased', () => { 111 | expect(DCAHub.increasePosition).to.not.have.been.called; 112 | expect(DCAHub.reducePosition).to.not.have.been.called; 113 | }); 114 | } 115 | }); 116 | -------------------------------------------------------------------------------- /test/unit/utils/base-companion.spec.ts: -------------------------------------------------------------------------------- 1 | import chai, { expect } from 'chai'; 2 | import { ethers } from 'hardhat'; 3 | import { contract, given, then, when } from '@test-utils/bdd'; 4 | import { snapshot } from '@test-utils/evm'; 5 | import behaviors from '@test-utils/behaviours'; 6 | import { BaseCompanionMock, BaseCompanionMock__factory, IERC20, IPermit2, ISwapper } from '@typechained'; 7 | import { FakeContract, smock } from '@defi-wonderland/smock'; 8 | import { BytesLike, Wallet, constants } from 'ethers'; 9 | import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; 10 | 11 | chai.use(smock.matchers); 12 | 13 | contract('BaseCompanion', () => { 14 | const AMOUNT = 123456789; 15 | const RECIPIENT = Wallet.createRandom(); 16 | let token: FakeContract; 17 | let permit2: FakeContract; 18 | let swapper: FakeContract; 19 | let baseCompanion: BaseCompanionMock; 20 | let caller: SignerWithAddress, governor: SignerWithAddress; 21 | let snapshotId: string; 22 | 23 | before('Setup accounts and contracts', async () => { 24 | const baseCompanionFactory: BaseCompanionMock__factory = await ethers.getContractFactory('BaseCompanionMock'); 25 | token = await smock.fake('IERC20'); 26 | permit2 = await smock.fake('IPermit2'); 27 | swapper = await smock.fake('ISwapper'); 28 | [caller, governor] = await ethers.getSigners(); 29 | baseCompanion = await baseCompanionFactory.deploy(swapper.address, swapper.address, governor.address, permit2.address); 30 | snapshotId = await snapshot.take(); 31 | }); 32 | 33 | beforeEach(async () => { 34 | await snapshot.revert(snapshotId); 35 | token.transfer.reset(); 36 | token.balanceOf.reset(); 37 | token.approve.reset(); 38 | token.transferFrom.returns(true); 39 | token.transfer.returns(true); 40 | token.approve.returns(true); 41 | }); 42 | 43 | describe('sendToRecipient', () => { 44 | when('sending to a recipient', () => { 45 | given(async () => { 46 | await baseCompanion.sendToRecipient(token.address, AMOUNT, RECIPIENT.address); 47 | }); 48 | then('internal function is called correctly', async () => { 49 | const calls = await baseCompanion.sendToRecipientCalls(); 50 | expect(calls).to.have.lengthOf(1); 51 | expect(calls[0].token).to.equal(token.address); 52 | expect(calls[0].amount).to.equal(AMOUNT); 53 | expect(calls[0].recipient).to.equal(RECIPIENT.address); 54 | }); 55 | }); 56 | }); 57 | 58 | describe('runSwap', () => { 59 | let swapExecution: BytesLike; 60 | given(async () => { 61 | const { data } = await swapper.populateTransaction.swap(token.address, 1000, token.address); 62 | swapExecution = data!; 63 | }); 64 | when('swap is executed', () => { 65 | given(async () => { 66 | await baseCompanion.runSwap(token.address, 0, swapExecution, token.address); 67 | }); 68 | then('max approval is given', () => { 69 | expect(token.approve).to.have.been.calledOnceWith(swapper.address, constants.MaxUint256); 70 | }); 71 | then('swapper is called correctly', () => { 72 | expect(swapper.swap).to.have.been.calledWith(token.address, 1000, token.address); 73 | }); 74 | then('balance is checked correctly', () => { 75 | expect(token.balanceOf).to.have.been.calledOnceWith(baseCompanion.address); 76 | }); 77 | }); 78 | when('allowance token is not set', () => { 79 | given(async () => { 80 | await baseCompanion.runSwap(constants.AddressZero, 0, swapExecution, token.address); 81 | }); 82 | then('approve is not called', () => { 83 | expect(token.approve).to.not.have.been.called; 84 | }); 85 | }); 86 | }); 87 | 88 | describe('sendBalanceOnContractToRecipient', () => { 89 | when('sending balance on contract to a recipient', () => { 90 | given(async () => { 91 | await baseCompanion.sendBalanceOnContractToRecipient(token.address, RECIPIENT.address); 92 | }); 93 | then('internal function is called correctly', async () => { 94 | const calls = await baseCompanion.sendBalanceOnContractToRecipientCalls(); 95 | expect(calls).to.have.lengthOf(1); 96 | expect(calls[0].token).to.equal(token.address); 97 | expect(calls[0].recipient).to.equal(RECIPIENT.address); 98 | }); 99 | }); 100 | }); 101 | 102 | describe('permitTakeFromCaller', () => { 103 | when('taking from caller with permit', () => { 104 | given(async () => { 105 | await baseCompanion.permitTakeFromCaller(token.address, 12345, 678910, 2468, '0x1234', swapper.address); 106 | }); 107 | then('internal function is called correctly', async () => { 108 | expect(permit2['permitTransferFrom(((address,uint256),uint256,uint256),(address,uint256),address,bytes)']).to.have.been.calledOnce; 109 | const { 110 | args: [permit, transferDetails, owner, signature], 111 | } = permit2['permitTransferFrom(((address,uint256),uint256,uint256),(address,uint256),address,bytes)'].getCall(0) as { args: any[] }; 112 | expect(permit.permitted.token).to.equal(token.address); 113 | expect(permit.permitted.amount).to.equal(12345); 114 | expect(permit.nonce).to.equal(678910); 115 | expect(permit.deadline).to.equal(2468); 116 | expect(transferDetails.to).to.equal(swapper.address); 117 | expect(transferDetails.requestedAmount).to.equal(12345); 118 | expect(owner).to.equal(caller.address); 119 | expect(signature).to.equal('0x1234'); 120 | }); 121 | }); 122 | }); 123 | 124 | describe('batchPermitTakeFromCaller', () => { 125 | when('taking from caller with permit', () => { 126 | given(async () => { 127 | await baseCompanion.batchPermitTakeFromCaller([{ token: token.address, amount: 12345 }], 678910, 2468, '0x1234', swapper.address); 128 | }); 129 | then('internal function is called correctly', async () => { 130 | expect(permit2['permitTransferFrom(((address,uint256)[],uint256,uint256),(address,uint256)[],address,bytes)']).to.have.been.calledOnce; 131 | const { 132 | args: [permit, transferDetails, owner, signature], 133 | } = permit2['permitTransferFrom(((address,uint256)[],uint256,uint256),(address,uint256)[],address,bytes)'].getCall(0) as { args: any[] }; 134 | expect(permit.permitted).to.have.lengthOf(1); 135 | expect(permit.permitted[0].token).to.equal(token.address); 136 | expect(permit.permitted[0].amount).to.equal(12345); 137 | expect(permit.nonce).to.equal(678910); 138 | expect(permit.deadline).to.equal(2468); 139 | expect(transferDetails).to.have.lengthOf(1); 140 | expect(transferDetails[0].to).to.equal(swapper.address); 141 | expect(transferDetails[0].requestedAmount).to.equal(12345); 142 | expect(owner).to.equal(caller.address); 143 | expect(signature).to.equal('0x1234'); 144 | }); 145 | }); 146 | }); 147 | 148 | describe('setSwapper', () => { 149 | const newSwapper = '0x0000000000000000000000000000000000000001'; 150 | const newAllowanceTarget = '0x0000000000000000000000000000000000000002'; 151 | when('setting a new swapper', () => { 152 | given(async () => { 153 | await baseCompanion.connect(governor).setSwapper(newSwapper, newAllowanceTarget); 154 | }); 155 | then('it is set correctly', async () => { 156 | expect(await baseCompanion.swapper()).to.equal(newSwapper); 157 | expect(await baseCompanion.allowanceTarget()).to.equal(newAllowanceTarget); 158 | }); 159 | }); 160 | behaviors.shouldBeExecutableOnlyByGovernor({ 161 | contract: () => baseCompanion, 162 | funcAndSignature: 'setSwapper', 163 | params: [newSwapper, newSwapper], 164 | governor: () => governor, 165 | }); 166 | }); 167 | }); 168 | -------------------------------------------------------------------------------- /test/utils/bdd.ts: -------------------------------------------------------------------------------- 1 | import { Suite, SuiteFunction } from 'mocha'; 2 | 3 | export const then = it; 4 | export const given = beforeEach; 5 | export const when: SuiteFunction = function (title: string, fn: (this: Suite) => void) { 6 | context('when ' + title, fn); 7 | }; 8 | when.only = (title: string, fn?: (this: Suite) => void) => context.only('when ' + title, fn!); 9 | when.skip = (title: string, fn: (this: Suite) => void) => context.skip('when ' + title, fn); 10 | 11 | export const contract = describe; 12 | -------------------------------------------------------------------------------- /test/utils/bn.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber, BigNumberish } from 'ethers'; 2 | import { expect } from 'chai'; 3 | 4 | const expectToEqualWithThreshold = ({ 5 | value, 6 | to, 7 | threshold, 8 | }: { 9 | value: BigNumber | number | string; 10 | to: BigNumber | number | string; 11 | threshold: BigNumber | number | string; 12 | }): void => { 13 | value = toBN(value); 14 | to = toBN(to); 15 | threshold = toBN(threshold); 16 | expect( 17 | to.sub(threshold).lte(value) && to.add(threshold).gte(value), 18 | `Expected ${value.toString()} to be between ${to.sub(threshold).toString()} and ${to.add(threshold).toString()}` 19 | ).to.be.true; 20 | }; 21 | 22 | const expectArraysToBeEqual = (arr1: BigNumber[] | number[] | string[], arr2: BigNumber[] | number[] | string[]): void => { 23 | const parsedArr1 = arr1.map((val: BigNumber | number | string) => toBN(val)); 24 | const parsedArr2 = arr2.map((val: BigNumber | number | string) => toBN(val)); 25 | parsedArr1.forEach((val: BigNumber, index: number) => { 26 | expect(val).to.be.equal(parsedArr2[index], `array differs on index ${index}`); 27 | }); 28 | }; 29 | 30 | const toBN = (value: BigNumberish): BigNumber => { 31 | return BigNumber.isBigNumber(value) ? value : BigNumber.from(value); 32 | }; 33 | 34 | export default { 35 | expectToEqualWithThreshold, 36 | expectArraysToBeEqual, 37 | toBN, 38 | }; 39 | -------------------------------------------------------------------------------- /test/utils/chainlink.ts: -------------------------------------------------------------------------------- 1 | export enum Denominations { 2 | USD = '0x0000000000000000000000000000000000000348', 3 | GBP = '0x000000000000000000000000000000000000033a', 4 | EUR = '0x00000000000000000000000000000000000003d2', 5 | JPY = '0x0000000000000000000000000000000000000188', 6 | KRW = '0x000000000000000000000000000000000000019a', 7 | CNY = '0x000000000000000000000000000000000000009c', 8 | AUD = '0x0000000000000000000000000000000000000024', 9 | CAD = '0x000000000000000000000000000000000000007c', 10 | CHF = '0x00000000000000000000000000000000000002F4', 11 | ARS = '0x0000000000000000000000000000000000000020', 12 | PHP = '0x0000000000000000000000000000000000000260', 13 | NZD = '0x000000000000000000000000000000000000022A', 14 | SGD = '0x00000000000000000000000000000000000002be', 15 | NGN = '0x0000000000000000000000000000000000000236', 16 | ZAR = '0x00000000000000000000000000000000000002c6', 17 | RUB = '0x0000000000000000000000000000000000000283', 18 | INR = '0x0000000000000000000000000000000000000164', 19 | BRL = '0x00000000000000000000000000000000000003Da', 20 | } 21 | -------------------------------------------------------------------------------- /test/utils/coingecko.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { BigNumber, utils } from 'ethers'; 3 | 4 | type CoingeckoDataPoints = { 5 | prices: [number, number][]; 6 | market_caps: [number, number][]; 7 | total_volumes: [number, number][]; 8 | }; 9 | 10 | export const getCoingeckoDataPoints = async (coin: string, currency: string, from: number, to: number): Promise => { 11 | const response = await axios.get( 12 | `https://api.coingecko.com/api/v3/coins/${coin}/market_chart/range?vs_currency=${currency}&from=${from}&to=${to}` 13 | ); 14 | const coingeckoDatapoints = response.data as CoingeckoDataPoints; 15 | return coingeckoDatapoints; 16 | }; 17 | 18 | export const getLastPrice = async (coin: string, currency: string): Promise => { 19 | return await getSimple(coin, currency); 20 | }; 21 | 22 | type CoingeckoSimple = { [coin: string]: { [currency: string]: number } }; 23 | 24 | export const getSimple = async (coin: string, currency: string): Promise => { 25 | const coingeckoSimple = (await axios.get(`https://api.coingecko.com/api/v3/simple/price?ids=${coin}&vs_currencies=${currency}`)) 26 | .data as CoingeckoSimple; 27 | return coingeckoSimple[coin][currency]; 28 | }; 29 | 30 | export const convertPriceToBigNumberWithDecimals = (price: number, decimals: number): BigNumber => { 31 | return utils.parseUnits(price.toFixed(decimals), decimals); 32 | }; 33 | 34 | export const convertPriceToNumberWithDecimals = (price: number, decimals: number): number => { 35 | return convertPriceToBigNumberWithDecimals(price, decimals).toNumber(); 36 | }; 37 | -------------------------------------------------------------------------------- /test/utils/constants.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber } from 'ethers'; 2 | 3 | const ZERO = BigNumber.from('0'); 4 | const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; 5 | const ZERO_BYTES32 = '0x0000000000000000000000000000000000000000000000000000000000000000'; 6 | const NOT_ZERO_ADDRESS = '0x0000000000000000000000000000000000000001'; 7 | const MAX_INT_256 = BigNumber.from('2').pow('255').sub(1); 8 | const MAX_UINT_256 = BigNumber.from('2').pow('256').sub(1); 9 | const MIN_INT_256 = BigNumber.from('-0x8000000000000000000000000000000000000000000000000000000000000000'); 10 | 11 | export default { 12 | ZERO, 13 | ZERO_ADDRESS, 14 | ZERO_BYTES32, 15 | NOT_ZERO_ADDRESS, 16 | MAX_INT_256, 17 | MIN_INT_256, 18 | MAX_UINT_256, 19 | }; 20 | -------------------------------------------------------------------------------- /test/utils/contracts.ts: -------------------------------------------------------------------------------- 1 | import { Contract, ContractFactory } from '@ethersproject/contracts'; 2 | import { TransactionResponse } from '@ethersproject/abstract-provider'; 3 | import { ContractInterface, Signer } from 'ethers'; 4 | import { getStatic } from 'ethers/lib/utils'; 5 | 6 | const deploy = async (contract: ContractFactory, args: any[]): Promise<{ tx: TransactionResponse; contract: Contract }> => { 7 | const deploymentTransactionRequest = await contract.getDeployTransaction(...args); 8 | const deploymentTx = await contract.signer.sendTransaction(deploymentTransactionRequest); 9 | const contractAddress = getStatic<(deploymentTx: TransactionResponse) => string>(contract.constructor, 'getContractAddress')(deploymentTx); 10 | const deployedContract = getStatic<(contractAddress: string, contractInterface: ContractInterface, signer?: Signer) => Contract>( 11 | contract.constructor, 12 | 'getContract' 13 | )(contractAddress, contract.interface, contract.signer); 14 | return { 15 | tx: deploymentTx, 16 | contract: deployedContract, 17 | }; 18 | }; 19 | 20 | export default { 21 | deploy, 22 | }; 23 | -------------------------------------------------------------------------------- /test/utils/dexes/oneinch.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber } from '@ethersproject/bignumber'; 2 | import axios from 'axios'; 3 | import axiosRetry from 'axios-retry'; 4 | import qs from 'qs'; 5 | 6 | axiosRetry(axios, { retries: 3, retryDelay: axiosRetry.exponentialDelay }); 7 | 8 | export type SwapParams = { 9 | tokenIn: string; 10 | tokenOut: string; 11 | amountIn: BigNumber; 12 | fromAddress: string; 13 | slippage: number; 14 | protocols?: string; 15 | receiver?: string; 16 | referrer?: string; 17 | fee?: number; 18 | gasPrice?: BigNumber; 19 | burnChi?: boolean; 20 | complexityLevel?: string; 21 | connectorTokens?: string; 22 | allowPartialFill?: boolean; 23 | disableEstimate?: boolean; 24 | gasLimit?: number; 25 | parts?: number; 26 | mainRouteParts?: number; 27 | }; 28 | 29 | type Token = { 30 | symbol: string; 31 | name: string; 32 | decimals: number; 33 | address: string; 34 | logoURI: string; 35 | }; 36 | 37 | type SwapPart = { 38 | name: string; 39 | part: number; 40 | fromTokenAddress: string; 41 | toTokenAddress: string; 42 | }; 43 | type SwapProtocol = SwapPart[]; 44 | type SwapProtocols = SwapProtocol[]; 45 | 46 | export type SwapResponse = { 47 | fromToken: Token; 48 | toToken: Token; 49 | fromTokenAmount: BigNumber; 50 | toTokenAmount: BigNumber; 51 | minAmountOut?: BigNumber; 52 | protocols: SwapProtocols; 53 | tx: { 54 | from: string; 55 | to: string; 56 | data: string; 57 | value: BigNumber; 58 | gasPrice: BigNumber; 59 | gas: BigNumber; 60 | }; 61 | }; 62 | 63 | export const swap = async (chainId: number, swapParams: SwapParams): Promise => { 64 | let data: SwapResponse; 65 | try { 66 | // https://${API_URL[quoteRequest.chainId]}/swap/v1/quote?${qs.stringify(quoteRequest)} 67 | 68 | const axiosProtocolResponse = (await axios.get(`https://api.1inch.exchange/v3.0/${chainId}/protocols`)) as any; 69 | const protocols = (axiosProtocolResponse.data.protocols as string[]).filter((protocol) => { 70 | return protocol.includes('ONE_INCH_LIMIT_ORDER') == false; 71 | }); 72 | const requestParams = { 73 | fromTokenAddress: swapParams.tokenIn, 74 | toTokenAddress: swapParams.tokenOut, 75 | // destReceiver: swapParams.receiver, 76 | amount: swapParams.amountIn.toString(), 77 | fromAddress: swapParams.fromAddress, 78 | slippage: swapParams.slippage, 79 | disableEstimate: swapParams.disableEstimate, 80 | allowPartialFill: swapParams.allowPartialFill, 81 | // protocols: protocols.join(',') 82 | }; 83 | ({ data } = await axios.get(`https://api.1inch.exchange/v3.0/${chainId}/swap?${qs.stringify(requestParams)}`)); 84 | } catch (err: any) { 85 | throw new Error(`Status code: ${err.response.data.statusCode}. Message: ${err.response.data.message}`); 86 | } 87 | if (swapParams.hasOwnProperty('slippage')) { 88 | const amountOut = BigNumber.from(data.toTokenAmount); 89 | data.minAmountOut = amountOut.sub(amountOut.mul(swapParams.slippage).div(100)); 90 | } 91 | return data; 92 | }; 93 | 94 | export default { 95 | swap, 96 | }; 97 | -------------------------------------------------------------------------------- /test/utils/dexes/paraswap.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber } from '@ethersproject/bignumber'; 2 | import wallet from '@test-utils/wallet'; 3 | import axios from 'axios'; 4 | import axiosRetry from 'axios-retry'; 5 | import { utils } from 'ethers'; 6 | import moment from 'moment'; 7 | import qs from 'qs'; 8 | 9 | axiosRetry(axios, { retries: 3, retryDelay: axiosRetry.exponentialDelay }); 10 | 11 | export type SwapParams = { 12 | srcToken: string; 13 | srcDecimals: number; 14 | destToken: string; 15 | destDecimals: number; 16 | amount: string; // In weis 17 | txOrigin: string; 18 | userAddress: string; 19 | receiver: string; 20 | side: 'SELL' | 'BUY'; 21 | network: '1' | '3' | '137' | '56' | '43114'; 22 | otherExchangePrices?: boolean; 23 | includeDEXS?: string; 24 | excludeDEXS?: string; 25 | }; 26 | 27 | export type SwapResponse = { 28 | from: string; 29 | to: string; 30 | allowanceTarget: string; 31 | value: string; 32 | data: string; 33 | gasPrice: string; 34 | chainId: number; 35 | }; 36 | 37 | export const swap = async (swapParams: SwapParams): Promise => { 38 | const priceResponse = await axios.get(`https://apiv5.paraswap.io/prices?${qs.stringify(swapParams)}`); 39 | const transactionQueryParams = { 40 | // gasPrice: utils.parseUnits('50', 'gwei').toNumber(), Optional 41 | ignoreChecks: true, 42 | ignoreGasEstimate: true, 43 | }; 44 | let transactionsBodyParams: any = { 45 | srcToken: swapParams.srcToken, 46 | srcDecimals: swapParams.srcDecimals, 47 | destToken: swapParams.destToken, 48 | destDecimals: swapParams.destDecimals, 49 | priceRoute: priceResponse.data.priceRoute, 50 | slippage: 0.1 * 100, // 1% 51 | userAddress: swapParams.userAddress, 52 | receiver: swapParams.receiver, 53 | deadline: moment().add('10', 'minutes').unix(), 54 | }; 55 | if (swapParams.side === 'SELL') { 56 | transactionsBodyParams.srcAmount = swapParams.amount; 57 | } else { 58 | transactionsBodyParams.destAmount = swapParams.amount; 59 | } 60 | try { 61 | const transactionResponse = await axios.post( 62 | `https://apiv5.paraswap.io/transactions/${swapParams.network}?${qs.stringify(transactionQueryParams)}`, 63 | transactionsBodyParams 64 | ); 65 | const finalData = { 66 | ...transactionResponse.data, 67 | // Ref.: https://developers.paraswap.network/smart-contracts#tokentransferproxy 68 | allowanceTarget: (priceResponse.data as any).priceRoute.tokenTransferProxy, 69 | }; 70 | return finalData; 71 | } catch (err: any) { 72 | throw new Error(`Error while fetching transactions params: ${err.response.data.error}`); 73 | } 74 | }; 75 | 76 | export default { 77 | swap, 78 | }; 79 | -------------------------------------------------------------------------------- /test/utils/event-utils.ts: -------------------------------------------------------------------------------- 1 | import { TransactionResponse, TransactionReceipt } from '@ethersproject/abstract-provider'; 2 | import { expect } from 'chai'; 3 | 4 | export async function expectNoEventWithName(response: TransactionResponse, eventName: string) { 5 | const receipt = await response.wait(); 6 | for (const event of getEvents(receipt)) { 7 | expect(event.event).not.to.equal(eventName); 8 | } 9 | } 10 | 11 | export async function readArgFromEvent(response: TransactionResponse, eventName: string, paramName: string): Promise { 12 | const receipt = await response.wait(); 13 | for (const event of getEvents(receipt)) { 14 | if (event.event === eventName) { 15 | return event.args[paramName]; 16 | } 17 | } 18 | } 19 | 20 | export async function readArgFromEventOrFail(response: TransactionResponse, eventName: string, paramName: string): Promise { 21 | const result = await readArgFromEvent(response, eventName, paramName); 22 | if (result) { 23 | return result; 24 | } 25 | throw new Error(`Failed to find event with name ${eventName}`); 26 | } 27 | 28 | function getEvents(receipt: TransactionReceipt): Event[] { 29 | // @ts-ignore 30 | return receipt.events; 31 | } 32 | 33 | type Event = { 34 | event: string; // Event name 35 | args: any; 36 | }; 37 | -------------------------------------------------------------------------------- /test/utils/evm.ts: -------------------------------------------------------------------------------- 1 | import { getNodeUrl } from '@utils/network'; 2 | import { network } from 'hardhat'; 3 | 4 | export let networkBeingForked: string; 5 | 6 | const advanceTimeAndBlock = async (time: number): Promise => { 7 | await advanceTime(time); 8 | await advanceBlock(); 9 | }; 10 | 11 | const advanceToTimeAndBlock = async (time: number): Promise => { 12 | await advanceToTime(time); 13 | await advanceBlock(); 14 | }; 15 | 16 | const advanceTime = async (time: number): Promise => { 17 | await network.provider.request({ 18 | method: 'evm_increaseTime', 19 | params: [time], 20 | }); 21 | }; 22 | 23 | const advanceToTime = async (time: number): Promise => { 24 | await network.provider.request({ 25 | method: 'evm_setNextBlockTimestamp', 26 | params: [time], 27 | }); 28 | }; 29 | 30 | const advanceBlock = async () => { 31 | await network.provider.request({ 32 | method: 'evm_mine', 33 | params: [], 34 | }); 35 | }; 36 | 37 | type ForkConfig = { network: string; skipHardhatDeployFork?: boolean } & Record; 38 | const reset = async ({ network: networkName, ...forkingConfig }: ForkConfig) => { 39 | if (!forkingConfig.skipHardhatDeployFork) { 40 | process.env.HARDHAT_DEPLOY_FORK = networkName; 41 | } 42 | const params = [ 43 | { 44 | forking: { 45 | ...forkingConfig, 46 | jsonRpcUrl: getNodeUrl(networkName), 47 | }, 48 | }, 49 | ]; 50 | await network.provider.request({ 51 | method: 'hardhat_reset', 52 | params, 53 | }); 54 | }; 55 | class SnapshotManager { 56 | snapshots: { [id: string]: string } = {}; 57 | 58 | async take(): Promise { 59 | const id = await this.takeSnapshot(); 60 | this.snapshots[id] = id; 61 | return id; 62 | } 63 | 64 | async revert(id: string): Promise { 65 | await this.revertSnapshot(this.snapshots[id]); 66 | this.snapshots[id] = await this.takeSnapshot(); 67 | } 68 | 69 | private async takeSnapshot(): Promise { 70 | return (await network.provider.request({ 71 | method: 'evm_snapshot', 72 | params: [], 73 | })) as string; 74 | } 75 | 76 | private async revertSnapshot(id: string) { 77 | await network.provider.request({ 78 | method: 'evm_revert', 79 | params: [id], 80 | }); 81 | } 82 | } 83 | 84 | export const snapshot = new SnapshotManager(); 85 | 86 | export default { 87 | advanceTimeAndBlock, 88 | advanceToTimeAndBlock, 89 | advanceTime, 90 | advanceToTime, 91 | advanceBlock, 92 | reset, 93 | }; 94 | -------------------------------------------------------------------------------- /test/utils/index.ts: -------------------------------------------------------------------------------- 1 | import behaviours from './behaviours'; 2 | import contracts from './contracts'; 3 | import constants from './constants'; 4 | import evm from './evm'; 5 | import bn from './bn'; 6 | import wallet from './wallet'; 7 | import * as coingecko from './coingecko'; 8 | 9 | export { contracts, coingecko, behaviours, bn, constants, evm, wallet }; 10 | -------------------------------------------------------------------------------- /test/utils/interval-utils.ts: -------------------------------------------------------------------------------- 1 | // TODO: Unify with core under one lib 2 | 3 | export class SwapInterval { 4 | static readonly ONE_MINUTE = new SwapInterval(60, '0x01'); 5 | static readonly FIVE_MINUTES = new SwapInterval(SwapInterval.ONE_MINUTE.seconds * 5, '0x02'); 6 | static readonly FIFTEEN_MINUTES = new SwapInterval(SwapInterval.FIVE_MINUTES.seconds * 3, '0x04'); 7 | static readonly THIRTY_MINUTES = new SwapInterval(SwapInterval.FIFTEEN_MINUTES.seconds * 2, '0x08'); 8 | static readonly ONE_HOUR = new SwapInterval(SwapInterval.THIRTY_MINUTES.seconds * 2, '0x10'); 9 | static readonly FOUR_HOURS = new SwapInterval(SwapInterval.ONE_HOUR.seconds * 4, '0x20'); 10 | static readonly ONE_DAY = new SwapInterval(SwapInterval.FOUR_HOURS.seconds * 6, '0x40'); 11 | static readonly ONE_WEEK = new SwapInterval(SwapInterval.ONE_DAY.seconds * 7, '0x80'); 12 | 13 | static readonly INTERVALS = [ 14 | SwapInterval.ONE_MINUTE, 15 | SwapInterval.FIVE_MINUTES, 16 | SwapInterval.FIFTEEN_MINUTES, 17 | SwapInterval.THIRTY_MINUTES, 18 | SwapInterval.ONE_HOUR, 19 | SwapInterval.FOUR_HOURS, 20 | SwapInterval.ONE_DAY, 21 | SwapInterval.ONE_WEEK, 22 | ]; 23 | 24 | private constructor(readonly seconds: number, readonly mask: string) {} 25 | 26 | public isInByteSet(byte: string): boolean { 27 | return (parseInt(byte) & parseInt(this.mask)) != 0; 28 | } 29 | 30 | static intervalsToByte(...intervals: SwapInterval[]): string { 31 | const finalMask = intervals.map((intervals) => parseInt(intervals.mask)).reduce((a, b) => a | b, 0); 32 | return '0x' + finalMask.toString(16).padStart(2, '0'); 33 | } 34 | 35 | static intervalsfromByte(byte: string): SwapInterval[] { 36 | let num = parseInt(byte); 37 | let index = 0; 38 | const result = []; 39 | while (index <= 8 && 1 << index <= num) { 40 | if ((num & (1 << index)) != 0) { 41 | result.push(SwapInterval.INTERVALS[index]); 42 | } 43 | index++; 44 | } 45 | return result; 46 | } 47 | } 48 | 49 | // TODO: Add tests for this file 50 | -------------------------------------------------------------------------------- /test/utils/swap-utils.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber } from 'ethers'; 2 | 3 | export type TokenAddress = string; 4 | 5 | export type Pair = { 6 | tokenA: TokenAddress; 7 | tokenB: TokenAddress; 8 | }; 9 | 10 | export type Borrow = { 11 | token: TokenAddress; 12 | amount: BigNumber; 13 | }; 14 | 15 | export type PairIndex = { 16 | indexTokenA: number; 17 | indexTokenB: number; 18 | }; 19 | 20 | export function buildGetNextSwapInfoInput( 21 | pairsToSwap: Pair[], 22 | checkAvailableToBorrow: TokenAddress[] 23 | ): { tokens: TokenAddress[]; pairIndexes: PairIndex[] } { 24 | const fakeAmounts = checkAvailableToBorrow.map((token) => ({ token, amount: BigNumber.from(0) })); 25 | const { tokens, pairIndexes } = buildSwapInput(pairsToSwap, fakeAmounts); 26 | return { tokens, pairIndexes }; 27 | } 28 | 29 | export function buildSwapInput( 30 | pairsToSwap: Pair[], 31 | borrow: Borrow[] 32 | ): { tokens: TokenAddress[]; pairIndexes: PairIndex[]; borrow: BigNumber[] } { 33 | const tokens: TokenAddress[] = getUniqueTokens(pairsToSwap, borrow); 34 | const pairIndexes = getIndexes(pairsToSwap, tokens); 35 | assertValid(pairIndexes); 36 | const toBorrow = buildBorrowArray(tokens, borrow); 37 | return { tokens, pairIndexes, borrow: toBorrow }; 38 | } 39 | 40 | function buildBorrowArray(tokens: TokenAddress[], borrow: Borrow[]): BigNumber[] { 41 | const borrowMap = new Map(borrow.map(({ token, amount }) => [token, amount])); 42 | return tokens.map((token) => borrowMap.get(token) ?? BigNumber.from(0)); 43 | } 44 | 45 | function assertValid(indexes: PairIndex[]) { 46 | for (const { indexTokenA, indexTokenB } of indexes) { 47 | if (indexTokenA === indexTokenB) { 48 | throw Error('Found duplicates in same pair'); 49 | } 50 | } 51 | 52 | for (let i = 1; i < indexes.length; i++) { 53 | if (indexes[i - 1].indexTokenA === indexes[i].indexTokenA && indexes[i - 1].indexTokenB === indexes[i].indexTokenB) { 54 | throw Error('Found duplicates'); 55 | } 56 | } 57 | } 58 | 59 | /** 60 | * Given a list of pairs and a list of sorted tokens, maps each pair into the index of each token 61 | * (inside the list of tokens). The list of indexes will also be sorted, first by tokenA and then by tokenB 62 | */ 63 | function getIndexes(pairs: Pair[], tokens: TokenAddress[]): PairIndex[] { 64 | return pairs 65 | .map(({ tokenA, tokenB }) => ({ indexTokenA: tokens.indexOf(tokenA), indexTokenB: tokens.indexOf(tokenB) })) 66 | .map(({ indexTokenA, indexTokenB }) => ({ 67 | indexTokenA: Math.min(indexTokenA, indexTokenB), 68 | indexTokenB: Math.max(indexTokenA, indexTokenB), 69 | })) 70 | .sort((a, b) => a.indexTokenA - b.indexTokenA || a.indexTokenB - b.indexTokenB); 71 | } 72 | 73 | /** Given a list of pairs, returns a sorted list of the tokens involved */ 74 | function getUniqueTokens(pairs: Pair[], borrow: Borrow[]): TokenAddress[] { 75 | const tokenSet: Set = new Set(); 76 | for (const { tokenA, tokenB } of pairs) { 77 | tokenSet.add(tokenA); 78 | tokenSet.add(tokenB); 79 | } 80 | 81 | for (const { token } of borrow) { 82 | tokenSet.add(token); 83 | } 84 | 85 | return [...tokenSet].sort((a, b) => a.localeCompare(b)); 86 | } 87 | -------------------------------------------------------------------------------- /test/utils/wallet.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber, constants, Wallet } from 'ethers'; 2 | import { getAddress } from 'ethers/lib/utils'; 3 | import { ethers, network } from 'hardhat'; 4 | import { randomHex } from 'web3-utils'; 5 | import { JsonRpcSigner } from '@ethersproject/providers'; 6 | 7 | const impersonate = async (address: string): Promise => { 8 | await network.provider.request({ 9 | method: 'hardhat_impersonateAccount', 10 | params: [address], 11 | }); 12 | return ethers.provider.getSigner(address); 13 | }; 14 | const generateRandom = async () => { 15 | const wallet = Wallet.createRandom().connect(ethers.provider); 16 | await ethers.provider.send('hardhat_setBalance', [wallet.address, constants.MaxUint256.toHexString().replace('0x0', '0x')]); 17 | return wallet; 18 | }; 19 | 20 | const setBalance = async ({ account, balance }: { account: string; balance: BigNumber }) => { 21 | await ethers.provider.send('hardhat_setBalance', [account, balance.toHexString().replace('0x0', '0x')]); 22 | }; 23 | 24 | // Note: we are hardcoding the random address to make tests deterministic. We couldn't generate a random address by using a seed 25 | export const generateRandomAddress = () => '0x37601c8d013fA4DFA82e9C0d416b70143f4cbFcF'; 26 | 27 | export default { 28 | impersonate, 29 | setBalance, 30 | generateRandom, 31 | generateRandomAddress, 32 | }; 33 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2019", 4 | "module": "commonjs", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "moduleResolution": "node", 8 | "resolveJsonModule": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "outDir": "dist", 11 | "baseUrl": ".", 12 | "paths": { 13 | "@artifacts/*": ["artifacts/*"], 14 | "@deploy/*": ["deploy/*"], 15 | "@typechained": ["typechained"], 16 | "@tasks": ["tasks"], 17 | "@typechained/*": ["typechained/*"], 18 | "@utils/*": ["utils/*"], 19 | "@test-utils": ["test/utils/index"], 20 | "@test-utils/*": ["test/utils/*"], 21 | "@e2e/*": ["test/e2e/*"], 22 | "@integration/*": ["test/integration/*"], 23 | "@unit/*": ["test/unit/*"] 24 | } 25 | }, 26 | "include": ["hardhat.config.ts", "./utils", "./scripts", "./deploy", "./test", "./tasks"] 27 | } 28 | -------------------------------------------------------------------------------- /tsconfig.publish.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2019", 4 | "module": "commonjs", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "resolveJsonModule": true, 8 | "declaration": true, 9 | "declarationMap": true, 10 | "outDir": "dist", 11 | "baseUrl": ".", 12 | "paths": { 13 | "@artifacts": ["artifacts/*"], 14 | "@deploy": ["deploy/*"], 15 | "@deployments": ["deployments/*"], 16 | "@typechained": ["typechained/index"] 17 | } 18 | }, 19 | "exclude": ["dist", "node_modules"], 20 | "include": ["./typechained"] 21 | } 22 | -------------------------------------------------------------------------------- /utils/network.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | 3 | export function getNodeUrl(networkName: string): string { 4 | if (networkName) { 5 | const uri = process.env[`ETH_NODE_URI_${networkName.toUpperCase().replace('-', '_')}`]; 6 | if (uri && uri !== '') { 7 | return uri; 8 | } 9 | } 10 | 11 | if (networkName === 'localhost') { 12 | // do not use ETH_NODE_URI 13 | return 'http://localhost:8545'; 14 | } 15 | 16 | let uri = process.env.ETH_NODE_URI; 17 | if (uri) { 18 | uri = uri.replace('{{networkName}}', networkName); 19 | } 20 | if (!uri || uri === '') { 21 | // throw new Error(`environment variable "ETH_NODE_URI" not configured `); 22 | return ''; 23 | } 24 | if (uri.indexOf('{{') >= 0) { 25 | throw new Error(`invalid uri or network not supported by node provider : ${uri}`); 26 | } 27 | return uri; 28 | } 29 | 30 | export function getMnemonic(networkName?: string): string { 31 | if (networkName) { 32 | const mnemonic = process.env[`MNEMONIC_${networkName.toUpperCase()}`]; 33 | if (mnemonic && mnemonic !== '') { 34 | return mnemonic; 35 | } 36 | } 37 | 38 | const mnemonic = process.env.MNEMONIC; 39 | if (!mnemonic || mnemonic === '') { 40 | return 'test test test test test test test test test test test junk'; 41 | } 42 | return mnemonic; 43 | } 44 | 45 | export function accounts(networkName?: string): { mnemonic: string } { 46 | return { mnemonic: getMnemonic(networkName) }; 47 | } 48 | -------------------------------------------------------------------------------- /workspace.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ], 7 | "settings": { 8 | "eslint.workingDirectories": [{ "mode": "auto" }], 9 | "editor.codeActionsOnSave": { "source.fixAll.eslint": true } 10 | }, 11 | "extensions": { 12 | "recommendations": [ 13 | "juanblanco.solidity", 14 | "esbenp.prettier-vscode", 15 | "dbaeumer.vscode-eslint", 16 | "mikestead.dotenv" 17 | ] 18 | } 19 | } 20 | --------------------------------------------------------------------------------